change(scan): Store one transaction ID per database row, to make queries easier (#8062)
* Upgrade the scanner database major version to 1 * Update format docs * Change the high-level scanner db format * Change the scanner serialization formats * Fix value format and tests * Fix incorrect types * Update documentation --------- Co-authored-by: Alfredo Garcia <oxarbitrage@gmail.com>
This commit is contained in:
parent
358e52bc64
commit
36f226362d
|
@ -1,6 +1,10 @@
|
||||||
//! The scanner task and scanning APIs.
|
//! The scanner task and scanning APIs.
|
||||||
|
|
||||||
use std::{collections::HashMap, sync::Arc, time::Duration};
|
use std::{
|
||||||
|
collections::{BTreeMap, HashMap},
|
||||||
|
sync::Arc,
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
use color_eyre::{eyre::eyre, Report};
|
use color_eyre::{eyre::eyre, Report};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
@ -29,7 +33,7 @@ use zebra_chain::{
|
||||||
serialization::ZcashSerialize,
|
serialization::ZcashSerialize,
|
||||||
transaction::Transaction,
|
transaction::Transaction,
|
||||||
};
|
};
|
||||||
use zebra_state::{ChainTipChange, SaplingScannedResult};
|
use zebra_state::{ChainTipChange, SaplingScannedResult, TransactionIndex};
|
||||||
|
|
||||||
use crate::storage::{SaplingScanningKey, Storage};
|
use crate::storage::{SaplingScanningKey, Storage};
|
||||||
|
|
||||||
|
@ -207,8 +211,8 @@ pub async fn scan_height_and_store_results(
|
||||||
let dfvk_res = scanned_block_to_db_result(dfvk_res);
|
let dfvk_res = scanned_block_to_db_result(dfvk_res);
|
||||||
let ivk_res = scanned_block_to_db_result(ivk_res);
|
let ivk_res = scanned_block_to_db_result(ivk_res);
|
||||||
|
|
||||||
storage.add_sapling_result(sapling_key.clone(), height, dfvk_res);
|
storage.add_sapling_results(sapling_key.clone(), height, dfvk_res);
|
||||||
storage.add_sapling_result(sapling_key, height, ivk_res);
|
storage.add_sapling_results(sapling_key, height, ivk_res);
|
||||||
|
|
||||||
Ok::<_, Report>(())
|
Ok::<_, Report>(())
|
||||||
})
|
})
|
||||||
|
@ -385,10 +389,17 @@ fn transaction_to_compact((index, tx): (usize, Arc<Transaction>)) -> CompactTx {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert a scanned block to a list of scanner database results.
|
/// Convert a scanned block to a list of scanner database results.
|
||||||
fn scanned_block_to_db_result<Nf>(scanned_block: ScannedBlock<Nf>) -> Vec<SaplingScannedResult> {
|
fn scanned_block_to_db_result<Nf>(
|
||||||
|
scanned_block: ScannedBlock<Nf>,
|
||||||
|
) -> BTreeMap<TransactionIndex, SaplingScannedResult> {
|
||||||
scanned_block
|
scanned_block
|
||||||
.transactions()
|
.transactions()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|tx| SaplingScannedResult::from(tx.txid.as_ref()))
|
.map(|tx| {
|
||||||
|
(
|
||||||
|
TransactionIndex::from_usize(tx.index),
|
||||||
|
SaplingScannedResult::from(tx.txid.as_ref()),
|
||||||
|
)
|
||||||
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,9 @@ use zebra_chain::{
|
||||||
block::Height,
|
block::Height,
|
||||||
parameters::{Network, NetworkUpgrade},
|
parameters::{Network, NetworkUpgrade},
|
||||||
};
|
};
|
||||||
use zebra_state::{SaplingScannedDatabaseEntry, SaplingScannedDatabaseIndex};
|
use zebra_state::{
|
||||||
|
SaplingScannedDatabaseEntry, SaplingScannedDatabaseIndex, TransactionIndex, TransactionLocation,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
|
|
||||||
|
@ -56,8 +58,8 @@ impl Storage {
|
||||||
pub fn new(config: &Config, network: Network) -> Self {
|
pub fn new(config: &Config, network: Network) -> Self {
|
||||||
let mut storage = Self::new_db(config, network);
|
let mut storage = Self::new_db(config, network);
|
||||||
|
|
||||||
for (key, birthday) in config.sapling_keys_to_scan.iter() {
|
for (sapling_key, birthday) in config.sapling_keys_to_scan.iter() {
|
||||||
storage.add_sapling_key(key.clone(), Some(zebra_chain::block::Height(*birthday)));
|
storage.add_sapling_key(sapling_key, Some(zebra_chain::block::Height(*birthday)));
|
||||||
}
|
}
|
||||||
|
|
||||||
storage
|
storage
|
||||||
|
@ -69,12 +71,12 @@ impl Storage {
|
||||||
///
|
///
|
||||||
/// This method can block while writing database files, so it must be inside spawn_blocking()
|
/// This method can block while writing database files, so it must be inside spawn_blocking()
|
||||||
/// in async code.
|
/// in async code.
|
||||||
pub fn add_sapling_key(&mut self, key: SaplingScanningKey, birthday: Option<Height>) {
|
pub fn add_sapling_key(&mut self, sapling_key: &SaplingScanningKey, birthday: Option<Height>) {
|
||||||
// It's ok to write some keys and not others during shutdown, so each key can get its own
|
// It's ok to write some keys and not others during shutdown, so each key can get its own
|
||||||
// batch. (They will be re-written on startup anyway.)
|
// batch. (They will be re-written on startup anyway.)
|
||||||
let mut batch = ScannerWriteBatch::default();
|
let mut batch = ScannerWriteBatch::default();
|
||||||
|
|
||||||
batch.insert_sapling_key(self, key, birthday);
|
batch.insert_sapling_key(self, sapling_key, birthday);
|
||||||
|
|
||||||
self.write_batch(batch);
|
self.write_batch(batch);
|
||||||
}
|
}
|
||||||
|
@ -91,33 +93,35 @@ impl Storage {
|
||||||
self.sapling_keys_and_birthday_heights()
|
self.sapling_keys_and_birthday_heights()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a sapling result to the storage.
|
/// Add the sapling results for `height` to the storage.
|
||||||
///
|
///
|
||||||
/// # Performance / Hangs
|
/// # Performance / Hangs
|
||||||
///
|
///
|
||||||
/// This method can block while writing database files, so it must be inside spawn_blocking()
|
/// This method can block while writing database files, so it must be inside spawn_blocking()
|
||||||
/// in async code.
|
/// in async code.
|
||||||
pub fn add_sapling_result(
|
pub fn add_sapling_results(
|
||||||
&mut self,
|
&mut self,
|
||||||
sapling_key: SaplingScanningKey,
|
sapling_key: SaplingScanningKey,
|
||||||
height: Height,
|
height: Height,
|
||||||
sapling_result: Vec<SaplingScannedResult>,
|
sapling_results: BTreeMap<TransactionIndex, SaplingScannedResult>,
|
||||||
) {
|
) {
|
||||||
// It's ok to write some results and not others during shutdown, so each result can get its
|
// We skip heights that have one or more results, so the results for each height must be
|
||||||
// own batch. (They will be re-scanned on startup anyway.)
|
// in a single batch.
|
||||||
let mut batch = ScannerWriteBatch::default();
|
let mut batch = ScannerWriteBatch::default();
|
||||||
|
|
||||||
let index = SaplingScannedDatabaseIndex {
|
for (index, sapling_result) in sapling_results {
|
||||||
sapling_key,
|
let index = SaplingScannedDatabaseIndex {
|
||||||
height,
|
sapling_key: sapling_key.clone(),
|
||||||
};
|
tx_loc: TransactionLocation::from_parts(height, index),
|
||||||
|
};
|
||||||
|
|
||||||
let entry = SaplingScannedDatabaseEntry {
|
let entry = SaplingScannedDatabaseEntry {
|
||||||
index,
|
index,
|
||||||
value: sapling_result,
|
value: Some(sapling_result),
|
||||||
};
|
};
|
||||||
|
|
||||||
batch.insert_sapling_result(self, entry);
|
batch.insert_sapling_result(self, entry);
|
||||||
|
}
|
||||||
|
|
||||||
self.write_batch(batch);
|
self.write_batch(batch);
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,10 @@ pub const SCANNER_COLUMN_FAMILIES_IN_CODE: &[&str] = &[
|
||||||
// TODO: add Orchard support
|
// TODO: add Orchard support
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// The major version number of the scanner database. This must be updated whenever the database
|
||||||
|
/// format changes.
|
||||||
|
const SCANNER_DATABASE_FORMAT_MAJOR_VERSION: u64 = 1;
|
||||||
|
|
||||||
impl Storage {
|
impl Storage {
|
||||||
// Creation
|
// Creation
|
||||||
|
|
||||||
|
@ -96,8 +100,8 @@ impl Storage {
|
||||||
|
|
||||||
/// The database format version in the running scanner code.
|
/// The database format version in the running scanner code.
|
||||||
pub fn database_format_version_in_code() -> Version {
|
pub fn database_format_version_in_code() -> Version {
|
||||||
// TODO: implement scanner database versioning
|
// TODO: implement in-place scanner database format upgrades
|
||||||
Version::new(0, 0, 0)
|
Version::new(SCANNER_DATABASE_FORMAT_MAJOR_VERSION, 0, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check for panics in code running in spawned threads.
|
/// Check for panics in code running in spawned threads.
|
||||||
|
|
|
@ -4,24 +4,34 @@
|
||||||
//!
|
//!
|
||||||
//! | name | key | value |
|
//! | name | key | value |
|
||||||
//! |------------------|-------------------------------|--------------------------|
|
//! |------------------|-------------------------------|--------------------------|
|
||||||
//! | `sapling_tx_ids` | `SaplingScannedDatabaseIndex` | `Vec<transaction::Hash>` |
|
//! | `sapling_tx_ids` | `SaplingScannedDatabaseIndex` | `Option<SaplingScannedResult>` |
|
||||||
//!
|
//!
|
||||||
//! And types:
|
//! And types:
|
||||||
//! SaplingScannedDatabaseIndex = `SaplingScanningKey` | `Height`
|
//! `SaplingScannedResult`: same as `transaction::Hash`, but with bytes in display order.
|
||||||
|
//! `None` is stored as a zero-length array of bytes.
|
||||||
|
//!
|
||||||
|
//! `SaplingScannedDatabaseIndex` = `SaplingScanningKey` | `TransactionLocation`
|
||||||
|
//! `TransactionLocation` = `Height` | `TransactionIndex`
|
||||||
//!
|
//!
|
||||||
//! This format allows us to efficiently find all the results for each key, and the latest height
|
//! This format allows us to efficiently find all the results for each key, and the latest height
|
||||||
//! for each key.
|
//! for each key.
|
||||||
//!
|
//!
|
||||||
//! If there are no results for a height, we store an empty list of results. This allows is to scan
|
//! If there are no results for a height, we store `None` as the result for the coinbase
|
||||||
//! each key from the next height after we restart. We also use this mechanism to store key
|
//! transaction. This allows is to scan each key from the next height after we restart. We also use
|
||||||
//! birthday heights, by storing the height before the birthday as the "last scanned" block.
|
//! this mechanism to store key birthday heights, by storing the height before the birthday as the
|
||||||
|
//! "last scanned" block.
|
||||||
|
|
||||||
use std::collections::{BTreeMap, HashMap};
|
use std::{
|
||||||
|
collections::{BTreeMap, HashMap},
|
||||||
|
ops::RangeBounds,
|
||||||
|
};
|
||||||
|
|
||||||
|
use itertools::Itertools;
|
||||||
|
|
||||||
use zebra_chain::block::Height;
|
use zebra_chain::block::Height;
|
||||||
use zebra_state::{
|
use zebra_state::{
|
||||||
AsColumnFamilyRef, ReadDisk, SaplingScannedDatabaseEntry, SaplingScannedDatabaseIndex,
|
AsColumnFamilyRef, ReadDisk, SaplingScannedDatabaseEntry, SaplingScannedDatabaseIndex,
|
||||||
SaplingScannedResult, SaplingScanningKey, WriteDisk,
|
SaplingScannedResult, SaplingScanningKey, TransactionIndex, WriteDisk,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::storage::Storage;
|
use crate::storage::Storage;
|
||||||
|
@ -36,16 +46,29 @@ pub const SAPLING_TX_IDS: &str = "sapling_tx_ids";
|
||||||
impl Storage {
|
impl Storage {
|
||||||
// Reading Sapling database entries
|
// Reading Sapling database entries
|
||||||
|
|
||||||
/// Returns the results for a specific key and block height.
|
/// Returns the result for a specific database index (key, block height, transaction index).
|
||||||
//
|
//
|
||||||
// TODO: add tests for this method
|
// TODO: add tests for this method
|
||||||
pub fn sapling_result_for_key_and_block(
|
pub fn sapling_result_for_index(
|
||||||
&self,
|
&self,
|
||||||
index: &SaplingScannedDatabaseIndex,
|
index: &SaplingScannedDatabaseIndex,
|
||||||
) -> Vec<SaplingScannedResult> {
|
) -> Option<SaplingScannedResult> {
|
||||||
self.db
|
self.db.zs_get(&self.sapling_tx_ids_cf(), &index)
|
||||||
.zs_get(&self.sapling_tx_ids_cf(), &index)
|
}
|
||||||
.unwrap_or_default()
|
|
||||||
|
/// Returns the results for a specific key and block height.
|
||||||
|
pub fn sapling_results_for_key_and_height(
|
||||||
|
&self,
|
||||||
|
sapling_key: &SaplingScanningKey,
|
||||||
|
height: Height,
|
||||||
|
) -> BTreeMap<TransactionIndex, Option<SaplingScannedResult>> {
|
||||||
|
let kh_min = SaplingScannedDatabaseIndex::min_for_key_and_height(sapling_key, height);
|
||||||
|
let kh_max = SaplingScannedDatabaseIndex::max_for_key_and_height(sapling_key, height);
|
||||||
|
|
||||||
|
self.sapling_results_in_range(kh_min..=kh_max)
|
||||||
|
.into_iter()
|
||||||
|
.map(|(result_index, txid)| (result_index.tx_loc.index, txid))
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns all the results for a specific key, indexed by height.
|
/// Returns all the results for a specific key, indexed by height.
|
||||||
|
@ -56,10 +79,19 @@ impl Storage {
|
||||||
let k_min = SaplingScannedDatabaseIndex::min_for_key(sapling_key);
|
let k_min = SaplingScannedDatabaseIndex::min_for_key(sapling_key);
|
||||||
let k_max = SaplingScannedDatabaseIndex::max_for_key(sapling_key);
|
let k_max = SaplingScannedDatabaseIndex::max_for_key(sapling_key);
|
||||||
|
|
||||||
self.db
|
// Get an iterator of individual transaction results, and turn it into a HashMap by height
|
||||||
.zs_items_in_range_ordered(&self.sapling_tx_ids_cf(), k_min..=k_max)
|
let results: HashMap<Height, Vec<Option<SaplingScannedResult>>> = self
|
||||||
|
.sapling_results_in_range(k_min..=k_max)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(index, result)| (index.height, result))
|
.map(|(index, result)| (index.tx_loc.height, result))
|
||||||
|
.into_group_map();
|
||||||
|
|
||||||
|
// But we want Vec<SaplingScannedResult>, with empty Vecs instead of [None, None, ...]
|
||||||
|
results
|
||||||
|
.into_iter()
|
||||||
|
.map(|(index, vector)| -> (Height, Vec<SaplingScannedResult>) {
|
||||||
|
(index, vector.into_iter().flatten().collect())
|
||||||
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,16 +117,16 @@ impl Storage {
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
|
|
||||||
let (index, results): (_, Vec<SaplingScannedResult>) = entry;
|
let sapling_key = entry.0.sapling_key;
|
||||||
let SaplingScannedDatabaseIndex {
|
let mut height = entry.0.tx_loc.height;
|
||||||
sapling_key,
|
let _first_result: Option<SaplingScannedResult> = entry.1;
|
||||||
mut height,
|
|
||||||
} = index;
|
|
||||||
|
|
||||||
// If there are no results, then it's a "skip up to height" marker, and the birthday
|
let height_results = self.sapling_results_for_key_and_height(&sapling_key, height);
|
||||||
// height is the next height. If there are some results, it's the actual birthday
|
|
||||||
// height.
|
// If there are no results for this block, then it's a "skip up to height" marker, and
|
||||||
if results.is_empty() {
|
// the birthday height is the next height. If there are some results, it's the actual
|
||||||
|
// birthday height.
|
||||||
|
if height_results.values().all(Option::is_none) {
|
||||||
height = height
|
height = height
|
||||||
.next()
|
.next()
|
||||||
.expect("results should only be stored for validated block heights");
|
.expect("results should only be stored for validated block heights");
|
||||||
|
@ -109,6 +141,17 @@ impl Storage {
|
||||||
keys
|
keys
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the Sapling indexes and results in the supplied range.
|
||||||
|
///
|
||||||
|
/// Convenience method for accessing raw data with the correct types.
|
||||||
|
fn sapling_results_in_range(
|
||||||
|
&self,
|
||||||
|
range: impl RangeBounds<SaplingScannedDatabaseIndex>,
|
||||||
|
) -> BTreeMap<SaplingScannedDatabaseIndex, Option<SaplingScannedResult>> {
|
||||||
|
self.db
|
||||||
|
.zs_items_in_range_ordered(&self.sapling_tx_ids_cf(), range)
|
||||||
|
}
|
||||||
|
|
||||||
// Column family convenience methods
|
// Column family convenience methods
|
||||||
|
|
||||||
/// Returns a handle to the `sapling_tx_ids` column family.
|
/// Returns a handle to the `sapling_tx_ids` column family.
|
||||||
|
@ -122,7 +165,7 @@ impl Storage {
|
||||||
|
|
||||||
/// Write `batch` to the database for this storage.
|
/// Write `batch` to the database for this storage.
|
||||||
pub(crate) fn write_batch(&self, batch: ScannerWriteBatch) {
|
pub(crate) fn write_batch(&self, batch: ScannerWriteBatch) {
|
||||||
// Just panic on errors for now
|
// Just panic on errors for now.
|
||||||
self.db
|
self.db
|
||||||
.write_batch(batch.0)
|
.write_batch(batch.0)
|
||||||
.expect("unexpected database error")
|
.expect("unexpected database error")
|
||||||
|
@ -147,12 +190,15 @@ impl ScannerWriteBatch {
|
||||||
/// Insert a sapling scanning `key`, and mark all heights before `birthday_height` so they
|
/// Insert a sapling scanning `key`, and mark all heights before `birthday_height` so they
|
||||||
/// won't be scanned.
|
/// won't be scanned.
|
||||||
///
|
///
|
||||||
/// If a result already exists for the height before the birthday, it is replaced with an empty
|
/// If a result already exists for the coinbase transaction at the height before the birthday,
|
||||||
/// result.
|
/// it is replaced with an empty result. This can happen if the user increases the birthday
|
||||||
|
/// height.
|
||||||
|
///
|
||||||
|
/// TODO: ignore incorrect changes to birthday heights
|
||||||
pub(crate) fn insert_sapling_key(
|
pub(crate) fn insert_sapling_key(
|
||||||
&mut self,
|
&mut self,
|
||||||
storage: &Storage,
|
storage: &Storage,
|
||||||
sapling_key: SaplingScanningKey,
|
sapling_key: &SaplingScanningKey,
|
||||||
birthday_height: Option<Height>,
|
birthday_height: Option<Height>,
|
||||||
) {
|
) {
|
||||||
let min_birthday_height = storage.min_sapling_birthday_height();
|
let min_birthday_height = storage.min_sapling_birthday_height();
|
||||||
|
@ -162,13 +208,10 @@ impl ScannerWriteBatch {
|
||||||
.unwrap_or(min_birthday_height)
|
.unwrap_or(min_birthday_height)
|
||||||
.max(min_birthday_height);
|
.max(min_birthday_height);
|
||||||
// And we want to skip up to the height before it.
|
// And we want to skip up to the height before it.
|
||||||
let skip_up_to_height = birthday_height.previous().unwrap_or(Height(0));
|
let skip_up_to_height = birthday_height.previous().unwrap_or(Height::MIN);
|
||||||
|
|
||||||
let index = SaplingScannedDatabaseIndex {
|
let index =
|
||||||
sapling_key,
|
SaplingScannedDatabaseIndex::min_for_key_and_height(sapling_key, skip_up_to_height);
|
||||||
height: skip_up_to_height,
|
self.zs_insert(&storage.sapling_tx_ids_cf(), index, None);
|
||||||
};
|
|
||||||
|
|
||||||
self.zs_insert(&storage.sapling_tx_ids_cf(), index, Vec::new());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,7 +46,7 @@ use zebra_chain::{
|
||||||
transparent::{CoinbaseData, Input},
|
transparent::{CoinbaseData, Input},
|
||||||
work::{difficulty::CompactDifficulty, equihash::Solution},
|
work::{difficulty::CompactDifficulty, equihash::Solution},
|
||||||
};
|
};
|
||||||
use zebra_state::SaplingScannedResult;
|
use zebra_state::{SaplingScannedResult, TransactionIndex};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::Config,
|
config::Config,
|
||||||
|
@ -189,7 +189,7 @@ fn scanning_fake_blocks_store_key_and_results() -> Result<()> {
|
||||||
let mut s = crate::storage::Storage::new(&Config::ephemeral(), Network::Mainnet);
|
let mut s = crate::storage::Storage::new(&Config::ephemeral(), Network::Mainnet);
|
||||||
|
|
||||||
// Insert the generated key to the database
|
// Insert the generated key to the database
|
||||||
s.add_sapling_key(key_to_be_stored.clone(), None);
|
s.add_sapling_key(&key_to_be_stored, None);
|
||||||
|
|
||||||
// Check key was added
|
// Check key was added
|
||||||
assert_eq!(s.sapling_keys().len(), 1);
|
assert_eq!(s.sapling_keys().len(), 1);
|
||||||
|
@ -210,7 +210,11 @@ fn scanning_fake_blocks_store_key_and_results() -> Result<()> {
|
||||||
let result = SaplingScannedResult::from(result.transactions()[0].txid.as_ref());
|
let result = SaplingScannedResult::from(result.transactions()[0].txid.as_ref());
|
||||||
|
|
||||||
// Add result to database
|
// Add result to database
|
||||||
s.add_sapling_result(key_to_be_stored.clone(), Height(1), vec![result]);
|
s.add_sapling_results(
|
||||||
|
key_to_be_stored.clone(),
|
||||||
|
Height(1),
|
||||||
|
[(TransactionIndex::from_usize(0), result)].into(),
|
||||||
|
);
|
||||||
|
|
||||||
// Check the result was added
|
// Check the result was added
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
|
@ -56,7 +56,7 @@ pub use service::{
|
||||||
chain_tip::{ChainTipChange, LatestChainTip, TipAction},
|
chain_tip::{ChainTipChange, LatestChainTip, TipAction},
|
||||||
check, init, spawn_init,
|
check, init, spawn_init,
|
||||||
watch_receiver::WatchReceiver,
|
watch_receiver::WatchReceiver,
|
||||||
OutputIndex, OutputLocation, TransactionLocation,
|
OutputIndex, OutputLocation, TransactionIndex, TransactionLocation,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "shielded-scan")]
|
#[cfg(feature = "shielded-scan")]
|
||||||
|
|
|
@ -85,7 +85,7 @@ pub mod arbitrary;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
pub use finalized_state::{OutputIndex, OutputLocation, TransactionLocation};
|
pub use finalized_state::{OutputIndex, OutputLocation, TransactionIndex, TransactionLocation};
|
||||||
|
|
||||||
use self::queued_blocks::{QueuedCheckpointVerified, QueuedSemanticallyVerified, SentHashes};
|
use self::queued_blocks::{QueuedCheckpointVerified, QueuedSemanticallyVerified, SentHashes};
|
||||||
|
|
||||||
|
|
|
@ -42,7 +42,8 @@ mod tests;
|
||||||
pub use disk_db::{DiskDb, DiskWriteBatch, ReadDisk, WriteDisk};
|
pub use disk_db::{DiskDb, DiskWriteBatch, ReadDisk, WriteDisk};
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
pub use disk_format::{
|
pub use disk_format::{
|
||||||
FromDisk, IntoDisk, OutputIndex, OutputLocation, TransactionLocation, MAX_ON_DISK_HEIGHT,
|
FromDisk, IntoDisk, OutputIndex, OutputLocation, TransactionIndex, TransactionLocation,
|
||||||
|
MAX_ON_DISK_HEIGHT,
|
||||||
};
|
};
|
||||||
pub use zebra_db::ZebraDb;
|
pub use zebra_db::ZebraDb;
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ pub mod scan;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
pub use block::{TransactionLocation, MAX_ON_DISK_HEIGHT};
|
pub use block::{TransactionIndex, TransactionLocation, MAX_ON_DISK_HEIGHT};
|
||||||
pub use transparent::{OutputIndex, OutputLocation};
|
pub use transparent::{OutputIndex, OutputLocation};
|
||||||
|
|
||||||
#[cfg(feature = "shielded-scan")]
|
#[cfg(feature = "shielded-scan")]
|
||||||
|
|
|
@ -89,7 +89,7 @@ impl TransactionIndex {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns this index as a `usize`
|
/// Returns this index as a `usize`.
|
||||||
pub fn as_usize(&self) -> usize {
|
pub fn as_usize(&self) -> usize {
|
||||||
self.0.into()
|
self.0.into()
|
||||||
}
|
}
|
||||||
|
@ -103,11 +103,21 @@ impl TransactionIndex {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns this index as a `u64`
|
/// Returns this index as a `u64`.
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn as_u64(&self) -> u64 {
|
pub fn as_u64(&self) -> u64 {
|
||||||
self.0.into()
|
self.0.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The minimum value of a transaction index.
|
||||||
|
///
|
||||||
|
/// This value corresponds to the coinbase transaction.
|
||||||
|
pub const MIN: Self = Self(u16::MIN);
|
||||||
|
|
||||||
|
/// The maximum value of a transaction index.
|
||||||
|
///
|
||||||
|
/// This value corresponds to the highest possible transaction index.
|
||||||
|
pub const MAX: Self = Self(u16::MAX);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A transaction's location in the chain, by block height and transaction index.
|
/// A transaction's location in the chain, by block height and transaction index.
|
||||||
|
@ -127,6 +137,11 @@ pub struct TransactionLocation {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TransactionLocation {
|
impl TransactionLocation {
|
||||||
|
/// Creates a transaction location from a block height and transaction index.
|
||||||
|
pub fn from_parts(height: Height, index: TransactionIndex) -> TransactionLocation {
|
||||||
|
TransactionLocation { height, index }
|
||||||
|
}
|
||||||
|
|
||||||
/// Creates a transaction location from a block height and transaction index.
|
/// Creates a transaction location from a block height and transaction index.
|
||||||
pub fn from_index(height: Height, transaction_index: u16) -> TransactionLocation {
|
pub fn from_index(height: Height, transaction_index: u16) -> TransactionLocation {
|
||||||
TransactionLocation {
|
TransactionLocation {
|
||||||
|
@ -150,6 +165,42 @@ impl TransactionLocation {
|
||||||
index: TransactionIndex::from_u64(transaction_index),
|
index: TransactionIndex::from_u64(transaction_index),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The minimum value of a transaction location.
|
||||||
|
///
|
||||||
|
/// This value corresponds to the genesis coinbase transaction.
|
||||||
|
pub const MIN: Self = Self {
|
||||||
|
height: Height::MIN,
|
||||||
|
index: TransactionIndex::MIN,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The maximum value of a transaction location.
|
||||||
|
///
|
||||||
|
/// This value corresponds to the last transaction in the highest possible block.
|
||||||
|
pub const MAX: Self = Self {
|
||||||
|
height: Height::MAX,
|
||||||
|
index: TransactionIndex::MAX,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The minimum value of a transaction location for `height`.
|
||||||
|
///
|
||||||
|
/// This value is the coinbase transaction.
|
||||||
|
pub const fn min_for_height(height: Height) -> Self {
|
||||||
|
Self {
|
||||||
|
height,
|
||||||
|
index: TransactionIndex::MIN,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The maximum value of a transaction location for `height`.
|
||||||
|
///
|
||||||
|
/// This value can be a valid entry, but it won't fit in a 2MB block.
|
||||||
|
pub const fn max_for_height(height: Height) -> Self {
|
||||||
|
Self {
|
||||||
|
height,
|
||||||
|
index: TransactionIndex::MAX,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Block and transaction trait impls
|
// Block and transaction trait impls
|
||||||
|
|
|
@ -4,23 +4,14 @@
|
||||||
//!
|
//!
|
||||||
//! # Correctness
|
//! # Correctness
|
||||||
//!
|
//!
|
||||||
//! Once format versions are implemented for the scanner database,
|
|
||||||
//! `zebra_scan::Storage::database_format_version_in_code()` must be incremented
|
//! `zebra_scan::Storage::database_format_version_in_code()` must be incremented
|
||||||
//! each time the database format (column, serialization, etc) changes.
|
//! each time the database format (column, serialization, etc) changes.
|
||||||
|
|
||||||
use zebra_chain::{block::Height, transaction};
|
use zebra_chain::{block::Height, transaction};
|
||||||
|
|
||||||
use crate::{FromDisk, IntoDisk};
|
use crate::{FromDisk, IntoDisk, TransactionLocation};
|
||||||
|
|
||||||
use super::block::HEIGHT_DISK_BYTES;
|
use super::block::TRANSACTION_LOCATION_DISK_BYTES;
|
||||||
|
|
||||||
/// The fixed length of the scanning result.
|
|
||||||
///
|
|
||||||
/// TODO: If the scanning result doesn't have a fixed length, either:
|
|
||||||
/// - deserialize using internal length or end markers,
|
|
||||||
/// - prefix it with a length, or
|
|
||||||
/// - stop storing vectors of results on disk, instead store each result with a unique key.
|
|
||||||
pub const SAPLING_SCANNING_RESULT_LENGTH: usize = 32;
|
|
||||||
|
|
||||||
/// The type used in Zebra to store Sapling scanning keys.
|
/// The type used in Zebra to store Sapling scanning keys.
|
||||||
/// It can represent a full viewing key or an individual viewing key.
|
/// It can represent a full viewing key or an individual viewing key.
|
||||||
|
@ -52,7 +43,7 @@ pub struct SaplingScannedDatabaseEntry {
|
||||||
pub index: SaplingScannedDatabaseIndex,
|
pub index: SaplingScannedDatabaseIndex,
|
||||||
|
|
||||||
/// The database column family value.
|
/// The database column family value.
|
||||||
pub value: Vec<SaplingScannedResult>,
|
pub value: Option<SaplingScannedResult>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A database column family key for a block scanned with a Sapling vieweing key.
|
/// A database column family key for a block scanned with a Sapling vieweing key.
|
||||||
|
@ -61,39 +52,62 @@ pub struct SaplingScannedDatabaseIndex {
|
||||||
/// The Sapling viewing key used to scan the block.
|
/// The Sapling viewing key used to scan the block.
|
||||||
pub sapling_key: SaplingScanningKey,
|
pub sapling_key: SaplingScanningKey,
|
||||||
|
|
||||||
/// The height of the block.
|
/// The transaction location: block height and transaction index.
|
||||||
pub height: Height,
|
pub tx_loc: TransactionLocation,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SaplingScannedDatabaseIndex {
|
impl SaplingScannedDatabaseIndex {
|
||||||
/// The minimum value of a sapling scanned database index.
|
/// The minimum value of a sapling scanned database index.
|
||||||
|
///
|
||||||
/// This value is guarateed to be the minimum, and not correspond to a valid key.
|
/// This value is guarateed to be the minimum, and not correspond to a valid key.
|
||||||
|
//
|
||||||
|
// Note: to calculate the maximum value, we need a key length.
|
||||||
pub const fn min() -> Self {
|
pub const fn min() -> Self {
|
||||||
Self {
|
Self {
|
||||||
// The empty string is the minimum value in RocksDB lexicographic order.
|
// The empty string is the minimum value in RocksDB lexicographic order.
|
||||||
sapling_key: String::new(),
|
sapling_key: String::new(),
|
||||||
// Genesis is the minimum height, and never has valid shielded transfers.
|
tx_loc: TransactionLocation::MIN,
|
||||||
height: Height(0),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The minimum value of a sapling scanned database index for `sapling_key`.
|
/// The minimum value of a sapling scanned database index for `sapling_key`.
|
||||||
/// This value is guarateed to be the minimum, and not correspond to a valid entry.
|
///
|
||||||
|
/// This value does not correspond to a valid entry.
|
||||||
|
/// (The genesis coinbase transaction does not have shielded transfers.)
|
||||||
pub fn min_for_key(sapling_key: &SaplingScanningKey) -> Self {
|
pub fn min_for_key(sapling_key: &SaplingScanningKey) -> Self {
|
||||||
Self {
|
Self {
|
||||||
sapling_key: sapling_key.clone(),
|
sapling_key: sapling_key.clone(),
|
||||||
// Genesis is the minimum height, and never has valid shielded transfers.
|
tx_loc: TransactionLocation::MIN,
|
||||||
height: Height(0),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The maximum value of a sapling scanned database index for `sapling_key`.
|
/// The maximum value of a sapling scanned database index for `sapling_key`.
|
||||||
/// This value is guarateed to be the maximum, and not correspond to a valid entry.
|
///
|
||||||
|
/// This value may correspond to a valid entry, but it won't be mined for many decades.
|
||||||
pub fn max_for_key(sapling_key: &SaplingScanningKey) -> Self {
|
pub fn max_for_key(sapling_key: &SaplingScanningKey) -> Self {
|
||||||
Self {
|
Self {
|
||||||
sapling_key: sapling_key.clone(),
|
sapling_key: sapling_key.clone(),
|
||||||
// The maximum height will never be mined - we'll increase it before that happens.
|
tx_loc: TransactionLocation::MAX,
|
||||||
height: Height::MAX,
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The minimum value of a sapling scanned database index for `sapling_key` and `height`.
|
||||||
|
///
|
||||||
|
/// This value can be a valid entry for shielded coinbase.
|
||||||
|
pub fn min_for_key_and_height(sapling_key: &SaplingScanningKey, height: Height) -> Self {
|
||||||
|
Self {
|
||||||
|
sapling_key: sapling_key.clone(),
|
||||||
|
tx_loc: TransactionLocation::min_for_height(height),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The maximum value of a sapling scanned database index for `sapling_key` and `height`.
|
||||||
|
///
|
||||||
|
/// This value can be a valid entry, but it won't fit in a 2MB block.
|
||||||
|
pub fn max_for_key_and_height(sapling_key: &SaplingScanningKey, height: Height) -> Self {
|
||||||
|
Self {
|
||||||
|
sapling_key: sapling_key.clone(),
|
||||||
|
tx_loc: TransactionLocation::max_for_height(height),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -120,7 +134,7 @@ impl IntoDisk for SaplingScannedDatabaseIndex {
|
||||||
let mut bytes = Vec::new();
|
let mut bytes = Vec::new();
|
||||||
|
|
||||||
bytes.extend(self.sapling_key.as_bytes());
|
bytes.extend(self.sapling_key.as_bytes());
|
||||||
bytes.extend(self.height.as_bytes());
|
bytes.extend(self.tx_loc.as_bytes());
|
||||||
|
|
||||||
bytes
|
bytes
|
||||||
}
|
}
|
||||||
|
@ -130,11 +144,11 @@ impl FromDisk for SaplingScannedDatabaseIndex {
|
||||||
fn from_bytes(bytes: impl AsRef<[u8]>) -> Self {
|
fn from_bytes(bytes: impl AsRef<[u8]>) -> Self {
|
||||||
let bytes = bytes.as_ref();
|
let bytes = bytes.as_ref();
|
||||||
|
|
||||||
let (sapling_key, height) = bytes.split_at(bytes.len() - HEIGHT_DISK_BYTES);
|
let (sapling_key, tx_loc) = bytes.split_at(bytes.len() - TRANSACTION_LOCATION_DISK_BYTES);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
sapling_key: SaplingScanningKey::from_bytes(sapling_key),
|
sapling_key: SaplingScanningKey::from_bytes(sapling_key),
|
||||||
height: Height::from_bytes(height),
|
tx_loc: TransactionLocation::from_bytes(tx_loc),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -153,22 +167,28 @@ impl FromDisk for SaplingScannedResult {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoDisk for Vec<SaplingScannedResult> {
|
impl IntoDisk for Option<SaplingScannedResult> {
|
||||||
type Bytes = Vec<u8>;
|
type Bytes = Vec<u8>;
|
||||||
|
|
||||||
fn as_bytes(&self) -> Self::Bytes {
|
fn as_bytes(&self) -> Self::Bytes {
|
||||||
self.iter()
|
let mut bytes = Vec::new();
|
||||||
.flat_map(SaplingScannedResult::as_bytes)
|
|
||||||
.collect()
|
if let Some(result) = self.as_ref() {
|
||||||
|
bytes.extend(result.as_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromDisk for Vec<SaplingScannedResult> {
|
impl FromDisk for Option<SaplingScannedResult> {
|
||||||
fn from_bytes(bytes: impl AsRef<[u8]>) -> Self {
|
fn from_bytes(bytes: impl AsRef<[u8]>) -> Self {
|
||||||
bytes
|
let bytes = bytes.as_ref();
|
||||||
.as_ref()
|
|
||||||
.chunks(SAPLING_SCANNING_RESULT_LENGTH)
|
if bytes.is_empty() {
|
||||||
.map(SaplingScannedResult::from_bytes)
|
None
|
||||||
.collect()
|
} else {
|
||||||
|
Some(SaplingScannedResult::from_bytes(bytes))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue