Skip to content

Commit 18d3c0c

Browse files
committed
Consensus tightening - forbid zero token transfers
1 parent f20f46c commit 18d3c0c

16 files changed

Lines changed: 632 additions & 172 deletions

File tree

chainstate/src/detail/ban_score.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ impl BanScore for ConnectTransactionError {
144144
ConnectTransactionError::ConcludeInputAmountsDontMatch(_, _) => 100,
145145
ConnectTransactionError::ProduceBlockFromStakeChangesStakerDestination(_, _) => 100,
146146
ConnectTransactionError::IdCreationError(err) => err.ban_score(),
147+
ConnectTransactionError::ZeroTokenTransfer(_) => 100,
147148
}
148149
}
149150
}

chainstate/src/detail/error_classification.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -304,9 +304,8 @@ impl BlockProcessingErrorClassification for ConnectTransactionError {
304304
| ConnectTransactionError::InsufficientCoinsFee(_, _)
305305
| ConnectTransactionError::AttemptToSpendFrozenToken(_)
306306
| ConnectTransactionError::ConcludeInputAmountsDontMatch(_, _)
307-
| ConnectTransactionError::ProduceBlockFromStakeChangesStakerDestination(_, _) => {
308-
BlockProcessingErrorClass::BadBlock
309-
}
307+
| ConnectTransactionError::ProduceBlockFromStakeChangesStakerDestination(_, _)
308+
| ConnectTransactionError::ZeroTokenTransfer(_) => BlockProcessingErrorClass::BadBlock,
310309

311310
ConnectTransactionError::StorageError(err) => err.classify(),
312311
ConnectTransactionError::UtxoError(err) => err.classify(),

chainstate/test-suite/src/tests/fungible_tokens_v1.rs

Lines changed: 211 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,10 @@ use chainstate_test_framework::{
3030
};
3131
use 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
};
5253
use crypto::key::{KeyKind, PrivateKey};
53-
use randomness::{CryptoRng, RngExt as _};
54+
use randomness::{CryptoRng, Rng, RngExt as _};
55+
use strum::IntoEnumIterator as _;
5456
use 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]
75387539
fn 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
}

chainstate/test-suite/src/tests/nft_burn.rs

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ use chainstate::{BlockError, ChainstateError, ConnectTransactionError};
1919
use chainstate_test_framework::{TestFramework, TransactionBuilder};
2020
use common::{
2121
chain::{
22-
ChainstateUpgradeBuilder, Destination, OutPointSourceId, TokenIssuanceVersion, TxInput,
23-
TxOutput, UtxoOutPoint, output_value::OutputValue, signature::inputsig::InputWitness,
24-
tokens::TokenId,
22+
self, ChainstateUpgradeBuilder, Destination, NetUpgrades, OutPointSourceId,
23+
TokenIssuanceVersion, TxInput, TxOutput, UtxoOutPoint, ZeroTokenTransferForbidden,
24+
output_value::OutputValue, signature::inputsig::InputWitness, tokens::TokenId,
2525
},
2626
primitives::{Amount, BlockHeight, CoinOrTokenId, Idable},
2727
};
@@ -34,10 +34,30 @@ use test_utils::{
3434
#[rstest]
3535
#[trace]
3636
#[case(Seed::from_entropy())]
37-
fn nft_burn_invalid_amount(#[case] seed: Seed) {
37+
fn nft_burn_invalid_amount(
38+
#[case] seed: Seed,
39+
#[values(ZeroTokenTransferForbidden::Yes, ZeroTokenTransferForbidden::No)]
40+
zero_token_transfer_forbidden: ZeroTokenTransferForbidden,
41+
) {
3842
utils::concurrency::model(move || {
3943
let mut rng = make_seedable_rng(seed);
40-
let mut tf = TestFramework::builder(&mut rng).build();
44+
45+
let mut tf = TestFramework::builder(&mut rng)
46+
.with_chain_config(
47+
chain::config::create_unit_test_config_builder()
48+
.chainstate_upgrades(
49+
NetUpgrades::initialize(vec![(
50+
BlockHeight::zero(),
51+
ChainstateUpgradeBuilder::latest()
52+
.zero_token_transfer_forbidden(zero_token_transfer_forbidden)
53+
.build(),
54+
)])
55+
.unwrap(),
56+
)
57+
.build(),
58+
)
59+
.build();
60+
4161
let genesis_outpoint_id = OutPointSourceId::BlockReward(tf.genesis().get_id().into());
4262
let first_tx_input = TxInput::from_utxo(genesis_outpoint_id, 0);
4363
let token_id = TokenId::from_tx_input(&first_tx_input);
@@ -88,8 +108,8 @@ fn nft_burn_invalid_amount(#[case] seed: Seed) {
88108
)
89109
);
90110

91-
// Burn zero NFT
92-
let _ = tf
111+
// Burn zero NFT; this is only allowed if zero_token_transfer_forbidden is No.
112+
let result = tf
93113
.make_block_builder()
94114
.add_transaction(
95115
TransactionBuilder::new()
@@ -100,8 +120,20 @@ fn nft_burn_invalid_amount(#[case] seed: Seed) {
100120
.add_output(TxOutput::Burn(OutputValue::TokenV1(token_id, Amount::ZERO)))
101121
.build(),
102122
)
103-
.build_and_process(&mut rng)
104-
.unwrap();
123+
.build_and_process(&mut rng);
124+
125+
match zero_token_transfer_forbidden {
126+
ZeroTokenTransferForbidden::Yes => {
127+
let expected_err =
128+
ChainstateError::ProcessBlockError(BlockError::StateUpdateFailed(
129+
ConnectTransactionError::ZeroTokenTransfer(token_id),
130+
));
131+
assert_eq!(result.unwrap_err(), expected_err);
132+
}
133+
ZeroTokenTransferForbidden::No => {
134+
result.unwrap();
135+
}
136+
}
105137
})
106138
}
107139

0 commit comments

Comments
 (0)