From 2f0fde40ef8949cecf63d7ed0f371d2df6708244 Mon Sep 17 00:00:00 2001 From: H Date: Tue, 21 Jul 2026 10:59:38 +0700 Subject: [PATCH 1/4] limit accounts by authorizer results --- plugins/chain_api_plugin/chain.swagger.yaml | 9 ++++ plugins/chain_plugin/account_query_db.cpp | 21 +++++++--- .../eosio/chain_plugin/account_query_db.hpp | 7 +++- .../test/test_account_query_db.cpp | 42 ++++++++++++++++++- 4 files changed, 71 insertions(+), 8 deletions(-) diff --git a/plugins/chain_api_plugin/chain.swagger.yaml b/plugins/chain_api_plugin/chain.swagger.yaml index 06a02946f8..dbd018f1a6 100644 --- a/plugins/chain_api_plugin/chain.swagger.yaml +++ b/plugins/chain_api_plugin/chain.swagger.yaml @@ -871,6 +871,11 @@ paths: description: List of authorizing keys items: $ref: "https://docs.eosnetwork.com/openapi/v2.0/PublicKey.yaml" + limit: + type: integer + default: 1000 + maximum: 1000 + description: Maximum number of account permission authorities to return. The server enforces an upper bound of 1000. responses: "200": description: OK @@ -881,6 +886,7 @@ paths: description: Result containing a list of accounts which are authorized, in whole or part, by the provided accounts and keys required: - accounts + - more properties: accounts: type: array @@ -909,6 +915,9 @@ paths: threshold: type: "integer" description: the sum of weights that must be met or exceeded to satisfy the permission + more: + type: boolean + description: True when additional matching account permission authorities were omitted because the result limit was reached. /get_transaction_status: post: description: Attempts to get current blockchain state and, if available, transaction information given the transaction id. For query to work, the transaction finality status feature must be enabled by configuring the chain plugin with the config option '--transaction-finality-status-max-storage-size-gb' in nodeos. diff --git a/plugins/chain_plugin/account_query_db.cpp b/plugins/chain_plugin/account_query_db.cpp index a4b26b3e22..f28f17c9c5 100644 --- a/plugins/chain_plugin/account_query_db.cpp +++ b/plugins/chain_plugin/account_query_db.cpp @@ -13,6 +13,7 @@ #include #include +#include #include using namespace eosio; @@ -424,6 +425,7 @@ namespace eosio::chain_apis { using result_t = account_query_db::get_accounts_by_authorizers_result; result_t result; + const auto limit = std::min(args.limit, account_query_db::max_results); // deduplicate inputs auto account_set = std::set(args.accounts.begin(), args.accounts.end()); @@ -432,8 +434,9 @@ namespace eosio::chain_apis { /** * Add a range of results */ - auto push_results = [&result](const auto& begin, const auto& end) { - for (auto itr = begin; itr != end; ++itr) { + auto push_results = [&result, limit](const auto& begin, const auto& end) { + auto itr = begin; + for (; itr != end && result.accounts.size() < limit; ++itr) { const auto& pi = itr->second.get(); const auto& authorizer = itr->first.value; auto weight = itr->first.weight; @@ -447,6 +450,11 @@ namespace eosio::chain_apis { pi.threshold }); } + if (itr != end) { + result.more = true; + return false; + } + return true; }; @@ -458,13 +466,15 @@ namespace eosio::chain_apis { 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); + if (!push_results(begin, end)) + return result; } 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); + if (!push_results(begin, end)) + return result; } } @@ -472,7 +482,8 @@ namespace eosio::chain_apis { // 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); + if (!push_results(begin, end)) + return result; } return result; 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 index 2b360e28db..8f3d81a185 100644 --- a/plugins/chain_plugin/include/eosio/chain_plugin/account_query_db.hpp +++ b/plugins/chain_plugin/include/eosio/chain_plugin/account_query_db.hpp @@ -10,6 +10,7 @@ namespace eosio::chain_apis { */ class account_query_db { public: + static constexpr uint32_t max_results = 1000; /** * Instantiate a new account query DB from the given chain controller @@ -54,6 +55,7 @@ namespace eosio::chain_apis { std::vector accounts; std::vector keys; + uint32_t limit = max_results; }; /** @@ -70,6 +72,7 @@ namespace eosio::chain_apis { }; std::vector accounts; + bool more = false; }; /** * Given a set of account names and public keys, find all account permission authorities that are, in part or whole, @@ -130,6 +133,6 @@ namespace fc { } } -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_params, (accounts)(keys)(limit)) 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)) +FC_REFLECT( eosio::chain_apis::account_query_db::get_accounts_by_authorizers_result, (accounts)(more)) diff --git a/plugins/chain_plugin/test/test_account_query_db.cpp b/plugins/chain_plugin/test/test_account_query_db.cpp index e8dddfb0a2..adb8fb68c0 100644 --- a/plugins/chain_plugin/test/test_account_query_db.cpp +++ b/plugins/chain_plugin/test/test_account_query_db.cpp @@ -92,6 +92,47 @@ BOOST_FIXTURE_TEST_CASE(updateauth_test, validating_tester) { try { } FC_LOG_AND_RETHROW() } +BOOST_FIXTURE_TEST_CASE(get_accounts_by_authorizers_limit_test, validating_tester) { try { + + auto aq_db = account_query_db(*control); + + auto c = control->accepted_block.connect([&](const block_signal_params& t) { + const auto& [ block, id ] = t; + aq_db.commit_block( block ); + }); + + produce_blocks(10); + + const auto tester_account = "tester"_n; + const auto shared_key = get_public_key(tester_account, "shared"); + create_account(tester_account); + + for (const auto permission : {"first"_n, "second"_n, "third"_n}) { + const auto trace_ptr = push_action(config::system_account_name, updateauth::get_name(), tester_account, + fc::mutable_variant_object() + ("account", tester_account) + ("permission", permission) + ("parent", "active") + ("auth", authority(shared_key, 1))); + aq_db.cache_transaction_trace(trace_ptr); + produce_block(); + } + + params pars; + pars.keys.emplace_back(shared_key); + pars.limit = 2; + + const auto limited_results = aq_db.get_accounts_by_authorizers(pars); + BOOST_TEST_REQUIRE(limited_results.accounts.size() == 2u); + BOOST_TEST_REQUIRE(limited_results.more == true); + + pars.limit = 3; + const auto complete_results = aq_db.get_accounts_by_authorizers(pars); + BOOST_TEST_REQUIRE(complete_results.accounts.size() == 3u); + BOOST_TEST_REQUIRE(complete_results.more == false); + +} FC_LOG_AND_RETHROW() } + BOOST_FIXTURE_TEST_CASE(updateauth_test_multi_threaded, validating_tester) { try { // instantiate an account_query_db @@ -286,4 +327,3 @@ BOOST_AUTO_TEST_CASE(fork_test) { try { } FC_LOG_AND_RETHROW() } BOOST_AUTO_TEST_SUITE_END() - From 1c6f3190b7ec5ad243e7160cae4a7c6d3c7a7ac4 Mon Sep 17 00:00:00 2001 From: H Date: Wed, 22 Jul 2026 23:32:12 +0700 Subject: [PATCH 2/4] add cursor pagination to account authorizer query --- plugins/chain_api_plugin/chain.swagger.yaml | 10 +- plugins/chain_plugin/account_query_db.cpp | 159 +++++++++++++++--- plugins/chain_plugin/chain_plugin.cpp | 4 +- .../eosio/chain_plugin/account_query_db.hpp | 10 +- .../test/test_account_query_db.cpp | 42 ++++- 5 files changed, 198 insertions(+), 27 deletions(-) diff --git a/plugins/chain_api_plugin/chain.swagger.yaml b/plugins/chain_api_plugin/chain.swagger.yaml index dbd018f1a6..70f75557fc 100644 --- a/plugins/chain_api_plugin/chain.swagger.yaml +++ b/plugins/chain_api_plugin/chain.swagger.yaml @@ -875,7 +875,11 @@ paths: type: integer default: 1000 maximum: 1000 - description: Maximum number of account permission authorities to return. The server enforces an upper bound of 1000. + description: Maximum number of account permission authorities to return per page. The server enforces an upper bound of 1000. A value of zero returns no accounts and sets more to indicate whether any match exists. + cursor: + type: string + default: "" + description: Opaque continuation cursor returned as next_cursor by a previous request. The accounts and keys inputs must be repeated unchanged when continuing a query. responses: "200": description: OK @@ -887,6 +891,7 @@ paths: required: - accounts - more + - next_cursor properties: accounts: type: array @@ -918,6 +923,9 @@ paths: more: type: boolean description: True when additional matching account permission authorities were omitted because the result limit was reached. + next_cursor: + type: string + description: Opaque cursor to pass as cursor in the next request when more is true. Empty for a complete response and for limit zero, which does not advance the query. /get_transaction_status: post: description: Attempts to get current blockchain state and, if available, transaction information given the transaction id. For query to work, the transaction finality status feature must be enabled by configuring the chain plugin with the config option '--transaction-finality-status-max-storage-size-gb' in nodeos. diff --git a/plugins/chain_plugin/account_query_db.cpp b/plugins/chain_plugin/account_query_db.cpp index f28f17c9c5..2c2272217d 100644 --- a/plugins/chain_plugin/account_query_db.cpp +++ b/plugins/chain_plugin/account_query_db.cpp @@ -4,6 +4,9 @@ #include #include +#include +#include + #include #include #include @@ -21,6 +24,22 @@ using namespace eosio::chain::literals; using namespace boost::multi_index; using namespace boost::bimaps; +namespace eosio::chain_apis { + struct account_query_cursor { + static constexpr uint8_t current_version = 1; + + uint8_t version = current_version; + std::variant input; + std::optional last_authorizing_account; + chain::weight_type weight = 0; + chain::name owner; + chain::name permission; + }; +} + +FC_REFLECT( eosio::chain_apis::account_query_cursor, + (version)(input)(last_authorizing_account)(weight)(owner)(permission) ) + namespace { /** * Structure to hold indirect reference to a `property_object` via {owner,name} as well as a non-standard @@ -82,13 +101,16 @@ namespace { struct weighted { T value; chain::weight_type weight; + chain::name owner; + chain::name permission; static weighted lower_bound_for( const T& value ) { - return {value, std::numeric_limits::min()}; + return {value, std::numeric_limits::min(), {}, {}}; } static weighted upper_bound_for( const T& value ) { - return {value, std::numeric_limits::max()}; + const auto max_name = chain::name{std::numeric_limits::max()}; + return {value, std::numeric_limits::max(), max_name, max_name}; } }; @@ -100,6 +122,22 @@ namespace { return {}; } } + + std::string encode_cursor( const eosio::chain_apis::account_query_cursor& cursor ) { + const auto packed = fc::raw::pack(cursor); + return fc::base64url_encode(packed.data(), packed.size()); + } + + eosio::chain_apis::account_query_cursor decode_cursor( const std::string& encoded ) { + try { + auto cursor = fc::raw::unpack(fc::base64url_decode(encoded)); + EOS_ASSERT(cursor.version == eosio::chain_apis::account_query_cursor::current_version, + eosio::chain::invalid_http_request, "Unsupported cursor version"); + return cursor; + } catch (...) { + EOS_THROW(eosio::chain::invalid_http_request, "Invalid cursor"); + } + } } namespace std { @@ -119,7 +157,8 @@ namespace std { 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); + return std::tie(lhs.value, lhs.weight, lhs.owner, lhs.permission) < + std::tie(rhs.value, rhs.weight, rhs.owner, rhs.permission); } }; @@ -172,12 +211,12 @@ namespace eosio::chain_apis { 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}); + name_bimap.insert(name_bimap_t::value_type {{a.permission, a.weight, pi.owner, pi.name}, pi}); } // for each key, add this permission info's non-owning reference to the bimap for keys for (const auto& k: po.auth.keys) { - key_bimap.insert(key_bimap_t::value_type {{k.key.to_public_key(), k.weight}, pi}); + key_bimap.insert(key_bimap_t::value_type {{k.key.to_public_key(), k.weight, pi.owner, pi.name}, pi}); } } @@ -420,7 +459,8 @@ namespace eosio::chain_apis { } account_query_db::get_accounts_by_authorizers_result - get_accounts_by_authorizers( const account_query_db::get_accounts_by_authorizers_params& args) const { + get_accounts_by_authorizers( const account_query_db::get_accounts_by_authorizers_params& args, + const fc::time_point& deadline) const { std::shared_lock read_lock(rw_mutex); using result_t = account_query_db::get_accounts_by_authorizers_result; @@ -428,15 +468,62 @@ namespace eosio::chain_apis { const auto limit = std::min(args.limit, account_query_db::max_results); // 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()); + std::set account_set; + for (const auto& account : args.accounts) { + FC_CHECK_DEADLINE(deadline); + account_set.emplace(account); + } + + std::set key_set; + for (const auto& key : args.keys) { + FC_CHECK_DEADLINE(deadline); + key_set.emplace(key); + } + + std::optional cursor; + if (!args.cursor.empty()) { + cursor = decode_cursor(args.cursor); + } + + auto account_itr = account_set.begin(); + auto key_itr = key_set.begin(); + bool resume_account = false; + bool resume_key = false; + + if (cursor) { + if (std::holds_alternative(cursor->input)) { + const auto& input = std::get(cursor->input); + account_itr = account_set.find(input); + EOS_ASSERT(account_itr != account_set.end(), chain::invalid_http_request, + "Cursor account is not present in the request"); + EOS_ASSERT(cursor->last_authorizing_account.has_value(), chain::invalid_http_request, + "Invalid account cursor"); + const auto& last_authorizer = *cursor->last_authorizing_account; + EOS_ASSERT(last_authorizer.actor == input.actor && + (input.permission.empty() || last_authorizer.permission == input.permission), + chain::invalid_http_request, "Invalid account cursor"); + resume_account = true; + } else { + const auto& input = std::get(cursor->input); + key_itr = key_set.find(input); + EOS_ASSERT(key_itr != key_set.end(), chain::invalid_http_request, + "Cursor key is not present in the request"); + account_itr = account_set.end(); + resume_key = true; + } + } + + std::string last_cursor; /** * Add a range of results */ - auto push_results = [&result, limit](const auto& begin, const auto& end) { + auto push_results = [&result, &last_cursor, limit, &deadline](const auto& begin, const auto& end, + const auto& input) { + FC_CHECK_DEADLINE(deadline); auto itr = begin; for (; itr != end && result.accounts.size() < limit; ++itr) { + FC_CHECK_DEADLINE(deadline); const auto& pi = itr->second.get(); const auto& authorizer = itr->first.value; auto weight = itr->first.weight; @@ -449,40 +536,71 @@ namespace eosio::chain_apis { weight, pi.threshold }); + + account_query_cursor next; + next.input = input; + if constexpr (std::is_same_v, chain::permission_level>) { + next.last_authorizing_account = authorizer; + } + next.weight = weight; + next.owner = pi.owner; + next.permission = pi.name; + last_cursor = encode_cursor(next); } if (itr != end) { result.more = true; + result.next_cursor = last_cursor; return false; } return true; }; - - for (const auto& a: account_set) { + for (; account_itr != account_set.end(); ++account_itr) { + FC_CHECK_DEADLINE(deadline); + const auto& a = *account_itr; 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})); + 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})); - if (!push_results(begin, end)) + if (resume_account) { + const auto& last_authorizer = *cursor->last_authorizing_account; + begin = name_bimap.left.upper_bound(weighted{ + last_authorizer, cursor->weight, cursor->owner, cursor->permission}); + resume_account = false; + } + if (!push_results(begin, end, a)) return result; } 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)); + 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)); - if (!push_results(begin, end)) + if (resume_account) { + const auto& last_authorizer = *cursor->last_authorizing_account; + begin = name_bimap.left.upper_bound(weighted{ + last_authorizer, cursor->weight, cursor->owner, cursor->permission}); + resume_account = false; + } + if (!push_results(begin, end, a)) return result; } } - for (const auto& k: key_set) { + for (; key_itr != key_set.end(); ++key_itr) { + FC_CHECK_DEADLINE(deadline); + const auto& k = *key_itr; // construct a range of all possible weights for a key - const auto begin = key_bimap.left.lower_bound(weighted::lower_bound_for(k)); + 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)); - if (!push_results(begin, end)) + if (resume_key) { + begin = key_bimap.left.upper_bound(weighted{ + k, cursor->weight, cursor->owner, cursor->permission}); + resume_key = false; + } + if (!push_results(begin, end, k)) return result; } @@ -539,8 +657,9 @@ namespace eosio::chain_apis { } 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); + 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 fc::time_point& deadline) const { + return _impl->get_accounts_by_authorizers(args, deadline); } } diff --git a/plugins/chain_plugin/chain_plugin.cpp b/plugins/chain_plugin/chain_plugin.cpp index b2b713acb2..17469c0014 100644 --- a/plugins/chain_plugin/chain_plugin.cpp +++ b/plugins/chain_plugin/chain_plugin.cpp @@ -2636,10 +2636,10 @@ read_only::get_transaction_id_result read_only::get_transaction_id( const read_o 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 fc::time_point& ) const +read_only::get_accounts_by_authorizers( const account_query_db::get_accounts_by_authorizers_params& args, const fc::time_point& deadline ) const { EOS_ASSERT(aqdb.has_value(), plugin_config_exception, "Account Queries being accessed when not enabled"); - return aqdb->get_accounts_by_authorizers(args); + return aqdb->get_accounts_by_authorizers(args, deadline); } namespace detail { 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 index 8f3d81a185..daebd86485 100644 --- a/plugins/chain_plugin/include/eosio/chain_plugin/account_query_db.hpp +++ b/plugins/chain_plugin/include/eosio/chain_plugin/account_query_db.hpp @@ -56,6 +56,7 @@ namespace eosio::chain_apis { std::vector accounts; std::vector keys; uint32_t limit = max_results; + std::string cursor; }; /** @@ -73,6 +74,7 @@ namespace eosio::chain_apis { std::vector accounts; bool more = false; + std::string next_cursor; }; /** * Given a set of account names and public keys, find all account permission authorities that are, in part or whole, @@ -81,7 +83,9 @@ namespace eosio::chain_apis { * @param args * @return */ - get_accounts_by_authorizers_result get_accounts_by_authorizers( const get_accounts_by_authorizers_params& args) const; + get_accounts_by_authorizers_result get_accounts_by_authorizers( + const get_accounts_by_authorizers_params& args, + const fc::time_point& deadline = fc::time_point::maximum()) const; private: std::unique_ptr _impl; @@ -133,6 +137,6 @@ namespace fc { } } -FC_REFLECT( eosio::chain_apis::account_query_db::get_accounts_by_authorizers_params, (accounts)(keys)(limit)) +FC_REFLECT( eosio::chain_apis::account_query_db::get_accounts_by_authorizers_params, (accounts)(keys)(limit)(cursor)) 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)(more)) +FC_REFLECT( eosio::chain_apis::account_query_db::get_accounts_by_authorizers_result, (accounts)(more)(next_cursor)) diff --git a/plugins/chain_plugin/test/test_account_query_db.cpp b/plugins/chain_plugin/test/test_account_query_db.cpp index adb8fb68c0..47b7abdb70 100644 --- a/plugins/chain_plugin/test/test_account_query_db.cpp +++ b/plugins/chain_plugin/test/test_account_query_db.cpp @@ -107,7 +107,8 @@ BOOST_FIXTURE_TEST_CASE(get_accounts_by_authorizers_limit_test, validating_teste const auto shared_key = get_public_key(tester_account, "shared"); create_account(tester_account); - for (const auto permission : {"first"_n, "second"_n, "third"_n}) { + // Insert out of lexical order so the test also verifies deterministic tie-breaking. + for (const auto permission : {"third"_n, "first"_n, "second"_n}) { const auto trace_ptr = push_action(config::system_account_name, updateauth::get_name(), tester_account, fc::mutable_variant_object() ("account", tester_account) @@ -125,11 +126,50 @@ BOOST_FIXTURE_TEST_CASE(get_accounts_by_authorizers_limit_test, validating_teste const auto limited_results = aq_db.get_accounts_by_authorizers(pars); BOOST_TEST_REQUIRE(limited_results.accounts.size() == 2u); BOOST_TEST_REQUIRE(limited_results.more == true); + BOOST_TEST_REQUIRE(!limited_results.next_cursor.empty()); + BOOST_TEST_REQUIRE(limited_results.accounts[0].permission_name == "first"_n); + BOOST_TEST_REQUIRE(limited_results.accounts[1].permission_name == "second"_n); + + pars.cursor = limited_results.next_cursor; + const auto continued_results = aq_db.get_accounts_by_authorizers(pars); + BOOST_TEST_REQUIRE(continued_results.accounts.size() == 1u); + BOOST_TEST_REQUIRE(continued_results.accounts[0].permission_name == "third"_n); + BOOST_TEST_REQUIRE(continued_results.more == false); + BOOST_TEST_REQUIRE(continued_results.next_cursor.empty()); + + pars.cursor.clear(); + pars.limit = 0; + const auto existence_results = aq_db.get_accounts_by_authorizers(pars); + BOOST_TEST_REQUIRE(existence_results.accounts.empty()); + BOOST_TEST_REQUIRE(existence_results.more == true); + BOOST_TEST_REQUIRE(existence_results.next_cursor.empty()); pars.limit = 3; const auto complete_results = aq_db.get_accounts_by_authorizers(pars); BOOST_TEST_REQUIRE(complete_results.accounts.size() == 3u); BOOST_TEST_REQUIRE(complete_results.more == false); + BOOST_TEST_REQUIRE(complete_results.next_cursor.empty()); + BOOST_TEST_REQUIRE(complete_results.accounts[0].permission_name == "first"_n); + BOOST_TEST_REQUIRE(complete_results.accounts[1].permission_name == "second"_n); + BOOST_TEST_REQUIRE(complete_results.accounts[2].permission_name == "third"_n); + + // Updating a tied row removes and reinserts it, but must not change the query order. + const auto trace_ptr = push_action(config::system_account_name, updateauth::get_name(), tester_account, + fc::mutable_variant_object() + ("account", tester_account) + ("permission", "first"_n) + ("parent", "active") + ("auth", authority(shared_key, 1))); + aq_db.cache_transaction_trace(trace_ptr); + produce_block(); + + const auto reordered_results = aq_db.get_accounts_by_authorizers(pars); + BOOST_TEST_REQUIRE(reordered_results.accounts.size() == 3u); + BOOST_TEST_REQUIRE(reordered_results.accounts[0].permission_name == "first"_n); + BOOST_TEST_REQUIRE(reordered_results.accounts[1].permission_name == "second"_n); + BOOST_TEST_REQUIRE(reordered_results.accounts[2].permission_name == "third"_n); + + BOOST_CHECK_THROW(aq_db.get_accounts_by_authorizers(pars, fc::time_point::min()), fc::timeout_exception); } FC_LOG_AND_RETHROW() } From cea34a18b6fd594eeeb83cd59e48465a01ae1b5f Mon Sep 17 00:00:00 2001 From: H Date: Wed, 22 Jul 2026 23:35:34 +0700 Subject: [PATCH 3/4] fix account query test signal hookup --- plugins/chain_plugin/test/test_account_query_db.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/chain_plugin/test/test_account_query_db.cpp b/plugins/chain_plugin/test/test_account_query_db.cpp index 47b7abdb70..4054c28fc8 100644 --- a/plugins/chain_plugin/test/test_account_query_db.cpp +++ b/plugins/chain_plugin/test/test_account_query_db.cpp @@ -96,7 +96,7 @@ BOOST_FIXTURE_TEST_CASE(get_accounts_by_authorizers_limit_test, validating_teste auto aq_db = account_query_db(*control); - auto c = control->accepted_block.connect([&](const block_signal_params& t) { + auto c = control->accepted_block().connect([&](const block_signal_params& t) { const auto& [ block, id ] = t; aq_db.commit_block( block ); }); From b07c495a15bf864ad49d8f0e6e5924a98791a053 Mon Sep 17 00:00:00 2001 From: H Date: Thu, 23 Jul 2026 00:27:21 +0700 Subject: [PATCH 4/4] preserve unpaginated account query behavior --- plugins/chain_api_plugin/chain.swagger.yaml | 9 +++--- plugins/chain_plugin/account_query_db.cpp | 31 ++++++++++++------- .../eosio/chain_plugin/account_query_db.hpp | 4 +-- .../test/test_account_query_db.cpp | 10 ++++++ 4 files changed, 36 insertions(+), 18 deletions(-) diff --git a/plugins/chain_api_plugin/chain.swagger.yaml b/plugins/chain_api_plugin/chain.swagger.yaml index 70f75557fc..02f1f30825 100644 --- a/plugins/chain_api_plugin/chain.swagger.yaml +++ b/plugins/chain_api_plugin/chain.swagger.yaml @@ -873,13 +873,12 @@ paths: $ref: "https://docs.eosnetwork.com/openapi/v2.0/PublicKey.yaml" limit: type: integer - default: 1000 maximum: 1000 - description: Maximum number of account permission authorities to return per page. The server enforces an upper bound of 1000. A value of zero returns no accounts and sets more to indicate whether any match exists. + description: Maximum number of account permission authorities to return per page, capped by the server at 1000. Supplying this field enables pagination. When omitted, the server preserves legacy behavior by attempting to return the complete result and failing if the request deadline expires. A value of zero returns no accounts and sets more to indicate whether any match exists. cursor: type: string default: "" - description: Opaque continuation cursor returned as next_cursor by a previous request. The accounts and keys inputs must be repeated unchanged when continuing a query. + description: Opaque continuation cursor returned as next_cursor by a previous paginated request. Limit is required and the accounts and keys inputs must be repeated unchanged when continuing a query. responses: "200": description: OK @@ -922,10 +921,10 @@ paths: description: the sum of weights that must be met or exceeded to satisfy the permission more: type: boolean - description: True when additional matching account permission authorities were omitted because the result limit was reached. + description: True when a paginated request has additional matching account permission authorities beyond the current page. next_cursor: type: string - description: Opaque cursor to pass as cursor in the next request when more is true. Empty for a complete response and for limit zero, which does not advance the query. + description: Opaque cursor to pass as cursor with a limit in the next request when more is true. Empty for an unpaginated or complete response and for limit zero, which does not advance the query. /get_transaction_status: post: description: Attempts to get current blockchain state and, if available, transaction information given the transaction id. For query to work, the transaction finality status feature must be enabled by configuring the chain plugin with the config option '--transaction-finality-status-max-storage-size-gb' in nodeos. diff --git a/plugins/chain_plugin/account_query_db.cpp b/plugins/chain_plugin/account_query_db.cpp index 2c2272217d..1c6f78eb3d 100644 --- a/plugins/chain_plugin/account_query_db.cpp +++ b/plugins/chain_plugin/account_query_db.cpp @@ -465,7 +465,14 @@ namespace eosio::chain_apis { using result_t = account_query_db::get_accounts_by_authorizers_result; result_t result; - const auto limit = std::min(args.limit, account_query_db::max_results); + EOS_ASSERT(args.cursor.empty() || args.limit.has_value(), chain::invalid_http_request, + "A limit is required when continuing with a cursor"); + // An omitted limit preserves the legacy complete-or-timeout behavior. An explicit limit opts into + // cursor pagination and is clamped to the server's maximum page size. + const bool paginated = args.limit.has_value(); + const auto limit = args.limit + ? static_cast(std::min(*args.limit, account_query_db::max_page_results)) + : std::numeric_limits::max(); // deduplicate inputs std::set account_set; @@ -518,8 +525,8 @@ namespace eosio::chain_apis { /** * Add a range of results */ - auto push_results = [&result, &last_cursor, limit, &deadline](const auto& begin, const auto& end, - const auto& input) { + auto push_results = [&result, &last_cursor, paginated, limit, &deadline](const auto& begin, const auto& end, + const auto& input) { FC_CHECK_DEADLINE(deadline); auto itr = begin; for (; itr != end && result.accounts.size() < limit; ++itr) { @@ -537,15 +544,17 @@ namespace eosio::chain_apis { pi.threshold }); - account_query_cursor next; - next.input = input; - if constexpr (std::is_same_v, chain::permission_level>) { - next.last_authorizing_account = authorizer; + if (paginated) { + account_query_cursor next; + next.input = input; + if constexpr (std::is_same_v, chain::permission_level>) { + next.last_authorizing_account = authorizer; + } + next.weight = weight; + next.owner = pi.owner; + next.permission = pi.name; + last_cursor = encode_cursor(next); } - next.weight = weight; - next.owner = pi.owner; - next.permission = pi.name; - last_cursor = encode_cursor(next); } if (itr != end) { result.more = true; 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 index daebd86485..1586637f4d 100644 --- a/plugins/chain_plugin/include/eosio/chain_plugin/account_query_db.hpp +++ b/plugins/chain_plugin/include/eosio/chain_plugin/account_query_db.hpp @@ -10,7 +10,7 @@ namespace eosio::chain_apis { */ class account_query_db { public: - static constexpr uint32_t max_results = 1000; + static constexpr uint32_t max_page_results = 1000; /** * Instantiate a new account query DB from the given chain controller @@ -55,7 +55,7 @@ namespace eosio::chain_apis { std::vector accounts; std::vector keys; - uint32_t limit = max_results; + std::optional limit; std::string cursor; }; diff --git a/plugins/chain_plugin/test/test_account_query_db.cpp b/plugins/chain_plugin/test/test_account_query_db.cpp index 4054c28fc8..bbc933e0f5 100644 --- a/plugins/chain_plugin/test/test_account_query_db.cpp +++ b/plugins/chain_plugin/test/test_account_query_db.cpp @@ -121,6 +121,12 @@ BOOST_FIXTURE_TEST_CASE(get_accounts_by_authorizers_limit_test, validating_teste params pars; pars.keys.emplace_back(shared_key); + + const auto legacy_results = aq_db.get_accounts_by_authorizers(pars); + BOOST_TEST_REQUIRE(legacy_results.accounts.size() == 3u); + BOOST_TEST_REQUIRE(legacy_results.more == false); + BOOST_TEST_REQUIRE(legacy_results.next_cursor.empty()); + pars.limit = 2; const auto limited_results = aq_db.get_accounts_by_authorizers(pars); @@ -137,6 +143,9 @@ BOOST_FIXTURE_TEST_CASE(get_accounts_by_authorizers_limit_test, validating_teste BOOST_TEST_REQUIRE(continued_results.more == false); BOOST_TEST_REQUIRE(continued_results.next_cursor.empty()); + pars.limit.reset(); + BOOST_CHECK_THROW(aq_db.get_accounts_by_authorizers(pars), invalid_http_request); + pars.cursor.clear(); pars.limit = 0; const auto existence_results = aq_db.get_accounts_by_authorizers(pars); @@ -169,6 +178,7 @@ BOOST_FIXTURE_TEST_CASE(get_accounts_by_authorizers_limit_test, validating_teste BOOST_TEST_REQUIRE(reordered_results.accounts[1].permission_name == "second"_n); BOOST_TEST_REQUIRE(reordered_results.accounts[2].permission_name == "third"_n); + pars.limit.reset(); BOOST_CHECK_THROW(aq_db.get_accounts_by_authorizers(pars, fc::time_point::min()), fc::timeout_exception); } FC_LOG_AND_RETHROW() }