diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index a7e1b979b2..261a5e38db 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -35,6 +35,9 @@ jobs: libboost-regex-dev \ libboost-coroutine-dev \ libcurl4-openssl-dev + sudo apt-get auto-remove -y + sudo apt-get clean -y + df -h - uses: actions/checkout@v1 with: submodules: recursive @@ -64,16 +67,20 @@ jobs: export CCACHE_DIR="$GITHUB_WORKSPACE/ccache" mkdir -p "$CCACHE_DIR" make -j 2 -C _build + df -h - name: Unit-Tests run: | _build/tests/app_test + _build/tests/es_test libraries/fc/tests/run-parallel-tests.sh _build/tests/chain_test libraries/fc/tests/run-parallel-tests.sh _build/tests/cli_test - _build/tests/es_test + df -h - name: Node-Test run: | + df -h pushd _build ../programs/build_helpers/run-node-test + df -h test-debug: name: Build and run tests in Debug mode runs-on: ubuntu-latest @@ -99,12 +106,19 @@ jobs: libboost-regex-dev \ libboost-coroutine-dev \ libcurl4-openssl-dev + sudo apt-get auto-remove -y + sudo apt-get clean -y + df -h - uses: actions/checkout@v1 with: submodules: recursive - name: Configure run: | mkdir -p _build + sudo mkdir -p /mnt/libraries /mnt/tests + sudo chmod a+rwx /mnt/libraries /mnt/tests + ln -s /mnt/libraries _build/libraries + ln -s /mnt/tests _build/tests pushd _build export -n BOOST_ROOT BOOST_INCLUDEDIR BOOST_LIBRARYDIR cmake -D CMAKE_BUILD_TYPE=Debug \ @@ -127,17 +141,24 @@ jobs: run: | export CCACHE_DIR="$GITHUB_WORKSPACE/ccache" mkdir -p "$CCACHE_DIR" + df -h make -j 2 -C _build + df -h - name: Unit-Tests run: | _build/tests/app_test + df -h + _build/tests/es_test + df -h libraries/fc/tests/run-parallel-tests.sh _build/tests/chain_test libraries/fc/tests/run-parallel-tests.sh _build/tests/cli_test - _build/tests/es_test + df -h - name: Node-Test run: | + df -h pushd _build ../programs/build_helpers/run-node-test + df -h prepare-mingw64-libs: name: Build 3rd-party libraries required for windows cross-build runs-on: ubuntu-latest @@ -233,6 +254,9 @@ jobs: ccache \ g++-mingw-w64-x86-64 \ mingw-w64-tools + sudo apt-get auto-remove -y + sudo apt-get clean -y + df -h - uses: actions/checkout@v1 with: submodules: recursive @@ -309,12 +333,15 @@ jobs: export CCACHE_DIR="$GITHUB_WORKSPACE/ccache" mkdir -p "$CCACHE_DIR" make -j 2 -C _build witness_node cli_wallet app_test cli_test chain_test + df -h - name: Unit-Tests run: | _build/tests/app_test libraries/fc/tests/run-parallel-tests.sh _build/tests/chain_test libraries/fc/tests/run-parallel-tests.sh _build/tests/cli_test + df -h - name: Node-Test run: | + df -h pushd _build ../programs/build_helpers/run-node-test diff --git a/.travis.yml b/.travis.yml index affc9cfe36..8d657f926b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,5 +31,7 @@ jobs: include: - stage: build for cache script: ./programs/build_helpers/build_protocol - - stage: build and test + - stage: build, test and scan partly with sonar script: ./programs/build_helpers/build_and_test + - stage: scan fully with sonar + script: ./programs/build_helpers/scan_with_sonar diff --git a/docker/default_config.ini b/docker/default_config.ini index 6d6c947b59..53aa03bcfc 100644 --- a/docker/default_config.ini +++ b/docker/default_config.ini @@ -82,6 +82,9 @@ rpc-endpoint = 0.0.0.0:8090 # For database_api_impl::get_limit_orders to set max limit value # api-limit-get-limit-orders = 300 +# For database_api_impl::get_limit_orders_by_account to set max limit value +# api-limit-get-limit-orders-by-account = 101 + # For database_api_impl::get_order_book to set max limit value # api-limit-get-order-book = 50 @@ -119,7 +122,7 @@ rpc-endpoint = 0.0.0.0:8090 # api-limit-get-withdraw-permissions-by-recipient = 101 # Space-separated list of plugins to activate -plugins = witness account_history market_history grouped_orders api_helper_indexes +plugins = witness account_history market_history grouped_orders api_helper_indexes custom_operations # Do not exit if api_helper_indexes plugin is not enabled. # ignore-api-helper-indexes-warning = true @@ -164,7 +167,7 @@ enable-stale-production = false partial-operations = true # Maximum number of operations per account will be kept in memory -max-ops-per-account = 1000 +max-ops-per-account = 100 # ============================================================================== diff --git a/libraries/app/api.cpp b/libraries/app/api.cpp index 99564106a2..1f22020adb 100644 --- a/libraries/app/api.cpp +++ b/libraries/app/api.cpp @@ -302,7 +302,7 @@ namespace graphene { namespace app { return *_debug_api; } - fc::api login_api::custom() const + fc::api login_api::custom_operations() const { FC_ASSERT(_custom_operations_api); return *_custom_operations_api; @@ -342,8 +342,12 @@ namespace graphene { namespace app { { FC_ASSERT( _app.chain_database() ); const auto& db = *_app.chain_database(); - uint64_t api_limit_get_account_history=_app.get_options().api_limit_get_account_history; - FC_ASSERT( limit <= api_limit_get_account_history ); + + const auto configured_limit = _app.get_options().api_limit_get_account_history; + FC_ASSERT( limit <= configured_limit, + "limit can not be greater than ${configured_limit}", + ("configured_limit", configured_limit) ); + vector result; account_id_type account; try { @@ -387,12 +391,16 @@ namespace graphene { namespace app { int operation_type, operation_history_id_type start, operation_history_id_type stop, - unsigned limit) const + unsigned limit ) const { FC_ASSERT( _app.chain_database() ); const auto& db = *_app.chain_database(); - uint64_t api_limit_get_account_history_operations=_app.get_options().api_limit_get_account_history_operations; - FC_ASSERT(limit <= api_limit_get_account_history_operations); + + const auto configured_limit = _app.get_options().api_limit_get_account_history_operations; + FC_ASSERT( limit <= configured_limit, + "limit can not be greater than ${configured_limit}", + ("configured_limit", configured_limit) ); + vector result; account_id_type account; try { @@ -427,12 +435,16 @@ namespace graphene { namespace app { vector history_api::get_relative_account_history( const std::string account_id_or_name, uint64_t stop, unsigned limit, - uint64_t start) const + uint64_t start ) const { FC_ASSERT( _app.chain_database() ); const auto& db = *_app.chain_database(); - uint64_t api_limit_get_relative_account_history=_app.get_options().api_limit_get_relative_account_history; - FC_ASSERT(limit <= api_limit_get_relative_account_history); + + const auto configured_limit = _app.get_options().api_limit_get_relative_account_history; + FC_ASSERT( limit <= configured_limit, + "limit can not be greater than ${configured_limit}", + ("configured_limit", configured_limit) ); + vector result; account_id_type account; try { @@ -469,24 +481,38 @@ namespace graphene { namespace app { return hist->tracked_buckets(); } - history_operation_detail history_api::get_account_history_by_operations(const std::string account_id_or_name, vector operation_types, uint32_t start, unsigned limit) + history_operation_detail history_api::get_account_history_by_operations( const std::string account_id_or_name, + flat_set operation_types, + uint32_t start, unsigned limit )const { - uint64_t api_limit_get_account_history_by_operations=_app.get_options().api_limit_get_account_history_by_operations; - FC_ASSERT(limit <= api_limit_get_account_history_by_operations); + const auto configured_limit = _app.get_options().api_limit_get_account_history_by_operations; + FC_ASSERT( limit <= configured_limit, + "limit can not be greater than ${configured_limit}", + ("configured_limit", configured_limit) ); + history_operation_detail result; - vector objs = get_relative_account_history(account_id_or_name, start, limit, limit + start - 1); - std::for_each(objs.begin(), objs.end(), [&](const operation_history_object &o) { - if (operation_types.empty() || find(operation_types.begin(), operation_types.end(), o.op.which()) != operation_types.end()) { - result.operation_history_objs.push_back(o); - } - }); + vector objs = get_relative_account_history( account_id_or_name, start, limit, + limit + start - 1 ); + result.total_count = objs.size(); + + if( operation_types.empty() ) + result.operation_history_objs = std::move(objs); + else + { + for( const operation_history_object &o : objs ) + { + if( operation_types.find(o.op.which()) != operation_types.end() ) { + result.operation_history_objs.push_back(o); + } + } + } - result.total_count = objs.size(); - return result; + return result; } vector history_api::get_market_history( std::string asset_a, std::string asset_b, - uint32_t bucket_seconds, fc::time_point_sec start, fc::time_point_sec end )const + uint32_t bucket_seconds, + fc::time_point_sec start, fc::time_point_sec end )const { try { FC_ASSERT(_app.chain_database()); const auto& db = *_app.chain_database(); @@ -577,9 +603,13 @@ namespace graphene { namespace app { ) { } asset_api::~asset_api() { } - vector asset_api::get_asset_holders( std::string asset, uint32_t start, uint32_t limit ) const { - uint64_t api_limit_get_asset_holders=_app.get_options().api_limit_get_asset_holders; - FC_ASSERT(limit <= api_limit_get_asset_holders); + vector asset_api::get_asset_holders( std::string asset, uint32_t start, uint32_t limit ) const + { + const auto configured_limit = _app.get_options().api_limit_get_asset_holders; + FC_ASSERT( limit <= configured_limit, + "limit can not be greater than ${configured_limit}", + ("configured_limit", configured_limit) ); + asset_id_type asset_id = database_api.get_asset_id_from_string( asset ); const auto& bal_idx = _db.get_index_type< account_balance_index >().indices().get< by_asset_balance >(); auto range = bal_idx.equal_range( boost::make_tuple( asset_id ) ); @@ -660,8 +690,11 @@ namespace graphene { namespace app { optional start, uint32_t limit )const { - uint64_t api_limit_get_grouped_limit_orders=_app.get_options().api_limit_get_grouped_limit_orders; - FC_ASSERT( limit <= api_limit_get_grouped_limit_orders ); + const auto configured_limit = _app.get_options().api_limit_get_grouped_limit_orders; + FC_ASSERT( limit <= configured_limit, + "limit can not be greater than ${configured_limit}", + ("configured_limit", configured_limit) ); + auto plugin = _app.get_plugin( "grouped_orders" ); FC_ASSERT( plugin ); const auto& limit_groups = plugin->limit_order_groups(); @@ -676,7 +709,7 @@ namespace graphene { namespace app { max_price = std::max( std::min( max_price, *start ), min_price ); auto itr = limit_groups.lower_bound( limit_order_group_key( group, max_price ) ); - // use an end itrator to try to avoid expensive price comparison + // use an end iterator to try to avoid expensive price comparison auto end = limit_groups.upper_bound( limit_order_group_key( group, min_price ) ); while( itr != end && result.size() < limit ) { @@ -696,12 +729,10 @@ namespace graphene { namespace app { const auto account_id = database_api.get_account_id_from_string(account_id_or_name); vector results; const auto& storage_index = _app.chain_database()->get_index_type(); - const auto& by_account_catalog_idx = storage_index.indices().get(); - auto itr = by_account_catalog_idx.lower_bound(make_tuple(account_id, catalog)); - while(itr != by_account_catalog_idx.end() && itr->account == account_id && itr->catalog == catalog) { - results.push_back(*itr); - ++itr; - } + const auto& by_account_catalog_idx = storage_index.indices().get(); + auto range = by_account_catalog_idx.equal_range(make_tuple(account_id, catalog)); + for( const account_storage_object& aso : boost::make_iterator_range( range.first, range.second ) ) + results.push_back(aso); return results; } diff --git a/libraries/app/api_objects.cpp b/libraries/app/api_objects.cpp index e002f061ae..a999ad99ca 100644 --- a/libraries/app/api_objects.cpp +++ b/libraries/app/api_objects.cpp @@ -38,8 +38,11 @@ market_ticker::market_ticker(const market_ticker_object& mto, quote = asset_quote.symbol; percent_change = "0"; lowest_ask = "0"; + lowest_ask_base_size = "0"; + lowest_ask_quote_size = "0"; highest_bid = "0"; - + highest_bid_base_size = "0"; + highest_bid_quote_size = "0"; fc::uint128_t bv; fc::uint128_t qv; price latest_price = asset( mto.latest_base, mto.base ) / asset( mto.latest_quote, mto.quote ); @@ -68,9 +71,19 @@ market_ticker::market_ticker(const market_ticker_object& mto, quote_volume = uint128_amount_to_string( qv, asset_quote.precision ); if(!orders.asks.empty()) - lowest_ask = orders.asks[0].price; + { + lowest_ask = orders.asks[0].price; + lowest_ask_base_size = orders.asks[0].base; + lowest_ask_quote_size = orders.asks[0].quote; + } + if(!orders.bids.empty()) - highest_bid = orders.bids[0].price; + { + highest_bid = orders.bids[0].price; + highest_bid_base_size = orders.bids[0].base; + highest_bid_quote_size = orders.bids[0].quote; + } + } market_ticker::market_ticker(const fc::time_point_sec& now, @@ -82,7 +95,11 @@ market_ticker::market_ticker(const fc::time_point_sec& now, quote = asset_quote.symbol; latest = "0"; lowest_ask = "0"; + lowest_ask_base_size = "0"; + lowest_ask_quote_size = "0"; highest_bid = "0"; + highest_bid_base_size = "0"; + highest_bid_quote_size = "0"; percent_change = "0"; base_volume = "0"; quote_volume = "0"; diff --git a/libraries/app/application.cpp b/libraries/app/application.cpp index de5bf8a334..6263ab1ec8 100644 --- a/libraries/app/application.cpp +++ b/libraries/app/application.cpp @@ -43,7 +43,6 @@ #include #include #include -#include #include #include @@ -125,41 +124,14 @@ void application_impl::reset_p2p_node(const fc::path& data_dir) if( _options->count("seed-node") ) { auto seeds = _options->at("seed-node").as>(); - for( const string& endpoint_string : seeds ) - { - try { - std::vector endpoints = resolve_string_to_ip_endpoints(endpoint_string); - for (const fc::ip::endpoint& endpoint : endpoints) - { - ilog("Adding seed node ${endpoint}", ("endpoint", endpoint)); - _p2p_network->add_node(endpoint); - _p2p_network->connect_to_endpoint(endpoint); - } - } catch( const fc::exception& e ) { - wlog( "caught exception ${e} while adding seed node ${endpoint}", - ("e", e.to_detail_string())("endpoint", endpoint_string) ); - } - } + _p2p_network->add_seed_nodes(seeds); } if( _options->count("seed-nodes") ) { auto seeds_str = _options->at("seed-nodes").as(); auto seeds = fc::json::from_string(seeds_str).as>(2); - for( const string& endpoint_string : seeds ) - { - try { - std::vector endpoints = resolve_string_to_ip_endpoints(endpoint_string); - for (const fc::ip::endpoint& endpoint : endpoints) - { - ilog("Adding seed node ${endpoint}", ("endpoint", endpoint)); - _p2p_network->add_node(endpoint); - } - } catch( const fc::exception& e ) { - wlog( "caught exception ${e} while adding seed node ${endpoint}", - ("e", e.to_detail_string())("endpoint", endpoint_string) ); - } - } + _p2p_network->add_seed_nodes(seeds); } else { @@ -167,20 +139,7 @@ void application_impl::reset_p2p_node(const fc::path& data_dir) vector seeds = { #include "../egenesis/seed-nodes.txt" }; - for( const string& endpoint_string : seeds ) - { - try { - std::vector endpoints = resolve_string_to_ip_endpoints(endpoint_string); - for (const fc::ip::endpoint& endpoint : endpoints) - { - ilog("Adding seed node ${endpoint}", ("endpoint", endpoint)); - _p2p_network->add_node(endpoint); - } - } catch( const fc::exception& e ) { - wlog( "caught exception ${e} while adding seed node ${endpoint}", - ("e", e.to_detail_string())("endpoint", endpoint_string) ); - } - } + _p2p_network->add_seed_nodes(seeds); } if( _options->count("p2p-endpoint") ) @@ -196,36 +155,6 @@ void application_impl::reset_p2p_node(const fc::path& data_dir) std::vector()); } FC_CAPTURE_AND_RETHROW() } -std::vector application_impl::resolve_string_to_ip_endpoints(const std::string& endpoint_string) -{ - try - { - string::size_type colon_pos = endpoint_string.find(':'); - if (colon_pos == std::string::npos) - FC_THROW("Missing required port number in endpoint string \"${endpoint_string}\"", - ("endpoint_string", endpoint_string)); - std::string port_string = endpoint_string.substr(colon_pos + 1); - try - { - uint16_t port = boost::lexical_cast(port_string); - - std::string hostname = endpoint_string.substr(0, colon_pos); - std::vector endpoints = fc::resolve(hostname, port); - if (endpoints.empty()) - FC_THROW_EXCEPTION( fc::unknown_host_exception, - "The host name can not be resolved: ${hostname}", - ("hostname", hostname) ); - return endpoints; - } - catch (const boost::bad_lexical_cast&) - { - FC_THROW("Bad port: ${port}", ("port", port_string)); - } - } - FC_CAPTURE_AND_RETHROW((endpoint_string)) -} - - void application_impl::new_connection( const fc::http::websocket_connection_ptr& c ) { auto wsc = std::make_shared(c, GRAPHENE_NET_MAX_NESTED_OBJECTS); @@ -343,6 +272,9 @@ void application_impl::set_api_limit() { if(_options->count("api-limit-get-limit-orders")){ _app_options.api_limit_get_limit_orders = _options->at("api-limit-get-limit-orders").as(); } + if(_options->count("api-limit-get-limit-orders-by-account")){ + _app_options.api_limit_get_limit_orders_by_account = _options->at("api-limit-get-limit-orders-by-account").as(); + } if(_options->count("api-limit-get-order-book")){ _app_options.api_limit_get_order_book = _options->at("api-limit-get-order-book").as(); } @@ -510,6 +442,9 @@ void application_impl::startup() if( _active_plugins.find( "market_history" ) != _active_plugins.end() ) _app_options.has_market_history_plugin = true; + if( _active_plugins.find( "api_helper_indexes" ) != _active_plugins.end() ) + _app_options.has_api_helper_indexes_plugin = true; + if( _options->count("api-access") ) { fc::path api_access_file = _options->at("api-access").as(); @@ -1076,6 +1011,8 @@ void application::set_program_options(boost::program_options::options_descriptio "For database_api_impl::list_assets and get_assets_by_issuer to set max limit value") ("api-limit-get-limit-orders",boost::program_options::value()->default_value(300), "For database_api_impl::get_limit_orders to set max limit value") + ("api-limit-get-limit-orders-by-account",boost::program_options::value()->default_value(101), + "For database_api_impl::get_limit_orders_by_account to set max limit value") ("api-limit-get-order-book",boost::program_options::value()->default_value(50), "For database_api_impl::get_order_book to set max limit value") ("api-limit-lookup-accounts",boost::program_options::value()->default_value(1000), diff --git a/libraries/app/application_impl.hxx b/libraries/app/application_impl.hxx index 175648e10f..accc8fe4f1 100644 --- a/libraries/app/application_impl.hxx +++ b/libraries/app/application_impl.hxx @@ -22,8 +22,6 @@ class application_impl : public net::node_delegate void reset_p2p_node(const fc::path& data_dir); - std::vector resolve_string_to_ip_endpoints(const std::string& endpoint_string); - void new_connection( const fc::http::websocket_connection_ptr& c ); void reset_websocket_server(); diff --git a/libraries/app/database_api.cpp b/libraries/app/database_api.cpp index adbe87fc7f..8ebaeaa309 100644 --- a/libraries/app/database_api.cpp +++ b/libraries/app/database_api.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include @@ -334,8 +335,15 @@ vector> database_api::get_key_references( vector> database_api_impl::get_key_references( vector keys )const { - uint64_t api_limit_get_key_references=_app_options->api_limit_get_key_references; - FC_ASSERT(keys.size() <= api_limit_get_key_references); + // api_helper_indexes plugin is required for accessing the secondary index + FC_ASSERT( _app_options && _app_options->has_api_helper_indexes_plugin, + "api_helper_indexes plugin is not enabled on this server." ); + + const auto configured_limit = _app_options->api_limit_get_key_references; + FC_ASSERT( keys.size() <= configured_limit, + "Number of querying keys can not be greater than ${configured_limit}", + ("configured_limit", configured_limit) ); + const auto& idx = _db.get_index_type(); const auto& aidx = dynamic_cast(idx); const auto& refs = aidx.get_secondary_index(); @@ -398,6 +406,11 @@ bool database_api_impl::is_public_key_registered(string public_key) const // An invalid public key was detected return false; } + + // api_helper_indexes plugin is required for accessing the secondary index + FC_ASSERT( _app_options && _app_options->has_api_helper_indexes_plugin, + "api_helper_indexes plugin is not enabled on this server." ); + const auto& idx = _db.get_index_type(); const auto& aidx = dynamic_cast(idx); const auto& refs = aidx.get_secondary_index(); @@ -451,10 +464,11 @@ std::map database_api::get_full_accounts( const vector database_api_impl::get_full_accounts( const vector& names_or_ids, optional subscribe ) { - FC_ASSERT( names_or_ids.size() <= _app_options->api_limit_get_full_accounts ); - - const auto& proposal_idx = _db.get_index_type< primary_index< proposal_index > >(); - const auto& proposals_by_account = proposal_idx.get_secondary_index(); + FC_ASSERT( _app_options, "Internal error" ); + const auto configured_limit = _app_options->api_limit_get_full_accounts; + FC_ASSERT( names_or_ids.size() <= configured_limit, + "Number of querying accounts can not be greater than ${configured_limit}", + ("configured_limit", configured_limit) ); bool to_subscribe = get_whether_to_subscribe( subscribe ); @@ -491,26 +505,33 @@ std::map database_api_impl::get_full_accounts( const size_t api_limit_get_full_accounts_lists = static_cast( _app_options->api_limit_get_full_accounts_lists ); - // Add the account's proposals - auto required_approvals_itr = proposals_by_account._account_to_proposals.find( account->id ); - if( required_approvals_itr != proposals_by_account._account_to_proposals.end() ) + // Add the account's proposals (if the data is available) + if( _app_options && _app_options->has_api_helper_indexes_plugin ) { - acnt.proposals.reserve( std::min(required_approvals_itr->second.size(), - api_limit_get_full_accounts_lists) ); - for( auto proposal_id : required_approvals_itr->second ) + const auto& proposal_idx = _db.get_index_type< primary_index< proposal_index > >(); + const auto& proposals_by_account = proposal_idx.get_secondary_index< + graphene::chain::required_approval_index>(); + + auto required_approvals_itr = proposals_by_account._account_to_proposals.find( account->id ); + if( required_approvals_itr != proposals_by_account._account_to_proposals.end() ) { - if(acnt.proposals.size() >= api_limit_get_full_accounts_lists) { - acnt.more_data_available.proposals = true; - break; + acnt.proposals.reserve( std::min(required_approvals_itr->second.size(), + api_limit_get_full_accounts_lists) ); + for( auto proposal_id : required_approvals_itr->second ) + { + if(acnt.proposals.size() >= api_limit_get_full_accounts_lists) { + acnt.more_data_available.proposals = true; + break; + } + acnt.proposals.push_back(proposal_id(_db)); } - acnt.proposals.push_back(proposal_id(_db)); } } // 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; @@ -640,6 +661,10 @@ vector database_api::get_account_references( const std::string vector database_api_impl::get_account_references( const std::string account_id_or_name )const { + // api_helper_indexes plugin is required for accessing the secondary index + FC_ASSERT( _app_options && _app_options->has_api_helper_indexes_plugin, + "api_helper_indexes plugin is not enabled on this server." ); + const auto& idx = _db.get_index_type(); const auto& aidx = dynamic_cast(idx); const auto& refs = aidx.get_secondary_index(); @@ -684,7 +709,12 @@ map database_api_impl::lookup_accounts( const string& lo uint32_t limit, optional subscribe )const { - FC_ASSERT( limit <= _app_options->api_limit_lookup_accounts ); + FC_ASSERT( _app_options, "Internal error" ); + const auto configured_limit = _app_options->api_limit_lookup_accounts; + FC_ASSERT( limit <= configured_limit, + "limit can not be greater than ${configured_limit}", + ("configured_limit", configured_limit) ); + const auto& accounts_by_name = _db.get_index_type().indices().get(); map result; @@ -738,7 +768,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 @@ -870,8 +900,11 @@ vector database_api::list_assets(const string& lower_boun vector database_api_impl::list_assets(const string& lower_bound_symbol, uint32_t limit)const { - uint64_t api_limit_get_assets = _app_options->api_limit_get_assets; - FC_ASSERT( limit <= api_limit_get_assets ); + FC_ASSERT( _app_options, "Internal error" ); + const auto configured_limit = _app_options->api_limit_get_assets; + FC_ASSERT( limit <= configured_limit, + "limit can not be greater than ${configured_limit}", + ("configured_limit", configured_limit) ); const auto& assets_by_symbol = _db.get_index_type().indices().get(); vector result; @@ -907,8 +940,11 @@ vector database_api::get_assets_by_issuer(const std::stri vector database_api_impl::get_assets_by_issuer(const std::string& issuer_name_or_id, asset_id_type start, uint32_t limit)const { - uint64_t api_limit_get_assets = _app_options->api_limit_get_assets; - FC_ASSERT( limit <= api_limit_get_assets ); + FC_ASSERT( _app_options, "Internal error" ); + const auto configured_limit = _app_options->api_limit_get_assets; + FC_ASSERT( limit <= configured_limit, + "limit can not be greater than ${configured_limit}", + ("configured_limit", configured_limit) ); vector result; const account_id_type account = get_account_from_string(issuer_name_or_id)->id; @@ -962,8 +998,11 @@ vector database_api::get_limit_orders(std::string a, std::st vector database_api_impl::get_limit_orders( const std::string& a, const std::string& b, uint32_t limit )const { - uint64_t api_limit_get_limit_orders=_app_options->api_limit_get_limit_orders; - FC_ASSERT( limit <= api_limit_get_limit_orders ); + FC_ASSERT( _app_options, "Internal error" ); + const auto configured_limit = _app_options->api_limit_get_limit_orders; + FC_ASSERT( limit <= configured_limit, + "limit can not be greater than ${configured_limit}", + ("configured_limit", configured_limit) ); const asset_id_type asset_a_id = get_asset_from_string(a)->id; const asset_id_type asset_b_id = get_asset_from_string(b)->id; @@ -971,6 +1010,46 @@ vector database_api_impl::get_limit_orders( const std::strin return get_limit_orders(asset_a_id, asset_b_id, limit); } +vector database_api::get_limit_orders_by_account( const string& account_name_or_id, + optional limit, optional start_id ) +{ + return my->get_limit_orders_by_account( account_name_or_id, limit, start_id ); +} + +vector database_api_impl::get_limit_orders_by_account( const string& account_name_or_id, + optional olimit, optional ostart_id ) +{ + uint32_t limit = olimit.valid() ? *olimit : 101; + + FC_ASSERT( _app_options, "Internal error" ); + const auto configured_limit = _app_options->api_limit_get_limit_orders_by_account; + FC_ASSERT( limit <= configured_limit, + "limit can not be greater than ${configured_limit}", + ("configured_limit", configured_limit) ); + + vector results; + + const account_object* account = get_account_from_string(account_name_or_id); + if (account == nullptr) + return results; + + limit_order_id_type start_id = ostart_id.valid() ? *ostart_id : limit_order_id_type(); + + const auto& index_by_account = _db.get_index_type().indices().get(); + auto lower_itr = index_by_account.lower_bound( std::make_tuple( account->id, start_id ) ); + auto upper_itr = index_by_account.upper_bound( account->id ); + + results.reserve( limit ); + uint32_t count = 0; + for ( ; lower_itr != upper_itr && count < limit; ++lower_itr, ++count) + { + const limit_order_object &order = *lower_itr; + results.emplace_back(order); + } + + return results; +} + vector database_api::get_account_limit_orders( const string& account_name_or_id, const string &base, const string "e, uint32_t limit, optional ostart_id, optional ostart_price ) @@ -982,7 +1061,11 @@ vector database_api_impl::get_account_limit_orders( const string& account_name_or_id, const string &base, const string "e, uint32_t limit, optional ostart_id, optional ostart_price ) { - FC_ASSERT( limit <= _app_options->api_limit_get_account_limit_orders ); + FC_ASSERT( _app_options, "Internal error" ); + const auto configured_limit = _app_options->api_limit_get_account_limit_orders; + FC_ASSERT( limit <= configured_limit, + "limit can not be greater than ${configured_limit}", + ("configured_limit", configured_limit) ); vector results; uint32_t count = 0; @@ -1003,9 +1086,9 @@ vector database_api_impl::get_account_limit_orders( FC_ASSERT(ostart_price->quote.asset_id == quote_id, "Quote asset inconsistent with start price"); } - const auto& index_by_account = _db.get_index_type().indices().get(); - limit_order_multi_index_type::index::type::const_iterator lower_itr; - limit_order_multi_index_type::index::type::const_iterator upper_itr; + const auto& index_by_account = _db.get_index_type().indices().get(); + limit_order_multi_index_type::index::type::const_iterator lower_itr; + limit_order_multi_index_type::index::type::const_iterator upper_itr; // if both order_id and price are invalid, query the first page if ( !ostart_id.valid() && !ostart_price.valid() ) @@ -1066,8 +1149,11 @@ vector database_api::get_call_orders(const std::string& a, ui vector database_api_impl::get_call_orders(const std::string& a, uint32_t limit)const { - uint64_t api_limit_get_call_orders = _app_options->api_limit_get_call_orders; - FC_ASSERT( limit <= api_limit_get_call_orders ); + FC_ASSERT( _app_options, "Internal error" ); + const auto configured_limit = _app_options->api_limit_get_call_orders; + FC_ASSERT( limit <= configured_limit, + "limit can not be greater than ${configured_limit}", + ("configured_limit", configured_limit) ); const asset_object* mia = get_asset_from_string(a); const auto& call_index = _db.get_index_type().indices().get(); @@ -1093,8 +1179,11 @@ vector database_api::get_call_orders_by_account(const std::st vector database_api_impl::get_call_orders_by_account(const std::string& account_name_or_id, asset_id_type start, uint32_t limit)const { - uint64_t api_limit_get_call_orders = _app_options->api_limit_get_call_orders; - FC_ASSERT( limit <= api_limit_get_call_orders ); + FC_ASSERT( _app_options, "Internal error" ); + const auto configured_limit = _app_options->api_limit_get_call_orders; + FC_ASSERT( limit <= configured_limit, + "limit can not be greater than ${configured_limit}", + ("configured_limit", configured_limit) ); vector result; const account_id_type account = get_account_from_string(account_name_or_id)->id; @@ -1116,8 +1205,11 @@ vector database_api::get_settle_orders(const std::strin vector database_api_impl::get_settle_orders(const std::string& a, uint32_t limit)const { - uint64_t api_limit_get_settle_orders = _app_options->api_limit_get_settle_orders; - FC_ASSERT( limit <= api_limit_get_settle_orders ); + FC_ASSERT( _app_options, "Internal error" ); + const auto configured_limit = _app_options->api_limit_get_settle_orders; + FC_ASSERT( limit <= configured_limit, + "limit can not be greater than ${configured_limit}", + ("configured_limit", configured_limit) ); const asset_id_type asset_a_id = get_asset_from_string(a)->id; const auto& settle_index = _db.get_index_type().indices().get(); @@ -1147,8 +1239,11 @@ vector database_api_impl::get_settle_orders_by_account( force_settlement_id_type start, uint32_t limit )const { - uint64_t api_limit_get_settle_orders = _app_options->api_limit_get_settle_orders; - FC_ASSERT( limit <= api_limit_get_settle_orders ); + FC_ASSERT( _app_options, "Internal error" ); + const auto configured_limit = _app_options->api_limit_get_settle_orders; + FC_ASSERT( limit <= configured_limit, + "limit can not be greater than ${configured_limit}", + ("configured_limit", configured_limit) ); vector result; const account_id_type account = get_account_from_string(account_name_or_id)->id; @@ -1197,7 +1292,12 @@ vector database_api::get_collateral_bids( const std::stri vector database_api_impl::get_collateral_bids( const std::string& asset, uint32_t limit, uint32_t skip )const { try { - FC_ASSERT( limit <= _app_options->api_limit_get_collateral_bids ); + FC_ASSERT( _app_options, "Internal error" ); + const auto configured_limit = _app_options->api_limit_get_collateral_bids; + FC_ASSERT( limit <= configured_limit, + "limit can not be greater than ${configured_limit}", + ("configured_limit", configured_limit) ); + const asset_id_type asset_id = get_asset_from_string(asset)->id; const asset_object& swan = asset_id(_db); FC_ASSERT( swan.is_market_issued() ); @@ -1313,8 +1413,12 @@ order_book database_api::get_order_book( const string& base, const string& quote order_book database_api_impl::get_order_book( const string& base, const string& quote, unsigned limit )const { - FC_ASSERT( limit <= _app_options->api_limit_get_order_book ); - + FC_ASSERT( _app_options, "Internal error" ); + const auto configured_limit = _app_options->api_limit_get_order_book; + FC_ASSERT( limit <= configured_limit, + "limit can not be greater than ${configured_limit}", + ("configured_limit", configured_limit) ); + order_book result; result.base = base; result.quote = quote; @@ -1363,7 +1467,10 @@ vector database_api_impl::get_top_markets(uint32_t limit)const { FC_ASSERT( _app_options && _app_options->has_market_history_plugin, "Market history plugin is not enabled." ); - FC_ASSERT( limit <= _app_options->api_limit_get_top_markets ); + const auto configured_limit = _app_options->api_limit_get_top_markets; + FC_ASSERT( limit <= configured_limit, + "limit can not be greater than ${configured_limit}", + ("configured_limit", configured_limit) ); const auto& volume_idx = _db.get_index_type().indices().get(); auto itr = volume_idx.rbegin(); @@ -1401,7 +1508,10 @@ vector database_api_impl::get_trade_history( const string& base, { FC_ASSERT( _app_options && _app_options->has_market_history_plugin, "Market history plugin is not enabled." ); - FC_ASSERT( limit <= _app_options->api_limit_get_trade_history ); + const auto configured_limit = _app_options->api_limit_get_trade_history; + FC_ASSERT( limit <= configured_limit, + "limit can not be greater than ${configured_limit}", + ("configured_limit", configured_limit) ); auto assets = lookup_asset_symbols( {base, quote} ); FC_ASSERT( assets[0], "Invalid base asset symbol: ${s}", ("s",base) ); @@ -1493,7 +1603,11 @@ vector database_api_impl::get_trade_history_by_sequence( { FC_ASSERT( _app_options && _app_options->has_market_history_plugin, "Market history plugin is not enabled." ); - FC_ASSERT( limit <= _app_options->api_limit_get_trade_history_by_sequence ); + const auto configured_limit = _app_options->api_limit_get_trade_history_by_sequence; + FC_ASSERT( limit <= configured_limit, + "limit can not be greater than ${configured_limit}", + ("configured_limit", configured_limit) ); + FC_ASSERT( start >= 0 ); int64_t start_seq = -start; @@ -1627,7 +1741,12 @@ map database_api::lookup_witness_accounts( const string map database_api_impl::lookup_witness_accounts( const string& lower_bound_name, uint32_t limit )const { - FC_ASSERT( limit <= _app_options->api_limit_lookup_witness_accounts ); + FC_ASSERT( _app_options, "Internal error" ); + const auto configured_limit = _app_options->api_limit_lookup_witness_accounts; + FC_ASSERT( limit <= configured_limit, + "limit can not be greater than ${configured_limit}", + ("configured_limit", configured_limit) ); + const auto& witnesses_by_id = _db.get_index_type().indices().get(); // we want to order witnesses by account name, but that name is in the account object @@ -1709,7 +1828,12 @@ map database_api::lookup_committee_member_acco map database_api_impl::lookup_committee_member_accounts( const string& lower_bound_name, uint32_t limit )const { - FC_ASSERT( limit <= _app_options->api_limit_lookup_committee_member_accounts ); + FC_ASSERT( _app_options, "Internal error" ); + const auto configured_limit = _app_options->api_limit_lookup_committee_member_accounts; + FC_ASSERT( limit <= configured_limit, + "limit can not be greater than ${configured_limit}", + ("configured_limit", configured_limit) ); + const auto& committee_members_by_id = _db.get_index_type().indices().get(); // we want to order committee_members by account name, but that name is in the account object @@ -1747,39 +1871,56 @@ uint64_t database_api_impl::get_committee_count()const // // ////////////////////////////////////////////////////////////////////// -vector database_api::get_all_workers()const +vector database_api::get_all_workers( const optional is_expired )const { - return my->get_all_workers(); + return my->get_all_workers( is_expired ); } -vector database_api_impl::get_all_workers()const +vector database_api_impl::get_all_workers( const optional is_expired )const { - vector result; - const auto& workers_idx = _db.get_index_type().indices().get(); - for( const auto& w : workers_idx ) - { - result.push_back( w ); - } - return result; + vector result; + + if( !is_expired.valid() ) // query for all workers + { + const auto& workers_idx = _db.get_index_type().indices().get(); + result.reserve( workers_idx.size() ); + for( const auto& w : workers_idx ) + { + result.push_back( w ); + } + } + else // query for workers that are expired only or not expired only + { + const time_point_sec now = _db.head_block_time(); + const auto& workers_idx = _db.get_index_type().indices().get(); + auto itr = *is_expired ? workers_idx.begin() : workers_idx.lower_bound( now ); + auto end = *is_expired ? workers_idx.upper_bound( now ) : workers_idx.end(); + for( ; itr != end; ++itr ) + { + result.push_back( *itr ); + } + } + + return result; } -vector> database_api::get_workers_by_account(const std::string account_id_or_name)const +vector database_api::get_workers_by_account(const std::string account_id_or_name)const { - return my->get_workers_by_account( account_id_or_name ); + return my->get_workers_by_account( account_id_or_name ); } -vector> database_api_impl::get_workers_by_account(const std::string account_id_or_name)const +vector database_api_impl::get_workers_by_account(const std::string account_id_or_name)const { - vector> result; + vector result; const auto& workers_idx = _db.get_index_type().indices().get(); const account_id_type account = get_account_from_string(account_id_or_name)->id; - for( const auto& w : workers_idx ) - { - if( w.worker_account == account ) - result.push_back( w ); - } - return result; + auto range = workers_idx.equal_range(account); + for(auto itr = range.first; itr != range.second; ++itr) + { + result.push_back( *itr ); + } + return result; } uint64_t database_api::get_worker_count()const @@ -1807,7 +1948,11 @@ vector database_api::lookup_vote_ids( const vector& votes vector database_api_impl::lookup_vote_ids( const vector& votes )const { - FC_ASSERT( votes.size() < _app_options->api_limit_lookup_vote_ids ); + FC_ASSERT( _app_options, "Internal error" ); + const auto configured_limit = _app_options->api_limit_lookup_vote_ids; + FC_ASSERT( votes.size() <= configured_limit, + "Number of querying votes can not be greater than ${configured_limit}", + ("configured_limit", configured_limit) ); const auto& witness_idx = _db.get_index_type().indices().get(); const auto& committee_idx = _db.get_index_type().indices().get(); @@ -1904,12 +2049,15 @@ set database_api::get_required_signatures( const signed_transac set database_api_impl::get_required_signatures( const signed_transaction& trx, const flat_set& available_keys )const { - bool allow_non_immediate_owner = ( _db.head_block_time() >= HARDFORK_CORE_584_TIME ); + auto chain_time = _db.head_block_time(); + bool allow_non_immediate_owner = ( chain_time >= HARDFORK_CORE_584_TIME ); + bool ignore_custom_op_reqd_auths = MUST_IGNORE_CUSTOM_OP_REQD_AUTHS( chain_time ); auto result = trx.get_required_signatures( _db.get_chain_id(), available_keys, [&]( account_id_type id ){ return &id(_db).active; }, [&]( account_id_type id ){ return &id(_db).owner; }, allow_non_immediate_owner, + ignore_custom_op_reqd_auths, _db.get_global_properties().parameters.max_authority_depth ); return result; } @@ -1925,34 +2073,36 @@ set
database_api::get_potential_address_signatures( const signed_transa set database_api_impl::get_potential_signatures( const signed_transaction& trx )const { - bool allow_non_immediate_owner = ( _db.head_block_time() >= HARDFORK_CORE_584_TIME ); + auto chain_time = _db.head_block_time(); + bool allow_non_immediate_owner = ( chain_time >= HARDFORK_CORE_584_TIME ); + bool ignore_custom_op_reqd_auths = MUST_IGNORE_CUSTOM_OP_REQD_AUTHS( chain_time ); + set result; - trx.get_required_signatures( - _db.get_chain_id(), - flat_set(), - [&]( account_id_type id ) - { - const auto& auth = id(_db).active; - for( const auto& k : auth.get_keys() ) - result.insert(k); - return &auth; - }, - [&]( account_id_type id ) - { - const auto& auth = id(_db).owner; - for( const auto& k : auth.get_keys() ) - result.insert(k); - return &auth; - }, - allow_non_immediate_owner, - _db.get_global_properties().parameters.max_authority_depth - ); + auto get_active = [this, &result]( account_id_type id ){ + const auto& auth = id( _db ).active; + for( const auto& k : auth.get_keys() ) + result.insert( k ); + return &auth; + }; + auto get_owner = [this, &result]( account_id_type id ){ + const auto& auth = id( _db ).owner; + for( const auto& k : auth.get_keys() ) + result.insert( k ); + return &auth; + }; + + trx.get_required_signatures( _db.get_chain_id(), + flat_set(), + get_active, get_owner, + allow_non_immediate_owner, + ignore_custom_op_reqd_auths, + _db.get_global_properties().parameters.max_authority_depth ); // Insert keys in required "other" authories flat_set required_active; flat_set required_owner; vector other; - trx.get_required_authorities( required_active, required_owner, other ); + trx.get_required_authorities( required_active, required_owner, other, ignore_custom_op_reqd_auths ); for( const auto& auth : other ) for( const auto& key : auth.get_keys() ) result.insert( key ); @@ -1962,26 +2112,30 @@ set database_api_impl::get_potential_signatures( const signed_t set
database_api_impl::get_potential_address_signatures( const signed_transaction& trx )const { + auto chain_time = _db.head_block_time(); + bool allow_non_immediate_owner = ( chain_time >= HARDFORK_CORE_584_TIME ); + bool ignore_custom_op_reqd_auths = MUST_IGNORE_CUSTOM_OP_REQD_AUTHS( chain_time ); + set
result; - trx.get_required_signatures( - _db.get_chain_id(), - flat_set(), - [&]( account_id_type id ) - { - const auto& auth = id(_db).active; - for( const auto& k : auth.get_addresses() ) - result.insert(k); - return &auth; - }, - [&]( account_id_type id ) - { - const auto& auth = id(_db).owner; - for( const auto& k : auth.get_addresses() ) - result.insert(k); - return &auth; - }, - _db.get_global_properties().parameters.max_authority_depth - ); + auto get_active = [this, &result]( account_id_type id ){ + const auto& auth = id( _db ).active; + for( const auto& k : auth.get_addresses() ) + result.insert( k ); + return &auth; + }; + auto get_owner = [this, &result]( account_id_type id ) { + const auto& auth = id( _db ).owner; + for (const auto& k : auth.get_addresses()) + result.insert( k ); + return &auth; + }; + + trx.get_required_signatures( _db.get_chain_id(), + flat_set(), + get_active, get_owner, + allow_non_immediate_owner, + ignore_custom_op_reqd_auths, + _db.get_global_properties().parameters.max_authority_depth ); return result; } @@ -1996,6 +2150,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; @@ -2021,7 +2177,9 @@ 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; }, - true ); + // 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) { @@ -2139,6 +2297,10 @@ vector database_api::get_proposed_transactions( const std::stri vector database_api_impl::get_proposed_transactions( const std::string account_id_or_name )const { + // api_helper_indexes plugin is required for accessing the secondary index + FC_ASSERT( _app_options && _app_options->has_api_helper_indexes_plugin, + "api_helper_indexes plugin is not enabled on this server." ); + const auto& proposal_idx = _db.get_index_type< primary_index< proposal_index > >(); const auto& proposals_by_account = proposal_idx.get_secondary_index(); @@ -2203,7 +2365,12 @@ vector database_api_impl::get_withdraw_permissions_b withdraw_permission_id_type start, uint32_t limit)const { - FC_ASSERT( limit <= _app_options->api_limit_get_withdraw_permissions_by_giver ); + FC_ASSERT( _app_options, "Internal error" ); + const auto configured_limit = _app_options->api_limit_get_withdraw_permissions_by_giver; + FC_ASSERT( limit <= configured_limit, + "limit can not be greater than ${configured_limit}", + ("configured_limit", configured_limit) ); + vector result; const auto& withdraw_idx = _db.get_index_type().indices().get(); @@ -2232,7 +2399,12 @@ vector database_api_impl::get_withdraw_permissions_b withdraw_permission_id_type start, uint32_t limit)const { - FC_ASSERT( limit <= _app_options->api_limit_get_withdraw_permissions_by_recipient ); + FC_ASSERT( _app_options, "Internal error" ); + const auto configured_limit = _app_options->api_limit_get_withdraw_permissions_by_recipient; + FC_ASSERT( limit <= configured_limit, + "limit can not be greater than ${configured_limit}", + ("configured_limit", configured_limit) ); + vector result; const auto& withdraw_idx = _db.get_index_type().indices().get(); @@ -2277,7 +2449,12 @@ vector database_api::get_htlc_by_from( const std::string account_id vector database_api_impl::get_htlc_by_from( const std::string account_id_or_name, htlc_id_type start, uint32_t limit ) const { - FC_ASSERT( limit <= _app_options->api_limit_get_htlc_by ); + FC_ASSERT( _app_options, "Internal error" ); + const auto configured_limit = _app_options->api_limit_get_htlc_by; + FC_ASSERT( limit <= configured_limit, + "limit can not be greater than ${configured_limit}", + ("configured_limit", configured_limit) ); + vector result; const auto& htlc_idx = _db.get_index_type< htlc_index >().indices().get< by_from_id >(); @@ -2302,8 +2479,12 @@ vector database_api::get_htlc_by_to( const std::string account_id_o vector database_api_impl::get_htlc_by_to( const std::string account_id_or_name, htlc_id_type start, uint32_t limit ) const { + FC_ASSERT( _app_options, "Internal error" ); + const auto configured_limit = _app_options->api_limit_get_htlc_by; + FC_ASSERT( limit <= configured_limit, + "limit can not be greater than ${configured_limit}", + ("configured_limit", configured_limit) ); - FC_ASSERT( limit <= _app_options->api_limit_get_htlc_by ); vector result; const auto& htlc_idx = _db.get_index_type< htlc_index >().indices().get< by_to_id >(); @@ -2326,7 +2507,11 @@ vector database_api::list_htlcs(const htlc_id_type start, uint32_t vector database_api_impl::list_htlcs(const htlc_id_type start, uint32_t limit) const { - FC_ASSERT( limit <= _app_options->api_limit_list_htlcs ); + FC_ASSERT( _app_options, "Internal error" ); + const auto configured_limit = _app_options->api_limit_list_htlcs; + FC_ASSERT( limit <= configured_limit, + "limit can not be greater than ${configured_limit}", + ("configured_limit", configured_limit) ); vector result; const auto& htlc_idx = _db.get_index_type().indices().get(); @@ -2408,8 +2593,11 @@ vector> database_api_impl::get_assets( const vec vector database_api_impl::get_limit_orders( const asset_id_type a, const asset_id_type b, const uint32_t limit )const { - uint64_t api_limit_get_limit_orders=_app_options->api_limit_get_limit_orders; - FC_ASSERT( limit <= api_limit_get_limit_orders ); + FC_ASSERT( _app_options, "Internal error" ); + const auto configured_limit = _app_options->api_limit_get_limit_orders; + FC_ASSERT( limit <= configured_limit, + "limit can not be greater than ${configured_limit}", + ("configured_limit", configured_limit) ); const auto& limit_order_idx = _db.get_index_type(); const auto& limit_price_idx = limit_order_idx.indices().get(); diff --git a/libraries/app/database_api_impl.hxx b/libraries/app/database_api_impl.hxx index e2e7c376a4..4f544da427 100644 --- a/libraries/app/database_api_impl.hxx +++ b/libraries/app/database_api_impl.hxx @@ -101,6 +101,9 @@ class database_api_impl : public std::enable_shared_from_this // Markets / feeds vector get_limit_orders( const std::string& a, const std::string& b, uint32_t limit)const; + vector get_limit_orders_by_account( const string& account_name_or_id, + optional limit, + optional start_id ); vector get_account_limit_orders( const string& account_name_or_id, const string &base, const string "e, uint32_t limit, @@ -150,8 +153,8 @@ class database_api_impl : public std::enable_shared_from_this uint64_t get_committee_count()const; // Workers - vector get_all_workers()const; - vector> get_workers_by_account(const std::string account_id_or_name)const; + vector get_all_workers( const optional is_expired = optional() )const; + vector get_workers_by_account(const std::string account_id_or_name)const; uint64_t get_worker_count()const; // Votes diff --git a/libraries/app/include/graphene/app/api.hpp b/libraries/app/include/graphene/app/api.hpp index 191380091c..92c08c266a 100644 --- a/libraries/app/include/graphene/app/api.hpp +++ b/libraries/app/include/graphene/app/api.hpp @@ -151,10 +151,10 @@ namespace graphene { namespace app { */ history_operation_detail get_account_history_by_operations( const std::string account_id_or_name, - vector operation_types, + flat_set operation_types, uint32_t start, unsigned limit - ); + )const; /** * @brief Get only asked operations relevant to the specified account @@ -534,12 +534,12 @@ namespace graphene { namespace app { /** * @brief Get all stored objects of an account in a particular catalog * - * @param account Account name to get info from + * @param account The account ID or name to get info from * @param catalog Category classification. Each account can store multiple catalogs. * * @return The vector of objects of the account or empty */ - vector get_storage_info(std::string account, std::string catalog)const; + vector get_storage_info(std::string account_id_or_name, std::string catalog)const; private: application& _app; @@ -598,7 +598,7 @@ namespace graphene { namespace app { /// @brief Retrieve the debug API (if available) fc::api debug()const; /// @brief Retrieve the custom operations API - fc::api custom()const; + fc::api custom_operations()const; /// @brief Called to enable an API, not reflected. void enable_api( const string& api_name ); @@ -695,5 +695,5 @@ FC_API(graphene::app::login_api, (asset) (orders) (debug) - (custom) + (custom_operations) ) diff --git a/libraries/app/include/graphene/app/api_objects.hpp b/libraries/app/include/graphene/app/api_objects.hpp index af7a96e1ab..ff7f027f09 100644 --- a/libraries/app/include/graphene/app/api_objects.hpp +++ b/libraries/app/include/graphene/app/api_objects.hpp @@ -100,7 +100,11 @@ namespace graphene { namespace app { string quote; string latest; string lowest_ask; + string lowest_ask_base_size; + string lowest_ask_quote_size; string highest_bid; + string highest_bid_base_size; + string highest_bid_quote_size; string percent_change; string base_volume; string quote_volume; @@ -178,7 +182,8 @@ FC_REFLECT( graphene::app::full_account, FC_REFLECT( graphene::app::order, (price)(quote)(base) ); FC_REFLECT( graphene::app::order_book, (base)(quote)(bids)(asks) ); FC_REFLECT( graphene::app::market_ticker, - (time)(base)(quote)(latest)(lowest_ask)(highest_bid)(percent_change)(base_volume)(quote_volume) ); + (time)(base)(quote)(latest)(lowest_ask)(lowest_ask_base_size)(lowest_ask_quote_size) + (highest_bid)(highest_bid_base_size)(highest_bid_quote_size)(percent_change)(base_volume)(quote_volume) ); FC_REFLECT( graphene::app::market_volume, (time)(base)(quote)(base_volume)(quote_volume) ); FC_REFLECT( graphene::app::market_trade, (sequence)(date)(price)(amount)(value)(side1_account_id)(side2_account_id) ); diff --git a/libraries/app/include/graphene/app/application.hpp b/libraries/app/include/graphene/app/application.hpp index b9c37fb6c6..96e2d431ec 100644 --- a/libraries/app/include/graphene/app/application.hpp +++ b/libraries/app/include/graphene/app/application.hpp @@ -39,7 +39,10 @@ namespace graphene { namespace app { { public: bool enable_subscribe_to_all = false; + + bool has_api_helper_indexes_plugin = false; bool has_market_history_plugin = false; + uint64_t api_limit_get_account_history_operations = 100; uint64_t api_limit_get_account_history = 100; uint64_t api_limit_get_grouped_limit_orders = 101; @@ -54,6 +57,7 @@ namespace graphene { namespace app { uint64_t api_limit_get_settle_orders = 300; uint64_t api_limit_get_assets = 101; uint64_t api_limit_get_limit_orders = 300; + uint64_t api_limit_get_limit_orders_by_account = 101; uint64_t api_limit_get_order_book = 50; uint64_t api_limit_list_htlcs = 100; uint64_t api_limit_lookup_accounts = 1000; diff --git a/libraries/app/include/graphene/app/database_api.hpp b/libraries/app/include/graphene/app/database_api.hpp index 1960e64824..4934f9ebc9 100644 --- a/libraries/app/include/graphene/app/database_api.hpp +++ b/libraries/app/include/graphene/app/database_api.hpp @@ -431,6 +431,25 @@ class database_api */ vector get_limit_orders(std::string a, std::string b, uint32_t limit)const; + /** + * @brief Fetch open limit orders in all markets relevant to the specified account, ordered by ID + * + * @param account_name_or_id The name or ID of an account to retrieve + * @param limit The limitation of items each query can fetch, not greater than a configured value + * @param start_id Start order id, fetch orders whose IDs are greater than or equal to this order + * + * @return List of limit orders of the specified account + * + * @note + * 1. if @p account_name_or_id cannot be tied to an account, an error will be returned + * 2. @p limit can be omitted or be null, if so the default value 101 will be used + * 3. @p start_id can be omitted or be null, if so the api will return the "first page" of orders + * 4. can only omit one or more arguments in the end of the list, but not one or more in the middle + */ + vector get_limit_orders_by_account( const string& account_name_or_id, + optional limit = 101, + optional start_id = optional() ); + /** * @brief Fetch all orders relevant to the specified account and specified market, result orders * are sorted descendingly by price @@ -446,7 +465,7 @@ class database_api * @return List of orders from @p account_name_or_id to the corresponding account * * @note - * 1. if @p account_name_or_id cannot be tied to an account, empty result will be returned + * 1. if @p account_name_or_id cannot be tied to an account, an error will be returned * 2. @p ostart_id and @p ostart_price can be empty, if so the api will return the "first page" of orders; * if @p ostart_id is specified, its price will be used to do page query preferentially, * otherwise the @p ostart_price will be used; @@ -674,18 +693,19 @@ class database_api /////////////////////// /** - * @brief Get all workers - * @return All the workers + * @brief Get workers + * @param is_expired null for all workers, true for expired workers only, false for non-expired workers only + * @return A list of worker objects * */ - vector get_all_workers()const; + vector get_all_workers( const optional is_expired = optional() )const; /** * @brief Get the workers owned by a given account * @param account_name_or_id The name or ID of the account whose worker should be retrieved * @return A list of worker objects owned by the account */ - vector> get_workers_by_account(const std::string account_name_or_id)const; + vector get_workers_by_account(const std::string account_name_or_id)const; /** * @brief Get the total number of workers registered with the blockchain @@ -952,6 +972,7 @@ FC_API(graphene::app::database_api, // Markets / feeds (get_order_book) (get_limit_orders) + (get_limit_orders_by_account) (get_account_limit_orders) (get_call_orders) (get_call_orders_by_account) diff --git a/libraries/chain/CMakeLists.txt b/libraries/chain/CMakeLists.txt index df07034a28..f6d4cdf478 100644 --- a/libraries/chain/CMakeLists.txt +++ b/libraries/chain/CMakeLists.txt @@ -51,6 +51,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/asset_evaluator.cpp b/libraries/chain/asset_evaluator.cpp index d3d4df1254..0f32a9a32b 100644 --- a/libraries/chain/asset_evaluator.cpp +++ b/libraries/chain/asset_evaluator.cpp @@ -32,19 +32,51 @@ #include -#include - namespace graphene { namespace chain { +namespace detail { + + // TODO review and remove code below and links to it after hf_1774 + void check_asset_options_hf_1774(const fc::time_point_sec& block_time, const asset_options& options) + { + if( block_time < HARDFORK_1774_TIME ) + { + FC_ASSERT( !options.extensions.value.reward_percent.valid() || + *options.extensions.value.reward_percent < GRAPHENE_100_PERCENT, + "Asset extension reward percent must be less than 100% till HARDFORK_1774_TIME!"); + } + } + + // TODO review and remove code below and links to it after HARDFORK_BSIP_81_TIME + void check_asset_options_hf_bsip81(const fc::time_point_sec& block_time, const asset_options& options) + { + if (block_time < HARDFORK_BSIP_81_TIME) { + // Taker fees should not be set until activation of BSIP81 + FC_ASSERT(!options.extensions.value.taker_fee_percent.valid(), + "Taker fee percent should not be defined before HARDFORK_BSIP_81_TIME"); + } + } + + void check_asset_claim_fees_hardfork_87_74_collatfee(const fc::time_point_sec& block_time, const asset_claim_fees_operation& op) + { + // HF_REMOVABLE: Following hardfork check should be removable after hardfork date passes: + FC_ASSERT( !op.extensions.value.claim_from_asset_id.valid() || + block_time >= HARDFORK_CORE_BSIP_87_74_COLLATFEE_TIME, + "Collateral-denominated fees are not yet active and therefore cannot be claimed." ); + } + +} // graphene::chain::detail void_result asset_create_evaluator::do_evaluate( const asset_create_operation& op ) { try { - database& d = db(); + const database& d = db(); const auto& chain_parameters = d.get_global_properties().parameters; FC_ASSERT( op.common_options.whitelist_authorities.size() <= chain_parameters.maximum_asset_whitelist_authorities ); FC_ASSERT( op.common_options.blacklist_authorities.size() <= chain_parameters.maximum_asset_whitelist_authorities ); + detail::check_asset_options_hf_1774(d.head_block_time(), op.common_options); + // Check that all authorities do exist for( auto id : op.common_options.whitelist_authorities ) d.get_object(id); @@ -55,15 +87,18 @@ void_result asset_create_evaluator::do_evaluate( const asset_create_operation& o auto asset_symbol_itr = asset_indx.find( op.symbol ); FC_ASSERT( asset_symbol_itr == asset_indx.end() ); + // Define now from the current block time + const time_point_sec now = d.head_block_time(); // This must remain due to "BOND.CNY" being allowed before this HF - if( d.head_block_time() > HARDFORK_385_TIME ) + if( now > HARDFORK_385_TIME ) { auto dotpos = op.symbol.rfind( '.' ); if( dotpos != std::string::npos ) { auto prefix = op.symbol.substr( 0, dotpos ); auto asset_symbol_itr = asset_indx.find( prefix ); - FC_ASSERT( asset_symbol_itr != asset_indx.end(), "Asset ${s} may only be created by issuer of ${p}, but ${p} has not been registered", + FC_ASSERT( asset_symbol_itr != asset_indx.end(), + "Asset ${s} may only be created by issuer of asset ${p}, but asset ${p} has not been created", ("s",op.symbol)("p",prefix) ); FC_ASSERT( asset_symbol_itr->issuer == op.issuer, "Asset ${s} may only be created by issuer of ${p}, ${i}", ("s",op.symbol)("p",prefix)("i", op.issuer(d).name) ); @@ -93,6 +128,9 @@ void_result asset_create_evaluator::do_evaluate( const asset_create_operation& o FC_ASSERT( op.precision == op.bitasset_opts->short_backing_asset(d).precision ); } + // Check the taker fee percent + detail::check_asset_options_hf_bsip81(now, op.common_options); + return void_result(); } FC_CAPTURE_AND_RETHROW( (op) ) } @@ -253,7 +291,8 @@ static void validate_new_issuer( const database& d, const asset_object& a, accou void_result asset_update_evaluator::do_evaluate(const asset_update_operation& o) { try { - database& d = db(); + const database& d = db(); + const time_point_sec now = d.head_block_time(); const asset_object& a = o.asset_to_update(d); auto a_copy = a; @@ -262,11 +301,13 @@ void_result asset_update_evaluator::do_evaluate(const asset_update_operation& o) if( o.new_issuer ) { - FC_ASSERT( d.head_block_time() < HARDFORK_CORE_199_TIME, + FC_ASSERT( now < HARDFORK_CORE_199_TIME, "Since Hardfork #199, updating issuer requires the use of asset_update_issuer_operation."); validate_new_issuer( d, a, *o.new_issuer ); } + detail::check_asset_options_hf_1774(d.head_block_time(), o.new_options); + if( a.dynamic_asset_data_id(d).current_supply != 0 ) { // new issuer_permissions must be subset of old issuer permissions @@ -292,6 +333,9 @@ void_result asset_update_evaluator::do_evaluate(const asset_update_operation& o) for( auto id : o.new_options.blacklist_authorities ) d.get_object(id); + // Check the taker fee percent + detail::check_asset_options_hf_bsip81(now, o.new_options); + return void_result(); } FC_CAPTURE_AND_RETHROW((o)) } @@ -420,6 +464,9 @@ void_result asset_update_bitasset_evaluator::do_evaluate(const asset_update_bita FC_ASSERT( asset_obj.dynamic_asset_data_id(d).current_supply == 0, "Cannot update a bitasset if there is already a current supply." ); + FC_ASSERT( asset_obj.dynamic_asset_data_id(d).accumulated_collateral_fees == 0, + "Must claim collateral-denominated fees before changing backing asset." ); + const asset_object& new_backing_asset = op.new_options.short_backing_asset(d); // check if the asset exists if( after_hf_core_922_931 ) @@ -692,10 +739,10 @@ void_result asset_settle_evaluator::do_evaluate(const asset_settle_evaluator::op if( bitasset.is_prediction_market ) FC_ASSERT( bitasset.has_settlement(), "global settlement must occur before force settling a prediction market" ); else if( bitasset.current_feed.settlement_price.is_null() - && ( d.head_block_time() <= HARDFORK_CORE_216_TIME + && ( d.head_block_time() <= HARDFORK_CORE_216_TIME // TODO check whether the HF check can be removed || !bitasset.has_settlement() ) ) FC_THROW_EXCEPTION(insufficient_feeds, "Cannot force settle with no price feed."); - FC_ASSERT(d.get_balance(d.get(op.account), *asset_to_settle) >= op.amount); + FC_ASSERT( d.get_balance( op.account, op.amount.asset_id ) >= op.amount, "Insufficient balance" ); return void_result(); } FC_CAPTURE_AND_RETHROW( (op) ) } @@ -719,8 +766,7 @@ operation_result asset_settle_evaluator::do_apply(const asset_settle_evaluator:: { if( d.get_dynamic_global_properties().next_maintenance_time > HARDFORK_CORE_184_TIME ) FC_THROW( "Settle amount is too small to receive anything due to rounding" ); - else // TODO remove this warning after hard fork core-184 - wlog( "Something for nothing issue (#184, variant F) occurred at block #${block}", ("block",d.head_block_num()) ); + // else do nothing. Before the hf, something for nothing issue (#184, variant F) could occur } asset pays = op.amount; @@ -739,7 +785,20 @@ operation_result asset_settle_evaluator::do_apply(const asset_settle_evaluator:: obj.settlement_fund -= settled_amount.amount; }); - d.adjust_balance( op.account, settled_amount ); + // The account who settles pays market fees to the issuer of the collateral asset after HF core-1780 + // + // TODO Check whether the HF check can be removed after the HF. + // Note: even if logically it can be removed, perhaps the removal will lead to a small + // performance loss. Needs testing. + if( d.head_block_time() >= HARDFORK_CORE_1780_TIME ) + { + const bool is_maker = false; // Settlement orders are takers + auto issuer_fees = d.pay_market_fees( fee_paying_account, settled_amount.asset_id(d), settled_amount , is_maker ); + settled_amount -= issuer_fees; + } + + if( settled_amount.amount > 0 ) + d.adjust_balance( op.account, settled_amount ); } d.modify( mia_dyn, [&]( asset_dynamic_data_object& obj ){ @@ -873,25 +932,58 @@ void_result asset_publish_feeds_evaluator::do_apply(const asset_publish_feed_ope } FC_CAPTURE_AND_RETHROW((o)) } - +/*** + * @brief evaluator for asset_claim_fees operation + * + * Checks that we are able to claim fees denominated in asset Y (the amount_to_claim asset), + * from some container asset X which is presumed to have accumulated the fees we wish to claim. + * The container asset is either explicitly named in the extensions, or else assumed as the same + * asset as the amount_to_claim asset. Evaluation fails if either (a) operation issuer is not + * the same as the container_asset issuer, or (b) container_asset has no fee bucket for + * amount_to_claim asset, or (c) accumulated fees are insufficient to cover amount claimed. + */ void_result asset_claim_fees_evaluator::do_evaluate( const asset_claim_fees_operation& o ) { try { - FC_ASSERT( o.amount_to_claim.asset_id(db()).issuer == o.issuer, "Asset fees may only be claimed by the issuer" ); + const database& d = db(); + + detail::check_asset_claim_fees_hardfork_87_74_collatfee(d.head_block_time(), o); // HF_REMOVABLE + + container_asset = o.extensions.value.claim_from_asset_id.valid() ? + &(*o.extensions.value.claim_from_asset_id)(d) : &o.amount_to_claim.asset_id(d); + + FC_ASSERT( container_asset->issuer == o.issuer, "Asset fees may only be claimed by the issuer" ); + FC_ASSERT( container_asset->can_accumulate_fee(d,o.amount_to_claim), + "Asset ${a} (${id}) is not backed by asset (${fid}) and does not hold it as fees.", + ("a",container_asset->symbol)("id",container_asset->id)("fid",o.amount_to_claim.asset_id) ); + + container_ddo = &container_asset->dynamic_asset_data_id(d); + + FC_ASSERT( o.amount_to_claim.amount <= ((container_asset->get_id() == o.amount_to_claim.asset_id) ? + container_ddo->accumulated_fees : + container_ddo->accumulated_collateral_fees), + "Attempt to claim more fees than have accumulated within asset ${a} (${id})", + ("a",container_asset->symbol)("id",container_asset->id)("ddo",*container_ddo) ); + return void_result(); } FC_CAPTURE_AND_RETHROW( (o) ) } +/*** + * @brief apply asset_claim_fees operation + */ void_result asset_claim_fees_evaluator::do_apply( const asset_claim_fees_operation& o ) { try { database& d = db(); - const asset_object& a = o.amount_to_claim.asset_id(d); - const asset_dynamic_data_object& addo = a.dynamic_asset_data_id(d); - FC_ASSERT( o.amount_to_claim.amount <= addo.accumulated_fees, "Attempt to claim more fees than have accumulated", ("addo",addo) ); - - d.modify( addo, [&]( asset_dynamic_data_object& _addo ) { - _addo.accumulated_fees -= o.amount_to_claim.amount; - }); + if ( container_asset->get_id() == o.amount_to_claim.asset_id ) { + d.modify( *container_ddo, [&o]( asset_dynamic_data_object& _addo ) { + _addo.accumulated_fees -= o.amount_to_claim.amount; + }); + } else { + d.modify( *container_ddo, [&o]( asset_dynamic_data_object& _addo ) { + _addo.accumulated_collateral_fees -= o.amount_to_claim.amount; + }); + } d.adjust_balance( o.issuer, o.amount_to_claim ); diff --git a/libraries/chain/asset_object.cpp b/libraries/chain/asset_object.cpp index 50e874f858..ce74e934b4 100644 --- a/libraries/chain/asset_object.cpp +++ b/libraries/chain/asset_object.cpp @@ -178,7 +178,7 @@ string asset_object::amount_to_string(share_type amount) const } FC_REFLECT_DERIVED_NO_TYPENAME( graphene::chain::asset_dynamic_data_object, (graphene::db::object), - (current_supply)(confidential_supply)(accumulated_fees)(fee_pool) ) + (current_supply)(confidential_supply)(accumulated_fees)(accumulated_collateral_fees)(fee_pool) ) FC_REFLECT_DERIVED_NO_TYPENAME( graphene::chain::asset_bitasset_data_object, (graphene::db::object), (asset_id) diff --git a/libraries/chain/committee_member_evaluator.cpp b/libraries/chain/committee_member_evaluator.cpp index 90620fc010..0d46163f61 100644 --- a/libraries/chain/committee_member_evaluator.cpp +++ b/libraries/chain/committee_member_evaluator.cpp @@ -72,17 +72,16 @@ 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(), - "Unable to set HTLC parameters until hardfork." ); - 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 be9ba48ede..5d8f957bca 100644 --- a/libraries/chain/db_block.cpp +++ b/libraries/chain/db_block.cpp @@ -690,13 +690,15 @@ processed_transaction database::_apply_transaction(const signed_transaction& trx if( !(skip & skip_transaction_signatures) ) { bool allow_non_immediate_owner = ( head_block_time() >= HARDFORK_CORE_584_TIME ); - auto get_active = [&]( account_id_type id ) { return &id(*this).active; }; - auto get_owner = [&]( account_id_type id ) { return &id(*this).owner; }; - trx.verify_authority( chain_id, - get_active, - get_owner, - allow_non_immediate_owner, - get_global_properties().parameters.max_authority_depth ); + 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, get_custom, allow_non_immediate_owner, + MUST_IGNORE_CUSTOM_OP_REQD_AUTHS(head_block_time()), + get_global_properties().parameters.max_authority_depth); } //Skip all manner of expiration and TaPoS checking if we're on block 1; It's impossible that the transaction is diff --git a/libraries/chain/db_debug.cpp b/libraries/chain/db_debug.cpp index e6ab40fb70..4ac73bc613 100644 --- a/libraries/chain/db_debug.cpp +++ b/libraries/chain/db_debug.cpp @@ -26,6 +26,7 @@ #include #include +#include #include #include #include @@ -46,6 +47,7 @@ void database::debug_dump() const auto& statistics_index = db.get_index_type().indices(); const auto& bids = db.get_index_type().indices(); const auto& settle_index = db.get_index_type().indices(); + const auto& htlcs = db.get_index_type().indices(); map total_balances; map total_debts; share_type core_in_orders; @@ -92,6 +94,8 @@ void database::debug_dump() total_balances[asset_id_type()] += asset_obj.dynamic_asset_data_id(db).fee_pool; // edump((total_balances[asset_obj.id])(asset_obj.dynamic_asset_data_id(db).current_supply ) ); } + for( const auto& htlc : htlcs ) + total_balances[htlc.transfer.asset_id] += htlc.transfer.amount; if( total_balances[asset_id_type()].value != core_asset_data.current_supply.value ) { diff --git a/libraries/chain/db_getter.cpp b/libraries/chain/db_getter.cpp index 3a4ff98c18..fd3265fa41 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,34 @@ 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) { + if (rejected_authorities != nullptr) + 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 8f5d29b6f4..54c80cb086 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 @@ -120,6 +122,9 @@ void database::initialize_evaluators() register_evaluator(); register_evaluator(); register_evaluator(); + register_evaluator(); + register_evaluator(); + register_evaluator(); } void database::initialize_indexes() @@ -143,6 +148,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 fdf756f184..5aa2eb6e1a 100644 --- a/libraries/chain/db_maint.cpp +++ b/libraries/chain/db_maint.cpp @@ -32,6 +32,7 @@ #include #include +#include #include #include #include @@ -44,6 +45,7 @@ #include #include #include +#include namespace graphene { namespace chain { @@ -969,6 +971,22 @@ void process_hf_1465( database& db ) } } +/**** + * @brief a one-time data process to correct current_supply of BTS token in the BitShares mainnet + */ +void process_hf_2103( database& db ) +{ + const balance_object* bal = db.find( balance_id_type( HARDFORK_CORE_2103_BALANCE_ID ) ); + if( bal != nullptr && bal->balance.amount < 0 ) + { + const asset_dynamic_data_object& ddo = bal->balance.asset_id(db).dynamic_data(db); + db.modify( ddo, [bal](asset_dynamic_data_object& obj) { + obj.current_supply -= bal->balance.amount; + }); + db.remove( *bal ); + } +} + void update_median_feeds(database& db) { time_point_sec head_time = db.head_block_time(); @@ -1061,6 +1079,18 @@ void process_hf_868_890( database& db, bool skip_check_call_orders ) } // for each market issued asset } + +/** + * @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(); @@ -1215,6 +1245,10 @@ void database::perform_chain_maintenance(const signed_block& next_block, const g if ( dgpo.next_maintenance_time <= HARDFORK_CORE_1465_TIME && next_maintenance_time > HARDFORK_CORE_1465_TIME ) process_hf_1465(*this); + // Fix supply issue + if ( dgpo.next_maintenance_time <= HARDFORK_CORE_2103_TIME && next_maintenance_time > HARDFORK_CORE_2103_TIME ) + process_hf_2103(*this); + modify(dgpo, [next_maintenance_time](dynamic_global_property_object& d) { d.next_maintenance_time = next_maintenance_time; d.accounts_registered_this_interval = 0; @@ -1236,6 +1270,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_market.cpp b/libraries/chain/db_market.cpp index 2a028dbe33..76c4ea6932 100644 --- a/libraries/chain/db_market.cpp +++ b/libraries/chain/db_market.cpp @@ -55,6 +55,26 @@ namespace detail { * No more asset updates may be issued. */ void database::globally_settle_asset( const asset_object& mia, const price& settlement_price ) +{ + auto maint_time = get_dynamic_global_properties().next_maintenance_time; + bool before_core_hardfork_1669 = ( maint_time <= HARDFORK_CORE_1669_TIME ); // whether to use call_price + + if( before_core_hardfork_1669 ) + { + globally_settle_asset_impl( mia, settlement_price, + get_index_type().indices().get() ); + } + else + { + globally_settle_asset_impl( mia, settlement_price, + get_index_type().indices().get() ); + } +} + +template +void database::globally_settle_asset_impl( const asset_object& mia, + const price& settlement_price, + const IndexType& call_index ) { try { const asset_bitasset_data_object& bitasset = mia.bitasset_data(*this); FC_ASSERT( !bitasset.has_settlement(), "black swan already occurred, it should not happen again" ); @@ -65,28 +85,29 @@ void database::globally_settle_asset( const asset_object& mia, const price& sett const asset_dynamic_data_object& mia_dyn = mia.dynamic_asset_data_id(*this); auto original_mia_supply = mia_dyn.current_supply; - const auto& call_price_index = get_index_type().indices().get(); - auto maint_time = get_dynamic_global_properties().next_maintenance_time; bool before_core_hardfork_342 = ( maint_time <= HARDFORK_CORE_342_TIME ); // better rounding // cancel all call orders and accumulate it into collateral_gathered - auto call_itr = call_price_index.lower_bound( price::min( bitasset.options.short_backing_asset, mia.id ) ); - auto call_end = call_price_index.upper_bound( price::max( bitasset.options.short_backing_asset, mia.id ) ); + auto call_itr = call_index.lower_bound( price::min( bitasset.options.short_backing_asset, mia.id ) ); + auto call_end = call_index.upper_bound( price::max( bitasset.options.short_backing_asset, mia.id ) ); + asset pays; while( call_itr != call_end ) { + const call_order_object& order = *call_itr; + ++call_itr; + if( before_core_hardfork_342 ) - pays = call_itr->get_debt() * settlement_price; // round down, in favor of call order + pays = order.get_debt() * settlement_price; // round down, in favor of call order else - pays = call_itr->get_debt().multiply_and_round_up( settlement_price ); // round up, in favor of global settlement fund + pays = order.get_debt().multiply_and_round_up( settlement_price ); // round up in favor of global-settle fund - if( pays > call_itr->get_collateral() ) - pays = call_itr->get_collateral(); + if( pays > order.get_collateral() ) + pays = order.get_collateral(); collateral_gathered += pays; - const auto& order = *call_itr; - ++call_itr; + FC_ASSERT( fill_call_order( order, pays, order.get_debt(), settlement_price, true ) ); // call order is maker } @@ -784,30 +805,82 @@ bool database::fill_limit_order( const limit_order_object& order, const asset& p const account_object& seller = order.seller(*this); const asset_object& recv_asset = receives.asset_id(*this); - auto issuer_fees = pay_market_fees(&seller, recv_asset, receives); + auto issuer_fees = pay_market_fees(&seller, recv_asset, receives, is_maker); pay_order( seller, receives - issuer_fees, pays ); assert( pays.asset_id != receives.asset_id ); push_applied_operation( fill_order_operation( order.id, order.seller, pays, receives, issuer_fees, fill_price, is_maker ) ); + // BSIP85: Maker order creation fee discount, https://github.com/bitshares/bsips/blob/master/bsip-0085.md + // if the order creation fee was paid in BTS, + // return round_down(deferred_fee * maker_fee_discount_percent) to the owner, + // then process the remaining deferred fee as before; + // if the order creation fee was paid in another asset, + // return round_down(deferred_paid_fee * maker_fee_discount_percent) to the owner, + // return round_down(deferred_fee * maker_fee_discount_percent) to the fee pool of the asset, + // then process the remaining deferred fee and deferred paid fee as before. + const uint16_t maker_discount_percent = get_global_properties().parameters.get_maker_fee_discount_percent(); + + // Save local copies for calculation + share_type deferred_fee = order.deferred_fee; + share_type deferred_paid_fee = order.deferred_paid_fee.amount; + // conditional because cheap integer comparison may allow us to avoid two expensive modify() and object lookups - if( order.deferred_fee > 0 ) + if( order.deferred_paid_fee.amount > 0 ) // implies head_block_time() > HARDFORK_CORE_604_TIME { - modify( seller.statistics(*this), [&]( account_statistics_object& statistics ) + share_type fee_pool_refund = 0; + if( is_maker && maker_discount_percent > 0 ) { - statistics.pay_fee( order.deferred_fee, get_global_properties().parameters.cashback_vesting_threshold ); - } ); - } + share_type refund = detail::calculate_percent( deferred_paid_fee, maker_discount_percent ); + // Note: it's possible that the deferred_paid_fee is very small, + // which can result in a zero refund due to rounding issue, + // in this case, no refund to the fee pool + if( refund > 0 ) + { + FC_ASSERT( refund <= deferred_paid_fee, "Internal error" ); + adjust_balance( order.seller, asset(refund, order.deferred_paid_fee.asset_id) ); + deferred_paid_fee -= refund; + + // deferred_fee might be positive too + FC_ASSERT( deferred_fee > 0, "Internal error" ); + fee_pool_refund = detail::calculate_percent( deferred_fee, maker_discount_percent ); + FC_ASSERT( fee_pool_refund <= deferred_fee, "Internal error" ); + deferred_fee -= fee_pool_refund; + } + } - if( order.deferred_paid_fee.amount > 0 ) // implies head_block_time() > HARDFORK_CORE_604_TIME - { const auto& fee_asset_dyn_data = order.deferred_paid_fee.asset_id(*this).dynamic_asset_data_id(*this); - modify( fee_asset_dyn_data, [&](asset_dynamic_data_object& addo) { - addo.accumulated_fees += order.deferred_paid_fee.amount; + modify( fee_asset_dyn_data, [deferred_paid_fee,fee_pool_refund](asset_dynamic_data_object& addo) { + addo.accumulated_fees += deferred_paid_fee; + addo.fee_pool += fee_pool_refund; }); } + if( order.deferred_fee > 0 ) + { + if( order.deferred_paid_fee.amount <= 0 // paid in CORE, or before HF 604 + && is_maker && maker_discount_percent > 0 ) + { + share_type refund = detail::calculate_percent( deferred_fee, maker_discount_percent ); + if( refund > 0 ) + { + FC_ASSERT( refund <= deferred_fee, "Internal error" ); + adjust_balance( order.seller, asset(refund, asset_id_type()) ); + deferred_fee -= refund; + } + } + // else do nothing here, because we have already processed it above, or no need to process + + if( deferred_fee > 0 ) + { + modify( seller.statistics(*this), [deferred_fee,this]( account_statistics_object& statistics ) + { + statistics.pay_fee( deferred_fee, get_global_properties().parameters.cashback_vesting_threshold ); + } ); + } + } + if( pays == order.amount_for_sale() ) { remove( order ); @@ -815,7 +888,7 @@ bool database::fill_limit_order( const limit_order_object& order, const asset& p } else { - modify( order, [&]( limit_order_object& b ) { + modify( order, [&pays]( limit_order_object& b ) { b.for_sale -= pays.amount; b.deferred_fee = 0; b.deferred_paid_fee.amount = 0; @@ -895,14 +968,22 @@ bool database::fill_settle_order( const force_settlement_object& settle, const a { try { bool filled = false; - auto issuer_fees = pay_market_fees( nullptr, get(receives.asset_id), receives); + const account_object* settle_owner_ptr = nullptr; + // The owner of the settle order pays market fees to the issuer of the collateral asset after HF core-1780 + // + // TODO Check whether the HF check can be removed after the HF. + // Note: even if logically it can be removed, perhaps the removal will lead to a small performance + // loss. Needs testing. + if( head_block_time() >= HARDFORK_CORE_1780_TIME ) + settle_owner_ptr = &settle.owner(*this); + + auto issuer_fees = pay_market_fees( settle_owner_ptr, get(receives.asset_id), receives, is_maker ); if( pays < settle.balance ) { modify(settle, [&pays](force_settlement_object& s) { s.balance -= pays; }); - filled = false; } else { filled = true; } @@ -942,6 +1023,14 @@ bool database::check_call_orders( const asset_object& mia, bool enable_black_swa if( !mia.is_market_issued() ) return false; const asset_bitasset_data_object& bitasset = ( bitasset_ptr ? *bitasset_ptr : mia.bitasset_data(*this) ); + + // price feeds can cause black swans in prediction markets + // The hardfork check may be able to be removed after the hardfork date + // if check_for_blackswan never triggered a black swan on a prediction market. + // NOTE: check_for_blackswan returning true does not always mean a black + // swan was triggered. + if ( maint_time >= HARDFORK_CORE_460_TIME && bitasset.is_prediction_market ) + return false; if( check_for_blackswan( mia, enable_black_swan, &bitasset ) ) return false; @@ -1137,16 +1226,31 @@ void database::pay_order( const account_object& receiver, const asset& receives, adjust_balance(receiver.get_id(), receives); } -asset database::calculate_market_fee( const asset_object& trade_asset, const asset& trade_amount ) +asset database::calculate_market_fee( const asset_object& trade_asset, const asset& trade_amount, const bool& is_maker) { assert( trade_asset.id == trade_amount.asset_id ); if( !trade_asset.charges_market_fees() ) return trade_asset.amount(0); - if( trade_asset.options.market_fee_percent == 0 ) + // Optimization: The fee is zero if the order is a maker, and the maker fee percent is 0% + if( is_maker && trade_asset.options.market_fee_percent == 0 ) return trade_asset.amount(0); - auto value = detail::calculate_percent(trade_amount.amount, trade_asset.options.market_fee_percent); + // Optimization: The fee is zero if the order is a taker, and the taker fee percent is 0% + const optional& taker_fee_percent = trade_asset.options.extensions.value.taker_fee_percent; + if(!is_maker && taker_fee_percent.valid() && *taker_fee_percent == 0) + return trade_asset.amount(0); + + uint16_t fee_percent; + if (is_maker) { + // Maker orders are charged the maker fee percent + fee_percent = trade_asset.options.market_fee_percent; + } else { + // Taker orders are charged the taker fee percent if they are valid. Otherwise, the maker fee percent. + fee_percent = taker_fee_percent.valid() ? *taker_fee_percent : trade_asset.options.market_fee_percent; + } + + auto value = detail::calculate_percent(trade_amount.amount, fee_percent); asset percent_fee = trade_asset.amount(value); if( percent_fee.amount > trade_asset.options.max_market_fee ) @@ -1155,12 +1259,33 @@ asset database::calculate_market_fee( const asset_object& trade_asset, const ass return percent_fee; } -asset database::pay_market_fees(const account_object* seller, const asset_object& recv_asset, const asset& receives ) +asset database::pay_market_fees(const account_object* seller, const asset_object& recv_asset, const asset& receives, + const bool& is_maker) { - const auto issuer_fees = calculate_market_fee( recv_asset, receives ); + const auto market_fees = calculate_market_fee( recv_asset, receives, is_maker ); + auto issuer_fees = market_fees; FC_ASSERT( issuer_fees <= receives, "Market fee shouldn't be greater than receives"); //Don't dirty undo state if not actually collecting any fees if ( issuer_fees.amount > 0 ) + { + // Share market fees to the network + const uint16_t network_percent = get_global_properties().parameters.get_market_fee_network_percent(); + if( network_percent > 0 ) + { + const auto network_fees_amt = detail::calculate_percent( issuer_fees.amount, network_percent ); + FC_ASSERT( network_fees_amt <= issuer_fees.amount, + "Fee shared to the network shouldn't be greater than total market fee" ); + if( network_fees_amt > 0 ) + { + const asset network_fees = recv_asset.amount( network_fees_amt ); + deposit_market_fee_vesting_balance( GRAPHENE_COMMITTEE_ACCOUNT, network_fees ); + issuer_fees -= network_fees; + } + } + } + + // Process the remaining fees + if ( issuer_fees.amount > 0 ) { // calculate and pay rewards asset reward = recv_asset.amount(0); @@ -1182,35 +1307,58 @@ asset database::pay_market_fees(const account_object* seller, const asset_object if ( reward_value > 0 && is_authorized_asset(*this, seller->registrar(*this), recv_asset) ) { reward = recv_asset.amount(reward_value); - FC_ASSERT( reward < issuer_fees, "Market reward should be less than issuer fees"); + // TODO after hf_1774, remove the `if` check, keep the code in `else` + if( head_block_time() < HARDFORK_1774_TIME ){ + FC_ASSERT( reward < issuer_fees, "Market reward should be less than issuer fees"); + } + else{ + FC_ASSERT( reward <= issuer_fees, "Market reward should not be greater than issuer fees"); + } // cut referrer percent from reward auto registrar_reward = reward; - if( seller->referrer != seller->registrar ) + + auto registrar = seller->registrar; + auto referrer = seller->referrer; + + // After HF core-1800, for funds going to temp-account, redirect to committee-account + if( head_block_time() >= HARDFORK_CORE_1800_TIME ) + { + if( registrar == GRAPHENE_TEMP_ACCOUNT ) + registrar = GRAPHENE_COMMITTEE_ACCOUNT; + if( referrer == GRAPHENE_TEMP_ACCOUNT ) + referrer = GRAPHENE_COMMITTEE_ACCOUNT; + } + + if( referrer != registrar ) { const auto referrer_rewards_value = detail::calculate_percent( reward.amount, seller->referrer_rewards_percentage ); - if ( referrer_rewards_value > 0 && is_authorized_asset(*this, seller->referrer(*this), recv_asset) ) + if ( referrer_rewards_value > 0 && is_authorized_asset(*this, referrer(*this), recv_asset) ) { FC_ASSERT ( referrer_rewards_value <= reward.amount.value, "Referrer reward shouldn't be greater than total reward" ); const asset referrer_reward = recv_asset.amount(referrer_rewards_value); registrar_reward -= referrer_reward; - deposit_market_fee_vesting_balance(seller->referrer, referrer_reward); + deposit_market_fee_vesting_balance(referrer, referrer_reward); } } - deposit_market_fee_vesting_balance(seller->registrar, registrar_reward); + if( registrar_reward.amount > 0 ) + deposit_market_fee_vesting_balance(registrar, registrar_reward); } } } - const auto& recv_dyn_data = recv_asset.dynamic_asset_data_id(*this); - modify( recv_dyn_data, [&issuer_fees, &reward]( asset_dynamic_data_object& obj ){ - obj.accumulated_fees += issuer_fees.amount - reward.amount; - }); + if( issuer_fees.amount > reward.amount ) + { + const auto& recv_dyn_data = recv_asset.dynamic_asset_data_id(*this); + modify( recv_dyn_data, [&issuer_fees, &reward]( asset_dynamic_data_object& obj ){ + obj.accumulated_fees += issuer_fees.amount - reward.amount; + }); + } } - return issuer_fees; + return market_fees; } } } diff --git a/libraries/chain/db_notify.cpp b/libraries/chain/db_notify.cpp index 6213badc1c..82c5097176 100644 --- a/libraries/chain/db_notify.cpp +++ b/libraries/chain/db_notify.cpp @@ -16,7 +16,9 @@ #include #include #include +#include #include +#include using namespace fc; namespace graphene { namespace chain { namespace detail { @@ -25,8 +27,13 @@ namespace graphene { namespace chain { namespace detail { struct get_impacted_account_visitor { flat_set& _impacted; - get_impacted_account_visitor( flat_set& impact ):_impacted(impact) {} - typedef void result_type; + bool _ignore_custom_op_reqd_auths; + + get_impacted_account_visitor( flat_set& impact, bool ignore_custom_operation_required_auths ) + : _impacted( impact ), _ignore_custom_op_reqd_auths( ignore_custom_operation_required_auths ) + {} + + using result_type = void; void operator()( const transfer_operation& op ) { @@ -154,7 +161,7 @@ struct get_impacted_account_visitor _impacted.insert( op.fee_payer() ); // fee_paying_account vector other; for( const auto& proposed_op : op.proposed_ops ) - operation_get_required_authorities( proposed_op.op, _impacted, _impacted, other ); + operation_get_required_authorities( proposed_op.op, _impacted, _impacted, other, _ignore_custom_op_reqd_auths ); for( auto& o : other ) add_authority_accounts( _impacted, o ); } @@ -214,6 +221,8 @@ struct get_impacted_account_visitor void operator()( const custom_operation& op ) { _impacted.insert( op.fee_payer() ); // payer + if( !_ignore_custom_op_reqd_auths ) + _impacted.insert( op.required_auths.begin(), op.required_auths.end() ); } void operator()( const assert_operation& op ) { @@ -281,24 +290,41 @@ 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 + } }; -} // detail +} // namespace detail -void operation_get_impacted_accounts( const operation& op, flat_set& result ) +void operation_get_impacted_accounts( const operation& op, flat_set& result, + bool ignore_custom_operation_required_auths ) { - detail::get_impacted_account_visitor vtor( result ); + detail::get_impacted_account_visitor vtor = detail::get_impacted_account_visitor( result, + ignore_custom_operation_required_auths ); op.visit( vtor ); } -void transaction_get_impacted_accounts( const transaction& tx, flat_set& result ) +void transaction_get_impacted_accounts( const transaction& tx, flat_set& result, + bool ignore_custom_operation_required_auths ) { for( const auto& op : tx.operations ) - operation_get_impacted_accounts( op, result ); + operation_get_impacted_accounts( op, result, ignore_custom_operation_required_auths ); } -void get_relevant_accounts( const object* obj, flat_set& accounts ) -{ +void get_relevant_accounts( const object* obj, flat_set& accounts, bool ignore_custom_operation_required_auths ) { if( obj->id.space() == protocol_ids ) { switch( (object_type)obj->id.type() ) @@ -344,12 +370,14 @@ void get_relevant_accounts( const object* obj, flat_set& accoun } case proposal_object_type:{ const auto& aobj = dynamic_cast(obj); FC_ASSERT( aobj != nullptr ); - transaction_get_impacted_accounts( aobj->proposed_transaction, accounts ); + transaction_get_impacted_accounts( aobj->proposed_transaction, accounts, + ignore_custom_operation_required_auths ); break; } case operation_history_object_type:{ const auto& aobj = dynamic_cast(obj); FC_ASSERT( aobj != nullptr ); - operation_get_impacted_accounts( aobj->op, accounts ); + operation_get_impacted_accounts( aobj->op, accounts, + ignore_custom_operation_required_auths ); break; } case withdraw_permission_object_type:{ const auto& aobj = dynamic_cast(obj); @@ -376,6 +404,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 ); } } } @@ -406,7 +439,8 @@ void get_relevant_accounts( const object* obj, flat_set& accoun } case impl_transaction_history_object_type:{ const auto& aobj = dynamic_cast(obj); FC_ASSERT( aobj != nullptr ); - transaction_get_impacted_accounts( aobj->trx, accounts ); + transaction_get_impacted_accounts( aobj->trx, accounts, + ignore_custom_operation_required_auths ); break; } case impl_blinded_balance_object_type:{ const auto& aobj = dynamic_cast(obj); @@ -458,6 +492,7 @@ void database::notify_changed_objects() if( _undo_db.enabled() ) { const auto& head_undo = _undo_db.head(); + auto chain_time = head_block_time(); // New if( !new_objects.empty() ) @@ -469,7 +504,8 @@ void database::notify_changed_objects() new_ids.push_back(item); auto obj = find_object(item); if(obj != nullptr) - get_relevant_accounts(obj, new_accounts_impacted); + get_relevant_accounts(obj, new_accounts_impacted, + MUST_IGNORE_CUSTOM_OP_REQD_AUTHS(chain_time)); } if( new_ids.size() ) @@ -484,7 +520,8 @@ void database::notify_changed_objects() for( const auto& item : head_undo.old_values ) { changed_ids.push_back(item.first); - get_relevant_accounts(item.second.get(), changed_accounts_impacted); + get_relevant_accounts(item.second.get(), changed_accounts_impacted, + MUST_IGNORE_CUSTOM_OP_REQD_AUTHS(chain_time)); } if( changed_ids.size() ) @@ -502,13 +539,14 @@ void database::notify_changed_objects() removed_ids.emplace_back( item.first ); auto obj = item.second.get(); removed.emplace_back( obj ); - get_relevant_accounts(obj, removed_accounts_impacted); + get_relevant_accounts(obj, removed_accounts_impacted, + MUST_IGNORE_CUSTOM_OP_REQD_AUTHS(chain_time)); } if( removed_ids.size() ) - GRAPHENE_TRY_NOTIFY( removed_objects, removed_ids, removed, removed_accounts_impacted) + GRAPHENE_TRY_NOTIFY( removed_objects, removed_ids, removed, removed_accounts_impacted ) } } } FC_CAPTURE_AND_LOG( (0) ) } -} } +} } // namespace graphene::chain 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/hardfork.d/BSIP_81.hf b/libraries/chain/hardfork.d/BSIP_81.hf new file mode 100644 index 0000000000..5f4851bdc9 --- /dev/null +++ b/libraries/chain/hardfork.d/BSIP_81.hf @@ -0,0 +1,5 @@ +// BSIP 81 (Simple Maker-Taker Market Fees) hardfork check +#ifndef HARDFORK_BSIP_81_TIME +// Jan 1 2030, midnight this is a dummy date until a hardfork date is scheduled +#define HARDFORK_BSIP_81_TIME (fc::time_point_sec( 1893456000 )) +#endif diff --git a/libraries/chain/hardfork.d/BSIP_85.hf b/libraries/chain/hardfork.d/BSIP_85.hf new file mode 100644 index 0000000000..7fb612a385 --- /dev/null +++ b/libraries/chain/hardfork.d/BSIP_85.hf @@ -0,0 +1,6 @@ +// BSIP 85 (Maker order creation fee discount) hardfork check +#ifndef HARDFORK_BSIP_85_TIME +// Jan 1 2030, midnight; this is a dummy date until a hardfork date is scheduled +#define HARDFORK_BSIP_85_TIME (fc::time_point_sec( 1893456000 )) +#define HARDFORK_BSIP_85_PASSED(now) (now >= HARDFORK_BSIP_85_TIME) +#endif diff --git a/libraries/chain/hardfork.d/BSIP_86.hf b/libraries/chain/hardfork.d/BSIP_86.hf new file mode 100644 index 0000000000..435a7d5cfb --- /dev/null +++ b/libraries/chain/hardfork.d/BSIP_86.hf @@ -0,0 +1,6 @@ +// BSIP 86 (Share market fees to the network) hardfork check +#ifndef HARDFORK_BSIP_86_TIME +// Jan 1 2030, midnight; this is a dummy date until a hardfork date is scheduled +#define HARDFORK_BSIP_86_TIME (fc::time_point_sec( 1893456000 )) +#define HARDFORK_BSIP_86_PASSED(now) (now >= HARDFORK_BSIP_86_TIME) +#endif diff --git a/libraries/chain/hardfork.d/CORE_1669.hf b/libraries/chain/hardfork.d/CORE_1669.hf new file mode 100644 index 0000000000..e185ed52ee --- /dev/null +++ b/libraries/chain/hardfork.d/CORE_1669.hf @@ -0,0 +1,4 @@ +// bitshares-core issue #1669 Stop using call_price when globally settling +#ifndef HARDFORK_CORE_1669_TIME +#define HARDFORK_CORE_1669_TIME (fc::time_point_sec( 1600000000 )) // a temporary date in the future +#endif diff --git a/libraries/chain/hardfork.d/CORE_1692.hf b/libraries/chain/hardfork.d/CORE_1692.hf new file mode 100644 index 0000000000..cbbe42c4ae --- /dev/null +++ b/libraries/chain/hardfork.d/CORE_1692.hf @@ -0,0 +1,4 @@ +// bitshares-core issue #1692 validation check of bid_collateral +#ifndef HARDFORK_CORE_1692_TIME +#define HARDFORK_CORE_1692_TIME (fc::time_point_sec( 1600000000 ) ) // Sep 2020 +#endif diff --git a/libraries/chain/hardfork.d/CORE_1774.hf b/libraries/chain/hardfork.d/CORE_1774.hf new file mode 100644 index 0000000000..b0f5f7814b --- /dev/null +++ b/libraries/chain/hardfork.d/CORE_1774.hf @@ -0,0 +1,4 @@ +// #1774 Too restrictive check in market fee sharing +#ifndef HARDFORK_1774_TIME +#define HARDFORK_1774_TIME (fc::time_point_sec( 1600000000 ) ) // September 13, 2020 12:26:40 PM +#endif diff --git a/libraries/chain/hardfork.d/CORE_1780.hf b/libraries/chain/hardfork.d/CORE_1780.hf new file mode 100644 index 0000000000..8e1358b15b --- /dev/null +++ b/libraries/chain/hardfork.d/CORE_1780.hf @@ -0,0 +1,4 @@ +// Market fees of settle orders aren't shared to referral program +#ifndef HARDFORK_CORE_1780_TIME +#define HARDFORK_CORE_1780_TIME (fc::time_point_sec( 1600000000 ) ) // September 13, 2020 3:26:40 PM (GMT) +#endif diff --git a/libraries/chain/hardfork.d/CORE_1800.hf b/libraries/chain/hardfork.d/CORE_1800.hf new file mode 100644 index 0000000000..39d8334cc8 --- /dev/null +++ b/libraries/chain/hardfork.d/CORE_1800.hf @@ -0,0 +1,4 @@ +// bitshares-core issue #1800 Fix "Temp-account market fee sharing" +#ifndef HARDFORK_CORE_1800_TIME +#define HARDFORK_CORE_1800_TIME (fc::time_point_sec( 1600000000 )) // Tue, 23 Jul 2019 13:35:00 UTC +#endif diff --git a/libraries/chain/hardfork.d/CORE_210.hf b/libraries/chain/hardfork.d/CORE_210.hf new file mode 100644 index 0000000000..0f28527475 --- /dev/null +++ b/libraries/chain/hardfork.d/CORE_210.hf @@ -0,0 +1,6 @@ +// #210 Check authorities on custom_operation +#ifndef HARDFORK_CORE_210_TIME +#define HARDFORK_CORE_210_TIME (fc::time_point_sec(1893456000)) // Jan 1 00:00:00 2030 (Not yet scheduled) +// Bugfix: pre-HF 210, custom_operation's required_auths field was ignored. +#define MUST_IGNORE_CUSTOM_OP_REQD_AUTHS(chain_time) (chain_time <= HARDFORK_CORE_210_TIME) +#endif diff --git a/libraries/chain/hardfork.d/CORE_2103.hf b/libraries/chain/hardfork.d/CORE_2103.hf new file mode 100644 index 0000000000..579354f71d --- /dev/null +++ b/libraries/chain/hardfork.d/CORE_2103.hf @@ -0,0 +1,5 @@ +// bitshares-core issue #2103 250M BTS supply +#ifndef HARDFORK_CORE_2103_TIME +#define HARDFORK_CORE_2103_TIME (fc::time_point_sec( 1600000000 ) ) // Sep 2020 +#define HARDFORK_CORE_2103_BALANCE_ID 56720 // 1.15.56720 +#endif diff --git a/libraries/chain/hardfork.d/CORE_460.hf b/libraries/chain/hardfork.d/CORE_460.hf new file mode 100644 index 0000000000..8f0bd3f6fa --- /dev/null +++ b/libraries/chain/hardfork.d/CORE_460.hf @@ -0,0 +1,4 @@ +// bitshares-core issue #460 Prediction Market price feed should not cause black swan +#ifndef HARDFORK_CORE_460_TIME +#define HARDFORK_CORE_460_TIME (fc::time_point_sec( 1609372800 ) ) // 2020-12-31T00:00:00 +#endif diff --git a/libraries/chain/hardfork.d/CORE_BSIP64.hf b/libraries/chain/hardfork.d/CORE_BSIP64.hf new file mode 100644 index 0000000000..a035996482 --- /dev/null +++ b/libraries/chain/hardfork.d/CORE_BSIP64.hf @@ -0,0 +1,4 @@ +// bitshares BSIP 64 HTLC modifications +#ifndef HARDFORK_CORE_BSIP64_TIME +#define HARDFORK_CORE_BSIP64_TIME (fc::time_point_sec( 1600000000 ) ) // Sep 2020 +#endif diff --git a/libraries/chain/hardfork.d/CORE_BSIP_87_74_COLLATFEE.hf b/libraries/chain/hardfork.d/CORE_BSIP_87_74_COLLATFEE.hf new file mode 100644 index 0000000000..3f1add317a --- /dev/null +++ b/libraries/chain/hardfork.d/CORE_BSIP_87_74_COLLATFEE.hf @@ -0,0 +1,7 @@ +// This hardfork enables the extension to asset_claim_fees_operation to claim collateral-denominated fees. +// These fees are collected by BSIPs 87 and 74. This should be set to match the earlier of either +// HARDFORK_CORE_BSIP87_TIME or HARDFORK_CORE_BSIP74_TIME. +// This hardfork check should be removable after the hardfork date passes. +#ifndef HARDFORK_CORE_BSIP_87_74_COLLATFEE_TIME +#define HARDFORK_CORE_BSIP_87_74_COLLATFEE_TIME (fc::time_point_sec( 1679955066 ) ) // Temporary date until actual hardfork date is set +#endif diff --git a/libraries/chain/htlc_evaluator.cpp b/libraries/chain/htlc_evaluator.cpp index 008980b9b3..339c2fc4bb 100644 --- a/libraries/chain/htlc_evaluator.cpp +++ b/libraries/chain/htlc_evaluator.cpp @@ -29,6 +29,42 @@ namespace graphene { namespace chain { + namespace detail + { + void check_htlc_create_hf_bsip64(const fc::time_point_sec& block_time, + const htlc_create_operation& op, const asset_object& asset_to_transfer) + { + if (block_time < HARDFORK_CORE_BSIP64_TIME) + { + // memo field added at harfork BSIP64 + // NOTE: both of these checks can be removed after hardfork time + FC_ASSERT( !op.extensions.value.memo.valid(), + "Memo unavailable until after HARDFORK BSIP64"); + // HASH160 added at hardfork BSIP64 + FC_ASSERT( !op.preimage_hash.is_type(), + "HASH160 unavailable until after HARDFORK BSIP64" ); + } + else + { + // this can be moved to the normal non-hf checks after HF_BSIP64 + // IF there were no restricted transfers before HF_BSIP64 + FC_ASSERT( !asset_to_transfer.is_transfer_restricted() + || op.from == asset_to_transfer.issuer || op.to == asset_to_transfer.issuer, + "Asset ${asset} cannot be transfered.", ("asset", asset_to_transfer.id) ); + } + } + + void check_htlc_redeem_hf_bsip64(const fc::time_point_sec& block_time, + const htlc_redeem_operation& op, const htlc_object* htlc_obj) + { + // TODO: The hardfork portion of this check can be removed if no HTLC redemptions are + // attempted on an HTLC with a 0 preimage size before the hardfork date. + if ( htlc_obj->conditions.hash_lock.preimage_size > 0U || + block_time < HARDFORK_CORE_BSIP64_TIME ) + FC_ASSERT(op.preimage.size() == htlc_obj->conditions.hash_lock.preimage_size, + "Preimage size mismatch."); + } + } // end of graphene::chain::details optional get_committee_htlc_options(graphene::chain::database& db) { @@ -43,14 +79,17 @@ namespace graphene { FC_ASSERT(htlc_options, "HTLC Committee options are not set."); // make sure the expiration is reasonable - FC_ASSERT( o.claim_period_seconds <= htlc_options->max_timeout_secs, "HTLC Timeout exceeds allowed length" ); + FC_ASSERT( o.claim_period_seconds <= htlc_options->max_timeout_secs, + "HTLC Timeout exceeds allowed length" ); // make sure the preimage length is reasonable - FC_ASSERT( o.preimage_size <= htlc_options->max_preimage_size, "HTLC preimage length exceeds allowed length" ); + FC_ASSERT( o.preimage_size <= htlc_options->max_preimage_size, + "HTLC preimage length exceeds allowed length" ); // make sure the sender has the funds for the HTLC FC_ASSERT( d.get_balance( o.from, o.amount.asset_id) >= (o.amount), "Insufficient funds") ; const auto& asset_to_transfer = o.amount.asset_id( d ); const auto& from_account = o.from( d ); const auto& to_account = o.to( d ); + detail::check_htlc_create_hf_bsip64(d.head_block_time(), o, asset_to_transfer); FC_ASSERT( is_authorized_asset( d, from_account, asset_to_transfer ), "Asset ${asset} is not authorized for account ${acct}.", ( "asset", asset_to_transfer.id )( "acct", from_account.id ) ); @@ -73,6 +112,8 @@ namespace graphene { esc.transfer.asset_id = o.amount.asset_id; esc.conditions.hash_lock.preimage_hash = o.preimage_hash; esc.conditions.hash_lock.preimage_size = o.preimage_size; + if ( o.extensions.value.memo.valid() ) + esc.memo = o.extensions.value.memo; esc.conditions.time_lock.expiration = dbase.head_block_time() + o.claim_period_seconds; }); return esc.id; @@ -99,9 +140,10 @@ namespace graphene { void_result htlc_redeem_evaluator::do_evaluate(const htlc_redeem_operation& o) { - htlc_obj = &db().get(o.htlc_id); + auto& d = db(); + htlc_obj = &d.get(o.htlc_id); + detail::check_htlc_redeem_hf_bsip64(d.head_block_time(), o, htlc_obj); - FC_ASSERT(o.preimage.size() == htlc_obj->conditions.hash_lock.preimage_size, "Preimage size mismatch."); const htlc_redeem_visitor vtor( o.preimage ); FC_ASSERT( htlc_obj->conditions.hash_lock.preimage_hash.visit( vtor ), "Provided preimage does not generate correct hash."); @@ -115,7 +157,8 @@ namespace graphene { db().adjust_balance(htlc_obj->transfer.to, amount); // notify related parties htlc_redeemed_operation virt_op( htlc_obj->id, htlc_obj->transfer.from, htlc_obj->transfer.to, o.redeemer, - amount, htlc_obj->conditions.hash_lock.preimage_hash, htlc_obj->conditions.hash_lock.preimage_size ); + amount, htlc_obj->conditions.hash_lock.preimage_hash, htlc_obj->conditions.hash_lock.preimage_size, + o.preimage ); db().push_applied_operation( virt_op ); db().remove(*htlc_obj); return void_result(); diff --git a/libraries/chain/include/graphene/chain/asset_evaluator.hpp b/libraries/chain/include/graphene/chain/asset_evaluator.hpp index 544b4b8b21..068f2cf93e 100644 --- a/libraries/chain/include/graphene/chain/asset_evaluator.hpp +++ b/libraries/chain/include/graphene/chain/asset_evaluator.hpp @@ -166,6 +166,9 @@ namespace graphene { namespace chain { void_result do_evaluate( const asset_claim_fees_operation& o ); void_result do_apply( const asset_claim_fees_operation& o ); + + const asset_object* container_asset = nullptr; + const asset_dynamic_data_object* container_ddo = nullptr; }; class asset_claim_pool_evaluator : public evaluator diff --git a/libraries/chain/include/graphene/chain/asset_object.hpp b/libraries/chain/include/graphene/chain/asset_object.hpp index 8264c20e1b..fd8dd4a904 100644 --- a/libraries/chain/include/graphene/chain/asset_object.hpp +++ b/libraries/chain/include/graphene/chain/asset_object.hpp @@ -65,6 +65,7 @@ namespace graphene { namespace chain { share_type current_supply; share_type confidential_supply; ///< total asset held in confidential balances share_type accumulated_fees; ///< fees accumulate to be paid out over time + share_type accumulated_collateral_fees; ///< accumulated collateral-denominated fees (for bitassets) share_type fee_pool; ///< in core asset }; @@ -164,6 +165,46 @@ namespace graphene { namespace chain { template share_type reserved( const DB& db )const { return options.max_supply - dynamic_data(db).current_supply; } + + /// @return true if asset can accumulate fees in the given denomination + template + bool can_accumulate_fee(const DB& db, const asset& fee) const { + return (( fee.asset_id == get_id() ) || + ( is_market_issued() && fee.asset_id == bitasset_data(db).options.short_backing_asset )); + } + + /*** + * @brief receive a fee asset to accrue in dynamic_data object + * + * Asset owners define various fees (market fees, force-settle fees, etc.) to be + * collected for the asset owners. These fees are typically denominated in the asset + * itself, but for bitassets some of the fees are denominated in the collateral + * asset. This will place the fee in the right container. + */ + template + void accumulate_fee(DB& db, const asset& fee) const + { + if (fee.amount == 0) return; + FC_ASSERT( fee.amount >= 0, "Fee amount must be non-negative." ); + const auto& dyn_data = dynamic_asset_data_id(db); + if (fee.asset_id == get_id()) { // fee same as asset + db.modify( dyn_data, [&fee]( asset_dynamic_data_object& obj ){ + obj.accumulated_fees += fee.amount; + }); + } else { // fee different asset; perhaps collateral-denominated fee + FC_ASSERT( is_market_issued(), + "Asset ${a} (${id}) cannot accept fee of asset (${fid}).", + ("a",this->symbol)("id",this->id)("fid",fee.asset_id) ); + const auto & bad = bitasset_data(db); + FC_ASSERT( fee.asset_id == bad.options.short_backing_asset, + "Asset ${a} (${id}) cannot accept fee of asset (${fid}).", + ("a",this->symbol)("id",this->id)("fid",fee.asset_id) ); + db.modify( dyn_data, [&fee]( asset_dynamic_data_object& obj ){ + obj.accumulated_collateral_fees += fee.amount; + }); + } + } + }; /** 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..d73194a11a --- /dev/null +++ b/libraries/chain/include/graphene/chain/custom_authority_object.hpp @@ -0,0 +1,119 @@ +/* + * 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 constexpr uint8_t space_id = protocol_ids; + static constexpr 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 + > + > + > + > 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 a5444396d2..e4c5b880a8 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 //////////////////// @@ -365,6 +378,13 @@ namespace graphene { namespace chain { void cancel_bid(const collateral_bid_object& bid, bool create_virtual_op = true); void execute_bid( const collateral_bid_object& bid, share_type debt_covered, share_type collateral_from_fund, const price_feed& current_feed ); + private: + template + void globally_settle_asset_impl( const asset_object& bitasset, + const price& settle_price, + const IndexType& call_index ); + + public: /** * @brief Process a new limit order through the markets * @param order The new order to process @@ -419,8 +439,9 @@ namespace graphene { namespace chain { // helpers to fill_order void pay_order( const account_object& receiver, const asset& receives, const asset& pays ); - asset calculate_market_fee(const asset_object& recv_asset, const asset& trade_amount); - asset pay_market_fees(const account_object* seller, const asset_object& recv_asset, const asset& receives ); + asset calculate_market_fee(const asset_object& recv_asset, const asset& trade_amount, const bool& is_maker); + asset pay_market_fees(const account_object* seller, const asset_object& recv_asset, const asset& receives, + const bool& is_maker); ///@} 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/include/graphene/chain/htlc_object.hpp b/libraries/chain/include/graphene/chain/htlc_object.hpp index 355b60065d..ab84d58565 100644 --- a/libraries/chain/include/graphene/chain/htlc_object.hpp +++ b/libraries/chain/include/graphene/chain/htlc_object.hpp @@ -59,6 +59,8 @@ namespace graphene { namespace chain { } time_lock; } conditions; + fc::optional memo; + /**** * Index helper for timelock */ diff --git a/libraries/chain/include/graphene/chain/impacted.hpp b/libraries/chain/include/graphene/chain/impacted.hpp index 9d986cb8ab..6bb43048ac 100644 --- a/libraries/chain/include/graphene/chain/impacted.hpp +++ b/libraries/chain/include/graphene/chain/impacted.hpp @@ -30,13 +30,12 @@ namespace graphene { namespace chain { -void operation_get_impacted_accounts( - const graphene::chain::operation& op, - fc::flat_set& result ); +void operation_get_impacted_accounts( const graphene::chain::operation& op, + fc::flat_set& result, + bool ignore_custom_operation_required_auths ); -void transaction_get_impacted_accounts( - const graphene::chain::transaction& tx, - fc::flat_set& result - ); +void transaction_get_impacted_accounts( const graphene::chain::transaction& tx, + fc::flat_set& result, + bool ignore_custom_operation_required_auths ); -} } // graphene::app \ No newline at end of file +} } // graphene::app diff --git a/libraries/chain/include/graphene/chain/market_evaluator.hpp b/libraries/chain/include/graphene/chain/market_evaluator.hpp index 60a37e5bd3..ca27ff5f9b 100644 --- a/libraries/chain/include/graphene/chain/market_evaluator.hpp +++ b/libraries/chain/include/graphene/chain/market_evaluator.hpp @@ -42,8 +42,6 @@ namespace graphene { namespace chain { void_result do_evaluate( const limit_order_create_operation& o ); object_id_type do_apply( const limit_order_create_operation& o ); - asset calculate_market_fee( const asset_object* aobj, const asset& trade_amount ); - /** override the default behavior defined by generic_evalautor */ virtual void convert_fee() override; diff --git a/libraries/chain/include/graphene/chain/market_object.hpp b/libraries/chain/include/graphene/chain/market_object.hpp index b68f05b5ad..71e9f108a1 100644 --- a/libraries/chain/include/graphene/chain/market_object.hpp +++ b/libraries/chain/include/graphene/chain/market_object.hpp @@ -70,6 +70,7 @@ class limit_order_object : public abstract_object struct by_price; struct by_expiration; struct by_account; +struct by_account_price; typedef multi_index_container< limit_order_object, indexed_by< @@ -87,7 +88,15 @@ typedef multi_index_container< >, composite_key_compare< std::greater, std::less > >, + // index used by APIs ordered_unique< tag, + composite_key< limit_order_object, + member, + member + > + >, + // index used by APIs + ordered_unique< tag, composite_key< limit_order_object, member, member, diff --git a/libraries/chain/include/graphene/chain/proposal_evaluator.hpp b/libraries/chain/include/graphene/chain/proposal_evaluator.hpp index e18ddd4226..454fb7ff48 100644 --- a/libraries/chain/include/graphene/chain/proposal_evaluator.hpp +++ b/libraries/chain/include/graphene/chain/proposal_evaluator.hpp @@ -56,6 +56,8 @@ namespace graphene { namespace chain { object_id_type do_apply( const proposal_create_operation& o ); transaction _proposed_trx; + flat_set _required_active_auths; + flat_set _required_owner_auths; hardfork_visitor_1479 vtor_1479; }; diff --git a/libraries/chain/include/graphene/chain/worker_object.hpp b/libraries/chain/include/graphene/chain/worker_object.hpp index 23243a02ae..3855a9f34f 100644 --- a/libraries/chain/include/graphene/chain/worker_object.hpp +++ b/libraries/chain/include/graphene/chain/worker_object.hpp @@ -145,13 +145,15 @@ class worker_object : public abstract_object struct by_account; struct by_vote_for; struct by_vote_against; +struct by_end_date; typedef multi_index_container< worker_object, indexed_by< ordered_unique< tag, member< object, object_id_type, &object::id > >, ordered_non_unique< tag, member< worker_object, account_id_type, &worker_object::worker_account > >, ordered_unique< tag, member< worker_object, vote_id_type, &worker_object::vote_for > >, - ordered_unique< tag, member< worker_object, vote_id_type, &worker_object::vote_against > > + ordered_unique< tag, member< worker_object, vote_id_type, &worker_object::vote_against > >, + ordered_non_unique< tag, member< worker_object, time_point_sec, &worker_object::work_end_date> > > > worker_object_multi_index_type; diff --git a/libraries/chain/is_authorized_asset.cpp b/libraries/chain/is_authorized_asset.cpp index b2842402fc..a8c3dc71a7 100644 --- a/libraries/chain/is_authorized_asset.cpp +++ b/libraries/chain/is_authorized_asset.cpp @@ -37,6 +37,14 @@ bool _is_authorized_asset( const account_object& acct, const asset_object& asset_obj) { + // committee-account is always allowed to transact after BSIP 86 + if( HARDFORK_BSIP_86_PASSED( d.head_block_time() ) ) + { + static const object_id_type committee_account_id( GRAPHENE_COMMITTEE_ACCOUNT ); + if( acct.id == committee_account_id ) + return true; + } + if( acct.allowed_assets.valid() ) { if( acct.allowed_assets->find( asset_obj.id ) == acct.allowed_assets->end() ) diff --git a/libraries/chain/market_evaluator.cpp b/libraries/chain/market_evaluator.cpp index 99b9b2e4f4..10a02125a4 100644 --- a/libraries/chain/market_evaluator.cpp +++ b/libraries/chain/market_evaluator.cpp @@ -400,13 +400,6 @@ void_result bid_collateral_evaluator::do_evaluate(const bid_collateral_operation FC_ASSERT( !_bitasset_data->is_prediction_market, "Cannot bid on a prediction market!" ); - if( o.additional_collateral.amount > 0 ) - { - FC_ASSERT( d.get_balance(*_paying_account, _bitasset_data->options.short_backing_asset(d)) >= o.additional_collateral, - "Cannot bid ${c} collateral when payer only has ${b}", ("c", o.additional_collateral.amount) - ("b", d.get_balance(*_paying_account, o.additional_collateral.asset_id(d)).amount) ); - } - const collateral_bid_index& bids = d.get_index_type(); const auto& index = bids.indices().get(); const auto& bid = index.find( boost::make_tuple( o.debt_covered.asset_id, o.bidder ) ); @@ -415,6 +408,22 @@ void_result bid_collateral_evaluator::do_evaluate(const bid_collateral_operation else FC_ASSERT( o.debt_covered.amount > 0, "Can't find bid to cancel?!"); + if( o.additional_collateral.amount > 0 ) + { + if( _bid && d.head_block_time() >= HARDFORK_CORE_1692_TIME ) // TODO: see if HF check can be removed after HF + { + asset delta = o.additional_collateral - _bid->get_additional_collateral(); + FC_ASSERT( d.get_balance(*_paying_account, _bitasset_data->options.short_backing_asset(d)) >= delta, + "Cannot increase bid from ${oc} to ${nc} collateral when payer only has ${b}", + ("oc", _bid->get_additional_collateral().amount)("nc", o.additional_collateral.amount) + ("b", d.get_balance(*_paying_account, o.additional_collateral.asset_id(d)).amount) ); + } else + FC_ASSERT( d.get_balance( *_paying_account, + _bitasset_data->options.short_backing_asset(d) ) >= o.additional_collateral, + "Cannot bid ${c} collateral when payer only has ${b}", ("c", o.additional_collateral.amount) + ("b", d.get_balance(*_paying_account, o.additional_collateral.asset_id(d)).amount) ); + } + return void_result(); } FC_CAPTURE_AND_RETHROW( (o) ) } diff --git a/libraries/chain/proposal_evaluator.cpp b/libraries/chain/proposal_evaluator.cpp index 29e77bb142..d5a244787b 100644 --- a/libraries/chain/proposal_evaluator.cpp +++ b/libraries/chain/proposal_evaluator.cpp @@ -28,6 +28,12 @@ namespace graphene { namespace chain { +namespace detail { + void check_asset_options_hf_1774(const fc::time_point_sec& block_time, const asset_options& options); + void check_asset_options_hf_bsip81(const fc::time_point_sec& block_time, const asset_options& options); + void check_asset_claim_fees_hardfork_87_74_collatfee(const fc::time_point_sec& block_time, const asset_claim_fees_operation& op); +} + struct proposal_operation_hardfork_visitor { typedef void result_type; @@ -41,16 +47,64 @@ struct proposal_operation_hardfork_visitor template void operator()(const T &v) const {} + void operator()(const graphene::chain::asset_create_operation &v) const { + // hf_1774 + detail::check_asset_options_hf_1774(block_time, v.common_options); + + // HARDFORK_BSIP_81 + detail::check_asset_options_hf_bsip81(block_time, v.common_options); + } + void operator()(const graphene::chain::asset_update_operation &v) const { + // hf_1774 + detail::check_asset_options_hf_1774(block_time, v.new_options); + + // HARDFORK_BSIP_81 + detail::check_asset_options_hf_bsip81(block_time, v.new_options); + } + + void operator()(const graphene::chain::asset_claim_fees_operation &v) const { + detail::check_asset_claim_fees_hardfork_87_74_collatfee(block_time, v); // HF_REMOVABLE + } + void operator()(const graphene::chain::committee_member_update_global_parameters_operation &op) const { if (block_time < HARDFORK_CORE_1468_TIME) { - FC_ASSERT(!op.new_parameters.extensions.value.updatable_htlc_options.valid(), "Unable to set HTLC options before hardfork 1468"); + FC_ASSERT(!op.new_parameters.extensions.value.updatable_htlc_options.valid(), + "Unable to set HTLC options before hardfork 1468"); FC_ASSERT(!op.new_parameters.current_fees->exists()); 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"); + } + if (!HARDFORK_BSIP_85_PASSED(block_time)) { + FC_ASSERT(!op.new_parameters.extensions.value.maker_fee_discount_percent.valid(), + "Unable to set maker_fee_discount_percent before hardfork BSIP 85"); + } + if (!HARDFORK_BSIP_86_PASSED(block_time)) { + FC_ASSERT(!op.new_parameters.extensions.value.market_fee_network_percent.valid(), + "Unable to set market_fee_network_percent before hardfork BSIP 86"); + } } void operator()(const graphene::chain::htlc_create_operation &op) const { FC_ASSERT( block_time >= HARDFORK_CORE_1468_TIME, "Not allowed until hardfork 1468" ); + if (block_time < HARDFORK_CORE_BSIP64_TIME) + { + // memo field added at harfork BSIP64 + // NOTE: both of these checks can be removed after hardfork time + FC_ASSERT( !op.extensions.value.memo.valid(), + "Memo unavailable until after HARDFORK BSIP64"); + // HASH160 added at hardfork BSIP64 + FC_ASSERT( !op.preimage_hash.is_type(), + "HASH160 unavailable until after HARDFORK BSIP64" ); + } } void operator()(const graphene::chain::htlc_redeem_operation &op) const { FC_ASSERT( block_time >= HARDFORK_CORE_1468_TIME, "Not allowed until hardfork 1468" ); @@ -58,6 +112,16 @@ 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; @@ -68,7 +132,8 @@ struct proposal_operation_hardfork_visitor // Do not allow more than 1 proposal_update in a proposal if ( op.op.is_type() ) { - FC_ASSERT( !already_contains_proposal_update, "At most one proposal update can be nested in a proposal!" ); + FC_ASSERT( !already_contains_proposal_update, + "At most one proposal update can be nested in a proposal!" ); already_contains_proposal_update = true; } } @@ -108,7 +173,7 @@ void hardfork_visitor_1479::operator()(const graphene::chain::proposal_create_op op.op.visit(*this); } -void_result proposal_create_evaluator::do_evaluate(const proposal_create_operation& o) +void_result proposal_create_evaluator::do_evaluate( const proposal_create_operation& o ) { try { const database& d = db(); @@ -117,9 +182,10 @@ void_result proposal_create_evaluator::do_evaluate(const proposal_create_operati proposal_operation_hardfork_visitor vtor( d, block_time ); vtor( o ); if( block_time < HARDFORK_CORE_214_TIME ) - { // cannot be removed after hf, unfortunately + { + // cannot be removed after hf, unfortunately hardfork_visitor_214 hf214; - for (const op_wrapper &op : o.proposed_ops) + for( const op_wrapper &op : o.proposed_ops ) op.op.visit( hf214 ); } vtor_1479( o ); @@ -128,52 +194,58 @@ void_result proposal_create_evaluator::do_evaluate(const proposal_create_operati FC_ASSERT( o.expiration_time > block_time, "Proposal has already expired on creation." ); FC_ASSERT( o.expiration_time <= block_time + global_parameters.maximum_proposal_lifetime, - "Proposal expiration time is too far in the future."); - FC_ASSERT( !o.review_period_seconds || fc::seconds(*o.review_period_seconds) < (o.expiration_time - block_time), - "Proposal review period must be less than its overall lifetime." ); - + "Proposal expiration time is too far in the future." ); + FC_ASSERT( !o.review_period_seconds || + fc::seconds( *o.review_period_seconds ) < ( o.expiration_time - block_time ), + "Proposal review period must be less than its overall lifetime." ); + + // Find all authorities required by the proposed operations + flat_set tmp_required_active_auths; + vector other; + for( auto& op : o.proposed_ops ) { - // If we're dealing with the committee authority, make sure this transaction has a sufficient review period. - flat_set auths; - vector other; - for( auto& op : o.proposed_ops ) - { - operation_get_required_authorities(op.op, auths, auths, other); - } - - FC_ASSERT( other.size() == 0 ); // TODO: what about other??? - - if( auths.find(GRAPHENE_COMMITTEE_ACCOUNT) != auths.end() ) - { - GRAPHENE_ASSERT( - o.review_period_seconds.valid(), - proposal_create_review_period_required, - "Review period not given, but at least ${min} required", - ("min", global_parameters.committee_proposal_review_period) - ); - GRAPHENE_ASSERT( - *o.review_period_seconds >= global_parameters.committee_proposal_review_period, - proposal_create_review_period_insufficient, - "Review period of ${t} specified, but at least ${min} required", - ("t", *o.review_period_seconds) - ("min", global_parameters.committee_proposal_review_period) - ); - } + operation_get_required_authorities( op.op, tmp_required_active_auths, _required_owner_auths, other, + MUST_IGNORE_CUSTOM_OP_REQD_AUTHS( block_time ) ); + } + // All accounts which must provide both owner and active authority should be omitted from the + // active authority set; owner authority approval implies active authority approval. + std::set_difference( tmp_required_active_auths.begin(), tmp_required_active_auths.end(), + _required_owner_auths.begin(), _required_owner_auths.end(), + std::inserter( _required_active_auths, _required_active_auths.begin() ) ); + + // TODO: what about other??? + FC_ASSERT ( other.empty(), + "Proposals containing operations requiring non-account authorities are not yet implemented." ); + + // If we're dealing with the committee authority, make sure this transaction has a sufficient review period. + if( _required_active_auths.count( GRAPHENE_COMMITTEE_ACCOUNT ) || + _required_owner_auths.count( GRAPHENE_COMMITTEE_ACCOUNT ) ) + { + GRAPHENE_ASSERT( o.review_period_seconds.valid(), + proposal_create_review_period_required, + "Review period not given, but at least ${min} required", + ("min", global_parameters.committee_proposal_review_period) ); + GRAPHENE_ASSERT( *o.review_period_seconds >= global_parameters.committee_proposal_review_period, + proposal_create_review_period_insufficient, + "Review period of ${t} specified, but at least ${min} required", + ("t", *o.review_period_seconds) + ("min", global_parameters.committee_proposal_review_period) ); } for( const op_wrapper& op : o.proposed_ops ) - _proposed_trx.operations.push_back(op.op); + _proposed_trx.operations.push_back( op.op ); _proposed_trx.validate(); return void_result(); } FC_CAPTURE_AND_RETHROW( (o) ) } -object_id_type proposal_create_evaluator::do_apply(const proposal_create_operation& o) +object_id_type proposal_create_evaluator::do_apply( const proposal_create_operation& o ) { try { database& d = db(); + auto chain_time = d.head_block_time(); - const proposal_object& proposal = d.create([&](proposal_object& proposal) { + const proposal_object& proposal = d.create( [&o, this, chain_time](proposal_object& proposal) { _proposed_trx.expiration = o.expiration_time; proposal.proposed_transaction = _proposed_trx; proposal.expiration_time = o.expiration_time; @@ -182,20 +254,10 @@ object_id_type proposal_create_evaluator::do_apply(const proposal_create_operati proposal.review_period_time = o.expiration_time - *o.review_period_seconds; //Populate the required approval sets - flat_set required_active; - vector other; - - // TODO: consider caching values from evaluate? - for( auto& op : _proposed_trx.operations ) - operation_get_required_authorities(op, required_active, proposal.required_owner_approvals, other); - - //All accounts which must provide both owner and active authority should be omitted from the active authority set; - //owner authority approval implies active authority approval. - std::set_difference(required_active.begin(), required_active.end(), - proposal.required_owner_approvals.begin(), proposal.required_owner_approvals.end(), - std::inserter(proposal.required_active_approvals, proposal.required_active_approvals.begin())); - - if( d.head_block_time() > HARDFORK_CORE_1479_TIME ) + proposal.required_owner_approvals.insert( _required_owner_auths.begin(), _required_owner_auths.end() ); + proposal.required_active_approvals.insert( _required_active_auths.begin(), _required_active_auths.end() ); + + if( chain_time > HARDFORK_CORE_1479_TIME ) FC_ASSERT( vtor_1479.nested_update_count == 0 || proposal.id.instance() > vtor_1479.max_update_instance, "Cannot update/delete a proposal with a future id!" ); else if( vtor_1479.nested_update_count > 0 && proposal.id.instance() <= vtor_1479.max_update_instance ) @@ -213,7 +275,7 @@ object_id_type proposal_create_evaluator::do_apply(const proposal_create_operati return proposal.id; } FC_CAPTURE_AND_RETHROW( (o) ) } -void_result proposal_update_evaluator::do_evaluate(const proposal_update_operation& o) +void_result proposal_update_evaluator::do_evaluate( const proposal_update_operation& o ) { try { database& d = db(); @@ -241,9 +303,9 @@ void_result proposal_update_evaluator::do_apply(const proposal_update_operation& { try { database& d = db(); - // Potential optimization: if _executed_proposal is true, we can skip the modify step and make push_proposal skip - // signature checks. This isn't done now because I just wrote all the proposals code, and I'm not yet 100% sure the - // required approvals are sufficient to authorize the transaction. + // Potential optimization: if _executed_proposal is true, we can skip the modify step and make push_proposal + // skip signature checks. This isn't done now because I just wrote all the proposals code, and I'm not yet + // 100% sure the required approvals are sufficient to authorize the transaction. d.modify(*_proposal, [&o](proposal_object& p) { p.available_active_approvals.insert(o.active_approvals_to_add.begin(), o.active_approvals_to_add.end()); p.available_owner_approvals.insert(o.owner_approvals_to_add.begin(), o.owner_approvals_to_add.end()); diff --git a/libraries/chain/proposal_object.cpp b/libraries/chain/proposal_object.cpp index 62502e2736..3c5244d62c 100644 --- a/libraries/chain/proposal_object.cpp +++ b/libraries/chain/proposal_object.cpp @@ -25,20 +25,26 @@ #include #include #include +#include + +#include namespace graphene { namespace chain { -bool proposal_object::is_authorized_to_execute(database& db) const +bool proposal_object::is_authorized_to_execute( database& db ) const { - transaction_evaluation_state dry_run_eval(&db); + transaction_evaluation_state dry_run_eval( &db ); try { bool allow_non_immediate_owner = ( db.head_block_time() >= HARDFORK_CORE_584_TIME ); - verify_authority( proposed_transaction.operations, + verify_authority( proposed_transaction.operations, available_key_approvals, - [&]( account_id_type id ){ return &id(db).active; }, - [&]( account_id_type id ){ return &id(db).owner; }, + [&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, true, /* allow committee */ available_active_approvals, diff --git a/libraries/chain/small_objects.cpp b/libraries/chain/small_objects.cpp index ed68a4eaef..c308519e4e 100644 --- a/libraries/chain/small_objects.cpp +++ b/libraries/chain/small_objects.cpp @@ -41,6 +41,7 @@ #include #include #include +#include #include @@ -127,7 +128,7 @@ FC_REFLECT_DERIVED_NO_TYPENAME( graphene::chain::htlc_object::condition_info::ti FC_REFLECT_DERIVED_NO_TYPENAME( graphene::chain::htlc_object::condition_info, BOOST_PP_SEQ_NIL, (hash_lock)(time_lock) ) FC_REFLECT_DERIVED_NO_TYPENAME( graphene::chain::htlc_object, (graphene::db::object), - (transfer) (conditions) ) + (transfer) (conditions) (memo) ) FC_REFLECT_DERIVED_NO_TYPENAME( graphene::chain::operation_history_object, (graphene::chain::object), (op)(result)(block_num)(trx_in_block)(op_in_trx)(virtual_op) ) @@ -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 2f776301cd..73a7f08f00 160000 --- a/libraries/fc +++ b/libraries/fc @@ -1 +1 @@ -Subproject commit 2f776301cd8410525d8b6f16127fcdaa0a16074d +Subproject commit 73a7f08f00456b0984cd431dd8f55bd901282e15 diff --git a/libraries/net/include/graphene/net/node.hpp b/libraries/net/include/graphene/net/node.hpp index fe03ac0cb6..43aa94a6e9 100644 --- a/libraries/net/include/graphene/net/node.hpp +++ b/libraries/net/include/graphene/net/node.hpp @@ -211,11 +211,34 @@ namespace graphene { namespace net { */ void add_node( const fc::ip::endpoint& ep ); + /***** + * @brief add a list of nodes to seed the p2p network + * @param seeds a vector of url strings + */ + void add_seed_nodes( std::vector seeds ); + + /**** + * @brief add a node to seed the p2p network + * @param in the url as a string + */ + void add_seed_node( const std::string& in); + /** * Attempt to connect to the specified endpoint immediately. */ virtual void connect_to_endpoint( const fc::ip::endpoint& ep ); + /** + * @brief Helper to convert a string to a collection of endpoints + * + * This converts a string (i.e. "bitshares.eu:665535" to a collection of endpoints. + * NOTE: Throws an exception if not in correct format or was unable to resolve URL. + * + * @param in the incoming string + * @returns a vector of endpoints + */ + static std::vector resolve_string_to_ip_endpoints( const std::string& in ); + /** * Specifies the network interface and port upon which incoming * connections should be accepted. diff --git a/libraries/net/node.cpp b/libraries/net/node.cpp index 0cd7af3fc4..f2fd144488 100644 --- a/libraries/net/node.cpp +++ b/libraries/net/node.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include @@ -70,6 +71,7 @@ #include #include #include +#include #include #include @@ -485,6 +487,43 @@ namespace graphene { namespace net { namespace detail { // _retrigger_connect_loop_promise->set_value(); } + void node_impl::update_seed_nodes_task() + { + VERIFY_CORRECT_THREAD(); + + try + { + dlog("Starting an iteration of update_seed_nodes loop."); + for( const std::string& endpoint_string : _seed_nodes ) + { + resolve_seed_node_and_add( endpoint_string ); + } + dlog("Done an iteration of update_seed_nodes loop."); + } + catch (const fc::canceled_exception&) + { + throw; + } + FC_CAPTURE_AND_LOG( (_seed_nodes) ) + + schedule_next_update_seed_nodes_task(); + } + + void node_impl::schedule_next_update_seed_nodes_task() + { + VERIFY_CORRECT_THREAD(); + + if( _node_is_shutting_down ) + return; + + if( _update_seed_nodes_loop_done.valid() && _update_seed_nodes_loop_done.canceled() ) + return; + + _update_seed_nodes_loop_done = fc::schedule( [this]() { update_seed_nodes_task(); }, + fc::time_point::now() + fc::hours(3), + "update_seed_nodes_loop" ); + } + bool node_impl::have_already_received_sync_item( const item_hash_t& item_hash ) { VERIFY_CORRECT_THREAD(); @@ -867,17 +906,16 @@ namespace graphene { namespace net { namespace detail { // timeout for any active peers is two block intervals uint32_t active_disconnect_timeout = 10 * _recent_block_interval_in_seconds; uint32_t active_send_keepalive_timeout = active_disconnect_timeout / 2; - - // set the ignored request time out to 1 second. When we request a block + + // set the ignored request time out to 6 second. When we request a block // or transaction from a peer, this timeout determines how long we wait for them // to reply before we give up and ask another peer for the item. // Ideally this should be significantly shorter than the block interval, because - // we'd like to realize the block isn't coming and fetch it from a different - // peer before the next block comes in. At the current target of 3 second blocks, - // 1 second seems reasonable. When we get closer to our eventual target of 1 second - // blocks, this will need to be re-evaluated (i.e., can we set the timeout to 500ms - // and still handle normal network & processing delays without excessive disconnects) - fc::microseconds active_ignored_request_timeout = fc::seconds(1); + // we'd like to realize the block isn't coming and fetch it from a different + // peer before the next block comes in. + // Increased to 6 from 1 in #1660 due to heavy load. May need to adjust further + // Note: #1660 is https://github.com/steemit/steem/issues/1660 + fc::microseconds active_ignored_request_timeout = fc::seconds(6); fc::time_point active_disconnect_threshold = fc::time_point::now() - fc::seconds(active_disconnect_timeout); fc::time_point active_send_keepalive_threshold = fc::time_point::now() - fc::seconds(active_send_keepalive_timeout); @@ -3849,6 +3887,20 @@ namespace graphene { namespace net { namespace detail { wlog( "Exception thrown while terminating Fetch updated peer lists loop, ignoring" ); } + try + { + _update_seed_nodes_loop_done.cancel_and_wait("node_impl::close()"); + dlog("Update seed nodes loop terminated"); + } + catch ( const fc::exception& e ) + { + wlog( "Exception thrown while terminating Update seed nodes loop, ignoring: ${e}", ("e", e) ); + } + catch (...) + { + wlog( "Exception thrown while terminating Update seed nodes loop, ignoring" ); + } + try { _bandwidth_monitor_loop_done.cancel_and_wait("node_impl::close()"); @@ -4253,6 +4305,7 @@ namespace graphene { namespace net { namespace detail { assert(!_accept_loop_complete.valid() && !_p2p_network_connect_loop_done.valid() && + !_update_seed_nodes_loop_done.valid() && !_fetch_sync_items_loop_done.valid() && !_fetch_item_loop_done.valid() && !_advertise_inventory_loop_done.valid() && @@ -4270,6 +4323,7 @@ namespace graphene { namespace net { namespace detail { _fetch_updated_peer_lists_loop_done = fc::async([=](){ fetch_updated_peer_lists_loop(); }, "fetch_updated_peer_lists_loop"); _bandwidth_monitor_loop_done = fc::async([=](){ bandwidth_monitor_loop(); }, "bandwidth_monitor_loop"); _dump_node_status_task_done = fc::async([=](){ dump_node_status_task(); }, "dump_node_status_task"); + schedule_next_update_seed_nodes_task(); } void node_impl::add_node(const fc::ip::endpoint& ep) @@ -4287,6 +4341,33 @@ namespace graphene { namespace net { namespace detail { trigger_p2p_network_connect_loop(); } + void node_impl::add_seed_node(const std::string& endpoint_string) + { + VERIFY_CORRECT_THREAD(); + _seed_nodes.insert( endpoint_string ); + resolve_seed_node_and_add( endpoint_string ); + } + + void node_impl::resolve_seed_node_and_add(const std::string& endpoint_string) + { + VERIFY_CORRECT_THREAD(); + std::vector endpoints; + ilog("Resolving seed node ${endpoint}", ("endpoint", endpoint_string)); + try + { + endpoints = graphene::net::node::resolve_string_to_ip_endpoints(endpoint_string); + } + catch(...) + { + wlog( "Unable to resolve endpoint during attempt to add seed node ${ep}", ("ep", endpoint_string) ); + } + for (const fc::ip::endpoint& endpoint : endpoints) + { + ilog("Adding seed node ${endpoint}", ("endpoint", endpoint)); + add_node(endpoint); + } + } + void node_impl::initiate_connect_to(const peer_connection_ptr& new_peer) { new_peer->get_socket().open(); @@ -5207,4 +5288,52 @@ namespace graphene { namespace net { namespace detail { } // end namespace detail + // TODO move this function to impl class + std::vector node::resolve_string_to_ip_endpoints(const std::string& in) + { + try + { + std::string::size_type colon_pos = in.find(':'); + if (colon_pos == std::string::npos) + FC_THROW("Missing required port number in endpoint string \"${endpoint_string}\"", + ("endpoint_string", in)); + std::string port_string = in.substr(colon_pos + 1); + try + { + uint16_t port = boost::lexical_cast(port_string); + + std::string hostname = in.substr(0, colon_pos); + std::vector endpoints = fc::resolve(hostname, port); + if (endpoints.empty()) + FC_THROW_EXCEPTION( fc::unknown_host_exception, + "The host name can not be resolved: ${hostname}", + ("hostname", hostname) ); + return endpoints; + } + catch (const boost::bad_lexical_cast&) + { + FC_THROW("Bad port: ${port}", ("port", port_string)); + } + } + FC_CAPTURE_AND_RETHROW((in)) + } + + void node::add_seed_nodes(std::vector seeds) + { + for(const std::string& endpoint_string : seeds ) + { + try { + add_seed_node(endpoint_string); + } catch( const fc::exception& e ) { + wlog( "caught exception ${e} while adding seed node ${endpoint}", + ("e", e.to_detail_string())("endpoint", endpoint_string) ); + } + } + } + + void node::add_seed_node(const std::string& in) + { + INVOKE_IN_IMPL(add_seed_node, in); + } + } } // end namespace graphene::net diff --git a/libraries/net/node_impl.hxx b/libraries/net/node_impl.hxx index 4ad6873050..a106cbc019 100644 --- a/libraries/net/node_impl.hxx +++ b/libraries/net/node_impl.hxx @@ -439,6 +439,14 @@ class node_impl : public peer_connection_delegate std::list > _handle_message_calls_in_progress; + /// used by the task that checks whether addresses of seed nodes have been updated + // @{ + boost::container::flat_set _seed_nodes; + fc::future _update_seed_nodes_loop_done; + void update_seed_nodes_task(); + void schedule_next_update_seed_nodes_task(); + // @} + node_impl(const std::string& user_agent); virtual ~node_impl(); @@ -584,6 +592,8 @@ class node_impl : public peer_connection_delegate void listen_to_p2p_network(); void connect_to_p2p_network(); void add_node( const fc::ip::endpoint& ep ); + void add_seed_node( const std::string& seed_string ); + void resolve_seed_node_and_add( const std::string& seed_string ); void initiate_connect_to(const peer_connection_ptr& peer); void connect_to_endpoint(const fc::ip::endpoint& ep); void listen_on_endpoint(const fc::ip::endpoint& ep , bool wait_if_not_available); diff --git a/libraries/net/peer_database.cpp b/libraries/net/peer_database.cpp index 0e4867675d..ac8c9eeb19 100644 --- a/libraries/net/peer_database.cpp +++ b/libraries/net/peer_database.cpp @@ -50,7 +50,8 @@ namespace graphene { namespace net { indexed_by, member >, + &potential_peer_record::last_seen_time>, + std::greater >, hashed_unique, member #include #include +#include #include @@ -128,12 +129,15 @@ void account_history_plugin_impl::update_account_histories( const signed_block& // get the set of accounts this operation applies to flat_set impacted; vector other; - operation_get_required_authorities( op.op, impacted, impacted, other ); // fee_payer is added here + // fee payer is added here + operation_get_required_authorities( op.op, impacted, impacted, other, + MUST_IGNORE_CUSTOM_OP_REQD_AUTHS( db.head_block_time() ) ); if( op.op.is_type< account_create_operation >() ) impacted.insert( op.result.get() ); else - graphene::chain::operation_get_impacted_accounts( op.op, impacted ); + operation_get_impacted_accounts( op.op, impacted, + MUST_IGNORE_CUSTOM_OP_REQD_AUTHS( db.head_block_time() ) ); for( auto& a : other ) for( auto& item : a.account_auths ) diff --git a/libraries/plugins/custom_operations/custom_evaluators.cpp b/libraries/plugins/custom_operations/custom_evaluators.cpp index 6270f011e0..0b300b5369 100644 --- a/libraries/plugins/custom_operations/custom_evaluators.cpp +++ b/libraries/plugins/custom_operations/custom_evaluators.cpp @@ -37,8 +37,9 @@ custom_generic_evaluator::custom_generic_evaluator(database& db, const account_i vector custom_generic_evaluator::do_apply(const account_storage_map& op) { - auto &index = _db->get_index_type().indices().get(); + const auto &index = _db->get_index_type().indices().get(); vector results; + results.reserve( op.key_values.size() ); if (op.remove) { @@ -54,29 +55,29 @@ vector custom_generic_evaluator::do_apply(const account_storage_ for(auto const& row: op.key_values) { if(row.first.length() > CUSTOM_OPERATIONS_MAX_KEY_SIZE) { - dlog("Key can't be bigger than ${max} characters", ("max", CUSTOM_OPERATIONS_MAX_KEY_SIZE)); + wlog("Key can't be bigger than ${max} characters", ("max", CUSTOM_OPERATIONS_MAX_KEY_SIZE)); continue; } auto itr = index.find(make_tuple(_account, op.catalog, row.first)); if(itr == index.end()) { try { - auto created = _db->create( [&op, this, &row]( account_storage_object& aso ) { - aso.catalog = op.catalog; + const auto& created = _db->create( + [&op, this, &row]( account_storage_object& aso ) { aso.account = _account; + aso.catalog = op.catalog; aso.key = row.first; if(row.second.valid()) aso.value = fc::json::from_string(*row.second); }); results.push_back(created.id); } - catch(const fc::parse_error_exception& e) { dlog(e.to_detail_string()); } + catch(const fc::parse_error_exception& e) { wlog(e.to_detail_string()); } } else { try { _db->modify(*itr, [&op, this, &row](account_storage_object &aso) { - aso.key = row.first; if(row.second.valid()) aso.value = fc::json::from_string(*row.second); else @@ -84,7 +85,7 @@ vector custom_generic_evaluator::do_apply(const account_storage_ }); results.push_back(itr->id); } - catch(const fc::parse_error_exception& e) { dlog((e.to_detail_string())); } + catch(const fc::parse_error_exception& e) { wlog((e.to_detail_string())); } } } } diff --git a/libraries/plugins/custom_operations/custom_operations_plugin.cpp b/libraries/plugins/custom_operations/custom_operations_plugin.cpp index 562bcaf217..64d5959d90 100644 --- a/libraries/plugins/custom_operations/custom_operations_plugin.cpp +++ b/libraries/plugins/custom_operations/custom_operations_plugin.cpp @@ -40,7 +40,7 @@ class custom_operations_plugin_impl { } virtual ~custom_operations_plugin_impl(); - void onBlock( const signed_block& b ); + void onBlock(); graphene::chain::database& database() { @@ -49,6 +49,8 @@ class custom_operations_plugin_impl custom_operations_plugin& _self; + uint32_t _start_block = 45000000; + private: }; @@ -69,7 +71,7 @@ struct custom_op_visitor } }; -void custom_operations_plugin_impl::onBlock( const signed_block& b ) +void custom_operations_plugin_impl::onBlock() { graphene::chain::database& db = database(); const vector >& hist = db.get_applied_operations(); @@ -88,8 +90,8 @@ void custom_operations_plugin_impl::onBlock( const signed_block& b ) custom_op_visitor vtor(db, custom_op.fee_payer()); unpacked.visit(vtor); } - catch (fc::exception e) { // only api node will know if the unpack, validate or apply fails - dlog("Custom operations plugin serializing error: ${ex} in operation: ${op}", + catch (fc::exception& e) { // only api node will know if the unpack, validate or apply fails + wlog("Custom operations plugin serializing error: ${ex} in operation: ${op}", ("ex", e.to_detail_string())("op", fc::json::to_string(custom_op))); continue; } @@ -126,14 +128,25 @@ void custom_operations_plugin::plugin_set_program_options( boost::program_options::options_description& cfg ) { + cli.add_options() + ("custom-operations-start-block", boost::program_options::value()->default_value(45000000), + "Start processing custom operations transactions with the plugin only after this block") + ; + cfg.add(cli); + } void custom_operations_plugin::plugin_initialize(const boost::program_options::variables_map& options) { database().add_index< primary_index< account_storage_index > >(); + if (options.count("custom-operations-start-block")) { + my->_start_block = options["custom-operations-start-block"].as(); + } + database().applied_block.connect( [this]( const signed_block& b) { - my->onBlock(b); + if( b.block_num() >= my->_start_block ) + my->onBlock(); } ); } diff --git a/libraries/plugins/custom_operations/include/graphene/custom_operations/custom_objects.hpp b/libraries/plugins/custom_operations/include/graphene/custom_operations/custom_objects.hpp index e56404f2aa..b5f6684c89 100644 --- a/libraries/plugins/custom_operations/include/graphene/custom_operations/custom_objects.hpp +++ b/libraries/plugins/custom_operations/include/graphene/custom_operations/custom_objects.hpp @@ -51,24 +51,13 @@ struct account_storage_object : public abstract_object optional value; }; -struct by_custom_id; -struct by_custom_account; -struct by_account_catalog; struct by_account_catalog_key; typedef multi_index_container< account_storage_object, indexed_by< - ordered_non_unique< tag, member< object, object_id_type, &object::id > >, - ordered_non_unique< tag, - member< account_storage_object, account_id_type, &account_storage_object::account > >, - ordered_non_unique< tag, - composite_key< account_storage_object, - member< account_storage_object, account_id_type, &account_storage_object::account >, - member< account_storage_object, string, &account_storage_object::catalog > - > - >, - ordered_non_unique< tag, + ordered_unique< tag, member< object, object_id_type, &object::id > >, + ordered_unique< tag, composite_key< account_storage_object, member< account_storage_object, account_id_type, &account_storage_object::account >, member< account_storage_object, string, &account_storage_object::catalog >, diff --git a/libraries/plugins/elasticsearch/elasticsearch_plugin.cpp b/libraries/plugins/elasticsearch/elasticsearch_plugin.cpp index 5cfee22ebd..9c4e279677 100644 --- a/libraries/plugins/elasticsearch/elasticsearch_plugin.cpp +++ b/libraries/plugins/elasticsearch/elasticsearch_plugin.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include namespace graphene { namespace elasticsearch { @@ -155,12 +156,15 @@ bool elasticsearch_plugin_impl::update_account_histories( const signed_block& b // get the set of accounts this operation applies to flat_set impacted; vector other; - operation_get_required_authorities( op.op, impacted, impacted, other ); // fee_payer is added here + // fee_payer is added here + operation_get_required_authorities( op.op, impacted, impacted, other, + MUST_IGNORE_CUSTOM_OP_REQD_AUTHS( db.head_block_time() ) ); if( op.op.is_type< account_create_operation >() ) impacted.insert( op.result.get() ); else - graphene::chain::operation_get_impacted_accounts( op.op, impacted ); + operation_get_impacted_accounts( op.op, impacted, + MUST_IGNORE_CUSTOM_OP_REQD_AUTHS( db.head_block_time() ) ); for( auto& a : other ) for( auto& item : a.account_auths ) diff --git a/libraries/protocol/CMakeLists.txt b/libraries/protocol/CMakeLists.txt index 11a3b66016..46cdcc04ec 100644 --- a/libraries/protocol/CMakeLists.txt +++ b/libraries/protocol/CMakeLists.txt @@ -7,6 +7,7 @@ list(APPEND SOURCES account.cpp confidential.cpp chain_parameters.cpp fee_schedule.cpp + fee_schedule_calc.cpp memo.cpp proposal.cpp transfer.cpp @@ -16,6 +17,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 +32,38 @@ list(APPEND SOURCES account.cpp htlc.cpp) +list(APPEND CUSTOM_AUTHS_FILES + custom_authorities/create_predicate_fwd_1.cpp + custom_authorities/create_predicate_fwd_2.cpp + custom_authorities/create_predicate_fwd_3.cpp + 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 + custom_authorities/list_8.cpp + custom_authorities/list_9.cpp + custom_authorities/list_10.cpp) + +file(GLOB CUSTOM_AUTHS_HEADERS "custom_authorities/*.hxx") + +add_library( graphene_protocol_custom_auths ${CUSTOM_AUTHS_FILES} ${CUSTOM_AUTHS_HEADERS} ) +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} ${SOURCES} PROPERTIES COMPILE_FLAGS /bigobj ) +else( MSVC ) + if( MINGW ) + set_source_files_properties( ${CUSTOM_AUTHS_FILES} ${SOURCES} PROPERTIES COMPILE_FLAGS -Wa,-mbig-obj ) + endif( MINGW ) +endif( MSVC ) + 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/asset_ops.cpp b/libraries/protocol/asset_ops.cpp index ba1b7f65f8..8ba6aa2b26 100644 --- a/libraries/protocol/asset_ops.cpp +++ b/libraries/protocol/asset_ops.cpp @@ -216,7 +216,13 @@ void asset_options::validate()const { FC_ASSERT( max_supply > 0 ); FC_ASSERT( max_supply <= GRAPHENE_MAX_SHARE_SUPPLY ); + // The non-negative maker fee must be less than or equal to 100% FC_ASSERT( market_fee_percent <= GRAPHENE_100_PERCENT ); + + // The non-negative taker fee must be less than or equal to 100% + if( extensions.value.taker_fee_percent.valid() ) + FC_ASSERT( *extensions.value.taker_fee_percent <= GRAPHENE_100_PERCENT ); + FC_ASSERT( max_market_fee >= 0 && max_market_fee <= GRAPHENE_MAX_SHARE_SUPPLY ); // There must be no high bits in permissions whose meaning is not known. FC_ASSERT( !(issuer_permissions & ~ASSET_ISSUER_PERMISSION_MASK) ); @@ -239,12 +245,14 @@ void asset_options::validate()const FC_ASSERT( whitelist_markets.find(item) == whitelist_markets.end() ); } if( extensions.value.reward_percent.valid() ) - FC_ASSERT( *extensions.value.reward_percent < GRAPHENE_100_PERCENT ); + FC_ASSERT( *extensions.value.reward_percent <= GRAPHENE_100_PERCENT ); } void asset_claim_fees_operation::validate()const { FC_ASSERT( fee.amount >= 0 ); FC_ASSERT( amount_to_claim.amount > 0 ); + if( extensions.value.claim_from_asset_id.valid() ) + FC_ASSERT( *extensions.value.claim_from_asset_id != amount_to_claim.asset_id ); } void asset_claim_pool_operation::validate()const { @@ -265,6 +273,7 @@ GRAPHENE_IMPLEMENT_EXTERNAL_SERIALIZATION( graphene::protocol::asset_settle_oper GRAPHENE_IMPLEMENT_EXTERNAL_SERIALIZATION( graphene::protocol::asset_fund_fee_pool_operation::fee_parameters_type ) GRAPHENE_IMPLEMENT_EXTERNAL_SERIALIZATION( graphene::protocol::asset_claim_pool_operation::fee_parameters_type ) GRAPHENE_IMPLEMENT_EXTERNAL_SERIALIZATION( graphene::protocol::asset_claim_fees_operation::fee_parameters_type ) +GRAPHENE_IMPLEMENT_EXTERNAL_SERIALIZATION( graphene::protocol::asset_claim_fees_operation::additional_options_type ) GRAPHENE_IMPLEMENT_EXTERNAL_SERIALIZATION( graphene::protocol::asset_update_operation::fee_parameters_type ) GRAPHENE_IMPLEMENT_EXTERNAL_SERIALIZATION( graphene::protocol::asset_update_issuer_operation::fee_parameters_type ) GRAPHENE_IMPLEMENT_EXTERNAL_SERIALIZATION( graphene::protocol::asset_update_bitasset_operation::fee_parameters_type ) diff --git a/libraries/protocol/chain_parameters.cpp b/libraries/protocol/chain_parameters.cpp index eed40e8416..1169224f41 100644 --- a/libraries/protocol/chain_parameters.cpp +++ b/libraries/protocol/chain_parameters.cpp @@ -78,6 +78,53 @@ namespace graphene { namespace protocol { return *this; } + void chain_parameters::validate()const + { + get_current_fees().validate(); + FC_ASSERT( reserve_percent_of_fee <= GRAPHENE_100_PERCENT ); + FC_ASSERT( network_percent_of_fee <= GRAPHENE_100_PERCENT ); + FC_ASSERT( lifetime_referrer_percent_of_fee <= GRAPHENE_100_PERCENT ); + FC_ASSERT( network_percent_of_fee + lifetime_referrer_percent_of_fee <= GRAPHENE_100_PERCENT ); + + FC_ASSERT( block_interval >= GRAPHENE_MIN_BLOCK_INTERVAL ); + FC_ASSERT( block_interval <= GRAPHENE_MAX_BLOCK_INTERVAL ); + FC_ASSERT( block_interval > 0 ); + FC_ASSERT( maintenance_interval > block_interval, + "Maintenance interval must be longer than block interval" ); + FC_ASSERT( maintenance_interval % block_interval == 0, + "Maintenance interval must be a multiple of block interval" ); + FC_ASSERT( maximum_transaction_size >= GRAPHENE_MIN_TRANSACTION_SIZE_LIMIT, + "Transaction size limit is too low" ); + FC_ASSERT( maximum_block_size >= GRAPHENE_MIN_BLOCK_SIZE_LIMIT, + "Block size limit is too low" ); + FC_ASSERT( maximum_time_until_expiration > block_interval, + "Maximum transaction expiration time must be greater than a block interval" ); + FC_ASSERT( maximum_proposal_lifetime - committee_proposal_review_period > block_interval, + "Committee proposal review period must be less than the maximum proposal lifetime" ); + if( extensions.value.market_fee_network_percent.valid() ) + { + FC_ASSERT( *extensions.value.market_fee_network_percent <= 3000, // GRAPHENE_100_PERCENT is 10000 + "The market_fee_network_percent parameter can not exceed 30%" ); + } + if( extensions.value.maker_fee_discount_percent.valid() ) + { + FC_ASSERT( *extensions.value.maker_fee_discount_percent <= GRAPHENE_100_PERCENT, + "The maker_fee_discount_percent parameter can not exceed 100%" ); + } + } + + uint16_t chain_parameters::get_market_fee_network_percent() const + { + return extensions.value.market_fee_network_percent.valid() ? + *extensions.value.market_fee_network_percent : 0; + } + + uint16_t chain_parameters::get_maker_fee_discount_percent() const + { + return extensions.value.maker_fee_discount_percent.valid() ? + *extensions.value.maker_fee_discount_percent : 0; + } + }} GRAPHENE_IMPLEMENT_EXTERNAL_SERIALIZATION( graphene::protocol::chain_parameters ) 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/create_predicate_fwd.hxx b/libraries/protocol/custom_authorities/create_predicate_fwd.hxx new file mode 100644 index 0000000000..d4cfface6c --- /dev/null +++ b/libraries/protocol/custom_authorities/create_predicate_fwd.hxx @@ -0,0 +1,97 @@ +/* + * 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. + */ + +/* This file contains forward declarations for externalized specializations of the create_predicate_function + * template. Generate using the following shell code, and paste below. + +FWD_FIELD_TYPES="share_type asset_id_type flat_set asset price" +FWD_FIELD_TYPES="$FWD_FIELD_TYPES string std::vector time_point_sec" +FWD_FIELD_TYPES="$FWD_FIELD_TYPES account_id_type flat_set public_key_type authority" +FWD_FIELD_TYPES="$FWD_FIELD_TYPES optional" +FWD_FIELD_TYPES="$FWD_FIELD_TYPES bool uint8_t uint16_t uint32_t unsigned_int extensions_type" + +for T in $FWD_FIELD_TYPES; do + echo "extern template" + echo "object_restriction_predicate<$T> create_predicate_function( " + echo " restriction_function func, restriction_argument arg );" +done + * ---------------- CUT ---------------- */ + +extern template +object_restriction_predicate create_predicate_function( + restriction_function func, restriction_argument arg ); +extern template +object_restriction_predicate create_predicate_function( + restriction_function func, restriction_argument arg ); +extern template +object_restriction_predicate> create_predicate_function( + restriction_function func, restriction_argument arg ); +extern template +object_restriction_predicate create_predicate_function( + restriction_function func, restriction_argument arg ); +extern template +object_restriction_predicate create_predicate_function( + restriction_function func, restriction_argument arg ); +extern template +object_restriction_predicate create_predicate_function( + restriction_function func, restriction_argument arg ); +extern template +object_restriction_predicate> create_predicate_function( + restriction_function func, restriction_argument arg ); +extern template +object_restriction_predicate create_predicate_function( + restriction_function func, restriction_argument arg ); +extern template +object_restriction_predicate create_predicate_function( + restriction_function func, restriction_argument arg ); +extern template +object_restriction_predicate> create_predicate_function( + restriction_function func, restriction_argument arg ); +extern template +object_restriction_predicate create_predicate_function( + restriction_function func, restriction_argument arg ); +extern template +object_restriction_predicate create_predicate_function( + restriction_function func, restriction_argument arg ); +extern template +object_restriction_predicate> create_predicate_function( + restriction_function func, restriction_argument arg ); +extern template +object_restriction_predicate create_predicate_function( + restriction_function func, restriction_argument arg ); +extern template +object_restriction_predicate create_predicate_function( + restriction_function func, restriction_argument arg ); +extern template +object_restriction_predicate create_predicate_function( + restriction_function func, restriction_argument arg ); +extern template +object_restriction_predicate create_predicate_function( + restriction_function func, restriction_argument arg ); +extern template +object_restriction_predicate create_predicate_function( + restriction_function func, restriction_argument arg ); +extern template +object_restriction_predicate create_predicate_function( + restriction_function func, restriction_argument arg ); diff --git a/libraries/protocol/custom_authorities/create_predicate_fwd_1.cpp b/libraries/protocol/custom_authorities/create_predicate_fwd_1.cpp new file mode 100644 index 0000000000..8ebb9a7b37 --- /dev/null +++ b/libraries/protocol/custom_authorities/create_predicate_fwd_1.cpp @@ -0,0 +1,67 @@ +/* + * 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" + +/* This file contains explicit specializations of the create_predicate_function template. + * Generate using the following shell code, and paste below. + +FWD_FIELD_TYPES="share_type asset_id_type flat_set asset price" +FWD_FIELD_TYPES="$FWD_FIELD_TYPES string std::vector time_point_sec" + +for T in $FWD_FIELD_TYPES; do + echo "template" + echo "object_restriction_predicate<$T> create_predicate_function( " + echo " restriction_function func, restriction_argument arg );" +done + */ + +namespace graphene { namespace protocol { + +template +object_restriction_predicate create_predicate_function( + restriction_function func, restriction_argument arg ); +template +object_restriction_predicate create_predicate_function( + restriction_function func, restriction_argument arg ); +template +object_restriction_predicate> create_predicate_function( + restriction_function func, restriction_argument arg ); +template +object_restriction_predicate create_predicate_function( + restriction_function func, restriction_argument arg ); +template +object_restriction_predicate create_predicate_function( + restriction_function func, restriction_argument arg ); +template +object_restriction_predicate create_predicate_function( + restriction_function func, restriction_argument arg ); +template +object_restriction_predicate> create_predicate_function( + restriction_function func, restriction_argument arg ); +template +object_restriction_predicate create_predicate_function( + restriction_function func, restriction_argument arg ); + +} } // namespace graphene::protocol diff --git a/libraries/protocol/custom_authorities/create_predicate_fwd_2.cpp b/libraries/protocol/custom_authorities/create_predicate_fwd_2.cpp new file mode 100644 index 0000000000..b4451b80f2 --- /dev/null +++ b/libraries/protocol/custom_authorities/create_predicate_fwd_2.cpp @@ -0,0 +1,57 @@ +/* + * 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" + +/* This file contains explicit specializations of the create_predicate_function template. + * Generate using the following shell code, and paste below. + +FWD_FIELD_TYPES="account_id_type flat_set public_key_type authority optional" + +for T in $FWD_FIELD_TYPES; do + echo "template" + echo "object_restriction_predicate<$T> create_predicate_function( " + echo " restriction_function func, restriction_argument arg );" +done + */ + +namespace graphene { namespace protocol { + +template +object_restriction_predicate create_predicate_function( + restriction_function func, restriction_argument arg ); +template +object_restriction_predicate> create_predicate_function( + restriction_function func, restriction_argument arg ); +template +object_restriction_predicate create_predicate_function( + restriction_function func, restriction_argument arg ); +template +object_restriction_predicate create_predicate_function( + restriction_function func, restriction_argument arg ); +template +object_restriction_predicate> create_predicate_function( + restriction_function func, restriction_argument arg ); + +} } // namespace graphene::protocol diff --git a/libraries/protocol/custom_authorities/create_predicate_fwd_3.cpp b/libraries/protocol/custom_authorities/create_predicate_fwd_3.cpp new file mode 100644 index 0000000000..6a008cc3ec --- /dev/null +++ b/libraries/protocol/custom_authorities/create_predicate_fwd_3.cpp @@ -0,0 +1,60 @@ +/* + * 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" + +/* This file contains explicit specializations of the create_predicate_function template. + * Generate using the following shell code, and paste below. + +FWD_FIELD_TYPES="bool uint8_t uint16_t uint32_t unsigned_int extensions_type" + +for T in $FWD_FIELD_TYPES; do + echo "template" + echo "object_restriction_predicate<$T> create_predicate_function( " + echo " restriction_function func, restriction_argument arg );" +done + */ + +namespace graphene { namespace protocol { + +template +object_restriction_predicate create_predicate_function( + restriction_function func, restriction_argument arg ); +template +object_restriction_predicate create_predicate_function( + restriction_function func, restriction_argument arg ); +template +object_restriction_predicate create_predicate_function( + restriction_function func, restriction_argument arg ); +template +object_restriction_predicate create_predicate_function( + restriction_function func, restriction_argument arg ); +template +object_restriction_predicate create_predicate_function( + restriction_function func, restriction_argument arg ); +template +object_restriction_predicate create_predicate_function( + restriction_function func, restriction_argument arg ); + +} } // namespace graphene::protocol diff --git a/libraries/protocol/custom_authorities/list_1.cpp b/libraries/protocol/custom_authorities/list_1.cpp new file mode 100644 index 0000000000..7102fe2ecc --- /dev/null +++ b/libraries/protocol/custom_authorities/list_1.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" +#include "sliced_lists.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_10.cpp b/libraries/protocol/custom_authorities/list_10.cpp new file mode 100644 index 0000000000..6bcd0a9a10 --- /dev/null +++ b/libraries/protocol/custom_authorities/list_10.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" +#include "sliced_lists.hxx" + +namespace graphene { namespace protocol { +using result_type = object_restriction_predicate; + +result_type get_restriction_predicate_list_10(size_t idx, vector rs) { + return typelist::runtime::dispatch(operation_list_10::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..c42458dcf1 --- /dev/null +++ b/libraries/protocol/custom_authorities/list_2.cpp @@ -0,0 +1,42 @@ +/* + * 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" +#include "sliced_lists.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..9158d6e003 --- /dev/null +++ b/libraries/protocol/custom_authorities/list_3.cpp @@ -0,0 +1,42 @@ +/* + * 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" +#include "sliced_lists.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..2ff2fefb64 --- /dev/null +++ b/libraries/protocol/custom_authorities/list_4.cpp @@ -0,0 +1,42 @@ +/* + * 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" +#include "sliced_lists.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..a1b93b9a44 --- /dev/null +++ b/libraries/protocol/custom_authorities/list_5.cpp @@ -0,0 +1,42 @@ +/* + * 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" +#include "sliced_lists.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..e5d5962c0e --- /dev/null +++ b/libraries/protocol/custom_authorities/list_6.cpp @@ -0,0 +1,42 @@ +/* + * 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" +#include "sliced_lists.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..9ac5c1bfcc --- /dev/null +++ b/libraries/protocol/custom_authorities/list_7.cpp @@ -0,0 +1,42 @@ +/* + * 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" +#include "sliced_lists.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/list_8.cpp b/libraries/protocol/custom_authorities/list_8.cpp new file mode 100644 index 0000000000..32774e285b --- /dev/null +++ b/libraries/protocol/custom_authorities/list_8.cpp @@ -0,0 +1,42 @@ +/* + * 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" +#include "sliced_lists.hxx" + +namespace graphene { namespace protocol { + +using result_type = object_restriction_predicate; + +result_type get_restriction_predicate_list_8(size_t idx, vector rs) { + return typelist::runtime::dispatch(operation_list_8::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_9.cpp b/libraries/protocol/custom_authorities/list_9.cpp new file mode 100644 index 0000000000..27b2e069dd --- /dev/null +++ b/libraries/protocol/custom_authorities/list_9.cpp @@ -0,0 +1,42 @@ +/* + * 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" +#include "sliced_lists.hxx" + +namespace graphene { namespace protocol { + +using result_type = object_restriction_predicate; + +result_type get_restriction_predicate_list_9(size_t idx, vector rs) { + return typelist::runtime::dispatch(operation_list_9::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..1580e564fb --- /dev/null +++ b/libraries/protocol/custom_authorities/restriction_predicate.cpp @@ -0,0 +1,191 @@ +/* + * 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" +#include "sliced_lists.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)); + if (typelist::contains()) + return get_restriction_predicate_list_8(typelist::index_of(), std::move(rs)); + if (typelist::contains()) + return get_restriction_predicate_list_9(typelist::index_of(), std::move(rs)); + if (typelist::contains()) + return get_restriction_predicate_list_10(typelist::index_of(), std::move(rs)); + if (typelist::contains()) + FC_THROW_EXCEPTION( fc::assert_exception, "Virtual operations not allowed!" ); + + // 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..9d2757b787 --- /dev/null +++ b/libraries/protocol/custom_authorities/restriction_predicate.hxx @@ -0,0 +1,595 @@ +/* + * 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) ) +} + +#include "create_predicate_fwd.hxx" + +/** + * @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 < static_cast(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(), static_cast(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(); + }; +} + +} } // 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_authorities/sliced_lists.hxx b/libraries/protocol/custom_authorities/sliced_lists.hxx new file mode 100644 index 0000000000..11ec334621 --- /dev/null +++ b/libraries/protocol/custom_authorities/sliced_lists.hxx @@ -0,0 +1,67 @@ +/* + * 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 + +namespace graphene { namespace protocol { +namespace typelist = fc::typelist; + +// 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>; +using operation_list_8 = static_variant>; +using operation_list_9 = static_variant + ::add // 43 + ::add // 45 + ::add_list> + ::add // 52 + ::finalize>; +using operation_list_10 = static_variant>; +using virtual_operations_list = 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); +object_restriction_predicate get_restriction_predicate_list_8(size_t idx, vector rs); +object_restriction_predicate get_restriction_predicate_list_9(size_t idx, vector rs); +object_restriction_predicate get_restriction_predicate_list_10(size_t idx, vector rs); + +} } // namespace graphene::protocol diff --git a/libraries/protocol/custom_authority.cpp b/libraries/protocol/custom_authority.cpp new file mode 100644 index 0000000000..f7217d01f2 --- /dev/null +++ b/libraries/protocol/custom_authority.cpp @@ -0,0 +1,100 @@ +/* + * 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; + // Note: practically the `*` won't cause an integer overflow, because k.price_per_byte is 32 bit + // and the results of pack_size() won't be too big + 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; + // Note: practically the `*` won't cause an integer overflow, because k.price_per_byte is 32 bit + // and the results of pack_size() won't be too big + 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"); + } + FC_ASSERT( new_enabled.valid() || new_valid_from.valid() || new_valid_to.valid() || new_auth.valid() + || !restrictions_to_remove.empty() || !restrictions_to_add.empty(), + "Must update something" ); +} + +void custom_authority_delete_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 delete custom authority for special accounts"); +} + +} } // graphene::protocol diff --git a/libraries/protocol/fee_schedule.cpp b/libraries/protocol/fee_schedule.cpp index 0c7129dc4b..a67f285625 100644 --- a/libraries/protocol/fee_schedule.cpp +++ b/libraries/protocol/fee_schedule.cpp @@ -25,7 +25,6 @@ #include #include -#include #define MAX_FEE_STABILIZATION_ITERATION 4 @@ -38,7 +37,7 @@ namespace graphene { namespace protocol { fee_schedule fee_schedule::get_default() { fee_schedule result; - for( int i = 0; i < fee_parameters().count(); ++i ) + for( size_t i = 0; i < fee_parameters().count(); ++i ) { fee_parameters x; x.set_which(i); result.parameters.insert(x); @@ -46,45 +45,6 @@ namespace graphene { namespace protocol { return result; } - struct fee_schedule_validate_visitor - { - typedef void result_type; - - template - void operator()( const T& p )const - { - //p.validate(); - } - }; - - void fee_schedule::validate()const - { - for( const auto& f : parameters ) - f.visit( fee_schedule_validate_visitor() ); - } - - struct calc_fee_visitor - { - typedef uint64_t result_type; - - const fee_schedule& param; - const int current_op; - calc_fee_visitor( const fee_schedule& p, const operation& op ):param(p),current_op(op.which()){} - - template - result_type operator()( const OpType& op )const - { - try { - return op.calculate_fee( param.get() ).value; - } catch (fc::assert_exception& e) { - fee_parameters params; params.set_which(current_op); - auto itr = param.parameters.find(params); - if( itr != param.parameters.end() ) params = *itr; - return op.calculate_fee( params.get() ).value; - } - } - }; - struct set_fee_visitor { typedef void result_type; @@ -118,30 +78,11 @@ namespace graphene { namespace protocol { this->scale = 0; } - asset fee_schedule::calculate_fee( const operation& op )const - { - uint64_t required_fee = op.visit( calc_fee_visitor( *this, op ) ); - if( scale != GRAPHENE_100_PERCENT ) - { - auto scaled = fc::uint128_t(required_fee) * scale; - scaled /= GRAPHENE_100_PERCENT; - FC_ASSERT( scaled <= GRAPHENE_MAX_SHARE_SUPPLY, - "Required fee after scaling would exceed maximum possible supply" ); - required_fee = static_cast(scaled); - } - return asset( required_fee ); - } - - asset fee_schedule::calculate_fee( const operation& op, const price& core_exchange_rate )const - { - return calculate_fee( op ).multiply_and_round_up( core_exchange_rate ); - } - asset fee_schedule::set_fee( operation& op, const price& core_exchange_rate )const { auto f = calculate_fee( op, core_exchange_rate ); auto f_max = f; - for( int i=0; i= GRAPHENE_MIN_BLOCK_INTERVAL ); - FC_ASSERT( block_interval <= GRAPHENE_MAX_BLOCK_INTERVAL ); - FC_ASSERT( block_interval > 0 ); - FC_ASSERT( maintenance_interval > block_interval, - "Maintenance interval must be longer than block interval" ); - FC_ASSERT( maintenance_interval % block_interval == 0, - "Maintenance interval must be a multiple of block interval" ); - FC_ASSERT( maximum_transaction_size >= GRAPHENE_MIN_TRANSACTION_SIZE_LIMIT, - "Transaction size limit is too low" ); - FC_ASSERT( maximum_block_size >= GRAPHENE_MIN_BLOCK_SIZE_LIMIT, - "Block size limit is too low" ); - FC_ASSERT( maximum_time_until_expiration > block_interval, - "Maximum transaction expiration time must be greater than a block interval" ); - FC_ASSERT( maximum_proposal_lifetime - committee_proposal_review_period > block_interval, - "Committee proposal review period must be less than the maximum proposal lifetime" ); - } - } } // graphene::protocol - -GRAPHENE_IMPLEMENT_EXTERNAL_SERIALIZATION( graphene::protocol::fee_schedule ) diff --git a/libraries/protocol/fee_schedule_calc.cpp b/libraries/protocol/fee_schedule_calc.cpp new file mode 100644 index 0000000000..6b21890d1d --- /dev/null +++ b/libraries/protocol/fee_schedule_calc.cpp @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2015 Cryptonomex, Inc., 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 + +#define MAX_FEE_STABILIZATION_ITERATION 4 + +namespace graphene { namespace protocol { + + struct calc_fee_visitor + { + typedef uint64_t result_type; + + const fee_schedule& param; + const int current_op; + calc_fee_visitor( const fee_schedule& p, const operation& op ):param(p),current_op(op.which()){} + + template + result_type operator()( const OpType& op )const + { + try { + return op.calculate_fee( param.get() ).value; + } catch (fc::assert_exception& e) { + fee_parameters params; params.set_which(current_op); + auto itr = param.parameters.find(params); + if( itr != param.parameters.end() ) params = *itr; + return op.calculate_fee( params.get() ).value; + } + } + }; + + template<> + uint64_t calc_fee_visitor::operator()(const htlc_create_operation& op)const + { + //TODO: refactor for performance (see https://github.com/bitshares/bitshares-core/issues/2150) + transfer_operation::fee_parameters_type t; + if (param.exists()) + t = param.get(); + return op.calculate_fee( param.get(), t.price_per_kbyte).value; + } + + asset fee_schedule::calculate_fee( const operation& op )const + { + uint64_t required_fee = op.visit( calc_fee_visitor( *this, op ) ); + if( scale != GRAPHENE_100_PERCENT ) + { + auto scaled = fc::uint128_t(required_fee) * scale; + scaled /= GRAPHENE_100_PERCENT; + FC_ASSERT( scaled <= GRAPHENE_MAX_SHARE_SUPPLY, + "Required fee after scaling would exceed maximum possible supply" ); + required_fee = static_cast(scaled); + } + return asset( required_fee ); + } + + asset fee_schedule::calculate_fee( const operation& op, const price& core_exchange_rate )const + { + return calculate_fee( op ).multiply_and_round_up( core_exchange_rate ); + } + +} } // graphene::protocol + +GRAPHENE_IMPLEMENT_EXTERNAL_SERIALIZATION( graphene::protocol::fee_schedule ) diff --git a/libraries/protocol/htlc.cpp b/libraries/protocol/htlc.cpp index b7fe9ef4a0..5694ec5a26 100644 --- a/libraries/protocol/htlc.cpp +++ b/libraries/protocol/htlc.cpp @@ -34,13 +34,16 @@ namespace graphene { namespace protocol { FC_ASSERT( amount.amount > 0, "HTLC amount should be greater than zero" ); } - share_type htlc_create_operation::calculate_fee( const fee_parameters_type& fee_params )const + share_type htlc_create_operation::calculate_fee( const fee_parameters_type& fee_params, + uint32_t fee_per_kb )const { uint64_t days = ( claim_period_seconds + SECONDS_PER_DAY - 1 ) / SECONDS_PER_DAY; // multiply with overflow check - uint64_t per_day_fee = fee_params.fee_per_day * days; - FC_ASSERT( days == 0 || per_day_fee / days == fee_params.fee_per_day, "Fee calculation overflow" ); - return fee_params.fee + per_day_fee; + share_type total_fee = fee_params.fee; + total_fee += share_type(fee_params.fee_per_day) * days; + if (extensions.value.memo.valid()) + total_fee += calculate_data_fee( fc::raw::pack_size(extensions.value.memo), fee_per_kb); + return total_fee; } void htlc_redeem_operation::validate()const { @@ -69,6 +72,7 @@ namespace graphene { namespace protocol { } } GRAPHENE_IMPLEMENT_EXTERNAL_SERIALIZATION( graphene::protocol::htlc_create_operation::fee_parameters_type ) +GRAPHENE_IMPLEMENT_EXTERNAL_SERIALIZATION( graphene::protocol::htlc_create_operation::additional_options_type ) GRAPHENE_IMPLEMENT_EXTERNAL_SERIALIZATION( graphene::protocol::htlc_redeem_operation::fee_parameters_type ) GRAPHENE_IMPLEMENT_EXTERNAL_SERIALIZATION( graphene::protocol::htlc_extend_operation::fee_parameters_type ) GRAPHENE_IMPLEMENT_EXTERNAL_SERIALIZATION( graphene::protocol::htlc_create_operation ) diff --git a/libraries/protocol/include/graphene/protocol/asset_ops.hpp b/libraries/protocol/include/graphene/protocol/asset_ops.hpp index 78c204ac06..9aa410d4ca 100644 --- a/libraries/protocol/include/graphene/protocol/asset_ops.hpp +++ b/libraries/protocol/include/graphene/protocol/asset_ops.hpp @@ -32,6 +32,8 @@ namespace graphene { namespace protocol { { fc::optional reward_percent; fc::optional> whitelist_market_fee_sharing; + // After BSIP81 activation, taker_fee_percent is the taker fee + fc::optional taker_fee_percent; }; typedef extension additional_asset_options_t; @@ -49,6 +51,8 @@ namespace graphene { namespace protocol { /// When this asset is traded on the markets, this percentage of the total traded will be exacted and paid /// to the issuer. This is a fixed point value, representing hundredths of a percent, i.e. a value of 100 /// in this field means a 1% fee is charged on market trades of this asset. + // BSIP81: Asset owners may specify different market fee rate for maker orders and taker orders + // After BSIP81 activation, market_fee_percent is the maker fee uint16_t market_fee_percent = 0; /// Market fees calculated as @ref market_fee_percent of the traded volume are capped to this value share_type max_market_fee = GRAPHENE_MAX_SHARE_SUPPLY; @@ -442,10 +446,21 @@ namespace graphene { namespace protocol { uint64_t fee = 20 * GRAPHENE_BLOCKCHAIN_PRECISION; }; + struct additional_options_type + { + /// Which asset to claim fees from. This is needed, e.g., to claim collateral- + /// denominated fees from a collateral-backed smart asset. If unset, assumed to be same + /// asset as amount_to_claim is denominated in, such as would be the case when claiming + /// market fees. If set, validation requires it to be a different asset_id than + /// amount_to_claim (else there would exist two ways to form the same request). + fc::optional claim_from_asset_id; + }; + asset fee; - account_id_type issuer; - asset amount_to_claim; /// amount_to_claim.asset_id->issuer must == issuer - extensions_type extensions; + account_id_type issuer; ///< must match issuer of asset from which we claim fees + asset amount_to_claim; + + extension extensions; account_id_type fee_payer()const { return issuer; } void validate()const; @@ -517,6 +532,8 @@ namespace graphene { namespace protocol { FC_REFLECT( graphene::protocol::asset_claim_fees_operation, (fee)(issuer)(amount_to_claim)(extensions) ) FC_REFLECT( graphene::protocol::asset_claim_fees_operation::fee_parameters_type, (fee) ) +FC_REFLECT( graphene::protocol::asset_claim_fees_operation::additional_options_type, (claim_from_asset_id) ) + FC_REFLECT( graphene::protocol::asset_claim_pool_operation, (fee)(issuer)(asset_id)(amount_to_claim)(extensions) ) FC_REFLECT( graphene::protocol::asset_claim_pool_operation::fee_parameters_type, (fee) ) @@ -544,7 +561,7 @@ FC_REFLECT( graphene::protocol::bitasset_options, (extensions) ) -FC_REFLECT( graphene::protocol::additional_asset_options, (reward_percent)(whitelist_market_fee_sharing) ) +FC_REFLECT( graphene::protocol::additional_asset_options, (reward_percent)(whitelist_market_fee_sharing)(taker_fee_percent) ) FC_REFLECT( graphene::protocol::asset_create_operation::fee_parameters_type, (symbol3)(symbol4)(long_symbol)(price_per_kbyte) ) FC_REFLECT( graphene::protocol::asset_global_settle_operation::fee_parameters_type, (fee) ) FC_REFLECT( graphene::protocol::asset_settle_operation::fee_parameters_type, (fee) ) @@ -615,6 +632,7 @@ GRAPHENE_DECLARE_EXTERNAL_SERIALIZATION( graphene::protocol::asset_settle_operat GRAPHENE_DECLARE_EXTERNAL_SERIALIZATION( graphene::protocol::asset_fund_fee_pool_operation::fee_parameters_type ) GRAPHENE_DECLARE_EXTERNAL_SERIALIZATION( graphene::protocol::asset_claim_pool_operation::fee_parameters_type ) GRAPHENE_DECLARE_EXTERNAL_SERIALIZATION( graphene::protocol::asset_claim_fees_operation::fee_parameters_type ) +GRAPHENE_DECLARE_EXTERNAL_SERIALIZATION( graphene::protocol::asset_claim_fees_operation::additional_options_type ) GRAPHENE_DECLARE_EXTERNAL_SERIALIZATION( graphene::protocol::asset_update_operation::fee_parameters_type ) GRAPHENE_DECLARE_EXTERNAL_SERIALIZATION( graphene::protocol::asset_update_issuer_operation::fee_parameters_type ) GRAPHENE_DECLARE_EXTERNAL_SERIALIZATION( graphene::protocol::asset_update_bitasset_operation::fee_parameters_type ) 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..7bde1f6b07 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,11 +82,13 @@ namespace graphene { namespace protocol { struct ext { optional< htlc_options > updatable_htlc_options; + optional< custom_authority_options_type > custom_authority_options; + optional< uint16_t > market_fee_network_percent; + optional< uint16_t > maker_fee_discount_percent; }; extension extensions; - /** defined in fee_schedule.cpp */ void validate()const; chain_parameters(); @@ -87,6 +97,12 @@ namespace graphene { namespace protocol { chain_parameters& operator=(const chain_parameters& other); chain_parameters& operator=(chain_parameters&& other); + /// If @ref market_fee_network_percent is valid, return the value it contains, otherwise return 0 + uint16_t get_market_fee_network_percent() const; + + /// If @ref maker_fee_discount_percent is valid, return the value it contains, otherwise return 0 + uint16_t get_maker_fee_discount_percent() const; + private: static void safe_copy(chain_parameters& to, const chain_parameters& from); }; @@ -98,8 +114,18 @@ 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) + (market_fee_network_percent) + (maker_fee_discount_percent) ) 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.hpp b/libraries/protocol/include/graphene/protocol/custom.hpp index 8be5ab1e60..59ef3757db 100644 --- a/libraries/protocol/include/graphene/protocol/custom.hpp +++ b/libraries/protocol/include/graphene/protocol/custom.hpp @@ -51,6 +51,9 @@ namespace graphene { namespace protocol { account_id_type fee_payer()const { return payer; } void validate()const; share_type calculate_fee(const fee_parameters_type& k)const; + void get_required_active_authorities( flat_set& auths )const { + auths.insert( required_auths.begin(), required_auths.end() ); + } }; } } // namespace graphene::protocol 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..7448c84aee --- /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/fee_schedule.hpp b/libraries/protocol/include/graphene/protocol/fee_schedule.hpp index 9d867819ef..275e87e704 100644 --- a/libraries/protocol/include/graphene/protocol/fee_schedule.hpp +++ b/libraries/protocol/include/graphene/protocol/fee_schedule.hpp @@ -177,7 +177,7 @@ namespace graphene { namespace protocol { /** * Validates all of the parameters are present and accounted for. */ - void validate()const; + void validate()const {} template const typename Operation::fee_parameters_type& get()const diff --git a/libraries/protocol/include/graphene/protocol/htlc.hpp b/libraries/protocol/include/graphene/protocol/htlc.hpp index 9c8058be16..606b2f383f 100644 --- a/libraries/protocol/include/graphene/protocol/htlc.hpp +++ b/libraries/protocol/include/graphene/protocol/htlc.hpp @@ -26,17 +26,20 @@ #include #include #include +#include #include // std::max namespace graphene { namespace protocol { typedef fc::ripemd160 htlc_algo_ripemd160; typedef fc::sha1 htlc_algo_sha1; typedef fc::sha256 htlc_algo_sha256; + typedef fc::hash160 htlc_algo_hash160; typedef fc::static_variant< htlc_algo_ripemd160, htlc_algo_sha1, - htlc_algo_sha256 + htlc_algo_sha256, + htlc_algo_hash160 > htlc_hash; struct htlc_create_operation : public base_operation @@ -45,6 +48,7 @@ namespace graphene { namespace protocol { uint64_t fee = 1 * GRAPHENE_BLOCKCHAIN_PRECISION; uint64_t fee_per_day = 1 * GRAPHENE_BLOCKCHAIN_PRECISION; }; + // paid to network asset fee; // where the held monies are to come from @@ -59,8 +63,13 @@ namespace graphene { namespace protocol { uint16_t preimage_size; // The time the funds will be returned to the source if not claimed uint32_t claim_period_seconds; - // for future expansion - extensions_type extensions; + + // additional extensions + struct additional_options_type + { + fc::optional memo; + }; + extension extensions; /*** * @brief Does simple validation of this object @@ -75,7 +84,7 @@ namespace graphene { namespace protocol { /**** * @brief calculates the fee to be paid for this operation */ - share_type calculate_fee(const fee_parameters_type& fee_params)const; + share_type calculate_fee(const fee_parameters_type& fee_params, uint32_t fee_per_kb)const; }; struct htlc_redeem_operation : public base_operation @@ -121,9 +130,10 @@ namespace graphene { namespace protocol { htlc_redeemed_operation() {} htlc_redeemed_operation( htlc_id_type htlc_id, account_id_type from, account_id_type to, - account_id_type redeemer, asset amount, const htlc_hash& preimage_hash, uint16_t preimage_size ) : + account_id_type redeemer, asset amount, const htlc_hash& preimage_hash, uint16_t preimage_size, + const std::vector& preimage ) : htlc_id(htlc_id), from(from), to(to), redeemer(redeemer), amount(amount), - htlc_preimage_hash(preimage_hash), htlc_preimage_size(preimage_size) {} + htlc_preimage_hash(preimage_hash), htlc_preimage_size(preimage_size), preimage(preimage) {} account_id_type fee_payer()const { return to; } void validate()const { FC_ASSERT( !"virtual operation" ); } @@ -137,6 +147,7 @@ namespace graphene { namespace protocol { uint16_t htlc_preimage_size; asset fee; + std::vector preimage; }; struct htlc_extend_operation : public base_operation @@ -206,6 +217,7 @@ namespace graphene { namespace protocol { FC_REFLECT_TYPENAME( graphene::protocol::htlc_hash ) FC_REFLECT( graphene::protocol::htlc_create_operation::fee_parameters_type, (fee) (fee_per_day) ) +FC_REFLECT( graphene::protocol::htlc_create_operation::additional_options_type, (memo)) FC_REFLECT( graphene::protocol::htlc_redeem_operation::fee_parameters_type, (fee) (fee_per_kb) ) FC_REFLECT( graphene::protocol::htlc_redeemed_operation::fee_parameters_type, ) // VIRTUAL FC_REFLECT( graphene::protocol::htlc_extend_operation::fee_parameters_type, (fee) (fee_per_day)) @@ -215,12 +227,13 @@ FC_REFLECT( graphene::protocol::htlc_create_operation, (fee)(from)(to)(amount)(preimage_hash)(preimage_size)(claim_period_seconds)(extensions)) FC_REFLECT( graphene::protocol::htlc_redeem_operation, (fee)(htlc_id)(redeemer)(preimage)(extensions)) FC_REFLECT( graphene::protocol::htlc_redeemed_operation, - (fee)(htlc_id)(from)(to)(redeemer)(amount)(htlc_preimage_hash)(htlc_preimage_size)) + (fee)(htlc_id)(from)(to)(redeemer)(amount)(htlc_preimage_hash)(htlc_preimage_size)(preimage)) FC_REFLECT( graphene::protocol::htlc_extend_operation, (fee)(htlc_id)(update_issuer)(seconds_to_add)(extensions)) FC_REFLECT( graphene::protocol::htlc_refund_operation, (fee)(htlc_id)(to)(original_htlc_recipient)(htlc_amount)(htlc_preimage_hash)(htlc_preimage_size)) GRAPHENE_DECLARE_EXTERNAL_SERIALIZATION( graphene::protocol::htlc_create_operation::fee_parameters_type ) +GRAPHENE_DECLARE_EXTERNAL_SERIALIZATION( graphene::protocol::htlc_create_operation::additional_options_type ) GRAPHENE_DECLARE_EXTERNAL_SERIALIZATION( graphene::protocol::htlc_redeem_operation::fee_parameters_type ) GRAPHENE_DECLARE_EXTERNAL_SERIALIZATION( graphene::protocol::htlc_extend_operation::fee_parameters_type ) GRAPHENE_DECLARE_EXTERNAL_SERIALIZATION( graphene::protocol::htlc_create_operation ) diff --git a/libraries/protocol/include/graphene/protocol/object_id.hpp b/libraries/protocol/include/graphene/protocol/object_id.hpp index bc26c1cb3a..34aa51c481 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 3a603ebf64..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 @@ -112,10 +116,11 @@ namespace graphene { namespace protocol { * * @return a set of required authorities for @ref op */ - void operation_get_required_authorities( const operation& op, + void operation_get_required_authorities( const operation& op, flat_set& active, flat_set& owner, - vector& other ); + vector& other, + bool ignore_custom_operation_required_auths ); void operation_validate( const operation& op ); 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 17b7874d41..b74c78628c 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 @@ -111,7 +117,8 @@ namespace graphene { namespace protocol { void get_required_authorities( flat_set& active, flat_set& owner, - vector& other )const; + vector& other, + bool ignore_custom_operation_required_auths )const; virtual uint64_t get_packed_size()const; @@ -145,13 +152,13 @@ namespace graphene { namespace protocol { * validation. */ set get_required_signatures( - const chain_id_type& chain_id, - const flat_set& available_keys, - const std::function& get_active, - const std::function& get_owner, - bool allow_non_immediate_owner, - uint32_t max_recursion = GRAPHENE_MAX_SIG_CHECK_DEPTH - )const; + const chain_id_type& chain_id, + const flat_set& available_keys, + const std::function& get_active, + const std::function& get_owner, + bool allow_non_immediate_owner, + bool ignore_custom_operation_required_authorities, + uint32_t max_recursion = GRAPHENE_MAX_SIG_CHECK_DEPTH )const; /** * Checks whether signatures in this signed transaction are sufficient to authorize the transaction. @@ -160,17 +167,22 @@ 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 + * required_auths field of custom_operation or not * @param max_recursion maximum level of recursion when verifying, since an account * can have another account in active authorities and/or owner authorities */ void verify_authority( - const chain_id_type& chain_id, - const std::function& get_active, - const std::function& get_owner, - bool allow_non_immediate_owner, - uint32_t max_recursion = GRAPHENE_MAX_SIG_CHECK_DEPTH )const; + 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; /** * This is a slower replacement for get_required_signatures() @@ -179,13 +191,14 @@ namespace graphene { namespace protocol { * non-minimal set. */ set minimize_required_signatures( - const chain_id_type& chain_id, - const flat_set& available_keys, - const std::function& get_active, - const std::function& get_owner, - bool allow_non_immediate_owner, - uint32_t max_recursion = GRAPHENE_MAX_SIG_CHECK_DEPTH - ) const; + const chain_id_type& chain_id, + 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; /** * @brief Extract public keys from signatures with given chain ID. @@ -241,8 +254,11 @@ namespace graphene { namespace protocol { * @param sigs a set of public keys * @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 + * @param ignore_custom_operation_required_auths See issue #210; whether to ignore the + * required_auths field of custom_operation or not * @param max_recursion maximum level of recursion when verifying, since an account * can have another account in active authorities and/or owner authorities * @param allow_committee whether to allow the special "committee account" to authorize the operations @@ -252,11 +268,13 @@ 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, - bool allow_committe = false, + bool allow_committee = false, const flat_set& active_aprovals = flat_set(), - const flat_set& owner_approvals = flat_set()); + const flat_set& owner_approvals = flat_set() ); /** * @brief captures the result of evaluating the operations contained in the transaction diff --git a/libraries/protocol/include/graphene/protocol/types.hpp b/libraries/protocol/include/graphene/protocol/types.hpp index b861efc78a..567a1230bb 100644 --- a/libraries/protocol/include/graphene/protocol/types.hpp +++ b/libraries/protocol/include/graphene/protocol/types.hpp @@ -44,6 +44,7 @@ #include #include #include +#include #include #include #include @@ -239,7 +240,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/operations.cpp b/libraries/protocol/operations.cpp index d1b71232c6..edff2b6305 100644 --- a/libraries/protocol/operations.cpp +++ b/libraries/protocol/operations.cpp @@ -61,25 +61,38 @@ struct operation_validator struct operation_get_required_auth { - typedef void result_type; + using result_type = void; flat_set& active; flat_set& owner; vector& other; + bool ignore_custom_op_reqd_auths; operation_get_required_auth( flat_set& a, - flat_set& own, - vector& oth ):active(a),owner(own),other(oth){} + flat_set& own, + vector& oth, + bool ignore_custom_operation_required_auths ) + : active( a ), owner( own ), other( oth ), + ignore_custom_op_reqd_auths( ignore_custom_operation_required_auths ) + {} template - void operator()( const T& v )const - { + void operator()( const T& v ) const { active.insert( v.fee_payer() ); - v.get_required_active_authorities( active ); - v.get_required_owner_authorities( owner ); + v.get_required_active_authorities( active ); + v.get_required_owner_authorities( owner ); v.get_required_authorities( other ); } + + void operator()( const custom_operation& op ) const { + active.insert( op.fee_payer() ); + if( !ignore_custom_op_reqd_auths ) { + op.get_required_active_authorities( active ); + op.get_required_owner_authorities( owner ); + op.get_required_authorities( other ); + } + } }; void operation_validate( const operation& op ) @@ -87,12 +100,13 @@ void operation_validate( const operation& op ) op.visit( operation_validator() ); } -void operation_get_required_authorities( const operation& op, +void operation_get_required_authorities( const operation& op, flat_set& active, flat_set& owner, - vector& other ) + vector& other, + bool ignore_custom_operation_required_auths ) { - op.visit( operation_get_required_auth( active, owner, other ) ); + op.visit( operation_get_required_auth( active, owner, other, ignore_custom_operation_required_auths ) ); } } } // namespace graphene::protocol 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 42b1d8ee0f..43ec06e164 100644 --- a/libraries/protocol/transaction.cpp +++ b/libraries/protocol/transaction.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include @@ -101,10 +102,11 @@ void transaction::set_reference_block( const block_id_type& reference_block ) void transaction::get_required_authorities( flat_set& active, flat_set& owner, - vector& other )const + vector& other, + bool ignore_custom_operation_required_auths )const { for( const auto& op : operations ) - operation_get_required_authorities( op, active, owner, other ); + operation_get_required_authorities( op, active, owner, other, ignore_custom_operation_required_auths ); for( const auto& account : owner ) active.erase( account ); } @@ -267,29 +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_committe, + 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; - for( const auto& op : ops ) - operation_get_required_authorities( op, required_active, required_owner, other ); - - if( !allow_committe ) - 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 ); + 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 ) { + 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" ); + for( const auto& auth : other ) { GRAPHENE_ASSERT( s.check_authority(&auth), tx_missing_other_auth, "Missing Authority", ("auth",auth)("sigs",sigs) ); @@ -316,7 +344,7 @@ void verify_authority( const vector& ops, const flat_set& signed_transaction::get_signature_keys( const chain_id_type& chain_id )const @@ -335,24 +363,24 @@ const flat_set& signed_transaction::get_signature_keys( const c } FC_CAPTURE_AND_RETHROW() } -set signed_transaction::get_required_signatures( - const chain_id_type& chain_id, - const flat_set& available_keys, - const std::function& get_active, - const std::function& get_owner, - bool allow_non_immediate_owner, - uint32_t max_recursion_depth )const +set signed_transaction::get_required_signatures( const chain_id_type& chain_id, + const flat_set& available_keys, + const std::function& get_active, + const std::function& get_owner, + bool allow_non_immediate_owner, + bool ignore_custom_operation_required_authorities, + uint32_t max_recursion_depth )const { flat_set required_active; flat_set required_owner; vector other; - get_required_authorities( required_active, required_owner, other ); + get_required_authorities( required_active, required_owner, other, ignore_custom_operation_required_authorities ); - const flat_set& signature_keys = get_signature_keys( chain_id ); + const flat_set& signature_keys = get_signature_keys(chain_id); sign_state s( signature_keys, get_active, get_owner, allow_non_immediate_owner, max_recursion_depth, available_keys ); for( const auto& auth : other ) - s.check_authority(&auth); + s.check_authority( &auth ); for( auto& owner : required_owner ) s.check_authority( get_owner( owner ) ); for( auto& active : required_active ) @@ -371,15 +399,18 @@ set signed_transaction::get_required_signatures( } set signed_transaction::minimize_required_signatures( - const chain_id_type& chain_id, - const flat_set& available_keys, - const std::function& get_active, - const std::function& get_owner, - bool allow_non_immediate_owner, - uint32_t max_recursion - ) const + const chain_id_type& chain_id, + 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 { - set< public_key_type > s = get_required_signatures( chain_id, available_keys, get_active, get_owner, max_recursion ); + set< public_key_type > s = get_required_signatures( chain_id, available_keys, get_active, get_owner, + allow_non_immediate_owner, + ignore_custom_operation_required_auths, max_recursion ); flat_set< public_key_type > result( s.begin(), s.end() ); for( const public_key_type& k : s ) @@ -387,8 +418,9 @@ set signed_transaction::minimize_required_signatures( result.erase( k ); try { - graphene::protocol::verify_authority( operations, result, get_active, get_owner, - allow_non_immediate_owner, max_recursion ); + 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 } catch( const tx_missing_owner_auth& e ) {} @@ -429,15 +461,17 @@ const flat_set& precomputable_transaction::get_signature_keys( return _signees; } -void signed_transaction::verify_authority( - const chain_id_type& chain_id, - const std::function& get_active, - const std::function& get_owner, - bool allow_non_immediate_owner, - uint32_t max_recursion )const +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, 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/libraries/wallet/include/graphene/wallet/wallet.hpp b/libraries/wallet/include/graphene/wallet/wallet.hpp index 217b2ff862..90cc6fd2c2 100644 --- a/libraries/wallet/include/graphene/wallet/wallet.hpp +++ b/libraries/wallet/include/graphene/wallet/wallet.hpp @@ -241,7 +241,7 @@ class wallet_api * @returns account_history_operation_detail */ account_history_operation_detail get_account_history_by_operations( string name, - vector operation_types, + flat_set operation_types, uint32_t start, int limit); /** Returns the block chain's rapidly-changing properties. @@ -376,6 +376,19 @@ class wallet_api */ signed_transaction sign_builder_transaction(transaction_handle_type transaction_handle, bool broadcast = true); + /** + * @ingroup Transaction Builder API + * + * Sign the transaction in a transaction builder and optionally broadcast to the network. + * @param transaction_handle handle of the transaction builder + * @param signing_keys Keys that must be used when signing the transaction + * @param broadcast whether to broadcast the signed transaction to the network + * @return a signed transaction + */ + signed_transaction sign_builder_transaction2(transaction_handle_type transaction_handle, + const vector& signing_keys = vector(), + bool broadcast = true); + /** Broadcast signed transaction * @param tx signed transaction * @returns the transaction ID along with the signed transaction. @@ -1433,12 +1446,13 @@ class wallet_api * @param preimage_hash the hash of the preimage * @param preimage_size the size of the preimage in bytes * @param claim_period_seconds how long after creation until the lock expires + * @param memo the memo * @param broadcast true if you wish to broadcast the transaction * @return the signed transaction */ signed_transaction htlc_create( string source, string destination, string amount, string asset_symbol, string hash_algorithm, const std::string& preimage_hash, uint32_t preimage_size, - const uint32_t claim_period_seconds, bool broadcast = false ); + const uint32_t claim_period_seconds, const std::string& memo, bool broadcast = false ); /**** * Update a hashed time lock contract @@ -1588,6 +1602,21 @@ class wallet_api */ signed_transaction sign_transaction(signed_transaction tx, bool broadcast = false); + /** Signs a transaction. + * + * Given a fully-formed transaction that is only lacking signatures, this signs + * the transaction with the inferred necessary keys and the explicitly provided keys, + * and optionally broadcasts the transaction + * @param tx the unsigned transaction + * @param signing_keys Keys that must be used when signing the transaction + * @param broadcast true if you wish to broadcast the transaction + * @return the signed version of the transaction + */ + signed_transaction sign_transaction2(signed_transaction tx, + const vector& signing_keys = vector(), + bool broadcast = true); + + /** Get transaction signers. * * Returns information about who signed the transaction, specifically, @@ -1742,8 +1771,7 @@ class wallet_api /** * Get \c account_storage_object of an account by using the custom operations plugin. * - * Storage data added to the map with @ref account_store_map and list data added by - * @ref account_list_accounts will be returned. + * Storage data added to the map with @ref account_store_map will be returned. * * @param account Account ID or name to get contact data from. * @param catalog The catalog to retrieve. @@ -1769,6 +1797,7 @@ FC_API( graphene::wallet::wallet_api, (set_fees_on_builder_transaction) (preview_builder_transaction) (sign_builder_transaction) + (sign_builder_transaction2) (broadcast_transaction) (propose_builder_transaction) (propose_builder_transaction2) @@ -1857,6 +1886,7 @@ FC_API( graphene::wallet::wallet_api, (save_wallet_file) (serialize_transaction) (sign_transaction) + (sign_transaction2) (add_transaction_signature) (get_transaction_signers) (get_key_references) diff --git a/libraries/wallet/operation_printer.cpp b/libraries/wallet/operation_printer.cpp index 0d948e023c..d3f950c4e9 100644 --- a/libraries/wallet/operation_printer.cpp +++ b/libraries/wallet/operation_printer.cpp @@ -47,6 +47,10 @@ class htlc_hash_to_string_visitor { return "SHA256 " + hash.str(); } + result_type operator()( const fc::hash160& hash )const + { + return "HASH160 " + hash.str(); + } }; std::string operation_printer::fee(const graphene::protocol::asset& a)const { @@ -54,6 +58,64 @@ std::string operation_printer::fee(const graphene::protocol::asset& a)const { return ""; } +string operation_printer::print_memo( const fc::optional& memo )const +{ + std::string outstr; + if( memo ) + { + if( wallet.is_locked() ) + { + out << " -- Unlock wallet to see memo."; + } else { + try { + FC_ASSERT( wallet._keys.count(memo->to) || wallet._keys.count(memo->from), + "Memo is encrypted to a key ${to} or ${from} not in this wallet.", + ("to", memo->to)("from",memo->from) ); + if( wallet._keys.count(memo->to) ) { + auto my_key = wif_to_key(wallet._keys.at(memo->to)); + FC_ASSERT(my_key, "Unable to recover private key to decrypt memo. Wallet may be corrupted."); + outstr = memo->get_message(*my_key, memo->from); + out << " -- Memo: " << outstr; + } else { + auto my_key = wif_to_key(wallet._keys.at(memo->from)); + FC_ASSERT(my_key, "Unable to recover private key to decrypt memo. Wallet may be corrupted."); + outstr = memo->get_message(*my_key, memo->to); + out << " -- Memo: " << outstr; + } + } catch (const fc::exception& e) { + out << " -- could not decrypt memo"; + } + } + } + return outstr; +} + +void operation_printer::print_preimage(const std::vector& preimage)const +{ + if (preimage.size() == 0) + return; + out << " with preimage \""; + // cut it at 300 bytes max + auto flags = out.flags(); + out << std::hex << setw(2) << setfill('0'); + for (size_t i = 0; i < std::min(300, preimage.size()); i++) + out << +preimage[i]; + out.flags(flags); + if (preimage.size() > 300) + out << "...(truncated due to size)"; + out << "\""; +} + +string operation_printer::print_redeem(const graphene::protocol::htlc_id_type& id, + const std::string& redeemer, const std::vector& preimage, + const graphene::protocol::asset& op_fee)const +{ + out << redeemer << " redeemed HTLC with id " + << std::string( static_cast(id)); + print_preimage( preimage ); + return fee(op_fee); +} + std::string operation_printer::operator()(const transfer_from_blind_operation& op)const { auto a = wallet.get_asset( op.fee.asset_id ); @@ -75,37 +137,12 @@ std::string operation_printer::operator()(const transfer_to_blind_operation& op) << " fee: " << fa.amount_to_pretty_string( op.fee ); return ""; } + string operation_printer::operator()(const transfer_operation& op) const { out << "Transfer " << wallet.get_asset(op.amount.asset_id).amount_to_pretty_string(op.amount) << " from " << wallet.get_account(op.from).name << " to " << wallet.get_account(op.to).name; - std::string memo; - if( op.memo ) - { - if( wallet.is_locked() ) - { - out << " -- Unlock wallet to see memo."; - } else { - try { - FC_ASSERT( wallet._keys.count(op.memo->to) || wallet._keys.count(op.memo->from), - "Memo is encrypted to a key ${to} or ${from} not in this wallet.", - ("to", op.memo->to)("from",op.memo->from) ); - if( wallet._keys.count(op.memo->to) ) { - auto my_key = wif_to_key(wallet._keys.at(op.memo->to)); - FC_ASSERT(my_key, "Unable to recover private key to decrypt memo. Wallet may be corrupted."); - memo = op.memo->get_message(*my_key, op.memo->from); - out << " -- Memo: " << memo; - } else { - auto my_key = wif_to_key(wallet._keys.at(op.memo->from)); - FC_ASSERT(my_key, "Unable to recover private key to decrypt memo. Wallet may be corrupted."); - memo = op.memo->get_message(*my_key, op.memo->to); - out << " -- Memo: " << memo; - } - } catch (const fc::exception& e) { - out << " -- could not decrypt memo"; - } - } - } + std::string memo = print_memo( op.memo ); fee(op.fee); return memo; } @@ -135,15 +172,12 @@ std::string operation_printer::operator()(const asset_create_operation& op) cons std::string operation_printer::operator()(const htlc_redeem_operation& op) const { - out << "Redeem HTLC with database id " - << std::to_string(op.htlc_id.space_id) - << "." << std::to_string(op.htlc_id.type_id) - << "." << std::to_string((uint64_t)op.htlc_id.instance) - << " with preimage \""; - for (unsigned char c : op.preimage) - out << c; - out << "\""; - return fee(op.fee); + return print_redeem(op.htlc_id, wallet.get_account(op.redeemer).name, op.preimage, op.fee); +} + +std::string operation_printer::operator()(const htlc_redeemed_operation& op) const +{ + return print_redeem(op.htlc_id, wallet.get_account(op.redeemer).name, op.preimage, op.fee); } std::string operation_printer::operator()(const htlc_create_operation& op) const @@ -152,20 +186,19 @@ std::string operation_printer::operator()(const htlc_create_operation& op) const auto fee_asset = wallet.get_asset( op.fee.asset_id ); auto to = wallet.get_account( op.to ); + auto from = wallet.get_account( op.from ); operation_result_printer rprinter(wallet); std::string database_id = result.visit(rprinter); - out << "Create HTLC to " << to.name + out << "Create HTLC from " << from.name << " to " << to.name << " with id " << database_id - << " preimage hash: [" - << op.preimage_hash.visit( vtor ) - << "] (Fee: " << fee_asset.amount_to_pretty_string( op.fee ) << ")"; + << " preimage hash: [" << op.preimage_hash.visit( vtor ) << "] "; + print_memo( op.extensions.value.memo ); // determine if the block that the HTLC is in is before or after LIB int32_t pending_blocks = hist.block_num - wallet.get_dynamic_global_properties().last_irreversible_block_num; if (pending_blocks > 0) out << " (pending " << std::to_string(pending_blocks) << " blocks)"; - - return ""; + return fee(op.fee); } std::string operation_result_printer::operator()(const void_result& x) const diff --git a/libraries/wallet/operation_printer.hpp b/libraries/wallet/operation_printer.hpp index 9ff09586cf..428ca469d1 100644 --- a/libraries/wallet/operation_printer.hpp +++ b/libraries/wallet/operation_printer.hpp @@ -98,6 +98,13 @@ struct operation_printer std::string operator()(const graphene::protocol::asset_create_operation& op)const; std::string operator()(const graphene::protocol::htlc_create_operation& op)const; std::string operator()(const graphene::protocol::htlc_redeem_operation& op)const; + std::string operator()(const graphene::protocol::htlc_redeemed_operation& op)const; + protected: + std::string print_memo( const fc::optional& memo)const; + void print_preimage( const std::vector& preimage)const; + std::string print_redeem(const graphene::protocol::htlc_id_type& id, + const std::string& redeemer, const std::vector& preimage, + const graphene::protocol::asset& op_fee)const; }; }}} // namespace graphene::wallet::detail diff --git a/libraries/wallet/wallet.cpp b/libraries/wallet/wallet.cpp index 927e4a803d..8e1ca9f840 100644 --- a/libraries/wallet/wallet.cpp +++ b/libraries/wallet/wallet.cpp @@ -202,10 +202,10 @@ uint64_t wallet_api::get_asset_count()const signed_transaction wallet_api::htlc_create( string source, string destination, string amount, string asset_symbol, string hash_algorithm, const std::string& preimage_hash, uint32_t preimage_size, - const uint32_t claim_period_seconds, bool broadcast) + const uint32_t claim_period_seconds, const std::string& memo, bool broadcast) { return my->htlc_create(source, destination, amount, asset_symbol, hash_algorithm, preimage_hash, preimage_size, - claim_period_seconds, broadcast); + claim_period_seconds, memo, broadcast); } fc::optional wallet_api::get_htlc(std::string htlc_id) const @@ -223,6 +223,8 @@ fc::optional wallet_api::get_htlc(std::string htlc_id) const const auto& asset = my->get_asset( obj.transfer.asset_id ); transfer["asset"] = asset.symbol; transfer["amount"] = graphene::app::uint128_amount_to_string( obj.transfer.amount.value, asset.precision ); + if (obj.memo.valid()) + transfer["memo"] = my->read_memo( *obj.memo ); class htlc_hash_to_variant_visitor { public: @@ -234,6 +236,8 @@ fc::optional wallet_api::get_htlc(std::string htlc_id) const { return convert("SHA1", obj.str()); } result_type operator()(const fc::sha256& obj)const { return convert("SHA256", obj.str()); } + result_type operator()(const fc::hash160& obj)const + { return convert("HASH160", obj.str()); } private: result_type convert(const std::string& type, const std::string& hash)const { @@ -369,7 +373,7 @@ vector wallet_api::get_relative_account_history( account_history_operation_detail wallet_api::get_account_history_by_operations( string name, - vector operation_types, + flat_set operation_types, uint32_t start, int limit) { @@ -526,6 +530,13 @@ signed_transaction wallet_api::sign_builder_transaction(transaction_handle_type return my->sign_builder_transaction(transaction_handle, broadcast); } +signed_transaction wallet_api::sign_builder_transaction2(transaction_handle_type transaction_handle, + const vector& explicit_keys, + bool broadcast) +{ + return my->sign_builder_transaction2(transaction_handle, explicit_keys, broadcast); +} + pair wallet_api::broadcast_transaction(signed_transaction tx) { return my->broadcast_transaction(tx); @@ -999,6 +1010,12 @@ signed_transaction wallet_api::sign_transaction(signed_transaction tx, bool broa return my->sign_transaction( tx, broadcast); } FC_CAPTURE_AND_RETHROW( (tx) ) } +signed_transaction wallet_api::sign_transaction2(signed_transaction tx, const vector& signing_keys, + bool broadcast /* = false */) +{ try { + return my->sign_transaction2( tx, signing_keys, broadcast); +} FC_CAPTURE_AND_RETHROW( (tx) ) } + flat_set wallet_api::get_transaction_signers(const signed_transaction &tx) const { try { return my->get_transaction_signers(tx); @@ -1905,9 +1922,9 @@ signed_transaction wallet_api::account_store_map(string account, string catalog, } vector wallet_api::get_account_storage(string account, string catalog) -{ +{ try { return my->_custom_operations->get_storage_info(account, catalog); -} +} FC_CAPTURE_AND_RETHROW( (account)(catalog) ) } signed_block_with_info::signed_block_with_info( const signed_block& block ) : signed_block( block ) diff --git a/libraries/wallet/wallet_api_impl.cpp b/libraries/wallet/wallet_api_impl.cpp index 5d23c20be4..c246e3b22a 100644 --- a/libraries/wallet/wallet_api_impl.cpp +++ b/libraries/wallet/wallet_api_impl.cpp @@ -56,9 +56,15 @@ namespace graphene { namespace wallet { namespace detail { _remote_api(rapi), _remote_db(rapi->database()), _remote_net_broadcast(rapi->network_broadcast()), - _remote_hist(rapi->history()), - _custom_operations(rapi->custom()) + _remote_hist(rapi->history()) { + try { + _custom_operations = rapi->custom_operations(); + } + catch(const fc::exception& e) + { + wlog("Custom operations API is not active on server."); + } chain_id_type remote_chain_id = _remote_db->get_chain_id(); if( remote_chain_id != _chain_id ) { @@ -199,7 +205,8 @@ namespace graphene { namespace wallet { namespace detail { void wallet_api_impl::init_prototype_ops() { operation op; - for( int t=0; t& signing_keys = vector(), bool broadcast = true); pair broadcast_transaction(signed_transaction tx); @@ -297,9 +299,10 @@ class wallet_api_impl signed_transaction htlc_create( string source, string destination, string amount, string asset_symbol, string hash_algorithm, const std::string& preimage_hash, uint32_t preimage_size, - const uint32_t claim_period_seconds, bool broadcast = false ); + const uint32_t claim_period_seconds, const std::string& memo, bool broadcast = false ); - signed_transaction htlc_redeem( string htlc_id, string issuer, const std::vector& preimage, bool broadcast ); + signed_transaction htlc_redeem( string htlc_id, string issuer, const std::vector& preimage, + bool broadcast ); signed_transaction htlc_extend ( string htlc_id, string issuer, const uint32_t seconds_to_add, bool broadcast); @@ -325,6 +328,9 @@ class wallet_api_impl bool broadcast ); signed_transaction sign_transaction(signed_transaction tx, bool broadcast = false); + signed_transaction sign_transaction2(signed_transaction tx, + const vector& signing_keys = vector(), + bool broadcast = false); flat_set get_transaction_signers(const signed_transaction &tx) const; diff --git a/libraries/wallet/wallet_builder.cpp b/libraries/wallet/wallet_builder.cpp index dbcbe12c5d..5dd7f00955 100644 --- a/libraries/wallet/wallet_builder.cpp +++ b/libraries/wallet/wallet_builder.cpp @@ -88,6 +88,15 @@ namespace graphene { namespace wallet { namespace detail { sign_transaction(_builder_transactions[transaction_handle], broadcast); } + signed_transaction wallet_api_impl::sign_builder_transaction2(transaction_handle_type + transaction_handle, const vector& signing_keys, bool broadcast) + { + FC_ASSERT(_builder_transactions.count(transaction_handle)); + + return _builder_transactions[transaction_handle] = + sign_transaction2(_builder_transactions[transaction_handle], signing_keys, broadcast); + } + signed_transaction wallet_api_impl::propose_builder_transaction( transaction_handle_type handle, time_point_sec expiration, uint32_t review_period_seconds, bool broadcast) { diff --git a/libraries/wallet/wallet_results.cpp b/libraries/wallet/wallet_results.cpp index 434c3e2b24..6dd31a3dcb 100644 --- a/libraries/wallet/wallet_results.cpp +++ b/libraries/wallet/wallet_results.cpp @@ -50,6 +50,7 @@ std::map> wallet_a operation_history_object& i = d.op; auto b = _remote_db->get_block_header(i.block_num); FC_ASSERT(b); + ss << i.block_num << " "; ss << b->timestamp.to_iso_string() << " "; i.op.visit(operation_printer(ss, *this, i)); ss << " \n"; @@ -67,6 +68,7 @@ std::map> wallet_a operation_history_object& i = d.op; auto b = _remote_db->get_block_header(i.block_num); FC_ASSERT(b); + ss << i.block_num << " "; ss << b->timestamp.to_iso_string() << " "; i.op.visit(operation_printer(ss, *this, i)); ss << " \n"; @@ -88,6 +90,7 @@ std::map> wallet_a operation_history_object& i = d.op; auto b = _remote_db->get_block_header(i.block_num); FC_ASSERT(b); + ss << i.block_num << " "; ss << b->timestamp.to_iso_string() << " "; i.op.visit(operation_printer(ss, *this, i)); ss << " transaction_id : "; diff --git a/libraries/wallet/wallet_sign.cpp b/libraries/wallet/wallet_sign.cpp index 5299099752..08722ed63b 100644 --- a/libraries/wallet/wallet_sign.cpp +++ b/libraries/wallet/wallet_sign.cpp @@ -311,9 +311,20 @@ namespace graphene { namespace wallet { namespace detail { } signed_transaction wallet_api_impl::sign_transaction( signed_transaction tx, bool broadcast ) + { + return sign_transaction2(tx, {}, broadcast); + } + + signed_transaction wallet_api_impl::sign_transaction2( signed_transaction tx, + const vector& signing_keys, bool broadcast) { set approving_key_set = get_owned_required_keys(tx); + // Add any explicit keys to the approving_key_set + for (const public_key_type& explicit_key : signing_keys) { + approving_key_set.insert(explicit_key); + } + auto dyn_props = get_dynamic_global_properties(); tx.set_reference_block( dyn_props.head_block_id ); diff --git a/libraries/wallet/wallet_transfer.cpp b/libraries/wallet/wallet_transfer.cpp index 7228cec5dc..57a8ae4960 100644 --- a/libraries/wallet/wallet_transfer.cpp +++ b/libraries/wallet/wallet_transfer.cpp @@ -43,6 +43,8 @@ namespace graphene { namespace wallet { namespace detail { return fc::sha256( hash ); if( name_upper == "SHA1" ) return fc::sha1( hash ); + if( name_upper == "HASH160" ) + return fc::hash160( hash ); FC_THROW_EXCEPTION( fc::invalid_arg_exception, "Unknown algorithm '${a}'", ("a",algorithm) ); } @@ -81,9 +83,9 @@ namespace graphene { namespace wallet { namespace detail { return sign_transaction(tx, broadcast); } FC_CAPTURE_AND_RETHROW( (from)(to)(amount)(asset_symbol)(memo)(broadcast) ) } - signed_transaction wallet_api_impl::htlc_create( string source, string destination, string amount, string asset_symbol, - string hash_algorithm, const std::string& preimage_hash, uint32_t preimage_size, - const uint32_t claim_period_seconds, bool broadcast ) + signed_transaction wallet_api_impl::htlc_create( string source, string destination, string amount, + string asset_symbol, string hash_algorithm, const std::string& preimage_hash, uint32_t preimage_size, + const uint32_t claim_period_seconds, const std::string& memo, bool broadcast ) { try { @@ -91,6 +93,8 @@ namespace graphene { namespace wallet { namespace detail { fc::optional asset_obj = get_asset(asset_symbol); FC_ASSERT(asset_obj, "Could not find asset matching ${asset}", ("asset", asset_symbol)); + const account_object& from_acct = get_account(source); + const account_object& to_acct = get_account(destination); htlc_create_operation create_op; create_op.from = get_account(source).id; create_op.to = get_account(destination).id; @@ -98,6 +102,15 @@ namespace graphene { namespace wallet { namespace detail { create_op.claim_period_seconds = claim_period_seconds; create_op.preimage_hash = do_hash( hash_algorithm, preimage_hash ); create_op.preimage_size = preimage_size; + if (!memo.empty()) + { + memo_data data; + data.from = from_acct.options.memo_key; + data.to = to_acct.options.memo_key; + data.set_message( + get_private_key(from_acct.options.memo_key), to_acct.options.memo_key, memo); + create_op.extensions.value.memo = data; + } signed_transaction tx; tx.operations.push_back(create_op); @@ -109,8 +122,8 @@ namespace graphene { namespace wallet { namespace detail { (preimage_hash)(preimage_size)(claim_period_seconds)(broadcast) ) } - signed_transaction wallet_api_impl::htlc_redeem( string htlc_id, string issuer, const std::vector& preimage, - bool broadcast ) + signed_transaction wallet_api_impl::htlc_redeem( string htlc_id, string issuer, + const std::vector& preimage, bool broadcast ) { try { @@ -134,7 +147,7 @@ namespace graphene { namespace wallet { namespace detail { } FC_CAPTURE_AND_RETHROW( (htlc_id)(issuer)(preimage)(broadcast) ) } - signed_transaction wallet_api_impl::htlc_extend ( string htlc_id, string issuer, const uint32_t seconds_to_add, + signed_transaction wallet_api_impl::htlc_extend ( string htlc_id, string issuer, const uint32_t seconds_to_add, bool broadcast) { try diff --git a/programs/build_helpers/build_and_test b/programs/build_helpers/build_and_test index ac1eea47f0..0b712e859f 100755 --- a/programs/build_helpers/build_and_test +++ b/programs/build_helpers/build_and_test @@ -9,8 +9,13 @@ programs/build_helpers/buildstep make.everything 2400 "programs/build_helpers/ma 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 run.sonar 1500 "which sonar-scanner && sonar-scanner" || true +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" +du -hs sonar_cache +# The first pass, skip some files. This will remove the skipped files from the cache, but is an acceptable trade-off +programs/build_helpers/buildstep prepare.sonar.part 1 "cp sonar-project.properties sonar-project.properties.bak; sed -i '/sonar\.exclusions=/d;s/#sonar\.exclusions.part/sonar.exclusions/' sonar-project.properties" +programs/build_helpers/buildstep run.sonar.part 1500 "which sonar-scanner && sonar-scanner" +programs/build_helpers/buildstep prepare.sonar.full 1 "cp sonar-project.properties.bak sonar-project.properties" +du -hs sonar_cache programs/build_helpers/buildstep end 0 ccache -s diff --git a/programs/build_helpers/build_protocol b/programs/build_helpers/build_protocol index f37a2326b2..2a0fc32771 100755 --- a/programs/build_helpers/build_protocol +++ b/programs/build_helpers/build_protocol @@ -6,11 +6,11 @@ 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 make.fc 230 "make -j 2 fc" +programs/build_helpers/buildstep make.custom_auths 700 "make -j 1 graphene_protocol_custom_auths" programs/build_helpers/buildstep make.protocol 250 "make -j 2 graphene_protocol" programs/build_helpers/buildstep make.chain 450 "make -j 2 graphene_chain" -programs/build_helpers/buildstep make.node 450 "make -j 2 witness_node" -programs/build_helpers/buildstep make.cli 450 "make -j 2 cli_wallet" -programs/build_helpers/buildstep make.cli_test 450 "make -j 2 cli_test" -programs/build_helpers/buildstep make.chain_test 450 "make -j 2 chain_test" +programs/build_helpers/buildstep make.node 600 "make -j 2 witness_node" +programs/build_helpers/buildstep make.cli 500 "make -j 2 cli_wallet" +programs/build_helpers/buildstep make.chain_test 750 "make -j 2 chain_test" programs/build_helpers/buildstep end 0 ccache -s diff --git a/programs/build_helpers/cat-parts.cmake b/programs/build_helpers/cat-parts.cmake index dd49fbaec2..1f65e2045e 100644 --- a/programs/build_helpers/cat-parts.cmake +++ b/programs/build_helpers/cat-parts.cmake @@ -10,7 +10,7 @@ file( MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/include/graphene/chain/" ) set( HARDFORK_REGENERATE TRUE ) -file( GLOB HARDFORKS "${CMAKE_CURRENT_SOURCE_DIR}/hardfork.d/*" ) +file( GLOB HARDFORKS "${CMAKE_CURRENT_SOURCE_DIR}/hardfork.d/*.hf" ) foreach( HF ${HARDFORKS} ) file( READ "${HF}" INCL ) string( CONCAT HARDFORK_CONTENT ${HARDFORK_CONTENT} ${INCL} ) @@ -19,9 +19,9 @@ endforeach( HF ) if( EXISTS ${HARDFORK_FILE} ) file( READ ${HARDFORK_FILE} HFF ) - if( ${HFF} STREQUAL ${HARDFORK_CONTENT} ) + if( "${HFF}" STREQUAL "${HARDFORK_CONTENT}" ) set( HARDFORK_REGENERATE FALSE ) - endif( ${HFF} STREQUAL ${HARDFORK_CONTENT} ) + endif( "${HFF}" STREQUAL "${HARDFORK_CONTENT}" ) endif( EXISTS ${HARDFORK_FILE} ) if( HARDFORK_REGENERATE ) diff --git a/programs/build_helpers/member_enumerator.cpp b/programs/build_helpers/member_enumerator.cpp index 6a292298ee..830878e314 100644 --- a/programs/build_helpers/member_enumerator.cpp +++ b/programs/build_helpers/member_enumerator.cpp @@ -103,7 +103,7 @@ void class_processor::process_class( const static_variant< T... >* dummy ) static_variant dummy2; static_variant_visitor vtor( this ); - for( int w=0; w/dev/null && [ $((`date +%s` - _START)) -lt 120 ]; do - programs/cli_wallet/cli_wallet -s ws://127.0.0.1:8090 -d -H 127.0.0.1:8091 >cli.log 2>&1 & + programs/cli_wallet/cli_wallet -sws://127.0.0.1:8090 -d -H127.0.0.1:8091 >cli.log 2>&1 & CLI_PID=$! sleep 2 done @@ -38,7 +38,7 @@ fi echo "Waiting for head_block 131071..." 1>&2 touch "$DATA_DIR"/info.json _START="`date +%s`" -while [ $(( "`date +%s`" - $_START )) -lt 180 ]; do +while [ $(( `date +%s` - $_START )) -lt 180 ]; do sleep 2 curl --silent -o "$DATA_DIR"/info.json --data '{"id":0,"method":"info","params":[]}' \ http://127.0.0.1:8091/rpc diff --git a/programs/build_helpers/scan_with_sonar b/programs/build_helpers/scan_with_sonar new file mode 100755 index 0000000000..ba133aafcd --- /dev/null +++ b/programs/build_helpers/scan_with_sonar @@ -0,0 +1,22 @@ +#!/bin/bash +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 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 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" +du -hs sonar_cache +# The first pass, skip some files. This will remove the skipped files from the cache, but is an acceptable trade-off +programs/build_helpers/buildstep prepare.sonar.part 1 "cp sonar-project.properties sonar-project.properties.bak; sed -i '/sonar\.exclusions=/d;s/#sonar\.exclusions.part/sonar.exclusions/' sonar-project.properties" +programs/build_helpers/buildstep run.sonar.part 1500 "which sonar-scanner && sonar-scanner" +programs/build_helpers/buildstep prepare.sonar.full 1 "cp sonar-project.properties.bak sonar-project.properties" +du -hs sonar_cache +# The second pass, scan all files +programs/build_helpers/buildstep run.sonar.full 2000 "which sonar-scanner && sonar-scanner" +du -hs sonar_cache +programs/build_helpers/buildstep end 0 +ccache -s + diff --git a/programs/build_helpers/set_sonar_branch b/programs/build_helpers/set_sonar_branch index aed0d722d5..9fb20a4ed1 100755 --- a/programs/build_helpers/set_sonar_branch +++ b/programs/build_helpers/set_sonar_branch @@ -22,55 +22,58 @@ clear_branch () { sed -i '/sonar\.branch/d' "$1" } -TARGET= +ORIGINAL_TARGET="$( grep 'sonar\.branch\.target' "$1" | sed 's=^.*[:=] *==' )" + +TARGET="$ORIGINAL_TARGET" FETCH= if [ -n "$TRAVIS_PULL_REQUEST" -a "$TRAVIS_PULL_REQUEST" != false ]; then - # PRs work per default + # PRs work per default, remove sonar.branch.* since they only work with sonar.pullrequest.* echo "Detected PR '$TRAVIS_PULL_REQUEST'" - clear_branch "$1" + TARGET= FETCH="$TRAVIS_BRANCH" elif [ -n "$TRAVIS_TAG" ]; then # Tag build is either master or testnet echo "Detected tag '$TRAVIS_TAG'" - clear_branch "$1" case "$TRAVIS_TAG" in *test*) TARGET=testnet; FETCH=testnet; ;; - *) FETCH=master; ;; + *) TARGET=master; FETCH=master; ;; esac else case "$TRAVIS_BRANCH" in - master|develop|testnet|next_hardfork) + master|develop|testnet|hardfork) # Long-lived branches stand for themselves echo "Detected long-lived branch '$TRAVIS_BRANCH'" - clear_branch "$1" + TARGET="$TRAVIS_BRANCH" FETCH="$TRAVIS_BRANCH" ;; *test*release*) # Testnet release branch will be merged into testnet echo "Detected testnet release branch '$TRAVIS_BRANCH'" - clear_branch "$1" TARGET=testnet FETCH=testnet ;; *release*) # Release branch will be merged into default (master) echo "Detected release branch '$TRAVIS_BRANCH'" - clear_branch "$1" + TARGET=master FETCH=master ;; *) # All other branches should have sonar.branch.target in their # sonar.properties, leave it unchanged echo "Detected normal branch '$TRAVIS_BRANCH'" - FETCH="$( grep 'sonar\.branch\.target' "$1" | sed 's=^.*[:=] *==' )" + FETCH="$TARGET" esac fi echo "Branch target '$TARGET', fetch target '$FETCH'" -if [ -n "$TARGET" ]; then - echo "sonar.branch.target=$TARGET" >>"$1" +if [ "$TARGET" != "$ORIGINAL_TARGET" ]; then + clear_branch "$1" + if [ -n "$TARGET" ]; then + echo "sonar.branch.target=$TARGET" >>"$1" + fi fi #if [ -n "$FETCH" ]; then # Unfortunately this leads to sonar failing. Apparently it needs a full diff --git a/programs/js_operation_serializer/main.cpp b/programs/js_operation_serializer/main.cpp index e2c3513589..f691496c49 100644 --- a/programs/js_operation_serializer/main.cpp +++ b/programs/js_operation_serializer/main.cpp @@ -293,7 +293,7 @@ struct serializer< fc::static_variant, false > { init = true; fc::static_variant var; - for( int i = 0; i < var.count(); ++i ) + for( size_t i = 0; i < var.count(); ++i ) { var.set_which(i); var.visit( register_type_visitor() ); @@ -372,7 +372,7 @@ int main( int argc, char** argv ) operation op; std::cout << "ChainTypes.operations=\n"; - for( int i = 0; i < op.count(); ++i ) + for( size_t i = 0; i < op.count(); ++i ) { op.set_which(i); op.visit( detail_ns::serialize_type_visitor(i) ); diff --git a/programs/size_checker/main.cpp b/programs/size_checker/main.cpp index a7c09308fc..e283e2d6e6 100644 --- a/programs/size_checker/main.cpp +++ b/programs/size_checker/main.cpp @@ -85,7 +85,7 @@ int main( int argc, char** argv ) idump( (witnesses) ); - for( int32_t i = 0; i < op.count(); ++i ) + for( size_t i = 0; i < op.count(); ++i ) { op.set_which(i); op.visit( size_check_type_visitor(i) ); diff --git a/programs/witness_node/main.cpp b/programs/witness_node/main.cpp index c3c76ae2d4..5959e8c8bf 100644 --- a/programs/witness_node/main.cpp +++ b/programs/witness_node/main.cpp @@ -67,13 +67,14 @@ int main(int argc, char** argv) { try { bpo::options_description app_options("BitShares Witness Node"); bpo::options_description cfg_options("BitShares Witness Node"); + std::string default_plugins = "witness account_history market_history grouped_orders " + "api_helper_indexes custom_operations"; app_options.add_options() ("help,h", "Print this help message and exit.") ("data-dir,d", bpo::value()->default_value("witness_node_data_dir"), "Directory containing databases, configuration file, etc.") ("version,v", "Display version information") - ("plugins", bpo::value() - ->default_value("witness account_history market_history grouped_orders api_helper_indexes"), + ("plugins", bpo::value()->default_value(default_plugins), "Space-separated list of plugins to activate") ("ignore-api-helper-indexes-warning", "Do not exit if api_helper_indexes plugin is not enabled."); @@ -84,9 +85,8 @@ int main(int argc, char** argv) { cfg_options.add(cfg); cfg_options.add_options() - ("plugins", bpo::value() - ->default_value("witness account_history market_history grouped_orders api_helper_indexes"), - "Space-separated list of plugins to activate") + ("plugins", bpo::value()->default_value(default_plugins), + "Space-separated list of plugins to activate") ("ignore-api-helper-indexes-warning", "Do not exit if api_helper_indexes plugin is not enabled."); auto witness_plug = node->register_plugin(); diff --git a/sonar-project.properties b/sonar-project.properties index e414924a5a..9b7b162cf0 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -7,6 +7,8 @@ sonar.links.issue=https://github.com/bitshares/bitshares-core/issues sonar.links.scm=https://github.com/bitshares/bitshares-core/tree/master sonar.tests=tests +# Used by the `build_and_test` script for the first pass when building with Travis CI, to skip some files +#sonar.exclusions.part=programs/build_helper/**/*,libraries/fc/**/*,libraries/egenesis/egenesis_full.cpp,libraries/chain/**/*,libraries/protocol/**/* sonar.exclusions=programs/build_helper/**/*,libraries/fc/**/*,libraries/egenesis/egenesis_full.cpp sonar.sources=libraries,programs sonar.cfamily.build-wrapper-output=bw-output @@ -15,5 +17,6 @@ sonar.cfamily.threads=2 sonar.cfamily.cache.enabled=true sonar.cfamily.cache.path=sonar_cache -# should be changed in hardfork branch and removed in release branches +# Decide which tree the current build belongs to in SonarCloud. +# Managed by the `set_sonar_branch` script when building with Travis CI. sonar.branch.target=develop diff --git a/tests/app/main.cpp b/tests/app/main.cpp index 654a6b6842..f8515a2d77 100644 --- a/tests/app/main.cpp +++ b/tests/app/main.cpp @@ -316,7 +316,7 @@ BOOST_AUTO_TEST_CASE( two_node_network ) BOOST_TEST_MESSAGE( "Broadcasting tx" ); app1.p2p_node()->broadcast(graphene::net::trx_message(trx)); - fc::usleep(fc::milliseconds(500)); + fc::usleep(fc::milliseconds(3000)); BOOST_CHECK_EQUAL( db1->get_balance( GRAPHENE_NULL_ACCOUNT, asset_id_type() ).amount.value, 1000000 ); BOOST_CHECK_EQUAL( db2->get_balance( GRAPHENE_NULL_ACCOUNT, asset_id_type() ).amount.value, 1000000 ); @@ -333,7 +333,7 @@ BOOST_AUTO_TEST_CASE( two_node_network ) BOOST_TEST_MESSAGE( "Broadcasting block" ); app2.p2p_node()->broadcast(graphene::net::block_message( block_1 )); - fc::usleep(fc::milliseconds(500)); + fc::usleep(fc::milliseconds(3000)); BOOST_TEST_MESSAGE( "Verifying nodes are still connected" ); BOOST_CHECK_EQUAL(app1.p2p_node()->get_connection_count(), 1u); BOOST_CHECK_EQUAL(app1.chain_database()->head_block_num(), 1u); diff --git a/tests/cli/main.cpp b/tests/cli/main.cpp index baf3852ba7..0615f8dff6 100644 --- a/tests/cli/main.cpp +++ b/tests/cli/main.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include @@ -142,6 +143,7 @@ std::shared_ptr start_application(fc::temp_directory ); cfg.emplace("genesis-json", boost::program_options::variable_value(create_genesis_file(app_dir), false)); cfg.emplace("seed-nodes", boost::program_options::variable_value(string("[]"), false)); + cfg.emplace("custom-operations-start-block", boost::program_options::variable_value(uint32_t(1), false)); app1->initialize(app_dir.path(), cfg); app1->initialize_plugins(cfg); @@ -174,12 +176,46 @@ bool generate_block(std::shared_ptr app, graphene::c } } -bool generate_block(std::shared_ptr app) +bool generate_block(std::shared_ptr app) { graphene::chain::signed_block returned_block; return generate_block(app, returned_block); } + +signed_block generate_block(std::shared_ptr app, uint32_t skip, const fc::ecc::private_key& key, int miss_blocks) +{ + // skip == ~0 will skip checks specified in database::validation_steps + skip |= database::skip_undo_history_check; + + auto db = app->chain_database(); + auto block = db->generate_block(db->get_slot_time(miss_blocks + 1), + db->get_scheduled_witness(miss_blocks + 1), + key, skip); + db->clear_pending(); + return block; +} + + +////// +// Generate blocks until the timestamp +////// +uint32_t generate_blocks(std::shared_ptr app, fc::time_point_sec timestamp) +{ + fc::ecc::private_key committee_key = fc::ecc::private_key::regenerate(fc::sha256::hash(string("nathan"))); + uint32_t skip = ~0; + auto db = app->chain_database(); + + generate_block(app); + auto slots_to_miss = db->get_slot_at_time(timestamp); + if( slots_to_miss <= 1 ) + return 1; + --slots_to_miss; + generate_block(app, skip, committee_key, slots_to_miss); + return 2; +} + + /////////// /// @brief Skip intermediate blocks, and generate a maintenance block /// @param app the application @@ -215,7 +251,8 @@ class client_connection client_connection( std::shared_ptr app, const fc::temp_directory& data_dir, - const int server_port_number + const int server_port_number, + const std::string custom_wallet_filename = "wallet.json" ) { wallet_data.chain_id = app->chain_database()->get_chain_id(); @@ -231,7 +268,7 @@ class client_connection BOOST_CHECK(remote_login_api->login( wallet_data.ws_user, wallet_data.ws_password ) ); wallet_api_ptr = std::make_shared(wallet_data, remote_login_api); - wallet_filename = data_dir.path().generic_string() + "/wallet.json"; + wallet_filename = data_dir.path().generic_string() + "/" + custom_wallet_filename; wallet_api_ptr->set_wallet_filename(wallet_filename); wallet_api = fc::api(wallet_api_ptr); @@ -239,17 +276,10 @@ class client_connection wallet_cli = std::make_shared(GRAPHENE_MAX_NESTED_OBJECTS); for( auto& name_formatter : wallet_api_ptr->get_result_formatters() ) wallet_cli->format_result( name_formatter.first, name_formatter.second ); - - boost::signals2::scoped_connection closed_connection(websocket_connection->closed.connect([=]{ - cerr << "Server has disconnected us.\n"; - wallet_cli->stop(); - })); - (void)(closed_connection); } ~client_connection() { - // wait for everything to finish up - fc::usleep(fc::milliseconds(500)); + wallet_cli->stop(); } public: fc::http::websocket_client websocket_client; @@ -316,10 +346,6 @@ struct cli_fixture ~cli_fixture() { BOOST_TEST_MESSAGE("Cleanup cli_wallet::boost_fixture_test_case"); - - // wait for everything to finish up - fc::usleep(fc::seconds(1)); - app1->shutdown(); #ifdef _WIN32 sockQuit(); @@ -397,7 +423,7 @@ BOOST_FIXTURE_TEST_CASE( create_new_account, cli_fixture ) BOOST_CHECK(con.wallet_api_ptr->import_key("jmjatlanta", bki.wif_priv_key)); con.wallet_api_ptr->save_wallet_file(con.wallet_filename); - // attempt to give jmjatlanta some bitsahres + // attempt to give jmjatlanta some bitshares BOOST_TEST_MESSAGE("Transferring bitshares from Nathan to jmjatlanta"); signed_transaction transfer_tx = con.wallet_api_ptr->transfer( "nathan", "jmjatlanta", "10000", "1.3.0", "Here are some CORE token for your new account", true @@ -495,6 +521,122 @@ BOOST_FIXTURE_TEST_CASE( cli_get_signed_transaction_signers, cli_fixture ) } } + +/////////////////////// +// Wallet RPC +// Test adding an unnecessary signature to a transaction +/////////////////////// +BOOST_FIXTURE_TEST_CASE(cli_sign_tx_with_unnecessary_signature, cli_fixture) { + try { + auto db = app1->chain_database(); + + account_object nathan_acct = con.wallet_api_ptr->get_account("nathan"); + INVOKE(upgrade_nathan_account); + + // Register Bob account + const auto bob_bki = con.wallet_api_ptr->suggest_brain_key(); + con.wallet_api_ptr->register_account( + "bob", bob_bki.pub_key, bob_bki.pub_key, "nathan", "nathan", 0, true + ); + + // Register Charlie account + const graphene::wallet::brain_key_info charlie_bki = con.wallet_api_ptr->suggest_brain_key(); + con.wallet_api_ptr->register_account( + "charlie", charlie_bki.pub_key, charlie_bki.pub_key, "nathan", "nathan", 0, true + ); + const account_object &charlie_acc = con.wallet_api_ptr->get_account("charlie"); + + // Import Bob's key + BOOST_CHECK(con.wallet_api_ptr->import_key("bob", bob_bki.wif_priv_key)); + + // Create transaction with a transfer operation from Nathan to Charlie + transfer_operation top; + top.from = nathan_acct.id; + top.to = charlie_acc.id; + top.amount = asset(5000); + top.fee = db->current_fee_schedule().calculate_fee(top); + + signed_transaction test_tx; + test_tx.operations.push_back(top); + + // Sign the transaction with the implied nathan's key and the explicitly yet unnecessary Bob's key + auto signed_trx = con.wallet_api_ptr->sign_transaction2(test_tx, {bob_bki.pub_key}, false); + + // Check for two signatures on the transaction + BOOST_CHECK_EQUAL(signed_trx.signatures.size(), 2); + flat_set signers = con.wallet_api_ptr->get_transaction_signers(signed_trx); + + // Check that the signed transaction contains both Nathan's required signature and Bob's unnecessary signature + BOOST_CHECK_EQUAL(nathan_acct.active.get_keys().size(), 1); + flat_set expected_signers = {bob_bki.pub_key, nathan_acct.active.get_keys().front()}; + flat_set actual_signers = con.wallet_api_ptr->get_transaction_signers(signed_trx); + BOOST_CHECK(signers == expected_signers); + + } catch (fc::exception &e) { + edump((e.to_detail_string())); + throw; + } +} + + +/////////////////////// +// Wallet RPC +// Test adding an unnecessary signature to a transaction builder +/////////////////////// +BOOST_FIXTURE_TEST_CASE(cli_sign_tx_builder_with_unnecessary_signature, cli_fixture) { + try { + auto db = app1->chain_database(); + + account_object nathan_acct = con.wallet_api_ptr->get_account("nathan"); + INVOKE(upgrade_nathan_account); + + // Register Bob account + const auto bob_bki = con.wallet_api_ptr->suggest_brain_key(); + con.wallet_api_ptr->register_account( + "bob", bob_bki.pub_key, bob_bki.pub_key, "nathan", "nathan", 0, true + ); + + // Register Charlie account + const graphene::wallet::brain_key_info charlie_bki = con.wallet_api_ptr->suggest_brain_key(); + con.wallet_api_ptr->register_account( + "charlie", charlie_bki.pub_key, charlie_bki.pub_key, "nathan", "nathan", 0, true + ); + const account_object &charlie_acc = con.wallet_api_ptr->get_account("charlie"); + + // Import Bob's key + BOOST_CHECK(con.wallet_api_ptr->import_key("bob", bob_bki.wif_priv_key)); + + // Use transaction builder to build a transaction with a transfer operation from Nathan to Charlie + graphene::wallet::transaction_handle_type tx_handle = con.wallet_api_ptr->begin_builder_transaction(); + + transfer_operation top; + top.from = nathan_acct.id; + top.to = charlie_acc.id; + top.amount = asset(5000); + + con.wallet_api_ptr->add_operation_to_builder_transaction(tx_handle, top); + con.wallet_api_ptr->set_fees_on_builder_transaction(tx_handle, GRAPHENE_SYMBOL); + + // Sign the transaction with the implied nathan's key and the explicitly yet unnecessary Bob's key + auto signed_trx = con.wallet_api_ptr->sign_builder_transaction2(tx_handle, {bob_bki.pub_key}, false); + + // Check for two signatures on the transaction + BOOST_CHECK_EQUAL(signed_trx.signatures.size(), 2); + flat_set signers = con.wallet_api_ptr->get_transaction_signers(signed_trx); + + // Check that the signed transaction contains both Nathan's required signature and Bob's unnecessary signature + BOOST_CHECK_EQUAL(nathan_acct.active.get_keys().size(), 1); + flat_set expected_signers = {bob_bki.pub_key, nathan_acct.active.get_keys().front()}; + flat_set actual_signers = con.wallet_api_ptr->get_transaction_signers(signed_trx); + BOOST_CHECK(signers == expected_signers); + + } catch (fc::exception &e) { + edump((e.to_detail_string())); + throw; + } +} + + BOOST_FIXTURE_TEST_CASE( cli_get_available_transaction_signers, cli_fixture ) { try @@ -706,7 +848,7 @@ BOOST_FIXTURE_TEST_CASE( account_history_pagination, cli_fixture ) { INVOKE(create_new_account); - // attempt to give jmjatlanta some bitsahres + // attempt to give jmjatlanta some bitshares BOOST_TEST_MESSAGE("Transferring bitshares from Nathan to jmjatlanta"); for(int i = 1; i <= 199; i++) { @@ -803,7 +945,7 @@ BOOST_AUTO_TEST_CASE( cli_multisig_transaction ) create_multisig_acct_tx.operations.push_back(account_create_op); con.wallet_api_ptr->sign_transaction(create_multisig_acct_tx, true); - // attempt to give cifer.test some bitsahres + // attempt to give cifer.test some bitshares BOOST_TEST_MESSAGE("Transferring bitshares from Nathan to cifer.test"); signed_transaction transfer_tx1 = con.wallet_api_ptr->transfer("nathan", "cifer.test", "10000", "1.3.0", "Here are some BTS for your new account", true); @@ -855,7 +997,7 @@ BOOST_AUTO_TEST_CASE( cli_multisig_transaction ) auto balances = con.wallet_api_ptr->list_account_balances( "cifer.test" ); for (auto b : balances) { if (b.asset_id == asset_id_type()) { - BOOST_ASSERT(b == asset(900000000 - 3000000)); + BOOST_CHECK(b == asset(900000000 - 3000000)); } } @@ -866,6 +1008,10 @@ BOOST_AUTO_TEST_CASE( cli_multisig_transaction ) throw; } app1->shutdown(); + app1.reset(); + // Intentional delay after app1->shutdown + std::cout << "cli_multisig_transaction conclusion: Intentional delay" << std::endl; + fc::usleep(fc::seconds(1)); } graphene::wallet::plain_keys decrypt_keys( const std::string& password, const vector& cipher_keys ) @@ -980,10 +1126,8 @@ BOOST_AUTO_TEST_CASE( cli_create_htlc ) BOOST_CHECK(!bki.brain_priv_key.empty()); signed_transaction create_acct_tx = con.wallet_api_ptr->create_account_with_brain_key(bki.brain_priv_key, "alice", "nathan", "nathan", true); - // save the private key for this new account in the wallet file - BOOST_CHECK(con.wallet_api_ptr->import_key("alice", bki.wif_priv_key)); con.wallet_api_ptr->save_wallet_file(con.wallet_filename); - // attempt to give alice some bitsahres + // attempt to give alice some bitshares BOOST_TEST_MESSAGE("Transferring bitshares from Nathan to alice"); signed_transaction transfer_tx = con.wallet_api_ptr->transfer("nathan", "alice", "10000", "1.3.0", "Here are some CORE token for your new account", true); @@ -995,17 +1139,15 @@ BOOST_AUTO_TEST_CASE( cli_create_htlc ) BOOST_CHECK(!bki.brain_priv_key.empty()); signed_transaction create_acct_tx = con.wallet_api_ptr->create_account_with_brain_key(bki.brain_priv_key, "bob", "nathan", "nathan", true); - // save the private key for this new account in the wallet file - BOOST_CHECK(con.wallet_api_ptr->import_key("bob", bki.wif_priv_key)); - con.wallet_api_ptr->save_wallet_file(con.wallet_filename); - // attempt to give bob some bitsahres + // this should cause resync which will import the keys of alice and bob + generate_block(app1); + // attempt to give bob some bitshares BOOST_TEST_MESSAGE("Transferring bitshares from Nathan to Bob"); signed_transaction transfer_tx = con.wallet_api_ptr->transfer("nathan", "bob", "10000", "1.3.0", "Here are some CORE token for your new account", true); con.wallet_api_ptr->issue_asset("bob", "5", "BOBCOIN", "Here are your BOBCOINs", true); } - BOOST_TEST_MESSAGE("Alice has agreed to buy 3 BOBCOIN from Bob for 3 BTS. Alice creates an HTLC"); // create an HTLC std::string preimage_string = "My Secret"; @@ -1022,7 +1164,7 @@ BOOST_AUTO_TEST_CASE( cli_create_htlc ) uint32_t timelock = fc::days(1).to_seconds(); graphene::chain::signed_transaction result_tx = con.wallet_api_ptr->htlc_create("alice", "bob", - "3", "1.3.0", "SHA256", hash_str, preimage_string.size(), timelock, true); + "3", "1.3.0", "SHA256", hash_str, preimage_string.size(), timelock, "", true); // normally, a wallet would watch block production, and find the transaction. Here, we can cheat: std::string alice_htlc_id_as_string; @@ -1044,7 +1186,7 @@ BOOST_AUTO_TEST_CASE( cli_create_htlc ) // Bob likes what he sees, so he creates an HTLC, using the info he retrieved from Alice's HTLC con.wallet_api_ptr->htlc_create("bob", "alice", - "3", "BOBCOIN", "SHA256", hash_str, preimage_string.size(), timelock, true); + "3", "BOBCOIN", "SHA256", hash_str, preimage_string.size(), timelock, "", true); // normally, a wallet would watch block production, and find the transaction. Here, we can cheat: std::string bob_htlc_id_as_string; @@ -1090,6 +1232,10 @@ BOOST_AUTO_TEST_CASE( cli_create_htlc ) throw; } app1->shutdown(); + app1.reset(); + // Intentional delay after app1->shutdown + std::cout << "cli_create_htlc conclusion: Intentional delay" << std::endl; + fc::usleep(fc::seconds(1)); } static string encapsulate( const graphene::wallet::signed_message& msg ) @@ -1264,3 +1410,412 @@ BOOST_FIXTURE_TEST_CASE( general_storage, cli_fixture ) throw; } } + +////// +// Template copied +////// +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; +} + +/////////////////////// +// Wallet RPC +// Test sign_builder_transaction2 with an account (bob) that has received a custom authorization +// to transfer funds from another account (alice) +/////////////////////// +BOOST_FIXTURE_TEST_CASE(cli_use_authorized_transfer, cli_fixture) { + try { + ////// + // Initialize the blockchain + ////// + auto db = app1->chain_database(); + + account_object nathan_acct = con.wallet_api_ptr->get_account("nathan"); + INVOKE(upgrade_nathan_account); + + // Register Alice account + const auto alice_bki = con.wallet_api_ptr->suggest_brain_key(); + con.wallet_api_ptr->register_account( + "alice", alice_bki.pub_key, alice_bki.pub_key, "nathan", "nathan", 0, true + ); + const account_object &alice_acc = con.wallet_api_ptr->get_account("alice"); + + // Register Bob account + const auto bob_bki = con.wallet_api_ptr->suggest_brain_key(); + con.wallet_api_ptr->register_account( + "bob", bob_bki.pub_key, bob_bki.pub_key, "nathan", "nathan", 0, true + ); + const account_object &bob_acc = con.wallet_api_ptr->get_account("bob"); + + // Register Charlie account + const graphene::wallet::brain_key_info charlie_bki = con.wallet_api_ptr->suggest_brain_key(); + con.wallet_api_ptr->register_account( + "charlie", charlie_bki.pub_key, charlie_bki.pub_key, "nathan", "nathan", 0, true + ); + const account_object &charlie_acc = con.wallet_api_ptr->get_account("charlie"); + + // Fund Alice's account + con.wallet_api_ptr->transfer("nathan", "alice", "450000", "1.3.0", "", true); + + // Initialize common variables + signed_transaction signed_trx; + + + ////// + // Initialize Alice's CLI wallet + ////// + client_connection con_alice(app1, app_dir, server_port_number, "wallet_alice.json"); + con_alice.wallet_api_ptr->set_password("supersecret"); + con_alice.wallet_api_ptr->unlock("supersecret"); + + // Import Alice's key + BOOST_CHECK(con_alice.wallet_api_ptr->import_key("alice", alice_bki.wif_priv_key)); + + + ////// + // Initialize the blockchain for BSIP 40 + ////// + generate_blocks(app1, HARDFORK_BSIP_40_TIME); + // Set committee parameters + app1->chain_database()->modify(app1->chain_database()->get_global_properties(), [](global_property_object& p) { + p.parameters.extensions.value.custom_authority_options = custom_authority_options_type(); + }); + + + ////// + // Alice authorizes Bob to transfer funds from her account to Charlie's account + ////// + graphene::wallet::transaction_handle_type tx_alice_handle = con_alice.wallet_api_ptr->begin_builder_transaction(); + + custom_authority_create_operation caop; + caop.account = alice_acc.get_id(); + caop.auth.add_authority(bob_acc.get_id(), 1); + caop.auth.weight_threshold = 1; + caop.enabled = true; + caop.valid_to = db->head_block_time() + 1000; + caop.operation_type = operation::tag::value; + + // Restriction should have "to" equal Charlie + vector restrictions; + auto to_index = member_index("to"); + restrictions.emplace_back(to_index, restriction::func_eq, charlie_acc.get_id()); + + con_alice.wallet_api_ptr->add_operation_to_builder_transaction(tx_alice_handle, caop); + asset ca_fee = con_alice.wallet_api_ptr->set_fees_on_builder_transaction(tx_alice_handle, GRAPHENE_SYMBOL); + + // Sign the transaction with the inferred Alice key + signed_trx = con_alice.wallet_api_ptr->sign_builder_transaction2(tx_alice_handle, {}, true); + + // Check for one signatures on the transaction + BOOST_CHECK_EQUAL(signed_trx.signatures.size(), 1); + + // Check that the signed transaction contains Alice's signature + flat_set expected_signers = {alice_bki.pub_key}; + flat_set actual_signers = con_alice.wallet_api_ptr->get_transaction_signers(signed_trx); + BOOST_CHECK(actual_signers == expected_signers); + + + ////// + // Initialize Bob's CLI wallet + ////// + client_connection con_bob(app1, app_dir, server_port_number, "wallet_bob.json"); + con_bob.wallet_api_ptr->set_password("supersecret"); + con_bob.wallet_api_ptr->unlock("supersecret"); + + // Import Bob's key + BOOST_CHECK(con_bob.wallet_api_ptr->import_key("bob", bob_bki.wif_priv_key)); + + + ////// + // Bob attempt to transfer funds from Alice to Charlie while using Bob's wallet + // This should succeed because Bob is authorized to transfer by Alice + ////// + graphene::wallet::transaction_handle_type tx_bob_handle = con_bob.wallet_api_ptr->begin_builder_transaction(); + + const asset transfer_amount = asset(123 * GRAPHENE_BLOCKCHAIN_PRECISION); + transfer_operation top; + top.from = alice_acc.id; + top.to = charlie_acc.id; + top.amount = transfer_amount; + + con_bob.wallet_api_ptr->add_operation_to_builder_transaction(tx_bob_handle, top); + asset transfer_fee = con_bob.wallet_api_ptr->set_fees_on_builder_transaction(tx_bob_handle, GRAPHENE_SYMBOL); + + // Sign the transaction with the explicit Bob key + signed_trx = con_bob.wallet_api_ptr->sign_builder_transaction2(tx_bob_handle, {bob_bki.pub_key}, true); + + // Check for one signatures on the transaction + BOOST_CHECK_EQUAL(signed_trx.signatures.size(), 1); + + // Check that the signed transaction contains Bob's signature + BOOST_CHECK_EQUAL(nathan_acct.active.get_keys().size(), 1); + expected_signers = {bob_bki.pub_key}; + actual_signers = con_bob.wallet_api_ptr->get_transaction_signers(signed_trx); + BOOST_CHECK(actual_signers == expected_signers); + + + ////// + // Check account balances + ////// + // Check Charlie's balances + vector charlie_balances = con.wallet_api_ptr->list_account_balances("charlie"); + BOOST_CHECK_EQUAL(charlie_balances.size(), 1); + asset charlie_core_balance = charlie_balances.front(); + asset expected_charlie_core_balance = transfer_amount; + BOOST_CHECK(charlie_core_balance == expected_charlie_core_balance); + + // Check Bob's balances + vector bob_balances = con.wallet_api_ptr->list_account_balances("bob"); + BOOST_CHECK_EQUAL(bob_balances.size(), 0); + + // Check Alice's balance + vector alice_balances = con.wallet_api_ptr->list_account_balances("alice"); + BOOST_CHECK_EQUAL(alice_balances.size(), 1); + asset alice_core_balance = alice_balances.front(); + asset expected_alice_balance = asset(450000 * GRAPHENE_BLOCKCHAIN_PRECISION) + - expected_charlie_core_balance + - ca_fee - transfer_fee; + BOOST_CHECK(alice_core_balance.asset_id == expected_alice_balance.asset_id); + BOOST_CHECK_EQUAL(alice_core_balance.amount.value, expected_alice_balance.amount.value); + + } catch (fc::exception &e) { + edump((e.to_detail_string())); + throw; + } +} + +BOOST_AUTO_TEST_CASE( cli_create_htlc_bsip64 ) +{ + using namespace graphene::chain; + using namespace graphene::app; + std::shared_ptr app1; + try { + fc::temp_directory app_dir( graphene::utilities::temp_directory_path() ); + + int server_port_number = 0; + app1 = start_application(app_dir, server_port_number); + // set committee parameters + app1->chain_database()->modify(app1->chain_database()->get_global_properties(), [](global_property_object& p) + { + graphene::chain::htlc_options params; + params.max_preimage_size = 1024; + params.max_timeout_secs = 60 * 60 * 24 * 28; + p.parameters.extensions.value.updatable_htlc_options = params; + }); + + // connect to the server + client_connection con(app1, app_dir, server_port_number); + + // get past hardforks + generate_blocks( app1, HARDFORK_CORE_BSIP64_TIME + 10); + + BOOST_TEST_MESSAGE("Setting wallet password"); + con.wallet_api_ptr->set_password("supersecret"); + con.wallet_api_ptr->unlock("supersecret"); + + // import Nathan account + BOOST_TEST_MESSAGE("Importing nathan key"); + std::vector nathan_keys{"5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3"}; + BOOST_CHECK_EQUAL(nathan_keys[0], "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3"); + BOOST_CHECK(con.wallet_api_ptr->import_key("nathan", nathan_keys[0])); + + BOOST_TEST_MESSAGE("Importing nathan's balance"); + std::vector import_txs = con.wallet_api_ptr->import_balance("nathan", nathan_keys, true); + account_object nathan_acct_before_upgrade = con.wallet_api_ptr->get_account("nathan"); + + // upgrade nathan + BOOST_TEST_MESSAGE("Upgrading Nathan to LTM"); + signed_transaction upgrade_tx = con.wallet_api_ptr->upgrade_account("nathan", true); + account_object nathan_acct_after_upgrade = con.wallet_api_ptr->get_account("nathan"); + + // verify that the upgrade was successful + BOOST_CHECK_PREDICATE( std::not_equal_to(), + (nathan_acct_before_upgrade.membership_expiration_date.sec_since_epoch()) + (nathan_acct_after_upgrade.membership_expiration_date.sec_since_epoch()) ); + BOOST_CHECK(nathan_acct_after_upgrade.is_lifetime_member()); + + // Create new asset called BOBCOIN + try + { + graphene::chain::asset_options asset_ops; + asset_ops.max_supply = 1000000; + asset_ops.core_exchange_rate = price(asset(2),asset(1,asset_id_type(1))); + fc::optional bit_opts; + con.wallet_api_ptr->create_asset("nathan", "BOBCOIN", 5, asset_ops, bit_opts, true); + } + catch(exception& e) + { + BOOST_FAIL(e.what()); + } + catch(...) + { + BOOST_FAIL("Unknown exception creating BOBCOIN"); + } + + // create a new account for Alice + { + graphene::wallet::brain_key_info bki = con.wallet_api_ptr->suggest_brain_key(); + BOOST_CHECK(!bki.brain_priv_key.empty()); + signed_transaction create_acct_tx = con.wallet_api_ptr->create_account_with_brain_key(bki.brain_priv_key, + "alice", "nathan", "nathan", true); + con.wallet_api_ptr->save_wallet_file(con.wallet_filename); + // attempt to give alice some bitshares + BOOST_TEST_MESSAGE("Transferring bitshares from Nathan to alice"); + signed_transaction transfer_tx = con.wallet_api_ptr->transfer("nathan", "alice", "10000", "1.3.0", + "Here are some CORE token for your new account", true); + } + + // create a new account for Bob + { + graphene::wallet::brain_key_info bki = con.wallet_api_ptr->suggest_brain_key(); + BOOST_CHECK(!bki.brain_priv_key.empty()); + signed_transaction create_acct_tx = con.wallet_api_ptr->create_account_with_brain_key(bki.brain_priv_key, + "bob", "nathan", "nathan", true); + // this should cause resync which will import the keys of alice and bob + generate_block(app1); + // attempt to give bob some bitshares + BOOST_TEST_MESSAGE("Transferring bitshares from Nathan to Bob"); + signed_transaction transfer_tx = con.wallet_api_ptr->transfer("nathan", "bob", "10000", "1.3.0", + "Here are some CORE token for your new account", true); + con.wallet_api_ptr->issue_asset("bob", "5", "BOBCOIN", "Here are your BOBCOINs", true); + } + + BOOST_TEST_MESSAGE("Alice has agreed to buy 3 BOBCOIN from Bob for 3 BTS. Alice creates an HTLC"); + // create an HTLC + std::string preimage_string = "My Super Long Secret that is larger than 50 charaters. How do I look?\n"; + fc::hash160 preimage_md = fc::hash160::hash(preimage_string); + std::stringstream ss; + for(size_t i = 0; i < preimage_md.data_size(); i++) + { + char d = preimage_md.data()[i]; + unsigned char uc = static_cast(d); + ss << std::setfill('0') << std::setw(2) << std::hex << (int)uc; + } + std::string hash_str = ss.str(); + BOOST_TEST_MESSAGE("Secret is " + preimage_string + " and hash is " + hash_str); + uint32_t timelock = fc::days(1).to_seconds(); + graphene::chain::signed_transaction result_tx + = con.wallet_api_ptr->htlc_create("alice", "bob", + "3", "1.3.0", "HASH160", hash_str, preimage_string.size(), timelock, "Alice to Bob", true); + + // normally, a wallet would watch block production, and find the transaction. Here, we can cheat: + std::string alice_htlc_id_as_string; + { + BOOST_TEST_MESSAGE("The system is generating a block"); + graphene::chain::signed_block result_block; + BOOST_CHECK(generate_block(app1, result_block)); + + // get the ID: + htlc_id_type htlc_id = result_block.transactions[result_block.transactions.size()-1] + .operation_results[0].get(); + alice_htlc_id_as_string = (std::string)(object_id_type)htlc_id; + BOOST_TEST_MESSAGE("Alice shares the HTLC ID with Bob. The HTLC ID is: " + alice_htlc_id_as_string); + } + + // Bob can now look over Alice's HTLC, to see if it is what was agreed to. + BOOST_TEST_MESSAGE("Bob retrieves the HTLC Object by ID to examine it."); + auto alice_htlc = con.wallet_api_ptr->get_htlc(alice_htlc_id_as_string); + BOOST_TEST_MESSAGE("The HTLC Object is: " + fc::json::to_pretty_string(alice_htlc)); + + // Bob likes what he sees, so he creates an HTLC, using the info he retrieved from Alice's HTLC + con.wallet_api_ptr->htlc_create("bob", "alice", + "3", "BOBCOIN", "HASH160", hash_str, preimage_string.size(), fc::hours(12).to_seconds(), + "Bob to Alice", true); + + // normally, a wallet would watch block production, and find the transaction. Here, we can cheat: + std::string bob_htlc_id_as_string; + { + BOOST_TEST_MESSAGE("The system is generating a block"); + graphene::chain::signed_block result_block; + BOOST_CHECK(generate_block(app1, result_block)); + + // get the ID: + htlc_id_type htlc_id = result_block.transactions[result_block.transactions.size()-1] + .operation_results[0].get(); + bob_htlc_id_as_string = (std::string)(object_id_type)htlc_id; + BOOST_TEST_MESSAGE("Bob shares the HTLC ID with Alice. The HTLC ID is: " + bob_htlc_id_as_string); + } + + // Alice can now look over Bob's HTLC, to see if it is what was agreed to: + BOOST_TEST_MESSAGE("Alice retrieves the HTLC Object by ID to examine it."); + auto bob_htlc = con.wallet_api_ptr->get_htlc(bob_htlc_id_as_string); + BOOST_TEST_MESSAGE("The HTLC Object is: " + fc::json::to_pretty_string(bob_htlc)); + + // Alice likes what she sees, so uses her preimage to get her BOBCOIN + { + BOOST_TEST_MESSAGE("Alice uses her preimage to retrieve the BOBCOIN"); + con.wallet_api_ptr->htlc_redeem(bob_htlc_id_as_string, "alice", preimage_string, true); + BOOST_TEST_MESSAGE("The system is generating a block"); + BOOST_CHECK(generate_block(app1)); + } + + // Bob can look at Alice's history to see her preimage + { + BOOST_TEST_MESSAGE("Bob can look at the history of Alice to see the preimage"); + std::vector hist = con.wallet_api_ptr->get_account_history("alice", 1); + BOOST_CHECK( hist[0].description.find("with preimage \"4d792") != hist[0].description.npos); + } + + // Bob can also look at his own history to see Alice's preimage + { + BOOST_TEST_MESSAGE("Bob can look at his own history to see the preimage"); + std::vector hist = con.wallet_api_ptr->get_account_history("bob", 1); + BOOST_CHECK( hist[0].description.find("with preimage \"4d792") != hist[0].description.npos); + } + + // Bob can use the preimage to retrieve his BTS + { + BOOST_TEST_MESSAGE("Bob uses Alice's preimage to retrieve the BOBCOIN"); + con.wallet_api_ptr->htlc_redeem(alice_htlc_id_as_string, "bob", preimage_string, true); + BOOST_TEST_MESSAGE("The system is generating a block"); + BOOST_CHECK(generate_block(app1)); + } + + // test operation_printer + auto hist = con.wallet_api_ptr->get_account_history("alice", 10); + for(size_t i = 0; i < hist.size(); ++i) + { + auto obj = hist[i]; + std::stringstream ss; + ss << "Description: " << obj.description << "\n"; + auto str = ss.str(); + BOOST_TEST_MESSAGE( str ); + if (i == 3 || i == 4) + { + BOOST_CHECK( str.find("HASH160 620e4d5ba") != std::string::npos ); + } + } + con.wallet_api_ptr->lock(); + hist = con.wallet_api_ptr->get_account_history("alice", 10); + for(size_t i = 0; i < hist.size(); ++i) + { + auto obj = hist[i]; + std::stringstream ss; + ss << "Description: " << obj.description << "\n"; + auto str = ss.str(); + BOOST_TEST_MESSAGE( str ); + if (i == 3 || i == 4) + { + BOOST_CHECK( str.find("HASH160 620e4d5ba") != std::string::npos ); + } + } + con.wallet_api_ptr->unlock("supersecret"); + + // wait for everything to finish up + fc::usleep(fc::seconds(1)); + } catch( fc::exception& e ) { + edump((e.to_detail_string())); + throw; + } + app1->shutdown(); + app1.reset(); + // Intentional delay after app1->shutdown + std::cout << "cli_create_htlc conclusion: Intentional delay" << std::endl; + fc::usleep(fc::seconds(1)); +} diff --git a/tests/common/database_fixture.cpp b/tests/common/database_fixture.cpp index 211957ce2a..9c1354b06c 100644 --- a/tests/common/database_fixture.cpp +++ b/tests/common/database_fixture.cpp @@ -42,6 +42,7 @@ #include #include #include +#include #include @@ -174,6 +175,11 @@ database_fixture::database_fixture(const fc::time_point_sec &initial_timestamp) options.insert(std::make_pair("api-limit-get-limit-orders", boost::program_options::variable_value( (uint64_t)350, false))); } + if(current_test_name =="api_limit_get_limit_orders_by_account") + { + options.insert(std::make_pair("api-limit-get-limit-orders-by-account", boost::program_options::variable_value( + (uint64_t)150, false))); + } if(current_test_name =="api_limit_get_call_orders") { options.insert(std::make_pair("api-limit-get-call-orders", boost::program_options::variable_value( @@ -217,7 +223,7 @@ database_fixture::database_fixture(const fc::time_point_sec &initial_timestamp) if(current_test_name =="api_limit_lookup_vote_ids") { options.insert(std::make_pair("api-limit-lookup-vote-ids", boost::program_options::variable_value - ((uint64_t)3, false))); + ((uint64_t)2, false))); } if(current_test_name =="api_limit_get_account_limit_orders") { @@ -331,7 +337,8 @@ database_fixture::database_fixture(const fc::time_point_sec &initial_timestamp) } else if( current_test_name == "asset_in_collateral" || current_test_name == "htlc_database_api" - || current_suite_name == "database_api_tests" ) + || current_suite_name == "database_api_tests" + || current_suite_name == "api_limit_tests" ) { auto ahiplugin = app.register_plugin(); ahiplugin->plugin_set_app(&app); @@ -343,6 +350,7 @@ database_fixture::database_fixture(const fc::time_point_sec &initial_timestamp) current_test_name == "custom_operations_account_storage_list_test") { auto custom_operations_plugin = app.register_plugin(); custom_operations_plugin->plugin_set_app(&app); + options.insert(std::make_pair("custom-operations-start-block", boost::program_options::variable_value(uint32_t(1), false))); custom_operations_plugin->plugin_initialize(options); custom_operations_plugin->plugin_startup(); } @@ -452,7 +460,7 @@ bool database_fixture::validation_current_test_name_for_setting_api_limit( const vector valid_testcase {"api_limit_get_account_history_operations","api_limit_get_account_history" ,"api_limit_get_grouped_limit_orders","api_limit_get_relative_account_history" ,"api_limit_get_account_history_by_operations","api_limit_get_asset_holders" - ,"api_limit_get_key_references","api_limit_get_limit_orders" + ,"api_limit_get_key_references","api_limit_get_limit_orders","api_limit_get_limit_orders_by_account" ,"api_limit_get_call_orders","api_limit_get_settle_orders" ,"api_limit_get_order_book","api_limit_lookup_accounts" ,"api_limit_lookup_witness_accounts","api_limit_lookup_committee_member_accounts" @@ -521,6 +529,7 @@ void database_fixture::verify_asset_supplies( const database& db ) { const auto& bad = asset_obj.bitasset_data(db); total_balances[bad.options.short_backing_asset] += bad.settlement_fund; + total_balances[bad.options.short_backing_asset] += dasset_obj.accumulated_collateral_fees; } total_balances[asset_obj.id] += dasset_obj.confidential_supply.value; } @@ -1455,6 +1464,12 @@ flat_map< uint64_t, graphene::chain::fee_parameters > database_fixture::get_htlc extend_param.fee_per_day = 2 * GRAPHENE_BLOCKCHAIN_PRECISION; ret_val[((operation)htlc_extend_operation()).which()] = extend_param; + // set the transfer kb fee to something other than default, to verify we're looking + // at the correct fee + transfer_operation::fee_parameters_type transfer_param; + transfer_param.price_per_kbyte *= 2; + ret_val[ ((operation)transfer_operation()).which() ] = transfer_param; + return ret_val; } @@ -1468,14 +1483,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/elasticsearch/main.cpp b/tests/elasticsearch/main.cpp index 060f505041..5e3b0458f2 100644 --- a/tests/elasticsearch/main.cpp +++ b/tests/elasticsearch/main.cpp @@ -34,6 +34,12 @@ #define BOOST_TEST_MODULE Elastic Search Database Tests #include +#ifdef NDEBUG + #define ES_WAIT_TIME (fc::milliseconds(1000)) +#else + #define ES_WAIT_TIME (fc::milliseconds(3000)) +#endif + using namespace graphene::chain; using namespace graphene::chain::test; using namespace graphene::app; @@ -54,7 +60,7 @@ BOOST_AUTO_TEST_CASE(elasticsearch_account_history) { // delete all first auto delete_account_history = graphene::utilities::deleteAll(es); - fc::usleep(fc::milliseconds(1000)); // this is because index.refresh_interval, nothing to worry + fc::usleep(ES_WAIT_TIME); // this is because index.refresh_interval, nothing to worry if(delete_account_history) { // all records deleted @@ -64,7 +70,7 @@ BOOST_AUTO_TEST_CASE(elasticsearch_account_history) { auto bob = create_account("bob"); generate_block(); - fc::usleep(fc::milliseconds(1000)); + fc::usleep(ES_WAIT_TIME); // for later use //int asset_create_op_id = operation::tag::value; @@ -89,7 +95,7 @@ BOOST_AUTO_TEST_CASE(elasticsearch_account_history) { auto willie = create_account("willie"); generate_block(); - fc::usleep(fc::milliseconds(1000)); // index.refresh_interval + fc::usleep(ES_WAIT_TIME); // index.refresh_interval es.endpoint = es.index_prefix + "*/data/_count"; res = graphene::utilities::simpleQuery(es); @@ -104,7 +110,7 @@ BOOST_AUTO_TEST_CASE(elasticsearch_account_history) { transfer(account_id_type()(db), bob, asset(300)); generate_block(); - fc::usleep(fc::milliseconds(1000)); // index.refresh_interval + fc::usleep(ES_WAIT_TIME); // index.refresh_interval res = graphene::utilities::simpleQuery(es); j = fc::json::from_string(res); @@ -145,14 +151,14 @@ BOOST_AUTO_TEST_CASE(elasticsearch_objects) { auto delete_objects = graphene::utilities::deleteAll(es); generate_block(); - fc::usleep(fc::milliseconds(1000)); + fc::usleep(ES_WAIT_TIME); if(delete_objects) { // all records deleted // asset and bitasset create_bitasset("USD", account_id_type()); generate_block(); - fc::usleep(fc::milliseconds(1000)); + fc::usleep(ES_WAIT_TIME); string query = "{ \"query\" : { \"bool\" : { \"must\" : [{\"match_all\": {}}] } } }"; es.endpoint = es.index_prefix + "*/data/_count"; @@ -195,10 +201,10 @@ BOOST_AUTO_TEST_CASE(elasticsearch_suite) { es.elasticsearch_url = "http://localhost:9200/"; es.index_prefix = "bitshares-"; auto delete_account_history = graphene::utilities::deleteAll(es); - fc::usleep(fc::milliseconds(1000)); + fc::usleep(ES_WAIT_TIME); es.index_prefix = "objects-"; auto delete_objects = graphene::utilities::deleteAll(es); - fc::usleep(fc::milliseconds(1000)); + fc::usleep(ES_WAIT_TIME); if(delete_account_history && delete_objects) { // all records deleted @@ -224,7 +230,7 @@ BOOST_AUTO_TEST_CASE(elasticsearch_history_api) { auto delete_account_history = graphene::utilities::deleteAll(es); generate_block(); - fc::usleep(fc::milliseconds(1000)); + fc::usleep(ES_WAIT_TIME); if(delete_account_history) { @@ -237,7 +243,7 @@ BOOST_AUTO_TEST_CASE(elasticsearch_history_api) { create_bitasset("OIL", dan.id); // create op 6 generate_block(); - fc::usleep(fc::milliseconds(1000)); + fc::usleep(ES_WAIT_TIME); graphene::app::history_api hist_api(app); app.enable_plugin("elasticsearch"); @@ -506,7 +512,7 @@ BOOST_AUTO_TEST_CASE(elasticsearch_history_api) { create_account("alice"); generate_block(); - fc::usleep(fc::milliseconds(1000)); + fc::usleep(ES_WAIT_TIME); // f(C, 0, 4, 10) = { 7 } histories = hist_api.get_account_history("alice", operation_history_id_type(0), 4, operation_history_id_type(10)); diff --git a/tests/performance/market_fee_sharing_tests.cpp b/tests/performance/market_fee_sharing_tests.cpp index 9718bb0613..964eb0f172 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/api_limit_tests.cpp b/tests/tests/api_limit_tests.cpp new file mode 100644 index 0000000000..5c332322a8 --- /dev/null +++ b/tests/tests/api_limit_tests.cpp @@ -0,0 +1,524 @@ +/* + * Copyright (c) 2018 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 "../common/database_fixture.hpp" + +using namespace graphene::chain; +using namespace graphene::chain::test; + +BOOST_FIXTURE_TEST_SUITE(api_limit_tests, database_fixture) + +BOOST_AUTO_TEST_CASE( api_limit_get_key_references ){ + try{ + const int num_keys = 210; + const int num_keys1 = 2; + vector< private_key_type > numbered_private_keys; + vector< public_key_type > numbered_key_id; + numbered_private_keys.reserve( num_keys ); + + graphene::app::database_api db_api1( db, &( app.get_options() )); + BOOST_CHECK_THROW( db_api1.get_key_references(numbered_key_id), fc::exception ); + + graphene::app::application_options opt = app.get_options(); + opt.has_api_helper_indexes_plugin = true; + graphene::app::database_api db_api( db, &opt ); + + for( int i=0; i > final_result=db_api.get_key_references(numbered_key_id); + BOOST_REQUIRE_EQUAL( final_result.size(), 2u ); + numbered_private_keys.reserve( num_keys ); + for( int i=num_keys1; iapp.get_options())); + + const account_object& alice = create_account("alice"); + const account_object& bob = create_account("bob"); + const account_object& carl = create_account("carl"); + const account_object& dan = create_account("dan"); + const account_object& fred = create_account("fred"); + const account_object& henry = create_account("henry"); + const account_object& kevin = create_account("kevin"); + const account_object& laura = create_account("laura"); + const account_object& lucy = create_account("lucy"); + const account_object& martin = create_account("martin"); + const account_object& patty = create_account("patty"); + + vector accounts; + accounts.push_back(alice.name); + accounts.push_back(bob.name); + accounts.push_back(carl.name); + accounts.push_back(dan.name); + accounts.push_back(fred.name); + accounts.push_back(henry.name); + accounts.push_back(kevin.name); + accounts.push_back(laura.name); + accounts.push_back(lucy.name); + accounts.push_back(martin.name); + accounts.push_back(patty.name); + + GRAPHENE_CHECK_THROW(db_api.get_full_accounts(accounts, false), fc::exception); + + accounts.erase(accounts.begin()); + auto full_accounts = db_api.get_full_accounts(accounts, false); + BOOST_CHECK(full_accounts.size() == 10); + + // not an account + accounts.erase(accounts.begin()); + accounts.push_back("nosuchaccount"); + + // non existing accounts will be ignored in the results + full_accounts = db_api.get_full_accounts(accounts, false); + BOOST_CHECK(full_accounts.size() == 9); + + } catch (fc::exception& e) { + edump((e.to_detail_string())); + throw; + } +} + +BOOST_AUTO_TEST_CASE( api_limit_get_limit_orders ){ + try{ + graphene::app::database_api db_api( db, &( app.get_options() )); + //account_id_type() do 3 ops + create_bitasset("USD", account_id_type()); + create_account("dan"); + create_account("bob"); + asset_id_type bit_jmj_id = create_bitasset("JMJBIT").id; + generate_block(); + fc::usleep(fc::milliseconds(100)); + GRAPHENE_CHECK_THROW(db_api.get_limit_orders(std::string(static_cast(asset_id_type())), + std::string(static_cast(bit_jmj_id)), 370), fc::exception); + vector limit_orders =db_api.get_limit_orders(std::string( + static_cast(asset_id_type())), + std::string(static_cast(bit_jmj_id)), 340); + BOOST_REQUIRE_EQUAL( limit_orders.size(), 0u); + + }catch (fc::exception& e) { + edump((e.to_detail_string())); + throw; + } +} + +BOOST_AUTO_TEST_CASE( api_limit_get_limit_orders_by_account ){ + try{ + graphene::app::database_api db_api( db, &( app.get_options() )); + const auto& test = create_user_issued_asset("TESTASSET"); + create_sell_order( account_id_type(), asset(1,asset_id_type()), test.amount(1) ); + GRAPHENE_CHECK_THROW(db_api.get_limit_orders_by_account( + std::string(static_cast(account_id_type())), 160), fc::exception); + vector limit_orders =db_api.get_limit_orders_by_account( + std::string(static_cast(account_id_type())), 145); + BOOST_REQUIRE_EQUAL( limit_orders.size(), 1u); + + }catch (fc::exception& e) { + edump((e.to_detail_string())); + throw; + } +} + +BOOST_AUTO_TEST_CASE( api_limit_get_call_orders ){ + try{ + graphene::app::database_api db_api( db, &( app.get_options() )); + //account_id_type() do 3 ops + auto nathan_private_key = generate_private_key("nathan"); + account_id_type nathan_id = create_account("nathan", nathan_private_key.get_public_key()).id; + transfer(account_id_type(), nathan_id, asset(100)); + asset_id_type bitusd_id = create_bitasset( + "USDBIT", nathan_id, 100, disable_force_settle).id; + generate_block(); + fc::usleep(fc::milliseconds(100)); + BOOST_CHECK( bitusd_id(db).is_market_issued() ); + GRAPHENE_CHECK_THROW(db_api.get_call_orders(std::string(static_cast(bitusd_id)), + 370), fc::exception); + vector< call_order_object> call_order =db_api.get_call_orders(std::string( + static_cast(bitusd_id)), 340); + BOOST_REQUIRE_EQUAL( call_order.size(), 0u); + }catch (fc::exception& e) { + edump((e.to_detail_string())); + throw; + } +} +BOOST_AUTO_TEST_CASE( api_limit_get_settle_orders ){ + try{ + graphene::app::database_api db_api( db, &( app.get_options() )); + //account_id_type() do 3 ops + auto nathan_private_key = generate_private_key("nathan"); + account_id_type nathan_id = create_account("nathan", nathan_private_key.get_public_key()).id; + transfer(account_id_type(), nathan_id, asset(100)); + asset_id_type bitusd_id = create_bitasset( + "USDBIT", nathan_id, 100, disable_force_settle).id; + generate_block(); + fc::usleep(fc::milliseconds(100)); + GRAPHENE_CHECK_THROW(db_api.get_settle_orders( + std::string(static_cast(bitusd_id)), 370), fc::exception); + vector result =db_api.get_settle_orders( + std::string(static_cast(bitusd_id)), 340); + BOOST_REQUIRE_EQUAL( result.size(), 0u); + }catch (fc::exception& e) { + edump((e.to_detail_string())); + throw; + } +} +BOOST_AUTO_TEST_CASE( api_limit_get_order_book ){ + try{ + graphene::app::database_api db_api( db, &( app.get_options() )); + auto nathan_private_key = generate_private_key("nathan"); + auto dan_private_key = generate_private_key("dan"); + account_id_type nathan_id = create_account("nathan", nathan_private_key.get_public_key()).id; + account_id_type dan_id = create_account("dan", dan_private_key.get_public_key()).id; + transfer(account_id_type(), nathan_id, asset(100)); + transfer(account_id_type(), dan_id, asset(100)); + asset_id_type bitusd_id = create_bitasset( + "USDBIT", nathan_id, 100, disable_force_settle).id; + asset_id_type bitdan_id = create_bitasset( + "DANBIT", dan_id, 100, disable_force_settle).id; + generate_block(); + fc::usleep(fc::milliseconds(100)); + GRAPHENE_CHECK_THROW(db_api.get_order_book(std::string(static_cast(bitusd_id)), + std::string(static_cast(bitdan_id)),89), fc::exception); + graphene::app::order_book result =db_api.get_order_book(std::string( + static_cast(bitusd_id)), std::string(static_cast(bitdan_id)),78); + BOOST_REQUIRE_EQUAL( result.bids.size(), 0u); + }catch (fc::exception& e) { + edump((e.to_detail_string())); + throw; + } +} + +BOOST_AUTO_TEST_CASE( api_limit_lookup_accounts ) { + try{ + graphene::app::database_api db_api( db, &( app.get_options() )); + ACTOR(bob); + GRAPHENE_CHECK_THROW(db_api.lookup_accounts("bob",220), fc::exception); + map result =db_api.lookup_accounts("bob",190); + BOOST_REQUIRE_EQUAL( result.size(), 17u); + + } catch (fc::exception& e) { + edump((e.to_detail_string())); + throw; + } +} + +BOOST_AUTO_TEST_CASE( api_limit_lookup_witness_accounts ) { + try{ + graphene::app::database_api db_api( db, &( app.get_options() )); + ACTORS((bob)) ; + GRAPHENE_CHECK_THROW(db_api.lookup_witness_accounts("bob",220), fc::exception); + map result =db_api.lookup_witness_accounts("bob",190); + BOOST_REQUIRE_EQUAL( result.size(), 10u); + + } catch (fc::exception& e) { + edump((e.to_detail_string())); + throw; + } +} +BOOST_AUTO_TEST_CASE( api_limit_get_full_accounts2 ) { + + try { + graphene::app::database_api db_api(db, &(this->app.get_options())); + vector accounts; + for (int i=0; i<201; i++) { + std::string acct_name = "mytempacct" + std::to_string(i); + const account_object& account_name=create_account(acct_name); + accounts.push_back(account_name.name); + } + GRAPHENE_CHECK_THROW(db_api.get_full_accounts(accounts, false), fc::exception); + accounts.erase(accounts.begin()); + auto full_accounts = db_api.get_full_accounts(accounts, false); + BOOST_REQUIRE_EQUAL(full_accounts.size(), 200u); + } catch (fc::exception& e) { + edump((e.to_detail_string())); + throw; + } +} +BOOST_AUTO_TEST_CASE(api_limit_get_withdraw_permissions_by_recipient){ + try{ + graphene::app::database_api db_api( db, &app.get_options()); + ACTORS((bob)) ; + withdraw_permission_id_type withdraw_permission; + GRAPHENE_CHECK_THROW(db_api.get_withdraw_permissions_by_recipient( + "bob",withdraw_permission, 251), fc::exception); + vector result =db_api.get_withdraw_permissions_by_recipient( + "bob",withdraw_permission,250); + BOOST_REQUIRE_EQUAL( result.size(), 0u); + }catch (fc::exception& e) { + edump((e.to_detail_string())); + throw; + } +} +BOOST_AUTO_TEST_CASE(api_limit_get_withdraw_permissions_by_giver){ + try{ + graphene::app::database_api db_api( db, &app.get_options()); + ACTORS((bob)) ; + withdraw_permission_id_type withdraw_permission; + GRAPHENE_CHECK_THROW(db_api.get_withdraw_permissions_by_giver( + "bob",withdraw_permission, 251), fc::exception); + vector result =db_api.get_withdraw_permissions_by_giver( + "bob",withdraw_permission,250); + BOOST_REQUIRE_EQUAL( result.size(), 0u); + }catch (fc::exception& e) { + edump((e.to_detail_string())); + throw; + } +} +BOOST_AUTO_TEST_CASE(api_limit_get_trade_history_by_sequence){ + try{ + app.enable_plugin("market_history"); + graphene::app::application_options opt=app.get_options(); + opt.has_market_history_plugin = true; + graphene::app::database_api db_api( db, &opt); + const auto& bitusd = create_bitasset("USDBIT"); + asset_id_type asset_1, asset_2; + asset_1 = bitusd.id; + asset_2 = asset_id_type(); + GRAPHENE_CHECK_THROW(db_api.get_trade_history_by_sequence( + std::string( static_cast(asset_1)), + std::string( static_cast(asset_2)), + 0,fc::time_point_sec(), 251), fc::exception); + vector result =db_api.get_trade_history_by_sequence( + std::string( static_cast(asset_1)), + std::string( static_cast(asset_2)), + 0,fc::time_point_sec(),250); + BOOST_REQUIRE_EQUAL( result.size(), 0u); + }catch (fc::exception& e) { + edump((e.to_detail_string())); + throw; + } +} + +BOOST_AUTO_TEST_CASE(api_limit_get_trade_history){ + try{ + app.enable_plugin("market_history"); + graphene::app::application_options opt=app.get_options(); + opt.has_market_history_plugin = true; + graphene::app::database_api db_api( db, &opt); + const auto& bitusd = create_bitasset("USDBIT"); + asset_id_type asset_1, asset_2; + asset_1 = bitusd.id; + asset_2 = asset_id_type(); + GRAPHENE_CHECK_THROW(db_api.get_trade_history( + std::string( static_cast(asset_1)), + std::string( static_cast(asset_2)), + fc::time_point_sec(),fc::time_point_sec(), + 251), fc::exception); + vector result =db_api.get_trade_history( + std::string( static_cast(asset_1)), + std::string( static_cast(asset_2)), + fc::time_point_sec(),fc::time_point_sec(),250); + BOOST_REQUIRE_EQUAL( result.size(), 0u); + }catch (fc::exception& e) { + edump((e.to_detail_string())); + throw; + } +} +BOOST_AUTO_TEST_CASE(api_limit_get_top_markets){ + try{ + app.enable_plugin("market_history"); + graphene::app::application_options opt=app.get_options(); + opt.has_market_history_plugin = true; + graphene::app::database_api db_api( db, &opt); + const auto& bitusd = create_bitasset("USDBIT"); + asset_id_type asset_1, asset_2; + asset_1 = bitusd.id; + asset_2 = asset_id_type(); + GRAPHENE_CHECK_THROW(db_api.get_top_markets(251), fc::exception); + vector result =db_api.get_top_markets(250); + BOOST_REQUIRE_EQUAL( result.size(), 0u); + }catch (fc::exception& e) { + edump((e.to_detail_string())); + throw; + } +} +BOOST_AUTO_TEST_CASE(api_limit_get_collateral_bids) { + try { + graphene::app::database_api db_api( db, &( app.get_options() )); + + int64_t init_balance = 10000; + ///account_id_type borrower, borrower2, feedproducer; + asset_id_type swan, back; + ACTORS((borrower) (borrower2) (feedproducer)) ; + const auto& bitusd = create_bitasset("USDBIT", feedproducer_id); + swan = bitusd.id; + back = asset_id_type(); + update_feed_producers(swan(db), {feedproducer_id}); + transfer(committee_account, borrower_id, asset(init_balance)); + transfer(committee_account, borrower2_id, asset(init_balance)); + + generate_blocks( HARDFORK_CORE_216_TIME ); + generate_block(); + + price_feed feed; + feed.maintenance_collateral_ratio = 1750; // need to set this explicitly, testnet has a different default + feed.settlement_price = swan(db).amount(1) / back(db).amount(1); + publish_feed(swan(db), feedproducer_id(db), feed); + // start out with 2:1 collateral + borrow(borrower_id(db), swan(db).amount(10), back(db).amount(2*10)); + borrow(borrower2_id(db), swan(db).amount(10), back(db).amount(4*10)); + //feed 1: 2 + feed.settlement_price = swan(db).amount(1) / back(db).amount(2); + publish_feed(swan(db), feedproducer_id(db), feed); + + // this sell order is designed to trigger a black swan + + create_sell_order( borrower2_id(db), swan(db).amount(1), back(db).amount(3) ); + BOOST_CHECK( swan(db).bitasset_data(db).has_settlement() ); + //making 3 collateral bids + for (int i=0; i<3; i++) { + + std::string acct_name = "mytempacct" + std::to_string(i); + account_id_type account_id=create_account(acct_name).id; + transfer(committee_account, account_id, asset(init_balance)); + bid_collateral(account_id(db), back(db).amount(10), swan(db).amount(1)); + } + auto swan_symbol = swan(db).symbol; + + + //validating normal case; total_bids =3 ; result_bids=3 + vector result_bids = db_api.get_collateral_bids(swan_symbol, 250, 0); + BOOST_CHECK_EQUAL( 3u, result_bids.size() ); + + //verify skip /// inefficient code test + //skip < total_bids; skip=1; total_bids =3 ; result_bids=2 + result_bids = db_api.get_collateral_bids(swan_symbol, 250, 1); + BOOST_CHECK_EQUAL( 2u, result_bids.size() ); + //skip= total_bids; skip=3; total_bids =3 ; result_bids=0 + result_bids = db_api.get_collateral_bids(swan_symbol, 250, 3); + BOOST_CHECK_EQUAL( 0u, result_bids.size() ); + //skip> total_bids; skip=4; total_bids =3 ; result_bids=0 + result_bids = db_api.get_collateral_bids(swan_symbol, 250, 4); + BOOST_CHECK_EQUAL( 0u, result_bids.size() ); + + //verify limit // inefficient code test + //limit= api_limit + for (int i=3; i<255; i++) { + std::string acct_name = "mytempacct" + std::to_string(i); + account_id_type account_id=create_account(acct_name).id; + transfer(committee_account, account_id, asset(init_balance)); + bid_collateral(account_id(db), back(db).amount(10), swan(db).amount(1)); + } + result_bids=db_api.get_collateral_bids(swan_symbol, 250, 0); + BOOST_CHECK_EQUAL( 250u, result_bids.size() ); + //limit> api_limit throw error + GRAPHENE_CHECK_THROW(db_api.get_collateral_bids(swan_symbol, 253, 3), fc::exception); + + } + catch (fc::exception& e) { + edump((e.to_detail_string())); + throw; + } +} +BOOST_AUTO_TEST_CASE(api_limit_get_account_limit_orders) { + try { + graphene::app::database_api db_api( db, &( app.get_options() )); + ACTORS((seller)); + const auto &bitcny = create_bitasset("CNY"); + const auto &core = asset_id_type()(db); + + int64_t init_balance(10000000); + transfer(committee_account, seller_id, asset(init_balance)); + + /// Create versatile orders + for (size_t i = 0; i < 250; ++i) { + BOOST_CHECK(create_sell_order(seller, core.amount(100), bitcny.amount(250+i))); + } + + + std::vector results=db_api.get_account_limit_orders(seller.name, GRAPHENE_SYMBOL, "CNY",250); + BOOST_REQUIRE_EQUAL( results.size(), 250u); + GRAPHENE_CHECK_THROW( db_api.get_account_limit_orders(seller.name, GRAPHENE_SYMBOL, "CNY",251), fc::exception); + + } + catch (fc::exception& e) { + edump((e.to_detail_string())); + throw; + } +} +BOOST_AUTO_TEST_CASE( api_limit_lookup_vote_ids ) { + try{ + graphene::app::database_api db_api( db, &( app.get_options() )); + ACTORS( (connie)(whitney)(wolverine) ); + fund(connie); + upgrade_to_lifetime_member(connie); + fund(whitney); + upgrade_to_lifetime_member(whitney); + fund(wolverine); + upgrade_to_lifetime_member(wolverine); + const auto& committee = create_committee_member( connie ); + const auto& witness = create_witness( whitney ); + const auto& worker = create_worker( wolverine_id ); + std::vector votes; + votes.push_back( committee.vote_id ); + votes.push_back( witness.vote_id ); + const auto results = db_api.lookup_vote_ids( votes ); + BOOST_REQUIRE_EQUAL( results.size(), 2u); + votes.push_back( worker.vote_for ); + GRAPHENE_CHECK_THROW(db_api.lookup_vote_ids(votes), fc::exception); + + } catch (fc::exception& e) { + edump((e.to_detail_string())); + throw; + } +} + +BOOST_AUTO_TEST_CASE( api_limit_lookup_committee_member_accounts ) { + try{ + graphene::app::database_api db_api( db, &( app.get_options() )); + ACTORS((bob)); + GRAPHENE_CHECK_THROW(db_api.lookup_committee_member_accounts("bob",220), fc::exception); + std::map result =db_api.lookup_committee_member_accounts("bob",190); + BOOST_REQUIRE_EQUAL( result.size(), 10u); + + } catch (fc::exception& e) { + edump((e.to_detail_string())); + throw; + } +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/tests/tests/authority_tests.cpp b/tests/tests/authority_tests.cpp index 4ffa93460e..4595d41c40 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 { @@ -320,7 +327,7 @@ BOOST_AUTO_TEST_CASE( proposed_single_account ) { vector other; flat_set active_set, owner_set; - operation_get_required_authorities(op,active_set,owner_set,other); + operation_get_required_authorities(op, active_set, owner_set, other, false); BOOST_CHECK_EQUAL(active_set.size(), 1lu); BOOST_CHECK_EQUAL(owner_set.size(), 0lu); BOOST_CHECK_EQUAL(other.size(), 0lu); @@ -328,7 +335,7 @@ BOOST_AUTO_TEST_CASE( proposed_single_account ) active_set.clear(); other.clear(); - operation_get_required_authorities(op.proposed_ops.front().op,active_set,owner_set,other); + operation_get_required_authorities(op.proposed_ops.front().op, active_set, owner_set, other, false); BOOST_CHECK_EQUAL(active_set.size(), 1lu); BOOST_CHECK_EQUAL(owner_set.size(), 0lu); BOOST_CHECK_EQUAL(other.size(), 0lu); @@ -1052,7 +1059,7 @@ BOOST_FIXTURE_TEST_CASE( bogus_signature, database_fixture ) flat_set active_set, owner_set; vector others; - trx.get_required_authorities( active_set, owner_set, others ); + trx.get_required_authorities(active_set, owner_set, others, false); PUSH_TX( db, trx, skip ); @@ -1192,10 +1199,10 @@ BOOST_FIXTURE_TEST_CASE( get_required_signatures_test, database_fixture ) ) -> bool { //wdump( (tx)(available_keys) ); - set result_set = tx.get_required_signatures( db.get_chain_id(), available_keys, - get_active, get_owner, false ); - set result_set2 = tx.get_required_signatures( db.get_chain_id(), available_keys, - get_active, get_owner, true ); + set result_set = tx.get_required_signatures(db.get_chain_id(), available_keys, + get_active, get_owner, false, false); + set result_set2 = tx.get_required_signatures(db.get_chain_id(), available_keys, + get_active, get_owner, true, false); //wdump( (result_set)(result_set2)(ref_set) ); return result_set == ref_set && result_set2 == ref_set; } ; @@ -1309,10 +1316,10 @@ BOOST_FIXTURE_TEST_CASE( nonminimal_sig_test, database_fixture ) ) -> bool { //wdump( (tx)(available_keys) ); - set result_set = tx.get_required_signatures( db.get_chain_id(), available_keys, - get_active, get_owner, false ); - set result_set2 = tx.get_required_signatures( db.get_chain_id(), available_keys, - get_active, get_owner, true ); + set result_set = tx.get_required_signatures(db.get_chain_id(), available_keys, + get_active, get_owner, false, false); + set result_set2 = tx.get_required_signatures(db.get_chain_id(), available_keys, + get_active, get_owner, true, false); //wdump( (result_set)(result_set2)(ref_set) ); return result_set == ref_set && result_set2 == ref_set; } ; @@ -1324,10 +1331,13 @@ BOOST_FIXTURE_TEST_CASE( nonminimal_sig_test, database_fixture ) ) -> bool { //wdump( (tx)(available_keys) ); - set result_set = tx.minimize_required_signatures( db.get_chain_id(), available_keys, - get_active, get_owner, false ); - set result_set2 = tx.minimize_required_signatures( db.get_chain_id(), available_keys, - get_active, get_owner, true ); + auto get_custom = make_get_custom(db); + set result_set = tx.minimize_required_signatures(db.get_chain_id(), available_keys, + 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, 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 ), fc::exception ); - GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, true ), 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 ); - tx.verify_authority( db.get_chain_id(), get_active, get_owner, true ); + 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) { @@ -1432,8 +1444,8 @@ BOOST_FIXTURE_TEST_CASE( parent_owner_test, database_fixture ) ) -> bool { //wdump( (tx)(available_keys) ); - set result_set = tx.get_required_signatures( db.get_chain_id(), available_keys, - get_active, get_owner, after_hf_584 ); + set result_set = tx.get_required_signatures(db.get_chain_id(), available_keys, + get_active, get_owner, after_hf_584, false); //wdump( (result_set)(ref_set) ); return result_set == ref_set; } ; @@ -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 ), 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 ); + 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 ); + 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 ); + 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 ); + 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 ); + 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 ); + 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 ); + 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 ), 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 ); + 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 ), 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 ); + 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 ), 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 ); + 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 ); + 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 ); + 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 ), 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 ); + 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 ); + 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 ); + 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 ), 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 ); + 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 ), 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 ); + 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 ), 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 ); + 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 ); + 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 ); + tx.verify_authority( db.get_chain_id(), get_active, get_owner, make_get_custom(db), true, false ); tx.clear_signatures(); // proposal tests @@ -1858,6 +1879,128 @@ BOOST_FIXTURE_TEST_CASE( parent_owner_test, database_fixture ) } } +BOOST_AUTO_TEST_CASE( custom_operation_required_auths_before_fork ) { + try { + ACTORS((alice)(bob)); + fund(alice, asset(10000000)); + enable_fees(); + + // Unable to test custom_operation required auths before fork if hardfork already passed + BOOST_REQUIRE(db.head_block_time() < HARDFORK_CORE_210_TIME); + + signed_transaction trx; + custom_operation op; + op.payer = alice_id; + op.required_auths.insert(bob_id); + op.fee = op.calculate_fee(db.current_fee_schedule().get()); + trx.operations.emplace_back(op); + trx.set_expiration(db.head_block_time() + 30); + sign(trx, alice_private_key); + // Op requires bob's authorization, but only alice signed. We're before the fork, so this should work anyways. + db.push_transaction(trx); + + // Now try the same thing, but with a proposal + proposal_create_operation pcop; + pcop.fee_paying_account = alice_id; + pcop.proposed_ops = {op_wrapper(op)}; + pcop.expiration_time = db.head_block_time() + 10; + pcop.fee = pcop.calculate_fee(db.current_fee_schedule().get()); + trx.operations = {pcop}; + trx.signatures.clear(); + sign(trx, alice_private_key); + proposal_id_type pid = db.push_transaction(trx).operation_results[0].get(); + + // Check bob is not listed as a required approver + BOOST_REQUIRE_EQUAL(pid(db).required_active_approvals.count(bob_id), 0); + + // Add alice's approval + proposal_update_operation puop; + puop.fee_paying_account = alice_id; + puop.proposal = pid; + puop.active_approvals_to_add = {alice_id}; + puop.fee = puop.calculate_fee(db.current_fee_schedule().get()); + trx.operations = {puop}; + trx.signatures.clear(); + sign(trx, alice_private_key); + db.push_transaction(trx); + + // The proposal should have processed. Check it's not still in the database + BOOST_REQUIRE(db.find(pid) == nullptr); + } catch (fc::exception& e) { + edump((e.to_detail_string())); + throw; + } +} + +BOOST_AUTO_TEST_CASE( custom_operation_required_auths_after_fork ) { + try { + ACTORS((alice)(bob)); + fund(alice, asset(10000000)); + + if (db.head_block_time() < HARDFORK_CORE_210_TIME) + generate_blocks(HARDFORK_CORE_210_TIME + 10); + + enable_fees(); + + signed_transaction trx; + custom_operation op; + op.payer = alice_id; + op.required_auths.insert(bob_id); + op.fee = op.calculate_fee(db.current_fee_schedule().get()); + trx.operations.emplace_back(op); + trx.set_expiration(db.head_block_time() + 30); + sign(trx, alice_private_key); + // Op require's bob's authorization, but only alice signed. This should throw. + GRAPHENE_REQUIRE_THROW(db.push_transaction(trx), tx_missing_active_auth); + sign(trx, bob_private_key); + // Now that bob has signed, it should work. + PUSH_TX(db, trx); + + // Now try the same thing, but with a proposal + proposal_create_operation pcop; + pcop.fee_paying_account = alice_id; + pcop.proposed_ops = {op_wrapper(op)}; + pcop.expiration_time = db.head_block_time() + 10; + pcop.fee = pcop.calculate_fee(db.current_fee_schedule().get()); + trx.operations = {pcop}; + trx.signatures.clear(); + sign(trx, alice_private_key); + proposal_id_type pid = db.push_transaction(trx).operation_results[0].get(); + + // Check bob is listed as a required approver + BOOST_REQUIRE_EQUAL(pid(db).required_active_approvals.count(bob_id), 1); + + // Add alice's approval + proposal_update_operation puop; + puop.fee_paying_account = alice_id; + puop.proposal = pid; + puop.active_approvals_to_add = {alice_id}; + puop.fee = puop.calculate_fee(db.current_fee_schedule().get()); + trx.operations = {puop}; + trx.signatures.clear(); + sign(trx, alice_private_key); + db.push_transaction(trx); + + // The proposal should not have processed without bob's approval. + // Check it's still in the database + BOOST_REQUIRE_EQUAL(pid(db).required_active_approvals.count(bob_id), 1); + + // Now add bob's approval + puop.active_approvals_to_add = {bob_id}; + trx.operations = {puop}; + trx.signatures.clear(); + sign(trx, alice_private_key); // Alice still pays fee + sign(trx, bob_private_key); + db.push_transaction(trx); + + // Now the proposal should have processed and been removed from the database + BOOST_REQUIRE(db.find(pid) == nullptr); + } catch (fc::exception& e) { + edump((e.to_detail_string())); + throw; + } +} + BOOST_FIXTURE_TEST_CASE( owner_delegation_test, database_fixture ) { try { ACTORS( (alice)(bob) ); @@ -1950,29 +2093,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 ), + 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 ), + 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 ), + 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 ), + 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 ); - tx.verify_authority( db.get_chain_id(), get_active, get_owner, true ); + 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 ); - tx.verify_authority( db.get_chain_id(), get_active, get_owner, true ); + 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(); @@ -1981,29 +2128,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 ), + 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 ), + 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 ); - tx.verify_authority( db.get_chain_id(), get_active, get_owner, true ); + 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 ); - tx.verify_authority( db.get_chain_id(), get_active, get_owner, true ); + 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 ), + 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 ), + 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/bsip85_tests.cpp b/tests/tests/bsip85_tests.cpp new file mode 100644 index 0000000000..88eaab71e0 --- /dev/null +++ b/tests/tests/bsip85_tests.cpp @@ -0,0 +1,256 @@ +/* + * Copyright (c) 2020 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 "../common/database_fixture.hpp" + +#include +#include +#include + +#include + +using namespace graphene::chain; +using namespace graphene::chain::test; + +BOOST_FIXTURE_TEST_SUITE( bsip85_tests, database_fixture ) + +BOOST_AUTO_TEST_CASE( hardfork_time_test ) +{ try { + + { + // The maker fee discount percent is 0 by default + BOOST_CHECK_EQUAL( db.get_global_properties().parameters.get_maker_fee_discount_percent(), 0 ); + + // Try to set new committee parameter before hardfork + proposal_create_operation cop = proposal_create_operation::committee_proposal( + db.get_global_properties().parameters, db.head_block_time() ); + cop.fee_paying_account = GRAPHENE_TEMP_ACCOUNT; + cop.expiration_time = db.head_block_time() + *cop.review_period_seconds + 10; + committee_member_update_global_parameters_operation cmuop; + cmuop.new_parameters.extensions.value.maker_fee_discount_percent = 1; + cop.proposed_ops.emplace_back( cmuop ); + trx.operations.push_back( cop ); + + // It should fail + GRAPHENE_CHECK_THROW( PUSH_TX(db, trx, ~0), fc::exception ); + trx.clear(); + + // The percent should still be 0 + BOOST_CHECK_EQUAL( db.get_global_properties().parameters.get_maker_fee_discount_percent(), 0 ); + } + + // Pass the hardfork + generate_blocks( HARDFORK_BSIP_85_TIME ); + set_expiration( db, trx ); + + { + // The maker fee discount percent is still 0 + BOOST_CHECK_EQUAL( db.get_global_properties().parameters.get_maker_fee_discount_percent(), 0 ); + + // Try to set new committee parameter after hardfork + proposal_create_operation cop = proposal_create_operation::committee_proposal( + db.get_global_properties().parameters, db.head_block_time() ); + cop.fee_paying_account = GRAPHENE_TEMP_ACCOUNT; + cop.expiration_time = db.head_block_time() + *cop.review_period_seconds + 10; + committee_member_update_global_parameters_operation cmuop; + cmuop.new_parameters.extensions.value.maker_fee_discount_percent = 10001; // 100.01% + cop.proposed_ops.emplace_back(cmuop); + trx.operations.push_back(cop); + + // Should fail since the value is too big + GRAPHENE_CHECK_THROW( PUSH_TX(db, trx, ~0), fc::exception ); + // The maker fee discount percent is still 0 + BOOST_CHECK_EQUAL( db.get_global_properties().parameters.get_maker_fee_discount_percent(), 0 ); + + trx.operations.clear(); + cop.proposed_ops.clear(); + cmuop.new_parameters.extensions.value.maker_fee_discount_percent = 1123; // 11.23% + cop.proposed_ops.emplace_back(cmuop); + trx.operations.push_back(cop); + + // Should succeed + processed_transaction ptx = PUSH_TX(db, trx, ~0); + trx.operations.clear(); + proposal_id_type prop_id = ptx.operation_results[0].get(); + + // The maker fee discount percent is still 0 + BOOST_CHECK_EQUAL( db.get_global_properties().parameters.get_maker_fee_discount_percent(), 0 ); + + // Approve the proposal + proposal_update_operation uop; + uop.fee_paying_account = GRAPHENE_TEMP_ACCOUNT; + uop.active_approvals_to_add = { get_account("init0").get_id(), get_account("init1").get_id(), + get_account("init2").get_id(), get_account("init3").get_id(), + get_account("init4").get_id(), get_account("init5").get_id(), + get_account("init6").get_id(), get_account("init7").get_id() }; + trx.operations.push_back(uop); + PUSH_TX(db, trx, ~0); + + // The maker fee discount percent is still 0 + BOOST_CHECK_EQUAL( db.get_global_properties().parameters.get_maker_fee_discount_percent(), 0 ); + + generate_blocks( prop_id( db ).expiration_time + 5 ); + generate_blocks( db.get_dynamic_global_properties().next_maintenance_time ); + generate_block(); + + // The maker fee discount percent should have changed + BOOST_CHECK_EQUAL( db.get_global_properties().parameters.get_maker_fee_discount_percent(), 1123 ); + + } +} FC_LOG_AND_RETHROW() } + +BOOST_AUTO_TEST_CASE( bsip85_maker_fee_discount_test ) +{ + try + { + ACTORS((alice)(bob)(izzy)); + + int64_t alice_b0 = 1000000, bob_b0 = 1000000; + int64_t pool_0 = 1000000, accum_0 = 0; + + transfer( account_id_type(), alice_id, asset(alice_b0) ); + transfer( account_id_type(), bob_id, asset(bob_b0) ); + + asset_id_type core_id = asset_id_type(); + int64_t cer_core_amount = 1801; + int64_t cer_usd_amount = 31; + price tmp_cer( asset( cer_core_amount ), asset( cer_usd_amount, asset_id_type(1) ) ); + const auto& usd_obj = create_user_issued_asset( "IZZYUSD", izzy_id(db), charge_market_fee, tmp_cer ); + asset_id_type usd_id = usd_obj.id; + issue_uia( alice_id, asset( alice_b0, usd_id ) ); + issue_uia( bob_id, asset( bob_b0, usd_id ) ); + + fund_fee_pool( committee_account( db ), usd_obj, pool_0 ); + + // If pay fee in CORE + int64_t order_create_fee = 547; + int64_t order_maker_refund = 61; // 547 * 11.23% = 61.4281 + + // If pay fee in USD + int64_t usd_create_fee = order_create_fee * cer_usd_amount / cer_core_amount; + if( usd_create_fee * cer_core_amount != order_create_fee * cer_usd_amount ) usd_create_fee += 1; + int64_t usd_maker_refund = usd_create_fee * 1123 / 10000; + // amount paid by fee pool + int64_t core_create_fee = usd_create_fee * cer_core_amount / cer_usd_amount; + int64_t core_maker_refund = usd_maker_refund == 0 ? 0 : core_create_fee * 1123 / 10000; + + fee_parameters::flat_set_type new_fees; + limit_order_create_operation::fee_parameters_type create_fee_params; + create_fee_params.fee = order_create_fee; + new_fees.insert( create_fee_params ); + + // Pass BSIP 85 HF time + // Note: no test case for the behavior before the HF since it's covered by other test cases + INVOKE( hardfork_time_test ); + set_expiration( db, trx ); + + // enable_fees() and change_fees() modifies DB directly, and results will be overwritten by block generation + // so we have to do it every time we stop generating/popping blocks and start doing tx's + enable_fees(); + change_fees( new_fees ); + + { + // prepare params + time_point_sec max_exp = time_point_sec::maximum(); + price cer = usd_id( db ).options.core_exchange_rate; + const auto* usd_stat = &usd_id( db ).dynamic_asset_data_id( db ); + + // balance data + int64_t alice_bc = alice_b0, bob_bc = bob_b0; // core balance + int64_t alice_bu = alice_b0, bob_bu = bob_b0; // usd balance + int64_t pool_b = pool_0, accum_b = accum_0; + + // Check order fill + BOOST_TEST_MESSAGE( "Creating ao1, then be filled by bo1" ); + // pays fee in core + const limit_order_object* ao1 = create_sell_order( alice_id, asset(1000), asset(200, usd_id) ); + const limit_order_id_type ao1id = ao1->id; + // pays fee in usd + const limit_order_object* bo1 = create_sell_order( bob_id, asset(200, usd_id), asset(1000), max_exp, cer ); + + BOOST_CHECK( db.find( ao1id ) == nullptr ); + BOOST_CHECK( bo1 == nullptr ); + + // data after order created + alice_bc -= 1000; // amount for sale + alice_bc -= order_create_fee; // fee + bob_bu -= 200; // amount for sale + bob_bu -= usd_create_fee; // fee + pool_b -= core_create_fee; // fee pool + accum_b += 0; + + // data after order filled + alice_bu += 200; // bob pays + alice_bc += order_maker_refund; // maker fee refund + bob_bc += 1000; // alice pays + accum_b += usd_create_fee; // bo1 paid fee, was taker, no refund + pool_b += 0; // no change + + BOOST_CHECK_EQUAL( get_balance( alice_id, core_id ), alice_bc ); + BOOST_CHECK_EQUAL( get_balance( alice_id, usd_id ), alice_bu ); + BOOST_CHECK_EQUAL( get_balance( bob_id, core_id ), bob_bc ); + BOOST_CHECK_EQUAL( get_balance( bob_id, usd_id ), bob_bu ); + BOOST_CHECK_EQUAL( usd_stat->fee_pool.value, pool_b ); + BOOST_CHECK_EQUAL( usd_stat->accumulated_fees.value, accum_b ); + + // Check partial fill + BOOST_TEST_MESSAGE( "Creating ao2, then be partially filled by bo2" ); + // pays fee in usd + const limit_order_object* ao2 = create_sell_order( alice_id, asset(1000), asset(200, usd_id), max_exp, cer ); + const limit_order_id_type ao2id = ao2->id; + // pays fee in core + const limit_order_object* bo2 = create_sell_order( bob_id, asset(100, usd_id), asset(500) ); + + BOOST_CHECK( db.find( ao2id ) != nullptr ); + BOOST_CHECK( bo2 == nullptr ); + + // data after order created + alice_bc -= 1000; // amount to sell + alice_bu -= usd_create_fee; // fee + pool_b -= core_create_fee; // fee pool + accum_b += 0; + bob_bc -= order_create_fee; // fee + bob_bu -= 100; // amount to sell + + // data after order filled + alice_bu += 100; // bob pays + alice_bu += usd_maker_refund; // maker fee refund + bob_bc += 500; + accum_b += usd_create_fee - usd_maker_refund; // ao2 paid fee deduct maker refund + pool_b += core_maker_refund; // ao2 maker refund + + BOOST_CHECK_EQUAL( get_balance( alice_id, core_id ), alice_bc ); + BOOST_CHECK_EQUAL( get_balance( alice_id, usd_id ), alice_bu ); + BOOST_CHECK_EQUAL( get_balance( bob_id, core_id ), bob_bc ); + BOOST_CHECK_EQUAL( get_balance( bob_id, usd_id ), bob_bu ); + BOOST_CHECK_EQUAL( usd_stat->fee_pool.value, pool_b ); + BOOST_CHECK_EQUAL( usd_stat->accumulated_fees.value, accum_b ); + + } + } + FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_SUITE_END() + diff --git a/tests/tests/bsip86_tests.cpp b/tests/tests/bsip86_tests.cpp new file mode 100644 index 0000000000..739720e1ba --- /dev/null +++ b/tests/tests/bsip86_tests.cpp @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2020 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 "../common/database_fixture.hpp" + +#include +#include +#include + +#include + +using namespace graphene::chain; +using namespace graphene::chain::test; + +BOOST_FIXTURE_TEST_SUITE( bsip86_tests, database_fixture ) + +BOOST_AUTO_TEST_CASE( hardfork_time_test ) +{ try { + + { + // The network fee percent is 0 by default + BOOST_CHECK_EQUAL( db.get_global_properties().parameters.get_market_fee_network_percent(), 0 ); + + // Try to set new committee parameter before hardfork + proposal_create_operation cop = proposal_create_operation::committee_proposal( + db.get_global_properties().parameters, db.head_block_time() ); + cop.fee_paying_account = GRAPHENE_TEMP_ACCOUNT; + cop.expiration_time = db.head_block_time() + *cop.review_period_seconds + 10; + committee_member_update_global_parameters_operation cmuop; + cmuop.new_parameters.extensions.value.market_fee_network_percent = 1; + cop.proposed_ops.emplace_back( cmuop ); + trx.operations.push_back( cop ); + + // It should fail + GRAPHENE_CHECK_THROW( PUSH_TX(db, trx, ~0), fc::exception ); + trx.clear(); + + // The percent should still be 0 + BOOST_CHECK_EQUAL( db.get_global_properties().parameters.get_market_fee_network_percent(), 0 ); + } + + // Pass the hardfork + generate_blocks( HARDFORK_BSIP_86_TIME ); + set_expiration( db, trx ); + + { + // The network fee percent is still 0 + BOOST_CHECK_EQUAL( db.get_global_properties().parameters.get_market_fee_network_percent(), 0 ); + + // Try to set new committee parameter after hardfork + proposal_create_operation cop = proposal_create_operation::committee_proposal( + db.get_global_properties().parameters, db.head_block_time() ); + cop.fee_paying_account = GRAPHENE_TEMP_ACCOUNT; + cop.expiration_time = db.head_block_time() + *cop.review_period_seconds + 10; + committee_member_update_global_parameters_operation cmuop; + cmuop.new_parameters.extensions.value.market_fee_network_percent = 3001; // 30.01% + cop.proposed_ops.emplace_back(cmuop); + trx.operations.push_back(cop); + + // Should fail since the value is too big + GRAPHENE_CHECK_THROW( PUSH_TX(db, trx, ~0), fc::exception ); + // The network fee percent is still 0 + BOOST_CHECK_EQUAL( db.get_global_properties().parameters.get_market_fee_network_percent(), 0 ); + + trx.operations.clear(); + cop.proposed_ops.clear(); + cmuop.new_parameters.extensions.value.market_fee_network_percent = 1123; // 11.23% + cop.proposed_ops.emplace_back(cmuop); + trx.operations.push_back(cop); + + // Should succeed + processed_transaction ptx = PUSH_TX(db, trx, ~0); + trx.operations.clear(); + proposal_id_type prop_id = ptx.operation_results[0].get(); + + // The network fee percent is still 0 + BOOST_CHECK_EQUAL( db.get_global_properties().parameters.get_market_fee_network_percent(), 0 ); + + // Approve the proposal + proposal_update_operation uop; + uop.fee_paying_account = GRAPHENE_TEMP_ACCOUNT; + uop.active_approvals_to_add = { get_account("init0").get_id(), get_account("init1").get_id(), + get_account("init2").get_id(), get_account("init3").get_id(), + get_account("init4").get_id(), get_account("init5").get_id(), + get_account("init6").get_id(), get_account("init7").get_id() }; + trx.operations.push_back(uop); + PUSH_TX(db, trx, ~0); + + // The network fee percent is still 0 + BOOST_CHECK_EQUAL( db.get_global_properties().parameters.get_market_fee_network_percent(), 0 ); + + generate_blocks( prop_id( db ).expiration_time + 5 ); + generate_blocks( db.get_dynamic_global_properties().next_maintenance_time ); + generate_block(); + + // The network fee percent should have changed + BOOST_CHECK_EQUAL( db.get_global_properties().parameters.get_market_fee_network_percent(), 1123 ); + + } +} FC_LOG_AND_RETHROW() } + +BOOST_AUTO_TEST_CASE( fee_sharing_test ) +{ try { + ACTORS((alice)(bob)); + + uint16_t market_fee_percent = 100; // 1% + price cer(asset(1, asset_id_type(1)), asset(1)); + + const asset_object& alicecoin = create_user_issued_asset( "ALICECOIN", alice_id(db), charge_market_fee, + cer, 4, market_fee_percent ); + const asset_object& aliceusd = create_user_issued_asset( "ALICEUSD", alice_id(db), 0 ); + + asset_id_type alicecoin_id = alicecoin.id; + asset_id_type aliceusd_id = aliceusd.id; + + // prepare users' balance + issue_uia( alice, aliceusd.amount( 20000000 ) ); + issue_uia( bob, alicecoin.amount( 10000000 ) ); + transfer( account_id_type(), alice_id, asset(10000000) ); + transfer( account_id_type(), bob_id, asset(10000000) ); + + // match and fill orders + create_sell_order( alice_id, aliceusd_id(db).amount(200000), alicecoin_id(db).amount(100000) ); + create_sell_order( bob_id, alicecoin_id(db).amount(100000), aliceusd_id(db).amount(200000) ); + + // check fee sharing + BOOST_CHECK_EQUAL( get_market_fee_reward( account_id_type(), alicecoin_id ), 0 ); + BOOST_CHECK_EQUAL( get_market_fee_reward( account_id_type(), aliceusd_id ), 0 ); + + // check issuer fees + BOOST_CHECK_EQUAL( alicecoin_id(db).dynamic_data(db).accumulated_fees.value, 1000 ); + BOOST_CHECK_EQUAL( aliceusd_id(db).dynamic_data(db).accumulated_fees.value, 0 ); + + // pass the hard fork + INVOKE( hardfork_time_test ); + set_expiration( db, trx ); + + // match and fill orders again + create_sell_order( alice_id, aliceusd_id(db).amount(200000), alicecoin_id(db).amount(100000) ); + create_sell_order( bob_id, alicecoin_id(db).amount(100000), aliceusd_id(db).amount(200000) ); + + // check fee sharing + BOOST_CHECK_EQUAL( get_market_fee_reward( account_id_type(), alicecoin_id ), 112 ); // 1000*11.23% + BOOST_CHECK_EQUAL( get_market_fee_reward( account_id_type(), aliceusd_id ), 0 ); + + // check issuer fees + BOOST_CHECK_EQUAL( alicecoin_id(db).dynamic_data(db).accumulated_fees.value, 1888 ); // 1000+1000-112 + BOOST_CHECK_EQUAL( aliceusd_id(db).dynamic_data(db).accumulated_fees.value, 0 ); + +} FC_LOG_AND_RETHROW() } + +BOOST_AUTO_TEST_SUITE_END() diff --git a/tests/tests/custom_authority_tests.cpp b/tests/tests/custom_authority_tests.cpp new file mode 100644 index 0000000000..e07aa10899 --- /dev/null +++ b/tests/tests/custom_authority_tests.cpp @@ -0,0 +1,6233 @@ +/* + * 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. + */ + +/** + * Readers of these custom active authority (CAA) tests may benefit by reviewing + * + * - rejection_indicator variant in restriction_predicate.hpp + * - function_type enum in restriction.hpp + * - GRAPHENE_OP_RESTRICTION_ARGUMENTS_VARIADIC in restriction.hpp + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../common/database_fixture.hpp" + +// Dependencies required by the HTLC-related tests +#include +#include + +// Dependencies for the voting and witness tests +#include + +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 outer-most sub-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 outer-most sub-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 attribute 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 expected rejection reason + // The rejection reason should be that the predicate was false + 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 outer-most sub-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 + 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 attribute of the restriction + BOOST_CHECK(predicate(update).rejection_path[1].get() == 0); + // Index 2 (the inner-most) rejection path refers to the expected rejection reason + // The rejection reason should be that the predicate was false + 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).success, true); + 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); + + // JSON-formatted Rejection path + //[ // A vector of predicate results + // [ + // 0, // Index 0 (the outer-most) rejection path + // 0 // The first and only outer-most sub-restriction + // ], + // [ + // 1, // Index 1 (the inner-most) rejection path + // [ // A vector of predicate results + // { + // "success": false, + // "rejection_path": [ + // [ + // 0, // Index 0 (the outer-most) rejection path + // 0 // Restriction 1 along this branch + // ], + // [ + // 2, // Rejection reason + // "predicate_was_false" + // ] + // ] + // }, + // { + // "success": false, + // "rejection_path": [ + // [ + // 0, // Index 0 (the outer-most) rejection path + // 0 // Restriction 1 along this branch + // ], + // [ + // 2, // Rejection reason + // "predicate_was_false" + // ] + // ] + // } + // ] + // ] + //] + + // C++ style check of the rejection path + BOOST_CHECK_EQUAL(predicate(transfer_to_1).rejection_path.size(), 2); + // Index 0 (the outer-most) rejection path refers to and only outer-most sub-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 asset_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(asset_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 quantity of transfers + // besides potentially depleting the 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); + // The failure should indicate the rejection path + // {"success":false,"rejection_path":[[0,0],[2,"predicate_was_false"]]} + EXPECT_EXCEPTION_STRING("\"rejection_path\":[[0,0],[2,\"predicate_was_false\"]]", [&] {PUSH_TX(db, trx);}); + + ////// + // Charlie attempts to transfer 100 CORE from Alice's account to Diana + // 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); + // The failure should indicate the rejection path + // {"success":false,"rejection_path":[[0,0],[2,"predicate_was_false"]]} + EXPECT_EXCEPTION_STRING("\"rejection_path\":[[0,0],[2,\"predicate_was_false\"]]", [&] {PUSH_TX(db, trx);}); + + + ////// + // 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); + // The failure should indicate the rejection path for the first custom authority + // "rejected_custom_auths":[["1.17.0",[0,{"success":false,"rejection_path":[[0,0],[2,"predicate_was_false"]]}]]] + EXPECT_EXCEPTION_STRING("\"rejection_path\":[[0,0],[2,\"predicate_was_false\"]]", [&] {PUSH_TX(db, trx);}); + // Check for reference to the second CAA 1.17.0 + // "rejected_custom_auths":[["1.17.0",[0,{"success":false,"rejection_path":[[0,0],[2,"predicate_was_false"]]}]]] + EXPECT_EXCEPTION_STRING("1.17.0", [&] {PUSH_TX(db, trx);}); + + + ////// + // 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); + // General check of the exception + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // Check the rejection path + EXPECT_EXCEPTION_STRING("\"rejection_path\":[[0,0],[2,\"predicate_was_false\"]]", [&] {PUSH_TX(db, trx);}); + // Check for reference to the second CAA 1.17.1 + // "rejected_custom_auths":[["1.17.1",[0,{"success":false,"rejection_path":[[0,0],[2,"predicate_was_false"]]}]]] + EXPECT_EXCEPTION_STRING("1.17.1", [&] {PUSH_TX(db, trx);}); + + ////// + // 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); + // The failure should not indicate any rejected custom auths because no CAA applies for Bob's attempt + // "rejected_custom_auths":[] + EXPECT_EXCEPTION_STRING("\"rejected_custom_auths\":[]", [&] {PUSH_TX(db, trx);}); + + } 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); + // The failure should not indicate any rejected custom auths because no CAA applies for Bob's attempt + // "rejected_custom_auths":[] + EXPECT_EXCEPTION_STRING("\"rejected_custom_auths\":[]", [&] {PUSH_TX(db, trx);}); + + + ////// + // Alice authorizes Bob to place limit orders that offer the any 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); + // The failure should not indicate any rejected custom auths because no CAA applies for Bob's attempt + // "rejected_custom_auths":[] + EXPECT_EXCEPTION_STRING("\"rejected_custom_auths\":[]", [&] {PUSH_TX(db, trx);}); + + + ////// + // 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); + // The failure should not indicate any rejected custom auths because no CAA applies for Bob's attempt + // "rejected_custom_auths":[] + EXPECT_EXCEPTION_STRING("\"rejected_custom_auths\":[]", [&] {PUSH_TX(db, trx);}); + + } FC_LOG_AND_RETHROW() + } + + + /** + * Test of authorization of one account (Alice) authorizing another key + * for restricted trading between between ACOIN1 and any BCOIN (BCOIN1, BCOIN2, and BCOIN3). + * + * The restricted trading authortization will be constructed with one custom authority + * containing two "logical_or" branches. One branch authorizes selling ACOINs for BCOINs. + * Another branch authorizes selling BCOINs for ACOINs. + */ + BOOST_AUTO_TEST_CASE(authorized_restricted_trading_key) { + 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: Fund some accounts + ////// + ACTORS((assetissuer)(alice)) + fund(alice, asset(5000 * GRAPHENE_BLOCKCHAIN_PRECISION)); + + + ////// + // 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()); + + + ////// + // Initialize: Create user-issued assets + ////// + upgrade_to_lifetime_member(assetissuer); + create_user_issued_asset("ACOIN1", assetissuer, UIA_ASSET_ISSUER_PERMISSION_MASK); + create_user_issued_asset("BCOIN1", assetissuer, UIA_ASSET_ISSUER_PERMISSION_MASK); + create_user_issued_asset("BCOIN2", assetissuer, UIA_ASSET_ISSUER_PERMISSION_MASK); + create_user_issued_asset("BCOIN3", assetissuer, UIA_ASSET_ISSUER_PERMISSION_MASK); + create_user_issued_asset("CCOIN1", assetissuer, UIA_ASSET_ISSUER_PERMISSION_MASK); + generate_blocks(1); + const asset_object &acoin1 = *db.get_index_type().indices().get().find("ACOIN1"); + const asset_object &bcoin1 = *db.get_index_type().indices().get().find("BCOIN1"); + const asset_object &bcoin2 = *db.get_index_type().indices().get().find("BCOIN2"); + const asset_object &bcoin3 = *db.get_index_type().indices().get().find("BCOIN3"); + const asset_object &ccoin1 = *db.get_index_type().indices().get().find("CCOIN1"); + + ////// + // Initialize: Issue UIAs + ////// + + // Lambda for issuing an asset to an account + auto issue_amount_to = [](const account_id_type &issuer, const asset &amount, const account_id_type &to) { + asset_issue_operation op; + op.issuer = issuer; + op.asset_to_issue = amount; + op.issue_to_account = to; + + return op; + }; + + // assetissuer issues A1, B1, and C1 to alice + asset_issue_operation issue_a1_to_alice_op + = issue_amount_to(assetissuer.get_id(), asset(1000, acoin1.id), alice.get_id()); + asset_issue_operation issue_b1_to_alice_op + = issue_amount_to(assetissuer.get_id(), asset(2000, bcoin1.id), alice.get_id()); + asset_issue_operation issue_c1_to_alice_op + = issue_amount_to(assetissuer.get_id(), asset(2000, ccoin1.id), alice.get_id()); + trx.clear(); + trx.operations = {issue_a1_to_alice_op, issue_b1_to_alice_op, issue_c1_to_alice_op}; + sign(trx, assetissuer_private_key); + PUSH_TX(db, trx); + + + ////// + // Some key attempts to create a limit order on behalf of Alice + // This should fail because the key 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 = acoin1.amount(60); + buy_order.min_to_receive = bcoin1.amount(15); + buy_order.expiration = time_point_sec::maximum(); + + trx.clear(); + trx.operations = {buy_order}; + sign(trx, some_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should not indicate any rejected custom auths because no CAA applies for the key's attempt + // "rejected_custom_auths":[] + EXPECT_EXCEPTION_STRING("\"rejected_custom_auths\":[]", [&] {PUSH_TX(db, trx);}); + + + ////// + // Alice authorizes a particular key to place limit orders that offer the any asset for sale + ////// + custom_authority_create_operation authorize_limit_orders; + authorize_limit_orders.account = alice.get_id(); + authorize_limit_orders.auth.add_authority(some_public_key, 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; + + auto amount_to_sell_index = member_index("amount_to_sell"); + auto min_to_receive_index = member_index("min_to_receive"); + auto asset_id_index = member_index("asset_id"); + + // Define the two set of assets: ACOINs and BCOINs + restriction is_acoin_rx = restriction(asset_id_index, FUNC(in), + flat_set{acoin1.id}); + restriction is_bcoin_rx = restriction(asset_id_index, FUNC(in), + flat_set{bcoin1.id, bcoin2.id, bcoin3.id}); + + // Custom Authority 1: Sell ACOINs to buy BCOINs + restriction sell_acoin_rx = restriction(amount_to_sell_index, FUNC(attr), vector{is_acoin_rx}); + + restriction buy_bcoin_rx = restriction(min_to_receive_index, FUNC(attr), vector{is_bcoin_rx}); + + vector branch_sell_acoin_buy_bcoin = {sell_acoin_rx, buy_bcoin_rx}; + + + // Custom Authority 2: Sell BCOINs to buy ACOINs + restriction sell_bcoin_rx = restriction(amount_to_sell_index, FUNC(attr), vector{is_bcoin_rx}); + restriction buy_acoin_rx = restriction(min_to_receive_index, FUNC(attr), vector{is_acoin_rx}); + + vector branch_sell_bcoin_buy_acoin = {sell_bcoin_rx, buy_acoin_rx}; + + + unsigned_int dummy_index = 999; + restriction trade_acoin_for_bcoin_rx = restriction(dummy_index, FUNC(logical_or), + vector>{branch_sell_acoin_buy_bcoin, + branch_sell_bcoin_buy_acoin}); + authorize_limit_orders.restrictions = {trade_acoin_for_bcoin_rx}; + //[ + // { + // "member_index": 999, + // "restriction_type": 11, + // "argument": [ + // 40, + // [ + // [ + // { + // "member_index": 2, + // "restriction_type": 10, + // "argument": [ + // 39, + // [ + // { + // "member_index": 1, + // "restriction_type": 6, + // "argument": [ + // 27, + // [ + // "1.3.2" + // ] + // ], + // "extensions": [] + // } + // ] + // ], + // "extensions": [] + // }, + // { + // "member_index": 3, + // "restriction_type": 10, + // "argument": [ + // 39, + // [ + // { + // "member_index": 1, + // "restriction_type": 6, + // "argument": [ + // 27, + // [ + // "1.3.3", + // "1.3.4", + // "1.3.5" + // ] + // ], + // "extensions": [] + // } + // ] + // ], + // "extensions": [] + // } + // ], + // [ + // { + // "member_index": 2, + // "restriction_type": 10, + // "argument": [ + // 39, + // [ + // { + // "member_index": 1, + // "restriction_type": 6, + // "argument": [ + // 27, + // [ + // "1.3.3", + // "1.3.4", + // "1.3.5" + // ] + // ], + // "extensions": [] + // } + // ] + // ], + // "extensions": [] + // }, + // { + // "member_index": 3, + // "restriction_type": 10, + // "argument": [ + // 39, + // [ + // { + // "member_index": 1, + // "restriction_type": 6, + // "argument": [ + // 27, + // [ + // "1.3.2" + // ] + // ], + // "extensions": [] + // } + // ] + // ], + // "extensions": [] + // } + // ] + // ] + // ], + // "extensions": [] + // } + //] + + // Broadcast the authorization + trx.clear(); + trx.operations = {authorize_limit_orders}; + sign(trx, alice_private_key); + PUSH_TX(db, trx); + + + // Authorize the cancellation of orders + custom_authority_create_operation authorize_limit_order_cancellations; + authorize_limit_order_cancellations.account = alice.get_id(); + authorize_limit_order_cancellations.auth.add_authority(some_public_key, 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); + + + ////// + // The key 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, some_private_key); + auto processed_buy = PUSH_TX(db, trx); + const limit_order_object *buy_order_object = + db.find( processed_buy.operation_results[0].get() ); + + + ////// + // The key attempts to cancel the limit order on behalf of Alice + // This should succeed because the key 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, some_private_key); + auto processed_cancelled = PUSH_TX(db, trx); + + + ////// + // Advance the blockchain to generate a distinctive hash ID for the buy order transaction + ////// + generate_blocks(1); + + + ////// + // The key attempts to create a limit order on behalf of Alice to sell ACOIN1 for CCOIN1 + // This should fail because the key is not authorized to sell ACOIN1 for CCOIN1 + ////// + buy_order = limit_order_create_operation(); + buy_order.seller = alice_id; + buy_order.amount_to_sell = acoin1.amount(60); + buy_order.min_to_receive = ccoin1.amount(15); + buy_order.expiration = time_point_sec::maximum(); + + trx.clear(); + trx.operations = {buy_order}; + sign(trx, some_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + + + ////// + // The key attempts to create a limit order on behalf of Alice to sell CCOIN1 for ACOIN1 + // This should fail because the key is not authorized to create this exchange offer + ////// + buy_order = limit_order_create_operation(); + buy_order.seller = alice_id; + buy_order.amount_to_sell = ccoin1.amount(60); + buy_order.min_to_receive = acoin1.amount(15); + buy_order.expiration = time_point_sec::maximum(); + + trx.clear(); + trx.operations = {buy_order}; + sign(trx, some_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + + + ////// + // The key attempts to create a limit order on behalf of Alice to sell BCOIN1 for CCOIN1 + // This should fail because the key is not authorized to create this exchange offer + ////// + buy_order = limit_order_create_operation(); + buy_order.seller = alice_id; + buy_order.amount_to_sell = bcoin1.amount(60); + buy_order.min_to_receive = ccoin1.amount(15); + buy_order.expiration = time_point_sec::maximum(); + + trx.clear(); + trx.operations = {buy_order}; + sign(trx, some_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + + + ////// + // The key attempts to create a limit order on behalf of Alice to sell CCOIN1 for BCOIN1 + // This should fail because the key is not authorized to create this exchange offer + ////// + buy_order = limit_order_create_operation(); + buy_order.seller = alice_id; + buy_order.amount_to_sell = ccoin1.amount(60); + buy_order.min_to_receive = bcoin1.amount(15); + buy_order.expiration = time_point_sec::maximum(); + + trx.clear(); + trx.operations = {buy_order}; + sign(trx, some_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + + + ////// + // The key attempts to create a limit order on behalf of Alice to sell BCOIN1 for BCOIN2 + // This should fail because the key is NOT authorized to create this exchange offer + ////// + buy_order = limit_order_create_operation(); + buy_order.seller = alice_id; + buy_order.amount_to_sell = bcoin1.amount(60); + buy_order.min_to_receive = bcoin2.amount(15); + buy_order.expiration = time_point_sec::maximum(); + + trx.clear(); + trx.operations = {buy_order}; + sign(trx, some_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + + + ////// + // The key attempts to create a limit order on behalf of Alice to sell ACOIN1 for BCOIN1 + // This should succeed because the key is authorized to create this offer + ////// + buy_order = limit_order_create_operation(); + buy_order.seller = alice_id; + buy_order.amount_to_sell = acoin1.amount(60); + buy_order.min_to_receive = bcoin1.amount(15); + buy_order.expiration = time_point_sec::maximum(); + + trx.clear(); + trx.operations = {buy_order}; + sign(trx, some_private_key); + PUSH_TX(db, trx); + + + ////// + // The key attempts to create a limit order on behalf of Alice to sell ACOIN1 for BCOIN2 + // This should succeed because the key is authorized to create this offer + ////// + buy_order = limit_order_create_operation(); + buy_order.seller = alice_id; + buy_order.amount_to_sell = acoin1.amount(60); + buy_order.min_to_receive = bcoin2.amount(15); + buy_order.expiration = time_point_sec::maximum(); + + trx.clear(); + trx.operations = {buy_order}; + sign(trx, some_private_key); + PUSH_TX(db, trx); + + + ////// + // The key attempts to create a limit order on behalf of Alice to sell ACOIN1 for BCOIN3 + // This should succeed because the key is authorized to create this offer + ////// + buy_order = limit_order_create_operation(); + buy_order.seller = alice_id; + buy_order.amount_to_sell = acoin1.amount(60); + buy_order.min_to_receive = bcoin3.amount(15); + buy_order.expiration = time_point_sec::maximum(); + + trx.clear(); + trx.operations = {buy_order}; + sign(trx, some_private_key); + PUSH_TX(db, trx); + + + ////// + // The key attempts to create a limit order on behalf of Alice to sell BCOIN1 for ACOIN1 + // This should succeed because the key is authorized to create this offer + ////// + buy_order = limit_order_create_operation(); + buy_order.seller = alice_id; + buy_order.amount_to_sell = bcoin1.amount(60); + buy_order.min_to_receive = acoin1.amount(15); + buy_order.expiration = time_point_sec::maximum(); + + trx.clear(); + trx.operations = {buy_order}; + sign(trx, some_private_key); + PUSH_TX(db, trx); + + } 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); + // The failure should not indicate any rejected custom auths because no CAA applies for Bob's attempt + // "rejected_custom_auths":[] + EXPECT_EXCEPTION_STRING("\"rejected_custom_auths\":[]", [&] {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("account4", faucet); + trx.clear(); + trx.operations = {create_op}; + sign(trx, some_private_key); + PUSH_TX(db, trx); + + } FC_LOG_AND_RETHROW() + } + + + /** + * Test of not equal (ne) restriction on an operation field + * Test of CAA for asset_issue_operation + * + * Scenario: Test of authorization of one account (alice) authorizing another account (bob) + * to issue an asset (ALICECOIN) to any account except a banned account (banned1) + */ + BOOST_AUTO_TEST_CASE(authorized_asset_issue_exceptions_1) { + 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((alice)(bob)(allowed1)(allowed2)(banned1)(allowed3)); + fund(alice, asset(500000 * GRAPHENE_BLOCKCHAIN_PRECISION)); + + + // Lambda for issuing an asset to an account + auto issue_amount_to = [&](const account_id_type &issuer, const asset &amount, const account_id_type &to) { + asset_issue_operation op; + op.issuer = issuer; + op.asset_to_issue = amount; + op.issue_to_account = to; + + return op; + }; + + ////// + // Create a UIA + ////// + upgrade_to_lifetime_member(alice); + create_user_issued_asset("ALICECOIN", alice, white_list); + create_user_issued_asset("SPECIALCOIN", alice, white_list); + generate_blocks(1); + const asset_object &alicecoin = *db.get_index_type().indices().get().find("ALICECOIN"); + const asset_object &specialcoin = *db.get_index_type().indices().get().find("SPECIALCOIN"); + const asset_id_type alicecoin_id = alicecoin.id; + + + ////// + // Attempt to issue the UIA to an account with the Alice key + // This should succeed because Alice is the issuer + ////// + asset_issue_operation issue_op = issue_amount_to(alice.get_id(), asset(100, alicecoin_id), allowed1.get_id()); + trx.clear(); + trx.operations = {issue_op}; + sign(trx, alice_private_key); + PUSH_TX(db, trx); + + + ////// + // Bob attempts to issue the UIA to an allowed account + // This should fail because Bob is not authorized to issue any ALICECOIN + ////// + issue_op = issue_amount_to(alice.get_id(), asset(100, alicecoin_id), allowed2.get_id()); + trx.clear(); + trx.operations = {issue_op}; + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should not indicate any rejected custom auths because no CAA applies for Bob's attempt + // "rejected_custom_auths":[] + EXPECT_EXCEPTION_STRING("\"rejected_custom_auths\":[]", [&] {PUSH_TX(db, trx);}); + + + ////// + // Alice authorizes Bob to issue assets on its behalf + // except for account banned1 + ////// + custom_authority_create_operation authorize_to_issue; + authorize_to_issue.account = alice.get_id(); + authorize_to_issue.auth.add_authority(bob.get_id(), 1); + authorize_to_issue.auth.weight_threshold = 1; + authorize_to_issue.enabled = true; + authorize_to_issue.valid_to = db.head_block_time() + 1000; + authorize_to_issue.operation_type = operation::tag::value; + + auto asset_index = member_index("asset_to_issue"); + auto asset_id_index = member_index("asset_id"); + authorize_to_issue.restrictions.emplace_back(restriction(asset_index, restriction::func_attr, vector{ + restriction(asset_id_index, restriction::func_eq, alicecoin_id)})); + auto issue_to_index = member_index("issue_to_account"); + authorize_to_issue.restrictions.emplace_back(issue_to_index, FUNC(ne), banned1.get_id()); + //[ + // { + // "member_index": 2, + // "restriction_type": 10, + // "argument": [ + // 39, + // [ + // { + // "member_index": 1, + // "restriction_type": 0, + // "argument": [ + // 8, + // "1.3.2" + // ], + // "extensions": [] + // } + // ] + // ], + // "extensions": [] + // }, + // { + // "member_index": 3, + // "restriction_type": 1, + // "argument": [ + // 7, + // "1.2.20" + // ], + // "extensions": [] + // } + //] + + trx.clear(); + trx.operations = {authorize_to_issue}; + sign(trx, alice_private_key); + PUSH_TX(db, trx); + + + ////// + // Advance the blockchain to generate a distinctive hash ID for the reused operation + ////// + generate_blocks(1); + + + ////// + // Bob attempts to issue the UIA to an allowed account + // This should succeed because Bob is now authorized to issue ALICECOIN + ////// + trx.clear(); + trx.operations.emplace_back(std::move(issue_op)); + sign(trx, bob_private_key); + PUSH_TX(db, trx); + + + ////// + // Bob attempts to issue the special coin to an allowed account + // This should fail because Bob is not authorized to issue SPECIALCOIN to any account + ////// + issue_op = issue_amount_to(alice.get_id(), asset(100, specialcoin.id), allowed3.get_id()); + trx.clear(); + trx.operations = {issue_op}; + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should indicate the rejection path + // "rejection_path":[[0,0],[0,0],[2,"predicate_was_false"] + // [0,0]: 0 is the rejection_indicator for an index to a sub-restriction; 0 is the index value for Restriction 1 + // [0,0]: 0 is the rejection_indicator for an index to a sub-restriction; 0 is the index value for the only argument + // [2,"predicate_was_false"]: 0 is the rejection_indicator for rejection_reason; "predicate_was_false" is the reason + EXPECT_EXCEPTION_STRING("\"rejection_path\":[[0,0],[0,0],[2,\"predicate_was_false\"]]", [&] {PUSH_TX(db, trx);}); + + + ////// + // Bob attempts to issue the UIA to a banned account with the Bob's key + // This should fail because Bob is not authorized to issue ALICECOIN to the banned account + ////// + issue_op = issue_amount_to(alice.get_id(), asset(100, alicecoin_id), banned1.get_id()); + trx.clear(); + trx.operations = {issue_op}; + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should indicate the rejection path + // "rejection_path":[[0,1],[2,"predicate_was_false"] + // [0,1]: 0 is the rejection_indicator for an index to a sub-restriction; 1 is the index value for Restriction 2 + // [2,"predicate_was_false"]: 0 is the rejection_indicator for rejection_reason; "predicate_was_false" is the reason + EXPECT_EXCEPTION_STRING("\"rejection_path\":[[0,1],[2,\"predicate_was_false\"]]", [&] {PUSH_TX(db, trx);}); + + + } FC_LOG_AND_RETHROW() + } + + + + + /** + * Test of not in (not_in) restriction on an operation field + * Test of CAA for asset_issue_operation + * + * Scenario: Test of authorization of one account (alice) authorizing another account (bob) + * to issue an asset (ALICECOIN) except to 3 banned accounts (banned1, banned2, banned3) + */ + BOOST_AUTO_TEST_CASE(authorized_asset_issue_exceptions_2) { + 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((alice)(bob)(allowed1)(allowed2)(banned1)(banned2)(banned3)(allowed3)); + fund(alice, asset(500000 * GRAPHENE_BLOCKCHAIN_PRECISION)); + + + // Lambda for issuing an asset to an account + auto issue_amount_to = [&](const account_id_type &issuer, const asset &amount, const account_id_type &to) { + asset_issue_operation op; + op.issuer = issuer; + op.asset_to_issue = amount; + op.issue_to_account = to; + + return op; + }; + + ////// + // Create user-issued assets + ////// + upgrade_to_lifetime_member(alice); + create_user_issued_asset("ALICECOIN", alice, white_list); + create_user_issued_asset("SPECIALCOIN", alice, white_list); + generate_blocks(1); + const asset_object &alicecoin = *db.get_index_type().indices().get().find("ALICECOIN"); + const asset_object &specialcoin = *db.get_index_type().indices().get().find("SPECIALCOIN"); + const asset_id_type alicecoin_id = alicecoin.id; + + + ////// + // Attempt to issue the UIA to an account with the Alice key + // This should succeed because Alice is the issuer + ////// + asset_issue_operation issue_op = issue_amount_to(alice.get_id(), asset(100, alicecoin_id), allowed1.get_id()); + trx.clear(); + trx.operations = {issue_op}; + sign(trx, alice_private_key); + PUSH_TX(db, trx); + + + ////// + // Bob attempts to issue the UIA to an allowed account + // This should fail because Bob is not authorized to issue any ALICECOIN + ////// + issue_op = issue_amount_to(alice.get_id(), asset(100, alicecoin_id), allowed2.get_id()); + trx.clear(); + trx.operations = {issue_op}; + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should not indicate any rejected custom auths because no CAA applies for Bob's attempt + // "rejected_custom_auths":[] + EXPECT_EXCEPTION_STRING("\"rejected_custom_auths\":[]", [&] {PUSH_TX(db, trx);}); + + + ////// + // Alice authorizes Bob to issue assets on its behalf + // except for accounts banned1, banned2, and banned3 + ////// + custom_authority_create_operation authorize_to_issue; + authorize_to_issue.account = alice.get_id(); + authorize_to_issue.auth.add_authority(bob.get_id(), 1); + authorize_to_issue.auth.weight_threshold = 1; + authorize_to_issue.enabled = true; + authorize_to_issue.valid_to = db.head_block_time() + 1000; + authorize_to_issue.operation_type = operation::tag::value; + + auto asset_index = member_index("asset_to_issue"); + auto asset_id_index = member_index("asset_id"); + authorize_to_issue.restrictions.emplace_back(restriction(asset_index, restriction::func_attr, vector{ + restriction(asset_id_index, restriction::func_eq, alicecoin_id)})); + auto issue_to_index = member_index("issue_to_account"); + authorize_to_issue.restrictions + .emplace_back(issue_to_index, FUNC(not_in), + flat_set{banned1.get_id(), banned2.get_id(), banned3.get_id()}); + //[ + // { + // "member_index": 2, + // "restriction_type": 10, + // "argument": [ + // 39, + // [ + // { + // "member_index": 1, + // "restriction_type": 0, + // "argument": [ + // 8, + // "1.3.2" + // ], + // "extensions": [] + // } + // ] + // ], + // "extensions": [] + // }, + // { + // "member_index": 3, + // "restriction_type": 7, + // "argument": [ + // 26, + // [ + // "1.2.20", + // "1.2.21", + // "1.2.22" + // ] + // ], + // "extensions": [] + // } + //] + + trx.clear(); + trx.operations = {authorize_to_issue}; + sign(trx, alice_private_key); + PUSH_TX(db, trx); + + + ////// + // Advance the blockchain to generate a distinctive hash ID for the reused operation + ////// + generate_blocks(1); + + + ////// + // Bob attempts to issue the UIA to an allowed account + // This should succeed because Bob is now authorized to issue ALICECOIN + ////// + trx.clear(); + trx.operations.emplace_back(std::move(issue_op)); + sign(trx, bob_private_key); + PUSH_TX(db, trx); + + + ////// + // Bob attempts to issue the special coin to an allowed account + // This should fail because Bob is not authorized to issue SPECIALCOIN to any account + ////// + issue_op = issue_amount_to(alice.get_id(), asset(100, specialcoin.id), allowed3.get_id()); + trx.clear(); + trx.operations = {issue_op}; + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should indicate the rejection path + // "rejection_path":[[0,0],[0,0],[2,"predicate_was_false"] + // [0,0]: 0 is the rejection_indicator for an index to a sub-restriction; 0 is the index value for Restriction 1 + // [0,0]: 0 is the rejection_indicator for an index to a sub-restriction; 0 is the index value for the only argument + // [2,"predicate_was_false"]: 0 is the rejection_indicator for rejection_reason; "predicate_was_false" is the reason + EXPECT_EXCEPTION_STRING("\"rejection_path\":[[0,0],[0,0],[2,\"predicate_was_false\"]]", [&] {PUSH_TX(db, trx);}); + + + ////// + // Bob attempts to issue the UIA to a banned account with the Bob's key + // This should fail because Bob is not authorized to issue ALICECOIN to banned account (banned1) + ////// + issue_op = issue_amount_to(alice.get_id(), asset(100, alicecoin.id), banned1.get_id()); + trx.clear(); + trx.operations = {issue_op}; + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should indicate the rejection path + // "rejection_path":[[0,1],[2,"predicate_was_false"] + // [0,1]: 0 is the rejection_indicator for an index to a sub-restriction; 1 is the index value for Restriction 2 + // [2,"predicate_was_false"]: 0 is the rejection_indicator for rejection_reason; "predicate_was_false" is the reason + EXPECT_EXCEPTION_STRING("\"rejection_path\":[[0,1],[2,\"predicate_was_false\"]]", [&] {PUSH_TX(db, trx);}); + + + ////// + // Bob attempts to issue the UIA to a banned account with the Bob's key + // This should fail because Bob is not authorized to issue ALICECOIN to banned account (banned2) + ////// + issue_op = issue_amount_to(alice.get_id(), asset(100, alicecoin.id), banned2.get_id()); + trx.clear(); + trx.operations = {issue_op}; + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should indicate the rejection path + // "rejection_path":[[0,1],[2,"predicate_was_false"] + // [0,1]: 0 is the rejection_indicator for an index to a sub-restriction; 1 is the index value for Restriction 2 + // [2,"predicate_was_false"]: 0 is the rejection_indicator for rejection_reason; "predicate_was_false" is the reason + EXPECT_EXCEPTION_STRING("\"rejection_path\":[[0,1],[2,\"predicate_was_false\"]]", [&] {PUSH_TX(db, trx);}); + + + ////// + // Bob attempts to issue the UIA to a banned account with the Bob's key + // This should fail because Bob is not authorized to issue ALICECOIN to banned account (banned3) + ////// + issue_op = issue_amount_to(alice.get_id(), asset(100, alicecoin.id), banned3.get_id()); + trx.clear(); + trx.operations = {issue_op}; + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should indicate the rejection path + // "rejection_path":[[0,1],[2,"predicate_was_false"] + // [0,1]: 0 is the rejection_indicator for an index to a sub-restriction; 1 is the index value for Restriction 2 + // [2,"predicate_was_false"]: 0 is the rejection_indicator for rejection_reason; "predicate_was_false" is the reason + EXPECT_EXCEPTION_STRING("\"rejection_path\":[[0,1],[2,\"predicate_was_false\"]]", [&] {PUSH_TX(db, trx);}); + + + ////// + // Bob attempts to issue the UIA to an allowed account + // This should succeed because Bob is authorized to issue ALICECOIN to any account + ////// + issue_op = issue_amount_to(alice.get_id(), asset(100, alicecoin.id), allowed3.get_id()); + trx.clear(); + trx.operations = {issue_op}; + sign(trx, bob_private_key); + PUSH_TX(db, trx); + + } FC_LOG_AND_RETHROW() + } + + + /** + * Test of in (in) restriction on an operation field + * Test of CAA for override_transfer_operation + * + * Scenario: Test of authorization of one account (alice) authorizing another account (bob) + * to override transfer an asset (ALICECOIN) from only 2 accounts (suspicious1, suspicious2) + */ + BOOST_AUTO_TEST_CASE(authorized_override_transfer) { + 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((alice)(bob)(allowed1)(allowed2)(suspicious1)(suspicious2)(allowed3)(arbitrator)); + fund(alice, asset(500000 * GRAPHENE_BLOCKCHAIN_PRECISION)); + + + // Lambda for issuing an asset to an account + auto issue_amount_to = [&](const account_id_type &issuer, const asset &amount, const account_id_type &to) { + asset_issue_operation op; + op.issuer = issuer; + op.asset_to_issue = amount; + op.issue_to_account = to; + + return op; + }; + + // Lambda for reserving an asset from an account + auto create_override = [&](const account_id_type &issuer, const account_id_type &from, const asset &amount, + const account_id_type &to) { + override_transfer_operation op; + op.issuer = issuer; + op.from = from; + op.amount = amount; + op.to = to; + + return op; + }; + + ////// + // Initialize: Create user-issued assets + ////// + upgrade_to_lifetime_member(alice); + create_user_issued_asset("ALICECOIN", alice, UIA_ASSET_ISSUER_PERMISSION_MASK); + create_user_issued_asset( "SPECIALCOIN", alice, UIA_ASSET_ISSUER_PERMISSION_MASK); + generate_blocks(1); + const asset_object &alicecoin = *db.get_index_type().indices().get().find("ALICECOIN"); + const asset_object &specialcoin + = *db.get_index_type().indices().get().find("SPECIALCOIN"); + + ////// + // Initialize: Alice issues her two coins to different accounts + ////// + asset_issue_operation issue_alice_to_allowed1_op + = issue_amount_to(alice.get_id(), asset(100, alicecoin.id), allowed1.get_id()); + asset_issue_operation issue_alice_to_allowed2_op + = issue_amount_to(alice.get_id(), asset(200, alicecoin.id), allowed2.get_id()); + asset_issue_operation issue_alice_to_allowed3_op + = issue_amount_to(alice.get_id(), asset(300, alicecoin.id), allowed3.get_id()); + asset_issue_operation issue_alice_to_suspicious1_op + = issue_amount_to(alice.get_id(), asset(100, alicecoin.id), suspicious1.get_id()); + asset_issue_operation issue_alice_to_suspicious2_op + = issue_amount_to(alice.get_id(), asset(200, alicecoin.id), suspicious2.get_id()); + + asset_issue_operation issue_special_to_allowed1_op + = issue_amount_to(alice.get_id(), asset(1000, specialcoin.id), allowed1.get_id()); + asset_issue_operation issue_special_to_allowed2_op + = issue_amount_to(alice.get_id(), asset(2000, specialcoin.id), allowed2.get_id()); + asset_issue_operation issue_special_to_allowed3_op + = issue_amount_to(alice.get_id(), asset(3000, specialcoin.id), allowed3.get_id()); + asset_issue_operation issue_special_to_suspicious1_op + = issue_amount_to(alice.get_id(), asset(1000, specialcoin.id), suspicious1.get_id()); + asset_issue_operation issue_special_to_suspicious2_op + = issue_amount_to(alice.get_id(), asset(2000, specialcoin.id), suspicious2.get_id()); + trx.clear(); + trx.operations = {issue_alice_to_allowed1_op, issue_alice_to_allowed2_op, issue_alice_to_allowed3_op, + issue_alice_to_suspicious1_op, issue_alice_to_suspicious2_op, + issue_special_to_allowed1_op, issue_special_to_allowed2_op, issue_special_to_allowed3_op, + issue_special_to_suspicious1_op, issue_special_to_suspicious2_op}; + sign(trx, alice_private_key); + PUSH_TX(db, trx); + + + ////// + // Alice attempts to override some ALICECOIN from some account + // This should succeed because Alice is the issuer + ////// + override_transfer_operation override_op + = create_override(alice.get_id(), allowed1.get_id(), asset(20, alicecoin.id), arbitrator.get_id()); + trx.clear(); + trx.operations = {override_op}; + sign(trx, alice_private_key); + PUSH_TX(db, trx); + int64_t allowed1_balance_alicecoin_after_override1 = get_balance(allowed1.get_id(), alicecoin.get_id()); + BOOST_CHECK_EQUAL(allowed1_balance_alicecoin_after_override1, 80); + + override_op + = create_override(alice.get_id(), suspicious1.get_id(), asset(20, alicecoin.id), arbitrator.get_id()); + trx.clear(); + trx.operations = {override_op}; + sign(trx, alice_private_key); + PUSH_TX(db, trx); + int64_t suspicious1_balance_alicecoin_after_override1 + = get_balance(suspicious1.get_id(), alicecoin.get_id()); + BOOST_CHECK_EQUAL(suspicious1_balance_alicecoin_after_override1, 80); + + override_op + = create_override(alice.get_id(), allowed1.get_id(), asset(200, specialcoin.id), arbitrator.get_id()); + trx.clear(); + trx.operations = {override_op}; + sign(trx, alice_private_key); + PUSH_TX(db, trx); + int64_t allowed1_balance_specialcoin_after_override1 = get_balance(allowed1.get_id(), specialcoin.id); + BOOST_CHECK_EQUAL(allowed1_balance_specialcoin_after_override1, 800); + + override_op + = create_override(alice.get_id(), suspicious1.get_id(), asset(200, specialcoin.id), arbitrator.get_id()); + trx.clear(); + trx.operations = {override_op}; + sign(trx, alice_private_key); + PUSH_TX(db, trx); + int64_t suspicious1_balance_specialcoin_after_override1 = get_balance(suspicious1.get_id(), specialcoin.id); + BOOST_CHECK_EQUAL(suspicious1_balance_specialcoin_after_override1, 800); + + + ////// + // Bob attempts to override some ALICECOIN and SPECIAL from some accounts + // This should fail because Bob is not authorized to override any ALICECOIN nor SPECIALCOIN + ////// + override_op = create_override(alice.get_id(), allowed1.get_id(), asset(25, alicecoin.id), arbitrator.get_id()); + trx.clear(); + trx.operations = {override_op}; + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should not indicate any rejected custom auths because no CAA applies for Bob's attempt + // "rejected_custom_auths":[] + EXPECT_EXCEPTION_STRING("\"rejected_custom_auths\":[]", [&] {PUSH_TX(db, trx);}); + + override_op = create_override(alice.get_id(), allowed1.get_id(), asset(25, specialcoin.id), arbitrator.get_id()); + trx.clear(); + trx.operations = {override_op}; + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should not indicate any rejected custom auths because no CAA applies for Bob's attempt + // "rejected_custom_auths":[] + EXPECT_EXCEPTION_STRING("\"rejected_custom_auths\":[]", [&] {PUSH_TX(db, trx);}); + + + ////// + // Alice authorizes Bob to override transfer ALICECOIN on its behalf + // only for accounts suspicious1, and suspicious2 + ////// + custom_authority_create_operation authorize_to_override; + authorize_to_override.account = alice.get_id(); + authorize_to_override.auth.add_authority(bob.get_id(), 1); + authorize_to_override.auth.weight_threshold = 1; + authorize_to_override.enabled = true; + authorize_to_override.valid_to = db.head_block_time() + 1000; + authorize_to_override.operation_type = operation::tag::value; + + auto amount_index = member_index("amount"); + auto asset_id_index = member_index("asset_id"); + authorize_to_override.restrictions + .emplace_back(restriction(amount_index, restriction::func_attr, vector{ + restriction(asset_id_index, restriction::func_eq, alicecoin.get_id())})); + auto from_index = member_index("from"); + authorize_to_override.restrictions + .emplace_back(from_index, FUNC(in), + flat_set{suspicious1.get_id(), suspicious2.get_id()}); + //[ + // { + // "member_index": 4, + // "restriction_type": 10, + // "argument": [ + // 39, + // [ + // { + // "member_index": 1, + // "restriction_type": 0, + // "argument": [ + // 8, + // "1.3.2" + // ], + // "extensions": [] + // } + // ] + // ], + // "extensions": [] + // }, + // { + // "member_index": 2, + // "restriction_type": 6, + // "argument": [ + // 26, + // [ + // "1.2.20", + // "1.2.21" + // ] + // ], + // "extensions": [] + // } + //] + + trx.clear(); + trx.operations = {authorize_to_override}; + sign(trx, alice_private_key); + PUSH_TX(db, trx); + + + ////// + // Advance the blockchain to generate a distinctive hash ID for the reused operation + ////// + generate_blocks(1); + + + ////// + // Bob attempts to override transfer some ALICECOIN from a suspicious account + // This should succeed because Bob is now authorized to override ALICECOIN from some accounts + ////// + override_op = create_override(alice.get_id(), suspicious1.get_id(), asset(25, alicecoin.id), arbitrator.get_id()); + trx.clear(); + trx.operations = {override_op}; + sign(trx, bob_private_key); + PUSH_TX(db, trx); + int64_t suspicious1_balance_alicecoin_after_override2 + = get_balance(suspicious1.get_id(), alicecoin.get_id()); + BOOST_CHECK_EQUAL(suspicious1_balance_alicecoin_after_override2, suspicious1_balance_alicecoin_after_override1 - 25); + + + ////// + // Bob attempts to override transfer some SPECIALCOIN from a suspicious account + // This should fail because Bob is not authorized to override SPECIALCOIN from any accounts + ////// + override_op = create_override(alice.get_id(), suspicious1.get_id(), asset(250, specialcoin.id), arbitrator.get_id()); + trx.clear(); + trx.operations = {override_op}; + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should indicate the rejection path + // "rejection_path":[[0,0],[0,0],[2,"predicate_was_false"] + // [0,0]: 0 is the rejection_indicator for an index to a sub-restriction; 0 is the index value for Restriction 1 + // [0,0]: 0 is the rejection_indicator for an index to a sub-restriction; 0 is the index value for the only argument + // [2,"predicate_was_false"]: 0 is the rejection_indicator for rejection_reason; "predicate_was_false" is the reason + EXPECT_EXCEPTION_STRING("\"rejection_path\":[[0,0],[0,0],[2,\"predicate_was_false\"]]", [&] {PUSH_TX(db, trx);}); + + + ////// + // Bob attempts to override transfer some SPECIALCOIN from an allowed account + // This should fail because Bob is not authorized to override SPECIALCOIN from any accounts + ////// + override_op = create_override(alice.get_id(), allowed3.get_id(), asset(250, specialcoin.id), arbitrator.get_id()); + trx.clear(); + trx.operations = {override_op}; + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should indicate the rejection path + // "rejection_path":[[0,0],[0,0],[2,"predicate_was_false"] + // [0,0]: 0 is the rejection_indicator for an index to a sub-restriction; 0 is the index value for Restriction 1 + // [0,0]: 0 is the rejection_indicator for an index to a sub-restriction; 0 is the index value for the only argument + // [2,"predicate_was_false"]: 0 is the rejection_indicator for rejection_reason; "predicate_was_false" is the reason + EXPECT_EXCEPTION_STRING("\"rejection_path\":[[0,0],[0,0],[2,\"predicate_was_false\"]]", [&] {PUSH_TX(db, trx);}); + + + ////// + // Bob attempts to override transfer some ALICECOIN from an allowed account + // This should fail because Bob is only authorized to override ALICECOIN from suspicious accounts + ////// + override_op = create_override(alice.get_id(), allowed2.get_id(), asset(20, alicecoin.id), arbitrator.get_id()); + trx.clear(); + trx.operations = {override_op}; + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should indicate the rejection path + // "rejection_path":[[0,1],[2,"predicate_was_false"] + // [0,1]: 0 is the rejection_indicator for an index to a sub-restriction; 1 is the index value for Restriction 2 + // [2,"predicate_was_false"]: 0 is the rejection_indicator for rejection_reason; "predicate_was_false" is the reason + EXPECT_EXCEPTION_STRING("\"rejection_path\":[[0,1],[2,\"predicate_was_false\"]]", [&] {PUSH_TX(db, trx);}); + int64_t allowed2_balance_alicecoin_after_no_override + = get_balance(allowed2.get_id(), alicecoin.get_id()); + BOOST_CHECK_EQUAL(allowed2_balance_alicecoin_after_no_override, 200); + int64_t allowed2_balance_specialcoin_no_override + = get_balance(allowed2.get_id(), specialcoin.get_id()); + BOOST_CHECK_EQUAL(allowed2_balance_specialcoin_no_override, 2000); + + + ////// + // Alice attempts to override transfer of SPECIAL COIN from an allowed account + // This should succeed because Alice has not revoked her own authorities as issuer + ////// + override_op = create_override(alice.get_id(), allowed3.get_id(), asset(500, specialcoin.id), arbitrator.get_id()); + trx.clear(); + trx.operations = {override_op}; + sign(trx, alice_private_key); + PUSH_TX(db, trx); + int64_t allowed3_balance_alicecoin_after_no_override + = get_balance(allowed3.get_id(), alicecoin.get_id()); + BOOST_CHECK_EQUAL(allowed3_balance_alicecoin_after_no_override, 300); + int64_t allowed3_balance_specialcoin_after_override1 + = get_balance(allowed3.get_id(), specialcoin.get_id()); + BOOST_CHECK_EQUAL(allowed3_balance_specialcoin_after_override1, 3000 - 500); + + } 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 coldwallet_balance_usd_before_offer = get_balance(coldwallet_id, usd_id); + BOOST_CHECK_EQUAL( 1000, coldwallet_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 asset_id_index = member_index("asset_id"); + op.restrictions.emplace_back(restriction(transfer_amount_index, restriction::func_attr, vector{ + restriction(asset_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); + // The failure should indicate the rejection path + // "rejection_path":[[0,0],[2,"predicate_was_false"] + // [0,0]: 0 is the rejection_indicator for an index to a sub-restriction; 0 is the index value for Restriction 1 + // [2,"predicate_was_false"]: 0 is the rejection_indicator for rejection_reason; "predicate_was_false" is the reason + EXPECT_EXCEPTION_STRING("\"rejection_path\":[[0,0],[2,\"predicate_was_false\"]]", [&] {PUSH_TX(db, trx);}); + + + ////// + // 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); + // The failure should indicate the rejection path + // "rejection_path":[[0,1],[0,0],[2,"predicate_was_false"] + // [0,1]: 0 is the rejection_indicator for an index to a sub-restriction; 1 is the index value for Restriction 2 + // [0,0]: 0 is the rejection_indicator for an index to a sub-restriction; 0 is the index value for the only argument + // [2,"predicate_was_false"]: 0 is the rejection_indicator for rejection_reason; "predicate_was_false" is the reason + EXPECT_EXCEPTION_STRING("\"rejection_path\":[[0,1],[0,0],[2,\"predicate_was_false\"]]", [&] {PUSH_TX(db, trx);}); + + + ////// + // 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() + } + + /** + * Test of a restriction on an optional operation field + * Variation of the the original transfer_with_memo test for CAA + * Bob is authorized to transfer Alice's account to Charlies's account if + * - the memo is not set OR + * - the memo is set where the "from" equal's Bob's public key and "to" equals Diana's public *active* key + * (The active key is chosen for simplicity. Other keys such as the memo key or an alternate key could also be used.) + */ + BOOST_AUTO_TEST_CASE(authorized_transfer_with_memo_1) { + try { + ////// + // Initialize the test + ////// + ACTORS((alice)(bob)(charlie)(diana)) + 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); + + transfer(account_id_type(), alice_id, asset(1000)); + BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 1000); + BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 0); + BOOST_CHECK_EQUAL(get_balance(charlie_id, asset_id_type()), 00); + BOOST_CHECK_EQUAL(get_balance(diana_id, asset_id_type()), 0); + + + ////// + // Alice transfers to Charlie with her own authorization + ////// + transfer_operation top; + top.from = alice.get_id(); + top.to = charlie.get_id(); + top.amount = asset(50); + top.memo = memo_data(); + top.memo->set_message(alice_private_key, bob_public_key, "Dear Bob,\n\nMoney!\n\nLove, Alice"); + trx.operations = {top}; + trx.sign(alice_private_key, db.get_chain_id()); + auto processed = PUSH_TX(db, trx); + + BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 950); + BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 0); + BOOST_CHECK_EQUAL(get_balance(charlie_id, asset_id_type()), 50); + BOOST_CHECK_EQUAL(get_balance(diana_id, asset_id_type()), 0); + + auto memo = db.get_recent_transaction(processed.id()).operations.front().get().memo; + BOOST_CHECK(memo); + BOOST_CHECK_EQUAL(memo->get_message(bob_private_key, alice_public_key), "Dear Bob,\n\nMoney!\n\nLove, Alice"); + + + ////// + // Bob attempts to transfer from Alice to Charlie + // This should fail because Bob is not authorized + ////// + generate_blocks(1); // Advance the blockchain to generate a distinctive hash ID for the re-used transfer op + top = transfer_operation(); + top.from = alice.get_id(); + top.to = charlie.get_id(); + top.amount = asset(50); + trx.clear(); + trx.operations = {top}; + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should not indicate any rejected custom auths because no CAA applies for Bob's attempt + // "rejected_custom_auths":[] + EXPECT_EXCEPTION_STRING("\"rejected_custom_auths\":[]", [&] {PUSH_TX(db, trx);}); + + + ////// + // Alice authorizes Bob to transfer to Charlie if + // - the memo is not set OR + // - the memo is set where the "from" equal's Bob's public key and "to" equals Diana's public key + ////// + custom_authority_create_operation caop; + caop.account = alice.get_id(); + caop.auth.add_authority(bob.get_id(), 1); + caop.auth.weight_threshold = 1; + caop.enabled = true; + caop.valid_to = db.head_block_time() + 1000; + caop.operation_type = operation::tag::value; + + vector restrictions; + + // Restriction 1 should have "to" to equal Charlie + auto to_index = member_index("to"); + auto memo_index = member_index("memo"); + auto to_inside_memo_index = member_index("to"); + restrictions.emplace_back(to_index, FUNC(eq), charlie.get_id()); + + // Restriction 2 is logical OR restriction + // Branch 1 should have the memo "to" to not be set (to equal void) + vector branch1 = vector{restriction(memo_index, FUNC(eq), void_t())}; + // Branch 2 should have the memo "to" reference Diana's public *active* key + // and "from" reference Bob's public *active* key + auto from_inside_memo_index = member_index("from"); + vector branch2 = vector{restriction(memo_index, restriction::func_attr, + vector{ + restriction(to_inside_memo_index, FUNC(eq), diana_public_key), + restriction(from_inside_memo_index, FUNC(eq), bob_public_key)})}; + unsigned_int dummy_index = 999; + restriction or_restriction = restriction(dummy_index, FUNC(logical_or), vector>{branch1, branch2}); + restrictions.emplace_back(or_restriction); + caop.restrictions = restrictions; + //[ + // { + // "member_index": 2, + // "restriction_type": 0, + // "argument": [ + // 7, + // "1.2.18" + // ], + // "extensions": [] + // }, + // { + // "member_index": 999, + // "restriction_type": 11, + // "argument": [ + // 40, + // [ + // [ + // { + // "member_index": 4, + // "restriction_type": 0, + // "argument": [ + // 0, + // {} + // ], + // "extensions": [] + // } + // ], + // [ + // { + // "member_index": 4, + // "restriction_type": 10, + // "argument": [ + // 39, + // [ + // { + // "member_index": 1, + // "restriction_type": 0, + // "argument": [ + // 5, + // "BTS6MWg7PpE6azCGwKuhB17DbtSqhzf8i25hspdhndsf7VfsLee7k" + // ], + // "extensions": [] + // }, + // { + // "member_index": 0, + // "restriction_type": 0, + // "argument": [ + // 5, + // "BTS5VE6Dgy9FUmd1mFotXwF88HkQN1KysCWLPqpVnDMjRvGRi1YrM" + // ], + // "extensions": [] + // } + // ] + // ], + // "extensions": [] + // } + // ] + // ] + // ], + // "extensions": [] + // } + //] + + trx.clear(); + trx.operations = {caop}; + sign(trx, alice_private_key); + PUSH_TX(db, trx); + + ////// + // Bob attempts to transfer from Alice to Charlie WITHOUT a memo + // This should succeed + ////// + generate_blocks(1); // Advance the blockchain to generate a distinctive hash ID for the re-used transfer op + trx.clear(); + trx.operations = {top}; + sign(trx, bob_private_key); + PUSH_TX(db, trx); + + BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 900); + BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 0); + BOOST_CHECK_EQUAL(get_balance(charlie_id, asset_id_type()), 100); + BOOST_CHECK_EQUAL(get_balance(diana_id, asset_id_type()), 0); + + ////// + // Bob attempts to transfer from Alice to Charlie with a memo + // where "from" equals Bob's public key and "to" equals Diana's public key + // This should succeed + ////// + generate_blocks(1); // Advance the blockchain to generate a distinctive hash ID for the similar transfer op + top = transfer_operation(); + top.from = alice.get_id(); + top.to = charlie.get_id(); + top.amount = asset(50); + top.memo = memo_data(); + top.memo->set_message(bob_private_key, diana_public_key, + "Dear Diana,\n\nOnly you should be able to read this\n\nLove, Bob"); + trx.clear(); + trx.operations = {top}; + sign(trx, bob_private_key); + processed = PUSH_TX(db, trx); + + BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 850); + BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 0); + BOOST_CHECK_EQUAL(get_balance(charlie_id, asset_id_type()), 150); + BOOST_CHECK_EQUAL(get_balance(diana_id, asset_id_type()), 0); + + memo = db.get_recent_transaction(processed.id()).operations.front().get().memo; + BOOST_CHECK(memo); + BOOST_CHECK_EQUAL(memo->get_message(diana_private_key, bob_public_key), + "Dear Diana,\n\nOnly you should be able to read this\n\nLove, Bob"); + + ////// + // Bob attempts to transfer from Alice to Charlie with a memo + // where "from" equals Bob's public key and "to" equals Charlie's public key + // This should fail because it violates the memo restriction + ////// + generate_blocks(1); // Advance the blockchain to generate a distinctive hash ID for the similar transfer op + top = transfer_operation(); + top.from = alice.get_id(); + top.to = charlie.get_id(); + top.amount = asset(50); + top.memo = memo_data(); + top.memo->set_message(bob_private_key, charlie_public_key, + "Dear Charlie,\n\nOnly you should be able to read this\n\nLove, Bob"); + trx.clear(); + trx.operations = {top}; + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + + // The failure should indicate a violation of both branches of the OR memo restrictions + // JSON style check of the rejection path + // JSON-formatted Rejection path + //[ // A vector of predicate results + // [ + // 0, // Index 0 (the outer-most) rejection path + // 1 // 1 is the index for Restriction 2 + // ], + // [ + // 1, // A (sub-)vector of predicate results + // [ + // { + // "success": false, + // "rejection_path": [ + // [ + // 0, // Index 0 of Branch 1 rejection path + // 0 // Restriction 1 along this branch + // ], + // [ + // 2, // Rejection reason + // "predicate_was_false" + // ] + // ] + // }, + // { + // "success": false, + // "rejection_path": [ + // [ + // 0, // Index 0 of Branch 2 rejection path + // 0 // Restriction 1 along this branch + // ], + // [ + // 0, // Index 1 of Branch 2 rejection path + // 0 // First and only attribute of sub-restriction + // ], + // [ + // 2, // Rejection reeason + // "predicate_was_false" + // ] + // ] + // } + // ] + // ] + //] + EXPECT_EXCEPTION_STRING("[[0,1],[1,[{\"success\":false,\"rejection_path\":[[0,0],[2,\"predicate_was_false\"]]},{\"success\":false,\"rejection_path\":[[0,0],[0,0],[2,\"predicate_was_false\"]]}]]]", [&] {PUSH_TX(db, trx);}); + + ////// + // Bob attempts to transfer from Alice to Diana + // This should fail because the transfer must be to Charlie + ////// + generate_blocks(1); // Advance the blockchain to generate a distinctive hash ID for the similar transfer op + top = transfer_operation(); + top.from = alice.get_id(); + top.to = diana.get_id(); + top.amount = asset(50); + trx.clear(); + trx.operations = {top}; + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // [0,0]: 0 is the rejection_indicator for an index to a sub-restriction; 0 is the index value for Restriction 1 + // [2,"predicate_was_false"]: 0 is the rejection_indicator for rejection_reason; "predicate_was_false" is the reason + EXPECT_EXCEPTION_STRING("\"rejection_path\":[[0,0],[2,\"predicate_was_false\"]]", [&] {PUSH_TX(db, trx);}); + + } FC_LOG_AND_RETHROW() + } + + + /** + * Test of a restriction on an optional operation field + * Variation of the the original transfer_with_memo test for CAA + * Bob is authorized to transfer from Alice's account to Charlies's account only if + * - the memo is set where the "from" equal's Bob's public key and "to" equals Diana's public *active* key + * (The active key is chosen for simplicity. Other keys such as the memo key or an alternate key could also be used.) + * + * A memo field is implicitly required. Attempts without a memo field should have a rejection reason of null_optional + */ + BOOST_AUTO_TEST_CASE(authorized_transfer_with_memo_2) { + try { + ////// + // Initialize the test + ////// + ACTORS((alice)(bob)(charlie)(diana)) + 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); + + transfer(account_id_type(), alice_id, asset(1000)); + BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 1000); + BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 0); + BOOST_CHECK_EQUAL(get_balance(charlie_id, asset_id_type()), 00); + BOOST_CHECK_EQUAL(get_balance(diana_id, asset_id_type()), 0); + + + ////// + // Alice transfers to Charlie with her own authorization + ////// + transfer_operation top; + top.from = alice.get_id(); + top.to = charlie.get_id(); + top.amount = asset(50); + top.memo = memo_data(); + top.memo->set_message(alice_private_key, bob_public_key, "Dear Bob,\n\nMoney!\n\nLove, Alice"); + trx.operations = {top}; + trx.sign(alice_private_key, db.get_chain_id()); + auto processed = PUSH_TX(db, trx); + + BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 950); + BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 0); + BOOST_CHECK_EQUAL(get_balance(charlie_id, asset_id_type()), 50); + BOOST_CHECK_EQUAL(get_balance(diana_id, asset_id_type()), 0); + + auto memo = db.get_recent_transaction(processed.id()).operations.front().get().memo; + BOOST_CHECK(memo); + BOOST_CHECK_EQUAL(memo->get_message(bob_private_key, alice_public_key), "Dear Bob,\n\nMoney!\n\nLove, Alice"); + + + ////// + // Bob attempts to transfer from Alice to Charlie + // This should fail because Bob is not authorized + ////// + generate_blocks(1); // Advance the blockchain to generate a distinctive hash ID for the re-used transfer op + top = transfer_operation(); + top.from = alice.get_id(); + top.to = charlie.get_id(); + top.amount = asset(50); + trx.clear(); + trx.operations = {top}; + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should not indicate any rejected custom auths because no CAA applies for Bob's attempt + // "rejected_custom_auths":[] + EXPECT_EXCEPTION_STRING("\"rejected_custom_auths\":[]", [&] {PUSH_TX(db, trx);}); + + + ////// + // Alice authorizes Bob to transfer to Charlie if + // - the memo is set where the "from" equal's Bob's public key and "to" equals Diana's public key + ////// + custom_authority_create_operation caop; + caop.account = alice.get_id(); + caop.auth.add_authority(bob.get_id(), 1); + caop.auth.weight_threshold = 1; + caop.enabled = true; + caop.valid_to = db.head_block_time() + 1000; + caop.operation_type = operation::tag::value; + + vector restrictions; + + // Restriction 1 should have "to" to equal Charlie + auto to_index = member_index("to"); + auto memo_index = member_index("memo"); + auto to_inside_memo_index = member_index("to"); + restrictions.emplace_back(to_index, FUNC(eq), charlie.get_id()); + + // Branch 2 should have the memo "to" reference Diana's public *active* key + // and "from" reference Bob's public *active* key + auto from_inside_memo_index = member_index("from"); + restrictions.emplace_back(restriction(memo_index, restriction::func_attr, + vector{ + restriction(to_inside_memo_index, FUNC(eq), diana_public_key), + restriction(from_inside_memo_index, FUNC(eq), bob_public_key)})); + caop.restrictions = restrictions; + //[ + // { + // "member_index": 2, + // "restriction_type": 0, + // "argument": [ + // 7, + // "1.2.18" + // ], + // "extensions": [] + // }, + // { + // "member_index": 4, + // "restriction_type": 10, + // "argument": [ + // 39, + // [ + // { + // "member_index": 1, + // "restriction_type": 0, + // "argument": [ + // 5, + // "BTS6MWg7PpE6azCGwKuhB17DbtSqhzf8i25hspdhndsf7VfsLee7k" + // ], + // "extensions": [] + // }, + // { + // "member_index": 0, + // "restriction_type": 0, + // "argument": [ + // 5, + // "BTS5VE6Dgy9FUmd1mFotXwF88HkQN1KysCWLPqpVnDMjRvGRi1YrM" + // ], + // "extensions": [] + // } + // ] + // ], + // "extensions": [] + // } + //] + + trx.clear(); + trx.operations = {caop}; + sign(trx, alice_private_key); + PUSH_TX(db, trx); + + + ////// + // Bob attempts to transfer from Alice to Charlie WITHOUT a memo + // This should fail because Restriction 2 expects a memo + ////// + generate_blocks(1); // Advance the blockchain to generate a distinctive hash ID for the re-used transfer op + trx.clear(); + trx.operations = {top}; + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // [0,1]: 0 is the rejection_indicator for an index to a sub-restriction; 1 is the index value for Restriction 2 + // [2,"null_optional"]: 0 is the rejection_indicator for rejection_reason; "null_optional" is the reason + EXPECT_EXCEPTION_STRING("\"rejection_path\":[[0,1],[2,\"null_optional\"]]", [&] {PUSH_TX(db, trx);}); + + + ////// + // Bob attempts to transfer from Alice to Charlie with a memo + // where "from" equals Bob's public key and "to" equals Diana's public key + // This should succeed + ////// + generate_blocks(1); // Advance the blockchain to generate a distinctive hash ID for the similar transfer op + top = transfer_operation(); + top.from = alice.get_id(); + top.to = charlie.get_id(); + top.amount = asset(50); + top.memo = memo_data(); + top.memo->set_message(bob_private_key, diana_public_key, + "Dear Diana,\n\nOnly you should be able to read this\n\nLove, Bob"); + trx.clear(); + trx.operations = {top}; + sign(trx, bob_private_key); + processed = PUSH_TX(db, trx); + + BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 900); + BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 0); + BOOST_CHECK_EQUAL(get_balance(charlie_id, asset_id_type()), 100); + BOOST_CHECK_EQUAL(get_balance(diana_id, asset_id_type()), 0); + + memo = db.get_recent_transaction(processed.id()).operations.front().get().memo; + BOOST_CHECK(memo); + BOOST_CHECK_EQUAL(memo->get_message(diana_private_key, bob_public_key), + "Dear Diana,\n\nOnly you should be able to read this\n\nLove, Bob"); + + ////// + // Bob attempts to transfer from Alice to Charlie with a memo + // where "from" equals Bob's public key and "to" equals Charlie's public key + // This should fail because it violates the memo restriction + ////// + generate_blocks(1); // Advance the blockchain to generate a distinctive hash ID for the similar transfer op + top = transfer_operation(); + top.from = alice.get_id(); + top.to = charlie.get_id(); + top.amount = asset(50); + top.memo = memo_data(); + top.memo->set_message(bob_private_key, charlie_public_key, + "Dear Charlie,\n\nOnly you should be able to read this\n\nLove, Bob"); + trx.clear(); + trx.operations = {top}; + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // [0,1]: 0 is the rejection_indicator for an index to a sub-restriction; 1 is the index value for Restriction 2 + // [0,0]: 0 is the rejection_indicator for an index to a sub-restriction; 0 is the index value for the only argument + // [2,"predicate_was_false"]: 0 is the rejection_indicator for rejection_reason; "null_optional" is the reason + EXPECT_EXCEPTION_STRING("\"rejection_path\":[[0,1],[0,0],[2,\"predicate_was_false\"]]", [&] {PUSH_TX(db, trx);}); + + ////// + // Bob attempts to transfer from Alice to Diana + // This should fail because transfer must be to Charlie + ////// + generate_blocks(1); // Advance the blockchain to generate a distinctive hash ID for the similar transfer op + top = transfer_operation(); + top.from = alice.get_id(); + top.to = diana.get_id(); + top.amount = asset(50); + trx.clear(); + trx.operations = {top}; + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // [0,0]: 0 is the rejection_indicator for an index to a sub-restriction; 0 is the index for Restriction 1 + // [2,"predicate_was_false"]: 0 is the rejection_indicator for rejection_reason; "predicate_was_false" is the reason + EXPECT_EXCEPTION_STRING("\"rejection_path\":[[0,0],[2,\"predicate_was_false\"]]", [&] {PUSH_TX(db, trx);}); + + } FC_LOG_AND_RETHROW() + } + + + /** + * Test of has none (has_none) restriction on a container field + * Test of CAA for asset_update_feed_producers_operation + * + * Scenario: Test of authorization of one account (alice) authorizing another account (bob) + * to update an asset's feed producers as long as the list does not contain + * untrusted producers (untrusted1, untrusted2, untrusted3) + */ + BOOST_AUTO_TEST_CASE(authorized_feed_producers_1) { + 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((alice)(bob)); + ACTORS((trusted1)(trusted2)(trusted3)(trusted4)(trusted5)(trusted6)); + ACTORS((untrusted1)(untrusted2)(untrusted3)); + fund(alice, asset(500000 * GRAPHENE_BLOCKCHAIN_PRECISION)); + + + // Lambda for update asset feed producers + auto create_producers_op = [&](const account_id_type &issuer, const asset_id_type &asset, const flat_set &new_producers) { + asset_update_feed_producers_operation op; + + op.issuer = issuer; + op.asset_to_update = asset; + op.new_feed_producers = new_producers; + + return op; + }; + + + ////// + // Create user-issued assets + ////// + upgrade_to_lifetime_member(alice); + create_bitasset("ALICECOIN", alice.get_id()); + generate_blocks(1); + const asset_object &alicecoin = *db.get_index_type().indices().get().find("ALICECOIN"); + + + ////// + // Alice attempts to update the feed producers for ALICECOIN + // This should succeed because Alice can update her own asset + ////// + flat_set new_producers = {trusted1.get_id(), trusted2.get_id()}; + asset_update_feed_producers_operation producers_op + = create_producers_op(alice.get_id(), alicecoin.id, new_producers); + trx.clear(); + trx.operations = {producers_op}; + sign(trx, alice_private_key); + PUSH_TX(db, trx); + + + ////// + // Advance the blockchain to generate a distinctive hash ID for the same transaction + ////// + generate_blocks(1); + + + ////// + // Bob attempts to update the feed producers for ALICECOIN + // This should fail because Bob is not authorized to update feed producers for ALICECOIN + ////// + new_producers = {trusted3.get_id()}; + producers_op = create_producers_op(alice.get_id(), alicecoin.id, new_producers); + trx.clear(); + trx.operations = {producers_op}; + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should not indicate any rejected custom auths because no CAA applies for Bob's attempt + // "rejected_custom_auths":[] + EXPECT_EXCEPTION_STRING("\"rejected_custom_auths\":[]", [&] {PUSH_TX(db, trx);}); + + + ////// + // Alice authorizes Bob to update the feed producers + // but must not select untrusted1, untrusted2, untrusted3 + ////// + custom_authority_create_operation authorize_to_update_feed_producers; + authorize_to_update_feed_producers.account = alice.get_id(); + authorize_to_update_feed_producers.auth.add_authority(bob.get_id(), 1); + authorize_to_update_feed_producers.auth.weight_threshold = 1; + authorize_to_update_feed_producers.enabled = true; + authorize_to_update_feed_producers.valid_to = db.head_block_time() + 1000; + + authorize_to_update_feed_producers.operation_type = operation::tag::value; + flat_set untrusted_producers = {untrusted1.get_id(), untrusted2.get_id(), untrusted3.get_id()}; + auto new_feed_producers_index = member_index("new_feed_producers"); + authorize_to_update_feed_producers.restrictions + .emplace_back(new_feed_producers_index, FUNC(has_none), untrusted_producers); + //[ + // { + // "member_index": 3, + // "restriction_type": 9, + // "argument": [ + // 26, + // [ + // "1.2.24", + // "1.2.25", + // "1.2.26" + // ] + // ], + // "extensions": [] + // } + //] + + trx.clear(); + trx.operations = {authorize_to_update_feed_producers}; + sign(trx, alice_private_key); + PUSH_TX(db, trx); + + + ////// + // Advance the blockchain to generate a distinctive hash ID for the same transaction + ////// + generate_blocks(1); + + + ////// + // Bob attempts to update the feed producers for ALICECOIN + // This should succeed because Bob is now authorized to update the feed producers + // and because the selected feed producers are acceptable + ////// + trx.clear(); + trx.operations = {producers_op}; + sign(trx, bob_private_key); + PUSH_TX(db, trx); + + + ////// + // Bob attempts to update the feed producers for ALICECOIN with 1 trusted and 1 untrusted account + // This should fail because Bob is not authorized to update the feed producers + // when an untrusted account is included + ////// + new_producers = {trusted4.get_id(), untrusted1.get_id()}; + producers_op = create_producers_op(alice.get_id(), alicecoin.id, new_producers); + trx.clear(); + trx.operations = {producers_op}; + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should indicate the rejection path + // "rejection_path":[[0,0],[2,"predicate_was_false"] + // [0,0]: 0 is the rejection_indicator for an index to a sub-restriction; 0 is the index value for Restriction 1 + // [2,"predicate_was_false"]: 0 is the rejection_indicator for rejection_reason; "predicate_was_false" is the reason + EXPECT_EXCEPTION_STRING("\"rejection_path\":[[0,0],[2,\"predicate_was_false\"]]", [&] {PUSH_TX(db, trx);}); + + + ////// + // Bob attempts to update the feed producers for ALICECOIN with 1 untrusted account + // This should fail because Bob is not authorized to update the feed producers + // when an untrusted account is included + ////// + new_producers = {trusted4.get_id(), untrusted1.get_id()}; + producers_op = create_producers_op(alice.get_id(), alicecoin.id, new_producers); + trx.clear(); + trx.operations = {producers_op}; + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should indicate the rejection path + // "rejection_path":[[0,0],[2,"predicate_was_false"] + // [0,0]: 0 is the rejection_indicator for an index to a sub-restriction; 0 is the index value for Restriction 1 + // [2,"predicate_was_false"]: 0 is the rejection_indicator for rejection_reason; "predicate_was_false" is the reason + EXPECT_EXCEPTION_STRING("\"rejection_path\":[[0,0],[2,\"predicate_was_false\"]]", [&] {PUSH_TX(db, trx);}); + + + ////// + // Bob attempts to update the feed producers for ALICECOIN with two untrusted accounts + // This should fail because Bob is not authorized to update the feed producers + // when an untrusted account is included + ////// + new_producers = {untrusted2.get_id(), untrusted3.get_id()}; + producers_op = create_producers_op(alice.get_id(), alicecoin.id, new_producers); + trx.clear(); + trx.operations = {producers_op}; + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should indicate the rejection path + // "rejection_path":[[0,0],[2,"predicate_was_false"] + // [0,0]: 0 is the rejection_indicator for an index to a sub-restriction; 0 is the index value for Restriction 1 + // [2,"predicate_was_false"]: 0 is the rejection_indicator for rejection_reason; "predicate_was_false" is the reason + EXPECT_EXCEPTION_STRING("\"rejection_path\":[[0,0],[2,\"predicate_was_false\"]]", [&] {PUSH_TX(db, trx);}); + + } FC_LOG_AND_RETHROW() + } + + + /** + * Test of has all (has_all) restriction on a container field + * Test of CAA for asset_update_feed_producers_operation + * + * Scenario: Test of authorization of one account (alice) authorizing another account (bob) + * to update an asset's feed producers as long as the list + * always includes trusted producers (trusted1, trusted2, trusted3) + */ + BOOST_AUTO_TEST_CASE(authorized_feed_producers_2) { + 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((alice)(bob)); + ACTORS((trusted1)(trusted2)(trusted3)); + ACTORS((unknown1)(unknown2)(unknown3)(unknown4)(unknown5)(unknown6)(unknown7)(unknown8)(unknown9)); + fund(alice, asset(500000 * GRAPHENE_BLOCKCHAIN_PRECISION)); + + + // Lambda for update asset feed producers + auto create_producers_op = [&](const account_id_type &issuer, const asset_id_type &asset, const flat_set &new_producers) { + asset_update_feed_producers_operation op; + + op.issuer = issuer; + op.asset_to_update = asset; + op.new_feed_producers = new_producers; + + return op; + }; + + + ////// + // Create user-issued assets + ////// + upgrade_to_lifetime_member(alice); + create_bitasset("ALICECOIN", alice.get_id()); + generate_blocks(1); + const asset_object &alicecoin = *db.get_index_type().indices().get().find("ALICECOIN"); + + + ////// + // Alice attempts to update the feed producers for ALICECOIN + // This should succeed because Alice can update her own asset + ////// + flat_set new_producers = {trusted1.get_id(), trusted2.get_id(), trusted3.get_id()}; + asset_update_feed_producers_operation producers_op + = create_producers_op(alice.get_id(), alicecoin.id, new_producers); + trx.clear(); + trx.operations = {producers_op}; + sign(trx, alice_private_key); + PUSH_TX(db, trx); + + + ////// + // Advance the blockchain to generate a distinctive hash ID for the same transaction + ////// + generate_blocks(1); + + + ////// + // Bob attempts to update the feed producers for ALICECOIN with the required feed producers + // and an extra account + // This should fail because Bob is not authorized to update feed producers for ALICECOIN + ////// + new_producers = {trusted1.get_id(), trusted2.get_id(), trusted3.get_id(), unknown1.get_id()}; + producers_op = create_producers_op(alice.get_id(), alicecoin.id, new_producers); + trx.clear(); + trx.operations = {producers_op}; + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should not indicate any rejected custom auths because no CAA applies for Bob's attempt + // "rejected_custom_auths":[] + EXPECT_EXCEPTION_STRING("\"rejected_custom_auths\":[]", [&] {PUSH_TX(db, trx);}); + + + ////// + // Alice authorizes Bob to update the feed producers + // but must not select untrusted1, untrusted2, untrusted3 + ////// + custom_authority_create_operation authorize_to_update_feed_producers; + authorize_to_update_feed_producers.account = alice.get_id(); + authorize_to_update_feed_producers.auth.add_authority(bob.get_id(), 1); + authorize_to_update_feed_producers.auth.weight_threshold = 1; + authorize_to_update_feed_producers.enabled = true; + authorize_to_update_feed_producers.valid_to = db.head_block_time() + 1000; + + authorize_to_update_feed_producers.operation_type = operation::tag::value; + flat_set trusted_producers = {trusted1.get_id(), trusted2.get_id(), trusted3.get_id()}; + auto new_feed_producers_index = member_index("new_feed_producers"); + authorize_to_update_feed_producers.restrictions + .emplace_back(new_feed_producers_index, FUNC(has_all), trusted_producers); + //[ + // { + // "member_index": 3, + // "restriction_type": 8, + // "argument": [ + // 26, + // [ + // "1.2.18", + // "1.2.19", + // "1.2.20" + // ] + // ], + // "extensions": [] + // } + //] + trx.clear(); + trx.operations = {authorize_to_update_feed_producers}; + sign(trx, alice_private_key); + PUSH_TX(db, trx); + + + ////// + // Advance the blockchain to generate a distinctive hash ID for the same transaction + ////// + generate_blocks(1); + + + ////// + // Bob attempts to update the feed producers for ALICECOIN with the required feed producers + // and an extra account + // This should succeed because Bob is now authorized to update the feed producers + // and because the all of the required feed producers are included + ////// + trx.clear(); + trx.operations = {producers_op}; + sign(trx, bob_private_key); + PUSH_TX(db, trx); + + + ////// + // Bob attempts to update the feed producers for ALICECOIN with none of the required feed producers + // This should fail not all of the required feed producers are included + ////// + new_producers = {unknown2.get_id(), unknown3.get_id()}; + producers_op = create_producers_op(alice.get_id(), alicecoin.id, new_producers); + trx.clear(); + trx.operations = {producers_op}; + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should indicate the rejection path + // "rejection_path":[[0,0],[2,"predicate_was_false"] + // [0,0]: 0 is the rejection_indicator for an index to a sub-restriction; 0 is the index value for Restriction 1 + // [2,"predicate_was_false"]: 0 is the rejection_indicator for rejection_reason; "predicate_was_false" is the reason + EXPECT_EXCEPTION_STRING("\"rejection_path\":[[0,0],[2,\"predicate_was_false\"]]", [&] {PUSH_TX(db, trx);}); + + + ////// + // Bob attempts to update the feed producers for ALICECOIN with only 1 of the required feed producers + // and extra accounts + // This should fail not all of the required feed producers are included + ////// + new_producers = {trusted1.get_id(), unknown2.get_id(), unknown3.get_id()}; + producers_op = create_producers_op(alice.get_id(), alicecoin.id, new_producers); + trx.clear(); + trx.operations = {producers_op}; + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should indicate the rejection path + // "rejection_path":[[0,0],[2,"predicate_was_false"] + // [0,0]: 0 is the rejection_indicator for an index to a sub-restriction; 0 is the index value for Restriction 1 + // [2,"predicate_was_false"]: 0 is the rejection_indicator for rejection_reason; "predicate_was_false" is the reason + EXPECT_EXCEPTION_STRING("\"rejection_path\":[[0,0],[2,\"predicate_was_false\"]]", [&] {PUSH_TX(db, trx);}); + + + ////// + // Bob attempts to update the feed producers for ALICECOIN with only 2 of the required feed producers + // and extra accounts + // This should fail not all of the required feed producers are included + ////// + new_producers = {trusted1.get_id(), unknown2.get_id(), unknown3.get_id(), trusted2.get_id()}; + producers_op = create_producers_op(alice.get_id(), alicecoin.id, new_producers); + trx.clear(); + trx.operations = {producers_op}; + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should indicate the rejection path + // "rejection_path":[[0,0],[2,"predicate_was_false"] + // [0,0]: 0 is the rejection_indicator for an index to a sub-restriction; 0 is the index value for Restriction 1 + // [2,"predicate_was_false"]: 0 is the rejection_indicator for rejection_reason; "predicate_was_false" is the reason + EXPECT_EXCEPTION_STRING("\"rejection_path\":[[0,0],[2,\"predicate_was_false\"]]", [&] {PUSH_TX(db, trx);}); + + + ////// + // Bob attempts to update the feed producers for ALICECOIN with all of the required feed producers + // and extra accounts + // This should succeed because Bob is now authorized to update the feed producers + // and because the all of the required feed producers are included + ////// + new_producers = {trusted1.get_id(), unknown2.get_id(), unknown3.get_id(), trusted2.get_id(), trusted3.get_id()}; + producers_op = create_producers_op(alice.get_id(), alicecoin.id, new_producers); + trx.clear(); + trx.operations = {producers_op}; + sign(trx, bob_private_key); + PUSH_TX(db, trx); + + ////// + // Bob attempts to update the feed producers for ALICECOIN with all of the required feed producers + // in a different order + // This should succeed because Bob is now authorized to update the feed producers + // and because the all of the required feed producers are included + ////// + new_producers = {trusted3.get_id(), trusted2.get_id(), trusted1.get_id()}; + producers_op = create_producers_op(alice.get_id(), alicecoin.id, new_producers); + trx.clear(); + trx.operations = {producers_op}; + sign(trx, bob_private_key); + PUSH_TX(db, trx); + + } FC_LOG_AND_RETHROW() + } + + + /** + * Generate a random pre-image for HTLC-related tests + */ + void generate_random_preimage(uint16_t key_size, std::vector& vec) + { + std::independent_bits_engine rbe; + std::generate(begin(vec), end(vec), std::ref(rbe)); + return; + } + + + /** + * Test of greater than or equal to (ge) restriction on a field + * Test of CAA for htlc_create_operation + * + * Scenario: Test of authorization of one account (alice) authorizing another account (bob) + * to create an HTLC operation as long as the pre-image size is greater than or equal to a specified size + * + * This test is similar to the HTLC test called "other_peoples_money" + */ + BOOST_AUTO_TEST_CASE(authorized_htlc_creation) { + try { + ////// + // Initialize the blockchain + ////// + time_point_sec LATER_HF_TIME + = (HARDFORK_BSIP_40_TIME > HARDFORK_CORE_1468_TIME) ? HARDFORK_BSIP_40_TIME : HARDFORK_CORE_1468_TIME; + generate_blocks(LATER_HF_TIME); + generate_blocks(5); + set_expiration(db, trx); + + // Initialize HTLC blockchain parameters + trx.clear(); + set_htlc_committee_parameters(); + generate_blocks(5); + + // Initialize CAA blockchain parameters + trx.clear(); + 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((alice)(bob)(gateway)); + int64_t init_balance(100 * GRAPHENE_BLOCKCHAIN_PRECISION ); + transfer( committee_account, alice_id, graphene::chain::asset(init_balance) ); + + + ////// + // Initialize: Pre-image sizes and pre-images to reduce the test variability + ////// + uint16_t pre_image_size_256 = 256; + std::vector pre_image_256(pre_image_size_256); + generate_random_preimage(pre_image_size_256, pre_image_256); + + // The minimum pre-image size that will be authorized by Alice + uint16_t authorized_minimum_pre_image_size_512 = 512; + + int64_t pre_image_size_512 = int64_t(authorized_minimum_pre_image_size_512 + 0); + std::vector pre_image_512(pre_image_size_512); + generate_random_preimage(pre_image_size_512, pre_image_512); + + int64_t pre_image_size_600 = int64_t(authorized_minimum_pre_image_size_512 + 88); + std::vector pre_image_600(pre_image_size_600); + generate_random_preimage(pre_image_size_600, pre_image_600); + + + ////// + // Alice attempts to put a contract on the blockchain using Alice's funds + // This should succeed because Alice is authorized to create HTLC for her own account + ////// + { + graphene::chain::htlc_create_operation create_operation; + create_operation.amount = graphene::chain::asset( 1 * GRAPHENE_BLOCKCHAIN_PRECISION ); + create_operation.from = alice_id; + create_operation.to = gateway_id; + create_operation.claim_period_seconds = 3; + create_operation.preimage_hash = hash_it( pre_image_256 ); + create_operation.preimage_size = pre_image_size_256; + create_operation.fee = db.current_fee_schedule().calculate_fee( create_operation ); + trx.clear(); + trx.operations.push_back(create_operation); + sign(trx, alice_private_key); + PUSH_TX( db, trx ); + } + + + ////// + // Advance the blockchain to generate distinctive hash IDs for the similar transactions + ////// + generate_blocks(1); + + + ////// + // Bob attempts to put a contract on the blockchain using Alice's funds + // This should fail because Bob is not authorized to create HTLC on behalf of Alice + ////// + { + graphene::chain::htlc_create_operation create_operation; + create_operation.amount = graphene::chain::asset( 1 * GRAPHENE_BLOCKCHAIN_PRECISION ); + create_operation.from = alice_id; + create_operation.to = gateway_id; + create_operation.claim_period_seconds = 3; + create_operation.preimage_hash = hash_it( pre_image_256 ); + create_operation.preimage_size = pre_image_size_256; + create_operation.fee = db.current_fee_schedule().calculate_fee( create_operation ); + trx.clear(); + trx.operations.push_back(create_operation); + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should not indicate any rejected custom auths because no CAA applies for Bob's attempt + // "rejected_custom_auths":[] + EXPECT_EXCEPTION_STRING("\"rejected_custom_auths\":[]", [&] {PUSH_TX(db, trx);}); + + } + + + ////// + // Alice authorizes Bob to create HTLC only to an account (gateway) + // and if the pre-image size is greater than or equal to 512 bytes + ////// + custom_authority_create_operation authorize_htlc_create; + authorize_htlc_create.account = alice.get_id(); + authorize_htlc_create.auth.add_authority(bob.get_id(), 1); + authorize_htlc_create.auth.weight_threshold = 1; + authorize_htlc_create.enabled = true; + authorize_htlc_create.valid_to = db.head_block_time() + 1000; + authorize_htlc_create.operation_type = operation::tag::value; + + auto to_index = member_index("to"); + authorize_htlc_create.restrictions.emplace_back(to_index, FUNC(eq), gateway.get_id()); + + auto preimage_size_index = member_index("preimage_size"); + authorize_htlc_create.restrictions.emplace_back(restriction(preimage_size_index, FUNC(ge), pre_image_size_512)); + //[ + // { + // "member_index": 2, + // "restriction_type": 0, + // "argument": [ + // 7, + // "1.2.18" + // ], + // "extensions": [] + // }, + // { + // "member_index": 5, + // "restriction_type": 5, + // "argument": [ + // 2, + // 512 + // ], + // "extensions": [] + // } + //] + trx.clear(); + trx.operations = {authorize_htlc_create}; + sign(trx, alice_private_key); + PUSH_TX(db, trx); + + + ////// + // Advance the blockchain to generate distinctive hash IDs for the similar transactions + ////// + generate_blocks(1); + + + ////// + // Bob attempts to put a contract on the blockchain using Alice's funds + // with a preimage size of 256. + // This should fail because Bob is not authorized to create HTLC on behalf of Alice + // if the preimage size is below the minimum value restriction. + ////// + { + graphene::chain::htlc_create_operation create_operation; + create_operation.amount = graphene::chain::asset( 1 * GRAPHENE_BLOCKCHAIN_PRECISION ); + create_operation.from = alice_id; + create_operation.to = gateway_id; + create_operation.claim_period_seconds = 3; + create_operation.preimage_hash = hash_it( pre_image_256 ); + create_operation.preimage_size = pre_image_size_256; + create_operation.fee = db.current_fee_schedule().calculate_fee( create_operation ); + trx.clear(); + trx.operations.push_back(create_operation); + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should indicate the rejection path + // "rejection_path":[[0,1],[2,"predicate_was_false"] + // [0,1]: 0 is the rejection_indicator for an index to a sub-restriction; 1 is the index value for Restriction 2 + // [2,"predicate_was_false"]: 0 is the rejection_indicator for rejection_reason; "predicate_was_false" is the reason + EXPECT_EXCEPTION_STRING("\"rejection_path\":[[0,1],[2,\"predicate_was_false\"]]", [&] {PUSH_TX(db, trx);}); + + } + + ////// + // Bob attempts to put a contract on the blockchain using Alice's funds + // with a preimage size of 512. + // This should succeed because Bob is authorized to create HTLC on behalf of Alice + // and the preimage size equals the minimum value restriction. + ////// + { + graphene::chain::htlc_create_operation create_operation; + create_operation.amount = graphene::chain::asset( 1 * GRAPHENE_BLOCKCHAIN_PRECISION ); + create_operation.from = alice_id; + create_operation.to = gateway_id; + create_operation.claim_period_seconds = 3; + create_operation.preimage_hash = hash_it( pre_image_512 ); + create_operation.preimage_size = pre_image_size_512; + create_operation.fee = db.current_fee_schedule().calculate_fee( create_operation ); + trx.clear(); + trx.operations.push_back(create_operation); + sign(trx, bob_private_key); + PUSH_TX( db, trx ); + + } + + + ////// + // Bob attempts to put a contract on the blockchain using Alice's funds + // with a preimage size of 600. + // This should succeed because Bob is authorized to create HTLC on behalf of Alice + // and the preimage size is greater than the minimum value restriction. + ////// + { + graphene::chain::htlc_create_operation create_operation; + create_operation.amount = graphene::chain::asset( 1 * GRAPHENE_BLOCKCHAIN_PRECISION ); + create_operation.from = alice_id; + create_operation.to = gateway_id; + create_operation.claim_period_seconds = 3; + create_operation.preimage_hash = hash_it( pre_image_600 ); + create_operation.preimage_size = pre_image_size_600; + create_operation.fee = db.current_fee_schedule().calculate_fee( create_operation ); + trx.clear(); + trx.operations.push_back(create_operation); + sign(trx, bob_private_key); + PUSH_TX( db, trx ); + + } + + } FC_LOG_AND_RETHROW() + } + + + /** + * Test of vector field size comparison + * Test of CAA for htlc_redeem_operation + * + * Scenario: Test of authorization of one account (gateway) authorizing another account (bob) + * to redeem an HTLC operation + */ + BOOST_AUTO_TEST_CASE(authorized_htlc_redeem) { + try { + ////// + // Initialize the blockchain + ////// + time_point_sec LATER_HF_TIME + = (HARDFORK_BSIP_40_TIME > HARDFORK_CORE_1468_TIME) ? HARDFORK_BSIP_40_TIME : HARDFORK_CORE_1468_TIME; + generate_blocks(LATER_HF_TIME); + generate_blocks(5); + set_expiration(db, trx); + + // Initialize HTLC blockchain parameters + trx.clear(); + set_htlc_committee_parameters(); + generate_blocks(5); + + // Initialize CAA blockchain parameters + trx.clear(); + db.modify(global_property_id_type()(db), [](global_property_object &gpo) { + gpo.parameters.extensions.value.custom_authority_options = custom_authority_options_type(); + }); + + // Update the expiration of the re-usable trx relative to the head block time + set_expiration(db, trx); + + + ////// + // Initialize: Accounts + ////// + ACTORS((alice)(bob)(gateway)); + int64_t init_balance(1000 * GRAPHENE_BLOCKCHAIN_PRECISION ); + transfer( committee_account, alice_id, graphene::chain::asset(init_balance) ); + int64_t init_gateway_balance(50 * GRAPHENE_BLOCKCHAIN_PRECISION); + transfer( committee_account, gateway_id, graphene::chain::asset(init_gateway_balance) ); + + + ////// + // Initialize: Pre-image sizes and pre-images to reduce the test variability + ////// + uint16_t pre_image_size_256 = 256; + std::vector pre_image_256(pre_image_size_256); + generate_random_preimage(pre_image_size_256, pre_image_256); + + + ////// + // Gateway puts a contract on the blockchain + // This should succeed because the gateway is authorized to create HTLC for its own account + ////// + share_type htlc_amount = 25 * GRAPHENE_BLOCKCHAIN_PRECISION; + { + graphene::chain::htlc_create_operation create_operation; + create_operation.amount = graphene::chain::asset( htlc_amount ); + create_operation.from = alice_id; + create_operation.to = gateway_id; + create_operation.claim_period_seconds = 86400; + create_operation.preimage_hash = hash_it( pre_image_256 ); + create_operation.preimage_size = pre_image_size_256; + create_operation.fee = db.current_fee_schedule().calculate_fee( create_operation ); + trx.clear(); + trx.operations.push_back(create_operation); + sign(trx, alice_private_key); + PUSH_TX( db, trx ); + } + + + ////// + // Advance the blockchain to get the finalized HTLC ID + ////// + generate_blocks(1); + graphene::chain::htlc_id_type alice_htlc_id = + db.get_index_type().indices().get().find(alice.get_id())->id; + + + ////// + // Bob attempts to redeem the HTLC on behalf of the gateway + // This should fail because Bob is not authorized to redeem on behalf of the gateway + ////// + graphene::chain::htlc_redeem_operation redeem_operation; + { + redeem_operation.redeemer = gateway_id; + redeem_operation.htlc_id = alice_htlc_id; + redeem_operation.preimage = pre_image_256; + redeem_operation.fee = db.current_fee_schedule().calculate_fee( redeem_operation ); + trx.clear(); + trx.operations.push_back(redeem_operation); + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should not indicate any rejected custom auths because no CAA applies for Bob's attempt + // "rejected_custom_auths":[] + EXPECT_EXCEPTION_STRING("\"rejected_custom_auths\":[]", [&] {PUSH_TX(db, trx);}); + + } + + + ////// + // Gateway authorizes Bob to redeem an HTLC + // only if the preimage length equals 200 bytes + // This length is incompatible with the HTLC pre-image that is already on the blockchain + ////// + custom_authority_create_operation authorize_htlc_redeem; + authorize_htlc_redeem.account = gateway.get_id(); + authorize_htlc_redeem.auth.add_authority(bob.get_id(), 1); + authorize_htlc_redeem.auth.weight_threshold = 1; + authorize_htlc_redeem.enabled = true; + authorize_htlc_redeem.valid_to = db.head_block_time() + 1000; + authorize_htlc_redeem.operation_type = operation::tag::value; + + auto preimage_index = member_index("preimage"); + authorize_htlc_redeem.restrictions.emplace_back(preimage_index, FUNC(eq), int64_t(200)); + //[ + // { + // "member_index": 3, + // "restriction_type": 0, + // "argument": [ + // 2, + // 200 + // ], + // "extensions": [] + // } + //] + + trx.clear(); + trx.operations = {authorize_htlc_redeem}; + sign(trx, gateway_private_key); + PUSH_TX(db, trx); + + + ////// + // Advance the blockchain to get the finalized CAA ID + ////// + generate_blocks(1); + auto caa = db.get_index_type().indices().get().find(gateway.get_id()); + custom_authority_id_type caa_id = caa->id; + + + ////// + // Bob attempts to redeem the HTLC + // This should fail because the authorization's restriction prohibits the redemption of this HTLC + ////// + { + trx.clear(); + trx.operations.push_back(redeem_operation); + sign(trx, bob_private_key); + + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should indicate the rejection path + // [0,0]: 0 is the rejection_indicator for an index to a sub-restriction; 0 is the index value for Restriction 1 + // [2,"predicate_was_false"]: 0 is the rejection_indicator for rejection_reason; "predicate_was_false" is the reason + EXPECT_EXCEPTION_STRING("\"rejection_path\":[[0,0],[2,\"predicate_was_false\"]]", [&] {PUSH_TX(db, trx);}); + + } + + + ////// + // Advance the blockchain to generate distinctive hash IDs for the similar transactions + ////// + generate_blocks(1); + + + ////// + // Gateway updates the authorization for to redeem an HTLC + // only if the preimage length equals 256 bytes + // This length is compatible with the HTLC pre-image that is already on the blockchain + ////// + custom_authority_update_operation update_authorization; + update_authorization.account = gateway.get_id(); + update_authorization.authority_to_update = caa_id; + uint16_t existing_restriction_index = 0; // The 0-based index of the first and only existing restriction + update_authorization.restrictions_to_remove = {existing_restriction_index}; + update_authorization.restrictions_to_add = + {restriction(preimage_index, FUNC(eq), int64_t(pre_image_size_256))}; + trx.clear(); + trx.operations = {update_authorization}; + sign(trx, gateway_private_key); + PUSH_TX(db, trx); + + + ////// + // Bob attempts to redeem the HTLC + // This should succeed because the redemption satisfies the authorization + ////// + { + trx.clear(); + trx.operations.push_back(redeem_operation); + sign(trx, bob_private_key); + PUSH_TX(db, trx); + + } + + } FC_LOG_AND_RETHROW() + } + + + /** + * Test of greater than (gt) and less than or equal to (le) restriction on a field + * Test of CAA for htlc_extend_operation + * + * Scenario: Test of authorization of one account (alice) authorizing another account (bob) + * to extend an HTLC operation as long as the extension is within a specified duration + */ + BOOST_AUTO_TEST_CASE(authorized_htlc_extension) { + try { + ////// + // Initialize the blockchain + ////// + time_point_sec LATER_HF_TIME + = (HARDFORK_BSIP_40_TIME > HARDFORK_CORE_1468_TIME) ? HARDFORK_BSIP_40_TIME : HARDFORK_CORE_1468_TIME; + generate_blocks(LATER_HF_TIME); + generate_blocks(5); + set_expiration(db, trx); + + // Initialize HTLC blockchain parameters + trx.clear(); + set_htlc_committee_parameters(); + generate_blocks(5); + + // Initialize CAA blockchain parameters + trx.clear(); + 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((alice)(bob)(gateway)); + int64_t init_balance(1000 * GRAPHENE_BLOCKCHAIN_PRECISION ); + transfer( committee_account, alice_id, graphene::chain::asset(init_balance) ); + int64_t init_gateway_balance(50 * GRAPHENE_BLOCKCHAIN_PRECISION); + transfer( committee_account, gateway_id, graphene::chain::asset(init_gateway_balance) ); + + + ////// + // Initialize: Pre-image sizes and pre-images to reduce the test variability + ////// + uint16_t pre_image_size_256 = 256; + std::vector pre_image_256(pre_image_size_256); + generate_random_preimage(pre_image_size_256, pre_image_256); + + + ////// + // Gateway puts a contract on the blockchain + // This should succeed because the gateway is authorized to create HTLC for its own account + ////// + share_type htlc_amount = 25 * GRAPHENE_BLOCKCHAIN_PRECISION; + { + graphene::chain::htlc_create_operation create_operation; + create_operation.amount = graphene::chain::asset( htlc_amount ); + create_operation.from = alice_id; + create_operation.to = gateway_id; + create_operation.claim_period_seconds = 86400; + create_operation.preimage_hash = hash_it( pre_image_256 ); + create_operation.preimage_size = pre_image_size_256; + create_operation.fee = db.current_fee_schedule().calculate_fee( create_operation ); + trx.clear(); + trx.operations.push_back(create_operation); + sign(trx, alice_private_key); + PUSH_TX( db, trx ); + } + + + ////// + // Advance the blockchain to get the finalized HTLC ID + ////// + generate_blocks(1); + graphene::chain::htlc_id_type alice_htlc_id = + db.get_index_type().indices().get().find(alice.get_id())->id; + + + ////// + // Bob attempts to extend the HTLC + // This should fail because Bob is not authorized to extend an HTLC on behalf of Alice + ////// + graphene::chain::htlc_extend_operation extend_operation; + { + extend_operation.update_issuer = alice_id; + extend_operation.htlc_id = alice_htlc_id; + extend_operation.seconds_to_add = int64_t(24 * 3600); + extend_operation.fee = db.current_fee_schedule().calculate_fee( extend_operation ); + trx.clear(); + trx.operations.push_back(extend_operation); + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should not indicate any rejected custom auths because no CAA applies for Bob's attempt + // "rejected_custom_auths":[] + EXPECT_EXCEPTION_STRING("\"rejected_custom_auths\":[]", [&] {PUSH_TX(db, trx);}); + + } + + + ////// + // Alice authorizes Bob to extend an HTLC + // by greater than 1 hour and less than or equal to 24 hours + ////// + custom_authority_create_operation authorize_htlc_extension; + authorize_htlc_extension.account = alice.get_id(); + authorize_htlc_extension.auth.add_authority(bob.get_id(), 1); + authorize_htlc_extension.auth.weight_threshold = 1; + authorize_htlc_extension.enabled = true; + authorize_htlc_extension.valid_to = db.head_block_time() + 1000; + authorize_htlc_extension.operation_type = operation::tag::value; + + // Authorization to extend is restricted to greater than 1 hour and less than or equal to 24 hours + vector restrictions; + auto extension_duration_index = member_index("seconds_to_add"); + // Duration extension greater than one hour + restriction restriction_gt_duration = restriction(extension_duration_index, FUNC(gt), int64_t(1 * 3600)); + restrictions.emplace_back(restriction_gt_duration); + // Duration extension less than or equal to 24 hours + restriction restriction_le_duration = restriction(extension_duration_index, FUNC(le), int64_t(24 * 3600)); + restrictions.emplace_back(restriction_le_duration); + authorize_htlc_extension.restrictions = restrictions; + //[ + // { + // "member_index": 3, + // "restriction_type": 4, + // "argument": [ + // 2, + // 3600 + // ], + // "extensions": [] + // }, + // { + // "member_index": 3, + // "restriction_type": 3, + // "argument": [ + // 2, + // 86400 + // ], + // "extensions": [] + // } + //] + trx.clear(); + trx.operations = {authorize_htlc_extension}; + sign(trx, alice_private_key); + PUSH_TX(db, trx); + + + ////// + // Advance the blockchain to generate distinctive hash IDs for the similar transactions + ////// + generate_blocks(1); + + + ////// + // Bob attempts to extend the HTLC + // This should succeed because Bob is conditionally authorized to extend + ////// + { + trx.clear(); + trx.operations.push_back(extend_operation); + sign(trx, bob_private_key); + PUSH_TX(db, trx); + + } + + + ////// + // Bob attempts to extend the HTLC by exactly 10 hours + // This should succeed because Bob is authorized to extend the HTLC + // if greater than 1 hour and less than or equal to 24 hours + ////// + { + extend_operation = htlc_extend_operation(); + extend_operation.update_issuer = alice_id; + extend_operation.htlc_id = alice_htlc_id; + extend_operation.seconds_to_add = int64_t(10 * 3600); + extend_operation.fee = db.current_fee_schedule().calculate_fee( extend_operation ); + trx.clear(); + trx.operations.push_back(extend_operation); + sign(trx, bob_private_key); + PUSH_TX(db, trx); + + } + + + ////// + // Bob attempts to extend the HTLC by exactly 1 hour + // This should fail because Bob is authorized to extend the HTLC + // if greater than 1 hour and less than or equal to 24 hours + ////// + { + extend_operation = htlc_extend_operation(); + extend_operation.update_issuer = alice_id; + extend_operation.htlc_id = alice_htlc_id; + extend_operation.seconds_to_add = int64_t(1 * 3600); + extend_operation.fee = db.current_fee_schedule().calculate_fee( extend_operation ); + trx.clear(); + trx.operations.push_back(extend_operation); + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should indicate the rejection path + // "rejection_path":[[0,0],[2,"predicate_was_false"] + // [0,0]: 0 is the rejection_indicator for an index to a sub-restriction; 0 is the index value for Restriction 1 + // [2,"predicate_was_false"]: 0 is the rejection_indicator for rejection_reason; "predicate_was_false" is the reason + EXPECT_EXCEPTION_STRING("\"rejection_path\":[[0,0],[2,\"predicate_was_false\"]]", [&] {PUSH_TX(db, trx);}); + + } + + + ////// + // Bob attempts to extend the HTLC by 24 hours plus 1 second + // This should fail because Bob is authorized to extend the HTLC + // if greater than 1 hour and less than or equal to 24 hours + ////// + { + extend_operation = htlc_extend_operation(); + extend_operation.update_issuer = alice_id; + extend_operation.htlc_id = alice_htlc_id; + extend_operation.seconds_to_add = int64_t( (24 * 3600) + 1); + extend_operation.fee = db.current_fee_schedule().calculate_fee( extend_operation ); + trx.clear(); + trx.operations.push_back(extend_operation); + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should indicate the rejection path + // "rejection_path":[[0,1],[2,"predicate_was_false"] + // [0,1]: 0 is the rejection_indicator for an index to a sub-restriction; 1 is the index value for Restriction 2 + // [2,"predicate_was_false"]: 0 is the rejection_indicator for rejection_reason; "predicate_was_false" is the reason + EXPECT_EXCEPTION_STRING("\"rejection_path\":[[0,1],[2,\"predicate_was_false\"]]", [&] {PUSH_TX(db, trx);}); + + } + + } FC_LOG_AND_RETHROW() + } + + + /** + * Test of variant assert (variant_assert) restriction on a field + * Test of CAA for vesting_balance_create_operation + * + * Scenario: Test of authorization of one account (alice) authorizing another account (bob) + * to create a coins-day vesting balance with a vesting duration of 800,000 seconds + */ + BOOST_AUTO_TEST_CASE(authorized_vesting_balance_create) { + 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((alice)(bob)(charlie)); + fund(alice, asset(500000 * GRAPHENE_BLOCKCHAIN_PRECISION)); + + + ////// + // Bob attempts to create a coins-day vesting balance for Alice + // This attempt should fail because Alice has not authorized Bob to create a vesting balance + ////// + vesting_balance_create_operation original_vb_op; + time_point_sec policy_start_time = db.head_block_time() + 86400; + { + vesting_balance_create_operation vb_op; + vb_op.creator = alice_id; + vb_op.owner = charlie_id; + vb_op.amount = graphene::chain::asset(60000); + vb_op.policy = cdd_vesting_policy_initializer(800000, policy_start_time); + vb_op.fee = db.current_fee_schedule().calculate_fee(vb_op); + trx.clear(); + trx.operations.push_back(vb_op); + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should not indicate any rejected custom auths because no CAA applies for Bob's attempt + // "rejected_custom_auths":[] + EXPECT_EXCEPTION_STRING("\"rejected_custom_auths\":[]", [&] {PUSH_TX(db, trx);}); + + original_vb_op = vb_op; + } + + + ////// + // Alice authorizes Bob to create a coins-day vesting balance from her funds + // only if the vesting duration equals 800,000 seconds + ////// + custom_authority_create_operation authorize_create_vesting; + authorize_create_vesting.account = alice.get_id(); + authorize_create_vesting.auth.add_authority(bob.get_id(), 1); + authorize_create_vesting.auth.weight_threshold = 1; + authorize_create_vesting.enabled = true; + authorize_create_vesting.valid_to = db.head_block_time() + 1000; + authorize_create_vesting.operation_type = operation::tag::value; + + // Restrict authorization to a coin-days vesting policy with a vesting duration of 800000 seconds + auto policy_index = member_index("policy"); + int64_t policy_tag = vesting_policy_initializer::tag::value; + auto vesting_seconds_index = member_index("vesting_seconds"); + vector policy_restrictions = {restriction(vesting_seconds_index, FUNC(eq), int64_t(800000))}; + pair> policy_argument(policy_tag, policy_restrictions); + authorize_create_vesting.restrictions = {restriction(policy_index, FUNC(variant_assert), policy_argument)}; + //[ + // { + // "member_index": 4, + // "restriction_type": 12, + // "argument": [ + // 41, + // [ + // 1, + // [ + // { + // "member_index": 1, + // "restriction_type": 0, + // "argument": [ + // 2, + // 800000 + // ], + // "extensions": [] + // } + // ] + // ] + // ], + // "extensions": [] + // } + //] + trx.clear(); + trx.operations = {authorize_create_vesting}; + sign(trx, alice_private_key); + PUSH_TX(db, trx); + + + ////// + // Advance the blockchain to generate distinctive hash IDs for the similar transactions + ////// + generate_blocks(1); + + + ////// + // Bob attempts to create a coins-day vesting balance for Alice with a vesting duration of 86400 seconds + // This attempt should fail because Alice has not authorized this duration + ////// + { + vesting_balance_create_operation vb_op; + vb_op.creator = alice_id; + vb_op.owner = charlie_id; + vb_op.amount = graphene::chain::asset(60000); + vb_op.policy = cdd_vesting_policy_initializer(86400, policy_start_time); + vb_op.fee = db.current_fee_schedule().calculate_fee(vb_op); + trx.clear(); + trx.operations.push_back(vb_op); + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should indicate the rejection path + // "rejection_path":[[0,0],[0,0],[2,"predicate_was_false"] + // [0,0]: 0 is the rejection_indicator for an index to a sub-restriction; 0 is the index value for Restriction 1 + // [0,0]: 0 is the rejection_indicator for an index to a sub-restriction; 0 is the index value for the only argument + // [2,"predicate_was_false"]: 0 is the rejection_indicator for rejection_reason; "predicate_was_false" is the reason + EXPECT_EXCEPTION_STRING("\"rejection_path\":[[0,0],[0,0],[2,\"predicate_was_false\"]", [&] {PUSH_TX(db, trx);}); + + } + + + ////// + // Bob attempts to create a linear vesting balance for Alice + // This attempt should fail because Alice has not authorized this type of vesting balance creation + ////// + { + vesting_balance_create_operation vb_op; + vb_op.creator = alice_id; + vb_op.owner = charlie_id; + vb_op.amount = graphene::chain::asset(60000); + linear_vesting_policy_initializer policy; + policy.begin_timestamp = policy_start_time; + policy.vesting_cliff_seconds = 800000; + policy.vesting_duration_seconds = 40000; + vb_op.policy = policy; + vb_op.fee = db.current_fee_schedule().calculate_fee(vb_op); + trx.clear(); + trx.operations.push_back(vb_op); + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should indicate the rejection path + // "rejection_path":[[0,0],[2,"incorrect_variant_type"] + // [0,0]: 0 is the rejection_indicator for an index to a sub-restriction; 0 is the index value for Restriction 1 + // [2,"incorrect_variant_type"]: 0 is the rejection_indicator for rejection_reason; "incorrect_variant_type" is the reason + EXPECT_EXCEPTION_STRING("\"rejection_path\":[[0,0],[2,\"incorrect_variant_type\"]", [&] {PUSH_TX(db, trx);}); + + } + + + ////// + // Bob attempts to create a coins-day vesting balance for Alice with a vesting duration of 800000 seconds + // This attempt should succeed because Alice has authorized authorized this type of vesting balance creation + // with this duration + ////// + { + trx.clear(); + trx.operations.push_back(original_vb_op); + sign(trx, bob_private_key); + PUSH_TX(db, trx); + + } + + } FC_LOG_AND_RETHROW() + } + + + /** + * Test of time restrictions on CAA + * Test of CAA for vesting_balance_withdraw_operation + * + * Scenario: Test of authorization of one account (alice) authorizing another account (bob) + * to withdraw vesting for a limited duration + */ + BOOST_AUTO_TEST_CASE(authorized_time_restrictions_1) { + 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((alice)(bob)(charlie)); + fund(charlie, asset(500000 * GRAPHENE_BLOCKCHAIN_PRECISION)); + + + ////// + // Charlie creates an instant vesting balance for Alice + ////// + vesting_balance_create_operation original_vb_op; + time_point_sec policy_start_time = db.head_block_time() + 86400; + vesting_balance_create_operation vb_op; + vb_op.creator = charlie_id; + vb_op.owner = alice_id; + vb_op.amount = graphene::chain::asset(60000); + vb_op.policy = instant_vesting_policy_initializer(); + vb_op.fee = db.current_fee_schedule().calculate_fee(vb_op); + trx.clear(); + trx.operations.push_back(vb_op); + sign(trx, charlie_private_key); + PUSH_TX(db, trx); + + + ////// + // Advance the blockchain to before withdrawal of vesting balance can start + ////// + generate_blocks(1); + set_expiration(db, trx); + vesting_balance_id_type vesting_balance_id = + db.get_index_type().indices().get().find(alice.get_id())->id; + + + ////// + // Bob attempts to withdraw some of the vesting balance on behalf of Alice + // This attempt should fail because Alice has not authorized Bob + ////// + { + asset partial_amount = asset(10000); + + vesting_balance_withdraw_operation vb_op; + vb_op.vesting_balance = vesting_balance_id; + vb_op.owner = alice_id; + vb_op.amount = partial_amount; + vb_op.fee = db.current_fee_schedule().calculate_fee(vb_op); + trx.clear(); + trx.operations.push_back(vb_op); + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should not indicate any rejected custom auths because no CAA applies for Bob's attempt + // "rejected_custom_auths":[] + EXPECT_EXCEPTION_STRING("\"rejected_custom_auths\":[]", [&] {PUSH_TX(db, trx);}); + + } + + + ////// + // Alice authorizes Bob to withdraw her vesting balance + ////// + custom_authority_create_operation authorize_create_vesting; + authorize_create_vesting.account = alice.get_id(); + authorize_create_vesting.auth.add_authority(bob.get_id(), 1); + authorize_create_vesting.auth.weight_threshold = 1; + authorize_create_vesting.enabled = true; + // Authorization is valid only for 3/5 of the maximum duration of a custom authority + time_point_sec authorization_end_time = policy_start_time + (GRAPHENE_DEFAULT_MAX_CUSTOM_AUTHORITY_LIFETIME_SECONDS * 3 / 5); + time_point_sec authorization_before_end_time = policy_start_time + (GRAPHENE_DEFAULT_MAX_CUSTOM_AUTHORITY_LIFETIME_SECONDS * 1 / 5); + authorize_create_vesting.valid_to = authorization_end_time; + authorize_create_vesting.operation_type = operation::tag::value; + trx.clear(); + trx.operations = {authorize_create_vesting}; + sign(trx, alice_private_key); + PUSH_TX(db, trx); + + + ////// + // Advance the blockchain to before the authorization expires + ////// + generate_blocks(authorization_before_end_time); + set_expiration(db, trx); + + + ////// + // Bob attempts to withdraw the available vesting balance for Alice + // This attempt should succeed because the authorization is active + ////// + { + asset partial_amount = asset(10000); + + vesting_balance_withdraw_operation vb_op; + vb_op.vesting_balance = vesting_balance_id; + vb_op.owner = alice_id; + vb_op.amount = partial_amount; + vb_op.fee = db.current_fee_schedule().calculate_fee(vb_op); + trx.clear(); + trx.operations.push_back(vb_op); + sign(trx, bob_private_key); + PUSH_TX(db, trx); + + } + + + ////// + // Advance the blockchain to after the authorization expires + ////// + time_point_sec after_authorization_end_time = authorization_end_time + 86400; + generate_blocks(after_authorization_end_time); + set_expiration(db, trx); + + + ////// + // Bob attempts to withdraw the available vesting balance for Alice + // This attempt should fail because the authorization has expired + ////// + { + asset partial_amount = asset(10000); + + vesting_balance_withdraw_operation vb_op; + vb_op.vesting_balance = vesting_balance_id; + vb_op.owner = alice_id; + vb_op.amount = partial_amount; + vb_op.fee = db.current_fee_schedule().calculate_fee(vb_op); + trx.clear(); + trx.operations.push_back(vb_op); + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should not indicate any rejected custom auths because no CAA applies for Bob's attempt + // "rejected_custom_auths":[] + EXPECT_EXCEPTION_STRING("\"rejected_custom_auths\":[]", [&] {PUSH_TX(db, trx);}); + + } + + } FC_LOG_AND_RETHROW() + } + + + /** + * Test of time restrictions on CAA + * Test of CAA for call_order_update_operation + * + * Scenario: Test of authorization of one account (alice) authorizing another account (bob) + * to update a call order only during a specfied time interval + */ + BOOST_AUTO_TEST_CASE(authorized_time_restrictions_2) { + 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)(alice)(bob)); + 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 + create_bitasset("USDBIT", feedproducer_id); + generate_blocks(1); + const asset_object &bitusd + = *db.get_index_type().indices().get().find("USDBIT"); + asset_id_type usd_id = bitusd.id; + + // Configure the smart asset + 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 alice with core asset + ////// + fund(alice, asset(init_balance)); + // alice will borrow 1000 bitUSD + borrow(alice, bitusd.amount(1000), asset(15000)); + int64_t alice_balance_usd_before_offer = get_balance(alice_id, usd_id); + BOOST_CHECK_EQUAL( 1000, alice_balance_usd_before_offer); + int64_t alice_balance_core_before_offer = get_balance(alice_id, core_id); + BOOST_CHECK_EQUAL( init_balance - 15000, alice_balance_core_before_offer ); + + + ////// + // Alice updates the collateral for the Alice debt position + ////// + { + call_order_update_operation op; + op.funding_account = alice_id; + op.delta_collateral = asset(1000); + op.delta_debt = asset(0, usd_id); + trx.clear(); + trx.operations.push_back(op); + sign(trx, alice_private_key); + PUSH_TX(db, trx); + } + + + ////// + // Bob attempts to update the collateral for Alice's debt position + // This attempt should fail because Bob is not authorized by Alice + ////// + { + call_order_update_operation op; + op.funding_account = alice_id; + op.delta_collateral = asset(2000); + op.delta_debt = asset(0, usd_id); + trx.clear(); + trx.operations.push_back(op); + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should not indicate any rejected custom auths because no CAA applies for Bob's attempt + // "rejected_custom_auths":[] + EXPECT_EXCEPTION_STRING("\"rejected_custom_auths\":[]", [&] {PUSH_TX(db, trx);}); + } + + + ////// + // Alice authorizes Bob to update her call order + ////// + custom_authority_create_operation authorize_call_order_update; + authorize_call_order_update.account = alice.get_id(); + authorize_call_order_update.auth.add_authority(bob.get_id(), 1); + authorize_call_order_update.auth.weight_threshold = 1; + authorize_call_order_update.enabled = true; + // Authorization is valid only for 2/5 of the maximum duration of a custom authority + time_point_sec before_authorization_start_time = + db.head_block_time() + (GRAPHENE_DEFAULT_MAX_CUSTOM_AUTHORITY_LIFETIME_SECONDS * 1 / 5); + time_point_sec authorization_start_time = + db.head_block_time() + (GRAPHENE_DEFAULT_MAX_CUSTOM_AUTHORITY_LIFETIME_SECONDS * 2 / 5); + time_point_sec authorization_middle_time = + db.head_block_time() + (GRAPHENE_DEFAULT_MAX_CUSTOM_AUTHORITY_LIFETIME_SECONDS * 3 / 5); + time_point_sec authorization_end_time = + db.head_block_time() + (GRAPHENE_DEFAULT_MAX_CUSTOM_AUTHORITY_LIFETIME_SECONDS * 4 / 5); + time_point_sec after_authorization_end_time = authorization_end_time + 86400; + authorize_call_order_update.valid_from = authorization_start_time; + authorize_call_order_update.valid_to = authorization_end_time; + authorize_call_order_update.operation_type = operation::tag::value; + trx.clear(); + trx.operations = {authorize_call_order_update}; + sign(trx, alice_private_key); + PUSH_TX(db, trx); + + + ////// + // Advance the blockchain to before the authorization starts + ////// + generate_blocks(before_authorization_start_time); + set_expiration(db, trx); + publish_feed(bitusd, feedproducer, current_feed); // Update the price feed + + + ////// + // Bob attempts to update the collateral for Alice's debt position + // This attempt should fail because authorization is not yet active + ////// + { + call_order_update_operation op; + op.funding_account = alice_id; + op.delta_collateral = asset(3000); + op.delta_debt = asset(0, usd_id); + trx.clear(); + trx.operations.push_back(op); + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should not indicate any rejected custom auths because the CAA is not yet active + // "rejected_custom_auths":[] + EXPECT_EXCEPTION_STRING("\"rejected_custom_auths\":[]", [&] {PUSH_TX(db, trx);}); + } + + + ////// + // Advance the blockchain to the start of the authorization period + ////// + generate_blocks(authorization_start_time); + set_expiration(db, trx); + publish_feed(bitusd, feedproducer, current_feed); // Update the price feed + + ////// + // Bob attempts to update the collateral for Alice's debt position + // This attempt should succeed because the Alice authorization is active + ////// + { + call_order_update_operation op; + op.funding_account = alice_id; + op.delta_collateral = asset(4000); + op.delta_debt = asset(0, usd_id); + trx.clear(); + trx.operations.push_back(op); + sign(trx, bob_private_key); + PUSH_TX(db, trx); + } + + + ////// + // Advance the blockchain to the end of the authorization period + ////// + generate_blocks(authorization_middle_time); + set_expiration(db, trx); + publish_feed(bitusd, feedproducer, current_feed); // Update the price feed + + + ////// + // Bob attempts to update the collateral for Alice's debt position + // This attempt should succeed because the Alice authorization is active + ////// + { + call_order_update_operation op; + op.funding_account = alice_id; + op.delta_collateral = asset(5000); + op.delta_debt = asset(0, usd_id); + trx.clear(); + trx.operations.push_back(op); + sign(trx, bob_private_key); + PUSH_TX(db, trx); + } + + + ////// + // Advance the blockchain to after the authorization expires + ////// + generate_blocks(after_authorization_end_time); + set_expiration(db, trx); + publish_feed(bitusd, feedproducer, current_feed); // Update the price feed + + + ////// + // Bob attempts to update the collateral for Alice's debt position + // This attempt should fail because the authorization has expired + ////// + { + call_order_update_operation op; + op.funding_account = alice_id; + op.delta_collateral = asset(6000); + op.delta_debt = asset(0, usd_id); + trx.clear(); + trx.operations.push_back(op); + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should not indicate any rejected custom auths because no CAA applies for Bob's attempt + // "rejected_custom_auths":[] + EXPECT_EXCEPTION_STRING("\"rejected_custom_auths\":[]", [&] {PUSH_TX(db, trx);}); + } + + } FC_LOG_AND_RETHROW() + } + + + /** + * Test of time restrictions on CAA + * Test of CAA for asset_reserve_operation + * Test of CAA in a proposed operation + * + * Scenario: Test of authorization of one account (alice) authorizing another account (bob) + * to reserve (burn) an asset only during a specfied timespan + */ + BOOST_AUTO_TEST_CASE(authorized_time_restrictions_3) { + try { + ////// + // Initialize: Accounts + ////// + ACTORS((assetissuer)(feedproducer)(alice)(bob)(charlie)); + int64_t init_balance(100 * GRAPHENE_BLOCKCHAIN_PRECISION); + fund(alice, asset(init_balance)); + + + // Lambda for issuing an asset to an account + auto issue_amount_to = [&](const account_id_type &issuer, const asset &amount, const account_id_type &to) { + asset_issue_operation op; + op.issuer = issuer; + op.asset_to_issue = amount; + op.issue_to_account = to; + + return op; + }; + + // Lambda for reserving an asset from an account + auto reserve_asset = [&](const account_id_type &reserver, const asset &amount) { + asset_reserve_operation op; + op.payer = reserver; + op.amount_to_reserve = amount; + + return op; + }; + + + ////// + // Initialize: Create user-issued assets + ////// + upgrade_to_lifetime_member(assetissuer); + create_user_issued_asset("SPECIALCOIN", assetissuer, UIA_ASSET_ISSUER_PERMISSION_MASK); + generate_blocks(1); + const asset_object &specialcoin + = *db.get_index_type().indices().get().find("SPECIALCOIN"); + + + ////// + // Initialize: assetissuer issues SPECIALCOIN to different accounts + ////// + asset_issue_operation issue_special_to_alice_op + = issue_amount_to(assetissuer.get_id(), asset(1000, specialcoin.id), alice.get_id()); + asset_issue_operation issue_special_to_charlie_op + = issue_amount_to(assetissuer.get_id(), asset(2000, specialcoin.id), charlie.get_id()); + trx.clear(); + trx.operations = {issue_special_to_alice_op, issue_special_to_charlie_op}; + sign(trx, assetissuer_private_key); + PUSH_TX(db, trx); + + + ////// + // Alice reserves some SPECIALCOIN from her account + ////// + asset_reserve_operation reserve_op = reserve_asset(alice.get_id(), asset(200, specialcoin.id)); + trx.clear(); + trx.operations = {reserve_op}; + sign(trx, alice_private_key); + PUSH_TX(db, trx); + int64_t allowed1_balance_specialcoin_after_override1 = get_balance(alice.get_id(), specialcoin.id); + BOOST_CHECK_EQUAL(allowed1_balance_specialcoin_after_override1, 800); + + + ////// + // Charlie reserves some SPECIALCOIN from his account + ////// + reserve_op = reserve_asset(charlie.get_id(), asset(200, specialcoin.id)); + trx.clear(); + trx.operations = {reserve_op}; + sign(trx, charlie_private_key); + PUSH_TX(db, trx); + int64_t charlie_balance_specialcoin_after_override1 = get_balance(charlie.get_id(), specialcoin.id); + BOOST_CHECK_EQUAL(charlie_balance_specialcoin_after_override1, 1800); + + + ////// + // Alice authorizes Bob to reserve her SPECIALCOIN + // This attempt should fail because the blockchain has not yet been initialized for CAA + ////// + custom_authority_create_operation authorize_reserve; + authorize_reserve.account = alice.get_id(); + authorize_reserve.auth.add_authority(bob.get_id(), 1); + authorize_reserve.auth.weight_threshold = 1; + authorize_reserve.enabled = true; + // Authorization is valid only for 2/5 of the maximum duration of a custom authority + time_point_sec before_authorization_start_time = + db.head_block_time() + (GRAPHENE_DEFAULT_MAX_CUSTOM_AUTHORITY_LIFETIME_SECONDS * 1 / 5); + time_point_sec authorization_start_time = + db.head_block_time() + (GRAPHENE_DEFAULT_MAX_CUSTOM_AUTHORITY_LIFETIME_SECONDS * 2 / 5); + time_point_sec authorization_middle_time = + db.head_block_time() + (GRAPHENE_DEFAULT_MAX_CUSTOM_AUTHORITY_LIFETIME_SECONDS * 3 / 5); + time_point_sec authorization_end_time = + db.head_block_time() + (GRAPHENE_DEFAULT_MAX_CUSTOM_AUTHORITY_LIFETIME_SECONDS * 4 / 5); + time_point_sec after_authorization_end_time = authorization_end_time + 86400; + authorize_reserve.valid_from = authorization_start_time; + authorize_reserve.valid_to = authorization_end_time; + authorize_reserve.operation_type = operation::tag::value; + trx.clear(); + trx.operations = {authorize_reserve}; + sign(trx, alice_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), fc::assert_exception); + + + ////// + // Alice creates a PROPOSAL to authorize Bob to reserve her SPECIALCOIN + // This attempt should fail because the blockchain has not yet been initialized for CAA + ////// + proposal_create_operation proposal; + proposal.fee_paying_account = alice.get_id(); + proposal.proposed_ops = {op_wrapper(authorize_reserve)}; + proposal.expiration_time = db.head_block_time() + 86400; + trx.clear(); + trx.operations = {authorize_reserve}; + sign(trx, alice_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), fc::assert_exception); + + + ////// + // Initialize the blockchain for CAA + ////// + 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); + + + ////// + // Alice creates a PROPOSAL to authorize Bob to reserve her SPECIALCOIN + // Authorization is valid only for 2/5 of the maximum duration of a custom authority + // This attempt should succeed because the blockchain is initialized for CAA + ////// + before_authorization_start_time = + db.head_block_time() + (GRAPHENE_DEFAULT_MAX_CUSTOM_AUTHORITY_LIFETIME_SECONDS * 1 / 5); + authorization_start_time = + db.head_block_time() + (GRAPHENE_DEFAULT_MAX_CUSTOM_AUTHORITY_LIFETIME_SECONDS * 2 / 5); + authorization_middle_time = + db.head_block_time() + (GRAPHENE_DEFAULT_MAX_CUSTOM_AUTHORITY_LIFETIME_SECONDS * 3 / 5); + authorization_end_time = + db.head_block_time() + (GRAPHENE_DEFAULT_MAX_CUSTOM_AUTHORITY_LIFETIME_SECONDS * 4 / 5); + after_authorization_end_time = authorization_end_time + 86400; + authorize_reserve.valid_from = authorization_start_time; + authorize_reserve.valid_to = authorization_end_time; + + proposal.fee_paying_account = alice.get_id(); + proposal.proposed_ops = {op_wrapper(authorize_reserve)}; + proposal.expiration_time = db.head_block_time() + 86400; + trx.clear(); + trx.operations = {proposal}; + sign(trx, alice_private_key); + PUSH_TX(db, trx); + + + ////// + // Advance the blockchain to get the finalized proposal ID + ////// + generate_blocks(1); + const proposal_object& prop = *db.get_index_type().indices().begin(); + proposal_id_type proposal_id = prop.id; + + // Alice approves the proposal + proposal_update_operation approve_proposal; + approve_proposal.proposal = proposal_id; + approve_proposal.fee_paying_account = alice.get_id(); + approve_proposal.active_approvals_to_add = {alice.get_id()}; + trx.clear(); + trx.operations = {approve_proposal}; + sign(trx, alice_private_key); + PUSH_TX(db, trx); + + + ////// + // Advance the blockchain to before the authorization starts + ////// + generate_blocks(before_authorization_start_time); + set_expiration(db, trx); + + + ////// + // Bob attempts to reserve some of Alice's SPECIALCOIN + // This attempt should fail because Bob the Alice authorization is not yet active + ////// + { + asset_reserve_operation reserve_op = reserve_asset(alice.get_id(), asset(200, specialcoin.id)); + trx.clear(); + trx.operations = {reserve_op}; + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should not indicate any rejected custom auths because the CAA is not yet active + // "rejected_custom_auths":[] + EXPECT_EXCEPTION_STRING("\"rejected_custom_auths\":[]", [&] {PUSH_TX(db, trx);}); + } + + + ////// + // Advance the blockchain to the start of the authorization period + ////// + generate_blocks(authorization_start_time); + set_expiration(db, trx); + + + ////// + // Bob attempts to update the collateral for Alice's debt position + // This should succeed because the authorization is active + ////// + { + asset_reserve_operation reserve_op = reserve_asset(alice.get_id(), asset(200, specialcoin.id)); + trx.clear(); + trx.operations = {reserve_op}; + sign(trx, bob_private_key); + PUSH_TX(db, trx); + } + + + ////// + // Advance the blockchain to the end of the authorization period + ////// + generate_blocks(authorization_middle_time); + set_expiration(db, trx); + + + ////// + // Bob attempts to update the collateral for Alice's debt position + // This should succeed because the authorization is active + ////// + { + asset_reserve_operation reserve_op = reserve_asset(alice.get_id(), asset(200, specialcoin.id)); + trx.clear(); + trx.operations = {reserve_op}; + sign(trx, bob_private_key); + PUSH_TX(db, trx); + } + + + ////// + // Advance the blockchain to after the authorization expires + ////// + generate_blocks(after_authorization_end_time); + set_expiration(db, trx); + + + ////// + // Bob attempts to update the collateral for Alice's debt position + // This should fail because Bob the authorization has expired + ////// + { + asset_reserve_operation reserve_op = reserve_asset(alice.get_id(), asset(200, specialcoin.id)); + trx.clear(); + trx.operations = {reserve_op}; + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should not indicate any rejected custom auths because no CAA applies for Bob's attempt + // "rejected_custom_auths":[] + EXPECT_EXCEPTION_STRING("\"rejected_custom_auths\":[]", [&] {PUSH_TX(db, trx);}); + } + + } FC_LOG_AND_RETHROW() + } + + + /** + * Test of string field restriction + * Test of CAA for asset_create_operation + * + * Scenario: Test of authorization of one account (alice) authorizing another account (bob) + * to create an asset with a description that starts with the literal string "ACOIN." + */ + BOOST_AUTO_TEST_CASE(authorized_asset_creation) { + 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((alice)(bob)); + fund(alice, asset(500000 * GRAPHENE_BLOCKCHAIN_PRECISION)); + upgrade_to_lifetime_member(alice); + fund(bob, asset(200000 * GRAPHENE_BLOCKCHAIN_PRECISION)); + + + // Lambda for issuing an asset to an account + auto create_uia = [&](const string& name, + const account_object& issuer, + uint16_t flags, + const additional_asset_options_t& options = additional_asset_options_t(), + const price& core_exchange_rate = price(asset(1, asset_id_type(1)), asset(1)), + uint8_t precision = 2 /* traditional precision for tests */, + uint16_t market_fee_percent = 0) { + + asset_create_operation op; + + op.issuer = issuer.id; + op.fee = asset(); + op.symbol = name; + op.common_options.max_supply = 0; + op.precision = precision; + op.common_options.core_exchange_rate = core_exchange_rate; + op.common_options.max_supply = GRAPHENE_MAX_SHARE_SUPPLY; + op.common_options.flags = flags; + op.common_options.issuer_permissions = flags; + op.common_options.market_fee_percent = market_fee_percent; + op.common_options.extensions = std::move(options); + + return op; + }; + + + ////// + // Alice creates a UIA + ////// + { + asset_create_operation create_uia_op = create_uia("ACOIN", alice, white_list); + trx.clear(); + trx.operations = {create_uia_op}; + sign(trx, alice_private_key); + PUSH_TX(db, trx); + } + + + ////// + // Bob attempts to create a UIA + // This should fail because Bob is not authorized by Alice to create any coin with Alice as the issuer + ////// + { + asset_create_operation create_uia_op = create_uia("ACOIN.BOB", alice, white_list); + trx.clear(); + trx.operations = {create_uia_op}; + sign(trx, bob_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should not indicate any rejected custom auths because no CAA applies for Bob's attempt + // "rejected_custom_auths":[] + EXPECT_EXCEPTION_STRING("\"rejected_custom_auths\":[]", [&] {PUSH_TX(db, trx);}); + } + + + ////// + // Alice authorizes Bob to create sub-token UIAs below ACOIN + ////// + { + custom_authority_create_operation authorize_uia_creation; + authorize_uia_creation.account = alice.get_id(); + authorize_uia_creation.auth.add_authority(bob.get_id(), 1); + authorize_uia_creation.auth.weight_threshold = 1; + authorize_uia_creation.enabled = true; + authorize_uia_creation.valid_to = db.head_block_time() + 86400; + authorize_uia_creation.operation_type = operation::tag::value; + + auto symbol_index = member_index("symbol"); + authorize_uia_creation.restrictions.emplace_back(symbol_index, FUNC(gt), string("ACOIN.")); + authorize_uia_creation.restrictions.emplace_back(symbol_index, FUNC(le), string("ACOIN.ZZZZZZZZZZZZZZZZ")); + //[ + // { + // "member_index": 2, + // "restriction_type": 4, + // "argument": [ + // 3, + // "ACOIN." + // ], + // "extensions": [] + // }, + // { + // "member_index": 2, + // "restriction_type": 3, + // "argument": [ + // 3, + // "ACOIN.ZZZZZZZZZZZZZZZZ" + // ], + // "extensions": [] + // } + //] + + trx.clear(); + trx.operations = {authorize_uia_creation}; + sign(trx, alice_private_key); + PUSH_TX(db, trx); + } + + + ////// + // Bob attempts to create a UIA with a symbol name below the authorized textual range + // This should fail because it violates Restriction 1 + ////// + { + asset_create_operation create_uia_op = create_uia("ABCOIN", alice, white_list); + trx.clear(); + trx.operations = {create_uia_op}; + sign(trx, bob_private_key); + + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should indicate the rejection path + // [0,0]: 0 is the rejection_indicator for an index to a sub-restriction; 0 is the index value for Restriction 1 + // [2,"predicate_was_false"]: 0 is the rejection_indicator for rejection_reason; "predicate_was_false" is the reason + EXPECT_EXCEPTION_STRING("\"rejection_path\":[[0,0],[2,\"predicate_was_false\"]]", [&] {PUSH_TX(db, trx);}); + } + + + ////// + // Bob attempts to create a UIA with a symobl name above the authorized textual range + // This should fail because it violates Restriction 2 + ////// + { + asset_create_operation create_uia_op = create_uia("BOB", alice, white_list); + trx.clear(); + trx.operations = {create_uia_op}; + sign(trx, bob_private_key); + + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should indicate the rejection path + // [0,1]: 0 is the rejection_indicator for an index to a sub-restriction; 1 is the index value for Restriction 2 + // [2,"predicate_was_false"]: 0 is the rejection_indicator for rejection_reason; "predicate_was_false" is the reason + EXPECT_EXCEPTION_STRING("\"rejection_path\":[[0,1],[2,\"predicate_was_false\"]]", [&] {PUSH_TX(db, trx);}); + } + + + ////// + // Bob attempts to create a sub-token of ACOIN + // This should succeed because this satisfies the sub-token restriction by Alice + ////// + { + asset_create_operation create_uia_op = create_uia("ACOIN.BOB", alice, white_list); + trx.clear(); + trx.operations = {create_uia_op}; + sign(trx, bob_private_key); + PUSH_TX(db, trx); + + create_uia_op = create_uia("ACOIN.CHARLIE", alice, white_list); + trx.clear(); + trx.operations = {create_uia_op}; + sign(trx, bob_private_key); + PUSH_TX(db, trx); + + create_uia_op = create_uia("ACOIN.DIANA", alice, white_list); + trx.clear(); + trx.operations = {create_uia_op}; + sign(trx, bob_private_key); + PUSH_TX(db, trx); + } + + + ////// + // Bob creates his own UIA that is similar to ACOIN + ////// + { + upgrade_to_lifetime_member(bob); + + asset_create_operation create_uia_op = create_uia("AACOIN", bob, white_list); + trx.clear(); + trx.operations = {create_uia_op}; + sign(trx, bob_private_key); + + PUSH_TX(db, trx); + + + create_uia_op = create_uia("AACOIN.TEST", bob, white_list); + trx.clear(); + trx.operations = {create_uia_op}; + sign(trx, bob_private_key); + + PUSH_TX(db, trx); + } + + + ////// + // Bob attempts to create a sub-token of AACOIN but with Alice as the issuer + // This should fail because it violates Restriction 1 + ////// + { + asset_create_operation create_uia_op = create_uia("AACOIN.BOB", alice, white_list); + trx.clear(); + trx.operations = {create_uia_op}; + sign(trx, bob_private_key); + + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should indicate the rejection path + // [0,0]: 0 is the rejection_indicator for an index to a sub-restriction; 0 is the index value for Restriction 1 + // [2,"predicate_was_false"]: 0 is the rejection_indicator for rejection_reason; "predicate_was_false" is the reason + EXPECT_EXCEPTION_STRING("\"rejection_path\":[[0,0],[2,\"predicate_was_false\"]]", [&] {PUSH_TX(db, trx);}); + } + + } FC_LOG_AND_RETHROW() + } + + /** + * Test of CAA for account_update_operation + * + * Scenario: Test of authorization of one account (alice) authorizing a key + * to ONLY update the voting slate of an account + */ + BOOST_AUTO_TEST_CASE(authorized_voting_key) { + 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((alice)); + fund(alice, asset(500000 * GRAPHENE_BLOCKCHAIN_PRECISION)); + upgrade_to_lifetime_member(alice); + + // Arbitrarily identify one of the active witnesses + flat_set witnesses = db.get_global_properties().active_witnesses; + auto itr_witnesses = witnesses.begin(); + witness_id_type witness0_id = itr_witnesses[0]; + const auto& idx = db.get_index_type().indices().get(); + witness_object witness0_obj = *idx.find(witness0_id); + + + ////// + // 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()); + + + ////// + // The key attempts to update the voting slate of Alice + // This should fail because the key is not authorized by Alice to update any part of her account + ////// + { + account_update_operation uop; + uop.account = alice.get_id(); + account_options alice_options = alice.options; + auto insert_result = alice_options.votes.insert(witness0_obj.vote_id); + if (!insert_result.second) + FC_THROW("Account ${account} was already voting for witness ${witness}", + ("account", alice)("witness", "init0")); + uop.new_options = alice_options; + + trx.clear(); + trx.operations.emplace_back(std::move(uop)); + sign(trx, some_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should not indicate any rejected custom auths because no CAA applies for Bob's attempt + // "rejected_custom_auths":[] + EXPECT_EXCEPTION_STRING("\"rejected_custom_auths\":[]", [&] { PUSH_TX(db, trx); }); + } + + + ////// + // Alice authorizes the key to update her voting slate + // by authorizing account updates EXCEPT for + // updating the owner key + // updating the active key + // updating the memo key + // updating the special owner authority + // updating the special active authority + ////// + { + custom_authority_create_operation authorize_account_update; + authorize_account_update.account = alice.get_id(); + authorize_account_update.auth.add_authority(some_public_key, 1); + authorize_account_update.auth.weight_threshold = 1; + authorize_account_update.enabled = true; + authorize_account_update.valid_to = db.head_block_time() + 86400; + authorize_account_update.operation_type = operation::tag::value; + + // Shall not update the owner key member + auto owner_index = member_index("owner"); + restriction no_owner = restriction(owner_index, FUNC(eq), void_t()); + + // Shall not update the active key member + auto active_index = member_index("active"); + restriction no_active = restriction(active_index, FUNC(eq), void_t()); + + // Shall not update the memo key member of the new_options member + auto new_options_index = member_index("new_options"); + auto memo_index = member_index("memo_key"); + restriction same_memo = restriction(new_options_index, FUNC(attr), + vector{ + restriction(memo_index, FUNC(eq), alice.options.memo_key)}); + + // Shall not update the extensions member + auto ext_index = member_index("extensions"); + restriction no_ext = restriction(ext_index, FUNC(eq), void_t()); + + auto owner_special_index = member_index("owner_special_authority"); + restriction no_special_owner = restriction(ext_index, FUNC(attr), + vector{ + restriction(owner_special_index, FUNC(eq), void_t())}); + + auto active_special_index = member_index("active_special_authority"); + restriction no_special_active = restriction(ext_index, FUNC(attr), + vector{ + restriction(active_special_index, FUNC(eq), void_t())}); + + // Shall not update the extensions member of the new_options member + auto new_options_ext_index = member_index("extensions"); + restriction no_new_options_ext = restriction(new_options_index, FUNC(attr), vector{ + restriction(new_options_ext_index, FUNC(eq), void_t())}); + + // Combine all of the shall not restrictions + vector shall_not_restrictions = {no_owner, no_active, no_special_owner, no_special_active, + same_memo}; + authorize_account_update.restrictions = shall_not_restrictions; + //[ + // { + // "member_index": 2, + // "restriction_type": 0, + // "argument": [ + // 0, + // {} + // ], + // "extensions": [] + // }, + // { + // "member_index": 3, + // "restriction_type": 0, + // "argument": [ + // 0, + // {} + // ], + // "extensions": [] + // }, + // { + // "member_index": 5, + // "restriction_type": 10, + // "argument": [ + // 39, + // [ + // { + // "member_index": 1, + // "restriction_type": 0, + // "argument": [ + // 0, + // {} + // ], + // "extensions": [] + // } + // ] + // ], + // "extensions": [] + // }, + // { + // "member_index": 5, + // "restriction_type": 10, + // "argument": [ + // 39, + // [ + // { + // "member_index": 2, + // "restriction_type": 0, + // "argument": [ + // 0, + // {} + // ], + // "extensions": [] + // } + // ] + // ], + // "extensions": [] + // }, + // { + // "member_index": 4, + // "restriction_type": 10, + // "argument": [ + // 39, + // [ + // { + // "member_index": 0, + // "restriction_type": 0, + // "argument": [ + // 5, + // "BTS7zsqi7QUAjTAdyynd6DVe8uv4K8gCTRHnAoMN9w9CA1xLCTDVv" + // ], + // "extensions": [] + // } + // ] + // ], + // "extensions": [] + // } + //] + + // Broadcast the transaction + trx.clear(); + trx.operations = {authorize_account_update}; + sign(trx, alice_private_key); + PUSH_TX(db, trx); + } + + + ////// + // The key attempts to update the owner key for alice + // This should fail because it is NOT authorized by alice + // It violates Restriction 1 (index-0) + ////// + { + account_update_operation uop; + uop.account = alice.get_id(); + + uop.owner = authority(1, some_public_key, 1); + + trx.clear(); + trx.operations.emplace_back(std::move(uop)); + sign(trx, some_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_owner_auth); + // The failure should indicate the rejection path + // {"success":false,"rejection_path":[[0,0],[2,"predicate_was_false"]]} + EXPECT_EXCEPTION_STRING("\"rejection_path\":[[0,0],[2,\"predicate_was_false\"]]", [&] {PUSH_TX(db, trx);}); + } + + + ////// + // The key attempts to update the active key for alice + // This should fail because it is NOT authorized by alice + // It violates Restriction 2 (index-1) + ////// + { + account_update_operation uop; + uop.account = alice.get_id(); + + uop.active = authority(1, some_public_key, 1); + + trx.clear(); + trx.operations.emplace_back(std::move(uop)); + sign(trx, some_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should indicate the rejection path + // {"success":false,"rejection_path":[[0,1],[2,"predicate_was_false"]]} + EXPECT_EXCEPTION_STRING("\"rejection_path\":[[0,1],[2,\"predicate_was_false\"]]", [&] {PUSH_TX(db, trx);}); + } + + + ////// + // The key attempts to update the special owner key for alice + // This should fail because it is NOT authorized by alice + // It violates Restriction 3 (index-2) + ////// + { + account_update_operation uop; + uop.account = alice.get_id(); + + uop.extensions.value.owner_special_authority = no_special_authority(); + + trx.clear(); + trx.operations.emplace_back(std::move(uop)); + sign(trx, some_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_owner_auth); + // The failure should indicate the rejection path + // "rejection_path":[[0,2],[0,0],[2,"predicate_was_false"] + // [0,2]: 0 is the rejection_indicator for an index to a sub-restriction; 2 is the index value for Restriction 3 + // [0,0]: 0 is the rejection_indicator for an index to a sub-restriction; 0 is the index value for the only argument + // [2,"predicate_was_false"]: 0 is the rejection_indicator for rejection_reason; "predicate_was_false" is the reason + EXPECT_EXCEPTION_STRING("\"rejection_path\":[[0,2],[0,0],[2,\"predicate_was_false\"]]", [&] {PUSH_TX(db, trx);}); + } + + + ////// + // The key attempts to update the special active key for alice + // This should fail because it is NOT authorized by alice + // It violates Restriction 4 (index-3) + ////// + { + account_update_operation uop; + uop.account = alice.get_id(); + + uop.extensions.value.active_special_authority = no_special_authority(); + + trx.clear(); + trx.operations.emplace_back(std::move(uop)); + sign(trx, some_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should indicate the rejection path + // "rejection_path":[[0,3],[0,0],[2,"predicate_was_false"] + // [0,3]: 0 is the rejection_indicator for an index to a sub-restriction; 3 is the index value for Restriction 4 + // [0,0]: 0 is the rejection_indicator for an index to a sub-restriction; 0 is the index value for the only argument + // [2,"predicate_was_false"]: 0 is the rejection_indicator for rejection_reason; "predicate_was_false" is the reason + EXPECT_EXCEPTION_STRING("\"rejection_path\":[[0,3],[0,0],[2,\"predicate_was_false\"]]", [&] {PUSH_TX(db, trx);}); + } + + + ////// + // The key attempts to update the memo key for alice + // This should fail because it is NOT authorized by alice + // It violates Restriction 5 (index-4) + ////// + { + account_update_operation uop; + uop.account = alice.get_id(); + + account_options alice_options = alice.options; + alice_options.memo_key = some_public_key; + uop.new_options = alice_options; + + trx.clear(); + trx.operations.emplace_back(std::move(uop)); + sign(trx, some_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should indicate the rejection path + // "rejection_path":[[0,4],[0,0],[2,"predicate_was_false"] + // [0,4]: 0 is the rejection_indicator for an index to a sub-restriction; 4 is the index value for Restriction 5 + // [0,0]: 0 is the rejection_indicator for an index to a sub-restriction; 0 is the index value for the only argument + // [2,"predicate_was_false"]: 0 is the rejection_indicator for rejection_reason; "predicate_was_false" is the reason + EXPECT_EXCEPTION_STRING("\"rejection_path\":[[0,4],[0,0],[2,\"predicate_was_false\"]]", [&] {PUSH_TX(db, trx);}); + } + + + ////// + // The key attempts to update the voting slate for alice + // This should succeed because the key is authorized by alice + ////// + { + account_update_operation uop; + uop.account = alice.get_id(); + account_options alice_options = alice.options; + auto insert_result = alice_options.votes.insert(witness0_obj.vote_id); + if (!insert_result.second) + FC_THROW("Account ${account} was already voting for witness ${witness}", + ("account", alice)("witness", "init0")); + uop.new_options = alice_options; + + trx.clear(); + trx.operations.emplace_back(std::move(uop)); + sign(trx, some_private_key); + PUSH_TX(db, trx); + } + + } FC_LOG_AND_RETHROW() + } + + + /** + * Test of CAA for witness_update_operation + * + * Scenario: Test of authorization of one account (alice) authorizing a key + * to ONLY change the signing key of a witness account + */ + BOOST_AUTO_TEST_CASE(authorized_change_witness_signing_key) { + 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 + ////// + // Create a new witness account (witness0) + ACTORS((witness0)); + // Upgrade witness account to LTM + upgrade_to_lifetime_member(witness0.id); + generate_block(); + + // Create the witnesses + // Get the witness0 identifier after a block has been generated + // to be sure of using the most up-to-date identifier for the account + const account_id_type witness0_identifier = get_account("witness0").id; + create_witness(witness0_identifier, witness0_private_key); + + generate_block(); + + // Find the witness ID for witness0 + const auto& idx = db.get_index_type().indices().get(); + witness_object witness0_obj = *idx.find(witness0_identifier); + BOOST_CHECK(witness0_obj.witness_account == witness0_identifier); + + + ////// + // 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()); + + + ////// + // Define an alternate witness signing key + ////// + fc::ecc::private_key alternate_signing_private_key = generate_private_key("some signing key"); + public_key_type alternate_signing_public_key = public_key_type(alternate_signing_private_key.get_public_key()); + // The current signing key should be different than the alternate signing public key + BOOST_CHECK(witness0_obj.signing_key != alternate_signing_public_key); + + + ////// + // The key attempts to update the signing key of witness0 + // This should fail because the key is NOT authorized by witness0 to update the signing key + ////// + { + witness_update_operation wop; + wop.witness = witness0_obj.id; + wop.witness_account = witness0_obj.witness_account; + + wop.new_signing_key = alternate_signing_public_key; + + trx.clear(); + trx.operations.emplace_back(std::move(wop)); + sign(trx, some_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should not indicate any rejected custom auths because no CAA applies for the key's attempt + // "rejected_custom_auths":[] + EXPECT_EXCEPTION_STRING("\"rejected_custom_auths\":[]", [&] { PUSH_TX(db, trx); }); + } + + + ////// + // Alice authorizes the key to only update the witness signing key + ////// + { + custom_authority_create_operation authorize_update_signing_key; + authorize_update_signing_key.account = witness0.get_id(); + authorize_update_signing_key.auth.add_authority(some_public_key, 1); + authorize_update_signing_key.auth.weight_threshold = 1; + authorize_update_signing_key.enabled = true; + authorize_update_signing_key.valid_to = db.head_block_time() + 86400; + authorize_update_signing_key.operation_type = operation::tag::value; + auto url_index = member_index("new_url"); + restriction no_url = restriction(url_index, FUNC(eq), void_t()); + authorize_update_signing_key.restrictions = {no_url}; + //[ + // { + // "member_index": 3, + // "restriction_type": 0, + // "argument": [ + // 0, + // {} + // ] + // } + //] + + // Broadcast the transaction + trx.clear(); + trx.operations = {authorize_update_signing_key}; + sign(trx, witness0_private_key); + PUSH_TX(db, trx); + } + + + ////// + // The key attempts to update the URL of witness0 + // This should fail because the key is NOT authorized by witness0 to update the URL + ////// + { + witness_update_operation wop; + wop.witness = witness0_obj.id; + wop.witness_account = witness0_obj.witness_account; + + wop.new_url = "NEW_URL"; + + trx.clear(); + trx.operations.emplace_back(std::move(wop)); + sign(trx, some_private_key); + BOOST_CHECK_THROW(PUSH_TX(db, trx), tx_missing_active_auth); + // The failure should indicate the rejection path + // {"success":false,"rejection_path":[[0,0],[2,"predicate_was_false"]]} + EXPECT_EXCEPTION_STRING("\"rejection_path\":[[0,0],[2,\"predicate_was_false\"]]", [&] {PUSH_TX(db, trx);}); + } + + + ////// + // The key attempts to update the signing key of witness0 + // This should succeed because the key is authorized by witness0 to update the signing key + ////// + { + witness_update_operation wop; + wop.witness = witness0_obj.id; + wop.witness_account = witness0_obj.witness_account; + + wop.new_signing_key = alternate_signing_public_key; + + trx.clear(); + trx.operations.emplace_back(std::move(wop)); + sign(trx, some_private_key); + PUSH_TX(db, trx); + + // Check the current signing key for witness0 + witness_object updated_witness0_obj = *idx.find(witness0_obj.witness_account); + BOOST_CHECK(updated_witness0_obj.witness_account == witness0_obj.witness_account); + BOOST_CHECK(updated_witness0_obj.signing_key == alternate_signing_public_key); + } + + } + FC_LOG_AND_RETHROW() + } + +BOOST_AUTO_TEST_SUITE_END() diff --git a/tests/tests/database_api_tests.cpp b/tests/tests/database_api_tests.cpp index 31f03e7781..36c160c271 100644 --- a/tests/tests/database_api_tests.cpp +++ b/tests/tests/database_api_tests.cpp @@ -66,7 +66,12 @@ BOOST_AUTO_TEST_CASE(is_registered) /*** * Assert */ - graphene::app::database_api db_api(db); + graphene::app::database_api db_api1(db); + BOOST_CHECK_THROW( db_api1.is_public_key_registered((string) nathan_public), fc::exception ); + + graphene::app::application_options opt = app.get_options(); + opt.has_api_helper_indexes_plugin = true; + graphene::app::database_api db_api( db, &opt ); BOOST_CHECK(db_api.is_public_key_registered((string) nathan_public)); BOOST_CHECK(db_api.is_public_key_registered((string) dan_public)); @@ -1085,6 +1090,122 @@ BOOST_AUTO_TEST_CASE( subscription_notification_test ) } FC_LOG_AND_RETHROW() } +BOOST_AUTO_TEST_CASE( get_all_workers ) +{ try { + graphene::app::database_api db_api( db, &( app.get_options() )); + ACTORS( (connie)(whitney)(wolverine) ); + + fund(connie); + upgrade_to_lifetime_member(connie); + fund(whitney); + upgrade_to_lifetime_member(whitney); + fund(wolverine); + upgrade_to_lifetime_member(wolverine); + + vector results; + + const auto& worker1 = create_worker( connie_id, 1000, fc::days(10) ); + worker_id_type worker1_id = worker1.id; + + BOOST_REQUIRE_EQUAL( db_api.get_all_workers().size(), 1 ); + BOOST_REQUIRE_EQUAL( db_api.get_all_workers(true).size(), 0 ); + BOOST_REQUIRE_EQUAL( db_api.get_all_workers(false).size(), 1 ); + BOOST_CHECK( db_api.get_all_workers().front().id == worker1_id ); + BOOST_CHECK( db_api.get_all_workers(false).front().id == worker1_id ); + + generate_blocks( db.head_block_time() + fc::days(11) ); + set_expiration( db, trx ); + + BOOST_REQUIRE_EQUAL( db_api.get_all_workers().size(), 1 ); + BOOST_REQUIRE_EQUAL( db_api.get_all_workers(true).size(), 1 ); + BOOST_REQUIRE_EQUAL( db_api.get_all_workers(false).size(), 0 ); + BOOST_CHECK( db_api.get_all_workers().front().id == worker1_id ); + BOOST_CHECK( db_api.get_all_workers(true).front().id == worker1_id ); + + const auto& worker2 = create_worker( whitney_id, 1000, fc::days(50) ); + worker_id_type worker2_id = worker2.id; + + BOOST_REQUIRE_EQUAL( db_api.get_all_workers().size(), 2 ); + BOOST_REQUIRE_EQUAL( db_api.get_all_workers(true).size(), 1 ); + BOOST_REQUIRE_EQUAL( db_api.get_all_workers(false).size(), 1 ); + BOOST_CHECK( db_api.get_all_workers().front().id == worker1_id ); + BOOST_CHECK( db_api.get_all_workers().back().id == worker2_id ); + BOOST_CHECK( db_api.get_all_workers(true).front().id == worker1_id ); + BOOST_CHECK( db_api.get_all_workers(false).front().id == worker2_id ); + + const auto& worker3 = create_worker( wolverine_id, 1000, fc::days(100) ); + worker_id_type worker3_id = worker3.id; + + BOOST_REQUIRE_EQUAL( db_api.get_all_workers().size(), 3 ); + BOOST_REQUIRE_EQUAL( db_api.get_all_workers(true).size(), 1 ); + BOOST_REQUIRE_EQUAL( db_api.get_all_workers(false).size(), 2 ); + BOOST_CHECK( db_api.get_all_workers().front().id == worker1_id ); + BOOST_CHECK( db_api.get_all_workers().back().id == worker3_id ); + BOOST_CHECK( db_api.get_all_workers(true).front().id == worker1_id ); + BOOST_CHECK( db_api.get_all_workers(false).front().id == worker2_id ); + BOOST_CHECK( db_api.get_all_workers(false).back().id == worker3_id ); + + generate_blocks( db.head_block_time() + fc::days(55) ); + set_expiration( db, trx ); + + BOOST_REQUIRE_EQUAL( db_api.get_all_workers().size(), 3 ); + BOOST_REQUIRE_EQUAL( db_api.get_all_workers(true).size(), 2 ); + BOOST_REQUIRE_EQUAL( db_api.get_all_workers(false).size(), 1 ); + BOOST_CHECK( db_api.get_all_workers().front().id == worker1_id ); + BOOST_CHECK( db_api.get_all_workers().back().id == worker3_id ); + BOOST_CHECK( db_api.get_all_workers(true).front().id == worker1_id ); + BOOST_CHECK( db_api.get_all_workers(true).back().id == worker2_id ); + BOOST_CHECK( db_api.get_all_workers(false).front().id == worker3_id ); + + generate_blocks( db.head_block_time() + fc::days(55) ); + set_expiration( db, trx ); + + BOOST_REQUIRE_EQUAL( db_api.get_all_workers().size(), 3 ); + BOOST_REQUIRE_EQUAL( db_api.get_all_workers(true).size(), 3 ); + BOOST_REQUIRE_EQUAL( db_api.get_all_workers(false).size(), 0 ); + BOOST_CHECK( db_api.get_all_workers().front().id == worker1_id ); + BOOST_CHECK( db_api.get_all_workers().back().id == worker3_id ); + BOOST_CHECK( db_api.get_all_workers(true).front().id == worker1_id ); + BOOST_CHECK( db_api.get_all_workers(true).back().id == worker3_id ); + +} FC_LOG_AND_RETHROW() } + +BOOST_AUTO_TEST_CASE( get_workers_by_account ) +{ try { + graphene::app::database_api db_api( db, &( app.get_options() )); + ACTORS( (connie)(whitney)(wolverine) ); + + fund(connie); + upgrade_to_lifetime_member(connie); + fund(whitney); + upgrade_to_lifetime_member(whitney); + fund(wolverine); + upgrade_to_lifetime_member(wolverine); + + vector results; + + const auto& worker1 = create_worker( connie_id ); + worker_id_type worker1_id = worker1.id; + + const auto& worker2 = create_worker( whitney_id, 1000, fc::days(50) ); + worker_id_type worker2_id = worker2.id; + + const auto& worker3 = create_worker( whitney_id, 1000, fc::days(100) ); + worker_id_type worker3_id = worker3.id; + + BOOST_REQUIRE_EQUAL( db_api.get_workers_by_account("connie").size(), 1 ); + BOOST_CHECK( db_api.get_workers_by_account("connie").front().id == worker1_id ); + + BOOST_REQUIRE_EQUAL( db_api.get_workers_by_account(string(whitney.id)).size(), 2 ); + BOOST_CHECK( db_api.get_workers_by_account(string(whitney.id)).front().id == worker2_id ); + BOOST_CHECK( db_api.get_workers_by_account(string(whitney.id)).back().id == worker3_id ); + + BOOST_REQUIRE_EQUAL( db_api.get_workers_by_account("wolverine").size(), 0 ); + + BOOST_REQUIRE_THROW( db_api.get_workers_by_account("not-a-user"), fc::exception ); + +} FC_LOG_AND_RETHROW() } + BOOST_AUTO_TEST_CASE( lookup_vote_ids ) { try { graphene::app::database_api db_api( db, &( app.get_options() )); @@ -1110,6 +1231,115 @@ BOOST_AUTO_TEST_CASE( lookup_vote_ids ) } FC_LOG_AND_RETHROW() } +BOOST_AUTO_TEST_CASE(get_limit_orders_by_account) +{ try { + graphene::app::database_api db_api( db, &( app.get_options() )); + ACTORS((seller)(buyer)(watcher)); + + const auto& bitcny = create_user_issued_asset("CNY"); + const auto& core = asset_id_type()(db); + + int64_t init_balance(10000000); + transfer( committee_account, seller_id, asset(init_balance) ); + issue_uia( buyer_id, bitcny.amount(init_balance) ); + BOOST_CHECK_EQUAL( 10000000, get_balance(seller, core) ); + BOOST_CHECK_EQUAL( 10000000, get_balance(buyer, bitcny) ); + + std::vector results, results2; + limit_order_object o; + + // limit too large + BOOST_CHECK_THROW( db_api.get_limit_orders_by_account( seller.name, 102 ), fc::exception ); + + // The order book is empty + results = db_api.get_limit_orders_by_account( seller.name ); + BOOST_CHECK_EQUAL( results.size(), 0 ); + + // Seller create 50 orders + for (size_t i = 0 ; i < 50 ; ++i) + { + BOOST_CHECK(create_sell_order(seller, core.amount(100), bitcny.amount(250))); + } + + // Get all orders + results = db_api.get_limit_orders_by_account( seller.name ); + BOOST_CHECK_EQUAL( results.size(), 50 ); + + // Seller create 200 orders + for (size_t i = 1 ; i < 101 ; ++i) + { + BOOST_CHECK(create_sell_order(seller, core.amount(100), bitcny.amount(250 + i))); + BOOST_CHECK(create_sell_order(seller, core.amount(100), bitcny.amount(250 - i))); + } + + // Buyer create 20 orders + for (size_t i = 0 ; i < 70 ; ++i) + { + BOOST_CHECK(create_sell_order(buyer, bitcny.amount(100), core.amount(5000 + i))); + } + + // Get the first 101 orders + results = db_api.get_limit_orders_by_account( seller.name ); + BOOST_CHECK_EQUAL( results.size(), 101 ); + for (size_t i = 0 ; i < results.size() - 1 ; ++i) + { + BOOST_CHECK(results[i].id < results[i+1].id); + } + BOOST_CHECK(results.front().sell_price == price(core.amount(100), bitcny.amount(250))); + BOOST_CHECK(results.back().sell_price == price(core.amount(100), bitcny.amount(276))); + o = results.back(); + + // Get the No. 101-201 orders + results = db_api.get_limit_orders_by_account( seller.name, {}, o.id ); + BOOST_CHECK_EQUAL( results.size(), 101 ); + for (size_t i = 0 ; i < results.size() - 1 ; ++i) + { + BOOST_CHECK(results[i].id < results[i+1].id); + } + BOOST_CHECK(results.front().sell_price == price(core.amount(100), bitcny.amount(276))); + BOOST_CHECK(results.back().sell_price == price(core.amount(100), bitcny.amount(326))); + o = results.back(); + + // Get the No. 201- orders + results = db_api.get_limit_orders_by_account( seller.name, {}, o.id ); + BOOST_CHECK_EQUAL( results.size(), 50 ); + for (size_t i = 0 ; i < results.size() - 1 ; ++i) + { + BOOST_CHECK(results[i].id < results[i+1].id); + } + BOOST_CHECK(results.front().sell_price == price(core.amount(100), bitcny.amount(326))); + BOOST_CHECK(results.back().sell_price == price(core.amount(100), bitcny.amount(150))); + + // Get the No. 201-210 orders + results2 = db_api.get_limit_orders_by_account( seller.name, 10, o.id ); + BOOST_CHECK_EQUAL( results2.size(), 10 ); + for (size_t i = 0 ; i < results2.size() - 1 ; ++i) + { + BOOST_CHECK(results2[i].id < results2[i+1].id); + BOOST_CHECK(results[i].id == results2[i].id); + } + BOOST_CHECK(results2.front().sell_price == price(core.amount(100), bitcny.amount(326))); + BOOST_CHECK(results2.back().sell_price == price(core.amount(100), bitcny.amount(170))); + + // Buyer has 70 orders, all IDs are greater than sellers + results = db_api.get_limit_orders_by_account( buyer.name, 90, o.id ); + BOOST_CHECK_EQUAL( results.size(), 70 ); + o = results.back(); + + // All seller's order IDs are smaller, so querying with a buyer's ID will get nothing + results = db_api.get_limit_orders_by_account( seller.name, 90, o.id ); + BOOST_CHECK_EQUAL( results.size(), 0 ); + + // Watcher has no order + results = db_api.get_limit_orders_by_account( watcher.name ); + BOOST_CHECK_EQUAL( results.size(), 0 ); + + // unregistered account, throws exception + BOOST_CHECK_THROW( db_api.get_limit_orders_by_account( "not-a-user", 10, limit_order_id_type() ), + fc::exception ); + +} FC_LOG_AND_RETHROW() } + BOOST_AUTO_TEST_CASE(get_account_limit_orders) { try { graphene::app::database_api db_api( db, &( app.get_options() )); @@ -1358,87 +1588,6 @@ BOOST_AUTO_TEST_CASE( verify_authority_multiple_accounts ) throw; } } -BOOST_AUTO_TEST_CASE( api_limit_get_key_references ){ - try{ - const int num_keys = 210; - const int num_keys1 = 2; - vector< private_key_type > numbered_private_keys; - vector< public_key_type > numbered_key_id; - numbered_private_keys.reserve( num_keys ); - graphene::app::database_api db_api( db, &( app.get_options() )); - for( int i=0; i > final_result=db_api.get_key_references(numbered_key_id); - BOOST_REQUIRE_EQUAL( final_result.size(), 2u ); - numbered_private_keys.reserve( num_keys ); - for( int i=num_keys1; iapp.get_options())); - - const account_object& alice = create_account("alice"); - const account_object& bob = create_account("bob"); - const account_object& carl = create_account("carl"); - const account_object& dan = create_account("dan"); - const account_object& fred = create_account("fred"); - const account_object& henry = create_account("henry"); - const account_object& kevin = create_account("kevin"); - const account_object& laura = create_account("laura"); - const account_object& lucy = create_account("lucy"); - const account_object& martin = create_account("martin"); - const account_object& patty = create_account("patty"); - - vector accounts; - accounts.push_back(alice.name); - accounts.push_back(bob.name); - accounts.push_back(carl.name); - accounts.push_back(dan.name); - accounts.push_back(fred.name); - accounts.push_back(henry.name); - accounts.push_back(kevin.name); - accounts.push_back(laura.name); - accounts.push_back(lucy.name); - accounts.push_back(martin.name); - accounts.push_back(patty.name); - - GRAPHENE_CHECK_THROW(db_api.get_full_accounts(accounts, false), fc::exception); - - accounts.erase(accounts.begin()); - auto full_accounts = db_api.get_full_accounts(accounts, false); - BOOST_CHECK(full_accounts.size() == 10); - - // not an account - accounts.erase(accounts.begin()); - accounts.push_back("nosuchaccount"); - - // non existing accounts will be ignored in the results - full_accounts = db_api.get_full_accounts(accounts, false); - BOOST_CHECK(full_accounts.size() == 9); - - } catch (fc::exception& e) { - edump((e.to_detail_string())); - throw; - } -} BOOST_AUTO_TEST_CASE( get_assets_by_issuer ) { try { @@ -1556,97 +1705,6 @@ BOOST_AUTO_TEST_CASE( get_settle_orders_by_account ) { } } -BOOST_AUTO_TEST_CASE( api_limit_get_limit_orders ){ - try{ - graphene::app::database_api db_api( db, &( app.get_options() )); - //account_id_type() do 3 ops - create_bitasset("USD", account_id_type()); - create_account("dan"); - create_account("bob"); - asset_id_type bit_jmj_id = create_bitasset("JMJBIT").id; - generate_block(); - fc::usleep(fc::milliseconds(100)); - GRAPHENE_CHECK_THROW(db_api.get_limit_orders(std::string(static_cast(asset_id_type())), - std::string(static_cast(bit_jmj_id)), 370), fc::exception); - vector limit_orders =db_api.get_limit_orders(std::string( - static_cast(asset_id_type())), - std::string(static_cast(bit_jmj_id)), 340); - BOOST_REQUIRE_EQUAL( limit_orders.size(), 0u); - - }catch (fc::exception& e) { - edump((e.to_detail_string())); - throw; - } -} -BOOST_AUTO_TEST_CASE( api_limit_get_call_orders ){ - try{ - graphene::app::database_api db_api( db, &( app.get_options() )); - //account_id_type() do 3 ops - auto nathan_private_key = generate_private_key("nathan"); - account_id_type nathan_id = create_account("nathan", nathan_private_key.get_public_key()).id; - transfer(account_id_type(), nathan_id, asset(100)); - asset_id_type bitusd_id = create_bitasset( - "USDBIT", nathan_id, 100, disable_force_settle).id; - generate_block(); - fc::usleep(fc::milliseconds(100)); - BOOST_CHECK( bitusd_id(db).is_market_issued() ); - GRAPHENE_CHECK_THROW(db_api.get_call_orders(std::string(static_cast(bitusd_id)), - 370), fc::exception); - vector< call_order_object> call_order =db_api.get_call_orders(std::string( - static_cast(bitusd_id)), 340); - BOOST_REQUIRE_EQUAL( call_order.size(), 0u); - }catch (fc::exception& e) { - edump((e.to_detail_string())); - throw; - } -} -BOOST_AUTO_TEST_CASE( api_limit_get_settle_orders ){ - try{ - graphene::app::database_api db_api( db, &( app.get_options() )); - //account_id_type() do 3 ops - auto nathan_private_key = generate_private_key("nathan"); - account_id_type nathan_id = create_account("nathan", nathan_private_key.get_public_key()).id; - transfer(account_id_type(), nathan_id, asset(100)); - asset_id_type bitusd_id = create_bitasset( - "USDBIT", nathan_id, 100, disable_force_settle).id; - generate_block(); - fc::usleep(fc::milliseconds(100)); - GRAPHENE_CHECK_THROW(db_api.get_settle_orders( - std::string(static_cast(bitusd_id)), 370), fc::exception); - vector result =db_api.get_settle_orders( - std::string(static_cast(bitusd_id)), 340); - BOOST_REQUIRE_EQUAL( result.size(), 0u); - }catch (fc::exception& e) { - edump((e.to_detail_string())); - throw; - } -} -BOOST_AUTO_TEST_CASE( api_limit_get_order_book ){ - try{ - graphene::app::database_api db_api( db, &( app.get_options() )); - auto nathan_private_key = generate_private_key("nathan"); - auto dan_private_key = generate_private_key("dan"); - account_id_type nathan_id = create_account("nathan", nathan_private_key.get_public_key()).id; - account_id_type dan_id = create_account("dan", dan_private_key.get_public_key()).id; - transfer(account_id_type(), nathan_id, asset(100)); - transfer(account_id_type(), dan_id, asset(100)); - asset_id_type bitusd_id = create_bitasset( - "USDBIT", nathan_id, 100, disable_force_settle).id; - asset_id_type bitdan_id = create_bitasset( - "DANBIT", dan_id, 100, disable_force_settle).id; - generate_block(); - fc::usleep(fc::milliseconds(100)); - GRAPHENE_CHECK_THROW(db_api.get_order_book(std::string(static_cast(bitusd_id)), - std::string(static_cast(bitdan_id)),89), fc::exception); - graphene::app::order_book result =db_api.get_order_book(std::string( - static_cast(bitusd_id)), std::string(static_cast(bitdan_id)),78); - BOOST_REQUIRE_EQUAL( result.bids.size(), 0u); - }catch (fc::exception& e) { - edump((e.to_detail_string())); - throw; - } -} - BOOST_AUTO_TEST_CASE( asset_in_collateral ) { try { ACTORS( (dan)(nathan) ); @@ -1783,292 +1841,5 @@ BOOST_AUTO_TEST_CASE( asset_in_collateral ) BOOST_CHECK_EQUAL( 1000, assets[1].total_backing_collateral->value ); } FC_LOG_AND_RETHROW() } -BOOST_AUTO_TEST_CASE( api_limit_lookup_accounts ) { - try{ - graphene::app::database_api db_api( db, &( app.get_options() )); - ACTOR(bob); - GRAPHENE_CHECK_THROW(db_api.lookup_accounts("bob",220), fc::exception); - map result =db_api.lookup_accounts("bob",190); - BOOST_REQUIRE_EQUAL( result.size(), 17u); - - } catch (fc::exception& e) { - edump((e.to_detail_string())); - throw; - } -} - -BOOST_AUTO_TEST_CASE( api_limit_lookup_witness_accounts ) { - try{ - graphene::app::database_api db_api( db, &( app.get_options() )); - ACTORS((bob)) ; - GRAPHENE_CHECK_THROW(db_api.lookup_witness_accounts("bob",220), fc::exception); - map result =db_api.lookup_witness_accounts("bob",190); - BOOST_REQUIRE_EQUAL( result.size(), 10u); - - } catch (fc::exception& e) { - edump((e.to_detail_string())); - throw; - } -} -BOOST_AUTO_TEST_CASE( api_limit_get_full_accounts2 ) { - - try { - graphene::app::database_api db_api(db, &(this->app.get_options())); - vector accounts; - for (int i=0; i<201; i++) { - std::string acct_name = "mytempacct" + std::to_string(i); - const account_object& account_name=create_account(acct_name); - accounts.push_back(account_name.name); - } - GRAPHENE_CHECK_THROW(db_api.get_full_accounts(accounts, false), fc::exception); - accounts.erase(accounts.begin()); - auto full_accounts = db_api.get_full_accounts(accounts, false); - BOOST_REQUIRE_EQUAL(full_accounts.size(), 200u); - } catch (fc::exception& e) { - edump((e.to_detail_string())); - throw; - } -} -BOOST_AUTO_TEST_CASE(api_limit_get_withdraw_permissions_by_recipient){ - try{ - graphene::app::database_api db_api( db, &app.get_options()); - ACTORS((bob)) ; - withdraw_permission_id_type withdraw_permission; - GRAPHENE_CHECK_THROW(db_api.get_withdraw_permissions_by_recipient( - "bob",withdraw_permission, 251), fc::exception); - vector result =db_api.get_withdraw_permissions_by_recipient( - "bob",withdraw_permission,250); - BOOST_REQUIRE_EQUAL( result.size(), 0u); - }catch (fc::exception& e) { - edump((e.to_detail_string())); - throw; - } -} -BOOST_AUTO_TEST_CASE(api_limit_get_withdraw_permissions_by_giver){ - try{ - graphene::app::database_api db_api( db, &app.get_options()); - ACTORS((bob)) ; - withdraw_permission_id_type withdraw_permission; - GRAPHENE_CHECK_THROW(db_api.get_withdraw_permissions_by_giver( - "bob",withdraw_permission, 251), fc::exception); - vector result =db_api.get_withdraw_permissions_by_giver( - "bob",withdraw_permission,250); - BOOST_REQUIRE_EQUAL( result.size(), 0u); - }catch (fc::exception& e) { - edump((e.to_detail_string())); - throw; - } -} -BOOST_AUTO_TEST_CASE(api_limit_get_trade_history_by_sequence){ - try{ - app.enable_plugin("market_history"); - graphene::app::application_options opt=app.get_options(); - opt.has_market_history_plugin = true; - graphene::app::database_api db_api( db, &opt); - const auto& bitusd = create_bitasset("USDBIT"); - asset_id_type asset_1, asset_2; - asset_1 = bitusd.id; - asset_2 = asset_id_type(); - GRAPHENE_CHECK_THROW(db_api.get_trade_history_by_sequence( - std::string( static_cast(asset_1)), - std::string( static_cast(asset_2)), - 0,fc::time_point_sec(), 251), fc::exception); - vector result =db_api.get_trade_history_by_sequence( - std::string( static_cast(asset_1)), - std::string( static_cast(asset_2)), - 0,fc::time_point_sec(),250); - BOOST_REQUIRE_EQUAL( result.size(), 0u); - }catch (fc::exception& e) { - edump((e.to_detail_string())); - throw; - } -} - -BOOST_AUTO_TEST_CASE(api_limit_get_trade_history){ - try{ - app.enable_plugin("market_history"); - graphene::app::application_options opt=app.get_options(); - opt.has_market_history_plugin = true; - graphene::app::database_api db_api( db, &opt); - const auto& bitusd = create_bitasset("USDBIT"); - asset_id_type asset_1, asset_2; - asset_1 = bitusd.id; - asset_2 = asset_id_type(); - GRAPHENE_CHECK_THROW(db_api.get_trade_history( - std::string( static_cast(asset_1)), - std::string( static_cast(asset_2)), - fc::time_point_sec(),fc::time_point_sec(), - 251), fc::exception); - vector result =db_api.get_trade_history( - std::string( static_cast(asset_1)), - std::string( static_cast(asset_2)), - fc::time_point_sec(),fc::time_point_sec(),250); - BOOST_REQUIRE_EQUAL( result.size(), 0u); - }catch (fc::exception& e) { - edump((e.to_detail_string())); - throw; - } -} -BOOST_AUTO_TEST_CASE(api_limit_get_top_markets){ - try{ - app.enable_plugin("market_history"); - graphene::app::application_options opt=app.get_options(); - opt.has_market_history_plugin = true; - graphene::app::database_api db_api( db, &opt); - const auto& bitusd = create_bitasset("USDBIT"); - asset_id_type asset_1, asset_2; - asset_1 = bitusd.id; - asset_2 = asset_id_type(); - GRAPHENE_CHECK_THROW(db_api.get_top_markets(251), fc::exception); - vector result =db_api.get_top_markets(250); - BOOST_REQUIRE_EQUAL( result.size(), 0u); - }catch (fc::exception& e) { - edump((e.to_detail_string())); - throw; - } -} -BOOST_AUTO_TEST_CASE(api_limit_get_collateral_bids) { - try { - graphene::app::database_api db_api( db, &( app.get_options() )); - - int64_t init_balance = 10000; - ///account_id_type borrower, borrower2, feedproducer; - asset_id_type swan, back; - ACTORS((borrower) (borrower2) (feedproducer)) ; - const auto& bitusd = create_bitasset("USDBIT", feedproducer_id); - swan = bitusd.id; - back = asset_id_type(); - update_feed_producers(swan(db), {feedproducer_id}); - transfer(committee_account, borrower_id, asset(init_balance)); - transfer(committee_account, borrower2_id, asset(init_balance)); - - generate_blocks( HARDFORK_CORE_216_TIME ); - generate_block(); - - price_feed feed; - feed.maintenance_collateral_ratio = 1750; // need to set this explicitly, testnet has a different default - feed.settlement_price = swan(db).amount(1) / back(db).amount(1); - publish_feed(swan(db), feedproducer_id(db), feed); - // start out with 2:1 collateral - borrow(borrower_id(db), swan(db).amount(10), back(db).amount(2*10)); - borrow(borrower2_id(db), swan(db).amount(10), back(db).amount(4*10)); - //feed 1: 2 - feed.settlement_price = swan(db).amount(1) / back(db).amount(2); - publish_feed(swan(db), feedproducer_id(db), feed); - - // this sell order is designed to trigger a black swan - - create_sell_order( borrower2_id(db), swan(db).amount(1), back(db).amount(3) ); - BOOST_CHECK( swan(db).bitasset_data(db).has_settlement() ); - //making 3 collateral bids - for (int i=0; i<3; i++) { - - std::string acct_name = "mytempacct" + std::to_string(i); - account_id_type account_id=create_account(acct_name).id; - transfer(committee_account, account_id, asset(init_balance)); - bid_collateral(account_id(db), back(db).amount(10), swan(db).amount(1)); - } - auto swan_symbol = swan(db).symbol; - - - //validating normal case; total_bids =3 ; result_bids=3 - vector result_bids = db_api.get_collateral_bids(swan_symbol, 250, 0); - BOOST_CHECK_EQUAL( 3u, result_bids.size() ); - - //verify skip /// inefficient code test - //skip < total_bids; skip=1; total_bids =3 ; result_bids=2 - result_bids = db_api.get_collateral_bids(swan_symbol, 250, 1); - BOOST_CHECK_EQUAL( 2u, result_bids.size() ); - //skip= total_bids; skip=3; total_bids =3 ; result_bids=0 - result_bids = db_api.get_collateral_bids(swan_symbol, 250, 3); - BOOST_CHECK_EQUAL( 0u, result_bids.size() ); - //skip> total_bids; skip=4; total_bids =3 ; result_bids=0 - result_bids = db_api.get_collateral_bids(swan_symbol, 250, 4); - BOOST_CHECK_EQUAL( 0u, result_bids.size() ); - - //verify limit // inefficient code test - //limit= api_limit - for (int i=3; i<255; i++) { - std::string acct_name = "mytempacct" + std::to_string(i); - account_id_type account_id=create_account(acct_name).id; - transfer(committee_account, account_id, asset(init_balance)); - bid_collateral(account_id(db), back(db).amount(10), swan(db).amount(1)); - } - result_bids=db_api.get_collateral_bids(swan_symbol, 250, 0); - BOOST_CHECK_EQUAL( 250u, result_bids.size() ); - //limit> api_limit throw error - GRAPHENE_CHECK_THROW(db_api.get_collateral_bids(swan_symbol, 253, 3), fc::exception); - - } - catch (fc::exception& e) { - edump((e.to_detail_string())); - throw; - } -} -BOOST_AUTO_TEST_CASE(api_limit_get_account_limit_orders) { - try { - graphene::app::database_api db_api( db, &( app.get_options() )); - ACTORS((seller)); - const auto &bitcny = create_bitasset("CNY"); - const auto &core = asset_id_type()(db); - - int64_t init_balance(10000000); - transfer(committee_account, seller_id, asset(init_balance)); - - /// Create versatile orders - for (size_t i = 0; i < 250; ++i) { - BOOST_CHECK(create_sell_order(seller, core.amount(100), bitcny.amount(250+i))); - } - - - std::vector results=db_api.get_account_limit_orders(seller.name, GRAPHENE_SYMBOL, "CNY",250); - BOOST_REQUIRE_EQUAL( results.size(), 250u); - GRAPHENE_CHECK_THROW( db_api.get_account_limit_orders(seller.name, GRAPHENE_SYMBOL, "CNY",251), fc::exception); - - } - catch (fc::exception& e) { - edump((e.to_detail_string())); - throw; - } -} -BOOST_AUTO_TEST_CASE( api_limit_lookup_vote_ids ) { - try{ - graphene::app::database_api db_api( db, &( app.get_options() )); - ACTORS( (connie)(whitney)(wolverine) ); - fund(connie); - upgrade_to_lifetime_member(connie); - fund(whitney); - upgrade_to_lifetime_member(whitney); - fund(wolverine); - upgrade_to_lifetime_member(wolverine); - const auto& committee = create_committee_member( connie ); - const auto& witness = create_witness( whitney ); - const auto& worker = create_worker( wolverine_id ); - std::vector votes; - votes.push_back( committee.vote_id ); - votes.push_back( witness.vote_id ); - const auto results = db_api.lookup_vote_ids( votes ); - BOOST_REQUIRE_EQUAL( results.size(), 2u); - votes.push_back( worker.vote_for ); - GRAPHENE_CHECK_THROW(db_api.lookup_vote_ids(votes), fc::exception); - - } catch (fc::exception& e) { - edump((e.to_detail_string())); - throw; - } -} -BOOST_AUTO_TEST_CASE( api_limit_lookup_committee_member_accounts ) { - try{ - graphene::app::database_api db_api( db, &( app.get_options() )); - ACTORS((bob)); - GRAPHENE_CHECK_THROW(db_api.lookup_committee_member_accounts("bob",220), fc::exception); - std::map result =db_api.lookup_committee_member_accounts("bob",190); - BOOST_REQUIRE_EQUAL( result.size(), 10u); - - } catch (fc::exception& e) { - edump((e.to_detail_string())); - throw; - } -} BOOST_AUTO_TEST_SUITE_END() diff --git a/tests/tests/history_api_tests.cpp b/tests/tests/history_api_tests.cpp index e8856fda28..877762bc3b 100644 --- a/tests/tests/history_api_tests.cpp +++ b/tests/tests/history_api_tests.cpp @@ -744,7 +744,7 @@ BOOST_AUTO_TEST_CASE(api_limit_get_relative_account_history) { BOOST_AUTO_TEST_CASE(api_limit_get_account_history_by_operations) { try { graphene::app::history_api hist_api(app); - vector operation_types; + flat_set operation_types; //account_id_type() do 3 ops create_bitasset("USD", account_id_type()); create_account("dan"); diff --git a/tests/tests/htlc_tests.cpp b/tests/tests/htlc_tests.cpp index fda3ab2fb9..1e27e051d1 100644 --- a/tests/tests/htlc_tests.cpp +++ b/tests/tests/htlc_tests.cpp @@ -63,7 +63,7 @@ void generate_random_preimage(uint16_t key_size, std::vector& vec) return; } -void advance_past_hardfork(database_fixture* db_fixture) +void advance_past_htlc_first_hardfork(database_fixture* db_fixture) { db_fixture->generate_blocks(HARDFORK_CORE_1468_TIME); set_expiration(db_fixture->db, db_fixture->trx); @@ -80,7 +80,7 @@ try { transfer( committee_account, alice_id, graphene::chain::asset(init_balance) ); - advance_past_hardfork(this); + advance_past_htlc_first_hardfork(this); uint16_t preimage_size = 256; std::vector pre_image(256); @@ -147,7 +147,8 @@ try { { graphene::chain::htlc_extend_operation big_extend; big_extend.htlc_id = alice_htlc_id; - big_extend.seconds_to_add = db.get_global_properties().parameters.extensions.value.updatable_htlc_options->max_timeout_secs + 10; + big_extend.seconds_to_add = db.get_global_properties().parameters.extensions.value + .updatable_htlc_options->max_timeout_secs + 10; big_extend.fee = db.get_global_properties().parameters.current_fees->calculate_fee(big_extend); big_extend.update_issuer = alice_id; trx.operations.push_back(big_extend); @@ -177,6 +178,347 @@ try { } FC_LOG_AND_RETHROW() } +/**** + * @brief helper to create htlc_create_operation + */ +htlc_create_operation create_htlc(const database& db, const account_id_type& from, const account_id_type& to, + const asset& amount, const graphene::protocol::htlc_hash& preimage_hash, uint16_t preimage_size, + uint64_t seconds, const fc::optional& memo = fc::optional()) +{ + htlc_create_operation ret_val; + ret_val.from = from; + ret_val.to = to; + ret_val.amount = amount; + ret_val.preimage_hash = preimage_hash; + ret_val.preimage_size = preimage_size; + ret_val.claim_period_seconds = seconds; + ret_val.extensions.value.memo = memo; + ret_val.fee = db.get_global_properties().parameters.current_fees->calculate_fee(ret_val); + return ret_val; +} + +/**** + * @brief helper to create a proposal + */ +proposal_create_operation create_proposal(const database& db, const account_id_type& from, + const htlc_create_operation& op, const fc::time_point_sec& expiration) +{ + proposal_create_operation ret_val; + ret_val.fee_paying_account = from; + ret_val.expiration_time = expiration; + ret_val.proposed_ops.emplace_back(op); + ret_val.fee = db.get_global_properties().parameters.current_fees->calculate_fee(ret_val); + return ret_val; +} + +BOOST_AUTO_TEST_CASE( htlc_hf_bsip64 ) +{ +try { + ACTORS((alice)(bob)(joker)); + + int64_t init_balance(100 * GRAPHENE_BLOCKCHAIN_PRECISION); + + transfer( committee_account, alice_id, graphene::chain::asset(init_balance) ); + transfer( committee_account, joker_id, graphene::chain::asset(init_balance) ); + transfer( committee_account, bob_id, graphene::chain::asset(init_balance) ); + + advance_past_htlc_first_hardfork(this); + + uint16_t preimage_size = 256; + std::vector pre_image(256); + generate_random_preimage(preimage_size, pre_image); + + graphene::chain::htlc_id_type alice_htlc_id; + // cler everything out + generate_block(); + trx.clear(); + + /*** + * Proposals before the hardfork + */ + { + BOOST_TEST_MESSAGE( + "Alice is creating a proposal with an HTLC that contains a memo before the hardfork (should fail)"); + memo_data data; + data.from = alice_public_key; + data.to = bob_public_key; + data.set_message( alice_private_key, bob_public_key, "Dear Bob,\n\nMoney!\n\nLove, Alice"); + graphene::chain::htlc_create_operation create_operation = create_htlc(db, alice_id, bob_id, + asset(3 * GRAPHENE_BLOCKCHAIN_PRECISION), hash_it(pre_image), 0, 60, data); + graphene::chain::proposal_create_operation prop1 = create_proposal( + db, alice_id, create_operation, db.head_block_time() + 100); + trx.operations.push_back(prop1); + sign(trx, alice_private_key); + REQUIRE_EXCEPTION_WITH_TEXT(PUSH_TX(db, trx, ~0), "memo"); + trx.clear(); + } + { + BOOST_TEST_MESSAGE("Alice is creating a proposal with HASH160 (should fail)"); + graphene::chain::htlc_create_operation create_operation = create_htlc(db, alice_id, bob_id, + asset(3 * GRAPHENE_BLOCKCHAIN_PRECISION), hash_it(pre_image), 0, 60); + graphene::chain::proposal_create_operation prop1 = create_proposal( + db, alice_id, create_operation, db.head_block_time() + 100); + trx.operations.push_back(prop1); + sign(trx, alice_private_key); + REQUIRE_EXCEPTION_WITH_TEXT(PUSH_TX(db, trx, ~0), "HASH160"); + trx.clear(); + } + { + BOOST_TEST_MESSAGE("Alice is creating a proposal with a preimage size of 0 (should pass)"); + graphene::chain::htlc_create_operation create_operation = create_htlc(db, alice_id, bob_id, + asset(3 * GRAPHENE_BLOCKCHAIN_PRECISION), hash_it(pre_image), 0, 60); + graphene::chain::proposal_create_operation prop1 = create_proposal( + db, alice_id, create_operation, db.head_block_time() + 100); + trx.operations.push_back(prop1); + sign(trx, alice_private_key); + PUSH_TX(db, trx, ~0); + trx.clear(); + } + /*** + * HTLC Contracts (non-proposals) before hardfork + */ + { + BOOST_TEST_MESSAGE("Alice is creating an HTLC that contains a memo before the hardfork (should fail)"); + memo_data data; + data.from = alice_public_key; + data.to = bob_public_key; + data.set_message( alice_private_key, bob_public_key, "Dear Bob,\n\nMoney!\n\nLove, Alice"); + graphene::chain::htlc_create_operation create_operation = create_htlc(db, alice_id, bob_id, + asset(3 * GRAPHENE_BLOCKCHAIN_PRECISION), hash_it(pre_image), 0, 60, data); + trx.operations.push_back(create_operation); + sign(trx, alice_private_key); + REQUIRE_EXCEPTION_WITH_TEXT(PUSH_TX(db, trx, ~0), "memo"); + trx.clear(); + } + { + BOOST_TEST_MESSAGE("Alice is creating an HTLC with HASH160 (should fail)"); + graphene::chain::htlc_create_operation create_operation = create_htlc(db, alice_id, bob_id, + asset(3 * GRAPHENE_BLOCKCHAIN_PRECISION), hash_it(pre_image), 0, 60); + trx.operations.push_back(create_operation); + sign(trx, alice_private_key); + REQUIRE_EXCEPTION_WITH_TEXT(PUSH_TX(db, trx, ~0), "HASH160"); + trx.clear(); + } + { + BOOST_TEST_MESSAGE("Alice is creating an HTLC with a preimage size of 0 (should pass)"); + graphene::chain::htlc_create_operation create_operation = create_htlc(db, alice_id, bob_id, + asset(3 * GRAPHENE_BLOCKCHAIN_PRECISION), hash_it(pre_image), 0, 60); + trx.operations.push_back(create_operation); + sign(trx, alice_private_key); + htlc_id_type htlc_id = PUSH_TX(db, trx, ~0).operation_results[0].get(); + trx.clear(); + BOOST_TEST_MESSAGE("Bob attempts to redeem, but can't because preimage size is 0 (should fail)"); + graphene::chain::htlc_redeem_operation redeem; + redeem.htlc_id = htlc_id; + redeem.preimage = pre_image; + redeem.redeemer = bob_id; + redeem.fee = db.current_fee_schedule().calculate_fee(redeem); + trx.operations.push_back(redeem); + REQUIRE_EXCEPTION_WITH_TEXT(PUSH_TX(db, trx, ~0), "Preimage size mismatch"); + trx.clear(); + } + + // Alice creates an asset + BOOST_TEST_MESSAGE("Create ALICECOIN so transfer_restricted can be controlled"); + const asset_id_type uia_id = create_user_issued_asset( "ALICECOIN", alice, transfer_restricted).id; + BOOST_TEST_MESSAGE("Issuing ALICECOIN to Bob"); + issue_uia(bob, asset(10000, uia_id) ); + // verify transfer restrictions are in place + REQUIRE_EXCEPTION_WITH_TEXT(transfer(bob, joker, asset(1, uia_id)), "transfer"); + trx.operations.clear(); + + { + BOOST_TEST_MESSAGE("Bob wants to transfer ALICECOIN, which is transfer_restricted (allowed before HF)"); + graphene::chain::htlc_create_operation create_operation = create_htlc(db, bob_id, joker_id, + asset(3, uia_id), hash_it(pre_image), preimage_size, 60); + trx.operations.push_back(create_operation); + sign(trx, alice_private_key); + PUSH_TX(db, trx, ~0); + trx.clear(); + } + { + BOOST_TEST_MESSAGE( + "Bob wants to transfer ALICECOIN within a proposal, always allowed, although will fail later"); + graphene::chain::htlc_create_operation create_operation = create_htlc(db, bob_id, joker_id, asset(3,uia_id), + hash_it(pre_image), preimage_size, 60); + graphene::chain::proposal_create_operation prop_create = create_proposal( + db, bob_id, create_operation, db.head_block_time() + 100); + trx.operations.push_back(prop_create); + sign(trx, bob_private_key); + PUSH_TX(db, trx, ~0); + trx.clear(); + } + + BOOST_TEST_MESSAGE("Fast Forwarding beyond HF BSIP64"); + generate_blocks( HARDFORK_CORE_BSIP64_TIME + 60 ); + set_expiration( db, trx ); + + /*** + * Proposals after the hardfork + */ + { + BOOST_TEST_MESSAGE("Alice is creating a proposal with an HTLC that contains a memo (should pass)"); + memo_data data; + data.from = alice_public_key; + data.to = bob_public_key; + data.set_message( alice_private_key, bob_public_key, "Dear Bob,\n\nMoney!\n\nLove, Alice"); + graphene::chain::htlc_create_operation create_operation = create_htlc(db, alice_id, bob_id, + asset(3 * GRAPHENE_BLOCKCHAIN_PRECISION), hash_it(pre_image), 0, 60, data); + graphene::chain::proposal_create_operation prop1 = create_proposal( + db, alice_id, create_operation, db.head_block_time() + 100); + trx.operations.push_back(prop1); + sign(trx, alice_private_key); + PUSH_TX(db, trx, ~0); + trx.clear(); + } + { + BOOST_TEST_MESSAGE("Alice is creating a proposal with HASH160 (should pass)"); + graphene::chain::htlc_create_operation create_operation = create_htlc(db, alice_id, bob_id, + asset(3 * GRAPHENE_BLOCKCHAIN_PRECISION), hash_it(pre_image), 0, 60); + graphene::chain::proposal_create_operation prop1 = create_proposal( + db, alice_id, create_operation, db.head_block_time() + 100); + trx.operations.push_back(prop1); + sign(trx, alice_private_key); + PUSH_TX(db, trx, ~0); + trx.clear(); + } + { + BOOST_TEST_MESSAGE("Alice is creating a proposal with a preimage size of 0 (should pass)"); + graphene::chain::htlc_create_operation create_operation = create_htlc(db, alice_id, bob_id, + asset(3 * GRAPHENE_BLOCKCHAIN_PRECISION), hash_it(pre_image), 0, 60); + graphene::chain::proposal_create_operation prop1 = create_proposal( + db, alice_id, create_operation, db.head_block_time() + 100); + trx.operations.push_back(prop1); + sign(trx, alice_private_key); + PUSH_TX(db, trx, ~0); + trx.clear(); + } + /*** + * HTLC Contracts (non-proposals) after hardfork + */ + { + BOOST_TEST_MESSAGE("Alice is creating an HTLC that contains a memo after the hardfork (should pass)"); + memo_data data; + data.from = alice_public_key; + data.to = bob_public_key; + data.set_message( alice_private_key, bob_public_key, "Dear Bob,\n\nMoney!\n\nLove, Alice"); + graphene::chain::htlc_create_operation create_operation = create_htlc(db, alice_id, bob_id, + asset(3 * GRAPHENE_BLOCKCHAIN_PRECISION), hash_it(pre_image), 0, 60, data); + trx.operations.push_back(create_operation); + sign(trx, alice_private_key); + PUSH_TX(db, trx, ~0); + trx.clear(); + } + { + BOOST_TEST_MESSAGE("Alice is creating an HTLC with HASH160 (should pass)"); + graphene::chain::htlc_create_operation create_operation = create_htlc(db, alice_id, bob_id, + asset(3 * GRAPHENE_BLOCKCHAIN_PRECISION), hash_it(pre_image), 0, 60); + trx.operations.push_back(create_operation); + sign(trx, alice_private_key); + PUSH_TX(db, trx, ~0); + trx.clear(); + } + { + BOOST_TEST_MESSAGE("Bob wants to transfer ALICECOIN, which is transfer_restricted (no longer allowed)"); + graphene::chain::htlc_create_operation create_operation = create_htlc(db, bob_id, joker_id, + asset(3, uia_id), hash_it(pre_image), preimage_size, 60); + trx.operations.push_back(create_operation); + sign(trx, alice_private_key); + REQUIRE_EXCEPTION_WITH_TEXT(PUSH_TX(db, trx, ~0), "transfer_restricted"); + trx.clear(); + } + { + BOOST_TEST_MESSAGE( + "Bob wants to transfer ALICECOIN within a proposal, always allowed, although will fail later"); + graphene::chain::htlc_create_operation create_operation = create_htlc(db, bob_id, joker_id, asset(3,uia_id), + hash_it(pre_image), preimage_size, 60); + graphene::chain::proposal_create_operation prop_create = create_proposal( + db, bob_id, create_operation, db.head_block_time() + 100); + trx.operations.push_back(prop_create); + sign(trx, bob_private_key); + PUSH_TX(db, trx, ~0); + trx.clear(); + } + { + BOOST_TEST_MESSAGE("A memo field should include a charge per kb (uses fee from transfer_operation)"); + htlc_create_operation op = create_htlc(db, alice_id, bob_id, asset(3), hash_it(pre_image), + preimage_size, 60); + asset no_memo_fee = op.fee; + memo_data data; + data.from = alice_public_key; + data.to = bob_public_key; + data.set_message( alice_private_key, bob_public_key, "Dear Bob,\n\nMoney!\n\nLove, Alice"); + op.extensions.value.memo = data; + asset with_memo_fee = db.current_fee_schedule().calculate_fee(op); + BOOST_CHECK_GT( with_memo_fee.amount.value, no_memo_fee.amount.value ); + } + // After HF 64, a zero in the preimage_size means 2 things, no preimage (can happen, but who would do such a + // thing), or simply skip the size validation. To test, we must attempt to redeem both cases + { + // case 1: 0 preimage with 0 preimage_size + BOOST_TEST_MESSAGE("Attempt to create an HTLC with no preimage (should pass)"); + htlc_create_operation op = create_htlc(db, alice_id, bob_id, asset(3 * GRAPHENE_BLOCKCHAIN_PRECISION), + hash_it(std::vector()), 0, 60); + trx.operations.push_back(op); + sign(trx, alice_private_key); + graphene::protocol::processed_transaction results = PUSH_TX(db, trx, ~0); + trx.operations.clear(); + htlc_id_type htlc_id = results.operation_results[0].get(); + BOOST_TEST_MESSAGE("Attempt to redeem HTLC that has no preimage, but include one anyway (should fail)"); + htlc_redeem_operation redeem; + redeem.htlc_id = htlc_id; + redeem.preimage = pre_image; + redeem.redeemer = bob_id; + redeem.fee = db.current_fee_schedule().calculate_fee(redeem); + trx.operations.push_back(redeem); + sign(trx, bob_private_key); + REQUIRE_EXCEPTION_WITH_TEXT(PUSH_TX(db, trx, ~0), "Provided preimage does not generate"); + trx.operations.clear(); + BOOST_TEST_MESSAGE("Attempt to redeem HTLC that has no preimage (should pass)"); + redeem.preimage = std::vector(); + redeem.fee = db.current_fee_schedule().calculate_fee(redeem); + trx.operations.push_back(redeem); + sign(trx, bob_private_key); + PUSH_TX(db, trx, ~0); + trx.operations.clear(); + } + { + // case 2: a real preimage with 0 preimage size + htlc_create_operation op = create_htlc(db, alice_id, bob_id, asset(3 * GRAPHENE_BLOCKCHAIN_PRECISION), + hash_it(pre_image), 0, 60); + trx.operations.push_back(op); + sign(trx, alice_private_key); + graphene::protocol::processed_transaction results = PUSH_TX(db, trx, ~0); + trx.operations.clear(); + htlc_id_type htlc_id = results.operation_results[0].get(); + BOOST_TEST_MESSAGE("Attempt to redeem with no preimage (should fail)"); + htlc_redeem_operation redeem; + redeem.htlc_id = htlc_id; + redeem.preimage = std::vector(); + redeem.redeemer = bob_id; + redeem.fee = db.current_fee_schedule().calculate_fee(redeem); + trx.operations.push_back(redeem); + sign(trx, bob_private_key); + REQUIRE_EXCEPTION_WITH_TEXT(PUSH_TX(db, trx, ~0), "Provided preimage does not generate"); + trx.operations.clear(); + BOOST_TEST_MESSAGE("Attempt to redeem with no preimage size, but incorrect preimage"); + redeem.preimage = std::vector{'H','e','l','l','o'}; + redeem.fee = db.current_fee_schedule().calculate_fee(redeem); + trx.operations.push_back(redeem); + sign(trx, bob_private_key); + REQUIRE_EXCEPTION_WITH_TEXT(PUSH_TX(db, trx, ~0), "Provided preimage does not generate"); + trx.operations.clear(); + BOOST_TEST_MESSAGE("Attempt to redeem with no preimage size (should pass)"); + redeem.preimage = pre_image; + redeem.fee = db.current_fee_schedule().calculate_fee(redeem); + trx.operations.push_back(redeem); + sign(trx, bob_private_key); + PUSH_TX(db, trx, ~0); + trx.operations.clear(); + } +} FC_LOG_AND_RETHROW() +} + BOOST_AUTO_TEST_CASE( htlc_fulfilled ) { try { @@ -188,7 +530,7 @@ try { transfer( committee_account, bob_id, graphene::chain::asset(init_balance) ); transfer( committee_account, joker_id, graphene::chain::asset(init_balance) ); - advance_past_hardfork(this); + advance_past_htlc_first_hardfork(this); uint16_t preimage_size = 256; std::vector pre_image(preimage_size); @@ -271,7 +613,7 @@ try { BOOST_AUTO_TEST_CASE( other_peoples_money ) { try { - advance_past_hardfork(this); + advance_past_htlc_first_hardfork(this); ACTORS((alice)(bob)); @@ -323,6 +665,9 @@ try { BOOST_AUTO_TEST_CASE( htlc_hardfork_test ) { try { + ACTORS( (alice) ); + int64_t init_balance(10000 * GRAPHENE_BLOCKCHAIN_PRECISION); + transfer( committee_account, alice_id, graphene::chain::asset(init_balance) ); { // try to set committee parameters before hardfork proposal_create_operation cop = proposal_create_operation::committee_proposal( @@ -388,7 +733,7 @@ BOOST_AUTO_TEST_CASE( htlc_hardfork_test ) // now things should start working... BOOST_TEST_MESSAGE("Advancing to HTLC hardfork time."); - advance_past_hardfork(this); + advance_past_htlc_first_hardfork(this); proposal_id_type good_proposal_id; BOOST_TEST_MESSAGE( "Creating a proposal to change the max_preimage_size to 2048 and set higher fees" ); @@ -411,7 +756,8 @@ BOOST_AUTO_TEST_CASE( htlc_hardfork_test ) new_fee_schedule->parameters.insert( (*itr).second); } } - proposal_create_operation cop = proposal_create_operation::committee_proposal(db.get_global_properties().parameters, db.head_block_time()); + proposal_create_operation cop = proposal_create_operation::committee_proposal(db.get_global_properties() + .parameters, db.head_block_time()); cop.fee_paying_account = GRAPHENE_TEMP_ACCOUNT; cop.expiration_time = db.head_block_time() + *cop.review_period_seconds + 10; committee_member_update_global_parameters_operation uop; @@ -442,12 +788,14 @@ BOOST_AUTO_TEST_CASE( htlc_hardfork_test ) } BOOST_TEST_MESSAGE( "Verifying that the parameters didn't change immediately" ); - BOOST_CHECK_EQUAL(db.get_global_properties().parameters.extensions.value.updatable_htlc_options->max_preimage_size, 19200u); + BOOST_CHECK_EQUAL( + db.get_global_properties().parameters.extensions.value.updatable_htlc_options->max_preimage_size, 19200u); BOOST_TEST_MESSAGE( "Generating blocks until proposal expires" ); generate_blocks(good_proposal_id(db).expiration_time + 5); BOOST_TEST_MESSAGE( "Verify that the parameters still have not changed" ); - BOOST_CHECK_EQUAL(db.get_global_properties().parameters.extensions.value.updatable_htlc_options->max_preimage_size, 19200u); + BOOST_CHECK_EQUAL(db.get_global_properties() + .parameters.extensions.value.updatable_htlc_options->max_preimage_size, 19200u); BOOST_TEST_MESSAGE( "Generating blocks until next maintenance interval" ); generate_blocks(db.get_dynamic_global_properties().next_maintenance_time); @@ -455,13 +803,17 @@ BOOST_AUTO_TEST_CASE( htlc_hardfork_test ) BOOST_TEST_MESSAGE( "Verify that the change has been implemented" ); - BOOST_CHECK_EQUAL(db.get_global_properties().parameters.extensions.value.updatable_htlc_options->max_preimage_size, 2048u); - const graphene::chain::fee_schedule& current_fee_schedule = *(db.get_global_properties().parameters.current_fees); + BOOST_CHECK_EQUAL(db.get_global_properties() + .parameters.extensions.value.updatable_htlc_options->max_preimage_size, 2048u); + const graphene::chain::fee_schedule& current_fee_schedule = + *(db.get_global_properties().parameters.current_fees); const htlc_create_operation::fee_parameters_type& htlc_fee = current_fee_schedule.get(); BOOST_CHECK_EQUAL(htlc_fee.fee, 2 * GRAPHENE_BLOCKCHAIN_PRECISION); - -} FC_LOG_AND_RETHROW() } + + } + FC_LOG_AND_RETHROW() +} BOOST_AUTO_TEST_CASE( htlc_before_hardfork ) { try { @@ -562,13 +914,13 @@ BOOST_AUTO_TEST_CASE( fee_calculations ) htlc_create_operation create; // no days create.claim_period_seconds = 0; - BOOST_CHECK_EQUAL( create.calculate_fee(create_fee).value, 2 ); + BOOST_CHECK_EQUAL( create.calculate_fee(create_fee, 2).value, 2 ); // exactly 1 day create.claim_period_seconds = 60 * 60 * 24; - BOOST_CHECK_EQUAL( create.calculate_fee(create_fee).value, 4 ); + BOOST_CHECK_EQUAL( create.calculate_fee(create_fee, 2).value, 4 ); // tad over a day create.claim_period_seconds++; - BOOST_CHECK_EQUAL( create.calculate_fee(create_fee).value, 6 ); + BOOST_CHECK_EQUAL( create.calculate_fee(create_fee, 2).value, 6 ); } // redeem { @@ -633,7 +985,7 @@ try { fund( alice, graphene::chain::asset(init_balance) ); fund( bob, graphene::chain::asset(init_balance) ); - advance_past_hardfork(this); + advance_past_htlc_first_hardfork(this); // blacklist bob { @@ -924,6 +1276,4 @@ try { } } - - BOOST_AUTO_TEST_SUITE_END() diff --git a/tests/tests/market_fee_sharing_tests.cpp b/tests/tests/market_fee_sharing_tests.cpp index 7276d87e60..4e6b12b35f 100644 --- a/tests/tests/market_fee_sharing_tests.cpp +++ b/tests/tests/market_fee_sharing_tests.cpp @@ -90,6 +90,20 @@ struct reward_database_fixture : database_fixture PUSH_TX( db, tx); } + void generate_blocks_past_hf1774() + { + generate_blocks( HARDFORK_1774_TIME ); + generate_block(); + set_expiration(db, trx); + } + + void generate_blocks_past_hf1800() + { + database_fixture::generate_blocks( HARDFORK_CORE_1800_TIME ); + database_fixture::generate_block(); + set_expiration(db, trx); + } + asset core_asset(int64_t x ) { return asset( x*core_precision ); @@ -108,7 +122,7 @@ struct reward_database_fixture : database_fixture BOOST_FIXTURE_TEST_SUITE( fee_sharing_tests, reward_database_fixture ) -BOOST_AUTO_TEST_CASE(create_asset_with_additional_options_after_hf) +BOOST_AUTO_TEST_CASE(cannot_create_asset_with_reward_percent_of_100_before_hf1774) { try { @@ -160,7 +174,7 @@ BOOST_AUTO_TEST_CASE(create_asset_with_additional_options_after_hf) FC_LOG_AND_RETHROW() } -BOOST_AUTO_TEST_CASE(update_additional_options_after_hf) +BOOST_AUTO_TEST_CASE(cannot_set_reward_percent_to_100_before_hf1774) { try { @@ -190,6 +204,59 @@ BOOST_AUTO_TEST_CASE(update_additional_options_after_hf) FC_LOG_AND_RETHROW() } +BOOST_AUTO_TEST_CASE(create_asset_with_reward_percent_of_100_after_hf1774) +{ + try + { + generate_blocks_past_hf1774(); + + ACTOR(issuer); + + uint16_t reward_percent = GRAPHENE_100_PERCENT; // 100.00% + flat_set whitelist = {issuer_id}; + price price(asset(1, asset_id_type(1)), asset(1)); + uint16_t market_fee_percent = 100; + + additional_asset_options_t options; + options.value.reward_percent = reward_percent; + options.value.whitelist_market_fee_sharing = whitelist; + + asset_object usd_asset = create_user_issued_asset("USD", + issuer, + charge_market_fee, + price, + 2, + market_fee_percent, + options); + + additional_asset_options usd_options = usd_asset.options.extensions.value; + BOOST_CHECK_EQUAL(reward_percent, *usd_options.reward_percent); + BOOST_CHECK(whitelist == *usd_options.whitelist_market_fee_sharing); + } + FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(set_reward_percent_to_100_after_hf1774) +{ + try + { + ACTOR(issuer); + + asset_object usd_asset = create_user_issued_asset("USD", issuer, charge_market_fee); // make a copy + + generate_blocks_past_hf1774(); + + uint16_t reward_percent = GRAPHENE_100_PERCENT; // 100.00% + flat_set whitelist = {issuer_id}; + update_asset(issuer_id, issuer_private_key, usd_asset.get_id(), reward_percent, whitelist); + + additional_asset_options options = usd_asset.get_id()(db).options.extensions.value; + BOOST_CHECK_EQUAL(reward_percent, *options.reward_percent); + BOOST_CHECK(whitelist == *options.whitelist_market_fee_sharing); + } + FC_LOG_AND_RETHROW() +} + BOOST_AUTO_TEST_CASE(asset_rewards_test) { try @@ -369,30 +436,170 @@ BOOST_AUTO_TEST_CASE(create_actors) { try { - ACTORS((jill)(izzyregistrar)(izzyreferrer)); + ACTORS((jill)(izzyregistrar)(izzyreferrer)(tempregistrar)); upgrade_to_lifetime_member(izzyregistrar); upgrade_to_lifetime_member(izzyreferrer); + upgrade_to_lifetime_member(tempregistrar); price price(asset(1, asset_id_type(1)), asset(1)); uint16_t market_fee_percent = 20 * GRAPHENE_1_PERCENT; - auto obj = jill_id(db); - const asset_object jillcoin = create_user_issued_asset( "JCOIN", jill, charge_market_fee, price, 2, market_fee_percent ); + const asset_object jillcoin = create_user_issued_asset( "JCOIN", jill, charge_market_fee, + price, 2, market_fee_percent ); const account_object alice = create_account("alice", izzyregistrar, izzyreferrer, 50/*0.5%*/); const account_object bob = create_account("bob", izzyregistrar, izzyreferrer, 50/*0.5%*/); + const account_object old = create_account("old", GRAPHENE_TEMP_ACCOUNT(db), + GRAPHENE_COMMITTEE_ACCOUNT(db), 50u); + const account_object tmp = create_account("tmp", tempregistrar, + GRAPHENE_TEMP_ACCOUNT(db), 50u); // prepare users' balance issue_uia( alice, jillcoin.amount( 20000000 ) ); transfer( committee_account, alice.get_id(), core_asset(1000) ); transfer( committee_account, bob.get_id(), core_asset(1000) ); + transfer( committee_account, old.get_id(), core_asset(1000) ); + transfer( committee_account, tmp.get_id(), core_asset(1000) ); transfer( committee_account, izzyregistrar.get_id(), core_asset(1000) ); transfer( committee_account, izzyreferrer.get_id(), core_asset(1000) ); + transfer( committee_account, tempregistrar.get_id(), core_asset(1000) ); } FC_LOG_AND_RETHROW() } +BOOST_AUTO_TEST_CASE(fee_shares_between_temp_acc_and_committee_acc_before_hf_1800) +{ + try + { + INVOKE(create_actors); + + GET_ACTOR(jill); + + constexpr auto jillcoin_reward_percent = 2*GRAPHENE_1_PERCENT; + const asset_object &jillcoin = get_asset("JCOIN"); + + flat_set whitelist; + update_asset(jill_id, jill_private_key, jillcoin.get_id(), jillcoin_reward_percent, whitelist); + + BOOST_CHECK_EQUAL( get_market_fee_reward( GRAPHENE_TEMP_ACCOUNT(db), jillcoin), 0); + BOOST_CHECK_EQUAL( get_market_fee_reward( GRAPHENE_COMMITTEE_ACCOUNT(db), jillcoin), 0); + + GET_ACTOR(alice); + GET_ACTOR(old); + + create_sell_order( alice, jillcoin.amount(100000), core_asset(1) ); + create_sell_order( old, core_asset(1), jillcoin.amount(100000) ); + + BOOST_CHECK_GT( get_market_fee_reward( GRAPHENE_TEMP_ACCOUNT(db), jillcoin), 0); + BOOST_CHECK_GT( get_market_fee_reward( GRAPHENE_COMMITTEE_ACCOUNT(db), jillcoin), 0); + + } + FC_LOG_AND_RETHROW() + +} + +BOOST_AUTO_TEST_CASE(fee_do_not_share_between_temp_acc_and_committee_acc_after_hf_1800) +{ + try + { + INVOKE(create_actors); + + generate_blocks_past_hf1800(); + GET_ACTOR(jill); + + constexpr auto jillcoin_reward_percent = 2*GRAPHENE_1_PERCENT; + const asset_object &jillcoin = get_asset("JCOIN"); + + flat_set whitelist; + update_asset(jill_id, jill_private_key, jillcoin.get_id(), jillcoin_reward_percent, whitelist); + + BOOST_CHECK_EQUAL( get_market_fee_reward( GRAPHENE_TEMP_ACCOUNT(db), jillcoin), 0); + BOOST_CHECK_EQUAL( get_market_fee_reward( GRAPHENE_COMMITTEE_ACCOUNT(db), jillcoin), 0); + + GET_ACTOR(alice); + GET_ACTOR(old); + + create_sell_order( alice, jillcoin.amount(100000), core_asset(1) ); + create_sell_order( old, core_asset(1), jillcoin.amount(100000) ); + + BOOST_CHECK_EQUAL( get_market_fee_reward( GRAPHENE_TEMP_ACCOUNT(db), jillcoin), 0); + BOOST_CHECK_GT( get_market_fee_reward( GRAPHENE_COMMITTEE_ACCOUNT(db), jillcoin), 0); + + } + FC_LOG_AND_RETHROW() + +} + +BOOST_AUTO_TEST_CASE(fee_shares_to_temp_referrer_before_hf_1800) +{ + try + { + INVOKE(create_actors); + + GET_ACTOR(jill); + GET_ACTOR(tempregistrar); + + constexpr auto jillcoin_reward_percent = 2*GRAPHENE_1_PERCENT; + const asset_object &jillcoin = get_asset("JCOIN"); + + flat_set whitelist; + update_asset(jill_id, jill_private_key, jillcoin.get_id(), jillcoin_reward_percent, whitelist); + + BOOST_CHECK_EQUAL( get_market_fee_reward( GRAPHENE_TEMP_ACCOUNT(db), jillcoin), 0); + BOOST_CHECK_EQUAL( get_market_fee_reward( GRAPHENE_COMMITTEE_ACCOUNT(db), jillcoin), 0); + BOOST_CHECK_EQUAL( get_market_fee_reward( tempregistrar, jillcoin), 0); + + GET_ACTOR(alice); + GET_ACTOR(tmp); + + create_sell_order( alice, jillcoin.amount(100000), core_asset(1) ); + create_sell_order( tmp, core_asset(1), jillcoin.amount(100000) ); + + BOOST_CHECK_GT( get_market_fee_reward( GRAPHENE_TEMP_ACCOUNT(db), jillcoin), 0); + BOOST_CHECK_EQUAL( get_market_fee_reward( GRAPHENE_COMMITTEE_ACCOUNT(db), jillcoin), 0); + BOOST_CHECK_GT( get_market_fee_reward( tempregistrar, jillcoin), 0); + + } + FC_LOG_AND_RETHROW() + +} + +BOOST_AUTO_TEST_CASE(fee_do_not_share_to_temp_referrer_after_hf_1800) +{ + try + { + INVOKE(create_actors); + + generate_blocks_past_hf1800(); + GET_ACTOR(jill); + GET_ACTOR(tempregistrar); + + constexpr auto jillcoin_reward_percent = 2*GRAPHENE_1_PERCENT; + const asset_object &jillcoin = get_asset("JCOIN"); + + flat_set whitelist; + update_asset(jill_id, jill_private_key, jillcoin.get_id(), jillcoin_reward_percent, whitelist); + + BOOST_CHECK_EQUAL( get_market_fee_reward( GRAPHENE_TEMP_ACCOUNT(db), jillcoin), 0); + BOOST_CHECK_EQUAL( get_market_fee_reward( GRAPHENE_COMMITTEE_ACCOUNT(db), jillcoin), 0); + BOOST_CHECK_EQUAL( get_market_fee_reward( tempregistrar, jillcoin), 0); + + GET_ACTOR(alice); + GET_ACTOR(tmp); + + create_sell_order( alice, jillcoin.amount(100000), core_asset(1) ); + create_sell_order( tmp, core_asset(1), jillcoin.amount(100000) ); + + BOOST_CHECK_EQUAL( get_market_fee_reward( GRAPHENE_TEMP_ACCOUNT(db), jillcoin), 0); + BOOST_CHECK_GT( get_market_fee_reward( GRAPHENE_COMMITTEE_ACCOUNT(db), jillcoin), 0); + BOOST_CHECK_GT( get_market_fee_reward( tempregistrar, jillcoin), 0); + + } + FC_LOG_AND_RETHROW() + +} + BOOST_AUTO_TEST_CASE(white_list_is_empty_test) { try @@ -530,7 +737,7 @@ BOOST_AUTO_TEST_CASE(create_asset_via_proposal_test) { ACTOR(issuer); price core_exchange_rate(asset(1, asset_id_type(1)), asset(1)); - + asset_create_operation create_op; create_op.issuer = issuer.id; create_op.fee = asset(); @@ -857,7 +1064,7 @@ BOOST_AUTO_TEST_CASE(white_list_asset_rewards_test) } BOOST_AUTO_TEST_CASE( create_vesting_balance_object_test ) -{ +{ /** * Test checks that an account could have duplicates VBO (with the same asset_type) * for any type of vesting_balance_type @@ -884,4 +1091,57 @@ BOOST_AUTO_TEST_CASE( create_vesting_balance_object_test ) } FC_LOG_AND_RETHROW() } + +BOOST_AUTO_TEST_CASE( possibility_to_set_100_reward_percent_after_hf1774 ) +{ + try + { + INVOKE(create_actors); + + generate_blocks_past_hf1774(); + GET_ACTOR(jill); + + constexpr auto jillcoin_reward_percent = 100*GRAPHENE_1_PERCENT; + const asset_object &jillcoin = get_asset("JCOIN"); + + update_asset(jill_id, jill_private_key, jillcoin.get_id(), jillcoin_reward_percent); + + GET_ACTOR(izzyregistrar); + GET_ACTOR(izzyreferrer); + BOOST_CHECK_EQUAL( get_market_fee_reward( izzyregistrar, jillcoin ), 0 ); + BOOST_CHECK_EQUAL( get_market_fee_reward( izzyreferrer, jillcoin ), 0 ); + + GET_ACTOR(alice); + GET_ACTOR(bob); + + const share_type sell_amount = 100000; + + // Alice and Bob place orders which match + create_sell_order( alice, jillcoin.amount(sell_amount), core_asset(1) ); + create_sell_order( bob, core_asset(1), jillcoin.amount(sell_amount) ); + + const auto izzyregistrar_reward = get_market_fee_reward( izzyregistrar, jillcoin ); + const auto izzyreferrer_reward = get_market_fee_reward( izzyreferrer, jillcoin ); + + BOOST_CHECK_GT(izzyregistrar_reward , 0); + BOOST_CHECK_GT(izzyreferrer_reward , 0); + + auto calculate_percent = [](const share_type& value, uint16_t percent) + { + auto a(value.value); + a *= percent; + a /= GRAPHENE_100_PERCENT; + return a; + }; + //jillcoin has 20% market fee percent, see create_actors + //all market fees are distributed between registrar and referrer + auto acc_rewards = izzyregistrar_reward + izzyreferrer_reward; + BOOST_CHECK_EQUAL( calculate_percent(sell_amount, 20*GRAPHENE_1_PERCENT), acc_rewards); + + //check referrer reward + BOOST_CHECK_EQUAL( calculate_percent(acc_rewards, alice.referrer_rewards_percentage), izzyreferrer_reward); + } + FC_LOG_AND_RETHROW() +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/tests/tests/operation_tests.cpp b/tests/tests/operation_tests.cpp index 205adb8281..ecf906b6a9 100644 --- a/tests/tests/operation_tests.cpp +++ b/tests/tests/operation_tests.cpp @@ -836,6 +836,68 @@ BOOST_AUTO_TEST_CASE( prediction_market_resolves_to_0 ) } } +/*** + * Prediction markets should not suffer a black swan (Issue #460) + */ +BOOST_AUTO_TEST_CASE( prediction_market_black_swan ) +{ + try { + ACTORS((judge)(dan)(nathan)); + + // progress to recent hardfork + generate_blocks( HARDFORK_CORE_1270_TIME ); + set_expiration( db, trx ); + + const auto& pmark = create_prediction_market("PMARK", judge_id); + + int64_t init_balance(1000000); + transfer(committee_account, judge_id, asset(init_balance)); + transfer(committee_account, dan_id, asset(init_balance)); + + update_feed_producers( pmark, { judge_id }); + price_feed feed; + feed.settlement_price = asset( 1, pmark.id ) / asset( 1 ); + publish_feed( pmark, judge, feed ); + + borrow( dan, pmark.amount(1000), asset(1000) ); + + // feed a price that will cause a black swan + feed.settlement_price = asset( 1, pmark.id ) / asset( 1000 ); + publish_feed( pmark, judge, feed ); + + // verify a black swan happened + GRAPHENE_REQUIRE_THROW(borrow( dan, pmark.amount(1000), asset(1000) ), fc::exception); + trx.clear(); + + // progress past hardfork + generate_blocks( HARDFORK_CORE_460_TIME + db.get_global_properties().parameters.maintenance_interval ); + set_expiration( db, trx ); + + // create another prediction market to test the hardfork + const auto& pmark2 = create_prediction_market("PMARKII", judge_id); + update_feed_producers( pmark2, { judge_id }); + price_feed feed2; + feed2.settlement_price = asset( 1, pmark2.id ) / asset( 1 ); + publish_feed( pmark2, judge, feed2 ); + + borrow( dan, pmark2.amount(1000), asset(1000) ); + + // feed a price that would have caused a black swan + feed2.settlement_price = asset( 1, pmark2.id ) / asset( 1000 ); + publish_feed( pmark2, judge, feed2 ); + + // verify a black swan did not happen + borrow( dan, pmark2.amount(1000), asset(1000) ); + + generate_block(~database::skip_transaction_dupe_check); + generate_blocks( db.get_dynamic_global_properties().next_maintenance_time ); + generate_block(); + } catch( const fc::exception& e) { + edump((e.to_detail_string())); + throw; + } +} + BOOST_AUTO_TEST_CASE( create_account_test ) { try { diff --git a/tests/tests/settle_tests.cpp b/tests/tests/settle_tests.cpp index 6310493ae5..51e4c24036 100644 --- a/tests/tests/settle_tests.cpp +++ b/tests/tests/settle_tests.cpp @@ -1501,6 +1501,354 @@ BOOST_AUTO_TEST_CASE( global_settle_rounding_test_after_hf_184 ) } FC_LOG_AND_RETHROW() } +BOOST_AUTO_TEST_CASE( create_bitassets ) +{ + try { + + generate_blocks( HARDFORK_480_TIME ); // avoid being affected by the price feed bug + generate_block(); + set_expiration( db, trx ); + + ACTORS((paul)(rachelregistrar)(rachelreferrer)); + + upgrade_to_lifetime_member(rachelregistrar); + upgrade_to_lifetime_member(rachelreferrer); + + constexpr auto market_fee_percent = 50 * GRAPHENE_1_PERCENT; + constexpr auto biteur_reward_percent = 90 * GRAPHENE_1_PERCENT; + constexpr auto referrer_reward_percent = 10 * GRAPHENE_1_PERCENT; + + const auto& biteur = create_bitasset( "EURBIT", paul_id, market_fee_percent, charge_market_fee, 2 ); + asset_id_type biteur_id = biteur.id; + + const auto& bitusd = create_bitasset( "USDBIT", paul_id, market_fee_percent, charge_market_fee, 2, biteur_id ); + + const account_object rachel = create_account( "rachel", rachelregistrar, rachelreferrer, + referrer_reward_percent ); + + transfer( committee_account, rachelregistrar_id, asset( 10000000 ) ); + transfer( committee_account, rachelreferrer_id, asset( 10000000 ) ); + transfer( committee_account, rachel.get_id(), asset( 10000000) ); + transfer( committee_account, paul_id, asset( 10000000000 ) ); + + asset_update_operation op; + op.issuer = biteur.issuer; + op.asset_to_update = biteur_id; + op.new_options.issuer_permissions = charge_market_fee; + op.new_options.extensions.value.reward_percent = biteur_reward_percent; + op.new_options.flags = bitusd.options.flags | charge_market_fee; + op.new_options.core_exchange_rate = price( asset(20,biteur_id), asset(1,asset_id_type()) ); + op.new_options.market_fee_percent = market_fee_percent; + trx.operations.push_back(op); + sign(trx, paul_private_key); + PUSH_TX(db, trx); + generate_block(); + trx.clear(); + set_expiration( db, trx ); + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE( market_fee_of_settle_order_before_hardfork_1780 ) +{ + try { + INVOKE(create_bitassets); + + GET_ACTOR(paul); + GET_ACTOR(rachel); + GET_ACTOR(rachelregistrar); + GET_ACTOR(rachelreferrer); + + const asset_object &biteur = get_asset( "EURBIT" ); + asset_id_type biteur_id = biteur.id; + const asset_object &bitusd = get_asset( "USDBIT" ); + asset_id_type bitusd_id = bitusd.id; + + const auto& core = asset_id_type()(db); + + {// add a feed to asset bitusd + update_feed_producers( bitusd, {paul_id} ); + price_feed feed; + feed.settlement_price = price( bitusd.amount(100), biteur.amount(5) ); + feed.core_exchange_rate = price( bitusd.amount(100), asset(1) ); + feed.maintenance_collateral_ratio = 175 * GRAPHENE_COLLATERAL_RATIO_DENOM / 100; + feed.maximum_short_squeeze_ratio = 110 * GRAPHENE_COLLATERAL_RATIO_DENOM / 100; + publish_feed( bitusd_id, paul_id, feed ); + } + + {// add a feed to asset biteur + update_feed_producers( biteur, {paul_id} ); + price_feed feed; + feed.settlement_price = price( biteur.amount(100), core.amount(5) ); + feed.maintenance_collateral_ratio = 175 * GRAPHENE_COLLATERAL_RATIO_DENOM / 100; + feed.maximum_short_squeeze_ratio = 110 * GRAPHENE_COLLATERAL_RATIO_DENOM / 100; + publish_feed( biteur_id, paul_id, feed ); + } + + enable_fees(); + + // paul gets some bitusd and biteur + borrow( paul_id, biteur.amount(20000), core.amount(2000) ); + borrow( paul_id, bitusd.amount(10000), biteur.amount(1000) ); + + // and transfer some bitusd to rachel + constexpr auto rachel_bitusd_count = 1000; + transfer( paul_id, rachel_id, asset(rachel_bitusd_count, bitusd_id) ); + + force_settle( rachel, bitusd.amount(rachel_bitusd_count) ); + generate_block(); + generate_blocks( db.head_block_time() + fc::hours(24) ); + set_expiration( db, trx ); + + // Check results + int64_t biteur_balance = 0; + int64_t biteur_accumulated_fee = 0; + int64_t bitusd_accumulated_fee = 0; + { + // 1 biteur = 20 bitusd see publish_feed + const auto biteur_expected_result = rachel_bitusd_count/20; + const auto biteur_market_fee = biteur_expected_result / 2; // market fee percent = 50% + biteur_balance += biteur_expected_result - biteur_market_fee; + + BOOST_CHECK_EQUAL( get_balance(rachel, biteur), biteur_balance ); + BOOST_CHECK_EQUAL( get_balance(rachel, bitusd), 0 ); + + const auto rachelregistrar_reward = get_market_fee_reward( rachelregistrar, biteur ); + const auto rachelreferrer_reward = get_market_fee_reward( rachelreferrer, biteur ); + + BOOST_CHECK_EQUAL( rachelregistrar_reward, 0 ); + BOOST_CHECK_EQUAL( rachelreferrer_reward, 0 ); + + // market fee + biteur_accumulated_fee += biteur_market_fee; + bitusd_accumulated_fee += 0; // usd market fee percent 50%, but call orders don't pay + BOOST_CHECK_EQUAL( biteur.dynamic_data(db).accumulated_fees.value, biteur_accumulated_fee); + BOOST_CHECK_EQUAL( bitusd.dynamic_data(db).accumulated_fees.value, bitusd_accumulated_fee ); + } + + // Update the feed to asset bitusd to trigger a global settlement + { + price_feed feed; + feed.settlement_price = price( bitusd.amount(10), biteur.amount(5) ); + feed.core_exchange_rate = price( bitusd.amount(100), asset(1) ); + feed.maintenance_collateral_ratio = 175 * GRAPHENE_COLLATERAL_RATIO_DENOM / 100; + feed.maximum_short_squeeze_ratio = 110 * GRAPHENE_COLLATERAL_RATIO_DENOM / 100; + publish_feed( bitusd_id, paul_id, feed ); + } + + // Transfer more bitusd to rachel + transfer( paul_id, rachel_id, asset(rachel_bitusd_count, bitusd_id) ); + // Instant settlement + force_settle( rachel, bitusd.amount(rachel_bitusd_count) ); + + // Check results + { + // 950 biteur = 9000 bitusd in settlement fund + const auto biteur_expected_result = rachel_bitusd_count * 950 / 9000; + const auto biteur_market_fee = 0; // market fee percent = 50%, but doesn't pay before hf + biteur_balance += biteur_expected_result - biteur_market_fee; + + BOOST_CHECK_EQUAL( get_balance(rachel, biteur), biteur_balance ); + BOOST_CHECK_EQUAL( get_balance(rachel, bitusd), 0 ); + + const auto rachelregistrar_reward = get_market_fee_reward( rachelregistrar, biteur ); + const auto rachelreferrer_reward = get_market_fee_reward( rachelreferrer, biteur ); + + BOOST_CHECK_EQUAL( rachelregistrar_reward, 0 ); + BOOST_CHECK_EQUAL( rachelreferrer_reward, 0 ); + + // No market fee for instant settlement before hf + biteur_accumulated_fee += 0; + bitusd_accumulated_fee += 0; + BOOST_CHECK_EQUAL( biteur.dynamic_data(db).accumulated_fees.value, biteur_accumulated_fee); + BOOST_CHECK_EQUAL( bitusd.dynamic_data(db).accumulated_fees.value, bitusd_accumulated_fee ); + } + + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE( market_fee_of_settle_order_after_hardfork_1780 ) +{ + try { + INVOKE(create_bitassets); + + generate_blocks( HARDFORK_CORE_1780_TIME ); + set_expiration( db, trx ); + + GET_ACTOR(paul); + GET_ACTOR(rachel); + GET_ACTOR(rachelregistrar); + GET_ACTOR(rachelreferrer); + + const asset_object &biteur = get_asset( "EURBIT" ); + asset_id_type biteur_id = biteur.id; + const asset_object &bitusd = get_asset( "USDBIT" ); + asset_id_type bitusd_id = bitusd.id; + + const auto& core = asset_id_type()(db); + + {// add a feed to asset bitusd + update_feed_producers( bitusd, {paul_id} ); + price_feed feed; + feed.settlement_price = price( bitusd.amount(100), biteur.amount(5) ); + feed.core_exchange_rate = price( bitusd.amount(100), asset(1) ); + feed.maintenance_collateral_ratio = 175 * GRAPHENE_COLLATERAL_RATIO_DENOM / 100; + feed.maximum_short_squeeze_ratio = 110 * GRAPHENE_COLLATERAL_RATIO_DENOM / 100; + publish_feed( bitusd_id, paul_id, feed ); + } + + {// add a feed to asset biteur + update_feed_producers( biteur, {paul_id} ); + price_feed feed; + feed.settlement_price = price( biteur.amount(100), core.amount(5) ); + feed.maintenance_collateral_ratio = 175 * GRAPHENE_COLLATERAL_RATIO_DENOM / 100; + feed.maximum_short_squeeze_ratio = 110 * GRAPHENE_COLLATERAL_RATIO_DENOM / 100; + publish_feed( biteur_id, paul_id, feed ); + } + + enable_fees(); + + // paul gets some bitusd and biteur + borrow( paul_id, biteur.amount(20000), core.amount(2000) ); + borrow( paul_id, bitusd.amount(10000), biteur.amount(1000) ); + + // and transfer some bitusd to rachel + constexpr auto rachel_bitusd_count = 1000; + transfer( paul_id, rachel_id, asset(rachel_bitusd_count, bitusd_id) ); + + force_settle( rachel, bitusd.amount(rachel_bitusd_count) ); + generate_block(); + generate_blocks( db.head_block_time() + fc::hours(24) ); + set_expiration( db, trx ); + + // Check results + int64_t biteur_balance = 0; + int64_t biteur_accumulated_fee = 0; + int64_t bitusd_accumulated_fee = 0; + { + // 1 biteur = 20 bitusd see publish_feed + const auto biteur_expected_result = rachel_bitusd_count/20; + const auto biteur_market_fee = biteur_expected_result / 2; // market fee percent = 50% + biteur_balance += biteur_expected_result - biteur_market_fee; + + BOOST_CHECK_EQUAL( get_balance(rachel, biteur), biteur_balance ); + BOOST_CHECK_EQUAL( get_balance(rachel, bitusd), 0 ); + + const auto rachelregistrar_reward = get_market_fee_reward( rachelregistrar, biteur ); + const auto rachelreferrer_reward = get_market_fee_reward( rachelreferrer, biteur ); + + const auto biteur_reward = biteur_market_fee * 9 / 10; // 90% + const auto referrer_reward = biteur_reward / 10; // 10% + const auto registrar_reward = biteur_reward - referrer_reward; + + BOOST_CHECK_EQUAL( rachelregistrar_reward, registrar_reward ); + BOOST_CHECK_EQUAL( rachelreferrer_reward, referrer_reward ); + + // market fee + biteur_accumulated_fee += biteur_market_fee - biteur_reward; + bitusd_accumulated_fee += 0; // usd market fee percent 50%, but call orders don't pay + BOOST_CHECK_EQUAL( biteur.dynamic_data(db).accumulated_fees.value, biteur_accumulated_fee); + BOOST_CHECK_EQUAL( bitusd.dynamic_data(db).accumulated_fees.value, bitusd_accumulated_fee ); + + } + + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE( market_fee_of_instant_settle_order_after_hardfork_1780 ) +{ + try { + INVOKE(create_bitassets); + + generate_blocks( HARDFORK_CORE_1780_TIME ); + set_expiration( db, trx ); + + GET_ACTOR(paul); + GET_ACTOR(rachel); + GET_ACTOR(rachelregistrar); + GET_ACTOR(rachelreferrer); + + const asset_object &biteur = get_asset( "EURBIT" ); + asset_id_type biteur_id = biteur.id; + const asset_object &bitusd = get_asset( "USDBIT" ); + asset_id_type bitusd_id = bitusd.id; + + const auto& core = asset_id_type()(db); + + {// add a feed to asset bitusd + update_feed_producers( bitusd, {paul_id} ); + price_feed feed; + feed.settlement_price = price( bitusd.amount(100), biteur.amount(5) ); + feed.core_exchange_rate = price( bitusd.amount(100), asset(1) ); + feed.maintenance_collateral_ratio = 175 * GRAPHENE_COLLATERAL_RATIO_DENOM / 100; + feed.maximum_short_squeeze_ratio = 110 * GRAPHENE_COLLATERAL_RATIO_DENOM / 100; + publish_feed( bitusd_id, paul_id, feed ); + } + + {// add a feed to asset biteur + update_feed_producers( biteur, {paul_id} ); + price_feed feed; + feed.settlement_price = price( biteur.amount(100), core.amount(5) ); + feed.maintenance_collateral_ratio = 175 * GRAPHENE_COLLATERAL_RATIO_DENOM / 100; + feed.maximum_short_squeeze_ratio = 110 * GRAPHENE_COLLATERAL_RATIO_DENOM / 100; + publish_feed( biteur_id, paul_id, feed ); + } + + enable_fees(); + + // paul gets some bitusd and biteur + borrow( paul_id, biteur.amount(20000), core.amount(2000) ); + borrow( paul_id, bitusd.amount(10000), biteur.amount(1000) ); + + // Update the feed to asset bitusd to trigger a global settlement + { + price_feed feed; + feed.settlement_price = price( bitusd.amount(10), biteur.amount(5) ); + feed.core_exchange_rate = price( bitusd.amount(100), asset(1) ); + feed.maintenance_collateral_ratio = 175 * GRAPHENE_COLLATERAL_RATIO_DENOM / 100; + feed.maximum_short_squeeze_ratio = 110 * GRAPHENE_COLLATERAL_RATIO_DENOM / 100; + publish_feed( bitusd_id, paul_id, feed ); + } + + // Transfer some bitusd to rachel + constexpr auto rachel_bitusd_count = 1000; + transfer( paul_id, rachel_id, asset(rachel_bitusd_count, bitusd_id) ); + // Instant settlement + force_settle( rachel, bitusd.amount(rachel_bitusd_count) ); // instant settlement + + // Check results + int64_t biteur_balance = 0; + int64_t biteur_accumulated_fee = 0; + int64_t bitusd_accumulated_fee = 0; + { + // 1000 biteur = 10000 bitusd in settlement fund + const auto biteur_expected_result = rachel_bitusd_count/10; + const auto biteur_market_fee = biteur_expected_result / 2; // market fee percent = 50% + biteur_balance += biteur_expected_result - biteur_market_fee; + + BOOST_CHECK_EQUAL( get_balance(rachel, biteur), biteur_balance ); + BOOST_CHECK_EQUAL( get_balance(rachel, bitusd), 0 ); + + const auto rachelregistrar_reward = get_market_fee_reward( rachelregistrar, biteur ); + const auto rachelreferrer_reward = get_market_fee_reward( rachelreferrer, biteur ); + + const auto biteur_reward = biteur_market_fee * 9 / 10; // 90% + const auto referrer_reward = biteur_reward / 10; // 10% + const auto registrar_reward = biteur_reward - referrer_reward; + + BOOST_CHECK_EQUAL( rachelregistrar_reward, registrar_reward ); + BOOST_CHECK_EQUAL( rachelreferrer_reward, referrer_reward ); + + // market fee + biteur_accumulated_fee += biteur_market_fee - biteur_reward; + bitusd_accumulated_fee += 0; // usd market fee percent 50%, but call orders don't pay + BOOST_CHECK_EQUAL( biteur.dynamic_data(db).accumulated_fees.value, biteur_accumulated_fee); + BOOST_CHECK_EQUAL( bitusd.dynamic_data(db).accumulated_fees.value, bitusd_accumulated_fee ); + + } + + } FC_LOG_AND_RETHROW() +} + /** * Test case to reproduce https://github.com/bitshares/bitshares-core/issues/1883. * When there is only one fill_order object in the ticker rolling buffer, it should only be rolled out once. diff --git a/tests/tests/simple_maker_taker_fee_tests.cpp b/tests/tests/simple_maker_taker_fee_tests.cpp new file mode 100644 index 0000000000..97a609b634 --- /dev/null +++ b/tests/tests/simple_maker_taker_fee_tests.cpp @@ -0,0 +1,1952 @@ +/* + * Copyright (c) 2020 Michel Santos, 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 +#include + +#include "../common/database_fixture.hpp" + + +using namespace graphene::chain; +using namespace graphene::chain::test; + +struct simple_maker_taker_database_fixture : database_fixture { + simple_maker_taker_database_fixture() + : database_fixture() { + } + + const limit_order_create_operation + create_sell_operation(account_id_type user, const asset &amount, const asset &recv) { + const time_point_sec order_expiration = time_point_sec::maximum(); + const price &fee_core_exchange_rate = price::unit_price(); + limit_order_create_operation op = create_sell_operation(user, amount, recv, order_expiration, + fee_core_exchange_rate); + return op; + } + + const limit_order_create_operation + create_sell_operation(account_id_type user, const asset &amount, const asset &recv, + const time_point_sec order_expiration, + const price &fee_core_exchange_rate) { + limit_order_create_operation op = create_sell_operation(user(db), amount, recv, order_expiration, + fee_core_exchange_rate); + return op; + } + + const limit_order_create_operation + create_sell_operation(const account_object &user, const asset &amount, const asset &recv, + const time_point_sec order_expiration, + const price &fee_core_exchange_rate) { + limit_order_create_operation sell_order; + sell_order.seller = user.id; + sell_order.amount_to_sell = amount; + sell_order.min_to_receive = recv; + sell_order.expiration = order_expiration; + + return sell_order; + } + + const asset_create_operation create_user_issued_asset_operation(const string &name, const account_object &issuer, + uint16_t flags, const price &core_exchange_rate, + uint8_t precision, uint16_t maker_fee_percent, + uint16_t taker_fee_percent) { + asset_create_operation creator; + creator.issuer = issuer.id; + creator.fee = asset(); + creator.symbol = name; + creator.common_options.max_supply = 0; + creator.precision = precision; + + creator.common_options.core_exchange_rate = core_exchange_rate; + creator.common_options.max_supply = GRAPHENE_MAX_SHARE_SUPPLY; + creator.common_options.flags = flags; + creator.common_options.issuer_permissions = flags; + creator.common_options.market_fee_percent = maker_fee_percent; + creator.common_options.extensions.value.taker_fee_percent = taker_fee_percent; + + return creator; + + } +}; + + +/** + * BSIP81: Asset owners may specify different market fee rate for maker orders and taker orders + */ +BOOST_FIXTURE_TEST_SUITE(simple_maker_taker_fee_tests, simple_maker_taker_database_fixture) + + /** + * Test of setting taker fee before HF and after HF for a UIA + */ + BOOST_AUTO_TEST_CASE(setting_taker_fees_uia) { + try { + // Initialize for the current time + trx.clear(); + set_expiration(db, trx); + + // Initialize actors + ACTORS((jill)(izzy)); + account_id_type issuer_id = jill.id; + fc::ecc::private_key issuer_private_key = jill_private_key; + + // Initialize tokens + price price(asset(1, asset_id_type(1)), asset(1)); + uint16_t market_fee_percent = 20 * GRAPHENE_1_PERCENT; + const asset_object jillcoin = create_user_issued_asset("JCOIN", jill, charge_market_fee, price, 2, + market_fee_percent); + + ////// + // Before HF, test inability to set taker fees + ////// + asset_update_operation uop; + uop.issuer = issuer_id; + uop.asset_to_update = jillcoin.get_id(); + uop.new_options = jillcoin.options; + uint16_t new_taker_fee_percent = uop.new_options.market_fee_percent / 2; + uop.new_options.extensions.value.taker_fee_percent = new_taker_fee_percent; + + trx.operations.push_back(uop); + db.current_fee_schedule().set_fee(trx.operations.back()); + sign(trx, issuer_private_key); + GRAPHENE_CHECK_THROW(PUSH_TX(db, trx), fc::exception); + // TODO: Check the specific exception? + + // Check the taker fee + asset_object updated_asset = jillcoin.get_id()(db); + BOOST_CHECK(!updated_asset.options.extensions.value.taker_fee_percent.valid()); + + + ////// + // Before HF, test inability to set taker fees with an asset update operation inside of a proposal + ////// + { + trx.clear(); + set_expiration(db, trx); + + uint64_t alternate_taker_fee_percent = new_taker_fee_percent * 2; + uop.new_options.extensions.value.taker_fee_percent = alternate_taker_fee_percent; + + proposal_create_operation cop; + cop.review_period_seconds = 86400; + uint32_t buffer_seconds = 60 * 60; + cop.expiration_time = db.head_block_time() + *cop.review_period_seconds + buffer_seconds; + cop.fee_paying_account = GRAPHENE_TEMP_ACCOUNT; + cop.proposed_ops.emplace_back(uop); + + trx.operations.push_back(cop); + // sign(trx, issuer_private_key); + GRAPHENE_CHECK_THROW(PUSH_TX(db, trx), fc::exception); + + // Check the taker fee is not changed because the proposal has not been approved + updated_asset = jillcoin.get_id()(db); + BOOST_CHECK(!updated_asset.options.extensions.value.taker_fee_percent.valid()); + } + + + ////// + // Before HF, test inability to set taker fees with an asset create operation inside of a proposal + ////// + { + trx.clear(); + set_expiration(db, trx); + + uint64_t maker_fee_percent = 10 * GRAPHENE_1_PERCENT; + uint64_t taker_fee_percent = 2 * GRAPHENE_1_PERCENT; + asset_create_operation ac_op = create_user_issued_asset_operation("JCOIN2", jill, charge_market_fee, price, + 2, + maker_fee_percent, taker_fee_percent); + + proposal_create_operation cop; + cop.review_period_seconds = 86400; + uint32_t buffer_seconds = 60 * 60; + cop.expiration_time = db.head_block_time() + *cop.review_period_seconds + buffer_seconds; + cop.fee_paying_account = GRAPHENE_TEMP_ACCOUNT; + cop.proposed_ops.emplace_back(ac_op); + + trx.operations.push_back(cop); + // sign(trx, issuer_private_key); + + GRAPHENE_CHECK_THROW(PUSH_TX(db, trx), fc::exception); // The proposal should be rejected + + } + + + ////// + // Advance to activate hardfork + ////// + generate_blocks(HARDFORK_BSIP_81_TIME); + generate_block(); + trx.clear(); + set_expiration(db, trx); + + + ////// + // After HF, test default values of taker fee after HF + // After the HF its default value should still not be set + ////// + updated_asset = jillcoin.get_id()(db); + uint16_t expected_taker_fee_percent = updated_asset.options.market_fee_percent; + BOOST_CHECK(!updated_asset.options.extensions.value.taker_fee_percent.valid()); + + + ////// + // After HF, test invalid taker fees + ////// + uop.new_options.extensions.value.taker_fee_percent = GRAPHENE_100_PERCENT + 1; + trx.clear(); + trx.operations.push_back(uop); + db.current_fee_schedule().set_fee(trx.operations.back()); + sign(trx, issuer_private_key); + GRAPHENE_CHECK_THROW(PUSH_TX(db, trx), fc::exception); // An exception should be thrown indicating the reason + // TODO: Check the specific exception? + + + ////// + // After HF, test that new values can be set + ////// + uop.new_options.extensions.value.taker_fee_percent = new_taker_fee_percent; + trx.clear(); + trx.operations.push_back(uop); + db.current_fee_schedule().set_fee(trx.operations.back()); + sign(trx, issuer_private_key); + PUSH_TX(db, trx); // No exception should be thrown + + // Check the taker fee + updated_asset = jillcoin.get_id()(db); + expected_taker_fee_percent = new_taker_fee_percent; + BOOST_CHECK(updated_asset.options.extensions.value.taker_fee_percent.valid()); + BOOST_CHECK_EQUAL(expected_taker_fee_percent, *updated_asset.options.extensions.value.taker_fee_percent); + + + ////// + // After HF, test ability to set taker fees with an asset update operation inside of a proposal + ////// + { + trx.clear(); + set_expiration(db, trx); + + uint64_t alternate_taker_fee_percent = new_taker_fee_percent * 2; + uop.new_options.extensions.value.taker_fee_percent = alternate_taker_fee_percent; + + proposal_create_operation cop; + cop.review_period_seconds = 86400; + uint32_t buffer_seconds = 60 * 60; + cop.expiration_time = db.head_block_time() + *cop.review_period_seconds + buffer_seconds; + cop.fee_paying_account = GRAPHENE_TEMP_ACCOUNT; + cop.proposed_ops.emplace_back(uop); + + trx.operations.push_back(cop); + // sign(trx, issuer_private_key); + processed_transaction processed = PUSH_TX(db, trx); // No exception should be thrown + + // Check the taker fee is not changed because the proposal has not been approved + updated_asset = jillcoin.get_id()(db); + expected_taker_fee_percent = new_taker_fee_percent; + BOOST_CHECK(updated_asset.options.extensions.value.taker_fee_percent.valid()); + BOOST_CHECK_EQUAL(expected_taker_fee_percent, *updated_asset.options.extensions.value.taker_fee_percent); + + + // Approve the proposal + trx.clear(); + proposal_id_type pid = processed.operation_results[0].get(); + + proposal_update_operation pup; + pup.fee_paying_account = jill.id; + pup.proposal = pid; + pup.active_approvals_to_add.insert(jill.id); + trx.operations.push_back(pup); + set_expiration(db, trx); + sign(trx, jill_private_key); + + PUSH_TX(db, trx); // No exception should be thrown + + // Advance to after proposal expires + generate_blocks(cop.expiration_time); + + // Check the taker fee is not changed because the proposal has not been approved + updated_asset = jillcoin.get_id()(db); + expected_taker_fee_percent = alternate_taker_fee_percent; + BOOST_CHECK(updated_asset.options.extensions.value.taker_fee_percent.valid()); + BOOST_CHECK_EQUAL(expected_taker_fee_percent, *updated_asset.options.extensions.value.taker_fee_percent); + + } + + + ////// + // After HF, test ability to set taker fees with an asset create operation inside of a proposal + ////// + { + trx.clear(); + set_expiration(db, trx); + + uint64_t maker_fee_percent = 10 * GRAPHENE_1_PERCENT; + uint64_t taker_fee_percent = 2 * GRAPHENE_1_PERCENT; + asset_create_operation ac_op = create_user_issued_asset_operation("JCOIN2", jill, charge_market_fee, price, + 2, + maker_fee_percent, taker_fee_percent); + + proposal_create_operation cop; + cop.review_period_seconds = 86400; + uint32_t buffer_seconds = 60 * 60; + cop.expiration_time = db.head_block_time() + *cop.review_period_seconds + buffer_seconds; + cop.fee_paying_account = GRAPHENE_TEMP_ACCOUNT; + cop.proposed_ops.emplace_back(ac_op); + + trx.operations.push_back(cop); + // sign(trx, issuer_private_key); + + processed_transaction processed = PUSH_TX(db, trx); // No exception should be thrown + + // Check the asset does not exist because the proposal has not been approved + const auto& asset_idx = db.get_index_type().indices().get(); + const auto itr = asset_idx.find("JCOIN2"); + BOOST_CHECK(itr == asset_idx.end()); + + // Approve the proposal + trx.clear(); + proposal_id_type pid = processed.operation_results[0].get(); + + proposal_update_operation pup; + pup.fee_paying_account = jill.id; + pup.proposal = pid; + pup.active_approvals_to_add.insert(jill.id); + trx.operations.push_back(pup); + set_expiration(db, trx); + sign(trx, jill_private_key); + + PUSH_TX(db, trx); // No exception should be thrown + + // Advance to after proposal expires + generate_blocks(cop.expiration_time); + + // Check the taker fee is not changed because the proposal has not been approved + BOOST_CHECK(asset_idx.find("JCOIN2") != asset_idx.end()); + updated_asset = *asset_idx.find("JCOIN2"); + expected_taker_fee_percent = taker_fee_percent; + BOOST_CHECK(updated_asset.options.extensions.value.taker_fee_percent.valid()); + BOOST_CHECK_EQUAL(expected_taker_fee_percent, *updated_asset.options.extensions.value.taker_fee_percent); + uint16_t expected_maker_fee_percent = maker_fee_percent; + BOOST_CHECK_EQUAL(expected_maker_fee_percent, updated_asset.options.market_fee_percent); + + } + + } FC_LOG_AND_RETHROW() + } + + + /** + * Test of setting taker fee before HF and after HF for a smart asset + */ + BOOST_AUTO_TEST_CASE(setting_taker_fees_smart_asset) { + try { + // Initialize for the current time + trx.clear(); + set_expiration(db, trx); + + // Initialize actors + ACTORS((smartissuer)(feedproducer)); + + // Initialize tokens + create_bitasset("SMARTBIT", smartissuer.id); + // Obtain asset object after a block is generated to obtain the final object that is commited to the database + generate_block(); + const asset_object &bitsmart = get_asset("SMARTBIT"); + + generate_blocks(HARDFORK_615_TIME); // get around Graphene issue #615 feed expiration bug + generate_block(); + + + ////// + // Before HF, test inability to set taker fees + ////// + asset_update_operation uop; + uop.issuer = smartissuer.id; + uop.asset_to_update = bitsmart.get_id(); + uop.new_options = bitsmart.options; + uint16_t new_taker_fee_percent = uop.new_options.market_fee_percent / 2; + uop.new_options.extensions.value.taker_fee_percent = new_taker_fee_percent; + + trx.operations.push_back(uop); + db.current_fee_schedule().set_fee(trx.operations.back()); + sign(trx, smartissuer_private_key); + GRAPHENE_CHECK_THROW(PUSH_TX(db, trx), fc::exception); // An exception should be thrown indicating the reason + // TODO: Check the specific exception? + + // Check the taker fee + asset_object updated_asset = bitsmart.get_id()(db); + BOOST_CHECK(!updated_asset.options.extensions.value.taker_fee_percent.valid()); + + + ////// + // Advance to activate hardfork + ////// + generate_blocks(HARDFORK_BSIP_81_TIME); + generate_block(); + trx.clear(); + set_expiration(db, trx); + + + ////// + // After HF, test default values of taker fee after HF + // After the HF its default value should still not be set + ////// + updated_asset = bitsmart.get_id()(db); + uint16_t expected_taker_fee_percent = updated_asset.options.market_fee_percent; + BOOST_CHECK(!updated_asset.options.extensions.value.taker_fee_percent.valid()); + + + ////// + // After HF, test invalid taker fees + ////// + uop.new_options.extensions.value.taker_fee_percent = GRAPHENE_100_PERCENT + 1; + trx.clear(); + trx.operations.push_back(uop); + db.current_fee_schedule().set_fee(trx.operations.back()); + sign(trx, smartissuer_private_key); + GRAPHENE_CHECK_THROW(PUSH_TX(db, trx), fc::exception); // An exception should be thrown indicating the reason + // TODO: Check the specific exception? + + + ////// + // After HF, test that new values can be set + ////// + uop.new_options.extensions.value.taker_fee_percent = new_taker_fee_percent; + trx.clear(); + trx.operations.push_back(uop); + db.current_fee_schedule().set_fee(trx.operations.back()); + sign(trx, smartissuer_private_key); + PUSH_TX(db, trx); // No exception should be thrown + + // Check the taker fee + updated_asset = bitsmart.get_id()(db); + expected_taker_fee_percent = new_taker_fee_percent; + BOOST_CHECK(updated_asset.options.extensions.value.taker_fee_percent.valid()); + BOOST_CHECK_EQUAL(expected_taker_fee_percent, *updated_asset.options.extensions.value.taker_fee_percent); + + } FC_LOG_AND_RETHROW() + } + + + /** + * Test the default taker fee values of multiple different assets after HF + */ + BOOST_AUTO_TEST_CASE(default_taker_fees) { + try { + // Initialize for the current time + trx.clear(); + set_expiration(db, trx); + + // Initialize actors + ACTORS((alice)(bob)(charlie)(smartissuer)); + + // Initialize tokens with custom market fees + price price(asset(1, asset_id_type(1)), asset(1)); + + const uint16_t alice1coin_market_fee_percent = 1 * GRAPHENE_1_PERCENT; + create_user_issued_asset("ALICE1COIN", alice, charge_market_fee, price, 2, + alice1coin_market_fee_percent); + + const uint16_t alice2coin_market_fee_percent = 2 * GRAPHENE_1_PERCENT; + create_user_issued_asset("ALICE2COIN", alice, charge_market_fee, price, 2, + alice2coin_market_fee_percent); + + const uint16_t bob1coin_market_fee_percent = 3 * GRAPHENE_1_PERCENT; + create_user_issued_asset("BOB1COIN", alice, charge_market_fee, price, 2, + bob1coin_market_fee_percent); + + const uint16_t bob2coin_market_fee_percent = 4 * GRAPHENE_1_PERCENT; + create_user_issued_asset("BOB2COIN", alice, charge_market_fee, price, 2, + bob2coin_market_fee_percent); + + const uint16_t charlie1coin_market_fee_percent = 4 * GRAPHENE_1_PERCENT; + create_user_issued_asset("CHARLIE1COIN", alice, charge_market_fee, price, 2, + charlie1coin_market_fee_percent); + + const uint16_t charlie2coin_market_fee_percent = 5 * GRAPHENE_1_PERCENT; + create_user_issued_asset("CHARLIE2COIN", alice, charge_market_fee, price, 2, + charlie2coin_market_fee_percent); + + const uint16_t bitsmart1coin_market_fee_percent = 7 * GRAPHENE_1_PERCENT; + create_bitasset("SMARTBIT1", smartissuer.id, bitsmart1coin_market_fee_percent); + + + const uint16_t bitsmart2coin_market_fee_percent = 8 * GRAPHENE_1_PERCENT; + create_bitasset("SMARTBIT2", smartissuer.id, bitsmart2coin_market_fee_percent); + + // Obtain asset object after a block is generated to obtain the final object that is commited to the database + generate_block(); + const asset_object& alice1coin = get_asset("ALICE1COIN"); + const asset_object& alice2coin = get_asset("ALICE2COIN"); + const asset_object& bob1coin = get_asset("BOB1COIN"); + const asset_object& bob2coin = get_asset("BOB2COIN"); + const asset_object& charlie1coin = get_asset("CHARLIE1COIN"); + const asset_object& charlie2coin = get_asset("CHARLIE2COIN"); + const asset_object& bitsmart1 = get_asset("SMARTBIT1"); + const asset_object& bitsmart2 = get_asset("SMARTBIT2"); + + + ////// + // Before HF, test the market/maker fees for each asset + ////// + asset_object updated_asset; + uint16_t expected_fee_percent; + + updated_asset = alice1coin.get_id()(db); + expected_fee_percent = alice1coin_market_fee_percent; + BOOST_CHECK_EQUAL(expected_fee_percent, updated_asset.options.market_fee_percent); + + updated_asset = alice2coin.get_id()(db); + expected_fee_percent = alice2coin_market_fee_percent; + BOOST_CHECK_EQUAL(expected_fee_percent, updated_asset.options.market_fee_percent); + + updated_asset = bob1coin.get_id()(db); + expected_fee_percent = bob1coin_market_fee_percent; + BOOST_CHECK_EQUAL(expected_fee_percent, updated_asset.options.market_fee_percent); + + updated_asset = bob2coin.get_id()(db); + expected_fee_percent = bob2coin_market_fee_percent; + BOOST_CHECK_EQUAL(expected_fee_percent, updated_asset.options.market_fee_percent); + + updated_asset = charlie1coin.get_id()(db); + expected_fee_percent = charlie1coin_market_fee_percent; + BOOST_CHECK_EQUAL(expected_fee_percent, updated_asset.options.market_fee_percent); + + updated_asset = charlie2coin.get_id()(db); + expected_fee_percent = charlie2coin_market_fee_percent; + BOOST_CHECK_EQUAL(expected_fee_percent, updated_asset.options.market_fee_percent); + + updated_asset = bitsmart1.get_id()(db); + expected_fee_percent = bitsmart1coin_market_fee_percent; + BOOST_CHECK_EQUAL(expected_fee_percent, updated_asset.options.market_fee_percent); + + updated_asset = bitsmart2.get_id()(db); + expected_fee_percent = bitsmart2coin_market_fee_percent; + BOOST_CHECK_EQUAL(expected_fee_percent, updated_asset.options.market_fee_percent); + + + ////// + // Before HF, test that taker fees are not set + ////// + // Check the taker fee + updated_asset = alice1coin.get_id()(db); + BOOST_CHECK(!updated_asset.options.extensions.value.taker_fee_percent.valid()); + + updated_asset = alice2coin.get_id()(db); + BOOST_CHECK(!updated_asset.options.extensions.value.taker_fee_percent.valid()); + + updated_asset = bob1coin.get_id()(db); + BOOST_CHECK(!updated_asset.options.extensions.value.taker_fee_percent.valid()); + + updated_asset = bob2coin.get_id()(db); + BOOST_CHECK(!updated_asset.options.extensions.value.taker_fee_percent.valid()); + + updated_asset = charlie1coin.get_id()(db); + BOOST_CHECK(!updated_asset.options.extensions.value.taker_fee_percent.valid()); + + updated_asset = charlie2coin.get_id()(db); + BOOST_CHECK(!updated_asset.options.extensions.value.taker_fee_percent.valid()); + + updated_asset = bitsmart1.get_id()(db); + BOOST_CHECK(!updated_asset.options.extensions.value.taker_fee_percent.valid()); + + updated_asset = bitsmart2.get_id()(db); + BOOST_CHECK(!updated_asset.options.extensions.value.taker_fee_percent.valid()); + + + ////// + // Advance to activate hardfork + ////// + generate_blocks(HARDFORK_BSIP_81_TIME); + generate_block(); + trx.clear(); + set_expiration(db, trx); + + + ////// + // After HF, test the maker fees for each asset are unchanged + ////// + updated_asset = alice1coin.get_id()(db); + expected_fee_percent = alice1coin_market_fee_percent; + BOOST_CHECK_EQUAL(expected_fee_percent, updated_asset.options.market_fee_percent); + + updated_asset = alice2coin.get_id()(db); + expected_fee_percent = alice2coin_market_fee_percent; + BOOST_CHECK_EQUAL(expected_fee_percent, updated_asset.options.market_fee_percent); + + updated_asset = bob1coin.get_id()(db); + expected_fee_percent = bob1coin_market_fee_percent; + BOOST_CHECK_EQUAL(expected_fee_percent, updated_asset.options.market_fee_percent); + + updated_asset = bob2coin.get_id()(db); + expected_fee_percent = bob2coin_market_fee_percent; + BOOST_CHECK_EQUAL(expected_fee_percent, updated_asset.options.market_fee_percent); + + updated_asset = charlie1coin.get_id()(db); + expected_fee_percent = charlie1coin_market_fee_percent; + BOOST_CHECK_EQUAL(expected_fee_percent, updated_asset.options.market_fee_percent); + + updated_asset = charlie2coin.get_id()(db); + expected_fee_percent = charlie2coin_market_fee_percent; + BOOST_CHECK_EQUAL(expected_fee_percent, updated_asset.options.market_fee_percent); + + updated_asset = bitsmart1.get_id()(db); + expected_fee_percent = bitsmart1coin_market_fee_percent; + BOOST_CHECK_EQUAL(expected_fee_percent, updated_asset.options.market_fee_percent); + + updated_asset = bitsmart2.get_id()(db); + expected_fee_percent = bitsmart2coin_market_fee_percent; + BOOST_CHECK_EQUAL(expected_fee_percent, updated_asset.options.market_fee_percent); + + + ////// + // After HF, test the taker fees for each asset are not set + ////// + updated_asset = alice1coin.get_id()(db); + expected_fee_percent = alice1coin_market_fee_percent; + BOOST_CHECK(!updated_asset.options.extensions.value.taker_fee_percent.valid()); + + updated_asset = alice2coin.get_id()(db); + expected_fee_percent = alice2coin_market_fee_percent; + BOOST_CHECK(!updated_asset.options.extensions.value.taker_fee_percent.valid()); + + updated_asset = bob1coin.get_id()(db); + expected_fee_percent = bob1coin_market_fee_percent; + BOOST_CHECK(!updated_asset.options.extensions.value.taker_fee_percent.valid()); + + updated_asset = bob2coin.get_id()(db); + expected_fee_percent = bob2coin_market_fee_percent; + BOOST_CHECK(!updated_asset.options.extensions.value.taker_fee_percent.valid()); + + updated_asset = charlie1coin.get_id()(db); + expected_fee_percent = charlie1coin_market_fee_percent; + BOOST_CHECK(!updated_asset.options.extensions.value.taker_fee_percent.valid()); + + updated_asset = charlie2coin.get_id()(db); + expected_fee_percent = charlie2coin_market_fee_percent; + BOOST_CHECK(!updated_asset.options.extensions.value.taker_fee_percent.valid()); + + updated_asset = bitsmart1.get_id()(db); + expected_fee_percent = bitsmart1coin_market_fee_percent; + BOOST_CHECK(!updated_asset.options.extensions.value.taker_fee_percent.valid()); + + updated_asset = bitsmart2.get_id()(db); + expected_fee_percent = bitsmart2coin_market_fee_percent; + BOOST_CHECK(!updated_asset.options.extensions.value.taker_fee_percent.valid()); + + } FC_LOG_AND_RETHROW() + } + + + /** + * Test of different maker and taker fees charged when filling limit orders after HF for a UIA + */ + BOOST_AUTO_TEST_CASE(simple_match_and_fill_with_different_fees_uia_1) { + try { + // Initialize for the current time + trx.clear(); + set_expiration(db, trx); + + // Initialize actors + ACTORS((jill)(izzy)(alice)(bob)); + + // Initialize tokens + price price(asset(1, asset_id_type(1)), asset(1)); + + const uint16_t JILL_PRECISION = 100; + const uint16_t jill_market_fee_percent = 2 * GRAPHENE_1_PERCENT; + create_user_issued_asset("JCOIN", jill, charge_market_fee, price, 2, + jill_market_fee_percent); + + const uint16_t IZZY_PRECISION = 1000; + const uint16_t izzy_market_fee_percent = 5 * GRAPHENE_1_PERCENT; + create_user_issued_asset("ICOIN", izzy, charge_market_fee, price, 3, + izzy_market_fee_percent); + + // Obtain asset object after a block is generated to obtain the final object that is commited to the database + generate_block(); + const asset_object& jillcoin = get_asset("JCOIN"); + const asset_object& izzycoin = get_asset("ICOIN"); + + + ////// + // Advance to activate hardfork + ////// + generate_blocks(HARDFORK_BSIP_81_TIME); + generate_block(); + trx.clear(); + set_expiration(db, trx); + + + ////// + // After HF, test that new values can be set + ////// + // Define the new taker fees + uint16_t jill_maker_fee_percent = jill_market_fee_percent; + uint16_t jill_taker_fee_percent = jill_maker_fee_percent / 2; + + uint16_t izzy_maker_fee_percent = izzy_market_fee_percent; + uint16_t izzy_taker_fee_percent = izzy_maker_fee_percent / 2; + + // Set the new taker fee for JILLCOIN + asset_update_operation uop; + uop.issuer = jill.id; + uop.asset_to_update = jillcoin.get_id(); + uop.new_options = jillcoin.options; + uop.new_options.extensions.value.taker_fee_percent = jill_taker_fee_percent; + + trx.clear(); + trx.operations.push_back(uop); + db.current_fee_schedule().set_fee(trx.operations.back()); + sign(trx, jill_private_key); + PUSH_TX(db, trx); // No exception should be thrown + + // Check the taker fee for JILLCOIN + asset_object updated_asset = jillcoin.get_id()(db); + uint16_t expected_taker_fee_percent = jill_taker_fee_percent; + BOOST_CHECK(updated_asset.options.extensions.value.taker_fee_percent.valid()); + BOOST_CHECK_EQUAL(expected_taker_fee_percent, *updated_asset.options.extensions.value.taker_fee_percent); + + // Set the new taker fee for IZZYCOIN + uop.issuer = izzy.id; + uop.asset_to_update = izzycoin.get_id(); + uop.new_options = izzycoin.options; + uop.new_options.extensions.value.taker_fee_percent = izzy_taker_fee_percent; + + trx.clear(); + trx.operations.push_back(uop); + db.current_fee_schedule().set_fee(trx.operations.back()); + sign(trx, izzy_private_key); + PUSH_TX(db, trx); // No exception should be thrown + + // Check the taker fee for IZZYCOIN + updated_asset = izzycoin.get_id()(db); + expected_taker_fee_percent = izzy_taker_fee_percent; + BOOST_CHECK(updated_asset.options.extensions.value.taker_fee_percent.valid()); + BOOST_CHECK_EQUAL(expected_taker_fee_percent, *updated_asset.options.extensions.value.taker_fee_percent); + + + ////// + // After HF, create limit orders that will perfectly match + ////// + BOOST_TEST_MESSAGE("Issuing 10 jillcoin to alice"); + issue_uia(alice, jillcoin.amount(10 * JILL_PRECISION)); + BOOST_TEST_MESSAGE("Checking alice's balance"); + BOOST_REQUIRE_EQUAL(get_balance(alice, jillcoin), 10 * JILL_PRECISION); + + BOOST_TEST_MESSAGE("Issuing 300 izzycoin to bob"); + issue_uia(bob, izzycoin.amount(300 * IZZY_PRECISION)); + BOOST_TEST_MESSAGE("Checking bob's balance"); + BOOST_REQUIRE_EQUAL(get_balance(bob, izzycoin), 300 * IZZY_PRECISION); + + // Alice and Bob place orders which match, and are completely filled by each other + // Alice is willing to sell 10 JILLCOIN for at least 300 IZZYCOIN + limit_order_create_operation alice_sell_op = create_sell_operation(alice.id, + jillcoin.amount(10 * JILL_PRECISION), + izzycoin.amount(300 * + IZZY_PRECISION)); + trx.clear(); + trx.operations.push_back(alice_sell_op); + asset alice_sell_fee = db.current_fee_schedule().set_fee(trx.operations.back()); + sign(trx, alice_private_key); + processed_transaction ptx = PUSH_TX(db, trx); // No exception should be thrown + limit_order_id_type alice_order_id = ptx.operation_results[0].get(); + + const limit_order_object* alice_order_before = db.find(alice_order_id); + BOOST_CHECK(alice_order_before != nullptr); + + // Bob is willing to sell 300 IZZYCOIN for at least 10 JILLCOIN + limit_order_create_operation bob_sell_op = create_sell_operation(bob.id, izzycoin.amount(300 * IZZY_PRECISION), + jillcoin.amount( + 10 * + JILL_PRECISION)); + trx.clear(); + trx.operations.push_back(bob_sell_op); + asset bob_sell_fee = db.current_fee_schedule().set_fee(trx.operations.back()); + sign(trx, bob_private_key); + ptx = PUSH_TX(db, trx); // No exception should be thrown + limit_order_id_type bob_order_id = ptx.operation_results[0].get(); + + // Check that the orders were filled by ensuring that they are no longer on the order books + const limit_order_object* alice_order = db.find(alice_order_id); + BOOST_CHECK(alice_order == nullptr); + const limit_order_object* bob_order = db.find(bob_order_id); + BOOST_CHECK(bob_order == nullptr); + + + // Check the new balances of the maker + // Alice was the maker; she is receiving IZZYCOIN + asset expected_izzy_fee = izzycoin.amount( + 300 * IZZY_PRECISION * izzy_maker_fee_percent / GRAPHENE_100_PERCENT); + BOOST_REQUIRE_EQUAL(get_balance(alice, izzycoin), + (300 * IZZY_PRECISION) - alice_sell_fee.amount.value - expected_izzy_fee.amount.value); + BOOST_REQUIRE_EQUAL(get_balance(alice, jillcoin), 0); + + // Check the new balance of the taker + // Bob was the taker; he is receiving JILLCOIN + asset expected_jill_fee = jillcoin.amount( + 10 * JILL_PRECISION * jill_taker_fee_percent / GRAPHENE_100_PERCENT); + BOOST_REQUIRE_EQUAL(get_balance(bob, jillcoin), + (10 * JILL_PRECISION) - bob_sell_fee.amount.value - expected_jill_fee.amount.value); + BOOST_REQUIRE_EQUAL(get_balance(bob, izzycoin), 0); + + // Check the asset issuer's accumulated fees + BOOST_CHECK(izzycoin.dynamic_asset_data_id(db).accumulated_fees == expected_izzy_fee.amount); + BOOST_CHECK(jillcoin.dynamic_asset_data_id(db).accumulated_fees == expected_jill_fee.amount); + + } FC_LOG_AND_RETHROW() + } + + + /** + * Test of different maker and taker fees charged when filling limit orders after HF for a UIA + * + * Test the filling of a taker fee when the **maker** fee percent is set to 0. This tests some optimizations + * in database::calculate_market_fee(). + */ + BOOST_AUTO_TEST_CASE(simple_match_and_fill_with_different_fees_uia_2) { + try { + // Initialize for the current time + trx.clear(); + set_expiration(db, trx); + + // Initialize actors + ACTORS((jill)(izzy)(alice)(bob)); + + // Initialize tokens + price price(asset(1, asset_id_type(1)), asset(1)); + + const uint16_t JILL_PRECISION = 100; + const uint16_t jill_market_fee_percent = 0 * GRAPHENE_1_PERCENT; + create_user_issued_asset("JCOIN", jill, charge_market_fee, price, 2, + jill_market_fee_percent); + + const uint16_t IZZY_PRECISION = 1000; + const uint16_t izzy_market_fee_percent = 0 * GRAPHENE_1_PERCENT; + create_user_issued_asset("ICOIN", izzy, charge_market_fee, price, 3, + izzy_market_fee_percent); + + // Obtain asset object after a block is generated to obtain the final object that is commited to the database + generate_block(); + const asset_object& jillcoin = get_asset("JCOIN"); + const asset_object& izzycoin = get_asset("ICOIN"); + + + ////// + // Advance to activate hardfork + ////// + generate_blocks(HARDFORK_BSIP_81_TIME); + generate_block(); + trx.clear(); + set_expiration(db, trx); + + + ////// + // After HF, test that new values can be set + ////// + // Define the new taker fees + uint16_t jill_maker_fee_percent = jill_market_fee_percent; + uint16_t jill_taker_fee_percent = 1 * GRAPHENE_1_PERCENT; + + uint16_t izzy_maker_fee_percent = izzy_market_fee_percent; + uint16_t izzy_taker_fee_percent = 3 * GRAPHENE_1_PERCENT; + + // Set the new taker fee for JILLCOIN + asset_update_operation uop; + uop.issuer = jill.id; + uop.asset_to_update = jillcoin.get_id(); + uop.new_options.market_fee_percent = jill_maker_fee_percent; + uop.new_options = jillcoin.options; + uop.new_options.extensions.value.taker_fee_percent = jill_taker_fee_percent; + + trx.clear(); + trx.operations.push_back(uop); + db.current_fee_schedule().set_fee(trx.operations.back()); + sign(trx, jill_private_key); + PUSH_TX(db, trx); // No exception should be thrown + + // Check the taker fee for JILLCOIN + asset_object updated_asset = jillcoin.get_id()(db); + uint16_t expected_taker_fee_percent = jill_taker_fee_percent; + BOOST_CHECK(updated_asset.options.extensions.value.taker_fee_percent.valid()); + BOOST_CHECK_EQUAL(expected_taker_fee_percent, *updated_asset.options.extensions.value.taker_fee_percent); + + // Set the new taker fee for IZZYCOIN + uop.issuer = izzy.id; + uop.asset_to_update = izzycoin.get_id(); + uop.new_options.market_fee_percent = izzy_maker_fee_percent; + uop.new_options = izzycoin.options; + uop.new_options.extensions.value.taker_fee_percent = izzy_taker_fee_percent; + + trx.clear(); + trx.operations.push_back(uop); + db.current_fee_schedule().set_fee(trx.operations.back()); + sign(trx, izzy_private_key); + PUSH_TX(db, trx); // No exception should be thrown + + // Check the taker fee for IZZYCOIN + updated_asset = izzycoin.get_id()(db); + expected_taker_fee_percent = izzy_taker_fee_percent; + BOOST_CHECK(updated_asset.options.extensions.value.taker_fee_percent.valid()); + BOOST_CHECK_EQUAL(expected_taker_fee_percent, *updated_asset.options.extensions.value.taker_fee_percent); + + + ////// + // After HF, create limit orders that will perfectly match + ////// + BOOST_TEST_MESSAGE("Issuing 10 jillcoin to alice"); + issue_uia(alice, jillcoin.amount(10 * JILL_PRECISION)); + BOOST_TEST_MESSAGE("Checking alice's balance"); + BOOST_REQUIRE_EQUAL(get_balance(alice, jillcoin), 10 * JILL_PRECISION); + + BOOST_TEST_MESSAGE("Issuing 300 izzycoin to bob"); + issue_uia(bob, izzycoin.amount(300 * IZZY_PRECISION)); + BOOST_TEST_MESSAGE("Checking bob's balance"); + BOOST_REQUIRE_EQUAL(get_balance(bob, izzycoin), 300 * IZZY_PRECISION); + + // Alice and Bob place orders which match, and are completely filled by each other + // Alice is willing to sell 10 JILLCOIN for at least 300 IZZYCOIN + limit_order_create_operation alice_sell_op = create_sell_operation(alice.id, + jillcoin.amount(10 * JILL_PRECISION), + izzycoin.amount(300 * + IZZY_PRECISION)); + trx.clear(); + trx.operations.push_back(alice_sell_op); + asset alice_sell_fee = db.current_fee_schedule().set_fee(trx.operations.back()); + sign(trx, alice_private_key); + processed_transaction ptx = PUSH_TX(db, trx); // No exception should be thrown + limit_order_id_type alice_order_id = ptx.operation_results[0].get(); + + const limit_order_object* alice_order_before = db.find(alice_order_id); + BOOST_CHECK(alice_order_before != nullptr); + + // Bob is willing to sell 300 IZZYCOIN for at least 10 JILLCOIN + limit_order_create_operation bob_sell_op = create_sell_operation(bob.id, izzycoin.amount(300 * IZZY_PRECISION), + jillcoin.amount( + 10 * + JILL_PRECISION)); + trx.clear(); + trx.operations.push_back(bob_sell_op); + asset bob_sell_fee = db.current_fee_schedule().set_fee(trx.operations.back()); + sign(trx, bob_private_key); + ptx = PUSH_TX(db, trx); // No exception should be thrown + limit_order_id_type bob_order_id = ptx.operation_results[0].get(); + + // Check that the orders were filled by ensuring that they are no longer on the order books + const limit_order_object* alice_order = db.find(alice_order_id); + BOOST_CHECK(alice_order == nullptr); + const limit_order_object* bob_order = db.find(bob_order_id); + BOOST_CHECK(bob_order == nullptr); + + + // Check the new balances of the maker + // Alice was the maker; she is receiving IZZYCOIN + asset expected_izzy_fee = izzycoin.amount( + 300 * IZZY_PRECISION * izzy_maker_fee_percent / GRAPHENE_100_PERCENT); + BOOST_REQUIRE_EQUAL(get_balance(alice, izzycoin), + (300 * IZZY_PRECISION) - alice_sell_fee.amount.value - expected_izzy_fee.amount.value); + BOOST_REQUIRE_EQUAL(get_balance(alice, jillcoin), 0); + + // Check the new balance of the taker + // Bob was the taker; he is receiving JILLCOIN + asset expected_jill_fee = jillcoin.amount( + 10 * JILL_PRECISION * jill_taker_fee_percent / GRAPHENE_100_PERCENT); + BOOST_REQUIRE_EQUAL(get_balance(bob, jillcoin), + (10 * JILL_PRECISION) - bob_sell_fee.amount.value - expected_jill_fee.amount.value); + BOOST_REQUIRE_EQUAL(get_balance(bob, izzycoin), 0); + + // Check the asset issuer's accumulated fees + BOOST_CHECK(izzycoin.dynamic_asset_data_id(db).accumulated_fees == expected_izzy_fee.amount); + BOOST_CHECK(jillcoin.dynamic_asset_data_id(db).accumulated_fees == expected_jill_fee.amount); + + } FC_LOG_AND_RETHROW() + } + + + /** + * Test of different maker and taker fees charged when filling limit orders after HF for a UIA + * + * Test the filling of a taker fee when the **taker** fee percent is set to 0. This tests some optimizations + * in database::calculate_market_fee(). + */ + BOOST_AUTO_TEST_CASE(simple_match_and_fill_with_different_fees_uia_3) { + try { + // Initialize for the current time + trx.clear(); + set_expiration(db, trx); + + // Initialize actors + ACTORS((jill)(izzy)(alice)(bob)); + + // Initialize tokens + price price(asset(1, asset_id_type(1)), asset(1)); + + const uint16_t JILL_PRECISION = 100; + const uint16_t jill_market_fee_percent = 2 * GRAPHENE_1_PERCENT; + create_user_issued_asset("JCOIN", jill, charge_market_fee, price, 2, + jill_market_fee_percent); + + const uint16_t IZZY_PRECISION = 1000; + const uint16_t izzy_market_fee_percent = 5 * GRAPHENE_1_PERCENT; + create_user_issued_asset("ICOIN", izzy, charge_market_fee, price, 3, + izzy_market_fee_percent); + + // Obtain asset object after a block is generated to obtain the final object that is commited to the database + generate_block(); + const asset_object& jillcoin = get_asset("JCOIN"); + const asset_object& izzycoin = get_asset("ICOIN"); + + + ////// + // Advance to activate hardfork + ////// + generate_blocks(HARDFORK_BSIP_81_TIME); + generate_block(); + trx.clear(); + set_expiration(db, trx); + + + ////// + // After HF, test that new values can be set + ////// + // Define the new taker fees + uint16_t jill_maker_fee_percent = jill_market_fee_percent; + uint16_t jill_taker_fee_percent = 0 * GRAPHENE_1_PERCENT; + + uint16_t izzy_maker_fee_percent = izzy_market_fee_percent; + uint16_t izzy_taker_fee_percent = 0 * GRAPHENE_1_PERCENT; + + // Set the new taker fee for JILLCOIN + asset_update_operation uop; + uop.issuer = jill.id; + uop.asset_to_update = jillcoin.get_id(); + uop.new_options.market_fee_percent = jill_maker_fee_percent; + uop.new_options = jillcoin.options; + uop.new_options.extensions.value.taker_fee_percent = jill_taker_fee_percent; + + trx.clear(); + trx.operations.push_back(uop); + db.current_fee_schedule().set_fee(trx.operations.back()); + sign(trx, jill_private_key); + PUSH_TX(db, trx); // No exception should be thrown + + // Check the taker fee for JILLCOIN + asset_object updated_asset = jillcoin.get_id()(db); + uint16_t expected_taker_fee_percent = jill_taker_fee_percent; + BOOST_CHECK(updated_asset.options.extensions.value.taker_fee_percent.valid()); + BOOST_CHECK_EQUAL(expected_taker_fee_percent, *updated_asset.options.extensions.value.taker_fee_percent); + + // Set the new taker fee for IZZYCOIN + uop.issuer = izzy.id; + uop.asset_to_update = izzycoin.get_id(); + uop.new_options.market_fee_percent = izzy_maker_fee_percent; + uop.new_options = izzycoin.options; + uop.new_options.extensions.value.taker_fee_percent = izzy_taker_fee_percent; + + trx.clear(); + trx.operations.push_back(uop); + db.current_fee_schedule().set_fee(trx.operations.back()); + sign(trx, izzy_private_key); + PUSH_TX(db, trx); // No exception should be thrown + + // Check the taker fee for IZZYCOIN + updated_asset = izzycoin.get_id()(db); + expected_taker_fee_percent = izzy_taker_fee_percent; + BOOST_CHECK(updated_asset.options.extensions.value.taker_fee_percent.valid()); + BOOST_CHECK_EQUAL(expected_taker_fee_percent, *updated_asset.options.extensions.value.taker_fee_percent); + + + ////// + // After HF, create limit orders that will perfectly match + ////// + BOOST_TEST_MESSAGE("Issuing 10 jillcoin to alice"); + issue_uia(alice, jillcoin.amount(10 * JILL_PRECISION)); + BOOST_TEST_MESSAGE("Checking alice's balance"); + BOOST_REQUIRE_EQUAL(get_balance(alice, jillcoin), 10 * JILL_PRECISION); + + BOOST_TEST_MESSAGE("Issuing 300 izzycoin to bob"); + issue_uia(bob, izzycoin.amount(300 * IZZY_PRECISION)); + BOOST_TEST_MESSAGE("Checking bob's balance"); + BOOST_REQUIRE_EQUAL(get_balance(bob, izzycoin), 300 * IZZY_PRECISION); + + // Alice and Bob place orders which match, and are completely filled by each other + // Alice is willing to sell 10 JILLCOIN for at least 300 IZZYCOIN + limit_order_create_operation alice_sell_op = create_sell_operation(alice.id, + jillcoin.amount(10 * JILL_PRECISION), + izzycoin.amount(300 * + IZZY_PRECISION)); + trx.clear(); + trx.operations.push_back(alice_sell_op); + asset alice_sell_fee = db.current_fee_schedule().set_fee(trx.operations.back()); + sign(trx, alice_private_key); + processed_transaction ptx = PUSH_TX(db, trx); // No exception should be thrown + limit_order_id_type alice_order_id = ptx.operation_results[0].get(); + + const limit_order_object* alice_order_before = db.find(alice_order_id); + BOOST_CHECK(alice_order_before != nullptr); + + // Bob is willing to sell 300 IZZYCOIN for at least 10 JILLCOIN + limit_order_create_operation bob_sell_op = create_sell_operation(bob.id, izzycoin.amount(300 * IZZY_PRECISION), + jillcoin.amount( + 10 * + JILL_PRECISION)); + trx.clear(); + trx.operations.push_back(bob_sell_op); + asset bob_sell_fee = db.current_fee_schedule().set_fee(trx.operations.back()); + sign(trx, bob_private_key); + ptx = PUSH_TX(db, trx); // No exception should be thrown + limit_order_id_type bob_order_id = ptx.operation_results[0].get(); + + // Check that the orders were filled by ensuring that they are no longer on the order books + const limit_order_object* alice_order = db.find(alice_order_id); + BOOST_CHECK(alice_order == nullptr); + const limit_order_object* bob_order = db.find(bob_order_id); + BOOST_CHECK(bob_order == nullptr); + + + // Check the new balances of the maker + // Alice was the maker; she is receiving IZZYCOIN + asset expected_izzy_fee = izzycoin.amount( + 300 * IZZY_PRECISION * izzy_maker_fee_percent / GRAPHENE_100_PERCENT); + BOOST_REQUIRE_EQUAL(get_balance(alice, izzycoin), + (300 * IZZY_PRECISION) - alice_sell_fee.amount.value - expected_izzy_fee.amount.value); + BOOST_REQUIRE_EQUAL(get_balance(alice, jillcoin), 0); + + // Check the new balance of the taker + // Bob was the taker; he is receiving JILLCOIN + asset expected_jill_fee = jillcoin.amount( + 10 * JILL_PRECISION * jill_taker_fee_percent / GRAPHENE_100_PERCENT); + BOOST_REQUIRE_EQUAL(get_balance(bob, jillcoin), + (10 * JILL_PRECISION) - bob_sell_fee.amount.value - expected_jill_fee.amount.value); + BOOST_REQUIRE_EQUAL(get_balance(bob, izzycoin), 0); + + // Check the asset issuer's accumulated fees + BOOST_CHECK(izzycoin.dynamic_asset_data_id(db).accumulated_fees == expected_izzy_fee.amount); + BOOST_CHECK(jillcoin.dynamic_asset_data_id(db).accumulated_fees == expected_jill_fee.amount); + + } FC_LOG_AND_RETHROW() + } + + + /** + * Test of **default** taker fees charged when filling limit orders after HF for a UIA. + * + * This test is similar to simple_match_and_fill_with_different_fees_uia_1 + * except that the taker fee is not explicitly set and instead defaults to the maker fee. + */ + BOOST_AUTO_TEST_CASE(simple_match_and_fill_with_different_fees_uia_4) { + try { + // Initialize for the current time + trx.clear(); + set_expiration(db, trx); + + // Initialize actors + ACTORS((jill)(izzy)(alice)(bob)); + + // Initialize tokens + price price(asset(1, asset_id_type(1)), asset(1)); + + const uint16_t JILL_PRECISION = 100; + const uint16_t jill_market_fee_percent = 2 * GRAPHENE_1_PERCENT; + create_user_issued_asset("JCOIN", jill, charge_market_fee, price, 2, + jill_market_fee_percent); + + const uint16_t IZZY_PRECISION = 1000; + const uint16_t izzy_market_fee_percent = 5 * GRAPHENE_1_PERCENT; + create_user_issued_asset("ICOIN", izzy, charge_market_fee, price, 3, + izzy_market_fee_percent); + + // Obtain asset object after a block is generated to obtain the final object that is commited to the database + generate_block(); + const asset_object& jillcoin = get_asset("JCOIN"); + const asset_object& izzycoin = get_asset("ICOIN"); + + + ////// + // Advance to activate hardfork + ////// + generate_blocks(HARDFORK_BSIP_81_TIME); + generate_block(); + trx.clear(); + set_expiration(db, trx); + + + ////// + // After HF, test that default taker values has not been set + ////// + // The taker fees should automatically default to maker fees if the taker fee is not explicitly set + // UNUSED: uint16_t jill_maker_fee_percent = jill_market_fee_percent; + uint16_t jill_taker_fee_percent = jill_market_fee_percent; + + uint16_t izzy_maker_fee_percent = izzy_market_fee_percent; + // UNUSED: uint16_t izzy_taker_fee_percent = izzy_market_fee_percent; + + // Check the taker fee for JCOIN: it should still not be set + asset_object updated_asset = jillcoin.get_id()(db); + BOOST_CHECK(!updated_asset.options.extensions.value.taker_fee_percent.valid()); + + // Check the taker fee for ICOIN: it should still not be set + updated_asset = izzycoin.get_id()(db); + BOOST_CHECK(!updated_asset.options.extensions.value.taker_fee_percent.valid()); + + + ////// + // After HF, create limit orders that will perfectly match + ////// + BOOST_TEST_MESSAGE("Issuing 10 jillcoin to alice"); + issue_uia(alice, jillcoin.amount(10 * JILL_PRECISION)); + BOOST_TEST_MESSAGE("Checking alice's balance"); + BOOST_REQUIRE_EQUAL(get_balance(alice, jillcoin), 10 * JILL_PRECISION); + + BOOST_TEST_MESSAGE("Issuing 300 izzycoin to bob"); + issue_uia(bob, izzycoin.amount(300 * IZZY_PRECISION)); + BOOST_TEST_MESSAGE("Checking bob's balance"); + BOOST_REQUIRE_EQUAL(get_balance(bob, izzycoin), 300 * IZZY_PRECISION); + + // Alice and Bob place orders which match, and are completely filled by each other + // Alice is willing to sell 10 JILLCOIN for at least 300 IZZYCOIN + limit_order_create_operation alice_sell_op = create_sell_operation(alice.id, + jillcoin.amount(10 * JILL_PRECISION), + izzycoin.amount(300 * + IZZY_PRECISION)); + trx.clear(); + trx.operations.push_back(alice_sell_op); + asset alice_sell_fee = db.current_fee_schedule().set_fee(trx.operations.back()); + sign(trx, alice_private_key); + processed_transaction ptx = PUSH_TX(db, trx); // No exception should be thrown + limit_order_id_type alice_order_id = ptx.operation_results[0].get(); + + const limit_order_object* alice_order_before = db.find(alice_order_id); + BOOST_CHECK(alice_order_before != nullptr); + + // Bob is willing to sell 300 IZZYCOIN for at least 10 JILLCOIN + limit_order_create_operation bob_sell_op = create_sell_operation(bob.id, izzycoin.amount(300 * IZZY_PRECISION), + jillcoin.amount( + 10 * + JILL_PRECISION)); + trx.clear(); + trx.operations.push_back(bob_sell_op); + asset bob_sell_fee = db.current_fee_schedule().set_fee(trx.operations.back()); + sign(trx, bob_private_key); + ptx = PUSH_TX(db, trx); // No exception should be thrown + limit_order_id_type bob_order_id = ptx.operation_results[0].get(); + + // Check that the orders were filled by ensuring that they are no longer on the order books + const limit_order_object* alice_order = db.find(alice_order_id); + BOOST_CHECK(alice_order == nullptr); + const limit_order_object* bob_order = db.find(bob_order_id); + BOOST_CHECK(bob_order == nullptr); + + + // Check the new balances of the maker + // Alice was the maker; she is receiving IZZYCOIN + asset expected_izzy_fee = izzycoin.amount( + 300 * IZZY_PRECISION * izzy_maker_fee_percent / GRAPHENE_100_PERCENT); + BOOST_REQUIRE_EQUAL(get_balance(alice, izzycoin), + (300 * IZZY_PRECISION) - alice_sell_fee.amount.value - expected_izzy_fee.amount.value); + BOOST_REQUIRE_EQUAL(get_balance(alice, jillcoin), 0); + + // Check the new balance of the taker + // Bob was the taker; he is receiving JILLCOIN + asset expected_jill_fee = jillcoin.amount( + 10 * JILL_PRECISION * jill_taker_fee_percent / GRAPHENE_100_PERCENT); + BOOST_REQUIRE_EQUAL(get_balance(bob, jillcoin), + (10 * JILL_PRECISION) - bob_sell_fee.amount.value - expected_jill_fee.amount.value); + BOOST_REQUIRE_EQUAL(get_balance(bob, izzycoin), 0); + + // Check the asset issuer's accumulated fees + BOOST_CHECK(izzycoin.dynamic_asset_data_id(db).accumulated_fees == expected_izzy_fee.amount); + BOOST_CHECK(jillcoin.dynamic_asset_data_id(db).accumulated_fees == expected_jill_fee.amount); + + } FC_LOG_AND_RETHROW() + } + + + + /** + * Test of different maker and taker fees charged when filling limit orders after HF for a smart asset + */ + BOOST_AUTO_TEST_CASE(simple_match_and_fill_with_different_fees_smart_asset) { + try { + // Initialize for the current time + trx.clear(); + set_expiration(db, trx); + + // Initialize actors + ACTORS((jill)(izzy)(alice)(bob)); + ACTORS((smartissuer)(feedproducer)); + + // Initialize tokens + price price(asset(1, asset_id_type(1)), asset(1)); + const uint16_t JILL_PRECISION = 100; + const uint16_t jill_market_fee_percent = 2 * GRAPHENE_1_PERCENT; + const asset_object jillcoin = create_user_issued_asset("JCOIN", jill, charge_market_fee, price, 2, + jill_market_fee_percent); + + const uint16_t SMARTBIT_PRECISION = 10000; + const uint16_t smartbit_market_fee_percent = 2 * GRAPHENE_1_PERCENT; + create_bitasset("SMARTBIT", smartissuer.id, smartbit_market_fee_percent, + charge_market_fee, 4); + + // Obtain asset object after a block is generated to obtain the final object that is commited to the database + generate_block(); + const asset_object &smartbit = get_asset("SMARTBIT"); + + const auto &core = asset_id_type()(db); + + update_feed_producers(smartbit, {feedproducer.id}); + + price_feed current_feed; + current_feed.settlement_price = smartbit.amount(100) / core.amount(100); + current_feed.maintenance_collateral_ratio = 1750; // need to set this explicitly, testnet has a different default + publish_feed(smartbit, feedproducer, current_feed); + + FC_ASSERT(smartbit.bitasset_data(db).current_feed.settlement_price == current_feed.settlement_price); + + + ////// + // Advance to activate hardfork + ////// + generate_blocks(HARDFORK_BSIP_81_TIME); + generate_block(); + trx.clear(); + set_expiration(db, trx); + + + ////// + // After HF, test that new values can be set + ////// + // Define the new taker fees + uint16_t jill_maker_fee_percent = jill_market_fee_percent; + uint16_t jill_taker_fee_percent = jill_maker_fee_percent / 2; + + uint16_t smartbit_maker_fee_percent = 1 * GRAPHENE_1_PERCENT; + uint16_t smartbit_taker_fee_percent = 3 * GRAPHENE_1_PERCENT; + + // Set the new taker fee for JILLCOIN + asset_update_operation uop; + uop.issuer = jill.id; + uop.asset_to_update = jillcoin.get_id(); + uop.new_options = jillcoin.options; + uop.new_options.extensions.value.taker_fee_percent = jill_taker_fee_percent; + + trx.clear(); + trx.operations.push_back(uop); + db.current_fee_schedule().set_fee(trx.operations.back()); + sign(trx, jill_private_key); + PUSH_TX(db, trx); // No exception should be thrown + + // Check the taker fee for JILLCOIN + asset_object updated_asset = jillcoin.get_id()(db); + uint16_t expected_taker_fee_percent = jill_taker_fee_percent; + BOOST_CHECK(updated_asset.options.extensions.value.taker_fee_percent.valid()); + BOOST_CHECK_EQUAL(expected_taker_fee_percent, *updated_asset.options.extensions.value.taker_fee_percent); + + + // Set the new taker fee for SMARTBIT + uop = asset_update_operation(); + uop.issuer = smartissuer.id; + uop.asset_to_update = smartbit.get_id(); + uop.new_options = smartbit.options; + uop.new_options.market_fee_percent = smartbit_maker_fee_percent; + uop.new_options.extensions.value.taker_fee_percent = smartbit_taker_fee_percent; + + trx.clear(); + trx.operations.push_back(uop); + db.current_fee_schedule().set_fee(trx.operations.back()); + sign(trx, smartissuer_private_key); + PUSH_TX(db, trx); // No exception should be thrown + + // Check the taker fee for SMARTBIT + updated_asset = smartbit.get_id()(db); + expected_taker_fee_percent = smartbit_taker_fee_percent; + BOOST_CHECK(updated_asset.options.extensions.value.taker_fee_percent.valid()); + BOOST_CHECK_EQUAL(expected_taker_fee_percent, *updated_asset.options.extensions.value.taker_fee_percent); + + // Check the maker fee for SMARTBIT + updated_asset = smartbit.get_id()(db); + expected_taker_fee_percent = smartbit_maker_fee_percent; + BOOST_CHECK_EQUAL(expected_taker_fee_percent, updated_asset.options.market_fee_percent); + + + ////// + // After HF, create limit orders that will perfectly match + ////// + BOOST_TEST_MESSAGE("Issuing 10 jillcoin to alice"); + issue_uia(alice, jillcoin.amount(10 * JILL_PRECISION)); + BOOST_TEST_MESSAGE("Checking alice's balance"); + BOOST_REQUIRE_EQUAL(get_balance(alice, jillcoin), 10 * JILL_PRECISION); + + BOOST_TEST_MESSAGE("Issuing 300 SMARTBIT to bob"); + transfer(committee_account, bob.id, asset(10000000)); + publish_feed(smartbit, feedproducer, current_feed); // Publish a recent feed + borrow(bob, smartbit.amount(300 * SMARTBIT_PRECISION), asset(2 * 300 * SMARTBIT_PRECISION)); + BOOST_TEST_MESSAGE("Checking bob's balance"); + BOOST_REQUIRE_EQUAL(get_balance(bob, smartbit), 300 * SMARTBIT_PRECISION); + + // Alice and Bob place orders which match, and are completely filled by each other + // Alice is willing to sell 10 JILLCOIN for at least 300 SMARTBIT + limit_order_create_operation alice_sell_op = create_sell_operation(alice.id, + jillcoin.amount(10 * JILL_PRECISION), + smartbit.amount(300 * SMARTBIT_PRECISION)); + trx.clear(); + trx.operations.push_back(alice_sell_op); + asset alice_sell_fee = db.current_fee_schedule().set_fee(trx.operations.back()); + sign(trx, alice_private_key); + processed_transaction ptx = PUSH_TX(db, trx); // No exception should be thrown + limit_order_id_type alice_order_id = ptx.operation_results[0].get(); + + const limit_order_object *alice_order_before = db.find(alice_order_id); + BOOST_CHECK(alice_order_before != nullptr); + + + // Bob is willing to sell 300 SMARTBIT for at least 10 JILLCOIN + limit_order_create_operation bob_sell_op + = create_sell_operation(bob.id, smartbit.amount(300 * SMARTBIT_PRECISION), + jillcoin.amount(10 * JILL_PRECISION)); + trx.clear(); + trx.operations.push_back(bob_sell_op); + asset bob_sell_fee = db.current_fee_schedule().set_fee(trx.operations.back()); + sign(trx, bob_private_key); + ptx = PUSH_TX(db, trx); // No exception should be thrown + limit_order_id_type bob_order_id = ptx.operation_results[0].get(); + + // Check that the orders were filled by ensuring that they are no longer on the order books + const limit_order_object *alice_order = db.find(alice_order_id); + BOOST_CHECK(alice_order == nullptr); + const limit_order_object *bob_order = db.find(bob_order_id); + BOOST_CHECK(bob_order == nullptr); + + + // Check the new balances of the maker + // Alice was the maker; she is receiving SMARTBIT + asset expected_smartbit_fee = smartbit.amount( + 300 * SMARTBIT_PRECISION * smartbit_maker_fee_percent / GRAPHENE_100_PERCENT); + BOOST_REQUIRE_EQUAL(get_balance(alice, smartbit), + (300 * SMARTBIT_PRECISION) - alice_sell_fee.amount.value - + expected_smartbit_fee.amount.value); + BOOST_REQUIRE_EQUAL(get_balance(alice, jillcoin), 0); + + // Check the new balance of the taker + // Bob was the taker; he is receiving JILLCOIN + asset expected_jill_fee = jillcoin.amount( + 10 * JILL_PRECISION * jill_taker_fee_percent / GRAPHENE_100_PERCENT); + BOOST_REQUIRE_EQUAL(get_balance(bob, jillcoin), + (10 * JILL_PRECISION) - bob_sell_fee.amount.value - expected_jill_fee.amount.value); + BOOST_REQUIRE_EQUAL(get_balance(bob, smartbit), 0); + + // Check the asset issuer's accumulated fees + BOOST_CHECK(smartbit.dynamic_asset_data_id(db).accumulated_fees == expected_smartbit_fee.amount); + BOOST_CHECK(jillcoin.dynamic_asset_data_id(db).accumulated_fees == expected_jill_fee.amount); + + } FC_LOG_AND_RETHROW() + } + + + /** + * Test of different maker and taker fees charged when filling limit orders after HF for a smart asset + * and a user-issued asset + * + * 1. (Order 1) An order will be placed to offer JCOIN + * + * 2. (Order 2) A matching-order will be placed to offer SMARTBIT. + * Order 2 is large enough that it should be partially filled, and Order 1 will be completely filled. + * Order 1 should be charged a maker fee, and Order 2 should be charged a taker fee. + * Order 2 should remain on the book. + * + * 3. (Order 3) A matching order will be placed to offer JCOIN. + * Order 3 should be charged a taker fee, and Order 2 should be charged a maker fee. + * + * Summary: Order 2 should be charged a taker fee when matching Order 1, + * and Order 2 should be charged a maker fee when matching Order 3. + */ + BOOST_AUTO_TEST_CASE(partial_maker_partial_taker_fills_1) { + try { + // Initialize for the current time + trx.clear(); + set_expiration(db, trx); + + // Initialize actors + ACTORS((jill)(izzy)(alice)(bob)(charlie)); + ACTORS((smartissuer)(feedproducer)); + + // Initialize tokens + price price(asset(1, asset_id_type(1)), asset(1)); + const uint16_t JILL_PRECISION = 100; + const uint16_t jill_market_fee_percent = 2 * GRAPHENE_1_PERCENT; + const asset_object jillcoin = create_user_issued_asset("JCOIN", jill, charge_market_fee, price, 2, + jill_market_fee_percent); + + const uint16_t SMARTBIT_PRECISION = 10000; + const uint16_t smartbit_market_fee_percent = 2 * GRAPHENE_1_PERCENT; + create_bitasset("SMARTBIT", smartissuer.id, smartbit_market_fee_percent, + charge_market_fee, 4); + // Obtain asset object after a block is generated to obtain the final object that is commited to the database + generate_block(); + const asset_object &smartbit = get_asset("SMARTBIT"); + + const auto &core = asset_id_type()(db); + + update_feed_producers(smartbit, {feedproducer.id}); + + price_feed current_feed; + current_feed.settlement_price = smartbit.amount(100) / core.amount(100); + current_feed.maintenance_collateral_ratio = 1750; // need to set this explicitly, testnet has a different default + publish_feed(smartbit, feedproducer, current_feed); + + FC_ASSERT(smartbit.bitasset_data(db).current_feed.settlement_price == current_feed.settlement_price); + + + ////// + // Advance to activate hardfork + ////// + generate_blocks(HARDFORK_BSIP_81_TIME); + generate_block(); + trx.clear(); + set_expiration(db, trx); + + + ////// + // After HF, test that new values can be set + ////// + // Define the new taker fees + uint16_t jill_maker_fee_percent = jill_market_fee_percent; + uint16_t jill_taker_fee_percent = jill_maker_fee_percent / 2; + + uint16_t smartbit_maker_fee_percent = 1 * GRAPHENE_1_PERCENT; + uint16_t smartbit_taker_fee_percent = 3 * GRAPHENE_1_PERCENT; + + // Set the new taker fee for JILLCOIN + asset_update_operation uop; + uop.issuer = jill.id; + uop.asset_to_update = jillcoin.get_id(); + uop.new_options = jillcoin.options; + uop.new_options.extensions.value.taker_fee_percent = jill_taker_fee_percent; + + trx.clear(); + trx.operations.push_back(uop); + db.current_fee_schedule().set_fee(trx.operations.back()); + sign(trx, jill_private_key); + PUSH_TX(db, trx); // No exception should be thrown + + // Check the taker fee for JILLCOIN + asset_object updated_asset = jillcoin.get_id()(db); + uint16_t expected_taker_fee_percent = jill_taker_fee_percent; + BOOST_CHECK(updated_asset.options.extensions.value.taker_fee_percent.valid()); + BOOST_CHECK_EQUAL(expected_taker_fee_percent, *updated_asset.options.extensions.value.taker_fee_percent); + + // Check the maker fee for JILLCOIN + uint16_t expected_maker_fee_percent = jill_maker_fee_percent; + BOOST_CHECK_EQUAL(expected_maker_fee_percent, updated_asset.options.market_fee_percent); + + + // Set the new taker fee for SMARTBIT + uop = asset_update_operation(); + uop.issuer = smartissuer.id; + uop.asset_to_update = smartbit.get_id(); + uop.new_options = smartbit.options; + uop.new_options.market_fee_percent = smartbit_maker_fee_percent; + uop.new_options.extensions.value.taker_fee_percent = smartbit_taker_fee_percent; + + trx.clear(); + trx.operations.push_back(uop); + db.current_fee_schedule().set_fee(trx.operations.back()); + sign(trx, smartissuer_private_key); + PUSH_TX(db, trx); // No exception should be thrown + + // Check the taker fee for SMARTBIT + updated_asset = smartbit.get_id()(db); + expected_taker_fee_percent = smartbit_taker_fee_percent; + BOOST_CHECK(updated_asset.options.extensions.value.taker_fee_percent.valid()); + BOOST_CHECK_EQUAL(expected_taker_fee_percent, *updated_asset.options.extensions.value.taker_fee_percent); + + // Check the maker fee for SMARTBIT + updated_asset = smartbit.get_id()(db); + expected_taker_fee_percent = smartbit_maker_fee_percent; + BOOST_CHECK_EQUAL(expected_taker_fee_percent, updated_asset.options.market_fee_percent); + + + ////// + // Create Orders 1 and 2 to match. + // Order 1 will be completely filled, and Order 2 will be partially filled. + ////// + // Initialize token balance of actors + BOOST_TEST_MESSAGE("Issuing 10 JCOIN to alice"); + issue_uia(alice, jillcoin.amount(10 * JILL_PRECISION)); + BOOST_TEST_MESSAGE("Checking alice's balance"); + BOOST_REQUIRE_EQUAL(get_balance(alice, jillcoin), 10 * JILL_PRECISION); + + BOOST_TEST_MESSAGE("Issuing 600 SMARTBIT to bob"); + transfer(committee_account, bob.id, asset(2 * 1000 * SMARTBIT_PRECISION)); + publish_feed(smartbit, feedproducer, current_feed); // Publish a recent feed + borrow(bob, smartbit.amount(600 * SMARTBIT_PRECISION), asset(2 * 600 * SMARTBIT_PRECISION)); + BOOST_TEST_MESSAGE("Checking bob's balance"); + BOOST_REQUIRE_EQUAL(get_balance(bob, smartbit), 600 * SMARTBIT_PRECISION); + + // Alice and Bob place orders which match, and are completely filled by each other + // Alice is willing to sell 10 JILLCOIN for at least 300 SMARTBIT + limit_order_create_operation order_1_op = create_sell_operation(alice.id, + jillcoin.amount(10 * JILL_PRECISION), + smartbit.amount(300 * SMARTBIT_PRECISION)); + trx.clear(); + trx.operations.push_back(order_1_op); + asset alice_sell_fee = db.current_fee_schedule().set_fee(trx.operations.back()); + sign(trx, alice_private_key); + processed_transaction ptx = PUSH_TX(db, trx); // No exception should be thrown + limit_order_id_type order_1_id = ptx.operation_results[0].get(); + + const limit_order_object *order_1_before = db.find(order_1_id); + BOOST_CHECK(order_1_before != nullptr); + + + // Bob is willing to sell 600 SMARTBIT for at least 20 JILLCOIN + limit_order_create_operation order_2_op + = create_sell_operation(bob.id, smartbit.amount(600 * SMARTBIT_PRECISION), + jillcoin.amount(20 * JILL_PRECISION)); + trx.clear(); + trx.operations.push_back(order_2_op); + asset order_2_sell_fee = db.current_fee_schedule().set_fee(trx.operations.back()); + sign(trx, bob_private_key); + ptx = PUSH_TX(db, trx); // No exception should be thrown + limit_order_id_type order_2_id = ptx.operation_results[0].get(); + + // Check that order 1 was completely filled by ensuring that they it is no longer on the order book + const limit_order_object *order_1 = db.find(order_1_id); + BOOST_CHECK(order_1 == nullptr); + // Check that order 2 was partially filled by ensuring that they it is still on the order book + const limit_order_object *order_2 = db.find(order_2_id); + BOOST_CHECK(order_2 != nullptr); + + + // Check the new balances of the maker + // Alice was the maker; she is receiving SMARTBIT + asset expected_smartbit_fee = smartbit.amount( + 300 * SMARTBIT_PRECISION * smartbit_maker_fee_percent / GRAPHENE_100_PERCENT); + int64_t expected_alice_balance_after_order_2 = + (300 * SMARTBIT_PRECISION) - alice_sell_fee.amount.value - expected_smartbit_fee.amount.value; + BOOST_REQUIRE_EQUAL(get_balance(alice, smartbit), expected_alice_balance_after_order_2); + BOOST_REQUIRE_EQUAL(get_balance(alice, jillcoin), 0); + + // Check the new balance of the taker + // Bob was the taker; he is receiving JILLCOIN + asset expected_jill_fee = jillcoin.amount( + 10 * JILL_PRECISION * jill_taker_fee_percent / GRAPHENE_100_PERCENT); + int64_t expected_bob_balance_after_order_2 = + (10 * JILL_PRECISION) - order_2_sell_fee.amount.value - expected_jill_fee.amount.value; + BOOST_REQUIRE_EQUAL(get_balance(bob, jillcoin), expected_bob_balance_after_order_2); + BOOST_REQUIRE_EQUAL(get_balance(bob, smartbit), 0); + + // Check the asset issuer's accumulated fees + share_type expected_smartbit_fee_after_order_2 = expected_smartbit_fee.amount; + share_type expected_jill_fee_after_order_2 = expected_jill_fee.amount; + BOOST_CHECK(smartbit.dynamic_asset_data_id(db).accumulated_fees == expected_smartbit_fee_after_order_2); + BOOST_CHECK(jillcoin.dynamic_asset_data_id(db).accumulated_fees == expected_jill_fee_after_order_2); + + + ////// + // Create Order 3 to match the remainder of match Order 2 + ////// + // Initialize token balance of actors + BOOST_TEST_MESSAGE("Issuing 5 JCOIN to charlie"); + trx.clear(); + issue_uia(charlie, jillcoin.amount(5 * JILL_PRECISION)); + BOOST_TEST_MESSAGE("Checking charlie's balance"); + BOOST_REQUIRE_EQUAL(get_balance(charlie, jillcoin), 5 * JILL_PRECISION); + + // Charlie is is willing to sell 5 JILLCOIN for at least 150 SMARTBIT + limit_order_create_operation order_3_op = create_sell_operation(charlie.id, + jillcoin.amount(5 * JILL_PRECISION), + smartbit.amount(150 * SMARTBIT_PRECISION)); + trx.clear(); + trx.operations.push_back(order_3_op); + asset charlie_sell_fee = db.current_fee_schedule().set_fee(trx.operations.back()); + sign(trx, charlie_private_key); + ptx = PUSH_TX(db, trx); // No exception should be thrown + limit_order_id_type order_3_id = ptx.operation_results[0].get(); + + // Order 3 should be completely filled + const limit_order_object *order_3 = db.find(order_3_id); + BOOST_CHECK(order_3 == nullptr); + + // Order 2 should be partially filled and still present on the order books + const limit_order_object *order_2_after = db.find(order_2_id); + BOOST_CHECK(order_2_after != nullptr); + + // Check the new balance of the taker + // Charlie was the taker; he is receiving SMARTBIT + expected_smartbit_fee = smartbit.amount( + 150 * SMARTBIT_PRECISION * smartbit_taker_fee_percent / GRAPHENE_100_PERCENT); + int64_t expected_charlie_balance_after_order_3 = + (150 * SMARTBIT_PRECISION) - charlie_sell_fee.amount.value - expected_smartbit_fee.amount.value; + BOOST_REQUIRE_EQUAL(get_balance(charlie, smartbit), expected_charlie_balance_after_order_3); + BOOST_REQUIRE_EQUAL(get_balance(charlie, jillcoin), 0); + + // Check the new balance of the maker + // Bob was the maker; he is receiving JILLCOIN + asset expected_jill_order_3_fee = jillcoin.amount( + 5 * JILL_PRECISION * jill_maker_fee_percent / GRAPHENE_100_PERCENT); + int64_t expected_bob_balance_after_order_3 = + expected_bob_balance_after_order_2 + + (5 * JILL_PRECISION) - expected_jill_order_3_fee.amount.value; + BOOST_REQUIRE_EQUAL(get_balance(bob, jillcoin), expected_bob_balance_after_order_3); + BOOST_REQUIRE_EQUAL(get_balance(bob, smartbit), 0); + + // Check the asset issuer's accumulated fees + share_type expected_smartbit_fee_after_order_3 = + expected_smartbit_fee_after_order_2 + expected_smartbit_fee.amount; + share_type expected_jill_fee_after_order_3 = expected_jill_fee_after_order_2 + expected_jill_order_3_fee.amount; + BOOST_CHECK(smartbit.dynamic_asset_data_id(db).accumulated_fees == expected_smartbit_fee_after_order_3); + BOOST_CHECK(jillcoin.dynamic_asset_data_id(db).accumulated_fees == expected_jill_fee_after_order_3); + + } FC_LOG_AND_RETHROW() + } + + + /** + * Test of **default** taker fees charged when filling limit orders after HF for a smart asset + * and a user-issued asset + * + * This test is similar to partial_maker_partial_taker_fills_1 except that + * (a) the taker fee is not explicitly set and instead defaults to the maker fee, and + * (b) Orders 1 and 2 are placed before the HF and Order 3 is placed after the HF. + * + * 1. (Order 1) An order will be placed to offer JCOIN + * + * 2. (Order 2) A matching-order will be placed to offer SMARTBIT. + * Order 2 is large enough that it should be partially filled, and Order 1 will be completely filled. + * Order 1 should be charged a maker fee, and Order 2 should be charged a taker fee. + * Order 2 should remain on the book. + * + * 3. (Order 3) A matching order will be placed to offer JCOIN. + * Order 3 should be charged a taker fee, and Order 2 should be charged a maker fee. + * + * Summary: Order 2 should be charged a taker fee when matching Order 1, + * and Order 2 should be charged a maker fee when matching Order 3. + */ + BOOST_AUTO_TEST_CASE(partial_maker_partial_taker_fills_2) { + try { + // Initialize for the current time + trx.clear(); + set_expiration(db, trx); + + // Initialize actors + ACTORS((jill)(izzy)(alice)(bob)(charlie)); + ACTORS((smartissuer)(feedproducer)); + + // Initialize tokens + price price(asset(1, asset_id_type(1)), asset(1)); + const uint16_t JILL_PRECISION = 100; + const uint16_t jill_market_fee_percent = 2 * GRAPHENE_1_PERCENT; + const asset_object jillcoin = create_user_issued_asset("JCOIN", jill, charge_market_fee, price, 2, + jill_market_fee_percent); + + const uint16_t SMARTBIT_PRECISION = 10000; + const uint16_t smartbit_market_fee_percent = 2 * GRAPHENE_1_PERCENT; + create_bitasset("SMARTBIT", smartissuer.id, smartbit_market_fee_percent, + charge_market_fee, 4); + + uint16_t jill_maker_fee_percent = jill_market_fee_percent; + uint16_t jill_taker_fee_percent = jill_market_fee_percent; + + uint16_t smartbit_maker_fee_percent = smartbit_market_fee_percent; + uint16_t smartbit_taker_fee_percent = smartbit_market_fee_percent; + + + // Obtain asset object after a block is generated to obtain the final object that is commited to the database + generate_block(); + const asset_object &smartbit = get_asset("SMARTBIT"); + + const auto &core = asset_id_type()(db); + + update_feed_producers(smartbit, {feedproducer.id}); + + price_feed current_feed; + current_feed.settlement_price = smartbit.amount(100) / core.amount(100); + current_feed.maintenance_collateral_ratio = 1750; // need to set this explicitly, testnet has a different default + publish_feed(smartbit, feedproducer, current_feed); + + FC_ASSERT(smartbit.bitasset_data(db).current_feed.settlement_price == current_feed.settlement_price); + + + ////// + // Create Orders 1 and 2 to match. + // Order 1 will be completely filled, and Order 2 will be partially filled. + ////// + // Initialize token balance of actors + BOOST_TEST_MESSAGE("Issuing 10 JCOIN to alice"); + issue_uia(alice, jillcoin.amount(10 * JILL_PRECISION)); + BOOST_TEST_MESSAGE("Checking alice's balance"); + BOOST_REQUIRE_EQUAL(get_balance(alice, jillcoin), 10 * JILL_PRECISION); + + BOOST_TEST_MESSAGE("Issuing 600 SMARTBIT to bob"); + transfer(committee_account, bob.id, asset(2 * 1000 * SMARTBIT_PRECISION)); + publish_feed(smartbit, feedproducer, current_feed); // Publish a recent feed + borrow(bob, smartbit.amount(600 * SMARTBIT_PRECISION), asset(2 * 600 * SMARTBIT_PRECISION)); + BOOST_TEST_MESSAGE("Checking bob's balance"); + BOOST_REQUIRE_EQUAL(get_balance(bob, smartbit), 600 * SMARTBIT_PRECISION); + + // Alice and Bob place orders which match, and are completely filled by each other + // Alice is willing to sell 10 JILLCOIN for at least 300 SMARTBIT + limit_order_create_operation order_1_op = create_sell_operation(alice.id, + jillcoin.amount(10 * JILL_PRECISION), + smartbit.amount(300 * SMARTBIT_PRECISION)); + trx.clear(); + trx.operations.push_back(order_1_op); + asset alice_sell_fee = db.current_fee_schedule().set_fee(trx.operations.back()); + sign(trx, alice_private_key); + processed_transaction ptx = PUSH_TX(db, trx); // No exception should be thrown + limit_order_id_type order_1_id = ptx.operation_results[0].get(); + + const limit_order_object *order_1_before = db.find(order_1_id); + BOOST_CHECK(order_1_before != nullptr); + + + // Bob is willing to sell 600 SMARTBIT for at least 20 JILLCOIN + limit_order_create_operation order_2_op + = create_sell_operation(bob.id, smartbit.amount(600 * SMARTBIT_PRECISION), + jillcoin.amount(20 * JILL_PRECISION)); + trx.clear(); + trx.operations.push_back(order_2_op); + asset order_2_sell_fee = db.current_fee_schedule().set_fee(trx.operations.back()); + sign(trx, bob_private_key); + ptx = PUSH_TX(db, trx); // No exception should be thrown + limit_order_id_type order_2_id = ptx.operation_results[0].get(); + + // Check that order 1 was completely filled by ensuring that they it is no longer on the order book + const limit_order_object *order_1 = db.find(order_1_id); + BOOST_CHECK(order_1 == nullptr); + // Check that order 2 was partially filled by ensuring that they it is still on the order book + const limit_order_object *order_2 = db.find(order_2_id); + BOOST_CHECK(order_2 != nullptr); + + + // Check the new balances of the maker + // Alice was the maker; she is receiving SMARTBIT + asset expected_smartbit_fee = smartbit.amount( + 300 * SMARTBIT_PRECISION * smartbit_maker_fee_percent / GRAPHENE_100_PERCENT); + int64_t expected_alice_balance_after_order_2 = + (300 * SMARTBIT_PRECISION) - alice_sell_fee.amount.value - expected_smartbit_fee.amount.value; + BOOST_REQUIRE_EQUAL(get_balance(alice, smartbit), expected_alice_balance_after_order_2); + BOOST_REQUIRE_EQUAL(get_balance(alice, jillcoin), 0); + + // Check the new balance of the taker + // Bob was the taker; he is receiving JILLCOIN + asset expected_jill_fee = jillcoin.amount( + 10 * JILL_PRECISION * jill_taker_fee_percent / GRAPHENE_100_PERCENT); + int64_t expected_bob_balance_after_order_2 = + (10 * JILL_PRECISION) - order_2_sell_fee.amount.value - expected_jill_fee.amount.value; + BOOST_REQUIRE_EQUAL(get_balance(bob, jillcoin), expected_bob_balance_after_order_2); + BOOST_REQUIRE_EQUAL(get_balance(bob, smartbit), 0); + + // Check the asset issuer's accumulated fees + share_type expected_smartbit_fee_after_order_2 = expected_smartbit_fee.amount; + share_type expected_jill_fee_after_order_2 = expected_jill_fee.amount; + BOOST_CHECK(smartbit.dynamic_asset_data_id(db).accumulated_fees == expected_smartbit_fee_after_order_2); + BOOST_CHECK(jillcoin.dynamic_asset_data_id(db).accumulated_fees == expected_jill_fee_after_order_2); + + + ////// + // After HF, The taker fees should automatically default to maker fees when the taker fee is not explicitly set + ////// + // Check the taker fee for JILLCOIN + asset_object updated_asset = jillcoin.get_id()(db); + BOOST_CHECK(!updated_asset.options.extensions.value.taker_fee_percent.valid()); + + // Check the maker fee for JILLCOIN + uint16_t expected_maker_fee_percent = jill_maker_fee_percent; + BOOST_CHECK_EQUAL(expected_maker_fee_percent, updated_asset.options.market_fee_percent); + + // Check the taker fee for SMARTBIT + updated_asset = smartbit.get_id()(db); + BOOST_CHECK(!updated_asset.options.extensions.value.taker_fee_percent.valid()); + + // Check the maker fee for SMARTBIT + expected_maker_fee_percent = smartbit_maker_fee_percent; + BOOST_CHECK_EQUAL(expected_maker_fee_percent, updated_asset.options.market_fee_percent); + + + ////// + // Create Order 3 to match the remainder of match Order 2 + ////// + // Initialize token balance of actors + BOOST_TEST_MESSAGE("Issuing 5 JCOIN to charlie"); + trx.clear(); + issue_uia(charlie, jillcoin.amount(5 * JILL_PRECISION)); + BOOST_TEST_MESSAGE("Checking charlie's balance"); + BOOST_REQUIRE_EQUAL(get_balance(charlie, jillcoin), 5 * JILL_PRECISION); + + // Charlie is is willing to sell 5 JILLCOIN for at least 150 SMARTBIT + limit_order_create_operation order_3_op = create_sell_operation(charlie.id, + jillcoin.amount(5 * JILL_PRECISION), + smartbit.amount(150 * SMARTBIT_PRECISION)); + trx.clear(); + trx.operations.push_back(order_3_op); + asset charlie_sell_fee = db.current_fee_schedule().set_fee(trx.operations.back()); + sign(trx, charlie_private_key); + ptx = PUSH_TX(db, trx); // No exception should be thrown + limit_order_id_type order_3_id = ptx.operation_results[0].get(); + + // Order 3 should be completely filled + const limit_order_object *order_3 = db.find(order_3_id); + BOOST_CHECK(order_3 == nullptr); + + // Order 2 should be partially filled and still present on the order books + const limit_order_object *order_2_after = db.find(order_2_id); + BOOST_CHECK(order_2_after != nullptr); + + // Check the new balance of the taker + // Charlie was the taker; he is receiving SMARTBIT + expected_smartbit_fee = smartbit.amount( + 150 * SMARTBIT_PRECISION * smartbit_taker_fee_percent / GRAPHENE_100_PERCENT); + int64_t expected_charlie_balance_after_order_3 = + (150 * SMARTBIT_PRECISION) - charlie_sell_fee.amount.value - expected_smartbit_fee.amount.value; + BOOST_REQUIRE_EQUAL(get_balance(charlie, smartbit), expected_charlie_balance_after_order_3); + BOOST_REQUIRE_EQUAL(get_balance(charlie, jillcoin), 0); + + // Check the new balance of the maker + // Bob was the maker; he is receiving JILLCOIN + asset expected_jill_order_3_fee = jillcoin.amount( + 5 * JILL_PRECISION * jill_maker_fee_percent / GRAPHENE_100_PERCENT); + int64_t expected_bob_balance_after_order_3 = + expected_bob_balance_after_order_2 + + (5 * JILL_PRECISION) - expected_jill_order_3_fee.amount.value; + BOOST_REQUIRE_EQUAL(get_balance(bob, jillcoin), expected_bob_balance_after_order_3); + BOOST_REQUIRE_EQUAL(get_balance(bob, smartbit), 0); + + // Check the asset issuer's accumulated fees + share_type expected_smartbit_fee_after_order_3 = + expected_smartbit_fee_after_order_2 + expected_smartbit_fee.amount; + share_type expected_jill_fee_after_order_3 = expected_jill_fee_after_order_2 + expected_jill_order_3_fee.amount; + BOOST_CHECK(smartbit.dynamic_asset_data_id(db).accumulated_fees == expected_smartbit_fee_after_order_3); + BOOST_CHECK(jillcoin.dynamic_asset_data_id(db).accumulated_fees == expected_jill_fee_after_order_3); + wdump((jillcoin.dynamic_asset_data_id(db).accumulated_fees)(expected_jill_fee_after_order_3)(expected_jill_fee_after_order_2)(expected_jill_fee.amount)); + wdump((get_asset("JCOIN").dynamic_asset_data_id(db).accumulated_fees)); + + } FC_LOG_AND_RETHROW() + } + +BOOST_AUTO_TEST_SUITE_END() \ No newline at end of file diff --git a/tests/tests/swan_tests.cpp b/tests/tests/swan_tests.cpp index d5aff8b996..e99c0607fb 100644 --- a/tests/tests/swan_tests.cpp +++ b/tests/tests/swan_tests.cpp @@ -406,6 +406,30 @@ BOOST_AUTO_TEST_CASE( recollateralize ) } } +/** Creates a black swan, bid, adjust bid before/after hf_1692 + */ +BOOST_AUTO_TEST_CASE( bid_issue_1692 ) +{ try { + init_standard_swan( 700 ); + + generate_blocks( HARDFORK_CORE_1692_TIME - 30 ); + + int64_t b2_balance = get_balance( borrower2(), back() ); + bid_collateral( borrower2(), back().amount(1000), swan().amount(100) ); + BOOST_CHECK_EQUAL( get_balance( borrower2(), back() ), b2_balance - 1000 ); + GRAPHENE_REQUIRE_THROW( bid_collateral( borrower2(), back().amount(b2_balance), swan().amount(200) ), + fc::assert_exception ); + GRAPHENE_REQUIRE_THROW( bid_collateral( borrower2(), back().amount(b2_balance-999), swan().amount(200) ), + fc::assert_exception ); + + generate_blocks( HARDFORK_CORE_1692_TIME + 30 ); + + bid_collateral( borrower2(), back().amount(b2_balance-999), swan().amount(200) ); + BOOST_CHECK_EQUAL( get_balance( borrower2(), back() ), 999 ); + bid_collateral( borrower2(), back().amount(b2_balance), swan().amount(200) ); + BOOST_CHECK_EQUAL( get_balance( borrower2(), back() ), 0 ); +} FC_LOG_AND_RETHROW() } + /** Creates a black swan, settles all debts, recovers price feed - asset should be revived */ BOOST_AUTO_TEST_CASE( revive_empty_recovered ) diff --git a/tests/tests/uia_tests.cpp b/tests/tests/uia_tests.cpp index e68b55d5dd..d0429ebe0c 100644 --- a/tests/tests/uia_tests.cpp +++ b/tests/tests/uia_tests.cpp @@ -161,6 +161,9 @@ BOOST_AUTO_TEST_CASE( issue_whitelist_uia ) BOOST_CHECK(is_authorized_asset( db, nathan_id(db), uia_id(db) )); BOOST_CHECK_EQUAL(get_balance(nathan_id, uia_id), 1000); + // committee-account is free as well + BOOST_CHECK( is_authorized_asset( db, account_id_type()(db), uia_id(db) ) ); + // Make a whitelist, now it should fail { BOOST_TEST_MESSAGE( "Changing the whitelist authority" ); @@ -178,6 +181,9 @@ BOOST_AUTO_TEST_CASE( issue_whitelist_uia ) trx.operations.back() = op; GRAPHENE_REQUIRE_THROW( PUSH_TX( db, trx, ~0 ), fc::exception ); + // committee-account is blocked as well + BOOST_CHECK( !is_authorized_asset( db, account_id_type()(db), uia_id(db) ) ); + account_whitelist_operation wop; wop.authorizing_account = izzy_id; wop.account_to_list = vikram_id; @@ -204,6 +210,11 @@ BOOST_AUTO_TEST_CASE( issue_whitelist_uia ) PUSH_TX( db, trx, ~0 ); BOOST_CHECK_EQUAL(get_balance(nathan_id, uia_id), 2000); + // committee-account is still blocked + BOOST_CHECK( !is_authorized_asset( db, account_id_type()(db), uia_id(db) ) ); + // izzy is still blocked + BOOST_CHECK( !is_authorized_asset( db, izzy_id(db), uia_id(db) ) ); + } catch(fc::exception& e) { edump((e.to_detail_string())); throw; @@ -215,6 +226,7 @@ BOOST_AUTO_TEST_CASE( transfer_whitelist_uia ) try { INVOKE(issue_whitelist_uia); const asset_object& advanced = get_asset("ADVANCED"); + const asset_id_type uia_id = advanced.id; const account_object& nathan = get_account("nathan"); const account_object& dan = create_account("dan"); account_id_type izzy_id = get_account("izzy").id; @@ -336,6 +348,20 @@ BOOST_AUTO_TEST_CASE( transfer_whitelist_uia ) trx.operations.back() = burn; PUSH_TX(db, trx, ~0); BOOST_CHECK_EQUAL(get_balance(dan, advanced), 40); + + // committee-account is still blocked + BOOST_CHECK( !is_authorized_asset( db, account_id_type()(db), uia_id(db) ) ); + // izzy is still blocked + BOOST_CHECK( !is_authorized_asset( db, izzy_id(db), uia_id(db) ) ); + + // Pass BSIP 86 hardfork + generate_blocks( HARDFORK_BSIP_86_TIME ); + + // committee-account is now unblocked + BOOST_CHECK( is_authorized_asset( db, account_id_type()(db), uia_id(db) ) ); + // izzy is still blocked + BOOST_CHECK( !is_authorized_asset( db, izzy_id(db), uia_id(db) ) ); + } catch(fc::exception& e) { edump((e.to_detail_string())); throw;