diff --git a/chain/src/chain.rs b/chain/src/chain.rs index e19298b58..70ab100ec 100644 --- a/chain/src/chain.rs +++ b/chain/src/chain.rs @@ -506,9 +506,9 @@ impl Chain { /// that has not yet sufficiently matured. pub fn verify_coinbase_maturity(&self, tx: &Transaction) -> Result<(), Error> { let height = self.next_block_height()?; - let mut txhashset = self.txhashset.write(); - txhashset::extending_readonly(&mut txhashset, |extension| { - extension.verify_coinbase_maturity(&tx.inputs(), height)?; + let txhashset = self.txhashset.read(); + txhashset::utxo_view(&txhashset, |utxo| { + utxo.verify_coinbase_maturity(&tx.inputs(), height)?; Ok(()) }) } diff --git a/chain/src/pipe.rs b/chain/src/pipe.rs index 3f949869c..3fcc55838 100644 --- a/chain/src/pipe.rs +++ b/chain/src/pipe.rs @@ -434,16 +434,10 @@ fn validate_block(block: &Block, ctx: &mut BlockContext<'_>) -> Result<(), Error Ok(()) } -/// TODO - This can move into the utxo_view. -/// Verify the block is not attempting to spend coinbase outputs -/// before they have sufficiently matured. -/// Note: requires a txhashset extension. -fn verify_coinbase_maturity( - block: &Block, - ext: &mut txhashset::Extension<'_>, -) -> Result<(), Error> { - ext.verify_coinbase_maturity(&block.inputs(), block.header.height)?; - Ok(()) +/// Verify the block is not spending coinbase outputs before they have sufficiently matured. +fn verify_coinbase_maturity(block: &Block, ext: &txhashset::Extension<'_>) -> Result<(), Error> { + ext.utxo_view() + .verify_coinbase_maturity(&block.inputs(), block.header.height) } /// Some "real magick" verification logic. @@ -649,7 +643,5 @@ pub fn rewind_and_apply_fork(b: &Block, ext: &mut txhashset::Extension<'_>) -> R } fn validate_utxo(block: &Block, ext: &txhashset::Extension<'_>) -> Result<(), Error> { - let utxo = ext.utxo_view(); - utxo.validate_block(block)?; - Ok(()) + ext.utxo_view().validate_block(block) } diff --git a/chain/src/txhashset/txhashset.rs b/chain/src/txhashset/txhashset.rs index b711caf1c..0ed33e087 100644 --- a/chain/src/txhashset/txhashset.rs +++ b/chain/src/txhashset/txhashset.rs @@ -20,7 +20,7 @@ use crate::core::core::hash::{Hash, Hashed}; use crate::core::core::merkle_proof::MerkleProof; use crate::core::core::pmmr::{self, ReadonlyPMMR, RewindablePMMR, PMMR}; use crate::core::core::{ - Block, BlockHeader, Input, Output, OutputFeatures, OutputIdentifier, TxKernel, TxKernelEntry, + Block, BlockHeader, Input, Output, OutputIdentifier, TxKernel, TxKernelEntry, }; use crate::core::global; use crate::core::ser::{PMMRIndexHashable, PMMRable}; @@ -353,11 +353,13 @@ where { let output_pmmr = ReadonlyPMMR::at(&trees.output_pmmr_h.backend, trees.output_pmmr_h.last_pos); + let header_pmmr = + ReadonlyPMMR::at(&trees.header_pmmr_h.backend, trees.header_pmmr_h.last_pos); // Create a new batch here to pass into the utxo_view. // Discard it (rollback) after we finish with the utxo_view. let batch = trees.commit_index.batch()?; - let utxo = UTXOView::new(output_pmmr, &batch); + let utxo = UTXOView::new(output_pmmr, header_pmmr, &batch); res = inner(&utxo); } res @@ -829,46 +831,11 @@ impl<'a> Extension<'a> { /// Build a view of the current UTXO set based on the output PMMR. pub fn utxo_view(&'a self) -> UTXOView<'a> { - UTXOView::new(self.output_pmmr.readonly_pmmr(), self.batch) - } - - /// Verify we are not attempting to spend any coinbase outputs - /// that have not sufficiently matured. - pub fn verify_coinbase_maturity( - &mut self, - inputs: &Vec, - height: u64, - ) -> Result<(), Error> { - // Find the greatest output pos of any coinbase - // outputs we are attempting to spend. - let pos = inputs - .iter() - .filter(|x| x.features.contains(OutputFeatures::COINBASE_OUTPUT)) - .filter_map(|x| self.batch.get_output_pos(&x.commitment()).ok()) - .max() - .unwrap_or(0); - - if pos > 0 { - // If we have not yet reached 1,000 / 1,440 blocks then - // we can fail immediately as coinbase cannot be mature. - if height < global::coinbase_maturity() { - return Err(ErrorKind::ImmatureCoinbase.into()); - } - - // Find the "cutoff" pos in the output MMR based on the - // header from 1,000 blocks ago. - let cutoff_height = height.checked_sub(global::coinbase_maturity()).unwrap_or(0); - let cutoff_header = self.get_header_by_height(cutoff_height)?; - let cutoff_pos = cutoff_header.output_mmr_size; - - // If any output pos exceed the cutoff_pos - // we know they have not yet sufficiently matured. - if pos > cutoff_pos { - return Err(ErrorKind::ImmatureCoinbase.into()); - } - } - - Ok(()) + UTXOView::new( + self.output_pmmr.readonly_pmmr(), + self.header_pmmr.readonly_pmmr(), + self.batch, + ) } /// Apply a new block to the existing state. diff --git a/chain/src/txhashset/utxo_view.rs b/chain/src/txhashset/utxo_view.rs index cd4c2d989..586ea108e 100644 --- a/chain/src/txhashset/utxo_view.rs +++ b/chain/src/txhashset/utxo_view.rs @@ -14,8 +14,10 @@ //! Lightweight readonly view into output MMR for convenience. -use crate::core::core::pmmr::ReadonlyPMMR; -use crate::core::core::{Block, Input, Output, Transaction}; +use crate::core::core::hash::Hash; +use crate::core::core::pmmr::{self, ReadonlyPMMR}; +use crate::core::core::{Block, BlockHeader, Input, Output, OutputFeatures, Transaction}; +use crate::core::global; use crate::core::ser::PMMRIndexHashable; use crate::error::{Error, ErrorKind}; use crate::store::Batch; @@ -23,17 +25,23 @@ use grin_store::pmmr::PMMRBackend; /// Readonly view of the UTXO set (based on output MMR). pub struct UTXOView<'a> { - pmmr: ReadonlyPMMR<'a, Output, PMMRBackend>, + output_pmmr: ReadonlyPMMR<'a, Output, PMMRBackend>, + header_pmmr: ReadonlyPMMR<'a, BlockHeader, PMMRBackend>, batch: &'a Batch<'a>, } impl<'a> UTXOView<'a> { /// Build a new UTXO view. pub fn new( - pmmr: ReadonlyPMMR<'a, Output, PMMRBackend>, + output_pmmr: ReadonlyPMMR<'a, Output, PMMRBackend>, + header_pmmr: ReadonlyPMMR<'a, BlockHeader, PMMRBackend>, batch: &'a Batch<'_>, ) -> UTXOView<'a> { - UTXOView { pmmr, batch } + UTXOView { + output_pmmr, + header_pmmr, + batch, + } } /// Validate a block against the current UTXO set. @@ -69,7 +77,7 @@ impl<'a> UTXOView<'a> { // Compare the hash in the output MMR at the expected pos. fn validate_input(&self, input: &Input) -> Result<(), Error> { if let Ok(pos) = self.batch.get_output_pos(&input.commitment()) { - if let Some(hash) = self.pmmr.get_hash(pos) { + if let Some(hash) = self.output_pmmr.get_hash(pos) { if hash == input.hash_with_index(pos - 1) { return Ok(()); } @@ -81,7 +89,7 @@ impl<'a> UTXOView<'a> { // Output is valid if it would not result in a duplicate commitment in the output MMR. fn validate_output(&self, output: &Output) -> Result<(), Error> { if let Ok(pos) = self.batch.get_output_pos(&output.commitment()) { - if let Some(out_mmr) = self.pmmr.get_data(pos) { + if let Some(out_mmr) = self.output_pmmr.get_data(pos) { if out_mmr.commitment() == output.commitment() { return Err(ErrorKind::DuplicateCommitment(output.commitment()).into()); } @@ -89,4 +97,57 @@ impl<'a> UTXOView<'a> { } Ok(()) } + + /// Verify we are not attempting to spend any coinbase outputs + /// that have not sufficiently matured. + pub fn verify_coinbase_maturity(&self, inputs: &Vec, height: u64) -> Result<(), Error> { + // Find the greatest output pos of any coinbase + // outputs we are attempting to spend. + let pos = inputs + .iter() + .filter(|x| x.features.contains(OutputFeatures::COINBASE_OUTPUT)) + .filter_map(|x| self.batch.get_output_pos(&x.commitment()).ok()) + .max() + .unwrap_or(0); + + if pos > 0 { + // If we have not yet reached 1,000 / 1,440 blocks then + // we can fail immediately as coinbase cannot be mature. + if height < global::coinbase_maturity() { + return Err(ErrorKind::ImmatureCoinbase.into()); + } + + // Find the "cutoff" pos in the output MMR based on the + // header from 1,000 blocks ago. + let cutoff_height = height.checked_sub(global::coinbase_maturity()).unwrap_or(0); + let cutoff_header = self.get_header_by_height(cutoff_height)?; + let cutoff_pos = cutoff_header.output_mmr_size; + + // If any output pos exceed the cutoff_pos + // we know they have not yet sufficiently matured. + if pos > cutoff_pos { + return Err(ErrorKind::ImmatureCoinbase.into()); + } + } + + Ok(()) + } + + /// Get the header hash for the specified pos from the underlying MMR backend. + fn get_header_hash(&self, pos: u64) -> Option { + self.header_pmmr.get_data(pos).map(|x| x.hash()) + } + + /// Get the header at the specified height based on the current state of the extension. + /// Derives the MMR pos from the height (insertion index) and retrieves the header hash. + /// Looks the header up in the db by hash. + pub fn get_header_by_height(&self, height: u64) -> Result { + let pos = pmmr::insertion_to_pmmr_index(height + 1); + if let Some(hash) = self.get_header_hash(pos) { + let header = self.batch.get_block_header(&hash)?; + Ok(header) + } else { + Err(ErrorKind::Other(format!("get header by height")).into()) + } + } }