feat(state): add transparent address indexes to the non-finalized state (#4022)

* Derive Hash for transparent address index types

* Expose some types used by transparent address indexes

* Add an empty transparent transfers type for transparent address indexes

* Update TransparentTransfers with created UTXOs

* Add spent transparent outputs to ContextuallyValidBlock

* Update TransparentTransfers with spent transparent outputs

* Ignore missing spent outputs, so that tests pass

* Remove empty TransparentTransfers after a spend revert

* Update TransparentTransfers with creating and spending transaction IDs

* Ignore duplicate created UTXOs, so that tests pass

* Add some TODO comments

* Remove accidental doctest formatting

* Add address transfers index accessor methods

* Use TransactionLocation in the non-finalized state

* Apply more address index assertions to production code

* Refactor deeply nested code and apply more assertions

* Return UTXOs in chain order

* Return transaction hashes in chain order

* Stop indexing each transparent output multiple times

* Run some more asserts during tests

* Tidy TODO comments

* Fix an incorrect assert condition

* Use OrderedUtxos so that spent UTXOs can be stored in chain order

* Update tests to use OrderedUtxos

* Update the index API for the getaddressutxos query

* Remove redundant arguments in tests

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
This commit is contained in:
teor 2022-04-13 03:21:46 +10:00 committed by GitHub
parent b7f6fdc222
commit e49c1d7034
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 722 additions and 132 deletions

View File

@ -185,6 +185,7 @@ impl Block {
///
/// `utxos` must contain the [`Utxo`]s of every input in this block,
/// including UTXOs created by earlier transactions in this block.
/// (It can also contain unrelated UTXOs, which are ignored.)
///
/// Note: the chain value pool has the opposite sign to the transaction
/// value pool.

View File

@ -373,7 +373,7 @@ impl Arbitrary for Block {
pub fn allow_all_transparent_coinbase_spends(
_: transparent::OutPoint,
_: transparent::CoinbaseSpendRestriction,
_: transparent::Utxo,
_: transparent::OrderedUtxo,
) -> Result<(), ()> {
Ok(())
}
@ -400,7 +400,7 @@ impl Block {
F: Fn(
transparent::OutPoint,
transparent::CoinbaseSpendRestriction,
transparent::Utxo,
transparent::OrderedUtxo,
) -> Result<T, E>
+ Copy
+ 'static,
@ -560,7 +560,7 @@ where
F: Fn(
transparent::OutPoint,
transparent::CoinbaseSpendRestriction,
transparent::Utxo,
transparent::OrderedUtxo,
) -> Result<T, E>
+ Copy
+ 'static,
@ -641,7 +641,7 @@ where
F: Fn(
transparent::OutPoint,
transparent::CoinbaseSpendRestriction,
transparent::Utxo,
transparent::OrderedUtxo,
) -> Result<T, E>
+ Copy
+ 'static,
@ -652,8 +652,6 @@ where
// choose an arbitrary spendable UTXO, in hash set order
while let Some((candidate_outpoint, candidate_utxo)) = utxos.iter().next() {
let candidate_utxo = candidate_utxo.clone().utxo;
attempts += 1;
// Avoid O(n^2) algorithmic complexity by giving up early,

View File

@ -9,7 +9,7 @@ use crate::{
};
/// An unspent `transparent::Output`, with accompanying metadata.
#[derive(Clone, Debug, PartialEq, Eq)]
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(
any(test, feature = "proptest-impl"),
derive(proptest_derive::Arbitrary, serde::Serialize)

View File

@ -1 +1,2 @@
cc 693962399f2771634758dccac882604c14751015ac280113fa9127fa96376c3a # entered unreachable code: older transaction versions only exist in finalized blocks pre sapling; PreparedBlock { .. , Block { .. , transactions: [V2 { .. }, .. ] } };
cc fef34a78971f81b3342e4530136fa7dccfdd7e1381b365f2231840181f510e7d # shrinks to (vec1, vec2) = (alloc::vec::Vec<alloc::sync::Arc<zebra_chain::block::Block>><alloc::sync::Arc<zebra_chain::block::Block>>, len=2, alloc::vec::Vec<alloc::sync::Arc<zebra_chain::block::Block>><alloc::sync::Arc<zebra_chain::block::Block>>, len=2)

View File

@ -1,3 +1,5 @@
//! Randomised data generation for state data.
use std::sync::Arc;
use zebra_chain::{
@ -102,15 +104,13 @@ impl ContextuallyValidBlock {
pub fn test_with_zero_spent_utxos(block: impl Into<PreparedBlock>) -> Self {
let block = block.into();
let zero_utxo = transparent::Utxo {
output: transparent::Output {
value: Amount::zero(),
lock_script: transparent::Script::new(&[]),
},
height: block::Height(1),
from_coinbase: false,
let zero_output = transparent::Output {
value: Amount::zero(),
lock_script: transparent::Script::new(&[]),
};
let zero_utxo = transparent::OrderedUtxo::new(zero_output, block::Height(1), 1);
let zero_spent_utxos = block
.block
.transactions
@ -145,7 +145,11 @@ impl ContextuallyValidBlock {
block,
hash,
height,
new_outputs: transparent::utxos_from_ordered_utxos(new_outputs),
new_outputs: new_outputs.clone(),
// Just re-use the outputs we created in this block, even though that's incorrect.
//
// TODO: fix the tests, and stop adding unrelated inputs and outputs.
spent_outputs: new_outputs,
transaction_hashes,
chain_value_pool_change: fake_chain_value_pool_change,
}

View File

@ -35,7 +35,7 @@ pub use request::{FinalizedBlock, HashOrHeight, PreparedBlock, ReadRequest, Requ
pub use response::{ReadResponse, Response};
pub use service::{
chain_tip::{ChainTipChange, LatestChainTip, TipAction},
init,
init, OutputLocation, TransactionLocation,
};
#[cfg(any(test, feature = "proptest-impl"))]

View File

@ -80,6 +80,8 @@ pub struct PreparedBlock {
/// Note: although these transparent outputs are newly created, they may not
/// be unspent, since a later transaction in a block can spend outputs of an
/// earlier transaction.
///
/// This field can also contain unrelated outputs, which are ignored.
pub new_outputs: HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
/// A precomputed list of the hashes of the transactions in this block,
/// in the same order as `block.transactions`.
@ -95,11 +97,38 @@ pub struct PreparedBlock {
/// Used by the state service and non-finalized [`Chain`].
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ContextuallyValidBlock {
/// The block to commit to the state.
pub(crate) block: Arc<Block>,
/// The hash of the block.
pub(crate) hash: block::Hash,
/// The height of the block.
pub(crate) height: block::Height,
pub(crate) new_outputs: HashMap<transparent::OutPoint, transparent::Utxo>,
/// New transparent outputs created in this block, indexed by
/// [`Outpoint`](transparent::Outpoint).
///
/// Note: although these transparent outputs are newly created, they may not
/// be unspent, since a later transaction in a block can spend outputs of an
/// earlier transaction.
///
/// This field can also contain unrelated outputs, which are ignored.
pub(crate) new_outputs: HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
/// The outputs spent by this block, indexed by the [`transparent::Input`]'s
/// [`Outpoint`](transparent::Outpoint).
///
/// Note: these inputs can come from earlier transactions in this block,
/// or earlier blocks in the chain.
///
/// This field can also contain unrelated outputs, which are ignored.
pub(crate) spent_outputs: HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
/// A precomputed list of the hashes of the transactions in this block,
/// in the same order as `block.transactions`.
pub(crate) transaction_hashes: Arc<[transaction::Hash]>,
/// The sum of the chain value pool changes of all transactions in this block.
pub(crate) chain_value_pool_change: ValueBalance<NegativeAllowed>,
}
@ -122,6 +151,8 @@ pub struct FinalizedBlock {
/// Note: although these transparent outputs are newly created, they may not
/// be unspent, since a later transaction in a block can spend outputs of an
/// earlier transaction.
///
/// This field can also contain unrelated outputs, which are ignored.
pub(crate) new_outputs: HashMap<transparent::OutPoint, transparent::Utxo>,
/// A precomputed list of the hashes of the transactions in this block,
/// in the same order as `block.transactions`.
@ -150,7 +181,7 @@ impl ContextuallyValidBlock {
/// [`Chain::update_chain_state_with`] returns success.
pub fn with_block_and_spent_utxos(
prepared: PreparedBlock,
mut spent_utxos: HashMap<transparent::OutPoint, transparent::Utxo>,
mut spent_outputs: HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
) -> Result<Self, ValueBalanceError> {
let PreparedBlock {
block,
@ -162,15 +193,19 @@ impl ContextuallyValidBlock {
// This is redundant for the non-finalized state,
// but useful to make some tests pass more easily.
spent_utxos.extend(utxos_from_ordered_utxos(new_outputs.clone()));
//
// TODO: fix the tests, and stop adding unrelated outputs.
spent_outputs.extend(new_outputs.clone());
Ok(Self {
block: block.clone(),
hash,
height,
new_outputs: transparent::utxos_from_ordered_utxos(new_outputs),
new_outputs,
spent_outputs: spent_outputs.clone(),
transaction_hashes,
chain_value_pool_change: block.chain_value_pool_change(&spent_utxos)?,
chain_value_pool_change: block
.chain_value_pool_change(&utxos_from_ordered_utxos(spent_outputs))?,
})
}
}
@ -213,14 +248,16 @@ impl From<ContextuallyValidBlock> for FinalizedBlock {
hash,
height,
new_outputs,
spent_outputs: _,
transaction_hashes,
chain_value_pool_change: _,
} = contextually_valid;
Self {
block,
hash,
height,
new_outputs,
new_outputs: utxos_from_ordered_utxos(new_outputs),
transaction_hashes,
}
}

View File

@ -66,6 +66,8 @@ pub mod arbitrary;
#[cfg(test)]
mod tests;
pub use finalized_state::{OutputLocation, TransactionLocation};
pub type QueuedBlock = (
PreparedBlock,
oneshot::Sender<Result<block::Hash, BoxError>>,
@ -507,7 +509,12 @@ impl StateService {
self.mem
.any_utxo(outpoint)
.or_else(|| self.queued_blocks.utxo(outpoint))
.or_else(|| self.disk.db().utxo(outpoint))
.or_else(|| {
self.disk
.db()
.utxo(outpoint)
.map(|ordered_utxo| ordered_utxo.utxo)
})
}
/// Return an iterator over the relevant chain of the block identified by

View File

@ -76,10 +76,10 @@ impl From<ContextuallyValidBlock> for ChainTipBlock {
block,
hash,
height,
new_outputs: _,
transaction_hashes,
chain_value_pool_change: _,
..
} = contextually_valid;
Self {
hash,
height,
@ -96,8 +96,8 @@ impl From<FinalizedBlock> for ChainTipBlock {
block,
hash,
height,
new_outputs: _,
transaction_hashes,
..
} = finalized;
Self {
hash,

View File

@ -1,6 +1,6 @@
//! Test vectors and randomised property tests for UTXO contextual validation
use std::{convert::TryInto, env, sync::Arc};
use std::{env, sync::Arc};
use proptest::prelude::*;
@ -43,19 +43,16 @@ fn accept_shielded_mature_coinbase_utxo_spend() {
value: Amount::zero(),
lock_script: transparent::Script::new(&[]),
};
let utxo = transparent::Utxo {
output,
height: created_height,
from_coinbase: true,
};
let ordered_utxo = transparent::OrderedUtxo::new(output, created_height, 0);
let min_spend_height = Height(created_height.0 + MIN_TRANSPARENT_COINBASE_MATURITY);
let spend_restriction = transparent::CoinbaseSpendRestriction::OnlyShieldedOutputs {
spend_height: min_spend_height,
};
let result = check::utxo::transparent_coinbase_spend(outpoint, spend_restriction, utxo.clone());
assert_eq!(result, Ok(utxo));
let result =
check::utxo::transparent_coinbase_spend(outpoint, spend_restriction, ordered_utxo.clone());
assert_eq!(result, Ok(ordered_utxo));
}
/// Check that non-shielded spends of coinbase transparent outputs fail.
@ -72,15 +69,11 @@ fn reject_unshielded_coinbase_utxo_spend() {
value: Amount::zero(),
lock_script: transparent::Script::new(&[]),
};
let utxo = transparent::Utxo {
output,
height: created_height,
from_coinbase: true,
};
let ordered_utxo = transparent::OrderedUtxo::new(output, created_height, 0);
let spend_restriction = transparent::CoinbaseSpendRestriction::SomeTransparentOutputs;
let result = check::utxo::transparent_coinbase_spend(outpoint, spend_restriction, utxo);
let result = check::utxo::transparent_coinbase_spend(outpoint, spend_restriction, ordered_utxo);
assert_eq!(result, Err(UnshieldedTransparentCoinbaseSpend { outpoint }));
}
@ -98,18 +91,14 @@ fn reject_immature_coinbase_utxo_spend() {
value: Amount::zero(),
lock_script: transparent::Script::new(&[]),
};
let utxo = transparent::Utxo {
output,
height: created_height,
from_coinbase: true,
};
let ordered_utxo = transparent::OrderedUtxo::new(output, created_height, 0);
let min_spend_height = Height(created_height.0 + MIN_TRANSPARENT_COINBASE_MATURITY);
let spend_height = Height(min_spend_height.0 - 1);
let spend_restriction =
transparent::CoinbaseSpendRestriction::OnlyShieldedOutputs { spend_height };
let result = check::utxo::transparent_coinbase_spend(outpoint, spend_restriction, utxo);
let result = check::utxo::transparent_coinbase_spend(outpoint, spend_restriction, ordered_utxo);
assert_eq!(
result,
Err(ImmatureTransparentCoinbaseSpend {

View File

@ -4,7 +4,7 @@ use std::collections::{HashMap, HashSet};
use zebra_chain::{
amount, block,
transparent::{self, CoinbaseSpendRestriction::*},
transparent::{self, utxos_from_ordered_utxos, CoinbaseSpendRestriction::*},
};
use crate::{
@ -37,23 +37,23 @@ use crate::{
/// - unshielded spends of a transparent coinbase output.
pub fn transparent_spend(
prepared: &PreparedBlock,
non_finalized_chain_unspent_utxos: &HashMap<transparent::OutPoint, transparent::Utxo>,
non_finalized_chain_unspent_utxos: &HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
non_finalized_chain_spent_utxos: &HashSet<transparent::OutPoint>,
finalized_state: &ZebraDb,
) -> Result<HashMap<transparent::OutPoint, transparent::Utxo>, ValidateContextError> {
) -> Result<HashMap<transparent::OutPoint, transparent::OrderedUtxo>, ValidateContextError> {
let mut block_spends = HashMap::new();
for (spend_tx_index_in_block, transaction) in prepared.block.transactions.iter().enumerate() {
let spends = transaction.inputs().iter().filter_map(|input| match input {
transparent::Input::PrevOut { outpoint, .. } => Some(outpoint),
// Coinbase inputs represent new coins,
// so there are no UTXOs to mark as spent.
transparent::Input::Coinbase { .. } => None,
});
// Coinbase inputs represent new coins,
// so there are no UTXOs to mark as spent.
let spends = transaction
.inputs()
.iter()
.filter_map(transparent::Input::outpoint);
for spend in spends {
let utxo = transparent_spend_chain_order(
*spend,
spend,
spend_tx_index_in_block,
&prepared.new_outputs,
non_finalized_chain_unspent_utxos,
@ -71,15 +71,15 @@ pub fn transparent_spend(
// so we check transparent coinbase maturity and shielding
// using known valid UTXOs during non-finalized chain validation.
let spend_restriction = transaction.coinbase_spend_restriction(prepared.height);
let utxo = transparent_coinbase_spend(*spend, spend_restriction, utxo)?;
let utxo = transparent_coinbase_spend(spend, spend_restriction, utxo)?;
// We don't delete the UTXOs until the block is committed,
// so we need to check for duplicate spends within the same block.
//
// See `transparent_spend_chain_order` for the relevant consensus rule.
if block_spends.insert(*spend, utxo).is_some() {
if block_spends.insert(spend, utxo).is_some() {
return Err(DuplicateTransparentSpend {
outpoint: *spend,
outpoint: spend,
location: "the same block",
});
}
@ -115,10 +115,10 @@ fn transparent_spend_chain_order(
spend: transparent::OutPoint,
spend_tx_index_in_block: usize,
block_new_outputs: &HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
non_finalized_chain_unspent_utxos: &HashMap<transparent::OutPoint, transparent::Utxo>,
non_finalized_chain_unspent_utxos: &HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
non_finalized_chain_spent_utxos: &HashSet<transparent::OutPoint>,
finalized_state: &ZebraDb,
) -> Result<transparent::Utxo, ValidateContextError> {
) -> Result<transparent::OrderedUtxo, ValidateContextError> {
if let Some(output) = block_new_outputs.get(&spend) {
// reject the spend if it uses an output from this block,
// but the output was not created by an earlier transaction
@ -132,7 +132,7 @@ fn transparent_spend_chain_order(
return Err(EarlyTransparentSpend { outpoint: spend });
} else {
// a unique spend of a previous transaction's output is ok
return Ok(output.utxo.clone());
return Ok(output.clone());
}
}
@ -184,15 +184,16 @@ fn transparent_spend_chain_order(
pub fn transparent_coinbase_spend(
outpoint: transparent::OutPoint,
spend_restriction: transparent::CoinbaseSpendRestriction,
utxo: transparent::Utxo,
) -> Result<transparent::Utxo, ValidateContextError> {
if !utxo.from_coinbase {
utxo: transparent::OrderedUtxo,
) -> Result<transparent::OrderedUtxo, ValidateContextError> {
if !utxo.utxo.from_coinbase {
return Ok(utxo);
}
match spend_restriction {
OnlyShieldedOutputs { spend_height } => {
let min_spend_height = utxo.height + block::Height(MIN_TRANSPARENT_COINBASE_MATURITY);
let min_spend_height =
utxo.utxo.height + block::Height(MIN_TRANSPARENT_COINBASE_MATURITY);
let min_spend_height =
min_spend_height.expect("valid UTXOs have coinbase heights far below Height::MAX");
if spend_height >= min_spend_height {
@ -202,7 +203,7 @@ pub fn transparent_coinbase_spend(
outpoint,
spend_height,
min_spend_height,
created_height: utxo.height,
created_height: utxo.utxo.height,
})
}
}
@ -222,10 +223,9 @@ pub fn transparent_coinbase_spend(
/// MUST be nonnegative."
///
/// https://zips.z.cash/protocol/protocol.pdf#transactions
#[allow(dead_code)]
pub fn remaining_transaction_value(
prepared: &PreparedBlock,
utxos: &HashMap<transparent::OutPoint, transparent::Utxo>,
utxos: &HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
) -> Result<(), ValidateContextError> {
for (tx_index_in_block, transaction) in prepared.block.transactions.iter().enumerate() {
// TODO: check coinbase transaction remaining value (#338, #1162)
@ -234,7 +234,7 @@ pub fn remaining_transaction_value(
}
// Check the remaining transparent value pool for this transaction
let value_balance = transaction.value_balance(utxos);
let value_balance = transaction.value_balance(&utxos_from_ordered_utxos(utxos.clone()));
match value_balance {
Ok(vb) => match vb.remaining_transaction_value() {
Ok(_) => Ok(()),

View File

@ -37,6 +37,8 @@ mod arbitrary;
#[cfg(test)]
mod tests;
pub use disk_format::{OutputLocation, TransactionLocation};
pub(super) use zebra_db::ZebraDb;
/// The finalized part of the chain state, stored in the db.

View File

@ -16,6 +16,7 @@ pub mod transparent;
mod tests;
pub use block::{TransactionIndex, TransactionLocation};
pub use transparent::OutputLocation;
/// Helper type for writing types to disk as raw bytes.
/// Also used to convert key types to raw bytes for disk lookups.

View File

@ -233,6 +233,7 @@ impl ZebraDb {
lookup_out_loc(finalized.height, &outpoint, &tx_hash_indexes)
}),
self.utxo(&outpoint)
.map(|ordered_utxo| ordered_utxo.utxo)
.or_else(|| finalized.new_outputs.get(&outpoint).cloned())
.expect("already checked UTXO was in state or block"),
)

View File

@ -374,22 +374,25 @@ fn snapshot_block_and_transaction_data(state: &FinalizedState) {
// The genesis transaction's UTXO is not indexed.
// This check also ignores spent UTXOs.
if let Some(stored_utxo) = &stored_utxo_by_out_loc {
assert_eq!(&stored_utxo.output, output);
assert_eq!(stored_utxo.height, query_height);
assert_eq!(&stored_utxo.utxo.output, output);
assert_eq!(stored_utxo.utxo.height, query_height);
assert_eq!(
stored_utxo.from_coinbase,
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.from_coinbase,
from_coinbase = stored_utxo.utxo.from_coinbase,
);
}
stored_utxos.push((output_location, stored_utxo_by_out_loc));
stored_utxos.push((
output_location,
stored_utxo_by_out_loc.map(|ordered_utxo| ordered_utxo.utxo),
));
}
}
}

View File

@ -15,7 +15,7 @@ use std::collections::{BTreeMap, HashMap};
use zebra_chain::{
amount::{Amount, NonNegative},
transparent::{self, Utxo},
transparent,
};
use crate::{
@ -72,7 +72,7 @@ impl ZebraDb {
/// Returns the transparent output for a [`transparent::OutPoint`],
/// if it is unspent in the finalized state.
pub fn utxo(&self, outpoint: &transparent::OutPoint) -> Option<transparent::Utxo> {
pub fn utxo(&self, outpoint: &transparent::OutPoint) -> Option<transparent::OrderedUtxo> {
let output_location = self.output_location(outpoint)?;
self.utxo_by_location(output_location)
@ -80,16 +80,21 @@ impl ZebraDb {
/// Returns the transparent output for an [`OutputLocation`],
/// if it is unspent in the finalized state.
pub fn utxo_by_location(&self, output_location: OutputLocation) -> Option<transparent::Utxo> {
pub fn utxo_by_location(
&self,
output_location: OutputLocation,
) -> Option<transparent::OrderedUtxo> {
let utxo_by_out_loc = self.db.cf_handle("utxo_by_outpoint").unwrap();
let output = self.db.zs_get(&utxo_by_out_loc, &output_location)?;
Some(Utxo::from_location(
let utxo = transparent::OrderedUtxo::new(
output,
output_location.height(),
output_location.transaction_index().as_usize(),
))
);
Some(utxo)
}
}

View File

@ -275,12 +275,12 @@ impl NonFinalizedState {
self.chain_set.iter().rev().find(|chain| predicate(chain))
}
/// Returns the `transparent::Output` pointed to by the given
/// `transparent::OutPoint` if it is present in any chain.
/// Returns the [`transparent::Utxo`] pointed to by the given
/// [`transparent::OutPoint`] if it is present in any chain.
pub fn any_utxo(&self, outpoint: &transparent::OutPoint) -> Option<transparent::Utxo> {
for chain in self.chain_set.iter().rev() {
if let Some(utxo) = chain.created_utxos.get(outpoint) {
return Some(utxo.clone());
return Some(utxo.utxo.clone());
}
}

View File

@ -26,25 +26,36 @@ use zebra_chain::{
work::difficulty::PartialCumulativeWork,
};
use crate::{service::check, ContextuallyValidBlock, HashOrHeight, ValidateContextError};
use crate::{
service::check, ContextuallyValidBlock, HashOrHeight, TransactionLocation, ValidateContextError,
};
use self::index::TransparentTransfers;
pub mod index;
#[derive(Debug, Clone)]
pub struct Chain {
// The function `eq_internal_state` must be updated every time a field is added to `Chain`.
/// The configured network for this chain.
network: Network,
/// The contextually valid blocks which form this non-finalized partial chain, in height order.
pub(crate) blocks: BTreeMap<block::Height, ContextuallyValidBlock>,
/// An index of block heights for each block hash in `blocks`.
pub height_by_hash: HashMap<block::Hash, block::Height>,
/// An index of block heights and transaction indexes for each transaction hash in `blocks`.
pub tx_by_hash: HashMap<transaction::Hash, (block::Height, usize)>,
/// An index of [`TransactionLocation`]s for each transaction hash in `blocks`.
pub tx_by_hash: HashMap<transaction::Hash, TransactionLocation>,
/// The [`Utxo`]s created by `blocks`.
///
/// Note that these UTXOs may not be unspent.
/// Outputs can be spent by later transactions or blocks in the chain.
pub(crate) created_utxos: HashMap<transparent::OutPoint, transparent::Utxo>,
//
// TODO: replace OutPoint with OutputLocation?
pub(crate) created_utxos: HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
/// The [`OutPoint`]s spent by `blocks`,
/// including those created by earlier transactions or blocks in the chain.
pub(crate) spent_utxos: HashSet<transparent::OutPoint>,
@ -90,6 +101,9 @@ pub struct Chain {
/// The Orchard nullifiers revealed by `blocks`.
pub(super) orchard_nullifiers: HashSet<orchard::Nullifier>,
/// Partial transparent address index data from `blocks`.
pub(super) partial_transparent_transfers: HashMap<transparent::Address, TransparentTransfers>,
/// The cumulative work represented by `blocks`.
///
/// Since the best chain is determined by the largest cumulative work,
@ -138,6 +152,7 @@ impl Chain {
sprout_nullifiers: Default::default(),
sapling_nullifiers: Default::default(),
orchard_nullifiers: Default::default(),
partial_transparent_transfers: Default::default(),
partial_cumulative_work: Default::default(),
history_tree,
chain_value_pools: finalized_tip_chain_value_pools,
@ -191,6 +206,9 @@ impl Chain {
self.sapling_nullifiers == other.sapling_nullifiers &&
self.orchard_nullifiers == other.orchard_nullifiers &&
// transparent address indexes
self.partial_transparent_transfers == other.partial_transparent_transfers &&
// proof of work
self.partial_cumulative_work == other.partial_cumulative_work &&
@ -334,9 +352,35 @@ impl Chain {
&self,
hash: transaction::Hash,
) -> Option<(&Arc<Transaction>, block::Height)> {
self.tx_by_hash
.get(&hash)
.map(|(height, index)| (&self.blocks[height].block.transactions[*index], *height))
self.tx_by_hash.get(&hash).map(|tx_loc| {
(
&self.blocks[&tx_loc.height].block.transactions[tx_loc.index.as_usize()],
tx_loc.height,
)
})
}
/// Returns the [`Transaction`] at [`TransactionLocation`], if it exists in this chain.
#[allow(dead_code)]
pub fn transaction_by_loc(&self, tx_loc: TransactionLocation) -> Option<&Arc<Transaction>> {
self.blocks
.get(&tx_loc.height)?
.block
.transactions
.get(tx_loc.index.as_usize())
}
/// Returns the [`transaction::Hash`] for the transaction at [`TransactionLocation`],
/// if it exists in this chain.
#[allow(dead_code)]
pub fn transaction_hash_by_loc(
&self,
tx_loc: TransactionLocation,
) -> Option<&transaction::Hash> {
self.blocks
.get(&tx_loc.height)?
.transaction_hashes
.get(tx_loc.index.as_usize())
}
/// Returns the block hash of the tip block.
@ -421,9 +465,10 @@ impl Chain {
/// Callers should also check the finalized state for available UTXOs.
/// If UTXOs remain unspent when a block is finalized, they are stored in the finalized state,
/// and removed from the relevant chain(s).
pub fn unspent_utxos(&self) -> HashMap<transparent::OutPoint, transparent::Utxo> {
pub fn unspent_utxos(&self) -> HashMap<transparent::OutPoint, transparent::OrderedUtxo> {
let mut unspent_utxos = self.created_utxos.clone();
unspent_utxos.retain(|out_point, _utxo| !self.spent_utxos.contains(out_point));
unspent_utxos
}
@ -460,6 +505,7 @@ impl Chain {
sprout_nullifiers: self.sprout_nullifiers.clone(),
sapling_nullifiers: self.sapling_nullifiers.clone(),
orchard_nullifiers: self.orchard_nullifiers.clone(),
partial_transparent_transfers: self.partial_transparent_transfers.clone(),
partial_cumulative_work: self.partial_cumulative_work,
history_tree,
chain_value_pools: self.chain_value_pools,
@ -503,11 +549,20 @@ impl UpdateWith<ContextuallyValidBlock> for Chain {
&mut self,
contextually_valid: &ContextuallyValidBlock,
) -> Result<(), ValidateContextError> {
let (block, hash, height, new_outputs, transaction_hashes, chain_value_pool_change) = (
let (
block,
hash,
height,
new_outputs,
spent_outputs,
transaction_hashes,
chain_value_pool_change,
) = (
contextually_valid.block.as_ref(),
contextually_valid.hash,
contextually_valid.height,
&contextually_valid.new_outputs,
&contextually_valid.spent_outputs,
&contextually_valid.transaction_hashes,
&contextually_valid.chain_value_pool_change,
);
@ -536,6 +591,7 @@ impl UpdateWith<ContextuallyValidBlock> for Chain {
{
let (
inputs,
outputs,
joinsplit_data,
sapling_shielded_data_per_spend_anchor,
sapling_shielded_data_shared_anchor,
@ -543,17 +599,20 @@ impl UpdateWith<ContextuallyValidBlock> for Chain {
) = match transaction.deref() {
V4 {
inputs,
outputs,
joinsplit_data,
sapling_shielded_data,
..
} => (inputs, joinsplit_data, sapling_shielded_data, &None, &None),
} => (inputs, outputs, joinsplit_data, sapling_shielded_data, &None, &None),
V5 {
inputs,
outputs,
sapling_shielded_data,
orchard_shielded_data,
..
} => (
inputs,
outputs,
&None,
&None,
sapling_shielded_data,
@ -565,18 +624,19 @@ impl UpdateWith<ContextuallyValidBlock> for Chain {
};
// add key `transaction.hash` and value `(height, tx_index)` to `tx_by_hash`
let transaction_location = TransactionLocation::from_usize(height, transaction_index);
let prior_pair = self
.tx_by_hash
.insert(transaction_hash, (height, transaction_index));
assert!(
prior_pair.is_none(),
.insert(transaction_hash, transaction_location);
assert_eq!(
prior_pair, None,
"transactions must be unique within a single chain"
);
// add the utxos this produced
self.update_chain_tip_with(new_outputs)?;
// add the utxos this consumed
self.update_chain_tip_with(inputs)?;
// index the utxos this produced
self.update_chain_tip_with(&(outputs, &transaction_hash, new_outputs))?;
// index the utxos this consumed
self.update_chain_tip_with(&(inputs, &transaction_hash, spent_outputs))?;
// add the shielded data
self.update_chain_tip_with(joinsplit_data)?;
@ -626,11 +686,20 @@ impl UpdateWith<ContextuallyValidBlock> for Chain {
contextually_valid: &ContextuallyValidBlock,
position: RevertPosition,
) {
let (block, hash, height, new_outputs, transaction_hashes, chain_value_pool_change) = (
let (
block,
hash,
height,
new_outputs,
spent_outputs,
transaction_hashes,
chain_value_pool_change,
) = (
contextually_valid.block.as_ref(),
contextually_valid.hash,
contextually_valid.height,
&contextually_valid.new_outputs,
&contextually_valid.spent_outputs,
&contextually_valid.transaction_hashes,
&contextually_valid.chain_value_pool_change,
);
@ -660,6 +729,7 @@ impl UpdateWith<ContextuallyValidBlock> for Chain {
{
let (
inputs,
outputs,
joinsplit_data,
sapling_shielded_data_per_spend_anchor,
sapling_shielded_data_shared_anchor,
@ -667,17 +737,20 @@ impl UpdateWith<ContextuallyValidBlock> for Chain {
) = match transaction.deref() {
V4 {
inputs,
outputs,
joinsplit_data,
sapling_shielded_data,
..
} => (inputs, joinsplit_data, sapling_shielded_data, &None, &None),
} => (inputs, outputs, joinsplit_data, sapling_shielded_data, &None, &None),
V5 {
inputs,
outputs,
sapling_shielded_data,
orchard_shielded_data,
..
} => (
inputs,
outputs,
&None,
&None,
sapling_shielded_data,
@ -688,17 +761,19 @@ impl UpdateWith<ContextuallyValidBlock> for Chain {
),
};
// remove the utxos this produced
// uses `tx_by_hash`
self.revert_chain_with(&(outputs, transaction_hash, new_outputs), position);
// remove the utxos this consumed
// uses `tx_by_hash`
self.revert_chain_with(&(inputs, transaction_hash, spent_outputs), position);
// remove `transaction.hash` from `tx_by_hash`
assert!(
self.tx_by_hash.remove(transaction_hash).is_some(),
"transactions must be present if block was added to chain"
);
// remove the utxos this produced
self.revert_chain_with(new_outputs, position);
// remove the utxos this consumed
self.revert_chain_with(inputs, position);
// remove the shielded data
self.revert_chain_with(joinsplit_data, position);
self.revert_chain_with(sapling_shielded_data_per_spend_anchor, position);
@ -747,52 +822,226 @@ impl UpdateWith<ContextuallyValidBlock> for Chain {
}
}
impl UpdateWith<HashMap<transparent::OutPoint, transparent::Utxo>> for Chain {
// Created UTXOs
//
// TODO: replace arguments with a struct
impl
UpdateWith<(
// The outputs from a transaction in this block
&Vec<transparent::Output>,
// The hash of the transaction that the outputs are from
&transaction::Hash,
// The UTXOs for all outputs created by this transaction (or block)
&HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
)> for Chain
{
fn update_chain_tip_with(
&mut self,
utxos: &HashMap<transparent::OutPoint, transparent::Utxo>,
&(created_outputs, creating_tx_hash, block_created_outputs): &(
&Vec<transparent::Output>,
&transaction::Hash,
&HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
),
) -> Result<(), ValidateContextError> {
self.created_utxos
.extend(utxos.iter().map(|(k, v)| (*k, v.clone())));
for output_index in 0..created_outputs.len() {
let outpoint = transparent::OutPoint {
hash: *creating_tx_hash,
index: output_index.try_into().expect("valid indexes fit in u32"),
};
let created_utxo = block_created_outputs
.get(&outpoint)
.expect("new_outputs contains all created UTXOs");
// Update the chain's created UTXOs
let previous_entry = self.created_utxos.insert(outpoint, created_utxo.clone());
assert_eq!(
previous_entry, None,
"unexpected created output: duplicate update or duplicate UTXO",
);
// Update the address index with this UTXO
if let Some(receiving_address) = created_utxo.utxo.output.address(self.network) {
let address_transfers = self
.partial_transparent_transfers
.entry(receiving_address)
.or_default();
let transaction_location = self.tx_by_hash.get(&outpoint.hash).expect(
"unexpected missing transaction hash: transaction must already be indexed",
);
address_transfers.update_chain_tip_with(&(
&outpoint,
created_utxo,
transaction_location,
))?;
}
}
Ok(())
}
fn revert_chain_with(
&mut self,
utxos: &HashMap<transparent::OutPoint, transparent::Utxo>,
_position: RevertPosition,
&(created_outputs, creating_tx_hash, block_created_outputs): &(
&Vec<transparent::Output>,
&transaction::Hash,
&HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
),
position: RevertPosition,
) {
self.created_utxos
.retain(|outpoint, _| !utxos.contains_key(outpoint));
for output_index in 0..created_outputs.len() {
let outpoint = transparent::OutPoint {
hash: *creating_tx_hash,
index: output_index.try_into().expect("valid indexes fit in u32"),
};
let created_utxo = block_created_outputs
.get(&outpoint)
.expect("new_outputs contains all created UTXOs");
// Revert the chain's created UTXOs
let removed_entry = self.created_utxos.remove(&outpoint);
assert!(
removed_entry.is_some(),
"unexpected revert of created output: duplicate revert or duplicate UTXO",
);
// Revert the address index for this UTXO
if let Some(receiving_address) = created_utxo.utxo.output.address(self.network) {
let address_transfers = self
.partial_transparent_transfers
.get_mut(&receiving_address)
.expect("block has previously been applied to the chain");
let transaction_location = self
.tx_by_hash
.get(&outpoint.hash)
.expect("transaction is reverted after its UTXOs are reverted");
address_transfers
.revert_chain_with(&(&outpoint, created_utxo, transaction_location), position);
// Remove this transfer if it is now empty
if address_transfers.is_empty() {
self.partial_transparent_transfers
.remove(&receiving_address);
}
}
}
}
}
impl UpdateWith<Vec<transparent::Input>> for Chain {
// Transparent inputs
//
// TODO: replace arguments with a struct
impl
UpdateWith<(
// The inputs from a transaction in this block
&Vec<transparent::Input>,
// The hash of the transaction that the inputs are from
&transaction::Hash,
// The outputs for all inputs spent in this transaction (or block)
&HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
)> for Chain
{
fn update_chain_tip_with(
&mut self,
inputs: &Vec<transparent::Input>,
&(spending_inputs, spending_tx_hash, spent_outputs): &(
&Vec<transparent::Input>,
&transaction::Hash,
&HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
),
) -> Result<(), ValidateContextError> {
for consumed_utxo in inputs {
match consumed_utxo {
transparent::Input::PrevOut { outpoint, .. } => {
self.spent_utxos.insert(*outpoint);
}
transparent::Input::Coinbase { .. } => {}
for spending_input in spending_inputs.iter() {
let spent_outpoint = if let Some(spent_outpoint) = spending_input.outpoint() {
spent_outpoint
} else {
continue;
};
// Index the spent outpoint in the chain
let first_spend = self.spent_utxos.insert(spent_outpoint);
assert!(
first_spend,
"unexpected duplicate spent output: should be checked earlier"
);
// TODO: fix tests to supply correct spent outputs, then turn this into an expect()
let spent_output = if let Some(spent_output) = spent_outputs.get(&spent_outpoint) {
spent_output
} else if !cfg!(test) {
panic!("unexpected missing spent output: all spent outputs must be indexed");
} else {
continue;
};
// Index the spent output for the address
if let Some(spending_address) = spent_output.utxo.output.address(self.network) {
let address_transfers = self
.partial_transparent_transfers
.entry(spending_address)
.or_default();
address_transfers.update_chain_tip_with(&(
spending_input,
spending_tx_hash,
spent_output,
))?;
}
}
Ok(())
}
fn revert_chain_with(&mut self, inputs: &Vec<transparent::Input>, _position: RevertPosition) {
for consumed_utxo in inputs {
match consumed_utxo {
transparent::Input::PrevOut { outpoint, .. } => {
assert!(
self.spent_utxos.remove(outpoint),
"spent_utxos must be present if block was added to chain"
);
fn revert_chain_with(
&mut self,
&(spending_inputs, spending_tx_hash, spent_outputs): &(
&Vec<transparent::Input>,
&transaction::Hash,
&HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
),
position: RevertPosition,
) {
for spending_input in spending_inputs.iter() {
let spent_outpoint = if let Some(spent_outpoint) = spending_input.outpoint() {
spent_outpoint
} else {
continue;
};
// Revert the spent outpoint in the chain
let spent_outpoint_was_removed = self.spent_utxos.remove(&spent_outpoint);
assert!(
spent_outpoint_was_removed,
"spent_utxos must be present if block was added to chain"
);
// TODO: fix tests to supply correct spent outputs, then turn this into an expect()
let spent_output = if let Some(spent_output) = spent_outputs.get(&spent_outpoint) {
spent_output
} else if !cfg!(test) {
panic!(
"unexpected missing reverted spent output: all spent outputs must be indexed"
);
} else {
continue;
};
// Revert the spent output for the address
if let Some(receiving_address) = spent_output.utxo.output.address(self.network) {
let address_transfers = self
.partial_transparent_transfers
.get_mut(&receiving_address)
.expect("block has previously been applied to the chain");
address_transfers
.revert_chain_with(&(spending_input, spending_tx_hash, spent_output), position);
// Remove this transfer if it is now empty
if address_transfers.is_empty() {
self.partial_transparent_transfers
.remove(&receiving_address);
}
transparent::Input::Coinbase { .. } => {}
}
}
}

View File

@ -0,0 +1,290 @@
//! Transparent address indexes for non-finalized chains.
use std::collections::{BTreeMap, BTreeSet, HashMap};
use mset::MultiSet;
use zebra_chain::{
amount::{Amount, NegativeAllowed},
block::Height,
transaction, transparent,
};
use crate::{
request::ContextuallyValidBlock, OutputLocation, TransactionLocation, ValidateContextError,
};
use super::{RevertPosition, UpdateWith};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TransparentTransfers {
/// The partial chain balance for a transparent address.
///
/// TODO:
/// - to avoid [`ReadStateService`] response inconsistencies when a block has just been finalized,
/// revert UTXO receives and spends that are at a height less than or equal to the finalized tip.
balance: Amount<NegativeAllowed>,
/// The partial list of transactions that spent or received UTXOs to a transparent address.
///
/// Since transactions can only be added to this set, it does not need special handling
/// for [`ReadStateService`] response inconsistencies.
///
/// The `getaddresstxids` RPC needs these transaction IDs to be sorted in chain order.
tx_ids: MultiSet<transaction::Hash>,
/// The partial list of UTXOs received by a transparent address.
///
/// The `getaddressutxos` RPC doesn't need these transaction IDs to be sorted in chain order,
/// but it might in future. So Zebra does it anyway.
///
/// TODO:
/// - to avoid [`ReadStateService`] response inconsistencies when a block has just been finalized,
/// combine the created UTXOs, combine the spent UTXOs, and then remove spent from created
///
/// Optional:
/// - store `Utxo`s in the chain, and just store the created locations for this address
/// - if we add an OutputLocation to UTXO, remove this OutputLocation,
/// and use the inner OutputLocation to sort Utxos in chain order
created_utxos: BTreeMap<OutputLocation, transparent::Output>,
/// The partial list of UTXOs spent by a transparent address.
///
/// The `getaddressutxos` RPC doesn't need these transaction IDs to be sorted in chain order,
/// but it might in future. So Zebra does it anyway.
///
/// Optional TODO:
/// - store spent `Utxo`s by location in the chain, use the chain spent UTXOs to filter,
/// and stop storing spent UTXOs by address
spent_utxos: BTreeSet<OutputLocation>,
}
// A created UTXO
//
// TODO: replace arguments with a struct
impl
UpdateWith<(
// The location of the UTXO
&transparent::OutPoint,
// The UTXO data
&transparent::OrderedUtxo,
// The location of the transaction that creates the UTXO
&TransactionLocation,
)> for TransparentTransfers
{
fn update_chain_tip_with(
&mut self,
&(outpoint, created_utxo, transaction_location): &(
&transparent::OutPoint,
&transparent::OrderedUtxo,
&TransactionLocation,
),
) -> Result<(), ValidateContextError> {
self.balance =
(self.balance + created_utxo.utxo.output.value().constrain().unwrap()).unwrap();
let output_location = OutputLocation::from_outpoint(*transaction_location, outpoint);
let previous_entry = self
.created_utxos
.insert(output_location, created_utxo.utxo.output.clone());
assert_eq!(
previous_entry, None,
"unexpected created output: duplicate update or duplicate UTXO",
);
self.tx_ids.insert(outpoint.hash);
Ok(())
}
fn revert_chain_with(
&mut self,
&(outpoint, created_utxo, transaction_location): &(
&transparent::OutPoint,
&transparent::OrderedUtxo,
&TransactionLocation,
),
_position: RevertPosition,
) {
self.balance =
(self.balance - created_utxo.utxo.output.value().constrain().unwrap()).unwrap();
let output_location = OutputLocation::from_outpoint(*transaction_location, outpoint);
let removed_entry = self.created_utxos.remove(&output_location);
assert!(
removed_entry.is_some(),
"unexpected revert of created output: duplicate update or duplicate UTXO",
);
let tx_id_was_removed = self.tx_ids.remove(&outpoint.hash);
assert!(
tx_id_was_removed,
"unexpected revert of created output transaction: \
duplicate revert, or revert of an output that was never updated",
);
}
}
// A transparent input
//
// TODO: replace arguments with a struct
impl
UpdateWith<(
// The transparent input data
&transparent::Input,
// The hash of the transaction the input is from
&transaction::Hash,
// The output spent by the input
// Includes the location of the transaction that created the output
&transparent::OrderedUtxo,
)> for TransparentTransfers
{
fn update_chain_tip_with(
&mut self,
&(spending_input, spending_tx_hash, spent_output): &(
&transparent::Input,
&transaction::Hash,
&transparent::OrderedUtxo,
),
) -> Result<(), ValidateContextError> {
// Spending a UTXO subtracts value from the balance
self.balance =
(self.balance - spent_output.utxo.output.value().constrain().unwrap()).unwrap();
let spent_outpoint = spending_input.outpoint().expect("checked by caller");
let spent_output_tx_loc = transaction_location(spent_output);
let output_location = OutputLocation::from_outpoint(spent_output_tx_loc, &spent_outpoint);
let spend_was_inserted = self.spent_utxos.insert(output_location);
assert!(
spend_was_inserted,
"unexpected spent output: duplicate update or duplicate spend",
);
self.tx_ids.insert(*spending_tx_hash);
Ok(())
}
fn revert_chain_with(
&mut self,
&(spending_input, spending_tx_hash, spent_output): &(
&transparent::Input,
&transaction::Hash,
&transparent::OrderedUtxo,
),
_position: RevertPosition,
) {
self.balance =
(self.balance + spent_output.utxo.output.value().constrain().unwrap()).unwrap();
let spent_outpoint = spending_input.outpoint().expect("checked by caller");
let spent_output_tx_loc = transaction_location(spent_output);
let output_location = OutputLocation::from_outpoint(spent_output_tx_loc, &spent_outpoint);
let spend_was_removed = self.spent_utxos.remove(&output_location);
assert!(
spend_was_removed,
"unexpected revert of spent output: \
duplicate revert, or revert of a spent output that was never updated",
);
let tx_id_was_removed = self.tx_ids.remove(spending_tx_hash);
assert!(
tx_id_was_removed,
"unexpected revert of spending input transaction: \
duplicate revert, or revert of an input that was never updated",
);
}
}
impl TransparentTransfers {
/// Returns true if there are no transfers for this address.
pub fn is_empty(&self) -> bool {
self.balance == Amount::<NegativeAllowed>::zero()
&& self.tx_ids.is_empty()
&& self.created_utxos.is_empty()
&& self.spent_utxos.is_empty()
}
/// Returns the partial balance for this address.
#[allow(dead_code)]
pub fn balance(&self) -> Amount<NegativeAllowed> {
self.balance
}
/// Returns the [`transaction::Hash`]es of the transactions that
/// sent or received transparent tranfers to this address,
/// in this partial chain, in chain order.
///
/// `chain_tx_by_hash` should be the `tx_by_hash` field from the [`Chain`] containing this index.
///
/// # Panics
///
/// If `chain_tx_by_hash` is missing some transaction hashes from this index.
#[allow(dead_code)]
pub fn tx_ids(
&self,
chain_tx_by_hash: &HashMap<transaction::Hash, TransactionLocation>,
) -> BTreeMap<TransactionLocation, transaction::Hash> {
self.tx_ids
.distinct_elements()
.map(|tx_hash| {
(
*chain_tx_by_hash
.get(tx_hash)
.expect("all hashes are indexed"),
*tx_hash,
)
})
.collect()
}
/// Returns the unspent transparent outputs sent to this address,
/// in this partial chain, in chain order.
///
/// `chain_blocks` should be the `blocks` field from the [`Chain`] containing this index.
///
/// # Panics
///
/// If `chain_blocks` is missing some transaction hashes from this index.
#[allow(dead_code)]
pub fn created_utxos(
&self,
chain_blocks: &BTreeMap<Height, ContextuallyValidBlock>,
) -> BTreeMap<OutputLocation, (transparent::Output, transaction::Hash)> {
self.created_utxos
.iter()
.map(|(output_location, output)| {
let tx_loc = output_location.transaction_location();
let transaction_hash =
chain_blocks[&tx_loc.height].transaction_hashes[tx_loc.index.as_usize()];
(*output_location, (output.clone(), transaction_hash))
})
.collect()
}
/// Returns the [`OutputLocation`]s of the spent transparent outputs sent to this address,
/// in this partial chain, in chain order.
#[allow(dead_code)]
pub fn spent_utxos(&self) -> &BTreeSet<OutputLocation> {
&self.spent_utxos
}
}
impl Default for TransparentTransfers {
fn default() -> Self {
Self {
balance: Amount::zero(),
tx_ids: Default::default(),
created_utxos: Default::default(),
spent_utxos: Default::default(),
}
}
}
/// Returns the transaction location for an [`OrderedUtxo`].
pub fn transaction_location(ordered_utxo: &transparent::OrderedUtxo) -> TransactionLocation {
TransactionLocation::from_usize(ordered_utxo.utxo.height, ordered_utxo.tx_index_in_block)
}

View File

@ -1,3 +1,5 @@
//! Initialization and arbitrary data generation functions for zebra-state.
use std::sync::Arc;
use proptest::prelude::*;