diff --git a/.github/workflows/ubuntu-2204.yml b/.github/workflows/ubuntu-2204.yml index 668e0d34..7a69dc89 100644 --- a/.github/workflows/ubuntu-2204.yml +++ b/.github/workflows/ubuntu-2204.yml @@ -6,25 +6,78 @@ 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" + # 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: "c4dc8dad19768a7ac3bc3519111c6ea632faf9a829eae9366fcb51bba94969f4" 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 + defaults: + run: + shell: bash 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/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 + 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: 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 -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}')" + 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/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..56f10e47 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. @@ -1786,14 +1898,25 @@ 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, - 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; + bool active_schedule_matches_last_scheduled_producers( const std::vector& active_schedule ) const; + std::vector get_last_scheduled_producers() const; + template class registration { public: 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/eosio.system.cpp b/contracts/eosio.system/src/eosio.system.cpp index 2e1f79a1..5e559401 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), @@ -622,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( @@ -652,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"); @@ -738,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; @@ -760,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"); @@ -816,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 new file mode 100644 index 00000000..dda12492 --- /dev/null +++ b/contracts/eosio.system/src/finalizer_key.cpp @@ -0,0 +1,330 @@ +#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; + } + + 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()); + + 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" ); + + 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()) + + ", 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"); + + // 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" ); + } + } + + 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/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; diff --git a/contracts/eosio.system/src/producer_pay.cpp b/contracts/eosio.system/src/producer_pay.cpp index 7a6254d6..78a97057 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 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. + 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/system_rotation.cpp b/contracts/eosio.system/src/system_rotation.cpp index 93b3f84b..02f159e5 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--; @@ -100,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) { @@ -113,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; @@ -133,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/contracts/eosio.system/src/voting.cpp b/contracts/eosio.system/src/voting.cpp index 6cfca82f..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; @@ -127,6 +134,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 +144,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, @@ -164,24 +176,69 @@ 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; + } + + 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); + } + + 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); 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()); } + + // 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) ); + } // TELOS END } @@ -366,7 +423,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" ); @@ -447,16 +504,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 ) { @@ -476,6 +539,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 ) { @@ -484,15 +551,29 @@ namespace eosiosystem { p.total_votes = 0; } _gstate.total_producer_vote_weight += pd.second.first; + // 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 { if( pd.second.second ) { + if( recalculating ) continue; check( false, ( "producer " + pd.first.to_string() + " is not registered" ).data() ); } } } + // 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); @@ -609,7 +690,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; + } }); } } @@ -650,7 +739,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/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_finalizer_key_tests.cpp b/tests/eosio.system_finalizer_key_tests.cpp new file mode 100644 index 00000000..d35eee10 --- /dev/null +++ b/tests/eosio.system_finalizer_key_tests.cpp @@ -0,0 +1,253 @@ +#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() + +// 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 ) ); + + // 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 + 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/eosio.system_finalizer_key_upstream_tests.cpp b/tests/eosio.system_finalizer_key_upstream_tests.cpp new file mode 100644 index 00000000..ac838b5f --- /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() diff --git a/tests/eosio.system_tester.hpp b/tests/eosio.system_tester.hpp index 91c374e8..e732c976 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 @@ -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) ); @@ -1096,6 +1101,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 +1209,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 +1265,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; diff --git a/tests/telos.system_tests.cpp b/tests/telos.system_tests.cpp index 214bc9af..92662449 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_legacy()->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; @@ -435,4 +491,269 @@ 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() + +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 +// (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() + +// 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_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()