diff --git a/chainstate/test-framework/src/block_builder.rs b/chainstate/test-framework/src/block_builder.rs index e3ebf14661..6bd53115dc 100644 --- a/chainstate/test-framework/src/block_builder.rs +++ b/chainstate/test-framework/src/block_builder.rs @@ -13,17 +13,18 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::collections::BTreeSet; +use std::collections::{BTreeMap, BTreeSet}; use crate::framework::BlockOutputs; -use crate::utils::{create_multiple_utxo_data, create_new_outputs, outputs_from_block}; +use crate::utils::{create_new_outputs, outputs_from_block}; use crate::TestFramework; use chainstate::{BlockSource, ChainstateError}; +use chainstate_storage::BlockchainStorageRead; use chainstate_types::BlockIndex; use common::chain::block::block_body::BlockBody; use common::chain::block::signed_block_header::{BlockHeaderSignature, BlockHeaderSignatureData}; use common::chain::block::BlockHeader; -use common::chain::{OutPointSourceId, UtxoOutPoint}; +use common::chain::{AccountNonce, AccountType, OutPointSourceId, UtxoOutPoint}; use common::{ chain::{ block::{timestamp::BlockTimestamp, BlockReward, ConsensusData}, @@ -37,6 +38,7 @@ use crypto::key::PrivateKey; use crypto::random::{CryptoRng, Rng}; use itertools::Itertools; use serialization::Encode; +use tokens_accounting::{InMemoryTokensAccounting, TokensAccountingDB}; /// The block builder that allows construction and processing of a block. pub struct BlockBuilder<'f> { @@ -47,8 +49,12 @@ pub struct BlockBuilder<'f> { consensus_data: ConsensusData, reward: BlockReward, block_source: BlockSource, - used_utxo: BTreeSet<UtxoOutPoint>, block_signing_key: Option<PrivateKey>, + + // need these fields to track info across the txs + used_utxo: BTreeSet<UtxoOutPoint>, + account_nonce_tracker: BTreeMap<AccountType, AccountNonce>, + tokens_data: InMemoryTokensAccounting, } impl<'f> BlockBuilder<'f> { @@ -61,6 +67,13 @@ impl<'f> BlockBuilder<'f> { let reward = BlockReward::new(Vec::new()); let block_source = BlockSource::Local; let used_utxo = BTreeSet::new(); + let account_nonce_tracker = BTreeMap::new(); + + let all_tokens_data = framework.storage.read_tokens_accounting_data().unwrap(); + let tokens_data = InMemoryTokensAccounting::from_values( + all_tokens_data.token_data, + all_tokens_data.circulating_supply, + ); Self { framework, @@ -70,8 +83,10 @@ impl<'f> BlockBuilder<'f> { consensus_data, reward, block_source, - used_utxo, block_signing_key: None, + used_utxo, + account_nonce_tracker, + tokens_data, } } @@ -87,36 +102,57 @@ impl<'f> BlockBuilder<'f> { self } - /// Adds a transaction that uses random utxos + /// Adds a transaction that uses random utxos and accounts pub fn add_test_transaction(mut self, rng: &mut (impl Rng + CryptoRng)) -> Self { - let utxo_set = self.framework.storage.read_utxo_set().unwrap(); - - if !utxo_set.is_empty() { - // TODO: get n utxos as inputs and create m new outputs - let index = rng.gen_range(0..utxo_set.len()); - let (outpoint, utxo) = utxo_set.iter().nth(index).unwrap(); - if !self.used_utxo.contains(outpoint) { - let new_utxo_data = create_multiple_utxo_data( - &self.framework.chainstate, - outpoint.source_id(), - outpoint.output_index() as usize, - utxo.output(), - rng, - ); - - if let Some((witness, input, output)) = new_utxo_data { - self.used_utxo.insert(outpoint.clone()); - return self.add_transaction( - SignedTransaction::new( - Transaction::new(0, vec![input], output).unwrap(), - vec![witness], - ) - .expect("invalid witness count"), - ); - } - } + let utxo_set = self + .framework + .storage + .read_utxo_set() + .unwrap() + .into_iter() + .filter(|(outpoint, _)| !self.used_utxo.contains(outpoint)) + .collect(); + + let account_nonce_getter = Box::new(|account: AccountType| -> Option<AccountNonce> { + self.account_nonce_tracker + .get(&account) + .copied() + .or_else(|| self.framework.storage.get_account_nonce_count(account).unwrap()) + }); + + let (tx, new_tokens_delta) = super::random_tx_maker::RandomTxMaker::new( + &self.framework.chainstate, + &utxo_set, + &self.tokens_data, + account_nonce_getter, + ) + .make(rng); + + if !tx.inputs().is_empty() && !tx.outputs().is_empty() { + // flush new tokens info to the in memory store + let mut tokens_db = TokensAccountingDB::new(&mut self.tokens_data); + tokens_db.merge_with_delta(new_tokens_delta).unwrap(); + + // update used utxo set because this function can be called multiple times without flushing data to storage + tx.inputs().iter().for_each(|input| { + match input { + TxInput::Utxo(utxo_outpoint) => { + self.used_utxo.insert(utxo_outpoint.clone()); + } + TxInput::Account(account) => { + self.account_nonce_tracker + .insert((*account.account()).into(), account.nonce()); + } + }; + }); + + let witnesses = tx.inputs().iter().map(|_| super::empty_witness(rng)).collect(); + let tx = SignedTransaction::new(tx, witnesses).expect("invalid witness count"); + + self.add_transaction(tx) + } else { + self } - self } /// Returns regular transaction output(s) if any, otherwise returns block reward outputs diff --git a/chainstate/test-framework/src/lib.rs b/chainstate/test-framework/src/lib.rs index d03a3efea5..4863637532 100644 --- a/chainstate/test-framework/src/lib.rs +++ b/chainstate/test-framework/src/lib.rs @@ -19,6 +19,7 @@ mod block_builder; mod framework; mod framework_builder; mod pos_block_builder; +mod random_tx_maker; mod transaction_builder; mod tx_verification_strategy; mod utils; diff --git a/chainstate/test-framework/src/random_tx_maker.rs b/chainstate/test-framework/src/random_tx_maker.rs new file mode 100644 index 0000000000..5ee055a6ef --- /dev/null +++ b/chainstate/test-framework/src/random_tx_maker.rs @@ -0,0 +1,407 @@ +// Copyright (c) 2023 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 std::{collections::BTreeMap, vec}; + +use crate::TestChainstate; + +use chainstate::chainstate_interface::ChainstateInterface; +use common::{ + chain::{ + output_value::OutputValue, + tokens::{make_token_id, NftIssuance, TokenId, TokenIssuance, TokenTotalSupply}, + AccountNonce, AccountOp, AccountOutPoint, AccountType, Destination, Transaction, TxInput, + TxOutput, UtxoOutPoint, + }, + primitives::Amount, +}; +use crypto::random::{CryptoRng, Rng}; +use itertools::Itertools; +use test_utils::nft_utils::*; +use tokens_accounting::{ + InMemoryTokensAccounting, TokensAccountingCache, TokensAccountingDB, TokensAccountingDeltaData, + TokensAccountingOperations, TokensAccountingView, +}; +use utxo::Utxo; + +pub struct RandomTxMaker<'a> { + chainstate: &'a TestChainstate, + utxo_set: &'a BTreeMap<UtxoOutPoint, Utxo>, + tokens_in_memory_store: &'a InMemoryTokensAccounting, + + account_nonce_getter: Box<dyn Fn(AccountType) -> Option<AccountNonce> + 'a>, + account_nonce_tracker: BTreeMap<AccountType, AccountNonce>, + + // Transaction is composed of multiple inputs and outputs + // but tokens can be issued only using input0 so a flag to check is required + token_can_be_issued: bool, +} + +impl<'a> RandomTxMaker<'a> { + pub fn new( + chainstate: &'a TestChainstate, + utxo_set: &'a BTreeMap<UtxoOutPoint, Utxo>, + tokens_in_memory_store: &'a InMemoryTokensAccounting, + account_nonce_getter: Box<dyn Fn(AccountType) -> Option<AccountNonce> + 'a>, + ) -> Self { + Self { + chainstate, + utxo_set, + tokens_in_memory_store, + account_nonce_getter, + account_nonce_tracker: BTreeMap::new(), + token_can_be_issued: true, + } + } + + pub fn make( + mut self, + rng: &mut (impl Rng + CryptoRng), + ) -> (Transaction, TokensAccountingDeltaData) { + // TODO: ideally all inputs/outputs should be shuffled but it would mess up with token issuance + // because ids are build from input0 + + let tokens_db = TokensAccountingDB::new(self.tokens_in_memory_store); + let mut tokens_cache = TokensAccountingCache::new(&tokens_db); + + // Select random number of utxos to spend + let inputs_with_utxos = self.select_utxos(rng); + + // Spend selected utxos + let (mut inputs, mut outputs) = + self.create_utxo_spending(rng, &mut tokens_cache, inputs_with_utxos); + + // Select random number of token accounts to spend from + let account_inputs = self.select_accounts(rng); + + // Spending from a token account requires paying fee. Find sufficient utxo per account input. + let fee = self.chainstate.get_chain_config().token_min_supply_change_fee(); + let fee_inputs = self + .utxo_set + .iter() + .filter(|(outpoint, utxo)| { + let input: TxInput = (**outpoint).clone().into(); + !inputs.iter().contains(&input) + && super::get_output_value(utxo.output()) + .map_or(false, |v| v.coin_amount().unwrap_or(Amount::ZERO) >= fee) + }) + .map(|(outpoint, _)| outpoint.clone().into()) + .take(inputs.len()) + .collect::<Vec<TxInput>>(); + + // If enough utxos to pay fees + if fee_inputs.len() == account_inputs.len() { + let (account_inputs, account_outputs) = + self.create_account_spending(rng, &mut tokens_cache, &account_inputs, fee_inputs); + + inputs.extend(account_inputs); + outputs.extend(account_outputs); + }; + + ( + Transaction::new(0, inputs, outputs).unwrap(), + tokens_cache.consume(), + ) + } + + fn select_utxos(&self, rng: &mut impl Rng) -> Vec<(UtxoOutPoint, TxOutput)> { + // TODO: it take several items from the beginning of the collection assuming that outpoints + // are ids thus the order changes with new insertions. But more sophisticated random selection can be implemented here + let number_of_inputs = rng.gen_range(1..5); + self.utxo_set + .iter() + .take(number_of_inputs) + .map(|(outpoint, utxo)| (outpoint.clone(), utxo.output().clone())) + .collect() + } + + fn select_accounts(&self, rng: &mut impl Rng) -> Vec<TokenId> { + // TODO: it take several items from the beginning of the collection assuming that outpoints + // are ids thus the order changes with new insertions. But more sophisticated random selection can be implemented here + let number_of_inputs = rng.gen_range(1..5); + self.tokens_in_memory_store + .tokens_data() + .iter() + .take(number_of_inputs) + .map(|(token_id, _)| *token_id) + .collect() + } + + fn get_next_nonce(&mut self, account: AccountType) -> AccountNonce { + *self + .account_nonce_tracker + .entry(account) + .and_modify(|nonce| { + *nonce = nonce.increment().unwrap(); + }) + .or_insert_with(|| { + (self.account_nonce_getter)(account) + .map_or(AccountNonce::new(0), |nonce| nonce.increment().unwrap()) + }) + } + + fn create_account_spending( + mut self, + rng: &mut (impl Rng + CryptoRng), + tokens_cache: &mut (impl TokensAccountingView + TokensAccountingOperations), + inputs: &[TokenId], + fee_inputs: Vec<TxInput>, + ) -> (Vec<TxInput>, Vec<TxOutput>) { + assert_eq!(inputs.len(), fee_inputs.len()); + + let mut result_inputs = Vec::new(); + let mut result_outputs = Vec::new(); + + let min_tx_fee = self.chainstate.get_chain_config().token_min_supply_change_fee(); + + for (i, token_id) in inputs.iter().copied().enumerate() { + if rng.gen_bool(0.9) { + let circulating_supply = + tokens_cache.get_circulating_supply(&token_id).unwrap().unwrap_or(Amount::ZERO); + let token_data = tokens_cache.get_token_data(&token_id).unwrap(); + + if let Some(token_data) = token_data { + let tokens_accounting::TokenData::FungibleToken(token_data) = token_data; + + if !token_data.is_locked() { + // mint + let supply_limit = match token_data.total_supply() { + TokenTotalSupply::Fixed(v) => *v, + TokenTotalSupply::Lockable | TokenTotalSupply::Unlimited => { + Amount::from_atoms(i128::MAX as u128) + } + }; + let supply_left = (supply_limit - circulating_supply).unwrap(); + let to_mint = + Amount::from_atoms(rng.gen_range(1..supply_left.into_atoms())); + + let new_nonce = self.get_next_nonce(AccountType::TokenSupply(token_id)); + let account_input = TxInput::Account(AccountOutPoint::new( + new_nonce, + AccountOp::MintTokens(token_id, to_mint), + )); + result_inputs.extend(vec![account_input, fee_inputs[i].clone()]); + + let outputs = vec![ + TxOutput::Transfer( + OutputValue::TokenV1(token_id, to_mint), + Destination::AnyoneCanSpend, + ), + TxOutput::Burn(OutputValue::Coin(min_tx_fee)), + ]; + result_outputs.extend(outputs); + + let _ = tokens_cache.mint_tokens(token_id, to_mint).unwrap(); + } + } + } else { + let is_locked = match tokens_cache.get_token_data(&token_id).unwrap().unwrap() { + tokens_accounting::TokenData::FungibleToken(data) => data.is_locked(), + }; + + if !is_locked { + let new_nonce = self.get_next_nonce(AccountType::TokenSupply(token_id)); + let account_input = TxInput::Account(AccountOutPoint::new( + new_nonce, + AccountOp::LockTokenSupply(token_id), + )); + result_inputs.extend(vec![account_input, fee_inputs[i].clone()]); + + let outputs = vec![TxOutput::Burn(OutputValue::Coin(min_tx_fee))]; + result_outputs.extend(outputs); + + let _ = tokens_cache.lock_circulating_supply(token_id).unwrap(); + } + } + } + + (result_inputs, result_outputs) + } + + /// Given an output as in input creates multiple new random outputs. + fn create_utxo_spending( + &mut self, + rng: &mut (impl Rng + CryptoRng), + tokens_cache: &mut (impl TokensAccountingView + TokensAccountingOperations), + inputs: Vec<(UtxoOutPoint, TxOutput)>, + ) -> (Vec<TxInput>, Vec<TxOutput>) { + let mut result_inputs = Vec::new(); + let mut result_outputs = Vec::new(); + let mut fee_input_to_change_supply: Option<TxInput> = None; + + for (i, (outpoint, input_utxo)) in inputs.iter().enumerate() { + if i > 0 { + self.token_can_be_issued = false; + } + + match super::get_output_value(input_utxo).unwrap() { + OutputValue::Coin(output_value) => { + // save output for potential unmint fee + if output_value + >= self.chainstate.get_chain_config().token_min_supply_change_fee() + && fee_input_to_change_supply.is_none() + && inputs.len() > 1 + { + fee_input_to_change_supply = Some(TxInput::Utxo(outpoint.clone())); + } else { + let new_outputs = self.spend_coins(rng, outpoint, output_value); + result_inputs.push(TxInput::Utxo(outpoint.clone())); + result_outputs.extend(new_outputs); + } + } + OutputValue::TokenV0(_) => { + unimplemented!("deprecated tokens version") + } + OutputValue::TokenV1(token_id, amount) => { + let (new_inputs, new_outputs) = self.spend_tokens_v1( + rng, + tokens_cache, + token_id, + amount, + &mut fee_input_to_change_supply, + ); + result_inputs.push(TxInput::Utxo(outpoint.clone())); + result_inputs.extend(new_inputs); + result_outputs.extend(new_outputs); + } + }; + } + (result_inputs, result_outputs) + } + + fn spend_coins( + &mut self, + rng: &mut (impl Rng + CryptoRng), + outpoint: &UtxoOutPoint, + coins: Amount, + ) -> Vec<TxOutput> { + let num_outputs = rng.gen_range(1..5); + let switch = rng.gen_range(0..3); + if switch == 0 && self.token_can_be_issued { + // issue token v1 + let min_tx_fee = self.chainstate.get_chain_config().token_min_issuance_fee(); + if coins >= min_tx_fee { + self.token_can_be_issued = false; + let change = (coins - min_tx_fee).unwrap(); + // Coin output is created intentionally besides issuance output in order to not waste utxo + // (e.g. single genesis output on issuance) + vec![ + TxOutput::IssueFungibleToken(Box::new(TokenIssuance::V1( + random_token_issuance_v1(self.chainstate.get_chain_config(), rng), + ))), + TxOutput::Transfer(OutputValue::Coin(change), Destination::AnyoneCanSpend), + TxOutput::Burn(OutputValue::Coin(min_tx_fee)), + ] + } else { + Vec::new() + } + } else if switch == 1 && self.token_can_be_issued { + // issue nft v1 + let min_tx_fee = self.chainstate.get_chain_config().token_min_issuance_fee(); + if coins >= min_tx_fee { + self.token_can_be_issued = false; + let change = (coins - min_tx_fee).unwrap(); + // Coin output is created intentionally besides issuance output in order to not waste utxo + // (e.g. single genesis output on issuance) + vec![ + TxOutput::IssueNft( + make_token_id(&[outpoint.clone().into()]).unwrap(), + Box::new(NftIssuance::V0(random_nft_issuance( + self.chainstate.get_chain_config(), + rng, + ))), + Destination::AnyoneCanSpend, + ), + TxOutput::Transfer(OutputValue::Coin(change), Destination::AnyoneCanSpend), + TxOutput::Burn(OutputValue::Coin(min_tx_fee)), + ] + } else { + Vec::new() + } + } else { + // transfer coins + (0..num_outputs) + .map(|_| { + let new_value = Amount::from_atoms(coins.into_atoms() / num_outputs); + debug_assert!(new_value >= Amount::from_atoms(1)); + TxOutput::Transfer(OutputValue::Coin(new_value), Destination::AnyoneCanSpend) + }) + .collect() + } + } + + fn spend_tokens_v1( + &mut self, + rng: &mut impl Rng, + tokens_cache: &mut (impl TokensAccountingView + TokensAccountingOperations), + token_id: TokenId, + amount: Amount, + fee_input: &mut Option<TxInput>, + ) -> (Vec<TxInput>, Vec<TxOutput>) { + let atoms_vec = test_utils::split_value(rng, amount.into_atoms()); + let mut result_inputs = Vec::new(); + let mut result_outputs = Vec::new(); + + for atoms in atoms_vec { + if rng.gen::<bool>() { + // transfer + result_outputs.push(TxOutput::Transfer( + OutputValue::TokenV1(token_id, Amount::from_atoms(atoms)), + Destination::AnyoneCanSpend, + )); + } else if rng.gen_bool(0.9) { + // unmint + let token_data = tokens_cache.get_token_data(&token_id).unwrap(); + + // check token_data as well because it can be an nft + if let (Some(fee_tx_input), Some(token_data)) = (&fee_input, token_data) { + let circulating_supply = + tokens_cache.get_circulating_supply(&token_id).unwrap(); + assert!(circulating_supply.is_some()); + + let tokens_accounting::TokenData::FungibleToken(token_data) = token_data; + if !token_data.is_locked() { + let to_unmint = Amount::from_atoms(atoms); + + let new_nonce = self.get_next_nonce(AccountType::TokenSupply(token_id)); + let account_input = TxInput::Account(AccountOutPoint::new( + new_nonce, + AccountOp::UnmintTokens(token_id), + )); + result_inputs.extend(vec![account_input, fee_tx_input.clone()]); + + let min_tx_fee = + self.chainstate.get_chain_config().token_min_supply_change_fee(); + let outputs = vec![ + TxOutput::Burn(OutputValue::TokenV1(token_id, to_unmint)), + TxOutput::Burn(OutputValue::Coin(min_tx_fee)), + ]; + result_outputs.extend(outputs); + + let _ = tokens_cache.unmint_tokens(token_id, to_unmint).unwrap(); + } + *fee_input = None; + } + } else { + // burn + result_outputs.push(TxOutput::Burn(OutputValue::TokenV1( + token_id, + Amount::from_atoms(atoms), + ))) + } + } + (result_inputs, result_outputs) + } +} diff --git a/chainstate/test-framework/src/utils.rs b/chainstate/test-framework/src/utils.rs index 9fe04cc1f2..1ea06f102b 100644 --- a/chainstate/test-framework/src/utils.rs +++ b/chainstate/test-framework/src/utils.rs @@ -39,11 +39,10 @@ use common::{ }; use crypto::{ key::{PrivateKey, PublicKey}, - random::{CryptoRng, Rng}, + random::Rng, vrf::{VRFPrivateKey, VRFPublicKey}, }; use pos_accounting::{PoSAccountingDB, PoSAccountingView}; -use test_utils::nft_utils::*; pub fn empty_witness(rng: &mut impl Rng) -> InputWitness { use crypto::random::SliceRandom; @@ -65,8 +64,10 @@ pub fn get_output_value(output: &TxOutput) -> Option<OutputValue> { | TxOutput::ProduceBlockFromStake(_, _) | TxOutput::CreateDelegationId(_, _) | TxOutput::DelegateStaking(_, _) - | TxOutput::IssueFungibleToken(_) - | TxOutput::IssueNft(_, _, _) => None, + | TxOutput::IssueFungibleToken(_) => None, + TxOutput::IssueNft(token_id, _, _) => { + Some(OutputValue::TokenV1(*token_id, Amount::from_atoms(1))) + } } } @@ -141,134 +142,7 @@ pub fn create_utxo_data( } } -/// Given an output as in input creates multiple new random outputs. -pub fn create_multiple_utxo_data( - chainstate: &TestChainstate, - outsrc: OutPointSourceId, - index: usize, - output: &TxOutput, - rng: &mut (impl Rng + CryptoRng), -) -> Option<(InputWitness, TxInput, Vec<TxOutput>)> { - let num_outputs = rng.gen_range(1..10); - let new_outputs = match get_output_value(output)? { - OutputValue::Coin(output_value) => { - let switch = rng.gen_range(0..3); - if switch == 0 { - // issue nft - let min_tx_fee = chainstate.get_chain_config().token_min_issuance_fee(); - if output_value >= min_tx_fee { - // Coin output is created intentionally besides issuance output in order to not waste utxo - // (e.g. single genesis output on issuance) - vec![ - TxOutput::Transfer( - random_nft_issuance(chainstate.get_chain_config(), rng).into(), - Destination::AnyoneCanSpend, - ), - TxOutput::Burn(OutputValue::Coin(min_tx_fee)), - ] - } else { - return None; - } - } else if switch == 1 { - // issue token - let min_tx_fee = chainstate.get_chain_config().token_min_issuance_fee(); - if output_value >= min_tx_fee { - // Coin output is created intentionally besides issuance output in order to not waste utxo - // (e.g. single genesis output on issuance) - vec![ - TxOutput::Transfer( - random_token_issuance(chainstate.get_chain_config(), rng).into(), - Destination::AnyoneCanSpend, - ), - TxOutput::Burn(OutputValue::Coin(min_tx_fee)), - ] - } else { - return None; - } - } else { - // spend the coin with multiple outputs - (0..num_outputs) - .map(|_| { - let new_value = Amount::from_atoms(output_value.into_atoms() / num_outputs); - debug_assert!(new_value >= Amount::from_atoms(1)); - TxOutput::Transfer(OutputValue::Coin(new_value), anyonecanspend_address()) - }) - .collect() - } - } - OutputValue::TokenV0(token_data) => match &*token_data { - TokenData::TokenTransfer(transfer) => { - if rng.gen::<bool>() { - // burn transferred tokens - let amount_to_burn = if transfer.amount.into_atoms() > 1 { - Amount::from_atoms(rng.gen_range(1..transfer.amount.into_atoms())) - } else { - transfer.amount - }; - vec![TxOutput::Burn( - TokenTransfer { - token_id: transfer.token_id, - amount: amount_to_burn, - } - .into(), - )] - } else { - // transfer tokens again - if transfer.amount.into_atoms() >= num_outputs { - // transfer with multiple outputs - (0..num_outputs) - .map(|_| { - let amount = - Amount::from_atoms(transfer.amount.into_atoms() / num_outputs); - TxOutput::Transfer( - TokenTransfer { - token_id: transfer.token_id, - amount, - } - .into(), - anyonecanspend_address(), - ) - }) - .collect() - } else { - // transfer with a single output - vec![TxOutput::Transfer( - OutputValue::TokenV0(token_data), - anyonecanspend_address(), - )] - } - } - } - TokenData::TokenIssuance(issuance) => { - if rng.gen::<bool>() { - vec![new_token_burn_output( - chainstate, - &outsrc, - Amount::from_atoms(rng.gen_range(1..issuance.amount_to_issue.into_atoms())), - )] - } else { - vec![new_token_transfer_output(chainstate, &outsrc, issuance.amount_to_issue)] - } - } - TokenData::NftIssuance(_issuance) => { - if rng.gen::<bool>() { - vec![new_token_burn_output(chainstate, &outsrc, Amount::from_atoms(1))] - } else { - vec![new_token_transfer_output(chainstate, &outsrc, Amount::from_atoms(1))] - } - } - }, - OutputValue::TokenV1(_, _) => unimplemented!(), - }; - - Some(( - empty_witness(rng), - TxInput::from_utxo(outsrc, index as u32), - new_outputs, - )) -} - -fn new_token_transfer_output( +pub fn new_token_transfer_output( chainstate: &TestChainstate, outsrc: &OutPointSourceId, amount: Amount, @@ -290,27 +164,6 @@ fn new_token_transfer_output( ) } -fn new_token_burn_output( - chainstate: &TestChainstate, - outsrc: &OutPointSourceId, - amount_to_burn: Amount, -) -> TxOutput { - TxOutput::Burn( - TokenTransfer { - token_id: match outsrc { - OutPointSourceId::Transaction(prev_tx) => { - chainstate.get_token_id_from_issuance_tx(prev_tx).expect("ok").expect("some") - } - OutPointSourceId::BlockReward(_) => { - panic!("cannot issue token in block reward") - } - }, - amount: amount_to_burn, - } - .into(), - ) -} - pub fn outputs_from_genesis(genesis: &Genesis) -> BlockOutputs { [( OutPointSourceId::BlockReward(genesis.get_id().into()), diff --git a/chainstate/test-suite/src/tests/fungible_tokens_v1.rs b/chainstate/test-suite/src/tests/fungible_tokens_v1.rs index b80bc34f39..92457737fd 100644 --- a/chainstate/test-suite/src/tests/fungible_tokens_v1.rs +++ b/chainstate/test-suite/src/tests/fungible_tokens_v1.rs @@ -2723,7 +2723,6 @@ fn reorg_test_2_tokens(#[case] seed: Seed) { ); // No reorg assert_eq!(tf.best_block_id(), block_c_id); - println!("token1: {}, token2: {}", token_id_1, token_id_2); // Mint some tokens let (block_e_id, _) = mint_tokens_in_block( diff --git a/chainstate/test-suite/src/tests/tx_verification_simulation.rs b/chainstate/test-suite/src/tests/tx_verification_simulation.rs index 56b05912c3..7432aa6889 100644 --- a/chainstate/test-suite/src/tests/tx_verification_simulation.rs +++ b/chainstate/test-suite/src/tests/tx_verification_simulation.rs @@ -15,10 +15,12 @@ use super::*; use chainstate_test_framework::TxVerificationStrategy; +use common::chain::{tokens::TokenIssuanceVersion, ChainstateUpgrade, Destination, NetUpgrades}; #[rstest] #[trace] #[case(Seed::from_entropy(), 20, 50, false)] +#[trace] #[case(Seed::from_entropy(), 20, 50, true)] fn simulation( #[case] seed: Seed, @@ -37,6 +39,18 @@ fn simulation( max_tip_age: Default::default(), }) .with_tx_verification_strategy(TxVerificationStrategy::Randomized(seed)) + .with_chain_config( + common::chain::config::Builder::test_chain() + .chainstate_upgrades( + NetUpgrades::initialize(vec![( + BlockHeight::zero(), + ChainstateUpgrade::new(TokenIssuanceVersion::V1), + )]) + .unwrap(), + ) + .genesis_unittest(Destination::AnyoneCanSpend) + .build(), + ) .build(); for _ in 0..rng.gen_range(10..max_blocks) { @@ -45,6 +59,7 @@ fn simulation( for _ in 0..rng.gen_range(10..max_tx_per_block) { block_builder = block_builder.add_test_transaction(&mut rng); } + block_builder.build_and_process().unwrap().unwrap(); } }); diff --git a/test-utils/src/nft_utils.rs b/test-utils/src/nft_utils.rs index 7a752a1b56..6848ad829b 100644 --- a/test-utils/src/nft_utils.rs +++ b/test-utils/src/nft_utils.rs @@ -17,7 +17,11 @@ use crate::random_ascii_alphanumeric_string; use common::{ chain::{ config::ChainConfig, - tokens::{Metadata, NftIssuanceV0, TokenCreator, TokenIssuanceV0}, + tokens::{ + Metadata, NftIssuanceV0, TokenCreator, TokenIssuanceV0, TokenIssuanceV1, + TokenTotalSupply, + }, + Destination, }, primitives::Amount, }; @@ -43,6 +47,20 @@ pub fn random_token_issuance(chain_config: &ChainConfig, rng: &mut impl Rng) -> } } +pub fn random_token_issuance_v1(chain_config: &ChainConfig, rng: &mut impl Rng) -> TokenIssuanceV1 { + let max_ticker_len = chain_config.token_max_ticker_len(); + let max_dec_count = chain_config.token_max_dec_count(); + let max_uri_len = chain_config.token_max_uri_len(); + + TokenIssuanceV1 { + token_ticker: random_ascii_alphanumeric_string(rng, 1..max_ticker_len).as_bytes().to_vec(), + number_of_decimals: rng.gen_range(1..max_dec_count), + metadata_uri: random_ascii_alphanumeric_string(rng, 1..max_uri_len).as_bytes().to_vec(), + total_supply: TokenTotalSupply::Lockable, + reissuance_controller: Destination::AnyoneCanSpend, + } +} + pub fn random_nft_issuance( chain_config: &ChainConfig, rng: &mut (impl Rng + CryptoRng), diff --git a/tokens-accounting/src/data.rs b/tokens-accounting/src/data.rs index 07487b358f..f2e5a9d195 100644 --- a/tokens-accounting/src/data.rs +++ b/tokens-accounting/src/data.rs @@ -33,6 +33,15 @@ pub struct TokensAccountingData { pub circulating_supply: BTreeMap<TokenId, Amount>, } +impl TokensAccountingData { + pub fn new() -> Self { + Self { + token_data: BTreeMap::new(), + circulating_supply: BTreeMap::new(), + } + } +} + #[derive(Clone, Encode, Decode, Debug, PartialEq, Eq)] pub struct TokensAccountingDeltaData { pub(crate) token_data: DeltaDataCollection<TokenId, TokenData>, diff --git a/tokens-accounting/src/storage/db.rs b/tokens-accounting/src/storage/db.rs index d4a7812773..3f5e3dc843 100644 --- a/tokens-accounting/src/storage/db.rs +++ b/tokens-accounting/src/storage/db.rs @@ -64,7 +64,7 @@ impl<S: TokensAccountingStorageWrite> TokensAccountingDB<S> { }) .collect::<Result<BTreeMap<_, _>, _>>()?; - let balance_undo = other + let circulating_supply_undo = other .circulating_supply .consume() .into_iter() @@ -98,7 +98,7 @@ impl<S: TokensAccountingStorageWrite> TokensAccountingDB<S> { Ok(TokensAccountingDeltaUndoData { token_data: DeltaDataUndoCollection::from_data(data_undo), - circulating_supply: DeltaAmountCollection::from_iter(balance_undo), + circulating_supply: DeltaAmountCollection::from_iter(circulating_supply_undo), }) } } diff --git a/tokens-accounting/src/storage/in_memory.rs b/tokens-accounting/src/storage/in_memory.rs index cd4e8c0883..18743129ba 100644 --- a/tokens-accounting/src/storage/in_memory.rs +++ b/tokens-accounting/src/storage/in_memory.rs @@ -45,6 +45,14 @@ impl InMemoryTokensAccounting { circulating_supply, } } + + pub fn tokens_data(&self) -> &BTreeMap<TokenId, TokenData> { + &self.tokens_data + } + + pub fn circulating_supply(&self) -> &BTreeMap<TokenId, Amount> { + &self.circulating_supply + } } impl TokensAccountingStorageRead for InMemoryTokensAccounting {