From 0e11ce48025a5ea73c4328ebe1ea1402cf6f252d Mon Sep 17 00:00:00 2001 From: Tobias Date: Tue, 12 May 2026 10:01:22 -0400 Subject: [PATCH 01/20] Add Savanna finalizer support to system contract --- contracts/eosio.system/CMakeLists.txt | 1 + .../include/eosio.system/eosio.system.hpp | 128 +++++++- contracts/eosio.system/src/eosio.system.cpp | 4 + contracts/eosio.system/src/finalizer_key.cpp | 282 ++++++++++++++++++ contracts/eosio.system/src/voting.cpp | 9 + 5 files changed, 421 insertions(+), 3 deletions(-) create mode 100644 contracts/eosio.system/src/finalizer_key.cpp diff --git a/contracts/eosio.system/CMakeLists.txt b/contracts/eosio.system/CMakeLists.txt index 117052b2..a4b4103f 100644 --- a/contracts/eosio.system/CMakeLists.txt +++ b/contracts/eosio.system/CMakeLists.txt @@ -4,6 +4,7 @@ add_contract( ${CMAKE_CURRENT_SOURCE_DIR}/src/eosio.system.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/delegate_bandwidth.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/exchange_state.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/finalizer_key.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/name_bidding.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/native.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/producer_pay.cpp diff --git a/contracts/eosio.system/include/eosio.system/eosio.system.hpp b/contracts/eosio.system/include/eosio.system/eosio.system.hpp index 0897316d..f7672355 100644 --- a/contracts/eosio.system/include/eosio.system/eosio.system.hpp +++ b/contracts/eosio.system/include/eosio.system/eosio.system.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -427,6 +428,75 @@ namespace eosiosystem { EOSLIB_SERIALIZE( producer_info2, (owner)(votepay_share)(last_votepay_share_update) ) }; + // finalizer_key_info stores information about a finalizer key. + struct [[eosio::table("finkeys"), eosio::contract("eosio.system")]] finalizer_key_info { + uint64_t id; // automatically generated ID for the key in the table + name finalizer_name; // name of the finalizer owning the key + std::string finalizer_key; // finalizer key in base64url format + std::vector finalizer_key_binary; // finalizer key in binary format in Affine little endian non-montgomery g1 + + uint64_t primary_key() const { return id; } + uint64_t by_fin_name() const { return finalizer_name.value; } + eosio::checksum256 by_fin_key() const { return eosio::sha256(finalizer_key_binary.data(), finalizer_key_binary.size()); } + + bool is_active(uint64_t finalizer_active_key_id) const { return id == finalizer_active_key_id; } + }; + + typedef eosio::multi_index< + "finkeys"_n, finalizer_key_info, + indexed_by<"byfinname"_n, const_mem_fun>, + indexed_by<"byfinkey"_n, const_mem_fun> + > finalizer_keys_table; + + // finalizer_info stores information about a finalizer. + struct [[eosio::table("finalizers"), eosio::contract("eosio.system")]] finalizer_info { + name finalizer_name; // finalizer's name + uint64_t active_key_id; // finalizer's active finalizer key's id in finalizer_keys_table + std::vector active_key_binary; // active finalizer key in binary format + uint32_t finalizer_key_count = 0; // number of finalizer keys registered by this finalizer + + uint64_t primary_key() const { return finalizer_name.value; } + }; + + typedef eosio::multi_index< "finalizers"_n, finalizer_info > finalizers_table; + + // finalizer_auth_info stores a finalizer's key id and its finalizer authority. + struct finalizer_auth_info { + finalizer_auth_info() = default; + explicit finalizer_auth_info(const finalizer_info& finalizer); + + uint64_t key_id; + eosio::finalizer_authority fin_authority; + + bool operator==(const finalizer_auth_info& other) const { + return key_id == other.key_id && + fin_authority.public_key == other.fin_authority.public_key; + } + + EOSLIB_SERIALIZE( finalizer_auth_info, (key_id)(fin_authority) ) + }; + + // A single entry storing information about last proposed finalizers. + struct [[eosio::table("lastpropfins"), eosio::contract("eosio.system")]] last_prop_finalizers_info { + std::vector last_proposed_finalizers; // sorted by ascending finalizer key id + + uint64_t primary_key() const { return 0; } + + EOSLIB_SERIALIZE( last_prop_finalizers_info, (last_proposed_finalizers) ) + }; + + typedef eosio::multi_index< "lastpropfins"_n, last_prop_finalizers_info > last_prop_fins_table; + + // A single entry storing next available finalizer key_id so IDs are never reused. + struct [[eosio::table("finkeyidgen"), eosio::contract("eosio.system")]] fin_key_id_generator_info { + uint64_t next_finalizer_key_id = 0; + uint64_t primary_key() const { return 0; } + + EOSLIB_SERIALIZE( fin_key_id_generator_info, (next_finalizer_key_id) ) + }; + + typedef eosio::multi_index< "finkeyidgen"_n, fin_key_id_generator_info > fin_key_id_gen_table; + // Voter info. Voter info stores information about the voter: // - `owner` the voter // - `proxy` the proxy set by the voter, if any @@ -903,6 +973,11 @@ namespace eosiosystem { voters_table _voters; producers_table _producers; producers_table2 _producers2; + finalizer_keys_table _finalizer_keys; + finalizers_table _finalizers; + last_prop_fins_table _last_prop_finalizers; + std::optional> _last_prop_finalizers_cached; + fin_key_id_gen_table _fin_key_id_generator; global_state_singleton _global; global_state2_singleton _global2; global_state3_singleton _global3; @@ -1411,10 +1486,47 @@ namespace eosiosystem { * * Deactivate the block producer with account name `producer`. * @param producer - the block producer account to unregister. - */ + */ [[eosio::action]] void unregprod( const name& producer ); + /** + * Permanently transition to Savanna consensus by establishing the first finalizer policy. + * + * @pre Requires authority of the system contract. + * @pre The current Telos producer schedule must have active finalizer keys. + */ + [[eosio::action]] + void switchtosvnn(); + + /** + * Register a BLS finalizer key for a registered producer. + * + * @param finalizer_name - producer account registering the finalizer key. + * @param finalizer_key - public finalizer key in base64url format. + * @param proof_of_possession - proof of possession signature in base64url format. + */ + [[eosio::action]] + void regfinkey( const name& finalizer_name, const std::string& finalizer_key, const std::string& proof_of_possession ); + + /** + * Activate a registered finalizer key. + * + * @param finalizer_name - producer account activating the finalizer key. + * @param finalizer_key - registered public finalizer key. + */ + [[eosio::action]] + void actfinkey( const name& finalizer_name, const std::string& finalizer_key ); + + /** + * Delete a registered finalizer key. + * + * @param finalizer_name - producer account deleting the finalizer key. + * @param finalizer_key - registered public finalizer key. + */ + [[eosio::action]] + void delfinkey( const name& finalizer_name, const std::string& finalizer_key ); + /** * Set ram action sets the ram supply. * @param max_ram_size - the amount of ram supply to set. @@ -1789,11 +1901,21 @@ namespace eosiosystem { void update_votes( const name& voter, const name& proxy, const std::vector& producers, bool voting ); void propagate_weight_change( const voter_info& voter ); double update_producer_votepay_share( const producers_table2::const_iterator& prod_itr, - const time_point& ct, - double shares_rate, bool reset_to_zero = false ); + const time_point& ct, + double shares_rate, bool reset_to_zero = false ); double update_total_votepay_share( const time_point& ct, double additional_shares_delta = 0.0, double shares_rate_delta = 0.0 ); + // defined in finalizer_key.cpp + bool is_savanna_consensus(); + bool has_active_finalizer_key( const name& producer ) const; + void set_proposed_finalizers( std::vector finalizers ); + const std::vector& get_last_proposed_finalizers(); + uint64_t get_next_finalizer_key_id(); + finalizers_table::const_iterator get_finalizer_itr( const name& finalizer_name ) const; + std::vector get_finalizers_for_producers( const std::vector& producers ) const; + std::vector get_last_scheduled_producers() const; + template class registration { public: diff --git a/contracts/eosio.system/src/eosio.system.cpp b/contracts/eosio.system/src/eosio.system.cpp index 2e1f79a1..cff610d7 100644 --- a/contracts/eosio.system/src/eosio.system.cpp +++ b/contracts/eosio.system/src/eosio.system.cpp @@ -45,6 +45,10 @@ namespace eosiosystem { _voters(get_self(), get_self().value), _producers(get_self(), get_self().value), _producers2(get_self(), get_self().value), + _finalizer_keys(get_self(), get_self().value), + _finalizers(get_self(), get_self().value), + _last_prop_finalizers(get_self(), get_self().value), + _fin_key_id_generator(get_self(), get_self().value), _global(get_self(), get_self().value), _global2(get_self(), get_self().value), _global3(get_self(), get_self().value), diff --git a/contracts/eosio.system/src/finalizer_key.cpp b/contracts/eosio.system/src/finalizer_key.cpp new file mode 100644 index 00000000..13d8cad6 --- /dev/null +++ b/contracts/eosio.system/src/finalizer_key.cpp @@ -0,0 +1,282 @@ +#include + +#include +#include + +#include + +namespace eosiosystem { + finalizer_auth_info::finalizer_auth_info(const finalizer_info& finalizer) + : key_id(finalizer.active_key_id) + , fin_authority( eosio::finalizer_authority{ + .description = finalizer.finalizer_name.to_string(), + .weight = 1, + .public_key = finalizer.active_key_binary }) + { + } + + bool system_contract::is_savanna_consensus() { + return !get_last_proposed_finalizers().empty(); + } + + bool system_contract::has_active_finalizer_key( const name& producer ) const { + auto finalizer = _finalizers.find( producer.value ); + return finalizer != _finalizers.end() && !finalizer->active_key_binary.empty(); + } + + eosio::bls_g1 to_binary(const std::string& finalizer_key) { + check(finalizer_key.compare(0, 7, "PUB_BLS") == 0, "finalizer key does not start with PUB_BLS: " + finalizer_key); + return eosio::decode_bls_public_key_to_g1(finalizer_key); + } + + static eosio::checksum256 get_finalizer_key_hash(const eosio::bls_g1& finalizer_key_binary) { + return eosio::sha256(finalizer_key_binary.data(), finalizer_key_binary.size()); + } + + static eosio::checksum256 get_finalizer_key_hash(const std::string& finalizer_key) { + const auto fin_key_g1 = to_binary(finalizer_key); + return get_finalizer_key_hash(fin_key_g1); + } + + finalizers_table::const_iterator system_contract::get_finalizer_itr( const name& finalizer_name ) const { + auto finalizer_itr = _finalizers.find(finalizer_name.value); + check( finalizer_itr != _finalizers.end(), "finalizer " + finalizer_name.to_string() + " has not registered any finalizer keys" ); + check( finalizer_itr->finalizer_key_count > 0, "finalizer " + finalizer_name.to_string() + " must have at least one registered finalizer key" ); + + return finalizer_itr; + } + + void system_contract::set_proposed_finalizers( std::vector proposed_finalizers ) { + std::sort( proposed_finalizers.begin(), proposed_finalizers.end(), []( const finalizer_auth_info& lhs, const finalizer_auth_info& rhs ) { + return lhs.key_id < rhs.key_id; + } ); + + const auto& last_proposed_finalizers = get_last_proposed_finalizers(); + if( proposed_finalizers == last_proposed_finalizers ) { + return; + } + + std::vector finalizer_authorities; + finalizer_authorities.reserve(proposed_finalizers.size()); + for( const auto& k: proposed_finalizers ) { + finalizer_authorities.emplace_back(k.fin_authority); + } + + eosio::finalizer_policy fin_policy { + .threshold = ( finalizer_authorities.size() * 2 ) / 3 + 1, + .finalizers = std::move(finalizer_authorities) + }; + + eosio::set_finalizers(std::move(fin_policy)); + + auto itr = _last_prop_finalizers.begin(); + if( itr == _last_prop_finalizers.end() ) { + _last_prop_finalizers.emplace( get_self(), [&]( auto& f ) { + f.last_proposed_finalizers = proposed_finalizers; + }); + } else { + _last_prop_finalizers.modify(itr, same_payer, [&]( auto& f ) { + f.last_proposed_finalizers = proposed_finalizers; + }); + } + + if( _last_prop_finalizers_cached.has_value() ) { + std::swap(*_last_prop_finalizers_cached, proposed_finalizers); + } else { + _last_prop_finalizers_cached.emplace(std::move(proposed_finalizers)); + } + } + + const std::vector& system_contract::get_last_proposed_finalizers() { + if( !_last_prop_finalizers_cached.has_value() ) { + const auto finalizers_itr = _last_prop_finalizers.begin(); + if( finalizers_itr == _last_prop_finalizers.end() ) { + _last_prop_finalizers_cached = {}; + } else { + _last_prop_finalizers_cached = finalizers_itr->last_proposed_finalizers; + } + } + + return *_last_prop_finalizers_cached; + } + + uint64_t system_contract::get_next_finalizer_key_id() { + uint64_t next_id = 0; + auto itr = _fin_key_id_generator.begin(); + + if( itr == _fin_key_id_generator.end() ) { + _fin_key_id_generator.emplace( get_self(), [&]( auto& f ) { + f.next_finalizer_key_id = next_id; + }); + } else { + next_id = itr->next_finalizer_key_id + 1; + _fin_key_id_generator.modify(itr, same_payer, [&]( auto& f ) { + f.next_finalizer_key_id = next_id; + }); + } + + return next_id; + } + + std::vector system_contract::get_finalizers_for_producers( const std::vector& producers ) const { + std::vector proposed_finalizers; + proposed_finalizers.reserve(producers.size()); + + for( const auto& item: producers ) { + const auto& producer_name = item.first.producer_name; + auto finalizer = _finalizers.find(producer_name.value); + check( finalizer != _finalizers.end() && !finalizer->active_key_binary.empty(), + "producer " + producer_name.to_string() + " does not have an active finalizer key" ); + + proposed_finalizers.emplace_back(*finalizer); + } + + return proposed_finalizers; + } + + std::vector system_contract::get_last_scheduled_producers() const { + std::vector scheduled_producers; + scheduled_producers.reserve(_gschedule_metrics.producers_metric.size()); + + for( const auto& metric: _gschedule_metrics.producers_metric ) { + const auto prod = _producers.find(metric.bp_name.value); + check( prod != _producers.end(), "scheduled producer " + metric.bp_name.to_string() + " is not registered" ); + check( prod->active(), "scheduled producer " + metric.bp_name.to_string() + " is not active" ); + + scheduled_producers.emplace_back( + eosio::producer_authority{ + .producer_name = prod->owner, + .authority = prod->get_producer_authority() + }, + prod->location + ); + } + + return scheduled_producers; + } + + void system_contract::switchtosvnn() { + require_auth(get_self()); + + check(!is_savanna_consensus(), "switchtosvnn can be run only once"); + check(_gstate.last_producer_schedule_size > 0, "producer schedule must be established before switching to Savanna"); + + const auto scheduled_producers = get_last_scheduled_producers(); + check( scheduled_producers.size() == _gstate.last_producer_schedule_size, + "Telos scheduled producer metrics do not match last producer schedule size" ); + + auto proposed_finalizers = get_finalizers_for_producers(scheduled_producers); + check( proposed_finalizers.size() == _gstate.last_producer_schedule_size, + "not enough scheduled producers have registered finalizer keys, has " + std::to_string(proposed_finalizers.size()) + + ", require " + std::to_string(_gstate.last_producer_schedule_size) ); + + set_proposed_finalizers(std::move(proposed_finalizers)); + check(is_savanna_consensus(), "switching to Savanna failed"); + } + + void system_contract::regfinkey( const name& finalizer_name, const std::string& finalizer_key, const std::string& proof_of_possession ) { + require_auth( finalizer_name ); + + auto producer = _producers.find( finalizer_name.value ); + check( producer != _producers.end(), "finalizer " + finalizer_name.to_string() + " is not a registered producer"); + + check(proof_of_possession.compare(0, 7, "SIG_BLS") == 0, "proof of possession signature does not start with SIG_BLS: " + proof_of_possession); + + const auto fin_key_g1 = to_binary(finalizer_key); + const auto pop_g2 = eosio::decode_bls_signature_to_g2(proof_of_possession); + + const auto idx = _finalizer_keys.get_index<"byfinkey"_n>(); + const auto hash = get_finalizer_key_hash(fin_key_g1); + check(idx.find(hash) == idx.end(), "duplicate finalizer key: " + finalizer_key); + + check(eosio::bls_pop_verify(fin_key_g1, pop_g2), "proof of possession check failed"); + + const auto finalizer_key_itr = _finalizer_keys.emplace( finalizer_name, [&]( auto& k ) { + k.id = get_next_finalizer_key_id(); + k.finalizer_name = finalizer_name; + k.finalizer_key = finalizer_key; + k.finalizer_key_binary = { fin_key_g1.begin(), fin_key_g1.end() }; + }); + + auto finalizer = _finalizers.find(finalizer_name.value); + if( finalizer == _finalizers.end() ) { + _finalizers.emplace( finalizer_name, [&]( auto& f ) { + f.finalizer_name = finalizer_name; + f.active_key_id = finalizer_key_itr->id; + f.active_key_binary = finalizer_key_itr->finalizer_key_binary; + f.finalizer_key_count = 1; + }); + } else { + _finalizers.modify( finalizer, same_payer, [&]( auto& f ) { + ++f.finalizer_key_count; + }); + } + } + + void system_contract::actfinkey( const name& finalizer_name, const std::string& finalizer_key ) { + require_auth( finalizer_name ); + + const auto finalizer = get_finalizer_itr(finalizer_name); + + const auto idx = _finalizer_keys.get_index<"byfinkey"_n>(); + const auto hash = get_finalizer_key_hash(finalizer_key); + const auto finalizer_key_itr = idx.find(hash); + check(finalizer_key_itr != idx.end(), "finalizer key was not registered: " + finalizer_key); + + check(finalizer_key_itr->finalizer_name == name(finalizer_name), "finalizer key was not registered by the finalizer: " + finalizer_key); + check(!finalizer_key_itr->is_active(finalizer->active_key_id), "finalizer key was already active: " + finalizer_key); + + const auto active_key_id = finalizer->active_key_id; + + _finalizers.modify( finalizer, same_payer, [&]( auto& f ) { + f.active_key_id = finalizer_key_itr->id; + f.active_key_binary = finalizer_key_itr->finalizer_key_binary; + }); + + const auto& last_proposed_finalizers = get_last_proposed_finalizers(); + if( last_proposed_finalizers.empty() ) { + return; + } + + auto itr = std::lower_bound(last_proposed_finalizers.begin(), last_proposed_finalizers.end(), active_key_id, [](const finalizer_auth_info& key, uint64_t id) { + return key.key_id < id; + }); + + if( itr != last_proposed_finalizers.end() && itr->key_id == active_key_id ) { + auto proposed_finalizers = last_proposed_finalizers; + auto& matching_entry = proposed_finalizers[itr - last_proposed_finalizers.begin()]; + + matching_entry.key_id = finalizer_key_itr->id; + matching_entry.fin_authority.public_key = finalizer_key_itr->finalizer_key_binary; + + set_proposed_finalizers(std::move(proposed_finalizers)); + } + } + + void system_contract::delfinkey( const name& finalizer_name, const std::string& finalizer_key ) { + require_auth( finalizer_name ); + + auto finalizer = get_finalizer_itr(finalizer_name); + + auto idx = _finalizer_keys.get_index<"byfinkey"_n>(); + auto hash = get_finalizer_key_hash(finalizer_key); + auto fin_key_itr = idx.find(hash); + check(fin_key_itr != idx.end(), "finalizer key was not registered: " + finalizer_key); + + check(fin_key_itr->finalizer_name == name(finalizer_name), "finalizer key " + finalizer_key + " was not registered by the finalizer " + finalizer_name.to_string()); + + if( fin_key_itr->is_active(finalizer->active_key_id) ) { + check( finalizer->finalizer_key_count == 1, "cannot delete an active key unless it is the last registered finalizer key, has " + std::to_string(finalizer->finalizer_key_count) + " keys"); + } + + if( finalizer->finalizer_key_count == 1 ) { + _finalizers.erase(finalizer); + } else { + _finalizers.modify(finalizer, same_payer, [&]( auto& f ) { + --f.finalizer_key_count; + }); + } + + idx.erase(fin_key_itr); + } +} /// namespace eosiosystem diff --git a/contracts/eosio.system/src/voting.cpp b/contracts/eosio.system/src/voting.cpp index 6cfca82f..7f7d2859 100644 --- a/contracts/eosio.system/src/voting.cpp +++ b/contracts/eosio.system/src/voting.cpp @@ -127,6 +127,7 @@ namespace eosiosystem { _gstate.last_producer_schedule_update = block_time; auto idx = _producers.get_index<"prototalvote"_n>(); + bool is_savanna = is_savanna_consensus(); // TELOS BEGIN uint32_t totalActiveVotedProds = uint32_t(std::distance(idx.begin(), idx.end())); @@ -136,6 +137,10 @@ namespace eosiosystem { active_producers.reserve(totalActiveVotedProds); for( auto it = idx.cbegin(); it != idx.cend() && active_producers.size() < totalActiveVotedProds /*TELOS*/ && 0 < it->total_votes && it->active(); ++it ) { + if( is_savanna && !has_active_finalizer_key(it->owner) ) { + continue; + } + active_producers.emplace_back( eosio::producer_authority{ .producer_name = it->owner, @@ -182,6 +187,10 @@ namespace eosiosystem { _gstate.last_producer_schedule_size = static_cast(top_producers.size()); } + + if( is_savanna ) { + set_proposed_finalizers( get_finalizers_for_producers(top_producers) ); + } // TELOS END } From 8032d16d274a68b99a5e707c53a85adf8d70720b Mon Sep 17 00:00:00 2001 From: Tobias Date: Tue, 12 May 2026 16:28:21 -0400 Subject: [PATCH 02/20] Guard Savanna switch on active schedule --- .../include/eosio.system/eosio.system.hpp | 1 + contracts/eosio.system/src/finalizer_key.cpp | 25 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/contracts/eosio.system/include/eosio.system/eosio.system.hpp b/contracts/eosio.system/include/eosio.system/eosio.system.hpp index f7672355..479e06a4 100644 --- a/contracts/eosio.system/include/eosio.system/eosio.system.hpp +++ b/contracts/eosio.system/include/eosio.system/eosio.system.hpp @@ -1914,6 +1914,7 @@ namespace eosiosystem { uint64_t get_next_finalizer_key_id(); finalizers_table::const_iterator get_finalizer_itr( const name& finalizer_name ) const; std::vector get_finalizers_for_producers( const std::vector& producers ) const; + bool active_schedule_matches_last_scheduled_producers( const std::vector& active_schedule ) const; std::vector get_last_scheduled_producers() const; template diff --git a/contracts/eosio.system/src/finalizer_key.cpp b/contracts/eosio.system/src/finalizer_key.cpp index 13d8cad6..77ed0cf7 100644 --- a/contracts/eosio.system/src/finalizer_key.cpp +++ b/contracts/eosio.system/src/finalizer_key.cpp @@ -134,6 +134,25 @@ namespace eosiosystem { return proposed_finalizers; } + bool system_contract::active_schedule_matches_last_scheduled_producers( const std::vector& active_schedule ) const { + if( active_schedule.size() != _gschedule_metrics.producers_metric.size() ) { + return false; + } + + std::vector sorted_active_schedule = active_schedule; + std::vector sorted_scheduled_producers; + sorted_scheduled_producers.reserve(_gschedule_metrics.producers_metric.size()); + + for( const auto& metric: _gschedule_metrics.producers_metric ) { + sorted_scheduled_producers.emplace_back(metric.bp_name); + } + + std::sort(sorted_active_schedule.begin(), sorted_active_schedule.end()); + std::sort(sorted_scheduled_producers.begin(), sorted_scheduled_producers.end()); + + return sorted_active_schedule == sorted_scheduled_producers; + } + std::vector system_contract::get_last_scheduled_producers() const { std::vector scheduled_producers; scheduled_producers.reserve(_gschedule_metrics.producers_metric.size()); @@ -165,6 +184,12 @@ namespace eosiosystem { check( scheduled_producers.size() == _gstate.last_producer_schedule_size, "Telos scheduled producer metrics do not match last producer schedule size" ); + const auto active_schedule = eosio::get_active_producers(); + check( active_schedule.size() == _gstate.last_producer_schedule_size, + "active producer schedule size does not match last producer schedule size" ); + check( active_schedule_matches_last_scheduled_producers(active_schedule), + "active producer schedule does not match Telos scheduled producer metrics" ); + auto proposed_finalizers = get_finalizers_for_producers(scheduled_producers); check( proposed_finalizers.size() == _gstate.last_producer_schedule_size, "not enough scheduled producers have registered finalizer keys, has " + std::to_string(proposed_finalizers.size()) + From 40dfd38163859c9dde79f09df40defde7ad1e799 Mon Sep 17 00:00:00 2001 From: Tobias Date: Tue, 19 May 2026 16:21:00 -0400 Subject: [PATCH 03/20] Fix Savanna missed-block autokick --- .../eosio.system/src/system_rotation.cpp | 1 - contracts/eosio.system/src/voting.cpp | 30 ++++++++++++++----- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/contracts/eosio.system/src/system_rotation.cpp b/contracts/eosio.system/src/system_rotation.cpp index 93b3f84b..0ad17df3 100644 --- a/contracts/eosio.system/src/system_rotation.cpp +++ b/contracts/eosio.system/src/system_rotation.cpp @@ -63,7 +63,6 @@ void system_contract::update_missed_blocks_per_rotation() { uint32_t(active_schedule_size)) && max_kick_bps > 0) { _producers.modify(pitr, same_payer, [&](auto &p) { - p.lifetime_missed_blocks += p.missed_blocks_per_rotation; p.kick(kick_type::REACHED_TRESHOLD); }); max_kick_bps--; diff --git a/contracts/eosio.system/src/voting.cpp b/contracts/eosio.system/src/voting.cpp index 7f7d2859..adf349a9 100644 --- a/contracts/eosio.system/src/voting.cpp +++ b/contracts/eosio.system/src/voting.cpp @@ -169,21 +169,37 @@ namespace eosiosystem { producers.push_back( std::move(item.first) ); // TELOS BEGIN + auto schedule_metrics_changed = [&]() { + if( _gschedule_metrics.producers_metric.size() != top_producers.size() ) { + return true; + } + + for( size_t i = 0; i < top_producers.size(); ++i ) { + if( _gschedule_metrics.producers_metric[i].bp_name != top_producers[i].first.producer_name ) { + return true; + } + } + + return false; + }; + auto schedule_version = set_proposed_producers(producers); if (schedule_version >= 0) { print("\n**new schedule was proposed**"); _gstate.last_proposed_schedule_update = block_time; - _gschedule_metrics.producers_metric.erase( _gschedule_metrics.producers_metric.begin(), _gschedule_metrics.producers_metric.end()); + if( schedule_metrics_changed() ) { + _gschedule_metrics.producers_metric.erase( _gschedule_metrics.producers_metric.begin(), _gschedule_metrics.producers_metric.end()); - std::vector psm; - std::for_each(top_producers.begin(), top_producers.end(), [&psm](auto &tp) { - auto bp_name = tp.first.producer_name; - psm.emplace_back(producer_metric{ bp_name, 12 }); - }); + std::vector psm; + std::for_each(top_producers.begin(), top_producers.end(), [&psm](auto &tp) { + auto bp_name = tp.first.producer_name; + psm.emplace_back(producer_metric{ bp_name, 12 }); + }); - _gschedule_metrics.producers_metric = psm; + _gschedule_metrics.producers_metric = psm; + } _gstate.last_producer_schedule_size = static_cast(top_producers.size()); } From 6ebd1716b001091a492e26d1be5faa2e215ebe54 Mon Sep 17 00:00:00 2001 From: Tobias Date: Thu, 4 Jun 2026 23:47:32 -0400 Subject: [PATCH 04/20] Make Savanna release checks Spring-compatible --- .github/workflows/ubuntu-2204.yml | 71 ++++++++++++++++++++++++------- contracts/CMakeLists.txt | 2 +- tests/CMakeLists.txt | 14 +++--- tests/eosio.limitauth_tests.cpp | 1 - tests/eosio.msig_tests.cpp | 4 +- tests/eosio.system_tester.hpp | 17 ++++++-- tests/eosio.system_tests.cpp | 24 +++++++---- tests/eosio.token_tests.cpp | 4 +- tests/eosio.wrap_tests.cpp | 4 +- tests/main.cpp | 2 - 10 files changed, 97 insertions(+), 46 deletions(-) diff --git a/.github/workflows/ubuntu-2204.yml b/.github/workflows/ubuntu-2204.yml index 668e0d34..604630ee 100644 --- a/.github/workflows/ubuntu-2204.yml +++ b/.github/workflows/ubuntu-2204.yml @@ -6,25 +6,64 @@ on: - "release/*" pull_request: types: [assigned, opened, synchronize, reopened, labeled] + workflow_dispatch: env: - BUILDER_IMAGE: "apm2006/leap:v3.2.5-cdt4.0.1" + CDT_VERSION: "4.1.1" + CDT_DEB_SHA256: "d946e6b64f297442d19e486401aa49f956080b07a1fa6335f29976106175b588" + TELOSZERO_REF: "teloszero-v1.2.2" + EXPECTED_EOSIO_SYSTEM_ABI_SHA256: "5545f53b6b2e407acb45c6d14e283866c451d8e47c567baeb74284a55c93c9e1" + EXPECTED_EOSIO_SYSTEM_WASM_SHA256: "c9e1cf99636e489479d6b39f63489c9b22d60b9367842290ce2e102a648cbbbb" jobs: - ubuntu-2404-build: - name: Ubuntu 22.04 | Build - runs-on: ubuntu-latest + ubuntu-2204-build: + name: Ubuntu 22.04 | Spring/Telos Zero build + runs-on: ubuntu-22.04 + container: apm2006/leap:v3.2.5-cdt4.0.1 steps: - name: Checkout uses: actions/checkout@v4 - - name: Build + - name: Checkout TelosZero Core + uses: actions/checkout@v4 + with: + repository: TelosNetwork/teloszero-core + ref: ${{ env.TELOSZERO_REF }} + path: spring-src + submodules: recursive + - name: Install CDT + run: | + set -euo pipefail + cdt_deb="/tmp/cdt_${CDT_VERSION}-1_amd64.deb" + curl -L --retry 3 --fail \ + "https://github.com/AntelopeIO/cdt/releases/download/v${CDT_VERSION}/cdt_${CDT_VERSION}-1_amd64.deb" \ + -o "${cdt_deb}" + echo "${CDT_DEB_SHA256} ${cdt_deb}" | sha256sum -c - + dpkg -i "${cdt_deb}" + cdt-cpp --version | grep "${CDT_VERSION}" + test -f "/usr/opt/cdt/${CDT_VERSION}/include/eosio/instant_finality.hpp" + - name: Build TelosZero tester libraries + run: | + set -euo pipefail + cmake -S spring-src -B spring-build -DCMAKE_BUILD_TYPE=Release + cmake --build spring-build --target eosio_testing -- -j "$(nproc)" + test -f spring-build/libraries/testing/libeosio_testing.a + test -d spring-build/lib/cmake/leap + - name: Build contracts + run: | + set -euo pipefail + cmake -S . -B build \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_TESTS=yes \ + -Dleap_DIR="${PWD}/spring-build/lib/cmake/leap" + cmake --build build -- -j "$(nproc)" VERBOSE=1 + - name: Test + run: | + set -euo pipefail + ctest --test-dir build/tests --output-on-failure -j "$(nproc)" + - name: Assert release artifact hashes run: | - set -e - export DOCKER="docker run --rm -v $(pwd):/root/target ${BUILDER_IMAGE}" - docker pull ${BUILDER_IMAGE} - echo ${DOCKER} - echo ===== - mkdir build - ${DOCKER} bash -c "cd build && cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTS=yes -Dleap_DIR=/opt/leap/build/lib/cmake/leap .." - echo ===== - ${DOCKER} bash -c "cd build && make -j $(nproc) VERBOSE=1" - echo ===== - ${DOCKER} bash -c 'cd build/tests && ctest -j $(nproc)' + set -euo pipefail + abi_hash="$(sha256sum build/contracts/eosio.system/eosio.system.abi | awk '{print $1}')" + wasm_hash="$(sha256sum build/contracts/eosio.system/eosio.system.wasm | awk '{print $1}')" + echo "eosio.system.abi ${abi_hash}" + echo "eosio.system.wasm ${wasm_hash}" + test "${abi_hash}" = "${EXPECTED_EOSIO_SYSTEM_ABI_SHA256}" + test "${wasm_hash}" = "${EXPECTED_EOSIO_SYSTEM_WASM_SHA256}" diff --git a/contracts/CMakeLists.txt b/contracts/CMakeLists.txt index f66e1afc..daaeb683 100644 --- a/contracts/CMakeLists.txt +++ b/contracts/CMakeLists.txt @@ -11,7 +11,7 @@ option(SYSTEM_BLOCKCHAIN_PARAMETERS find_package(cdt) set(CDT_VERSION_MIN "3.0") -set(CDT_VERSION_SOFT_MAX "4.0") +set(CDT_VERSION_SOFT_MAX "4.1") # set(CDT_VERSION_HARD_MAX "") # Check the version of CDT diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8c0ab125..c0c5626a 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,26 +1,26 @@ cmake_minimum_required(VERSION 3.5) -set(EOSIO_VERSION_MIN "3.1") +set(EOSIO_VERSION_MIN "1.2") set(EOSIO_VERSION_SOFT_MAX "4.1") # set(EOSIO_VERSION_HARD_MAX "") find_package(leap) -# Check the version of Leap +# Check the version of the Spring/Leap tester package. set(VERSION_MATCH_ERROR_MSG "") eosio_check_version(VERSION_OUTPUT "${EOSIO_VERSION}" "${EOSIO_VERSION_MIN}" "${EOSIO_VERSION_SOFT_MAX}" "${EOSIO_VERSION_HARD_MAX}" VERSION_MATCH_ERROR_MSG) if(VERSION_OUTPUT STREQUAL "MATCH") - message(STATUS "Using Leap version ${EOSIO_VERSION}") + message(STATUS "Using Spring/Leap version ${EOSIO_VERSION}") elseif(VERSION_OUTPUT STREQUAL "WARN") message( WARNING - "Using Leap version ${EOSIO_VERSION} even though it exceeds the maximum supported version of ${EOSIO_VERSION_SOFT_MAX}; continuing with configuration, however build may fail.\nIt is recommended to use Leap version ${EOSIO_VERSION_SOFT_MAX}.x" + "Using Spring/Leap version ${EOSIO_VERSION} even though it exceeds the maximum supported version of ${EOSIO_VERSION_SOFT_MAX}; continuing with configuration, however build may fail.\nIt is recommended to use Spring/Leap version ${EOSIO_VERSION_SOFT_MAX}.x" ) else() # INVALID OR MISMATCH message( FATAL_ERROR - "Found Leap version ${EOSIO_VERSION} but it does not satisfy version requirements: ${VERSION_MATCH_ERROR_MSG}\nPlease use Leap version ${EOSIO_VERSION_SOFT_MAX}.x" + "Found Spring/Leap version ${EOSIO_VERSION} but it does not satisfy version requirements: ${VERSION_MATCH_ERROR_MSG}\nPlease use Spring/Leap version ${EOSIO_VERSION_SOFT_MAX}.x" ) endif(VERSION_OUTPUT STREQUAL "MATCH") @@ -38,7 +38,9 @@ foreach(TEST_SUITE ${UNIT_TESTS}) # create an independent target for each test s execute_process( COMMAND bash -c - "grep -E 'BOOST_AUTO_TEST_SUITE\\s*[(]' ${TEST_SUITE} | grep -vE '//.*BOOST_AUTO_TEST_SUITE\\s*[(]' | cut -d ')' -f 1 | cut -d '(' -f 2" + "grep -E 'BOOST_AUTO_TEST_SUITE\\s*[(]' \"$1\" | grep -vE '//.*BOOST_AUTO_TEST_SUITE\\s*[(]' | cut -d ')' -f 1 | cut -d '(' -f 2" + _ + "${TEST_SUITE}" OUTPUT_VARIABLE SUITE_NAME OUTPUT_STRIP_TRAILING_WHITESPACE) # get the test suite name from the *.cpp file if(NOT "" STREQUAL "${SUITE_NAME}") # ignore empty lines diff --git a/tests/eosio.limitauth_tests.cpp b/tests/eosio.limitauth_tests.cpp index ded793c2..c873d9ce 100644 --- a/tests/eosio.limitauth_tests.cpp +++ b/tests/eosio.limitauth_tests.cpp @@ -1,4 +1,3 @@ -#include #include #include #include diff --git a/tests/eosio.msig_tests.cpp b/tests/eosio.msig_tests.cpp index f3a9f968..108f1376 100644 --- a/tests/eosio.msig_tests.cpp +++ b/tests/eosio.msig_tests.cpp @@ -4,8 +4,6 @@ #include #include -#include - #include #include "contracts.hpp" #include "test_symbol.hpp" @@ -18,7 +16,7 @@ using namespace fc; using mvo = fc::mutable_variant_object; -class eosio_msig_tester : public tester { +class eosio_msig_tester : public legacy_validating_tester { public: eosio_msig_tester() { create_accounts( { "eosio.msig"_n, "eosio.stake"_n, "eosio.ram"_n, "eosio.ramfee"_n, "alice"_n, "bob"_n, "carol"_n } ); diff --git a/tests/eosio.system_tester.hpp b/tests/eosio.system_tester.hpp index 91c374e8..9c1e0e71 100644 --- a/tests/eosio.system_tester.hpp +++ b/tests/eosio.system_tester.hpp @@ -19,7 +19,7 @@ using mvo = fc::mutable_variant_object; #ifdef NON_VALIDATING_TEST #define TESTER tester #else -#define TESTER validating_tester +#define TESTER legacy_validating_tester #endif #endif @@ -1096,6 +1096,17 @@ class eosio_system_tester : public TESTER { return data.empty() ? fc::variant() : abi_ser.binary_to_variant( "refund_request", data, abi_serializer::create_yield_function(abi_serializer_max_time) ); } + action_result refund( name owner ) { + return push_action( owner, "refund"_n, mvo() + ("owner", owner) ); + } + + action_result bidrefund( name bidder, name newname ) { + return push_action( bidder, "bidrefund"_n, mvo() + ("bidder", bidder) + ("newname", newname) ); + } + abi_serializer initialize_multisig() { abi_serializer msig_abi_ser; { @@ -1193,7 +1204,7 @@ class eosio_system_tester : public TESTER { } produce_blocks( 250 ); - auto producer_keys = control->head_block_state()->active_schedule.producers; + auto producer_keys = control->head_block_state_legacy()->active_schedule.producers; BOOST_REQUIRE_EQUAL( 21, producer_keys.size() ); BOOST_REQUIRE_EQUAL( name("defproducera"), producer_keys[0].producer_name ); @@ -1249,7 +1260,7 @@ class eosio_system_tester : public TESTER { } produce_blocks( 250 ); - auto producer_keys = control->head_block_state()->active_schedule.producers; + auto producer_keys = control->head_block_state_legacy()->active_schedule.producers; BOOST_REQUIRE_EQUAL( 21, producer_keys.size() ); BOOST_REQUIRE_EQUAL( "tprodaaaaaaa"_n, producer_keys[0].producer_name ); diff --git a/tests/eosio.system_tests.cpp b/tests/eosio.system_tests.cpp index 997c316f..486003fc 100644 --- a/tests/eosio.system_tests.cpp +++ b/tests/eosio.system_tests.cpp @@ -8,7 +8,6 @@ #include #include #include -#include #include "eosio.system_tester.hpp" struct _abi_hash { @@ -207,6 +206,7 @@ BOOST_FIXTURE_TEST_CASE( stake_unstake, eosio_system_tester ) try { //after 3 days funds should be released produce_block( fc::hours(1) ); produce_blocks(1); + BOOST_REQUIRE_EQUAL( success(), refund( "alice1111111"_n ) ); BOOST_REQUIRE_EQUAL( core_sym::from_string("1000.0000"), get_balance( "alice1111111" ) ); BOOST_REQUIRE_EQUAL( init_eosio_stake_balance, get_balance( "eosio.stake"_n ) ); @@ -236,6 +236,7 @@ BOOST_FIXTURE_TEST_CASE( stake_unstake, eosio_system_tester ) try { //after 3 days funds should be released produce_block( fc::hours(1) ); produce_blocks(1); + BOOST_REQUIRE_EQUAL( success(), refund( "alice1111111"_n ) ); REQUIRE_MATCHING_OBJECT( voter( "alice1111111", core_sym::from_string("0.0000") ), get_voter_info( "alice1111111" ) ); produce_blocks(1); @@ -285,6 +286,7 @@ BOOST_FIXTURE_TEST_CASE( stake_unstake_with_transfer, eosio_system_tester ) try produce_block( fc::hours(1) ); produce_blocks(1); + BOOST_REQUIRE_EQUAL( success(), refund( "alice1111111"_n ) ); BOOST_REQUIRE_EQUAL( core_sym::from_string("1300.0000"), get_balance( "alice1111111" ) ); //stake should be equal to what was staked in constructor, voting power should be 0 @@ -355,6 +357,7 @@ BOOST_FIXTURE_TEST_CASE( stake_while_pending_refund, eosio_system_tester ) try { produce_block( fc::hours(1) ); produce_blocks(1); + BOOST_REQUIRE_EQUAL( success(), refund( "alice1111111"_n ) ); BOOST_REQUIRE_EQUAL( core_sym::from_string("1300.0000"), get_balance( "alice1111111" ) ); //stake should be equal to what was staked in constructor, voting power should be 0 @@ -631,6 +634,7 @@ BOOST_FIXTURE_TEST_CASE( adding_stake_partial_unstake, eosio_system_tester ) try BOOST_REQUIRE_EQUAL( core_sym::from_string("550.0000"), get_balance( "alice1111111" ) ); produce_block( fc::days(1) ); produce_blocks(1); + BOOST_REQUIRE_EQUAL( success(), refund( "alice1111111"_n ) ); BOOST_REQUIRE_EQUAL( core_sym::from_string("850.0000"), get_balance( "alice1111111" ) ); } FC_LOG_AND_RETHROW() @@ -1120,6 +1124,7 @@ BOOST_FIXTURE_TEST_CASE( vote_for_producer, eosio_system_tester, * boost::unit_t //carol1111111 should receive funds in 3 days produce_block( fc::days(3) ); produce_block(); + BOOST_REQUIRE_EQUAL( success(), refund( "carol1111111"_n ) ); BOOST_REQUIRE_EQUAL( core_sym::from_string("3000.0000"), get_balance( "carol1111111" ) ); } FC_LOG_AND_RETHROW() @@ -2880,7 +2885,7 @@ BOOST_FIXTURE_TEST_CASE(producers_upgrade_system_contract, eosio_system_tester) ); transaction_trace_ptr trace; - control->applied_transaction.connect( + control->applied_transaction().connect( [&]( std::tuple p ) { trace = std::get<0>(p); } ); @@ -3238,7 +3243,7 @@ BOOST_FIXTURE_TEST_CASE( elect_producers /*_and_parameters*/, eosio_system_teste activate_network(); // TELOS END produce_blocks(250); - auto producer_keys = control->head_block_state()->active_schedule.producers; + auto producer_keys = control->head_block_state_legacy()->active_schedule.producers; BOOST_REQUIRE_EQUAL( 1, producer_keys.size() ); BOOST_REQUIRE_EQUAL( name("defproducer1"), producer_keys[0].producer_name ); @@ -3254,7 +3259,7 @@ BOOST_FIXTURE_TEST_CASE( elect_producers /*_and_parameters*/, eosio_system_teste BOOST_REQUIRE_EQUAL( success(), vote( "bob111111111"_n, { "defproducer2"_n } ) ); ilog("."); produce_blocks(250); - producer_keys = control->head_block_state()->active_schedule.producers; + producer_keys = control->head_block_state_legacy()->active_schedule.producers; BOOST_REQUIRE_EQUAL( 2, producer_keys.size() ); BOOST_REQUIRE_EQUAL( name("defproducer1"), producer_keys[0].producer_name ); BOOST_REQUIRE_EQUAL( name("defproducer2"), producer_keys[1].producer_name ); @@ -3265,7 +3270,7 @@ BOOST_FIXTURE_TEST_CASE( elect_producers /*_and_parameters*/, eosio_system_teste // elect 3 producers BOOST_REQUIRE_EQUAL( success(), vote( "bob111111111"_n, { "defproducer2"_n, "defproducer3"_n } ) ); produce_blocks(250); - producer_keys = control->head_block_state()->active_schedule.producers; + producer_keys = control->head_block_state_legacy()->active_schedule.producers; BOOST_REQUIRE_EQUAL( 3, producer_keys.size() ); BOOST_REQUIRE_EQUAL( name("defproducer1"), producer_keys[0].producer_name ); BOOST_REQUIRE_EQUAL( name("defproducer2"), producer_keys[1].producer_name ); @@ -3276,7 +3281,7 @@ BOOST_FIXTURE_TEST_CASE( elect_producers /*_and_parameters*/, eosio_system_teste // try to go back to 2 producers and fail BOOST_REQUIRE_EQUAL( success(), vote( "bob111111111"_n, { "defproducer3"_n } ) ); produce_blocks(250); - producer_keys = control->head_block_state()->active_schedule.producers; + producer_keys = control->head_block_state_legacy()->active_schedule.producers; BOOST_REQUIRE_EQUAL( 3, producer_keys.size() ); // The test below is invalid now, producer schedule is not updated if there are @@ -3393,6 +3398,7 @@ BOOST_FIXTURE_TEST_CASE( multiple_namebids, eosio_system_tester ) try { const asset initial_names_balance = get_balance("eosio.names"_n); BOOST_REQUIRE_EQUAL( success(), bidname( "alice", "prefb", core_sym::from_string("1.1001") ) ); + BOOST_REQUIRE_EQUAL( success(), bidrefund( "bob"_n, "prefb"_n ) ); BOOST_REQUIRE_EQUAL( core_sym::from_string( "9997.9997" ), get_balance("bob") ); BOOST_REQUIRE_EQUAL( core_sym::from_string( "9998.8999" ), get_balance("alice") ); BOOST_REQUIRE_EQUAL( initial_names_balance + core_sym::from_string("0.1001"), get_balance("eosio.names"_n) ); @@ -3404,6 +3410,7 @@ BOOST_FIXTURE_TEST_CASE( multiple_namebids, eosio_system_tester ) try { BOOST_REQUIRE_EQUAL( core_sym::from_string( "10000.0000" ), get_balance("david") ); BOOST_REQUIRE_EQUAL( success(), bidname( "david", "prefd", core_sym::from_string("1.9900") ) ); + BOOST_REQUIRE_EQUAL( success(), bidrefund( "carl"_n, "prefd"_n ) ); BOOST_REQUIRE_EQUAL( core_sym::from_string( "9999.0000" ), get_balance("carl") ); BOOST_REQUIRE_EQUAL( core_sym::from_string( "9998.0100" ), get_balance("david") ); } @@ -3687,7 +3694,7 @@ BOOST_FIXTURE_TEST_CASE( setparams, eosio_system_tester ) try { } transaction_trace_ptr trace; - control->applied_transaction.connect( + control->applied_transaction().connect( [&]( std::tuple p ) { trace = std::get<0>(p); } ); @@ -3777,7 +3784,7 @@ BOOST_FIXTURE_TEST_CASE( wasmcfg, eosio_system_tester ) try { } transaction_trace_ptr trace; - control->applied_transaction.connect( + control->applied_transaction().connect( [&]( std::tuple p ) { trace = std::get<0>(p); } ); @@ -4871,6 +4878,7 @@ BOOST_FIXTURE_TEST_CASE( ramfee_namebid_to_rex, eosio_system_tester ) try { BOOST_REQUIRE_EQUAL( success(), bidname( carol, "rndmbid"_n, core_sym::from_string("23.7000") ) ); BOOST_REQUIRE_EQUAL( core_sym::from_string("23.7000"), get_balance( "eosio.names"_n ) ); BOOST_REQUIRE_EQUAL( success(), bidname( alice, "rndmbid"_n, core_sym::from_string("29.3500") ) ); + BOOST_REQUIRE_EQUAL( success(), bidrefund( carol, "rndmbid"_n ) ); BOOST_REQUIRE_EQUAL( core_sym::from_string("29.3500"), get_balance( "eosio.names"_n )); produce_block( fc::hours(24) ); diff --git a/tests/eosio.token_tests.cpp b/tests/eosio.token_tests.cpp index 68e7d554..00925610 100644 --- a/tests/eosio.token_tests.cpp +++ b/tests/eosio.token_tests.cpp @@ -3,8 +3,6 @@ #include #include "eosio.system_tester.hpp" -#include "Runtime/Runtime.h" - #include using namespace eosio::testing; @@ -16,7 +14,7 @@ using namespace std; using mvo = fc::mutable_variant_object; -class eosio_token_tester : public tester { +class eosio_token_tester : public legacy_validating_tester { public: eosio_token_tester() { diff --git a/tests/eosio.wrap_tests.cpp b/tests/eosio.wrap_tests.cpp index 8f694c8f..324adfa2 100644 --- a/tests/eosio.wrap_tests.cpp +++ b/tests/eosio.wrap_tests.cpp @@ -2,8 +2,6 @@ #include #include -#include - #include #include "contracts.hpp" @@ -16,7 +14,7 @@ using namespace fc; using mvo = fc::mutable_variant_object; -class eosio_wrap_tester : public tester { +class eosio_wrap_tester : public legacy_validating_tester { public: eosio_wrap_tester() { diff --git a/tests/main.cpp b/tests/main.cpp index e3d61750..44d671db 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -8,12 +8,10 @@ #include #include #include -#include #include "eosio.system_tester.hpp" using namespace eosio_system; -#define BOOST_TEST_STATIC_LINK void translate_fc_exception(const fc::exception &e) { std::cerr << "\033[33m" << e.to_detail_string() << "\033[0m" << std::endl; From 79a0e8dbad342f93ce215fa0a90feb43dc28daae Mon Sep 17 00:00:00 2001 From: Justin Giudici Date: Wed, 10 Jun 2026 17:36:25 +0000 Subject: [PATCH 05/20] Make schedule metrics comparison membership-based Positional comparison could spuriously rebuild schedule metrics (wiping missed-block counters) when producers sharing a location value swapped sort order, or when a producer changed location without membership changing. All consumers of producers_metric are name-keyed, so compare sorted name sets instead. Also document why set_proposed_finalizers is not gated on set_proposed_producers success. --- contracts/eosio.system/src/voting.cpp | 35 +++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/contracts/eosio.system/src/voting.cpp b/contracts/eosio.system/src/voting.cpp index adf349a9..591c3a52 100644 --- a/contracts/eosio.system/src/voting.cpp +++ b/contracts/eosio.system/src/voting.cpp @@ -169,18 +169,36 @@ namespace eosiosystem { producers.push_back( std::move(item.first) ); // TELOS BEGIN + // Only rebuild schedule metrics when schedule *membership* changes. + // The comparison uses sorted producer names rather than positions: + // top_producers is sorted by location (with possible ties, and a + // non-stable sort over vote-ordered input), so positional order can + // differ between proposals even when membership is identical. All + // consumers of producers_metric are name-keyed, so order is irrelevant. + // Under Savanna, set_proposed_producers() succeeds even for unchanged + // schedules; rebuilding metrics on every successful proposal would wipe + // missed-block counters before autokick could reach its threshold. auto schedule_metrics_changed = [&]() { if( _gschedule_metrics.producers_metric.size() != top_producers.size() ) { return true; } - for( size_t i = 0; i < top_producers.size(); ++i ) { - if( _gschedule_metrics.producers_metric[i].bp_name != top_producers[i].first.producer_name ) { - return true; - } + std::vector prev_names; + prev_names.reserve(_gschedule_metrics.producers_metric.size()); + for( const auto& metric: _gschedule_metrics.producers_metric ) { + prev_names.emplace_back(metric.bp_name); + } + + std::vector new_names; + new_names.reserve(top_producers.size()); + for( const auto& tp: top_producers ) { + new_names.emplace_back(tp.first.producer_name); } - return false; + std::sort(prev_names.begin(), prev_names.end()); + std::sort(new_names.begin(), new_names.end()); + + return prev_names != new_names; }; auto schedule_version = set_proposed_producers(producers); @@ -204,6 +222,13 @@ namespace eosiosystem { _gstate.last_producer_schedule_size = static_cast(top_producers.size()); } + // Propose the finalizer policy for the new top producers. This is + // intentionally not gated on set_proposed_producers() succeeding, to + // match upstream reference-contracts behavior: set_proposed_finalizers() + // is itself a no-op when the policy is unchanged, and proposing here + // ensures the finalizer policy converges with the producer schedule on + // the next successful proposal even if this one was rejected (e.g. a + // prior pending schedule has not activated yet). if( is_savanna ) { set_proposed_finalizers( get_finalizers_for_producers(top_producers) ); } From 39fb3db6007f685dae6044282cf065dfe5007cca Mon Sep 17 00:00:00 2001 From: Justin Giudici Date: Wed, 10 Jun 2026 17:36:25 +0000 Subject: [PATCH 06/20] Remove deferred refund transactions for Spring compatibility Deferred transactions are disabled under Spring/Savanna: the deferred refund in changebw and the deferred bidrefund in bidname would silently never execute, and may hard-fail once DISABLE_DEFERRED_TRXS protocol features are active. Refunds remain in their tables and must be claimed explicitly via the existing refund/bidrefund actions, matching upstream reference-contracts. The PR's test changes already assumed this behavior. --- .../eosio.system/src/delegate_bandwidth.cpp | 21 ++++--------------- contracts/eosio.system/src/name_bidding.cpp | 13 ++++-------- 2 files changed, 8 insertions(+), 26 deletions(-) diff --git a/contracts/eosio.system/src/delegate_bandwidth.cpp b/contracts/eosio.system/src/delegate_bandwidth.cpp index f828f1f0..bc11539b 100644 --- a/contracts/eosio.system/src/delegate_bandwidth.cpp +++ b/contracts/eosio.system/src/delegate_bandwidth.cpp @@ -263,7 +263,6 @@ namespace eosiosystem { //create/update/delete refund auto net_balance = stake_net_delta; auto cpu_balance = stake_cpu_delta; - bool need_deferred_trx = false; // net and cpu are same sign by assertions in delegatebw and undelegatebw @@ -298,9 +297,6 @@ namespace eosiosystem { if ( req->is_empty() ) { refunds_tbl.erase( req ); - need_deferred_trx = false; - } else { - need_deferred_trx = true; } } else if ( net_balance.amount < 0 || cpu_balance.amount < 0 ) { //need to create refund refunds_tbl.emplace( from, [&]( refund_request& r ) { @@ -319,22 +315,13 @@ namespace eosiosystem { } r.request_time = current_time_point(); }); - need_deferred_trx = true; } // else stake increase requested with no existing row in refunds_tbl -> nothing to do with refunds_tbl } /// end if is_delegating_to_self || is_undelegating - if ( need_deferred_trx ) { - eosio::transaction out; - out.actions.emplace_back( permission_level{from, active_permission}, - get_self(), "refund"_n, - from - ); - out.delay_sec = refund_delay_sec; - eosio::cancel_deferred( from.value ); // TODO: Remove this line when replacing deferred transactions is fixed - out.send( from.value, from, true ); - } else { - eosio::cancel_deferred( from.value ); - } + // Deferred transactions are disabled under Spring/Savanna, so the + // refund is no longer scheduled automatically. Any pending refund + // remains in refunds_tbl and must be claimed explicitly with the + // refund action once refund_delay_sec has elapsed. auto transfer_amount = net_balance + cpu_balance; if ( 0 < transfer_amount.amount ) { diff --git a/contracts/eosio.system/src/name_bidding.cpp b/contracts/eosio.system/src/name_bidding.cpp index 4c01f353..866b86aa 100644 --- a/contracts/eosio.system/src/name_bidding.cpp +++ b/contracts/eosio.system/src/name_bidding.cpp @@ -49,15 +49,10 @@ namespace eosiosystem { }); } - eosio::transaction t; - t.actions.emplace_back( permission_level{current->high_bidder, active_permission}, - get_self(), "bidrefund"_n, - std::make_tuple( current->high_bidder, newname ) - ); - t.delay_sec = 0; - uint128_t deferred_id = (uint128_t(newname.value) << 64) | current->high_bidder.value; - eosio::cancel_deferred( deferred_id ); - t.send( deferred_id, bidder ); + // Deferred transactions are disabled under Spring/Savanna, so the + // outbid bidder is no longer refunded automatically. The refund + // remains in bid_refund_table and must be claimed explicitly with + // the bidrefund action. bids.modify( current, bidder, [&]( auto& b ) { b.high_bidder = bidder; From 817a377721b97effa13541c3a91b0060eebdac87 Mon Sep 17 00:00:00 2001 From: Justin Giudici Date: Wed, 10 Jun 2026 17:36:25 +0000 Subject: [PATCH 07/20] Gate release hash assertion and pin TelosZero ref by commit Assert expected eosio.system artifact hashes only on workflow_dispatch and release branches so regular PRs do not fail CI on every legitimate contract change; hashes are still printed on every run. Pin TELOSZERO_REF to the commit SHA of teloszero-v1.2.2 since tags are mutable. --- .github/workflows/ubuntu-2204.yml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ubuntu-2204.yml b/.github/workflows/ubuntu-2204.yml index 604630ee..8f660760 100644 --- a/.github/workflows/ubuntu-2204.yml +++ b/.github/workflows/ubuntu-2204.yml @@ -10,7 +10,8 @@ on: env: CDT_VERSION: "4.1.1" CDT_DEB_SHA256: "d946e6b64f297442d19e486401aa49f956080b07a1fa6335f29976106175b588" - TELOSZERO_REF: "teloszero-v1.2.2" + # Commit SHA of tag teloszero-v1.2.2 (tags are mutable; pin the commit) + TELOSZERO_REF: "fcb9f5fc627619a8b06cd6b871c462b1d70d3aa4" EXPECTED_EOSIO_SYSTEM_ABI_SHA256: "5545f53b6b2e407acb45c6d14e283866c451d8e47c567baeb74284a55c93c9e1" EXPECTED_EOSIO_SYSTEM_WASM_SHA256: "c9e1cf99636e489479d6b39f63489c9b22d60b9367842290ce2e102a648cbbbb" jobs: @@ -58,12 +59,21 @@ jobs: run: | set -euo pipefail ctest --test-dir build/tests --output-on-failure -j "$(nproc)" - - name: Assert release artifact hashes + - name: Report build artifact hashes run: | set -euo pipefail abi_hash="$(sha256sum build/contracts/eosio.system/eosio.system.abi | awk '{print $1}')" wasm_hash="$(sha256sum build/contracts/eosio.system/eosio.system.wasm | awk '{print $1}')" echo "eosio.system.abi ${abi_hash}" echo "eosio.system.wasm ${wasm_hash}" + # Only enforce the expected release hashes on manually-dispatched runs + # and release branches. Asserting on every PR would fail CI for any + # legitimate contract change until the expected hashes are updated. + - name: Assert release artifact hashes + if: github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/heads/release/') + run: | + set -euo pipefail + abi_hash="$(sha256sum build/contracts/eosio.system/eosio.system.abi | awk '{print $1}')" + wasm_hash="$(sha256sum build/contracts/eosio.system/eosio.system.wasm | awk '{print $1}')" test "${abi_hash}" = "${EXPECTED_EOSIO_SYSTEM_ABI_SHA256}" test "${wasm_hash}" = "${EXPECTED_EOSIO_SYSTEM_WASM_SHA256}" From 0899a807caba8c0d831010e31f260f86f5801b0a Mon Sep 17 00:00:00 2001 From: Justin Giudici Date: Wed, 10 Jun 2026 17:36:25 +0000 Subject: [PATCH 08/20] Add finalizer key and Savanna transition tests Covers regfinkey validation (registration, PoP, duplicates), the actfinkey/delfinkey lifecycle, switchtosvnn authorization and guard checks plus the one-way transition, and a regression test asserting schedule metrics survive no-op schedule proposals under Savanna (the autokick fix). Authored without a local Spring build; needs a CI run to verify. --- tests/eosio.system_finalizer_key_tests.cpp | 225 +++++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 tests/eosio.system_finalizer_key_tests.cpp diff --git a/tests/eosio.system_finalizer_key_tests.cpp b/tests/eosio.system_finalizer_key_tests.cpp new file mode 100644 index 00000000..b783c0e7 --- /dev/null +++ b/tests/eosio.system_finalizer_key_tests.cpp @@ -0,0 +1,225 @@ +#include +#include +#include +#include + +#include "eosio.system_tester.hpp" + +/* + * Tests for the Savanna finalizer key actions (regfinkey, actfinkey, + * delfinkey) and the switchtosvnn consensus transition added in PR #59. + * + * NOTE: written against the Spring (TelosZero core) tester. These tests + * were authored without a local Spring build available and need a CI run + * to verify; they are modeled on AntelopeIO/reference-contracts + * eosio.system finalizer key tests and the local Telos tester helpers. + */ + +using namespace eosio_system; + +struct finalizer_key_tester : eosio_system_tester { + static fc::crypto::blslib::bls_private_key new_bls_key() { + return fc::crypto::blslib::bls_private_key::generate(); + } + + fc::variant get_finalizer_info( const account_name& act ) { + vector data = get_row_by_account( config::system_account_name, config::system_account_name, "finalizers"_n, act ); + return data.empty() ? fc::variant() : abi_ser.binary_to_variant( "finalizer_info", data, abi_serializer::create_yield_function(abi_serializer_max_time) ); + } + + fc::variant get_last_prop_finalizers() { + vector data = get_row_by_account( config::system_account_name, config::system_account_name, "lastpropfins"_n, account_name(0) ); + return data.empty() ? fc::variant() : abi_ser.binary_to_variant( "last_prop_finalizers_info", data, abi_serializer::create_yield_function(abi_serializer_max_time) ); + } + + action_result regfinkey( const account_name& finalizer, const std::string& key, const std::string& pop ) { + return push_action( finalizer, "regfinkey"_n, mvo() + ("finalizer_name", finalizer) + ("finalizer_key", key) + ("proof_of_possession", pop) ); + } + + action_result actfinkey( const account_name& finalizer, const std::string& key ) { + return push_action( finalizer, "actfinkey"_n, mvo() + ("finalizer_name", finalizer) + ("finalizer_key", key) ); + } + + action_result delfinkey( const account_name& finalizer, const std::string& key ) { + return push_action( finalizer, "delfinkey"_n, mvo() + ("finalizer_name", finalizer) + ("finalizer_key", key) ); + } + + action_result switchtosvnn( const account_name& signer ) { + return push_action( signer, "switchtosvnn"_n, mvo() ); + } +}; + +BOOST_AUTO_TEST_SUITE(eosio_system_finalizer_key_tests) + +BOOST_FIXTURE_TEST_CASE( regfinkey_validation, finalizer_key_tester ) try { + const account_name alice = "alice1111111"_n; + const account_name prod = "defproducer1"_n; + + create_account_with_resources( prod, config::system_account_name, core_sym::from_string("10.0000"), false ); + transfer( config::system_account_name, prod, core_sym::from_string("1000.0000"), config::system_account_name ); + BOOST_REQUIRE_EQUAL( success(), stake( prod, prod, core_sym::from_string("100.0000"), core_sym::from_string("100.0000") ) ); + BOOST_REQUIRE_EQUAL( success(), regproducer( prod ) ); + + const auto key = new_bls_key(); + const std::string pubkey = key.get_public_key().to_string(); + const std::string pop = key.proof_of_possession().to_string(); + + // not a registered producer + BOOST_REQUIRE_EQUAL( wasm_assert_msg( "finalizer alice1111111 is not a registered producer" ), + regfinkey( alice, pubkey, pop ) ); + + // malformed key / pop prefixes + BOOST_REQUIRE_EQUAL( wasm_assert_msg( "finalizer key does not start with PUB_BLS: UNKNOWN_" + pubkey.substr(8) ), + regfinkey( prod, "UNKNOWN_" + pubkey.substr(8), pop ) ); + BOOST_REQUIRE_EQUAL( wasm_assert_msg( "proof of possession signature does not start with SIG_BLS: UNKNOWN_" + pop.substr(8) ), + regfinkey( prod, pubkey, "UNKNOWN_" + pop.substr(8) ) ); + + // proof of possession must match the key + const auto other = new_bls_key(); + BOOST_REQUIRE_EQUAL( wasm_assert_msg( "proof of possession check failed" ), + regfinkey( prod, pubkey, other.proof_of_possession().to_string() ) ); + + // success + BOOST_REQUIRE_EQUAL( success(), regfinkey( prod, pubkey, pop ) ); + auto fin = get_finalizer_info( prod ); + BOOST_REQUIRE_EQUAL( fin["finalizer_name"].as_string(), prod.to_string() ); + BOOST_REQUIRE_EQUAL( fin["finalizer_key_count"].as(), 1u ); + + // duplicate key rejected + BOOST_REQUIRE_EQUAL( wasm_assert_msg( "duplicate finalizer key: " + pubkey ), + regfinkey( prod, pubkey, pop ) ); +} FC_LOG_AND_RETHROW() + +BOOST_FIXTURE_TEST_CASE( finalizer_key_lifecycle, finalizer_key_tester ) try { + const account_name prod = "defproducer1"_n; + + create_account_with_resources( prod, config::system_account_name, core_sym::from_string("10.0000"), false ); + transfer( config::system_account_name, prod, core_sym::from_string("1000.0000"), config::system_account_name ); + BOOST_REQUIRE_EQUAL( success(), stake( prod, prod, core_sym::from_string("100.0000"), core_sym::from_string("100.0000") ) ); + BOOST_REQUIRE_EQUAL( success(), regproducer( prod ) ); + + const auto key1 = new_bls_key(); + const auto key2 = new_bls_key(); + const std::string pub1 = key1.get_public_key().to_string(); + const std::string pub2 = key2.get_public_key().to_string(); + + BOOST_REQUIRE_EQUAL( success(), regfinkey( prod, pub1, key1.proof_of_possession().to_string() ) ); + BOOST_REQUIRE_EQUAL( success(), regfinkey( prod, pub2, key2.proof_of_possession().to_string() ) ); + BOOST_REQUIRE_EQUAL( get_finalizer_info( prod )["finalizer_key_count"].as(), 2u ); + + // first registered key is the active one; activating it again fails + BOOST_REQUIRE_EQUAL( wasm_assert_msg( "finalizer key was already active: " + pub1 ), + actfinkey( prod, pub1 ) ); + + // unregistered key cannot be activated + const auto key3 = new_bls_key(); + BOOST_REQUIRE_EQUAL( wasm_assert_msg( "finalizer key was not registered: " + key3.get_public_key().to_string() ), + actfinkey( prod, key3.get_public_key().to_string() ) ); + + // switch active key to key2 + BOOST_REQUIRE_EQUAL( success(), actfinkey( prod, pub2 ) ); + + // an active key cannot be deleted while other keys remain + BOOST_REQUIRE_EQUAL( wasm_assert_msg( "cannot delete an active key unless it is the last registered finalizer key, has 2 keys" ), + delfinkey( prod, pub2 ) ); + + // delete the non-active key, then the last (active) key + BOOST_REQUIRE_EQUAL( success(), delfinkey( prod, pub1 ) ); + BOOST_REQUIRE_EQUAL( get_finalizer_info( prod )["finalizer_key_count"].as(), 1u ); + BOOST_REQUIRE_EQUAL( success(), delfinkey( prod, pub2 ) ); + BOOST_REQUIRE( get_finalizer_info( prod ).is_null() ); +} FC_LOG_AND_RETHROW() + +BOOST_FIXTURE_TEST_CASE( switchtosvnn_guards_and_transition, finalizer_key_tester ) try { + // requires the system account's authority + BOOST_REQUIRE_EQUAL( error( "missing authority of eosio" ), + switchtosvnn( "alice1111111"_n ) ); + + // establish an active 21-producer schedule + auto producer_names = active_and_vote_producers(); + + // every scheduled producer must have an active finalizer key + BOOST_REQUIRE_EQUAL( wasm_assert_msg( "producer " + producer_names[0].to_string() + " does not have an active finalizer key" ), + switchtosvnn( config::system_account_name ) ); + + // register a finalizer key for each scheduled producer + for( const auto& p : producer_names ) { + const auto key = new_bls_key(); + BOOST_REQUIRE_EQUAL( success(), + regfinkey( p, key.get_public_key().to_string(), key.proof_of_possession().to_string() ) ); + } + + BOOST_REQUIRE_EQUAL( success(), switchtosvnn( config::system_account_name ) ); + + // first finalizer policy stored, one finalizer per scheduled producer + auto lpf = get_last_prop_finalizers(); + BOOST_REQUIRE( !lpf.is_null() ); + BOOST_REQUIRE_EQUAL( lpf["last_proposed_finalizers"].get_array().size(), producer_names.size() ); + + // transition proceeds + produce_blocks( 10 ); + + // one-way switch + BOOST_REQUIRE_EQUAL( wasm_assert_msg( "switchtosvnn can be run only once" ), + switchtosvnn( config::system_account_name ) ); +} FC_LOG_AND_RETHROW() + +// Regression for the Savanna autokick fix: once on Savanna, +// set_proposed_producers() succeeds even when the schedule is unchanged. +// Schedule metrics (and their in-flight missed-block counters) must survive +// those no-op proposals instead of being rebuilt every scheduling round. +BOOST_FIXTURE_TEST_CASE( savanna_schedule_metrics_preserved, finalizer_key_tester ) try { + auto producer_names = active_and_vote_producers(); + + for( const auto& p : producer_names ) { + const auto key = new_bls_key(); + BOOST_REQUIRE_EQUAL( success(), + regfinkey( p, key.get_public_key().to_string(), key.proof_of_possession().to_string() ) ); + } + BOOST_REQUIRE_EQUAL( success(), switchtosvnn( config::system_account_name ) ); + produce_blocks( 10 ); + + auto get_metrics = [&]() { + vector data = get_row_by_account( config::system_account_name, config::system_account_name, + "schedulemetr"_n, "schedulemetr"_n ); + BOOST_REQUIRE( !data.empty() ); + return abi_ser.binary_to_variant( "schedule_metrics_state", data, abi_serializer::create_yield_function(abi_serializer_max_time) ); + }; + + auto before = get_metrics(); + const auto before_metrics = before["producers_metric"].get_array(); + BOOST_REQUIRE_EQUAL( before_metrics.size(), producer_names.size() ); + + // cross several update_elected_producers() rounds (one per ~120 slots) + // with unchanged membership + produce_blocks( 500 ); + + auto after = get_metrics(); + const auto after_metrics = after["producers_metric"].get_array(); + + // membership must be unchanged - the metrics vector must not have been + // rebuilt (a rebuild resets every producer's counters to 12, which was + // the bug that disabled autokick under Savanna) + BOOST_REQUIRE_EQUAL( after_metrics.size(), before_metrics.size() ); + + // at least one producer should be mid-cycle (counter != 12) at any given + // instant while blocks are being produced; if every single counter reads + // exactly 12 the metrics were just wiped + bool any_mid_cycle = false; + for( const auto& m : after_metrics ) { + if( m["missed_blocks_per_cycle"].as() != 12 ) { + any_mid_cycle = true; + break; + } + } + BOOST_REQUIRE( any_mid_cycle ); +} FC_LOG_AND_RETHROW() + +BOOST_AUTO_TEST_SUITE_END() From de46331fc5e8563ccd2790af2982f2308eaf8da8 Mon Sep 17 00:00:00 2001 From: Justin Giudici Date: Wed, 10 Jun 2026 19:12:49 +0000 Subject: [PATCH 09/20] Update expected eosio.system WASM hash for fixed contract Rebuilt with CDT 4.1.1 after the schedule-metrics and deferred-refund fixes. ABI hash is unchanged (5545f53b...) confirming the fixes are ABI-neutral; the WASM hash necessarily changed with the code. Verified reproducible across two independent builds. --- .github/workflows/ubuntu-2204.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ubuntu-2204.yml b/.github/workflows/ubuntu-2204.yml index 8f660760..4f61eae9 100644 --- a/.github/workflows/ubuntu-2204.yml +++ b/.github/workflows/ubuntu-2204.yml @@ -13,7 +13,7 @@ env: # Commit SHA of tag teloszero-v1.2.2 (tags are mutable; pin the commit) TELOSZERO_REF: "fcb9f5fc627619a8b06cd6b871c462b1d70d3aa4" EXPECTED_EOSIO_SYSTEM_ABI_SHA256: "5545f53b6b2e407acb45c6d14e283866c451d8e47c567baeb74284a55c93c9e1" - EXPECTED_EOSIO_SYSTEM_WASM_SHA256: "c9e1cf99636e489479d6b39f63489c9b22d60b9367842290ce2e102a648cbbbb" + EXPECTED_EOSIO_SYSTEM_WASM_SHA256: "dd8e45ea805ea915fa91be785cc162717f008ffb30219a2a354649eebc214ade" jobs: ubuntu-2204-build: name: Ubuntu 22.04 | Spring/Telos Zero build From 40420290742576f3225fa649b26fd644b3136e14 Mon Sep 17 00:00:00 2001 From: Tobias Date: Tue, 9 Jun 2026 00:13:04 -0400 Subject: [PATCH 10/20] Fix stale BP rotation duplicate schedules --- .../eosio.system/src/system_rotation.cpp | 33 ++++++----- tests/telos.system_tests.cpp | 56 +++++++++++++++++++ 2 files changed, 76 insertions(+), 13 deletions(-) diff --git a/contracts/eosio.system/src/system_rotation.cpp b/contracts/eosio.system/src/system_rotation.cpp index 0ad17df3..02f159e5 100644 --- a/contracts/eosio.system/src/system_rotation.cpp +++ b/contracts/eosio.system/src/system_rotation.cpp @@ -99,6 +99,17 @@ std::vector system_contract::check_rotation_state( std:: std::vector::iterator it_bp = prods.end(); std::vector::iterator it_sbp = prods.end(); + auto clear_rotation = [&]() { + set_bps_rotation(name(0), name(0)); + it_bp = prods.end(); + it_sbp = prods.end(); + + if(total_active_voted_prods <= TOP_PRODUCERS) { + _grotation.bp_out_index = TOP_PRODUCERS; + _grotation.sbp_in_index = MAX_PRODUCERS+1; + } + }; + if (_grotation.next_rotation_time <= block_time) { if (total_active_voted_prods > TOP_PRODUCERS) { @@ -112,13 +123,17 @@ std::vector system_contract::check_rotation_state( std:: it_sbp = prods.begin() + int32_t(_grotation.sbp_in_index); set_bps_rotation(bp_name, sbp_name); - } + } else { + clear_rotation(); + } update_rotation_time(block_time); restart_missed_blocks_per_rotation(prods); } else { - if(_grotation.bp_currently_out != name(0) && _grotation.sbp_currently_in != name(0)) { + if(total_active_voted_prods <= TOP_PRODUCERS) { + clear_rotation(); + } else if(_grotation.bp_currently_out != name(0) && _grotation.sbp_currently_in != name(0)) { auto bp_name = _grotation.bp_currently_out; it_bp = std::find_if(prods.begin(), prods.end(), [&bp_name](const producer_location_pair &g) { return g.first.producer_name == bp_name; @@ -132,17 +147,9 @@ std::vector system_contract::check_rotation_state( std:: auto _sbp_index = std::distance(prods.begin(), it_sbp); if(it_bp == prods.end() || it_sbp == prods.end()) { - set_bps_rotation(name(0), name(0)); - - if(total_active_voted_prods < TOP_PRODUCERS) { - _grotation.bp_out_index = TOP_PRODUCERS; - _grotation.sbp_in_index = MAX_PRODUCERS+1; - } - } else if (total_active_voted_prods > TOP_PRODUCERS && - (!is_in_range(_bp_index, 0, TOP_PRODUCERS) || !is_in_range(_sbp_index, TOP_PRODUCERS, MAX_PRODUCERS))) { - set_bps_rotation(name(0), name(0)); - it_bp = prods.end(); - it_sbp = prods.end(); + clear_rotation(); + } else if (!is_in_range(_bp_index, 0, TOP_PRODUCERS) || !is_in_range(_sbp_index, TOP_PRODUCERS, MAX_PRODUCERS)) { + clear_rotation(); } } } diff --git a/tests/telos.system_tests.cpp b/tests/telos.system_tests.cpp index 214bc9af..a3f8ee93 100644 --- a/tests/telos.system_tests.cpp +++ b/tests/telos.system_tests.cpp @@ -1,5 +1,6 @@ #include #include +#include #include "eosio.system_tester.hpp" @@ -127,6 +128,61 @@ BOOST_FIXTURE_TEST_CASE(producer_onblock_check, eosio_system_tester) try { } FC_LOG_AND_RETHROW() +BOOST_FIXTURE_TEST_CASE(rotation_clears_when_standby_moves_into_top_21, eosio_system_tester) try { + const asset large_asset = core_sym::from_string("80.0000"); + create_account_with_resources( "rotvoter1111"_n, config::system_account_name, core_sym::from_string("1.0000"), false, large_asset, large_asset ); + + std::vector producer_names; + producer_names.reserve(22); + const std::string root("rprod"); + for(uint8_t i = 0; i < 22; i++) { + producer_names.emplace_back(root + toBase31(i)); + } + + setup_producer_accounts(producer_names); + for (const auto& p: producer_names) { + BOOST_REQUIRE_EQUAL(success(), regproducer(p)); + } + + transfer(config::system_account_name, "rotvoter1111"_n, core_sym::from_string("400000000.0000"), config::system_account_name); + BOOST_REQUIRE_EQUAL(success(), stake("rotvoter1111"_n, core_sym::from_string("150000000.0000"), core_sym::from_string("150000000.0000"))); + BOOST_REQUIRE_EQUAL(success(), vote("rotvoter1111"_n, producer_names)); + + activate_network(); + produce_blocks(250); + + auto rotation = get_rotation_state(); + const name bp_out = rotation["bp_currently_out"].as(); + const name sbp_in = rotation["sbp_currently_in"].as(); + BOOST_REQUIRE_NE(name(0), bp_out); + BOOST_REQUIRE_NE(name(0), sbp_in); + + name removed_producer = name(0); + for (const auto& p: producer_names) { + if (p != bp_out && p != sbp_in) { + removed_producer = p; + break; + } + } + BOOST_REQUIRE_NE(name(0), removed_producer); + BOOST_REQUIRE_EQUAL(success(), push_action(removed_producer, "unregprod"_n, mvo()("producer", removed_producer))); + + produce_block(fc::minutes(2)); + produce_blocks(1); + + rotation = get_rotation_state(); + BOOST_REQUIRE_EQUAL(name(0), rotation["bp_currently_out"].as()); + BOOST_REQUIRE_EQUAL(name(0), rotation["sbp_currently_in"].as()); + + const auto active_schedule = control->head_block_state()->active_schedule.producers; + std::set scheduled_producers; + for (const auto& producer: active_schedule) { + scheduled_producers.insert(producer.producer_name); + } + + BOOST_REQUIRE_EQUAL(active_schedule.size(), scheduled_producers.size()); +} FC_LOG_AND_RETHROW() + BOOST_FIXTURE_TEST_CASE(producer_pay, eosio_system_tester, * boost::unit_test::tolerance(1e-10)) try { const double usecs_per_year = 52 * 7 * 24 * 3600 * 1000000ll; const double secs_per_year = 52 * 7 * 24 * 3600; From e92aac0ee0b5da46603307515a5f22bdf7aade76 Mon Sep 17 00:00:00 2001 From: Justin Giudici Date: Wed, 10 Jun 2026 16:18:14 -0400 Subject: [PATCH 11/20] Update rollup test and release hash --- .github/workflows/ubuntu-2204.yml | 2 +- tests/telos.system_tests.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ubuntu-2204.yml b/.github/workflows/ubuntu-2204.yml index 4f61eae9..db0602ce 100644 --- a/.github/workflows/ubuntu-2204.yml +++ b/.github/workflows/ubuntu-2204.yml @@ -13,7 +13,7 @@ env: # Commit SHA of tag teloszero-v1.2.2 (tags are mutable; pin the commit) TELOSZERO_REF: "fcb9f5fc627619a8b06cd6b871c462b1d70d3aa4" EXPECTED_EOSIO_SYSTEM_ABI_SHA256: "5545f53b6b2e407acb45c6d14e283866c451d8e47c567baeb74284a55c93c9e1" - EXPECTED_EOSIO_SYSTEM_WASM_SHA256: "dd8e45ea805ea915fa91be785cc162717f008ffb30219a2a354649eebc214ade" + EXPECTED_EOSIO_SYSTEM_WASM_SHA256: "c4dc8dad19768a7ac3bc3519111c6ea632faf9a829eae9366fcb51bba94969f4" jobs: ubuntu-2204-build: name: Ubuntu 22.04 | Spring/Telos Zero build diff --git a/tests/telos.system_tests.cpp b/tests/telos.system_tests.cpp index a3f8ee93..3314db24 100644 --- a/tests/telos.system_tests.cpp +++ b/tests/telos.system_tests.cpp @@ -174,7 +174,7 @@ BOOST_FIXTURE_TEST_CASE(rotation_clears_when_standby_moves_into_top_21, eosio_sy BOOST_REQUIRE_EQUAL(name(0), rotation["bp_currently_out"].as()); BOOST_REQUIRE_EQUAL(name(0), rotation["sbp_currently_in"].as()); - const auto active_schedule = control->head_block_state()->active_schedule.producers; + const auto active_schedule = control->head_block_state_legacy()->active_schedule.producers; std::set scheduled_producers; for (const auto& producer: active_schedule) { scheduled_producers.insert(producer.producer_name); From a007bb605c83606a11d61a212837e487f26d7b67 Mon Sep 17 00:00:00 2001 From: Justin Giudici Date: Wed, 10 Jun 2026 16:45:55 -0400 Subject: [PATCH 12/20] Use bash for Spring build workflow --- .github/workflows/ubuntu-2204.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ubuntu-2204.yml b/.github/workflows/ubuntu-2204.yml index db0602ce..23d96cf1 100644 --- a/.github/workflows/ubuntu-2204.yml +++ b/.github/workflows/ubuntu-2204.yml @@ -19,6 +19,9 @@ jobs: name: Ubuntu 22.04 | Spring/Telos Zero build runs-on: ubuntu-22.04 container: apm2006/leap:v3.2.5-cdt4.0.1 + defaults: + run: + shell: bash steps: - name: Checkout uses: actions/checkout@v4 From 531daee12e2ab2864b53bcc450d01916c1ecd4d1 Mon Sep 17 00:00:00 2001 From: Justin Giudici Date: Wed, 10 Jun 2026 16:53:29 -0400 Subject: [PATCH 13/20] Fix CDT header checks in workflow --- .github/workflows/ubuntu-2204.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ubuntu-2204.yml b/.github/workflows/ubuntu-2204.yml index 23d96cf1..7a69dc89 100644 --- a/.github/workflows/ubuntu-2204.yml +++ b/.github/workflows/ubuntu-2204.yml @@ -42,7 +42,8 @@ jobs: echo "${CDT_DEB_SHA256} ${cdt_deb}" | sha256sum -c - dpkg -i "${cdt_deb}" cdt-cpp --version | grep "${CDT_VERSION}" - test -f "/usr/opt/cdt/${CDT_VERSION}/include/eosio/instant_finality.hpp" + test -f "/usr/opt/cdt/${CDT_VERSION}/include/eosiolib/contracts/eosio/instant_finality.hpp" + test -f "/usr/opt/cdt/${CDT_VERSION}/include/eosiolib/core/eosio/crypto_bls_ext.hpp" - name: Build TelosZero tester libraries run: | set -euo pipefail From cd529d1642982381a85811246535536804cc51e0 Mon Sep 17 00:00:00 2001 From: Justin Giudici Date: Wed, 10 Jun 2026 22:30:16 -0400 Subject: [PATCH 14/20] Add autokick threshold and location-swap regression tests missed_block_autokick_threshold_and_lifetime: simulates a scheduled producer missing every production window until the missed-block threshold (648 for a 21-producer schedule) is crossed, then asserts the producer is kicked exactly once with REACHED_TRESHOLD and that lifetime_missed_blocks is counted a single time - regression for the double-count fixed in this branch. schedule_metrics_survive_location_swap: swaps two producers' locations so the proposed schedule reorders without membership changing, then asserts the schedule metrics vector retains its original order (i.e. was not rebuilt) - regression for the membership-based comparison. --- tests/telos.system_tests.cpp | 164 +++++++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) diff --git a/tests/telos.system_tests.cpp b/tests/telos.system_tests.cpp index 3314db24..0b579acd 100644 --- a/tests/telos.system_tests.cpp +++ b/tests/telos.system_tests.cpp @@ -491,4 +491,168 @@ BOOST_FIXTURE_TEST_CASE(multi_producer_pay, eosio_system_tester, * boost::unit_t } } FC_LOG_AND_RETHROW() +// Regression for the membership-based schedule metrics comparison: when two +// producers swap location values the proposed schedule ORDER changes (it is +// sorted by location) but membership does not. The schedule metrics vector +// must NOT be rebuilt in that case — a rebuild would wipe in-flight +// missed-block counters. The old metrics order is the fingerprint: if the +// vector were rebuilt it would be stored in the new (swapped) order. +BOOST_FIXTURE_TEST_CASE(schedule_metrics_survive_location_swap, eosio_system_tester) try { + const asset large_asset = core_sym::from_string("80.0000"); + create_account_with_resources( "locvoter1111"_n, config::system_account_name, core_sym::from_string("1.0000"), false, large_asset, large_asset ); + + std::vector producer_names; + std::map locations; + const std::string root("lprod"); + for(uint8_t i = 0; i < 8; i++) { + name p = name(root + toBase31(i)); + producer_names.emplace_back(p); + locations[p] = 10 * (i + 1); + } + setup_producer_accounts(producer_names); + for (const auto& p: producer_names) { + BOOST_REQUIRE_EQUAL(success(), push_action(p, "regproducer"_n, mvo() + ("producer", p) + ("producer_key", get_public_key(p, "active")) + ("url", "") + ("location", locations[p]))); + } + + transfer(config::system_account_name, "locvoter1111"_n, core_sym::from_string("400000000.0000"), config::system_account_name); + BOOST_REQUIRE_EQUAL(success(), stake("locvoter1111"_n, core_sym::from_string("150000000.0000"), core_sym::from_string("150000000.0000"))); + BOOST_REQUIRE_EQUAL(success(), vote("locvoter1111"_n, producer_names)); + + activate_network(); + produce_blocks(300); + + auto metrics_before = get_gmetrics_state(); + BOOST_REQUIRE(!metrics_before.is_null()); + auto rows_before = metrics_before["producers_metric"].get_array(); + BOOST_REQUIRE_EQUAL(rows_before.size(), 8u); + + // swap the locations of the producers at metric positions 1 and 2 + const name p1 = rows_before[1]["bp_name"].as(); + const name p2 = rows_before[2]["bp_name"].as(); + BOOST_REQUIRE_EQUAL(success(), push_action(p1, "regproducer"_n, mvo() + ("producer", p1)("producer_key", get_public_key(p1, "active")) + ("url", "")("location", locations[p2]))); + BOOST_REQUIRE_EQUAL(success(), push_action(p2, "regproducer"_n, mvo() + ("producer", p2)("producer_key", get_public_key(p2, "active")) + ("url", "")("location", locations[p1]))); + + // cross at least one update_elected_producers() proposal and let the + // reordered schedule activate + produce_block(fc::minutes(2)); + produce_blocks(300); + + // the reordered schedule must have actually activated (i.e. a real, + // successful proposal happened)... + const auto active_schedule = control->head_block_state_legacy()->active_schedule.producers; + BOOST_REQUIRE_EQUAL(active_schedule.size(), 8u); + int pos1 = -1, pos2 = -1; + for (size_t i = 0; i < active_schedule.size(); ++i) { + if (active_schedule[i].producer_name == p1) pos1 = int(i); + if (active_schedule[i].producer_name == p2) pos2 = int(i); + } + BOOST_REQUIRE(pos1 >= 0 && pos2 >= 0); + BOOST_REQUIRE_LT(pos2, pos1); // order flipped by the location swap + + // ...but the schedule metrics vector must NOT have been rebuilt: same + // membership, and crucially still in the ORIGINAL order + auto metrics_after = get_gmetrics_state(); + auto rows_after = metrics_after["producers_metric"].get_array(); + BOOST_REQUIRE_EQUAL(rows_after.size(), rows_before.size()); + for (size_t i = 0; i < rows_before.size(); ++i) { + BOOST_REQUIRE_EQUAL(rows_before[i]["bp_name"].as(), rows_after[i]["bp_name"].as()); + } +} FC_LOG_AND_RETHROW() + +// End-to-end missed-block autokick: a scheduled producer that stops producing +// must accumulate missed blocks across rotation cycles, get kicked once the +// threshold is crossed, and have lifetime_missed_blocks counted exactly once +// (regression for the double-count where update_missed_blocks_per_rotation +// added missed_blocks_per_rotation before kick() added it again). +BOOST_FIXTURE_TEST_CASE(missed_block_autokick_threshold_and_lifetime, eosio_system_tester) try { + const asset large_asset = core_sym::from_string("80.0000"); + create_account_with_resources( "kickvoter111"_n, config::system_account_name, core_sym::from_string("1.0000"), false, large_asset, large_asset ); + + std::vector producer_names; + const std::string root("kprod"); + for(uint8_t i = 0; i < 25; i++) { + producer_names.emplace_back(root + toBase31(i)); + } + setup_producer_accounts(producer_names); + for (const auto& p: producer_names) { + BOOST_REQUIRE_EQUAL(success(), regproducer(p)); + } + + transfer(config::system_account_name, "kickvoter111"_n, core_sym::from_string("400000000.0000"), config::system_account_name); + BOOST_REQUIRE_EQUAL(success(), stake("kickvoter111"_n, core_sym::from_string("150000000.0000"), core_sym::from_string("150000000.0000"))); + BOOST_REQUIRE_EQUAL(success(), vote("kickvoter111"_n, producer_names)); + + activate_network(); + // fund the inflation offset account so hourly claimrewards snapshots in + // onblock succeed over the long block range below + transfer(name("eosio"), name("exrsrv.tf"), core_sym::from_string("400000000.0000"), config::system_account_name); + produce_blocks(300); + + auto metrics = get_gmetrics_state(); + BOOST_REQUIRE(!metrics.is_null()); + auto rows = metrics["producers_metric"].get_array(); + BOOST_REQUIRE_EQUAL(rows.size(), 21u); + + // pick a victim that is in the schedule and not part of the rotation pair + auto rotation = get_rotation_state(); + const name bp_out = rotation["bp_currently_out"].as(); + const name sbp_in = rotation["sbp_currently_in"].as(); + name victim = name(0); + for (const auto& r: rows) { + const name n = r["bp_name"].as(); + if (n != bp_out && n != sbp_in) { victim = n; break; } + } + BOOST_REQUIRE_NE(victim, name(0)); + + // threshold for a 21-producer schedule over the 12h rotation window: + // 0.15 * 2*43200/(21-1) = 648 missed blocks + const uint32_t threshold = 648; + + // produce blocks, skipping the victim's production windows entirely + uint32_t last_missed = 0; + bool kicked = false; + uint32_t blocks_done = 0; + const uint32_t max_blocks = 20000; + while (blocks_done < max_blocks) { + if (control->pending_block_producer() == victim) { + produce_block(fc::milliseconds(500 * 12)); // skip the victim's 12-slot window + } else { + produce_block(); + } + ++blocks_done; + if (blocks_done % 250 == 0) { + auto info = get_producer_info(victim); + if (!info["is_active"].as()) { kicked = true; break; } + last_missed = info["missed_blocks_per_rotation"].as(); + } + } + BOOST_REQUIRE_MESSAGE(kicked, "victim was not kicked within " + std::to_string(max_blocks) + + " blocks; last missed_blocks_per_rotation=" + std::to_string(last_missed)); + + // missed blocks accumulated across cycles (would stay near zero if schedule + // metrics were being rebuilt every proposal) + BOOST_REQUIRE_GT(last_missed, 100u); + + auto info = get_producer_info(victim); + BOOST_REQUIRE_EQUAL(false, info["is_active"].as()); + BOOST_REQUIRE_EQUAL(1u, info["times_kicked"].as()); + BOOST_REQUIRE_EQUAL(1u, info["kick_reason_id"].as()); + BOOST_REQUIRE_EQUAL(0u, info["missed_blocks_per_rotation"].as()); + + // lifetime_missed_blocks must be counted exactly once: just over the + // threshold, and no more than a few cycles beyond the last observation. + // The old double-count bug would report roughly twice the threshold. + const uint32_t lifetime = info["lifetime_missed_blocks"].as(); + BOOST_REQUIRE_GT(lifetime, threshold); + BOOST_REQUIRE_LE(lifetime, last_missed + 60); +} FC_LOG_AND_RETHROW() + BOOST_AUTO_TEST_SUITE_END() From 05a39982354b0388d0472dc2c693b75c54fca6dd Mon Sep 17 00:00:00 2001 From: Justin Giudici Date: Wed, 10 Jun 2026 22:59:25 -0400 Subject: [PATCH 15/20] Port upstream reference-contracts finalizer key test suite Ports AntelopeIO/reference-contracts tests/eosio.finalizer_key_tests.cpp (17 cases, 1194 assertions) - the suite EOS mainnet's Savanna transition was validated with - adapted for Telos: - get_row_by_id replaced with get_row_by_account-based lookup - switchtosvnn shortfall error: Telos's per-producer guard fires before the aggregate count check - finalizers-replaced test rewritten for Telos semantics (BP rotation above 21 candidates; schedule freezes when keyed candidates drop below the last schedule size instead of refilling from standbys) All 17 cases pass against the Telos contract. --- ...io.system_finalizer_key_upstream_tests.cpp | 697 ++++++++++++++++++ 1 file changed, 697 insertions(+) create mode 100644 tests/eosio.system_finalizer_key_upstream_tests.cpp diff --git a/tests/eosio.system_finalizer_key_upstream_tests.cpp b/tests/eosio.system_finalizer_key_upstream_tests.cpp new file mode 100644 index 00000000..1c2a2b56 --- /dev/null +++ b/tests/eosio.system_finalizer_key_upstream_tests.cpp @@ -0,0 +1,697 @@ +// Ported from AntelopeIO/reference-contracts tests/eosio.finalizer_key_tests.cpp +// (the suite EOS mainnet's Savanna transition was validated with), adapted to +// the Telos system contract: +// - fixture/suite renamed to avoid clashes with eosio.system_finalizer_key_tests.cpp +// - get_row_by_id replaced with a get_row_by_account-based lookup +// - switchtosvnn shortfall error differs: Telos's per-producer guard in +// get_finalizers_for_producers fires before the aggregate count check +// - update_elected_producers_finalizers_replaced_test rewritten for Telos +// semantics (BP rotation above 21 candidates; schedule freezes if active +// candidates drop below the last schedule size, instead of refilling) +#include "eosio.system_tester.hpp" + +#include + +using namespace eosio_system; + +struct key_pair_t { + std::string pub_key; + std::string pop; +}; + +// Those are needed to unpack last_prop_finalizers_info +struct finalizer_authority_t { + std::string description; + uint64_t weight = 0; + std::vector public_key; +}; +FC_REFLECT(finalizer_authority_t, (description)(weight)(public_key)) + +struct finalizer_auth_info { + uint64_t key_id; + finalizer_authority_t fin_authority; +}; +FC_REFLECT(finalizer_auth_info, (key_id)(fin_authority)) + +struct last_prop_finalizers_info { + std::vector last_proposed_finalizers; +}; +FC_REFLECT(last_prop_finalizers_info, (last_proposed_finalizers)) + +struct finalizer_key_upstream_tester : eosio_system_tester { + static const std::vector key_pair; + + fc::variant get_finalizer_key_info( uint64_t id ) { + vector data = get_row_by_account( config::system_account_name, config::system_account_name, "finkeys"_n, account_name(id) ); + return data.empty() ? fc::variant() : abi_ser.binary_to_variant( "finalizer_key_info", data, abi_serializer::create_yield_function(abi_serializer_max_time) ); + } + + fc::variant get_finalizer_info( const account_name& act ) { + vector data = get_row_by_account( config::system_account_name, config::system_account_name, "finalizers"_n, act ); + return data.empty() ? fc::variant() : abi_ser.binary_to_variant( "finalizer_info", data, abi_serializer::create_yield_function(abi_serializer_max_time) ); + } + + std::vector get_last_prop_finalizers_info() { + const auto* table_id_itr = control->db().find( + boost::make_tuple(config::system_account_name, config::system_account_name, "lastpropfins"_n)); + + if (!table_id_itr) { + return {}; + } + + auto t_id = table_id_itr->id; + const auto& idx = control->db().get_index(); + + if( idx.begin() == idx.end() ) { + return {}; + } + + vector data; + auto itr = idx.lower_bound( boost::make_tuple( t_id, 0 ) ); + data.resize( itr->value.size() ); + memcpy( data.data(), itr->value.data(), data.size() ); + fc::variant fins_info = data.empty() ? fc::variant() : abi_ser.binary_to_variant( "last_prop_finalizers_info", data, abi_serializer::create_yield_function(abi_serializer_max_time) ); + std::vector finalizers = fins_info["last_proposed_finalizers"].as>(); + + return finalizers; + }; + + std::unordered_set get_last_prop_fin_ids() { + auto finalizers = get_last_prop_finalizers_info(); + + std::unordered_set last_prop_fin_ids; + for(auto f: finalizers) { + last_prop_fin_ids.insert(f.key_id); + } + + return last_prop_fin_ids; + }; + + action_result register_finalizer_key( const account_name& act, const std::string& finalizer_key, const std::string& pop ) { + return push_action( act, "regfinkey"_n, mvo() + ("finalizer_name", act) + ("finalizer_key", finalizer_key) + ("proof_of_possession", pop) ); + } + + action_result activate_finalizer_key( const account_name& act, const std::string& finalizer_key ) { + return push_action( act, "actfinkey"_n, mvo() + ("finalizer_name", act) + ("finalizer_key", finalizer_key) ); + } + + action_result delete_finalizer_key( const account_name& act, const std::string& finalizer_key ) { + return push_action( act, "delfinkey"_n, mvo() + ("finalizer_name", act) + ("finalizer_key", finalizer_key) ); + } + + void register_finalizer_keys(const std::vector& producer_names, uint32_t num_keys_to_register) { + uint32_t i = 0; + for (const auto& p: producer_names) { + BOOST_REQUIRE_EQUAL( success(), register_finalizer_key(p, key_pair[i].pub_key, key_pair[i].pop)); + ++i; + + if ( i == num_keys_to_register ) { + break; + } + } + } + + // Verify finalizers_table and last_prop_fins_table match + void verify_last_proposed_finalizers(const std::vector& producer_names) { + auto last_finalizers = get_last_prop_finalizers_info(); + BOOST_REQUIRE_EQUAL( 21, last_finalizers.size() ); + for( auto& p : producer_names ) { + auto finalizer_info = get_finalizer_info(p); + uint64_t active_key_id = finalizer_info["active_key_id"].as_uint64(); + auto itr = std::find_if(last_finalizers.begin(), last_finalizers.end(), [&active_key_id](const finalizer_auth_info& f) { return f.key_id == active_key_id; }); + // finalizer's active key id is in last proposed finalizers table + BOOST_REQUIRE_EQUAL( true, itr != last_finalizers.end() ); + // finalizer's active key matches one in last proposed finalizers table + BOOST_REQUIRE_EQUAL( true, itr->fin_authority.public_key == finalizer_info["active_key_binary"].as>() ); + } + } +}; + + +const std::vector finalizer_key_upstream_tester::key_pair { + {"PUB_BLS_9Pbv53DbC9EDGGPEZUrlKuMGeXYWHzIPfmn-Ncrxj7sOTiTEOYIkcCgBrvfKYiIG0BsiafG1dRK9MV40aGWKREE4rWHpiDLIkHR91huq9CEdeZHUb2bRXvcxs0RAa7oADXSTmw", "SIG_BLS_IyjIzDWKzlekYSiRbJmBqYmWoiMsmHX_1aB0Hkqk1woeSJLbtd9D83lMDi6LiMEEpP79rP-AKiJAg1rKHODkt2habnn-y4jFMl76egQOv2KHLjobVzWWj_nz667VxeoF2FwF-NMnX7FQfpPgVRINSDKc5maRADRYQGrnlE-boh_23GBuCaxELnAFD-4L_ZEZRr8B9UIA62Vj9JBS_dExquL3wtlhx7vNq0MAGf9J3_Q69BsTuH_OMH6bs5m2AF4ZqiKpdQ" }, + { "PUB_BLS_WIuHboHhjl3zxB3pt1bdSdF9O6GPOmjMgQvxbzpJBPYYIrWIpcHr6uGbuNGHh8ITlMwLhb_7Fe2QTUStMU0sOgLlXl737hu9GBNyF9W7ZIc7U1xSOmBS5-OpCjViTs4DXQJrjg", "SIG_BLS_RL0hu5YQiXvuvDYdi6LuQqPAISBmS0OuUzGhRezqRYHQC1yL2VlV4lg17rDFzekFCYeB4Agtv0P3Zs3mCDRVwnIbZ3ETin-ONvXKo78Vi71BM4g-AWqdbXbvIbp86u0IXzWjQgSNTuKP2Jj4AqKwhNbDhMiqH5VXUuV1Ogp7P-kN6TcJiqQfqs69iAWklDgYMeDT9Y6qvrb7E1ZtkRbFzISKBrKDYCJ1KFYL7XgknPdqydjPiHEJEIX8ORNwPEgG11PbZQ"}, + {"PUB_BLS_OKI5k1cZcEAvkXQqWCMICE5BSXd1lI5VH_n-PagmHOP6MnKPpydAcS9qgHxm0bIWJwyzvMUdpm-TNqlQQGcQggAn_3GUQx7zSlrdzSFZ3h2kIXjpIDNDB2ZMQJ6wqYIXNX3kZg", "SIG_BLS_BGQbDqe4kpCtMEZsl4cfsSYGwquW8jGeq_9zppptRU4XQvAqRT_flXSg84sR9PMY9_m1lMsa6S9nnlilQ7SdMooOzOXVvYzNFmLuCR6b2c4jDZrJQGtgYleUINFQZEgBhzYS1WRbnlDIsRCBPbS_mDbfqQ80g-DKkUCcyeY0Cr7CJ9IoafmcVWJLFPWV0ukRcGAk0XKBWVbI23REXbgBeKD_XNMJe5PHJS36gZYWyaiO_wHIXAbUuYgOptmC0rAIEPVGow"}, + {"PUB_BLS_0kQpMupOH51c0cFLk-IoFALA9ePsCWys46A7HgExjvHzVqFNYBGIO4-SzhcdqToMCrs457K092irPa1JlUGCxdO0rluiyu6bJAYemTqPQZvzNYihZJ8OULOObIcsN8gW1aZNmA", "SIG_BLS_lTVkt0YINpwBIHXbthD_b21srBBJB5C6BMeyhA4RUCP4tkiyw9I4DUlJyR90v7AS-yhOHCFwDNmB1ld0wQYxnhxim1xB0zThhH9PZkkQH-Iksh46EujwEDSMQ7tLaqcXFEB9SmO9Yf8uaNEzY0qn11dgOWnAyxH4wb73QHE0g75gutiZ8XwugjPrpXUSj_ERiJcXE15_jr1TGGVYo1AIJYvhkiIpT4K3mNqtcFnawbhw2hr5Q0RXLGrSHCq21ncTRvtEUw"}, + {"PUB_BLS_pCUtXfVRKcErpxc0nh4wfSyEsCwj2pdQadJiXxArz-OTxhbkl12rezpiSlff01YWYvwGf_N11z1p6KyBUEyJFmOGTi-o89InkPoanMLe-CH9_HoifsbXLgKISdfs7CAZ-_DELA","SIG_BLS_R-vksnp0wpwJ0I8E0d4DIk4AuGsL6q3tzwoqk7wzKbPoQhHdtAcD1D00um8WQoUSdSAT1Z9PtFRFOUdsvS5oBLmOPqCtXW-GWiKH4GonU-NttXGdAnndPNTchvXzkEkSWSPl-2NUx1Ep2TUxvxyLuSkjE5-HFYGFZQuYTClf5-YZzstUUgAARrk2IZtsMpsI4l3_LM6bgfCrVNLxA5INOut1-EheK1M8ZBupmY9Ue6pMOJ4Po6LF8uAeLDxvDOgAToKDIQ"}, + {"PUB_BLS_FUu1bOokTWaf00LpoZl1OaO1Rr01EjxZ46fOs4FIgLVSf_n-WDG7QhMYFj5JGBoTWxUe-Hkk0VsvBzwVM85F4ZBEa8H6dLgQhevdjiYth8TDC2ZOSkcQdEUmtpcn4-8Hjy9aMg", "SIG_BLS_2nCBKKw377gYomeIq3_3kktsry6lZPr4_pCggFBL6sQSvkCuCeAi5igbdY9tNEkVJ9u6NVH5_JhkLvN7-oyw83B9WwajWZf7sERxebilDl5Spy-YLxXvorH8BOFap1kEIf5JktH4tsmwwJgySqZTGsGXzRIu3K1fk3F2rZ0I9aAE5XR2hYPrIIhV0Vv2T68PpAg1RUDuaxhXSM1woMA0IeCIvSISLF52m4XOvAu4tBRf03sNQzc2oXCLb9BZ4HYHJK7rSQ"}, + {"PUB_BLS_kYxGibaaU5EfQrnJpWcjwmY07j6px-uwJ0J_6RE7f9DrqhI3-NVyjw_jayvZ03wUjt96D7mecxSjEw3ZVHlguTykrjkI06otfpvv9Go_yS0gGmNYIRSWpOKtBIY8cIkEFVc8Ng", "SIG_BLS_NdXmsMAdWh8qSvUqDjKCJVjCjnCuDIeXQ_4XzFvCNGxZGwEDNhWgMVNdcLoEl7cUbiiuIdZVPC16E5_iYbSGfkPUBfx3Vh03GTy31a3dsXSciEM2VVGlBg5c_2vZTwEAbyYrVUT-sdt0nxO139I3Mk6Y6bauELYVkMVvv9E9Y_MjbkRnKDNQDqJACg46FEkFk-YQ0vvOA7sDyTP70BFwvF89DsE3db_g1sPjDfj7Kk3bda4KqMFncDObXIPk8QsY6gqzvw"}, + {"PUB_BLS_p819OPn04Fz4EI1QKrAOY0cKeGBPAyM1vFOc7yZn-cMSjIgyuSy3VVqNl8SgS2wRVE_kW9Ix2DIowByUsUrDVhJ-aay-VeP-EGzqpJLXu-baMRiGO5Wq8HQHMv-70zsXicba1Q", "SIG_BLS_38H7fX82apWxSCHS9-r3xW14hoH0zeFjqCq3bKajptvsEREzvY-m8njOocQJpQwJWmG5lxQ0Ij9JAu1qS9NMvnzt3tGWYWPK5cvczO6y4JrGPsa9c_Qx-g4YEFvj0oILtypCHl_c3moWttBZRra-QIESSeRnrsgtjpbb7EDORTeSa7_fp15aLaKbl3pugUIZU7uINgsCetG-mim3v4KiBRcZbAkG_m-dwqY099vTTFMA7Vt3xc3HOMk7urrvxAsQP2GnFA"}, + {"PUB_BLS_tbqdEUxBSTTpN60gIz8CGQL19G9mlOK9hcFuRQc9HQ9aZnF84BJQh3BP2mDG-kYDom_pohAEWry2w1e6AdgDcIcxpP2IbiZm000rDXZu7Bfv4Ebh6X7fVtDUt4piD9gCHXVw1A", "SIG_BLS_GmwLJWgq905FbJaJm0oTi8wiXlGIAm865thKx5MVVpGetMTh_9LtEEi0-iB0mmwC4OrVYbEoZwRSPsiMpA_VEcmrubWMN1noXEdjFG69LJVn9GLS4guCJQe_0W3_i9IDwIv7fixBaJyj8-iWvDR2n_vAmKdMUTDDtfdWeeXZ6yEwdYfsoyjIqUhXQq3Axk4AxiVFZ7yfiOkk3aGKOVwdCwd623WgiPldI-DEPUS0GaqXwgXtEyLtlDtdC13NovwC-9Jmsw"}, + {"PUB_BLS_mtAiKKCgt5QKDDn14QuQhs2AugC8XFQFQr7qUqHLodf264yUawOtzMr__juNTFQUWOr-JfKfUW1GfxLi0xXOYmEpw4wZaDm6VQCgTsOtTp5gaFcp1IJiVHDG1UuDB_oFKmMA1Q", "SIG_BLS_RMIvMoGMmd3tBHrDZSR_lQiR-mx0BJE7ZU_3HrdYfmhO8O9TE8iphSs3rETKFfIRIEU9N46DQBWU0t9wQLD468Pi92Gt6zEYREctw4_tNDtcqBGrYfr7g69qX97uqzsUulqyT_lRLH4-rwhPlRJgI6hg-G4C4bXTjUs_2LGoEhpvD6Y6LDTR9ek_Buuwra0OojtZ7anuFotVOD4uuBf34IQPyWrO9-X5RJ9bv19C09vCLbO8Djf8JCfr9cI0sh8TrOj4Gg"}, + {"PUB_BLS_j5fD6oG8C76CX7HzWxWf3XSU3gvROUybbTD9W1B_w2ilH98zBzXZ5VHmtIZCQ7YHUyQISAbjDUKwR2h1SL_jyHSw7G18Fj8Q8WK_I-UWdATbvH0xjjnmM-FQurPIxRsWsu7VvQ", "SIG_BLS_kzxMg5-VsVPgSKLdLJwRpJE7Zh8GtX8KaC-5zGU0RTm0mHx4wU_IbhqXBgEYrLkPhTtD7GIOzIMQgHTjsFxfDXOw2_s9dFiHwtjzPvOnQ5f4zFgmXipWjFBOSKzFP-sUwk4mMuvFO0dbva1RghLvk1G5T8H_aNGnWyb8ywQwC06axbBw5_poiU6DtonashoZ-ei2YRttxKdUbcg87BghKZNuBUfRXiNe1p4knuKM5o9W96pbI6uz6Dryn3IVuaMKUhtlnw"}, + {"PUB_BLS_M8ZZq5MspUdQmeclKbv1vZnSNy-2Qn_Yc9euP3zzG_bNfRwLPzA0UNQNmW66OZELZM_4lQ2scuQbb2gSKK462_p9we3ea_aTf18GLTinFm6Pp8Xr-ESveFEc48rx4kcQjY_Ddw", "SIG_BLS_ZRps69hLvs23lYlr68NEt6iIDvuUBhbDjIaUX3prARqYGJZB2FnB24tOLV83EE4IBrmGw-frILt1v5NRHUDJHgaoMndsmR2C2itoUDY7af7nOmxB5xaI5SNSiO_I2mMYQBDGaThDcnhCdalhzkDVmhOUeXR3wGSmNbPccTUf0J_DS5FmBzwnroaYJe8C6o0WRn4K0Wdsy5r_QNBsYtmSXxZf6vSdQJyaWQMIyowRHE3ve1L0F9PMVlNzZndemVwWExKQag"}, + {"PUB_BLS_aYnBaZVLoANPkIe3uyD453xc2NNYnIXupXTlAWbjaZ32CiGqfJYjtUJuoDf4i2wCMi_cQrv2tlLGpcYKql8g_B3NbFQrmHHwc80JaJi_YTVn4LDkIi1-jtHpsQ9pU28Zqfqx2Q", "SIG_BLS_kfYCci5ZsW0AJGWSxVxyx3Q3Lud_xB_VO1u0-9cJi1yfGC-rNbo_erFzbeOgLCIHYIE-kE4BWNJuvbO5sZ05walDhp33cCCMMeiEZUlk-sFUQdyhVJ4HtnsTea45jSMCjn650lXufOXlAbo3D_LrL_GH8y24OZVcI--iFuesi97SzEHifWBhLPy_yEBHcY4Gj0Ll37-y6moBx8L2NO4ZzSEiYViIyMQHHBTcb_qwPHzHR1TJeuKWp8hnnf3dirUA4n1XdQ"}, + {"PUB_BLS_xY48yStp-XsqjKikK50yzyGmK_6NacUmsJXUJNbjT-FKbeQ1bvbb4kEhtBP9K8QXfjRNvogpO2LSnK7G7P2xe7PP9NAZY-VmmeWBCUxCU_ZBPB7OiYeMr9JT9gXCLusQl-Amfg", "SIG_BLS_wYQJ7gnJCWhhsIjK1zbJzUXNVde_h0FH_6l6WtkiG0jdVTp-zkQtGzJR-P3aSPoFveLLpf8pTBU8vosk0eP3Ur3oRMl7Xnh-Xy9dWq0JglRSZQm-n1999TFYkWNH8T0HiNYFaXXBt5NA3LnMkoulZ16DsHfDxfw3Ggsq7BTJg68dsDrWscGXoa6C5ixU8lMOa2nf7sY_U6Qm0jnvNqLBCe1sNlgVDPhYXguIanAx92rEvW0KAFBykS96wg9sV2sRXsuJTQ"}, + {"PUB_BLS_v-d2afMcdbQp1EyukkQ_CUw8Q2a1mNC5ir4aJUUGTz96ER57K8TEwZGnYdoTmh0I5hzT3YaXkAuhvdkxuRKGi0saJgpkSP6UA8QKWqF0oJ3dAh7pG6mhZUGWybi51R8EZttgQg", "SIG_BLS_qgw6olECCtBviiEwjk3ve4Unomlkk37jSoG5IPfExsLXh-MfPGfkxZZfLDXzBWsY_wb7LZh2oxPKKYl_wXzIVGLFfSk8623WNegg8Hy7C32Uo2mlm0Eg-3fdjs_OFKIXdVZwheDXOOBycMAUEV1FR-jOC7TfOASqcO6eOAyo8Vjy0VoyIQx2mI73ocQFQeQLUUigmylx4mB2jjnbbZ7JY4y4X1gWIYiSoe8PoxRYmZJdSz8W9Ku0dRN9f8ir4SoQJ8lfKw"}, + {"PUB_BLS_TGdjRLnSlH5lN75APoANTjnuZ3mh8Uqy4GEqzoc6su9t7dOmrsoAtKn2TqHaZFYYt1MuoDF8eYBwS1YHN8w_vbaEzUupBtWdU-QMzOxw4Qei6CLt5xlju1WfRXaPCu8IQXbOTw", "SIG_BLS_v6xg1vlIB4QZzZCx1CUQSTSOnhRJQpx7Qf3VB2NdAXRs09vrmB2fxVfvxdkwngYUQgcwjs_XBvSGX0ECvqYz2wNHmTS-8O0SIeI2Ne6BRcLO_s47ssG7ookuMOWD14sYeM1embZy4BLD68hapxPeyEzY6c9U8knwHXOs_PmgTfxPKbj_Gd53n6YogEHyltIQV_xo83mdu-eG2ny4pQ0PgTBJakhbKfL5z_TtAHCkUbVO84Gnx4rPYW8JjaeNbkUHAa9IRA"}, + {"PUB_BLS_Dow8yQTh_mVETS4ky1y8ae9zcDuQZtHXlYEMnTTx10nczdrwZExP-6bWQcYpfu4Cj5UGJdxjHjHhdk1BQItK22FaZ_bZb1yURrSuqmr7CeuU9HyRPOZ3id03xCbcaFoFqvif5g", "SIG_BLS_C5uvcsKu09x5sBzWjmz7V3amYKnKPOJrw8s0FyaohXvF71b2_WZogus1K692VdgTTckPItM__Xe38TgXb5rJTCpD7nKiKjXTpuKQ2IMM0PEQFXhY_1sVrUXYdIghX9cJm0_wILLcJc39--lVCrPKC0lGdV5d1RUy3cnb-Z4au4BFUSh5vY9oE7R0jf9jqO8FOiB0G7VFurRPeWVKfQbtgohriP1_TmmDH9iTjtgzvKO3YvZAxrnxiYCTocXy_poMekgNnQ"}, + {"PUB_BLS_1ZV6MNrDyg1_VOqkIQ6xaYoYi6diO_obKjSnj9-wspP2PQGZ1mIPqJ-2zMuHuv4F-D0mFVkh4TBZd_ildGCr7foEzxUG25f1DjSuhcAfC-X-q0eYh1NxnZzeiHCKjU0OXnAfXQ", "SIG_BLS_Ht4ZYXmuDXUw3jDgi7dwvUhm9nj8cs3kMgqJYmUvH5Kb75BmjKOgOE8EOi4NsWQNoA6JmvjZ5WZyFFm0Fv8uNmvgrlWu4xU_uEkmThkD5C47dk88tRi-Ve74GjjjOIMEDkUjmMka18dUDu-UTPqy56ju-XGDIwvPrED8ZqK7L_3ZOSPcb21QXb2iVEP-UbQXJHnDaU4zSbKt60EFaWEWgPt501A98Gwti0aBJFPJjW8jaziErlCHUKmJaEISjjUCQBbc4Q"}, + {"PUB_BLS_xCZ8dBxW7GCPvCh2v5AEAMLpPlqb5MY6KQ4AjgZiH63WCrsPF-tNOSb1tj3Tml0WQHnXtuQtAXdysAX6wGX92zvCtl779vtEnXcSkZrcoXQ036IeDg91B7s4HZVDdTIRhkWdIg", "SIG_BLS_P7bbvvAPtz1hX86X2NSEZKn6AljBpq00pL8wwpIrEjlVkWh-tSwPephv9SNyoTkE8_4HB5e97-MPILmEpYp_5lphbcimrCIZsO53dWxxHmp7TgxofGmzk6_fSDCuZI4M3fsnI3-NnjzpRKg8YK7FmAv0clHA9wj6nLa7HPsDsV8hHqAyAJwxxttMpSOpBMkESUuLXJL0AuaRsTU0KUCGeNQQxCkR_tYhOUPLZwh_Lc__35PxpZQk5Ivm1UmDZRcNRQUXEA"}, + {"PUB_BLS_lG6RfU8c0kJVvFNA8ikdU1dOtt_FrsHd8L7OMRHO9D-ct18IztEKqkHBAb6cXJEL2VL7cr2rI_LL20Fgu_9e4PIG4U54o5lp_ydwAweGtay1190QB5l8RDGT-s_sshMBkV6N8A", "SIG_BLS_--zVqQC10legB0F_UiBsQlgdNDi-VfFPo4KJPa8-Npf_bKqqaTjvHrKl56398ecBpo2GQbPeYvEM0aE2Pg-lfQLbRlFCKuOBvHsz01nBmlJU3Ai1wkwiIaLrhURIrhELcnFnA4EF60DSwMTND5FGRYHqpX3nZULiu5z-a6hrvrFUMDuCiI6fMblhX1-3sbcTwQAm7X8m-V70X_1zNHI1uuZEC4AdSouXdyG3vgeg10osfnBQn7XMhJr4SA45EW8QLBwQ4w"}, + {"PUB_BLS_3yY58HBFvJwwIcc-nADx4WqAAfLOhVa3ORc_bqN7bH5zsFImQHv98GRInYU8PWQUBbE-mNM_nsjBYcjPGMXRkePZTlgNfWtFkm5X6JkWLshvwMSzABl86UF1i7eXPbkWaHLzHQ", "SIG_BLS_pWQzUEOXHvgrf7UL7rSvwFgd6tZ7BrYtUFZG2AQgvC76cj5bUf9Cg0h-yTwOMkEDX48ZuPu4BsbifACr1cnXQ8vSYYFMcthVCL04FF6nGzh3Rungs4MyUDO0OwmXEtMW9U39CnquCoGhjFtgq--En4PuGmPhBnpDaDZzUau3Y0pqnji3gWpEM_wYJQUDvtIYUkXmOFXQvizWwYKMT2F7bXr6TLRIcIoMv7qkKReiNSBwnId8qY6rsqEd1MFpb1cJV6meeQ" }, + {"PUB_BLS_DwHoibpeQ6i1VnoF8cjYFI-Z6etehxApERYpKpjP8IvVEXeDUaUO54siI6oSFAYB4ZqKrBykU081FDqpf7ao41McS_N92cnNNjB56_gB1uGmjYlQMJgrj26fX2tveZYT2BXsTQ", "SIG_BLS_zfP89oJUxuryHX0Ok45UbzM3It-pqoBxgcqnF3fZ9Z48e6AYHkPWiwvktfBlsYYVYUtSYvERBbu2YVf9S-DA5i7F-feHoHKEf0MNEam-w-qy09lZn0jaT8OX4JIK078ItlLfMULIkLyQ_RxzjX9G-khbsXN0ody6GD3L4JV69Ol_fRPt9OH20CFkgKfUF18FESPSxIPXgyawxPJY3ND-GAfsftZmggTrcJsLJMab7wkJ4j_8L1I9hBfZ9tO3H3AZ1hQ5Hg"} +}; + +BOOST_AUTO_TEST_SUITE(eosio_system_finalizer_key_upstream_tests) + +const name alice = "alice1111111"_n; +const name bob = "bob111111111"_n; + +const std::string finalizer_key_1 = "PUB_BLS_6j4Y3LfsRiBxY-DgvqrZNMCttHftBQPIWwDiN2CMhHWULjN1nGwM1O_nEEJefqwAG4X09n4Kdt4a1mfZ1ES1cLGjQo6uLLSloiVW4i9BUhMHU2nVujP1_U_9ihdI3egZ17N-iA"; +const std::string finalizer_key_2 = "PUB_BLS_gtaOjOTa0NzDt8etBDqLoZKlfKTpTalcdfmbTJknLUJB2Fu4Cv-uoa8unF3bJ5kFewaCzf3tjYUyNE6CDSrwvYP5Nw47Y9oE9x4nqJWfJykMOoaI0kJz-GDrGN2nZdUAp5tWEg"; +const std::string finalizer_key_3 = "PUB_BLS_CT8khvZYYZdObeIV9aTnd8fZ8bdaCL1UpSRyqNLZZM5sdrOSpOjDTAY2drTYGvQPVS21BhtD8acLJhqGyTfjqrWjyY5FTfqLdcligofSpa2lrG3FqKVNeUULR5QgcIMYga4vkQ"; +const std::string finalizer_key_4 = "PUB_BLS_hJYC9REVk4Pzgt3NMycIaCRpRqTX8IIEAB8xhWg6pOYsV7n9gJnTTUOzPGH8VE4FPhxvzJuvrb5TNeR5MHwIjPMMKVPYHI-dDFwl5Oqj0yH9uoKRcMqjEaFZ5VYX3zMJuA1jQQ"; + +const std::string pop_1 = "SIG_BLS_N5r73_i50OVkydasCVVBOqqAqM4XQo_-DHgNawK77bcf06Bx0_rh5TNn9iZewNMZ6ecyEjs_sEkwjAXplhqyqf7S9FqSt8mfRxO7pE3bUZS0Z-Fxitsh9X0l_-kj3Z8VD8IwsaUwBLacudzShIXA-5E47cEqYoV3bGhANerKuDhZ4Pesm2xotAScK0pcNp0LbTNj0MZpVr0u6kJh169IoeG4ngCvD6uE2EicNrzyvDhu0u925Q1cm5z_bVha-DsANq3zcA"; +const std::string pop_2 = "SIG_BLS_9e1SzM60bWLdxwz4lYNQMNzMGeFuzFgJDYy7WykmynRVRQeIx2O2xnyzwv1WXvgYHLyMYZ4wK0Y_kU6jl330WazkBsw-_GzvIOGy8fnBnt5AyMaj9X5bhDbvB5MZc0QQz4-P2Z4SltTY17ZItGeekkjX_fgQ9kegM4qnuGU-2iqFj5i3Qf322L77b2SHjFoLmxdFOsfGpz7LyImSP8GcZH39W30cj5bmxfsp_90tGdAkz-7DG9nhSHYxFq6qTqMGijVPGg"; +const std::string pop_3 = "SIG_BLS_cJTQMGv1isqpcHEfhogLhlU56bKpGgo-Svi3Z4NXvWcly5TJo8hDChodIV-aEHgMqr06LuZftR7WFvgGkbOSdmwdO4t58R3RYOMSK-jjif2z-fEwCl7jsxUutASIRwIYtTI7h6NLCjARiKNi5BkES33xY8wYMWf-JkgbpsD2cGsZW4hkMW7T2j_1w89HmNwCn4V_hjlPM_kgz45RoYpKq4w2QEaLdCYTJ6xYOfc9Occc15c76dd1jjty_yT2RMAKO0mfUA"; +const std::string pop_4 = "SIG_BLS_GNlwGxjL-LCVDApTHernv8Hj6EqlsxWlZBUzOu6DcJmNNuHsfetXK14RPJ-L63wVnhPRL9aNrQAURy2wYJ1__rNiGk-nUMZ5RDTO7tO2EPTiyySq9cbzgn43vKG8FgsA4gbNlqVFeTCbo5CgGj8m9vXNV4-Cv68WW-ivcwJzDtnNA3O9PPIpRY6_HhbmwTUVrHL2v7X_arNCyAf29nucAYNOsCM-br6F6HwpSjqSxi4-KqcFfQCWbAbn_SgJNVAA4yx5fQ"; + +const std::string finalizer_key_binary_1 = "ea3e18dcb7ec46207163e0e0beaad934c0adb477ed0503c85b00e237608c8475942e33759c6c0cd4efe710425e7eac001b85f4f67e0a76de1ad667d9d444b570b1a3428eae2cb4a5a22556e22f415213075369d5ba33f5fd4ffd8a1748dde819"; +const std::string finalizer_key_binary_2 = "82d68e8ce4dad0dcc3b7c7ad043a8ba192a57ca4e94da95c75f99b4c99272d4241d85bb80affaea1af2e9c5ddb2799057b0682cdfded8d8532344e820d2af0bd83f9370e3b63da04f71e27a8959f27290c3a8688d24273f860eb18dda765d500"; +const std::string finalizer_key_binary_3 = "093f2486f65861974e6de215f5a4e777c7d9f1b75a08bd54a52472a8d2d964ce6c76b392a4e8c34c063676b4d81af40f552db5061b43f1a70b261a86c937e3aab5a3c98e454dfa8b75c9628287d2a5ada5ac6dc5a8a54d79450b479420708318"; +const std::string finalizer_key_binary_4 = "849602f511159383f382ddcd33270868246946a4d7f08204001f3185683aa4e62c57b9fd8099d34d43b33c61fc544e053e1c6fcc9bafadbe5335e479307c088cf30c2953d81c8f9d0c5c25e4eaa3d321fdba829170caa311a159e55617df3309"; + +BOOST_FIXTURE_TEST_CASE(register_finalizer_key_failure_tests, finalizer_key_upstream_tester) try { + { // bob111111111 does not have Alice's authority + BOOST_REQUIRE_EQUAL( error( "missing authority of bob111111111" ), + push_action(alice, "regfinkey"_n, mvo() + ("finalizer_name", "bob111111111") + ("finalizer_key", finalizer_key_1 ) + ("proof_of_possession", pop_1 ) + ) ); + } + + { // attempt to register finalizer_key for an unregistered producer + BOOST_REQUIRE_EQUAL( wasm_assert_msg( "finalizer alice1111111 is not a registered producer" ), + register_finalizer_key(alice, finalizer_key_1, pop_1)); + } + + // Now alice1111111 registers as a producer + BOOST_REQUIRE_EQUAL( success(), regproducer(alice) ); + + + { // finalizer key does not start with PUB_BLS + BOOST_REQUIRE_EQUAL( wasm_assert_msg( "finalizer key does not start with PUB_BLS: UB_BLS_6j4Y3LfsRiBxY-DgvqrZNMCttHftBQPIWwDiN2CMhHWULjN1nGwM1O_nEEJefqwAG4X09n4Kdt4a1mfZ1ES1cLGjQo6uLLSloiVW4i9BUhMHU2nVujP1_U_9ihdI3egZ17N-iA"), + push_action(alice, "regfinkey"_n, mvo() + ("finalizer_name", "alice1111111") + ("finalizer_key", "UB_BLS_6j4Y3LfsRiBxY-DgvqrZNMCttHftBQPIWwDiN2CMhHWULjN1nGwM1O_nEEJefqwAG4X09n4Kdt4a1mfZ1ES1cLGjQo6uLLSloiVW4i9BUhMHU2nVujP1_U_9ihdI3egZ17N-iA" ) + ("proof_of_possession", pop_1 ) + ) ); + } + + { // proof_of_possession does not start with SIG_BLS + BOOST_REQUIRE_EQUAL( wasm_assert_msg( "proof of possession signature does not start with SIG_BLS: XIG_BLS_N5r73_i50OVkydasCVVBOqqAqM4XQo_-DHgNawK77bcf06Bx0_rh5TNn9iZewNMZ6ecyEjs_sEkwjAXplhqyqf7S9FqSt8mfRxO7pE3bUZS0Z-Fxitsh9X0l_-kj3Z8VD8IwsaUwBLacudzShIXA-5E47cEqYoV3bGhANerKuDhZ4Pesm2xotAScK0pcNp0LbTNj0MZpVr0u6kJh169IoeG4ngCvD6uE2EicNrzyvDhu0u925Q1cm5z_bVha-DsANq3zcA" ), + push_action(alice, "regfinkey"_n, mvo() + ("finalizer_name", "alice1111111") + ("finalizer_key", finalizer_key_1) + ("proof_of_possession", "XIG_BLS_N5r73_i50OVkydasCVVBOqqAqM4XQo_-DHgNawK77bcf06Bx0_rh5TNn9iZewNMZ6ecyEjs_sEkwjAXplhqyqf7S9FqSt8mfRxO7pE3bUZS0Z-Fxitsh9X0l_-kj3Z8VD8IwsaUwBLacudzShIXA-5E47cEqYoV3bGhANerKuDhZ4Pesm2xotAScK0pcNp0LbTNj0MZpVr0u6kJh169IoeG4ngCvD6uE2EicNrzyvDhu0u925Q1cm5z_bVha-DsANq3zcA") + ) ); + } + + { // proof_of_possession fails + BOOST_REQUIRE_EQUAL( wasm_assert_msg( "proof of possession check failed" ), + push_action(alice, "regfinkey"_n, mvo() + ("finalizer_name", "alice1111111") + // use a valid formatted finalizer_key for another signature + ("finalizer_key", finalizer_key_1) + ("proof_of_possession", pop_2) + ) ); + } +} // register_finalizer_key_invalid_key_tests +FC_LOG_AND_RETHROW() + +BOOST_FIXTURE_TEST_CASE(register_finalizer_key_by_same_finalizer_tests, finalizer_key_upstream_tester) try { + BOOST_REQUIRE_EQUAL( success(), regproducer(alice) ); + + // Register first finalizer key + BOOST_REQUIRE_EQUAL( success(), register_finalizer_key(alice, finalizer_key_1, pop_1) ); + + // Make sure binary format is correct, which is important + auto alice_info = get_finalizer_info(alice); + BOOST_REQUIRE_EQUAL( "alice1111111", alice_info["finalizer_name"].as_string() ); + BOOST_REQUIRE_EQUAL( 1, alice_info["finalizer_key_count"].as_uint64() ); + BOOST_REQUIRE_EQUAL( finalizer_key_binary_1, alice_info["active_key_binary"].as_string() ); + + // Cross check finalizer keys table + uint64_t active_key_id = alice_info["active_key_id"].as_uint64(); + auto fin_key_info = get_finalizer_key_info(active_key_id); + BOOST_REQUIRE_EQUAL( "alice1111111", fin_key_info["finalizer_name"].as_string() ); + BOOST_REQUIRE_EQUAL( finalizer_key_1, fin_key_info["finalizer_key"].as_string() ); + + // Register second finalizer key + BOOST_REQUIRE_EQUAL( success(), register_finalizer_key(alice, finalizer_key_2, pop_2 )); + + alice_info = get_finalizer_info(alice); + BOOST_REQUIRE_EQUAL( 2, alice_info["finalizer_key_count"].as_uint64() ); // count incremented by 1 + BOOST_REQUIRE_EQUAL( active_key_id, alice_info["active_key_id"].as_uint64() ); // active key should not change +} +FC_LOG_AND_RETHROW() // register_finalizer_key_by_same_finalizer_tests + +BOOST_FIXTURE_TEST_CASE(register_finalizer_key_duplicate_key_tests, finalizer_key_upstream_tester) try { + BOOST_REQUIRE_EQUAL( success(), regproducer(alice) ); + + // The first finalizer key + BOOST_REQUIRE_EQUAL( success(), register_finalizer_key(alice, finalizer_key_1, pop_1) ); + + auto alice_info = get_finalizer_info(alice); + BOOST_REQUIRE_EQUAL( "alice1111111", alice_info["finalizer_name"].as_string() ); + BOOST_REQUIRE_EQUAL( 1, alice_info["finalizer_key_count"].as_uint64() ); + + // Tries to register the same finalizer key + BOOST_REQUIRE_EQUAL( wasm_assert_msg( "duplicate finalizer key: " + finalizer_key_1 ), + register_finalizer_key(alice, finalizer_key_1, pop_1) ); + + // finalizer key count still 1 + alice_info = get_finalizer_info(alice); + BOOST_REQUIRE_EQUAL( 1, alice_info["finalizer_key_count"].as_uint64() ); +} +FC_LOG_AND_RETHROW() // register_finalizer_key_duplicate_key_tests + +BOOST_FIXTURE_TEST_CASE(register_finalizer_key_by_different_finalizers_tests, finalizer_key_upstream_tester) try { + // register 2 producers + BOOST_REQUIRE_EQUAL( success(), regproducer(alice) ); + BOOST_REQUIRE_EQUAL( success(), regproducer(bob) ); + + // alice1111111 registers a finalizer key + BOOST_REQUIRE_EQUAL( success(), register_finalizer_key(alice, finalizer_key_1, pop_1) ); + BOOST_REQUIRE_EQUAL( success(), register_finalizer_key(alice, finalizer_key_2, pop_2) ); + + auto alice_info = get_finalizer_info(alice); + BOOST_REQUIRE_EQUAL( "alice1111111", alice_info["finalizer_name"].as_string() ); + BOOST_REQUIRE_EQUAL( finalizer_key_binary_1, alice_info["active_key_binary"].as_string() ); + BOOST_REQUIRE_EQUAL( 2, alice_info["finalizer_key_count"].as_uint64() ); + + // bob111111111 registers another finalizer key + BOOST_REQUIRE_EQUAL( success(), register_finalizer_key(bob, finalizer_key_3, pop_3) ); + BOOST_REQUIRE_EQUAL( success(), register_finalizer_key(bob, finalizer_key_4, pop_4) ); + + auto bob_info = get_finalizer_info(bob); + BOOST_REQUIRE_EQUAL( 2, bob_info["finalizer_key_count"].as_uint64() ); + BOOST_REQUIRE_EQUAL( finalizer_key_binary_3, bob_info["active_key_binary"].as_string() ); +} +FC_LOG_AND_RETHROW() // register_finalizer_key_by_different_finalizers_tests + + +BOOST_FIXTURE_TEST_CASE(register_duplicate_key_from_different_finalizers_tests, finalizer_key_upstream_tester) try { + BOOST_REQUIRE_EQUAL( success(), regproducer(alice) ); + BOOST_REQUIRE_EQUAL( success(), regproducer(bob) ); + + // The first finalizer key + BOOST_REQUIRE_EQUAL( success(), register_finalizer_key(alice, finalizer_key_1, pop_1) ); + + auto fin_info = get_finalizer_info(alice); + BOOST_REQUIRE_EQUAL( "alice1111111", fin_info["finalizer_name"].as_string() ); + BOOST_REQUIRE_EQUAL( 1, fin_info["finalizer_key_count"].as_uint64() ); + + // bob111111111 tries to register the same finalizer key as the first one + BOOST_REQUIRE_EQUAL( wasm_assert_msg( "duplicate finalizer key: " + finalizer_key_1 ), + register_finalizer_key(bob, finalizer_key_1, pop_1) ); +} +FC_LOG_AND_RETHROW() // register_duplicate_key_from_different_finalizers_tests + +BOOST_FIXTURE_TEST_CASE(activate_finalizer_key_failure_tests, finalizer_key_upstream_tester) try { + // bob111111111 does not have Alice's authority + BOOST_REQUIRE_EQUAL( error( "missing authority of bob111111111" ), + push_action(alice, "actfinkey"_n, mvo() + ("finalizer_name", "bob111111111") + ("finalizer_key", finalizer_key_1 ) + ) ); + + // Register producers + BOOST_REQUIRE_EQUAL( success(), regproducer(alice) ); + BOOST_REQUIRE_EQUAL( success(), regproducer(bob) ); + + // finalizer has not registered any finalizer keys yet. + BOOST_REQUIRE_EQUAL( wasm_assert_msg( "finalizer alice1111111 has not registered any finalizer keys" ), + activate_finalizer_key(alice, finalizer_key_1) ); + + // Alice registers a finalizer key + BOOST_REQUIRE_EQUAL( success(), register_finalizer_key(alice, finalizer_key_1, pop_1) ); + BOOST_REQUIRE_EQUAL( success(), register_finalizer_key(bob, finalizer_key_2, pop_2) ); + + // Activate a finalizer key not registered by anyone + BOOST_REQUIRE_EQUAL( wasm_assert_msg( "finalizer key was not registered: " + finalizer_key_3 ), + activate_finalizer_key(alice, finalizer_key_3) ); + + // Activate a finalizer key not registered by Alice + BOOST_REQUIRE_EQUAL( wasm_assert_msg( "finalizer key was not registered by the finalizer: " + finalizer_key_2 ), + activate_finalizer_key(alice, finalizer_key_2) ); + + // Activate a finalizer key that is already active (the first key registered is + // automatically set to active + BOOST_REQUIRE_EQUAL( wasm_assert_msg( "finalizer key was already active: " + finalizer_key_1 ), + activate_finalizer_key(alice, finalizer_key_1) ); +} +FC_LOG_AND_RETHROW() // activate_finalizer_key_failure_tests + +BOOST_FIXTURE_TEST_CASE(activate_finalizer_key_success_tests, finalizer_key_upstream_tester) try { + // Alice registers as a producer + BOOST_REQUIRE_EQUAL( success(), regproducer(alice) ); + + // Alice registers two finalizer keys. The first key is active + BOOST_REQUIRE_EQUAL( success(), register_finalizer_key(alice, finalizer_key_1, pop_1) ); + BOOST_REQUIRE_EQUAL( success(), register_finalizer_key(alice, finalizer_key_2, pop_2) ); + + // Check finalizer_key_1 is the active key + auto alice_info = get_finalizer_info(alice); + uint64_t active_key_id = alice_info["active_key_id"].as_uint64(); + auto finalizer_key_info = get_finalizer_key_info(active_key_id); + BOOST_REQUIRE_EQUAL( "alice1111111", finalizer_key_info["finalizer_name"].as_string() ); + BOOST_REQUIRE_EQUAL( finalizer_key_1, finalizer_key_info["finalizer_key"].as_string() ); + + // Activate the second key + BOOST_REQUIRE_EQUAL( success(), activate_finalizer_key(alice, finalizer_key_2) ); + + // Check finalizer_key_2 is the active key + alice_info = get_finalizer_info(alice); + active_key_id = alice_info["active_key_id"].as_uint64(); + finalizer_key_info = get_finalizer_key_info(active_key_id); + BOOST_REQUIRE_EQUAL( "alice1111111", finalizer_key_info["finalizer_name"].as_string() ); + BOOST_REQUIRE_EQUAL( finalizer_key_2, finalizer_key_info["finalizer_key"].as_string() ); + + // Make sure active_key_binary is correct. This test is important. + BOOST_REQUIRE_EQUAL( finalizer_key_binary_2, alice_info["active_key_binary"].as_string() ); +} +FC_LOG_AND_RETHROW() // activate_finalizer_key_success_tests + +BOOST_FIXTURE_TEST_CASE(delete_finalizer_key_failure_tests, finalizer_key_upstream_tester) try { + // bob111111111 does not have Alice's authority + BOOST_REQUIRE_EQUAL( error( "missing authority of bob111111111" ), + push_action(alice, "delfinkey"_n, mvo() + ("finalizer_name", "bob111111111") + ("finalizer_key", finalizer_key_1 ) + ) ); + + // Register producers + BOOST_REQUIRE_EQUAL( success(), regproducer(alice) ); + BOOST_REQUIRE_EQUAL( success(), regproducer(bob) ); + + // finalizer has not registered any finalizer keys yet. + BOOST_REQUIRE_EQUAL( wasm_assert_msg( "finalizer alice1111111 has not registered any finalizer keys" ), + delete_finalizer_key(alice, finalizer_key_1) ); + + // Alice and Bob register finalizer keys + BOOST_REQUIRE_EQUAL( success(), register_finalizer_key(alice, finalizer_key_1, pop_1) ); + BOOST_REQUIRE_EQUAL( success(), register_finalizer_key(bob, finalizer_key_2, pop_2) ); + BOOST_REQUIRE_EQUAL( success(), register_finalizer_key(bob, finalizer_key_3, pop_3) ); + + // Alice tries to delete a finalizer key not registered by anyone + BOOST_REQUIRE_EQUAL( wasm_assert_msg( "finalizer key was not registered: " + finalizer_key_4 ), + delete_finalizer_key(alice, finalizer_key_4) ); + + // Alice tries to delete a finalizer key registered by Bob + BOOST_REQUIRE_EQUAL( wasm_assert_msg( "finalizer key " + finalizer_key_2 + " was not registered by the finalizer alice1111111" ), + delete_finalizer_key(alice, finalizer_key_2) ); + + // Make sure finalizer_key_2 is Bob's active finalizer key and Bob has 2 keys + auto bob_info = get_finalizer_info(bob); + uint64_t active_key_id = bob_info["active_key_id"].as_uint64(); + auto finalizer_key_info = get_finalizer_key_info(active_key_id); + BOOST_REQUIRE_EQUAL( finalizer_key_2, finalizer_key_info["finalizer_key"].as_string() ); + BOOST_REQUIRE_EQUAL( 2, bob_info["finalizer_key_count"].as_uint64() ); + + // Bob tries to delete his active finalizer key but he has 2 keys + BOOST_REQUIRE_EQUAL( wasm_assert_msg( "cannot delete an active key unless it is the last registered finalizer key, has 2 keys" ), + delete_finalizer_key(bob, finalizer_key_2) ); + +} +FC_LOG_AND_RETHROW() // delete_finalizer_key_failure_tests + +BOOST_FIXTURE_TEST_CASE(delete_finalizer_key_success_test, finalizer_key_upstream_tester) try { + // Alice registers as a producer + BOOST_REQUIRE_EQUAL( success(), regproducer(alice) ); + + // Alice registers two keys and the first key is active + BOOST_REQUIRE_EQUAL( success(), register_finalizer_key(alice, finalizer_key_1, pop_1) ); + BOOST_REQUIRE_EQUAL( success(), register_finalizer_key(alice, finalizer_key_2, pop_2) ); + + // Check finalizer_key_1 is the active key + auto alice_info = get_finalizer_info(alice); + uint64_t active_key_id = alice_info["active_key_id"].as_uint64(); + auto finalizer_key_count_before = alice_info["finalizer_key_count"].as_uint64(); + auto finalizer_key_info = get_finalizer_key_info(active_key_id); + BOOST_REQUIRE_EQUAL( "alice1111111", finalizer_key_info["finalizer_name"].as_string() ); + BOOST_REQUIRE_EQUAL( finalizer_key_1, finalizer_key_info["finalizer_key"].as_string() ); + + // Delete the non-active key + BOOST_REQUIRE_EQUAL( success(), delete_finalizer_key(alice, finalizer_key_2) ); + + alice_info = get_finalizer_info(alice); + auto finalizer_key_count_after = alice_info["finalizer_key_count"].as_uint64(); + BOOST_REQUIRE_EQUAL( finalizer_key_count_before - 1, finalizer_key_count_after ); +} +FC_LOG_AND_RETHROW() // delete_finalizer_key_success_test + +BOOST_FIXTURE_TEST_CASE(delete_last_finalizer_key_test, finalizer_key_upstream_tester) try { + // Alice registers as a producer + BOOST_REQUIRE_EQUAL( success(), regproducer(alice) ); + + // Alice registers one key and it is active + BOOST_REQUIRE_EQUAL( success(), register_finalizer_key(alice, finalizer_key_1, pop_1) ); + + // Check finalizer_key_1 is the active key + auto alice_info = get_finalizer_info(alice); + uint64_t active_key_id = alice_info["active_key_id"].as_uint64(); + auto finalizer_key_info = get_finalizer_key_info(active_key_id); + BOOST_REQUIRE_EQUAL( "alice1111111", finalizer_key_info["finalizer_name"].as_string() ); + BOOST_REQUIRE_EQUAL( finalizer_key_1, finalizer_key_info["finalizer_key"].as_string() ); + + // Delete it + BOOST_REQUIRE_EQUAL( success(), delete_finalizer_key(alice, finalizer_key_1) ); + + // Both finalizer_key_1 and alice should be removed from finalizers and finalizer_keys tables + BOOST_REQUIRE_EQUAL( true, get_finalizer_key_info(active_key_id).is_null() ); + BOOST_REQUIRE_EQUAL( true, get_finalizer_info(alice).is_null() ); +} +FC_LOG_AND_RETHROW() // delete_last_finalizer_key_test + +BOOST_FIXTURE_TEST_CASE(switchtosvnn_success_tests, finalizer_key_upstream_tester) try { + // Register and vote 26 producers + auto producer_names = active_and_vote_producers(); + // Register 21 finalizer keys for the first 21 producers + register_finalizer_keys(producer_names, 21); + BOOST_REQUIRE_EQUAL(success(), push_action( config::system_account_name, "switchtosvnn"_n, mvo()) ); + + // Verify finalizers_table and last_prop_fins_table match + verify_last_proposed_finalizers(producer_names); + + // Produce enough blocks so transition to Savanna finishes + produce_blocks(504); // 21 Producers * 12 Blocks per producer * 2 rounds to reach Legacy finality + + // If head_finality_data has value, it means we are at least after or on + // Savanna Genesis Block + BOOST_REQUIRE_EQUAL( true, control->head_finality_data().has_value() ); + + // Cannot switch to Savanna multiple times + BOOST_REQUIRE_EQUAL( wasm_assert_msg( "switchtosvnn can be run only once" ), + push_action( config::system_account_name, "switchtosvnn"_n, mvo()) ); +} +FC_LOG_AND_RETHROW() + +// Activate 3 keys after transition to Savanna +BOOST_FIXTURE_TEST_CASE(multi_activation_tests, finalizer_key_upstream_tester) try { + // Register and vote 26 producers + auto producer_names = active_and_vote_producers(); + // Register 21 finalizer keys for the first 21 producers + register_finalizer_keys(producer_names, 21); + BOOST_REQUIRE_EQUAL(success(), push_action( config::system_account_name, "switchtosvnn"_n, mvo()) ); + + // Verify finalizers_table and last_prop_fins_table match + verify_last_proposed_finalizers(producer_names); + + // Produce enough blocks so transition to Savanna finishes + produce_blocks(504); // 21 Producers * 12 Blocks per producer * 2 rounds to reach Legacy finality + + // If head_finality_data has value, it means we are at least after or on + // Savanna Genesis Block + BOOST_REQUIRE_EQUAL( true, control->head_finality_data().has_value() ); + + // Register two more key for defproducera + account_name producera = "defproducera"_n; + BOOST_REQUIRE_EQUAL( success(), register_finalizer_key(producera, finalizer_key_1, pop_1) ); + BOOST_REQUIRE_EQUAL( success(), register_finalizer_key(producera, finalizer_key_2, pop_2) ); + + auto producera_info = get_finalizer_info(producera); + auto active_key_id_before = producera_info["active_key_id"].as_uint64(); + auto last_prop_fin_ids_before = get_last_prop_fin_ids(); + BOOST_REQUIRE_EQUAL( 21, last_prop_fin_ids_before.size() ); + + // Activate finalizer_key_1 + BOOST_REQUIRE_EQUAL( success(), activate_finalizer_key(producera, finalizer_key_1) ); + produce_block(); + + // Make sure last proposed finalizers set has changed + producera_info = get_finalizer_info(producera); + auto active_key_id_after = producera_info["active_key_id"].as_uint64(); + auto last_prop_fin_ids_after = get_last_prop_fin_ids(); + BOOST_REQUIRE_EQUAL( 21, last_prop_fin_ids_after.size() ); + + last_prop_fin_ids_before.erase(active_key_id_before); + last_prop_fin_ids_before.insert(active_key_id_after); + BOOST_REQUIRE_EQUAL( true, last_prop_fin_ids_before == last_prop_fin_ids_after ); + + // Activate finalizer_key_2 + last_prop_fin_ids_before = last_prop_fin_ids_after; + active_key_id_before = active_key_id_after; + + BOOST_REQUIRE_EQUAL( success(), activate_finalizer_key(producera, finalizer_key_2) ); + produce_block(); + + // Make sure last proposed finalizers set has changed + producera_info = get_finalizer_info(producera); + active_key_id_after = producera_info["active_key_id"].as_uint64(); + last_prop_fin_ids_after = get_last_prop_fin_ids(); + BOOST_REQUIRE_EQUAL( 21, last_prop_fin_ids_after.size() ); + + last_prop_fin_ids_before.erase(active_key_id_before); + last_prop_fin_ids_before.insert(active_key_id_after); + BOOST_REQUIRE_EQUAL( true, last_prop_fin_ids_before == last_prop_fin_ids_after ); +} +FC_LOG_AND_RETHROW() + +BOOST_FIXTURE_TEST_CASE(switchtosvnn_missing_authority_tests, finalizer_key_upstream_tester) try { + BOOST_REQUIRE_EQUAL( error( "missing authority of eosio" ), + push_action( alice, "switchtosvnn"_n, mvo()) ); +} +FC_LOG_AND_RETHROW() + +BOOST_FIXTURE_TEST_CASE(switchtosvnn_not_enough_finalizer_keys_tests, finalizer_key_upstream_tester) try { + auto producer_names = active_and_vote_producers(); + + // Register 20 finalizer keys + register_finalizer_keys(producer_names, 20); + + // Have only 20 finalizer keys, short by 1 + BOOST_REQUIRE_EQUAL( wasm_assert_msg( "producer defproduceru does not have an active finalizer key" ), + push_action( config::system_account_name, "switchtosvnn"_n, mvo()) ); +} +FC_LOG_AND_RETHROW() + +// Finalizers are not changed in current schedule rounds +BOOST_FIXTURE_TEST_CASE(update_elected_producers_no_finalizers_changed_test, finalizer_key_upstream_tester) try { + auto producer_names = active_and_vote_producers(); + register_finalizer_keys(producer_names, 21); + BOOST_REQUIRE_EQUAL(success(), push_action( config::system_account_name, "switchtosvnn"_n, mvo()) ); + + // Produce enough blocks so transition to Savanna finishes + produce_blocks(504); // 21 Producers * 12 Blocks per producer * 2 rounds to reach Legacy finality + + // head_finality_data is available when nodoes is in Savanna + BOOST_REQUIRE_EQUAL( true, control->head_finality_data().has_value() ); + + // Verify finalizers_table and last_prop_fins_table match + verify_last_proposed_finalizers(producer_names); + + auto last_finkey_ids = get_last_prop_fin_ids(); + + // Produce for one round + produce_block( fc::minutes(2) ); + + // Since finalizer keys have not changed, last_finkey_ids should be the same + auto last_finkey_ids_2 = get_last_prop_fin_ids(); + BOOST_REQUIRE_EQUAL( 21, last_finkey_ids_2.size() ); + BOOST_REQUIRE_EQUAL( true, last_finkey_ids == last_finkey_ids_2 ); +} +FC_LOG_AND_RETHROW() + +// An active finalizer activates another key. The change takes effect immediately. +BOOST_FIXTURE_TEST_CASE(update_elected_producers_finalizers_changed_test, finalizer_key_upstream_tester) try { + auto producer_names = active_and_vote_producers(); + register_finalizer_keys(producer_names, 21); + BOOST_REQUIRE_EQUAL(success(), push_action( config::system_account_name, "switchtosvnn"_n, mvo()) ); + + // Produce enough blocks so transition to Savanna finishes + produce_blocks(504); // 21 Producers * 12 Blocks per producer * 2 rounds to reach Legacy finality + + // head_finality_data is available when nodoes is in Savanna + BOOST_REQUIRE_EQUAL( true, control->head_finality_data().has_value() ); + + // Verify finalizers_table and last_prop_fins_table match + verify_last_proposed_finalizers(producer_names); + // Verify last finalizer key id table contains all finalzer keys + auto last_finkey_ids = get_last_prop_fin_ids(); + + // Pick a producer + name test_producer = producer_names.back(); + + // Take a note of old active_key_id + auto p_info = get_finalizer_info(test_producer); + uint64_t old_id = p_info["active_key_id"].as_uint64(); + + // Register and activate a new finalizer key + BOOST_REQUIRE_EQUAL( success(), register_finalizer_key(test_producer, finalizer_key_1, pop_1) ); + BOOST_REQUIRE_EQUAL( success(), activate_finalizer_key(test_producer, finalizer_key_1)); + + // Since producer is an active producer, the finalizer key change takes effective + // immediately. + auto last_finkey_ids_2 = get_last_prop_fin_ids(); + BOOST_REQUIRE_EQUAL( 21, last_finkey_ids_2.size() ); + + // Take a note of new active_key_id + auto p_info_2 = get_finalizer_info(test_producer); + uint64_t new_id = p_info_2["active_key_id"].as_uint64(); + + // After replace the old_id with new_id in last_finkey_ids, + // last_finkey_ids should be the same as last_finkey_ids_2 + last_finkey_ids.erase(old_id); + last_finkey_ids.insert(new_id); + BOOST_REQUIRE_EQUAL( true, last_finkey_ids == last_finkey_ids_2 ); +} +FC_LOG_AND_RETHROW() + +// Telos adaptation of upstream's "finalizer replaced" test. Under Telos, +// if the keyed candidate count drops below the last schedule size the +// schedule and finalizer policy freeze rather than refilling from standbys, +// and voting more than 21 producers engages BP rotation. So the replacement +// flow is: add a NEW keyed producer first, then delete an existing +// finalizer's only key, leaving exactly 21 keyed candidates, and verify the +// policy converges to include the new producer and exclude the deleted one. +BOOST_FIXTURE_TEST_CASE(update_elected_producers_finalizers_replaced_test, finalizer_key_upstream_tester) try { + auto producer_names = active_and_vote_producers(); + register_finalizer_keys(producer_names, 21); + BOOST_REQUIRE_EQUAL(success(), push_action(config::system_account_name, "switchtosvnn"_n, mvo())); + + produce_blocks(504); + BOOST_REQUIRE_EQUAL( true, control->head_finality_data().has_value() ); + verify_last_proposed_finalizers(producer_names); + auto last_finkey_ids = get_last_prop_fin_ids(); + + // defproducerv: a new producer with a registered finalizer key + account_name producerv = "defproducerv"_n; + setup_producer_accounts({producerv}, core_sym::from_string("1000.0000")); + BOOST_REQUIRE_EQUAL( success(), regproducer(producerv) ); + BOOST_REQUIRE_EQUAL( success(), register_finalizer_key(producerv, finalizer_key_1, pop_1) ); + uint64_t producerv_id = get_finalizer_info(producerv)["active_key_id"].as_uint64(); + + // vote it in alongside the original 21 + std::vector new_votes = producer_names; + new_votes.push_back(producerv); + std::sort(new_votes.begin(), new_votes.end()); + BOOST_REQUIRE_EQUAL( success(), vote("alice1111111"_n, new_votes) ); + + // defproducera deletes its only finalizer key, losing schedule eligibility; + // exactly 21 keyed candidates remain + name producera = "defproducera"_n; + uint64_t deleted_id = get_finalizer_info(producera)["active_key_id"].as_uint64(); + auto deleted_key_str = get_finalizer_key_info(deleted_id)["finalizer_key"].as_string(); + BOOST_REQUIRE_EQUAL( success(), delete_finalizer_key(producera, deleted_key_str) ); + + produce_blocks(504); + + auto last_finkey_ids_2 = get_last_prop_fin_ids(); + BOOST_REQUIRE_EQUAL( 21u, last_finkey_ids_2.size() ); + BOOST_REQUIRE_EQUAL( 1u, last_finkey_ids_2.count(producerv_id) ); + BOOST_REQUIRE_EQUAL( 0u, last_finkey_ids_2.count(deleted_id) ); +} +FC_LOG_AND_RETHROW() + +BOOST_AUTO_TEST_SUITE_END() From b6c3da646f241bec211977a8c6f5bfb20832ef12 Mon Sep 17 00:00:00 2001 From: Tobias Date: Wed, 17 Jun 2026 12:38:31 -0400 Subject: [PATCH 16/20] Audit remediation: fix recalc halt, EVM-vote auth/underflow, kick underflow, onblock get_balance abort, Savanna key-deletion freeze Resolves the contract-side findings from the security audit of this branch (see AUDIT_telos_system_contract.md). Node-side findings (BLS length overflow, QC bitset OOB) are addressed in a separate teloszero-core PR. voting.cpp (findings #1/#2/#3 - latent CRITICAL chain halt, pre-existing): - Clamp _gstate.total_producer_vote_weight to >= 0 at both mutation sites (update_votes and propagate_weight_change), mirroring the per-producer clamp and the EVM-vote path. Without this the discarded negative mass leaked into the unclamped global and could drift it below the recalculate_votes() `<= -0.1` trigger. - Make the recalculate_votes() replay non-aborting (new `recalculating` flag): it runs inside onblock, so a check(false) on a stale inactive producer or a deregistered proxy left in a voter row would permanently halt block production. Skip such entries instead of asserting. eosio.system.cpp (findings #4/#5 - EVM-vote bridge, dormant on mainnet): - require_auth(get_self()) on getevmvote and setbpevmstat (were permissionless; they mutate consensus-relevant vote weight / send system EVM txs). - Fix the uint64 underflow in the vote delta: subtract smaller-from-larger and carry the sign explicitly, instead of double(current - previous) which wrapped to ~1.84e19 on any vote decrease and inflated total_votes. - Documented the remaining partial-sync caveat (full per-producer EVM-component redesign deferred; auth gate removes attacker reachability meanwhile). finalizer_key.cpp (finding #9 - Savanna schedule/finalizer freeze, NEW): - Reject delfinkey of an active producer's last finalizer key under Savanna; require unregprod first, so the keyed-producer set cannot silently fall below last_producer_schedule_size via key deletion. producer_pay.cpp (finding #10): - Non-aborting tedp core-balance lookup in claimrewards_snapshot()/pay() so a missing balance row cannot abort onblock. system_kick.cpp (finding #11): - Clamp the block_counter_correction subtraction instead of letting the uint32 underflow wrongly autokick a producer. tests: - evm_vote_sync_actions_require_system_auth (getevmvote/setbpevmstat auth). - delfinkey_blocked_for_active_producer_under_savanna. Co-Authored-By: Claude Opus 4.8 --- .../include/eosio.system/eosio.system.hpp | 2 +- contracts/eosio.system/src/eosio.system.cpp | 21 +++++++- contracts/eosio.system/src/finalizer_key.cpp | 12 +++++ contracts/eosio.system/src/producer_pay.cpp | 27 +++++++++- contracts/eosio.system/src/system_kick.cpp | 8 ++- contracts/eosio.system/src/voting.cpp | 50 +++++++++++++++---- tests/eosio.system_finalizer_key_tests.cpp | 27 ++++++++++ tests/telos.system_tests.cpp | 18 +++++++ 8 files changed, 149 insertions(+), 16 deletions(-) diff --git a/contracts/eosio.system/include/eosio.system/eosio.system.hpp b/contracts/eosio.system/include/eosio.system/eosio.system.hpp index 479e06a4..56f10e47 100644 --- a/contracts/eosio.system/include/eosio.system/eosio.system.hpp +++ b/contracts/eosio.system/include/eosio.system/eosio.system.hpp @@ -1898,7 +1898,7 @@ namespace eosiosystem { // defined in voting.cpp void register_producer( const name& producer, const eosio::block_signing_authority& producer_authority, const std::string& url, uint16_t location ); void update_elected_producers( const block_timestamp& timestamp ); - void update_votes( const name& voter, const name& proxy, const std::vector& producers, bool voting ); + void update_votes( const name& voter, const name& proxy, const std::vector& producers, bool voting, bool recalculating = false ); void propagate_weight_change( const voter_info& voter ); double update_producer_votepay_share( const producers_table2::const_iterator& prod_itr, const time_point& ct, diff --git a/contracts/eosio.system/src/eosio.system.cpp b/contracts/eosio.system/src/eosio.system.cpp index cff610d7..3859c35a 100644 --- a/contracts/eosio.system/src/eosio.system.cpp +++ b/contracts/eosio.system/src/eosio.system.cpp @@ -656,6 +656,15 @@ namespace eosiosystem { } void system_contract::getevmvote( std::vector bps ) { + // getevmvote mutates consensus-relevant producer vote weight (and, under Savanna, the + // finalizer set). It must NOT be permissionless: an unauthenticated caller can choose + // which BPs to sync and when, and can drive the partial-sync inflation described in the + // audit. Gate it on the system account. NOTE: the caller is responsible for syncing the + // COMPLETE set of producers whose EVM vote changed (both increases and decreases) in one + // transaction; partial syncs leave un-synced producers stale. A future redesign should + // track a per-producer EVM-vote component separately from total_votes before EVM voting + // is enabled on mainnet. + require_auth( get_self() ); eosio::check(_gvoting_config.evm_voting_contract != eosio::checksum160(), "EVM voting contract not set"); @@ -742,7 +751,14 @@ namespace eosiosystem { uint256_t current_vote = eosio_evm::checksum256ToValue(total_votes_of_bp->value); uint256_t current_vote_normalized = current_vote / ten_power_14; // Divide by 1e14 uint64_t current_vote_normalized_u64 = current_vote_normalized.lo.lo; - double vote_delta = double(current_vote_normalized_u64 - previous_vote_normalized_u64); + // Compute the delta without unsigned wraparound. Both operands are uint64; the + // previous form `double(current - previous)` underflowed to ~1.84e19 whenever a + // BP's EVM vote DECREASED (normal via decay/unvote), inflating total_votes and + // corrupting the schedule/finalizer set. Subtract the smaller from the larger and + // carry the sign explicitly. + double vote_delta = (current_vote_normalized_u64 >= previous_vote_normalized_u64) + ? double(current_vote_normalized_u64 - previous_vote_normalized_u64) + : -double(previous_vote_normalized_u64 - current_vote_normalized_u64); p.total_votes += vote_delta; if ( p.total_votes < 0 ) { p.total_votes = 0; @@ -764,6 +780,9 @@ namespace eosiosystem { eosio::check(is_changed, "None of the BPs EVM votes has been changed"); } void system_contract::setbpevmstat( eosio::name bp ) { + // Sends a system-authored inline eosio.evm `raw` transaction and is consensus-adjacent; + // must not be permissionless. Gate on the system account. + require_auth( get_self() ); eosio::check(_gvoting_config.evm_voting_contract != eosio::checksum160(), "EVM voting contract not set"); diff --git a/contracts/eosio.system/src/finalizer_key.cpp b/contracts/eosio.system/src/finalizer_key.cpp index 77ed0cf7..3d9e06b1 100644 --- a/contracts/eosio.system/src/finalizer_key.cpp +++ b/contracts/eosio.system/src/finalizer_key.cpp @@ -292,6 +292,18 @@ namespace eosiosystem { if( fin_key_itr->is_active(finalizer->active_key_id) ) { check( finalizer->finalizer_key_count == 1, "cannot delete an active key unless it is the last registered finalizer key, has " + std::to_string(finalizer->finalizer_key_count) + " keys"); + + // Deleting the active (and, per the check above, last) key removes this producer from + // the keyed set. Under Savanna a still-active producer that drops its only finalizer key + // shrinks the keyed producer count, which can push it below last_producer_schedule_size + // and freeze both the producer-schedule and finalizer-policy updates (audit finding #9). + // Require the producer to unregister first so the keyed set cannot silently fall below + // the active schedule via key deletion. + if( is_savanna_consensus() ) { + auto prod = _producers.find( finalizer_name.value ); + check( prod == _producers.end() || !prod->is_active, + "an active producer cannot delete its last finalizer key under Savanna; call unregprod first" ); + } } if( finalizer->finalizer_key_count == 1 ) { diff --git a/contracts/eosio.system/src/producer_pay.cpp b/contracts/eosio.system/src/producer_pay.cpp index 7a6254d6..30a7740d 100644 --- a/contracts/eosio.system/src/producer_pay.cpp +++ b/contracts/eosio.system/src/producer_pay.cpp @@ -13,6 +13,29 @@ namespace eosiosystem { using eosio::microseconds; using eosio::token; + namespace { + // Non-aborting core-token balance lookup. eosio::token::get_balance() calls accounts.get() + // which asserts ("no balance with specified symbol") when the owner has no row for the + // symbol. claimrewards_snapshot() runs inside onblock, so that assert would HALT block + // production if eosio.tedp's core-symbol balance row were ever absent (e.g. closed). The + // eosio.token `accounts` table struct is private in eosio.token.hpp, so mirror its layout + // (no [[eosio::table]] attribute -> not added to eosio.system's ABI) and treat a missing + // row as a zero balance. Downstream logic already handles amount <= 0 by issuing. + struct token_account_balance { + eosio::asset balance; + uint64_t primary_key() const { return balance.symbol.code().raw(); } + + EOSLIB_SERIALIZE( token_account_balance, (balance) ) + }; + typedef eosio::multi_index< "accounts"_n, token_account_balance > token_accounts_table; + + eosio::asset get_balance_or_zero( const eosio::name& token_contract, const eosio::name& owner, const eosio::symbol& sym ) { + token_accounts_table acnts( token_contract, owner.value ); + auto it = acnts.find( sym.code().raw() ); + return it == acnts.end() ? eosio::asset( 0, sym ) : it->balance; + } + } + void system_contract::onblock( ignore ) { using namespace eosio; @@ -298,7 +321,7 @@ namespace eosiosystem { auto new_tokens = to_workers + to_producers; //NOTE: This line can cause failure if eosio.tedp doesn't have a balance emplacement - asset tedp_balance = eosio::token::get_balance(token_account, tedp_account, core_symbol().code()); + asset tedp_balance = get_balance_or_zero(token_account, tedp_account, core_symbol()); int64_t transfer_tokens = 0; int64_t issue_tokens = 0; @@ -449,7 +472,7 @@ namespace eosiosystem { check(payouts_made, "No payouts are due"); // Gets the TEDP account balance - asset tedp_balance = eosio::token::get_balance(token_account, tedp_account, core_symbol().code()); + asset tedp_balance = get_balance_or_zero(token_account, tedp_account, core_symbol()); // Calculates the amount of TLOS need to be issued int64_t issue_tokens = 0; diff --git a/contracts/eosio.system/src/system_kick.cpp b/contracts/eosio.system/src/system_kick.cpp index d8e2f9c0..57469eaf 100644 --- a/contracts/eosio.system/src/system_kick.cpp +++ b/contracts/eosio.system/src/system_kick.cpp @@ -86,7 +86,13 @@ namespace eosiosystem { if (_gschedule_metrics.last_onblock_caller == "eosio"_n) { for (auto &pm : _gschedule_metrics.producers_metric) { if (pm.bp_name == producer) { - pm.missed_blocks_per_cycle -= uint32_t(_gschedule_metrics.block_counter_correction); + // Clamp instead of letting the uint32 subtraction underflow to ~2^32 (which would + // make missed_blocks_per_rotation cross the kick threshold and wrongly deactivate a + // producer). block_counter_correction can exceed a single cycle's block count. + uint32_t correction = uint32_t(_gschedule_metrics.block_counter_correction); + pm.missed_blocks_per_cycle = (pm.missed_blocks_per_cycle > correction) + ? (pm.missed_blocks_per_cycle - correction) + : 0; break; } } diff --git a/contracts/eosio.system/src/voting.cpp b/contracts/eosio.system/src/voting.cpp index 591c3a52..82a3f2a4 100644 --- a/contracts/eosio.system/src/voting.cpp +++ b/contracts/eosio.system/src/voting.cpp @@ -416,7 +416,7 @@ namespace eosiosystem { } // voteupdate - void system_contract::update_votes( const name& voter_name, const name& proxy, const std::vector& producers, bool voting ) { + void system_contract::update_votes( const name& voter_name, const name& proxy, const std::vector& producers, bool voting, bool recalculating ) { //validate input if ( proxy ) { check( producers.size() == 0, "cannot vote for producers and proxy at same time" ); @@ -497,16 +497,22 @@ namespace eosiosystem { if( proxy ) { auto new_proxy = _voters.find( proxy.value ); - check( new_proxy != _voters.end(), "invalid proxy specified" ); //if ( !voting ) { data corruption } else { wrong vote } - check( !voting || new_proxy->is_proxy, "proxy not found" ); - - _voters.modify( new_proxy, same_payer, [&]( auto& vp ) { - vp.proxied_vote_weight += voter->staked; - }); + const bool stale_proxy = ( new_proxy == _voters.end() || !new_proxy->is_proxy ); + // During a recalculation replay (which runs inside onblock), a voter may still reference + // a proxy that has since deregistered or disappeared. Skip the stale proxy contribution + // instead of aborting: a check(false) here would halt block production. + if( !( recalculating && stale_proxy ) ) { + check( new_proxy != _voters.end(), "invalid proxy specified" ); //if ( !voting ) { data corruption } else { wrong vote } + check( !voting || new_proxy->is_proxy, "proxy not found" ); + + _voters.modify( new_proxy, same_payer, [&]( auto& vp ) { + vp.proxied_vote_weight += voter->staked; + }); - if((*new_proxy).last_vote_weight > 0){ - _gstate.total_activated_stake += totalStaked - voter->last_stake; - propagate_weight_change( *new_proxy ); + if((*new_proxy).last_vote_weight > 0){ + _gstate.total_activated_stake += totalStaked - voter->last_stake; + propagate_weight_change( *new_proxy ); + } } } else { if( new_vote_weight >= 0 ) { @@ -526,6 +532,10 @@ namespace eosiosystem { auto pitr = _producers.find( pd.first.value ); if( pitr != _producers.end() ) { if( voting && !pitr->active() && pd.second.second /* from new set */ ) { + // During a recalculation replay (runs inside onblock), a voter may still list a + // producer that has since been deactivated. Skip it instead of aborting; a + // check(false) on this path would permanently halt block production. + if( recalculating ) continue; check( false, ( "producer " + pitr->owner.to_string() + " is not currently registered" ).data() ); } _producers.modify( pitr, same_payer, [&]( auto& p ) { @@ -534,10 +544,18 @@ namespace eosiosystem { p.total_votes = 0; } _gstate.total_producer_vote_weight += pd.second.first; + // Keep the global aggregate consistent with the per-producer clamp above. Without + // this, the negative mass discarded by the per-producer clamp leaks into the + // unclamped global and can drift it below the recalculate_votes() `<= -0.1` + // trigger. The EVM-vote path (eosio.system.cpp) already clamps identically. + if ( _gstate.total_producer_vote_weight < 0 ) { + _gstate.total_producer_vote_weight = 0; + } //check( p.total_votes >= 0, "something bad happened" ); }); } else { if( pd.second.second ) { + if( recalculating ) continue; check( false, ( "producer " + pd.first.to_string() + " is not registered" ).data() ); } } @@ -659,7 +677,15 @@ namespace eosiosystem { auto &pitr = _producers.get(acnt.value, "producer not found"); // data corruption _producers.modify(pitr, same_payer, [&](auto &p) { p.total_votes += delta; + if (p.total_votes < 0) { + p.total_votes = 0; + } _gstate.total_producer_vote_weight += delta; + // Clamp the global aggregate (see update_votes) so a negative delta cannot drift + // it below the recalculate_votes() trigger. + if (_gstate.total_producer_vote_weight < 0) { + _gstate.total_producer_vote_weight = 0; + } }); } } @@ -700,7 +726,9 @@ namespace eosiosystem { }); processed_proxies[voter->owner] = true; } - update_votes(voter->owner, voter->proxy, voter->producers, true); + // recalculating=true: this replay runs inside onblock, so it must never abort on a + // stale (now-inactive/removed) producer or deregistered proxy left in a voter row. + update_votes(voter->owner, voter->proxy, voter->producers, true, /*recalculating=*/true); } } } diff --git a/tests/eosio.system_finalizer_key_tests.cpp b/tests/eosio.system_finalizer_key_tests.cpp index b783c0e7..a4c01d81 100644 --- a/tests/eosio.system_finalizer_key_tests.cpp +++ b/tests/eosio.system_finalizer_key_tests.cpp @@ -222,4 +222,31 @@ BOOST_FIXTURE_TEST_CASE( savanna_schedule_metrics_preserved, finalizer_key_teste BOOST_REQUIRE( any_mid_cycle ); } FC_LOG_AND_RETHROW() +// Regression for audit finding #9: under Savanna an active scheduled producer must not be able +// to delete its last finalizer key. Doing so removes it from the keyed set and can push the keyed +// producer count below last_producer_schedule_size, freezing both producer-schedule and +// finalizer-policy updates. The producer must unregister first. +BOOST_FIXTURE_TEST_CASE( delfinkey_blocked_for_active_producer_under_savanna, finalizer_key_tester ) try { + auto producer_names = active_and_vote_producers(); + const auto victim = producer_names[0]; + + std::string victim_pub; + for( const auto& p : producer_names ) { + const auto key = new_bls_key(); + const auto pub = key.get_public_key().to_string(); + if( p == victim ) victim_pub = pub; + BOOST_REQUIRE_EQUAL( success(), + regfinkey( p, pub, key.proof_of_possession().to_string() ) ); + } + BOOST_REQUIRE_EQUAL( success(), switchtosvnn( config::system_account_name ) ); + + // active producer cannot drop its only (active) finalizer key while on Savanna + BOOST_REQUIRE_EQUAL( wasm_assert_msg( "an active producer cannot delete its last finalizer key under Savanna; call unregprod first" ), + delfinkey( victim, victim_pub ) ); + + // once the producer is unregistered (inactive), the key may be deleted + BOOST_REQUIRE_EQUAL( success(), push_action( victim, "unregprod"_n, mvo()("producer", victim) ) ); + BOOST_REQUIRE_EQUAL( success(), delfinkey( victim, victim_pub ) ); +} FC_LOG_AND_RETHROW() + BOOST_AUTO_TEST_SUITE_END() diff --git a/tests/telos.system_tests.cpp b/tests/telos.system_tests.cpp index 0b579acd..24de3412 100644 --- a/tests/telos.system_tests.cpp +++ b/tests/telos.system_tests.cpp @@ -655,4 +655,22 @@ BOOST_FIXTURE_TEST_CASE(missed_block_autokick_threshold_and_lifetime, eosio_syst BOOST_REQUIRE_LE(lifetime, last_missed + 60); } FC_LOG_AND_RETHROW() +// Regression for audit finding #4: getevmvote and setbpevmstat mutate consensus-relevant +// producer vote weight (and, under Savanna, the finalizer set) / drive system-authored EVM +// transactions. They were permissionless; they must require the system account's authority. +// require_auth(get_self()) is the FIRST statement, so a non-system caller is rejected before any +// EVM-state lookup, and a system caller proceeds to the "EVM voting contract not set" guard +// (EVM voting is unconfigured in the tester) - proving authorization is the gate. +BOOST_FIXTURE_TEST_CASE( evm_vote_sync_actions_require_system_auth, eosio_system_tester ) try { + BOOST_REQUIRE_EQUAL( error( "missing authority of eosio" ), + push_action( "alice1111111"_n, "getevmvote"_n, mvo()("bps", std::vector{}) ) ); + BOOST_REQUIRE_EQUAL( error( "missing authority of eosio" ), + push_action( "bob111111111"_n, "setbpevmstat"_n, mvo()("bp", "alice1111111"_n) ) ); + + BOOST_REQUIRE_EQUAL( wasm_assert_msg( "EVM voting contract not set" ), + push_action( config::system_account_name, "getevmvote"_n, mvo()("bps", std::vector{}) ) ); + BOOST_REQUIRE_EQUAL( wasm_assert_msg( "EVM voting contract not set" ), + push_action( config::system_account_name, "setbpevmstat"_n, mvo()("bp", "alice1111111"_n) ) ); +} FC_LOG_AND_RETHROW() + BOOST_AUTO_TEST_SUITE_END() From 08c0863563b59d898d7b04015901d64bed2a57f8 Mon Sep 17 00:00:00 2001 From: Tobias Date: Wed, 17 Jun 2026 13:34:41 -0400 Subject: [PATCH 17/20] Address review: scope finalizer-key deletion guard, order EVM account checks, whitespace finalizer_key.cpp: the delfinkey guard added earlier blocked ANY active producer from deleting its last finalizer key, which also blocked the normal finalizer- replacement flow (and broke update_elected_producers_finalizers_replaced_test). Scope it to the actual freeze condition: block only when deleting would drop the number of keyed, active, voted producers below last_producer_schedule_size (i.e. no replacement to keep the schedule full). When enough other keyed producers remain, the deletion is allowed and the schedule replaces this one. Update the Telos delfinkey regression test message to match. eosio.system.cpp: in setvotedecay and setbpevmstat, check that the eosio EVM account row exists before reading ->address (previously dereferenced the iterator before the existence check). tests: strip trailing whitespace flagged by `git diff --check`. Co-Authored-By: Claude Opus 4.8 --- contracts/eosio.system/src/eosio.system.cpp | 4 +-- contracts/eosio.system/src/finalizer_key.cpp | 31 +++++++++++++------ tests/eosio.system_finalizer_key_tests.cpp | 5 +-- ...io.system_finalizer_key_upstream_tests.cpp | 2 +- 4 files changed, 27 insertions(+), 15 deletions(-) diff --git a/contracts/eosio.system/src/eosio.system.cpp b/contracts/eosio.system/src/eosio.system.cpp index 3859c35a..5e559401 100644 --- a/contracts/eosio.system/src/eosio.system.cpp +++ b/contracts/eosio.system/src/eosio.system.cpp @@ -626,8 +626,8 @@ namespace eosiosystem { std::array evm_voting_contract_address = _gvoting_config.evm_voting_contract.extract_as_byte_array(); auto accounts_byaccount = account.get_index(); auto eosio_account = accounts_byaccount.find(get_self().value); - std::optional eosio_account_address = eosio_account->address; eosio::check(eosio_account != accounts_byaccount.end(),"eosio EVM address not found"); + std::optional eosio_account_address = eosio_account->address; // Encode EVM transaction auto tx_hex = rlp::encode( @@ -839,8 +839,8 @@ namespace eosiosystem { // Get eosio EVM address nonce auto accounts_byaccount = account.get_index(); auto eosio_account = accounts_byaccount.find(get_self().value); - std::optional eosio_account_address = eosio_account->address; eosio::check(eosio_account != accounts_byaccount.end(),"eosio EVM address not found"); + std::optional eosio_account_address = eosio_account->address; // Encode EVM transaction auto tx_hex = rlp::encode( diff --git a/contracts/eosio.system/src/finalizer_key.cpp b/contracts/eosio.system/src/finalizer_key.cpp index 3d9e06b1..dda12492 100644 --- a/contracts/eosio.system/src/finalizer_key.cpp +++ b/contracts/eosio.system/src/finalizer_key.cpp @@ -293,16 +293,27 @@ namespace eosiosystem { if( fin_key_itr->is_active(finalizer->active_key_id) ) { check( finalizer->finalizer_key_count == 1, "cannot delete an active key unless it is the last registered finalizer key, has " + std::to_string(finalizer->finalizer_key_count) + " keys"); - // Deleting the active (and, per the check above, last) key removes this producer from - // the keyed set. Under Savanna a still-active producer that drops its only finalizer key - // shrinks the keyed producer count, which can push it below last_producer_schedule_size - // and freeze both the producer-schedule and finalizer-policy updates (audit finding #9). - // Require the producer to unregister first so the keyed set cannot silently fall below - // the active schedule via key deletion. - if( is_savanna_consensus() ) { - auto prod = _producers.find( finalizer_name.value ); - check( prod == _producers.end() || !prod->is_active, - "an active producer cannot delete its last finalizer key under Savanna; call unregprod first" ); + // Deleting the active (and, per the check above, last) key removes this producer from the + // keyed set. Under Savanna, update_elected_producers() stops proposing (freezing both the + // producer schedule and finalizer policy) when the number of keyed, active, voted + // producers falls below last_producer_schedule_size. Block the deletion only in that case + // -- i.e. when there is no replacement to keep the schedule full. When enough other keyed + // producers remain, the schedule simply replaces this one, which is allowed (this is the + // normal finalizer-replacement flow). + auto prod = _producers.find( finalizer_name.value ); + if( is_savanna_consensus() && prod != _producers.end() && prod->is_active ) { + uint32_t other_keyed_active = 0; + for( auto fitr = _finalizers.begin(); fitr != _finalizers.end(); ++fitr ) { + if( fitr->finalizer_name == finalizer_name || fitr->active_key_binary.empty() ) { + continue; + } + auto other_prod = _producers.find( fitr->finalizer_name.value ); + if( other_prod != _producers.end() && other_prod->is_active && other_prod->total_votes > 0 ) { + ++other_keyed_active; + } + } + check( other_keyed_active >= _gstate.last_producer_schedule_size, + "deleting this finalizer key would drop the keyed producer set below the active schedule size; unregister the producer or register a replacement first" ); } } diff --git a/tests/eosio.system_finalizer_key_tests.cpp b/tests/eosio.system_finalizer_key_tests.cpp index a4c01d81..d35eee10 100644 --- a/tests/eosio.system_finalizer_key_tests.cpp +++ b/tests/eosio.system_finalizer_key_tests.cpp @@ -240,8 +240,9 @@ BOOST_FIXTURE_TEST_CASE( delfinkey_blocked_for_active_producer_under_savanna, fi } BOOST_REQUIRE_EQUAL( success(), switchtosvnn( config::system_account_name ) ); - // active producer cannot drop its only (active) finalizer key while on Savanna - BOOST_REQUIRE_EQUAL( wasm_assert_msg( "an active producer cannot delete its last finalizer key under Savanna; call unregprod first" ), + // With exactly last_producer_schedule_size (21) keyed producers, dropping one would leave too + // few keyed candidates to fill the schedule, so the deletion is blocked (no replacement). + BOOST_REQUIRE_EQUAL( wasm_assert_msg( "deleting this finalizer key would drop the keyed producer set below the active schedule size; unregister the producer or register a replacement first" ), delfinkey( victim, victim_pub ) ); // once the producer is unregistered (inactive), the key may be deleted diff --git a/tests/eosio.system_finalizer_key_upstream_tests.cpp b/tests/eosio.system_finalizer_key_upstream_tests.cpp index 1c2a2b56..ac838b5f 100644 --- a/tests/eosio.system_finalizer_key_upstream_tests.cpp +++ b/tests/eosio.system_finalizer_key_upstream_tests.cpp @@ -430,7 +430,7 @@ BOOST_FIXTURE_TEST_CASE(delete_finalizer_key_success_test, finalizer_key_upstrea // Alice registers as a producer BOOST_REQUIRE_EQUAL( success(), regproducer(alice) ); - // Alice registers two keys and the first key is active + // Alice registers two keys and the first key is active BOOST_REQUIRE_EQUAL( success(), register_finalizer_key(alice, finalizer_key_1, pop_1) ); BOOST_REQUIRE_EQUAL( success(), register_finalizer_key(alice, finalizer_key_2, pop_2) ); From acb8e5698466c048b658d0dec5799dd2bad6a41f Mon Sep 17 00:00:00 2001 From: Tobias Date: Thu, 18 Jun 2026 10:40:01 -0400 Subject: [PATCH 18/20] Floor global vote weight once after the delta loop; fix TEDP comment voting.cpp: move the `total_producer_vote_weight >= 0` floor to a single check after the producer_deltas loop instead of clamping per iteration. The map is iterated in name order, so on a set-changing re-vote a negative (old-producer) delta can be applied before its offsetting positive (new-producer) delta; a per-iteration clamp would discard that negative mass on a transient zero-crossing and leave the global biased upward. Flooring once after the loop gives the exact net while still keeping the global from drifting below the recalculate_votes() trigger. Behaviour is unchanged whenever the running total never crosses zero mid-loop (e.g. mainnet, where the aggregate is far from zero). producer_pay.cpp: correct a comment -- the TEDP account constant is exrsrv.tf, not "eosio.tedp". Co-Authored-By: Claude Opus 4.8 --- contracts/eosio.system/src/producer_pay.cpp | 2 +- contracts/eosio.system/src/voting.cpp | 20 +++++++++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/contracts/eosio.system/src/producer_pay.cpp b/contracts/eosio.system/src/producer_pay.cpp index 30a7740d..78a97057 100644 --- a/contracts/eosio.system/src/producer_pay.cpp +++ b/contracts/eosio.system/src/producer_pay.cpp @@ -17,7 +17,7 @@ namespace eosiosystem { // Non-aborting core-token balance lookup. eosio::token::get_balance() calls accounts.get() // which asserts ("no balance with specified symbol") when the owner has no row for the // symbol. claimrewards_snapshot() runs inside onblock, so that assert would HALT block - // production if eosio.tedp's core-symbol balance row were ever absent (e.g. closed). The + // production if the TEDP account's (exrsrv.tf) core-symbol balance row were ever absent. The // eosio.token `accounts` table struct is private in eosio.token.hpp, so mirror its layout // (no [[eosio::table]] attribute -> not added to eosio.system's ABI) and treat a missing // row as a zero balance. Downstream logic already handles amount <= 0 by issuing. diff --git a/contracts/eosio.system/src/voting.cpp b/contracts/eosio.system/src/voting.cpp index 82a3f2a4..43b35f4b 100644 --- a/contracts/eosio.system/src/voting.cpp +++ b/contracts/eosio.system/src/voting.cpp @@ -544,13 +544,10 @@ namespace eosiosystem { p.total_votes = 0; } _gstate.total_producer_vote_weight += pd.second.first; - // Keep the global aggregate consistent with the per-producer clamp above. Without - // this, the negative mass discarded by the per-producer clamp leaks into the - // unclamped global and can drift it below the recalculate_votes() `<= -0.1` - // trigger. The EVM-vote path (eosio.system.cpp) already clamps identically. - if ( _gstate.total_producer_vote_weight < 0 ) { - _gstate.total_producer_vote_weight = 0; - } + // The global aggregate is floored at >= 0 once, after this loop (see below), rather + // than per iteration: clamping here would discard negative mass mid-loop and bias + // the total upward when a set-changing re-vote applies old (negative) and new + // (positive) deltas in name order. //check( p.total_votes >= 0, "something bad happened" ); }); } else { @@ -561,6 +558,15 @@ namespace eosiosystem { } } + // Floor the global aggregate once, after every per-producer delta has been applied. Each + // producer total is clamped to >= 0 individually, which discards negative mass from the + // unclamped global; flooring here keeps the global from drifting below the + // recalculate_votes() `<= -0.1` trigger, while giving the exact net (no ordering-dependent + // upward bias from clamping mid-loop). + if ( _gstate.total_producer_vote_weight < 0 ) { + _gstate.total_producer_vote_weight = 0; + } + _voters.modify( voter, same_payer, [&]( auto& av ) { av.last_vote_weight = new_vote_weight; av.last_stake = int64_t(totalStaked); From 12aaa85f2d1a23df70d56b144663d2e3f0014a6e Mon Sep 17 00:00:00 2001 From: Tobias Date: Tue, 30 Jun 2026 12:16:42 -0400 Subject: [PATCH 19/20] Restore producer kick penalty registration guard --- contracts/eosio.system/src/voting.cpp | 7 +++++++ tests/telos.system_tests.cpp | 27 +++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/contracts/eosio.system/src/voting.cpp b/contracts/eosio.system/src/voting.cpp index 43b35f4b..f39789f9 100644 --- a/contracts/eosio.system/src/voting.cpp +++ b/contracts/eosio.system/src/voting.cpp @@ -45,6 +45,13 @@ namespace eosiosystem { }, producer_authority ); if ( prod != _producers.end() ) { + if ( prod->kick_penalty_hours > 0 ) { + const auto penalty_expiration_time = prod->last_time_kicked.to_time_point() + + microseconds( int64_t(prod->kick_penalty_hours) * useconds_per_hour ); + check( ct > penalty_expiration_time, + "Producer is not allowed to register at this time. Please fix your node and try again later." ); + } + _producers.modify( prod, producer, [&]( producer_info& info ){ info.producer_key = producer_key; info.is_active = true; diff --git a/tests/telos.system_tests.cpp b/tests/telos.system_tests.cpp index 24de3412..d39775a8 100644 --- a/tests/telos.system_tests.cpp +++ b/tests/telos.system_tests.cpp @@ -567,6 +567,33 @@ BOOST_FIXTURE_TEST_CASE(schedule_metrics_survive_location_swap, eosio_system_tes } } FC_LOG_AND_RETHROW() +BOOST_FIXTURE_TEST_CASE(votebpout_penalty_blocks_early_reregistration, eosio_system_tester) try { + const name producer = "penaltyprod"_n; + setup_producer_accounts({producer}); + BOOST_REQUIRE_EQUAL(success(), regproducer(producer)); + + BOOST_REQUIRE_EQUAL(success(), push_action(config::system_account_name, "votebpout"_n, mvo() + ("bp", producer)("penalty_hours", 168))); + + auto kicked_info = get_producer_info(producer); + BOOST_REQUIRE_EQUAL(false, kicked_info["is_active"].as()); + BOOST_REQUIRE_EQUAL(168u, kicked_info["kick_penalty_hours"].as()); + BOOST_REQUIRE_EQUAL(2u, kicked_info["kick_reason_id"].as()); + + BOOST_REQUIRE_EQUAL(wasm_assert_msg("Producer is not allowed to register at this time. Please fix your node and try again later."), + push_action(producer, "regproducer"_n, mvo() + ("producer", producer)("producer_key", get_public_key(producer, "active")) + ("url", "")("location", 0))); + + produce_block(fc::seconds(168 * 3600 + 1)); + BOOST_REQUIRE_EQUAL(success(), push_action(producer, "regproducer"_n, mvo() + ("producer", producer)("producer_key", get_public_key(producer, "active")) + ("url", "")("location", 0))); + + auto restored_info = get_producer_info(producer); + BOOST_REQUIRE_EQUAL(true, restored_info["is_active"].as()); +} FC_LOG_AND_RETHROW() + // End-to-end missed-block autokick: a scheduled producer that stops producing // must accumulate missed blocks across rotation cycles, get kicked once the // threshold is crossed, and have lifetime_missed_blocks counted exactly once From e0aff4ef8d2b117b60e3183e8c0e5954963d0c18 Mon Sep 17 00:00:00 2001 From: Tobias Date: Wed, 1 Jul 2026 14:14:14 -0400 Subject: [PATCH 20/20] Add system contract regression tests --- tests/eosio.system_tester.hpp | 5 ++++ tests/telos.system_tests.cpp | 56 +++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/tests/eosio.system_tester.hpp b/tests/eosio.system_tester.hpp index 9c1e0e71..e732c976 100644 --- a/tests/eosio.system_tester.hpp +++ b/tests/eosio.system_tester.hpp @@ -1085,6 +1085,11 @@ class eosio_system_tester : public TESTER { return data.empty() ? fc::variant() : abi_ser.binary_to_variant( "rotation_state", data, abi_serializer::create_yield_function(abi_serializer_max_time) ); } + fc::variant get_voting_config() { + vector data = get_row_by_account( config::system_account_name, config::system_account_name, "votingconfig"_n, "votingconfig"_n ); + return data.empty() ? fc::variant() : abi_ser.binary_to_variant( "votingconfig", data, abi_serializer::create_yield_function(abi_serializer_max_time) ); + } + fc::variant get_payment_info( name account ) { vector data = get_row_by_account( config::system_account_name, config::system_account_name, "payments"_n, account ); return data.empty() ? fc::variant() : abi_ser.binary_to_variant( "payment_info", data, abi_serializer::create_yield_function(abi_serializer_max_time) ); diff --git a/tests/telos.system_tests.cpp b/tests/telos.system_tests.cpp index d39775a8..92662449 100644 --- a/tests/telos.system_tests.cpp +++ b/tests/telos.system_tests.cpp @@ -700,4 +700,60 @@ BOOST_FIXTURE_TEST_CASE( evm_vote_sync_actions_require_system_auth, eosio_system push_action( config::system_account_name, "setbpevmstat"_n, mvo()("bp", "alice1111111"_n) ) ); } FC_LOG_AND_RETHROW() +BOOST_FIXTURE_TEST_CASE( voting_config_actions_require_system_auth, eosio_system_tester ) try { + const std::string evm_contract = "1111111111111111111111111111111111111111"; + + BOOST_REQUIRE_EQUAL( error( "missing authority of eosio" ), + push_action( "alice1111111"_n, "setvotecontr"_n, mvo()("contract", evm_contract) ) ); + BOOST_REQUIRE_EQUAL( error( "missing authority of eosio" ), + push_action( "bob111111111"_n, "setselfstake"_n, mvo()("self_stake_boost_multiplier", uint64_t(10)) ) ); + BOOST_REQUIRE_EQUAL( error( "missing authority of eosio" ), + push_action( "carol1111111"_n, "setvotedecay"_n, mvo() + ("decay_start_epoch", uint64_t(1000)) + ("decay_increase_yearly", uint64_t(10)) ) ); +} FC_LOG_AND_RETHROW() + +BOOST_FIXTURE_TEST_CASE( voting_config_actions_update_and_validate, eosio_system_tester ) try { + auto cfg = get_voting_config(); + BOOST_REQUIRE( !cfg.is_null() ); + BOOST_REQUIRE_EQUAL( 0u, cfg["decay_start_epoch"].as() ); + BOOST_REQUIRE_EQUAL( 0u, cfg["decay_increase_yearly"].as() ); + BOOST_REQUIRE_EQUAL( 0u, cfg["self_stake_boost_multiplier"].as() ); + + BOOST_REQUIRE_EQUAL( wasm_assert_msg( "EVM voting contract not set" ), + push_action( config::system_account_name, "setvotedecay"_n, mvo() + ("decay_start_epoch", uint64_t(1000)) + ("decay_increase_yearly", uint64_t(10)) ) ); + + BOOST_REQUIRE_EQUAL( wasm_assert_msg( "self_stake_boost_multiplier must be <= 1000" ), + push_action( config::system_account_name, "setselfstake"_n, + mvo()("self_stake_boost_multiplier", uint64_t(1001)) ) ); + + BOOST_REQUIRE_EQUAL( success(), + push_action( config::system_account_name, "setselfstake"_n, + mvo()("self_stake_boost_multiplier", uint64_t(250)) ) ); + cfg = get_voting_config(); + BOOST_REQUIRE_EQUAL( 250u, cfg["self_stake_boost_multiplier"].as() ); + + const std::string evm_contract = "1111111111111111111111111111111111111111"; + BOOST_REQUIRE_EQUAL( success(), + push_action( config::system_account_name, "setvotecontr"_n, mvo()("contract", evm_contract) ) ); + cfg = get_voting_config(); + BOOST_REQUIRE_EQUAL( evm_contract, cfg["evm_voting_contract"].as_string() ); + + BOOST_REQUIRE_EQUAL( wasm_assert_msg( "decay_increase_yearly must be <= 100 (100%)" ), + push_action( config::system_account_name, "setvotedecay"_n, mvo() + ("decay_start_epoch", uint64_t(1000)) + ("decay_increase_yearly", uint64_t(101)) ) ); + + BOOST_REQUIRE_EQUAL( wasm_assert_msg( "eosio EVM address not found" ), + push_action( config::system_account_name, "setvotedecay"_n, mvo() + ("decay_start_epoch", uint64_t(1000)) + ("decay_increase_yearly", uint64_t(10)) ) ); + + cfg = get_voting_config(); + BOOST_REQUIRE_EQUAL( 0u, cfg["decay_start_epoch"].as() ); + BOOST_REQUIRE_EQUAL( 0u, cfg["decay_increase_yearly"].as() ); +} FC_LOG_AND_RETHROW() + BOOST_AUTO_TEST_SUITE_END()