diff --git a/libraries/app/database_api.cpp b/libraries/app/database_api.cpp index ce77752697..5b0a43468e 100644 --- a/libraries/app/database_api.cpp +++ b/libraries/app/database_api.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include @@ -510,7 +511,7 @@ std::map database_api_impl::get_full_accounts( const // Add the account's balances const auto& balances = _db.get_index_type< primary_index< account_balance_index > >(). get_secondary_index< balances_by_account_index >().get_account_balances( account->id ); - for( const auto balance : balances ) + for( const auto& balance : balances ) { if(acnt.balances.size() >= api_limit_get_full_accounts_lists) { acnt.more_data_available.balances = true; @@ -738,7 +739,7 @@ vector database_api_impl::get_account_balances( const std::string& accoun const auto& balance_index = _db.get_index_type< primary_index< account_balance_index > >(); const auto& balances = balance_index.get_secondary_index< balances_by_account_index >() .get_account_balances( acnt ); - for( const auto balance : balances ) + for( const auto& balance : balances ) result.push_back( balance.second->get_balance() ); } else @@ -2002,6 +2003,8 @@ bool database_api_impl::verify_authority( const signed_transaction& trx )const trx.verify_authority( _db.get_chain_id(), [this]( account_id_type id ){ return &id(_db).active; }, [this]( account_id_type id ){ return &id(_db).owner; }, + [this]( account_id_type id, const operation& op, rejected_predicate_map* rejects ) { + return _db.get_viable_custom_authorities(id, op, rejects); }, allow_non_immediate_owner, _db.get_global_properties().parameters.max_authority_depth ); return true; @@ -2027,6 +2030,8 @@ bool database_api_impl::verify_account_authority( const string& account_name_or_ graphene::chain::verify_authority(ops, keys, [this]( account_id_type id ){ return &id(_db).active; }, [this]( account_id_type id ){ return &id(_db).owner; }, + // Use a no-op lookup for custom authorities; we don't want it even if one does apply for our dummy op + [](auto, auto, auto*) { return vector(); }, true, MUST_IGNORE_CUSTOM_OP_REQD_AUTHS(_db.head_block_time()) ); } catch (fc::exception& ex) diff --git a/libraries/chain/CMakeLists.txt b/libraries/chain/CMakeLists.txt index fbcd6ab866..9e3b8f1be9 100644 --- a/libraries/chain/CMakeLists.txt +++ b/libraries/chain/CMakeLists.txt @@ -55,6 +55,7 @@ add_library( graphene_chain htlc_evaluator.cpp confidential_evaluator.cpp special_authority_evaluation.cpp + custom_authority_evaluator.cpp buyback.cpp account_object.cpp diff --git a/libraries/chain/committee_member_evaluator.cpp b/libraries/chain/committee_member_evaluator.cpp index 90620fc010..94054edb49 100644 --- a/libraries/chain/committee_member_evaluator.cpp +++ b/libraries/chain/committee_member_evaluator.cpp @@ -72,17 +72,30 @@ void_result committee_member_update_evaluator::do_apply( const committee_member_ return void_result(); } FC_CAPTURE_AND_RETHROW( (op) ) } -void_result committee_member_update_global_parameters_evaluator::do_evaluate(const committee_member_update_global_parameters_operation& o) +void_result committee_member_update_global_parameters_evaluator::do_evaluate( + const committee_member_update_global_parameters_operation& o) { try { FC_ASSERT(trx_state->_is_proposed_trx); - FC_ASSERT( db().head_block_time() > HARDFORK_CORE_1468_TIME || !o.new_parameters.extensions.value.updatable_htlc_options.valid(), + auto now = db().head_block_time(); + FC_ASSERT( now > HARDFORK_CORE_1468_TIME || !o.new_parameters.extensions.value.updatable_htlc_options.valid(), "Unable to set HTLC parameters until hardfork." ); + if (!HARDFORK_BSIP_40_PASSED( now )) { + FC_ASSERT( !o.new_parameters.extensions.value.custom_authority_options.valid(), + "Unable to set Custom Authority Options until hardfork BSIP 40." ); + FC_ASSERT( !o.new_parameters.current_fees->exists(), + "Unable to set Custom Authority operation fees until hardfork BSIP 40." ); + FC_ASSERT( !o.new_parameters.current_fees->exists(), + "Unable to set Custom Authority operation fees until hardfork BSIP 40." ); + FC_ASSERT( !o.new_parameters.current_fees->exists(), + "Unable to set Custom Authority operation fees until hardfork BSIP 40." ); + } return void_result(); } FC_CAPTURE_AND_RETHROW( (o) ) } -void_result committee_member_update_global_parameters_evaluator::do_apply(const committee_member_update_global_parameters_operation& o) +void_result committee_member_update_global_parameters_evaluator::do_apply( + const committee_member_update_global_parameters_operation& o) { try { db().modify(db().get_global_properties(), [&o](global_property_object& p) { p.pending_parameters = o.new_parameters; diff --git a/libraries/chain/custom_authority_evaluator.cpp b/libraries/chain/custom_authority_evaluator.cpp new file mode 100644 index 0000000000..1962932c5c --- /dev/null +++ b/libraries/chain/custom_authority_evaluator.cpp @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2019 Contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include +#include +#include +#include +#include +#include + +namespace graphene { namespace chain { + +void_result custom_authority_create_evaluator::do_evaluate(const custom_authority_create_operation& op) +{ try { + const database& d = db(); + auto now = d.head_block_time(); + FC_ASSERT(HARDFORK_BSIP_40_PASSED(now), "Custom active authorities are not yet enabled"); + + op.account(d); + + const auto& config = d.get_global_properties().parameters.extensions.value.custom_authority_options; + FC_ASSERT(config.valid(), "Cannot use custom authorities yet: global configuration not set"); + FC_ASSERT(op.valid_to > now, "Custom authority expiration must be in the future"); + FC_ASSERT((op.valid_to - now).to_seconds() <= config->max_custom_authority_lifetime_seconds, + "Custom authority lifetime exceeds maximum limit"); + + bool operation_forked_in = hardfork_visitor(now).visit((operation::tag_type)op.operation_type.value); + FC_ASSERT(operation_forked_in, "Cannot create custom authority for operation which is not valid yet"); + + auto restriction_count = restriction::restriction_count(op.restrictions); + FC_ASSERT(restriction_count <= config->max_custom_authority_restrictions, + "Custom authority has more than the maximum number of restrictions"); + + for (const auto& account_weight_pair : op.auth.account_auths) + account_weight_pair.first(d); + + const auto& index = d.get_index_type().indices().get(); + auto range = index.equal_range(op.account); + FC_ASSERT(std::distance(range.first, range.second) < config->max_custom_authorities_per_account, + "Cannot create custom authority: account already has maximum number"); + range = index.equal_range(boost::make_tuple(op.account, op.operation_type)); + FC_ASSERT(std::distance(range.first, range.second) < config->max_custom_authorities_per_account_op, + "Cannot create custom authority: account already has maximum number for this operation type"); + + return void_result(); +} FC_CAPTURE_AND_RETHROW((op)) } + +object_id_type custom_authority_create_evaluator::do_apply(const custom_authority_create_operation& op) +{ try { + database& d = db(); + + return d.create([&op] (custom_authority_object& obj) mutable { + obj.account = op.account; + obj.enabled = op.enabled; + obj.valid_from = op.valid_from; + obj.valid_to = op.valid_to; + obj.operation_type = op.operation_type; + obj.auth = op.auth; + std::for_each(op.restrictions.begin(), op.restrictions.end(), [&obj](const restriction& r) mutable { + obj.restrictions.insert(std::make_pair(obj.restriction_counter++, r)); + }); + }).id; +} FC_CAPTURE_AND_RETHROW((op)) } + +void_result custom_authority_update_evaluator::do_evaluate(const custom_authority_update_operation& op) +{ try { + const database& d = db(); + auto now = d.head_block_time(); + old_object = &op.authority_to_update(d); + FC_ASSERT(old_object->account == op.account, "Cannot update a different account's custom authority"); + + if (op.new_enabled) + FC_ASSERT(*op.new_enabled != old_object->enabled, + "Custom authority update specifies an enabled flag, but flag is not changed"); + + const auto& config = d.get_global_properties().parameters.extensions.value.custom_authority_options; + auto valid_from = old_object->valid_from; + auto valid_to = old_object->valid_to; + if (op.new_valid_from) { + FC_ASSERT(*op.new_valid_from != old_object->valid_from, + "Custom authority update specifies a new valid from date, but date is not changed"); + valid_from = *op.new_valid_from; + } + if (op.new_valid_to) { + FC_ASSERT(*op.new_valid_to != old_object->valid_to, + "Custom authority update specifies a new valid to date, but date is not changed"); + FC_ASSERT(*op.new_valid_to > now, "Custom authority expiration must be in the future"); + FC_ASSERT((*op.new_valid_to - now).to_seconds() <= config->max_custom_authority_lifetime_seconds, + "Custom authority lifetime exceeds maximum limit"); + valid_to = *op.new_valid_to; + } + FC_ASSERT(valid_from < valid_to, "Custom authority validity begin date must be before expiration date"); + + if (op.new_auth) { + FC_ASSERT(*op.new_auth != old_object->auth, + "Custom authority update specifies a new authentication authority, but authority is not changed"); + for (const auto& account_weight_pair : op.new_auth->account_auths) + account_weight_pair.first(d); + } + + std::for_each(op.restrictions_to_remove.begin(), op.restrictions_to_remove.end(), [this](uint16_t id) { + FC_ASSERT(old_object->restrictions.count(id) == 1, "Cannot remove restriction ID ${I}: ID not found", + ("I", id)); + }); + if (!op.restrictions_to_add.empty()) { + // Sanity check + if (!old_object->restrictions.empty()) + FC_ASSERT((--old_object->restrictions.end())->first < old_object->restriction_counter, + "LOGIC ERROR: Restriction counter overlaps restrictions. Please report this error."); + FC_ASSERT(old_object->restriction_counter + op.restrictions_to_add.size() > old_object->restriction_counter, + "Unable to add restrictions: causes wraparound of restriction IDs"); + } + + // Add up the restriction counts for all old restrictions not being removed, and all new ones + size_t restriction_count = 0; + for (const auto& restriction_pair : old_object->restrictions) + if (op.restrictions_to_remove.count(restriction_pair.first) == 0) + restriction_count += restriction_pair.second.restriction_count(); + restriction_count += restriction::restriction_count(op.restrictions_to_add); + // Check restriction count against limit + FC_ASSERT(restriction_count <= config->max_custom_authority_restrictions, + "Cannot update custom authority: updated authority would exceed the maximum number of restrictions"); + + get_restriction_predicate(op.restrictions_to_add, old_object->operation_type); + return void_result(); +} FC_CAPTURE_AND_RETHROW((op)) } + +void_result custom_authority_update_evaluator::do_apply(const custom_authority_update_operation& op) +{ try { + database& d = db(); + + d.modify(*old_object, [&op](custom_authority_object& obj) { + if (op.new_enabled) obj.enabled = *op.new_enabled; + if (op.new_valid_from) obj.valid_from = *op.new_valid_from; + if (op.new_valid_to) obj.valid_to = *op.new_valid_to; + if (op.new_auth) obj.auth = *op.new_auth; + + std::for_each(op.restrictions_to_remove.begin(), op.restrictions_to_remove.end(), [&obj](auto id) mutable { + obj.restrictions.erase(id); + }); + std::for_each(op.restrictions_to_add.begin(), op.restrictions_to_add.end(), [&obj](const auto& r) mutable { + obj.restrictions.insert(std::make_pair(obj.restriction_counter++, r)); + }); + + // Clear the predicate cache + obj.clear_predicate_cache(); + }); + + return void_result(); +} FC_CAPTURE_AND_RETHROW((op)) } + +void_result custom_authority_delete_evaluator::do_evaluate(const custom_authority_delete_operation& op) +{ try { + const database& d = db(); + + old_object = &op.authority_to_delete(d); + FC_ASSERT(old_object->account == op.account, "Cannot delete a different account's custom authority"); + + return void_result(); +} FC_CAPTURE_AND_RETHROW((op)) } + +void_result custom_authority_delete_evaluator::do_apply(const custom_authority_delete_operation& op) +{ try { + database& d = db(); + + d.remove(*old_object); + + return void_result(); +} FC_CAPTURE_AND_RETHROW((op)) } + +} } // graphene::chain diff --git a/libraries/chain/db_block.cpp b/libraries/chain/db_block.cpp index ad61974d51..7361bb2ede 100644 --- a/libraries/chain/db_block.cpp +++ b/libraries/chain/db_block.cpp @@ -651,8 +651,11 @@ processed_transaction database::_apply_transaction(const signed_transaction& trx bool allow_non_immediate_owner = ( head_block_time() >= HARDFORK_CORE_584_TIME ); auto get_active = [this]( account_id_type id ) { return &id(*this).active; }; auto get_owner = [this]( account_id_type id ) { return &id(*this).owner; }; + auto get_custom = [this]( account_id_type id, const operation& op, rejected_predicate_map* rejects ) { + return get_viable_custom_authorities(id, op, rejects); + }; - trx.verify_authority(chain_id, get_active, get_owner, allow_non_immediate_owner, + trx.verify_authority(chain_id, get_active, get_owner, get_custom, allow_non_immediate_owner, MUST_IGNORE_CUSTOM_OP_REQD_AUTHS(head_block_time()), get_global_properties().parameters.max_authority_depth); } diff --git a/libraries/chain/db_getter.cpp b/libraries/chain/db_getter.cpp index 3a4ff98c18..65579eb25f 100644 --- a/libraries/chain/db_getter.cpp +++ b/libraries/chain/db_getter.cpp @@ -27,6 +27,7 @@ #include #include #include +#include namespace graphene { namespace chain { @@ -95,6 +96,33 @@ node_property_object& database::node_properties() return _node_property_object; } +vector database::get_viable_custom_authorities( + account_id_type account, const operation &op, + rejected_predicate_map* rejected_authorities) const +{ + const auto& index = get_index_type().indices().get(); + auto range = index.equal_range(boost::make_tuple(account, unsigned_int(op.which()), true)); + + auto is_valid = [now=head_block_time()](const custom_authority_object& auth) { return auth.is_valid(now); }; + vector> valid_auths; + std::copy_if(range.first, range.second, std::back_inserter(valid_auths), is_valid); + + vector results; + for (const auto& cust_auth : valid_auths) { + try { + auto result = cust_auth.get().get_predicate()(op); + if (result.success) + results.emplace_back(cust_auth.get().auth); + else if (rejected_authorities != nullptr) + rejected_authorities->insert(std::make_pair(cust_auth.get().id, std::move(result))); + } catch (fc::exception& e) { + rejected_authorities->insert(std::make_pair(cust_auth.get().id, std::move(e))); + } + } + + return results; +} + uint32_t database::last_non_undoable_block_num() const { //see https://github.com/bitshares/bitshares-core/issues/377 diff --git a/libraries/chain/db_init.cpp b/libraries/chain/db_init.cpp index 124cede1eb..ae720226b8 100644 --- a/libraries/chain/db_init.cpp +++ b/libraries/chain/db_init.cpp @@ -47,6 +47,7 @@ #include #include #include +#include #include #include @@ -63,6 +64,7 @@ #include #include #include +#include #include @@ -127,6 +129,8 @@ const uint8_t worker_object::type_id; const uint8_t htlc_object::space_id; const uint8_t htlc_object::type_id; +const uint8_t custom_authority_object::space_id; +const uint8_t custom_authority_object::type_id; void database::initialize_evaluators() { @@ -178,6 +182,9 @@ void database::initialize_evaluators() register_evaluator(); register_evaluator(); register_evaluator(); + register_evaluator(); + register_evaluator(); + register_evaluator(); } void database::initialize_indexes() @@ -207,6 +214,7 @@ void database::initialize_indexes() add_index< primary_index >(); add_index< primary_index >(); add_index< primary_index< htlc_index> >(); + add_index< primary_index< custom_authority_index> >(); //Implementation object indexes add_index< primary_index >(); diff --git a/libraries/chain/db_maint.cpp b/libraries/chain/db_maint.cpp index 7618fda50b..5ff519eab1 100644 --- a/libraries/chain/db_maint.cpp +++ b/libraries/chain/db_maint.cpp @@ -44,6 +44,7 @@ #include #include #include +#include namespace graphene { namespace chain { @@ -1143,6 +1144,17 @@ void process_hf_935( database& db ) } } +/** + * @brief Remove any custom active authorities whose expiration dates are in the past + * @param db A mutable database reference + */ +void delete_expired_custom_authorities( database& db ) +{ + const auto& index = db.get_index_type().indices().get(); + while (!index.empty() && index.begin()->valid_to < db.head_block_time()) + db.remove(*index.begin()); +} + void database::perform_chain_maintenance(const signed_block& next_block, const global_property_object& global_props) { const auto& gpo = get_global_properties(); @@ -1323,6 +1335,7 @@ void database::perform_chain_maintenance(const signed_block& next_block, const g } process_bitassets(); + delete_expired_custom_authorities(*this); // process_budget needs to run at the bottom because // it needs to know the next_maintenance_time diff --git a/libraries/chain/db_notify.cpp b/libraries/chain/db_notify.cpp index b3e9933c6d..83bd84577e 100644 --- a/libraries/chain/db_notify.cpp +++ b/libraries/chain/db_notify.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include @@ -289,6 +290,21 @@ struct get_impacted_account_visitor { _impacted.insert( op.fee_payer() ); } + void operator()( const custom_authority_create_operation& op ) + { + _impacted.insert( op.fee_payer() ); // account + add_authority_accounts( _impacted, op.auth ); + } + void operator()( const custom_authority_update_operation& op ) + { + _impacted.insert( op.fee_payer() ); // account + if ( op.new_auth ) + add_authority_accounts(_impacted, *op.new_auth); + } + void operator()( const custom_authority_delete_operation& op ) + { + _impacted.insert( op.fee_payer() ); // account + } }; void graphene::chain::operation_get_impacted_accounts( const operation& op, flat_set& result, bool ignore_custom_operation_required_auths ) { @@ -381,6 +397,11 @@ void get_relevant_accounts( const object* obj, flat_set& accoun accounts.insert( htlc_obj->transfer.from ); accounts.insert( htlc_obj->transfer.to ); break; + } case custom_authority_object_type:{ + const auto* cust_auth_obj = dynamic_cast( obj ); + FC_ASSERT( cust_auth_obj != nullptr ); + accounts.insert( cust_auth_obj->account ); + add_authority_accounts( accounts, cust_auth_obj->auth ); } } } diff --git a/libraries/chain/hardfork.d/BSIP_40.hf b/libraries/chain/hardfork.d/BSIP_40.hf new file mode 100644 index 0000000000..db05db0cc7 --- /dev/null +++ b/libraries/chain/hardfork.d/BSIP_40.hf @@ -0,0 +1,6 @@ +// BSIP 40 (Custom Active Authorities) hardfork check +#ifndef HARDFORK_BSIP_40_TIME +// Jan 1 2030, midnight; this is a dummy date until a hardfork date is scheduled +#define HARDFORK_BSIP_40_TIME (fc::time_point_sec( 1893456000 )) +#define HARDFORK_BSIP_40_PASSED(now) (now >= HARDFORK_BSIP_40_TIME) +#endif diff --git a/libraries/chain/include/graphene/chain/custom_authority_evaluator.hpp b/libraries/chain/include/graphene/chain/custom_authority_evaluator.hpp new file mode 100644 index 0000000000..3d33db1a7a --- /dev/null +++ b/libraries/chain/include/graphene/chain/custom_authority_evaluator.hpp @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2019 Contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#pragma once + +#include + +#include + +namespace graphene { namespace chain { +class custom_authority_object; + +class custom_authority_create_evaluator : public evaluator { +public: + using operation_type = custom_authority_create_operation; + + void_result do_evaluate(const operation_type& op); + object_id_type do_apply(const operation_type& op); +}; + +class custom_authority_update_evaluator : public evaluator { +public: + using operation_type = custom_authority_update_operation; + const custom_authority_object* old_object = nullptr; + + void_result do_evaluate(const operation_type& op); + void_result do_apply(const operation_type& op); +}; + +class custom_authority_delete_evaluator : public evaluator { +public: + using operation_type = custom_authority_delete_operation; + const custom_authority_object* old_object = nullptr; + + void_result do_evaluate(const operation_type& op); + void_result do_apply(const operation_type& op); +}; + +} } // graphene::chain diff --git a/libraries/chain/include/graphene/chain/custom_authority_object.hpp b/libraries/chain/include/graphene/chain/custom_authority_object.hpp new file mode 100644 index 0000000000..c58b16d282 --- /dev/null +++ b/libraries/chain/include/graphene/chain/custom_authority_object.hpp @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2019 Contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#pragma once +#include +#include +#include +#include +#include +#include + +namespace graphene { namespace chain { + + /** + * @brief Tracks account custom authorities + * @ingroup object + * + */ + class custom_authority_object : public abstract_object { + /// Unreflected field to store a cache of the predicate function + /// Note that this cache can be modified when the object is const! + mutable optional predicate_cache; + + public: + static const uint8_t space_id = protocol_ids; + static const uint8_t type_id = custom_authority_object_type; + + account_id_type account; + bool enabled; + time_point_sec valid_from; + time_point_sec valid_to; + unsigned_int operation_type; + authority auth; + flat_map restrictions; + uint16_t restriction_counter = 0; + + /// Check whether the custom authority is valid + bool is_valid(time_point_sec now) const { return enabled && now >= valid_from && now < valid_to; } + + /// Get the restrictions as a vector rather than a map + vector get_restrictions() const { + vector rs; + std::transform(restrictions.begin(), restrictions.end(), + std::back_inserter(rs), [](auto i) { return i.second; }); + return rs; + } + /// Get predicate, from cache if possible, and update cache if not (modifies const object!) + restriction_predicate_function get_predicate() const { + if (!predicate_cache.valid()) + update_predicate_cache(); + + return *predicate_cache; + } + /// Regenerate predicate function and update predicate cache + void update_predicate_cache() const { + predicate_cache = get_restriction_predicate(get_restrictions(), operation_type); + } + /// Clear the cache of the predicate function + void clear_predicate_cache() { predicate_cache.reset(); } + }; + + struct by_account_custom; + struct by_expiration; + + /** + * @ingroup object_index + */ + typedef multi_index_container< + custom_authority_object, + indexed_by< + ordered_unique, member>, + ordered_unique, + composite_key, + member, + member, + member + >>, + ordered_unique, + composite_key, + member + >, + composite_key_compare, std::less> + > + > + > custom_authority_multi_index_type; + + /** + * @ingroup object_index + */ + using custom_authority_index = generic_index; + +} } // graphene::chain + +MAP_OBJECT_ID_TO_TYPE(graphene::chain::custom_authority_object) + +FC_REFLECT_TYPENAME(graphene::chain::custom_authority_object) + +GRAPHENE_DECLARE_EXTERNAL_SERIALIZATION(graphene::chain::custom_authority_object) diff --git a/libraries/chain/include/graphene/chain/database.hpp b/libraries/chain/include/graphene/chain/database.hpp index a769d6e950..fe30c89fc4 100644 --- a/libraries/chain/include/graphene/chain/database.hpp +++ b/libraries/chain/include/graphene/chain/database.hpp @@ -43,6 +43,8 @@ #include +namespace graphene { namespace protocol { struct predicate_result; } } + namespace graphene { namespace chain { using graphene::db::abstract_object; using graphene::db::object; @@ -279,6 +281,17 @@ namespace graphene { namespace chain { node_property_object& node_properties(); + /** + * @brief Get a list of custom authorities which can validate the provided operation for the provided account + * @param account The account whose authority is required + * @param op The operation requring the specified account's authority + * @param rejected_authorities [Optional] A pointer to a map that should be populated with the custom + * authorities which were valid, but rejected because the operation did not comply with the restrictions + * @return A vector of authorities which can be used to authorize op in place of account + */ + vector get_viable_custom_authorities( + account_id_type account, const operation& op, + rejected_predicate_map* rejected_authorities = nullptr )const; uint32_t last_non_undoable_block_num() const; //////////////////// db_init.cpp //////////////////// diff --git a/libraries/chain/include/graphene/chain/hardfork_visitor.hpp b/libraries/chain/include/graphene/chain/hardfork_visitor.hpp new file mode 100644 index 0000000000..f9126c8fc6 --- /dev/null +++ b/libraries/chain/include/graphene/chain/hardfork_visitor.hpp @@ -0,0 +1,83 @@ +#pragma once +/* + * Copyright (c) 2019 Contributors + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include + +#include + +#include + +#include +#include + +namespace graphene { namespace chain { +using namespace protocol; +namespace TL { using namespace fc::typelist; } + +/** + * @brief The hardfork_visitor struct checks whether a given operation type has been hardforked in or not + * + * This visitor can be invoked in several different ways, including operation::visit, typelist::runtime::dispatch, or + * direct invocation by calling the visit() method passing an operation variant, narrow operation type, operation tag, + * or templating on the narrow operation type + */ +struct hardfork_visitor { + using result_type = bool; + using first_unforked_op = custom_authority_create_operation; + using BSIP_40_ops = TL::list; + fc::time_point_sec now; + + hardfork_visitor(fc::time_point_sec now) : now(now) {} + + /// The real visitor implementations. Future operation types get added in here. + /// @{ + template + std::enable_if_t::value < operation::tag::value, bool> + visit() { return true; } + template + std::enable_if_t(), bool> + visit() { return HARDFORK_BSIP_40_PASSED(now); } + /// @} + + /// typelist::runtime::dispatch adaptor + template + std::enable_if_t(), bool> + operator()(W) { return visit(); } + /// static_variant::visit adaptor + template + std::enable_if_t(), bool> + operator()(const Op&) { return visit(); } + /// Tag adaptor + bool visit(operation::tag_type tag) { + return TL::runtime::dispatch(operation::list(), (size_t)tag, *this); + } + /// operation adaptor + bool visit(const operation& op) { + return visit(op.which()); + } +}; + +} } // namespace graphene::chain diff --git a/libraries/chain/proposal_evaluator.cpp b/libraries/chain/proposal_evaluator.cpp index 39f838183a..349cc1b82a 100644 --- a/libraries/chain/proposal_evaluator.cpp +++ b/libraries/chain/proposal_evaluator.cpp @@ -100,6 +100,16 @@ struct proposal_operation_hardfork_visitor FC_ASSERT(!op.new_parameters.current_fees->exists()); FC_ASSERT(!op.new_parameters.current_fees->exists()); } + if (!HARDFORK_BSIP_40_PASSED(block_time)) { + FC_ASSERT(!op.new_parameters.extensions.value.custom_authority_options.valid(), + "Unable to set Custom Authority Options before hardfork BSIP 40"); + FC_ASSERT(!op.new_parameters.current_fees->exists(), + "Unable to define fees for custom authority operations prior to hardfork BSIP 40"); + FC_ASSERT(!op.new_parameters.current_fees->exists(), + "Unable to define fees for custom authority operations prior to hardfork BSIP 40"); + FC_ASSERT(!op.new_parameters.current_fees->exists(), + "Unable to define fees for custom authority operations prior to hardfork BSIP 40"); + } } void operator()(const graphene::chain::htlc_create_operation &op) const { FC_ASSERT( block_time >= HARDFORK_CORE_1468_TIME, "Not allowed until hardfork 1468" ); @@ -110,6 +120,15 @@ struct proposal_operation_hardfork_visitor void operator()(const graphene::chain::htlc_extend_operation &op) const { FC_ASSERT( block_time >= HARDFORK_CORE_1468_TIME, "Not allowed until hardfork 1468" ); } + void operator()(const graphene::chain::custom_authority_create_operation&) const { + FC_ASSERT( HARDFORK_BSIP_40_PASSED(block_time), "Not allowed until hardfork BSIP 40" ); + } + void operator()(const graphene::chain::custom_authority_update_operation&) const { + FC_ASSERT( HARDFORK_BSIP_40_PASSED(block_time), "Not allowed until hardfork BSIP 40" ); + } + void operator()(const graphene::chain::custom_authority_delete_operation&) const { + FC_ASSERT( HARDFORK_BSIP_40_PASSED(block_time), "Not allowed until hardfork BSIP 40" ); + } // loop and self visit in proposals void operator()(const graphene::chain::proposal_create_operation &v) const { bool already_contains_proposal_update = false; diff --git a/libraries/chain/proposal_object.cpp b/libraries/chain/proposal_object.cpp index 1b192d8475..3c5244d62c 100644 --- a/libraries/chain/proposal_object.cpp +++ b/libraries/chain/proposal_object.cpp @@ -27,6 +27,8 @@ #include #include +#include + namespace graphene { namespace chain { bool proposal_object::is_authorized_to_execute( database& db ) const @@ -39,6 +41,8 @@ bool proposal_object::is_authorized_to_execute( database& db ) const available_key_approvals, [&db]( account_id_type id ){ return &id( db ).active; }, [&db]( account_id_type id ){ return &id( db ).owner; }, + [&db]( account_id_type id, const operation& op, rejected_predicate_map* rejects ){ + return db.get_viable_custom_authorities(id, op, rejects); }, allow_non_immediate_owner, MUST_IGNORE_CUSTOM_OP_REQD_AUTHS( db.head_block_time() ), db.get_global_properties().parameters.max_authority_depth, diff --git a/libraries/chain/small_objects.cpp b/libraries/chain/small_objects.cpp index ed68a4eaef..0233edc54a 100644 --- a/libraries/chain/small_objects.cpp +++ b/libraries/chain/small_objects.cpp @@ -41,6 +41,7 @@ #include #include #include +#include #include @@ -189,6 +190,10 @@ FC_REFLECT_DERIVED_NO_TYPENAME( graphene::chain::worker_object, (graphene::db::o (url) ) +FC_REFLECT_DERIVED_NO_TYPENAME( graphene::chain::custom_authority_object, (graphene::db::object), + (account)(enabled)(valid_from)(valid_to)(operation_type) + (auth)(restrictions)(restriction_counter) ) + GRAPHENE_IMPLEMENT_EXTERNAL_SERIALIZATION( graphene::chain::balance_object ) GRAPHENE_IMPLEMENT_EXTERNAL_SERIALIZATION( graphene::chain::block_summary_object ) GRAPHENE_IMPLEMENT_EXTERNAL_SERIALIZATION( graphene::chain::budget_record ) @@ -210,3 +215,4 @@ GRAPHENE_IMPLEMENT_EXTERNAL_SERIALIZATION( graphene::chain::withdraw_permission_ GRAPHENE_IMPLEMENT_EXTERNAL_SERIALIZATION( graphene::chain::witness_object ) GRAPHENE_IMPLEMENT_EXTERNAL_SERIALIZATION( graphene::chain::witness_schedule_object ) GRAPHENE_IMPLEMENT_EXTERNAL_SERIALIZATION( graphene::chain::worker_object ) +GRAPHENE_IMPLEMENT_EXTERNAL_SERIALIZATION( graphene::chain::custom_authority_object ) diff --git a/libraries/fc b/libraries/fc index 9db1417b25..bae416a446 160000 --- a/libraries/fc +++ b/libraries/fc @@ -1 +1 @@ -Subproject commit 9db1417b2529e7456226676f0c0ad1f09e2dd449 +Subproject commit bae416a4462a9d59b0f3078a407218bbe12f0f96 diff --git a/libraries/protocol/CMakeLists.txt b/libraries/protocol/CMakeLists.txt index 11a3b66016..877a1afc9f 100644 --- a/libraries/protocol/CMakeLists.txt +++ b/libraries/protocol/CMakeLists.txt @@ -16,6 +16,8 @@ list(APPEND SOURCES account.cpp asset.cpp authority.cpp special_authority.cpp + restriction.cpp + custom_authority.cpp committee_member.cpp custom.cpp market.cpp @@ -29,8 +31,26 @@ list(APPEND SOURCES account.cpp htlc.cpp) +list(APPEND CUSTOM_AUTHS_FILES + custom_authorities/restriction_predicate.cpp + custom_authorities/list_1.cpp + custom_authorities/list_2.cpp + custom_authorities/list_3.cpp + custom_authorities/list_4.cpp + custom_authorities/list_5.cpp + custom_authorities/list_6.cpp + custom_authorities/list_7.cpp) + +add_library( graphene_protocol_custom_auths ${CUSTOM_AUTHS_FILES} ) +target_link_libraries( graphene_protocol_custom_auths fc ) +target_include_directories( graphene_protocol_custom_auths PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include" ) + +if( MSVC ) + set_source_files_properties( ${CUSTOM_AUTHS_FILES} PROPERTIES COMPILE_FLAGS /bigobj ) +endif() + add_library( graphene_protocol ${SOURCES} ${HEADERS} ) -target_link_libraries( graphene_protocol fc ) +target_link_libraries( graphene_protocol fc graphene_protocol_custom_auths ) target_include_directories( graphene_protocol PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include" ) install( TARGETS diff --git a/libraries/protocol/custom_authorities/BOOST_LICENSE_1_0.txt b/libraries/protocol/custom_authorities/BOOST_LICENSE_1_0.txt new file mode 100644 index 0000000000..36b7cd93cd --- /dev/null +++ b/libraries/protocol/custom_authorities/BOOST_LICENSE_1_0.txt @@ -0,0 +1,23 @@ +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/libraries/protocol/custom_authorities/list_1.cpp b/libraries/protocol/custom_authorities/list_1.cpp new file mode 100644 index 0000000000..ba01e0ac44 --- /dev/null +++ b/libraries/protocol/custom_authorities/list_1.cpp @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2019 Contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "restriction_predicate.hxx" + +namespace graphene { namespace protocol { +using result_type = object_restriction_predicate; + +result_type get_restriction_predicate_list_1(size_t idx, vector rs) { + return typelist::runtime::dispatch(operation_list_1::list(), idx, [&rs] (auto t) -> result_type { + using Op = typename decltype(t)::type; + return [p=restrictions_to_predicate(std::move(rs), true)] (const operation& op) { + FC_ASSERT(op.which() == operation::tag::value, + "Supplied operation is incorrect type for restriction predicate"); + return p(op.get()); + }; + }); +} +} } diff --git a/libraries/protocol/custom_authorities/list_2.cpp b/libraries/protocol/custom_authorities/list_2.cpp new file mode 100644 index 0000000000..a1027358ba --- /dev/null +++ b/libraries/protocol/custom_authorities/list_2.cpp @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2019 Contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "restriction_predicate.hxx" + +namespace graphene { namespace protocol { + +using result_type = object_restriction_predicate; + +result_type get_restriction_predicate_list_2(size_t idx, vector rs) { + return typelist::runtime::dispatch(operation_list_2::list(), idx, [&rs] (auto t) -> result_type { + using Op = typename decltype(t)::type; + return [p=restrictions_to_predicate(std::move(rs), true)] (const operation& op) { + FC_ASSERT(op.which() == operation::tag::value, + "Supplied operation is incorrect type for restriction predicate"); + return p(op.get()); + }; + }); +} +} } diff --git a/libraries/protocol/custom_authorities/list_3.cpp b/libraries/protocol/custom_authorities/list_3.cpp new file mode 100644 index 0000000000..81f5504029 --- /dev/null +++ b/libraries/protocol/custom_authorities/list_3.cpp @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2019 Contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "restriction_predicate.hxx" + +namespace graphene { namespace protocol { + +using result_type = object_restriction_predicate; + +result_type get_restriction_predicate_list_3(size_t idx, vector rs) { + return typelist::runtime::dispatch(operation_list_3::list(), idx, [&rs] (auto t) -> result_type { + using Op = typename decltype(t)::type; + return [p=restrictions_to_predicate(std::move(rs), true)] (const operation& op) { + FC_ASSERT(op.which() == operation::tag::value, + "Supplied operation is incorrect type for restriction predicate"); + return p(op.get()); + }; + }); +} +} } diff --git a/libraries/protocol/custom_authorities/list_4.cpp b/libraries/protocol/custom_authorities/list_4.cpp new file mode 100644 index 0000000000..5ec0f63638 --- /dev/null +++ b/libraries/protocol/custom_authorities/list_4.cpp @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2019 Contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "restriction_predicate.hxx" + +namespace graphene { namespace protocol { + +using result_type = object_restriction_predicate; + +result_type get_restriction_predicate_list_4(size_t idx, vector rs) { + return typelist::runtime::dispatch(operation_list_4::list(), idx, [&rs] (auto t) -> result_type { + using Op = typename decltype(t)::type; + return [p=restrictions_to_predicate(std::move(rs), true)] (const operation& op) { + FC_ASSERT(op.which() == operation::tag::value, + "Supplied operation is incorrect type for restriction predicate"); + return p(op.get()); + }; + }); +} +} } diff --git a/libraries/protocol/custom_authorities/list_5.cpp b/libraries/protocol/custom_authorities/list_5.cpp new file mode 100644 index 0000000000..72734400b1 --- /dev/null +++ b/libraries/protocol/custom_authorities/list_5.cpp @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2019 Contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "restriction_predicate.hxx" + +namespace graphene { namespace protocol { + +using result_type = object_restriction_predicate; + +result_type get_restriction_predicate_list_5(size_t idx, vector rs) { + return typelist::runtime::dispatch(operation_list_5::list(), idx, [&rs] (auto t) -> result_type { + using Op = typename decltype(t)::type; + return [p=restrictions_to_predicate(std::move(rs), true)] (const operation& op) { + FC_ASSERT(op.which() == operation::tag::value, + "Supplied operation is incorrect type for restriction predicate"); + return p(op.get()); + }; + }); +} +} } diff --git a/libraries/protocol/custom_authorities/list_6.cpp b/libraries/protocol/custom_authorities/list_6.cpp new file mode 100644 index 0000000000..cbbc351140 --- /dev/null +++ b/libraries/protocol/custom_authorities/list_6.cpp @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2019 Contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "restriction_predicate.hxx" + +namespace graphene { namespace protocol { + +using result_type = object_restriction_predicate; + +result_type get_restriction_predicate_list_6(size_t idx, vector rs) { + return typelist::runtime::dispatch(operation_list_6::list(), idx, [&rs] (auto t) -> result_type { + using Op = typename decltype(t)::type; + return [p=restrictions_to_predicate(std::move(rs), true)] (const operation& op) { + FC_ASSERT(op.which() == operation::tag::value, + "Supplied operation is incorrect type for restriction predicate"); + return p(op.get()); + }; + }); +} +} } diff --git a/libraries/protocol/custom_authorities/list_7.cpp b/libraries/protocol/custom_authorities/list_7.cpp new file mode 100644 index 0000000000..21bb50f41b --- /dev/null +++ b/libraries/protocol/custom_authorities/list_7.cpp @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2019 Contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "restriction_predicate.hxx" + +namespace graphene { namespace protocol { + +using result_type = object_restriction_predicate; + +result_type get_restriction_predicate_list_7(size_t idx, vector rs) { + return typelist::runtime::dispatch(operation_list_7::list(), idx, [&rs] (auto t) -> result_type { + using Op = typename decltype(t)::type; + return [p=restrictions_to_predicate(std::move(rs), true)] (const operation& op) { + FC_ASSERT(op.which() == operation::tag::value, + "Supplied operation is incorrect type for restriction predicate"); + return p(op.get()); + }; + }); +} +} } diff --git a/libraries/protocol/custom_authorities/restriction_predicate.cpp b/libraries/protocol/custom_authorities/restriction_predicate.cpp new file mode 100644 index 0000000000..1b1094b3aa --- /dev/null +++ b/libraries/protocol/custom_authorities/restriction_predicate.cpp @@ -0,0 +1,179 @@ +/* + * Copyright (c) 2019 Contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include + +#include "restriction_predicate.hxx" + +namespace graphene { namespace protocol { + +restriction_predicate_function get_restriction_predicate(vector rs, operation::tag_type op_type) { + auto f = typelist::runtime::dispatch(operation::list(), op_type, [&rs](auto t) -> restriction_predicate_function { + using Op = typename decltype(t)::type; + if (typelist::contains()) + return get_restriction_predicate_list_1(typelist::index_of(), std::move(rs)); + if (typelist::contains()) + return get_restriction_predicate_list_2(typelist::index_of(), std::move(rs)); + if (typelist::contains()) + return get_restriction_predicate_list_3(typelist::index_of(), std::move(rs)); + if (typelist::contains()) + return get_restriction_predicate_list_4(typelist::index_of(), std::move(rs)); + if (typelist::contains()) + return get_restriction_predicate_list_5(typelist::index_of(), std::move(rs)); + if (typelist::contains()) + return get_restriction_predicate_list_6(typelist::index_of(), std::move(rs)); + if (typelist::contains()) + return get_restriction_predicate_list_7(typelist::index_of(), std::move(rs)); + + // Compile time check that we'll never get to the exception below + static_assert(typelist::contains, Op>(), ""); + FC_THROW_EXCEPTION(fc::assert_exception, + "LOGIC ERROR: Operation type not handled by custom authorities implementation. " + "Please report this error."); + }); + + // Wrap function in a layer that, if the function returns an error, reverses the order of the rejection path. This + // is because the order the path is created in, from the top of the call stack to the bottom, is counterintuitive. + return [f=std::move(f)](const operation& op) { return f(op).reverse_path(); }; +} + +predicate_result& predicate_result::reverse_path() { + if (success == true) + return *this; + auto reverse_subpaths = [](rejection_indicator& indicator) { + if (indicator.is_type>()) { + auto& results = indicator.get>(); + for (predicate_result& result : results) result.reverse_path(); + } + }; + std::reverse(rejection_path.begin(), rejection_path.end()); + std::for_each(rejection_path.begin(), rejection_path.end(), reverse_subpaths); + return *this; +} + +// These are some compile-time tests of the metafunctions and predicate type analysis. They are turned off to make +// building faster; they only need to be enabled when making changes in restriction_predicate.hxx +#if false +static_assert(!is_container, ""); +static_assert(is_container>, ""); +static_assert(is_container>, ""); +static_assert(is_container, ""); +static_assert(is_flat_set>, ""); +static_assert(!is_flat_set>, ""); + +static_assert(predicate_eq()(10, 20) == false, ""); +static_assert(predicate_eq()(10, 5) == false, ""); +static_assert(predicate_eq()(10, 10) == true, ""); + +static_assert(predicate_eq::valid == false, ""); +static_assert(predicate_eq::valid == false, ""); +static_assert(predicate_eq::valid == false, ""); +static_assert(predicate_eq::valid == true, ""); +static_assert(predicate_eq::valid == true, ""); +static_assert(predicate_eq, int64_t>::valid == true, ""); +static_assert(predicate_eq, int64_t>::valid == true, ""); +static_assert(predicate_eq::valid == true, ""); +static_assert(predicate_eq::valid == false, ""); +static_assert(predicate_eq::valid == false, ""); +static_assert(predicate_eq, int64_t>::valid == true, ""); +static_assert(predicate_eq, int64_t>::valid == true, ""); +static_assert(predicate_eq, void_t>::valid == true, ""); +static_assert(predicate_eq, flat_set>::valid == true, ""); +static_assert(predicate_eq, string>::valid == false, ""); +static_assert(predicate_eq::valid == true, ""); +static_assert(predicate_ne::valid == false, ""); +static_assert(predicate_ne::valid == false, ""); +static_assert(predicate_ne::valid == true, ""); +static_assert(predicate_ne::valid == true, ""); +static_assert(predicate_ne, int64_t>::valid == true, ""); +static_assert(predicate_ne, int64_t>::valid == true, ""); +static_assert(predicate_ne::valid == true, ""); +static_assert(predicate_ne::valid == false, ""); +static_assert(predicate_ne::valid == false, ""); +static_assert(predicate_ne, int64_t>::valid == true, ""); +static_assert(predicate_ne, int64_t>::valid == true, ""); +static_assert(predicate_ne, void_t>::valid == true, ""); +static_assert(predicate_ne::valid == true, ""); + +static_assert(predicate_compare()(20, 10) == 1, ""); +static_assert(predicate_compare()(5, 10) == -1, ""); +static_assert(predicate_compare()(10, 10) == 0, ""); +static_assert(predicate_lt()(20, 10) == false, ""); +static_assert(predicate_lt()(5, 10) == true, ""); +static_assert(predicate_lt()(10, 10) == false, ""); +static_assert(predicate_le()(20, 10) == false, ""); +static_assert(predicate_le()(5, 10) == true, ""); +static_assert(predicate_le()(10, 10) == true, ""); +static_assert(predicate_gt()(20, 10) == true, ""); +static_assert(predicate_gt()(5, 10) == false, ""); +static_assert(predicate_gt()(10, 10) == false, ""); +static_assert(predicate_ge()(20, 10) == true, ""); +static_assert(predicate_ge()(5, 10) == false, ""); +static_assert(predicate_ge()(10, 10) == true, ""); + +static_assert(predicate_compare::valid == true, ""); +static_assert(predicate_compare::valid == true, ""); +static_assert(predicate_compare::valid == true, ""); +static_assert(predicate_compare, int64_t>::valid == false, ""); +static_assert(predicate_compare, int64_t>::valid == true, ""); +static_assert(predicate_compare, int64_t>::valid == true, ""); +static_assert(predicate_compare, string>::valid == true, ""); +static_assert(predicate_lt::valid == true, ""); +static_assert(predicate_lt::valid == true, ""); +static_assert(predicate_lt::valid == true, ""); +static_assert(predicate_lt, int64_t>::valid == false, ""); +static_assert(predicate_lt, int64_t>::valid == true, ""); +static_assert(predicate_lt, int64_t>::valid == true, ""); +static_assert(predicate_lt, string>::valid == true, ""); + +static_assert(predicate_in::valid == false, ""); +static_assert(predicate_in>::valid == false, ""); +static_assert(predicate_in>::valid == true, ""); +static_assert(predicate_in, flat_set>::valid == false, ""); +static_assert(predicate_in, flat_set>::valid == true, ""); +static_assert(predicate_not_in::valid == false, ""); +static_assert(predicate_not_in>::valid == false, ""); +static_assert(predicate_not_in>::valid == true, ""); +static_assert(predicate_not_in, flat_set>::valid == false, ""); +static_assert(predicate_not_in, flat_set>::valid == true, ""); + +static_assert(predicate_has_all::valid == false, ""); +static_assert(predicate_has_all>::valid == false, ""); +static_assert(predicate_has_all>::valid == false, ""); +static_assert(predicate_has_all, flat_set>::valid == true, ""); +static_assert(predicate_has_all, flat_set>::valid == false, ""); +static_assert(predicate_has_all>, flat_set>::valid == true, ""); +static_assert(predicate_has_none::valid == false, ""); +static_assert(predicate_has_none>::valid == false, ""); +static_assert(predicate_has_none>::valid == false, ""); +static_assert(predicate_has_none, flat_set>::valid == true, ""); +static_assert(predicate_has_none, flat_set>::valid == false, ""); +static_assert(predicate_has_none>, flat_set>::valid == true, ""); + +#endif + +} } // namespace graphene::protocol diff --git a/libraries/protocol/custom_authorities/restriction_predicate.hxx b/libraries/protocol/custom_authorities/restriction_predicate.hxx new file mode 100644 index 0000000000..c32741db09 --- /dev/null +++ b/libraries/protocol/custom_authorities/restriction_predicate.hxx @@ -0,0 +1,609 @@ +/* + * Copyright (c) 2019 Contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include + +#include + +#include "safe_compare.hpp" + +namespace graphene { namespace protocol { +namespace typelist = fc::typelist; +using std::declval; +using std::size_t; +using restriction_function = restriction::function_type; +using restriction_argument = restriction::argument_type; + +// Make our own std::void_t since the real one isn't available in C++14 +template using make_void = void; + +// Metafunction to check if type is some instantiation of fc::safe +template constexpr static bool is_safe = false; +template constexpr static bool is_safe> = true; + +// Metafunction to check if type is a flat_set of any element type +template struct is_flat_set_impl : std::false_type {}; +template struct is_flat_set_impl> : std::true_type {}; +template constexpr static bool is_flat_set = is_flat_set_impl::value; + +// We use our own is_integral which does not consider bools integral (to disallow comparison between bool and ints) +template constexpr static bool is_integral = !std::is_same::value && + !std::is_same>::value && + (is_safe || std::is_integral::value); + +// Metafunction to check if two types are comparable, which means not void_t, and either the same or both integral +template +constexpr static bool comparable_types = !std::is_same::value && + (std::is_same::value || (is_integral && is_integral)); + +// Metafunction to check if type is a container +template +struct is_container_impl : std::false_type {}; +template +struct is_container_impl().size())>> : std::true_type {}; +template constexpr static bool is_container = is_container_impl::value; + +// Type alias for a predicate on a particular field type +template +using object_restriction_predicate = std::function; + +// Get the actual number when type might be a safe +template::value>> +const auto& to_num(const I& i) { return i; } +template +const auto& to_num(const fc::safe& i) { return i.value; } +inline auto to_num(const fc::time_point_sec& t) { return t.sec_since_epoch(); } + +namespace safenum = boost::safe_numerics::safe_compare; + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// *** Restriction Predicate Logic *** +// +// This file implements the core logic of Custom Active Authorities. A CAA is an authority which is permitted by an +// account to execute a particular authority on that account's behalf, with some restrictions on the content of that +// operation. This file implements the logic to validate those restrictions, and create a predicate function which +// takes a particular operation and determines whether it complies with the restrictions or not. +// +// The restrictions are a recursive structure, which applies to a particular operation struct, but may recurse to +// specify restrictions on fields or subfields of that struct. This file explores the restriction structure in tandem +// with the operation struct to verify that all of the restrictions are valid and to produce a predicate function. +// Note that this file operates primarily on restriction data, but only operation *types*, meaning the actual +// operation value does not appear until the predicate returned by this file is run. +// +// As a result, this file is very template heavy, and does a good deal of type manipulation. Its contents are +// organized as a series of layers, which recursively examine the restrictions and types they apply to, and finally, +// once all the types have been resolved, a predicate function is created which evaluates the restrictions on an +// operation. +// +// To give an overview of the logic, the layers stack up like so, from beginning (bottom of file) to end: +// - restrictions_to_predicate() -- takes a vector and creates a predicate for each of them, +// but returns a single predicate that returns true only if all sub-predicates return true +// - create_field_predicate() -- Resolves which field of Object the restriction is referencing by indexing +// into the object's reflected fields with the predicate's member_index +// - create_logical_or_predicate() -- If the predicate is a logical OR function, the predicate does not +// specify a field to examine; rather, the predicates in its branches do. Thus this function recurses into +// restrictions_to_predicate for each branch of the OR, and combines the resulting predicates in a predicate +// which returns true if any branch of the OR passes +// - create_predicate_function() -- switches on restriction type to determine which predicate template to use +// going forward +// - make_predicate -- Determines what type the restriction argument is and creates +// a predicate functor for that type +// - attribute_assertion -- If the restriction is an attribute assertion, instead of using make_predicate +// to create a predicate function, we first recurse into restrictions_to_predicate with Field as the Object +// - variant_assertion -- If the restriction is a variant assertion, instead of using make_predicate, we +// recurse into restrictions_to_predicate with the variant value as the Object +// - embed_argument() -- Embeds the argument into the predicate if it is a valid type +// for the predicate, and throws otherwise. +// - predicate_xyz -- These are functors implementing the various predicate function types +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// These typelists contain the argument types legal for various function types: + +// Valid for magnitude comparisons and equality comparisons +using comparable_types_list = typelist::list; +// Valid for list functions (in, not_in, has_all, has_none) +struct make_flat_set { template struct transform { using type = flat_set; }; }; +using list_types_list = typelist::transform, + comparable_types_list>, + make_flat_set>; +// Valid for equality comparisons but not necessarily magnitude comparisons +using equality_types_list = typename typelist::concat, + comparable_types_list, list_types_list>; +// Valid for attritube assertions +using attr_types_list = typelist::list>; +// Valid for logical or assertions +using or_types_list = typelist::list>>; + +//////////////////////////////////////////////// PREDICATE FUNCTORS //////////////////////////////////////////////// +// An invalid predicate which throws upon construction. Inherited by other predicates when arg types are incompatible +template +struct predicate_invalid { + constexpr static bool valid = false; + predicate_invalid() { FC_THROW_EXCEPTION(fc::assert_exception, "Invalid types for predicate"); } + bool operator()(const A&, const B&) const { return false; } +}; +// Equality comparison +template struct predicate_eq : predicate_invalid {}; +template +struct predicate_eq::value>> { + // Simple comparison, same type + constexpr static bool valid = true; + constexpr bool operator()(const Field& f, const Argument& a) const { return f == a; } +}; +template +struct predicate_eq && is_integral && + !std::is_same::value>> { + // Simple comparison, integral types + constexpr static bool valid = true; + constexpr bool operator()(const Field& f, const Argument& a) const { return safenum::equal(to_num(f), to_num(a)); } +}; +template +struct predicate_eq && is_integral>> { + // Compare container size against int + constexpr static bool valid = true; + bool operator()(const Field& f, const Argument& a) const { return safenum::equal(f.size(), to_num(a)); } +}; +template +struct predicate_eq, Argument, std::enable_if_t>> + : predicate_eq { + // Compare optional value against comparable type + using base = predicate_eq; + bool operator()(const fc::optional& f, const Argument& a) const { + if (!f.valid()) return predicate_result::Rejection(predicate_result::null_optional); + return (*this)(*f, a); + } +}; +template +struct predicate_eq, void_t, void> { + // Compare optional value against void_t (checks that optional is null) + constexpr static bool valid = true; + bool operator()(const fc::optional& f, const void_t&) const { return !f.valid(); } +}; +// Not-equal is just an equality comparison wrapped in a negator +template struct predicate_ne : predicate_eq { + using equal = predicate_eq; + bool operator()(const Field& f, const Argument& a) const { return !equal::operator()(f, a); } +}; + +// Shared implementation for all inequality comparisons +template struct predicate_compare : predicate_invalid {}; +template +struct predicate_compare::value>> { + // Simple comparison, same types + constexpr static bool valid = true; + constexpr int8_t operator()(const Field& f, const Argument& a) const { + return fa? 1 : 0); + } +}; +template +struct predicate_compare && is_integral && + !std::is_same::value>> { + // Simple comparison, integral types + constexpr static bool valid = true; + constexpr int8_t operator()(const Field& f, const Argument& a) const { + auto nf = to_num(f); + auto na = to_num(a); + return safenum::less_than(nf, na)? -1 : (safenum::greater_than(nf, na)? 1 : 0); + } +}; +template +struct predicate_compare, Argument, void> : predicate_compare { + // Compare optional value against comparable type + constexpr static bool valid = true; + constexpr int8_t operator()(const fc::optional& f, const Argument& a) const { + if (!f.valid()) return predicate_result::Rejection(predicate_result::null_optional); + return (*this)(*f, a); + } +}; +// The actual inequality predicates +template struct predicate_lt : predicate_compare { + using base = predicate_compare; + constexpr bool operator()(const Field& f, const Argument& a) const { return base::operator()(f, a) < 0; } +}; +template struct predicate_le : predicate_compare { + using base = predicate_compare; + constexpr bool operator()(const Field& f, const Argument& a) const { return base::operator()(f, a) <= 0; } +}; +template struct predicate_gt : predicate_compare { + using base = predicate_compare; + constexpr bool operator()(const Field& f, const Argument& a) const { return base::operator()(f, a) > 0; } +}; +template struct predicate_ge : predicate_compare { + using base = predicate_compare; + constexpr bool operator()(const Field& f, const Argument& a) const { return base::operator()(f, a) >= 0; } +}; + +// Field-in-list predicate +template struct predicate_in : predicate_invalid {}; +template +struct predicate_in, std::enable_if_t && !is_safe>> { + // Simple inclusion check + constexpr static bool valid = true; + bool operator()(const Field& f, const flat_set& c) const { return c.count(f) != 0; } +}; +template +struct predicate_in, flat_set, std::enable_if_t>> { + // Check for safe value + constexpr static bool valid = true; + bool operator()(const fc::safe& f, const flat_set& c) const { return c.count(f.value) != 0; } +}; +template +struct predicate_in, flat_set, std::enable_if_t>> { + // Check for optional value + constexpr static bool valid = true; + bool operator()(const fc::optional& f, const flat_set& c) const { + if (!f.valid()) return predicate_result::Rejection(predicate_result::null_optional); + return c.count(*f) != 0; + } +}; +template +struct predicate_in, + std::enable_if_t && + comparable_types>> { + // Check all values in container are in argument + constexpr static bool valid = true; + // Unsorted container + template, bool> = true> + bool operator()(const Container& c, const flat_set& a) const { + return std::all_of(c.begin(), c.end(), [&a](const auto& ce) { return a.count(ce) > 0; }); + } + // Sorted container + template, bool> = true> + bool operator()(const Container& c, const flat_set& a) const { + return std::includes(a.begin(), a.end(), c.begin(), c.end()); + } +}; +// Field-not-in-list is just field-in-list wrapped in a negator +template struct predicate_not_in : predicate_in { + using base = predicate_in; + bool operator()(const Field& f, const Container& c) const { return !base::operator()(f, c); } +}; +// Container-field-not-in-list is not a simple negation of predicate_in, specialize here +template +struct predicate_not_in, + std::enable_if_t && + comparable_types>> { + constexpr static bool valid = true; + // Unsorted container + template, bool> = true> + bool operator()(const Container& c, const flat_set& a) const { + return std::none_of(c.begin(), c.end(), [&a](const auto& ce) { return a.count(ce) > 0; }); + } + // Sorted container + template, bool> = true> + bool operator()(const Container& c, const flat_set& a) const { + flat_set intersection; + std::set_intersection(c.begin(), c.end(), a.begin(), a.end(), + std::inserter(intersection, intersection.begin())); + return intersection.empty(); + } +}; + +// List-contains-list predicate +template struct predicate_has_all : predicate_invalid {}; +template +struct predicate_has_all, flat_set, + std::enable_if_t>> { + // Field is already flat_set + constexpr static bool valid = true; + bool operator()(const flat_set& f, const flat_set& a) const { + if (f.size() < a.size()) return false; + return std::includes(f.begin(), f.end(), a.begin(), a.end()); + } +}; +template +struct predicate_has_all, + std::enable_if_t && !is_flat_set && + comparable_types>> { + // Field is other container; convert to flat_set + constexpr static bool valid = true; + bool operator()(const FieldContainer& f, const flat_set& a) const { + if (f.size() < a.size()) return false; + std::set fs(f.begin(), f.end()); + return std::includes(fs.begin(), fs.end(), a.begin(), a.end()); + } +}; +template +struct predicate_has_all, Argument, void> : predicate_has_all { + // Field is optional container + bool operator()(const fc::optional& f, const Argument& a) const { + if (!f.valid()) return predicate_result::Rejection(predicate_result::null_optional); + return (*this)(*f, a); + } +}; + +// List contains none of list predicate +template struct predicate_has_none : predicate_invalid {}; +template +struct predicate_has_none, flat_set, + std::enable_if_t>> { + // Field is already flat_set + constexpr static bool valid = true; + bool operator()(const flat_set& f, const flat_set& a) const { + flat_set intersection; + std::set_intersection(f.begin(), f.end(), a.begin(), a.end(), + std::inserter(intersection, intersection.begin())); + return intersection.empty(); + } +}; +template +struct predicate_has_none, + std::enable_if_t && !is_flat_set && + comparable_types>> { + // Field is other container + constexpr static bool valid = true; + bool operator()(const FieldContainer& f, const flat_set& a) const { + return !std::any_of(f.begin(), f.end(), [&a](const auto& fe) { return a.count(fe) > 0; }); + } +}; +template +struct predicate_has_none, Argument, void> : predicate_has_all { + // Field is optional container + bool operator()(const fc::optional& f, const Argument& a) const { + if (!f.valid()) return predicate_result::Rejection(predicate_result::null_optional); + return (*this)(*f, a); + } +}; +////////////////////////////////////////////// END PREDICATE FUNCTORS ////////////////////////////////////////////// + +// Forward declaration of restrictions_to_predicate, because attribute assertions and logical ORs recurse into it +template object_restriction_predicate restrictions_to_predicate(vector, bool); + +template +struct attribute_assertion { + static object_restriction_predicate create(vector&& rs) { + return restrictions_to_predicate(std::move(rs), false); + } +}; +template +struct attribute_assertion> { + static object_restriction_predicate> create(vector&& rs) { + return [p=restrictions_to_predicate(std::move(rs), false)](const fc::optional& f) { + if (!f.valid()) return predicate_result::Rejection(predicate_result::null_optional); + return p(*f); + }; + } +}; +template +struct attribute_assertion> { + static object_restriction_predicate> create(vector&& rs) { + return [p=restrictions_to_predicate(std::move(rs), false)](const extension& x) { + return p(x.value); + }; + } +}; + +template +struct variant_assertion { + static object_restriction_predicate create(restriction::variant_assert_argument_type&&) { + FC_THROW_EXCEPTION(fc::assert_exception, "Invalid variant assertion on non-variant field", + ("Field", fc::get_typename::name())); + } +}; +template +struct variant_assertion> { + using Variant = static_variant; + + template + static auto make_predicate(vector&& rs) { + return [p=restrictions_to_predicate(std::move(rs), true)](const Variant& v) { + if (v.which() == Variant::template tag::value) + return p(v.template get()); + return predicate_result::Rejection(predicate_result::incorrect_variant_type); + }; + } + static object_restriction_predicate create(restriction::variant_assert_argument_type&& arg) { + return typelist::runtime::dispatch(typelist::list(), arg.first, + [&arg](auto t) -> object_restriction_predicate { + using Value = typename decltype(t)::type; + return variant_assertion::make_predicate(std::move(arg.second)); + }); + } +}; +template +struct variant_assertion>> { + using Variant = static_variant; + using Optional = fc::optional; + static object_restriction_predicate create(restriction::variant_assert_argument_type&& arg) { + return typelist::runtime::dispatch(typelist::list(), arg.first, + [&arg](auto t) -> object_restriction_predicate { + using Value = typename decltype(t)::type; + auto pred = variant_assertion::template make_predicate(std::move(arg.second)); + return [p=std::move(pred)](const Optional& opt) { + if (!opt.valid()) return predicate_result::Rejection(predicate_result::null_optional); + return p(*opt); + }; + }); + } +}; + +// Embed the argument into the predicate functor +template> +object_restriction_predicate embed_argument(P p, A a, short) { + return [p=std::move(p), a=std::move(a)](const F& f) { + if (p(f, a)) return predicate_result::Success(); + return predicate_result::Rejection(predicate_result::predicate_was_false); + }; +} +template +object_restriction_predicate embed_argument(P, A, long) { + FC_THROW_EXCEPTION(fc::assert_exception, "Invalid types for predicate"); +} + +// Resolve the argument type and make a predicate for it +template class Predicate, typename Field, typename ArgVariant> +object_restriction_predicate make_predicate(ArgVariant arg) { + return typelist::runtime::dispatch(typename ArgVariant::list(), arg.which(), + [&arg](auto t) mutable -> object_restriction_predicate { + using Arg = typename decltype(t)::type; + return embed_argument(Predicate(), std::move(arg.template get()), short()); + }); +} + +template +object_restriction_predicate create_predicate_function(restriction_function func, restriction_argument arg) { + try { + switch(func) { + case restriction::func_eq: + return make_predicate(static_variant::import_from(std::move(arg))); + case restriction::func_ne: + return make_predicate(static_variant::import_from(std::move(arg))); + case restriction::func_lt: + return make_predicate(static_variant + ::import_from(std::move(arg))); + case restriction::func_le: + return make_predicate(static_variant + ::import_from(std::move(arg))); + case restriction::func_gt: + return make_predicate(static_variant + ::import_from(std::move(arg))); + case restriction::func_ge: + return make_predicate(static_variant + ::import_from(std::move(arg))); + case restriction::func_in: + return make_predicate(static_variant::import_from(std::move(arg))); + case restriction::func_not_in: + return make_predicate(static_variant + ::import_from(std::move(arg))); + case restriction::func_has_all: + return make_predicate(static_variant + ::import_from(std::move(arg))); + case restriction::func_has_none: + return make_predicate(static_variant + ::import_from(std::move(arg))); + case restriction::func_attr: + FC_ASSERT(arg.which() == restriction_argument::tag>::value, + "Argument type for attribute assertion must be restriction list"); + return attribute_assertion::create(std::move(arg.get>())); + case restriction::func_variant_assert: + FC_ASSERT(arg.which() == restriction_argument::tag::value, + "Argument type for attribute assertion must be pair of variant tag and restriction list"); + return variant_assertion::create(std::move(arg.get())); + default: + FC_THROW_EXCEPTION(fc::assert_exception, "Invalid function type on restriction"); + } + } FC_CAPTURE_AND_RETHROW( (fc::get_typename::name())(func)(arg) ) +} + +/** + * @brief Create a predicate asserting on the field of the object a restriction is referencing + * + * @tparam Object The type the restriction restricts + * + * A restriction specifies requirements about a field of an object. This struct shifts the focus from the object type + * the restriction references to the particular field type, creates a predicate on that field, and wraps that + * predicate to accept the object type and invoke the inner predicate on the specified field. + */ +template::native_members>() != 0>> +object_restriction_predicate create_field_predicate(restriction&& r, short) { + using member_list = typename fc::reflector::native_members; + FC_ASSERT(r.member_index < typelist::length(), "Invalid member index ${I} for object ${O}", + ("I", r.member_index)("O", fc::get_typename::name())); + auto predicator = [f=r.restriction_type, a=std::move(r.argument)](auto t) -> object_restriction_predicate { + using FieldReflection = typename decltype(t)::type; + using Field = typename FieldReflection::type; + auto p = create_predicate_function(static_cast(f), std::move(a)); + return [p=std::move(p)](const Object& o) { return p(FieldReflection::get(o)); }; + }; + return typelist::runtime::dispatch(member_list(), r.member_index.value, predicator); +} +template +object_restriction_predicate create_field_predicate(restriction&&, long) { + FC_THROW_EXCEPTION(fc::assert_exception, "Invalid restriction references member of non-object type: ${O}", + ("O", fc::get_typename::name())); +} + +template +object_restriction_predicate create_logical_or_predicate(vector> rs) { + FC_ASSERT(rs.size() > 1, "Logical OR must have at least two branches"); + auto to_predicate = std::bind(restrictions_to_predicate, std::placeholders::_1, false); + + vector> predicates; + std::transform(std::make_move_iterator(rs.begin()), std::make_move_iterator(rs.end()), + std::back_inserter(predicates), to_predicate); + + return [predicates=std::move(predicates)](const Object& obj) { + vector rejections; + bool success = std::any_of(predicates.begin(), predicates.end(), + [o=std::cref(obj), &rejections](const auto& p) { + auto result = p(o); + if (!result) rejections.push_back(std::move(result)); + return !!result; + }); + if (success) return predicate_result::Success(); + return predicate_result::Rejection(std::move(rejections)); + }; +} + +template +object_restriction_predicate restrictions_to_predicate(vector rs, bool allow_empty) { + if (!allow_empty) + FC_ASSERT(!rs.empty(), "Empty attribute assertions and logical OR branches are not permitted"); + + vector> predicates; + std::transform(std::make_move_iterator(rs.begin()), std::make_move_iterator(rs.end()), + std::back_inserter(predicates), [](restriction&& r) { + if (r.restriction_type.value == restriction::func_logical_or) { + FC_ASSERT(r.argument.which() == restriction_argument::tag>>::value, + "Restriction argument for logical OR function type must be list of restriction lists."); + return create_logical_or_predicate(std::move(r.argument.get>>())); + } + return create_field_predicate(std::move(r), short()); + }); + + return [predicates=std::move(predicates)](const Object& obj) { + for (size_t i = 0; i < predicates.size(); ++i) { + auto result = predicates[i](obj); + if (!result) { + result.rejection_path.push_back(i); + return result; + } + } + return predicate_result::Success(); + }; +} + +// To make the build gentler on RAM, break the operation list into several pieces to build over several files +using operation_list_1 = static_variant>; +using operation_list_2 = static_variant>; +using operation_list_3 = static_variant>; +using operation_list_4 = static_variant>; +using operation_list_5 = static_variant>; +using operation_list_6 = static_variant>; +using operation_list_7 = static_variant>; + +object_restriction_predicate get_restriction_predicate_list_1(size_t idx, vector rs); +object_restriction_predicate get_restriction_predicate_list_2(size_t idx, vector rs); +object_restriction_predicate get_restriction_predicate_list_3(size_t idx, vector rs); +object_restriction_predicate get_restriction_predicate_list_4(size_t idx, vector rs); +object_restriction_predicate get_restriction_predicate_list_5(size_t idx, vector rs); +object_restriction_predicate get_restriction_predicate_list_6(size_t idx, vector rs); +object_restriction_predicate get_restriction_predicate_list_7(size_t idx, vector rs); + +} } // namespace graphene::protocol diff --git a/libraries/protocol/custom_authorities/safe_compare.hpp b/libraries/protocol/custom_authorities/safe_compare.hpp new file mode 100644 index 0000000000..5b05065c6f --- /dev/null +++ b/libraries/protocol/custom_authorities/safe_compare.hpp @@ -0,0 +1,185 @@ +#ifndef BOOST_NUMERIC_SAFE_COMPARE_HPP +#define BOOST_NUMERIC_SAFE_COMPARE_HPP + +// MS compatible compilers support #pragma once +#if defined(_MSC_VER) && (_MSC_VER >= 1020) +# pragma once +#endif + +// Copyright (c) 2012 Robert Ramey +// +// Distributed under the Boost Software License, Version 1.0. (See +// accompanying file BOOST_LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) + +#include +#include + +namespace boost { +namespace safe_numerics { +namespace safe_compare { + +//////////////////////////////////////////////////// +// safe comparison on primitive integral types +namespace safe_compare_detail { + template + using make_unsigned = typename std::conditional< + std::is_signed::value, + std::make_unsigned, + T + >::type; + + // both arguments unsigned or signed + template + struct less_than { + template + constexpr static bool invoke(const T & t, const U & u){ + return t < u; + } + }; + + // T unsigned, U signed + template<> + struct less_than { + template + constexpr static bool invoke(const T & t, const U & u){ + return + (u < 0) ? + false + : + less_than::invoke( + t, + static_cast::type &>(u) + ) + ; + } + }; + // T signed, U unsigned + template<> + struct less_than { + template + constexpr static bool invoke(const T & t, const U & u){ + return + (t < 0) ? + true + : + less_than::invoke( + static_cast::type &>(t), + u + ) + ; + } + }; +} // safe_compare_detail + +template +typename std::enable_if< + std::is_integral::value && std::is_integral::value, + bool +>::type +constexpr less_than(const T & lhs, const U & rhs) { + return safe_compare_detail::less_than< + std::is_signed::value, + std::is_signed::value + >::template invoke(lhs, rhs); +} + +template +typename std::enable_if< + std::is_floating_point::value && std::is_floating_point::value, + bool +>::type +constexpr less_than(const T & lhs, const U & rhs) { + return lhs < rhs; +} + +template +constexpr bool greater_than(const T & lhs, const U & rhs) { + return less_than(rhs, lhs); +} + +template +constexpr bool less_than_equal(const T & lhs, const U & rhs) { + return ! greater_than(lhs, rhs); +} + +template +constexpr bool greater_than_equal(const T & lhs, const U & rhs) { + return ! less_than(lhs, rhs); +} + +namespace safe_compare_detail { + // both arguments unsigned or signed + template + struct equal { + template + constexpr static bool invoke(const T & t, const U & u){ + return t == u; + } + }; + + // T unsigned, U signed + template<> + struct equal { + template + constexpr static bool invoke(const T & t, const U & u){ + return + (u < 0) ? + false + : + equal::invoke( + t, + static_cast::type &>(u) + ) + ; + } + }; + // T signed, U unsigned + template<> + struct equal { + template + constexpr static bool invoke(const T & t, const U & u){ + return + (t < 0) ? + false + : + equal::invoke( + static_cast::type &>(t), + u + ) + ; + } + }; +} // safe_compare_detail + +template +typename std::enable_if< + std::is_integral::value && std::is_integral::value, + bool +>::type +constexpr equal(const T & lhs, const U & rhs) { + return safe_compare_detail::equal< + std::numeric_limits::is_signed, + std::numeric_limits::is_signed + >::template invoke(lhs, rhs); +} + +template +typename std::enable_if< + std::is_floating_point::value && std::is_floating_point::value, + bool +>::type +constexpr equal(const T & lhs, const U & rhs) { + return lhs == rhs; +} + +template +constexpr bool not_equal(const T & lhs, const U & rhs) { + return ! equal(lhs, rhs); +} + +} // safe_compare +} // safe_numerics +} // boost + +#endif // BOOST_NUMERIC_SAFE_COMPARE_HPP diff --git a/libraries/protocol/custom_authority.cpp b/libraries/protocol/custom_authority.cpp new file mode 100644 index 0000000000..2a6559c3e1 --- /dev/null +++ b/libraries/protocol/custom_authority.cpp @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2018 Abit More, and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#include +#include +#include + +#include + +namespace graphene { namespace protocol { + +share_type custom_authority_create_operation::calculate_fee(const fee_parameters_type& k)const { + share_type core_fee_required = k.basic_fee; + core_fee_required += k.price_per_byte * (fc::raw::pack_size(restrictions) + fc::raw::pack_size(auth)); + return core_fee_required; +} + +void custom_authority_create_operation::validate()const { + FC_ASSERT(fee.amount >= 0, "Fee amount can not be negative"); + + FC_ASSERT(account != GRAPHENE_TEMP_ACCOUNT + && account != GRAPHENE_COMMITTEE_ACCOUNT + && account != GRAPHENE_WITNESS_ACCOUNT + && account != GRAPHENE_RELAXED_COMMITTEE_ACCOUNT, + "Can not create custom authority for special accounts"); + + FC_ASSERT(valid_from < valid_to, "valid_from must be earlier than valid_to"); + + // Note: The authentication authority can be empty, but it cannot be impossible to satisify. Disable the authority + // using the `enabled` boolean rather than setting an impossible authority. + + FC_ASSERT(auth.address_auths.size() == 0, "Address authorities are not supported"); + FC_ASSERT(!auth.is_impossible(), "Cannot use an imposible authority threshold"); + + // Validate restrictions by constructing a predicate for them; this throws if restrictions aren't valid + get_restriction_predicate(restrictions, operation_type); +} + +share_type custom_authority_update_operation::calculate_fee(const fee_parameters_type& k)const { + share_type core_fee_required = k.basic_fee; + core_fee_required += k.price_per_byte * fc::raw::pack_size(restrictions_to_add); + if (new_auth) + core_fee_required += k.price_per_byte * fc::raw::pack_size(*new_auth); + return core_fee_required; +} + +void custom_authority_update_operation::validate()const { + FC_ASSERT(fee.amount >= 0, "Fee amount can not be negative"); + + FC_ASSERT(account != GRAPHENE_TEMP_ACCOUNT + && account != GRAPHENE_COMMITTEE_ACCOUNT + && account != GRAPHENE_WITNESS_ACCOUNT + && account != GRAPHENE_RELAXED_COMMITTEE_ACCOUNT, + "Can not create custom authority for special accounts"); + if (new_valid_from && new_valid_to) + FC_ASSERT(*new_valid_from < *new_valid_to, "valid_from must be earlier than valid_to"); + if (new_auth) { + FC_ASSERT(!new_auth->is_impossible(), "Cannot use an impossible authority threshold"); + FC_ASSERT(new_auth->address_auths.size() == 0, "Address auth is not supported"); + } +} + +} } // graphene::protocol diff --git a/libraries/protocol/include/graphene/protocol/authority.hpp b/libraries/protocol/include/graphene/protocol/authority.hpp index ba309ef9e8..292734a6cb 100644 --- a/libraries/protocol/include/graphene/protocol/authority.hpp +++ b/libraries/protocol/include/graphene/protocol/authority.hpp @@ -107,8 +107,9 @@ namespace graphene { namespace protocol { (a.key_auths == b.key_auths) && (a.address_auths == b.address_auths); } + friend bool operator!= ( const authority& a, const authority& b ) { return !(a==b); } uint32_t num_auths()const { return account_auths.size() + key_auths.size() + address_auths.size(); } - void clear() { account_auths.clear(); key_auths.clear(); } + void clear() { account_auths.clear(); key_auths.clear(); address_auths.clear(); weight_threshold = 0; } static authority null_authority() { diff --git a/libraries/protocol/include/graphene/protocol/chain_parameters.hpp b/libraries/protocol/include/graphene/protocol/chain_parameters.hpp index 3ccf499b58..ff398cf815 100644 --- a/libraries/protocol/include/graphene/protocol/chain_parameters.hpp +++ b/libraries/protocol/include/graphene/protocol/chain_parameters.hpp @@ -35,6 +35,14 @@ namespace graphene { namespace protocol { uint32_t max_preimage_size; }; + struct custom_authority_options_type + { + uint32_t max_custom_authority_lifetime_seconds = GRAPHENE_DEFAULT_MAX_CUSTOM_AUTHORITY_LIFETIME_SECONDS; + uint32_t max_custom_authorities_per_account = GRAPHENE_DEFAULT_MAX_CUSTOM_AUTHORITIES_PER_ACCOUNT; + uint32_t max_custom_authorities_per_account_op = GRAPHENE_DEFAULT_MAX_CUSTOM_AUTHORITIES_PER_ACCOUNT_OP; + uint32_t max_custom_authority_restrictions = GRAPHENE_DEFAULT_MAX_CUSTOM_AUTHORITY_RESTRICTIONS; + }; + struct chain_parameters { /** using a shared_ptr breaks the circular dependency created between operations and the fee schedule */ @@ -74,6 +82,7 @@ namespace graphene { namespace protocol { struct ext { optional< htlc_options > updatable_htlc_options; + optional< custom_authority_options_type > custom_authority_options; }; extension extensions; @@ -98,8 +107,16 @@ FC_REFLECT( graphene::protocol::htlc_options, (max_preimage_size) ) +FC_REFLECT( graphene::protocol::custom_authority_options_type, + (max_custom_authority_lifetime_seconds) + (max_custom_authorities_per_account) + (max_custom_authorities_per_account_op) + (max_custom_authority_restrictions) +) + FC_REFLECT( graphene::protocol::chain_parameters::ext, (updatable_htlc_options) + (custom_authority_options) ) FC_REFLECT( graphene::protocol::chain_parameters, diff --git a/libraries/protocol/include/graphene/protocol/config.hpp b/libraries/protocol/include/graphene/protocol/config.hpp index 8ea4bcc56b..b535e67158 100644 --- a/libraries/protocol/include/graphene/protocol/config.hpp +++ b/libraries/protocol/include/graphene/protocol/config.hpp @@ -138,3 +138,12 @@ ///@} #define GRAPHENE_FBA_STEALTH_DESIGNATED_ASSET (asset_id_type(743)) + +/// Maximum duration before a custom authority can expire (1 month) +#define GRAPHENE_DEFAULT_MAX_CUSTOM_AUTHORITY_LIFETIME_SECONDS (60*60*24*30) +/// Maximum number of custom authorities a particular account can set +#define GRAPHENE_DEFAULT_MAX_CUSTOM_AUTHORITIES_PER_ACCOUNT 10 +/// Maximum number of custom authorities a particular account can set for a particular operation +#define GRAPHENE_DEFAULT_MAX_CUSTOM_AUTHORITIES_PER_ACCOUNT_OP 3 +/// Maximum number of restrictions a custom authority can contain +#define GRAPHENE_DEFAULT_MAX_CUSTOM_AUTHORITY_RESTRICTIONS 10 diff --git a/libraries/protocol/include/graphene/protocol/custom_authority.hpp b/libraries/protocol/include/graphene/protocol/custom_authority.hpp new file mode 100644 index 0000000000..6802271cce --- /dev/null +++ b/libraries/protocol/include/graphene/protocol/custom_authority.hpp @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2019 Contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#pragma once +#include +#include +#include +#include + +namespace graphene { namespace protocol { + + /** + * @brief Create a new custom authority + * @ingroup operations + */ + struct custom_authority_create_operation : public base_operation { + struct fee_parameters_type { + uint64_t basic_fee = GRAPHENE_BLOCKCHAIN_PRECISION; + uint32_t price_per_byte = GRAPHENE_BLOCKCHAIN_PRECISION / 10; + }; + + /// Operation fee + asset fee; + /// Account which is setting the custom authority; also pays the fee + account_id_type account; + /// Whether the custom authority is enabled or not + bool enabled; + /// Date when custom authority becomes active + time_point_sec valid_from; + /// Expiration date for custom authority + time_point_sec valid_to; + /// Tag of the operation this custom authority can authorize + unsigned_int operation_type; + /// Authentication requirements for the custom authority + authority auth; + /// Restrictions on operations this custom authority can authenticate + vector restrictions; + + extensions_type extensions; + + account_id_type fee_payer()const { return account; } + void validate()const; + share_type calculate_fee(const fee_parameters_type& k)const; + }; + + /** + * @brief Update a custom authority + * @ingroup operations + */ + struct custom_authority_update_operation : public base_operation { + struct fee_parameters_type { + uint64_t basic_fee = GRAPHENE_BLOCKCHAIN_PRECISION; + uint32_t price_per_byte = GRAPHENE_BLOCKCHAIN_PRECISION / 10; + }; + + /// Operation fee + asset fee; + /// Account which owns the custom authority to update; also pays the fee + account_id_type account; + /// ID of the custom authority to update + custom_authority_id_type authority_to_update; + /// Change to whether the custom authority is enabled or not + optional new_enabled; + /// Change to the custom authority begin date + optional new_valid_from; + /// Change to the custom authority expiration date + optional new_valid_to; + /// Change to the authentication for the custom authority + optional new_auth; + /// Set of IDs of restrictions to remove + flat_set restrictions_to_remove; + /// Vector of new restrictions + vector restrictions_to_add; + + extensions_type extensions; + + account_id_type fee_payer()const { return account; } + void validate()const; + share_type calculate_fee(const fee_parameters_type& k)const; + }; + + + /** + * @brief Delete a custom authority + * @ingroup operations + */ + struct custom_authority_delete_operation : public base_operation { + struct fee_parameters_type { uint64_t fee = GRAPHENE_BLOCKCHAIN_PRECISION; }; + + /// Operation fee + asset fee; + /// Account which owns the custom authority to update; also pays the fee + account_id_type account; + /// ID of the custom authority to delete + custom_authority_id_type authority_to_delete; + + extensions_type extensions; + + account_id_type fee_payer()const { return account; } + void validate()const {} + share_type calculate_fee(const fee_parameters_type& k)const { return k.fee; } + }; + +} } // graphene::protocol + +FC_REFLECT(graphene::protocol::custom_authority_create_operation::fee_parameters_type, (basic_fee)(price_per_byte)) +FC_REFLECT(graphene::protocol::custom_authority_update_operation::fee_parameters_type, (basic_fee)(price_per_byte)) +FC_REFLECT(graphene::protocol::custom_authority_delete_operation::fee_parameters_type, (fee)) + +FC_REFLECT(graphene::protocol::custom_authority_create_operation, + (fee)(account)(enabled)(valid_from)(valid_to)(operation_type)(auth)(restrictions)(extensions)) + +FC_REFLECT(graphene::protocol::custom_authority_update_operation, + (fee)(account)(authority_to_update)(new_enabled)(new_valid_from) + (new_valid_to)(new_auth)(restrictions_to_remove)(restrictions_to_add)(extensions)) +FC_REFLECT(graphene::protocol::custom_authority_delete_operation, (fee)(account)(authority_to_delete)(extensions)) diff --git a/libraries/protocol/include/graphene/protocol/object_id.hpp b/libraries/protocol/include/graphene/protocol/object_id.hpp index f4f02ab914..7f627e4b19 100644 --- a/libraries/protocol/include/graphene/protocol/object_id.hpp +++ b/libraries/protocol/include/graphene/protocol/object_id.hpp @@ -169,6 +169,10 @@ struct reflector > { typedef graphene::db::object_id type; typedef std::true_type is_defined; + using native_members = typelist::list>; + using inherited_members = typelist::list<>; + using members = native_members; + using base_classes = typelist::list<>; enum member_count_enum { local_member_count = 1, total_member_count = 1 @@ -180,6 +184,10 @@ struct reflector > visitor.TEMPLATE operator()( "instance" ); } }; +namespace member_names { +template +struct member_name, 0> { static constexpr const char* value = "instance"; }; +} inline void to_variant( const graphene::db::object_id_type& var, fc::variant& vo, uint32_t max_depth = 1 ) diff --git a/libraries/protocol/include/graphene/protocol/operations.hpp b/libraries/protocol/include/graphene/protocol/operations.hpp index f7ddc01a02..9506628699 100644 --- a/libraries/protocol/include/graphene/protocol/operations.hpp +++ b/libraries/protocol/include/graphene/protocol/operations.hpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -101,7 +102,10 @@ namespace graphene { namespace protocol { htlc_redeem_operation, htlc_redeemed_operation, // VIRTUAL htlc_extend_operation, - htlc_refund_operation // VIRTUAL + htlc_refund_operation, // VIRTUAL + custom_authority_create_operation, + custom_authority_update_operation, + custom_authority_delete_operation > operation; /// @} // operations group diff --git a/libraries/protocol/include/graphene/protocol/restriction.hpp b/libraries/protocol/include/graphene/protocol/restriction.hpp new file mode 100644 index 0000000000..334cf63b03 --- /dev/null +++ b/libraries/protocol/include/graphene/protocol/restriction.hpp @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2018 Abit More, and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#pragma once +#include +#include + +namespace graphene { namespace protocol { + +/** + * Defines the set of valid operation restritions as a discriminated union type. + */ +struct restriction { + enum function_type { + func_eq, + func_ne, + func_lt, + func_le, + func_gt, + func_ge, + func_in, + func_not_in, + func_has_all, + func_has_none, + func_attr, + func_logical_or, + func_variant_assert, + FUNCTION_TYPE_COUNT ///< Sentry value which contains the number of different types + }; + + // A variant assertion argument is a pair of the tag expected to be in the variant, and the restrictions to apply + // to the value + using variant_assert_argument_type = pair>; + +#define GRAPHENE_OP_RESTRICTION_ARGUMENTS_VARIADIC \ + /* 0 */ void_t, \ + /* 1 */ bool, \ + /* 2 */ int64_t, \ + /* 3 */ string, \ + /* 4 */ time_point_sec, \ + /* 5 */ public_key_type, \ + /* 6 */ fc::sha256, \ + /* 7 */ account_id_type, \ + /* 8 */ asset_id_type, \ + /* 9 */ force_settlement_id_type, \ + /* 10 */ committee_member_id_type, \ + /* 11 */ witness_id_type, \ + /* 12 */ limit_order_id_type, \ + /* 13 */ call_order_id_type, \ + /* 14 */ custom_id_type, \ + /* 15 */ proposal_id_type, \ + /* 16 */ withdraw_permission_id_type, \ + /* 17 */ vesting_balance_id_type, \ + /* 18 */ worker_id_type, \ + /* 19 */ balance_id_type, \ + /* 20 */ flat_set, \ + /* 21 */ flat_set, \ + /* 22 */ flat_set, \ + /* 23 */ flat_set, \ + /* 24 */ flat_set, \ + /* 25 */ flat_set, \ + /* 26 */ flat_set, \ + /* 27 */ flat_set, \ + /* 28 */ flat_set, \ + /* 29 */ flat_set, \ + /* 30 */ flat_set, \ + /* 31 */ flat_set, \ + /* 32 */ flat_set, \ + /* 33 */ flat_set, \ + /* 34 */ flat_set, \ + /* 35 */ flat_set, \ + /* 36 */ flat_set, \ + /* 37 */ flat_set, \ + /* 38 */ flat_set, \ + /* 39 */ vector, \ + /* 40 */ vector>, \ + /* 41 */ variant_assert_argument_type + + using argument_type = fc::static_variant; + + unsigned_int member_index; + unsigned_int restriction_type; + argument_type argument; + + extensions_type extensions; + + restriction() = default; + restriction(const unsigned_int& member_index, function_type type, const argument_type& argument) + : member_index(member_index), restriction_type(type), argument(argument) {} + + static size_t restriction_count(const vector& restrictions); + size_t restriction_count() const; +}; + +} } // graphene::protocol + +FC_REFLECT_ENUM(graphene::protocol::restriction::function_type, + (func_eq) + (func_ne) + (func_lt) + (func_le) + (func_gt) + (func_ge) + (func_in) + (func_not_in) + (func_has_all) + (func_has_none) + (func_attr) + (func_logical_or) + (func_variant_assert) + (FUNCTION_TYPE_COUNT)) + +FC_REFLECT(graphene::protocol::restriction, + (member_index) + (restriction_type) + (argument) + (extensions)) + + diff --git a/libraries/protocol/include/graphene/protocol/restriction_predicate.hpp b/libraries/protocol/include/graphene/protocol/restriction_predicate.hpp new file mode 100644 index 0000000000..f492aa4ffa --- /dev/null +++ b/libraries/protocol/include/graphene/protocol/restriction_predicate.hpp @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2019 Contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#pragma once + +#include +#include + +#include + +namespace graphene { namespace protocol { + +/// A type describing the result of a restriction predicate +struct predicate_result { + /// Whether or not the operation complied with the restrictions or not + bool success = false; + + /// Enumeration of the general reasons a predicate may reject + enum rejection_reason { + predicate_was_false, + null_optional, + incorrect_variant_type + }; + + /// An indicator of what rejection occurred at a particular restriction -- either an index to a sub-restriction, a + /// list of rejection results from the branches of a logical OR, or the immediate reason for rejection + using rejection_indicator = static_variant, rejection_reason>; + /// Failure indicators, ordered from the outermost restriction to the innermost (the location of the rejection) + vector rejection_path; + + static predicate_result Rejection(rejection_reason reason) { return {false, {reason}}; } + static predicate_result Rejection(vector branches) { return {false, {std::move(branches)}}; } + static predicate_result Success() { return {true, {}}; } + + operator bool() const { return success; } + + /// Reverse the order of the rejection path. Returns a reference to this object + predicate_result& reverse_path(); +}; + +/// A restriction predicate is a function accepting an operation and returning a predicate_result +using restriction_predicate_function = std::function; + +/** + * @brief get_restriction_predicate Get a predicate function for the supplied restriction + * @param rs The restrictions to evaluate operations against + * @param op_type The tag specifying which operation type the restrictions apply to + * @return A predicate function which evaluates an operation to determine whether it complies with the restriction + */ +restriction_predicate_function get_restriction_predicate(vector rs, operation::tag_type op_type); + +} } // namespace graphene::protocol + +FC_REFLECT_ENUM(graphene::protocol::predicate_result::rejection_reason, + (predicate_was_false)(null_optional)(incorrect_variant_type)) +FC_REFLECT_TYPENAME(graphene::protocol::predicate_result::rejection_indicator) +FC_REFLECT(graphene::protocol::predicate_result, (success)(rejection_path)) diff --git a/libraries/protocol/include/graphene/protocol/transaction.hpp b/libraries/protocol/include/graphene/protocol/transaction.hpp index 505c3bebef..c41049de97 100644 --- a/libraries/protocol/include/graphene/protocol/transaction.hpp +++ b/libraries/protocol/include/graphene/protocol/transaction.hpp @@ -25,6 +25,12 @@ #include namespace graphene { namespace protocol { + struct predicate_result; + + using rejected_predicate = static_variant; + using rejected_predicate_map = map; + using custom_authority_lookup = std::function(account_id_type, const operation&, + rejected_predicate_map*)>; /** * @defgroup transactions Transactions @@ -161,6 +167,7 @@ namespace graphene { namespace protocol { * @param chain_id the ID of a block chain * @param get_active callback function to retrieve active authorities of a given account * @param get_owner callback function to retrieve owner authorities of a given account + * @param get_custom callback function to retrieve viable custom authorities for a given account and operation * @param allow_non_immediate_owner whether to allow owner authority of non-immediately * required accounts to authorize operations in the transaction * @param ignore_custom_operation_required_auths See issue #210; whether to ignore the @@ -172,6 +179,7 @@ namespace graphene { namespace protocol { const chain_id_type& chain_id, const std::function& get_active, const std::function& get_owner, + const custom_authority_lookup& get_custom, bool allow_non_immediate_owner, bool ignore_custom_operation_required_auths, uint32_t max_recursion = GRAPHENE_MAX_SIG_CHECK_DEPTH )const; @@ -187,6 +195,7 @@ namespace graphene { namespace protocol { const flat_set& available_keys, const std::function& get_active, const std::function& get_owner, + const custom_authority_lookup& get_custom, bool allow_non_immediate_owner, bool ignore_custom_operation_required_auths, uint32_t max_recursion = GRAPHENE_MAX_SIG_CHECK_DEPTH) const; @@ -258,6 +267,7 @@ namespace graphene { namespace protocol { void verify_authority( const vector& ops, const flat_set& sigs, const std::function& get_active, const std::function& get_owner, + const custom_authority_lookup& get_custom, bool allow_non_immediate_owner, bool ignore_custom_operation_required_auths, uint32_t max_recursion = GRAPHENE_MAX_SIG_CHECK_DEPTH, diff --git a/libraries/protocol/include/graphene/protocol/types.hpp b/libraries/protocol/include/graphene/protocol/types.hpp index b861efc78a..2134f3b772 100644 --- a/libraries/protocol/include/graphene/protocol/types.hpp +++ b/libraries/protocol/include/graphene/protocol/types.hpp @@ -239,7 +239,8 @@ GRAPHENE_DEFINE_IDS(protocol, protocol_ids, /*protocol objects are not prefixed* (vesting_balance) (worker) (balance) - (htlc)) + (htlc) + (custom_authority)) FC_REFLECT(graphene::protocol::public_key_type, (key_data)) FC_REFLECT(graphene::protocol::public_key_type::binary_key, (data)(check)) diff --git a/libraries/protocol/restriction.cpp b/libraries/protocol/restriction.cpp new file mode 100644 index 0000000000..908c5bea7b --- /dev/null +++ b/libraries/protocol/restriction.cpp @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2019 Contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include + +namespace graphene { namespace protocol { + +struct adder { + size_t sum = 0; + void operator()(const restriction& r) { sum += r.restriction_count(); } + void operator()(const vector& r) { sum += std::for_each(r.begin(), r.end(), adder()).sum; } +}; + +size_t restriction::restriction_count(const vector& restrictions) { + return std::for_each(restrictions.begin(), restrictions.end(), adder()).sum; +} + +size_t restriction::restriction_count() const { + if (argument.is_type>()) { + const vector& rs = argument.get>(); + return 1 + std::for_each(rs.begin(), rs.end(), adder()).sum; + } else if (argument.is_type>>()) { + const vector>& rs = argument.get>>(); + return 1 + std::for_each(rs.begin(), rs.end(), adder()).sum; + } else if (argument.is_type()) { + const variant_assert_argument_type& arg = argument.get(); + return 1 + std::for_each(arg.second.begin(), arg.second.end(), adder()).sum; + } + return 1; +} + +} } // namespace graphene::protocol diff --git a/libraries/protocol/transaction.cpp b/libraries/protocol/transaction.cpp index 153bed7ea5..adad4ad910 100644 --- a/libraries/protocol/transaction.cpp +++ b/libraries/protocol/transaction.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include @@ -268,32 +269,55 @@ struct sign_state void verify_authority( const vector& ops, const flat_set& sigs, const std::function& get_active, const std::function& get_owner, + const custom_authority_lookup& get_custom, bool allow_non_immediate_owner, bool ignore_custom_operation_required_auths, uint32_t max_recursion_depth, bool allow_committee, const flat_set& active_aprovals, const flat_set& owner_approvals ) -{ try { +{ + rejected_predicate_map rejected_custom_auths; + try { flat_set required_active; flat_set required_owner; vector other; + sign_state s( sigs, get_active, get_owner, allow_non_immediate_owner, max_recursion_depth ); + for( auto& id : active_aprovals ) + s.approved_by.insert( id ); + for( auto& id : owner_approvals ) + s.approved_by.insert( id ); + + auto approved_by_custom_authority = [&s, &rejected_custom_auths, get_custom = std::move(get_custom)]( + account_id_type account, + operation op ) mutable { + auto viable_custom_auths = get_custom( account, op, &rejected_custom_auths ); + for( const auto& auth : viable_custom_auths ) + if( s.check_authority( &auth ) ) return true; + return false; + }; + for( const auto& op : ops ) { - operation_get_required_authorities( op, required_active, required_owner, other, + flat_set operation_required_active; + operation_get_required_authorities( op, operation_required_active, required_owner, other, ignore_custom_operation_required_auths ); + + auto itr = operation_required_active.begin(); + while ( itr != operation_required_active.end() ) { + if ( approved_by_custom_authority( *itr, op ) ) + itr = operation_required_active.erase( itr ); + else + ++itr; + } + + required_active.insert( operation_required_active.begin(), operation_required_active.end() ); } if( !allow_committee ) GRAPHENE_ASSERT( required_active.find(GRAPHENE_COMMITTEE_ACCOUNT) == required_active.end(), invalid_committee_approval, "Committee account may only propose transactions" ); - sign_state s( sigs, get_active, get_owner, allow_non_immediate_owner, max_recursion_depth ); - for( auto& id : active_aprovals ) - s.approved_by.insert( id ); - for( auto& id : owner_approvals ) - s.approved_by.insert( id ); - for( const auto& auth : other ) { GRAPHENE_ASSERT( s.check_authority(&auth), tx_missing_other_auth, "Missing Authority", ("auth",auth)("sigs",sigs) ); @@ -320,7 +344,7 @@ void verify_authority( const vector& ops, const flat_set& signed_transaction::get_signature_keys( const chain_id_type& chain_id )const @@ -379,6 +403,7 @@ set signed_transaction::minimize_required_signatures( const flat_set& available_keys, const std::function& get_active, const std::function& get_owner, + const custom_authority_lookup &get_custom, bool allow_non_immediate_owner, bool ignore_custom_operation_required_auths, uint32_t max_recursion )const @@ -392,7 +417,7 @@ set signed_transaction::minimize_required_signatures( result.erase( k ); try { - graphene::protocol::verify_authority( operations, result, get_active, get_owner, + graphene::protocol::verify_authority( operations, result, get_active, get_owner, get_custom, allow_non_immediate_owner,ignore_custom_operation_required_auths, max_recursion ); continue; // element stays erased if verify_authority is ok @@ -438,13 +463,14 @@ const flat_set& precomputable_transaction::get_signature_keys( void signed_transaction::verify_authority( const chain_id_type& chain_id, const std::function& get_active, const std::function& get_owner, + const custom_authority_lookup& get_custom, bool allow_non_immediate_owner, bool ignore_custom_operation_required_auths, uint32_t max_recursion )const { try { graphene::protocol::verify_authority( operations, get_signature_keys( chain_id ), get_active, get_owner, - allow_non_immediate_owner, ignore_custom_operation_required_auths, - max_recursion ); + get_custom, allow_non_immediate_owner, + ignore_custom_operation_required_auths, max_recursion ); } FC_CAPTURE_AND_RETHROW( (*this) ) } } } // graphene::protocol diff --git a/programs/build_helpers/build_and_test b/programs/build_helpers/build_and_test index 062f4d1883..5768a4d69a 100755 --- a/programs/build_helpers/build_and_test +++ b/programs/build_helpers/build_and_test @@ -4,12 +4,12 @@ set -e programs/build_helpers/buildstep -s 3500 ccache -s programs/build_helpers/buildstep Prepare 1 "sed -i '/tests/d' libraries/fc/CMakeLists.txt" -programs/build_helpers/buildstep cmake 5 "cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_C_FLAGS=--coverage -DCMAKE_CXX_FLAGS=--coverage -DBoost_USE_STATIC_LIBS=OFF -DCMAKE_CXX_OUTPUT_EXTENSION_REPLACE=ON ." +programs/build_helpers/buildstep cmake 5 "cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_C_FLAGS=--coverage -DCMAKE_CXX_FLAGS=--coverage -DCMAKE_CXX_FLAGS_DEBUG=-DTRAVIS_BUILD -DBoost_USE_STATIC_LIBS=OFF -DCMAKE_CXX_OUTPUT_EXTENSION_REPLACE=ON ." programs/build_helpers/buildstep make.everything 2400 "programs/build_helpers/make_with_sonar bw-output -j 2 witness_node chain_test cli_test" set -o pipefail programs/build_helpers/buildstep run.chain_test 240 "libraries/fc/tests/run-parallel-tests.sh tests/chain_test" programs/build_helpers/buildstep run.cli_test 60 "libraries/fc/tests/run-parallel-tests.sh tests/cli_test" -programs/build_helpers/buildstep prepare.sonar 20 "find libraries/[acdenptuw]*/CMakeFiles/*.dir programs/[cdgjsw]*/CMakeFiles/*.dir -type d -print | while read d; do gcov -o \"\$d\" \"\${d/CMakeFiles*.dir//}\"/*.cpp; done >/dev/null; programs/build_helpers/set_sonar_branch sonar-project.properties" || true +programs/build_helpers/buildstep prepare.sonar 20 "find libraries/[acdenptuw]*/CMakeFiles/*.dir programs/[cdgjsw]*/CMakeFiles/*.dir -name graphene_protocol_custom_auths.dir -prune -o -name custom_authorities -prune -o -type d -print | while read d; do gcov -o \"\$d\" \"\${d/CMakeFiles*.dir//}\"/*.cpp; done >/dev/null; programs/build_helpers/set_sonar_branch sonar-project.properties" || true programs/build_helpers/buildstep run.sonar 1200 "which sonar-scanner && sonar-scanner" || true programs/build_helpers/buildstep end 0 ccache -s diff --git a/programs/build_helpers/build_protocol b/programs/build_helpers/build_protocol index 9eb783cf37..1bac5cf225 100755 --- a/programs/build_helpers/build_protocol +++ b/programs/build_helpers/build_protocol @@ -4,8 +4,9 @@ set -e programs/build_helpers/buildstep -s 3500 ccache -s programs/build_helpers/buildstep Prepare 1 "sed -i '/tests/d' libraries/fc/CMakeLists.txt" -programs/build_helpers/buildstep cmake 5 "cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_C_FLAGS=--coverage -DCMAKE_CXX_FLAGS=--coverage -DBoost_USE_STATIC_LIBS=OFF -DCMAKE_CXX_OUTPUT_EXTENSION_REPLACE=ON ." +programs/build_helpers/buildstep cmake 5 "cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_C_FLAGS=--coverage -DCMAKE_CXX_FLAGS=--coverage -DCMAKE_CXX_FLAGS_DEBUG=-DTRAVIS_BUILD -DBoost_USE_STATIC_LIBS=OFF -DCMAKE_CXX_OUTPUT_EXTENSION_REPLACE=ON ." programs/build_helpers/buildstep make.fc 200 "make -j 2 fc" -programs/build_helpers/buildstep make.custom_auths 1000 "make -j 2 graphene_protocol" +programs/build_helpers/buildstep make.custom_auths 1300 "make -j 1 graphene_protocol_custom_auths" +programs/build_helpers/buildstep make.protocol 400 "make -j 2 graphene_protocol" programs/build_helpers/buildstep end 0 ccache -s diff --git a/tests/common/database_fixture.cpp b/tests/common/database_fixture.cpp index c8bf3517d9..56d35c8c77 100644 --- a/tests/common/database_fixture.cpp +++ b/tests/common/database_fixture.cpp @@ -41,6 +41,7 @@ #include #include #include +#include #include @@ -1452,14 +1453,15 @@ void database_fixture::set_htlc_committee_parameters() std::shared_ptr new_fee_schedule = std::make_shared(); new_fee_schedule->scale = GRAPHENE_100_PERCENT; // replace the old with the new - flat_map params_map = get_htlc_fee_parameters(); + flat_map htlc_fees = get_htlc_fee_parameters(); for(auto param : existing_fee_schedule.parameters) { - auto itr = params_map.find(param.which()); - if (itr == params_map.end()) - new_fee_schedule->parameters.insert(param); - else - { + auto itr = htlc_fees.find(param.which()); + if (itr == htlc_fees.end()) { + // Only define fees for operations which are already forked in! + if (hardfork_visitor(db.head_block_time()).visit(param.which())) + new_fee_schedule->parameters.insert(param); + } else { new_fee_schedule->parameters.insert( (*itr).second); } } diff --git a/tests/performance/market_fee_sharing_tests.cpp b/tests/performance/market_fee_sharing_tests.cpp index 5c431595ea..0140f837e7 100644 --- a/tests/performance/market_fee_sharing_tests.cpp +++ b/tests/performance/market_fee_sharing_tests.cpp @@ -25,6 +25,7 @@ #include #include +#include #include "../common/database_fixture.hpp" using namespace graphene::chain; diff --git a/tests/tests/authority_tests.cpp b/tests/tests/authority_tests.cpp index c5e4031fe3..8906ba0dd8 100644 --- a/tests/tests/authority_tests.cpp +++ b/tests/tests/authority_tests.cpp @@ -33,6 +33,8 @@ #include #include +#include + #include #include @@ -43,6 +45,11 @@ using namespace graphene::chain::test; BOOST_FIXTURE_TEST_SUITE( authority_tests, database_fixture ) +auto make_get_custom(const database& db) { + return [&db](account_id_type id, const operation& op, rejected_predicate_map* rejects) { + return db.get_viable_custom_authorities(id, op, rejects); }; +} + BOOST_AUTO_TEST_CASE( simple_single_signature ) { try { try { @@ -1324,10 +1331,13 @@ BOOST_FIXTURE_TEST_CASE( nonminimal_sig_test, database_fixture ) ) -> bool { //wdump( (tx)(available_keys) ); + auto get_custom = make_get_custom(db); set result_set = tx.minimize_required_signatures(db.get_chain_id(), available_keys, - get_active, get_owner, false, false); + get_active, get_owner, get_custom, + false, false); set result_set2 = tx.minimize_required_signatures(db.get_chain_id(), available_keys, - get_active, get_owner, true, false); + get_active, get_owner, get_custom, + true, false); //wdump( (result_set)(result_set2)(ref_set) ); return result_set == ref_set && result_set2 == ref_set; } ; @@ -1346,11 +1356,13 @@ BOOST_FIXTURE_TEST_CASE( nonminimal_sig_test, database_fixture ) BOOST_CHECK( chk( tx, { alice_public_key, bob_public_key }, { alice_public_key, bob_public_key } ) ); BOOST_CHECK( chk_min( tx, { alice_public_key, bob_public_key }, { alice_public_key } ) ); - GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, false, false ), fc::exception ); - GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, true, false ), fc::exception ); + GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, make_get_custom(db), + false, false ), fc::exception ); + GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, make_get_custom(db), + true, false ), fc::exception ); sign( tx, alice_private_key ); - tx.verify_authority( db.get_chain_id(), get_active, get_owner, false, false ); - tx.verify_authority( db.get_chain_id(), get_active, get_owner, true, false ); + tx.verify_authority( db.get_chain_id(), get_active, get_owner, make_get_custom(db), false, false ); + tx.verify_authority( db.get_chain_id(), get_active, get_owner, make_get_custom(db), true, false ); } catch(fc::exception& e) { @@ -1549,87 +1561,96 @@ BOOST_FIXTURE_TEST_CASE( parent_owner_test, database_fixture ) BOOST_CHECK( chk( tx, true, { gavin_active_pub, gavin_owner_pub }, { gavin_active_pub } ) ); sign( tx, alice_owner_key ); - GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, false, false ), fc::exception ); + GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, make_get_custom(db), + false, false ), fc::exception ); GRAPHENE_REQUIRE_THROW( PUSH_TX( db, tx, database::skip_transaction_dupe_check ), fc::exception ); - tx.verify_authority( db.get_chain_id(), get_active, get_owner, true, false ); + tx.verify_authority( db.get_chain_id(), get_active, get_owner, make_get_custom(db), + true, false ); tx.clear_signatures(); sign( tx, alice_active_key ); - tx.verify_authority( db.get_chain_id(), get_active, get_owner, false, false ); + tx.verify_authority( db.get_chain_id(), get_active, get_owner, make_get_custom(db), false, false ); PUSH_TX( db, tx, database::skip_transaction_dupe_check ); - tx.verify_authority( db.get_chain_id(), get_active, get_owner, true, false ); + tx.verify_authority( db.get_chain_id(), get_active, get_owner, make_get_custom(db), true, false ); tx.clear_signatures(); sign( tx, bob_owner_key ); - tx.verify_authority( db.get_chain_id(), get_active, get_owner, false, false ); + tx.verify_authority( db.get_chain_id(), get_active, get_owner, make_get_custom(db), false, false ); PUSH_TX( db, tx, database::skip_transaction_dupe_check ); - tx.verify_authority( db.get_chain_id(), get_active, get_owner, true, false ); + tx.verify_authority( db.get_chain_id(), get_active, get_owner, make_get_custom(db), true, false ); tx.clear_signatures(); sign( tx, bob_active_key ); - tx.verify_authority( db.get_chain_id(), get_active, get_owner, false, false ); + tx.verify_authority( db.get_chain_id(), get_active, get_owner, make_get_custom(db), false, false ); PUSH_TX( db, tx, database::skip_transaction_dupe_check ); - tx.verify_authority( db.get_chain_id(), get_active, get_owner, true, false ); + tx.verify_authority( db.get_chain_id(), get_active, get_owner, make_get_custom(db), true, false ); tx.clear_signatures(); sign( tx, cindy_owner_key ); - GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, false, false ), fc::exception ); + GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, make_get_custom(db), + false, false ), fc::exception ); GRAPHENE_REQUIRE_THROW( PUSH_TX( db, tx, database::skip_transaction_dupe_check ), fc::exception ); - tx.verify_authority( db.get_chain_id(), get_active, get_owner, true, false ); + tx.verify_authority( db.get_chain_id(), get_active, get_owner, make_get_custom(db), true, false ); tx.clear_signatures(); sign( tx, cindy_active_key ); - GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, false, false ), fc::exception ); + GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, make_get_custom(db), + false, false ), fc::exception ); GRAPHENE_REQUIRE_THROW( PUSH_TX( db, tx, database::skip_transaction_dupe_check ), fc::exception ); - tx.verify_authority( db.get_chain_id(), get_active, get_owner, true, false ); + tx.verify_authority( db.get_chain_id(), get_active, get_owner, make_get_custom(db), true, false ); tx.clear_signatures(); sign( tx, daisy_owner_key ); - GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, false, false ), fc::exception ); + GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, make_get_custom(db), + false, false ), fc::exception ); GRAPHENE_REQUIRE_THROW( PUSH_TX( db, tx, database::skip_transaction_dupe_check ), fc::exception ); - tx.verify_authority( db.get_chain_id(), get_active, get_owner, true, false ); + tx.verify_authority( db.get_chain_id(), get_active, get_owner, make_get_custom(db), true, false ); tx.clear_signatures(); sign( tx, daisy_active_key ); - tx.verify_authority( db.get_chain_id(), get_active, get_owner, false, false ); + tx.verify_authority( db.get_chain_id(), get_active, get_owner, make_get_custom(db), false, false ); PUSH_TX( db, tx, database::skip_transaction_dupe_check ); - tx.verify_authority( db.get_chain_id(), get_active, get_owner, true, false ); + tx.verify_authority( db.get_chain_id(), get_active, get_owner, make_get_custom(db), true, false ); tx.clear_signatures(); sign( tx, edwin_owner_key ); - GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, false, false ), fc::exception ); + GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, make_get_custom(db), + false, false ), fc::exception ); GRAPHENE_REQUIRE_THROW( PUSH_TX( db, tx, database::skip_transaction_dupe_check ), fc::exception ); - tx.verify_authority( db.get_chain_id(), get_active, get_owner, true, false ); + tx.verify_authority( db.get_chain_id(), get_active, get_owner, make_get_custom(db), true, false ); tx.clear_signatures(); sign( tx, edwin_active_key ); - tx.verify_authority( db.get_chain_id(), get_active, get_owner, false, false ); + tx.verify_authority( db.get_chain_id(), get_active, get_owner, make_get_custom(db), false, false ); PUSH_TX( db, tx, database::skip_transaction_dupe_check ); - tx.verify_authority( db.get_chain_id(), get_active, get_owner, true, false ); + tx.verify_authority( db.get_chain_id(), get_active, get_owner, make_get_custom(db), true, false ); tx.clear_signatures(); sign( tx, frank_owner_key ); - GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, false, false ), fc::exception ); + GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, make_get_custom(db), + false, false ), fc::exception ); GRAPHENE_REQUIRE_THROW( PUSH_TX( db, tx, database::skip_transaction_dupe_check ), fc::exception ); - tx.verify_authority( db.get_chain_id(), get_active, get_owner, true, false ); + tx.verify_authority( db.get_chain_id(), get_active, get_owner, make_get_custom(db), true, false ); tx.clear_signatures(); sign( tx, frank_active_key ); - GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, false, false ), fc::exception ); + GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, make_get_custom(db), + false, false ), fc::exception ); GRAPHENE_REQUIRE_THROW( PUSH_TX( db, tx, database::skip_transaction_dupe_check ), fc::exception ); - tx.verify_authority( db.get_chain_id(), get_active, get_owner, true, false ); + tx.verify_authority( db.get_chain_id(), get_active, get_owner, make_get_custom(db), true, false ); tx.clear_signatures(); sign( tx, gavin_owner_key ); - GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, false, false ), fc::exception ); + GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, make_get_custom(db), + false, false ), fc::exception ); GRAPHENE_REQUIRE_THROW( PUSH_TX( db, tx, database::skip_transaction_dupe_check ), fc::exception ); - tx.verify_authority( db.get_chain_id(), get_active, get_owner, true, false ); + tx.verify_authority( db.get_chain_id(), get_active, get_owner, make_get_custom(db), true, false ); tx.clear_signatures(); sign( tx, gavin_active_key ); - tx.verify_authority( db.get_chain_id(), get_active, get_owner, false, false ); + tx.verify_authority( db.get_chain_id(), get_active, get_owner, make_get_custom(db), false, false ); PUSH_TX( db, tx, database::skip_transaction_dupe_check ); - tx.verify_authority( db.get_chain_id(), get_active, get_owner, true, false ); + tx.verify_authority( db.get_chain_id(), get_active, get_owner, make_get_custom(db), true, false ); tx.clear_signatures(); // proposal tests @@ -2036,29 +2057,33 @@ BOOST_FIXTURE_TEST_CASE( missing_owner_auth_test, database_fixture ) tx.operations.push_back( op ); // not signed, should throw tx_missing_owner_auth - GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, false, false ), + GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, make_get_custom(db), + false, false ), graphene::chain::tx_missing_owner_auth ); - GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, true, false ), + GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, make_get_custom(db), + true, false ), graphene::chain::tx_missing_owner_auth ); // signed with alice's active key, should throw tx_missing_owner_auth sign( tx, alice_active_key ); - GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, false, false ), + GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, make_get_custom(db), + false, false ), graphene::chain::tx_missing_owner_auth ); - GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, true, false ), + GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, make_get_custom(db), + true, false ), graphene::chain::tx_missing_owner_auth ); // signed with alice's owner key, should not throw tx.clear_signatures(); sign( tx, alice_owner_key ); - tx.verify_authority( db.get_chain_id(), get_active, get_owner, false, false ); - tx.verify_authority( db.get_chain_id(), get_active, get_owner, true, false ); + tx.verify_authority( db.get_chain_id(), get_active, get_owner, make_get_custom(db), false, false ); + tx.verify_authority( db.get_chain_id(), get_active, get_owner, make_get_custom(db), true, false ); // signed with both alice's owner key and active key, // it does not throw due to https://github.com/bitshares/bitshares-core/issues/580 sign( tx, alice_active_key ); - tx.verify_authority( db.get_chain_id(), get_active, get_owner, false, false ); - tx.verify_authority( db.get_chain_id(), get_active, get_owner, true, false ); + tx.verify_authority( db.get_chain_id(), get_active, get_owner, make_get_custom(db), false, false ); + tx.verify_authority( db.get_chain_id(), get_active, get_owner, make_get_custom(db), true, false ); // creating a transaction that needs active permission tx.clear(); @@ -2067,29 +2092,32 @@ BOOST_FIXTURE_TEST_CASE( missing_owner_auth_test, database_fixture ) tx.operations.push_back( op ); // not signed, should throw tx_missing_active_auth - GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, false, false ), + GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, make_get_custom(db), + false, false ), graphene::chain::tx_missing_active_auth ); - GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, true, false ), + GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, make_get_custom(db), + true, false ), graphene::chain::tx_missing_active_auth ); // signed with alice's active key, should not throw sign( tx, alice_active_key ); - tx.verify_authority( db.get_chain_id(), get_active, get_owner, false, false ); - tx.verify_authority( db.get_chain_id(), get_active, get_owner, true, false ); + tx.verify_authority( db.get_chain_id(), get_active, get_owner, make_get_custom(db), false, false ); + tx.verify_authority( db.get_chain_id(), get_active, get_owner, make_get_custom(db), true, false ); // signed with alice's owner key, should not throw tx.clear_signatures(); sign( tx, alice_owner_key ); - tx.verify_authority( db.get_chain_id(), get_active, get_owner, false, false ); - tx.verify_authority( db.get_chain_id(), get_active, get_owner, true, false ); + tx.verify_authority( db.get_chain_id(), get_active, get_owner, make_get_custom(db), false, false ); + tx.verify_authority( db.get_chain_id(), get_active, get_owner, make_get_custom(db), true, false ); // signed with both alice's owner key and active key, should throw tx_irrelevant_sig sign( tx, alice_active_key ); - GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, false, false ), + GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, make_get_custom(db), + false, false ), graphene::chain::tx_irrelevant_sig ); - GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, true, false ), + GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, make_get_custom(db), + true, false ), graphene::chain::tx_irrelevant_sig ); - } catch(fc::exception& e) { diff --git a/tests/tests/custom_authority_tests.cpp b/tests/tests/custom_authority_tests.cpp new file mode 100644 index 0000000000..d08551c472 --- /dev/null +++ b/tests/tests/custom_authority_tests.cpp @@ -0,0 +1,1745 @@ +/* + * Copyright (c) 2019 Contributors + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../common/database_fixture.hpp" + +using namespace graphene::chain; +using namespace graphene::chain::test; + +namespace graphene { namespace protocol { +bool operator==(const restriction& a, const restriction& b) { + if (std::tie(a.member_index, a.restriction_type) != std::tie(b.member_index, b.restriction_type)) + return false; + if (a.argument.is_type()) + return b.argument.is_type(); + using Value_Argument = static_variant>; + return Value_Argument::import_from(a.argument) == Value_Argument::import_from(b.argument); +} +} } + + +BOOST_FIXTURE_TEST_SUITE(custom_authority_tests, database_fixture) + +#define FUNC(TYPE) BOOST_PP_CAT(restriction::func_, TYPE) + +template +unsigned_int member_index(string name) { + unsigned_int index; + fc::typelist::runtime::for_each(typename fc::reflector::native_members(), [&name, &index](auto t) mutable { + if (name == decltype(t)::type::get_name()) + index = decltype(t)::type::index; + }); + return index; +} + +template +void expect_exception_string(const string& s, Expression e) { + try{ + e(); + FC_THROW_EXCEPTION(fc::assert_exception, "Expected exception with string ${s}, but no exception thrown", + ("s", s)); + } catch (const fc::exception& e) { + FC_ASSERT(e.to_detail_string().find(s) != string::npos, "Did not find expected string ${s} in exception: ${e}", + ("s", s)("e", e)); + } +} +#define EXPECT_EXCEPTION_STRING(S, E) \ + BOOST_TEST_CHECKPOINT("Expect exception containing string: " S); \ + expect_exception_string(S, E) + +BOOST_AUTO_TEST_CASE(restriction_predicate_tests) { try { + using namespace graphene::protocol; + ////// + // Create a restriction that authorizes transfers only made to Account ID 12 + ////// + vector restrictions; + auto to_index = member_index("to"); + restrictions.emplace_back(to_index, FUNC(eq), account_id_type(12)); + + ////// + // Create an operation that transfers to Account ID 0 + // This should violate the restriction + ////// + transfer_operation transfer; + // Check that the proposed operation to account ID 0 is not compliant with the restriction to account ID 12 + BOOST_CHECK(get_restriction_predicate(restrictions, operation::tag::value)(transfer) == false); + // Inspect the reasons why the proposed operation was rejected + // The rejection path will reference portions of the restrictions + //[ + // { + // "member_index": 2, + // "restriction_type": 0, + // "argument": [ + // 7, + // "1.2.12" + // ], + // "extensions": [] + // } + //] + BOOST_CHECK_EQUAL(restriction::restriction_count(restrictions), 1); + BOOST_CHECK(get_restriction_predicate(restrictions, operation::tag::value)(transfer) + .rejection_path.size() == 2); + // Index 0 (the outer-most) rejection path refers to the first and only restriction + BOOST_CHECK(get_restriction_predicate(restrictions, operation::tag::value)(transfer) + .rejection_path[0].get() == 0); + // Index 1 (the inner-most) rejection path refers to the first and only argument for an account ID of 1.2.12 + BOOST_CHECK(get_restriction_predicate(restrictions, operation::tag::value)(transfer) + .rejection_path[1].get() == predicate_result::predicate_was_false); + + ////// + // Create an operation that transfer to Account ID 12 + // This should satisfy the restriction + ////// + transfer.to = account_id_type(12); + BOOST_CHECK(get_restriction_predicate(restrictions, operation::tag::value)(transfer) == true); + BOOST_CHECK(get_restriction_predicate(restrictions, operation::tag::value)(transfer) + .rejection_path.size() == 0); + + + ////// + // Create an INVALID restriction that references an invalid member index + // (Index 6 is greater than the highest 0-based index of 5) + // of the transfer operation + ////// + restrictions.front() = restriction(fc::typelist::length::native_members>(), + FUNC(eq), account_id_type(12)); + //[ + // { + // "member_index": 6, + // "restriction_type": 0, + // "argument": [ + // 7, + // "1.2.12" + // ], + // "extensions": [] + // } + //] + // + // This restriction should throw an exception related to an invalid member index + // 10 assert_exception: Assert Exception + // r.member_index < typelist::length(): Invalid member index 6 for object graphene::protocol::transfer_operation + // {"I":6,"O":"graphene::protocol::transfer_operation"} + // th_a restriction_predicate.hxx:493 create_field_predicate + BOOST_CHECK_THROW(get_restriction_predicate(restrictions, operation::tag::value), + fc::assert_exception); + + + ////// + // Create an INVALID restriction that compares a transfer operation's account ID type to an asset ID type + ////// + restrictions.front() = restriction(to_index, FUNC(eq), asset_id_type(12)); + //[ + // { + // "member_index": 2, + // "restriction_type": 0, + // "argument": [ + // 8, + // "1.3.12" + // ], + // "extensions": [] + // } + //] + // + // This restriction should throw an exception related to invalid type + // 10 assert_exception: Assert Exception + // Invalid types for predicate + // {} + // th_a restriction_predicate.hxx:147 predicate_invalid + // + // {"fc::get_typename::name()":"graphene::protocol::account_id_type","func":"func_eq","arg":[8,"1.3.12"]} + // th_a restriction_predicate.hxx:476 create_predicate_function + BOOST_CHECK_THROW(get_restriction_predicate(restrictions, operation::tag::value), + fc::assert_exception); + + ////// + // Create a restriction such that the operation fee must be paid with Asset ID 0 + ////// + auto fee_index = member_index("fee"); + auto asset_id_index = member_index("asset_id"); + restrictions.front() = restriction(fee_index, FUNC(attr), + vector{restriction(asset_id_index, FUNC(eq), asset_id_type(0))}); + + ////// + // Check the transfer operation that pays the fee with Asset ID 0 + // This should satisfy the restriction. + ////// + BOOST_CHECK(get_restriction_predicate(restrictions, operation::tag::value)(transfer) == true); + BOOST_CHECK(get_restriction_predicate(restrictions, operation::tag::value)(transfer) + .rejection_path.size() == 0); + + ////// + // Change the restriction such that the operation fee must be paid with Asset ID 1 + ////// + restrictions.front().argument.get>().front().argument = asset_id_type(1); + //[ + // { + // "member_index": 0, + // "restriction_type": 10, + // "argument": [ + // 39, + // [ + // { + // "member_index": 1, + // "restriction_type": 0, + // "argument": [ + // 8, + // "1.3.1" + // ], + // "extensions": [] + // } + // ] + // ], + // "extensions": [] + // } + //] + + BOOST_CHECK_EQUAL(restriction::restriction_count(restrictions), 2); + ////// + // Check the transfer operation that pays the fee with Asset ID 0 against the restriction. + // This should violate the restriction. + ////// + BOOST_CHECK(get_restriction_predicate(restrictions, operation::tag::value)(transfer) == false); + // Inspect the reasons why the proposed operation was rejected + // The rejection path will reference portions of the restrictions + BOOST_CHECK(get_restriction_predicate(restrictions, operation::tag::value)(transfer) + .rejection_path.size() == 3); + // Index 0 (the outer-most) rejection path refers to the first and only restriction + BOOST_CHECK(get_restriction_predicate(restrictions, operation::tag::value)(transfer) + .rejection_path[0].get() == 0); + // Index 1 rejection path refers to the first and only argument of the restriction + BOOST_CHECK(get_restriction_predicate(restrictions, operation::tag::value)(transfer) + .rejection_path[1].get() == 0); + // Index 2 (the inner-most) rejection path refers to the first and only argument + BOOST_CHECK(get_restriction_predicate(restrictions, operation::tag::value)(transfer) + .rejection_path[2].get() == predicate_result::predicate_was_false); + + ////// + // Create a restriction that authorizes transfers only to Account ID 12 + ////// + restrictions.emplace_back(to_index, FUNC(eq), account_id_type(12)); + //[ + // { + // "member_index": 0, + // "restriction_type": 10, + // "argument": [ + // 39, + // [ + // { + // "member_index": 1, + // "restriction_type": 0, + // "argument": [ + // 8, + // "1.3.1" + // ], + // "extensions": [] + // } + // ] + // ], + // "extensions": [] + // }, + // { + // "member_index": 2, + // "restriction_type": 0, + // "argument": [ + // 7, + // "1.2.12" + // ], + // "extensions": [] + // } + //] + BOOST_CHECK_EQUAL(restriction::restriction_count(restrictions), 3); + + ////// + // Create a transfer operation that authorizes transfer to Account ID 12 + // This operation should satisfy the restriction + ////// + transfer.to = account_id_type(12); + transfer.fee.asset_id = asset_id_type(1); + BOOST_CHECK(get_restriction_predicate(restrictions, operation::tag::value)(transfer) == true); + BOOST_CHECK(get_restriction_predicate(restrictions, operation::tag::value)(transfer) + .rejection_path.size() == 0); + + ////// + // Create a transfer operation that transfers to Account ID 10 + // This operation should violate the restriction + ////// + transfer.to = account_id_type(10); + BOOST_CHECK(get_restriction_predicate(restrictions, operation::tag::value)(transfer) == false); + // Inspect the reasons why the proposed operation was rejected + // The rejection path will reference portions of the restrictions + BOOST_CHECK(get_restriction_predicate(restrictions, operation::tag::value)(transfer) + .rejection_path.size() == 2); + // Index 0 (the outer-most) rejection path refers to the first and only restriction + BOOST_CHECK(get_restriction_predicate(restrictions, operation::tag::value)(transfer) + .rejection_path[0].get() == 1); + // Index 1 (the inner-most) rejection path refers to the first and only argument + BOOST_CHECK(get_restriction_predicate(restrictions, operation::tag::value)(transfer) + .rejection_path[1].get() == predicate_result::predicate_was_false); + + ////// + // Create a restriction where the ext.owner_special_authority field is unspecified + ////// + restrictions.clear(); + auto extensions_index = member_index("extensions"); + auto authority_index = member_index("owner_special_authority"); + restrictions.emplace_back(extensions_index, FUNC(attr), + vector{restriction(authority_index, FUNC(eq), void_t())}); + //[ + // { + // "member_index": 5, + // "restriction_type": 10, + // "argument": [ + // 39, + // [ + // { + // "member_index": 1, + // "restriction_type": 0, + // "argument": [ + // 0, + // {} + // ], + // "extensions": [] + // } + // ] + // ], + // "extensions": [] + // } + //] + BOOST_CHECK_EQUAL(restriction::restriction_count(restrictions), 2); + auto predicate = get_restriction_predicate(restrictions, operation::tag::value); + + ////// + // Create an account update operation without any owner_special_authority extension + ////// + account_update_operation update; + // The transfer operation should violate the restriction because it does not have an ext field + BOOST_CHECK_THROW(predicate(transfer), fc::assert_exception); + // The update operation should satisfy the restriction + BOOST_CHECK(predicate(update) == true); + BOOST_CHECK(predicate(update).rejection_path.size() == 0); + + ////// + // Change the update operation to include an owner_special_authority + // This should violate the restriction + ////// + update.extensions.value.owner_special_authority = special_authority(); + BOOST_CHECK(predicate(update) == false); + BOOST_CHECK_EQUAL(predicate(update).rejection_path.size(), 3); + // Index 0 (the outer-most) rejection path refers to the first and only restriction + BOOST_CHECK(predicate(update).rejection_path[0].get() == 0); + // Index 1 rejection path refers to the first and only argument of the restriction + BOOST_CHECK(predicate(update).rejection_path[1].get() == 0); + // Index 2 (the inner-most) rejection path refers to the first and only argument + BOOST_CHECK(predicate(update).rejection_path[2].get() == + predicate_result::predicate_was_false); + + ////// + // Change the restriction where the ext.owner_special_authority field must be specified + ////// + restrictions.front().argument.get>().front().restriction_type = FUNC(ne); + //[ + // { + // "member_index": 5, + // "restriction_type": 10, + // "argument": [ + // 39, + // [ + // { + // "member_index": 1, + // "restriction_type": 1, + // "argument": [ + // 0, + // {} + // ], + // "extensions": [] + // } + // ] + // ], + // "extensions": [] + // } + //] + + ////// + // The update operation should satisfy the new restriction because the ext.owner_special_authority is specified + ////// + predicate = get_restriction_predicate(restrictions, operation::tag::value); + BOOST_CHECK(predicate(update) == true); +} FC_LOG_AND_RETHROW() } + +BOOST_AUTO_TEST_CASE(container_in_not_in_checks) { try { + vector restrictions; + restrictions.emplace_back(member_index("new_feed_producers"), FUNC(in), + flat_set{account_id_type(5), account_id_type(6), account_id_type(7)}); + auto pred = get_restriction_predicate(restrictions, operation::tag::value); + + asset_update_feed_producers_operation op; + BOOST_CHECK(pred(op)); + op.new_feed_producers = {account_id_type(1)}; + BOOST_CHECK(!pred(op)); + op.new_feed_producers = {account_id_type(5)}; + BOOST_CHECK(pred(op)); + op.new_feed_producers = {account_id_type(5), account_id_type(6)}; + BOOST_CHECK(pred(op)); + op.new_feed_producers = {account_id_type(5), account_id_type(6), account_id_type(7)}; + BOOST_CHECK(pred(op)); + op.new_feed_producers = {account_id_type(1), account_id_type(5), account_id_type(6), account_id_type(7)}; + BOOST_CHECK(!pred(op)); + op.new_feed_producers = {account_id_type(5), account_id_type(6), account_id_type(7), account_id_type(8)}; + BOOST_CHECK(!pred(op)); + + restrictions.front().restriction_type = FUNC(not_in); + pred = get_restriction_predicate(restrictions, operation::tag::value); + op.new_feed_producers.clear(); + BOOST_CHECK(pred(op)); + op.new_feed_producers = {account_id_type(1)}; + BOOST_CHECK(pred(op)); + op.new_feed_producers = {account_id_type(5)}; + BOOST_CHECK(!pred(op)); + op.new_feed_producers = {account_id_type(5), account_id_type(6)}; + BOOST_CHECK(!pred(op)); + op.new_feed_producers = {account_id_type(5), account_id_type(6), account_id_type(7)}; + BOOST_CHECK(!pred(op)); + op.new_feed_producers = {account_id_type(1), account_id_type(5), account_id_type(6), account_id_type(7)}; + BOOST_CHECK(!pred(op)); + op.new_feed_producers = {account_id_type(5), account_id_type(6), account_id_type(7), account_id_type(8)}; + BOOST_CHECK(!pred(op)); +} FC_LOG_AND_RETHROW() } + + /** + * Test predicates containing logical ORs + * Test of authorization and revocation of one account (Alice) authorizing multiple other accounts (Bob and Charlie) + * to transfer out of her account by using a single custom active authority with two logical OR branches. + * + * This can alternatively be achieved by using two custom active authority authorizations + * as is done in multiple_transfer_custom_auths + */ + BOOST_AUTO_TEST_CASE(logical_or_transfer_predicate_tests) { + try { + using namespace graphene::protocol; + ////// + // Create a restriction that authorizes transfers only made to Account ID 12 or Account 15 + ////// + auto to_index = member_index("to"); + vector branch1 = vector{restriction(to_index, FUNC(eq), account_id_type(12))}; + vector branch2 = vector{restriction(to_index, FUNC(eq), account_id_type(15))}; + unsigned_int dummy_index = 999; + vector or_restrictions = { + restriction(dummy_index, FUNC(logical_or), vector>{branch1, branch2})}; + //[ + // { + // "member_index": 999, + // "restriction_type": 11, + // "argument": [ + // 40, + // [ + // [ + // { + // "member_index": 2, + // "restriction_type": 0, + // "argument": [ + // 7, + // "1.2.12" + // ], + // "extensions": [] + // } + // ], + // [ + // { + // "member_index": 2, + // "restriction_type": 0, + // "argument": [ + // 7, + // "1.2.15" + // ], + // "extensions": [] + // } + // ] + // ] + // ], + // "extensions": [] + // } + //] + BOOST_CHECK_EQUAL(restriction::restriction_count(or_restrictions), 3); + auto predicate = get_restriction_predicate(or_restrictions, operation::tag::value); + + ////// + // Create an operation that transfers to Account ID 12 + // This should satisfy the restriction because Account ID 12 is authorized to transfer + ////// + transfer_operation transfer_to_12 = transfer_operation(); + transfer_to_12.to = account_id_type(12); + BOOST_CHECK_EQUAL(predicate(transfer_to_12).rejection_path.size(), 0); + + ////// + // Create an operation that transfers to Account ID 15 + // This should satisfy the restriction because Account ID 15 is authorized to transfer + ////// + transfer_operation transfer_to_15 = transfer_operation(); + transfer_to_15.to = account_id_type(15); + BOOST_CHECK(predicate(transfer_to_15) == true); + BOOST_CHECK_EQUAL(predicate(transfer_to_15).rejection_path.size(), 0); + + ////// + // Create an operation that transfers to Account ID 1 + // This should violate the restriction because Account 1 is not authorized to transfer + ////// + transfer_operation transfer_to_1; + transfer_to_1.to = account_id_type(1); + BOOST_CHECK(predicate(transfer_to_1) == false); + BOOST_CHECK_EQUAL(predicate(transfer_to_1).rejection_path.size(), 2); + // Index 0 (the outer-most) rejection path refers to the first and only restriction + BOOST_CHECK(predicate(transfer_to_1).rejection_path[0].get() == 0); + // Index 1 (the inner-most) rejection path refers to the first and only argument: + // the vector of branches each of which are one level deep + vector branch_results = predicate( + transfer_to_1).rejection_path[1].get>(); + unsigned long nbr_branches = branch_results.size(); + BOOST_CHECK_EQUAL(nbr_branches, 2); + for (unsigned long j = 0; j < nbr_branches; ++j) { + predicate_result &result = branch_results.at(j); + BOOST_CHECK_EQUAL(result.success, false); + + BOOST_CHECK_EQUAL(result.rejection_path.size(), 2); + // Index 0 (the outer-most) rejection path refers to the first and only restriction + BOOST_CHECK_EQUAL(result.rejection_path[0].get(), 0); + // Index 1 (the inner-most) rejection path refers to the first and only argument for an account ID: + // either 1.2.12 or 1.2.15 + BOOST_CHECK(result.rejection_path[1].get() == + predicate_result::predicate_was_false); + + } + + } FC_LOG_AND_RETHROW() + } + + +BOOST_AUTO_TEST_CASE(custom_auths) { try { + ////// + // Initialize the test + ////// + generate_blocks(HARDFORK_BSIP_40_TIME); + generate_blocks(5); + db.modify(global_property_id_type()(db), [](global_property_object& gpo) { + gpo.parameters.extensions.value.custom_authority_options = custom_authority_options_type(); + }); + set_expiration(db, trx); + ACTORS((alice)(bob)) + fund(alice, asset(1000*GRAPHENE_BLOCKCHAIN_PRECISION)); + fund(bob, asset(1000*GRAPHENE_BLOCKCHAIN_PRECISION)); + + ////// + // Create a custom authority where Bob is authorized to transfer from Alice's account + // if and only if the transfer amount is less than 100 of Asset ID 0. + // This custom authority is NOT YET published. + ////// + custom_authority_create_operation op; + op.account = alice.get_id(); + op.auth.add_authority(bob.get_id(), 1); + op.auth.weight_threshold = 1; + op.enabled = true; + op.valid_to = db.head_block_time() + 1000; + op.operation_type = operation::tag::value; + auto transfer_amount_index = member_index("amount"); + auto asset_amount_index = member_index("amount"); + auto assed_id_index = member_index("asset_id"); + op.restrictions = {restriction(transfer_amount_index, restriction::func_attr, vector{ + restriction(asset_amount_index, restriction::func_lt, + int64_t(100*GRAPHENE_BLOCKCHAIN_PRECISION)), + restriction(assed_id_index, restriction::func_eq, asset_id_type(0))})}; + //[ + // { + // "member_index": 3, + // "restriction_type": 10, + // "argument": [ + // 39, + // [ + // { + // "member_index": 0, + // "restriction_type": 2, + // "argument": [ + // 2, + // 10000000 + // ], + // "extensions": [] + // }, + // { + // "member_index": 1, + // "restriction_type": 0, + // "argument": [ + // 8, + // "1.3.0" + // ], + // "extensions": [] + // } + // ] + // ], + // "extensions": [] + // } + //] + BOOST_CHECK_EQUAL(restriction::restriction_count(op.restrictions), 3); + + + ////// + // Bob attempts to transfer 99 CORE from Alice's account + // This attempt should fail because it is attempted before the custom authority is published + ////// + transfer_operation top; + top.to = bob.get_id(); + top.from = alice.get_id(); + top.amount.amount = 99 * GRAPHENE_BLOCKCHAIN_PRECISION; + trx.operations = {top}; + sign(trx, bob_private_key); + // No custom auth yet; bob's transfer should reject + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + + ////// + // Alice publishes the custom authority + ////// + trx.clear(); + trx.operations = {op}; + sign(trx, alice_private_key); + PUSH_TX(db, trx); + + custom_authority_id_type auth_id = + db.get_index_type().indices().get().find(alice_id)->id; + + ////// + // Bob attempts to transfer 99 CORE from Alice's account + // This attempt should succeed because it is attempted after the custom authority is published + ////// + trx.clear(); + trx.operations = {top}; + sign(trx, bob_private_key); + PUSH_TX(db, trx); + + ////// + // Bob attempts to transfer 100 CORE from Alice's account + // This attempt should fail because it exceeds the authorized amount + ////// + trx.operations.front().get().amount.amount = 100*GRAPHENE_BLOCKCHAIN_PRECISION; + trx.clear_signatures(); + sign(trx, bob_private_key); + // If bob tries to transfer 100, it rejects because the restriction is strictly less than 100 + EXPECT_EXCEPTION_STRING("\"rejection_path\":[[0,0],[0,0],[2,\"predicate_was_false\"]]", [&] {PUSH_TX(db, trx);}); + + ////// + // Update the custom authority so that Bob is authorized to transfer from Alice's account + // if and only if the transfer amount EXACTLY EQUALS 100 of Asset ID 0. + // This custom authority is NOT YET published. + ////// + op.restrictions.front().argument.get>().front().restriction_type = restriction::func_eq; + custom_authority_update_operation uop; + uop.account = alice.get_id(); + uop.authority_to_update = auth_id; + uop.restrictions_to_remove = {0}; + uop.restrictions_to_add = {op.restrictions.front()}; + trx.clear(); + trx.operations = {uop}; + sign(trx, alice_private_key); + PUSH_TX(db, trx); + + BOOST_CHECK(auth_id(db).get_restrictions() == uop.restrictions_to_add); + + ////// + // Bob attempts to transfer 99 CORE from Alice's account + // This attempt should fail because only transfers of 100 CORE are authorized + ////// + trx.clear(); + trx.operations = {top}; + trx.expiration += 5; + sign(trx, bob_private_key); + // The transfer of 99 should reject because the requirement is for exactly 100 + EXPECT_EXCEPTION_STRING("\"rejection_path\":[[0,0],[0,0],[2,\"predicate_was_false\"]]", [&] {PUSH_TX(db, trx);}); + + ////// + // Bob attempts to transfer 100 CORE from Alice's account + // This attempt should succeed because transfers of exactly 100 CORE are authorized by Alice + ////// + trx.operations.front().get().amount.amount = 100*GRAPHENE_BLOCKCHAIN_PRECISION; + trx.clear_signatures(); + sign(trx, bob_private_key); + PUSH_TX(db, trx); + auto transfer = trx; + + generate_block(); + + ////// + // Bob attempts to transfer 100 CORE from Alice's account AGAIN + // This attempt should succeed because there are no limits to the transfer size nor quantity + // besides the available CORE in Alice's account + ////// + trx.expiration += 5; + trx.clear_signatures(); + sign(trx, bob_private_key); + PUSH_TX(db, trx); + + ////// + // Alice revokes the custom authority for Bob + ////// + custom_authority_delete_operation dop; + dop.account = alice.get_id(); + dop.authority_to_delete = auth_id; + trx.clear(); + trx.operations = {dop}; + sign(trx, alice_private_key); + PUSH_TX(db, trx); + + ////// + // Bob attempts to transfer 100 CORE from Alice's account + // This attempt should fail because it is attempted after the custom authority has been revoked + ////// + transfer.expiration += 10; + transfer.clear_signatures(); + sign(transfer, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, transfer), tx_missing_active_auth); +} FC_LOG_AND_RETHROW() } + + + /** + * Test of authorization and revocation of one account (Alice) authorizing multiple other accounts (Bob and Charlie) + * to transfer out of her account by using two distinct custom active authorities. + * + * This can alternatively be achieved by using a single custom active authority with two logical OR branches + * as is done in logical_or_transfer_predicate_tests + */ + BOOST_AUTO_TEST_CASE(multiple_transfer_custom_auths) { + try { + ////// + // Initialize the test + ////// + generate_blocks(HARDFORK_BSIP_40_TIME); + generate_blocks(5); + db.modify(global_property_id_type()(db), [](global_property_object &gpo) { + gpo.parameters.extensions.value.custom_authority_options = custom_authority_options_type(); + }); + set_expiration(db, trx); + ACTORS((alice)(bob)(charlie)(diana)) + fund(alice, asset(1000 * GRAPHENE_BLOCKCHAIN_PRECISION)); + fund(bob, asset(1000 * GRAPHENE_BLOCKCHAIN_PRECISION)); + + + ////// + // Bob attempts to transfer 100 CORE from Alice's account to Charlie + // This attempt should fail because Alice has not authorized anyone to transfer from her account + ////// + transfer_operation bob_transfers_from_alice_to_charlie; + bob_transfers_from_alice_to_charlie.to = charlie.get_id(); + bob_transfers_from_alice_to_charlie.from = alice.get_id(); + bob_transfers_from_alice_to_charlie.amount.amount = 100 * GRAPHENE_BLOCKCHAIN_PRECISION; + trx.operations = {bob_transfers_from_alice_to_charlie}; + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + + ////// + // Bob attempts to transfer 100 CORE from Alice's account to Diana + // This attempt should fail because Alice has not authorized anyone to transfer from her account + ////// + transfer_operation bob_transfers_from_alice_to_diana; + bob_transfers_from_alice_to_diana.to = diana.get_id(); + bob_transfers_from_alice_to_diana.from = alice.get_id(); + bob_transfers_from_alice_to_diana.amount.amount = 60 * GRAPHENE_BLOCKCHAIN_PRECISION; + trx.operations = {bob_transfers_from_alice_to_diana}; + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + + ////// + // Charlie attempts to transfer 100 CORE from Alice's account to Diana + // This attempt should fail because Alice has not authorized anyone to transfer from her account + ////// + transfer_operation charlie_transfers_from_alice_to_diana; + charlie_transfers_from_alice_to_diana.to = diana.get_id(); + charlie_transfers_from_alice_to_diana.from = alice.get_id(); + charlie_transfers_from_alice_to_diana.amount.amount = 25 * GRAPHENE_BLOCKCHAIN_PRECISION; + trx.operations = {charlie_transfers_from_alice_to_diana}; + sign(trx, charlie_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + + ////// + // Create a custom authority where Bob is authorized to transfer from Alice's account to Charlie + ////// + custom_authority_create_operation op; + op.account = alice.get_id(); + op.auth.add_authority(bob.get_id(), 1); + op.auth.weight_threshold = 1; + op.enabled = true; + op.valid_to = db.head_block_time() + 1000; + op.operation_type = operation::tag::value; + auto to_index = member_index("to"); + vector restrictions; + restrictions.emplace_back(to_index, FUNC(eq), charlie.get_id()); + op.restrictions = restrictions; + //[ + // { + // "member_index": 2, + // "restriction_type": 0, + // "argument": [ + // 7, + // "1.2.18" + // ], + // "extensions": [] + // } + //] + + // Alice publishes the custom authority + trx.clear(); + trx.operations = {op}; + sign(trx, alice_private_key); + PUSH_TX(db, trx); + + custom_authority_id_type ca_bob_transfers_from_alice_to_charlie = + db.get_index_type().indices().get().find(alice_id)->id; + + ////// + // Bob attempts to transfer 100 CORE from Alice's account to Charlie + // This attempt should succeed because it is attempted after the custom authority is published + ////// + trx.clear(); + trx.operations = {bob_transfers_from_alice_to_charlie}; + sign(trx, bob_private_key); + PUSH_TX(db, trx); + + ////// + // Bob attempts to transfer 100 CORE from Alice's account to Diana + // This attempt should fail because Alice has not authorized Bob to transfer to Diana + ////// + trx.clear(); + trx.operations = {bob_transfers_from_alice_to_diana}; + sign(trx, charlie_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + + ////// + // Charlie attempts to transfer 100 CORE from Alice's account to Charlie + // This attempt should fail because Alice has not authorized Charlie to transfer to Diana + ////// + trx.clear(); + trx.operations = {charlie_transfers_from_alice_to_diana}; + sign(trx, charlie_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + + + + ////// + // Advance the blockchain to generate distinctive hash IDs for the re-used transactions + ////// + generate_blocks(1); + + ////// + // Create a custom authority where Charlie is authorized to transfer from Alice's account to Diana + ////// + op = custom_authority_create_operation(); + op.account = alice.get_id(); + op.auth.add_authority(charlie.get_id(), 1); + op.auth.weight_threshold = 1; + op.enabled = true; + op.valid_to = db.head_block_time() + 1000; + op.operation_type = operation::tag::value; + restrictions.clear(); + restrictions.emplace_back(to_index, FUNC(eq), diana.get_id()); + op.restrictions = restrictions; + //[ + // { + // "member_index": 2, + // "restriction_type": 0, + // "argument": [ + // 7, + // "1.2.19" + // ], + // "extensions": [] + // } + //] + + // Alice publishes the additional custom authority + trx.clear(); + trx.operations = {op}; + sign(trx, alice_private_key); + PUSH_TX(db, trx); + + // Note the additional custom authority + const auto &ca_index = db.get_index_type().indices().get(); + + auto ca_alice_range = ca_index.equal_range(alice_id); + long nbr_alice_auths = std::distance(ca_alice_range.first, ca_alice_range.second); + BOOST_CHECK_EQUAL(2, nbr_alice_auths); + auto iter = ca_alice_range.first; + custom_authority_id_type *ca_charlie_transfers_from_alice_to_diana = nullptr; + while (iter != ca_index.end()) { + custom_authority_id_type ca_id = iter->id; + const custom_authority_object *ca = db.find(ca_id); + flat_map ca_authorities = ca->auth.account_auths; + BOOST_CHECK_EQUAL(1, ca_authorities.size()); + if (ca_authorities.find(charlie.get_id()) != ca_authorities.end()) { + ca_charlie_transfers_from_alice_to_diana = &ca_id; + break; + } + + iter++; + } + BOOST_CHECK(ca_charlie_transfers_from_alice_to_diana != nullptr); + + ////// + // Charlie attempts to transfer 100 CORE from Alice's account to Diana + // This attempt should succeed because it is attempted after the custom authority is published + ////// + trx.clear(); + trx.operations = {charlie_transfers_from_alice_to_diana}; + sign(trx, charlie_private_key); + PUSH_TX(db, trx); + + ////// + // Bob should still be able to transfer from Alice to Charlie + // Bob attempts to transfer 100 CORE from Alice's account to Charlie + // This attempt should succeed because it was previously authorized by Alice + ////// + trx.clear(); + trx.operations = {bob_transfers_from_alice_to_charlie}; + sign(trx, bob_private_key); + PUSH_TX(db, trx); + + ////// + // Bob attempts to transfer 100 CORE from Alice's account to Diana + // This attempt should fail because Alice has not authorized Bob to transfer to Diana + ////// + trx.clear(); + trx.operations = {bob_transfers_from_alice_to_diana}; + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + + + + ////// + // Advance the blockchain to generate distinctive hash IDs for the re-used transactions + ////// + generate_blocks(1); + + ////// + // Alice revokes the custom authority for Bob + ////// + custom_authority_delete_operation revoke_bob_authorization; + revoke_bob_authorization.account = alice.get_id(); + revoke_bob_authorization.authority_to_delete = ca_bob_transfers_from_alice_to_charlie; + trx.clear(); + trx.operations = {revoke_bob_authorization}; + sign(trx, alice_private_key); + PUSH_TX(db, trx); + + ////// + // Bob attempts to transfer 100 CORE from Alice's account to Charlie + // This attempt should fail because Alice has revoked authorization for Bob to transfer from her account + ////// + trx.clear(); + trx.operations = {bob_transfers_from_alice_to_charlie}; + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + + ////// + // Charlie attempts to transfer 100 CORE from Alice's account to Diana + // This attempt should succeed because Alice should still be authorized to transfer from Alice account + ////// + trx.clear(); + trx.operations = {charlie_transfers_from_alice_to_diana}; + sign(trx, charlie_private_key); + PUSH_TX(db, trx); + + ////// + // Bob attempts to transfer 100 CORE from Alice's account to Diana + // This attempt should fail because Alice has not authorized Bob to transfer to Diana + ////// + trx.clear(); + trx.operations = {bob_transfers_from_alice_to_diana}; + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + + } FC_LOG_AND_RETHROW() + } + + /** + * Test of authorization and revocation of one account (Alice) authorizing another account (Bob) + * to trade with her account but not to transfer out of her account + */ + BOOST_AUTO_TEST_CASE(authorized_trader_custom_auths) { + try { + ////// + // Initialize the blockchain + ////// + generate_blocks(HARDFORK_BSIP_40_TIME); + generate_blocks(5); + db.modify(global_property_id_type()(db), [](global_property_object &gpo) { + gpo.parameters.extensions.value.custom_authority_options = custom_authority_options_type(); + }); + set_expiration(db, trx); + + + ////// + // Initialize: Define a market-issued asset called USDBIT + ////// + ACTORS((feedproducer)) + create_bitasset("USDBIT", feedproducer_id); + generate_blocks(1); + const auto& bitusd = *db.get_index_type().indices().get().find("USDBIT"); + const auto &core = asset_id_type()(db); + update_feed_producers(bitusd, {feedproducer.id}); + + price_feed current_feed; + current_feed.maintenance_collateral_ratio = 1750; + current_feed.maximum_short_squeeze_ratio = 1100; + current_feed.settlement_price = bitusd.amount(1) / core.amount(5); + publish_feed(bitusd, feedproducer, current_feed); + + + ////// + // Initialize: Fund some accounts + ////// + ACTORS((alice)(bob)(charlie)(diana)) + fund(alice, asset(5000 * GRAPHENE_BLOCKCHAIN_PRECISION)); + fund(bob, asset(100 * GRAPHENE_BLOCKCHAIN_PRECISION)); + + + ////// + // Bob attempts to create a limit order on behalf of Alice + // This should fail because Bob is not authorized to trade with her account + ////// + set_expiration( db, trx ); + trx.operations.clear(); + + limit_order_create_operation buy_order; + buy_order.seller = alice_id; + buy_order.amount_to_sell = core.amount(59); + buy_order.min_to_receive = bitusd.amount(7); + buy_order.expiration = time_point_sec::maximum(); + + trx.clear(); + trx.operations = {buy_order}; + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + + + ////// + // Alice authorizes Bob to place limit orders that offer the core asset for sale + ////// + custom_authority_create_operation authorize_limit_orders; + authorize_limit_orders.account = alice.get_id(); + authorize_limit_orders.auth.add_authority(bob.get_id(), 1); + authorize_limit_orders.auth.weight_threshold = 1; + authorize_limit_orders.enabled = true; + authorize_limit_orders.valid_to = db.head_block_time() + 1000; + authorize_limit_orders.operation_type = operation::tag::value; + trx.clear(); + trx.operations = {authorize_limit_orders}; + sign(trx, alice_private_key); + PUSH_TX(db, trx); + + auto caa = + db.get_index_type().indices().get().find(alice.get_id()); + custom_authority_id_type auth_id = caa->id; + + custom_authority_create_operation authorize_limit_order_cancellations; + authorize_limit_order_cancellations.account = alice.get_id(); + authorize_limit_order_cancellations.auth.add_authority(bob.get_id(), 1); + authorize_limit_order_cancellations.auth.weight_threshold = 1; + authorize_limit_order_cancellations.enabled = true; + authorize_limit_order_cancellations.valid_to = db.head_block_time() + 1000; + authorize_limit_order_cancellations.operation_type = operation::tag::value; + trx.clear(); + trx.operations = {authorize_limit_order_cancellations}; + sign(trx, alice_private_key); + PUSH_TX(db, trx); + + + ////// + // Advance the blockchain to generate a distinctive hash ID for the buy order transaction + ////// + generate_blocks(1); + + + ////// + // Bob attempts to create a limit order on behalf of Alice + // This should succeed because Bob is authorized to create limit orders + ////// + trx.clear(); + trx.operations = {buy_order}; + sign(trx, bob_private_key); + auto processed_buy = PUSH_TX(db, trx); + const limit_order_object *buy_order_object = db.find( processed_buy.operation_results[0].get() ); + + + ////// + // Bob attempts to cancel the limit order on behalf of Alice + // This should succeed because Bob is authorized to cancel limit orders + ////// + limit_order_cancel_operation cancel_order; + cancel_order.fee_paying_account = alice_id; + cancel_order.order = buy_order_object->id; + trx.clear(); + trx.operations = {cancel_order}; + sign(trx, bob_private_key); + auto processed_cancelled = PUSH_TX(db, trx); + + ////// + // Bob attempts to transfer funds out of Alice's account + // This should fail because Bob is not authorized to transfer funds out of her account + ////// + transfer_operation top; + top.to = bob.get_id(); + top.from = alice.get_id(); + top.amount.amount = 99 * GRAPHENE_BLOCKCHAIN_PRECISION; + trx.operations = {top}; + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + + + ////// + // Advance the blockchain to generate a distinctive hash ID for the buy order transaction + ////// + generate_blocks(1); + + + ////// + // Alice attempts to create her own limit order + // This should succeed because Alice has not relinquished her own authority to trade + ////// + buy_order = limit_order_create_operation(); + buy_order.seller = alice_id; + buy_order.amount_to_sell = core.amount(59); + buy_order.min_to_receive = bitusd.amount(7); + buy_order.expiration = time_point_sec::maximum(); + trx.clear(); + trx.operations = {buy_order}; + sign(trx, alice_private_key); + PUSH_TX(db, trx); + + + ////// + // Alice revokes/disables the authorization to create limit orders + ////// + custom_authority_update_operation disable_authorizations; + disable_authorizations.account = alice.get_id(); + disable_authorizations.authority_to_update = auth_id; + disable_authorizations.new_enabled = false; + trx.clear(); + trx.operations = {disable_authorizations}; + sign(trx, alice_private_key); + PUSH_TX(db, trx); + + + ////// + // Advance the blockchain to generate a distinctive hash ID for the buy order transaction + ////// + generate_blocks(1); + + + ////// + // Bob attempts to create a limit order on behalf of Alice + // This should fail because Bob is not authorized to trade with her account + ////// + trx.clear(); + trx.operations = {buy_order}; + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + + } FC_LOG_AND_RETHROW() + } + + + /** + * Test of authorization of one account (feedproducer) authorizing another account (Bob) + * to publish feeds. The authorization remains associated with account even when the account changes its keys. + */ + BOOST_AUTO_TEST_CASE(feed_publisher_authorizes_other_account) { + try { + ////// + // Initialize the blockchain + ////// + generate_blocks(HARDFORK_BSIP_40_TIME); + generate_blocks(5); + db.modify(global_property_id_type()(db), [](global_property_object &gpo) { + gpo.parameters.extensions.value.custom_authority_options = custom_authority_options_type(); + }); + set_expiration(db, trx); + + + ////// + // Initialize: Define a market-issued asset called USDBIT + ////// + ACTORS((feedproducer)); + create_bitasset("USDBIT", feedproducer_id); + generate_blocks(1); + const auto& bitusd = *db.get_index_type().indices().get().find("USDBIT"); + const auto &core = asset_id_type()(db); + update_feed_producers(bitusd, {feedproducer.id}); + + price_feed current_feed; + current_feed.maintenance_collateral_ratio = 1750; + current_feed.maximum_short_squeeze_ratio = 1100; + current_feed.settlement_price = bitusd.amount(1) / core.amount(5); + publish_feed(bitusd, feedproducer, current_feed); + + + ////// + // Initialize: Fund other accounts + ////// + ACTORS((bob)) + fund(bob, asset(100 * GRAPHENE_BLOCKCHAIN_PRECISION)); + + + ////// + // Advance the blockchain to generate a distinctive hash ID for the publish feed transaction + ////// + generate_blocks(1); + + + ////// + // Bob attempts to publish feed of USDBIT on behalf of feedproducer + // This should fail because Bob is not authorized to publish the feed + ////// + asset_publish_feed_operation pop; + pop.publisher = feedproducer.id; + pop.asset_id = bitusd.id; + pop.feed = current_feed; + if (pop.feed.core_exchange_rate.is_null()) + pop.feed.core_exchange_rate = pop.feed.settlement_price; + trx.clear(); + trx.operations.emplace_back(std::move(pop)); + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + + + ////// + // feedproducer authorizes Bob to publish feeds on its behalf + ////// + custom_authority_create_operation authorize_feed_publishing; + authorize_feed_publishing.account = feedproducer.get_id(); + authorize_feed_publishing.auth.add_authority(bob.get_id(), 1); + authorize_feed_publishing.auth.weight_threshold = 1; + authorize_feed_publishing.enabled = true; + authorize_feed_publishing.valid_to = db.head_block_time() + 1000; + authorize_feed_publishing.operation_type = operation::tag::value; + trx.clear(); + trx.operations = {authorize_feed_publishing}; + sign(trx, feedproducer_private_key); + PUSH_TX(db, trx); + + custom_authority_id_type auth_id = + db.get_index_type().indices().get().find(feedproducer.id)->id; + + ////// + // Bob attempts to publish feed of USDBIT on behalf of feedproducer + // This should succeed because Bob is authorized by feedproducer to publish the feed + ////// + trx.clear(); + trx.operations.emplace_back(std::move(pop)); + sign(trx, bob_private_key); + PUSH_TX(db, trx); + + + ////// + // Advance the blockchain to generate a distinctive hash ID for the publish feed transaction + ////// + generate_blocks(1); + + + ////// + // Bob creates a new key + ////// + fc::ecc::private_key new_bob_private_key = generate_private_key("new Bob key"); + public_key_type new_bob_public_key = public_key_type(new_bob_private_key.get_public_key()); + + + ////// + // Bob attempts to publish feed of USDBIT on behalf of feedproducer with new key + // This should fail because the new key is not associated with Bob on the blockchain + ////// + trx.clear(); + trx.operations.emplace_back(std::move(pop)); + sign(trx, new_bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + + + ////// + // Advance the blockchain to generate a distinctive hash ID for the publish feed transaction + ////// + generate_blocks(1); + + + ////// + // Bob changes his account's active key + ////// + account_update_operation uop; + uop.account = bob.get_id(); + uop.active = authority(1, new_bob_public_key, 1); + trx.clear(); + trx.operations.emplace_back(std::move(uop)); + sign(trx, bob_private_key); + PUSH_TX(db, trx); + + + ////// + // Bob attempts to publish feed of USDBIT on behalf of feedproducer + // This should succeed because Bob's new key is associated with Bob's authorized account. + ////// + trx.clear(); + trx.operations.emplace_back(std::move(pop)); + sign(trx, new_bob_private_key); + PUSH_TX(db, trx); + + + ////// + // Feedproducer revokes/disables the authorization by disabling it + ////// + custom_authority_update_operation disable_authorizations; + disable_authorizations.account = feedproducer.get_id(); + disable_authorizations.authority_to_update = auth_id; + disable_authorizations.new_enabled = false; + trx.clear(); + trx.operations = {disable_authorizations}; + sign(trx, feedproducer_private_key); + PUSH_TX(db, trx); + + + ////// + // Advance the blockchain to generate a distinctive hash ID for the publish feed transaction + ////// + generate_blocks(1); + + + ////// + // Bob attempts to publish feed of USDBIT on behalf of feedproducer with new key + // This should fail because Bob's account is no longer authorized by feedproducer + ////// + trx.clear(); + trx.operations.emplace_back(std::move(pop)); + sign(trx, new_bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + + } FC_LOG_AND_RETHROW() + } + + + /** + * Test of authorization of one account (feedproducer) authorizing another key + * to publish feeds + */ + BOOST_AUTO_TEST_CASE(authorized_feed_publisher_other_key_custom_auths) { + try { + ////// + // Initialize the blockchain + ////// + generate_blocks(HARDFORK_BSIP_40_TIME); + generate_blocks(5); + db.modify(global_property_id_type()(db), [](global_property_object &gpo) { + gpo.parameters.extensions.value.custom_authority_options = custom_authority_options_type(); + }); + set_expiration(db, trx); + + + ////// + // Initialize: Define a market-issued asset called USDBIT + ////// + ACTORS((feedproducer)); + const auto &bitusd = create_bitasset("USDBIT", feedproducer_id); + const auto &core = asset_id_type()(db); + update_feed_producers(bitusd, {feedproducer.id}); + + price_feed current_feed; + current_feed.maintenance_collateral_ratio = 1750; + current_feed.maximum_short_squeeze_ratio = 1100; + current_feed.settlement_price = bitusd.amount(1) / core.amount(5); + // publish_feed(bitusd, feedproducer, current_feed); + asset_publish_feed_operation pop; + pop.publisher = feedproducer.id; + pop.asset_id = bitusd.id; + pop.feed = current_feed; + if (pop.feed.core_exchange_rate.is_null()) + pop.feed.core_exchange_rate = pop.feed.settlement_price; + + + ////// + // Advance the blockchain to generate a distinctive hash ID for the publish feed transaction + ////// + generate_blocks(1); + + + ////// + // Define a key that can be authorized + // This can be a new key or an existing key. The existing key may even be the active key of an account. + ////// + fc::ecc::private_key some_private_key = generate_private_key("some key"); + public_key_type some_public_key = public_key_type(some_private_key.get_public_key()); + + + ////// + // feedproducer authorizes a key to publish feeds on its behalf + ////// + custom_authority_create_operation authorize_feed_publishing; + authorize_feed_publishing.account = feedproducer.get_id(); + authorize_feed_publishing.auth.add_authority(some_public_key, 1); + authorize_feed_publishing.auth.weight_threshold = 1; + authorize_feed_publishing.enabled = true; + authorize_feed_publishing.valid_to = db.head_block_time() + 1000; + authorize_feed_publishing.operation_type = operation::tag::value; + trx.clear(); + trx.operations = {authorize_feed_publishing}; + sign(trx, feedproducer_private_key); + PUSH_TX(db, trx); + + + ////// + // Any software client with this key attempts to publish feed of USDBIT on behalf of feedproducer + // This should succeed because the pusher of this transaction signs the transaction with the authorized key + ////// + trx.clear(); + trx.operations.emplace_back(std::move(pop)); + sign(trx, some_private_key); + PUSH_TX(db, trx); + + } FC_LOG_AND_RETHROW() + } + + + /** + * Test of authorization of one account (faucet) authorizing another key + * to register accounts + */ + BOOST_AUTO_TEST_CASE(authorized_faucet_other_key_custom_auths) { + try { + ////// + // Initialize the blockchain + ////// + generate_blocks(HARDFORK_BSIP_40_TIME); + generate_blocks(5); + db.modify(global_property_id_type()(db), [](global_property_object &gpo) { + gpo.parameters.extensions.value.custom_authority_options = custom_authority_options_type(); + }); + set_expiration(db, trx); + + + ////// + // Initialize: faucet account + ////// + ACTORS((faucet)(charlie)); + fund(faucet, asset(500000 * GRAPHENE_BLOCKCHAIN_PRECISION)); + account_upgrade_operation uop; + uop.account_to_upgrade = faucet.get_id(); + uop.upgrade_to_lifetime_member = true; + trx.clear(); + trx.operations.emplace_back(std::move(uop)); + sign(trx, faucet_private_key); + PUSH_TX(db, trx); + + // Lambda for creating account + auto create_account_by_name = [&](const string &name, const account_object& registrar) { + account_create_operation create_op; + create_op.name = name; + public_key_type new_key = public_key_type(generate_private_key(name + " seed").get_public_key()); + create_op.registrar = registrar.id; + create_op.owner = authority(1, new_key, 1); + create_op.active = authority(1, new_key, 1); + create_op.options.memo_key = new_key; + create_op.options.voting_account = GRAPHENE_PROXY_TO_SELF_ACCOUNT; + + return create_op; + }; + + + ////// + // Attempt to register an account with this key + // This should succeed because faucet is a lifetime member account + ////// + string name = "account1"; + account_create_operation create_op = create_account_by_name(name, faucet); + trx.clear(); + trx.operations = {create_op}; + sign(trx, faucet_private_key); + PUSH_TX(db, trx); + + + ////// + // Define a key that can be authorized + // This can be a new key or an existing key. The existing key may even be the active key of an account. + ////// + fc::ecc::private_key some_private_key = generate_private_key("some key"); + public_key_type some_public_key = public_key_type(some_private_key.get_public_key()); + + + ////// + // Attempt to register an account with this key + // This should fail because the key is not authorized to register any accounts + ////// + name = "account2"; + create_op = create_account_by_name(name, faucet); + trx.clear(); + trx.operations = {create_op}; + sign(trx, some_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + + + ////// + // faucet authorizes a key to register accounts on its behalf + ////// + custom_authority_create_operation authorize_account_registration; + authorize_account_registration.account = faucet.get_id(); + authorize_account_registration.auth.add_authority(some_public_key, 1); + authorize_account_registration.auth.weight_threshold = 1; + authorize_account_registration.enabled = true; + authorize_account_registration.valid_to = db.head_block_time() + 1000; + authorize_account_registration.operation_type = operation::tag::value; + trx.clear(); + trx.operations = {authorize_account_registration}; + sign(trx, faucet_private_key); + PUSH_TX(db, trx); + + + ////// + // Advance the blockchain to generate a distinctive hash ID for the account registration transaction + ////// + generate_blocks(1); + + + ////// + // Attempt to register an account with this key + // This should succeed because the key is authorized to register any accounts + ////// + trx.clear(); + trx.operations.emplace_back(std::move(create_op)); + sign(trx, some_private_key); + PUSH_TX(db, trx); + + + ////// + // Attempt to register an account with this key + // This should succeed because the key is authorized to register any accounts + ////// + create_op = create_account_by_name("account3", faucet); + trx.clear(); + trx.operations = {create_op}; + sign(trx, some_private_key); + PUSH_TX(db, trx); + + + ////// + // Attempt to transfer funds out of the faucet account + // This should fail because the key is not authorized to transfer from the faucet account + ////// + transfer_operation top; + top.amount.amount = 99 * GRAPHENE_BLOCKCHAIN_PRECISION; + top.from = faucet.get_id(); + top.to = charlie.get_id(); + top.fee.asset_id = asset_id_type(1); + trx.clear(); + trx.operations = {top}; + sign(trx, some_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + + + ////// + // Attempt to register an account with this key + // This should succeed because the key is authorized to register any accounts + ////// + create_op = create_account_by_name("account4", faucet); + trx.clear(); + trx.operations = {create_op}; + sign(trx, some_private_key); + PUSH_TX(db, trx); + + } FC_LOG_AND_RETHROW() + } + + /** + * Test of authorization of a key to transfer one asset type (USDBIT) from one account (coldwallet) + * to another account (hotwallet) + */ + BOOST_AUTO_TEST_CASE(authorized_cold_wallet_key_custom_auths) { + try { + ////// + // Initialize the blockchain + ////// + generate_blocks(HARDFORK_BSIP_40_TIME); + generate_blocks(5); + db.modify(global_property_id_type()(db), [](global_property_object &gpo) { + gpo.parameters.extensions.value.custom_authority_options = custom_authority_options_type(); + }); + set_expiration(db, trx); + + + ////// + // Initialize: Accounts + ACTORS((feedproducer)(coldwallet)(hotwallet)(hacker)); + int64_t init_balance(100 * GRAPHENE_BLOCKCHAIN_PRECISION); + + ////// + // Initialize: Define a market-issued asset called USDBIT + ////// + // Define core asset + const auto &core = asset_id_type()(db); + asset_id_type core_id = core.id; + + // Create a smart asset + const asset_object &bitusd = create_bitasset("USDBIT", feedproducer_id); + asset_id_type usd_id = bitusd.id; + update_feed_producers(bitusd, {feedproducer.id}); + price_feed current_feed; + current_feed.maintenance_collateral_ratio = 1750; + current_feed.maximum_short_squeeze_ratio = 1100; + current_feed.settlement_price = bitusd.amount(1) / core.amount(5); + publish_feed(bitusd, feedproducer, current_feed); + + + ////// + // Fund coldwallet with core asset + ////// + fund(coldwallet, asset(init_balance)); + // coldwallet will borrow 1000 bitUSD + borrow(coldwallet, bitusd.amount(1000), asset(15000)); + int64_t alice_balance_usd_before_offer = get_balance(coldwallet_id, usd_id); + BOOST_CHECK_EQUAL( 1000, alice_balance_usd_before_offer); + int64_t coldwallet_balance_core_before_offer = get_balance(coldwallet_id, core_id); + BOOST_CHECK_EQUAL( init_balance - 15000, coldwallet_balance_core_before_offer ); + + + ////// + // Define a key that can be authorized + // This can be a new key or an existing key. The existing key may even be the active key of an account. + ////// + fc::ecc::private_key some_private_key = generate_private_key("some key"); + public_key_type some_public_key = public_key_type(some_private_key.get_public_key()); + + + ////// + // Create a custom authority where the key is authorized to transfer from the coldwallet account + // if and only if the transfer asset type is USDBIT and the recipient account is hotwallet. + ////// + custom_authority_create_operation op; + op.account = coldwallet.get_id(); + op.auth.add_authority(some_public_key, 1); + op.auth.weight_threshold = 1; + op.enabled = true; + op.valid_to = db.head_block_time() + 1000; + + op.operation_type = operation::tag::value; + + auto to_index = member_index("to"); + op.restrictions.emplace_back(to_index, FUNC(eq), hotwallet_id); + + auto transfer_amount_index = member_index("amount"); + auto assed_id_index = member_index("asset_id"); + op.restrictions.emplace_back(restriction(transfer_amount_index, restriction::func_attr, vector{ + restriction(assed_id_index, restriction::func_eq, usd_id)})); + //[ + // { + // "member_index": 2, + // "restriction_type": 0, + // "argument": [ + // 7, + // "1.2.18" + // ], + // "extensions": [] + // }, + // { + // "member_index": 3, + // "restriction_type": 10, + // "argument": [ + // 39, + // [ + // { + // "member_index": 1, + // "restriction_type": 0, + // "argument": [ + // 8, + // "1.3.2" + // ], + // "extensions": [] + // } + // ] + // ], + // "extensions": [] + // } + //] + BOOST_CHECK_EQUAL(restriction::restriction_count(op.restrictions), 3); + + // Publish the new custom authority + trx.clear(); + trx.operations = {op}; + sign(trx, coldwallet_private_key); + PUSH_TX(db, trx); + + + ////// + // Attempt to transfer USDBIT asset out of the coldwallet to the hacker account + // This should fail because the key is not authorized to transfer to the hacker account + ////// + transfer_operation top; + top.from = coldwallet.get_id(); + top.to = hacker.get_id(); + top.amount.asset_id = usd_id; + top.amount.amount = 99; + top.fee.asset_id = core_id; + trx.clear(); + trx.operations = {top}; + sign(trx, some_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + + + ////// + // Attempt to transfer CORE asset out of the coldwallet to the hotwallet account + // This should fail because the key is not authorized to transfer core asset to the hotwallet account + ////// + top = transfer_operation(); + top.from = coldwallet.get_id(); + top.to = hotwallet.get_id(); + top.amount.asset_id = core_id; + top.amount.amount = 99; + top.fee.asset_id = core_id; + trx.clear(); + trx.operations = {top}; + sign(trx, some_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + + + ////// + // Attempt to transfer USDBIT asset out of the coldwallet to the hotwallet account + // This should succeed because the key is authorized to transfer USDBIT asset to the hotwallet account + ////// + top = transfer_operation(); + top.from = coldwallet.get_id(); + top.to = hotwallet.get_id(); + top.amount.asset_id = usd_id; + top.amount.amount = 99; + top.fee.asset_id = core_id; + trx.clear(); + trx.operations = {top}; + sign(trx, some_private_key); + PUSH_TX(db, trx); + + } FC_LOG_AND_RETHROW() + } + +BOOST_AUTO_TEST_SUITE_END()