From 7e11c0eb9c9c860a858302b8a15e2094126431d3 Mon Sep 17 00:00:00 2001 From: Henry de Valence Date: Mon, 23 Nov 2020 12:02:57 -0800 Subject: [PATCH] state: track UTXO provenance This commit changes the state system and database format to track the provenance of UTXOs, in addition to the outputs themselves. Specifically, it tracks the following additional metadata: - the height at which the UTXO was created; - whether or not the UTXO was created from a coinbase transaction or not. This metadata will allow us to: - check the coinbase maturity consensus rule; - check the coinbase inputs => no transparent outputs rule; - implement lookup of transactions by utxo (using the height to find the block and then scanning the block) for a future RPC mechanism. Closes #1342 --- Cargo.lock | 1 + zebra-consensus/src/block.rs | 19 +++++-- zebra-consensus/src/script.rs | 17 +++--- zebra-consensus/src/transaction.rs | 4 +- zebra-state/Cargo.toml | 1 + zebra-state/src/constants.rs | 3 +- zebra-state/src/lib.rs | 2 + zebra-state/src/request.rs | 46 +++++++++++++++- zebra-state/src/response.rs | 5 +- zebra-state/src/service.rs | 10 ++-- zebra-state/src/service/finalized_state.rs | 22 ++++---- .../service/finalized_state/disk_format.rs | 32 ++++++++--- .../src/service/non_finalized_state.rs | 11 ++-- .../src/service/non_finalized_state/chain.rs | 54 ++++++------------- .../non_finalized_state/queued_blocks.rs | 6 +-- zebra-state/src/service/tests.rs | 15 +++--- zebra-state/src/service/utxo.rs | 48 ++++++----------- zebra-state/src/tests.rs | 10 +--- zebra-state/src/utxo.rs | 39 ++++++++++++++ 19 files changed, 206 insertions(+), 139 deletions(-) create mode 100644 zebra-state/src/utxo.rs diff --git a/Cargo.lock b/Cargo.lock index 398349c1fd9..7c240829de8 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 564ae804f94..c86c3983de3 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 36b986a16c9..9dc4a4d105e 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 313ff3bcf24..894e6450548 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 fb6afc6806f..5f1e5da34c0 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 29e0d02491e..42d9b648c50 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 f97a82c2a11..2558a62d0e1 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 7c78112ad63..8f9fa39740c 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 b171bfe69a4..fd6df1af127 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 35f09eb7120..79caea6bd31 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 e801b5a3f54..732982442b6 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 f3d150f8135..92ec14abf3c 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 bf849cf8e7e..5b1fdc75080 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 583f4320e82..127afe013eb 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.clone(), 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 f8007e57a92..8e0f4090ce1 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 3442cad4f84..504a1b3ca20 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 099dee24279..c0740096f46 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 dee35a69bb6..91f0f4aca31 100644 --- a/zebra-state/src/tests.rs +++ b/zebra-state/src/tests.rs @@ -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 00000000000..5c7ec2079c8 --- /dev/null +++ b/zebra-state/src/utxo.rs @@ -0,0 +1,39 @@ +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 +}