use std::collections::HashSet;
use anyhow::Result;
use itertools::Itertools;
use neptune_consensus::block::Block;
use neptune_database::NeptuneLevelDb;
use neptune_database::WriteBatchAsync;
use neptune_database::create_db_if_missing;
use neptune_database::storage::storage_schema::traits::*;
use neptune_mutator_set::addition_record::AdditionRecord;
use neptune_mutator_set::removal_record::absolute_index_set::AbsoluteIndexSet;
use neptune_primitives::announcement_flag::AnnouncementFlag;
use neptune_primitives::block_height::BlockHeight;
use neptune_primitives::data_directory::DataDirectory;
use serde::Deserialize;
use serde::Serialize;
use tasm_lib::prelude::Digest;
use tasm_lib::prelude::Tip5;
use tracing::warn;
pub const MAX_NUM_BLOCKS_IN_LOOKUP_LIST: usize = 10_000;
#[derive(Debug)]
pub struct RustyUtxoIndex {
pub(crate) db: NeptuneLevelDb<UtxoIndexKey, UtxoIndexValue>,
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
pub(crate) enum UtxoIndexKey {
SyncLabel,
AnnouncementsByBlock(Digest),
IndexSetDigestsByBlock(Digest),
BlocksByAnnouncementFlag(AnnouncementFlag),
BlocksByAdditionRecord(AdditionRecord),
BlockByIndexSetDigest(Digest),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) enum UtxoIndexValue {
SyncLabel(Digest),
AnnouncementsByBlock(Vec<AnnouncementFlag>),
IndexSetDigestsByBlock(Vec<Digest>),
BlocksByAnnouncementFlag(Vec<BlockHeight>),
BlocksByAdditionRecord(Vec<BlockHeight>),
BlockByIndexSetDigest(BlockHeight),
}
impl UtxoIndexValue {
fn expect_sync_label(self) -> Digest {
match self {
UtxoIndexValue::SyncLabel(digest) => digest,
_ => panic!("Expected SyncLabel found {:?}", self),
}
}
fn expect_announcements_by_block(self) -> Vec<AnnouncementFlag> {
match self {
UtxoIndexValue::AnnouncementsByBlock(flags) => flags,
_ => panic!("Expected AnnouncementsByBlock found {:?}", self),
}
}
fn expect_index_set_digests_by_block(self) -> Vec<Digest> {
match self {
UtxoIndexValue::IndexSetDigestsByBlock(index_set_digests) => index_set_digests,
_ => panic!("Expected IndexSetDigestsByBlock found {:?}", self),
}
}
pub(crate) fn expect_blocks_by_announcements(self) -> Vec<BlockHeight> {
match self {
UtxoIndexValue::BlocksByAnnouncementFlag(block_heights) => block_heights,
_ => panic!("Expected BlocksByAnnouncementFlag found {:?}", self),
}
}
fn expect_blocks_by_addition_records(self) -> Vec<BlockHeight> {
match self {
UtxoIndexValue::BlocksByAdditionRecord(block_heights) => block_heights,
_ => panic!("Expected BlocksByAdditionRecord found {:?}", self),
}
}
fn expect_block_by_index_set_digest(self) -> BlockHeight {
match self {
UtxoIndexValue::BlockByIndexSetDigest(height) => height,
_ => panic!("Expected BlockByIndexSetDigest found {:?}", self),
}
}
}
impl RustyUtxoIndex {
pub(crate) async fn is_empty(&self) -> bool {
self.sync_label().await == Default::default()
}
pub(crate) async fn block_was_indexed(&self, block_hash: Digest) -> bool {
self.db
.get(UtxoIndexKey::AnnouncementsByBlock(block_hash))
.await
.is_some()
}
pub(crate) async fn initialize(data_dir: &DataDirectory) -> Result<Self> {
let utxo_index_db_dir_path = data_dir.utxo_index_dir_path();
DataDirectory::create_dir_if_not_exists(&utxo_index_db_dir_path).await?;
let utxo_index = NeptuneLevelDb::<UtxoIndexKey, UtxoIndexValue>::new(
&utxo_index_db_dir_path,
&create_db_if_missing(),
)
.await?;
let mut utxo_index = RustyUtxoIndex { db: utxo_index };
if utxo_index.db.get(UtxoIndexKey::SyncLabel).await.is_none() {
utxo_index
.db
.put(
UtxoIndexKey::SyncLabel,
UtxoIndexValue::SyncLabel(Digest::default()),
)
.await;
}
Ok(utxo_index)
}
pub async fn announcement_flags(&self, block_hash: Digest) -> Option<Vec<AnnouncementFlag>> {
let key = UtxoIndexKey::AnnouncementsByBlock(block_hash);
self.db
.get(key)
.await
.map(|x| x.expect_announcements_by_block())
}
pub(crate) async fn index_set_digests(&self, block_hash: Digest) -> Option<Vec<Digest>> {
let key = UtxoIndexKey::IndexSetDigestsByBlock(block_hash);
self.db
.get(key)
.await
.map(|x| x.expect_index_set_digests_by_block())
}
pub async fn blocks_by_announcement_flags(
&self,
announcement_flags: &HashSet<AnnouncementFlag>,
) -> HashSet<BlockHeight> {
let mut block_heights = HashSet::new();
for flag in announcement_flags {
let key = UtxoIndexKey::BlocksByAnnouncementFlag(*flag);
let matching_blocks = self
.db
.get(key)
.await
.map(|x| x.expect_blocks_by_announcements())
.unwrap_or_default();
block_heights.extend(matching_blocks);
}
block_heights
}
pub(crate) async fn blocks_by_addition_record(
&self,
addition_record: AdditionRecord,
) -> HashSet<BlockHeight> {
let key = UtxoIndexKey::BlocksByAdditionRecord(addition_record);
let blocks = self
.db
.get(key)
.await
.map(|x| x.expect_blocks_by_addition_records())
.unwrap_or_default();
blocks.into_iter().collect()
}
pub async fn block_by_index_set(&self, index_set: &AbsoluteIndexSet) -> Option<BlockHeight> {
let index_set_digest = Tip5::hash(index_set);
let key = UtxoIndexKey::BlockByIndexSetDigest(index_set_digest);
self.db
.get(key)
.await
.map(|x| x.expect_block_by_index_set_digest())
}
pub(crate) async fn index_block(&mut self, block: &Block) {
let hash = block.hash();
let height = block.header().height;
let tx_kernel = &block.body().transaction_kernel;
let announcement_flags: HashSet<AnnouncementFlag> = tx_kernel
.announcements
.iter()
.filter_map(|ann| AnnouncementFlag::try_from(ann).ok())
.collect();
let mut announcement_flags = announcement_flags.iter().copied().collect_vec();
announcement_flags.sort_unstable();
let mut batch_writes = WriteBatchAsync::new();
batch_writes.op_write(
UtxoIndexKey::AnnouncementsByBlock(hash),
UtxoIndexValue::AnnouncementsByBlock(announcement_flags.clone()),
);
for announcement_flag in announcement_flags {
let announcement_flag = UtxoIndexKey::BlocksByAnnouncementFlag(announcement_flag);
let mut block_heights = self
.db
.get(announcement_flag)
.await
.map(|x| x.expect_blocks_by_announcements())
.unwrap_or_default();
if block_heights.contains(&height) {
continue;
}
if block_heights.len() >= MAX_NUM_BLOCKS_IN_LOOKUP_LIST {
warn!(
"List of block heights matching announcement flag exceeds max.\
Not adding new block to list."
);
continue;
}
block_heights.push(height);
batch_writes.op_write(
announcement_flag,
UtxoIndexValue::BlocksByAnnouncementFlag(block_heights),
);
}
for addition_record in block
.all_addition_records()
.expect("Block must have mutator set update")
{
let addition_record = UtxoIndexKey::BlocksByAdditionRecord(addition_record);
let mut block_heights = self
.db
.get(addition_record)
.await
.map(|x| x.expect_blocks_by_addition_records())
.unwrap_or_default();
if block_heights.contains(&height) {
continue;
}
if block_heights.len() >= MAX_NUM_BLOCKS_IN_LOOKUP_LIST {
warn!(
"List of block heights matching addition record exceeds max.\
Not adding new block to list."
);
continue;
}
block_heights.push(height);
batch_writes.op_write(
addition_record,
UtxoIndexValue::BlocksByAdditionRecord(block_heights),
);
}
let index_set_digests = tx_kernel
.inputs
.iter()
.map(|rr| Tip5::hash(&rr.absolute_indices))
.collect_vec();
for index_set_digest in &index_set_digests {
batch_writes.op_write(
UtxoIndexKey::BlockByIndexSetDigest(*index_set_digest),
UtxoIndexValue::BlockByIndexSetDigest(height),
);
}
batch_writes.op_write(
UtxoIndexKey::IndexSetDigestsByBlock(hash),
UtxoIndexValue::IndexSetDigestsByBlock(index_set_digests),
);
batch_writes.op_write(UtxoIndexKey::SyncLabel, UtxoIndexValue::SyncLabel(hash));
self.db.batch_write(batch_writes).await;
}
pub(crate) async fn sync_label(&self) -> Digest {
self.db
.get(UtxoIndexKey::SyncLabel)
.await
.expect("UTXO index must have a SyncLabel set")
.expect_sync_label()
}
}
impl StorageWriter for RustyUtxoIndex {
async fn persist(&mut self) {
self.db.flush().await;
}
async fn drop_unpersisted(&mut self) {
unimplemented!("utxo index does not need it")
}
}
#[cfg(any(test, feature = "test-helpers"))]
impl RustyUtxoIndex {
pub async fn block_heights_by_announcements(
&self,
announcements: &[neptune_consensus::transaction::announcement::Announcement],
) -> std::collections::HashSet<neptune_primitives::block_height::BlockHeight> {
let announcement_flags: std::collections::HashSet<AnnouncementFlag> = announcements
.iter()
.filter_map(|ann| AnnouncementFlag::try_from(ann).ok())
.collect();
self.blocks_by_announcement_flags(&announcement_flags).await
}
}