1use std::{
5 cmp::min,
6 collections::{BTreeMap, HashMap, HashSet},
7 ops::Range,
8 sync::Arc,
9 task::{Context, Poll},
10};
11
12use bytes::Bytes;
13use fjall::Readable;
14use futures::channel::oneshot;
15use indexmap::{IndexMap, IndexSet};
16use rayon::{
17 iter::{Either, IntoParallelIterator, ParallelIterator},
18 prelude::*,
19 ThreadPool,
20};
21use tapes::TapesRead;
22use tower::Service;
23
24use cuprate_helper::{
25 asynch::InfallibleOneshotReceiver,
26 cast::{u64_to_usize, usize_to_u64},
27 map::{combine_low_high_bits_to_u128, split_u128_into_low_high_bits},
28};
29use cuprate_types::{
30 blockchain::{BlockchainReadRequest, BlockchainResponse},
31 output_cache::OutputCache,
32 rpc::{
33 ChainInfo, CoinbaseTxSum, OutputDistributionData, OutputHistogramEntry,
34 OutputHistogramInput,
35 },
36 Chain, ChainId, ExtendedBlockHeader, PreRctOutputDistributionInput, TransactionBlobs,
37 TxInBlockchain, TxsInBlock,
38};
39
40use crate::{
41 error::{BlockchainError, DbResult},
42 ops::{
43 alt_block::{
44 alt_block_height, get_alt_block, get_alt_block_extended_header_from_height,
45 get_alt_block_hash, get_alt_block_information, get_alt_chain_history_ranges,
46 },
47 block::{
48 block_exists, block_height, get_block, get_block_by_hash, get_block_complete_entry,
49 get_block_complete_entry_from_height, get_block_extended_header_from_height,
50 },
51 blockchain::find_split_point,
52 output::{
53 get_num_outputs_with_amount, id_to_output_on_chain, unlocked_and_recent_instances,
54 },
55 tx::{get_split_tx_blobs, get_tx_blob_from_id},
56 },
57 service::{
58 free::{compact_history_genesis_not_included, compact_history_index_to_height_offset},
59 ResponseResult,
60 },
61 types::{
62 AltBlockHeight, AltChainInfo, Amount, AmountIndex, BlockHash, BlockHeight,
63 CompactAltBlockInfo, KeyImage, Output, PreRctOutputId, RawChainId,
64 },
65 BlockchainDatabase,
66};
67
68#[derive(Clone)]
70pub struct BlockchainReadHandle {
71 pub pool: Arc<ThreadPool>,
76
77 pub blockchain: Arc<BlockchainDatabase>,
78}
79
80impl Service<BlockchainReadRequest> for BlockchainReadHandle {
81 type Response = BlockchainResponse;
82 type Error = BlockchainError;
83 type Future = InfallibleOneshotReceiver<Result<Self::Response, Self::Error>>;
84
85 fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
86 Poll::Ready(Ok(()))
87 }
88
89 fn call(&mut self, req: BlockchainReadRequest) -> Self::Future {
90 let (tx, rx) = oneshot::channel();
91
92 let db = Arc::clone(&self.blockchain);
93 self.pool.spawn(move || {
94 let res = map_request(&db, req);
95
96 let _ = tx.send(res);
97 });
98
99 InfallibleOneshotReceiver::from(rx)
100 }
101}
102
103fn map_request(
115 env: &BlockchainDatabase, request: BlockchainReadRequest, ) -> Result<BlockchainResponse, BlockchainError> {
118 use BlockchainReadRequest as R;
119
120 match request {
123 R::BlockCompleteEntries(block_hashes) => block_complete_entries(env, block_hashes),
124 R::BlockCompleteEntriesByHeight(heights) => block_complete_entries_by_height(env, heights),
125 R::BlockCompleteEntriesAboveSplitPoint {
126 chain,
127 start_height,
128 no_miner_tx,
129 len,
130 pruned,
131 } => block_complete_entries_above_split_point(
132 env,
133 &chain,
134 start_height,
135 no_miner_tx,
136 len,
137 pruned,
138 ),
139 R::BlockExtendedHeader(block) => block_extended_header(env, block),
140 R::BlockHash(block, chain) => block_hash(env, block, chain),
141 R::BlockHashInRange(blocks, chain) => block_hash_in_range(env, blocks, chain),
142 R::FindBlock(block_hash) => find_block(env, block_hash),
143 R::FilterUnknownHashes(hashes) => filter_unknown_hashes(env, hashes),
144 R::BlockExtendedHeaderInRange(range, chain) => {
145 block_extended_header_in_range(env, range, chain)
146 }
147 R::ChainHeight => chain_height(env),
148 R::GeneratedCoins(height) => generated_coins(env, height),
149 R::CumulativeRctOutsInRange(range) => cumulative_rct_outs_in_range(env, range),
150 R::Outputs {
151 outputs: map,
152 get_txid,
153 } => outputs(env, map, get_txid),
154 R::OutputsVec { outputs, get_txid } => outputs_vec(env, outputs, get_txid),
155 R::NumberOutputsWithAmount(vec) => number_outputs_with_amount(env, vec),
156 R::KeyImagesSpent(set) => key_images_spent(env, set),
157 R::KeyImagesSpentVec(set) => key_images_spent_vec(env, set),
158 R::CompactChainHistory => compact_chain_history(env),
159 R::NextChainEntry(block_hashes, amount) => next_chain_entry(env, &block_hashes, amount),
160 R::FindFirstUnknown(block_ids) => find_first_unknown(env, &block_ids),
161 R::TxsInBlock {
162 block_hash,
163 tx_indexes,
164 } => txs_in_block(env, block_hash, tx_indexes),
165 R::AltBlocksInChain(chain_id) => alt_blocks_in_chain(env, chain_id),
166 R::Block { height } => block(env, height),
167 R::BlockByHash(hash) => block_by_hash(env, hash),
168 R::TotalTxCount => Ok(total_tx_count(env)),
169 R::DatabaseSize => Ok(database_size(env)),
170 R::OutputHistogram(input) => output_histogram(env, &input),
171 R::CoinbaseTxSum { height, count } => coinbase_tx_sum(env, height, count),
172 R::AltChains => alt_chains(env),
173 R::AltChainCount => alt_chain_count(env),
174 R::Transactions { tx_hashes } => transactions(env, tx_hashes),
175 R::TotalRctOutputs => Ok(total_rct_outputs(env)),
176 R::TxOutputIndexes { tx_hash } => tx_output_indexes(env, &tx_hash),
177 R::PreRctOutputDistribution(input) => pre_rct_output_distribution(env, &input),
178 }
179
180 }
182
183fn block_complete_entries(db: &BlockchainDatabase, block_hashes: Vec<BlockHash>) -> ResponseResult {
205 let (tx_ro, tapes) = db.read_transactions()?;
206
207 let (missing_hashes, blocks) = block_hashes
208 .into_par_iter()
209 .map(
210 |block_hash| match get_block_complete_entry(db, &block_hash, false, &tx_ro, &tapes) {
211 Err(BlockchainError::NotFound) => Ok(Either::Left(block_hash)),
212 res => res.map(Either::Right),
213 },
214 )
215 .collect::<DbResult<_>>()?;
216
217 let blockchain_height = crate::ops::blockchain::chain_height(db, &tapes)?;
218
219 Ok(BlockchainResponse::BlockCompleteEntries {
220 blocks,
221 missing_hashes,
222 blockchain_height,
223 })
224}
225
226fn block_complete_entries_above_split_point(
228 db: &BlockchainDatabase,
229 chain: &[[u8; 32]],
230 start_height: Option<usize>,
231 no_miner_tx: bool,
232 len: usize,
233 pruned: bool,
234) -> ResponseResult {
235 const MAX_TOTAL_SIZE: usize = 50 * 1024 * 1024;
239 const MAX_TOTAL_TXS: usize = 10_000;
243
244 let (tx_ro, tapes) = db.read_transactions()?;
245
246 let blockchain_height = crate::ops::blockchain::chain_height(db, &tapes)?;
247 let top_hash = tapes
248 .read_entry(&db.block_infos, usize_to_u64(blockchain_height - 1))?
249 .ok_or(BlockchainError::NotFound)?
250 .block_hash;
251
252 let height = if let Some(h) = start_height {
254 if h >= blockchain_height {
255 return Ok(BlockchainResponse::BlockCompleteEntriesAboveSplitPoint {
256 blocks: vec![],
257 output_indices: vec![],
258 blockchain_height,
259 start_height: h,
260 top_hash,
261 });
262 }
263 h
264 } else {
265 let split = find_split_point(db, chain, false, false, &tx_ro)?;
266
267 if split == chain.len() {
268 return Err(BlockchainError::NotFound);
269 }
270
271 block_height(db, &tx_ro, &chain[split])?.ok_or(BlockchainError::NotFound)?
272 };
273
274 if height == blockchain_height {
275 return Ok(BlockchainResponse::BlockCompleteEntriesAboveSplitPoint {
276 blocks: vec![],
277 output_indices: vec![],
278 blockchain_height,
279 start_height: height,
280 top_hash,
281 });
282 }
283
284 let mut tx_count = 0;
285 let mut total_size = 0;
286
287 let blocks: Vec<_> = (height..min(height + len, blockchain_height))
288 .map_while(|height| {
289 if total_size >= MAX_TOTAL_SIZE || tx_count >= MAX_TOTAL_TXS {
290 return None;
291 }
292
293 let block = match get_block_complete_entry_from_height(height, pruned, &tapes, db) {
294 Ok(v) => v,
295 Err(e) => return Some(Err(e)),
296 };
297
298 tx_count += block.txs.len() + 1;
299
300 let tx_blobs_size = match &block.txs {
301 TransactionBlobs::None => 0,
302 TransactionBlobs::Normal(b) => b.iter().map(Bytes::len).sum(),
303 TransactionBlobs::Pruned(p) => p.iter().map(|p| p.blob.len() + 32).sum(),
304 };
305
306 total_size += block.block.len() + tx_blobs_size;
307
308 Some(Ok(block))
309 })
310 .collect::<DbResult<_>>()?;
311
312 let first_tx_idx = tapes
313 .read_entry(&db.block_infos, usize_to_u64(height))?
314 .ok_or(BlockchainError::NotFound)?
315 .mining_tx_index;
316
317 let mut output_indices = Vec::with_capacity(blocks.len());
318 output_indices.push(Vec::with_capacity(8));
319
320 let mut last_height = height;
321 let mut miner_tx = true;
322
323 for (i, tx_info) in tapes.iter_from(&db.tx_infos, first_tx_idx)?.enumerate() {
324 let tx_info = tx_info?;
325
326 if tx_info.height != last_height {
327 if tx_info.height == height + blocks.len() {
328 break;
330 }
331 last_height = tx_info.height;
332 miner_tx = true;
333 output_indices.push(Vec::with_capacity(8));
334 }
335
336 if no_miner_tx && miner_tx {
339 miner_tx = false;
340 output_indices.last_mut().unwrap().push(vec![]);
341 continue;
342 }
343 miner_tx = false;
344
345 let o_indexes = if tx_info.is_v1_tx() {
346 let res = tx_ro
348 .get(
349 &db.v1_tx_outputs,
350 (first_tx_idx + usize_to_u64(i)).to_le_bytes(),
351 )?
352 .ok_or(BlockchainError::NotFound)?;
353
354 res.chunks(8)
355 .map(|chunk| u64::from_le_bytes(chunk.try_into().unwrap()))
356 .collect::<Vec<_>>()
357 } else {
358 (0..tx_info.numb_rct_outputs)
360 .map(|i| usize_to_u64(i) + tx_info.rct_output_start_idx)
361 .collect()
362 };
363
364 output_indices.last_mut().unwrap().push(o_indexes);
365 }
366
367 Ok(BlockchainResponse::BlockCompleteEntriesAboveSplitPoint {
368 blocks,
369 output_indices,
370 blockchain_height,
371 start_height: height,
372 top_hash,
373 })
374}
375
376fn block_complete_entries_by_height(
378 db: &BlockchainDatabase,
379 block_heights: Vec<BlockHeight>,
380) -> ResponseResult {
381 let tapes = db.linear_tapes.reader();
382
383 let blocks = block_heights
384 .into_par_iter()
385 .map(|height| get_block_complete_entry_from_height(height, false, &tapes, db))
386 .collect::<DbResult<_>>()?;
387
388 Ok(BlockchainResponse::BlockCompleteEntriesByHeight(blocks))
389}
390
391#[inline]
393fn block_extended_header(db: &BlockchainDatabase, block_height: BlockHeight) -> ResponseResult {
394 let tapes = db.linear_tapes.reader();
395
396 Ok(BlockchainResponse::BlockExtendedHeader(
397 get_block_extended_header_from_height(block_height, &tapes, db)?,
398 ))
399}
400
401#[inline]
403fn block_hash(db: &BlockchainDatabase, block_height: BlockHeight, chain: Chain) -> ResponseResult {
404 let (tx_ro, tapes) = db.read_transactions()?;
405
406 let block_hash = match chain {
407 Chain::Main => {
408 tapes
409 .read_entry(&db.block_infos, usize_to_u64(block_height))?
410 .ok_or(BlockchainError::NotFound)?
411 .block_hash
412 }
413 Chain::Alt(chain) => get_alt_block_hash(db, &block_height, chain, &tx_ro, &tapes)?,
414 };
415
416 Ok(BlockchainResponse::BlockHash(block_hash))
417}
418
419#[inline]
421fn block_hash_in_range(
422 db: &BlockchainDatabase,
423 range: Range<usize>,
424 chain: Chain,
425) -> ResponseResult {
426 let (tx_ro, tapes) = db.read_transactions()?;
427
428 if range.is_empty() {
429 return Ok(BlockchainResponse::BlockHashInRange(vec![]));
430 }
431
432 let block_hash = match chain {
433 Chain::Main => tapes
434 .iter_from(&db.block_infos, usize_to_u64(range.start))?
435 .map(|info| Ok(info?.block_hash))
436 .take(range.len())
437 .collect::<Result<_, BlockchainError>>()?,
438 Chain::Alt(chain) => range
439 .into_par_iter()
440 .map(|block_height| get_alt_block_hash(db, &block_height, chain, &tx_ro, &tapes))
441 .collect::<DbResult<Vec<_>>>()?,
442 };
443
444 Ok(BlockchainResponse::BlockHashInRange(block_hash))
445}
446
447fn find_block(db: &BlockchainDatabase, block_hash: BlockHash) -> ResponseResult {
449 let tx_ro = db.fjall.snapshot();
450
451 let location = if let Some(height) = block_height(db, &tx_ro, &block_hash)? {
453 Some((Chain::Main, height))
454 } else {
455 alt_block_height(db, &tx_ro, &block_hash)?
456 .map(|alt| (Chain::Alt(alt.chain_id.into()), alt.height))
457 };
458
459 Ok(BlockchainResponse::FindBlock(location))
460}
461
462#[inline]
464fn filter_unknown_hashes(
465 db: &BlockchainDatabase,
466 mut hashes: HashSet<BlockHash>,
467) -> ResponseResult {
468 let tx_ro = db.fjall.snapshot();
469
470 let mut err = None;
471
472 hashes.retain(|block_hash| match block_exists(db, block_hash, &tx_ro) {
473 Ok(exists) => exists,
474 Err(e) => {
475 err.get_or_insert(e);
476 false
477 }
478 });
479
480 if let Some(e) = err {
481 Err(e)
482 } else {
483 Ok(BlockchainResponse::FilterUnknownHashes(hashes))
484 }
485}
486
487#[inline]
489fn block_extended_header_in_range(
490 db: &BlockchainDatabase,
491 range: Range<BlockHeight>,
492 chain: Chain,
493) -> ResponseResult {
494 let (tx_ro, tapes) = db.read_transactions()?;
495
496 let vec = match chain {
498 Chain::Main => range
499 .into_iter()
500 .map(|block_height| get_block_extended_header_from_height(block_height, &tapes, db))
501 .collect::<DbResult<Vec<ExtendedBlockHeader>>>()?,
502 Chain::Alt(chain_id) => {
503 let ranges = { get_alt_chain_history_ranges(db, range, chain_id, &tx_ro)? };
504
505 ranges
506 .iter()
507 .rev()
508 .flat_map(|(chain, range)| {
509 range.clone().map(|height| match *chain {
510 Chain::Main => get_block_extended_header_from_height(height, &tapes, db),
511 Chain::Alt(chain_id) => get_alt_block_extended_header_from_height(
512 db,
513 &AltBlockHeight {
514 chain_id: chain_id.into(),
515 height,
516 },
517 &tx_ro,
518 ),
519 })
520 })
521 .collect::<DbResult<Vec<_>>>()?
522 }
523 };
524
525 Ok(BlockchainResponse::BlockExtendedHeaderInRange(vec))
526}
527
528#[inline]
530fn chain_height(db: &BlockchainDatabase) -> ResponseResult {
531 let tapes = db.linear_tapes.reader();
532
533 let chain_height = tapes
534 .fixed_sized_tape_len(&db.block_infos)
535 .expect("Required tape not found");
536
537 if chain_height == 0 {
538 return Err(BlockchainError::NotFound);
539 }
540
541 let block_hash = tapes
542 .read_entry(&db.block_infos, chain_height - 1)?
543 .ok_or(BlockchainError::NotFound)?
544 .block_hash;
545
546 Ok(BlockchainResponse::ChainHeight(
547 chain_height.try_into().unwrap(),
548 block_hash,
549 ))
550}
551
552#[inline]
554fn generated_coins(db: &BlockchainDatabase, height: usize) -> ResponseResult {
555 let tapes = db.linear_tapes.reader();
556
557 Ok(BlockchainResponse::GeneratedCoins(
558 tapes
559 .read_entry(&db.block_infos, usize_to_u64(height))?
560 .map_or(0, |info| info.cumulative_generated_coins),
561 ))
562}
563
564#[inline]
566fn cumulative_rct_outs_in_range(db: &BlockchainDatabase, range: Range<usize>) -> ResponseResult {
567 let tapes = db.linear_tapes.reader();
568
569 let cumulative_rct_outs = tapes
570 .iter_from(&db.block_infos, usize_to_u64(range.start))?
571 .take(range.len())
572 .map(|info| Ok(info?.cumulative_rct_outs))
573 .collect::<DbResult<Vec<u64>>>()?;
574
575 if cumulative_rct_outs.len() < range.len() {
576 return Err(BlockchainError::NotFound);
577 }
578
579 Ok(BlockchainResponse::CumulativeRctOutsInRange(
580 cumulative_rct_outs,
581 ))
582}
583
584#[inline]
586fn outputs(
587 db: &BlockchainDatabase,
588 outputs: IndexMap<Amount, IndexSet<AmountIndex>>,
589 get_txid: bool,
590) -> ResponseResult {
591 let (tx_ro, tapes) = db.read_transactions()?;
594
595 let amount_of_outs = outputs
596 .par_iter()
597 .map(|(&amount, _)| {
598 if amount == 0 {
599 Ok((
600 amount,
601 tapes
602 .fixed_sized_tape_len(&db.rct_outputs)
603 .expect("Required tape not found"),
604 ))
605 } else {
606 match get_num_outputs_with_amount(db, &tx_ro, amount) {
608 Ok(count) => Ok((amount, count)),
609 Err(e) => Err(e),
610 }
611 }
612 })
613 .collect::<Result<_, _>>()?;
614
615 let inner_map = |amount, amount_index| {
618 let id = PreRctOutputId {
619 amount,
620 amount_index,
621 };
622
623 let output_on_chain = match id_to_output_on_chain(db, &id, get_txid, &tx_ro, &tapes) {
624 Ok(output) => output,
625 Err(BlockchainError::NotFound) => return Ok(Either::Right(amount_index)),
626 Err(e) => return Err(e),
627 };
628
629 Ok(Either::Left((amount_index, output_on_chain)))
630 };
631
632 let (map, wanted_outputs) = outputs
633 .into_par_iter()
634 .map(|(amount, amount_index_set)| {
635 let (left, right) = amount_index_set
636 .into_par_iter()
637 .map(|amount_index| inner_map(amount, amount_index))
638 .collect::<Result<_, _>>()?;
639
640 Ok(((amount, left), (amount, right)))
641 })
642 .collect::<DbResult<(IndexMap<_, IndexMap<_, _>>, IndexMap<_, IndexSet<_>>)>>()?;
643
644 let cache = OutputCache::new(map, amount_of_outs, wanted_outputs);
645
646 Ok(BlockchainResponse::Outputs(cache))
647}
648
649#[inline]
651fn outputs_vec(
652 db: &BlockchainDatabase,
653 outputs: Vec<(Amount, AmountIndex)>,
654 get_txid: bool,
655) -> ResponseResult {
656 let (tx_ro, tapes) = db.read_transactions()?;
657
658 let result = outputs
659 .into_iter()
660 .map(|(amount, amount_index)| {
661 let id = PreRctOutputId {
662 amount,
663 amount_index,
664 };
665 let output = id_to_output_on_chain(db, &id, get_txid, &tx_ro, &tapes)?;
666 Ok((amount, vec![(amount_index, output)]))
667 })
668 .collect::<DbResult<Vec<_>>>()?;
669
670 Ok(BlockchainResponse::OutputsVec(result))
671}
672
673#[inline]
675fn number_outputs_with_amount(db: &BlockchainDatabase, amounts: Vec<Amount>) -> ResponseResult {
676 let (tx_ro, tapes) = db.read_transactions()?;
677
678 let num_rct_outputs = u64_to_usize(
680 tapes
681 .fixed_sized_tape_len(&db.rct_outputs)
682 .expect("Required tape not found"),
683 );
684
685 let map = amounts
687 .into_par_iter()
688 .map(|amount| {
689 if amount == 0 {
690 Ok((amount, num_rct_outputs))
692 } else {
693 match get_num_outputs_with_amount(db, &tx_ro, amount) {
695 Ok(count) => Ok((amount, u64_to_usize(count))),
696 Err(e) => Err(e),
697 }
698 }
699 })
700 .collect::<DbResult<HashMap<Amount, usize>>>()?;
701
702 Ok(BlockchainResponse::NumberOutputsWithAmount(map))
703}
704
705#[inline]
707fn key_images_spent(db: &BlockchainDatabase, key_images: HashSet<KeyImage>) -> ResponseResult {
708 let tx_ro = db.fjall.snapshot();
709
710 match key_images
717 .into_par_iter()
718 .map(|ki| tx_ro.contains_key(&db.key_images, ki))
719 .find_any(|result| !matches!(result, Ok(false)))
725 {
726 None => Ok(BlockchainResponse::KeyImagesSpent(false)), Some(Ok(true)) => Ok(BlockchainResponse::KeyImagesSpent(true)), Some(Err(e)) => Err(e.into()), Some(Ok(false)) => unreachable!(),
730 }
731}
732
733fn key_images_spent_vec(db: &BlockchainDatabase, key_images: Vec<KeyImage>) -> ResponseResult {
735 let tx_ro = db.fjall.snapshot();
736
737 Ok(BlockchainResponse::KeyImagesSpentVec(
739 key_images
740 .into_par_iter()
741 .map(|ki| tx_ro.contains_key(&db.key_images, ki))
742 .collect::<Result<_, _>>()?,
743 ))
744}
745
746fn compact_chain_history(db: &BlockchainDatabase) -> ResponseResult {
748 let tapes = db.linear_tapes.reader();
749
750 let get_block_info = |height| -> Result<_, BlockchainError> {
751 tapes
752 .read_entry(&db.block_infos, height)?
753 .ok_or(BlockchainError::NotFound)
754 };
755
756 let top_block_height = tapes
757 .fixed_sized_tape_len(&db.block_infos)
758 .expect("Required tape not open")
759 - 1;
760
761 let top_block_info = get_block_info(top_block_height)?;
762 let cumulative_difficulty = combine_low_high_bits_to_u128(
763 top_block_info.cumulative_difficulty_low,
764 top_block_info.cumulative_difficulty_high,
765 );
766
767 const INITIAL_BLOCKS: usize = 11;
769
770 let mut block_ids = (0..)
772 .map(compact_history_index_to_height_offset::<INITIAL_BLOCKS>)
773 .map_while(|i| top_block_height.checked_sub(usize_to_u64(i)))
774 .map(|height| Ok(get_block_info(height)?.block_hash))
775 .collect::<DbResult<Vec<_>>>()?;
776
777 if compact_history_genesis_not_included::<INITIAL_BLOCKS>(u64_to_usize(top_block_height)) {
778 block_ids.push(get_block_info(0)?.block_hash);
779 }
780
781 Ok(BlockchainResponse::CompactChainHistory {
782 cumulative_difficulty,
783 block_ids,
784 })
785}
786
787fn next_chain_entry(
794 db: &BlockchainDatabase,
795 block_ids: &[BlockHash],
796 next_entry_size: usize,
797) -> ResponseResult {
798 let (tx_ro, tapes) = db.read_transactions()?;
799
800 let idx = find_split_point(db, block_ids, false, false, &tx_ro)?;
801
802 if idx == block_ids.len() {
804 return Ok(BlockchainResponse::NextChainEntry {
805 start_height: None,
806 chain_height: 0,
807 block_ids: vec![],
808 block_weights: vec![],
809 cumulative_difficulty: 0,
810 first_block_blob: None,
811 });
812 }
813
814 let first_known_block_hash = block_ids[idx];
816 let first_known_height =
817 block_height(db, &tx_ro, &first_known_block_hash)?.ok_or(BlockchainError::NotFound)?;
818
819 let chain_height = crate::ops::blockchain::chain_height(db, &tapes)?;
820 let last_height_in_chain_entry = min(first_known_height + next_entry_size, chain_height);
821
822 let entry_count = last_height_in_chain_entry - first_known_height;
823 let mut block_infos = vec![crate::types::BlockInfo::default(); entry_count];
824 tapes.read_entries(
825 &db.block_infos,
826 usize_to_u64(first_known_height),
827 &mut block_infos,
828 )?;
829
830 let (block_ids, block_weights) = block_infos
831 .iter()
832 .map(|info| (info.block_hash, info.weight))
833 .unzip::<_, _, Vec<_>, Vec<_>>();
834
835 let top_block_info = tapes
836 .read_entry(&db.block_infos, usize_to_u64(chain_height) - 1)?
837 .ok_or(BlockchainError::NotFound)?;
838
839 let first_block_blob = if block_ids.len() >= 2 {
840 Some(get_block(&(first_known_height + 1), None, &tapes, db)?.serialize())
841 } else {
842 None
843 };
844
845 Ok(BlockchainResponse::NextChainEntry {
846 start_height: Some(first_known_height),
847 chain_height,
848 block_ids,
849 block_weights,
850 cumulative_difficulty: combine_low_high_bits_to_u128(
851 top_block_info.cumulative_difficulty_low,
852 top_block_info.cumulative_difficulty_high,
853 ),
854 first_block_blob,
855 })
856}
857
858fn find_first_unknown(db: &BlockchainDatabase, block_ids: &[BlockHash]) -> ResponseResult {
865 let tx_ro = db.fjall.snapshot();
866 let idx = find_split_point(db, block_ids, true, true, &tx_ro)?;
867
868 Ok(if idx == block_ids.len() {
869 BlockchainResponse::FindFirstUnknown(None)
870 } else if idx == 0 {
871 BlockchainResponse::FindFirstUnknown(Some((0, 0)))
872 } else {
873 let last_known_height = usize::from_le_bytes(
874 tx_ro
875 .get(&db.block_heights, block_ids[idx - 1])?
876 .unwrap()
877 .as_ref()
878 .try_into()
879 .unwrap(),
880 );
881
882 BlockchainResponse::FindFirstUnknown(Some((idx, last_known_height + 1)))
883 })
884}
885
886fn txs_in_block(
888 db: &BlockchainDatabase,
889 block_hash: BlockHash,
890 missing_txs: Vec<u64>,
891) -> ResponseResult {
892 let (tx_ro, tapes) = db.read_transactions()?;
893
894 let (block, txs) = if let Some(block_height) = block_height(db, &tx_ro, &block_hash)? {
896 let block_info = tapes
897 .read_entry(&db.block_infos, usize_to_u64(block_height))?
898 .ok_or(BlockchainError::NotFound)?;
899
900 let block = get_block(&block_height, Some(&block_info), &tapes, db)?;
901 let first_tx_index = block_info.mining_tx_index + 1;
902
903 if block.transactions.len() < missing_txs.len()
904 || missing_txs
905 .iter()
906 .any(|&i| i >= usize_to_u64(block.transactions.len()))
907 {
908 return Ok(BlockchainResponse::TxsInBlock(None));
909 }
910
911 let txs = missing_txs
912 .into_iter()
913 .map(|index_offset| get_tx_blob_from_id(&(first_tx_index + index_offset), &tapes, db))
914 .collect::<DbResult<_>>()?;
915
916 (block, txs)
917 } else {
918 let alt_block_height =
919 alt_block_height(db, &tx_ro, &block_hash)?.ok_or(BlockchainError::NotFound)?;
920
921 let block = get_alt_block(db, &alt_block_height, &tx_ro)?;
922
923 if block.transactions.len() < missing_txs.len()
924 || missing_txs
925 .iter()
926 .any(|&i| i >= usize_to_u64(block.transactions.len()))
927 {
928 return Ok(BlockchainResponse::TxsInBlock(None));
929 }
930
931 let alt_transaction_blobs = db.alt_transaction_blobs.load();
932 let txs = missing_txs
933 .into_iter()
934 .map(|index_offset| {
935 let tx_hash = &block.transactions[u64_to_usize(index_offset)];
936
937 tx_ro
938 .get(&**alt_transaction_blobs, tx_hash)?
939 .map(|blob| blob.to_vec())
940 .ok_or(BlockchainError::NotFound)
941 })
942 .collect::<DbResult<_>>()?;
943
944 (block, txs)
945 };
946
947 Ok(BlockchainResponse::TxsInBlock(Some(TxsInBlock {
948 block: block.serialize(),
949 txs,
950 })))
951}
952
953fn alt_blocks_in_chain(db: &BlockchainDatabase, chain_id: ChainId) -> ResponseResult {
955 let tx_ro = db.fjall.snapshot();
956
957 let history = { get_alt_chain_history_ranges(db, 0..usize::MAX, chain_id, &tx_ro)? };
959
960 let blocks = history
962 .iter()
963 .rev()
964 .skip(1)
965 .flat_map(|(chain_id, range)| {
966 let Chain::Alt(chain_id) = chain_id else {
967 panic!("Should not have main chain blocks here we skipped last range");
968 };
969
970 range.clone().map(|height| {
971 get_alt_block_information(
972 db,
973 &AltBlockHeight {
974 chain_id: (*chain_id).into(),
975 height,
976 },
977 &tx_ro,
978 )
979 })
980 })
981 .collect::<DbResult<_>>()?;
982
983 Ok(BlockchainResponse::AltBlocksInChain(blocks))
984}
985
986fn block(db: &BlockchainDatabase, block_height: BlockHeight) -> ResponseResult {
988 let tapes = db.linear_tapes.reader();
989
990 Ok(BlockchainResponse::Block(get_block(
991 &block_height,
992 None,
993 &tapes,
994 db,
995 )?))
996}
997
998fn block_by_hash(db: &BlockchainDatabase, block_hash: BlockHash) -> ResponseResult {
1000 let (tx_ro, tapes) = db.read_transactions()?;
1001
1002 Ok(BlockchainResponse::Block(get_block_by_hash(
1003 db,
1004 &block_hash,
1005 &tx_ro,
1006 &tapes,
1007 )?))
1008}
1009
1010fn total_tx_count(db: &BlockchainDatabase) -> BlockchainResponse {
1012 let tapes = db.linear_tapes.reader();
1013 let count = u64_to_usize(
1014 tapes
1015 .fixed_sized_tape_len(&db.tx_infos)
1016 .expect("tx_infos tape exists"),
1017 );
1018
1019 BlockchainResponse::TotalTxCount(count)
1020}
1021
1022fn dir_size(path: &std::path::Path) -> u64 {
1024 let Ok(entries) = std::fs::read_dir(path) else {
1025 return 0;
1026 };
1027 entries
1028 .filter_map(Result::ok)
1029 .map(|e| match e.file_type() {
1030 Ok(ft) if ft.is_dir() => dir_size(&e.path()),
1031 Ok(ft) if ft.is_file() => e.metadata().map_or(0, |m| m.len()),
1032 _ => 0,
1033 })
1034 .sum()
1035}
1036
1037fn database_size(db: &BlockchainDatabase) -> BlockchainResponse {
1039 let blob_size = dir_size(&db.config.blob_dir);
1041 let index_size = if db.config.index_dir == db.config.blob_dir {
1042 0
1043 } else {
1044 dir_size(&db.config.index_dir)
1045 };
1046 let database_size = blob_size + index_size;
1047
1048 let free_space = u64::MAX;
1050
1051 BlockchainResponse::DatabaseSize {
1052 database_size,
1053 free_space,
1054 }
1055}
1056
1057fn output_histogram(db: &BlockchainDatabase, input: &OutputHistogramInput) -> ResponseResult {
1059 let (tx_ro, tapes) = db.read_transactions()?;
1060
1061 let num_rct_outputs = tapes
1062 .fixed_sized_tape_len(&db.rct_outputs)
1063 .expect("rct_outputs tape exists");
1064
1065 let amounts_and_counts: BTreeMap<Amount, u64> = if input.amounts.is_empty() {
1066 let mut result = BTreeMap::new();
1067
1068 if num_rct_outputs > 0 {
1070 result.insert(0_u64, num_rct_outputs);
1071 }
1072
1073 let mut next = tx_ro.first_key_value(&db.pre_rct_outputs);
1077 while let Some(guard) = next {
1078 let amount = Amount::from_be_bytes(guard.key()?[..8].try_into().unwrap());
1079
1080 result.insert(amount, get_num_outputs_with_amount(db, &tx_ro, amount)?);
1081
1082 next = match amount.checked_add(1) {
1084 Some(next_amount) => tx_ro
1085 .range(&db.pre_rct_outputs, next_amount.to_be_bytes()..)
1086 .next(),
1087 None => None,
1088 };
1089 }
1090
1091 result
1092 } else {
1093 input
1095 .amounts
1096 .iter()
1097 .map(|&amount| {
1098 let count = if amount == 0 {
1099 num_rct_outputs
1100 } else {
1101 get_num_outputs_with_amount(db, &tx_ro, amount)?
1102 };
1103 Ok((amount, count))
1104 })
1105 .collect::<DbResult<_>>()?
1106 };
1107
1108 let current_height = tapes
1109 .fixed_sized_tape_len(&db.block_infos)
1110 .expect("block_infos tape exists");
1111
1112 let histogram = amounts_and_counts
1113 .into_iter()
1114 .filter(|&(_, total_instances)| {
1115 (input.min_count == 0 || total_instances >= input.min_count)
1117 && (input.max_count == 0 || total_instances <= input.max_count)
1118 })
1119 .map(|(amount, total_instances)| {
1120 let (unlocked_instances, recent_instances) =
1121 if input.unlocked || input.recent_cutoff > 0 {
1122 unlocked_and_recent_instances(
1123 db,
1124 &tx_ro,
1125 &tapes,
1126 amount,
1127 total_instances,
1128 current_height,
1129 input.recent_cutoff,
1130 )?
1131 } else {
1132 (0, 0)
1133 };
1134
1135 Ok(OutputHistogramEntry {
1136 amount,
1137 total_instances,
1138 unlocked_instances,
1139 recent_instances,
1140 })
1141 })
1142 .collect::<DbResult<_>>()?;
1143
1144 Ok(BlockchainResponse::OutputHistogram(histogram))
1145}
1146
1147fn coinbase_tx_sum(db: &BlockchainDatabase, height: usize, count: u64) -> ResponseResult {
1149 let tapes = db.linear_tapes.reader();
1150
1151 let start_cumulative = if height == 0 {
1152 0_u64
1153 } else {
1154 tapes
1155 .read_entry(&db.block_infos, usize_to_u64(height - 1))?
1156 .ok_or(BlockchainError::NotFound)?
1157 .cumulative_generated_coins
1158 };
1159
1160 let (emission_amount, fee_amount, _) = (height..)
1161 .zip(tapes.iter_from(&db.block_infos, usize_to_u64(height))?)
1162 .take(u64_to_usize(count))
1163 .try_fold(
1164 (0_u128, 0_u128, start_cumulative),
1165 |(emission_amount, fee_amount, prev_cumulative), (h, block_info)| {
1166 let block_info = block_info?;
1167
1168 let base_reward = block_info
1170 .cumulative_generated_coins
1171 .saturating_sub(prev_cumulative);
1172
1173 let block = get_block(&h, Some(&block_info), &tapes, db)?;
1175 let coinbase_output: u64 = block
1176 .miner_transaction()
1177 .prefix()
1178 .outputs
1179 .iter()
1180 .map(|o| o.amount.unwrap_or(0))
1181 .sum();
1182
1183 DbResult::Ok((
1184 emission_amount + u128::from(base_reward),
1185 fee_amount + u128::from(coinbase_output.saturating_sub(base_reward)),
1186 block_info.cumulative_generated_coins,
1187 ))
1188 },
1189 )?;
1190
1191 let (emission_amount, emission_amount_top64) = split_u128_into_low_high_bits(emission_amount);
1192 let (fee_amount, fee_amount_top64) = split_u128_into_low_high_bits(fee_amount);
1193
1194 Ok(BlockchainResponse::CoinbaseTxSum(CoinbaseTxSum {
1195 emission_amount,
1196 emission_amount_top64,
1197 fee_amount,
1198 fee_amount_top64,
1199 }))
1200}
1201
1202fn alt_chains(db: &BlockchainDatabase) -> ResponseResult {
1204 let (tx_ro, tapes) = db.read_transactions()?;
1205 let alt_chain_infos = db.alt_chain_infos.load();
1206 let alt_block_infos = db.alt_block_infos.load();
1207
1208 let mut chains = Vec::new();
1209
1210 for guard in alt_chain_infos.iter() {
1211 let (chain_id_bytes, chain_info_bytes) = guard.into_inner()?;
1212
1213 let chain_id = RawChainId(u64::from_le_bytes(
1214 chain_id_bytes.as_ref().try_into().unwrap(),
1215 ));
1216 let chain_info: AltChainInfo = bytemuck::pod_read_unaligned(chain_info_bytes.as_ref());
1217
1218 let tip = chain_info.chain_height - 1;
1219 let history =
1220 get_alt_chain_history_ranges(db, 0..chain_info.chain_height, chain_id.into(), &tx_ro)?;
1221
1222 let tip_height = AltBlockHeight {
1223 chain_id,
1224 height: tip,
1225 };
1226 let tip_info_bytes = tx_ro
1227 .get(&**alt_block_infos, bytemuck::bytes_of(&tip_height))?
1228 .ok_or(BlockchainError::NotFound)?;
1229 let tip_info: CompactAltBlockInfo = bytemuck::pod_read_unaligned(tip_info_bytes.as_ref());
1230
1231 let main_fork_height = history
1234 .last()
1235 .map(|(_, r)| r.end.saturating_sub(1))
1236 .ok_or(BlockchainError::NotFound)?;
1237
1238 let mut block_hashes = Vec::new();
1240 for (segment_chain, height_range) in &history {
1241 let Chain::Alt(segment_chain_id) = segment_chain else {
1242 break;
1243 };
1244 let raw_id = RawChainId::from(*segment_chain_id);
1245 for height in height_range.clone().rev() {
1246 let alt_h = AltBlockHeight {
1247 chain_id: raw_id,
1248 height,
1249 };
1250 let info_bytes = tx_ro
1251 .get(&**alt_block_infos, bytemuck::bytes_of(&alt_h))?
1252 .ok_or(BlockchainError::NotFound)?;
1253 let info: CompactAltBlockInfo = bytemuck::pod_read_unaligned(info_bytes.as_ref());
1254 block_hashes.push(info.block_hash);
1255 }
1256 }
1257
1258 let main_chain_parent_block = tapes
1260 .read_entry(&db.block_infos, usize_to_u64(main_fork_height))?
1261 .map(|info| info.block_hash)
1262 .ok_or(BlockchainError::NotFound)?;
1263
1264 let length = usize_to_u64(block_hashes.len());
1265
1266 chains.push(ChainInfo {
1267 block_hash: tip_info.block_hash,
1268 block_hashes,
1269 difficulty: tip_info.cumulative_difficulty_low,
1270 difficulty_top64: tip_info.cumulative_difficulty_high,
1271 height: usize_to_u64(tip),
1272 length,
1273 main_chain_parent_block,
1274 });
1275 }
1276
1277 Ok(BlockchainResponse::AltChains(chains))
1278}
1279
1280fn alt_chain_count(db: &BlockchainDatabase) -> ResponseResult {
1282 let count = db.alt_chain_infos.load().len()?;
1283 Ok(BlockchainResponse::AltChainCount(count))
1284}
1285
1286fn transactions(db: &BlockchainDatabase, tx_hashes: Vec<[u8; 32]>) -> ResponseResult {
1288 let (tx_ro, tapes) = db.read_transactions()?;
1289 let chain_height = crate::ops::blockchain::chain_height(db, &tapes)?;
1290
1291 let mut txs = Vec::with_capacity(tx_hashes.len());
1292 let mut missed_txs = Vec::new();
1293 let mut block_timestamps = HashMap::new();
1294
1295 for tx_hash in tx_hashes {
1296 let Some(tx_id) = tx_ro.get(&db.tx_ids, tx_hash)? else {
1297 missed_txs.push(tx_hash);
1298 continue;
1299 };
1300
1301 let tx_id = u64::from_le_bytes(tx_id.as_ref().try_into().unwrap());
1302 let tx_info = tapes
1303 .read_entry(&db.tx_infos, tx_id)?
1304 .ok_or(BlockchainError::NotFound)?;
1305 let block_info = tapes
1306 .read_entry(&db.block_infos, usize_to_u64(tx_info.height))?
1307 .ok_or(BlockchainError::NotFound)?;
1308 let is_miner_tx = tx_id == block_info.mining_tx_index;
1309
1310 let (pruned_blob, prunable_blob, prunable_hash) =
1311 get_split_tx_blobs(&tx_info, is_miner_tx, &tapes, db)?;
1312
1313 let block_timestamp = if let Some(timestamp) = block_timestamps.get(&tx_info.height) {
1314 *timestamp
1315 } else {
1316 let timestamp =
1317 get_block_extended_header_from_height(tx_info.height, &tapes, db)?.timestamp;
1318 block_timestamps.insert(tx_info.height, timestamp);
1319 timestamp
1320 };
1321
1322 let output_indices = if tx_info.is_v1_tx() {
1323 let output_indices = tx_ro
1324 .get(&db.v1_tx_outputs, tx_id.to_le_bytes())?
1325 .ok_or(BlockchainError::NotFound)?;
1326
1327 output_indices
1328 .chunks(8)
1329 .map(|chunk| u64::from_le_bytes(chunk.try_into().unwrap()))
1330 .collect()
1331 } else {
1332 (0..tx_info.numb_rct_outputs)
1333 .map(|i| usize_to_u64(i) + tx_info.rct_output_start_idx)
1334 .collect()
1335 };
1336
1337 txs.push(TxInBlockchain {
1338 block_height: usize_to_u64(tx_info.height),
1339 block_timestamp,
1340 confirmations: usize_to_u64(chain_height - tx_info.height),
1341 output_indices,
1342 tx_hash,
1343 pruned_blob,
1344 prunable_blob,
1345 prunable_hash,
1346 });
1347 }
1348
1349 Ok(BlockchainResponse::Transactions { txs, missed_txs })
1350}
1351
1352fn total_rct_outputs(db: &BlockchainDatabase) -> BlockchainResponse {
1354 let tapes = db.linear_tapes.reader();
1355
1356 let len = tapes
1357 .fixed_sized_tape_len(&db.rct_outputs)
1358 .expect("Required tape not found");
1359
1360 BlockchainResponse::TotalRctOutputs(len)
1361}
1362
1363fn tx_output_indexes(db: &BlockchainDatabase, tx_hash: &[u8; 32]) -> ResponseResult {
1365 let (tx_ro, tapes) = db.read_transactions()?;
1366
1367 let tx_id = tx_ro
1368 .get(&db.tx_ids, tx_hash)?
1369 .ok_or(BlockchainError::NotFound)?;
1370
1371 let tx_id = u64::from_le_bytes(tx_id.as_ref().try_into().unwrap());
1372
1373 let tx_info = tapes
1374 .read_entry(&db.tx_infos, tx_id)?
1375 .ok_or(BlockchainError::NotFound)?;
1376
1377 let o_indexes = if tx_info.is_v1_tx() {
1378 let bytes = tx_ro
1379 .get(&db.v1_tx_outputs, tx_id.to_le_bytes())?
1380 .ok_or(BlockchainError::NotFound)?;
1381
1382 bytemuck::pod_collect_to_vec(bytes.as_ref())
1383 } else {
1384 (0..tx_info.numb_rct_outputs)
1385 .map(|i| usize_to_u64(i) + tx_info.rct_output_start_idx)
1386 .collect()
1387 };
1388
1389 Ok(BlockchainResponse::TxOutputIndexes(o_indexes))
1390}
1391
1392fn pre_rct_output_distribution(
1394 db: &BlockchainDatabase,
1395 input: &PreRctOutputDistributionInput,
1396) -> ResponseResult {
1397 let (tx_ro, tapes) = db.read_transactions()?;
1398 let chain_height = u64_to_usize(
1399 tapes
1400 .fixed_sized_tape_len(&db.block_infos)
1401 .expect("block_infos tape exists"),
1402 );
1403
1404 if input.to_height.is_some_and(|h| h.get() < input.from_height) {
1405 return Err(BlockchainError::NotFound);
1406 }
1407
1408 let to_height = input.to_height.map_or(chain_height - 1, |h| {
1409 let h = h.get();
1410 u64_to_usize(h)
1411 });
1412
1413 if to_height >= chain_height {
1414 return Err(BlockchainError::NotFound);
1415 }
1416
1417 let mut result = Vec::with_capacity(input.amounts.len());
1418
1419 for &amount in &input.amounts {
1420 let amount = amount.get();
1421
1422 let start_height = u64_to_usize(input.from_height);
1423
1424 if start_height > to_height {
1425 return Err(BlockchainError::NotFound);
1426 }
1427
1428 let mut per_block: Vec<u64> = vec![0; to_height - start_height + 1];
1429 let mut below_start: u64 = 0;
1430
1431 for guard in tx_ro.prefix(&db.pre_rct_outputs, amount.to_be_bytes()) {
1433 let output: Output = bytemuck::pod_read_unaligned(guard.value()?.as_ref());
1434 let h = output.height;
1435 if h < start_height {
1437 below_start += 1;
1438 } else if h <= to_height {
1439 per_block[h - start_height] += 1;
1440 } else {
1441 break;
1444 }
1445 }
1446
1447 per_block[0] += below_start;
1450
1451 let distribution = if input.cumulative {
1452 let mut cumulative = per_block;
1453 for i in 1..cumulative.len() {
1454 cumulative[i] += cumulative[i - 1];
1455 }
1456 cumulative
1457 } else {
1458 per_block
1459 };
1460
1461 result.push(OutputDistributionData {
1462 amount,
1463 distribution,
1464 start_height: usize_to_u64(start_height),
1465 base: 0,
1466 });
1467 }
1468
1469 Ok(BlockchainResponse::PreRctOutputDistribution(result))
1470}