Skip to main content

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_oxide::{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::{
33    transactions::start_tx_verification, Database, ExtendedConsensusError, VerificationContext,
34};
35
36mod alt_block;
37mod batch_prepare;
38mod free;
39
40pub use alt_block::sanity_check_alt_block;
41pub use batch_prepare::{batch_prepare_main_chain_blocks, BatchPrepareCache};
42use free::pull_ordered_transactions;
43
44/// An error encountered while verifying a block.
45///
46/// [`Self::pow_valid`] records whether the block's proof-of-work was successfully verified before
47/// the error occurred.
48#[derive(Debug, thiserror::Error)]
49#[error("{inner}")]
50pub struct BlockVerificationError {
51    /// Whether the block's proof-of-work was successfully verified.
52    pub pow_valid: bool,
53    /// The underlying error.
54    #[source]
55    pub inner: ExtendedConsensusError,
56}
57
58impl BlockVerificationError {
59    /// Create an error that occurred after the block's proof-of-work was verified.
60    pub fn valid_pow(inner: impl Into<ExtendedConsensusError>) -> Self {
61        Self {
62            pow_valid: true,
63            inner: inner.into(),
64        }
65    }
66
67    /// Create an error that occurred before the block's proof-of-work was verified.
68    pub fn invalid_pow(inner: impl Into<ExtendedConsensusError>) -> Self {
69        Self {
70            pow_valid: false,
71            inner: inner.into(),
72        }
73    }
74
75    /// Create a fatal error from a service failure.
76    pub fn fatal(inner: tower::BoxError) -> Self {
77        Self {
78            pow_valid: false,
79            inner: ExtendedConsensusError::FatalError(inner),
80        }
81    }
82}
83
84impl From<BlockVerificationError> for ExtendedConsensusError {
85    fn from(inner: BlockVerificationError) -> Self {
86        inner.inner
87    }
88}
89
90/// A pre-prepared block with all data needed to verify it, except the block's proof of work.
91#[derive(Debug)]
92pub struct PreparedBlockExPow {
93    /// The block.
94    pub block: Block,
95    /// The serialised block's bytes.
96    pub block_blob: Vec<u8>,
97
98    /// The block's hard-fork vote.
99    pub hf_vote: HardFork,
100    /// The block's hard-fork version.
101    pub hf_version: HardFork,
102
103    /// The block's hash.
104    pub block_hash: [u8; 32],
105    /// The height of the block.
106    pub height: usize,
107
108    /// The weight of the block's miner transaction.
109    pub miner_tx_weight: usize,
110}
111
112impl PreparedBlockExPow {
113    /// Prepare a new block.
114    ///
115    /// # Errors
116    /// This errors if either the `block`'s:
117    /// - Hard-fork values are invalid
118    /// - Miner transaction is missing a miner input
119    pub fn new(block: Block) -> Result<Self, ConsensusError> {
120        let (hf_version, hf_vote) = HardFork::from_block_header(&block.header)
121            .map_err(|_| BlockError::HardForkError(HardForkError::HardForkUnknown))?;
122
123        let Some(Input::Gen(height)) = block.miner_transaction().prefix().inputs.first() else {
124            return Err(ConsensusError::Block(BlockError::MinerTxError(
125                MinerTxError::InputNotOfTypeGen,
126            )));
127        };
128
129        Ok(Self {
130            block_blob: block.serialize(),
131            hf_vote,
132            hf_version,
133
134            block_hash: block.hash(),
135            height: *height,
136
137            miner_tx_weight: block.miner_transaction().weight(),
138            block,
139        })
140    }
141}
142
143/// A pre-prepared block with all data needed to verify it.
144#[derive(Debug)]
145pub struct PreparedBlock {
146    /// The block
147    pub block: Block,
148    /// The serialised block's bytes
149    pub block_blob: Vec<u8>,
150
151    /// The block's hf vote
152    pub hf_vote: HardFork,
153    /// The block's hf version
154    pub hf_version: HardFork,
155
156    /// The block's hash
157    pub block_hash: [u8; 32],
158    /// The block's POW hash.
159    pub pow_hash: [u8; 32],
160
161    /// The weight of the block's miner transaction.
162    pub miner_tx_weight: usize,
163}
164
165impl PreparedBlock {
166    /// Creates a new [`PreparedBlock`].
167    ///
168    /// The randomX VM must be Some if RX is needed or this will panic.
169    /// The randomX VM must also be initialised with the correct seed.
170    pub fn new<R: RandomX>(block: Block, randomx_vm: Option<&R>) -> Result<Self, ConsensusError> {
171        let (hf_version, hf_vote) = HardFork::from_block_header(&block.header)
172            .map_err(|_| BlockError::HardForkError(HardForkError::HardForkUnknown))?;
173
174        let [Input::Gen(height)] = &block.miner_transaction().prefix().inputs[..] else {
175            return Err(ConsensusError::Block(BlockError::MinerTxError(
176                MinerTxError::InputNotOfTypeGen,
177            )));
178        };
179
180        Ok(Self {
181            block_blob: block.serialize(),
182            hf_vote,
183            hf_version,
184
185            block_hash: block.hash(),
186            pow_hash: calculate_pow_hash(
187                randomx_vm,
188                &block.serialize_pow_hash(),
189                *height,
190                &hf_version,
191            )?,
192
193            miner_tx_weight: block.miner_transaction().weight(),
194            block,
195        })
196    }
197
198    /// Creates a new [`PreparedBlock`] from a [`PreparedBlockExPow`].
199    ///
200    /// This function will give an invalid proof-of-work hash if `randomx_vm` is not initialised
201    /// with the correct seed.
202    ///
203    /// # Panics
204    /// This function will panic if `randomx_vm` is
205    /// [`None`] even though RandomX is needed.
206    fn new_prepped<R: RandomX>(
207        block: PreparedBlockExPow,
208        randomx_vm: Option<&R>,
209    ) -> Result<Self, ConsensusError> {
210        Ok(Self {
211            block_blob: block.block_blob,
212            hf_vote: block.hf_vote,
213            hf_version: block.hf_version,
214
215            block_hash: block.block_hash,
216            pow_hash: calculate_pow_hash(
217                randomx_vm,
218                &block.block.serialize_pow_hash(),
219                block.height,
220                &block.hf_version,
221            )?,
222
223            miner_tx_weight: block.block.miner_transaction().weight(),
224            block: block.block,
225        })
226    }
227
228    /// Creates a new [`PreparedBlock`] from an [`AltBlockInformation`].
229    pub fn new_alt_block(block: AltBlockInformation) -> Result<Self, ConsensusError> {
230        Ok(Self {
231            block_blob: block.block_blob,
232            hf_vote: HardFork::from_vote(block.block.header.hardfork_signal),
233            hf_version: HardFork::from_version(block.block.header.hardfork_version)
234                .map_err(|_| BlockError::HardForkError(HardForkError::HardForkUnknown))?,
235            block_hash: block.block_hash,
236            pow_hash: block.pow_hash,
237            miner_tx_weight: block.block.miner_transaction().weight(),
238            block: block.block,
239        })
240    }
241}
242
243/// Fully verify a block and all its transactions.
244pub async fn verify_main_chain_block<D>(
245    block: Block,
246    txs: HashMap<[u8; 32], TransactionVerificationData>,
247    context_svc: &mut BlockchainContextService,
248    database: D,
249) -> Result<VerifiedBlockInformation, BlockVerificationError>
250where
251    D: Database + Clone + Send + 'static,
252{
253    let context = context_svc.blockchain_context().clone();
254    tracing::debug!("got blockchain context: {:?}", context);
255
256    tracing::debug!(
257        "Preparing block for verification, expected height: {}",
258        context.chain_height
259    );
260
261    // Set up the block and just pass it to [`verify_prepped_main_chain_block`]
262
263    // We just use the raw `hardfork_version` here, no need to turn it into a `HardFork`.
264    let rx_vms = if block.header.hardfork_version < 12 {
265        HashMap::new()
266    } else {
267        let BlockChainContextResponse::RxVms(rx_vms) = context_svc
268            .ready()
269            .await
270            .map_err(BlockVerificationError::fatal)?
271            .call(BlockChainContextRequest::CurrentRxVms)
272            .await
273            .map_err(BlockVerificationError::fatal)?
274        else {
275            panic!("Blockchain context service returned wrong response!");
276        };
277
278        rx_vms
279    };
280
281    let height = context.chain_height;
282    let prepped_block = rayon_spawn_async(move || {
283        PreparedBlock::new(
284            block,
285            rx_vms.get(&randomx_seed_height(height)).map(AsRef::as_ref),
286        )
287    })
288    .await
289    .map_err(BlockVerificationError::invalid_pow)?;
290
291    check_block_pow(&prepped_block.pow_hash, context.next_difficulty)
292        .map_err(ConsensusError::Block)
293        .map_err(BlockVerificationError::invalid_pow)?;
294
295    // Check that the txs included are what we need and that there are not any extra.
296    let ordered_txs = pull_ordered_transactions(&prepped_block.block, txs)
297        .map_err(BlockVerificationError::valid_pow)?;
298
299    verify_prepped_main_chain_block(
300        prepped_block,
301        ordered_txs,
302        context_svc,
303        &mut VerificationContext::Database(database),
304    )
305    .await
306}
307
308/// Fully verify a block that has already been prepared using [`batch_prepare_main_chain_blocks`].
309pub async fn verify_prepped_main_chain_block<D>(
310    prepped_block: PreparedBlock,
311    mut txs: Vec<TransactionVerificationData>,
312    context_svc: &mut BlockchainContextService,
313    verification_context: &mut VerificationContext<D>,
314) -> Result<VerifiedBlockInformation, BlockVerificationError>
315where
316    D: Database + Clone + Send + 'static,
317{
318    let context = context_svc.blockchain_context();
319
320    tracing::debug!("verifying block: {}", hex::encode(prepped_block.block_hash));
321
322    check_block_pow(&prepped_block.pow_hash, context.next_difficulty)
323        .map_err(ConsensusError::Block)
324        .map_err(BlockVerificationError::invalid_pow)?;
325
326    if prepped_block.block.transactions.len() != txs.len() {
327        return Err(BlockVerificationError::valid_pow(ConsensusError::Block(
328            BlockError::TxsIncludedWithBlockIncorrect,
329        )));
330    }
331
332    if !prepped_block.block.transactions.is_empty() {
333        for (expected_tx_hash, tx) in prepped_block.block.transactions.iter().zip(txs.iter()) {
334            if expected_tx_hash != &tx.tx_hash {
335                return Err(BlockVerificationError::valid_pow(ConsensusError::Block(
336                    BlockError::TxsIncludedWithBlockIncorrect,
337                )));
338            }
339        }
340
341        let temp = start_tx_verification()
342            .append_prepped_txs(mem::take(&mut txs))
343            .prepare()
344            .map_err(BlockVerificationError::valid_pow)?
345            .full(
346                context.chain_height,
347                context.top_hash,
348                context.current_adjusted_timestamp_for_time_lock(),
349                context.current_hf,
350                verification_context,
351            )
352            .verify()
353            .await
354            .map_err(BlockVerificationError::valid_pow)?;
355
356        txs = temp;
357    }
358
359    let block_weight =
360        prepped_block.miner_tx_weight + txs.iter().map(|tx| tx.tx_weight).sum::<usize>();
361    let total_fees = txs.iter().map(|tx| tx.fee).sum::<u64>();
362
363    tracing::debug!("Verifying block header.");
364    let (_, generated_coins) = check_block(
365        &prepped_block.block,
366        total_fees,
367        block_weight,
368        prepped_block.block_blob.len(),
369        &context.context_to_verify_block,
370    )
371    .map_err(ConsensusError::Block)
372    .map_err(BlockVerificationError::valid_pow)?;
373
374    let block = VerifiedBlockInformation {
375        block_hash: prepped_block.block_hash,
376        block: prepped_block.block,
377        block_blob: prepped_block.block_blob,
378        txs: txs
379            .into_iter()
380            .map(|tx| {
381                let tx_weight = tx.tx_weight;
382                let fee = tx.fee;
383                let tx_hash = tx.tx_hash;
384                let (tx, tx_prunable_blob) = tx.tx.pruned_with_prunable();
385                VerifiedTransactionInformation {
386                    tx_prunable_blob,
387                    tx_pruned: tx.serialize(),
388                    tx_weight,
389                    fee,
390                    tx_hash,
391                    tx,
392                }
393            })
394            .collect(),
395        pow_hash: prepped_block.pow_hash,
396        generated_coins,
397        weight: block_weight,
398        height: context.chain_height,
399        long_term_weight: context.next_block_long_term_weight(block_weight),
400        cumulative_difficulty: context.cumulative_difficulty + context.next_difficulty,
401    };
402
403    if let VerificationContext::BatchPrepareCache(batch_prep_cache) = verification_context {
404        batch_prep_cache.output_cache.add_block_to_cache(&block);
405    }
406
407    Ok(block)
408}