Skip to content

Commit d792933

Browse files
committed
Consensus tightening - forbid URIs with invalid characters in ChangeTokenMetadataUri
1 parent 18d3c0c commit d792933

15 files changed

Lines changed: 328 additions & 51 deletions

File tree

chainstate/src/detail/ban_score.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,7 @@ impl BanScore for TokensError {
359359
TokensError::InvariantBrokenUndoIssuanceOnNonexistentToken(_) => 100,
360360
TokensError::InvariantBrokenRegisterIssuanceWithDuplicateId(_) => 100,
361361
TokensError::TokenMetadataUriTooLarge(_) => 100,
362+
TokensError::IncorrectMetadataUri(_) => 100,
362363
}
363364
}
364365
}

chainstate/src/detail/error_classification.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,7 @@ impl BlockProcessingErrorClassification for TokensError {
493493
| TokensError::CoinOrTokenOverflow(_)
494494
| TokensError::InsufficientTokenFees(_)
495495
| TokensError::TokenMetadataUriTooLarge(_)
496+
| TokensError::IncorrectMetadataUri(_)
496497
| TokensError::InvariantBrokenUndoIssuanceOnNonexistentToken(_)
497498
| TokensError::InvariantBrokenRegisterIssuanceWithDuplicateId(_) => {
498499
BlockProcessingErrorClass::BadBlock

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

Lines changed: 161 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ use chainstate_test_framework::{
3131
use common::{
3232
chain::{
3333
self, AccountCommand, AccountNonce, AccountType, Block, ChainstateUpgradeBuilder,
34-
Destination, GenBlock, NetUpgrades, OrderAccountCommand, OrderData, OutPointSourceId,
35-
SignedTransaction, Transaction, TxInput, TxOutput, TxOutputTag, UtxoOutPoint,
36-
ZeroTokenTransferForbidden,
34+
ChangeTokenMetadataUriValidityCheckRequired, Destination, GenBlock, NetUpgrades,
35+
OrderAccountCommand, OrderData, OutPointSourceId, SignedTransaction, Transaction, TxInput,
36+
TxOutput, TxOutputTag, UtxoOutPoint, ZeroTokenTransferForbidden,
3737
htlc::{HashedTimelockContract, HtlcSecret},
3838
make_order_id, make_token_id,
3939
output_value::OutputValue,
@@ -6679,6 +6679,164 @@ fn check_change_metadata_uri(#[case] seed: Seed) {
66796679
});
66806680
}
66816681

6682+
// Historically, we allowed changing token's metadata uri to one with invalid (non-alphanum and
6683+
// non-rfc3986) chars even though issuing a token with such a uri would fail. After the corresponding
6684+
// fork, ChangeTokenMetadataUri commands having a uri with invalid chars are no longer allowed.
6685+
#[rstest]
6686+
#[trace]
6687+
#[case(Seed::from_entropy())]
6688+
fn check_change_metadata_uri_invalid_chars(#[case] seed: Seed) {
6689+
utils::concurrency::model(move || {
6690+
let mut rng = make_seedable_rng(seed);
6691+
6692+
// We'll be creating 1 block after the genesis, so the fork height should be at least 2.
6693+
let fork_height = BlockHeight::new(rng.random_range(2..=5));
6694+
let chain_config = chain::config::create_unit_test_config_builder()
6695+
.chainstate_upgrades(
6696+
NetUpgrades::initialize(vec![
6697+
(
6698+
BlockHeight::zero(),
6699+
ChainstateUpgradeBuilder::latest()
6700+
.change_token_metadata_uri_validity_check_required(
6701+
ChangeTokenMetadataUriValidityCheckRequired::No,
6702+
)
6703+
.build(),
6704+
),
6705+
(
6706+
fork_height,
6707+
ChainstateUpgradeBuilder::latest()
6708+
.change_token_metadata_uri_validity_check_required(
6709+
ChangeTokenMetadataUriValidityCheckRequired::Yes,
6710+
)
6711+
.build(),
6712+
),
6713+
])
6714+
.unwrap(),
6715+
)
6716+
.build();
6717+
6718+
let mut tf = TestFramework::builder(&mut rng).with_chain_config(chain_config).build();
6719+
6720+
let (token_id, issuance_block_id, issuance_tx, issuance, mut utxo_with_change) =
6721+
issue_token_from_genesis(
6722+
&mut rng,
6723+
&mut tf,
6724+
TokenTotalSupply::Lockable,
6725+
IsTokenFreezable::No,
6726+
);
6727+
let issuance_v1 =
6728+
assert_matches_return_val!(issuance, TokenIssuance::V1(issuance), issuance);
6729+
let uri_with_invalid_chars = "https://💖🚁🌭.🦠🚀🚖🚧";
6730+
let fee = tf.chain_config().token_change_metadata_uri_fee();
6731+
6732+
let mut next_nonce = AccountNonce::new(0);
6733+
let mut coins_amount = tf.coin_amount_from_utxo(&utxo_with_change);
6734+
6735+
// Before the fork, changing the uri to one with invalid chars is allowed.
6736+
{
6737+
let initial_block_height = tf.best_block_index().block_height().next_height();
6738+
let block_count = (fork_height - initial_block_height).unwrap().to_int();
6739+
for i in 0..block_count {
6740+
coins_amount = (coins_amount - fee).unwrap();
6741+
6742+
// Make sure the uri is different every time.
6743+
let new_metadata_uri = format!("{uri_with_invalid_chars}{i}").as_bytes().to_vec();
6744+
6745+
let tx = TransactionBuilder::new()
6746+
.add_input(
6747+
TxInput::from_command(
6748+
next_nonce,
6749+
AccountCommand::ChangeTokenMetadataUri(
6750+
token_id,
6751+
new_metadata_uri.clone(),
6752+
),
6753+
),
6754+
InputWitness::NoSignature(None),
6755+
)
6756+
.add_input(utxo_with_change.into(), InputWitness::NoSignature(None))
6757+
.add_output(TxOutput::Transfer(
6758+
OutputValue::Coin(coins_amount),
6759+
Destination::AnyoneCanSpend,
6760+
))
6761+
.build();
6762+
6763+
let tx_id = tx.transaction().get_id();
6764+
utxo_with_change = UtxoOutPoint::new(tx_id.into(), 0);
6765+
next_nonce = next_nonce.increment().unwrap();
6766+
6767+
tf.make_block_builder()
6768+
.add_transaction(tx)
6769+
.build_and_process(&mut rng)
6770+
.unwrap()
6771+
.unwrap();
6772+
6773+
check_fungible_token(
6774+
&tf,
6775+
&mut rng,
6776+
&token_id,
6777+
&ExpectedFungibleTokenData {
6778+
issuance: TokenIssuance::V1(TokenIssuanceV1 {
6779+
token_ticker: issuance_v1.token_ticker.clone(),
6780+
number_of_decimals: issuance_v1.number_of_decimals,
6781+
metadata_uri: new_metadata_uri,
6782+
total_supply: issuance_v1.total_supply,
6783+
authority: issuance_v1.authority.clone(),
6784+
is_freezable: issuance_v1.is_freezable,
6785+
}),
6786+
issuance_tx: issuance_tx.clone(),
6787+
issuance_block_id,
6788+
circulating_supply: None,
6789+
is_locked: false,
6790+
is_frozen: IsTokenFrozen::No(IsTokenFreezable::No),
6791+
},
6792+
true,
6793+
);
6794+
}
6795+
}
6796+
6797+
// Sanity check
6798+
{
6799+
let new_block_height = tf.best_block_index().block_height().next_height();
6800+
assert_eq!(new_block_height, fork_height);
6801+
}
6802+
6803+
// After the fork, changing the uri to one with invalid chars is forbidden.
6804+
let result = tf
6805+
.make_block_builder()
6806+
.add_transaction(
6807+
TransactionBuilder::new()
6808+
.add_input(
6809+
TxInput::from_command(
6810+
next_nonce,
6811+
AccountCommand::ChangeTokenMetadataUri(
6812+
token_id,
6813+
uri_with_invalid_chars.as_bytes().to_vec(),
6814+
),
6815+
),
6816+
InputWitness::NoSignature(None),
6817+
)
6818+
.add_input(
6819+
utxo_with_change.clone().into(),
6820+
InputWitness::NoSignature(None),
6821+
)
6822+
.build(),
6823+
)
6824+
.build_and_process(&mut rng);
6825+
assert_eq!(
6826+
result.unwrap_err(),
6827+
ChainstateError::ProcessBlockError(BlockError::CheckBlockFailed(
6828+
chainstate::CheckBlockError::CheckTransactionFailed(
6829+
chainstate::CheckBlockTransactionsError::CheckTransactionError(
6830+
tx_verifier::CheckTransactionError::TokensError(
6831+
TokensError::IncorrectMetadataUri(token_id)
6832+
)
6833+
)
6834+
)
6835+
))
6836+
);
6837+
});
6838+
}
6839+
66826840
#[rstest]
66836841
#[trace]
66846842
#[case(Seed::from_entropy())]

chainstate/tx-verifier/src/transaction_verifier/check_transaction.rs

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ use std::collections::BTreeSet;
1818
use chainstate_types::PropertyQueryError;
1919
use common::{
2020
chain::{
21-
AccountCommand, ChainConfig, ChangeTokenMetadataUriActivated, HtlcActivated, OrderId,
22-
OrdersVersion, SignedTransaction, TokenIssuanceVersion, Transaction, TransactionSize,
23-
TxInput, TxOutput,
21+
AccountCommand, ChainConfig, ChangeTokenMetadataUriActivated,
22+
ChangeTokenMetadataUriValidityCheckRequired, HtlcActivated, OrderId, OrdersVersion,
23+
SignedTransaction, TokenIssuanceVersion, Transaction, TransactionSize, TxInput, TxOutput,
2424
output_value::OutputValue,
2525
signature::inputsig::InputWitness,
2626
tokens::{NftIssuance, get_tokens_issuance_count},
@@ -32,7 +32,9 @@ use utils::ensure;
3232

3333
use crate::{
3434
error::TokensError,
35-
transaction_verifier::tokens_check::{check_nft_issuance_data, check_tokens_issuance},
35+
transaction_verifier::tokens_check::{
36+
check_nft_issuance_data, check_tokens_issuance, check_utils::is_uri_valid,
37+
},
3638
};
3739

3840
#[derive(Error, Debug, PartialEq, Eq, Clone)]
@@ -152,12 +154,10 @@ fn check_tokens_tx(
152154
block_height: BlockHeight,
153155
tx: &SignedTransaction,
154156
) -> Result<(), CheckTransactionError> {
157+
let cs_upgrade = &chain_config.chainstate_upgrades().version_at_height(block_height).1;
158+
155159
// Check if v0 tokens are allowed to be used at this height
156-
let latest_token_version = chain_config
157-
.chainstate_upgrades()
158-
.version_at_height(block_height)
159-
.1
160-
.token_issuance_version();
160+
let latest_token_version = cs_upgrade.token_issuance_version();
161161

162162
match latest_token_version {
163163
TokenIssuanceVersion::V0 => { /* do nothing */ }
@@ -208,11 +208,9 @@ fn check_tokens_tx(
208208
),)
209209
);
210210

211-
let change_token_metadata_uri_activated = chain_config
212-
.chainstate_upgrades()
213-
.version_at_height(block_height)
214-
.1
215-
.change_token_metadata_uri_activated();
211+
let change_token_metadata_uri_activated = cs_upgrade.change_token_metadata_uri_activated();
212+
let change_token_metadata_uri_validity_check_required =
213+
cs_upgrade.change_token_metadata_uri_validity_check_required();
216214

217215
// Check token metadata uri change
218216
tx.inputs().iter().try_for_each(|input| match input {
@@ -240,6 +238,19 @@ fn check_tokens_tx(
240238
*token_id
241239
))
242240
);
241+
242+
match change_token_metadata_uri_validity_check_required {
243+
ChangeTokenMetadataUriValidityCheckRequired::Yes => {
244+
ensure!(
245+
is_uri_valid(metadata_uri),
246+
CheckTransactionError::TokensError(TokensError::IncorrectMetadataUri(
247+
*token_id
248+
))
249+
);
250+
}
251+
ChangeTokenMetadataUriValidityCheckRequired::No => { /* do nothing */ }
252+
}
253+
243254
Ok(())
244255
}
245256
},

chainstate/tx-verifier/src/transaction_verifier/error.rs

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -153,13 +153,13 @@ pub enum SignatureDestinationGetterError {
153153
SpendingFromAccountInBlockReward,
154154
#[error("Attempted to verify signature for not spendable output")]
155155
SigVerifyOfNotSpendableOutput,
156-
#[error("Pool data not found for signature verification {0}")]
156+
#[error("Pool data not found for signature verification {0:x}")]
157157
PoolDataNotFound(PoolId),
158-
#[error("Delegation data not found for signature verification {0}")]
158+
#[error("Delegation data not found for signature verification {0:x}")]
159159
DelegationDataNotFound(DelegationId),
160-
#[error("Token data not found for signature verification {0}")]
160+
#[error("Token data not found for signature verification {0:x}")]
161161
TokenDataNotFound(TokenId),
162-
#[error("Order data not found for signature verification {0}")]
162+
#[error("Order data not found for signature verification {0:x}")]
163163
OrderDataNotFound(OrderId),
164164
#[error("Utxo for the outpoint not fount: {0:?}")]
165165
UtxoOutputNotFound(UtxoOutPoint),
@@ -201,28 +201,30 @@ pub enum TokenIssuanceError {
201201
MediaHashTooShort,
202202
#[error("The media hash is too long")]
203203
MediaHashTooLong,
204-
#[error("Token id {0} from issuance does not match calculated token id {1}")]
204+
#[error("Token id {0:x} from issuance does not match calculated token id {1:x}")]
205205
TokenIdMismatch(TokenId, TokenId),
206206
}
207207

208208
#[derive(Error, Debug, PartialEq, Eq, Clone)]
209209
pub enum TokensError {
210210
#[error("Blockchain storage error: {0}")]
211211
StorageError(#[from] chainstate_storage::Error),
212-
#[error("Issuance error {0} in transaction {1}")]
212+
#[error("Issuance error {0} in transaction {1:x}")]
213213
IssueError(TokenIssuanceError, Id<Transaction>),
214-
#[error("Too many tokens issuance in transaction {0}")]
214+
#[error("Too many tokens issuance in transaction {0:x}")]
215215
MultipleTokenIssuanceInTransaction(Id<Transaction>),
216216
#[error("Coin or token overflow {0:?}")]
217217
CoinOrTokenOverflow(CoinOrTokenId),
218-
#[error("Insufficient token issuance fee in transaction {0}")]
218+
#[error("Insufficient token issuance fee in transaction {0:x}")]
219219
InsufficientTokenFees(Id<Transaction>),
220-
#[error("Invariant broken - attempt undo issuance on non-existent token {0}")]
220+
#[error("Invariant broken - attempt undo issuance on non-existent token {0:x}")]
221221
InvariantBrokenUndoIssuanceOnNonexistentToken(TokenId),
222-
#[error("Invariant broken - attempt register issuance on non-existent token {0}")]
222+
#[error("Invariant broken - attempt register issuance on non-existent token {0:x}")]
223223
InvariantBrokenRegisterIssuanceWithDuplicateId(TokenId),
224-
#[error("Token {0} metadata uri is to large")]
224+
#[error("Token {0:x} metadata uri is to large")]
225225
TokenMetadataUriTooLarge(TokenId),
226+
#[error("Token {0:x} metadata URI is incorrect")]
227+
IncorrectMetadataUri(TokenId),
226228
}
227229

228230
#[derive(Error, Debug, PartialEq, Eq, Clone)]
@@ -235,7 +237,7 @@ pub enum SpendStakeError {
235237
InvalidBlockRewardOutputType,
236238
#[error("Stake pool data in kernel doesn't match data in block reward output")]
237239
StakePoolDataMismatch,
238-
#[error("Pool id in kernel {0} doesn't match the expected pool id {1}")]
240+
#[error("Pool id in kernel {0:x} doesn't match the expected pool id {1:x}")]
239241
StakePoolIdMismatch(PoolId, PoolId),
240242
#[error("Consensus PoS error: {0}")]
241243
ConsensusPoSError(#[from] consensus::ConsensusPoSError),

chainstate/tx-verifier/src/transaction_verifier/tokens_check/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ use common::chain::{
2424
use serialization::{DecodeAll, Encode};
2525
use utils::ensure;
2626

27-
mod check_utils;
27+
pub mod check_utils;
2828

2929
pub fn check_nft_issuance_data(
3030
chain_config: &ChainConfig,

0 commit comments

Comments
 (0)