diff --git a/Cargo.lock b/Cargo.lock index 398349c1..7c240829 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3263,6 +3263,7 @@ dependencies = [ "once_cell", "primitive-types", "proptest", + "proptest-derive", "rocksdb", "serde", "spandoc", diff --git a/zebra-consensus/src/block.rs b/zebra-consensus/src/block.rs index 564ae804..c86c3983 100644 --- a/zebra-consensus/src/block.rs +++ b/zebra-consensus/src/block.rs @@ -167,7 +167,7 @@ where let mut async_checks = FuturesUnordered::new(); - let known_utxos = known_utxos(&block); + let known_utxos = new_outputs(&block); for transaction in &block.transactions { let rsp = transaction_verifier .ready_and() @@ -221,15 +221,24 @@ where } } -fn known_utxos(block: &Block) -> Arc> { - let mut map = HashMap::default(); +fn new_outputs(block: &Block) -> Arc> { + let mut new_outputs = HashMap::default(); + let height = block.coinbase_height().expect("block has coinbase height"); for transaction in &block.transactions { let hash = transaction.hash(); + let from_coinbase = transaction.is_coinbase(); for (index, output) in transaction.outputs().iter().cloned().enumerate() { let index = index as u32; - map.insert(transparent::OutPoint { hash, index }, output); + new_outputs.insert( + transparent::OutPoint { hash, index }, + zs::Utxo { + output, + height, + from_coinbase, + }, + ); } } - Arc::new(map) + Arc::new(new_outputs) } diff --git a/zebra-consensus/src/script.rs b/zebra-consensus/src/script.rs index 36b986a1..9dc4a4d1 100644 --- a/zebra-consensus/src/script.rs +++ b/zebra-consensus/src/script.rs @@ -3,6 +3,7 @@ use std::{collections::HashMap, future::Future, pin::Pin, sync::Arc}; use tracing::Instrument; use zebra_chain::{parameters::ConsensusBranchId, transaction::Transaction, transparent}; +use zebra_state::Utxo; use crate::BoxError; @@ -40,7 +41,7 @@ impl Verifier { pub struct Request { pub transaction: Arc, pub input_index: usize, - pub known_utxos: Arc>, + pub known_utxos: Arc>, } impl tower::Service for Verifier @@ -81,17 +82,21 @@ where async move { tracing::trace!("awaiting outpoint lookup"); - let output = if let Some(output) = known_utxos.get(&outpoint) { + let utxo = if let Some(output) = known_utxos.get(&outpoint) { tracing::trace!("UXTO in known_utxos, discarding query"); output.clone() - } else if let zebra_state::Response::Utxo(output) = query.await? { - output + } else if let zebra_state::Response::Utxo(utxo) = query.await? { + utxo } else { unreachable!("AwaitUtxo always responds with Utxo") }; - tracing::trace!(?output, "got UTXO"); + tracing::trace!(?utxo, "got UTXO"); - zebra_script::is_valid(transaction, branch_id, (input_index as u32, output))?; + zebra_script::is_valid( + transaction, + branch_id, + (input_index as u32, utxo.output), + )?; tracing::trace!("script verification succeeded"); Ok(()) diff --git a/zebra-consensus/src/transaction.rs b/zebra-consensus/src/transaction.rs index 313ff3bc..894e6450 100644 --- a/zebra-consensus/src/transaction.rs +++ b/zebra-consensus/src/transaction.rs @@ -56,13 +56,13 @@ pub enum Request { Block { transaction: Arc, /// Additional UTXOs which are known at the time of verification. - known_utxos: Arc>, + known_utxos: Arc>, }, /// Verify the supplied transaction as part of the mempool. Mempool { transaction: Arc, /// Additional UTXOs which are known at the time of verification. - known_utxos: Arc>, + known_utxos: Arc>, }, } diff --git a/zebra-state/Cargo.toml b/zebra-state/Cargo.toml index fb6afc68..5f1e5da3 100644 --- a/zebra-state/Cargo.toml +++ b/zebra-state/Cargo.toml @@ -36,4 +36,5 @@ spandoc = "0.2" tempdir = "0.3.7" tokio = { version = "0.3", features = ["full"] } proptest = "0.10.1" +proptest-derive = "0.2" primitive-types = "0.7.3" diff --git a/zebra-state/src/constants.rs b/zebra-state/src/constants.rs index 29e0d024..42d9b648 100644 --- a/zebra-state/src/constants.rs +++ b/zebra-state/src/constants.rs @@ -11,4 +11,5 @@ pub const MIN_TRASPARENT_COINBASE_MATURITY: u32 = 100; /// coinbase transactions. pub const MAX_BLOCK_REORG_HEIGHT: u32 = MIN_TRASPARENT_COINBASE_MATURITY - 1; -pub const DATABASE_FORMAT_VERSION: u32 = 2; +/// The database format version, incremented each time the database format changes. +pub const DATABASE_FORMAT_VERSION: u32 = 3; diff --git a/zebra-state/src/lib.rs b/zebra-state/src/lib.rs index f97a82c2..2558a62d 100644 --- a/zebra-state/src/lib.rs +++ b/zebra-state/src/lib.rs @@ -15,6 +15,7 @@ mod request; mod response; mod service; mod util; +mod utxo; // TODO: move these to integration tests. #[cfg(test)] @@ -26,3 +27,4 @@ pub use error::{BoxError, CloneError, CommitBlockError, ValidateContextError}; pub use request::{FinalizedBlock, HashOrHeight, PreparedBlock, Request}; pub use response::Response; pub use service::init; +pub use utxo::Utxo; diff --git a/zebra-state/src/request.rs b/zebra-state/src/request.rs index 7c78112a..8f9fa397 100644 --- a/zebra-state/src/request.rs +++ b/zebra-state/src/request.rs @@ -5,6 +5,8 @@ use zebra_chain::{ transaction, transparent, }; +use crate::Utxo; + // Allow *only* this unused import, so that rustdoc link resolution // will work with inline links. #[allow(unused_imports)] @@ -71,7 +73,7 @@ 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. - pub new_outputs: HashMap, + pub new_outputs: HashMap, // TODO: add these parameters when we can compute anchors. // sprout_anchor: sprout::tree::Root, // sapling_anchor: sapling::tree::Root, @@ -88,6 +90,13 @@ pub struct FinalizedBlock { pub(crate) block: Arc, pub(crate) hash: block::Hash, pub(crate) height: block::Height, + /// 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. + pub(crate) new_outputs: HashMap, } // Doing precomputation in this From impl means that it will be done in @@ -100,10 +109,45 @@ impl From> for FinalizedBlock { .expect("finalized blocks must have a valid coinbase height"); let hash = block.hash(); + let mut new_outputs = HashMap::default(); + for transaction in &block.transactions { + let hash = transaction.hash(); + let from_coinbase = transaction.is_coinbase(); + for (index, output) in transaction.outputs().iter().cloned().enumerate() { + let index = index as u32; + new_outputs.insert( + transparent::OutPoint { hash, index }, + Utxo { + output, + height, + from_coinbase, + }, + ); + } + } + Self { block, height, hash, + new_outputs, + } + } +} + +impl From for FinalizedBlock { + fn from(prepared: PreparedBlock) -> Self { + let PreparedBlock { + block, + height, + hash, + new_outputs, + } = prepared; + Self { + block, + height, + hash, + new_outputs, } } } diff --git a/zebra-state/src/response.rs b/zebra-state/src/response.rs index b171bfe6..fd6df1af 100644 --- a/zebra-state/src/response.rs +++ b/zebra-state/src/response.rs @@ -2,9 +2,10 @@ use std::sync::Arc; use zebra_chain::{ block::{self, Block}, transaction::Transaction, - transparent, }; +use crate::Utxo; + // Allow *only* this unused import, so that rustdoc link resolution // will work with inline links. #[allow(unused_imports)] @@ -33,5 +34,5 @@ pub enum Response { Block(Option>), /// The response to a `AwaitUtxo` request - Utxo(transparent::Output), + Utxo(Utxo), } diff --git a/zebra-state/src/service.rs b/zebra-state/src/service.rs index 35f09eb7..79caea6b 100644 --- a/zebra-state/src/service.rs +++ b/zebra-state/src/service.rs @@ -21,7 +21,7 @@ use zebra_chain::{ use crate::{ request::HashOrHeight, BoxError, CommitBlockError, Config, FinalizedBlock, PreparedBlock, - Request, Response, ValidateContextError, + Request, Response, Utxo, ValidateContextError, }; use self::finalized_state::FinalizedState; @@ -251,12 +251,12 @@ impl StateService { .or_else(|| self.disk.height(hash)) } - /// Return the utxo pointed to by `outpoint` if it exists in any chain. - pub fn utxo(&self, outpoint: &transparent::OutPoint) -> Option { + /// Return the [`Utxo`] pointed to by `outpoint` if it exists in any chain. + pub fn utxo(&self, outpoint: &transparent::OutPoint) -> Option { self.mem .utxo(outpoint) - .or_else(|| self.disk.utxo(outpoint)) .or_else(|| self.queued_blocks.utxo(outpoint)) + .or_else(|| self.disk.utxo(outpoint)) } /// Return an iterator over the relevant chain of the block identified by @@ -404,7 +404,7 @@ impl Service for StateService { let (rsp_tx, rsp_rx) = oneshot::channel(); - self.pending_utxos.scan_block(&finalized.block); + self.pending_utxos.check_against(&finalized.new_outputs); self.disk.queue_and_commit_finalized((finalized, rsp_tx)); async move { diff --git a/zebra-state/src/service/finalized_state.rs b/zebra-state/src/service/finalized_state.rs index e801b5a3..73298244 100644 --- a/zebra-state/src/service/finalized_state.rs +++ b/zebra-state/src/service/finalized_state.rs @@ -11,7 +11,7 @@ use zebra_chain::{ transaction::{self, Transaction}, }; -use crate::{BoxError, Config, FinalizedBlock, HashOrHeight}; +use crate::{BoxError, Config, FinalizedBlock, HashOrHeight, Utxo}; use self::disk_format::{DiskDeserialize, DiskSerialize, FromDisk, IntoDisk, TransactionLocation}; @@ -149,6 +149,7 @@ impl FinalizedState { block, hash, height, + new_outputs, } = finalized; let hash_by_height = self.db.cf_handle("hash_by_height").unwrap(); @@ -205,7 +206,13 @@ impl FinalizedState { return batch; } - // Index each transaction + // Index all new transparent outputs + for (outpoint, utxo) in new_outputs.into_iter() { + batch.zs_insert(utxo_by_outpoint, outpoint, utxo); + } + + // Index each transaction, spent inputs, nullifiers + // TODO: move computation into FinalizedBlock as with transparent outputs for (transaction_index, transaction) in block.transactions.iter().enumerate() { let transaction_hash = transaction.hash(); let transaction_location = TransactionLocation { @@ -228,15 +235,6 @@ impl FinalizedState { } } - // Index all new transparent outputs - for (index, output) in transaction.outputs().iter().enumerate() { - let outpoint = transparent::OutPoint { - hash: transaction_hash, - index: index as _, - }; - batch.zs_insert(utxo_by_outpoint, outpoint, output); - } - // Mark sprout and sapling nullifiers as spent for sprout_nullifier in transaction.sprout_nullifiers() { batch.zs_insert(sprout_nullifiers, sprout_nullifier, ()); @@ -305,7 +303,7 @@ impl FinalizedState { /// Returns the `transparent::Output` pointed to by the given /// `transparent::OutPoint` if it is present. - pub fn utxo(&self, outpoint: &transparent::OutPoint) -> Option { + pub fn utxo(&self, outpoint: &transparent::OutPoint) -> Option { let utxo_by_outpoint = self.db.cf_handle("utxo_by_outpoint").unwrap(); self.db.zs_get(utxo_by_outpoint, outpoint) } diff --git a/zebra-state/src/service/finalized_state/disk_format.rs b/zebra-state/src/service/finalized_state/disk_format.rs index f3d150f8..92ec14ab 100644 --- a/zebra-state/src/service/finalized_state/disk_format.rs +++ b/zebra-state/src/service/finalized_state/disk_format.rs @@ -5,10 +5,12 @@ use zebra_chain::{ block, block::Block, sapling, - serialization::{ZcashDeserialize, ZcashSerialize}, + serialization::{ZcashDeserialize, ZcashDeserializeInto, ZcashSerialize}, sprout, transaction, transparent, }; +use crate::Utxo; + #[derive(Debug, Clone, Copy, PartialEq)] pub struct TransactionLocation { pub height: block::Height, @@ -184,19 +186,33 @@ impl FromDisk for block::Height { } } -impl IntoDisk for transparent::Output { +impl IntoDisk for Utxo { type Bytes = Vec; fn as_bytes(&self) -> Self::Bytes { - self.zcash_serialize_to_vec() - .expect("serialization to vec doesn't fail") + let mut bytes = vec![0; 5]; + bytes[0..4].copy_from_slice(&self.height.0.to_be_bytes()); + bytes[4] = self.from_coinbase as u8; + self.output + .zcash_serialize(&mut bytes) + .expect("serialization to vec doesn't fail"); + bytes } } -impl FromDisk for transparent::Output { +impl FromDisk for Utxo { fn from_bytes(bytes: impl AsRef<[u8]>) -> Self { - Self::zcash_deserialize(bytes.as_ref()) - .expect("deserialization format should match the serialization format used by IntoDisk") + let (meta_bytes, output_bytes) = bytes.as_ref().split_at(5); + let height = block::Height(u32::from_be_bytes(meta_bytes[0..4].try_into().unwrap())); + let from_coinbase = meta_bytes[4] == 1u8; + let output = output_bytes + .zcash_deserialize_into() + .expect("db has serialized data"); + Self { + output, + height, + from_coinbase, + } } } @@ -370,6 +386,6 @@ mod tests { fn roundtrip_transparent_output() { zebra_test::init(); - proptest!(|(val in any::())| assert_value_properties(val)); + proptest!(|(val in any::())| assert_value_properties(val)); } } diff --git a/zebra-state/src/service/non_finalized_state.rs b/zebra-state/src/service/non_finalized_state.rs index bf849cf8..5b1fdc75 100644 --- a/zebra-state/src/service/non_finalized_state.rs +++ b/zebra-state/src/service/non_finalized_state.rs @@ -15,7 +15,7 @@ use zebra_chain::{ transparent, }; -use crate::{FinalizedBlock, HashOrHeight, PreparedBlock}; +use crate::{FinalizedBlock, HashOrHeight, PreparedBlock, Utxo}; use self::chain::Chain; @@ -64,12 +64,7 @@ impl NonFinalizedState { self.update_metrics_for_chains(); - // Construct a finalized block. - FinalizedBlock { - block: finalizing.block, - hash: finalizing.hash, - height: finalizing.height, - } + finalizing.into() } /// Commit block to the non-finalized state. @@ -148,7 +143,7 @@ impl NonFinalizedState { /// Returns the `transparent::Output` pointed to by the given /// `transparent::OutPoint` if it is present. - pub fn utxo(&self, outpoint: &transparent::OutPoint) -> Option { + pub fn utxo(&self, outpoint: &transparent::OutPoint) -> Option { for chain in self.chain_set.iter().rev() { if let Some(output) = chain.created_utxos.get(outpoint) { return Some(output.clone()); diff --git a/zebra-state/src/service/non_finalized_state/chain.rs b/zebra-state/src/service/non_finalized_state/chain.rs index 583f4320..3dc3ec0c 100644 --- a/zebra-state/src/service/non_finalized_state/chain.rs +++ b/zebra-state/src/service/non_finalized_state/chain.rs @@ -10,7 +10,7 @@ use zebra_chain::{ work::difficulty::PartialCumulativeWork, }; -use crate::PreparedBlock; +use crate::{PreparedBlock, Utxo}; #[derive(Default, Clone)] pub struct Chain { @@ -18,7 +18,7 @@ pub struct Chain { pub height_by_hash: HashMap, pub tx_by_hash: HashMap, - pub created_utxos: HashMap, + pub created_utxos: HashMap, spent_utxos: HashSet, sprout_anchors: HashSet, sapling_anchors: HashSet, @@ -155,14 +155,13 @@ impl UpdateWith for Chain { // for each transaction in block for (transaction_index, transaction) in block.transactions.iter().enumerate() { - let (inputs, outputs, shielded_data, joinsplit_data) = match transaction.deref() { + let (inputs, shielded_data, joinsplit_data) = match transaction.deref() { transaction::Transaction::V4 { inputs, - outputs, shielded_data, joinsplit_data, .. - } => (inputs, outputs, shielded_data, joinsplit_data), + } => (inputs, shielded_data, joinsplit_data), _ => unreachable!( "older transaction versions only exist in finalized blocks pre sapling", ), @@ -179,7 +178,7 @@ impl UpdateWith for Chain { ); // add the utxos this produced - self.update_chain_state_with(&(transaction_hash, outputs)); + self.update_chain_state_with(&prepared.new_outputs); // add the utxos this consumed self.update_chain_state_with(inputs); // add sprout anchor and nullifiers @@ -209,14 +208,13 @@ impl UpdateWith for Chain { // for each transaction in block for transaction in &block.transactions { - let (inputs, outputs, shielded_data, joinsplit_data) = match transaction.deref() { + let (inputs, shielded_data, joinsplit_data) = match transaction.deref() { transaction::Transaction::V4 { inputs, - outputs, shielded_data, joinsplit_data, .. - } => (inputs, outputs, shielded_data, joinsplit_data), + } => (inputs, shielded_data, joinsplit_data), _ => unreachable!( "older transaction versions only exist in finalized blocks pre sapling", ), @@ -230,7 +228,7 @@ impl UpdateWith for Chain { ); // remove the utxos this produced - self.revert_chain_state_with(&(transaction_hash, outputs)); + self.revert_chain_state_with(&prepared.new_outputs); // remove the utxos this consumed self.revert_chain_state_with(inputs); // remove sprout anchor and nullifiers @@ -241,37 +239,15 @@ impl UpdateWith for Chain { } } -impl UpdateWith<(transaction::Hash, &Vec)> for Chain { - fn update_chain_state_with( - &mut self, - (transaction_hash, outputs): &(transaction::Hash, &Vec), - ) { - for (utxo_index, output) in outputs.iter().enumerate() { - self.created_utxos.insert( - transparent::OutPoint { - hash: *transaction_hash, - index: utxo_index as u32, - }, - output.clone(), - ); - } +impl UpdateWith> for Chain { + fn update_chain_state_with(&mut self, utxos: &HashMap) { + self.created_utxos + .extend(utxos.iter().map(|(k, v)| (*k, v.clone()))); } - fn revert_chain_state_with( - &mut self, - (transaction_hash, outputs): &(transaction::Hash, &Vec), - ) { - for (utxo_index, _) in outputs.iter().enumerate() { - assert!( - self.created_utxos - .remove(&transparent::OutPoint { - hash: *transaction_hash, - index: utxo_index as u32, - }) - .is_some(), - "created_utxos must be present if block was" - ); - } + fn revert_chain_state_with(&mut self, utxos: &HashMap) { + self.created_utxos + .retain(|outpoint, _| !utxos.contains_key(outpoint)); } } diff --git a/zebra-state/src/service/non_finalized_state/queued_blocks.rs b/zebra-state/src/service/non_finalized_state/queued_blocks.rs index f8007e57..8e0f4090 100644 --- a/zebra-state/src/service/non_finalized_state/queued_blocks.rs +++ b/zebra-state/src/service/non_finalized_state/queued_blocks.rs @@ -6,7 +6,7 @@ use std::{ use tracing::instrument; use zebra_chain::{block, transparent}; -use crate::service::QueuedBlock; +use crate::{service::QueuedBlock, Utxo}; /// A queue of blocks, awaiting the arrival of parent blocks. #[derive(Default)] @@ -18,7 +18,7 @@ pub struct QueuedBlocks { /// Hashes from `queued_blocks`, indexed by block height. by_height: BTreeMap>, /// Known UTXOs. - known_utxos: HashMap, + known_utxos: HashMap, } impl QueuedBlocks { @@ -150,7 +150,7 @@ impl QueuedBlocks { } /// Try to look up this UTXO in any queued block. - pub fn utxo(&self, outpoint: &transparent::OutPoint) -> Option { + pub fn utxo(&self, outpoint: &transparent::OutPoint) -> Option { self.known_utxos.get(outpoint).cloned() } } diff --git a/zebra-state/src/service/tests.rs b/zebra-state/src/service/tests.rs index 3442cad4..504a1b3c 100644 --- a/zebra-state/src/service/tests.rs +++ b/zebra-state/src/service/tests.rs @@ -8,7 +8,7 @@ use zebra_chain::{ }; use zebra_test::{prelude::*, transcript::Transcript}; -use crate::{init, BoxError, Config, Request, Response}; +use crate::{init, BoxError, Config, Request, Response, Utxo}; const LAST_BLOCK_HEIGHT: u32 = 10; @@ -80,16 +80,19 @@ async fn test_populated_state_responds_correctly( Ok(Response::Transaction(Some(transaction.clone()))), )); - for (index, output) in transaction.outputs().iter().enumerate() { + let from_coinbase = transaction.is_coinbase(); + for (index, output) in transaction.outputs().iter().cloned().enumerate() { let outpoint = transparent::OutPoint { hash: transaction_hash, index: index as _, }; + let utxo = Utxo { + output, + height, + from_coinbase, + }; - transcript.push(( - Request::AwaitUtxo(outpoint), - Ok(Response::Utxo(output.clone())), - )); + transcript.push((Request::AwaitUtxo(outpoint), Ok(Response::Utxo(utxo)))); } } } diff --git a/zebra-state/src/service/utxo.rs b/zebra-state/src/service/utxo.rs index 099dee24..c0740096 100644 --- a/zebra-state/src/service/utxo.rs +++ b/zebra-state/src/service/utxo.rs @@ -1,12 +1,14 @@ -#![allow(dead_code)] -use crate::{BoxError, Response}; use std::collections::HashMap; use std::future::Future; + use tokio::sync::broadcast; -use zebra_chain::{block::Block, transparent}; + +use zebra_chain::transparent; + +use crate::{BoxError, Response, Utxo}; #[derive(Debug, Default)] -pub struct PendingUtxos(HashMap>); +pub struct PendingUtxos(HashMap>); impl PendingUtxos { /// Returns a future that will resolve to the `transparent::Output` pointed @@ -33,41 +35,23 @@ impl PendingUtxos { } } - /// Notify all utxo requests waiting for the `transparent::Output` pointed to - /// by the given `transparent::OutPoint` that the `Output` has arrived. - pub fn respond(&mut self, outpoint: &transparent::OutPoint, output: transparent::Output) { - if let Some(sender) = self.0.remove(&outpoint) { + /// Notify all requests waiting for the [`Utxo`] pointed to by the given + /// [`transparent::OutPoint`] that the [`Utxo`] has arrived. + pub fn respond(&mut self, outpoint: &transparent::OutPoint, utxo: Utxo) { + if let Some(sender) = self.0.remove(outpoint) { // Adding the outpoint as a field lets us crossreference // with the trace of the verification that made the request. tracing::trace!(?outpoint, "found pending UTXO"); - let _ = sender.send(output); + let _ = sender.send(utxo); } } /// Check the list of pending UTXO requests against the supplied UTXO index. - pub fn check_against(&mut self, utxos: &HashMap) { - for (outpoint, output) in utxos.iter() { - self.respond(outpoint, output.clone()); - } - } - - /// Scan through unindexed transactions in the given `block` - /// to determine whether it contains any requested UTXOs. - pub fn scan_block(&mut self, block: &Block) { - if self.0.is_empty() { - return; - } - - tracing::trace!("scanning new block for pending UTXOs"); - for transaction in block.transactions.iter() { - let transaction_hash = transaction.hash(); - for (index, output) in transaction.outputs().iter().enumerate() { - let outpoint = transparent::OutPoint { - hash: transaction_hash, - index: index as _, - }; - - self.respond(&outpoint, output.clone()); + pub fn check_against(&mut self, utxos: &HashMap) { + for (outpoint, utxo) in utxos.iter() { + if let Some(sender) = self.0.remove(outpoint) { + tracing::trace!(?outpoint, "found pending UTXO"); + let _ = sender.send(utxo.clone()); } } } diff --git a/zebra-state/src/tests.rs b/zebra-state/src/tests.rs index dee35a69..3c788967 100644 --- a/zebra-state/src/tests.rs +++ b/zebra-state/src/tests.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, convert::TryFrom, mem, sync::Arc}; +use std::{convert::TryFrom, mem, sync::Arc}; use primitive_types::U256; use zebra_chain::{ @@ -21,15 +21,7 @@ impl Prepare for Arc { let block = self; let hash = block.hash(); let height = block.coinbase_height().unwrap(); - - let mut new_outputs = HashMap::new(); - for transaction in &block.transactions { - let hash = transaction.hash(); - for (index, output) in transaction.outputs().iter().cloned().enumerate() { - let index = index as u32; - new_outputs.insert(transparent::OutPoint { hash, index }, output); - } - } + let new_outputs = crate::utxo::new_outputs(&block); PreparedBlock { block, diff --git a/zebra-state/src/utxo.rs b/zebra-state/src/utxo.rs new file mode 100644 index 00000000..5ccfa3a1 --- /dev/null +++ b/zebra-state/src/utxo.rs @@ -0,0 +1,42 @@ +// needed to make clippy happy with derive(Arbitrary) +#![allow(clippy::unit_arg)] + +use zebra_chain::{block, transparent}; + +/// An unspent `transparent::Output`, with accompanying metadata. +#[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(test, derive(proptest_derive::Arbitrary))] +pub struct Utxo { + /// The output itself. + pub output: transparent::Output, + /// The height at which the output was created. + pub height: block::Height, + /// Whether the output originated in a coinbase transaction. + pub from_coinbase: bool, +} + +#[cfg(test)] +pub fn new_outputs(block: &block::Block) -> std::collections::HashMap { + use std::collections::HashMap; + + let height = block.coinbase_height().expect("block has coinbase height"); + + let mut new_outputs = HashMap::default(); + for transaction in &block.transactions { + let hash = transaction.hash(); + let from_coinbase = transaction.is_coinbase(); + for (index, output) in transaction.outputs().iter().cloned().enumerate() { + let index = index as u32; + new_outputs.insert( + transparent::OutPoint { hash, index }, + Utxo { + output, + height, + from_coinbase, + }, + ); + } + } + + new_outputs +}