diff --git a/chainstate-storage/src/internal/mod.rs b/chainstate-storage/src/internal/mod.rs index 9b49cc128a..285ca1411a 100644 --- a/chainstate-storage/src/internal/mod.rs +++ b/chainstate-storage/src/internal/mod.rs @@ -25,10 +25,7 @@ use common::{ }; use serialization::{Codec, Decode, DecodeAll, Encode}; use storage::traits::{self, MapMut, MapRef, TransactionRo, TransactionRw}; -use utxo::{ - utxo_storage::{UtxosStorageRead, UtxosStorageWrite}, - BlockUndo, Utxo, -}; +use utxo::{BlockUndo, Utxo, UtxosStorageRead, UtxosStorageWrite}; use crate::{BlockchainStorage, BlockchainStorageRead, BlockchainStorageWrite, Transactional}; diff --git a/chainstate-storage/src/internal/test.rs b/chainstate-storage/src/internal/test.rs index 42025d9e98..70a77314da 100644 --- a/chainstate-storage/src/internal/test.rs +++ b/chainstate-storage/src/internal/test.rs @@ -266,7 +266,7 @@ fn create_rand_utxo(rng: &mut impl Rng, block_height: u64) -> Utxo { let is_block_reward = random_value % 3 == 0; // generate utxo - Utxo::new(output, is_block_reward, BlockHeight::new(block_height)) + Utxo::new_for_blockchain(output, is_block_reward, BlockHeight::new(block_height)) } /// returns a block undo with random utxos and TxUndos. diff --git a/chainstate-storage/src/internal/utxo_db.rs b/chainstate-storage/src/internal/utxo_db.rs index faa5dcbc35..a841ffe622 100644 --- a/chainstate-storage/src/internal/utxo_db.rs +++ b/chainstate-storage/src/internal/utxo_db.rs @@ -23,9 +23,7 @@ mod test { use crypto::random::Rng; use rstest::rstest; use test_utils::random::{make_seedable_rng, Seed}; - use utxo::utxo_storage::UtxosDBMut; - use utxo::utxo_storage::{UtxosStorageRead, UtxosStorageWrite}; - use utxo::Utxo; + use utxo::{Utxo, UtxosDBMut, UtxosStorageRead, UtxosStorageWrite}; fn create_utxo(block_height: u64, output_value: u128) -> (Utxo, OutPoint) { // just a random value generated, and also a random `is_block_reward` value. @@ -34,7 +32,7 @@ mod test { OutputValue::Coin(Amount::from_atoms(output_value)), OutputPurpose::Transfer(Destination::PublicKey(pub_key)), ); - let utxo = Utxo::new(output, true, BlockHeight::new(block_height)); + let utxo = Utxo::new_for_blockchain(output, true, BlockHeight::new(block_height)); // create the id based on the `is_block_reward` value. let id = OutPointSourceId::BlockReward(Id::new(H256::random())); diff --git a/chainstate-storage/src/lib.rs b/chainstate-storage/src/lib.rs index 3fb37f4ec4..700fa36b70 100644 --- a/chainstate-storage/src/lib.rs +++ b/chainstate-storage/src/lib.rs @@ -21,7 +21,7 @@ use common::chain::OutPointSourceId; use common::chain::{Block, GenBlock}; use common::primitives::{BlockHeight, Id}; use storage::traits; -use utxo::utxo_storage::{UtxosStorageRead, UtxosStorageWrite}; +use utxo::{UtxosStorageRead, UtxosStorageWrite}; mod internal; #[cfg(any(test, feature = "mock"))] diff --git a/chainstate-storage/src/mock.rs b/chainstate-storage/src/mock.rs index 9239737a3c..f478535daf 100644 --- a/chainstate-storage/src/mock.rs +++ b/chainstate-storage/src/mock.rs @@ -23,10 +23,7 @@ use common::{ }, primitives::{BlockHeight, Id}, }; -use utxo::{ - utxo_storage::{UtxosStorageRead, UtxosStorageWrite}, - BlockUndo, Utxo, -}; +use utxo::{BlockUndo, Utxo, UtxosStorageRead, UtxosStorageWrite}; mockall::mock! { /// A mock object for blockchain storage diff --git a/chainstate/src/detail/mod.rs b/chainstate/src/detail/mod.rs index 011de8772c..887ac5a364 100644 --- a/chainstate/src/detail/mod.rs +++ b/chainstate/src/detail/mod.rs @@ -38,7 +38,7 @@ use common::{ }; use logging::log; use utils::eventhandler::{EventHandler, EventsController}; -use utxo::utxo_storage::UtxosDBMut; +use utxo::UtxosDBMut; use self::{ orphan_blocks::{OrphanBlocksRef, OrphanBlocksRefMut}, diff --git a/utxo/src/utxo_impl/mod.rs b/utxo/src/cache.rs similarity index 55% rename from utxo/src/utxo_impl/mod.rs rename to utxo/src/cache.rs index acc83a4109..dcdbbb998f 100644 --- a/utxo/src/utxo_impl/mod.rs +++ b/utxo/src/cache.rs @@ -13,205 +13,40 @@ // See the License for the specific language governing permissions and // limitations under the License. -//TODO: remove once the functions are used. -#![allow(dead_code)] -use crate::{Error, TxUndo}; -use common::chain::{GenBlock, OutPoint, OutPointSourceId, Transaction, TxOutput}; -use common::primitives::{BlockHeight, Id, Idable}; +use crate::{ + utxo_entry::{IsDirty, IsFresh, UtxoEntry}, + {Error, FlushableUtxoView, TxUndo, Utxo, UtxoSource, UtxosView}, +}; +use common::{ + chain::{GenBlock, OutPoint, OutPointSourceId, Transaction}, + primitives::{BlockHeight, Id, Idable}, +}; use logging::log; -use serialization::{Decode, Encode}; -use std::collections::BTreeMap; -use std::fmt::{Debug, Formatter}; - -pub mod utxo_storage; - -//todo: proper placement and derivation of this max -const MAX_OUTPUTS_PER_BLOCK: u32 = 500; - -// Determines whether the utxo is for the blockchain of for mempool -#[derive(Debug, Clone, Eq, PartialEq, Encode, Decode)] -pub enum UtxoSource { - /// At which height this containing tx was included in the active block chain - BlockChain(BlockHeight), - MemPool, -} - -impl UtxoSource { - fn is_mempool(&self) -> bool { - match self { - UtxoSource::BlockChain(_) => false, - UtxoSource::MemPool => true, - } - } - - fn blockchain_height(&self) -> Result { - match self { - UtxoSource::BlockChain(h) => Ok(*h), - UtxoSource::MemPool => Err(crate::Error::NoBlockchainHeightFound), - } - } -} - -/// The Unspent Transaction Output -#[derive(Debug, Clone, Eq, PartialEq, Encode, Decode)] -pub struct Utxo { - output: TxOutput, - is_block_reward: bool, - /// identifies whether the utxo is for the blockchain or for mempool. - source: UtxoSource, -} - -impl Utxo { - pub fn new(output: TxOutput, is_block_reward: bool, height: BlockHeight) -> Self { - Self { - output, - is_block_reward, - source: UtxoSource::BlockChain(height), - } - } - - /// a utxo for mempool, that does not need the block height. - pub fn new_for_mempool(output: TxOutput, is_block_reward: bool) -> Self { - Self { - output, - is_block_reward, - source: UtxoSource::MemPool, - } - } - - pub fn is_block_reward(&self) -> bool { - self.is_block_reward - } - - pub fn source_height(&self) -> &UtxoSource { - &self.source - } - - pub fn output(&self) -> &TxOutput { - &self.output - } - - pub fn set_height(&mut self, value: UtxoSource) { - self.source = value - } -} - -pub trait UtxosView { - /// Retrieves utxo. - fn utxo(&self, outpoint: &OutPoint) -> Option; - - /// Checks whether outpoint is unspent. - fn has_utxo(&self, outpoint: &OutPoint) -> bool; - - /// Retrieves the block hash of the best block in this view - fn best_block_hash(&self) -> Id; - - /// Estimated size of the whole view (None if not implemented) - fn estimated_size(&self) -> Option; - - fn derive_cache(&self) -> UtxosCache; -} +use std::{ + collections::BTreeMap, + fmt::{Debug, Formatter}, +}; #[derive(Clone)] pub struct ConsumedUtxoCache { - container: BTreeMap, - best_block: Id, -} - -pub trait FlushableUtxoView { - /// Performs bulk modification - fn batch_write(&mut self, utxos: ConsumedUtxoCache) -> Result<(), Error>; -} - -// flush the cache into the provided base. This will consume the cache and throw it away. -// It uses the batch_write function since it's available in different kinds of views. -pub fn flush_to_base(cache: UtxosCache, base: &mut T) -> Result<(), Error> { - base.batch_write(cache.consume()) + pub(crate) container: BTreeMap, + pub(crate) best_block: Id, } #[derive(Clone)] pub struct UtxosCache<'a> { parent: Option<&'a dyn UtxosView>, current_block_hash: Id, - utxos: BTreeMap, - //TODO: do we need this? + // pub(crate) visibility is required for tests that are in a different mod + pub(crate) utxos: BTreeMap, + // TODO: calculate memory usage (mintlayer/mintlayer-core#354) + #[allow(dead_code)] memory_usage: usize, } -/// Tells the state of the utxo -#[derive(Debug, Clone, Eq, PartialEq, Encode, Decode)] -#[allow(clippy::large_enum_variant)] -pub enum UtxoStatus { - Spent, - Entry(Utxo), -} - -impl UtxoStatus { - fn into_option(self) -> Option { - match self { - UtxoStatus::Spent => None, - UtxoStatus::Entry(utxo) => Some(utxo), - } - } -} - -/// Just the Utxo with additional information. -#[derive(Debug, Clone, Eq, PartialEq, Encode, Decode)] -pub(crate) struct UtxoEntry { - status: UtxoStatus, - /// The utxo entry is dirty when this version is different from the parent. - is_dirty: bool, - /// The utxo entry is fresh when the parent does not have this utxo or - /// if it exists in parent but not in current cache. - is_fresh: bool, -} - -impl UtxoEntry { - pub fn new(utxo: Utxo, is_fresh: bool, is_dirty: bool) -> UtxoEntry { - UtxoEntry { - status: UtxoStatus::Entry(utxo), - is_dirty, - is_fresh, - } - } - - pub fn is_dirty(&self) -> bool { - self.is_dirty - } - - pub fn is_fresh(&self) -> bool { - self.is_fresh - } - - pub fn is_spent(&self) -> bool { - self.status == UtxoStatus::Spent - } - - pub fn utxo(&self) -> Option { - match &self.status { - UtxoStatus::Spent => None, - UtxoStatus::Entry(utxo) => Some(utxo.clone()), - } - } - - pub fn take_utxo(self) -> Option { - match self.status { - UtxoStatus::Spent => None, - UtxoStatus::Entry(utxo) => Some(utxo), - } - } - - fn utxo_mut(&mut self) -> Option<&mut Utxo> { - match &mut self.status { - UtxoStatus::Spent => None, - UtxoStatus::Entry(utxo) => Some(utxo), - } - } -} - impl<'a> UtxosCache<'a> { #[cfg(test)] - pub(crate) fn new_for_test(best_block: Id) -> Self { + pub fn new_for_test(best_block: Id) -> Self { Self { parent: None, current_block_hash: best_block, @@ -220,7 +55,7 @@ impl<'a> UtxosCache<'a> { } } - /// returns a UtxoEntry, given the outpoint. + /// Returns a UtxoEntry, given the outpoint. // the reason why it's not a `&UtxoEntry`, is because the flags are bound to change esp. // when the utxo was actually retrieved from the parent. fn get_utxo_entry(&self, outpoint: &OutPoint) -> Option { @@ -230,14 +65,12 @@ impl<'a> UtxosCache<'a> { // since the utxo does not exist in this view, try to check from parent. self.parent.and_then(|parent| { - parent.utxo(outpoint).map(|utxo| UtxoEntry { - // if the utxo exists in parent: - // dirty is FALSE because this view does not have the utxo, therefore is different from parent - // fresh is FALSE because this view does not have the utxo but the parent has. - status: UtxoStatus::Entry(utxo), - is_dirty: false, - is_fresh: false, - }) + // if the utxo exists in parent: + // dirty is FALSE because this view does not have the utxo, therefore is different from parent + // fresh is FALSE because this view does not have the utxo but the parent has. + parent + .utxo(outpoint) + .map(|utxo| UtxoEntry::new(Some(utxo), IsFresh::No, IsDirty::No)) }) } @@ -264,50 +97,42 @@ impl<'a> UtxosCache<'a> { for (idx, output) in tx.outputs().iter().enumerate() { let outpoint = OutPoint::new(id.clone(), idx as u32); - // by default no overwrite allowed. let overwrite = check_for_overwrite && self.has_utxo(&outpoint); - - let utxo = Utxo { - output: output.clone(), - // TODO: where do we get the block reward from the transaction? - is_block_reward: false, - source: source.clone(), - }; + let utxo = Utxo::new(output.clone(), false, source.clone()); self.add_utxo(&outpoint, utxo, overwrite)?; } Ok(()) } - /// Mark the inputs of tx as 'spent'. - /// returns a TxUndo if function is a success; - /// or an error if the tx's input cannot be spent. + /// Marks the inputs of a transaction as 'spent', adds outputs to the utxo set. + /// Returns a TxUndo if function is a success or an error if the tx's input cannot be spent. pub fn spend_utxos(&mut self, tx: &Transaction, height: BlockHeight) -> Result { let tx_undo: Result, Error> = tx.inputs().iter().map(|tx_in| self.spend_utxo(tx_in.outpoint())).collect(); - self.add_utxos(tx, UtxoSource::BlockChain(height), false)?; + self.add_utxos(tx, UtxoSource::Blockchain(height), false)?; tx_undo.map(TxUndo::new) } - /// Adds a utxo entry in the cache. + /// Adds an utxo entry to the cache pub fn add_utxo( &mut self, outpoint: &OutPoint, utxo: Utxo, possible_overwrite: bool, // TODO: change this to an enum that explains what happens ) -> Result<(), Error> { + // TODO: update the memory usage + // self.memory_usage should be deducted based on this current entry. + let is_fresh = match self.utxos.get(outpoint) { None => { // An insert can be done. This utxo doesn't exist yet, so it's fresh. !possible_overwrite } Some(curr_entry) => { - // TODO: update the memory usage - // self.memory_usage should be deducted based on this current entry. - if !possible_overwrite { if !curr_entry.is_spent() { // Attempted to overwrite an existing utxo @@ -324,16 +149,16 @@ impl<'a> UtxosCache<'a> { // is 'spent' when the block adding it is disconnected and then // re-added when it is also added in a newly connected block). // if utxo is spent and is not dirty, then it can be marked as fresh. - !curr_entry.is_dirty || curr_entry.is_fresh + !curr_entry.is_dirty() || curr_entry.is_fresh() } else { // copy from the original entry - curr_entry.is_fresh + curr_entry.is_fresh() } } }; // create a new entry - let new_entry = UtxoEntry::new(utxo, is_fresh, true); + let new_entry = UtxoEntry::new(Some(utxo), IsFresh::from(is_fresh), IsDirty::Yes); // TODO: update the memory usage // self.memory_usage should be added based on this new entry. @@ -351,16 +176,12 @@ impl<'a> UtxosCache<'a> { // self.memory_usage must be deducted from this entry's size // check whether this entry is fresh - if entry.is_fresh { + if entry.is_fresh() { // This is only available in this view. Remove immediately. self.utxos.remove(outpoint); } else { // mark this as 'spent' - let new_entry = UtxoEntry { - status: UtxoStatus::Spent, - is_dirty: true, - is_fresh: false, - }; + let new_entry = UtxoEntry::new(None, IsFresh::No, IsDirty::Yes); self.utxos.insert(outpoint.clone(), new_entry); } @@ -374,28 +195,33 @@ impl<'a> UtxosCache<'a> { /// Returns a mutable reference of the utxo, given the outpoint. pub fn get_mut_utxo(&mut self, outpoint: &OutPoint) -> Option<&mut Utxo> { - let status = self.get_utxo_entry(outpoint)?; - let utxo: Utxo = status.status.into_option()?; + let entry = self.get_utxo_entry(outpoint)?; + let utxo = entry.utxo()?; let utxo: &mut UtxoEntry = self.utxos.entry(outpoint.clone()).or_insert_with(|| { //TODO: update the memory storage here - UtxoEntry::new(utxo, status.is_fresh, status.is_dirty) + UtxoEntry::new( + Some(utxo.clone()), + IsFresh::from(entry.is_fresh()), + IsDirty::from(entry.is_dirty()), + ) }); utxo.utxo_mut() } /// removes the utxo in the cache with the outpoint - pub(crate) fn uncache(&mut self, outpoint: &OutPoint) -> Option { + pub fn uncache(&mut self, outpoint: &OutPoint) -> Result<(), Error> { let key = outpoint; if let Some(entry) = self.utxos.get(key) { // see bitcoin's Uncache. - if !entry.is_fresh && !entry.is_dirty { + if !entry.is_fresh() && !entry.is_dirty() { //todo: decrement the memory usage - return self.utxos.remove(key); + self.utxos.remove(key); + return Ok(()); } } - None + Err(Error::NoUtxoFound) } pub fn consume(self) -> ConsumedUtxoCache { @@ -421,7 +247,7 @@ impl<'a> UtxosView for UtxosCache<'a> { fn utxo(&self, outpoint: &OutPoint) -> Option { let key = outpoint; if let Some(res) = self.utxos.get(key) { - return res.utxo(); + return res.utxo().cloned(); } // if utxo is not found in this view, use parent's `get_utxo`. @@ -449,17 +275,20 @@ impl<'a> FlushableUtxoView for UtxosCache<'a> { fn batch_write(&mut self, utxo_entries: ConsumedUtxoCache) -> Result<(), Error> { for (key, entry) in utxo_entries.container { // Ignore non-dirty entries (optimization). - if entry.is_dirty { + if entry.is_dirty() { let parent_entry = self.utxos.get(&key); match parent_entry { None => { // The parent cache does not have an entry, while the child cache does. // We can ignore it if it's both spent and FRESH in the child - if !(entry.is_fresh && entry.is_spent()) { + if !(entry.is_fresh() && entry.is_spent()) { // Create the utxo in the parent cache, move the data up // and mark it as dirty. - let mut entry_copy = entry.clone(); - entry_copy.is_dirty = true; + let entry_copy = UtxoEntry::new( + entry.utxo().cloned(), + IsFresh::from(entry.is_fresh()), + IsDirty::Yes, + ); self.utxos.insert(key, entry_copy); // TODO: increase the memory usage @@ -476,15 +305,17 @@ impl<'a> FlushableUtxoView for UtxosCache<'a> { return Err(Error::FreshUtxoAlreadyExists); } - if parent_entry.is_fresh && entry.is_spent() { + if parent_entry.is_fresh() && entry.is_spent() { // The grandparent cache does not have an entry, and the utxo // has been spent. We can just delete it from the parent cache. self.utxos.remove(&key); } else { // A normal modification. - let mut entry_copy = entry.clone(); - entry_copy.is_dirty = true; - entry_copy.is_fresh = parent_entry.is_fresh; + let entry_copy = UtxoEntry::new( + entry.utxo().cloned(), + IsFresh::from(parent_entry.is_fresh()), + IsDirty::Yes, + ); self.utxos.insert(key, entry_copy); // TODO: update the memory usage @@ -503,56 +334,79 @@ impl<'a> FlushableUtxoView for UtxosCache<'a> { } } -#[cfg(test)] -mod test; - -#[cfg(test)] -pub mod test_helper; - -#[cfg(test)] -mod simulation; - #[cfg(test)] mod unit_test { + use super::*; + use crate::tests::test_helper::{insert_single_entry, Presence}; use common::primitives::H256; use rstest::rstest; use test_utils::random::{make_seedable_rng, Seed}; - use crate::test_helper::{insert_single_entry, Presence, DIRTY, FRESH}; - use crate::UtxosCache; - #[rstest] #[trace] #[case(Seed::from_entropy())] - fn test_uncache(#[case] seed: Seed) { + fn uncache_absent(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let mut cache = UtxosCache::new_for_test(H256::random().into()); - // when the entry is not dirty and not fresh - let (utxo, outp) = - insert_single_entry(&mut rng, &mut cache, &Presence::Present, Some(0), None); - let res = cache.uncache(&outp).expect("should return an entry"); - assert_eq!(res.utxo(), Some(utxo)); + // when the outpoint does not exist. + let (_, outp) = insert_single_entry(&mut rng, &mut cache, Presence::Absent, None, None); + assert_eq!(Error::NoUtxoFound, cache.uncache(&outp).unwrap_err()); assert!(!cache.has_utxo_in_cache(&outp)); + } - // when the outpoint does not exist. - let (_, outp) = insert_single_entry(&mut rng, &mut cache, &Presence::Absent, None, None); - assert_eq!(cache.uncache(&outp), None); + #[rstest] + #[trace] + #[case(Seed::from_entropy())] + fn uncache_not_fresh_not_dirty(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); + let mut cache = UtxosCache::new_for_test(H256::random().into()); + + // when the entry is not dirty and not fresh + let (_, outp) = insert_single_entry( + &mut rng, + &mut cache, + Presence::Present, + Some((IsFresh::No, IsDirty::No)), + None, + ); + assert!(cache.uncache(&outp).is_ok()); assert!(!cache.has_utxo_in_cache(&outp)); + } - // when the entry is fresh, entry cannot be removed. - let (_, outp) = - insert_single_entry(&mut rng, &mut cache, &Presence::Present, Some(FRESH), None); - assert_eq!(cache.uncache(&outp), None); + #[rstest] + #[trace] + #[case(Seed::from_entropy())] + fn uncache_dirty_not_fresh(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); + let mut cache = UtxosCache::new_for_test(H256::random().into()); // when the entry is dirty, entry cannot be removed. - let (_, outp) = - insert_single_entry(&mut rng, &mut cache, &Presence::Present, Some(DIRTY), None); - assert_eq!(cache.uncache(&outp), None); + let (_, outp) = insert_single_entry( + &mut rng, + &mut cache, + Presence::Present, + Some((IsFresh::No, IsDirty::Yes)), + None, + ); + assert_eq!(Error::NoUtxoFound, cache.uncache(&outp).unwrap_err()); + } + + #[rstest] + #[trace] + #[case(Seed::from_entropy())] + fn uncache_fresh_and_dirty(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); + let mut cache = UtxosCache::new_for_test(H256::random().into()); // when the entry is both fresh and dirty, entry cannot be removed. - let (_, outp) = - insert_single_entry(&mut rng, &mut cache, &Presence::Present, Some(FRESH), None); - assert_eq!(cache.uncache(&outp), None); + let (_, outp) = insert_single_entry( + &mut rng, + &mut cache, + Presence::Present, + Some((IsFresh::Yes, IsDirty::Yes)), + None, + ); + assert_eq!(Error::NoUtxoFound, cache.uncache(&outp).unwrap_err()); } } diff --git a/utxo/src/error.rs b/utxo/src/error.rs new file mode 100644 index 0000000000..0a0182fb4d --- /dev/null +++ b/utxo/src/error.rs @@ -0,0 +1,40 @@ +// Copyright (c) 2022 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use thiserror::Error; + +#[derive(Error, Debug, Eq, PartialEq)] +pub enum Error { + #[error("Attempted to overwrite an existing utxo")] + OverwritingUtxo, + #[error( + "The utxo was marked FRESH in the child cache, but the utxo exists in the parent cache. This can be considered a fatal error." + )] + FreshUtxoAlreadyExists, + #[error("Attempted to spend a UTXO that's already spent")] + UtxoAlreadySpent, + #[error("Attempted to spend a non-existing UTXO")] + NoUtxoFound, + #[error("Attempted to get the block height of a UTXO source that is based on the mempool")] + NoBlockchainHeightFound, + #[error("Database error: `{0}`")] + DBError(String), +} + +impl From for Error { + fn from(e: chainstate_types::storage_result::Error) -> Self { + Error::DBError(format!("{:?}", e)) + } +} diff --git a/utxo/src/lib.rs b/utxo/src/lib.rs index a0d4b3d6ce..d75bcd880b 100644 --- a/utxo/src/lib.rs +++ b/utxo/src/lib.rs @@ -13,35 +13,22 @@ // See the License for the specific language governing permissions and // limitations under the License. +mod cache; +mod error; +mod storage; mod undo; -mod utxo_impl; +mod utxo; +mod utxo_entry; +mod view; -pub use undo::*; -pub use utxo_impl::*; +pub use crate::{ + cache::{ConsumedUtxoCache, UtxosCache}, + error::Error, + storage::{UtxosDB, UtxosDBMut, UtxosStorageRead, UtxosStorageWrite}, + undo::{BlockUndo, TxUndo}, + utxo::{Utxo, UtxoSource}, + view::{flush_to_base, FlushableUtxoView, UtxosView}, +}; -use thiserror::Error; - -#[allow(dead_code)] -#[derive(Error, Debug, Eq, PartialEq)] -pub enum Error { - #[error("Attempted to overwrite an existing utxo")] - OverwritingUtxo, - #[error( - "The utxo was marked FRESH in the child cache, but the utxo exists in the parent cache. This can be considered a fatal error." - )] - FreshUtxoAlreadyExists, - #[error("Attempted to spend a UTXO that's already spent")] - UtxoAlreadySpent, - #[error("Attempted to spend a non-existing UTXO")] - NoUtxoFound, - #[error("Attempted to get the block height of a UTXO source that is based on the mempool")] - NoBlockchainHeightFound, - #[error("Database error: `{0}`")] - DBError(String), -} - -impl From for Error { - fn from(e: chainstate_types::storage_result::Error) -> Self { - Error::DBError(format!("{:?}", e)) - } -} +#[cfg(test)] +mod tests; diff --git a/utxo/src/utxo_impl/utxo_storage/in_memory.rs b/utxo/src/storage/in_memory.rs similarity index 95% rename from utxo/src/utxo_impl/utxo_storage/in_memory.rs rename to utxo/src/storage/in_memory.rs index 8181b445c4..6d5b8e874b 100644 --- a/utxo/src/utxo_impl/utxo_storage/in_memory.rs +++ b/utxo/src/storage/in_memory.rs @@ -13,8 +13,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::collections::{BTreeMap, HashMap}; - use super::{UtxosStorageRead, UtxosStorageWrite}; use crate::{BlockUndo, Utxo, UtxosCache, UtxosView}; use chainstate_types::storage_result::Error; @@ -22,6 +20,7 @@ use common::{ chain::{Block, GenBlock, OutPoint}, primitives::Id, }; +use std::collections::BTreeMap; #[derive(Clone)] pub struct UtxosDBInMemoryImpl { @@ -33,13 +32,14 @@ pub struct UtxosDBInMemoryImpl { impl UtxosDBInMemoryImpl { pub fn new(best_block: Id, initial_utxos: BTreeMap) -> Self { Self { - store: BTreeMap::new(), + store: initial_utxos, undo_store: BTreeMap::new(), best_block_id: best_block, } } - pub(crate) fn internal_store(&mut self) -> &BTreeMap { + #[cfg(test)] + pub fn internal_store(&mut self) -> &BTreeMap { &self.store } } diff --git a/utxo/src/utxo_impl/utxo_storage/mod.rs b/utxo/src/storage/mod.rs similarity index 91% rename from utxo/src/utxo_impl/utxo_storage/mod.rs rename to utxo/src/storage/mod.rs index 521be3e1eb..524b0ce36c 100644 --- a/utxo/src/utxo_impl/utxo_storage/mod.rs +++ b/utxo/src/storage/mod.rs @@ -13,19 +13,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -#![allow(dead_code, unused_variables, unused_imports)] -// todo: remove ^ when all untested codes are tested - -pub mod in_memory; mod rw_impls; mod view_impls; -use std::collections::BTreeMap; - use crate::{BlockUndo, FlushableUtxoView, Utxo, UtxosView}; use chainstate_types::storage_result::Error; use common::{ - chain::{Block, ChainConfig, GenBlock, OutPoint, TxOutput}, + chain::{Block, ChainConfig, GenBlock, OutPoint}, primitives::{BlockHeight, Id}, }; @@ -93,7 +87,7 @@ impl<'a, S: UtxosStorageWrite> UtxosDBMut<'a, S> { utxos_cache .add_utxo( &OutPoint::new(genesis_id.into(), index as u32), - Utxo::new(output.clone(), false, BlockHeight::new(0)), + Utxo::new_for_blockchain(output.clone(), false, BlockHeight::new(0)), false, ) .expect("Adding genesis utxo failed"); @@ -107,5 +101,8 @@ impl<'a, S: UtxosStorageWrite> UtxosDBMut<'a, S> { } } +#[cfg(test)] +mod in_memory; + #[cfg(test)] mod test; diff --git a/utxo/src/utxo_impl/utxo_storage/rw_impls.rs b/utxo/src/storage/rw_impls.rs similarity index 99% rename from utxo/src/utxo_impl/utxo_storage/rw_impls.rs rename to utxo/src/storage/rw_impls.rs index 8a59d0d57c..a7c9d727be 100644 --- a/utxo/src/utxo_impl/utxo_storage/rw_impls.rs +++ b/utxo/src/storage/rw_impls.rs @@ -13,17 +13,14 @@ // See the License for the specific language governing permissions and // limitations under the License. +use super::{UtxosDB, UtxosDBMut, UtxosStorageRead, UtxosStorageWrite}; +use crate::{BlockUndo, Utxo}; +use chainstate_types::storage_result::Error as StorageError; use common::{ chain::{Block, GenBlock, OutPoint}, primitives::Id, }; -use crate::{BlockUndo, Utxo}; - -use super::{UtxosDB, UtxosDBMut, UtxosStorageRead, UtxosStorageWrite}; - -use chainstate_types::storage_result::Error as StorageError; - impl<'a, S: UtxosStorageRead> UtxosStorageRead for UtxosDBMut<'a, S> { fn get_utxo(&self, outpoint: &OutPoint) -> Result, StorageError> { self.0.get_utxo(outpoint) diff --git a/utxo/src/utxo_impl/utxo_storage/test.rs b/utxo/src/storage/test.rs similarity index 92% rename from utxo/src/utxo_impl/utxo_storage/test.rs rename to utxo/src/storage/test.rs index 0cdbe107cb..e180e4cb97 100644 --- a/utxo/src/utxo_impl/utxo_storage/test.rs +++ b/utxo/src/storage/test.rs @@ -13,23 +13,24 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::in_memory::UtxosDBInMemoryImpl; -use super::*; -use crate::test_helper::{convert_to_utxo, create_tx_inputs, create_tx_outputs, create_utxo}; -use crate::utxo_impl::{ - flush_to_base, utxo_storage::UtxosDB, FlushableUtxoView, Utxo, UtxoEntry, UtxosCache, UtxosView, +use super::{in_memory::UtxosDBInMemoryImpl, *}; +use crate::{ + flush_to_base, + tests::test_helper::{convert_to_utxo, create_tx_inputs, create_tx_outputs, create_utxo}, + utxo_entry::{IsDirty, IsFresh, UtxoEntry}, + ConsumedUtxoCache, FlushableUtxoView, UtxosCache, UtxosView, }; -use crate::ConsumedUtxoCache; -use common::chain::block::timestamp::BlockTimestamp; -use common::chain::config::create_mainnet; -use common::chain::signature::inputsig::InputWitness; -use common::chain::{Destination, OutPointSourceId, Transaction, TxInput, TxOutput}; -use common::primitives::{Amount, BlockHeight, Idable}; -use common::primitives::{Id, H256}; -use crypto::random::{seq, Rng}; +use common::{ + chain::{ + block::timestamp::BlockTimestamp, signature::inputsig::InputWitness, OutPointSourceId, + Transaction, TxInput, + }, + primitives::{BlockHeight, Id, Idable, H256}, +}; +use crypto::random::Rng; use itertools::Itertools; use rstest::rstest; -use std::collections::{BTreeMap, HashMap}; +use std::collections::BTreeMap; use test_utils::random::{make_seedable_rng, Seed}; fn create_transactions( @@ -100,7 +101,7 @@ fn create_utxo_entries(rng: &mut impl Rng, num_of_utxos: u8) -> BTreeMap(db: &S, outpoint: &OutPoint) -> Option { db.get_utxo(outpoint).unwrap_or_else(|e| { panic!( @@ -49,7 +41,7 @@ mod utxosdb_utxosview_impls { e)) } - pub fn estimated_size(db: &S) -> Option { + pub fn estimated_size(_db: &S) -> Option { None } @@ -105,16 +97,13 @@ impl<'a, S: UtxosStorageWrite> UtxosView for UtxosDBMut<'a, S> { } impl<'a, S: UtxosStorageWrite> FlushableUtxoView for UtxosDBMut<'a, S> { - fn batch_write( - &mut self, - utxos: crate::utxo_impl::ConsumedUtxoCache, - ) -> Result<(), crate::Error> { + fn batch_write(&mut self, utxos: ConsumedUtxoCache) -> Result<(), crate::Error> { // check each entry if it's dirty. Only then will the db be updated. for (key, entry) in utxos.container { let outpoint = &key; if entry.is_dirty() { if let Some(utxo) = entry.utxo() { - self.0.set_utxo(outpoint, utxo)?; + self.0.set_utxo(outpoint, utxo.clone())?; } else { // entry is spent self.0.del_utxo(outpoint)?; diff --git a/utxo/src/tests/mod.rs b/utxo/src/tests/mod.rs new file mode 100644 index 0000000000..05cfb083db --- /dev/null +++ b/utxo/src/tests/mod.rs @@ -0,0 +1,503 @@ +// Copyright (c) 2022 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod simulation; +pub mod test_helper; + +use crate::{ + flush_to_base, + tests::test_helper::Presence::{self, *}, + utxo_entry::{IsDirty, IsFresh, UtxoEntry}, + ConsumedUtxoCache, + Error::{self, *}, + FlushableUtxoView, Utxo, UtxoSource, UtxosCache, UtxosView, +}; +use common::{ + chain::{OutPoint, OutPointSourceId, Transaction, TxInput}, + primitives::{BlockHeight, Id, Idable, H256}, +}; +use crypto::random::{seq, Rng}; +use itertools::Itertools; +use rstest::rstest; +use std::collections::BTreeMap; +use test_utils::random::{make_seedable_rng, Seed}; + +/// Checks `add_utxo` method behaviour. +/// # Arguments +/// `cache_presence` - initial state of the cache +/// `cache_flags` - The flags of the existing utxo entry for testing +/// `possible_overwrite` - to set the `possible_overwrite` of the `add_utxo` method +/// `result_flags` - the result ( dirty/not, fresh/not ) after calling the `add_utxo` method. +/// `op_result` - the result of calling `add_utxo` method, whether it succeeded or not. +fn check_add_utxo( + rng: &mut impl Rng, + cache_presence: Presence, + cache_flags: Option<(IsFresh, IsDirty)>, + possible_overwrite: bool, + result_flags: Option<(IsFresh, IsDirty)>, + op_result: Result<(), Error>, +) { + let mut cache = UtxosCache::new_for_test(H256::random().into()); + let (_, outpoint) = + test_helper::insert_single_entry(rng, &mut cache, cache_presence, cache_flags, None); + + // perform the add_utxo. + let (utxo, _) = test_helper::create_utxo(rng, 0); + let add_result = cache.add_utxo(&outpoint, utxo, possible_overwrite); + + assert_eq!(add_result, op_result); + + if add_result.is_ok() { + let key = &outpoint; + let ret_value = cache.utxos.get(key); + + test_helper::check_flags(ret_value, result_flags, false); + } +} + +/// Checks `spend_utxo` method behaviour. +/// # Arguments +/// `parent_presence` - initial state of the parent cache. +/// `cache_presence` - initial state of the cache. +/// `cache_flags` - The flags of a utxo entry in a cache. +/// `result_flags` - the result ( dirty/not, fresh/not ) after performing `spend_utxo`. +fn check_spend_utxo( + rng: &mut impl Rng, + parent_presence: Presence, + cache_presence: Presence, + cache_flags: Option<(IsFresh, IsDirty)>, + spend_result: Result<(), Error>, + result_flags: Option<(IsFresh, IsDirty)>, +) { + // initialize the parent cache. + let mut parent = UtxosCache::new_for_test(H256::random().into()); + let (_, parent_outpoint) = test_helper::insert_single_entry( + rng, + &mut parent, + parent_presence, + // parent flags are irrelevant, but this combination can be used for both spent/unspent + Some((IsFresh::No, IsDirty::Yes)), + None, + ); + + // initialize the child cache + let mut child = match parent_presence { + Absent => UtxosCache::new_for_test(H256::random().into()), + _ => UtxosCache::new(&parent), + }; + + let (_, child_outpoint) = test_helper::insert_single_entry( + rng, + &mut child, + cache_presence, + cache_flags, + Some(parent_outpoint), + ); + + // perform the spend_utxo + let res = child.spend_utxo(&child_outpoint); + + assert_eq!(spend_result.map(|_| ()), res.map(|_| ())); + + let key = &child_outpoint; + let ret_value = child.utxos.get(key); + + test_helper::check_flags(ret_value, result_flags, true); +} + +/// Checks `batch_write` method behaviour. +/// # Arguments +/// `parent_presence` - initial state of the parent cache. +/// `parent_flags` - The flags of a utxo entry in the parent. None if the parent is empty. +/// `child_presence` - To determine whether or not a utxo entry will be written to parent. +/// `child_flags` - The flags of a utxo entry indicated by the `child_presence`. None if presence is Absent. +/// `result` - The result of the parent after performing the `batch_write`. +/// `result_flags` - the pair of `result`, indicating whether it is dirty/not, fresh/not or nothing at all. +fn check_write_utxo( + rng: &mut impl Rng, + parent_presence: Presence, + child_presence: Presence, + result: Result, + parent_flags: Option<(IsFresh, IsDirty)>, + child_flags: Option<(IsFresh, IsDirty)>, + result_flags: Option<(IsFresh, IsDirty)>, +) { + //initialize the parent cache + let mut parent = UtxosCache::new_for_test(H256::random().into()); + let (_, outpoint) = + test_helper::insert_single_entry(rng, &mut parent, parent_presence, parent_flags, None); + let key = &outpoint; + + // prepare the map for batch write. + let mut single_entry_map = BTreeMap::new(); + + // inserts utxo in the map + if let Some((is_fresh, is_dirty)) = child_flags { + match child_presence { + Absent => { + panic!("Please use `Present` or `Spent` presence when child flags are specified."); + } + Present => { + let (utxo, _) = test_helper::create_utxo(rng, 0); + let entry = UtxoEntry::new(Some(utxo), is_fresh, is_dirty); + single_entry_map.insert(key.clone(), entry); + } + Spent => { + let entry = UtxoEntry::new(None, is_fresh, is_dirty); + single_entry_map.insert(key.clone(), entry); + } + } + } + + // perform batch write + let single_entry_cache = ConsumedUtxoCache { + container: single_entry_map, + best_block: Id::new(H256::random()), + }; + let res = parent.batch_write(single_entry_cache); + let entry = parent.utxos.get(key); + + match result { + Ok(result_presence) => { + match result_presence { + Absent => { + // no need to check for the flags, it's empty. + assert!(entry.is_none()); + } + other => test_helper::check_flags(entry, result_flags, !(other == Present)), + } + } + Err(e) => { + assert_eq!(res, Err(e)); + } + } +} + +/// Checks the `get_mut_utxo` method behaviour. +fn check_get_mut_utxo( + rng: &mut impl Rng, + parent_presence: Presence, + cache_presence: Presence, + result_presence: Presence, + cache_flags: Option<(IsFresh, IsDirty)>, + result_flags: Option<(IsFresh, IsDirty)>, +) { + let mut parent = UtxosCache::new_for_test(H256::random().into()); + let (parent_utxo, parent_outpoint) = test_helper::insert_single_entry( + rng, + &mut parent, + parent_presence, + // parent flags are irrelevant, but this combination can be used for both spent/unspent + Some((IsFresh::No, IsDirty::Yes)), + None, + ); + + let mut child = match parent_presence { + Absent => UtxosCache::new_for_test(H256::random().into()), + _ => UtxosCache::new(&parent), + }; + let (child_utxo, child_outpoint) = test_helper::insert_single_entry( + rng, + &mut child, + cache_presence, + cache_flags, + Some(parent_outpoint), + ); + let key = &child_outpoint; + + let mut expected_utxo: Option = None; + { + // perform the get_mut_utxo + let utxo_opt = child.get_mut_utxo(&child_outpoint); + + if let Some(utxo) = utxo_opt { + match cache_presence { + Absent => { + // utxo should be similar to the parent's. + assert_eq!(&parent_utxo, utxo); + } + _ => { + // utxo should be similar to the child's. + assert_eq!(&child_utxo, utxo); + } + } + + // let's try to update the utxo. + let old_height_num = match utxo.source() { + UtxoSource::Blockchain(h) => h, + UtxoSource::Mempool => panic!("Unexpected arm"), + }; + let new_height_num = + old_height_num.checked_add(1).expect("should be able to increment"); + let new_height = UtxoSource::Blockchain(new_height_num); + + utxo.set_height(new_height.clone()); + assert_eq!(new_height, *utxo.source()); + assert_eq!( + new_height_num, + utxo.source().blockchain_height().expect("Must be a height") + ); + expected_utxo = Some(utxo.clone()); + } + } + + let entry = child.utxos.get(key); + match result_presence { + Absent => { + assert!(entry.is_none()); + } + other => { + // check whether the update actually happened. + if let Some(expected_utxo) = expected_utxo { + let actual_utxo_entry = &entry.expect("should have an existing entry"); + let actual_utxo = actual_utxo_entry.utxo().expect("should have an existing utxo."); + assert_eq!(expected_utxo, *actual_utxo); + } + test_helper::check_flags(entry, result_flags, !(other == Present)) + } + } +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +#[rustfmt::skip] +fn add_utxo_test(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); + /* + CACHE CACHE Flags Possible RESULT flags RESULT of `add_utxo` method + PRESENCE Overwrite + */ + check_add_utxo(&mut rng, Absent, None, false, Some((IsFresh::Yes, IsDirty::Yes)), Ok(())); + check_add_utxo(&mut rng, Absent, None, true, Some((IsFresh::No, IsDirty::Yes)), Ok(())); + + check_add_utxo(&mut rng, Spent, Some((IsFresh::Yes, IsDirty::No)), false, Some((IsFresh::Yes, IsDirty::Yes)), Ok(())); + check_add_utxo(&mut rng, Spent, Some((IsFresh::Yes, IsDirty::No)), true, Some((IsFresh::Yes, IsDirty::Yes)), Ok(())); + + check_add_utxo(&mut rng, Spent, Some((IsFresh::No, IsDirty::Yes)), false, Some((IsFresh::No, IsDirty::Yes)), Ok(())); + check_add_utxo(&mut rng, Spent, Some((IsFresh::No, IsDirty::Yes)), true, Some((IsFresh::No, IsDirty::Yes)), Ok(())); + + check_add_utxo(&mut rng, Present, Some((IsFresh::No, IsDirty::No)), false, None, Err(OverwritingUtxo)); + check_add_utxo(&mut rng, Present, Some((IsFresh::No, IsDirty::No)), true, Some((IsFresh::No, IsDirty::Yes)), Ok(())); + + check_add_utxo(&mut rng, Present, Some((IsFresh::No, IsDirty::Yes)), false, None, Err(OverwritingUtxo)); + check_add_utxo(&mut rng, Present, Some((IsFresh::No, IsDirty::Yes)), true, Some((IsFresh::No, IsDirty::Yes)), Ok(())); + + check_add_utxo(&mut rng, Present, Some((IsFresh::Yes, IsDirty::Yes)), false, None, Err(OverwritingUtxo)); + check_add_utxo(&mut rng, Present, Some((IsFresh::Yes, IsDirty::Yes)), true, Some((IsFresh::Yes, IsDirty::Yes)), Ok(())); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +#[rustfmt::skip] +fn spend_utxo_test(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); + /* + PARENT CACHE + PRESENCE PRESENCE CACHE Flags RESULT RESULT Flags + */ + check_spend_utxo(&mut rng, Absent, Absent, None, Err(Error::NoUtxoFound), None); + check_spend_utxo(&mut rng, Absent, Spent, Some((IsFresh::Yes, IsDirty::No)), Err(Error::UtxoAlreadySpent), None); + check_spend_utxo(&mut rng, Absent, Spent, Some((IsFresh::No, IsDirty::Yes)), Err(Error::UtxoAlreadySpent), Some((IsFresh::No, IsDirty::Yes))); + check_spend_utxo(&mut rng, Absent, Present, Some((IsFresh::No, IsDirty::No)), Ok(()), Some((IsFresh::No, IsDirty::Yes))); + check_spend_utxo(&mut rng, Absent, Present, Some((IsFresh::No, IsDirty::Yes)), Ok(()), Some((IsFresh::No, IsDirty::Yes))); + check_spend_utxo(&mut rng, Absent, Present, Some((IsFresh::Yes, IsDirty::Yes)), Ok(()), None); + check_spend_utxo(&mut rng, Spent, Absent, None, Err(Error::NoUtxoFound), None); + check_spend_utxo(&mut rng, Spent, Absent, Some((IsFresh::Yes, IsDirty::No)), Err(Error::NoUtxoFound), None); + check_spend_utxo(&mut rng, Spent, Present, Some((IsFresh::No, IsDirty::No)), Ok(()), Some((IsFresh::No, IsDirty::Yes))); + check_spend_utxo(&mut rng, Spent, Present, Some((IsFresh::No, IsDirty::Yes)), Ok(()), Some((IsFresh::No, IsDirty::Yes))); + check_spend_utxo(&mut rng, Spent, Present, Some((IsFresh::Yes, IsDirty::Yes)), Ok(()), None); + check_spend_utxo(&mut rng, Present, Absent, None, Ok(()), Some((IsFresh::No, IsDirty::Yes))); + check_spend_utxo(&mut rng, Present, Spent, Some((IsFresh::Yes, IsDirty::No)), Err(Error::UtxoAlreadySpent), None); + check_spend_utxo(&mut rng, Present, Spent, Some((IsFresh::No, IsDirty::Yes)), Err(Error::UtxoAlreadySpent), Some((IsFresh::No, IsDirty::Yes))); + check_spend_utxo(&mut rng, Present, Present, Some((IsFresh::No, IsDirty::No)), Ok(()), Some((IsFresh::No, IsDirty::Yes))); + check_spend_utxo(&mut rng, Present, Present, Some((IsFresh::No, IsDirty::Yes)), Ok(()), Some((IsFresh::No, IsDirty::Yes))); + check_spend_utxo(&mut rng, Present, Present, Some((IsFresh::Yes, IsDirty::Yes)), Ok(()), None); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +#[rustfmt::skip] +fn batch_write_test(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); + /* + PARENT CACHE RESULT + PRESENCE PRESENCE PRESENCE PARENT Flags CACHE Flags RESULT Flags + */ + check_write_utxo(&mut rng, Absent, Absent, Ok(Absent), None, None, None); + check_write_utxo(&mut rng, Absent, Spent , Ok(Spent), None, Some((IsFresh::No, IsDirty::Yes)), Some((IsFresh::No, IsDirty::Yes))); + check_write_utxo(&mut rng, Absent, Present, Ok(Present), None, Some((IsFresh::No, IsDirty::Yes)), Some((IsFresh::No, IsDirty::Yes))); + check_write_utxo(&mut rng, Absent, Present, Ok(Present), None, Some((IsFresh::Yes, IsDirty::Yes)), Some((IsFresh::Yes, IsDirty::Yes))); + check_write_utxo(&mut rng, Spent , Absent, Ok(Spent), Some((IsFresh::Yes, IsDirty::No)), None, Some((IsFresh::Yes, IsDirty::No))); + check_write_utxo(&mut rng, Spent , Absent, Ok(Spent), Some((IsFresh::No, IsDirty::Yes)), None, Some((IsFresh::No, IsDirty::Yes))); + check_write_utxo(&mut rng, Spent , Spent , Ok(Absent), Some((IsFresh::Yes, IsDirty::No)), Some((IsFresh::No, IsDirty::Yes)), None); + check_write_utxo(&mut rng, Spent , Spent , Ok(Spent), Some((IsFresh::No, IsDirty::Yes)), Some((IsFresh::No, IsDirty::Yes)), Some((IsFresh::No, IsDirty::Yes))); + check_write_utxo(&mut rng, Spent , Present, Ok(Present), Some((IsFresh::Yes, IsDirty::No)), Some((IsFresh::No, IsDirty::Yes)), Some((IsFresh::Yes, IsDirty::Yes))); + check_write_utxo(&mut rng, Spent , Present, Ok(Present), Some((IsFresh::Yes, IsDirty::No)), Some((IsFresh::Yes, IsDirty::Yes)), Some((IsFresh::Yes, IsDirty::Yes))); + check_write_utxo(&mut rng, Spent , Present, Ok(Present), Some((IsFresh::No, IsDirty::Yes)), Some((IsFresh::No, IsDirty::Yes)), Some((IsFresh::No, IsDirty::Yes))); + check_write_utxo(&mut rng, Spent , Present, Ok(Present), Some((IsFresh::No, IsDirty::Yes)), Some((IsFresh::Yes, IsDirty::Yes)), Some((IsFresh::No, IsDirty::Yes))); + check_write_utxo(&mut rng, Present, Absent, Ok(Present), Some((IsFresh::No, IsDirty::No)), None, Some((IsFresh::No, IsDirty::No))); + check_write_utxo(&mut rng, Present, Absent, Ok(Present), Some((IsFresh::No, IsDirty::Yes)), None, Some((IsFresh::No, IsDirty::Yes))); + check_write_utxo(&mut rng, Present, Absent, Ok(Present), Some((IsFresh::Yes, IsDirty::Yes)), None , Some((IsFresh::Yes, IsDirty::Yes))); + check_write_utxo(&mut rng, Present, Spent , Ok(Spent), Some((IsFresh::No, IsDirty::No)), Some((IsFresh::No, IsDirty::Yes)), Some((IsFresh::No, IsDirty::Yes))); + check_write_utxo(&mut rng, Present, Spent , Ok(Spent), Some((IsFresh::No, IsDirty::Yes)), Some((IsFresh::No, IsDirty::Yes)), Some((IsFresh::No, IsDirty::Yes))); + check_write_utxo(&mut rng, Present, Spent , Ok(Absent), Some((IsFresh::Yes, IsDirty::Yes)), Some((IsFresh::No, IsDirty::Yes)), None); + check_write_utxo(&mut rng, Present, Present, Ok(Present), Some((IsFresh::No, IsDirty::No)), Some((IsFresh::No, IsDirty::Yes)), Some((IsFresh::No, IsDirty::Yes))); + check_write_utxo(&mut rng, Present, Present, Err(FreshUtxoAlreadyExists), Some((IsFresh::No, IsDirty::No)), Some((IsFresh::Yes, IsDirty::Yes)), None); + check_write_utxo(&mut rng, Present, Present, Ok(Present), Some((IsFresh::No, IsDirty::Yes)), Some((IsFresh::No, IsDirty::Yes)), Some((IsFresh::No, IsDirty::Yes))); + check_write_utxo(&mut rng, Present, Present, Err(FreshUtxoAlreadyExists), Some((IsFresh::No, IsDirty::Yes)), Some((IsFresh::Yes, IsDirty::Yes)), None); + check_write_utxo(&mut rng, Present, Present, Ok(Present), Some((IsFresh::Yes, IsDirty::Yes)), Some((IsFresh::No, IsDirty::Yes)), Some((IsFresh::Yes, IsDirty::Yes))); + check_write_utxo(&mut rng, Present, Present, Err(FreshUtxoAlreadyExists), Some((IsFresh::Yes, IsDirty::Yes)), Some((IsFresh::Yes, IsDirty::Yes)), None); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +#[rustfmt::skip] +fn access_utxo_test(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); + /* + PARENT CACHE RESULT CACHE + PRESENCE PRESENCE PRESENCE Flags RESULT Flags + */ + check_get_mut_utxo(&mut rng, Absent, Absent, Absent, None, None); + check_get_mut_utxo(&mut rng, Absent, Spent , Spent , Some((IsFresh::Yes, IsDirty::No)), Some((IsFresh::Yes, IsDirty::No))); + check_get_mut_utxo(&mut rng, Absent, Spent , Spent , Some((IsFresh::No, IsDirty::Yes)), Some((IsFresh::No, IsDirty::Yes))); + check_get_mut_utxo(&mut rng, Absent, Present, Present, Some((IsFresh::No, IsDirty::No)), Some((IsFresh::No, IsDirty::No))); + check_get_mut_utxo(&mut rng, Absent, Present, Present, Some((IsFresh::No, IsDirty::Yes)), Some((IsFresh::No, IsDirty::Yes))); + check_get_mut_utxo(&mut rng, Absent, Present, Present, Some((IsFresh::Yes, IsDirty::Yes)), Some((IsFresh::Yes, IsDirty::Yes))); + check_get_mut_utxo(&mut rng, Spent , Absent, Absent, None, None); + check_get_mut_utxo(&mut rng, Spent , Spent , Spent , Some((IsFresh::Yes, IsDirty::No)), Some((IsFresh::Yes, IsDirty::No))); + check_get_mut_utxo(&mut rng, Spent , Spent , Spent , Some((IsFresh::No, IsDirty::Yes)), Some((IsFresh::No, IsDirty::Yes))); + check_get_mut_utxo(&mut rng, Spent , Present, Present, Some((IsFresh::No, IsDirty::No)), Some((IsFresh::No, IsDirty::No))); + check_get_mut_utxo(&mut rng, Spent , Present, Present, Some((IsFresh::No, IsDirty::Yes)), Some((IsFresh::No, IsDirty::Yes))); + check_get_mut_utxo(&mut rng, Spent , Present, Present, Some((IsFresh::Yes, IsDirty::Yes)), Some((IsFresh::Yes, IsDirty::Yes))); + check_get_mut_utxo(&mut rng, Present, Absent, Present, None, Some((IsFresh::No, IsDirty::No))); + check_get_mut_utxo(&mut rng, Present, Spent , Spent , Some((IsFresh::Yes, IsDirty::No)), Some((IsFresh::Yes, IsDirty::No))); + check_get_mut_utxo(&mut rng, Present, Spent , Spent , Some((IsFresh::No, IsDirty::Yes)), Some((IsFresh::No, IsDirty::Yes))); + check_get_mut_utxo(&mut rng, Present, Present, Present, Some((IsFresh::No, IsDirty::No)), Some((IsFresh::No, IsDirty::No))); + check_get_mut_utxo(&mut rng, Present, Present, Present, Some((IsFresh::No, IsDirty::Yes)), Some((IsFresh::No, IsDirty::Yes))); + check_get_mut_utxo(&mut rng, Present, Present, Present, Some((IsFresh::Yes, IsDirty::Yes)), Some((IsFresh::Yes, IsDirty::Yes))); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn derive_cache_test(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); + let mut cache = UtxosCache::new_for_test(H256::random().into()); + + let (utxo, outpoint_1) = test_helper::create_utxo(&mut rng, 10); + assert!(cache.add_utxo(&outpoint_1, utxo, false).is_ok()); + + let (utxo, outpoint_2) = test_helper::create_utxo(&mut rng, 20); + assert!(cache.add_utxo(&outpoint_2, utxo, false).is_ok()); + + let mut extra_cache = cache.derive_cache(); + assert!(extra_cache.utxos.is_empty()); + + assert!(extra_cache.has_utxo(&outpoint_1)); + assert!(extra_cache.has_utxo(&outpoint_2)); + + let (utxo, outpoint) = test_helper::create_utxo(&mut rng, 30); + assert!(extra_cache.add_utxo(&outpoint, utxo, true).is_ok()); + + assert!(!cache.has_utxo(&outpoint)); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn blockchain_or_mempool_utxo_test(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); + let mut cache = UtxosCache::new_for_test(H256::random().into()); + + let (utxo, outpoint_1) = test_helper::create_utxo(&mut rng, 10); + assert!(cache.add_utxo(&outpoint_1, utxo, false).is_ok()); + + let (utxo, outpoint_2) = test_helper::create_utxo_for_mempool(&mut rng); + assert!(cache.add_utxo(&outpoint_2, utxo, false).is_ok()); + + let res = cache.utxo(&outpoint_2).expect("should contain utxo"); + assert!(res.source().is_mempool()); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn multiple_update_utxos_test(#[case] seed: Seed) { + use common::chain::signature::inputsig::InputWitness; + + let mut rng = make_seedable_rng(seed); + let mut cache = UtxosCache::new_for_test(H256::random().into()); + + // let's test `add_utxos` + let tx = Transaction::new( + 0x00, + vec![], + test_helper::create_tx_outputs(&mut rng, 10), + 0x01, + ) + .unwrap(); + assert!(cache.add_utxos(&tx, UtxoSource::Blockchain(BlockHeight::new(2)), false).is_ok()); + + // check that the outputs of tx are added in the cache. + tx.outputs().iter().enumerate().for_each(|(i, x)| { + let id = OutPointSourceId::from(tx.get_id()); + let outpoint = OutPoint::new(id, i as u32); + + let utxo = cache.utxo(&outpoint).expect("utxo should exist"); + assert_eq!(utxo.output(), x); + }); + + // let's spend some outputs.; + // randomly take half of the outputs to spend. + let results = + seq::index::sample(&mut rng, tx.outputs().len(), tx.outputs().len() / 2).into_vec(); + let to_spend = results + .into_iter() + .map(|idx| { + let id = OutPointSourceId::from(tx.get_id()); + TxInput::new(id, idx as u32, InputWitness::NoSignature(None)) + }) + .collect_vec(); + + // create a new transaction + let new_tx = Transaction::new(0x00, to_spend.clone(), vec![], 0).expect("should succeed"); + // let's test `spend_utxos` + let tx_undo = cache.spend_utxos(&new_tx, BlockHeight::new(2)).expect("should return txundo"); + + // check that these utxos came from the tx's output + tx_undo.inner().iter().for_each(|x| { + assert!(tx.outputs().contains(x.output())); + }); + + // check that the spent utxos should not exist in the cache anymore. + to_spend.iter().for_each(|input| { + assert!(cache.utxo(input.outpoint()).is_none()); + }); +} + +#[test] +fn check_best_block_after_flush() { + let mut cache1 = UtxosCache::new_for_test(H256::random().into()); + let cache2 = UtxosCache::new_for_test(H256::random().into()); + assert_ne!(cache1.best_block_hash(), cache2.best_block_hash()); + let expected_hash = cache2.best_block_hash(); + assert!(flush_to_base(cache2, &mut cache1).is_ok()); + assert_eq!(expected_hash, cache1.best_block_hash()); +} diff --git a/utxo/src/utxo_impl/simulation.rs b/utxo/src/tests/simulation.rs similarity index 96% rename from utxo/src/utxo_impl/simulation.rs rename to utxo/src/tests/simulation.rs index 9c64bfadbc..e2cf7aea9d 100644 --- a/utxo/src/utxo_impl/simulation.rs +++ b/utxo/src/tests/simulation.rs @@ -13,7 +13,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::{utxo_impl::test_helper::create_utxo, FlushableUtxoView, Utxo, UtxosCache, UtxosView}; +use super::test_helper::create_utxo; +use crate::{FlushableUtxoView, Utxo, UtxosCache, UtxosView}; use common::{chain::OutPoint, primitives::H256}; use crypto::random::Rng; use rstest::rstest; @@ -129,10 +130,10 @@ fn populate_cache( if i % 10 == 0 { if rng.gen::() && prev_result.len() > 1 { let idx = rng.gen_range(0..prev_result.len()); - cache.uncache(&prev_result[idx].0); + let _ = cache.uncache(&prev_result[idx].0); } else if result.len() > 1 { let idx = rng.gen_range(0..result.len()); - cache.uncache(&result[idx].0); + let _ = cache.uncache(&result[idx].0); } removed_an_entry = true; } diff --git a/utxo/src/utxo_impl/test_helper.rs b/utxo/src/tests/test_helper.rs similarity index 78% rename from utxo/src/utxo_impl/test_helper.rs rename to utxo/src/tests/test_helper.rs index 4769772453..256cd6fe6e 100644 --- a/utxo/src/utxo_impl/test_helper.rs +++ b/utxo/src/tests/test_helper.rs @@ -13,31 +13,30 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::{Utxo, UtxoEntry, UtxosCache}; -use common::chain::signature::inputsig::InputWitness; -use common::chain::tokens::OutputValue; -use common::chain::{ - Destination, GenBlock, OutPoint, OutPointSourceId, OutputPurpose, Transaction, TxInput, - TxOutput, +use crate::{ + utxo_entry::{IsDirty, IsFresh, UtxoEntry}, + Utxo, UtxosCache, +}; +use common::{ + chain::{ + signature::inputsig::InputWitness, tokens::OutputValue, Destination, GenBlock, OutPoint, + OutPointSourceId, OutputPurpose, Transaction, TxInput, TxOutput, + }, + primitives::{Amount, BlockHeight, Id, H256}, +}; +use crypto::{ + key::{KeyKind, PrivateKey}, + random::{seq, Rng}, }; -use common::primitives::{Amount, BlockHeight, Id, H256}; -use crypto::key::{KeyKind, PrivateKey}; -use crypto::random::{seq, Rng}; use itertools::Itertools; -pub const FRESH: u8 = 1; -pub const DIRTY: u8 = 2; - -#[derive(Clone, Eq, PartialEq)] +#[derive(Clone, Copy, Eq, PartialEq)] pub enum Presence { Absent, Present, Spent, } -use crate::UtxoStatus; -use Presence::{Absent, Present, Spent}; - pub fn create_tx_outputs(rng: &mut impl Rng, size: u32) -> Vec { let mut tx_outputs = vec![]; for _ in 0..size { @@ -73,7 +72,7 @@ pub fn convert_to_utxo(output: TxOutput, height: u64, output_idx: usize) -> (Out let utxo_id: Id = Id::new(H256::random()); let id = OutPointSourceId::BlockReward(utxo_id); let outpoint = OutPoint::new(id, output_idx as u32); - let utxo = Utxo::new(output, true, BlockHeight::new(height)); + let utxo = Utxo::new_for_blockchain(output, true, BlockHeight::new(height)); (outpoint, utxo) } @@ -100,7 +99,7 @@ fn inner_create_utxo(rng: &mut impl Rng, block_height: Option) -> (Utxo, Ou // generate utxo let utxo = match block_height { None => Utxo::new_for_mempool(output, is_block_reward), - Some(height) => Utxo::new(output, is_block_reward, BlockHeight::new(height)), + Some(height) => Utxo::new_for_blockchain(output, is_block_reward, BlockHeight::new(height)), }; // create the id based on the `is_block_reward` value. @@ -129,8 +128,8 @@ fn inner_create_utxo(rng: &mut impl Rng, block_height: Option) -> (Utxo, Ou pub fn insert_single_entry( rng: &mut impl Rng, cache: &mut UtxosCache, - cache_presence: &Presence, - cache_flags: Option, + cache_presence: Presence, + cache_flags: Option<(IsFresh, IsDirty)>, outpoint: Option, ) -> (Utxo, OutPoint) { let rng_height = rng.gen_range(0..(u64::MAX - 1)); @@ -139,21 +138,14 @@ pub fn insert_single_entry( let key = &outpoint; match cache_presence { - Absent => { + Presence::Absent => { // there shouldn't be an existing entry. Don't bother with the cache flags. } other => { - let flags = cache_flags.expect("please provide flags."); - let is_dirty = (flags & DIRTY) == DIRTY; - let is_fresh = (flags & FRESH) == FRESH; - + let (is_fresh, is_dirty) = cache_flags.expect("please provide flags."); let entry = match other { - Present => UtxoEntry::new(utxo.clone(), is_fresh, is_dirty), - Spent => UtxoEntry { - status: UtxoStatus::Spent, - is_dirty, - is_fresh, - }, + Presence::Present => UtxoEntry::new(Some(utxo.clone()), is_fresh, is_dirty), + Presence::Spent => UtxoEntry::new(None, is_fresh, is_dirty), _ => { panic!("something wrong in the code.") } @@ -170,14 +162,14 @@ pub fn insert_single_entry( /// checks the dirty, fresh, and spent flags. pub(crate) fn check_flags( result_entry: Option<&UtxoEntry>, - expected_flags: Option, + expected_flags: Option<(IsFresh, IsDirty)>, is_spent: bool, ) { - if let Some(flags) = expected_flags { + if let Some((is_fresh, is_dirty)) = expected_flags { let result_entry = result_entry.expect("this should have an entry inside"); - assert_eq!(result_entry.is_dirty(), (flags & DIRTY) == DIRTY); - assert_eq!(result_entry.is_fresh(), (flags & FRESH) == FRESH); + assert_eq!(IsDirty::from(result_entry.is_dirty()), is_dirty); + assert_eq!(IsFresh::from(result_entry.is_fresh()), is_fresh); assert_eq!(result_entry.is_spent(), is_spent); } else { assert!(result_entry.is_none()); diff --git a/utxo/src/undo.rs b/utxo/src/undo.rs index 31659c0e66..980a2472de 100644 --- a/utxo/src/undo.rs +++ b/utxo/src/undo.rs @@ -13,8 +13,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -#![allow(unused, dead_code)] - use crate::Utxo; use common::primitives::BlockHeight; use serialization::{Decode, Encode}; @@ -74,8 +72,7 @@ impl BlockUndo { #[cfg(test)] pub mod test { use super::*; - use crate::test_helper::create_utxo; - use crypto::random::Rng; + use crate::tests::test_helper::create_utxo; use rstest::rstest; use test_utils::random::{make_seedable_rng, Seed}; diff --git a/utxo/src/utxo.rs b/utxo/src/utxo.rs new file mode 100644 index 0000000000..5f478463ed --- /dev/null +++ b/utxo/src/utxo.rs @@ -0,0 +1,98 @@ +// Copyright (c) 2022 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::Error; +use common::{chain::TxOutput, primitives::BlockHeight}; +use serialization::{Decode, Encode}; +use std::fmt::Debug; + +/// Determines whether the utxo is for the blockchain of for mempool +#[derive(Debug, Clone, Eq, PartialEq, Encode, Decode)] +pub enum UtxoSource { + /// At which height this containing tx was included in the active block chain + Blockchain(BlockHeight), + Mempool, +} + +impl UtxoSource { + pub fn is_mempool(&self) -> bool { + match self { + UtxoSource::Blockchain(_) => false, + UtxoSource::Mempool => true, + } + } + + pub fn blockchain_height(&self) -> Result { + match self { + UtxoSource::Blockchain(h) => Ok(*h), + UtxoSource::Mempool => Err(crate::Error::NoBlockchainHeightFound), + } + } +} + +/// The Unspent Transaction Output +#[derive(Debug, Clone, Eq, PartialEq, Encode, Decode)] +pub struct Utxo { + output: TxOutput, + is_block_reward: bool, + /// identifies whether the utxo is for the blockchain or for mempool. + source: UtxoSource, +} + +impl Utxo { + pub fn new(output: TxOutput, is_block_reward: bool, source: UtxoSource) -> Self { + Self { + output, + is_block_reward, + source, + } + } + + pub fn new_for_blockchain( + output: TxOutput, + is_block_reward: bool, + height: BlockHeight, + ) -> Self { + Self { + output, + is_block_reward, + source: UtxoSource::Blockchain(height), + } + } + + pub fn new_for_mempool(output: TxOutput, is_block_reward: bool) -> Self { + Self { + output, + is_block_reward, + source: UtxoSource::Mempool, + } + } + + pub fn is_block_reward(&self) -> bool { + self.is_block_reward + } + + pub fn source(&self) -> &UtxoSource { + &self.source + } + + pub fn output(&self) -> &TxOutput { + &self.output + } + + pub fn set_height(&mut self, value: UtxoSource) { + self.source = value + } +} diff --git a/utxo/src/utxo_entry.rs b/utxo/src/utxo_entry.rs new file mode 100644 index 0000000000..6a5747dbf5 --- /dev/null +++ b/utxo/src/utxo_entry.rs @@ -0,0 +1,177 @@ +// Copyright (c) 2022 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::Utxo; +use serialization::{Decode, Encode}; +use std::fmt::Debug; + +/// Tells the state of the utxo +#[derive(Debug, Clone, Eq, PartialEq, Encode, Decode)] +#[allow(clippy::large_enum_variant)] +pub enum UtxoStatus { + Spent, + Entry(Utxo), +} + +/// The utxo entry is fresh when the parent does not have this utxo or +/// if it exists in parent but not in current cache. +#[derive(Debug, Clone, Eq, PartialEq, Encode, Decode)] +pub enum IsFresh { + Yes, + No, +} + +impl From for IsFresh { + fn from(v: bool) -> Self { + if v { + IsFresh::Yes + } else { + IsFresh::No + } + } +} + +/// The utxo entry is dirty when this version is different from the parent. +#[derive(Debug, Clone, Eq, PartialEq, Encode, Decode)] +pub enum IsDirty { + Yes, + No, +} + +impl From for IsDirty { + fn from(v: bool) -> Self { + if v { + IsDirty::Yes + } else { + IsDirty::No + } + } +} + +/// Just the Utxo with additional information. +#[derive(Debug, Clone, Eq, PartialEq, Encode, Decode)] +pub struct UtxoEntry { + status: UtxoStatus, + is_fresh: IsFresh, + is_dirty: IsDirty, +} + +impl UtxoEntry { + pub fn new(utxo: Option, is_fresh: IsFresh, is_dirty: IsDirty) -> UtxoEntry { + let entry = UtxoEntry { + status: match utxo { + Some(utxo) => UtxoStatus::Entry(utxo), + None => UtxoStatus::Spent, + }, + is_fresh, + is_dirty, + }; + + // Out of these 2^3 = 8 states, only some combinations are valid: + // - unspent, FRESH, DIRTY (e.g. a new utxo created in the cache) + // - unspent, not FRESH, DIRTY (e.g. a utxo changed in the cache during a reorg) + // - unspent, not FRESH, not DIRTY (e.g. an unspent utxo fetched from the parent cache) + // - spent, FRESH, not DIRTY (e.g. a spent utxo fetched from the parent cache) + // - spent, not FRESH, DIRTY (e.g. a utxo is spent and spentness needs to be flushed to the parent) + match &entry.status { + UtxoStatus::Entry(_) => assert!(!entry.is_fresh() || entry.is_dirty()), + &UtxoStatus::Spent => assert!(entry.is_fresh() ^ entry.is_dirty()), + } + + entry + } + + pub fn is_dirty(&self) -> bool { + match self.is_dirty { + IsDirty::Yes => true, + IsDirty::No => false, + } + } + + pub fn is_fresh(&self) -> bool { + match self.is_fresh { + IsFresh::Yes => true, + IsFresh::No => false, + } + } + + pub fn is_spent(&self) -> bool { + self.status == UtxoStatus::Spent + } + + pub fn utxo(&self) -> Option<&Utxo> { + match &self.status { + UtxoStatus::Spent => None, + UtxoStatus::Entry(utxo) => Some(utxo), + } + } + + pub fn utxo_mut(&mut self) -> Option<&mut Utxo> { + match &mut self.status { + UtxoStatus::Spent => None, + UtxoStatus::Entry(utxo) => Some(utxo), + } + } + + pub fn take_utxo(self) -> Option { + match self.status { + UtxoStatus::Spent => None, + UtxoStatus::Entry(utxo) => Some(utxo), + } + } +} + +#[cfg(test)] +mod unit_test { + use super::*; + use crate::UtxoSource; + use common::{ + chain::{tokens::OutputValue, Destination, OutputPurpose, TxOutput}, + primitives::Amount, + }; + use rstest::rstest; + + fn some_utxo() -> Option { + Some(Utxo::new( + TxOutput::new( + OutputValue::Coin(Amount::from_atoms(1)), + OutputPurpose::Transfer(Destination::AnyoneCanSpend), + ), + false, + UtxoSource::Mempool, + )) + } + + #[rustfmt::skip] + #[rstest] + #[case(some_utxo(), IsFresh::Yes, IsDirty::Yes)] + #[case(some_utxo(), IsFresh::No, IsDirty::Yes)] + #[case(some_utxo(), IsFresh::No, IsDirty::No)] + #[case(None, IsFresh::Yes, IsDirty::No)] + #[case(None, IsFresh::No, IsDirty::Yes)] + #[should_panic] + #[case(None, IsFresh::Yes, IsDirty::Yes)] + #[should_panic] + #[case(None, IsFresh::No, IsDirty::No)] + #[should_panic] + #[case(some_utxo(), IsFresh::Yes, IsDirty::No)] + fn create_utxo_entry( + #[case] utxo: Option, + #[case] is_fresh: IsFresh, + #[case] is_dirty: IsDirty, + ) { + let _ = UtxoEntry::new(utxo, is_fresh, is_dirty); + } +} diff --git a/utxo/src/utxo_impl/test.rs b/utxo/src/utxo_impl/test.rs deleted file mode 100644 index 3d770c5b35..0000000000 --- a/utxo/src/utxo_impl/test.rs +++ /dev/null @@ -1,551 +0,0 @@ -// Copyright (c) 2022 RBB S.r.l -// opensource@mintlayer.org -// SPDX-License-Identifier: MIT -// Licensed under the MIT License; -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use common::primitives::{BlockHeight, Id, Idable, H256}; - -use crate::utxo_impl::test_helper::Presence::{Absent, Present, Spent}; -use crate::Error::{self, FreshUtxoAlreadyExists, OverwritingUtxo}; -use crate::{ - flush_to_base, ConsumedUtxoCache, FlushableUtxoView, Utxo, UtxoEntry, UtxosCache, UtxosView, -}; - -use crate::test_helper::create_tx_outputs; -use crate::utxo_impl::test_helper::{ - check_flags, create_utxo, create_utxo_for_mempool, insert_single_entry, Presence, DIRTY, FRESH, -}; -use crate::utxo_impl::{UtxoSource, UtxoStatus}; -use common::chain::{OutPoint, OutPointSourceId, Transaction, TxInput}; -use crypto::random::{seq, Rng}; -use itertools::Itertools; -use rstest::rstest; -use std::collections::BTreeMap; -use test_utils::random::{make_seedable_rng, Seed}; - -/// Checks `add_utxo` method behaviour. -/// # Arguments -/// `cache_presence` - initial state of the cache -/// `cache_flags` - The flags of the existing utxo entry for testing -/// `possible_overwrite` - to set the `possible_overwrite` of the `add_utxo` method -/// `result_flags` - the result ( dirty/not, fresh/not ) after calling the `add_utxo` method. -/// `op_result` - the result of calling `add_utxo` method, whether it succeeded or not. -fn check_add_utxo( - rng: &mut impl Rng, - cache_presence: Presence, - cache_flags: Option, - possible_overwrite: bool, - result_flags: Option, - op_result: Result<(), Error>, -) { - let mut cache = UtxosCache::new_for_test(H256::random().into()); - let (_, outpoint) = insert_single_entry(rng, &mut cache, &cache_presence, cache_flags, None); - - // perform the add_utxo. - let (utxo, _) = create_utxo(rng, 0); - let add_result = cache.add_utxo(&outpoint, utxo, possible_overwrite); - - assert_eq!(add_result, op_result); - - if add_result.is_ok() { - let key = &outpoint; - let ret_value = cache.utxos.get(key); - - check_flags(ret_value, result_flags, false); - } -} - -/// Checks `spend_utxo` method behaviour. -/// # Arguments -/// `parent_presence` - initial state of the parent cache. -/// `cache_presence` - initial state of the cache. -/// `cache_flags` - The flags of a utxo entry in a cache. -/// `result_flags` - the result ( dirty/not, fresh/not ) after performing `spend_utxo`. -fn check_spend_utxo( - rng: &mut impl Rng, - parent_presence: Presence, - cache_presence: Presence, - cache_flags: Option, - spend_result: Result<(), Error>, - result_flags: Option, -) { - // initialize the parent cache. - let mut parent = UtxosCache::new_for_test(H256::random().into()); - let (_, parent_outpoint) = insert_single_entry( - rng, - &mut parent, - &parent_presence, - Some(FRESH | DIRTY), - None, - ); - - // initialize the child cache - let mut child = match parent_presence { - Absent => UtxosCache::new_for_test(H256::random().into()), - _ => UtxosCache::new(&parent), - }; - - let (_, child_outpoint) = insert_single_entry( - rng, - &mut child, - &cache_presence, - cache_flags, - Some(parent_outpoint), - ); - - // perform the spend_utxo - let res = child.spend_utxo(&child_outpoint); - - assert_eq!(spend_result.map(|_| ()), res.map(|_| ())); - - let key = &child_outpoint; - let ret_value = child.utxos.get(key); - - check_flags(ret_value, result_flags, true); -} - -/// Checks `batch_write` method behaviour. -/// # Arguments -/// `parent_presence` - initial state of the parent cache. -/// `parent_flags` - The flags of a utxo entry in the parent. None if the parent is empty. -/// `child_presence` - To determine whether or not a utxo entry will be written to parent. -/// `child_flags` - The flags of a utxo entry indicated by the `child_presence`. None if presence is Absent. -/// `result` - The result of the parent after performing the `batch_write`. -/// `result_flags` - the pair of `result`, indicating whether it is dirty/not, fresh/not or nothing at all. -fn check_write_utxo( - rng: &mut impl Rng, - parent_presence: Presence, - child_presence: Presence, - result: Result, - parent_flags: Option, - child_flags: Option, - result_flags: Option, -) { - //initialize the parent cache - let mut parent = UtxosCache::new_for_test(H256::random().into()); - let (_, outpoint) = insert_single_entry(rng, &mut parent, &parent_presence, parent_flags, None); - let key = &outpoint; - - // prepare the map for batch write. - let mut single_entry_map = BTreeMap::new(); - - // inserts utxo in the map - if let Some(child_flags) = child_flags { - let is_fresh = (child_flags & FRESH) == FRESH; - let is_dirty = (child_flags & DIRTY) == DIRTY; - - match child_presence { - Absent => { - panic!("Please use `Present` or `Spent` presence when child flags are specified."); - } - Present => { - let (utxo, _) = create_utxo(rng, 0); - let entry = UtxoEntry::new(utxo, is_fresh, is_dirty); - single_entry_map.insert(key.clone(), entry); - } - Spent => { - let entry = UtxoEntry { - status: UtxoStatus::Spent, - is_dirty, - is_fresh, - }; - single_entry_map.insert(key.clone(), entry); - } - } - } - - // perform batch write - let single_entry_cache = ConsumedUtxoCache { - container: single_entry_map, - best_block: Id::new(H256::random()), - }; - let res = parent.batch_write(single_entry_cache); - let entry = parent.utxos.get(key); - - match result { - Ok(result_presence) => { - match result_presence { - Absent => { - // no need to check for the flags, it's empty. - assert!(entry.is_none()); - } - other => check_flags(entry, result_flags, !(other == Present)), - } - } - Err(e) => { - assert_eq!(res, Err(e)); - } - } -} - -/// Checks the `get_mut_utxo` method behaviour. -fn check_get_mut_utxo( - rng: &mut impl Rng, - parent_presence: Presence, - cache_presence: Presence, - result_presence: Presence, - cache_flags: Option, - result_flags: Option, -) { - let mut parent = UtxosCache::new_for_test(H256::random().into()); - let (parent_utxo, parent_outpoint) = insert_single_entry( - rng, - &mut parent, - &parent_presence, - Some(FRESH | DIRTY), - None, - ); - - let mut child = match parent_presence { - Absent => UtxosCache::new_for_test(H256::random().into()), - _ => UtxosCache::new(&parent), - }; - let (child_utxo, child_outpoint) = insert_single_entry( - rng, - &mut child, - &cache_presence, - cache_flags, - Some(parent_outpoint), - ); - let key = &child_outpoint; - - let mut expected_utxo: Option = None; - { - // perform the get_mut_utxo - let utxo_opt = child.get_mut_utxo(&child_outpoint); - - if let Some(utxo) = utxo_opt { - match cache_presence { - Absent => { - // utxo should be similar to the parent's. - assert_eq!(&parent_utxo, utxo); - } - _ => { - // utxo should be similar to the child's. - assert_eq!(&child_utxo, utxo); - } - } - - // let's try to update the utxo. - let old_height_num = match utxo.source_height() { - UtxoSource::BlockChain(h) => h, - UtxoSource::MemPool => panic!("Unexpected arm"), - }; - let new_height_num = - old_height_num.checked_add(1).expect("should be able to increment"); - let new_height = UtxoSource::BlockChain(new_height_num); - - utxo.set_height(new_height.clone()); - assert_eq!(new_height, *utxo.source_height()); - assert_eq!( - new_height_num, - utxo.source_height().blockchain_height().expect("Must be a height") - ); - expected_utxo = Some(utxo.clone()); - } - } - - let entry = child.utxos.get(key); - match result_presence { - Absent => { - assert!(entry.is_none()); - } - other => { - // check whether the update actually happened. - if let Some(expected_utxo) = expected_utxo { - let actual_utxo_entry = &entry.expect("should have an existing entry"); - let actual_utxo = actual_utxo_entry.utxo().expect("should have an existing utxo."); - assert_eq!(expected_utxo, actual_utxo); - } - check_flags(entry, result_flags, !(other == Present)) - } - } -} - -#[rstest] -#[trace] -#[case(Seed::from_entropy())] -#[rustfmt::skip] -fn add_utxo_test(#[case] seed: Seed) { - let mut rng = make_seedable_rng(seed); - /* - CACHE CACHE Flags Possible RESULT flags RESULT of `add_utxo` method - PRESENCE Overwrite - */ - check_add_utxo(&mut rng, Absent, None, false, Some(FRESH | DIRTY), Ok(())); - check_add_utxo(&mut rng, Absent, None, true, Some(DIRTY), Ok(())); - - check_add_utxo(&mut rng, Spent, Some(0), false, Some(FRESH | DIRTY), Ok(())); - check_add_utxo(&mut rng, Spent, Some(0), true, Some(DIRTY), Ok(())); - - check_add_utxo(&mut rng, Spent, Some(FRESH), false, Some(FRESH | DIRTY), Ok(())); - check_add_utxo(&mut rng, Spent, Some(FRESH), true, Some(FRESH | DIRTY), Ok(())); - - check_add_utxo(&mut rng, Spent, Some(DIRTY), false, Some(DIRTY), Ok(())); - check_add_utxo(&mut rng, Spent, Some(DIRTY), true, Some(DIRTY), Ok(())); - - check_add_utxo(&mut rng, Spent, Some(FRESH | DIRTY),false, Some(FRESH | DIRTY), Ok(())); - check_add_utxo(&mut rng, Spent, Some(FRESH | DIRTY),true, Some(FRESH | DIRTY), Ok(())); - - check_add_utxo(&mut rng, Present, Some(0), false, None, Err(OverwritingUtxo)); - check_add_utxo(&mut rng, Present, Some(0), true, Some(DIRTY), Ok(())); - - check_add_utxo(&mut rng, Present, Some(FRESH), false, None, Err(OverwritingUtxo)); - check_add_utxo(&mut rng, Present, Some(FRESH), true, Some(FRESH | DIRTY), Ok(())); - - check_add_utxo(&mut rng, Present, Some(DIRTY), false, None, Err(OverwritingUtxo)); - check_add_utxo(&mut rng, Present, Some(DIRTY), true, Some(DIRTY), Ok(())); - - check_add_utxo(&mut rng, Present, Some(FRESH | DIRTY), false, None, Err(OverwritingUtxo)); - check_add_utxo(&mut rng, Present, Some(FRESH | DIRTY), true, Some(FRESH | DIRTY), Ok(())); -} - -#[rstest] -#[trace] -#[case(Seed::from_entropy())] -#[rustfmt::skip] -fn spend_utxo_test(#[case] seed: Seed) { - let mut rng = make_seedable_rng(seed); - /* - PARENT CACHE - PRESENCE PRESENCE CACHE Flags RESULT RESULT Flags - */ - check_spend_utxo(&mut rng, Absent, Absent, None, Err(Error::NoUtxoFound), None); - check_spend_utxo(&mut rng, Absent, Spent, Some(0), Err(Error::UtxoAlreadySpent), Some(DIRTY)); - check_spend_utxo(&mut rng, Absent, Spent, Some(FRESH), Err(Error::UtxoAlreadySpent), None); - check_spend_utxo(&mut rng, Absent, Spent, Some(DIRTY), Err(Error::UtxoAlreadySpent), Some(DIRTY)); - check_spend_utxo(&mut rng, Absent, Spent, Some(FRESH | DIRTY), Err(Error::UtxoAlreadySpent), None); - check_spend_utxo(&mut rng, Absent, Present, Some(0), Ok(()), Some(DIRTY)); - check_spend_utxo(&mut rng, Absent, Present, Some(FRESH), Ok(()), None); - check_spend_utxo(&mut rng, Absent, Present, Some(DIRTY), Ok(()), Some(DIRTY)); - check_spend_utxo(&mut rng, Absent, Present, Some(FRESH | DIRTY), Ok(()), None); - // this should fail, since there's nothing to remove. - check_spend_utxo(&mut rng, Spent, Absent, None, Err(Error::NoUtxoFound), None); - check_spend_utxo(&mut rng, Spent, Spent, Some(0), Err(Error::UtxoAlreadySpent), Some(DIRTY)); - // this should fail, as there's nothing to remove. - check_spend_utxo(&mut rng, Spent, Absent, Some(FRESH), Err(Error::NoUtxoFound), None); - check_spend_utxo(&mut rng, Spent, Spent, Some(DIRTY), Err(Error::UtxoAlreadySpent), Some(DIRTY)); - check_spend_utxo(&mut rng, Spent, Spent, Some(FRESH | DIRTY), Err(Error::UtxoAlreadySpent), None); - check_spend_utxo(&mut rng, Spent, Present, Some(0), Ok(()), Some(DIRTY)); - check_spend_utxo(&mut rng, Spent, Present, Some(FRESH), Ok(()), None); - check_spend_utxo(&mut rng, Spent, Present, Some(DIRTY), Ok(()), Some(DIRTY)); - check_spend_utxo(&mut rng, Spent, Present, Some(FRESH | DIRTY), Ok(()), None); - check_spend_utxo(&mut rng, Present, Absent, None, Ok(()), Some(DIRTY)); - check_spend_utxo(&mut rng, Present, Spent, Some(0), Err(Error::UtxoAlreadySpent), Some(DIRTY)); - check_spend_utxo(&mut rng, Present, Spent, Some(FRESH), Err(Error::UtxoAlreadySpent), None); - check_spend_utxo(&mut rng, Present, Spent, Some(DIRTY), Err(Error::UtxoAlreadySpent), Some(DIRTY)); - check_spend_utxo(&mut rng, Present, Spent, Some(FRESH | DIRTY), Err(Error::UtxoAlreadySpent), None); - check_spend_utxo(&mut rng, Present, Present, Some(0), Ok(()), Some(DIRTY)); - check_spend_utxo(&mut rng, Present, Present, Some(FRESH), Ok(()), None); - check_spend_utxo(&mut rng, Present, Present, Some(DIRTY), Ok(()), Some(DIRTY)); - check_spend_utxo(&mut rng, Present, Present, Some(FRESH | DIRTY), Ok(()), None); -} - -#[rstest] -#[trace] -#[case(Seed::from_entropy())] -#[rustfmt::skip] -fn batch_write_test(#[case] seed: Seed) { - let mut rng = make_seedable_rng(seed); - /* - PARENT CACHE RESULT - PRESENCE PRESENCE PRESENCE PARENT Flags CACHE Flags RESULT Flags - */ - check_write_utxo(&mut rng, Absent, Absent, Ok(Absent), None, None, None); - check_write_utxo(&mut rng, Absent, Spent , Ok(Spent), None, Some(DIRTY), Some(DIRTY)); - check_write_utxo(&mut rng, Absent, Spent , Ok(Absent), None, Some(FRESH | DIRTY), None ); - check_write_utxo(&mut rng, Absent, Present, Ok(Present), None, Some(DIRTY), Some(DIRTY)); - check_write_utxo(&mut rng, Absent, Present, Ok(Present), None, Some(FRESH | DIRTY), Some(FRESH | DIRTY)); - check_write_utxo(&mut rng, Spent , Absent, Ok(Spent), Some(0), None, Some(0)); - check_write_utxo(&mut rng, Spent , Absent, Ok(Spent), Some(FRESH), None, Some(FRESH)); - check_write_utxo(&mut rng, Spent , Absent, Ok(Spent), Some(DIRTY), None, Some(DIRTY)); - check_write_utxo(&mut rng, Spent , Absent, Ok(Spent), Some(FRESH | DIRTY), None, Some(FRESH | DIRTY)); - check_write_utxo(&mut rng, Spent , Spent , Ok(Spent), Some(0), Some(DIRTY), Some(DIRTY)); - check_write_utxo(&mut rng, Spent , Spent , Ok(Spent), Some(0), Some(FRESH | DIRTY), Some(DIRTY)); - check_write_utxo(&mut rng, Spent , Spent , Ok(Absent), Some(FRESH), Some(DIRTY), None); - check_write_utxo(&mut rng, Spent , Spent , Ok(Absent), Some(FRESH), Some(FRESH | DIRTY), None); - check_write_utxo(&mut rng, Spent , Spent , Ok(Spent), Some(DIRTY), Some(DIRTY), Some(DIRTY)); - check_write_utxo(&mut rng, Spent , Spent , Ok(Spent), Some(DIRTY), Some(FRESH | DIRTY), Some(DIRTY)); - check_write_utxo(&mut rng, Spent , Spent , Ok(Absent), Some(FRESH | DIRTY), Some(DIRTY), None); - check_write_utxo(&mut rng, Spent , Spent , Ok(Absent), Some(FRESH | DIRTY), Some(FRESH | DIRTY), None); - check_write_utxo(&mut rng, Spent , Present, Ok(Present), Some(0), Some(DIRTY), Some(DIRTY)); - check_write_utxo(&mut rng, Spent , Present, Ok(Present), Some(0), Some(FRESH | DIRTY), Some(DIRTY)); - check_write_utxo(&mut rng, Spent , Present, Ok(Present), Some(FRESH), Some(DIRTY), Some(FRESH | DIRTY)); - check_write_utxo(&mut rng, Spent , Present, Ok(Present), Some(FRESH), Some(FRESH | DIRTY), Some(FRESH | DIRTY)); - check_write_utxo(&mut rng, Spent , Present, Ok(Present), Some(DIRTY), Some(DIRTY), Some(DIRTY)); - check_write_utxo(&mut rng, Spent , Present, Ok(Present), Some(DIRTY), Some(FRESH | DIRTY), Some(DIRTY)); - check_write_utxo(&mut rng, Spent , Present, Ok(Present), Some(FRESH | DIRTY), Some(DIRTY), Some(FRESH | DIRTY)); - check_write_utxo(&mut rng, Spent , Present, Ok(Present), Some(FRESH | DIRTY), Some(FRESH | DIRTY), Some(FRESH | DIRTY)); - check_write_utxo(&mut rng, Present, Absent, Ok(Present), Some(0), None, Some(0)); - check_write_utxo(&mut rng, Present, Absent, Ok(Present), Some(FRESH), None, Some(FRESH)); - check_write_utxo(&mut rng, Present, Absent, Ok(Present), Some(DIRTY), None, Some(DIRTY)); - check_write_utxo(&mut rng, Present, Absent, Ok(Present), Some(FRESH | DIRTY), None , Some(FRESH | DIRTY)); - check_write_utxo(&mut rng, Present, Spent , Ok(Spent), Some(0), Some(DIRTY), Some(DIRTY)); - check_write_utxo(&mut rng, Present, Spent , Err(FreshUtxoAlreadyExists), Some(0), Some(FRESH | DIRTY), None); - check_write_utxo(&mut rng, Present, Spent , Ok(Absent), Some(FRESH), Some(DIRTY), None); - check_write_utxo(&mut rng, Present, Spent , Err(FreshUtxoAlreadyExists), Some(FRESH), Some(FRESH | DIRTY), None); - check_write_utxo(&mut rng, Present, Spent , Ok(Spent), Some(DIRTY), Some(DIRTY), Some(DIRTY)); - check_write_utxo(&mut rng, Present, Spent , Err(FreshUtxoAlreadyExists), Some(DIRTY), Some(FRESH | DIRTY), None); - check_write_utxo(&mut rng, Present, Spent , Ok(Absent), Some(FRESH | DIRTY), Some(DIRTY), None); - check_write_utxo(&mut rng, Present, Spent , Err(FreshUtxoAlreadyExists), Some(FRESH | DIRTY), Some(FRESH | DIRTY), None); - check_write_utxo(&mut rng, Present, Present, Ok(Present), Some(0), Some(DIRTY), Some(DIRTY)); - check_write_utxo(&mut rng, Present, Present, Err(FreshUtxoAlreadyExists), Some(0), Some(FRESH | DIRTY), None); - check_write_utxo(&mut rng, Present, Present, Ok(Present), Some(FRESH), Some(DIRTY), Some(FRESH | DIRTY)); - check_write_utxo(&mut rng, Present, Present, Err(FreshUtxoAlreadyExists), Some(FRESH), Some(FRESH | DIRTY), None); - check_write_utxo(&mut rng, Present, Present, Ok(Present), Some(DIRTY), Some(DIRTY), Some(DIRTY)); - check_write_utxo(&mut rng, Present, Present, Err(FreshUtxoAlreadyExists), Some(DIRTY), Some(FRESH | DIRTY), None); - check_write_utxo(&mut rng, Present, Present, Ok(Present), Some(FRESH | DIRTY), Some(DIRTY), Some(FRESH | DIRTY)); - check_write_utxo(&mut rng, Present, Present, Err(FreshUtxoAlreadyExists), Some(FRESH | DIRTY), Some(FRESH | DIRTY), None); -} - -#[rstest] -#[trace] -#[case(Seed::from_entropy())] -#[rustfmt::skip] -fn access_utxo_test(#[case] seed: Seed) { - let mut rng = make_seedable_rng(seed); - /* - PARENT CACHE RESULT CACHE - PRESENCE PRESENCE PRESENCE Flags RESULT Flags - */ - check_get_mut_utxo(&mut rng, Absent, Absent, Absent, None, None); - check_get_mut_utxo(&mut rng, Absent, Spent , Spent , Some(0), Some(0)); - check_get_mut_utxo(&mut rng, Absent, Spent , Spent , Some(FRESH), Some(FRESH)); - check_get_mut_utxo(&mut rng, Absent, Spent , Spent , Some(DIRTY), Some(DIRTY)); - check_get_mut_utxo(&mut rng, Absent, Spent , Spent , Some(FRESH | DIRTY),Some(FRESH | DIRTY)); - check_get_mut_utxo(&mut rng, Absent, Present, Present, Some(0), Some(0)); - check_get_mut_utxo(&mut rng, Absent, Present, Present, Some(FRESH), Some(FRESH)); - check_get_mut_utxo(&mut rng, Absent, Present, Present, Some(DIRTY), Some(DIRTY)); - check_get_mut_utxo(&mut rng, Absent, Present, Present, Some(FRESH | DIRTY),Some(FRESH | DIRTY)); - check_get_mut_utxo(&mut rng, Spent , Absent, Absent, None, None); - check_get_mut_utxo(&mut rng, Spent , Spent , Spent , Some(0), Some(0)); - check_get_mut_utxo(&mut rng, Spent , Spent , Spent , Some(FRESH), Some(FRESH)); - check_get_mut_utxo(&mut rng, Spent , Spent , Spent , Some(DIRTY), Some(DIRTY)); - check_get_mut_utxo(&mut rng, Spent , Spent , Spent , Some(FRESH | DIRTY),Some(FRESH | DIRTY)); - check_get_mut_utxo(&mut rng, Spent , Present, Present, Some(0), Some(0)); - check_get_mut_utxo(&mut rng, Spent , Present, Present, Some(FRESH), Some(FRESH)); - check_get_mut_utxo(&mut rng, Spent , Present, Present, Some(DIRTY), Some(DIRTY)); - check_get_mut_utxo(&mut rng, Spent , Present, Present, Some(FRESH | DIRTY),Some(FRESH | DIRTY)); - check_get_mut_utxo(&mut rng, Present, Absent, Present, None, Some(0)); - check_get_mut_utxo(&mut rng, Present, Spent , Spent , Some(0), Some(0)); - check_get_mut_utxo(&mut rng, Present, Spent , Spent , Some(FRESH), Some(FRESH)); - check_get_mut_utxo(&mut rng, Present, Spent , Spent , Some(DIRTY), Some(DIRTY)); - check_get_mut_utxo(&mut rng, Present, Spent , Spent , Some(FRESH | DIRTY),Some(FRESH | DIRTY)); - check_get_mut_utxo(&mut rng, Present, Present, Present, Some(0), Some(0)); - check_get_mut_utxo(&mut rng, Present, Present, Present, Some(FRESH), Some(FRESH)); - check_get_mut_utxo(&mut rng, Present, Present, Present, Some(DIRTY), Some(DIRTY)); - check_get_mut_utxo(&mut rng, Present, Present, Present, Some(FRESH | DIRTY),Some(FRESH | DIRTY)); -} - -#[rstest] -#[trace] -#[case(Seed::from_entropy())] -fn derive_cache_test(#[case] seed: Seed) { - let mut rng = make_seedable_rng(seed); - let mut cache = UtxosCache::new_for_test(H256::random().into()); - - let (utxo, outpoint_1) = create_utxo(&mut rng, 10); - assert!(cache.add_utxo(&outpoint_1, utxo, false).is_ok()); - - let (utxo, outpoint_2) = create_utxo(&mut rng, 20); - assert!(cache.add_utxo(&outpoint_2, utxo, false).is_ok()); - - let mut extra_cache = cache.derive_cache(); - assert!(extra_cache.utxos.is_empty()); - - assert!(extra_cache.has_utxo(&outpoint_1)); - assert!(extra_cache.has_utxo(&outpoint_2)); - - let (utxo, outpoint) = create_utxo(&mut rng, 30); - assert!(extra_cache.add_utxo(&outpoint, utxo, true).is_ok()); - - assert!(!cache.has_utxo(&outpoint)); -} - -#[rstest] -#[trace] -#[case(Seed::from_entropy())] -fn blockchain_or_mempool_utxo_test(#[case] seed: Seed) { - let mut rng = make_seedable_rng(seed); - let mut cache = UtxosCache::new_for_test(H256::random().into()); - - let (utxo, outpoint_1) = create_utxo(&mut rng, 10); - assert!(cache.add_utxo(&outpoint_1, utxo, false).is_ok()); - - let (utxo, outpoint_2) = create_utxo_for_mempool(&mut rng); - assert!(cache.add_utxo(&outpoint_2, utxo, false).is_ok()); - - let res = cache.utxo(&outpoint_2).expect("should contain utxo"); - assert!(res.source_height().is_mempool()); - assert_eq!(res.source, UtxoSource::MemPool); -} - -#[rstest] -#[trace] -#[case(Seed::from_entropy())] -fn multiple_update_utxos_test(#[case] seed: Seed) { - use common::chain::signature::inputsig::InputWitness; - - let mut rng = make_seedable_rng(seed); - let mut cache = UtxosCache::new_for_test(H256::random().into()); - - // let's test `add_utxos` - let tx = Transaction::new(0x00, vec![], create_tx_outputs(&mut rng, 10), 0x01).unwrap(); - assert!(cache.add_utxos(&tx, UtxoSource::BlockChain(BlockHeight::new(2)), false).is_ok()); - - // check that the outputs of tx are added in the cache. - tx.outputs().iter().enumerate().for_each(|(i, x)| { - let id = OutPointSourceId::from(tx.get_id()); - let outpoint = OutPoint::new(id, i as u32); - - let utxo = cache.utxo(&outpoint).expect("utxo should exist"); - assert_eq!(utxo.output(), x); - }); - - // let's spend some outputs.; - // randomly take half of the outputs to spend. - let results = - seq::index::sample(&mut rng, tx.outputs().len(), tx.outputs().len() / 2).into_vec(); - let to_spend = results - .into_iter() - .map(|idx| { - let id = OutPointSourceId::from(tx.get_id()); - TxInput::new(id, idx as u32, InputWitness::NoSignature(None)) - }) - .collect_vec(); - - // create a new transaction - let new_tx = Transaction::new(0x00, to_spend.clone(), vec![], 0).expect("should succeed"); - // let's test `spend_utxos` - let tx_undo = cache.spend_utxos(&new_tx, BlockHeight::new(2)).expect("should return txundo"); - - // check that these utxos came from the tx's output - tx_undo.inner().iter().for_each(|x| { - assert!(tx.outputs().contains(x.output())); - }); - - // check that the spent utxos should not exist in the cache anymore. - to_spend.iter().for_each(|input| { - assert!(cache.utxo(input.outpoint()).is_none()); - }); -} - -#[test] -fn check_best_block_after_flush() { - let mut cache1 = UtxosCache::new_for_test(H256::random().into()); - let cache2 = UtxosCache::new_for_test(H256::random().into()); - assert_ne!(cache1.best_block_hash(), cache2.best_block_hash()); - let expected_hash = cache2.best_block_hash(); - assert!(flush_to_base(cache2, &mut cache1).is_ok()); - assert_eq!(expected_hash, cache1.best_block_hash()); -} diff --git a/utxo/src/view.rs b/utxo/src/view.rs new file mode 100644 index 0000000000..08002909ad --- /dev/null +++ b/utxo/src/view.rs @@ -0,0 +1,48 @@ +// Copyright (c) 2022 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::{ConsumedUtxoCache, Error, Utxo, UtxosCache}; +use common::{ + chain::{GenBlock, OutPoint}, + primitives::Id, +}; + +pub trait UtxosView { + /// Retrieves utxo. + fn utxo(&self, outpoint: &OutPoint) -> Option; + + /// Checks whether outpoint is unspent. + fn has_utxo(&self, outpoint: &OutPoint) -> bool; + + /// Retrieves the block hash of the best block in this view + fn best_block_hash(&self) -> Id; + + /// Estimated size of the whole view (None if not implemented) + fn estimated_size(&self) -> Option; + + fn derive_cache(&self) -> UtxosCache; +} + +pub trait FlushableUtxoView { + /// Performs bulk modification + fn batch_write(&mut self, utxos: ConsumedUtxoCache) -> Result<(), Error>; +} + +/// Flush the cache into the provided base. This will consume the cache and throw it away. +/// It uses the batch_write function since it's available in different kinds of views. +pub fn flush_to_base(cache: UtxosCache, base: &mut T) -> Result<(), Error> { + let consumed_cache = cache.consume(); + base.batch_write(consumed_cache) +}