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:
parent
b7f6fdc222
commit
e49c1d7034
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"))]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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(()),
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 { .. } => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
//! Initialization and arbitrary data generation functions for zebra-state.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use proptest::prelude::*;
|
||||
|
|
|
|||
Loading…
Reference in New Issue