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}