cuprate_consensus_context/
alt_chains.rs

1use std::{collections::HashMap, sync::Arc};
2
3use tower::ServiceExt;
4
5use cuprate_consensus_rules::{blocks::BlockError, ConsensusError};
6use cuprate_types::{
7    blockchain::{BlockchainReadRequest, BlockchainResponse},
8    Chain, ChainId,
9};
10
11use crate::{
12    ContextCacheError, __private::Database, difficulty::DifficultyCache, rx_vms::RandomXVm,
13    weight::BlockWeightsCache,
14};
15
16pub(crate) mod sealed {
17    /// A token that should be hard to create from outside this crate.
18    ///
19    /// It is currently possible to safely create this from outside this crate, **DO NOT** rely on this
20    /// as it will be broken once we find a way to completely seal this.
21    #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
22    pub struct AltChainRequestToken;
23}
24
25/// The context cache of an alternative chain.
26#[derive(Debug, Clone)]
27pub struct AltChainContextCache {
28    /// The alt chain weight cache, [`None`] if it has not been built yet.
29    pub weight_cache: Option<BlockWeightsCache>,
30    /// The alt chain difficulty cache, [`None`] if it has not been built yet.
31    pub difficulty_cache: Option<DifficultyCache>,
32
33    /// A cached RX VM.
34    pub cached_rx_vm: Option<(usize, Arc<RandomXVm>)>,
35
36    /// The chain height of the alt chain.
37    pub chain_height: usize,
38    /// The top hash of the alt chain.
39    pub top_hash: [u8; 32],
40    /// The [`ChainId`] of the alt chain.
41    pub chain_id: Option<ChainId>,
42    /// The parent [`Chain`] of this alt chain.
43    pub parent_chain: Chain,
44}
45
46impl AltChainContextCache {
47    /// Add a new block to the cache.
48    pub fn add_new_block(
49        &mut self,
50        height: usize,
51        block_hash: [u8; 32],
52        block_weight: usize,
53        long_term_block_weight: usize,
54        timestamp: u64,
55        cumulative_difficulty: u128,
56    ) {
57        if let Some(difficulty_cache) = &mut self.difficulty_cache {
58            difficulty_cache.new_block(height, timestamp, cumulative_difficulty);
59        }
60
61        if let Some(weight_cache) = &mut self.weight_cache {
62            weight_cache.new_block(height, block_weight, long_term_block_weight);
63        }
64
65        self.chain_height += 1;
66        self.top_hash = block_hash;
67    }
68}
69
70/// A map of top IDs to alt chains.
71pub(crate) struct AltChainMap {
72    alt_cache_map: HashMap<[u8; 32], Box<AltChainContextCache>>,
73}
74
75impl AltChainMap {
76    pub(crate) fn new() -> Self {
77        Self {
78            alt_cache_map: HashMap::new(),
79        }
80    }
81
82    pub(crate) fn clear(&mut self) {
83        self.alt_cache_map.clear();
84    }
85
86    /// Add an alt chain cache to the map.
87    pub(crate) fn add_alt_cache(&mut self, alt_cache: Box<AltChainContextCache>) {
88        self.alt_cache_map.insert(alt_cache.top_hash, alt_cache);
89    }
90
91    /// Attempts to take an [`AltChainContextCache`] from the map, returning [`None`] if no cache is
92    /// present.
93    pub(crate) async fn get_alt_chain_context<D: Database>(
94        &mut self,
95        prev_id: [u8; 32],
96        database: D,
97    ) -> Result<Box<AltChainContextCache>, ContextCacheError> {
98        if let Some(cache) = self.alt_cache_map.remove(&prev_id) {
99            return Ok(cache);
100        }
101
102        // find the block with hash == prev_id.
103        let BlockchainResponse::FindBlock(res) = database
104            .oneshot(BlockchainReadRequest::FindBlock(prev_id))
105            .await?
106        else {
107            panic!("Database returned wrong response");
108        };
109
110        let Some((parent_chain, top_height)) = res else {
111            // Couldn't find prev_id
112            return Err(ConsensusError::Block(BlockError::PreviousIDIncorrect).into());
113        };
114
115        Ok(Box::new(AltChainContextCache {
116            weight_cache: None,
117            difficulty_cache: None,
118            cached_rx_vm: None,
119            chain_height: top_height + 1,
120            top_hash: prev_id,
121            chain_id: None,
122            parent_chain,
123        }))
124    }
125}
126
127/// Builds a [`DifficultyCache`] for an alt chain.
128pub(crate) async fn get_alt_chain_difficulty_cache<D: Database + Clone>(
129    prev_id: [u8; 32],
130    main_chain_difficulty_cache: &DifficultyCache,
131    mut database: D,
132) -> Result<DifficultyCache, ContextCacheError> {
133    // find the block with hash == prev_id.
134    let BlockchainResponse::FindBlock(res) = database
135        .ready()
136        .await?
137        .call(BlockchainReadRequest::FindBlock(prev_id))
138        .await?
139    else {
140        panic!("Database returned wrong response");
141    };
142
143    let Some((chain, top_height)) = res else {
144        // Can't find prev_id
145        return Err(ConsensusError::Block(BlockError::PreviousIDIncorrect).into());
146    };
147
148    Ok(match chain {
149        Chain::Main => {
150            // prev_id is in main chain, we can use the fast path and clone the main chain cache.
151            let mut difficulty_cache = main_chain_difficulty_cache.clone();
152            difficulty_cache
153                .pop_blocks_main_chain(
154                    difficulty_cache.last_accounted_height - top_height,
155                    database,
156                )
157                .await?;
158
159            difficulty_cache
160        }
161        Chain::Alt(_) => {
162            // prev_id is in an alt chain, completely rebuild the cache.
163            DifficultyCache::init_from_chain_height(
164                top_height + 1,
165                main_chain_difficulty_cache.config,
166                database,
167                chain,
168            )
169            .await?
170        }
171    })
172}
173
174/// Builds a [`BlockWeightsCache`] for an alt chain.
175pub(crate) async fn get_alt_chain_weight_cache<D: Database + Clone>(
176    prev_id: [u8; 32],
177    main_chain_weight_cache: &BlockWeightsCache,
178    mut database: D,
179) -> Result<BlockWeightsCache, ContextCacheError> {
180    // find the block with hash == prev_id.
181    let BlockchainResponse::FindBlock(res) = database
182        .ready()
183        .await?
184        .call(BlockchainReadRequest::FindBlock(prev_id))
185        .await?
186    else {
187        panic!("Database returned wrong response");
188    };
189
190    let Some((chain, top_height)) = res else {
191        // Can't find prev_id
192        return Err(ConsensusError::Block(BlockError::PreviousIDIncorrect).into());
193    };
194
195    Ok(match chain {
196        Chain::Main => {
197            // prev_id is in main chain, we can use the fast path and clone the main chain cache.
198            let mut weight_cache = main_chain_weight_cache.clone();
199            weight_cache
200                .pop_blocks_main_chain(weight_cache.tip_height - top_height, database)
201                .await?;
202
203            weight_cache
204        }
205        Chain::Alt(_) => {
206            // prev_id is in an alt chain, completely rebuild the cache.
207            BlockWeightsCache::init_from_chain_height(
208                top_height + 1,
209                main_chain_weight_cache.config,
210                database,
211                chain,
212            )
213            .await?
214        }
215    })
216}