diff --git a/README.md b/README.md index e9490780f2..3ebe2c7268 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ nodeos --full-version ``` You should see a [semantic version](https://semver.org) string followed by a `git` commit hash with no errors. For example: ``` -v3.1.2-0b64f879e3ebe2e4df09d2e62f1fc164cc1125d1 +v1.0.1-9026a03c09c9b4f93edca696b5eef259f0ab96b3 ``` ## Build and Install from Source @@ -84,7 +84,7 @@ cd spring ``` ### Step 2 - Checkout Release Tag or Branch -Choose which [release](https://github.com/AntelopeIO/spring/releases) or [branch](#branches) you would like to build, then check it out. If you are not sure, use the [latest release](https://github.com/AntelopeIO/spring/releases/latest). For example, if you want to build release 3.1.2 then you would check it out using its tag, `v3.1.2`. In the example below, replace `v0.0.0` with your selected release tag accordingly: +Choose which [release](https://github.com/AntelopeIO/spring/releases) or [branch](#branches) you would like to build, then check it out. If you are not sure, use the [latest release](https://github.com/AntelopeIO/spring/releases/latest). For example, if you want to build release 1.0.1 then you would check it out using its tag, `v1.0.1`. In the example below, replace `v0.0.0` with your selected release tag accordingly: ```bash git fetch --all --tags git checkout v0.0.0 diff --git a/libraries/chain/controller.cpp b/libraries/chain/controller.cpp index a0e3724d50..3b7a4f6747 100644 --- a/libraries/chain/controller.cpp +++ b/libraries/chain/controller.cpp @@ -3882,62 +3882,44 @@ struct controller_impl { // and quorum_certificate_extension in block extension are valid. // Called from net-threads. It is thread safe as signed_block is never modified after creation. // ----------------------------------------------------------------------------- - void verify_qc_claim( const block_id_type& id, const signed_block_ptr& b, const block_header_state& prev ) { + void verify_proper_block_exts( const block_id_type& id, const signed_block_ptr& b, const block_header_state& prev ) { + assert(b->is_proper_svnn_block()); + auto qc_ext_id = quorum_certificate_extension::extension_id(); auto f_ext_id = finality_extension::extension_id(); // extract current block extension and previous header extension auto block_exts = b->validate_and_extract_extensions(); const finality_extension* prev_finality_ext = prev.header_extension(); - std::optional header_ext = b->extract_header_extension(f_ext_id); + std::optional finality_ext = b->extract_header_extension(f_ext_id); bool qc_extension_present = block_exts.count(qc_ext_id) != 0; uint32_t block_num = b->block_num(); - if( !header_ext ) { - // If there is no header extension, ensure the block does not have a QC and also the previous - // block doesn't have a header extension either. Then return early. - // ------------------------------------------------------------------------------------------ - EOS_ASSERT( !qc_extension_present, - invalid_qc_claim, - "Block #${b} includes a QC block extension, but doesn't have a finality header extension", - ("b", block_num) ); - - EOS_ASSERT( !prev_finality_ext, - invalid_qc_claim, - "Block #${b} doesn't have a finality header extension even though its predecessor does.", - ("b", block_num) ); + // This function is called only in Savanna. Finality block header + // extension must exist + EOS_ASSERT( finality_ext, block_validate_exception, + "Proper Savanna block #${b} does not have a finality header extension", + ("b", block_num) ); - dlog("received block: #${bn} ${t} ${prod} ${id}, no qc claim, previous: ${p}", - ("bn", block_num)("t", b->timestamp)("prod", b->producer)("id", id)("p", b->previous)); - return; - } - - assert(header_ext); - const auto& f_ext = std::get(*header_ext); + assert(finality_ext); + const auto& f_ext = std::get(*finality_ext); const auto new_qc_claim = f_ext.qc_claim; dlog("received block: #${bn} ${t} ${prod} ${id}, qc claim: ${qc}, previous: ${p}", ("bn", block_num)("t", b->timestamp)("prod", b->producer)("id", id) ("qc", new_qc_claim)("p", b->previous)); - // If there is a header extension, but the previous block does not have a header extension, - // ensure the block does not have a QC and the QC claim of the current block has a block_num - // of the current block’s number and that it is a claim of a weak QC. Then return early. + // The only time a block should have a finality block header extension but + // its parent block does not, is if it is a Savanna Genesis block (which is + // necessarily a Transition block). Since verify_proper_block_exts will not be called + // on Transition blocks, previous block may not be a Legacy block // ------------------------------------------------------------------------------------------------- - if (!prev_finality_ext) { - EOS_ASSERT( !qc_extension_present && new_qc_claim.block_num == block_num && new_qc_claim.is_strong_qc == false, - invalid_qc_claim, - "Block #${b}, which is the finality transition block, doesn't have the expected extensions", - ("b", block_num) ); - return; - } - - // at this point both current block and its parent have IF extensions, and we are past the - // IF transition block - // ---------------------------------------------------------------------------------------- - assert(header_ext && prev_finality_ext); + EOS_ASSERT( !prev.header.is_legacy_block(), block_validate_exception, + "Proper Savanna block #${b} may not have previous block that is a Legacy block", + ("b", block_num) ); + assert(prev_finality_ext); const auto& prev_qc_claim = prev_finality_ext->qc_claim; // validate QC claim against previous block QC info @@ -3999,18 +3981,107 @@ struct controller_impl { bsp->verify_qc(qc_proof); } + void verify_legacy_block_exts( const signed_block_ptr& b, const block_header_state_legacy& prev ) { + assert(b->is_legacy_block()); + + uint32_t block_num = b->block_num(); + auto block_exts = b->validate_and_extract_extensions(); + auto qc_ext_id = quorum_certificate_extension::extension_id(); + bool qc_extension_present = block_exts.count(qc_ext_id) != 0; + + EOS_ASSERT( !qc_extension_present, invalid_qc_claim, + "Legacy block #${b} includes a QC block extension", + ("b", block_num) ); + + EOS_ASSERT( !b->is_proper_svnn_block(), block_validate_exception, + "Legacy block #${b} has invalid schedule_version", + ("b", block_num) ); + + // Verify we don't go back from Savanna (Transition or Proper) block to Legacy block + EOS_ASSERT( prev.header.is_legacy_block(), block_validate_exception, + "Legacy block #${b} must have previous block that is also a Legacy block", + ("b", block_num) ); + } + + void verify_transition_block_exts( const signed_block_ptr& b, const block_header_state_legacy& prev ) { + assert(!b->is_legacy_block() && !b->is_proper_svnn_block()); + + uint32_t block_num = b->block_num(); + auto block_exts = b->validate_and_extract_extensions(); + auto qc_ext_id = quorum_certificate_extension::extension_id(); + bool qc_extension_present = block_exts.count(qc_ext_id) != 0; + + EOS_ASSERT( !qc_extension_present, invalid_qc_claim, + "Transition block #${b} includes a QC block extension", + ("b", block_num) ); + + EOS_ASSERT( !prev.header.is_proper_svnn_block(), block_validate_exception, + "Transition block #${b} may not have previous block that is a Proper Savanna block", + ("b", block_num) ); + + auto f_ext_id = finality_extension::extension_id(); + std::optional finality_ext = b->extract_header_extension(f_ext_id); + assert(finality_ext); + const auto& f_ext = std::get(*finality_ext); + + EOS_ASSERT( !f_ext.new_proposer_policy_diff, block_validate_exception, + "Transition block #${b} has new_proposer_policy_diff", + ("b", block_num) ); + + if (auto it = prev.header_exts.find(finality_extension::extension_id()); it != prev.header_exts.end()) { + // Transition block other than Genesis Block + const auto& prev_finality_ext = std::get(it->second); + EOS_ASSERT( f_ext.qc_claim == prev_finality_ext.qc_claim, invalid_qc_claim, + "Non Genesis Transition block #${b} QC claim ${this_qc_claim} not equal to previous QC claim ${prev_qc_claim}", + ("b", block_num)("this_qc_claim", f_ext.qc_claim)("prev_qc_claim", prev_finality_ext.qc_claim) ); + EOS_ASSERT( !f_ext.new_finalizer_policy_diff, block_validate_exception, + "Non Genesis Transition block #${b} finality block header extension may not have new_finalizer_policy_diff", + ("b", block_num) ); + } else { + // Savanna Genesis Block + qc_claim_t genesis_qc_claim {.block_num = block_num, .is_strong_qc = false}; + EOS_ASSERT( f_ext.qc_claim == genesis_qc_claim, invalid_qc_claim, + "Savanna Genesis block #${b} has invalid QC claim ${qc_claim}", + ("b", block_num)("qc_claim", f_ext.qc_claim) ); + EOS_ASSERT( f_ext.new_finalizer_policy_diff, block_validate_exception, + "Savanna Genesis block #${b} finality block header extension misses new_finalizer_policy_diff", + ("b", block_num) ); + + // apply_diff will FC_ASSERT if new_finalizer_policy_diff is malformed + try { + finalizer_policy no_policy; + finalizer_policy genesis_policy = no_policy.apply_diff(*f_ext.new_finalizer_policy_diff); + EOS_ASSERT( genesis_policy.generation == 1, block_validate_exception, + "Savanna Genesis block #${b} finalizer policy generation (${g}) not 1", + ("b", block_num)("g", genesis_policy.generation) ); + } EOS_RETHROW_EXCEPTIONS(block_validate_exception, "applying diff of Savanna Genesis Block") + } + } + // thread safe, expected to be called from thread other than the main thread template block_handle create_block_state_i( ForkDB& forkdb, const block_id_type& id, const signed_block_ptr& b, const BS& prev ) { - constexpr bool savanna_mode = std::is_same_v, block_state>; - if constexpr (savanna_mode) { - // Verify claim made by finality_extension in block header extension and - // quorum_certificate_extension in block extension are valid. - // This is the only place the evaluation is done. - verify_qc_claim(id, b, prev); + constexpr bool is_proper_savanna_block = std::is_same_v, block_state>; + assert(is_proper_savanna_block == b->is_proper_svnn_block()); + + if constexpr (is_proper_savanna_block) { + EOS_ASSERT( b->is_proper_svnn_block(), block_validate_exception, + "create_block_state_i cannot be called on block #${b} which is not a Proper Savanna block unless the prev block state provided is of type block_state", + ("b", b->block_num()) ); + verify_proper_block_exts(id, b, prev); + } else { + EOS_ASSERT( !b->is_proper_svnn_block(), block_validate_exception, + "create_block_state_i cannot be called on block #${b} which is a Proper Savanna block unless the prev block state provided is of type block_state_legacy", + ("b", b->block_num()) ); + + if (b->is_legacy_block()) { + verify_legacy_block_exts(b, prev); + } else { + verify_transition_block_exts(b, prev); + } } - auto trx_mroot = calculate_trx_merkle( b->transactions, savanna_mode ); + auto trx_mroot = calculate_trx_merkle( b->transactions, is_proper_savanna_block ); EOS_ASSERT( b->transaction_mroot == trx_mroot, block_validate_exception, "invalid block transaction merkle root ${b} != ${c}", ("b", b->transaction_mroot)("c", trx_mroot) ); @@ -4030,14 +4101,14 @@ struct controller_impl { EOS_ASSERT( id == bsp->id(), block_validate_exception, "provided id ${id} does not match block id ${bid}", ("id", id)("bid", bsp->id()) ); - if constexpr (savanna_mode) { + if constexpr (is_proper_savanna_block) { integrate_received_qc_to_block(bsp); // Save the received QC as soon as possible, no matter whether the block itself is valid or not consider_voting(bsp, use_thread_pool_t::no); } if (!should_terminate(bsp->block_num())) { forkdb.add(bsp, ignore_duplicate_t::yes); - if constexpr (savanna_mode) + if constexpr (is_proper_savanna_block) vote_processor.notify_new_block(async_aggregation); } @@ -4095,7 +4166,7 @@ struct controller_impl { return fork_db.apply>(unlinkable, f); } - // thread safe, QC already verified by verify_qc_claim + // thread safe, QC already verified by verify_proper_block_exts void integrate_received_qc_to_block(const block_state_ptr& bsp_in) { // extract QC from block extension assert(bsp_in->block); diff --git a/libraries/chain/include/eosio/chain/block_header.hpp b/libraries/chain/include/eosio/chain/block_header.hpp index f051ec5107..899b9b306a 100644 --- a/libraries/chain/include/eosio/chain/block_header.hpp +++ b/libraries/chain/include/eosio/chain/block_header.hpp @@ -92,6 +92,9 @@ namespace eosio::chain { // finality extension must exist. bool is_proper_svnn_block() const { return ( schedule_version == proper_svnn_schedule_version ); } + // Returns true if the block is a pure Legacy block + bool is_legacy_block() const { return !contains_header_extension(finality_extension::extension_id()); } + header_extension_multimap validate_and_extract_header_extensions()const; std::optional extract_header_extension(uint16_t extension_id)const; template Ext extract_header_extension()const { diff --git a/libraries/chain/include/eosio/chain/finality/finalizer_policy.hpp b/libraries/chain/include/eosio/chain/finality/finalizer_policy.hpp index 66abe98841..1fa8dbfe3f 100644 --- a/libraries/chain/include/eosio/chain/finality/finalizer_policy.hpp +++ b/libraries/chain/include/eosio/chain/finality/finalizer_policy.hpp @@ -6,8 +6,10 @@ namespace eosio::chain { - static_assert(std::numeric_limits::max() >= config::max_finalizers - 1); using finalizers_differ = fc::ordered_diff; + // Verify finalizers_differ::size_type can represent all index values in the + // diff between two policies that could each hold up to max_finalizers entries. + static_assert(std::numeric_limits::max() >= config::max_finalizers - 1); using finalizers_diff_t = finalizers_differ::diff_result; struct finalizer_policy_diff { diff --git a/libraries/chain/include/eosio/chain/finality/proposer_policy.hpp b/libraries/chain/include/eosio/chain/finality/proposer_policy.hpp index 3b5ebc7b29..207dec2973 100644 --- a/libraries/chain/include/eosio/chain/finality/proposer_policy.hpp +++ b/libraries/chain/include/eosio/chain/finality/proposer_policy.hpp @@ -5,8 +5,10 @@ namespace eosio::chain { -static_assert(std::numeric_limits::max() >= config::max_proposers - 1); using producer_auth_differ = fc::ordered_diff; +// Verify producer_auth_differ::size_type can represent all index values in the +// diff between two policies that could each hold up to max_proposers entries. +static_assert(std::numeric_limits::max() >= config::max_proposers - 1); using producer_auth_diff_t = producer_auth_differ::diff_result; struct proposer_policy_diff { diff --git a/libraries/libfc/include/fc/container/ordered_diff.hpp b/libraries/libfc/include/fc/container/ordered_diff.hpp index e2440d6ab9..d028ad7a97 100644 --- a/libraries/libfc/include/fc/container/ordered_diff.hpp +++ b/libraries/libfc/include/fc/container/ordered_diff.hpp @@ -1,5 +1,8 @@ #pragma once +#include +#include + #include #include @@ -9,6 +12,8 @@ namespace fc { * @class ordered_diff * @brief Provides ability to generate and apply diff of containers of type T * + * NOTE: Part of Spring Consensus. Used for finalizer and proposer policies. + * * Example use: * std::vector source = { 'a', 'b', 'f', 'c', 'd' }; * std::vector target = { 'b', 'f', 'c', 'd', 'e', 'h' }; @@ -24,6 +29,8 @@ template && std::random_access_iterator::iterator> class ordered_diff { public: + using size_type = SizeType; + struct diff_result { Container remove_indexes; Container> insert_indexes; @@ -34,13 +41,16 @@ class ordered_diff { size_t s = 0; size_t t = 0; + FC_ASSERT(source.empty() || (source.size() - 1) <= std::numeric_limits::max()); + FC_ASSERT(target.empty() || (target.size() - 1) <= std::numeric_limits::max()); + diff_result result; while (s < source.size() || t < target.size()) { + assert(s <= source.size()); + assert(t <= target.size()); if (s < source.size() && t < target.size()) { if (source[s] == target[t]) { // nothing to do, skip over - assert(s <= std::numeric_limits::max()); - assert(t <= std::numeric_limits::max()); ++s; ++t; } else { // not equal @@ -65,8 +75,9 @@ class ordered_diff { assert(t <= std::numeric_limits::max()); result.insert_indexes.emplace_back(t, target[t]); ++t; - } else { // source[s + 1] == target[t] - // target matches next source, remove current source + } else { + // not misalignment by one and source not equal to next target, so remove from source + // may be inserted later by other conditions if needed assert(s <= std::numeric_limits::max()); result.remove_indexes.push_back(s); ++s; @@ -97,12 +108,16 @@ class ordered_diff { // Remove from the source based on diff.remove_indexes std::ptrdiff_t offset = 0; for (SizeType index : diff.remove_indexes) { + FC_ASSERT(index + offset < container.size(), "diff.remove_indexes index ${idx} + offset ${o} not in range ${s}", + ("idx", index)("o", offset)("s", container.size())); container.erase(container.begin() + index + offset); --offset; } // Insert into the source based on diff.insert_indexes for (auto& [index, value] : diff.insert_indexes) { + FC_ASSERT(index <= container.size(), "diff.insert_indexes index ${idx} not in range ${s}", + ("idx", index)("s", container.size())); container.insert(container.begin() + index, std::move(value)); } return container; diff --git a/libraries/libfc/test/test_ordered_diff.cpp b/libraries/libfc/test/test_ordered_diff.cpp index 6477c3f8a3..eeb9687dd3 100644 --- a/libraries/libfc/test/test_ordered_diff.cpp +++ b/libraries/libfc/test/test_ordered_diff.cpp @@ -3,6 +3,9 @@ #include #include +#include +#include + using namespace fc; BOOST_AUTO_TEST_SUITE(ordered_diff_tests) @@ -39,6 +42,13 @@ BOOST_AUTO_TEST_CASE(ordered_diff_test) try { source = ordered_diff::apply_diff(std::move(source), result); BOOST_TEST(source == target); } + { // All elements removed, size 1 + vector source = {'a'}; + vector target; + auto result = ordered_diff::diff(source, target); + source = ordered_diff::apply_diff(std::move(source), result); + BOOST_TEST(source == target); + } { // All elements inserted vector source; vector target = {'a', 'b', 'c', 'd', 'e'}; @@ -46,6 +56,13 @@ BOOST_AUTO_TEST_CASE(ordered_diff_test) try { source = ordered_diff::apply_diff(std::move(source), result); BOOST_TEST(source == target); } + { // All elements inserted, size 1 + vector source; + vector target = {'a'}; + auto result = ordered_diff::diff(source, target); + source = ordered_diff::apply_diff(std::move(source), result); + BOOST_TEST(source == target); + } { // No change vector source = {'a', 'b', 'c', 'd', 'e'}; vector target = source; @@ -53,6 +70,13 @@ BOOST_AUTO_TEST_CASE(ordered_diff_test) try { source = ordered_diff::apply_diff(std::move(source), result); BOOST_TEST(source == target); } + { // No change, size 1 + vector source = {'a'}; + vector target = source; + auto result = ordered_diff::diff(source, target); + source = ordered_diff::apply_diff(std::move(source), result); + BOOST_TEST(source == target); + } { // Mix of removals and inserts vector source = {'a', 'b', 'c', 'd', 'e'}; vector target = {'a', 'c', 'e', 'f', 'g', 'h'}; @@ -74,6 +98,20 @@ BOOST_AUTO_TEST_CASE(ordered_diff_test) try { source = ordered_diff::apply_diff(std::move(source), result); BOOST_TEST(source == target); } + { // Complete change, size 1 + vector source = {'a'}; + vector target = {'f'}; + auto result = ordered_diff::diff(source, target); + source = ordered_diff::apply_diff(std::move(source), result); + BOOST_TEST(source == target); + } + { // Complete change equal sizes + vector source = {'a', 'b', 'c', 'd'}; + vector target = {'f', 'g', 'h', 'i'}; + auto result = ordered_diff::diff(source, target); + source = ordered_diff::apply_diff(std::move(source), result); + BOOST_TEST(source == target); + } { // Diff order vector source = {'a', 'b', 'c', 'd', 'e'}; vector target = {'e', 'd', 'c', 'b', 'a'}; @@ -81,6 +119,20 @@ BOOST_AUTO_TEST_CASE(ordered_diff_test) try { source = ordered_diff::apply_diff(std::move(source), result); BOOST_TEST(source == target); } + { // Diff order, size 2 + vector source = {'a', 'b'}; + vector target = {'b', 'a'}; + auto result = ordered_diff::diff(source, target); + source = ordered_diff::apply_diff(std::move(source), result); + BOOST_TEST(source == target); + } + { // Diff order, size 2 + vector source = {'b', 'a'}; + vector target = {'a', 'b'}; + auto result = ordered_diff::diff(source, target); + source = ordered_diff::apply_diff(std::move(source), result); + BOOST_TEST(source == target); + } { // shift left vector source = {'a', 'b', 'c', 'd', 'e'}; vector target = {'b', 'c', 'd', 'e', 'f'}; @@ -119,6 +171,27 @@ BOOST_AUTO_TEST_CASE(ordered_diff_test) try { source = ordered_diff::apply_diff(std::move(source), result); BOOST_TEST(source == target); } + { // full, random + std::random_device rnd_device; + std::mt19937 mersenne_engine {rnd_device()}; + std::uniform_int_distribution dist {0, std::numeric_limits::max()}; + auto gen = [&](){ return dist(mersenne_engine); }; + vector source(std::numeric_limits::max()+1); + std::generate(source.begin(), source.end(), gen); + vector target(source.size()); + std::reverse_copy(source.begin(), source.end(), target.begin()); + auto result = ordered_diff::diff(source, target); + source = ordered_diff::apply_diff(std::move(source), result); + BOOST_TEST(source == target); + target.clear(); + result = ordered_diff::diff(source, target); + source = ordered_diff::apply_diff(std::move(source), result); + BOOST_TEST(source == target); + source.clear(); + result = ordered_diff::diff(source, target); + source = ordered_diff::apply_diff(std::move(source), result); + BOOST_TEST(source == target); + } { // non-unique full vector source(std::numeric_limits::max()*2); std::iota(source.begin(), source.begin()+std::numeric_limits::max(), 0);