Skip to main content

cuprate_consensus/block/
alt_block.rs

1//! Alt Blocks
2//!
3//! Alt blocks are sanity checked by [`sanity_check_alt_block`], that function will also compute the cumulative
4//! difficulty of the alt chain so callers will know if they should re-org to the alt chain.
5use std::{collections::HashMap, sync::Arc};
6
7use monero_oxide::{block::Block, transaction::Input};
8use tower::{Service, ServiceExt};
9
10use cuprate_consensus_context::{
11    difficulty::DifficultyCache,
12    rx_vms::RandomXVm,
13    weight::{self, BlockWeightsCache},
14    AltChainContextCache, AltChainRequestToken, BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW,
15};
16use cuprate_consensus_rules::{
17    blocks::{
18        check_block_pow, check_block_weight, check_timestamp, randomx_seed_height, BlockError,
19    },
20    miner_tx::MinerTxError,
21    ConsensusError,
22};
23use cuprate_helper::{asynch::rayon_spawn_async, cast::u64_to_usize};
24use cuprate_types::{
25    AltBlockInformation, Chain, ChainId, TransactionVerificationData,
26    VerifiedTransactionInformation,
27};
28
29use crate::{
30    block::{free::pull_ordered_transactions, BlockVerificationError, PreparedBlock},
31    BlockChainContextRequest, BlockChainContextResponse, ExtendedConsensusError,
32};
33
34/// This function sanity checks an alt-block.
35///
36/// Returns [`AltBlockInformation`], which contains the cumulative difficulty of the alt chain.
37///
38/// This function only checks the block's proof-of-work and its weight.
39pub async fn sanity_check_alt_block<C>(
40    block: Block,
41    txs: HashMap<[u8; 32], TransactionVerificationData>,
42    mut context_svc: C,
43) -> Result<AltBlockInformation, BlockVerificationError>
44where
45    C: Service<
46            BlockChainContextRequest,
47            Response = BlockChainContextResponse,
48            Error = tower::BoxError,
49        > + Send
50        + 'static,
51    C::Future: Send + 'static,
52{
53    // Fetch the alt-chains context cache.
54    let BlockChainContextResponse::AltChainContextCache(alt_context_cache) = context_svc
55        .ready()
56        .await
57        .map_err(BlockVerificationError::fatal)?
58        .call(BlockChainContextRequest::AltChainContextCache {
59            prev_id: block.header.previous,
60            _token: AltChainRequestToken,
61        })
62        .await
63        .map_err(BlockVerificationError::fatal)?
64    else {
65        panic!("Context service returned wrong response!");
66    };
67
68    let Some(mut alt_context_cache) = alt_context_cache else {
69        return Err(BlockVerificationError::invalid_pow(ConsensusError::Block(
70            BlockError::PreviousIDIncorrect,
71        )));
72    };
73
74    // Check if the block's miner input is formed correctly.
75    let [Input::Gen(height)] = &block.miner_transaction().prefix().inputs[..] else {
76        return Err(BlockVerificationError::invalid_pow(ConsensusError::Block(
77            BlockError::MinerTxError(MinerTxError::InputNotOfTypeGen),
78        )));
79    };
80
81    if *height != alt_context_cache.chain_height {
82        return Err(BlockVerificationError::invalid_pow(ConsensusError::Block(
83            BlockError::MinerTxError(MinerTxError::InputsHeightIncorrect),
84        )));
85    }
86
87    // prep the alt block.
88    let prepped_block = {
89        let rx_vm = alt_rx_vm(
90            alt_context_cache.chain_height,
91            block.header.hardfork_version,
92            alt_context_cache.parent_chain,
93            &mut alt_context_cache,
94            &mut context_svc,
95        )
96        .await
97        .map_err(BlockVerificationError::invalid_pow)?;
98
99        rayon_spawn_async(move || PreparedBlock::new(block, rx_vm.as_deref()))
100            .await
101            .map_err(BlockVerificationError::invalid_pow)?
102    };
103
104    // get the difficulty cache for this alt chain.
105    let difficulty_cache = alt_difficulty_cache(
106        prepped_block.block.header.previous,
107        &mut alt_context_cache,
108        &mut context_svc,
109    )
110    .await
111    .map_err(BlockVerificationError::invalid_pow)?;
112
113    // Check the alt block timestamp is in the correct range.
114    //
115    // Unlike monerod we also check the future time limit.
116    let median_timestamp =
117        difficulty_cache.median_timestamp(u64_to_usize(BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW));
118    check_timestamp(&prepped_block.block, median_timestamp)
119        .map_err(ConsensusError::Block)
120        .map_err(BlockVerificationError::invalid_pow)?;
121
122    let next_difficulty = difficulty_cache.next_difficulty(prepped_block.hf_version);
123    // make sure the block's PoW is valid for this difficulty.
124    check_block_pow(&prepped_block.pow_hash, next_difficulty)
125        .map_err(ConsensusError::Block)
126        .map_err(BlockVerificationError::invalid_pow)?;
127
128    // TODO: weight check before PoW check?
129
130    let cumulative_difficulty = difficulty_cache.cumulative_difficulty() + next_difficulty;
131
132    let ordered_txs = pull_ordered_transactions(&prepped_block.block, txs)
133        .map_err(BlockVerificationError::valid_pow)?;
134
135    let block_weight =
136        prepped_block.miner_tx_weight + ordered_txs.iter().map(|tx| tx.tx_weight).sum::<usize>();
137
138    let alt_weight_cache = alt_weight_cache(
139        prepped_block.block.header.previous,
140        &mut alt_context_cache,
141        &mut context_svc,
142    )
143    .await
144    .map_err(BlockVerificationError::valid_pow)?;
145
146    // Check the block weight is below the limit.
147    check_block_weight(
148        block_weight,
149        alt_weight_cache.median_for_block_reward(prepped_block.hf_version),
150    )
151    .map_err(ConsensusError::Block)
152    .map_err(BlockVerificationError::valid_pow)?;
153
154    let long_term_weight = weight::calculate_block_long_term_weight(
155        prepped_block.hf_version,
156        block_weight,
157        alt_weight_cache.median_long_term_weight(),
158    );
159
160    // Get the chainID or generate a new one if this is the first alt block in this alt chain.
161    let chain_id = *alt_context_cache
162        .chain_id
163        .get_or_insert_with(|| ChainId(rand::random()));
164
165    // Create the alt block info.
166    let block_info = AltBlockInformation {
167        block_hash: prepped_block.block_hash,
168        block: prepped_block.block,
169        block_blob: prepped_block.block_blob,
170        txs: ordered_txs
171            .into_iter()
172            .map(|tx| {
173                let tx_weight = tx.tx_weight;
174                let fee = tx.fee;
175                let tx_hash = tx.tx_hash;
176                let (tx, tx_prunable_blob) = tx.tx.pruned_with_prunable();
177                VerifiedTransactionInformation {
178                    tx_prunable_blob,
179                    tx_pruned: tx.serialize(),
180                    tx_weight,
181                    fee,
182                    tx_hash,
183                    tx,
184                }
185            })
186            .collect(),
187        pow_hash: prepped_block.pow_hash,
188        weight: block_weight,
189        height: alt_context_cache.chain_height,
190        long_term_weight,
191        cumulative_difficulty,
192        chain_id,
193    };
194
195    // Add this block to the cache.
196    alt_context_cache.add_new_block(
197        block_info.height,
198        block_info.block_hash,
199        block_info.weight,
200        block_info.long_term_weight,
201        block_info.block.header.timestamp,
202        cumulative_difficulty,
203    );
204
205    // Add this alt cache back to the context service.
206    context_svc
207        .oneshot(BlockChainContextRequest::AddAltChainContextCache {
208            cache: alt_context_cache,
209            _token: AltChainRequestToken,
210        })
211        .await
212        .map_err(BlockVerificationError::fatal)?;
213
214    Ok(block_info)
215}
216
217/// Retrieves the alt RX VM for the chosen block height.
218///
219/// If the `hf` is less than 12 (the height RX activates), then [`None`] is returned.
220async fn alt_rx_vm<C>(
221    block_height: usize,
222    hf: u8,
223    parent_chain: Chain,
224    alt_chain_context: &mut AltChainContextCache,
225    context_svc: C,
226) -> Result<Option<Arc<RandomXVm>>, ExtendedConsensusError>
227where
228    C: Service<
229            BlockChainContextRequest,
230            Response = BlockChainContextResponse,
231            Error = tower::BoxError,
232        > + Send,
233    C::Future: Send + 'static,
234{
235    if hf < 12 {
236        return Ok(None);
237    }
238
239    let seed_height = randomx_seed_height(block_height);
240
241    let cached_vm = match alt_chain_context.cached_rx_vm.take() {
242        // If the VM is cached and the height is the height we need, we can use this VM.
243        Some((cached_seed_height, vm)) if seed_height == cached_seed_height => {
244            (cached_seed_height, vm)
245        }
246        // Otherwise we need to make a new VM.
247        _ => {
248            let BlockChainContextResponse::AltChainRxVM(vm) = context_svc
249                .oneshot(BlockChainContextRequest::AltChainRxVM {
250                    height: block_height,
251                    chain: parent_chain,
252                    _token: AltChainRequestToken,
253                })
254                .await?
255            else {
256                panic!("Context service returned wrong response!");
257            };
258
259            (seed_height, vm)
260        }
261    };
262
263    Ok(Some(Arc::clone(
264        &alt_chain_context.cached_rx_vm.insert(cached_vm).1,
265    )))
266}
267
268/// Returns the [`DifficultyCache`] for the alt chain.
269async fn alt_difficulty_cache<C>(
270    prev_id: [u8; 32],
271    alt_chain_context: &mut AltChainContextCache,
272    context_svc: C,
273) -> Result<&mut DifficultyCache, ExtendedConsensusError>
274where
275    C: Service<
276            BlockChainContextRequest,
277            Response = BlockChainContextResponse,
278            Error = tower::BoxError,
279        > + Send,
280    C::Future: Send + 'static,
281{
282    // First look to see if the difficulty cache for this alt chain is already cached.
283    match &mut alt_chain_context.difficulty_cache {
284        Some(cache) => Ok(cache),
285        // Otherwise make a new one.
286        difficulty_cache => {
287            let BlockChainContextResponse::AltChainDifficultyCache(cache) = context_svc
288                .oneshot(BlockChainContextRequest::AltChainDifficultyCache {
289                    prev_id,
290                    _token: AltChainRequestToken,
291                })
292                .await?
293            else {
294                panic!("Context service returned wrong response!");
295            };
296
297            let Some(cache) = cache else {
298                return Err(ConsensusError::Block(BlockError::PreviousIDIncorrect).into());
299            };
300
301            Ok(difficulty_cache.insert(cache))
302        }
303    }
304}
305
306/// Returns the [`BlockWeightsCache`] for the alt chain.
307async fn alt_weight_cache<C>(
308    prev_id: [u8; 32],
309    alt_chain_context: &mut AltChainContextCache,
310    context_svc: C,
311) -> Result<&mut BlockWeightsCache, ExtendedConsensusError>
312where
313    C: Service<
314            BlockChainContextRequest,
315            Response = BlockChainContextResponse,
316            Error = tower::BoxError,
317        > + Send,
318    C::Future: Send + 'static,
319{
320    // First look to see if the weight cache for this alt chain is already cached.
321    match &mut alt_chain_context.weight_cache {
322        Some(cache) => Ok(cache),
323        // Otherwise make a new one.
324        weight_cache => {
325            let BlockChainContextResponse::AltChainWeightCache(cache) = context_svc
326                .oneshot(BlockChainContextRequest::AltChainWeightCache {
327                    prev_id,
328                    _token: AltChainRequestToken,
329                })
330                .await?
331            else {
332                panic!("Context service returned wrong response!");
333            };
334
335            let Some(cache) = cache else {
336                return Err(ConsensusError::Block(BlockError::PreviousIDIncorrect).into());
337            };
338
339            Ok(weight_cache.insert(cache))
340        }
341    }
342}