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