use std::sync::Arc;
use serde::Serialize;
use zebra_chain::{
block::{self, Block, Height, SerializedBlock},
orchard,
parameters::Network,
sapling,
serialization::{ZcashDeserializeInto, ZcashSerialize},
transaction::{self, Transaction},
transparent,
};
use crate::{
service::{
finalized_state::{
disk_format::{
block::TransactionIndex, transparent::OutputLocation, FromDisk, TransactionLocation,
},
zebra_db::transparent::BALANCE_BY_TRANSPARENT_ADDR,
FinalizedState,
},
read::ADDRESS_HEIGHTS_FULL_RANGE,
},
Config,
};
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
struct Tip {
height: u32,
block_hash: String,
}
impl From<(Height, block::Hash)> for Tip {
fn from((height, hash): (Height, block::Hash)) -> Tip {
Tip {
height: height.0,
block_hash: hash.to_string(),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize)]
struct BlockHash(String);
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
struct BlockData {
height: u32,
#[serde(with = "hex")]
block: SerializedBlock,
}
impl BlockData {
pub fn new(height: Height, block: &Block) -> BlockData {
BlockData {
height: height.0,
block: block.into(),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
struct TransactionHashByLocation {
loc: Option<TransactionLocation>,
#[serde(with = "hex")]
hash: transaction::Hash,
}
impl TransactionHashByLocation {
pub fn new(
loc: Option<TransactionLocation>,
hash: transaction::Hash,
) -> TransactionHashByLocation {
TransactionHashByLocation { loc, hash }
}
}
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize)]
struct TransactionData {
loc: TransactionLocation,
transaction: String,
}
impl TransactionData {
pub fn new(loc: TransactionLocation, transaction: &Transaction) -> TransactionData {
let transaction = transaction
.zcash_serialize_to_vec()
.expect("serialization of stored transaction succeeds");
TransactionData {
loc,
transaction: hex::encode(transaction),
}
}
}
#[test]
fn test_block_and_transaction_data() {
let _init_guard = zebra_test::init();
for network in Network::iter() {
test_block_and_transaction_data_with_network(network);
}
}
fn test_block_and_transaction_data_with_network(network: Network) {
let mut net_suffix = network.to_string();
net_suffix.make_ascii_lowercase();
let mut state = FinalizedState::new(
&Config::ephemeral(),
&network,
#[cfg(feature = "elasticsearch")]
false,
);
let mut settings = insta::Settings::clone_current();
settings.set_snapshot_suffix("no_blocks");
settings.bind(|| snapshot_block_and_transaction_data(&state));
let blocks = network.blockchain_map();
for height in 0..=2 {
let block: Arc<Block> = blocks
.get(&height)
.expect("block height has test data")
.zcash_deserialize_into()
.expect("test data deserializes");
state
.commit_finalized_direct(block.into(), None, "snapshot tests")
.expect("test block is valid");
let mut settings = insta::Settings::clone_current();
settings.set_snapshot_suffix(format!("{net_suffix}_{height}"));
settings.bind(|| snapshot_block_and_transaction_data(&state));
settings.bind(|| snapshot_transparent_address_data(&state, height));
}
}
fn snapshot_block_and_transaction_data(state: &FinalizedState) {
let tip = state.tip();
insta::assert_ron_snapshot!("tip", tip.map(Tip::from));
if let Some((max_height, tip_block_hash)) = tip {
let sapling_tree = state
.sapling_tree_by_height(&block::Height::MIN)
.expect("the genesis block in the database has a Sapling tree");
let orchard_tree = state
.orchard_tree_by_height(&block::Height::MIN)
.expect("the genesis block in the database has an Orchard tree");
assert_eq!(*sapling_tree, sapling::tree::NoteCommitmentTree::default());
assert_eq!(*orchard_tree, orchard::tree::NoteCommitmentTree::default());
let mut stored_block_hashes = Vec::new();
let mut stored_blocks = Vec::new();
let mut stored_transaction_hashes = Vec::new();
let mut stored_transactions = Vec::new();
let mut stored_utxos = Vec::new();
let stored_sprout_trees = state.sprout_trees_full_map();
let mut stored_sapling_trees = Vec::new();
let mut stored_orchard_trees = Vec::new();
let sprout_tree_at_tip = state.sprout_tree_for_tip();
let sapling_tree_at_tip = state.sapling_tree_for_tip();
let orchard_tree_at_tip = state.orchard_tree_for_tip();
let history_tree_at_tip = state.history_tree();
for query_height in 0..=max_height.0 {
let query_height = Height(query_height);
let stored_block_hash = state
.hash(query_height)
.expect("heights up to tip have hashes");
let stored_height = state
.height(stored_block_hash)
.expect("hashes up to tip have heights");
let stored_block = state
.block(query_height.into())
.expect("heights up to tip have blocks");
let sapling_tree_by_height = state
.sapling_tree_by_height(&query_height)
.expect("heights up to tip have Sapling trees");
let orchard_tree_by_height = state
.orchard_tree_by_height(&query_height)
.expect("heights up to tip have Orchard trees");
assert_eq!(stored_height, query_height);
assert_eq!(
stored_block
.coinbase_height()
.expect("stored blocks have valid heights"),
query_height,
);
if query_height == max_height {
assert_eq!(stored_block_hash, tip_block_hash);
if let Some(stored_tree) = stored_sprout_trees.get(&sprout_tree_at_tip.root()) {
assert_eq!(
&sprout_tree_at_tip, stored_tree,
"unexpected missing sprout tip tree:\n\
all trees: {stored_sprout_trees:?}"
);
} else {
assert_eq!(sprout_tree_at_tip, Default::default());
}
assert_eq!(sapling_tree_at_tip, sapling_tree_by_height);
assert_eq!(orchard_tree_at_tip, orchard_tree_by_height);
if let Some(history_tree_at_tip) = history_tree_at_tip.as_ref().as_ref() {
assert_eq!(history_tree_at_tip.current_height(), max_height);
assert_eq!(history_tree_at_tip.network(), &state.network());
}
}
stored_block_hashes.push((stored_height, BlockHash(stored_block_hash.to_string())));
stored_blocks.push(BlockData::new(stored_height, &stored_block));
stored_sapling_trees.push((stored_height, sapling_tree_by_height));
stored_orchard_trees.push((stored_height, orchard_tree_by_height));
for tx_index in 0..stored_block.transactions.len() {
let block_transaction = &stored_block.transactions[tx_index];
let transaction_location = TransactionLocation::from_usize(query_height, tx_index);
let transaction_hash = block_transaction.hash();
let transaction_data =
TransactionData::new(transaction_location, block_transaction);
let (direct_transaction, direct_transaction_height, _) = state
.transaction(transaction_hash)
.expect("transactions in blocks must also be available directly");
let stored_transaction_hash = state
.transaction_hash(transaction_location)
.expect("hashes of transactions in blocks must be indexed by location");
let stored_transaction_location = state
.transaction_location(transaction_hash)
.expect("locations of transactions in blocks must be indexed by hash");
assert_eq!(
&direct_transaction, block_transaction,
"transactions in block must be the same as transactions looked up directly",
);
assert_eq!(
direct_transaction_height, transaction_location.height,
"transaction heights must be the same as their block heights",
);
assert_eq!(stored_transaction_hash, transaction_hash);
assert_eq!(stored_transaction_location, transaction_location);
let stored_transaction_hash = TransactionHashByLocation::new(
Some(stored_transaction_location),
transaction_hash,
);
stored_transaction_hashes.push(stored_transaction_hash);
stored_transactions.push(transaction_data);
for output_index in 0..stored_block.transactions[tx_index].outputs().len() {
let output = &stored_block.transactions[tx_index].outputs()[output_index];
let outpoint =
transparent::OutPoint::from_usize(transaction_hash, output_index);
let output_location =
OutputLocation::from_usize(query_height, tx_index, output_index);
let stored_output_location = state
.output_location(&outpoint)
.expect("all outpoints are indexed");
let stored_utxo_by_outpoint = state.utxo(&outpoint);
let stored_utxo_by_out_loc = state.utxo_by_location(output_location);
assert_eq!(stored_output_location, output_location);
assert_eq!(stored_utxo_by_out_loc, stored_utxo_by_outpoint);
if let Some(stored_utxo) = &stored_utxo_by_out_loc {
assert_eq!(&stored_utxo.utxo.output, output);
assert_eq!(stored_utxo.utxo.height, query_height);
assert_eq!(
stored_utxo.utxo.from_coinbase,
transaction_location.index == TransactionIndex::from_usize(0),
"coinbase transactions must be the first transaction in a block:\n\
from_coinbase was: {from_coinbase},\n\
but transaction index was: {tx_index},\n\
at: {transaction_location:?},\n\
{output_location:?}",
from_coinbase = stored_utxo.utxo.from_coinbase,
);
}
stored_utxos.push((
output_location,
stored_utxo_by_out_loc.map(|ordered_utxo| ordered_utxo.utxo),
));
}
}
}
assert!(
is_sorted(&stored_block_hashes),
"unsorted: {stored_block_hashes:?}"
);
assert!(
is_sorted(&stored_transactions),
"unsorted: {stored_transactions:?}"
);
insta::assert_ron_snapshot!("block_hashes", stored_block_hashes);
insta::assert_ron_snapshot!("blocks", stored_blocks);
insta::assert_ron_snapshot!("transaction_hashes", stored_transaction_hashes);
insta::assert_ron_snapshot!("transactions", stored_transactions);
insta::assert_ron_snapshot!("utxos", stored_utxos);
insta::assert_ron_snapshot!("sprout_tree_at_tip", sprout_tree_at_tip);
insta::assert_ron_snapshot!(
"sprout_trees",
stored_sprout_trees,
{
"." => insta::sorted_redaction()
}
);
insta::assert_ron_snapshot!("sapling_trees", stored_sapling_trees);
insta::assert_ron_snapshot!("orchard_trees", stored_orchard_trees);
insta::assert_debug_snapshot!("history_tree", (max_height, history_tree_at_tip));
}
}
fn snapshot_transparent_address_data(state: &FinalizedState, height: u32) {
let balance_by_transparent_addr = state.cf_handle(BALANCE_BY_TRANSPARENT_ADDR).unwrap();
let utxo_loc_by_transparent_addr_loc =
state.cf_handle("utxo_loc_by_transparent_addr_loc").unwrap();
let tx_loc_by_transparent_addr_loc = state.cf_handle("tx_loc_by_transparent_addr_loc").unwrap();
let mut stored_address_balances = Vec::new();
let mut stored_address_utxo_locations = Vec::new();
let mut stored_address_utxos = Vec::new();
let mut stored_address_transaction_locations = Vec::new();
let addresses =
state.full_iterator_cf(&balance_by_transparent_addr, rocksdb::IteratorMode::Start);
let utxo_address_location_count = state
.full_iterator_cf(
&utxo_loc_by_transparent_addr_loc,
rocksdb::IteratorMode::Start,
)
.count();
let transaction_address_location_count = state
.full_iterator_cf(
&tx_loc_by_transparent_addr_loc,
rocksdb::IteratorMode::Start,
)
.count();
let addresses: Vec<transparent::Address> = addresses
.map(|result| result.expect("unexpected database error"))
.map(|(key, _value)| transparent::Address::from_bytes(key))
.collect();
if height == 0 {
assert_eq!(addresses.len(), 0);
assert_eq!(utxo_address_location_count, 0);
assert_eq!(transaction_address_location_count, 0);
return;
}
for address in addresses {
let stored_address_balance_location = state
.address_balance_location(&address)
.expect("address indexes are consistent");
let stored_address_location = stored_address_balance_location.address_location();
let mut stored_utxo_locations = Vec::new();
for address_utxo_loc in state.address_utxo_locations(stored_address_location) {
assert_eq!(address_utxo_loc.address_location(), stored_address_location);
stored_utxo_locations.push(address_utxo_loc.unspent_output_location());
}
let mut stored_utxos = Vec::new();
for (utxo_loc, utxo) in state.address_utxos(&address) {
assert!(stored_utxo_locations.contains(&utxo_loc));
stored_utxos.push(utxo);
}
let mut stored_transaction_locations = Vec::new();
for transaction_location in
state.address_transaction_locations(stored_address_location, ADDRESS_HEIGHTS_FULL_RANGE)
{
assert_eq!(
transaction_location.address_location(),
stored_address_location
);
stored_transaction_locations.push(transaction_location.transaction_location());
}
assert!(
is_sorted(&stored_utxo_locations),
"unsorted: {stored_utxo_locations:?}\n\
for address: {address:?}",
);
assert!(
is_sorted(&stored_transaction_locations),
"unsorted: {stored_transaction_locations:?}\n\
for address: {address:?}",
);
stored_address_balances.push((address.to_string(), stored_address_balance_location));
stored_address_utxo_locations.push((stored_address_location, stored_utxo_locations));
stored_address_utxos.push((address.clone(), stored_utxos));
stored_address_transaction_locations.push((address, stored_transaction_locations));
}
insta::assert_ron_snapshot!("address_balances", stored_address_balances);
insta::assert_ron_snapshot!("address_utxos", stored_address_utxo_locations);
insta::assert_ron_snapshot!("address_utxo_data", stored_address_utxos);
insta::assert_ron_snapshot!(
"address_transaction_locations",
stored_address_transaction_locations
);
}
pub fn is_sorted<T: Ord + Clone>(list: &[T]) -> bool {
let mut sorted_list = list.to_owned();
sorted_list.sort();
list == sorted_list
}