@@ -30,9 +30,10 @@ use chainstate_test_framework::{
3030} ;
3131use common:: {
3232 chain:: {
33- AccountCommand , AccountNonce , AccountType , Block , ChainstateUpgradeBuilder , Destination ,
34- GenBlock , OrderAccountCommand , OrderData , OutPointSourceId , SignedTransaction , Transaction ,
35- TxInput , TxOutput , UtxoOutPoint ,
33+ self , AccountCommand , AccountNonce , AccountType , Block , ChainstateUpgradeBuilder ,
34+ Destination , GenBlock , NetUpgrades , OrderAccountCommand , OrderData , OutPointSourceId ,
35+ SignedTransaction , Transaction , TxInput , TxOutput , TxOutputTag , UtxoOutPoint ,
36+ ZeroTokenTransferForbidden ,
3637 htlc:: { HashedTimelockContract , HtlcSecret } ,
3738 make_order_id, make_token_id,
3839 output_value:: OutputValue ,
@@ -50,9 +51,10 @@ use common::{
5051 primitives:: { Amount , BlockHeight , CoinOrTokenId , Id , Idable , amount:: SignedAmount } ,
5152} ;
5253use crypto:: key:: { KeyKind , PrivateKey } ;
53- use randomness:: { CryptoRng , RngExt as _} ;
54+ use randomness:: { CryptoRng , Rng , RngExt as _} ;
55+ use strum:: IntoEnumIterator as _;
5456use test_utils:: {
55- assert_matches_return_val, gen_text_with_non_ascii,
57+ assert_matches , assert_matches_return_val, gen_text_with_non_ascii,
5658 random:: { Seed , make_seedable_rng} ,
5759 random_ascii_alphanumeric_string, split_value,
5860} ;
@@ -7530,47 +7532,219 @@ fn token_id_generation_v1_activation(#[case] seed: Seed) {
75307532 } ) ;
75317533}
75327534
7533- // Transferring zero tokens is allowed.
7534- // TODO: perhaps we should prohibit it?
7535+ // Transferring zero tokens is allowed before the corresponding fork and forbidden after.
75357536#[ rstest]
7536- #[ trace]
75377537#[ case( Seed :: from_entropy( ) ) ]
7538+ #[ trace]
75387539fn zero_amount_transfer ( #[ case] seed : Seed ) {
75397540 utils:: concurrency:: model ( move || {
75407541 let mut rng = make_seedable_rng ( seed) ;
7541- let mut tf = TestFramework :: builder ( & mut rng) . build ( ) ;
75427542
7543- let ( token_id, _, _, _, utxo_with_change) = issue_token_from_genesis (
7543+ // We'll be creating 1 or 2 blocks after the genesis, so the fork height should be at least 3.
7544+ let fork_height = BlockHeight :: new ( rng. random_range ( 3 ..=5 ) ) ;
7545+ let chain_config = chain:: config:: create_unit_test_config_builder ( )
7546+ . chainstate_upgrades (
7547+ NetUpgrades :: initialize ( vec ! [
7548+ (
7549+ BlockHeight :: zero( ) ,
7550+ ChainstateUpgradeBuilder :: latest( )
7551+ . zero_token_transfer_forbidden( ZeroTokenTransferForbidden :: No )
7552+ . build( ) ,
7553+ ) ,
7554+ (
7555+ fork_height,
7556+ ChainstateUpgradeBuilder :: latest( )
7557+ . zero_token_transfer_forbidden( ZeroTokenTransferForbidden :: Yes )
7558+ . build( ) ,
7559+ ) ,
7560+ ] )
7561+ . unwrap ( ) ,
7562+ )
7563+ . build ( ) ;
7564+
7565+ let mut tf = TestFramework :: builder ( & mut rng) . with_chain_config ( chain_config) . build ( ) ;
7566+
7567+ let ( real_token_id, _, _, _, utxo_with_change) = issue_token_from_genesis (
75447568 & mut rng,
75457569 & mut tf,
75467570 TokenTotalSupply :: Unlimited ,
75477571 IsTokenFreezable :: No ,
75487572 ) ;
75497573
7550- let tx = TransactionBuilder :: new ( )
7551- . add_input ( utxo_with_change. into ( ) , InputWitness :: NoSignature ( None ) )
7552- . add_output ( TxOutput :: Transfer (
7553- OutputValue :: TokenV1 ( token_id, Amount :: ZERO ) ,
7554- Destination :: AnyoneCanSpend ,
7555- ) )
7556- . build ( ) ;
7574+ // Optionally, mint some tokens.
7575+ let mut utxo_with_change = if rng. random_bool ( 0.5 ) {
7576+ let amount_to_mint = Amount :: from_atoms ( rng. random_range ( 1 ..100_000 ) ) ;
7577+ let best_block_id = tf. best_block_id ( ) ;
7578+ let ( _, mint_tx_id) = mint_tokens_in_block (
7579+ & mut rng,
7580+ & mut tf,
7581+ best_block_id,
7582+ utxo_with_change,
7583+ real_token_id,
7584+ amount_to_mint,
7585+ true ,
7586+ ) ;
75577587
7558- tf. make_block_builder ( )
7559- . add_transaction ( tx)
7560- . build_and_process ( & mut rng)
7561- . unwrap ( )
7562- . unwrap ( ) ;
7588+ UtxoOutPoint :: new ( mint_tx_id. into ( ) , 1 )
7589+ } else {
7590+ utxo_with_change
7591+ } ;
7592+ let coins_amount = tf. coin_amount_from_utxo ( & utxo_with_change) ;
7593+
7594+ let bogus_token_id = TokenId :: random_using ( & mut rng) ;
7595+
7596+ let zero_transfer_outputs_with_real_token =
7597+ make_zero_transfer_outputs_for_token_zero_amount_transfer_test (
7598+ & real_token_id,
7599+ & mut rng,
7600+ ) ;
7601+ let zero_transfer_outputs_with_bogus_token =
7602+ make_zero_transfer_outputs_for_token_zero_amount_transfer_test (
7603+ & bogus_token_id,
7604+ & mut rng,
7605+ ) ;
7606+
7607+ // Before the fork zero transfers are allowed.
7608+ {
7609+ let initial_block_height = tf. best_block_index ( ) . block_height ( ) . next_height ( ) ;
7610+ for _ in initial_block_height. iter_up_to ( fork_height) {
7611+ let mut block_builder = tf. make_block_builder ( ) ;
7612+
7613+ for tx_output in zero_transfer_outputs_with_real_token
7614+ . iter ( )
7615+ . chain ( zero_transfer_outputs_with_bogus_token. iter ( ) )
7616+ {
7617+ let tx = TransactionBuilder :: new ( )
7618+ . add_input ( utxo_with_change. into ( ) , InputWitness :: NoSignature ( None ) )
7619+ . add_output ( TxOutput :: Transfer (
7620+ OutputValue :: Coin ( coins_amount) ,
7621+ Destination :: AnyoneCanSpend ,
7622+ ) )
7623+ . add_output ( tx_output. clone ( ) )
7624+ . build ( ) ;
7625+ let tx_id = tx. transaction ( ) . get_id ( ) ;
7626+
7627+ block_builder = block_builder. add_transaction ( tx) ;
7628+
7629+ utxo_with_change = UtxoOutPoint :: new ( tx_id. into ( ) , 0 )
7630+ }
7631+
7632+ block_builder. build_and_process ( & mut rng) . unwrap ( ) . unwrap ( ) ;
7633+ }
7634+ }
7635+
7636+ // Sanity check
7637+ {
7638+ let new_block_height = tf. best_block_index ( ) . block_height ( ) . next_height ( ) ;
7639+ assert_eq ! ( new_block_height, fork_height) ;
7640+ }
7641+
7642+ // After the fork zero transfers are not allowed.
7643+ {
7644+ for tx_output in zero_transfer_outputs_with_real_token
7645+ . iter ( )
7646+ . chain ( zero_transfer_outputs_with_bogus_token. iter ( ) )
7647+ {
7648+ let tx = TransactionBuilder :: new ( )
7649+ . add_input (
7650+ utxo_with_change. clone ( ) . into ( ) ,
7651+ InputWitness :: NoSignature ( None ) ,
7652+ )
7653+ . add_output ( TxOutput :: Transfer (
7654+ OutputValue :: Coin ( coins_amount) ,
7655+ Destination :: AnyoneCanSpend ,
7656+ ) )
7657+ . add_output ( tx_output. clone ( ) )
7658+ . build ( ) ;
7659+
7660+ let err = tf
7661+ . make_block_builder ( )
7662+ . add_transaction ( tx)
7663+ . build_and_process ( & mut rng)
7664+ . unwrap_err ( ) ;
7665+ assert_matches ! (
7666+ err,
7667+ ChainstateError :: ProcessBlockError ( BlockError :: StateUpdateFailed (
7668+ ConnectTransactionError :: ZeroTokenTransfer ( _) ,
7669+ ) )
7670+ )
7671+ }
7672+ }
75637673 } ) ;
75647674}
75657675
7566- // For a frozen token, even zero amount transfers are not allowed.
7676+ // Create zero amount transfer outputs for the given token. This is used in the zero amount transfer
7677+ // tests - the one above and the one in nft_transfer tests.
7678+ // Note: CreateOrder output is missing here; this is because orders with zero value are checked
7679+ // separately, see order_with_zero_value in orders_tests.
7680+ pub fn make_zero_transfer_outputs_for_token_zero_amount_transfer_test (
7681+ token_id : & TokenId ,
7682+ rng : & mut impl Rng ,
7683+ ) -> Vec < TxOutput > {
7684+ TxOutputTag :: iter ( )
7685+ . filter_map ( |tag| match tag {
7686+ TxOutputTag :: Transfer => Some ( TxOutput :: Transfer (
7687+ OutputValue :: TokenV1 ( * token_id, Amount :: ZERO ) ,
7688+ Destination :: AnyoneCanSpend ,
7689+ ) ) ,
7690+ TxOutputTag :: LockThenTransfer => Some ( TxOutput :: LockThenTransfer (
7691+ OutputValue :: TokenV1 ( * token_id, Amount :: ZERO ) ,
7692+ Destination :: AnyoneCanSpend ,
7693+ OutputTimeLock :: UntilHeight ( BlockHeight :: new ( rng. random ( ) ) ) ,
7694+ ) ) ,
7695+ TxOutputTag :: Burn => Some ( TxOutput :: Burn ( OutputValue :: TokenV1 (
7696+ * token_id,
7697+ Amount :: ZERO ,
7698+ ) ) ) ,
7699+ TxOutputTag :: Htlc => Some ( TxOutput :: Htlc (
7700+ OutputValue :: TokenV1 ( * token_id, Amount :: ZERO ) ,
7701+ Box :: new ( HashedTimelockContract {
7702+ secret_hash : rng. random ( ) ,
7703+ spend_key : Destination :: AnyoneCanSpend ,
7704+ refund_timelock : OutputTimeLock :: UntilHeight ( BlockHeight :: new ( rng. random ( ) ) ) ,
7705+ refund_key : Destination :: AnyoneCanSpend ,
7706+ } ) ,
7707+ ) ) ,
7708+
7709+ TxOutputTag :: CreateStakePool
7710+ | TxOutputTag :: ProduceBlockFromStake
7711+ | TxOutputTag :: CreateDelegationId
7712+ | TxOutputTag :: DelegateStaking
7713+ | TxOutputTag :: IssueFungibleToken
7714+ | TxOutputTag :: IssueNft
7715+ | TxOutputTag :: DataDeposit
7716+ | TxOutputTag :: CreateOrder => None ,
7717+ } )
7718+ . collect ( )
7719+ }
7720+
7721+ // For a frozen token, zero amount transfers are not allowed even before the fork.
75677722#[ rstest]
75687723#[ trace]
75697724#[ case( Seed :: from_entropy( ) ) ]
7570- fn zero_amount_transfer_of_frozen_token ( #[ case] seed : Seed ) {
7725+ fn zero_amount_transfer_of_frozen_token (
7726+ #[ case] seed : Seed ,
7727+ #[ values( ZeroTokenTransferForbidden :: Yes , ZeroTokenTransferForbidden :: No ) ]
7728+ zero_token_transfer_forbidden : ZeroTokenTransferForbidden ,
7729+ ) {
75717730 utils:: concurrency:: model ( move || {
75727731 let mut rng = make_seedable_rng ( seed) ;
7573- let mut tf = TestFramework :: builder ( & mut rng) . build ( ) ;
7732+
7733+ let mut tf = TestFramework :: builder ( & mut rng)
7734+ . with_chain_config (
7735+ chain:: config:: create_unit_test_config_builder ( )
7736+ . chainstate_upgrades (
7737+ NetUpgrades :: initialize ( vec ! [ (
7738+ BlockHeight :: zero( ) ,
7739+ ChainstateUpgradeBuilder :: latest( )
7740+ . zero_token_transfer_forbidden( zero_token_transfer_forbidden)
7741+ . build( ) ,
7742+ ) ] )
7743+ . unwrap ( ) ,
7744+ )
7745+ . build ( ) ,
7746+ )
7747+ . build ( ) ;
75747748
75757749 let ( token_id, _, _, _, utxo_with_change) = issue_token_from_genesis (
75767750 & mut rng,
@@ -7616,11 +7790,17 @@ fn zero_amount_transfer_of_frozen_token(#[case] seed: Seed) {
76167790
76177791 let result = tf. make_block_builder ( ) . add_transaction ( tx) . build_and_process ( & mut rng) ;
76187792
7619- assert_eq ! (
7620- result. unwrap_err( ) ,
7621- ChainstateError :: ProcessBlockError ( BlockError :: StateUpdateFailed (
7622- ConnectTransactionError :: AttemptToSpendFrozenToken ( token_id)
7623- ) )
7624- ) ;
7793+ let expected_err = match zero_token_transfer_forbidden {
7794+ ZeroTokenTransferForbidden :: Yes => ChainstateError :: ProcessBlockError (
7795+ BlockError :: StateUpdateFailed ( ConnectTransactionError :: ZeroTokenTransfer ( token_id) ) ,
7796+ ) ,
7797+ ZeroTokenTransferForbidden :: No => {
7798+ ChainstateError :: ProcessBlockError ( BlockError :: StateUpdateFailed (
7799+ ConnectTransactionError :: AttemptToSpendFrozenToken ( token_id) ,
7800+ ) )
7801+ }
7802+ } ;
7803+
7804+ assert_eq ! ( result. unwrap_err( ) , expected_err) ;
76257805 } ) ;
76267806}
0 commit comments