diff --git a/libraries/chain/CMakeLists.txt b/libraries/chain/CMakeLists.txt index e0410326..c654ae6c 100644 --- a/libraries/chain/CMakeLists.txt +++ b/libraries/chain/CMakeLists.txt @@ -123,6 +123,7 @@ add_library( graphene_chain offer_object.cpp offer_evaluator.cpp nft_evaluator.cpp + nft_object.cpp ${HEADERS} ${PROTOCOL_HEADERS} diff --git a/libraries/chain/custom_account_authority_evaluator.cpp b/libraries/chain/custom_account_authority_evaluator.cpp index cce68775..6b7044a4 100644 --- a/libraries/chain/custom_account_authority_evaluator.cpp +++ b/libraries/chain/custom_account_authority_evaluator.cpp @@ -46,8 +46,12 @@ void_result create_custom_account_authority_evaluator::do_evaluate(const custom_ const custom_permission_object &pobj = op.permission_id(d); FC_ASSERT(pobj.account == op.owner_account, "Only owner account can update account authority object"); FC_ASSERT(op.valid_to > now, "valid_to expiry should be in future"); + FC_ASSERT((op.valid_to - op.valid_from) <= fc::seconds(d.get_global_properties().parameters.rbac_max_account_authority_lifetime()), "Validity of the auth beyond max expiry"); rbac_operation_hardfork_visitor rvtor(now); rvtor(op.operation_type); + const auto& cindex = d.get_index_type().indices().get(); + auto count = cindex.count(boost::make_tuple(op.permission_id)); + FC_ASSERT(count < d.get_global_properties().parameters.rbac_max_authorities_per_permission(), "Max operations that can be linked to a permission reached"); return void_result(); } FC_CAPTURE_AND_RETHROW((op)) @@ -93,6 +97,7 @@ void_result update_custom_account_authority_evaluator::do_evaluate(const custom_ valid_to = *op.new_valid_to; } FC_ASSERT(valid_from < valid_to, "valid_from should be before valid_to"); + FC_ASSERT((valid_to - valid_from) <= fc::seconds(d.get_global_properties().parameters.rbac_max_account_authority_lifetime()), "Validity of the auth beyond max expiry"); return void_result(); } FC_CAPTURE_AND_RETHROW((op)) diff --git a/libraries/chain/custom_permission_evaluator.cpp b/libraries/chain/custom_permission_evaluator.cpp index dff6d110..3a8b0722 100644 --- a/libraries/chain/custom_permission_evaluator.cpp +++ b/libraries/chain/custom_permission_evaluator.cpp @@ -26,6 +26,8 @@ void_result create_custom_permission_evaluator::do_evaluate(const custom_permiss const auto &pindex = d.get_index_type().indices().get(); auto pitr = pindex.find(boost::make_tuple(op.owner_account, op.permission_name)); FC_ASSERT(pitr == pindex.end(), "Permission name already exists for the given account"); + auto count = pindex.count(boost::make_tuple(op.owner_account)); + FC_ASSERT(count < d.get_global_properties().parameters.rbac_max_permissions_per_account(), "Max permissions per account reached"); return void_result(); } FC_CAPTURE_AND_RETHROW((op)) diff --git a/libraries/chain/include/graphene/chain/config.hpp b/libraries/chain/include/graphene/chain/config.hpp index fe552610..42c2fd13 100644 --- a/libraries/chain/include/graphene/chain/config.hpp +++ b/libraries/chain/include/graphene/chain/config.hpp @@ -236,4 +236,11 @@ #define GPOS_VESTING_LOCKIN_PERIOD (60*60*24*30) // 1 month #define RBAC_MIN_PERMISSION_NAME_LENGTH 3 -#define RBAC_MAX_PERMISSION_NAME_LENGTH 10 \ No newline at end of file +#define RBAC_MAX_PERMISSION_NAME_LENGTH 10 +#define RBAC_MAX_PERMISSIONS_PER_ACCOUNT 5 // 5 per account +#define RBAC_MAX_ACCOUNT_AUTHORITY_LIFETIME 180*24*60*60 // 6 months +#define RBAC_MAX_AUTHS_PER_PERMISSION 15 // 15 ops linked per permission + +#define NFT_TOKEN_MIN_LENGTH 3 +#define NFT_TOKEN_MAX_LENGTH 15 +#define NFT_URI_MAX_LENGTH GRAPHENE_MAX_URL_LENGTH \ No newline at end of file diff --git a/libraries/chain/include/graphene/chain/protocol/chain_parameters.hpp b/libraries/chain/include/graphene/chain/protocol/chain_parameters.hpp index 5ab8ae7c..ddac3115 100644 --- a/libraries/chain/include/graphene/chain/protocol/chain_parameters.hpp +++ b/libraries/chain/include/graphene/chain/protocol/chain_parameters.hpp @@ -48,6 +48,10 @@ namespace graphene { namespace chain { optional < uint32_t > gpos_subperiod = GPOS_SUBPERIOD; optional < uint32_t > gpos_period_start = HARDFORK_GPOS_TIME.sec_since_epoch(); optional < uint32_t > gpos_vesting_lockin_period = GPOS_VESTING_LOCKIN_PERIOD; + /* rbac parameters */ + optional < uint16_t > rbac_max_permissions_per_account = RBAC_MAX_PERMISSIONS_PER_ACCOUNT; + optional < uint32_t > rbac_max_account_authority_lifetime = RBAC_MAX_ACCOUNT_AUTHORITY_LIFETIME; + optional < uint16_t > rbac_max_authorities_per_permission = RBAC_MAX_AUTHS_PER_PERMISSION; }; struct chain_parameters @@ -138,7 +142,16 @@ namespace graphene { namespace chain { } inline uint32_t gpos_vesting_lockin_period()const { return extensions.value.gpos_vesting_lockin_period.valid() ? *extensions.value.gpos_vesting_lockin_period : GPOS_VESTING_LOCKIN_PERIOD; /// GPOS vesting lockin period - } + } + inline uint16_t rbac_max_permissions_per_account()const { + return extensions.value.rbac_max_permissions_per_account.valid() ? *extensions.value.rbac_max_permissions_per_account : RBAC_MAX_PERMISSIONS_PER_ACCOUNT; + } + inline uint32_t rbac_max_account_authority_lifetime()const { + return extensions.value.rbac_max_account_authority_lifetime.valid() ? *extensions.value.rbac_max_account_authority_lifetime : RBAC_MAX_ACCOUNT_AUTHORITY_LIFETIME; + } + inline uint16_t rbac_max_authorities_per_permission()const { + return extensions.value.rbac_max_authorities_per_permission.valid() ? *extensions.value.rbac_max_authorities_per_permission : RBAC_MAX_AUTHS_PER_PERMISSION; + } }; } } // graphene::chain @@ -156,6 +169,9 @@ FC_REFLECT( graphene::chain::parameter_extension, (gpos_subperiod) (gpos_period_start) (gpos_vesting_lockin_period) + (rbac_max_permissions_per_account) + (rbac_max_account_authority_lifetime) + (rbac_max_authorities_per_permission) ) FC_REFLECT( graphene::chain::chain_parameters, diff --git a/libraries/chain/include/graphene/chain/protocol/nft_ops.hpp b/libraries/chain/include/graphene/chain/protocol/nft_ops.hpp index 5f495553..388ce14b 100644 --- a/libraries/chain/include/graphene/chain/protocol/nft_ops.hpp +++ b/libraries/chain/include/graphene/chain/protocol/nft_ops.hpp @@ -17,6 +17,7 @@ namespace graphene { namespace chain { optional revenue_split; account_id_type fee_payer()const { return owner; } + void validate() const; }; struct nft_metadata_update_operation : public base_operation @@ -33,6 +34,7 @@ namespace graphene { namespace chain { optional revenue_split; account_id_type fee_payer()const { return owner; } + void validate() const; }; struct nft_mint_operation : public base_operation @@ -49,6 +51,7 @@ namespace graphene { namespace chain { std::string token_uri; account_id_type fee_payer()const { return payer; } + void validate() const; }; struct nft_safe_transfer_from_operation : public base_operation diff --git a/libraries/chain/nft_evaluator.cpp b/libraries/chain/nft_evaluator.cpp index b3c58cae..be77474b 100644 --- a/libraries/chain/nft_evaluator.cpp +++ b/libraries/chain/nft_evaluator.cpp @@ -5,6 +5,7 @@ namespace graphene { namespace chain { void_result nft_metadata_create_evaluator::do_evaluate( const nft_metadata_create_operation& op ) { try { + op.owner(db()); const auto& idx_nft_md_by_name = db().get_index_type().indices().get(); FC_ASSERT( idx_nft_md_by_name.find(op.name) == idx_nft_md_by_name.end(), "NFT name already in use" ); const auto& idx_nft_md_by_symbol = db().get_index_type().indices().get(); @@ -33,6 +34,7 @@ object_id_type nft_metadata_create_evaluator::do_apply( const nft_metadata_creat void_result nft_metadata_update_evaluator::do_evaluate( const nft_metadata_update_operation& op ) { try { + op.owner(db()); const auto& idx_nft_md = db().get_index_type().indices().get(); auto itr_nft_md = idx_nft_md.find(op.nft_metadata_id); FC_ASSERT( itr_nft_md != idx_nft_md.end(), "NFT metadata not found" ); @@ -65,6 +67,12 @@ void_result nft_metadata_update_evaluator::do_apply( const nft_metadata_update_o void_result nft_mint_evaluator::do_evaluate( const nft_mint_operation& op ) { try { + op.payer(db()); + op.owner(db()); + op.approved(db()); + for(const auto& op_iter: op.approved_operators) { + op_iter(db()); + } const auto& idx_nft_md = db().get_index_type().indices().get(); auto itr_nft_md = idx_nft_md.find(op.nft_metadata_id); FC_ASSERT( itr_nft_md != idx_nft_md.end(), "NFT metadata not found" ); @@ -171,6 +179,7 @@ object_id_type nft_approve_evaluator::do_apply( const nft_approve_operation& op void_result nft_set_approval_for_all_evaluator::do_evaluate( const nft_set_approval_for_all_operation& op ) { try { + op.owner(db()); const auto& idx_acc = db().get_index_type().indices().get(); auto itr_operator = idx_acc.find(op.operator_); diff --git a/libraries/chain/nft_object.cpp b/libraries/chain/nft_object.cpp new file mode 100644 index 00000000..c72c727b --- /dev/null +++ b/libraries/chain/nft_object.cpp @@ -0,0 +1,69 @@ +#include +#include + +namespace graphene +{ +namespace chain +{ + +bool is_valid_nft_token_name(const string &name) +{ + try + { + const size_t len = name.size(); + // NFT_TOKEN_MIN_LENGTH <= len minimum length check + if (len < NFT_TOKEN_MIN_LENGTH) + { + return false; + } + // len <= NFT_TOKEN_MAX_LENGTH max length check + if (len > NFT_TOKEN_MAX_LENGTH) + { + return false; + } + // First character should be a letter between a-z/A-Z + if (!((name[0] >= 'a' && name[0] <= 'z') || (name[0] >= 'A' && name[0] <= 'Z'))) + { + return false; + } + // Any character should either be a small case letter a-z or a digit 0-9 + for (const auto &ch : name) + { + if (!((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || (ch == ' '))) + { + return false; + } + } + + return true; + } + FC_CAPTURE_AND_RETHROW((name)) +} + +void nft_metadata_create_operation::validate() const +{ + FC_ASSERT(fee.amount >= 0, "Fee must not be negative"); + FC_ASSERT(is_valid_nft_token_name(name), "Invalid NFT name provided"); + FC_ASSERT(is_valid_nft_token_name(symbol), "Invalid NFT symbol provided"); + FC_ASSERT(base_uri.length() <= NFT_URI_MAX_LENGTH, "Invalid NFT Base URI"); +} + +void nft_metadata_update_operation::validate() const +{ + FC_ASSERT(fee.amount >= 0, "Fee must not be negative"); + if(name) + FC_ASSERT(is_valid_nft_token_name(*name), "Invalid NFT name provided"); + if(symbol) + FC_ASSERT(is_valid_nft_token_name(*symbol), "Invalid NFT symbol provided"); + if(base_uri) + FC_ASSERT((*base_uri).length() <= NFT_URI_MAX_LENGTH, "Invalid NFT Base URI"); +} + +void nft_mint_operation::validate() const +{ + FC_ASSERT(fee.amount >= 0, "Fee must not be negative"); + FC_ASSERT(token_uri.length() <= NFT_URI_MAX_LENGTH, "Invalid NFT Token URI"); +} + +} // namespace chain +} // namespace graphene diff --git a/tests/tests/nft_tests.cpp b/tests/tests/nft_tests.cpp index 3c058093..89ef0cc3 100644 --- a/tests/tests/nft_tests.cpp +++ b/tests/tests/nft_tests.cpp @@ -27,10 +27,35 @@ BOOST_AUTO_TEST_CASE( nft_metadata_create_test ) { nft_metadata_create_operation op; op.owner = mdowner_id; - op.name = "NFT Test"; op.symbol = "NFT"; op.base_uri = "http://nft.example.com"; - + op.name = "123"; + BOOST_CHECK_THROW(op.validate(), fc::exception); + op.name = ""; + BOOST_CHECK_THROW(op.validate(), fc::exception); + op.name = "1ab"; + BOOST_CHECK_THROW(op.validate(), fc::exception); + op.name = ".abc"; + BOOST_CHECK_THROW(op.validate(), fc::exception); + op.name = "abc."; + BOOST_CHECK_THROW(op.validate(), fc::exception); + op.name = "ABC"; + BOOST_CHECK_NO_THROW(op.validate()); + op.name = "abcdefghijklmnopq"; + BOOST_CHECK_THROW(op.validate(), fc::exception); + op.name = "ab"; + BOOST_CHECK_THROW(op.validate(), fc::exception); + op.name = "***"; + BOOST_CHECK_THROW(op.validate(), fc::exception); + op.name = "a12"; + BOOST_CHECK_NO_THROW(op.validate()); + op.name = "a1b"; + BOOST_CHECK_NO_THROW(op.validate()); + op.name = "abc"; + BOOST_CHECK_NO_THROW(op.validate()); + op.name = "abc123defg12345"; + BOOST_CHECK_NO_THROW(op.validate()); + op.name = "NFT Test"; trx.operations.push_back(op); sign(trx, mdowner_private_key); PUSH_TX(db, trx, ~0);