Skip to content

feat(drive-abci): improve token name localization validation #2593

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
May 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
pub mod v0;

use crate::data_contract::associated_token::token_configuration_convention::accessors::v0::TokenConfigurationConventionV0Getters;
use crate::data_contract::associated_token::token_configuration_convention::accessors::v0::{
TokenConfigurationConventionV0Getters, TokenConfigurationConventionV0Setters,
};
use crate::data_contract::associated_token::token_configuration_convention::TokenConfigurationConvention;
use crate::data_contract::associated_token::token_configuration_localization::TokenConfigurationLocalization;
use std::collections::BTreeMap;
Expand Down Expand Up @@ -34,9 +36,26 @@ impl TokenConfigurationConventionV0Getters for TokenConfigurationConvention {
}
}

fn decimals(&self) -> u16 {
fn decimals(&self) -> u8 {
match self {
TokenConfigurationConvention::V0(v0) => v0.decimals(),
}
}
}

impl TokenConfigurationConventionV0Setters for TokenConfigurationConvention {
fn set_localizations(
&mut self,
localizations: BTreeMap<String, TokenConfigurationLocalization>,
) {
match self {
TokenConfigurationConvention::V0(v0) => v0.set_localizations(localizations),
}
}

fn set_decimals(&mut self, decimals: u8) {
match self {
TokenConfigurationConvention::V0(v0) => v0.set_decimals(decimals),
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ pub trait TokenConfigurationConventionV0Getters {
fn localizations_mut(&mut self) -> &mut BTreeMap<String, TokenConfigurationLocalization>;

/// Returns the decimals value.
fn decimals(&self) -> u16;
fn decimals(&self) -> u8;
}

/// Accessor trait for setters of `TokenConfigurationConventionV0`
Expand All @@ -26,5 +26,5 @@ pub trait TokenConfigurationConventionV0Setters {
);

/// Sets the decimals value.
fn set_decimals(&mut self, decimals: u16);
fn set_decimals(&mut self, decimals: u8);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
use crate::consensus::basic::data_contract::{
DecimalsOverLimitError, InvalidTokenLanguageCodeError, InvalidTokenNameCharacterError,
InvalidTokenNameLengthError,
};
use crate::consensus::basic::token::MissingDefaultLocalizationError;
use crate::data_contract::associated_token::token_configuration_convention::accessors::v0::TokenConfigurationConventionV0Getters;
use crate::data_contract::associated_token::token_configuration_convention::TokenConfigurationConvention;
use crate::data_contract::associated_token::token_configuration_localization::accessors::v0::TokenConfigurationLocalizationV0Getters;
use crate::validation::SimpleConsensusValidationResult;
use once_cell::sync::Lazy;
use regex::Regex;

static LANG_CODE_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^[a-zA-Z]{2,3}(-[a-zA-Z]{4})?(-[a-zA-Z]{2}|\d{3})?(-[a-zA-Z0-9]{4,8})*(-[a-wy-zA-WY-Z0-9](-[a-zA-Z0-9]{2,8})+)*(-x(-[a-zA-Z0-9]{1,8})+)?$").unwrap()
});

impl TokenConfigurationConvention {
#[inline(always)]
Expand All @@ -16,6 +28,85 @@ impl TokenConfigurationConvention {
);
}

// Max decimals is defined as 16
if self.decimals() > 16 {
return SimpleConsensusValidationResult::new_with_error(
DecimalsOverLimitError::new(self.decimals(), 16).into(),
);
}

for (language, localization) in self.localizations() {
let singular_form = localization.singular_form();
let plural_form = localization.plural_form();

if !singular_form
.chars()
.all(|c| !c.is_control() && !c.is_whitespace())
{
// This would mean we have an invalid character
return SimpleConsensusValidationResult::new_with_error(
InvalidTokenNameCharacterError::new(
"singular form".to_string(),
singular_form.to_string(),
)
.into(),
);
}

if !plural_form
.chars()
.all(|c| !c.is_control() && !c.is_whitespace())
{
// This would mean we have an invalid character
return SimpleConsensusValidationResult::new_with_error(
InvalidTokenNameCharacterError::new(
"plural form".to_string(),
plural_form.to_string(),
)
.into(),
);
}

if !language
.chars()
.all(|c| !c.is_control() && !c.is_whitespace())
{
// This would mean we have an invalid character
return SimpleConsensusValidationResult::new_with_error(
InvalidTokenNameCharacterError::new(
"language code".to_string(),
language.clone(),
)
.into(),
);
}

if singular_form.len() < 3 || singular_form.len() > 25 {
return SimpleConsensusValidationResult::new_with_error(
InvalidTokenNameLengthError::new(singular_form.len(), 3, 25, "singular form")
.into(),
);
}
if plural_form.len() < 3 || plural_form.len() > 25 {
return SimpleConsensusValidationResult::new_with_error(
InvalidTokenNameLengthError::new(plural_form.len(), 3, 25, "plural form")
.into(),
);
}

if language.len() < 2 || language.len() > 12 {
return SimpleConsensusValidationResult::new_with_error(
InvalidTokenNameLengthError::new(language.len(), 2, 12, "language code").into(),
);
}

if !LANG_CODE_REGEX.is_match(language) {
return SimpleConsensusValidationResult::new_with_error(
InvalidTokenLanguageCodeError::new(language.clone()).into(),
);
}
}

// If we reach here with no errors, return an empty result
SimpleConsensusValidationResult::new()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use crate::data_contract::associated_token::token_configuration_convention::accessors::v0::TokenConfigurationConventionV0Getters;
use crate::data_contract::associated_token::token_configuration_convention::accessors::v0::{
TokenConfigurationConventionV0Getters, TokenConfigurationConventionV0Setters,
};
use crate::data_contract::associated_token::token_configuration_localization::accessors::v0::TokenConfigurationLocalizationV0Getters;
use crate::data_contract::associated_token::token_configuration_localization::TokenConfigurationLocalization;
use bincode::Encode;
Expand Down Expand Up @@ -34,11 +36,11 @@ pub struct TokenConfigurationConventionV0 {
///
/// This value is used by clients to determine formatting and user interface display.
#[serde(default = "default_decimals")]
pub decimals: u16,
pub decimals: u8,
}

// Default function for `decimals`
fn default_decimals() -> u16 {
fn default_decimals() -> u8 {
8 // Default value for decimals
}

Expand Down Expand Up @@ -82,7 +84,20 @@ impl TokenConfigurationConventionV0Getters for TokenConfigurationConventionV0 {
&mut self.localizations
}

fn decimals(&self) -> u16 {
fn decimals(&self) -> u8 {
self.decimals
}
}

impl TokenConfigurationConventionV0Setters for TokenConfigurationConventionV0 {
fn set_localizations(
&mut self,
localizations: BTreeMap<String, TokenConfigurationLocalization>,
) {
self.localizations = localizations;
}

fn set_decimals(&mut self, decimals: u8) {
self.decimals = decimals;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ use crate::data_contract::accessors::v0::DataContractV0Getters;

use crate::consensus::basic::data_contract::{
DuplicateKeywordsError, IncompatibleDataContractSchemaError, InvalidDataContractVersionError,
InvalidDescriptionLengthError, InvalidKeywordLengthError, TooManyKeywordsError,
InvalidDescriptionLengthError, InvalidKeywordCharacterError, InvalidKeywordLengthError,
TooManyKeywordsError,
};
use crate::consensus::state::data_contract::data_contract_update_action_not_allowed_error::DataContractUpdateActionNotAllowedError;
use crate::consensus::state::data_contract::data_contract_update_permission_error::DataContractUpdatePermissionError;
Expand Down Expand Up @@ -267,10 +268,11 @@ impl DataContract {
}

if self.keywords() != new_data_contract.keywords() {
// Validate there are no more than 20 keywords
if new_data_contract.keywords().len() > 20 {
// Validate there are no more than 50 keywords
if new_data_contract.keywords().len() > 50 {
return Ok(SimpleConsensusValidationResult::new_with_error(
TooManyKeywordsError::new(self.id(), self.keywords().len() as u8).into(),
TooManyKeywordsError::new(self.id(), new_data_contract.keywords().len() as u8)
.into(),
));
}

Expand All @@ -284,6 +286,20 @@ impl DataContract {
));
}

if !keyword
.chars()
.all(|c| !c.is_control() && !c.is_whitespace())
{
// This would mean we have an invalid character
return Ok(SimpleConsensusValidationResult::new_with_error(
InvalidKeywordCharacterError::new(
new_data_contract.id(),
keyword.to_string(),
)
.into(),
));
}

// Then check uniqueness
if !seen_keywords.insert(keyword) {
return Ok(SimpleConsensusValidationResult::new_with_error(
Expand All @@ -296,7 +312,8 @@ impl DataContract {
if self.description() != new_data_contract.description() {
// Validate the description is between 3 and 100 characters
if let Some(description) = new_data_contract.description() {
if !(description.len() >= 3 && description.len() <= 100) {
let char_count = description.chars().count();
if !(3..=100).contains(&char_count) {
return Ok(SimpleConsensusValidationResult::new_with_error(
InvalidDescriptionLengthError::new(self.id(), description.to_string())
.into(),
Expand Down
5 changes: 4 additions & 1 deletion packages/rs-dpp/src/data_contract/v1/serialization/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,10 @@ impl DataContractV1 {
updated_at_epoch,
groups,
tokens,
keywords,
keywords: keywords
.into_iter()
.map(|keyword| keyword.to_lowercase())
.collect(),
description,
};

Expand Down
35 changes: 26 additions & 9 deletions packages/rs-dpp/src/errors/consensus/basic/basic_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,20 @@ use crate::consensus::basic::data_contract::{
ContestedUniqueIndexOnMutableDocumentTypeError, ContestedUniqueIndexWithUniqueIndexError,
DataContractHaveNewUniqueIndexError, DataContractImmutablePropertiesUpdateError,
DataContractInvalidIndexDefinitionUpdateError, DataContractTokenConfigurationUpdateError,
DataContractUniqueIndicesChangedError, DuplicateIndexError, DuplicateIndexNameError,
GroupExceedsMaxMembersError, GroupMemberHasPowerOfZeroError, GroupMemberHasPowerOverLimitError,
GroupNonUnilateralMemberPowerHasLessThanRequiredPowerError, GroupPositionDoesNotExistError,
GroupTotalPowerLessThanRequiredError, IncompatibleDataContractSchemaError,
IncompatibleDocumentTypeSchemaError, IncompatibleRe2PatternError, InvalidCompoundIndexError,
InvalidDataContractIdError, InvalidDataContractVersionError, InvalidDocumentTypeNameError,
DataContractUniqueIndicesChangedError, DecimalsOverLimitError, DuplicateIndexError,
DuplicateIndexNameError, GroupExceedsMaxMembersError, GroupMemberHasPowerOfZeroError,
GroupMemberHasPowerOverLimitError, GroupNonUnilateralMemberPowerHasLessThanRequiredPowerError,
GroupPositionDoesNotExistError, GroupTotalPowerLessThanRequiredError,
IncompatibleDataContractSchemaError, IncompatibleDocumentTypeSchemaError,
IncompatibleRe2PatternError, InvalidCompoundIndexError, InvalidDataContractIdError,
InvalidDataContractVersionError, InvalidDocumentTypeNameError,
InvalidDocumentTypeRequiredSecurityLevelError, InvalidIndexPropertyTypeError,
InvalidIndexedPropertyConstraintError, InvalidTokenBaseSupplyError,
InvalidTokenDistributionFunctionDivideByZeroError,
InvalidIndexedPropertyConstraintError, InvalidKeywordCharacterError,
InvalidTokenBaseSupplyError, InvalidTokenDistributionFunctionDivideByZeroError,
InvalidTokenDistributionFunctionIncoherenceError,
InvalidTokenDistributionFunctionInvalidParameterError,
InvalidTokenDistributionFunctionInvalidParameterTupleError,
InvalidTokenDistributionFunctionInvalidParameterTupleError, InvalidTokenLanguageCodeError,
InvalidTokenNameCharacterError, InvalidTokenNameLengthError,
NewTokensDestinationIdentityOptionRequiredError, NonContiguousContractGroupPositionsError,
NonContiguousContractTokenPositionsError, SystemPropertyIndexAlreadyPresentError,
UndefinedIndexPropertyError, UniqueIndicesLimitReachedError,
Expand Down Expand Up @@ -537,6 +539,21 @@ pub enum BasicError {
NewTokensDestinationIdentityOptionRequiredError(
NewTokensDestinationIdentityOptionRequiredError,
),

#[error(transparent)]
InvalidKeywordCharacterError(InvalidKeywordCharacterError),

#[error(transparent)]
InvalidTokenNameCharacterError(InvalidTokenNameCharacterError),

#[error(transparent)]
DecimalsOverLimitError(DecimalsOverLimitError),

#[error(transparent)]
InvalidTokenNameLengthError(InvalidTokenNameLengthError),

#[error(transparent)]
InvalidTokenLanguageCodeError(InvalidTokenLanguageCodeError),
}

impl From<BasicError> for ConsensusError {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use crate::consensus::basic::BasicError;
use crate::consensus::ConsensusError;
use crate::errors::ProtocolError;
use bincode::{Decode, Encode};
use platform_serialization_derive::{PlatformDeserialize, PlatformSerialize};
use platform_value::Identifier;
use thiserror::Error;

#[derive(
Error, Debug, Clone, PartialEq, Eq, Encode, Decode, PlatformSerialize, PlatformDeserialize,
)]
#[error("Data contract {data_contract_id} has a keyword with invalid characters. Keywords must not contain whitespace or control characters.")]
#[platform_serialize(unversioned)]
pub struct InvalidKeywordCharacterError {
/*

DO NOT CHANGE ORDER OF FIELDS WITHOUT INTRODUCING OF NEW VERSION

*/
data_contract_id: Identifier,
keyword: String,
}

impl InvalidKeywordCharacterError {
pub fn new(data_contract_id: Identifier, keyword: String) -> Self {
Self {
data_contract_id,
keyword,
}
}

pub fn data_contract_id(&self) -> &Identifier {
&self.data_contract_id
}

pub fn keyword(&self) -> &str {
&self.keyword
}
}

impl From<InvalidKeywordCharacterError> for ConsensusError {
fn from(err: InvalidKeywordCharacterError) -> Self {
Self::BasicError(BasicError::InvalidKeywordCharacterError(err))
}
}
Loading