From 767222e176cbab86dbf5a91066dee19f9cf5f919 Mon Sep 17 00:00:00 2001 From: Heorhii Azarov Date: Thu, 19 Sep 2024 11:01:17 +0300 Subject: [PATCH 1/2] Rework check frozen tokens function --- .../test-framework/src/random_tx_maker.rs | 35 ++- .../src/tests/fungible_tokens_v1.rs | 275 +++++++++++++++++- .../src/transaction_verifier/mod.rs | 132 ++++++--- 3 files changed, 380 insertions(+), 62 deletions(-) diff --git a/chainstate/test-framework/src/random_tx_maker.rs b/chainstate/test-framework/src/random_tx_maker.rs index 86ffdc119..d5b4fa307 100644 --- a/chainstate/test-framework/src/random_tx_maker.rs +++ b/chainstate/test-framework/src/random_tx_maker.rs @@ -95,7 +95,13 @@ fn get_random_token<'a>( .iter() .choose(rng) .and_then(|(token_id, _)| { - tip_view.get_token_data(token_id).unwrap().is_some().then_some(*token_id) + tip_view + .get_token_data(token_id) + .unwrap() + .is_some_and(|data| match data { + tokens_accounting::TokenData::FungibleToken(data) => !data.is_frozen(), + }) + .then_some(*token_id) }) .map(|token_id| { ( @@ -257,6 +263,20 @@ impl<'a> RandomTxMaker<'a> { let orders_db = OrdersAccountingDB::new(self.orders_store); let mut orders_cache = OrdersAccountingCache::new(&orders_db); + // Select random number of accounts to spend from + let account_inputs = self.select_accounts(rng); + + // Spend from accounts first then from utxo because account commands affect the state (like freezing tokens) + let (account_inputs, account_outputs) = self.create_account_spending( + rng, + &mut tokens_cache, + &pos_db, + &mut pos_delta, + &mut orders_cache, + &account_inputs, + key_manager, + ); + // Select random number of utxos to spend let inputs_with_utxos = { let mut inputs_with_utxos = self.select_utxos(rng); @@ -298,19 +318,6 @@ impl<'a> RandomTxMaker<'a> { inputs_with_utxos, ); - // Select random number of accounts to spend from - let account_inputs = self.select_accounts(rng); - - let (account_inputs, account_outputs) = self.create_account_spending( - rng, - &mut tokens_cache, - &pos_db, - &mut pos_delta, - &mut orders_cache, - &account_inputs, - key_manager, - ); - let mut inputs: Vec<_> = inputs.into_iter().chain(account_inputs).collect(); outputs.extend(account_outputs); diff --git a/chainstate/test-suite/src/tests/fungible_tokens_v1.rs b/chainstate/test-suite/src/tests/fungible_tokens_v1.rs index a79ad8afc..57e2239d3 100644 --- a/chainstate/test-suite/src/tests/fungible_tokens_v1.rs +++ b/chainstate/test-suite/src/tests/fungible_tokens_v1.rs @@ -33,8 +33,8 @@ use common::{ make_token_id, IsTokenFreezable, IsTokenUnfreezable, TokenId, TokenIssuance, TokenIssuanceV1, TokenTotalSupply, }, - AccountCommand, AccountNonce, AccountType, Block, Destination, GenBlock, OutPointSourceId, - SignedTransaction, Transaction, TxInput, TxOutput, UtxoOutPoint, + AccountCommand, AccountNonce, AccountType, Block, Destination, GenBlock, OrderData, + OutPointSourceId, SignedTransaction, Transaction, TxInput, TxOutput, UtxoOutPoint, }, primitives::{amount::SignedAmount, Amount, BlockHeight, CoinOrTokenId, Id, Idable}, }; @@ -4121,9 +4121,7 @@ fn check_freezable_supply(#[case] seed: Seed) { assert_eq!( result.unwrap_err(), ChainstateError::ProcessBlockError(BlockError::StateUpdateFailed( - ConnectTransactionError::TokensAccountingError( - tokens_accounting::Error::CannotLockFrozenToken(token_id) - ) + ConnectTransactionError::AttemptToSpendFrozenToken(token_id) )) ); @@ -4230,6 +4228,89 @@ fn check_freezable_supply(#[case] seed: Seed) { )) ); + // Try to implicitly burn frozen tokens + let result = tf + .make_block_builder() + .add_transaction( + TransactionBuilder::new() + // token input + .add_input( + TxInput::from_utxo(mint_tx_id.into(), 0), + InputWitness::NoSignature(None), + ) + // coin input + .add_input( + TxInput::from_utxo(freeze_tx_id.into(), 0), + InputWitness::NoSignature(None), + ) + // coin output + .add_output(TxOutput::Transfer( + OutputValue::Coin(token_supply_change_fee), + Destination::AnyoneCanSpend, + )) + .build(), + ) + .build_and_process(&mut rng); + + assert_eq!( + result.unwrap_err(), + ChainstateError::ProcessBlockError(BlockError::StateUpdateFailed( + ConnectTransactionError::AttemptToSpendFrozenToken(token_id) + )) + ); + + // Try to create an order with frozen token + let order_data = OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::TokenV1(token_id, amount_to_mint), + OutputValue::Coin(Amount::ZERO), + ); + let result = tf + .make_block_builder() + .add_transaction( + TransactionBuilder::new() + .add_input( + TxInput::from_utxo(mint_tx_id.into(), 0), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::AnyoneCanTake(Box::new(order_data))) + .build(), + ) + .build_and_process(&mut rng); + + assert_eq!( + result.unwrap_err(), + ChainstateError::ProcessBlockError(BlockError::StateUpdateFailed( + ConnectTransactionError::AttemptToSpendFrozenToken(token_id) + )) + ); + + // Try to create an order with frozen token + let order_data = OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::Coin(Amount::ZERO), + OutputValue::TokenV1(token_id, amount_to_mint), + ); + let result = tf + .make_block_builder() + .add_transaction( + TransactionBuilder::new() + .add_input( + TxInput::from_utxo(mint_tx_id.into(), 0), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::AnyoneCanTake(Box::new(order_data))) + .build(), + ) + .build_and_process(&mut rng); + + assert_eq!( + result.unwrap_err(), + ChainstateError::ProcessBlockError(BlockError::StateUpdateFailed( + ConnectTransactionError::AttemptToSpendFrozenToken(token_id) + )) + ); + // Unfreeze the token let unfreeze_tx = TransactionBuilder::new() .add_input( @@ -4264,7 +4345,181 @@ fn check_freezable_supply(#[case] seed: Seed) { tokens_accounting::TokenData::FungibleToken(data) => assert!(!data.is_frozen()), }; - // Now all operations are available again. Try mint/unmint/transfer + // Now all operations are available again. Try mint/transfer + tf.make_block_builder() + .add_transaction( + TransactionBuilder::new() + .add_input( + TxInput::from_command( + AccountNonce::new(3), + AccountCommand::MintTokens(token_id, amount_to_mint), + ), + InputWitness::NoSignature(None), + ) + .add_input( + TxInput::from_utxo(unfreeze_tx_id.into(), 0), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Transfer( + OutputValue::TokenV1(token_id, amount_to_mint), + Destination::AnyoneCanSpend, + )) + .build(), + ) + .build_and_process(&mut rng) + .unwrap(); + }); +} + +// Check that if token is frozen/unfrozen by an input command it takes effect only +// after tx is processed. Meaning tx outputs are not aware of state change by inputs in the same tx. +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn check_freeze_unfreeze_takes_effect_after_submit(#[case] seed: Seed) { + utils::concurrency::model(move || { + let mut rng = make_seedable_rng(seed); + let mut tf = TestFramework::builder(&mut rng).build(); + + let token_supply_change_fee = + tf.chainstate.get_chain_config().token_supply_change_fee(BlockHeight::zero()); + + let (token_id, _, utxo_with_change) = issue_token_from_genesis( + &mut rng, + &mut tf, + TokenTotalSupply::Lockable, + IsTokenFreezable::Yes, + ); + + // Mint some tokens + let amount_to_mint = Amount::from_atoms(rng.gen_range(1..100_000_000)); + let best_block_id = tf.best_block_id(); + let (_, mint_tx_id) = mint_tokens_in_block( + &mut rng, + &mut tf, + best_block_id, + utxo_with_change, + token_id, + amount_to_mint, + true, + ); + + // Freeze the token and transfer at the same tx + let freeze_tx = TransactionBuilder::new() + .add_input( + TxInput::from_command( + AccountNonce::new(1), + AccountCommand::FreezeToken(token_id, IsTokenUnfreezable::Yes), + ), + InputWitness::NoSignature(None), + ) + .add_input( + TxInput::from_utxo(mint_tx_id.into(), 0), + InputWitness::NoSignature(None), + ) + .add_input( + TxInput::from_utxo(mint_tx_id.into(), 1), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Transfer( + OutputValue::TokenV1(token_id, amount_to_mint), + Destination::AnyoneCanSpend, + )) + .add_output(TxOutput::Transfer( + OutputValue::Coin((token_supply_change_fee * 5).unwrap()), + Destination::AnyoneCanSpend, + )) + .build(); + let freeze_tx_id = freeze_tx.transaction().get_id(); + tf.make_block_builder() + .add_transaction(freeze_tx) + .build_and_process(&mut rng) + .unwrap(); + + // Check result + let actual_token_data = TokensAccountingStorageRead::get_token_data( + &tf.storage.transaction_ro().unwrap(), + &token_id, + ) + .unwrap(); + match actual_token_data.unwrap() { + tokens_accounting::TokenData::FungibleToken(data) => assert!(data.is_frozen()), + }; + + // Try unfreeze the token and transfer at the same tx + let result = tf + .make_block_builder() + .add_transaction( + TransactionBuilder::new() + .add_input( + TxInput::from_command( + AccountNonce::new(2), + AccountCommand::UnfreezeToken(token_id), + ), + InputWitness::NoSignature(None), + ) + .add_input( + TxInput::from_utxo(freeze_tx_id.into(), 0), + InputWitness::NoSignature(None), + ) + .add_input( + TxInput::from_utxo(freeze_tx_id.into(), 1), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Transfer( + OutputValue::Coin((token_supply_change_fee * 3).unwrap()), + Destination::AnyoneCanSpend, + )) + .add_output(TxOutput::Transfer( + OutputValue::TokenV1(token_id, amount_to_mint), + Destination::AnyoneCanSpend, + )) + .build(), + ) + .build_and_process(&mut rng); + + assert_eq!( + result.unwrap_err(), + ChainstateError::ProcessBlockError(BlockError::StateUpdateFailed( + ConnectTransactionError::AttemptToSpendFrozenToken(token_id) + )) + ); + + // Unfreeze the token + let unfreeze_tx = TransactionBuilder::new() + .add_input( + TxInput::from_command( + AccountNonce::new(2), + AccountCommand::UnfreezeToken(token_id), + ), + InputWitness::NoSignature(None), + ) + .add_input( + TxInput::from_utxo(freeze_tx_id.into(), 1), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Transfer( + OutputValue::Coin((token_supply_change_fee * 3).unwrap()), + Destination::AnyoneCanSpend, + )) + .build(); + let unfreeze_tx_id = unfreeze_tx.transaction().get_id(); + tf.make_block_builder() + .add_transaction(unfreeze_tx) + .build_and_process(&mut rng) + .unwrap(); + + // Check result + let actual_token_data = TokensAccountingStorageRead::get_token_data( + &tf.storage.transaction_ro().unwrap(), + &token_id, + ) + .unwrap(); + match actual_token_data.unwrap() { + tokens_accounting::TokenData::FungibleToken(data) => assert!(!data.is_frozen()), + }; + + // Now all operations are available again. Try mint/transfer tf.make_block_builder() .add_transaction( TransactionBuilder::new() @@ -5110,9 +5365,7 @@ fn check_change_authority_for_frozen_token(#[case] seed: Seed) { assert_eq!( result.unwrap_err(), ChainstateError::ProcessBlockError(BlockError::StateUpdateFailed( - ConnectTransactionError::TokensAccountingError( - tokens_accounting::Error::CannotChangeAuthorityForFrozenToken(token_id) - ) + ConnectTransactionError::AttemptToSpendFrozenToken(token_id) )) ); @@ -5634,9 +5887,7 @@ fn check_change_metadata_for_frozen_token(#[case] seed: Seed) { assert_eq!( result.unwrap_err(), ChainstateError::ProcessBlockError(BlockError::StateUpdateFailed( - ConnectTransactionError::TokensAccountingError( - tokens_accounting::Error::CannotChangeMetadataUriForFrozenToken(token_id) - ) + ConnectTransactionError::AttemptToSpendFrozenToken(token_id) )) ); diff --git a/chainstate/tx-verifier/src/transaction_verifier/mod.rs b/chainstate/tx-verifier/src/transaction_verifier/mod.rs index 53c471b1e..30f499988 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/mod.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/mod.rs @@ -32,7 +32,8 @@ use accounting::BlockRewardUndo; use constraints_value_accumulator::AccumulatedFee; use orders_accounting::{ OrdersAccountingCache, OrdersAccountingDB, OrdersAccountingDeltaData, - OrdersAccountingOperations, OrdersAccountingUndo, OrdersAccountingView, + OrdersAccountingOperations, OrdersAccountingStorageRead, OrdersAccountingUndo, + OrdersAccountingView, }; use tokens_accounting::{ TokenAccountingUndo, TokensAccountingCache, TokensAccountingDB, TokensAccountingDeltaData, @@ -82,7 +83,7 @@ use pos_accounting::{ PoSAccountingDB, PoSAccountingDelta, PoSAccountingDeltaData, PoSAccountingOperations, PoSAccountingUndo, PoSAccountingView, }; -use utxo::{ConsumedUtxoCache, UtxosCache, UtxosDB, UtxosView}; +use utxo::{ConsumedUtxoCache, UtxosCache, UtxosDB, UtxosStorageRead, UtxosView}; /// The change that a block has caused to the blockchain state #[derive(Debug, Eq, PartialEq)] @@ -685,43 +686,102 @@ where &self, tx: &Transaction, ) -> Result<(), ConnectTransactionError> { - tx.outputs() + let check_not_frozen = |token_id| { + // TODO: when NFTs are stored in accounting None should become an error + if let Some(token_data) = self.get_token_data(&token_id)? { + match token_data { + tokens_accounting::TokenData::FungibleToken(data) => { + ensure!( + !data.is_frozen(), + ConnectTransactionError::AttemptToSpendFrozenToken(token_id) + ); + } + }; + } + Ok(()) + }; + + let check_tx_output = |output: &TxOutput| match output { + TxOutput::Transfer(output_value, _) + | TxOutput::Burn(output_value) + | TxOutput::LockThenTransfer(output_value, _, _) + | TxOutput::Htlc(output_value, _) => match output_value { + OutputValue::Coin(_) | OutputValue::TokenV0(_) => Ok(()), + OutputValue::TokenV1(ref token_id, _) => check_not_frozen(*token_id), + }, + TxOutput::AnyoneCanTake(data) => { + [data.ask(), data.give()].iter().try_for_each(|v| match v { + OutputValue::TokenV0(_) | OutputValue::Coin(_) => Ok(()), + OutputValue::TokenV1(token_id, _) => check_not_frozen(*token_id), + }) + } + TxOutput::CreateStakePool(_, _) + | TxOutput::ProduceBlockFromStake(_, _) + | TxOutput::CreateDelegationId(_, _) + | TxOutput::DelegateStaking(_, _) + | TxOutput::IssueFungibleToken(_) + | TxOutput::IssueNft(_, _, _) + | TxOutput::DataDeposit(_) => Ok(()), + }; + + tx.inputs() .iter() - .try_for_each(|output| -> Result<(), ConnectTransactionError> { - match output { - TxOutput::Transfer(output_value, _) - | TxOutput::Burn(output_value) - | TxOutput::LockThenTransfer(output_value, _, _) - | TxOutput::Htlc(output_value, _) => { - match output_value { - OutputValue::Coin(_) | OutputValue::TokenV0(_) => Ok(()), - OutputValue::TokenV1(ref token_id, _) => { - // TODO: when NFTs are stored in accounting None should become an error - if let Some(token_data) = self.get_token_data(token_id)? { - match token_data { - tokens_accounting::TokenData::FungibleToken(data) => { - ensure!( - !data.is_frozen(), - ConnectTransactionError::AttemptToSpendFrozenToken( - *token_id - ) - ); - } - }; - } - Ok(()) - } - } + .try_for_each(|input| -> Result<(), ConnectTransactionError> { + match input { + TxInput::Utxo(utxo_outpoint) => { + let utxo = self + .get_utxo(utxo_outpoint) + .map_err(|_| ConnectTransactionError::TxVerifierStorage)? + .ok_or(ConnectTransactionError::MissingOutputOrSpent( + utxo_outpoint.clone(), + ))?; + check_tx_output(utxo.output()) } - TxOutput::CreateStakePool(_, _) - | TxOutput::ProduceBlockFromStake(_, _) - | TxOutput::CreateDelegationId(_, _) - | TxOutput::DelegateStaking(_, _) - | TxOutput::IssueFungibleToken(_) - | TxOutput::IssueNft(_, _, _) - | TxOutput::DataDeposit(_) - | TxOutput::AnyoneCanTake(_) => Ok(()), + TxInput::Account(account_outpoint) => match account_outpoint.account() { + AccountSpending::DelegationBalance(_, _) => Ok(()), + }, + TxInput::AccountCommand(_, account_command) => match account_command { + AccountCommand::MintTokens(id, _) + | AccountCommand::UnmintTokens(id) + | AccountCommand::LockTokenSupply(id) + | AccountCommand::FreezeToken(id, _) + | AccountCommand::ChangeTokenAuthority(id, _) + | AccountCommand::ChangeTokenMetadataUri(id, _) => check_not_frozen(*id), + | AccountCommand::UnfreezeToken(_) => Ok(()), + AccountCommand::ConcludeOrder(id) => { + let order_data = self.get_order_data(id)?.ok_or( + ConnectTransactionError::OrdersAccountingError( + orders_accounting::Error::OrderDataNotFound(*id), + ), + )?; + [order_data.ask(), order_data.give()].iter().try_for_each(|v| match v { + OutputValue::TokenV0(_) | OutputValue::Coin(_) => Ok(()), + OutputValue::TokenV1(token_id, _) => check_not_frozen(*token_id), + }) + } + AccountCommand::FillOrder(id, fill_value, _) => { + let order_data = self.get_order_data(id)?.ok_or( + ConnectTransactionError::OrdersAccountingError( + orders_accounting::Error::OrderDataNotFound(*id), + ), + )?; + [fill_value, order_data.ask(), order_data.give()].iter().try_for_each( + |v| match v { + OutputValue::TokenV0(_) | OutputValue::Coin(_) => Ok(()), + OutputValue::TokenV1(token_id, _) => { + check_not_frozen(*token_id) + } + }, + ) + } + }, } + })?; + + tx.outputs() + .iter() + .try_for_each(|output| -> Result<(), ConnectTransactionError> { + check_tx_output(output) }) } From b140bd113f73a4d02251d7b73c9f2ed042791587 Mon Sep 17 00:00:00 2001 From: Heorhii Azarov Date: Thu, 19 Sep 2024 12:57:58 +0300 Subject: [PATCH 2/2] Introduce forks --- .../src/tests/chainstate_storage_tests.rs | 8 +++-- .../test-suite/src/tests/fungible_tokens.rs | 6 +++- .../src/tests/fungible_tokens_v1.rs | 2 ++ chainstate/test-suite/src/tests/htlc.rs | 10 +++++-- chainstate/test-suite/src/tests/nft_burn.rs | 3 +- .../test-suite/src/tests/nft_issuance.rs | 6 ++-- .../test-suite/src/tests/nft_transfer.rs | 6 ++-- .../test-suite/src/tests/orders_tests.rs | 2 ++ chainstate/test-suite/src/tests/tx_fee.rs | 5 ++-- .../src/transaction_verifier/mod.rs | 19 ++++++++++-- common/src/chain/config/builder.rs | 29 +++++++++++++++++-- common/src/chain/config/mod.rs | 5 ++-- .../src/chain/upgrades/chainstate_upgrade.rs | 14 +++++++++ common/src/chain/upgrades/mod.rs | 5 ++-- 14 files changed, 97 insertions(+), 23 deletions(-) diff --git a/chainstate/test-suite/src/tests/chainstate_storage_tests.rs b/chainstate/test-suite/src/tests/chainstate_storage_tests.rs index dade3a9c6..279169f82 100644 --- a/chainstate/test-suite/src/tests/chainstate_storage_tests.rs +++ b/chainstate/test-suite/src/tests/chainstate_storage_tests.rs @@ -25,8 +25,9 @@ use common::{ output_value::OutputValue, tokens::{make_token_id, NftIssuance, TokenAuxiliaryData, TokenIssuanceV0}, ChainstateUpgrade, ChangeTokenMetadataUriActivated, DataDepositFeeVersion, Destination, - HtlcActivated, NetUpgrades, OrdersActivated, OutPointSourceId, RewardDistributionVersion, - TokenIssuanceVersion, TokensFeeVersion, Transaction, TxInput, TxOutput, UtxoOutPoint, + FrozenTokensValidationVersion, HtlcActivated, NetUpgrades, OrdersActivated, + OutPointSourceId, RewardDistributionVersion, TokenIssuanceVersion, TokensFeeVersion, + Transaction, TxInput, TxOutput, UtxoOutPoint, }, primitives::{Amount, Id, Idable}, }; @@ -121,6 +122,7 @@ fn store_fungible_token_v0(#[case] seed: Seed) { TokensFeeVersion::V1, DataDepositFeeVersion::V1, ChangeTokenMetadataUriActivated::Yes, + FrozenTokensValidationVersion::V1, HtlcActivated::Yes, OrdersActivated::Yes, ), @@ -203,6 +205,7 @@ fn store_nft_v0(#[case] seed: Seed) { TokensFeeVersion::V1, DataDepositFeeVersion::V1, ChangeTokenMetadataUriActivated::Yes, + FrozenTokensValidationVersion::V1, HtlcActivated::Yes, OrdersActivated::Yes, ), @@ -516,6 +519,7 @@ fn store_aux_data_from_issue_nft(#[case] seed: Seed) { TokensFeeVersion::V1, DataDepositFeeVersion::V1, ChangeTokenMetadataUriActivated::Yes, + FrozenTokensValidationVersion::V1, HtlcActivated::Yes, OrdersActivated::Yes, ), diff --git a/chainstate/test-suite/src/tests/fungible_tokens.rs b/chainstate/test-suite/src/tests/fungible_tokens.rs index 954daf0e0..1de20308c 100644 --- a/chainstate/test-suite/src/tests/fungible_tokens.rs +++ b/chainstate/test-suite/src/tests/fungible_tokens.rs @@ -21,7 +21,7 @@ use chainstate::{ }; use chainstate_test_framework::{get_output_value, TestFramework, TransactionBuilder}; use common::chain::tokens::{Metadata, NftIssuanceV0, TokenIssuanceV0, TokenTransfer}; -use common::chain::{RewardDistributionVersion, UtxoOutPoint}; +use common::chain::{FrozenTokensValidationVersion, RewardDistributionVersion, UtxoOutPoint}; use common::primitives::{id, BlockHeight, Id}; use common::{ chain::{ @@ -59,6 +59,7 @@ fn make_test_framework_with_v0(rng: &mut (impl Rng + CryptoRng)) -> TestFramewor TokensFeeVersion::V1, DataDepositFeeVersion::V1, ChangeTokenMetadataUriActivated::Yes, + FrozenTokensValidationVersion::V1, HtlcActivated::Yes, OrdersActivated::Yes, ), @@ -966,6 +967,7 @@ fn no_v0_issuance_after_v1(#[case] seed: Seed) { TokensFeeVersion::V1, DataDepositFeeVersion::V1, ChangeTokenMetadataUriActivated::Yes, + FrozenTokensValidationVersion::V1, HtlcActivated::Yes, OrdersActivated::Yes, ), @@ -1032,6 +1034,7 @@ fn no_v0_transfer_after_v1(#[case] seed: Seed) { TokensFeeVersion::V1, DataDepositFeeVersion::V1, ChangeTokenMetadataUriActivated::Yes, + FrozenTokensValidationVersion::V1, HtlcActivated::Yes, OrdersActivated::Yes, ), @@ -1044,6 +1047,7 @@ fn no_v0_transfer_after_v1(#[case] seed: Seed) { TokensFeeVersion::V1, DataDepositFeeVersion::V1, ChangeTokenMetadataUriActivated::Yes, + FrozenTokensValidationVersion::V1, HtlcActivated::Yes, OrdersActivated::Yes, ), diff --git a/chainstate/test-suite/src/tests/fungible_tokens_v1.rs b/chainstate/test-suite/src/tests/fungible_tokens_v1.rs index 57e2239d3..f8c5dcf68 100644 --- a/chainstate/test-suite/src/tests/fungible_tokens_v1.rs +++ b/chainstate/test-suite/src/tests/fungible_tokens_v1.rs @@ -6072,6 +6072,7 @@ fn test_change_metadata_uri_activation(#[case] seed: Seed) { common::chain::TokensFeeVersion::V1, common::chain::DataDepositFeeVersion::V1, common::chain::ChangeTokenMetadataUriActivated::No, + common::chain::FrozenTokensValidationVersion::V1, common::chain::HtlcActivated::Yes, common::chain::OrdersActivated::Yes, ), @@ -6084,6 +6085,7 @@ fn test_change_metadata_uri_activation(#[case] seed: Seed) { common::chain::TokensFeeVersion::V1, common::chain::DataDepositFeeVersion::V1, common::chain::ChangeTokenMetadataUriActivated::Yes, + common::chain::FrozenTokensValidationVersion::V1, common::chain::HtlcActivated::Yes, common::chain::OrdersActivated::Yes, ), diff --git a/chainstate/test-suite/src/tests/htlc.rs b/chainstate/test-suite/src/tests/htlc.rs index a011039b5..8df51e09c 100644 --- a/chainstate/test-suite/src/tests/htlc.rs +++ b/chainstate/test-suite/src/tests/htlc.rs @@ -39,9 +39,9 @@ use common::{ timelock::OutputTimeLock, tokens::{make_token_id, TokenData, TokenIssuance, TokenTransfer}, AccountCommand, AccountNonce, ChainConfig, ChainstateUpgrade, - ChangeTokenMetadataUriActivated, DataDepositFeeVersion, Destination, HtlcActivated, - OrdersActivated, RewardDistributionVersion, TokenIssuanceVersion, TokensFeeVersion, - TxInput, TxOutput, + ChangeTokenMetadataUriActivated, DataDepositFeeVersion, Destination, + FrozenTokensValidationVersion, HtlcActivated, OrdersActivated, RewardDistributionVersion, + TokenIssuanceVersion, TokensFeeVersion, TxInput, TxOutput, }, primitives::{Amount, Idable}, }; @@ -590,6 +590,7 @@ fn fork_activation(#[case] seed: Seed) { TokensFeeVersion::V1, DataDepositFeeVersion::V1, ChangeTokenMetadataUriActivated::Yes, + FrozenTokensValidationVersion::V1, HtlcActivated::No, OrdersActivated::No, ), @@ -602,6 +603,7 @@ fn fork_activation(#[case] seed: Seed) { TokensFeeVersion::V1, DataDepositFeeVersion::V1, ChangeTokenMetadataUriActivated::Yes, + FrozenTokensValidationVersion::V1, HtlcActivated::Yes, OrdersActivated::No, ), @@ -693,6 +695,7 @@ fn spend_tokens(#[case] seed: Seed) { TokensFeeVersion::V1, DataDepositFeeVersion::V1, ChangeTokenMetadataUriActivated::Yes, + FrozenTokensValidationVersion::V1, HtlcActivated::Yes, OrdersActivated::Yes, ), @@ -705,6 +708,7 @@ fn spend_tokens(#[case] seed: Seed) { TokensFeeVersion::V1, DataDepositFeeVersion::V1, ChangeTokenMetadataUriActivated::Yes, + FrozenTokensValidationVersion::V1, HtlcActivated::Yes, OrdersActivated::Yes, ), diff --git a/chainstate/test-suite/src/tests/nft_burn.rs b/chainstate/test-suite/src/tests/nft_burn.rs index 23ed642e9..5ab906c8b 100644 --- a/chainstate/test-suite/src/tests/nft_burn.rs +++ b/chainstate/test-suite/src/tests/nft_burn.rs @@ -21,7 +21,7 @@ use common::chain::{ HtlcActivated, OrdersActivated, RewardDistributionVersion, TokenIssuanceVersion, TokensFeeVersion, TxInput, TxOutput, }; -use common::chain::{OutPointSourceId, UtxoOutPoint}; +use common::chain::{FrozenTokensValidationVersion, OutPointSourceId, UtxoOutPoint}; use common::primitives::{Amount, BlockHeight, CoinOrTokenId, Idable}; use randomness::Rng; use rstest::rstest; @@ -218,6 +218,7 @@ fn no_v0_issuance_after_v1(#[case] seed: Seed) { TokensFeeVersion::V1, DataDepositFeeVersion::V1, ChangeTokenMetadataUriActivated::Yes, + FrozenTokensValidationVersion::V1, HtlcActivated::Yes, OrdersActivated::Yes, ), diff --git a/chainstate/test-suite/src/tests/nft_issuance.rs b/chainstate/test-suite/src/tests/nft_issuance.rs index b3ee6af3c..09de0cc69 100644 --- a/chainstate/test-suite/src/tests/nft_issuance.rs +++ b/chainstate/test-suite/src/tests/nft_issuance.rs @@ -23,8 +23,8 @@ use common::chain::{ signature::inputsig::InputWitness, tokens::{is_rfc3986_valid_symbol, make_token_id, Metadata, NftIssuance, NftIssuanceV0}, Block, ChainstateUpgrade, ChangeTokenMetadataUriActivated, DataDepositFeeVersion, Destination, - HtlcActivated, OrdersActivated, OutPointSourceId, RewardDistributionVersion, - TokenIssuanceVersion, TokensFeeVersion, TxInput, TxOutput, + FrozenTokensValidationVersion, HtlcActivated, OrdersActivated, OutPointSourceId, + RewardDistributionVersion, TokenIssuanceVersion, TokensFeeVersion, TxInput, TxOutput, }; use common::primitives::{BlockHeight, Idable}; use randomness::{CryptoRng, Rng}; @@ -1654,6 +1654,7 @@ fn no_v0_issuance_after_v1(#[case] seed: Seed) { TokensFeeVersion::V1, DataDepositFeeVersion::V1, ChangeTokenMetadataUriActivated::Yes, + FrozenTokensValidationVersion::V1, HtlcActivated::Yes, OrdersActivated::Yes, ), @@ -1720,6 +1721,7 @@ fn only_ascii_alphanumeric_after_v1(#[case] seed: Seed) { TokensFeeVersion::V1, DataDepositFeeVersion::V1, ChangeTokenMetadataUriActivated::Yes, + FrozenTokensValidationVersion::V1, HtlcActivated::Yes, OrdersActivated::Yes, ), diff --git a/chainstate/test-suite/src/tests/nft_transfer.rs b/chainstate/test-suite/src/tests/nft_transfer.rs index 66f6eb112..40581fe00 100644 --- a/chainstate/test-suite/src/tests/nft_transfer.rs +++ b/chainstate/test-suite/src/tests/nft_transfer.rs @@ -22,8 +22,9 @@ use common::{ signature::inputsig::InputWitness, tokens::{make_token_id, NftIssuance, TokenId}, ChainstateUpgrade, ChangeTokenMetadataUriActivated, DataDepositFeeVersion, Destination, - HtlcActivated, NetUpgrades, OrdersActivated, OutPointSourceId, RewardDistributionVersion, - TokenIssuanceVersion, TokensFeeVersion, TxInput, TxOutput, + FrozenTokensValidationVersion, HtlcActivated, NetUpgrades, OrdersActivated, + OutPointSourceId, RewardDistributionVersion, TokenIssuanceVersion, TokensFeeVersion, + TxInput, TxOutput, }, primitives::{Amount, BlockHeight, CoinOrTokenId}, }; @@ -372,6 +373,7 @@ fn ensure_nft_cannot_be_printed_from_tokens_op(#[case] seed: Seed) { TokensFeeVersion::V1, DataDepositFeeVersion::V1, ChangeTokenMetadataUriActivated::Yes, + FrozenTokensValidationVersion::V1, HtlcActivated::Yes, OrdersActivated::Yes, ), diff --git a/chainstate/test-suite/src/tests/orders_tests.rs b/chainstate/test-suite/src/tests/orders_tests.rs index ab73f3fda..d88335d8d 100644 --- a/chainstate/test-suite/src/tests/orders_tests.rs +++ b/chainstate/test-suite/src/tests/orders_tests.rs @@ -1507,6 +1507,7 @@ fn test_activation(#[case] seed: Seed) { common::chain::TokensFeeVersion::V1, common::chain::DataDepositFeeVersion::V1, common::chain::ChangeTokenMetadataUriActivated::Yes, + common::chain::FrozenTokensValidationVersion::V1, common::chain::HtlcActivated::No, common::chain::OrdersActivated::No, ), @@ -1519,6 +1520,7 @@ fn test_activation(#[case] seed: Seed) { common::chain::TokensFeeVersion::V1, common::chain::DataDepositFeeVersion::V1, common::chain::ChangeTokenMetadataUriActivated::Yes, + common::chain::FrozenTokensValidationVersion::V1, common::chain::HtlcActivated::No, common::chain::OrdersActivated::Yes, ), diff --git a/chainstate/test-suite/src/tests/tx_fee.rs b/chainstate/test-suite/src/tests/tx_fee.rs index 5b42e2a3e..a4c824ac1 100644 --- a/chainstate/test-suite/src/tests/tx_fee.rs +++ b/chainstate/test-suite/src/tests/tx_fee.rs @@ -34,8 +34,8 @@ use common::{ TokenTotalSupply, }, ChainConfig, ChainstateUpgrade, ChangeTokenMetadataUriActivated, DataDepositFeeVersion, - Destination, HtlcActivated, NetUpgrades, OrdersActivated, TokenIssuanceVersion, - TokensFeeVersion, TxInput, TxOutput, UtxoOutPoint, + Destination, FrozenTokensValidationVersion, HtlcActivated, NetUpgrades, OrdersActivated, + TokenIssuanceVersion, TokensFeeVersion, TxInput, TxOutput, UtxoOutPoint, }, primitives::{Amount, Fee, Idable}, }; @@ -579,6 +579,7 @@ fn issue_fungible_token_v0(#[case] seed: Seed) { TokensFeeVersion::V1, DataDepositFeeVersion::V1, ChangeTokenMetadataUriActivated::Yes, + FrozenTokensValidationVersion::V1, HtlcActivated::Yes, OrdersActivated::Yes, ), diff --git a/chainstate/tx-verifier/src/transaction_verifier/mod.rs b/chainstate/tx-verifier/src/transaction_verifier/mod.rs index 30f499988..864a58739 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/mod.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/mod.rs @@ -75,7 +75,8 @@ use common::{ signed_transaction::SignedTransaction, tokens::make_token_id, AccountCommand, AccountNonce, AccountSpending, AccountType, Block, ChainConfig, - DelegationId, GenBlock, Transaction, TxInput, TxOutput, UtxoOutPoint, + DelegationId, FrozenTokensValidationVersion, GenBlock, Transaction, TxInput, TxOutput, + UtxoOutPoint, }, primitives::{id::WithId, Amount, BlockHeight, Fee, Id, Idable}, }; @@ -542,7 +543,7 @@ where tx_source: &TransactionSourceForConnect, tx: &Transaction, ) -> Result<(), ConnectTransactionError> { - self.check_operations_with_frozen_tokens(tx)?; + self.check_operations_with_frozen_tokens(tx, tx_source.expected_block_height())?; let input_undos = tx .inputs() @@ -685,6 +686,7 @@ where fn check_operations_with_frozen_tokens( &self, tx: &Transaction, + block_height: BlockHeight, ) -> Result<(), ConnectTransactionError> { let check_not_frozen = |token_id| { // TODO: when NFTs are stored in accounting None should become an error @@ -735,7 +737,18 @@ where .ok_or(ConnectTransactionError::MissingOutputOrSpent( utxo_outpoint.clone(), ))?; - check_tx_output(utxo.output()) + + match self + .chain_config + .as_ref() + .chainstate_upgrades() + .version_at_height(block_height) + .1 + .frozen_tokens_validation_version() + { + FrozenTokensValidationVersion::V0 => Ok(()), + FrozenTokensValidationVersion::V1 => check_tx_output(utxo.output()), + } } TxInput::Account(account_outpoint) => match account_outpoint.account() { AccountSpending::DelegationBalance(_, _) => Ok(()), diff --git a/common/src/chain/config/builder.rs b/common/src/chain/config/builder.rs index 9743fa5e4..53ca6dc17 100644 --- a/common/src/chain/config/builder.rs +++ b/common/src/chain/config/builder.rs @@ -29,9 +29,9 @@ use crate::{ pos_initial_difficulty, pow::PoWChainConfigBuilder, ChainstateUpgrade, ChangeTokenMetadataUriActivated, CoinUnit, ConsensusUpgrade, - DataDepositFeeVersion, Destination, GenBlock, Genesis, HtlcActivated, NetUpgrades, - OrdersActivated, PoSChainConfig, PoSConsensusVersion, PoWChainConfig, - RewardDistributionVersion, TokenIssuanceVersion, TokensFeeVersion, + DataDepositFeeVersion, Destination, FrozenTokensValidationVersion, GenBlock, Genesis, + HtlcActivated, NetUpgrades, OrdersActivated, PoSChainConfig, PoSConsensusVersion, + PoWChainConfig, RewardDistributionVersion, TokenIssuanceVersion, TokensFeeVersion, }, primitives::{ id::WithId, per_thousand::PerThousand, semver::SemVer, Amount, BlockCount, BlockDistance, @@ -55,6 +55,8 @@ const TESTNET_STAKER_REWARD_AND_TOKENS_FEE_FORK_HEIGHT: BlockHeight = BlockHeigh const TESTNET_HTLC_AND_DATA_DEPOSIT_FEE_FORK_HEIGHT: BlockHeight = BlockHeight::new(297_550); // The fork, at which order outputs become valid const TESTNET_ORDERS_FORK_HEIGHT: BlockHeight = BlockHeight::new(99_999_999); +// The fork, at which rules for validating frozen tokens changed +const MAINNET_FROZEN_TOKENS_VALIDATION_FORK_HEIGHT: BlockHeight = BlockHeight::new(99_999_999); // The fork, at which txs with htlc and orders outputs become valid const MAINNET_HTLC_AND_ORDERS_FORK_HEIGHT: BlockHeight = BlockHeight::new(99_999_999); @@ -172,10 +174,24 @@ impl ChainType { TokensFeeVersion::V1, DataDepositFeeVersion::V0, ChangeTokenMetadataUriActivated::No, + FrozenTokensValidationVersion::V0, HtlcActivated::No, OrdersActivated::No, ), ), + ( + MAINNET_FROZEN_TOKENS_VALIDATION_FORK_HEIGHT, + ChainstateUpgrade::new( + TokenIssuanceVersion::V1, + RewardDistributionVersion::V1, + TokensFeeVersion::V1, + DataDepositFeeVersion::V1, + ChangeTokenMetadataUriActivated::Yes, + FrozenTokensValidationVersion::V1, + HtlcActivated::Yes, + OrdersActivated::Yes, + ), + ), ( MAINNET_HTLC_AND_ORDERS_FORK_HEIGHT, ChainstateUpgrade::new( @@ -184,6 +200,7 @@ impl ChainType { TokensFeeVersion::V1, DataDepositFeeVersion::V1, ChangeTokenMetadataUriActivated::Yes, + FrozenTokensValidationVersion::V1, HtlcActivated::Yes, OrdersActivated::Yes, ), @@ -200,6 +217,7 @@ impl ChainType { TokensFeeVersion::V1, DataDepositFeeVersion::V1, ChangeTokenMetadataUriActivated::Yes, + FrozenTokensValidationVersion::V1, HtlcActivated::Yes, OrdersActivated::Yes, ), @@ -216,6 +234,7 @@ impl ChainType { TokensFeeVersion::V0, DataDepositFeeVersion::V0, ChangeTokenMetadataUriActivated::No, + FrozenTokensValidationVersion::V0, HtlcActivated::No, OrdersActivated::No, ), @@ -228,6 +247,7 @@ impl ChainType { TokensFeeVersion::V0, DataDepositFeeVersion::V0, ChangeTokenMetadataUriActivated::No, + FrozenTokensValidationVersion::V0, HtlcActivated::No, OrdersActivated::No, ), @@ -240,6 +260,7 @@ impl ChainType { TokensFeeVersion::V1, DataDepositFeeVersion::V0, ChangeTokenMetadataUriActivated::No, + FrozenTokensValidationVersion::V0, HtlcActivated::No, OrdersActivated::No, ), @@ -252,6 +273,7 @@ impl ChainType { TokensFeeVersion::V1, DataDepositFeeVersion::V1, ChangeTokenMetadataUriActivated::Yes, + FrozenTokensValidationVersion::V0, HtlcActivated::Yes, OrdersActivated::No, ), @@ -264,6 +286,7 @@ impl ChainType { TokensFeeVersion::V1, DataDepositFeeVersion::V1, ChangeTokenMetadataUriActivated::Yes, + FrozenTokensValidationVersion::V1, HtlcActivated::Yes, OrdersActivated::Yes, ), diff --git a/common/src/chain/config/mod.rs b/common/src/chain/config/mod.rs index c5591873a..c325eedfe 100644 --- a/common/src/chain/config/mod.rs +++ b/common/src/chain/config/mod.rs @@ -53,8 +53,8 @@ use super::output_value::OutputValue; use super::{stakelock::StakePoolData, RequiredConsensus}; use super::{ ChainstateUpgrade, ChangeTokenMetadataUriActivated, ConsensusUpgrade, DataDepositFeeVersion, - HtlcActivated, OrdersActivated, RewardDistributionVersion, TokenIssuanceVersion, - TokensFeeVersion, + FrozenTokensValidationVersion, HtlcActivated, OrdersActivated, RewardDistributionVersion, + TokenIssuanceVersion, TokensFeeVersion, }; const DEFAULT_MAX_FUTURE_BLOCK_TIME_OFFSET_V1: Duration = Duration::from_secs(120); @@ -902,6 +902,7 @@ pub fn create_unit_test_config_builder() -> Builder { TokensFeeVersion::V1, DataDepositFeeVersion::V1, ChangeTokenMetadataUriActivated::Yes, + FrozenTokensValidationVersion::V1, HtlcActivated::Yes, OrdersActivated::Yes, ), diff --git a/common/src/chain/upgrades/chainstate_upgrade.rs b/common/src/chain/upgrades/chainstate_upgrade.rs index d1dd94d45..80ccce140 100644 --- a/common/src/chain/upgrades/chainstate_upgrade.rs +++ b/common/src/chain/upgrades/chainstate_upgrade.rs @@ -63,6 +63,12 @@ pub enum ChangeTokenMetadataUriActivated { No, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)] +pub enum FrozenTokensValidationVersion { + V0, + V1, +} + #[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd)] pub struct ChainstateUpgrade { token_issuance_version: TokenIssuanceVersion, @@ -70,17 +76,20 @@ pub struct ChainstateUpgrade { tokens_fee_version: TokensFeeVersion, data_deposit_fee_version: DataDepositFeeVersion, change_token_metadata_uri_activated: ChangeTokenMetadataUriActivated, + frozen_tokens_validation_version: FrozenTokensValidationVersion, htlc_activated: HtlcActivated, orders_activated: OrdersActivated, } impl ChainstateUpgrade { + #[allow(clippy::too_many_arguments)] pub fn new( token_issuance_version: TokenIssuanceVersion, reward_distribution_version: RewardDistributionVersion, tokens_fee_version: TokensFeeVersion, data_deposit_fee_version: DataDepositFeeVersion, change_token_metadata_uri_activated: ChangeTokenMetadataUriActivated, + frozen_tokens_validation_version: FrozenTokensValidationVersion, htlc_activated: HtlcActivated, orders_activated: OrdersActivated, ) -> Self { @@ -90,6 +99,7 @@ impl ChainstateUpgrade { tokens_fee_version, data_deposit_fee_version, change_token_metadata_uri_activated, + frozen_tokens_validation_version, htlc_activated, orders_activated, } @@ -122,6 +132,10 @@ impl ChainstateUpgrade { pub fn change_token_metadata_uri_activated(&self) -> ChangeTokenMetadataUriActivated { self.change_token_metadata_uri_activated } + + pub fn frozen_tokens_validation_version(&self) -> FrozenTokensValidationVersion { + self.frozen_tokens_validation_version + } } impl Activate for ChainstateUpgrade {} diff --git a/common/src/chain/upgrades/mod.rs b/common/src/chain/upgrades/mod.rs index 20c049e20..b9df4d5b1 100644 --- a/common/src/chain/upgrades/mod.rs +++ b/common/src/chain/upgrades/mod.rs @@ -18,8 +18,9 @@ mod consensus_upgrade; mod netupgrade; pub use chainstate_upgrade::{ - ChainstateUpgrade, ChangeTokenMetadataUriActivated, DataDepositFeeVersion, HtlcActivated, - OrdersActivated, RewardDistributionVersion, TokenIssuanceVersion, TokensFeeVersion, + ChainstateUpgrade, ChangeTokenMetadataUriActivated, DataDepositFeeVersion, + FrozenTokensValidationVersion, HtlcActivated, OrdersActivated, RewardDistributionVersion, + TokenIssuanceVersion, TokensFeeVersion, }; pub use consensus_upgrade::{ConsensusUpgrade, PoSStatus, PoWStatus, RequiredConsensus}; pub use netupgrade::{Activate, NetUpgrades};