diff --git a/libraries/chain/controller.cpp b/libraries/chain/controller.cpp index e720bf5ad5..b6eb1c0eae 100644 --- a/libraries/chain/controller.cpp +++ b/libraries/chain/controller.cpp @@ -1274,6 +1274,57 @@ struct controller_impl { apply_handlers[receiver][make_pair(contract,action)] = v; } + void set_trx_expiration(signed_transaction& trx) { + if (is_builtin_activated(builtin_protocol_feature_t::no_duplicate_deferred_id)) { + trx.expiration = time_point_sec(); + trx.ref_block_num = 0; + trx.ref_block_prefix = 0; + } else { + trx.expiration = time_point_sec{ + pending_block_time() + fc::microseconds(999'999)}; // Round up to nearest second to avoid appearing expired + trx.set_reference_block(chain_head.id()); + } + } + + getpeerkeys_res_t get_top_producer_keys(fc::time_point deadline) { + try { + auto get_getpeerkeys_transaction = [&]() { + auto perms = vector{}; + action act(perms, config::system_account_name, "getpeerkeys"_n, {}); + signed_transaction trx; + + trx.actions.emplace_back(std::move(act)); + set_trx_expiration(trx); + return trx; + }; + + auto metadata = transaction_metadata::create_no_recover_keys( + std::make_shared(get_getpeerkeys_transaction()), + transaction_metadata::trx_type::read_only); + + // allow a max of 20ms for getpeerkeys + auto trace = push_transaction(metadata, deadline, fc::milliseconds(20), 0, false, 0); + + if( trace->except_ptr ) + std::rethrow_exception(trace->except_ptr); + if( trace->except) + throw *trace->except; + getpeerkeys_res_t res; + if (!trace->action_traces.empty()) { + const auto& act_trace = trace->action_traces[0]; + const auto& retval = act_trace.return_value; + if (!retval.empty()) { + // in some tests, the system contract is not set and the return value is empty. + fc::datastream ds(retval.data(), retval.size()); + fc::raw::unpack(ds, res); + } + } + + return res; + } + FC_LOG_AND_RETHROW() + } + controller_impl( const controller::config& cfg, controller& s, protocol_feature_set&& pfs, const chain_id_type& chain_id ) :rnh(), self(s), @@ -1322,9 +1373,6 @@ struct controller_impl { const auto& [ block, id] = t; wasmif.current_lib(block->block_num()); vote_processor.notify_lib(block->block_num()); - - // update peer public keys from chainbase db - peer_keys_db.update_peer_keys(self, block->block_num()); }); #define SET_APP_HANDLER( receiver, contract, action) \ @@ -2623,14 +2671,7 @@ struct controller_impl { // Deliver onerror action containing the failed deferred transaction directly back to the sender. etrx.actions.emplace_back( vector{{gtrx.sender, config::active_name}}, onerror( gtrx.sender_id, gtrx.packed_trx.data(), gtrx.packed_trx.size() ) ); - if( is_builtin_activated( builtin_protocol_feature_t::no_duplicate_deferred_id ) ) { - etrx.expiration = time_point_sec(); - etrx.ref_block_num = 0; - etrx.ref_block_prefix = 0; - } else { - etrx.expiration = time_point_sec{pending_block_time() + fc::microseconds(999'999)}; // Round up to nearest second to avoid appearing expired - etrx.set_reference_block( chain_head.id() ); - } + set_trx_expiration(etrx); auto& bb = std::get(pending->_block_stage); @@ -3339,9 +3380,20 @@ struct controller_impl { } guard_pending.cancel(); + return onblock_trace; } /// start_block + void update_peer_keys(fc::time_point deadline) { + try { + // update peer public keys from chainbase db using a readonly trx + auto block_num = chain_head.block_num(); + if (block_num % 120 == 0) { // update once/minute + peer_keys_db.update_peer_keys(get_top_producer_keys(deadline)); + } + } FC_LOG_AND_DROP() + } + void assemble_block(bool validating, std::optional validating_qc_data, const block_state_ptr& validating_bsp) { EOS_ASSERT( pending, block_validate_exception, "it is not valid to finalize when there is no pending block"); @@ -4554,12 +4606,13 @@ struct controller_impl { return applied_trxs; } - void interrupt_apply_block_transaction() { - // Only interrupt transaction if applying a block. Speculative trxs already have a deadline set so they - // have limited run time already. This is to allow killing a long-running transaction in a block being - // validated. - if (!replaying && applying_block) { - ilog("Interrupting apply block"); + void interrupt_transaction() { + // Do not interrupt during replay. ctrl-c during replay is handled at block boundaries. + // Interrupt both speculative trxs and trxs while applying a block. + // This is to allow killing a long-running transaction in a block being validated during apply block. + // This also allows killing a trx when a block is received to prioritize block validation. + if (!replaying) { + dlog("Interrupting trx..."); main_thread_timer.interrupt_timer(); } } @@ -4788,14 +4841,7 @@ struct controller_impl { signed_transaction trx; trx.actions.emplace_back(std::move(on_block_act)); - if( is_builtin_activated( builtin_protocol_feature_t::no_duplicate_deferred_id ) ) { - trx.expiration = time_point_sec(); - trx.ref_block_num = 0; - trx.ref_block_prefix = 0; - } else { - trx.expiration = time_point_sec{pending_block_time() + fc::microseconds(999'999)}; // Round up to nearest second to avoid appearing expired - trx.set_reference_block( chain_head.id() ); - } + set_trx_expiration(trx); return trx; } @@ -5293,6 +5339,10 @@ transaction_trace_ptr controller::start_block( block_timestamp_type when, bs, std::optional(), deadline ); } +void controller::update_peer_keys(fc::time_point deadline) { + my->update_peer_keys(deadline); +} + void controller::assemble_and_complete_block( const signer_callback_type& signer_callback ) { validate_db_available_size(); @@ -5338,8 +5388,8 @@ deque controller::abort_block() { return my->abort_block(); } -void controller::interrupt_apply_block_transaction() { - my->interrupt_apply_block_transaction(); +void controller::interrupt_transaction() { + my->interrupt_transaction(); } boost::asio::io_context& controller::get_thread_pool() { @@ -5793,8 +5843,12 @@ void controller::set_peer_keys_retrieval_active(bool active) { my->peer_keys_db.set_active(active); } -std::optional controller::get_peer_key(name n) const { - return my->peer_keys_db.get_peer_key(n); +peer_info_t controller::get_peer_info(name n) const { + return my->peer_keys_db.get_peer_info(n); +} + +getpeerkeys_res_t controller::get_top_producer_keys(fc::time_point deadline) { + return my->get_top_producer_keys(deadline); } db_read_mode controller::get_read_mode()const { diff --git a/libraries/chain/include/eosio/chain/controller.hpp b/libraries/chain/include/eosio/chain/controller.hpp index 56dc02f13b..ae2cc15d85 100644 --- a/libraries/chain/include/eosio/chain/controller.hpp +++ b/libraries/chain/include/eosio/chain/controller.hpp @@ -80,6 +80,23 @@ namespace eosio::chain { class resource_limits_manager; }; + // vector, sorted by rank, of the top-50 producers by `total_votes` (whether + // active or not) and their peer key if populated on-chain. + // ------------------------------------------------------------------------- + struct peerkeys_t { + name producer_name; + std::optional peer_key; + }; + using getpeerkeys_res_t = std::vector; + + struct peer_info_t { + // rank by `total_votes` of all producers, active or not, may not match schedule rank + uint32_t rank{std::numeric_limits::max()}; + std::optional key; + + bool operator==(const peer_info_t&) const = default; + }; + struct controller_impl; using chainbase::database; using chainbase::pinnable_mapped_file; @@ -209,7 +226,7 @@ namespace eosio::chain { deque abort_block(); /// Expected to be called from signal handler, or producer_plugin - void interrupt_apply_block_transaction(); + void interrupt_transaction(); /** * @@ -230,6 +247,7 @@ namespace eosio::chain { void assemble_and_complete_block( const signer_callback_type& signer_callback ); void sign_block( const signer_callback_type& signer_callback ); void commit_block(); + void update_peer_keys(fc::time_point deadline); void testing_allow_voting(bool val); bool get_testing_allow_voting_flag(); void set_async_voting(async_t val); @@ -428,7 +446,8 @@ namespace eosio::chain { chain_id_type get_chain_id()const; void set_peer_keys_retrieval_active(bool active); - std::optional get_peer_key(name n) const; // thread safe + peer_info_t get_peer_info(name n) const; // thread safe + getpeerkeys_res_t get_top_producer_keys(fc::time_point deadline); // must be called from main thread // thread safe db_read_mode get_read_mode()const; @@ -518,3 +537,5 @@ namespace eosio::chain { }; // controller } /// eosio::chain + +FC_REFLECT(eosio::chain::peerkeys_t, (producer_name)(peer_key)) \ No newline at end of file diff --git a/libraries/chain/include/eosio/chain/peer_keys_db.hpp b/libraries/chain/include/eosio/chain/peer_keys_db.hpp index 4e80312b7b..a567ed0296 100644 --- a/libraries/chain/include/eosio/chain/peer_keys_db.hpp +++ b/libraries/chain/include/eosio/chain/peer_keys_db.hpp @@ -13,34 +13,34 @@ namespace eosio::chain { */ class peer_keys_db_t { public: - struct v0_data { // must match the one in eosio.system.hpp - std::optional pubkey; - }; - - using peer_key_map_t = boost::unordered_flat_map>; + using peer_key_map_t = boost::unordered_flat_map>; + using new_peers_t = flat_set; peer_keys_db_t(); void set_active(bool b) { _active = b; } // must be called from main thread - size_t update_peer_keys(const controller& chain, uint32_t lib_number); - - // safe to be called from any thread - std::optional get_peer_key(name n) const; + // return the new peers either: + // - added to the top selected producers (according to "getpeerkeys"_n in system contracts) + // - removed from the top selected producers + // - whose key changed + // since the last call to update_peer_keys + // --------------------------------------- + new_peers_t update_peer_keys(const getpeerkeys_res_t& v); // safe to be called from any thread - size_t size() const; + // peers no longer in top selected producers will have a rank of std::numeric_limits::max() + // ---------------------------------------------------------------------------------- + peer_info_t get_peer_info(name n) const; private: std::optional _get_version(const chainbase::database& db); bool _active = false; // if not active (the default), no update occurs - uint32_t _block_num = 0; // below map includes keys registered up to _block_num (inclusive) mutable fc::mutex _m; - peer_key_map_t _peer_key_map GUARDED_BY(_m); + peer_key_map_t _peer_info_map GUARDED_BY(_m); }; } // namespace eosio::chain -FC_REFLECT(eosio::chain::peer_keys_db_t::v0_data, (pubkey)) diff --git a/libraries/chain/peer_keys_db.cpp b/libraries/chain/peer_keys_db.cpp index ecd0e40e4d..1b2e9acc7d 100644 --- a/libraries/chain/peer_keys_db.cpp +++ b/libraries/chain/peer_keys_db.cpp @@ -5,85 +5,48 @@ namespace eosio::chain { peer_keys_db_t::peer_keys_db_t() : _active(false) {} -std::optional peer_keys_db_t::get_peer_key(name n) const { +peer_info_t peer_keys_db_t::get_peer_info(name n) const { fc::lock_guard g(_m); - if (auto it = _peer_key_map.find(n); it != _peer_key_map.end()) - return std::optional(it->second); - return std::optional{}; + assert(_active); + if (auto it = _peer_info_map.find(n); it != _peer_info_map.end()) + return it->second; + return peer_info_t{}; } -size_t peer_keys_db_t::size() const { - fc::lock_guard g(_m); - return _peer_key_map.size(); -} - -// we update the keys that were registered up to lib_number (inclusive) -// -------------------------------------------------------------------- -size_t peer_keys_db_t::update_peer_keys(const controller& chain, uint32_t lib_number) { - size_t num_updated = 0; - if (!_active || lib_number <= _block_num) - return num_updated; // nothing to do - - try { - const auto& db = chain.db(); - const auto table_ref = boost::make_tuple("eosio"_n, "eosio"_n, "peerkeys"_n); - const auto* t_id = db.find(table_ref); - EOS_ASSERT(t_id != nullptr, misc_exception, "cannot retrieve `peerkeys` table"); +peer_keys_db_t::new_peers_t peer_keys_db_t::update_peer_keys(const getpeerkeys_res_t& v) { + if (!_active || v.empty()) + return {}; + + // create hash_map of current top selected producers (according to "getpeerkeys"_n in system contracts) + // ---------------------------------------------------------------------------------------------------- + peer_key_map_t current; + for (size_t i=0; i(i), v[i].peer_key}; - const auto& secidx = db.get_index(); - - const auto lower = secidx.lower_bound(std::make_tuple(t_id->id._id, static_cast(_block_num + 1))); - const auto upper = secidx.upper_bound(std::make_tuple(t_id->id._id, static_cast(lib_number))); - - if (upper == lower) { - // no new keys registered - _block_num = lib_number; - return num_updated; + fc::lock_guard g(_m); + new_peers_t res; + + // remove those that aren't among the top producers anymore + // -------------------------------------------------------- + for (auto it = _peer_info_map.begin(); it != _peer_info_map.end(); ) { + if (!current.contains(it->first)) { + res.insert(it->first); + it = _peer_info_map.erase(it); + } else { + ++it; } - - fc::lock_guard g(_m); // we only need to protect access to _peer_key_map - - for (auto itr = lower; itr != upper; ++itr) { - try { - const auto* itr2 = - db.find(boost::make_tuple(t_id->id, itr->primary_key)); - - name row_name; - uint32_t row_block_num; - uint8_t row_version; - std::variant row_variant; - - const auto& obj = *itr2; - fc::datastream ds(obj.value.data(), obj.value.size()); - // must match `struct peer_key;` in eosio.system.hpp - // ------------------------------------------------- - fc::raw::unpack(ds, row_name); - EOS_ASSERT(row_name.good(), misc_exception, "deserialized invalid name from `peerkeys`"); - - fc::raw::unpack(ds, row_block_num); - EOS_ASSERT(row_block_num > static_cast(_block_num), misc_exception, - "deserialized invalid version from `peerkeys`"); - - fc::raw::unpack(ds, row_version); - if (row_version != 0) - continue; - - fc::raw::unpack(ds, row_variant); - EOS_ASSERT(std::holds_alternative(row_variant), misc_exception, "deserialized invalid data from `peerkeys`"); - auto& data = std::get(row_variant); - if (data.pubkey) { - EOS_ASSERT(data.pubkey->valid(), misc_exception, "deserialized invalid public key from `peerkeys`"); - - _peer_key_map[row_name] = *data.pubkey; - ++num_updated; - } - } - FC_LOG_AND_DROP(("skipping invalid record deserialized from `peerkeys`")); + } + + // add new ones to _peer_info_map and updated modified ones + // -------------------------------------------------------- + for (auto& pi : current) { + if (!_peer_info_map.contains(pi.first) || _peer_info_map[pi.first] != pi.second) { + _peer_info_map[pi.first] = pi.second; + res.insert(pi.first); } - - _block_num = lib_number; // mark that we have updated up to lib_number - } FC_LOG_AND_DROP(("Error when updating peer_keys_db")); - return num_updated; + } + + return res; } } // namespace eosio::chain \ No newline at end of file diff --git a/plugins/producer_plugin/producer_plugin.cpp b/plugins/producer_plugin/producer_plugin.cpp index dd78badc7c..c723b35bde 100644 --- a/plugins/producer_plugin/producer_plugin.cpp +++ b/plugins/producer_plugin/producer_plugin.cpp @@ -712,7 +712,7 @@ class producer_plugin_impl : public std::enable_shared_from_this _is_savanna_active = false; std::vector _protocol_features_to_activate; bool _protocol_features_signaled = false; // to mark whether it has been signaled in start_block @@ -1217,6 +1217,11 @@ class producer_plugin_impl : public std::enable_shared_from_thischain().interrupt_transaction(); + } }; void new_chain_banner(const eosio::chain::controller& db) @@ -2351,6 +2356,12 @@ producer_plugin_impl::start_block_result producer_plugin_impl::start_block() { return start_block_result::exhausted; } + // Here we use readonly transactions to update our internal data structures from chainbase data + // (typically every minute or so). + // Currently the only update is the peer public_keys db (updated via "getpeerkeys"_n trx) + // --------------------------------------------------------------------------------------------- + chain.update_peer_keys(preprocess_deadline); + if (!process_incoming_trxs(preprocess_deadline, incoming_itr)) return start_block_result::exhausted; @@ -2843,7 +2854,7 @@ void producer_plugin_impl::schedule_production_loop() { // we failed to start a block, so try again later? _timer.async_wait([this, cid = ++_timer_corelation_id](const boost::system::error_code& ec) { if (ec != boost::asio::error::operation_aborted && cid == _timer_corelation_id) { - chain_plug->chain().interrupt_apply_block_transaction(); + interrupt_transaction(); app().executor().post(priority::high, exec_queue::read_write, [this]() { schedule_production_loop(); }); @@ -2929,7 +2940,7 @@ void producer_plugin_impl::schedule_delayed_production_loop(const std::weak_ptr< _timer.expires_at(epoch + boost::posix_time::microseconds(wake_up_time->time_since_epoch().count())); _timer.async_wait([this, cid = ++_timer_corelation_id](const boost::system::error_code& ec) { if (ec != boost::asio::error::operation_aborted && cid == _timer_corelation_id) { - chain_plug->chain().interrupt_apply_block_transaction(); + interrupt_transaction(); app().executor().post(priority::high, exec_queue::read_write, [this]() { schedule_production_loop(); }); @@ -3049,9 +3060,14 @@ void producer_plugin::received_block(uint32_t block_num, chain::fork_db_add_t fo my->_received_block = block_num; // fork_db_add_t::fork_switch means head block of best fork (different from the current branch) is received. // Since a better fork is available, interrupt current block validation and allow a fork switch to the better branch. - if (fork_db_add_result == fork_db_add_t::fork_switch) { - fc_ilog(_log, "new best fork received"); - my->chain_plug->chain().interrupt_apply_block_transaction(); + if (my->_is_savanna_active) { // interrupt during transition causes issues, so only allow after transition + if (fork_db_add_result == fork_db_add_t::appended_to_head) { + fc_tlog(_log, "new head block received, interrupting trx"); + my->interrupt_transaction(); + } else if (fork_db_add_result == fork_db_add_t::fork_switch) { + fc_ilog(_log, "new best fork received, interrupting trx"); + my->interrupt_transaction(); + } } } @@ -3060,7 +3076,7 @@ void producer_plugin::interrupt() { fc_ilog(_log, "interrupt"); app().executor().stop(); // shutdown any blocking read_only_execution_task my->interrupt_read_only(); - my->chain_plug->chain().interrupt_apply_block_transaction(); + my->interrupt_transaction(); } void producer_plugin_impl::interrupt_read_only() { diff --git a/tests/nodeos_under_min_avail_ram.py b/tests/nodeos_under_min_avail_ram.py index a210d0b263..be7992047d 100755 --- a/tests/nodeos_under_min_avail_ram.py +++ b/tests/nodeos_under_min_avail_ram.py @@ -43,6 +43,8 @@ maxRAMFlag="--chain-state-db-size-mb" maxRAMValue=1010 extraNodeosArgs=" %s %d %s %d --http-max-response-time-ms 990000 " % (minRAMFlag, minRAMValue, maxRAMFlag, maxRAMValue) + # test relies on production continuing on restart + extraNodeosArgs+=" --production-pause-vote-timeout-ms 0 " if cluster.launch(onlyBios=False, pnodes=pNodes, totalNodes=totalNodes, totalProducers=totalNodes, activateIF=activateIF, extraNodeosArgs=extraNodeosArgs) is False: Utils.cmdError("launcher") errorExit("Failed to stand up eos cluster.") diff --git a/unittests/checktime_tests.cpp b/unittests/checktime_tests.cpp index 704f679663..9c4ee3b578 100644 --- a/unittests/checktime_tests.cpp +++ b/unittests/checktime_tests.cpp @@ -139,7 +139,7 @@ BOOST_AUTO_TEST_CASE( checktime_interrupt_test) { try { BOOST_FAIL("Timed out waiting for block start"); } std::this_thread::sleep_for( std::chrono::milliseconds(100) ); - c.interrupt_apply_block_transaction(); + c.interrupt_transaction(); } ); // apply block, caught in an "infinite" loop diff --git a/unittests/contracts/eosio.system/CMakeLists.txt b/unittests/contracts/eosio.system/CMakeLists.txt index afef80d2ee..4116cde49c 100644 --- a/unittests/contracts/eosio.system/CMakeLists.txt +++ b/unittests/contracts/eosio.system/CMakeLists.txt @@ -1,5 +1,5 @@ if( EOSIO_COMPILE_TEST_CONTRACTS ) - add_contract( eosio.system eosio.system eosio.system.cpp ) + add_contract( eosio.system eosio.system eosio.system.cpp peer_keys.cpp ) target_include_directories(eosio.system PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include diff --git a/unittests/contracts/eosio.system/eosio.system.abi b/unittests/contracts/eosio.system/eosio.system.abi index 35bd49c352..08d7d76e42 100644 --- a/unittests/contracts/eosio.system/eosio.system.abi +++ b/unittests/contracts/eosio.system/eosio.system.abi @@ -5,6 +5,10 @@ { "new_type_name": "B_pair_time_point_sec_int64_E", "type": "pair_time_point_sec_int64" + }, + { + "new_type_name": "getpeerkeys_res_t", + "type": "peerkeys_t[]" } ], "structs": [ @@ -383,38 +387,30 @@ ] }, { - "name": "delegated_bandwidth", + "name": "deleteauth", "base": "", "fields": [ { - "name": "from", + "name": "account", "type": "name" }, { - "name": "to", + "name": "permission", "type": "name" - }, - { - "name": "net_weight", - "type": "asset" - }, - { - "name": "cpu_weight", - "type": "asset" } ] }, { - "name": "deleteauth", + "name": "delpeerkey", "base": "", "fields": [ { - "name": "account", + "name": "proposer_finalizer_name", "type": "name" }, { - "name": "permission", - "type": "name" + "name": "key", + "type": "public_key" } ] }, @@ -584,6 +580,11 @@ } ] }, + { + "name": "getpeerkeys", + "base": "", + "fields": [] + }, { "name": "init", "base": "", @@ -716,6 +717,34 @@ } ] }, + { + "name": "peer_key", + "base": "", + "fields": [ + { + "name": "account", + "type": "name" + }, + { + "name": "data", + "type": "variant_v0_data" + } + ] + }, + { + "name": "peerkeys_t", + "base": "", + "fields": [ + { + "name": "producer_name", + "type": "name" + }, + { + "name": "peer_key", + "type": "public_key?" + } + ] + }, { "name": "permission_level", "base": "", @@ -839,24 +868,16 @@ ] }, { - "name": "refund_request", + "name": "regpeerkey", "base": "", "fields": [ { - "name": "owner", + "name": "proposer_finalizer_name", "type": "name" }, { - "name": "request_time", - "type": "time_point_sec" - }, - { - "name": "net_amount", - "type": "asset" - }, - { - "name": "cpu_amount", - "type": "asset" + "name": "key", + "type": "public_key" } ] }, @@ -1367,24 +1388,12 @@ ] }, { - "name": "user_resources", + "name": "v0_data", "base": "", "fields": [ { - "name": "owner", - "type": "name" - }, - { - "name": "net_weight", - "type": "asset" - }, - { - "name": "cpu_weight", - "type": "asset" - }, - { - "name": "ram_bytes", - "type": "int64" + "name": "pubkey", + "type": "public_key?" } ] }, @@ -1479,6 +1488,72 @@ "type": "asset" } ] + }, + { + "name": "delegated_bandwidth", + "base": "", + "fields": [ + { + "name": "from", + "type": "name" + }, + { + "name": "to", + "type": "name" + }, + { + "name": "net_weight", + "type": "asset" + }, + { + "name": "cpu_weight", + "type": "asset" + } + ] + }, + { + "name": "refund_request", + "base": "", + "fields": [ + { + "name": "owner", + "type": "name" + }, + { + "name": "request_time", + "type": "time_point_sec" + }, + { + "name": "net_amount", + "type": "asset" + }, + { + "name": "cpu_amount", + "type": "asset" + } + ] + }, + { + "name": "user_resources", + "base": "", + "fields": [ + { + "name": "owner", + "type": "name" + }, + { + "name": "net_weight", + "type": "asset" + }, + { + "name": "cpu_weight", + "type": "asset" + }, + { + "name": "ram_bytes", + "type": "int64" + } + ] } ], "actions": [ @@ -1552,6 +1627,11 @@ "type": "deleteauth", "ricardian_contract": "" }, + { + "name": "delpeerkey", + "type": "delpeerkey", + "ricardian_contract": "" + }, { "name": "deposit", "type": "deposit", @@ -1567,6 +1647,11 @@ "type": "fundnetloan", "ricardian_contract": "" }, + { + "name": "getpeerkeys", + "type": "getpeerkeys", + "ricardian_contract": "" + }, { "name": "init", "type": "init", @@ -1597,6 +1682,11 @@ "type": "refund", "ricardian_contract": "" }, + { + "name": "regpeerkey", + "type": "regpeerkey", + "ricardian_contract": "" + }, { "name": "regproducer", "type": "regproducer", @@ -1740,13 +1830,6 @@ "key_names": [], "key_types": [] }, - { - "name": "delband", - "type": "delegated_bandwidth", - "index_type": "i64", - "key_names": [], - "key_types": [] - }, { "name": "global", "type": "eosio_global_state", @@ -1782,6 +1865,13 @@ "key_names": [], "key_types": [] }, + { + "name": "peerkeys", + "type": "peer_key", + "index_type": "i64", + "key_names": [], + "key_types": [] + }, { "name": "producers", "type": "producer_info", @@ -1803,13 +1893,6 @@ "key_names": [], "key_types": [] }, - { - "name": "refunds", - "type": "refund_request", - "index_type": "i64", - "key_names": [], - "key_types": [] - }, { "name": "rexbal", "type": "rex_balance", @@ -1839,21 +1922,45 @@ "key_types": [] }, { - "name": "userres", - "type": "user_resources", + "name": "voters", + "type": "voter_info", "index_type": "i64", "key_names": [], "key_types": [] }, { - "name": "voters", - "type": "voter_info", + "name": "delband", + "type": "delegated_bandwidth", + "index_type": "i64", + "key_names": [], + "key_types": [] + }, + { + "name": "refunds", + "type": "refund_request", + "index_type": "i64", + "key_names": [], + "key_types": [] + }, + { + "name": "userres", + "type": "user_resources", "index_type": "i64", "key_names": [], "key_types": [] } ], "ricardian_clauses": [], - "variants": [], - "action_results": [] + "variants": [ + { + "name": "variant_v0_data", + "types": ["v0_data"] + } + ], + "action_results": [ + { + "name": "getpeerkeys", + "result_type": "getpeerkeys_res_t" + } + ] } \ No newline at end of file diff --git a/unittests/contracts/eosio.system/eosio.system.cpp b/unittests/contracts/eosio.system/eosio.system.cpp index ceed74ea74..47a4c8c06a 100644 --- a/unittests/contracts/eosio.system/eosio.system.cpp +++ b/unittests/contracts/eosio.system/eosio.system.cpp @@ -7,6 +7,7 @@ #include "voting.cpp" #include "exchange_state.cpp" #include "rex.cpp" +#include "peer_keys.hpp" namespace eosiosystem { diff --git a/unittests/contracts/eosio.system/eosio.system.wasm b/unittests/contracts/eosio.system/eosio.system.wasm index a714dc5b14..904abd0c83 100644 Binary files a/unittests/contracts/eosio.system/eosio.system.wasm and b/unittests/contracts/eosio.system/eosio.system.wasm differ diff --git a/unittests/contracts/eosio.system/peer_keys.cpp b/unittests/contracts/eosio.system/peer_keys.cpp new file mode 100644 index 0000000000..695bc65654 --- /dev/null +++ b/unittests/contracts/eosio.system/peer_keys.cpp @@ -0,0 +1,67 @@ +#include +#include "eosio.system.hpp" +#include "peer_keys.hpp" + +#include +#include + +namespace eosiosystem { + +void peer_keys::regpeerkey(const name& proposer_finalizer_name, const public_key& key) { + require_auth(proposer_finalizer_name); + peer_keys_table peer_keys_table(get_self(), get_self().value); + check(!std::holds_alternative(key), "webauthn keys not allowed in regpeerkey action"); + + auto peers_itr = peer_keys_table.find(proposer_finalizer_name.value); + if (peers_itr == peer_keys_table.end()) { + peer_keys_table.emplace(proposer_finalizer_name, [&](auto& row) { + row.init_row(proposer_finalizer_name); + row.set_public_key(key); + }); + } else { + const auto& prev_key = peers_itr->get_public_key(); + check(!prev_key || *prev_key != key, "Provided key is the same as currently stored one"); + peer_keys_table.modify(peers_itr, eosio::same_payer, [&](auto& row) { + row.update_row(); + row.set_public_key(key); + }); + } +} + +void peer_keys::delpeerkey(const name& proposer_finalizer_name, const public_key& key) { + require_auth(proposer_finalizer_name); + peer_keys_table peer_keys_table(get_self(), get_self().value); + + // not updating the version here. deleted keys will persist in the memory hashmap + auto peers_itr = peer_keys_table.find(proposer_finalizer_name.value); + check(peers_itr != peer_keys_table.end(), "Key not present for name: " + proposer_finalizer_name.to_string()); + const auto& prev_key = peers_itr->get_public_key(); + check(prev_key && *prev_key == key, "Current key does not match the provided one"); + peer_keys_table.erase(peers_itr); +} + +peer_keys::getpeerkeys_res_t peer_keys::getpeerkeys() { + peer_keys_table peer_keys_table(get_self(), get_self().value); + producers_table producers(get_self(), get_self().value); + constexpr size_t max_return = 50; + + getpeerkeys_res_t resp; + resp.reserve(max_return); + + auto idx = producers.get_index<"prototalvote"_n>(); + + // this is a simpler implementation than the one in `eos-system-contracts`. + // the one in `eos-system-contracts` iterates over both ends of the "prototalvote"_n index + // (to take into account non-active producers) + // ---------------------------------------------------------------------------------------- + for( auto it = idx.cbegin(); it != idx.cend() && resp.size() < max_return; ++it ) { + auto peers_itr = peer_keys_table.find(it->owner.value); + if (peers_itr == peer_keys_table.end()) + resp.push_back(peerkeys_t{it->owner, {}}); + else + resp.push_back(peerkeys_t{it->owner, peers_itr->get_public_key()}); + } + return resp; +} + +} // namespace eosiosystem diff --git a/unittests/contracts/eosio.system/peer_keys.hpp b/unittests/contracts/eosio.system/peer_keys.hpp new file mode 100644 index 0000000000..3269075961 --- /dev/null +++ b/unittests/contracts/eosio.system/peer_keys.hpp @@ -0,0 +1,83 @@ +#pragma once + +#include +#include +#include + +#include +#include + +namespace eosiosystem { + +using eosio::name; +using eosio::public_key; + +// ------------------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------------------- +struct [[eosio::table("peerkeys"), eosio::contract("eosio.system")]] peer_key { + struct v0_data { + std::optional pubkey; // peer key for network message authentication + EOSLIB_SERIALIZE(v0_data, (pubkey)) + }; + + name account; + std::variant data; + + uint64_t primary_key() const { return account.value; } + + void set_public_key(const public_key& key) { data = v0_data{key}; } + const std::optional& get_public_key() const { + return std::visit([](auto& v) -> const std::optional& { return v.pubkey; }, data); + } + void update_row() {} + void init_row(name n) { *this = peer_key{n, v0_data{}}; } +}; + +// ------------------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------------------- +typedef eosio::multi_index<"peerkeys"_n, peer_key> peer_keys_table; + +// ------------------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------------------- +struct [[eosio::contract("eosio.system")]] peer_keys : public eosio::contract { + + peer_keys(name s, name code, eosio::datastream ds) + : eosio::contract(s, code, ds) {} + + struct peerkeys_t { + name producer_name; + std::optional peer_key; + + EOSLIB_SERIALIZE(peerkeys_t, (producer_name)(peer_key)) + }; + + using getpeerkeys_res_t = std::vector; + + /** + * Action to register a public key for a proposer or finalizer name. + * This key will be used to validate a network peer's identity. + * A proposer or finalizer can only have have one public key registered at a time. + * If a key is already registered for `proposer_finalizer_name`, and `regpeerkey` is + * called with a different key, the new key replaces the previous one in `peer_keys_table` + */ + [[eosio::action]] + void regpeerkey(const name& proposer_finalizer_name, const public_key& key); + + /** + * Action to delete a public key for a proposer or finalizer name. + * + * An existing public key for a given account can be changed by calling `regpeerkey` again. + */ + [[eosio::action]] + void delpeerkey(const name& proposer_finalizer_name, const public_key& key); + + /** + * Returns a list of top-50 producers (in rank order) along with their peer public key if it was + * added via the regpeerkey action. + */ + [[eosio::action]] + getpeerkeys_res_t getpeerkeys(); + +}; + +} // namespace eosiosystem \ No newline at end of file diff --git a/unittests/eosio_system_tester.hpp b/unittests/eosio_system_tester.hpp index 15ad0221ea..550e90e70b 100644 --- a/unittests/eosio_system_tester.hpp +++ b/unittests/eosio_system_tester.hpp @@ -564,7 +564,7 @@ class eosio_system_tester : public T { ) ); } - T::produce_block(); + T::produce_blocks( 2 * 21 ); T::produce_block(fc::seconds(1000)); auto producer_keys = T::control->active_producers().producers; diff --git a/unittests/getpeerkeys_tests.cpp b/unittests/getpeerkeys_tests.cpp new file mode 100644 index 0000000000..ff67516845 --- /dev/null +++ b/unittests/getpeerkeys_tests.cpp @@ -0,0 +1,57 @@ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wsign-compare" +#include +#pragma GCC diagnostic pop + +#include +#include +#include + +#include +#include + +#include +#include "eosio_system_tester.hpp" + +using namespace eosio_system; + +class getpeerkeys_tester : public eosio_system_tester { +public: + action_result regpeerkey( const name& proposer, const fc::crypto::public_key& key ) { + return push_action(proposer, "regpeerkey"_n, mvo()("proposer_finalizer_name", proposer)("key", key)); + } +}; + +BOOST_AUTO_TEST_SUITE(getpeerkeys_tests) + +BOOST_FIXTURE_TEST_CASE( getpeerkeys_test, getpeerkeys_tester ) { try { + std::vector prod_names = active_and_vote_producers(); + + for (size_t i=0; iget_top_producer_keys(fc::time_point::maximum()); // call readonly action from controller + BOOST_REQUIRE_EQUAL(peerkeys.size(), 21); + + size_t num_found = 0; + for (size_t i=0; i