Skip to main content

cuprate_consensus_context/
lib.rs

1//! # Blockchain Context
2//!
3//! This crate contains a service to get cached context from the blockchain: [`BlockchainContext`].
4//! This is used during contextual validation, this does not have all the data for contextual validation
5//! (outputs) for that you will need a [`Database`].
6
7// Used in documentation references for [`BlockChainContextRequest`]
8// FIXME: should we pull in a dependency just to link docs?
9use monero_oxide as _;
10
11use std::{
12    cmp::min,
13    collections::HashMap,
14    future::Future,
15    num::NonZero,
16    pin::Pin,
17    sync::Arc,
18    task::{Context, Poll},
19};
20
21use arc_swap::Cache;
22use futures::{channel::oneshot, FutureExt};
23use monero_oxide::block::Block;
24use tokio::sync::mpsc;
25use tokio_util::sync::PollSender;
26use tower::Service;
27
28use cuprate_consensus_rules::{
29    blocks::ContextToVerifyBlock, current_unix_timestamp, ConsensusError, HardFork,
30};
31
32pub mod difficulty;
33pub mod distribution;
34pub mod hardforks;
35pub mod rx_vms;
36pub mod weight;
37
38mod alt_chains;
39mod task;
40
41use cuprate_types::{
42    rpc::{ChainInfo, FeeEstimate, HardForkInfo, OutputDistributionData},
43    Chain,
44};
45use difficulty::DifficultyCache;
46use rx_vms::RandomXVm;
47use weight::BlockWeightsCache;
48
49pub use alt_chains::{sealed::AltChainRequestToken, AltChainContextCache};
50pub use difficulty::DifficultyCacheConfig;
51pub use hardforks::HardForkConfig;
52pub use weight::BlockWeightsCacheConfig;
53
54pub const BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW: u64 = 60;
55
56/// Config for the context service.
57pub struct ContextConfig {
58    /// Hard-forks config.
59    pub hard_fork_cfg: HardForkConfig,
60    /// Difficulty config.
61    pub difficulty_cfg: DifficultyCacheConfig,
62    /// Block weight config.
63    pub weights_config: BlockWeightsCacheConfig,
64}
65
66impl ContextConfig {
67    /// Get the config for main-net.
68    pub const fn main_net() -> Self {
69        Self {
70            hard_fork_cfg: HardForkConfig::main_net(),
71            difficulty_cfg: DifficultyCacheConfig::main_net(),
72            weights_config: BlockWeightsCacheConfig::main_net(),
73        }
74    }
75
76    /// Get the config for stage-net.
77    pub const fn stage_net() -> Self {
78        Self {
79            hard_fork_cfg: HardForkConfig::stage_net(),
80            // These 2 have the same config as main-net.
81            difficulty_cfg: DifficultyCacheConfig::main_net(),
82            weights_config: BlockWeightsCacheConfig::main_net(),
83        }
84    }
85
86    /// Get the config for test-net.
87    pub const fn test_net() -> Self {
88        Self {
89            hard_fork_cfg: HardForkConfig::test_net(),
90            // These 2 have the same config as main-net.
91            difficulty_cfg: DifficultyCacheConfig::main_net(),
92            weights_config: BlockWeightsCacheConfig::main_net(),
93        }
94    }
95
96    /// Get the config for fake-chain (regtest).
97    pub const fn fake_chain() -> Self {
98        Self {
99            hard_fork_cfg: HardForkConfig::fake_chain(),
100            difficulty_cfg: DifficultyCacheConfig::main_net(),
101            weights_config: BlockWeightsCacheConfig::main_net(),
102        }
103    }
104}
105
106/// Initialize the blockchain context service.
107///
108/// This function will request a lot of data from the database so it may take a while.
109pub async fn initialize_blockchain_context<D>(
110    cfg: ContextConfig,
111    database: D,
112) -> Result<BlockchainContextService, ContextCacheError>
113where
114    D: Database + Clone + Send + Sync + 'static,
115    D::Future: Send + 'static,
116{
117    let (context_task, context_cache) = task::ContextTask::init_context(cfg, database).await?;
118
119    // TODO: make buffer size configurable.
120    let (tx, rx) = mpsc::channel(15);
121
122    tokio::spawn(context_task.run(rx));
123
124    Ok(BlockchainContextService {
125        cached_context: Cache::new(context_cache),
126
127        channel: PollSender::new(tx),
128    })
129}
130
131/// Raw blockchain context, gotten from [`BlockchainContext`]. This data may turn invalid so is not ok to keep
132/// around. You should keep around [`BlockchainContext`] instead.
133#[derive(Debug, Clone, Eq, PartialEq)]
134pub struct BlockchainContext {
135    /// The current cumulative difficulty.
136    pub cumulative_difficulty: u128,
137    /// Context to verify a block, as needed by [`cuprate_consensus_rules`]
138    pub context_to_verify_block: ContextToVerifyBlock,
139    /// The median long term block weight.
140    median_long_term_weight: usize,
141    /// The top blocks timestamp (will be [`None`] if the top block is the genesis).
142    top_block_timestamp: Option<u64>,
143}
144
145impl std::ops::Deref for BlockchainContext {
146    type Target = ContextToVerifyBlock;
147    fn deref(&self) -> &Self::Target {
148        &self.context_to_verify_block
149    }
150}
151
152impl BlockchainContext {
153    /// Returns the timestamp the should be used when checking locked outputs.
154    ///
155    /// ref: <https://cuprate.github.io/monero-book/consensus_rules/transactions/unlock_time.html#getting-the-current-time>
156    pub fn current_adjusted_timestamp_for_time_lock(&self) -> u64 {
157        // FIXME: use if let chain with Rust 2024.
158        if self.current_hf < HardFork::V13 {
159            return current_unix_timestamp();
160        }
161
162        let Some(median) = self.median_block_timestamp else {
163            return current_unix_timestamp();
164        };
165
166        let block_time = self.current_hf.block_time().as_secs();
167        let adjusted_median = median + (BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW + 1) * block_time / 2;
168
169        // This is safe as we just checked if the median was None and this will only be none for genesis and the first block.
170        let adjusted_top_block = self.top_block_timestamp.unwrap() + block_time;
171
172        min(adjusted_median, adjusted_top_block)
173    }
174
175    /// Returns the next blocks long term weight from its block weight.
176    pub fn next_block_long_term_weight(&self, block_weight: usize) -> usize {
177        weight::calculate_block_long_term_weight(
178            self.current_hf,
179            block_weight,
180            self.median_long_term_weight,
181        )
182    }
183}
184
185/// Data needed from a new block to add it to the context cache.
186#[derive(Debug, Clone)]
187pub struct NewBlockData {
188    /// The block's hash.
189    pub block_hash: [u8; 32],
190    /// The block's height.
191    pub height: usize,
192    /// The block's timestamp.
193    pub timestamp: u64,
194    /// The block's weight.
195    pub weight: usize,
196    /// long term weight of this block.
197    pub long_term_weight: usize,
198    /// The coins generated by this block.
199    pub generated_coins: u64,
200    /// The block's hf vote.
201    pub vote: HardFork,
202    /// The cumulative difficulty of the chain.
203    pub cumulative_difficulty: u128,
204    /// The number of RCT outputs in this block.
205    pub numb_rct_outputs: usize,
206}
207
208/// A request to the blockchain context cache.
209#[derive(Debug, Clone)]
210pub enum BlockChainContextRequest {
211    /// Gets all the current RandomX VMs.
212    CurrentRxVms,
213
214    /// Get the next difficulties for these blocks.
215    ///
216    /// Inputs: a list of block timestamps and hfs
217    ///
218    /// The number of difficulties returned will be one more than the number of timestamps/ hfs.
219    BatchGetDifficulties(Vec<(u64, HardFork)>),
220
221    /// Add a VM that has been created outside of the blockchain context service to the blockchain context.
222    /// This is useful when batch calculating POW as you may need to create a new VM if you batch a lot of blocks together,
223    /// it would be wasteful to then not give this VM to the context service to then use when it needs to init a VM with the same
224    /// seed.
225    ///
226    /// This should include the seed used to init this VM and the VM.
227    NewRXVM(([u8; 32], Arc<RandomXVm>)),
228
229    /// A request to add a new block to the cache.
230    Update(NewBlockData),
231
232    /// Pop blocks from the cache to the specified height.
233    PopBlocks {
234        /// The number of blocks to pop from the top of the chain.
235        ///
236        /// # Panics
237        ///
238        /// This will panic if the number of blocks will pop the genesis block.
239        numb_blocks: usize,
240    },
241
242    /// Get information on all hardforks.
243    HardForkInfos,
244
245    /// Get the current fee estimate.
246    FeeEstimate {
247        /// TODO
248        grace_blocks: u64,
249    },
250
251    /// Get the RCT output distribution.
252    RctOutputDistribution {
253        /// The height to start the distribution from.
254        from_height: u64,
255        /// The height to end the distribution at, [`None`] means the top block.
256        to_height: Option<NonZero<u64>>,
257        /// Whether the distribution should be cumulative.
258        cumulative: bool,
259    },
260
261    /// Calculate proof-of-work for this block.
262    CalculatePow {
263        /// The hardfork of the protocol at this block height.
264        hardfork: HardFork,
265        /// The height of the block.
266        height: usize,
267        /// The block data.
268        ///
269        /// This is boxed because [`Block`] causes this enum to be 1200 bytes,
270        /// where the 2nd variant is only 96 bytes.
271        block: Box<Block>,
272        /// The seed hash for the proof-of-work.
273        seed_hash: [u8; 32],
274    },
275
276    /// Clear the alt chain context caches.
277    ClearAltCache,
278
279    /// Get information on all the current alternate chains.
280    AltChains,
281
282    //----------------------------------------------------------------------------------------------------------- AltChainRequests
283    /// A request for an alt chain context cache.
284    ///
285    /// This variant is private and is not callable from outside this crate, the block verifier service will
286    /// handle getting the alt cache.
287    AltChainContextCache {
288        /// The previous block field in a [`BlockHeader`](monero_oxide::block::BlockHeader).
289        prev_id: [u8; 32],
290        /// An internal token to prevent external crates calling this request.
291        _token: AltChainRequestToken,
292    },
293
294    /// A request for a difficulty cache of an alternative chain.
295    ///
296    /// This variant is private and is not callable from outside this crate, the block verifier service will
297    /// handle getting the difficulty cache of an alt chain.
298    AltChainDifficultyCache {
299        /// The previous block field in a [`BlockHeader`](monero_oxide::block::BlockHeader).
300        prev_id: [u8; 32],
301        /// An internal token to prevent external crates calling this request.
302        _token: AltChainRequestToken,
303    },
304
305    /// A request for a block weight cache of an alternative chain.
306    ///
307    /// This variant is private and is not callable from outside this crate, the block verifier service will
308    /// handle getting the weight cache of an alt chain.
309    AltChainWeightCache {
310        /// The previous block field in a [`BlockHeader`](monero_oxide::block::BlockHeader).
311        prev_id: [u8; 32],
312        /// An internal token to prevent external crates calling this request.
313        _token: AltChainRequestToken,
314    },
315
316    /// A request for a RX VM for an alternative chain.
317    ///
318    /// Response variant: [`BlockChainContextResponse::AltChainRxVM`].
319    ///
320    /// This variant is private and is not callable from outside this crate, the block verifier service will
321    /// handle getting the randomX VM of an alt chain.
322    AltChainRxVM {
323        /// The height the RandomX VM is needed for.
324        height: usize,
325        /// The chain to look in for the seed.
326        chain: Chain,
327        /// An internal token to prevent external crates calling this request.
328        _token: AltChainRequestToken,
329    },
330
331    /// A request to add an alt chain context cache to the context cache.
332    ///
333    /// This variant is private and is not callable from outside this crate, the block verifier service will
334    /// handle returning the alt cache to the context service.
335    AddAltChainContextCache {
336        /// The cache.
337        cache: Box<AltChainContextCache>,
338        /// An internal token to prevent external crates calling this request.
339        _token: AltChainRequestToken,
340    },
341}
342
343pub enum BlockChainContextResponse {
344    /// A generic Ok response.
345    ///
346    /// Response to:
347    /// - [`BlockChainContextRequest::NewRXVM`]
348    /// - [`BlockChainContextRequest::Update`]
349    /// - [`BlockChainContextRequest::PopBlocks`]
350    /// - [`BlockChainContextRequest::ClearAltCache`]
351    /// - [`BlockChainContextRequest::AddAltChainContextCache`]
352    Ok,
353
354    /// Response to [`BlockChainContextRequest::CurrentRxVms`]
355    ///
356    /// A map of seed height to RandomX VMs.
357    RxVms(HashMap<usize, Arc<RandomXVm>>),
358
359    /// A list of difficulties.
360    BatchDifficulties(Vec<u128>),
361
362    /// Response to [`BlockChainContextRequest::HardForkInfos`]
363    HardForkInfos(Vec<HardForkInfo>),
364
365    /// Response to [`BlockChainContextRequest::FeeEstimate`]
366    FeeEstimate(FeeEstimate),
367
368    /// Response to [`BlockChainContextRequest::RctOutputDistribution`]
369    RctOutputDistribution(OutputDistributionData),
370
371    /// Response to [`BlockChainContextRequest::CalculatePow`]
372    CalculatePow([u8; 32]),
373
374    /// Response to [`BlockChainContextRequest::AltChains`]
375    ///
376    /// If the inner [`Vec::is_empty`], there were no alternate chains.
377    AltChains(Vec<ChainInfo>),
378
379    /// An alt chain context cache.
380    AltChainContextCache(Box<AltChainContextCache>),
381
382    /// A difficulty cache for an alt chain.
383    AltChainDifficultyCache(DifficultyCache),
384
385    /// A randomX VM for an alt chain.
386    AltChainRxVM(Arc<RandomXVm>),
387
388    /// A weight cache for an alt chain
389    AltChainWeightCache(BlockWeightsCache),
390}
391
392/// The blockchain context service.
393#[derive(Clone)]
394pub struct BlockchainContextService {
395    cached_context: Cache<Arc<arc_swap::ArcSwap<BlockchainContext>>, Arc<BlockchainContext>>,
396
397    channel: PollSender<task::ContextTaskRequest>,
398}
399
400impl BlockchainContextService {
401    /// Get the current [`BlockchainContext`] from the cache.
402    pub fn blockchain_context(&mut self) -> &BlockchainContext {
403        self.cached_context.load()
404    }
405
406    /// Get a snapshot of the current [`BlockchainContext`].
407    pub fn blockchain_context_snapshot(&self) -> arc_swap::Guard<Arc<BlockchainContext>> {
408        self.cached_context.arc_swap().load()
409    }
410}
411
412impl Service<BlockChainContextRequest> for BlockchainContextService {
413    type Response = BlockChainContextResponse;
414    type Error = tower::BoxError;
415    type Future =
416        Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send + 'static>>;
417
418    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
419        self.channel
420            .poll_reserve(cx)
421            .map_err(|_| "Context service channel closed".into())
422    }
423
424    fn call(&mut self, req: BlockChainContextRequest) -> Self::Future {
425        let (tx, rx) = oneshot::channel();
426
427        let req = task::ContextTaskRequest {
428            req,
429            tx,
430            span: tracing::Span::current(),
431        };
432
433        let res = self.channel.send_item(req);
434
435        async move {
436            res.map_err(|_| "Context service closed.")?;
437            rx.await.expect("Oneshot closed without response!")
438        }
439        .boxed()
440    }
441}
442
443#[derive(Debug, thiserror::Error)]
444pub enum ContextCacheError {
445    /// A consensus error.
446    #[error("{0}")]
447    ConErr(#[from] ConsensusError),
448    /// A database error.
449    #[error("Database error: {0}")]
450    DBErr(#[from] tower::BoxError),
451}
452
453use __private::Database;
454
455pub mod __private {
456    use std::future::Future;
457
458    use cuprate_types::blockchain::{BlockchainReadRequest, BlockchainResponse};
459
460    /// A type alias trait used to represent a database, so we don't have to write [`tower::Service`] bounds
461    /// everywhere.
462    ///
463    /// Automatically implemented for:
464    /// ```ignore
465    /// tower::Service<BCReadRequest, Response = BCResponse, Error = tower::BoxError>
466    /// ```
467    pub trait Database:
468        tower::Service<
469        BlockchainReadRequest,
470        Response = BlockchainResponse,
471        Error = tower::BoxError,
472        Future = Self::Future2,
473    >
474    {
475        type Future2: Future<Output = Result<Self::Response, Self::Error>> + Send + 'static;
476    }
477
478    impl<
479            T: tower::Service<
480                BlockchainReadRequest,
481                Response = BlockchainResponse,
482                Error = tower::BoxError,
483            >,
484        > Database for T
485    where
486        T::Future: Future<Output = Result<Self::Response, Self::Error>> + Send + 'static,
487    {
488        type Future2 = T::Future;
489    }
490}