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