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, 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, ExtendedConsensusError>
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(mut alt_context_cache) = context_svc
55        .ready()
56        .await?
57        .call(BlockChainContextRequest::AltChainContextCache {
58            prev_id: block.header.previous,
59            _token: AltChainRequestToken,
60        })
61        .await?
62    else {
63        panic!("Context service returned wrong response!");
64    };
65
66    // Check if the block's miner input is formed correctly.
67    let [Input::Gen(height)] = &block.miner_transaction().prefix().inputs[..] else {
68        return Err(ConsensusError::Block(BlockError::MinerTxError(
69            MinerTxError::InputNotOfTypeGen,
70        ))
71        .into());
72    };
73
74    if *height != alt_context_cache.chain_height {
75        return Err(ConsensusError::Block(BlockError::MinerTxError(
76            MinerTxError::InputsHeightIncorrect,
77        ))
78        .into());
79    }
80
81    // prep the alt block.
82    let prepped_block = {
83        let rx_vm = alt_rx_vm(
84            alt_context_cache.chain_height,
85            block.header.hardfork_version,
86            alt_context_cache.parent_chain,
87            &mut alt_context_cache,
88            &mut context_svc,
89        )
90        .await?;
91
92        rayon_spawn_async(move || PreparedBlock::new(block, rx_vm.as_deref())).await?
93    };
94
95    // get the difficulty cache for this alt chain.
96    let difficulty_cache = alt_difficulty_cache(
97        prepped_block.block.header.previous,
98        &mut alt_context_cache,
99        &mut context_svc,
100    )
101    .await?;
102
103    // Check the alt block timestamp is in the correct range.
104    //
105    // Unlike monerod we also check the future time limit.
106    let median_timestamp =
107        difficulty_cache.median_timestamp(u64_to_usize(BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW));
108    check_timestamp(&prepped_block.block, median_timestamp).map_err(ConsensusError::Block)?;
109
110    let next_difficulty = difficulty_cache.next_difficulty(prepped_block.hf_version);
111    // make sure the block's PoW is valid for this difficulty.
112    check_block_pow(&prepped_block.pow_hash, next_difficulty).map_err(ConsensusError::Block)?;
113
114    let cumulative_difficulty = difficulty_cache.cumulative_difficulty() + next_difficulty;
115
116    let ordered_txs = pull_ordered_transactions(&prepped_block.block, txs)?;
117
118    let block_weight =
119        prepped_block.miner_tx_weight + ordered_txs.iter().map(|tx| tx.tx_weight).sum::<usize>();
120
121    let alt_weight_cache = alt_weight_cache(
122        prepped_block.block.header.previous,
123        &mut alt_context_cache,
124        &mut context_svc,
125    )
126    .await?;
127
128    // Check the block weight is below the limit.
129    check_block_weight(
130        block_weight,
131        alt_weight_cache.median_for_block_reward(prepped_block.hf_version),
132    )
133    .map_err(ConsensusError::Block)?;
134
135    let long_term_weight = weight::calculate_block_long_term_weight(
136        prepped_block.hf_version,
137        block_weight,
138        alt_weight_cache.median_long_term_weight(),
139    );
140
141    // Get the chainID or generate a new one if this is the first alt block in this alt chain.
142    let chain_id = *alt_context_cache
143        .chain_id
144        .get_or_insert_with(|| ChainId(rand::random()));
145
146    // Create the alt block info.
147    let block_info = AltBlockInformation {
148        block_hash: prepped_block.block_hash,
149        block: prepped_block.block,
150        block_blob: prepped_block.block_blob,
151        txs: ordered_txs
152            .into_iter()
153            .map(|tx| {
154                let tx_weight = tx.tx_weight;
155                let fee = tx.fee;
156                let tx_hash = tx.tx_hash;
157                let (tx, tx_prunable_blob) = tx.tx.pruned_with_prunable();
158                VerifiedTransactionInformation {
159                    tx_prunable_blob,
160                    tx_pruned: tx.serialize(),
161                    tx_weight,
162                    fee,
163                    tx_hash,
164                    tx,
165                }
166            })
167            .collect(),
168        pow_hash: prepped_block.pow_hash,
169        weight: block_weight,
170        height: alt_context_cache.chain_height,
171        long_term_weight,
172        cumulative_difficulty,
173        chain_id,
174    };
175
176    // Add this block to the cache.
177    alt_context_cache.add_new_block(
178        block_info.height,
179        block_info.block_hash,
180        block_info.weight,
181        block_info.long_term_weight,
182        block_info.block.header.timestamp,
183        cumulative_difficulty,
184    );
185
186    // Add this alt cache back to the context service.
187    context_svc
188        .oneshot(BlockChainContextRequest::AddAltChainContextCache {
189            cache: alt_context_cache,
190            _token: AltChainRequestToken,
191        })
192        .await?;
193
194    Ok(block_info)
195}
196
197/// Retrieves the alt RX VM for the chosen block height.
198///
199/// If the `hf` is less than 12 (the height RX activates), then [`None`] is returned.
200async fn alt_rx_vm<C>(
201    block_height: usize,
202    hf: u8,
203    parent_chain: Chain,
204    alt_chain_context: &mut AltChainContextCache,
205    context_svc: C,
206) -> Result<Option<Arc<RandomXVm>>, ExtendedConsensusError>
207where
208    C: Service<
209            BlockChainContextRequest,
210            Response = BlockChainContextResponse,
211            Error = tower::BoxError,
212        > + Send,
213    C::Future: Send + 'static,
214{
215    if hf < 12 {
216        return Ok(None);
217    }
218
219    let seed_height = randomx_seed_height(block_height);
220
221    let cached_vm = match alt_chain_context.cached_rx_vm.take() {
222        // If the VM is cached and the height is the height we need, we can use this VM.
223        Some((cached_seed_height, vm)) if seed_height == cached_seed_height => {
224            (cached_seed_height, vm)
225        }
226        // Otherwise we need to make a new VM.
227        _ => {
228            let BlockChainContextResponse::AltChainRxVM(vm) = context_svc
229                .oneshot(BlockChainContextRequest::AltChainRxVM {
230                    height: block_height,
231                    chain: parent_chain,
232                    _token: AltChainRequestToken,
233                })
234                .await?
235            else {
236                panic!("Context service returned wrong response!");
237            };
238
239            (seed_height, vm)
240        }
241    };
242
243    Ok(Some(Arc::clone(
244        &alt_chain_context.cached_rx_vm.insert(cached_vm).1,
245    )))
246}
247
248/// Returns the [`DifficultyCache`] for the alt chain.
249async fn alt_difficulty_cache<C>(
250    prev_id: [u8; 32],
251    alt_chain_context: &mut AltChainContextCache,
252    context_svc: C,
253) -> Result<&mut DifficultyCache, ExtendedConsensusError>
254where
255    C: Service<
256            BlockChainContextRequest,
257            Response = BlockChainContextResponse,
258            Error = tower::BoxError,
259        > + Send,
260    C::Future: Send + 'static,
261{
262    // First look to see if the difficulty cache for this alt chain is already cached.
263    match &mut alt_chain_context.difficulty_cache {
264        Some(cache) => Ok(cache),
265        // Otherwise make a new one.
266        difficulty_cache => {
267            let BlockChainContextResponse::AltChainDifficultyCache(cache) = context_svc
268                .oneshot(BlockChainContextRequest::AltChainDifficultyCache {
269                    prev_id,
270                    _token: AltChainRequestToken,
271                })
272                .await?
273            else {
274                panic!("Context service returned wrong response!");
275            };
276
277            Ok(difficulty_cache.insert(cache))
278        }
279    }
280}
281
282/// Returns the [`BlockWeightsCache`] for the alt chain.
283async fn alt_weight_cache<C>(
284    prev_id: [u8; 32],
285    alt_chain_context: &mut AltChainContextCache,
286    context_svc: C,
287) -> Result<&mut BlockWeightsCache, ExtendedConsensusError>
288where
289    C: Service<
290            BlockChainContextRequest,
291            Response = BlockChainContextResponse,
292            Error = tower::BoxError,
293        > + Send,
294    C::Future: Send + 'static,
295{
296    // First look to see if the weight cache for this alt chain is already cached.
297    match &mut alt_chain_context.weight_cache {
298        Some(cache) => Ok(cache),
299        // Otherwise make a new one.
300        weight_cache => {
301            let BlockChainContextResponse::AltChainWeightCache(cache) = context_svc
302                .oneshot(BlockChainContextRequest::AltChainWeightCache {
303                    prev_id,
304                    _token: AltChainRequestToken,
305                })
306                .await?
307            else {
308                panic!("Context service returned wrong response!");
309            };
310
311            Ok(weight_cache.insert(cache))
312        }
313    }
314}