diff --git a/plugins/chain_api_plugin/chain.swagger.yaml b/plugins/chain_api_plugin/chain.swagger.yaml index f52db0cd9ee..a5bb8b04622 100644 --- a/plugins/chain_api_plugin/chain.swagger.yaml +++ b/plugins/chain_api_plugin/chain.swagger.yaml @@ -680,3 +680,63 @@ paths: more: type: integer description: "In case there's more activated protocol features than the input parameter `limit` requested, returns the ordinal of the next activated protocol feature which was not returned, otherwise zero." + /get_accounts_by_authorizers: + post: + description: Given a set of account names and public keys, find all account permission authorities that are, in part or whole, satisfiable + operationId: get_accounts_by_authorizers + requestBody: + content: + application/json: + schema: + type: object + properties: + accounts: + type: array + description: List of authorizing accounts and/or actor/permissions + items: + anyOf: + - $ref: "https://eosio.github.io/schemata/v2.0/oas/Name.yaml" + - $ref: "https://eosio.github.io/schemata/v2.0/oas/Authority.yaml" + keys: + type: array + description: List of authorizing keys + items: + $ref: "https://eosio.github.io/schemata/v2.0/oas/PublicKey.yaml" + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + description: Result containing a list of accounts which are authorized, in whole or part, by the provided accounts and keys + required: + - accounts + properties: + accounts: + type: array + description: An array of each account,permission,authorizing-data triplet in the response + items: + type: object + description: the information for a single account,permission,authorizing-data triplet + required: + - account_name + - permission_name + - authorizer + - weight + - threshold + properties: + account_name: + $ref: "https://eosio.github.io/schemata/v2.0/oas/Name.yaml" + permission_name: + $ref: "https://eosio.github.io/schemata/v2.0/oas/Name.yaml" + authorizer: + oneOf: + - $ref: "https://eosio.github.io/schemata/v2.0/oas/PublicKey.yaml" + - $ref: "https://eosio.github.io/schemata/v2.0/oas/Authority.yaml" + weight: + type: "integer" + description: the weight that this authorizer adds to satisfy the permission + threshold: + type: "integer" + description: the sum of weights that must be met or exceeded to satisfy the permission diff --git a/plugins/chain_api_plugin/chain_api_plugin.cpp b/plugins/chain_api_plugin/chain_api_plugin.cpp index bd45eb2f7ce..84c21244b42 100644 --- a/plugins/chain_api_plugin/chain_api_plugin.cpp +++ b/plugins/chain_api_plugin/chain_api_plugin.cpp @@ -31,6 +31,23 @@ struct async_result_visitor : public fc::visitor { } }; +namespace { + template + T parse_params(const std::string& body) { + if (body.empty()) { + EOS_THROW(chain::invalid_http_request, "A Request body is required"); + } + + try { + try { + return fc::json::from_string(body).as(); + } catch (const chain::chain_exception& e) { // EOS_RETHROW_EXCEPTIONS does not re-type these so, re-code it + throw fc::exception(e); + } + } EOS_RETHROW_EXCEPTIONS(chain::invalid_http_request, "Unable to parse valid input from POST body"); + } +} + #define CALL_WITH_400(api_name, api_handle, api_namespace, call_name, http_response_code, params_type) \ {std::string("/v1/" #api_name "/" #call_name), \ [api_handle](string, string body, url_response_callback cb) mutable { \ @@ -73,12 +90,17 @@ struct async_result_visitor : public fc::visitor { #define CHAIN_RO_CALL_ASYNC(call_name, call_result, http_response_code, params_type) CALL_ASYNC_WITH_400(chain, ro_api, chain_apis::read_only, call_name, call_result, http_response_code, params_type) #define CHAIN_RW_CALL_ASYNC(call_name, call_result, http_response_code, params_type) CALL_ASYNC_WITH_400(chain, rw_api, chain_apis::read_write, call_name, call_result, http_response_code, params_type) +#define CHAIN_RO_CALL_WITH_400(call_name, http_response_code, params_type) CALL_WITH_400(chain, ro_api, chain_apis::read_only, call_name, http_response_code, params_type) + + + void chain_api_plugin::plugin_startup() { ilog( "starting chain_api_plugin" ); my.reset(new chain_api_plugin_impl(app().get_plugin().chain())); - auto ro_api = app().get_plugin().get_read_only_api(); - auto rw_api = app().get_plugin().get_read_write_api(); - + auto& chain = app().get_plugin(); + auto ro_api = chain.get_read_only_api(); + auto rw_api = chain.get_read_write_api(); + auto& _http_plugin = app().get_plugin(); ro_api.set_shorten_abi_errors( !_http_plugin.verbose_errors() ); @@ -111,6 +133,12 @@ void chain_api_plugin::plugin_startup() { CHAIN_RW_CALL_ASYNC(push_transactions, chain_apis::read_write::push_transactions_results, 202, http_params_types::params_required), CHAIN_RW_CALL_ASYNC(send_transaction, chain_apis::read_write::send_transaction_results, 202, http_params_types::params_required) }); + + if (chain.account_queries_enabled()) { + _http_plugin.add_async_api({ + CHAIN_RO_CALL_WITH_400(get_accounts_by_authorizers, 200, http_params_types::params_required), + }); + } } void chain_api_plugin::plugin_shutdown() {} diff --git a/plugins/chain_plugin/CMakeLists.txt b/plugins/chain_plugin/CMakeLists.txt index e93657213c2..d118b01df87 100644 --- a/plugins/chain_plugin/CMakeLists.txt +++ b/plugins/chain_plugin/CMakeLists.txt @@ -1,5 +1,6 @@ file(GLOB HEADERS "include/eosio/chain_plugin/*.hpp") add_library( chain_plugin + account_query_db.cpp chain_plugin.cpp ${HEADERS} ) @@ -10,3 +11,5 @@ endif() target_link_libraries( chain_plugin eosio_chain appbase resource_monitor_plugin ) target_include_directories( chain_plugin PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include" "${CMAKE_CURRENT_SOURCE_DIR}/../chain_interface/include" "${CMAKE_CURRENT_SOURCE_DIR}/../../libraries/appbase/include" "${CMAKE_CURRENT_SOURCE_DIR}/../resource_monitor_plugin/include") + +add_subdirectory( test ) \ No newline at end of file diff --git a/plugins/chain_plugin/account_query_db.cpp b/plugins/chain_plugin/account_query_db.cpp new file mode 100644 index 00000000000..c455333ea45 --- /dev/null +++ b/plugins/chain_plugin/account_query_db.cpp @@ -0,0 +1,483 @@ +#include + +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +#include + +using namespace eosio; +using namespace boost::multi_index; +using namespace boost::bimaps; + +namespace { + /** + * Structure to hold indirect reference to a `property_object` via {owner,name} as well as a non-standard + * index over `last_updated` for roll-back support + */ + struct permission_info { + // indexed data + chain::name owner; + chain::name name; + fc::time_point last_updated; + + // un-indexed data + uint32_t threshold; + + using cref = std::reference_wrapper; + }; + + struct by_owner_name; + struct by_last_updated; + + /** + * Multi-index providing fast lookup for {owner,name} as well as {last_updated} + */ + using permission_info_index_t = multi_index_container< + permission_info, + indexed_by< + ordered_unique< + tag, + composite_key, + member + > + >, + ordered_non_unique< + tag, + member + > + > + >; + + /** + * Utility function to identify on-block action + * @param p + * @return + */ + bool is_onblock(const chain::transaction_trace_ptr& p) { + if (p->action_traces.empty()) + return false; + const auto& act = p->action_traces[0].act; + if (act.account != eosio::chain::config::system_account_name || act.name != N(onblock) || + act.authorization.size() != 1) + return false; + const auto& auth = act.authorization[0]; + return auth.actor == eosio::chain::config::system_account_name && + auth.permission == eosio::chain::config::active_name; + } + + template + struct weighted { + T value; + chain::weight_type weight; + + static weighted lower_bound_for( const T& value ) { + return {value, std::numeric_limits::min()}; + } + + static weighted upper_bound_for( const T& value ) { + return {value, std::numeric_limits::max()}; + } + }; + + template + auto make_optional_authorizer(const Input& authorizer) -> fc::optional { + if constexpr (std::is_same_v) { + return authorizer; + } else { + return {}; + } + } +} + +namespace std { + /** + * support for using `permission_info::cref` in ordered containers + */ + template<> + struct less { + bool operator()( const permission_info::cref& lhs, const permission_info::cref& rhs ) const { + return std::uintptr_t(&lhs.get()) < std::uintptr_t(&rhs.get()); + } + }; + + /** + * support for using `weighted` in ordered containers + */ + template + struct less> { + bool operator()( const weighted& lhs, const weighted& rhs ) const { + return std::tie(lhs.value, lhs.weight) < std::tie(rhs.value, rhs.weight); + } + }; + +} + +namespace eosio::chain_apis { + /** + * Implementation details of the account query DB + */ + struct account_query_db_impl { + account_query_db_impl(const chain::controller& controller) + :controller(controller) + {} + + /** + * Build the initial database from the chain controller by extracting the information contained in the + * blockchain state at the current HEAD + */ + void build_account_query_map() { + std::unique_lock write_lock(rw_mutex); + + ilog("Building account query DB"); + auto start = fc::time_point::now(); + const auto& index = controller.db().get_index().indices().get(); + + for (const auto& po : index ) { + const auto& pi = permission_info_index.emplace( permission_info{ po.owner, po.name, po.last_updated, po.auth.threshold } ).first; + add_to_bimaps(*pi, po); + } + auto duration = fc::time_point::now() - start; + ilog("Finished building account query DB in ${sec}", ("sec", (duration.count() / 1'000'000.0 ))); + } + + /** + * Add a permission to the bimaps for keys and accounts + * @param pi - the ephemeral permission info structure being added + * @param po - the chain data associted with this permission + */ + void add_to_bimaps( const permission_info& pi, const chain::permission_object& po ) { + // For each account, add this permission info's non-owning reference to the bimap for accounts + for (const auto& a : po.auth.accounts) { + name_bimap.insert(name_bimap_t::value_type {{a.permission, a.weight}, pi}); + } + + // for each key, add this permission info's non-owning reference to the bimap for keys + for (const auto& k: po.auth.keys) { + chain::public_key_type key = k.key; + key_bimap.insert(key_bimap_t::value_type {{std::move(key), k.weight}, pi}); + } + } + + /** + * Remove a permission from the bimaps for keys and accounts + * @param pi - the ephemeral permission info structure being removed + */ + void remove_from_bimaps( const permission_info& pi ) { + // remove all entries from the name bimap that refer to this permission_info's reference + const auto name_range = name_bimap.right.equal_range(pi); + name_bimap.right.erase(name_range.first, name_range.second); + + // remove all entries from the key bimap that refer to this permission_info's reference + const auto key_range = key_bimap.right.equal_range(pi); + key_bimap.right.erase(key_range.first, key_range.second); + } + + bool is_rollback_required( const chain::block_state_ptr& bsp ) const { + std::shared_lock read_lock(rw_mutex); + const auto t = bsp->block->timestamp.to_time_point(); + const auto& index = permission_info_index.get(); + + if (index.empty()) { + return false; + } else { + const auto& pi = (*index.rbegin()); + if (pi.last_updated < t) { + return false; + } + } + + return true; + } + + /** + * Given a time_point, remove all permissions that were last updated at or after that time_point + * this will effectively remove any updates that happened at or after that time point + * + * For each removed entry, this will create a new entry if there exists an equivalent {owner, name} permission + * at the HEAD state of the chain. + * @param bsp - the block to rollback before + */ + void rollback_to_before( const chain::block_state_ptr& bsp ) { + const auto t = bsp->block->timestamp.to_time_point(); + auto& index = permission_info_index.get(); + const auto& permission_by_owner = controller.db().get_index().indices().get(); + + while (!index.empty()) { + const auto& pi = (*index.rbegin()); + if (pi.last_updated < t) { + break; + } + + // remove this entry from the bimaps + remove_from_bimaps(pi); + + auto itr = permission_by_owner.find(std::make_tuple(pi.owner, pi.name)); + if (itr == permission_by_owner.end()) { + // this permission does not exist at this point in the chains history + index.erase(index.iterator_to(pi)); + } else { + const auto& po = *itr; + index.modify(index.iterator_to(pi), [&po](auto& mutable_pi) { + mutable_pi.last_updated = po.last_updated; + mutable_pi.threshold = po.auth.threshold; + }); + add_to_bimaps(pi, po); + } + } + } + + /** + * Store a potentially relevant transaction trace in a short lived cache so that it can be processed if its + * committed to by a block + * @param trace + */ + void cache_transaction_trace( const chain::transaction_trace_ptr& trace ) { + if( !trace->receipt ) return; + // include only executed transactions; soft_fail included so that onerror (and any inlines via onerror) are included + if((trace->receipt->status != chain::transaction_receipt_header::executed && + trace->receipt->status != chain::transaction_receipt_header::soft_fail)) { + return; + } + if( is_onblock( trace )) { + onblock_trace.emplace( trace ); + } else if( trace->failed_dtrx_trace ) { + cached_trace_map[trace->failed_dtrx_trace->id] = trace; + } else { + cached_trace_map[trace->id] = trace; + } + } + + using permission_set_t = std::set; + /** + * Pre-Commit step with const qualifier to guarantee it does not mutate + * the thread-safe data set + * @param bsp + */ + auto commit_block_prelock( const chain::block_state_ptr& bsp ) const { + permission_set_t updated; + permission_set_t deleted; + + /** + * process traces to find `updateauth`, `deleteauth` and `newaccount` calls maintaining a final set of + * permissions to either update or delete. Intra-block changes are discarded + */ + auto process_trace = [&](const chain::transaction_trace_ptr& trace) { + for( const auto& at : trace->action_traces ) { + if (std::tie(at.receiver, at.act.account) != std::tie(chain::config::system_account_name,chain::config::system_account_name)) { + continue; + } + + if (at.act.name == chain::updateauth::get_name()) { + auto data = at.act.data_as(); + auto itr = updated.emplace(chain::permission_level{data.account, data.permission}).first; + deleted.erase(*itr); + } else if (at.act.name == chain::deleteauth::get_name()) { + auto data = at.act.data_as(); + auto itr = deleted.emplace(chain::permission_level{data.account, data.permission}).first; + updated.erase(*itr); + } else if (at.act.name == chain::newaccount::get_name()) { + auto data = at.act.data_as(); + updated.emplace(chain::permission_level{data.name, N(owner)}); + updated.emplace(chain::permission_level{data.name, N(active)}); + } + } + }; + + if( onblock_trace ) + process_trace(*onblock_trace); + + for( const auto& r : bsp->block->transactions ) { + chain::transaction_id_type id; + if( r.trx.contains()) { + id = r.trx.get(); + } else { + id = r.trx.get().id(); + } + + const auto it = cached_trace_map.find( id ); + if( it != cached_trace_map.end() ) { + process_trace( it->second ); + } + } + + return std::make_tuple(std::move(updated), std::move(deleted), is_rollback_required(bsp)); + } + + /** + * Commit a block of transactions to the account query DB + * transaction traces need to be in the cache prior to this call + * @param bsp + */ + void commit_block(const chain::block_state_ptr& bsp ) { + permission_set_t updated; + permission_set_t deleted; + bool rollback_required = false; + + std::tie(updated, deleted, rollback_required) = commit_block_prelock(bsp); + + // optimistic skip of locking section if there is nothing to do + if (!updated.empty() || !deleted.empty() || rollback_required) { + std::unique_lock write_lock(rw_mutex); + + rollback_to_before(bsp); + auto& index = permission_info_index.get(); + const auto& permission_by_owner = controller.db().get_index().indices().get(); + + // for each updated permission, find the new values and update the account query db + for (const auto& up: updated) { + auto key = std::make_tuple(up.actor, up.permission); + auto source_itr = permission_by_owner.find(key); + EOS_ASSERT(source_itr != permission_by_owner.end(), chain::plugin_exception, "chain data is missing"); + auto itr = index.find(key); + if (itr == index.end()) { + const auto& po = *source_itr; + itr = index.emplace(permission_info{ po.owner, po.name, po.last_updated, po.auth.threshold }).first; + } else { + remove_from_bimaps(*itr); + index.modify(itr, [&](auto& mutable_pi){ + mutable_pi.last_updated = source_itr->last_updated; + mutable_pi.threshold = source_itr->auth.threshold; + }); + } + + add_to_bimaps(*itr, *source_itr); + } + + // for all deleted permissions, process their removal from the account query DB + for (const auto& dp: deleted) { + auto key = std::make_tuple(dp.actor, dp.permission); + auto itr = index.find(key); + if (itr != index.end()) { + remove_from_bimaps(*itr); + index.erase(itr); + } + } + } + + // drop any unprocessed cached traces + cached_trace_map.clear(); + onblock_trace.reset(); + } + + account_query_db::get_accounts_by_authorizers_result + get_accounts_by_authorizers( const account_query_db::get_accounts_by_authorizers_params& args) const { + std::shared_lock read_lock(rw_mutex); + + using result_t = account_query_db::get_accounts_by_authorizers_result; + result_t result; + + // deduplicate inputs + auto account_set = std::set(args.accounts.begin(), args.accounts.end()); + const auto key_set = std::set(args.keys.begin(), args.keys.end()); + + /** + * Add a range of results + */ + auto push_results = [&result](const auto& begin, const auto& end) { + for (auto itr = begin; itr != end; ++itr) { + const auto& pi = itr->second.get(); + const auto& authorizer = itr->first.value; + auto weight = itr->first.weight; + + result.accounts.emplace_back(result_t::account_result{ + pi.owner, + pi.name, + make_optional_authorizer(authorizer), + make_optional_authorizer(authorizer), + weight, + pi.threshold + }); + } + }; + + + for (const auto& a: account_set) { + if (a.permission.empty()) { + // empty permission is a wildcard + // construct a range between the lower bound of the given account and the lower bound of the + // next possible account name + const auto begin = name_bimap.left.lower_bound(weighted::lower_bound_for({a.actor, a.permission})); + const auto next_account_name = chain::name(a.actor.to_uint64_t() + 1); + const auto end = name_bimap.left.lower_bound(weighted::lower_bound_for({next_account_name, a.permission})); + push_results(begin, end); + } else { + // construct a range of all possible weights for an account/permission pair + const auto p = chain::permission_level{a.actor, a.permission}; + const auto begin = name_bimap.left.lower_bound(weighted::lower_bound_for(p)); + const auto end = name_bimap.left.upper_bound(weighted::upper_bound_for(p)); + push_results(begin, end); + } + } + + for (const auto& k: key_set) { + // construct a range of all possible weights for a key + const auto begin = key_bimap.left.lower_bound(weighted::lower_bound_for(k)); + const auto end = key_bimap.left.upper_bound(weighted::upper_bound_for(k)); + push_results(begin, end); + } + + return result; + } + + /** + * Convenience aliases + */ + using cached_trace_map_t = std::map; + using onblock_trace_t = std::optional; + + const chain::controller& controller; ///< the controller to read data from + cached_trace_map_t cached_trace_map; ///< temporary cache of uncommitted traces + onblock_trace_t onblock_trace; ///< temporary cache of on_block trace + + + using name_bimap_t = bimap>, multiset_of>; + using key_bimap_t = bimap>, multiset_of>; + + /* + * The structures below are shared between the writing thread and the reading thread(s) and must be protected + * by the `rw_mutex` + */ + permission_info_index_t permission_info_index; ///< multi-index that holds ephemeral indices + name_bimap_t name_bimap; ///< many:many bimap of names:permission_infos + key_bimap_t key_bimap; ///< many:many bimap of keys:permission_infos + + mutable std::shared_mutex rw_mutex; ///< mutex for read/write locking on the Multi-index and bimaps + }; + + account_query_db::account_query_db( const chain::controller& controller ) + :_impl(std::make_unique(controller)) + { + _impl->build_account_query_map(); + } + + account_query_db::~account_query_db() = default; + account_query_db & account_query_db::operator=(account_query_db &&) = default; + + void account_query_db::cache_transaction_trace( const chain::transaction_trace_ptr& trace ) { + try { + _impl->cache_transaction_trace(trace); + } FC_LOG_AND_DROP(("ACCOUNT DB cache_transaction_trace ERROR")); + } + + void account_query_db::commit_block(const chain::block_state_ptr& block ) { + try { + _impl->commit_block(block); + } FC_LOG_AND_DROP(("ACCOUNT DB commit_block ERROR")); + } + + account_query_db::get_accounts_by_authorizers_result account_query_db::get_accounts_by_authorizers( const account_query_db::get_accounts_by_authorizers_params& args) const { + return _impl->get_accounts_by_authorizers(args); + } + +} diff --git a/plugins/chain_plugin/chain_plugin.cpp b/plugins/chain_plugin/chain_plugin.cpp index cc8d319f2cd..edd2069fe3a 100644 --- a/plugins/chain_plugin/chain_plugin.cpp +++ b/plugins/chain_plugin/chain_plugin.cpp @@ -147,6 +147,7 @@ class chain_plugin_impl { flat_map loaded_checkpoints; bool accept_transactions = false; bool api_accept_transactions = true; + bool account_queries_enabled = false; std::optional fork_db; std::optional chain_config; @@ -185,6 +186,7 @@ class chain_plugin_impl { std::optional accepted_transaction_connection; std::optional applied_transaction_connection; + fc::optional _account_query_db; }; chain_plugin::chain_plugin() @@ -331,6 +333,7 @@ void chain_plugin::set_program_options(options_description& cli, options_descrip }), "Number of threads to use for EOS VM OC tier-up") ("eos-vm-oc-enable", bpo::bool_switch(), "Enable EOS VM OC tier-up runtime") #endif + ("enable-account-queries", bpo::value()->default_value(false), "enable queries to find accounts by various metadata.") ("max-nonprivileged-inline-action-size", bpo::value()->default_value(config::default_max_nonprivileged_inline_action_size), "maximum allowed size (in bytes) of an inline action for a nonprivileged account") ; @@ -1110,6 +1113,8 @@ void chain_plugin::plugin_initialize(const variables_map& options) { my->chain_config->eosvmoc_tierup = true; #endif + my->account_queries_enabled = options.at("enable-account-queries").as(); + my->chain.emplace( *my->chain_config, std::move(pfs), *chain_id ); // initialize deep mind logging @@ -1187,6 +1192,10 @@ void chain_plugin::plugin_initialize(const variables_map& options) { ("blk", blk) ); } + + if (my->_account_query_db) { + my->_account_query_db->commit_block(blk); + } my->accepted_block_channel.publish( priority::high, blk ); } ); @@ -1209,6 +1218,10 @@ void chain_plugin::plugin_initialize(const variables_map& options) { ); } + if (my->_account_query_db) { + my->_account_query_db->cache_transaction_trace(std::get<0>(t)); + } + my->applied_transaction_channel.publish( priority::low, std::get<0>(t) ); } ); @@ -1256,6 +1269,17 @@ void chain_plugin::plugin_startup() } my->chain_config.reset(); + + if (my->account_queries_enabled) { + my->account_queries_enabled = false; + try { + my->_account_query_db.emplace(*my->chain); + my->account_queries_enabled = true; + } FC_LOG_AND_DROP(("Unable to enable account queries")); + } + + + } FC_CAPTURE_AND_RETHROW() } void chain_plugin::plugin_shutdown() { @@ -1286,6 +1310,11 @@ void chain_apis::read_write::validate() const { "Not allowed, node has api-accept-transactions = false" ); } +chain_apis::read_only chain_plugin::get_read_only_api() const { + return chain_apis::read_only(chain(), my->_account_query_db, get_abi_serializer_max_time()); +} + + bool chain_plugin::accept_block(const signed_block_ptr& block, const block_id_type& id ) { return my->incoming_block_sync_method(block, id); } @@ -1560,6 +1589,11 @@ void chain_plugin::handle_bad_alloc() { //return -2 -- it's what programs/nodeos/main.cpp reports for std::exception std::_Exit(-2); } + +bool chain_plugin::account_queries_enabled() const { + return my->account_queries_enabled; +} + namespace chain_apis { @@ -2705,6 +2739,12 @@ read_only::get_transaction_id_result read_only::get_transaction_id( const read_o return params.id(); } +account_query_db::get_accounts_by_authorizers_result read_only::get_accounts_by_authorizers( const account_query_db::get_accounts_by_authorizers_params& args) const +{ + EOS_ASSERT(aqdb.valid(), plugin_config_exception, "Account Queries being accessed when not enabled"); + return aqdb->get_accounts_by_authorizers(args); +} + namespace detail { struct ram_market_exchange_state_t { asset ignore1; diff --git a/plugins/chain_plugin/include/eosio/chain_plugin/account_query_db.hpp b/plugins/chain_plugin/include/eosio/chain_plugin/account_query_db.hpp new file mode 100644 index 00000000000..3fc7e5df0cf --- /dev/null +++ b/plugins/chain_plugin/include/eosio/chain_plugin/account_query_db.hpp @@ -0,0 +1,136 @@ +#pragma once +#include +#include +#include + +namespace eosio::chain_apis { + /** + * This class manages the ephemeral indices and data that provide the `get_accounts_by_authorizers` RPC call + * There is no persistence and the indices/caches are recreated when the class is instantiated based on the + * current state of the chain. + */ + class account_query_db { + public: + + /** + * Instantiate a new account query DB from the given chain controller + * The caller is expected to manage lifetimes such that this controller reference does not go stale + * for the life of the account query DB + * @param chain - controller to read data from + */ + account_query_db( const class eosio::chain::controller& chain ); + ~account_query_db(); + + /** + * Allow moving the account query DB (including by assignment) + */ + account_query_db(account_query_db&&); + account_query_db& operator=(account_query_db&&); + + /** + * Add a transaction trace to the account query DB that has been applied to the contoller even though it may + * not yet be committed to by a block. + * + * @param trace + */ + void cache_transaction_trace( const chain::transaction_trace_ptr& trace ); + + /** + * Add a block to the account query DB, committing all the cached traces it represents and dumping any + * uncommitted traces. + * @param block + */ + void commit_block(const chain::block_state_ptr& block ); + + /** + * parameters for the get_accounts_by_authorizers RPC + */ + struct get_accounts_by_authorizers_params{ + /** + * This structure is an concrete alias of `chain::permission_level` to facilitate + * specialized rules when transforming to/from variants. + */ + struct permission_level : public chain::permission_level { + }; + + std::vector accounts; + std::vector keys; + }; + + /** + * Result of the get_accounts_by_authorizers RPC + */ + struct get_accounts_by_authorizers_result{ + struct account_result { + chain::name account_name; + chain::name permission_name; + fc::optional authorizing_account; + fc::optional authorizing_key; + chain::weight_type weight; + uint32_t threshold; + }; + + std::vector accounts; + }; + /** + * Given a set of account names and public keys, find all account permission authorities that are, in part or whole, + * satisfiable. + * + * @param args + * @return + */ + get_accounts_by_authorizers_result get_accounts_by_authorizers( const get_accounts_by_authorizers_params& args) const; + + private: + std::unique_ptr _impl; + }; + +} + +namespace fc { + using params = eosio::chain_apis::account_query_db::get_accounts_by_authorizers_params; + /** + * Overloaded to_variant so that permission is only present if it is set + * @param a + * @param v + */ + inline void to_variant(const params::permission_level& a, fc::variant& v) { + if (a.permission.empty()) { + v = a.actor.to_string(); + } else { + v = mutable_variant_object() + ("actor", a.actor.to_string()) + ("permission", a.permission.to_string()); + } + } + + /** + * Overloaded from_variant to allow parsing an account with a permission wildcard (empty) from a string + * instead of an object + * @param v + * @param a + */ + inline void from_variant(const fc::variant& v, params::permission_level& a) { + if (v.is_string()) { + from_variant(v, a.actor); + a.permission = {}; + } else if (v.is_object()) { + const auto& vo = v.get_object(); + if(vo.contains("actor")) + from_variant(vo["actor"], a.actor); + else + EOS_THROW(eosio::chain::invalid_http_request, "Missing Actor field"); + + if(vo.contains("permission") && vo.size() == 2) + from_variant(vo["permission"], a.permission); + else if (vo.size() == 1) + a.permission = {}; + else + EOS_THROW(eosio::chain::invalid_http_request, "Unrecognized fields in account"); + } + } +} + +FC_REFLECT( eosio::chain_apis::account_query_db::get_accounts_by_authorizers_params, (accounts)(keys)) +FC_REFLECT( eosio::chain_apis::account_query_db::get_accounts_by_authorizers_result::account_result, (account_name)(permission_name)(authorizing_account)(authorizing_key)(weight)(threshold)) +FC_REFLECT( eosio::chain_apis::account_query_db::get_accounts_by_authorizers_result, (accounts)) diff --git a/plugins/chain_plugin/include/eosio/chain_plugin/chain_plugin.hpp b/plugins/chain_plugin/include/eosio/chain_plugin/chain_plugin.hpp index 485d7c2cc4e..259bd3f5c2b 100644 --- a/plugins/chain_plugin/include/eosio/chain_plugin/chain_plugin.hpp +++ b/plugins/chain_plugin/include/eosio/chain_plugin/chain_plugin.hpp @@ -16,6 +16,8 @@ #include #include +#include + #include namespace fc { class variant; } @@ -77,15 +79,16 @@ string convert_to_string(const float128_t& source, const string& key_type, const class read_only { const controller& db; + const fc::optional& aqdb; const fc::microseconds abi_serializer_max_time; bool shorten_abi_errors = true; public: static const string KEYi64; - read_only(const controller& db, const fc::microseconds& abi_serializer_max_time) - : db(db), abi_serializer_max_time(abi_serializer_max_time) {} - + read_only(const controller& db, const fc::optional& aqdb, const fc::microseconds& abi_serializer_max_time) + : db(db), aqdb(aqdb), abi_serializer_max_time(abi_serializer_max_time) {} + void validate() const {} void set_shorten_abi_errors( bool f ) { shorten_abi_errors = f; } @@ -604,6 +607,10 @@ class read_only { return result; } + using get_accounts_by_authorizers_result = account_query_db::get_accounts_by_authorizers_result; + using get_accounts_by_authorizers_params = account_query_db::get_accounts_by_authorizers_params; + get_accounts_by_authorizers_result get_accounts_by_authorizers( const get_accounts_by_authorizers_params& args) const; + chain::symbol extract_core_symbol()const; friend struct resolver_factory; @@ -725,9 +732,9 @@ class chain_plugin : public plugin { void plugin_shutdown(); void handle_sighup() override; - chain_apis::read_only get_read_only_api() const { return chain_apis::read_only(chain(), get_abi_serializer_max_time()); } chain_apis::read_write get_read_write_api() { return chain_apis::read_write(chain(), get_abi_serializer_max_time(), api_accept_transactions()); } - + chain_apis::read_only get_read_only_api() const; + bool accept_block( const chain::signed_block_ptr& block, const chain::block_id_type& id ); void accept_transaction(const chain::packed_transaction_ptr& trx, chain::plugin_interface::next_function next); @@ -763,6 +770,8 @@ class chain_plugin : public plugin { static void handle_db_exhaustion(); static void handle_bad_alloc(); + + bool account_queries_enabled() const; private: static void log_guard_exception(const chain::guard_exception& e); diff --git a/plugins/chain_plugin/test/CMakeLists.txt b/plugins/chain_plugin/test/CMakeLists.txt new file mode 100644 index 00000000000..5c7c86dddd8 --- /dev/null +++ b/plugins/chain_plugin/test/CMakeLists.txt @@ -0,0 +1,5 @@ +add_executable( test_account_query_db test_account_query_db.cpp ) + +target_link_libraries( test_account_query_db chain_plugin eosio_testing) + +add_test(NAME test_account_query_db COMMAND plugins/chain_plugin/test/test_account_query_db WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) diff --git a/plugins/chain_plugin/test/test_account_query_db.cpp b/plugins/chain_plugin/test/test_account_query_db.cpp new file mode 100644 index 00000000000..36cceb67647 --- /dev/null +++ b/plugins/chain_plugin/test/test_account_query_db.cpp @@ -0,0 +1,101 @@ +#define BOOST_TEST_MODULE account_query_db +#include +#include +#include +#include +#include +#include + +#ifdef NON_VALIDATING_TEST +#define TESTER tester +#else +#define TESTER validating_tester +#endif + +using namespace eosio; +using namespace eosio::chain; +using namespace eosio::testing; +using namespace eosio::chain_apis; + +using params = account_query_db::get_accounts_by_authorizers_params; +using results = account_query_db::get_accounts_by_authorizers_result; + +bool find_account_name(results rst, account_name name){ + for (const auto acc : rst.accounts){ + if (acc.account_name == name){ + return true; + } + } + return false; +} +bool find_account_auth(results rst, account_name name, permission_name perm){ + for (const auto acc : rst.accounts){ + if (acc.account_name == name && acc.permission_name == perm) + return true; + } + return false; +} + +BOOST_AUTO_TEST_SUITE(account_query_db_tests) + +BOOST_FIXTURE_TEST_CASE(newaccount_test, TESTER) { try { + + // instantiate an account_query_db + auto aq_db = account_query_db(*control); + + //link aq_db to the `accepted_block` signal on the controller + auto c2 = control->accepted_block.connect([&](const block_state_ptr& blk) { + aq_db.commit_block( blk); + }); + + produce_blocks(10); + + account_name tester_account = N(tester); + const auto trace_ptr = create_account(tester_account); + aq_db.cache_transaction_trace(trace_ptr); + produce_block(); + + params pars; + pars.keys.emplace_back(get_public_key(tester_account, "owner")); + const auto results = aq_db.get_accounts_by_authorizers(pars); + + BOOST_TEST_REQUIRE(find_account_name(results, tester_account) == true); + +} FC_LOG_AND_RETHROW() } + +BOOST_FIXTURE_TEST_CASE(updateauth_test, TESTER) { try { + + // instantiate an account_query_db + auto aq_db = account_query_db(*control); + + //link aq_db to the `accepted_block` signal on the controller + auto c = control->accepted_block.connect([&](const block_state_ptr& blk) { + aq_db.commit_block( blk); + }); + + produce_blocks(10); + + const auto& tester_account = N(tester); + const string role = "first"; + produce_block(); + create_account(tester_account); + + const auto trace_ptr = push_action(config::system_account_name, updateauth::get_name(), tester_account, fc::mutable_variant_object() + ("account", tester_account) + ("permission", N(role)) + ("parent", "active") + ("auth", authority(get_public_key(tester_account, role), 5)) + ); + aq_db.cache_transaction_trace(trace_ptr); + produce_block(); + + params pars; + pars.keys.emplace_back(get_public_key(tester_account, role)); + const auto results = aq_db.get_accounts_by_authorizers(pars); + + BOOST_TEST_REQUIRE(find_account_auth(results, tester_account, N(role)) == true); + +} FC_LOG_AND_RETHROW() } + +BOOST_AUTO_TEST_SUITE_END() + diff --git a/tests/chain_plugin_tests.cpp b/tests/chain_plugin_tests.cpp index a0bfded340d..01790f6afce 100644 --- a/tests/chain_plugin_tests.cpp +++ b/tests/chain_plugin_tests.cpp @@ -89,7 +89,8 @@ BOOST_FIXTURE_TEST_CASE( get_block_with_invalid_abi, TESTER ) try { char headnumstr[20]; sprintf(headnumstr, "%d", headnum); chain_apis::read_only::get_block_params param{headnumstr}; - chain_apis::read_only plugin(*(this->control), fc::microseconds::maximum()); + chain_apis::read_only plugin(*(this->control), {}, fc::microseconds::maximum()); + // block should be decoded successfully std::string block_str = json::to_pretty_string(plugin.get_block(param)); diff --git a/tests/get_table_tests.cpp b/tests/get_table_tests.cpp index bbfab2e095e..f3eaa720d78 100644 --- a/tests/get_table_tests.cpp +++ b/tests/get_table_tests.cpp @@ -89,7 +89,7 @@ BOOST_FIXTURE_TEST_CASE( get_scope_test, TESTER ) try { produce_blocks(1); // iterate over scope - eosio::chain_apis::read_only plugin(*(this->control), fc::microseconds::maximum()); + eosio::chain_apis::read_only plugin(*(this->control), {}, fc::microseconds::maximum()); eosio::chain_apis::read_only::get_table_by_scope_params param{N(eosio.token), N(accounts), "inita", "", 10}; eosio::chain_apis::read_only::get_table_by_scope_result result = plugin.read_only::get_table_by_scope(param); @@ -194,7 +194,7 @@ BOOST_FIXTURE_TEST_CASE( get_table_test, TESTER ) try { produce_blocks(1); // get table: normal case - eosio::chain_apis::read_only plugin(*(this->control), fc::microseconds::maximum()); + eosio::chain_apis::read_only plugin(*(this->control), {}, fc::microseconds::maximum()); eosio::chain_apis::read_only::get_table_rows_params p; p.code = N(eosio.token); p.scope = "inita"; @@ -363,7 +363,7 @@ BOOST_FIXTURE_TEST_CASE( get_table_by_seckey_test, TESTER ) try { produce_blocks(1); // get table: normal case - eosio::chain_apis::read_only plugin(*(this->control), fc::microseconds::maximum()); + eosio::chain_apis::read_only plugin(*(this->control), {}, fc::microseconds::maximum()); eosio::chain_apis::read_only::get_table_rows_params p; p.code = N(eosio); p.scope = "eosio"; @@ -515,7 +515,7 @@ BOOST_FIXTURE_TEST_CASE( get_table_next_key_test, TESTER ) try { // } - chain_apis::read_only plugin(*(this->control), fc::microseconds::maximum()); + chain_apis::read_only plugin(*(this->control), {}, fc::microseconds::maximum()); chain_apis::read_only::get_table_rows_params params = []{ chain_apis::read_only::get_table_rows_params params{}; params.json=true; diff --git a/tests/validate-reflection.py b/tests/validate-reflection.py index 20822a19f3d..263eda54ce7 100755 --- a/tests/validate-reflection.py +++ b/tests/validate-reflection.py @@ -267,7 +267,7 @@ def create_scope(type, name, inherit, start, content, parent_scope): class ClassStruct(EmptyScope): field_pattern = re.compile(r'\n\s*?(?:mutable\s+)?(%s\w[\w:]*(?:\s*<\s*%s\w[\w:\s.,]*\s*(?:\s*<\s*%s\w[\w:]*\s*(?:\s*<\s*%s\w[\w:\s.,]*\s*(?:,\s*%s\w[\w:\s.,]*\s*)*>\s*)?(?:,\s*%s\w[\w:]*\s*(?:\s*<\s*%s\w[\w:\s.,]*\s*(?:,\s*%s\w[\w:\s.,]*\s*)*>\s*)?)?>\s*)?(?:,\s*%s\w[\w:]*\s*(?:\s*<\s*%s\w[\w:\s.,]*\s*(?:\s*<\s*%s\w[\w:]*\s*(?:,\s*%s\w[\w:\s.,]*\s*)*>\s*)?(?:,\s*%s\w[\w:]*\s*(?:\s*<\s*%s\w[\w:\s.,]*\s*(?:,\s*%s\w[\w:\s.,]*\s*)*>\s*)?)?>\s*)?)?>\s*)?)(?:\*\s+|\s+\*|\s+)(\w+)\s*(?:;|=\s*[-]?\w[\w:]*(?:\s*[-/\*\+]\s*[-]?\w[\w:]*)*\s*;|=\s*(?:\w[\w:]*(?:<[^\n;]>)?)?(?:{|(?:\([^\)]*\)?|(?:\"[^\"]*\")?)\s*;)|\s*{[^\}]*}\s*;)' % (EmptyScope.multi_word_type_pattern, EmptyScope.multi_word_type_pattern, EmptyScope.multi_word_type_pattern, EmptyScope.multi_word_type_pattern, EmptyScope.multi_word_type_pattern, EmptyScope.multi_word_type_pattern, EmptyScope.multi_word_type_pattern, EmptyScope.multi_word_type_pattern, EmptyScope.multi_word_type_pattern, EmptyScope.multi_word_type_pattern, EmptyScope.multi_word_type_pattern, EmptyScope.multi_word_type_pattern, EmptyScope.multi_word_type_pattern, EmptyScope.multi_word_type_pattern, EmptyScope.multi_word_type_pattern), re.MULTILINE | re.DOTALL) enum_field_pattern = re.compile(r'[,\{]\s*?(\w+)\s*(?:=\s*[^,}\s]+)?\s*(?:,|})', re.MULTILINE | re.DOTALL) - class_pattern = re.compile(r'(%s|%s|%s(?:\s+class)?)\s+(\w+)(?:\s+final)?\s*(:\s*(public\s+)?([^<\s]+)[^{]*)?\s*\{' % (EmptyScope.struct_str, EmptyScope.class_str, EmptyScope.enum_str), re.MULTILINE | re.DOTALL) + class_pattern = re.compile(r'(%s|(?)?;')