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