diff --git a/libraries/chain/controller.cpp b/libraries/chain/controller.cpp index ebb16ca3f7..cfb39f4d9e 100644 --- a/libraries/chain/controller.cpp +++ b/libraries/chain/controller.cpp @@ -1425,9 +1425,9 @@ struct controller_impl { [&](const block_state_ptr& head) { return head->make_block_ref(); }}); // doesn't matter chain_head is not updated for IRREVERSIBLE, cannot be in irreversible mode and be a finalizer my_finalizers.set_default_safety_information( - finalizer_safety_information{ .last_vote = ref, - .lock = ref, - .votes_forked_since_latest_strong_vote = false}); + finalizer_safety_information{ .last_vote = ref, + .lock = ref, + .other_branch_latest_time = {} }); } } @@ -1636,9 +1636,9 @@ struct controller_impl { // we create the non-legacy fork_db, as from this point we may need to cast votes to participate // to the IF consensus. See https://github.com/AntelopeIO/leap/issues/2070#issuecomment-1941901836 my_finalizers.set_default_safety_information( - finalizer_safety_information{.last_vote = prev->make_block_ref(), - .lock = prev->make_block_ref(), - .votes_forked_since_latest_strong_vote = false}); + finalizer_safety_information{.last_vote = prev->make_block_ref(), + .lock = prev->make_block_ref(), + .other_branch_latest_time = {} }); } } }); @@ -2040,9 +2040,9 @@ struct controller_impl { auto set_finalizer_defaults = [&](auto& forkdb) -> void { auto lib = forkdb.root(); my_finalizers.set_default_safety_information( - finalizer_safety_information{ .last_vote = {}, - .lock = lib->make_block_ref(), - .votes_forked_since_latest_strong_vote = false }); + finalizer_safety_information{ .last_vote = {}, + .lock = lib->make_block_ref(), + .other_branch_latest_time = {} }); }; fork_db.apply_s(set_finalizer_defaults); } else { @@ -2050,9 +2050,9 @@ struct controller_impl { auto set_finalizer_defaults = [&](auto& forkdb) -> void { auto lib = forkdb.root(); my_finalizers.set_default_safety_information( - finalizer_safety_information{ .last_vote = {}, - .lock = lib->make_block_ref(), - .votes_forked_since_latest_strong_vote = false }); + finalizer_safety_information{.last_vote = {}, + .lock = lib->make_block_ref(), + .other_branch_latest_time = {} }); }; fork_db.apply_s(set_finalizer_defaults); } diff --git a/libraries/chain/finality/finalizer.cpp b/libraries/chain/finality/finalizer.cpp index c88153accf..c0ccb8ac76 100644 --- a/libraries/chain/finality/finalizer.cpp +++ b/libraries/chain/finality/finalizer.cpp @@ -61,35 +61,37 @@ finalizer::vote_result finalizer::decide_vote(const block_state_ptr& bsp) { // If we vote, update `fsi.last_vote` and also `fsi.lock` if we have a newer commit qc // ----------------------------------------------------------------------------------- if (can_vote) { - // if `fsi.last_vote` is not set, it will be initialized with a timestamp slot of 0, so we - // don't need to check fsi.last_vote.empty() - // --------------------------------------------------------------------------------------- - bool voting_strong = fsi.last_vote.timestamp <= bsp->core.latest_qc_block_timestamp(); - - if (!voting_strong) { - // we can vote strong if the proposal is a descendant of (i.e. extends) our last vote id AND - // the latest (weak) vote did not mask a prior (weak) vote for a block not on the same branch. - // ------------------------------------------------------------------------------------------- - voting_strong = !fsi.votes_forked_since_latest_strong_vote && bsp->core.extends(fsi.last_vote.block_id); + const auto latest_qc_block_timestamp = bsp->core.latest_qc_block_timestamp(); + + // If `fsi.last_vote` is not set, it will be initialized with a timestamp slot of 0, + // which means `fsi.last_vote.timestamp` would always be less than or equal + // to `latest_qc_block_timestamp`. + // So, we don't need to separately check for the case where `fsi.last_vote.empty()` is true. + if (fsi.last_vote.timestamp <= latest_qc_block_timestamp) { + res.decision = vote_decision::strong_vote; + } else if (bsp->core.extends(fsi.last_vote.block_id)) { + // If `fsi.other_branch_latest_time` is not present, it will have a timestamp slot of + // 0, which means it will always be less than or equal to `latest_qc_block_timestamp`. + // So, we don't need to separately check for the case where + // `fsi.other_branch_latest_time` is not present. + if (fsi.other_branch_latest_time <= latest_qc_block_timestamp) { + res.decision = vote_decision::strong_vote; + } else { + res.decision = vote_decision::weak_vote; + } + } else { + res.decision = vote_decision::weak_vote; + fsi.other_branch_latest_time = fsi.last_vote.timestamp; } - auto& latest_qc_claim__block_ref = bsp->core.get_block_reference(bsp->core.latest_qc_claim().block_num); - if (voting_strong) { - fsi.votes_forked_since_latest_strong_vote = false; // always reset to false on strong vote - if (latest_qc_claim__block_ref.timestamp > fsi.lock.timestamp) - fsi.lock = latest_qc_claim__block_ref; - } else { - // On a weak vote, if `votes_forked_since_latest_strong_vote` was already true, then it should remain true. - // The only way `votes_forked_since_latest_strong_vote` can change from false to true is on a weak vote - // for a block b where the last_vote references a block that is not an ancestor of b - // -------------------------------------------------------------------------------------------------------- - fsi.votes_forked_since_latest_strong_vote = - fsi.votes_forked_since_latest_strong_vote || !bsp->core.extends(fsi.last_vote.block_id); + if (res.decision == vote_decision::strong_vote) { + fsi.other_branch_latest_time = {}; + if (latest_qc_block_timestamp > fsi.lock.timestamp) { + fsi.lock = bsp->core.get_block_reference(bsp->core.latest_qc_claim().block_num); + } } fsi.last_vote = bsp->make_block_ref(); - - res.decision = voting_strong ? vote_decision::strong_vote : vote_decision::weak_vote; } fc_dlog(vote_logger, "block=${bn} ${id}, liveness_check=${l}, safety_check=${s}, monotony_check=${m}, can vote=${can_vote}, voting=${v}, locked=${lbn} ${lid}", @@ -103,9 +105,9 @@ finalizer::vote_result finalizer::decide_vote(const block_state_ptr& bsp) { bool finalizer::maybe_update_fsi(const block_state_ptr& bsp) { auto& latest_qc_claim__block_ref = bsp->core.get_block_reference(bsp->core.latest_qc_claim().block_num); if (latest_qc_claim__block_ref.timestamp > fsi.lock.timestamp && bsp->timestamp() > fsi.last_vote.timestamp) { - fsi.lock = latest_qc_claim__block_ref; - fsi.last_vote = bsp->make_block_ref(); - fsi.votes_forked_since_latest_strong_vote = false; // always reset to false on strong vote + fsi.lock = latest_qc_claim__block_ref; + fsi.last_vote = bsp->make_block_ref(); + fsi.other_branch_latest_time = {}; // always reset on strong vote return true; } return false; @@ -175,47 +177,93 @@ void my_finalizers_t::maybe_update_fsi(const block_state_ptr& bsp, const qc_t& r void my_finalizers_t::save_finalizer_safety_info() const { - if (!persist_file.is_open()) { + if (!cfile_ds.is_open()) { EOS_ASSERT(!persist_file_path.empty(), finalizer_safety_exception, "path for storing finalizer safety information file not specified"); if (!std::filesystem::exists(persist_file_path.parent_path())) std::filesystem::create_directories(persist_file_path.parent_path()); - persist_file.set_file_path(persist_file_path); - persist_file.open(fc::cfile::truncate_rw_mode); + cfile_ds.set_file_path(persist_file_path); + cfile_ds.open(fc::cfile::truncate_rw_mode); } try { - static_assert(sizeof(finalizer_safety_information) == 152, "If size changes then need to handle loading old files"); - static_assert(sizeof(decltype(bls_public_key{}.affine_non_montgomery_le())) == 96, - "If size changes then need to handle loading old files, fc::pack uses affine_non_montgomery_le()"); - - persist_file.seek(0); - fc::raw::pack(persist_file, fsi_t::magic); - fc::raw::pack(persist_file, current_safety_file_version); - fc::raw::pack(persist_file, (uint64_t)(finalizers.size() + inactive_safety_info.size())); - for (const auto& [pub_key, f] : finalizers) { - fc::raw::pack(persist_file, pub_key); - fc::raw::pack(persist_file, f.fsi); - } - if (!inactive_safety_info_written) { + // optimize by only calculating crc for inactive once + if (inactive_safety_info_written_pos == 0) { + persist_file.seekp(0); + fc::raw::pack(persist_file, fsi_t::magic); + fc::raw::pack(persist_file, current_safety_file_version); + uint64_t size = finalizers.size() + inactive_safety_info.size(); + fc::raw::pack(persist_file, size); + // save also the fsi that was originally present in the file, but which applied to // finalizers not configured anymore. for (const auto& [pub_key, fsi] : inactive_safety_info) { fc::raw::pack(persist_file, pub_key); fc::raw::pack(persist_file, fsi); } - inactive_safety_info_written = true; + inactive_safety_info_written_pos = persist_file.tellp(); + inactive_crc32 = persist_file.crc(); + } else { + persist_file.seekp(inactive_safety_info_written_pos, inactive_crc32); + } + + // active finalizers + for (const auto& [pub_key, f] : finalizers) { + fc::raw::pack(persist_file, pub_key); + fc::raw::pack(persist_file, f.fsi); } - persist_file.flush(); + + uint32_t cs = persist_file.checksum(); + fc::raw::pack(persist_file, cs); + + cfile_ds.flush(); } FC_LOG_AND_RETHROW() } // ---------------------------------------------------------------------------------------- + +// Corresponds to safety_file_version_0 +struct finalizer_safety_information_v0 { + block_ref last_vote; + block_ref lock; + bool votes_forked_since_latest_strong_vote {false}; +}; + +void my_finalizers_t::load_finalizer_safety_info_v0(fsi_map& res) { + uint64_t num_finalizers {0}; + fc::raw::unpack(persist_file, num_finalizers); + for (size_t i=0; i #include #include +#include #include #include #include @@ -34,7 +35,7 @@ namespace eosio::chain { struct finalizer_safety_information { block_ref last_vote; block_ref lock; - bool votes_forked_since_latest_strong_vote {false}; + block_timestamp_type other_branch_latest_time; static constexpr uint64_t magic = 0x5AFE11115AFE1111ull; @@ -43,7 +44,7 @@ namespace eosio::chain { auto operator==(const finalizer_safety_information& o) const { return last_vote == o.last_vote && lock == o.lock && - votes_forked_since_latest_strong_vote == o.votes_forked_since_latest_strong_vote; + other_branch_latest_time == o.other_branch_latest_time; } }; @@ -70,21 +71,34 @@ namespace eosio::chain { // ---------------------------------------------------------------------------------------- struct my_finalizers_t { public: - static constexpr uint64_t current_safety_file_version = 0; + /// + /// Version 0: Spring 1.0.0 RC2 - File has fixed packed sizes with inactive safety info written to the end + /// of the file. Consists of [finalizer public_key, FSI].. + /// Version 1: Spring 1.0.0 RC3 - File has inactive FSIs written at the beginning of the file. Uses crc32 + /// checksum to verify data on read. + /// + static constexpr uint64_t safety_file_version_0 = 0; + static constexpr uint64_t safety_file_version_1 = 1; + static constexpr uint64_t current_safety_file_version = safety_file_version_1; using fsi_t = finalizer_safety_information; using fsi_map = std::map; using vote_t = std::tuple; private: + using persist_file_t = fc::datastream_crc>; + using finalizer_map_t = std::map; + const std::filesystem::path persist_file_path; // where we save the safety data std::atomic has_voted{false}; // true if this node has voted and updated safety info mutable std::mutex mtx; - mutable fc::datastream persist_file; // we want to keep the file open for speed - std::map finalizers; // the active finalizers for this node, loaded at startup, not mutated afterwards + mutable fc::datastream cfile_ds; // we want to keep the file open for speed + mutable persist_file_t persist_file{cfile_ds};// we want to calculate checksum + finalizer_map_t finalizers; // the active finalizers for this node, loaded at startup, not mutated afterwards fsi_map inactive_safety_info; // loaded at startup, not mutated afterwards fsi_t default_fsi = fsi_t::unset_fsi(); // default provided at spring startup - mutable bool inactive_safety_info_written{false}; + mutable long inactive_safety_info_written_pos{0}; + mutable boost::crc_32_type inactive_crc32; // cached value public: explicit my_finalizers_t(const std::filesystem::path& persist_file_path) @@ -167,13 +181,18 @@ namespace eosio::chain { // for testing purposes only, not thread safe const fsi_t& get_fsi(const bls_public_key& k) { return finalizers[k].fsi; } void set_fsi(const bls_public_key& k, const fsi_t& fsi) { finalizers[k].fsi = fsi; } + + private: + void load_finalizer_safety_info_v0(fsi_map& res); + void load_finalizer_safety_info_v1(fsi_map& res); + }; } namespace std { inline std::ostream& operator<<(std::ostream& os, const eosio::chain::finalizer_safety_information& fsi) { - os << "fsi(" << fsi.last_vote << ", " << fsi.lock << ", " << fsi.votes_forked_since_latest_strong_vote << ")"; + os << "fsi(" << fsi.last_vote << ", " << fsi.lock << ", " << fsi.other_branch_latest_time << ")"; return os; } @@ -191,5 +210,5 @@ namespace std { } } -FC_REFLECT(eosio::chain::finalizer_safety_information, (last_vote)(lock)(votes_forked_since_latest_strong_vote)) +FC_REFLECT(eosio::chain::finalizer_safety_information, (last_vote)(lock)(other_branch_latest_time)) FC_REFLECT_ENUM(eosio::chain::finalizer::vote_decision, (strong_vote)(weak_vote)(no_vote)) diff --git a/libraries/libfc/CMakeLists.txt b/libraries/libfc/CMakeLists.txt index 18b1b2e7a2..c9c88f83a8 100644 --- a/libraries/libfc/CMakeLists.txt +++ b/libraries/libfc/CMakeLists.txt @@ -103,7 +103,7 @@ if(APPLE) find_library(corefoundation_framework CoreFoundation) endif() target_link_libraries( fc PUBLIC Boost::date_time Boost::chrono Boost::iostreams Boost::interprocess Boost::multi_index Boost::dll - Boost::multiprecision Boost::beast Boost::asio Boost::thread Threads::Threads + Boost::multiprecision Boost::beast Boost::asio Boost::thread Boost::crc Threads::Threads boringssl ZLIB::ZLIB ${PLATFORM_SPECIFIC_LIBS} ${CMAKE_DL_LIBS} secp256k1 bls12-381 ${security_framework} ${corefoundation_framework}) add_subdirectory( test ) diff --git a/libraries/libfc/include/fc/io/cfile.hpp b/libraries/libfc/include/fc/io/cfile.hpp index f3cd05ded1..5ca5c90461 100644 --- a/libraries/libfc/include/fc/io/cfile.hpp +++ b/libraries/libfc/include/fc/io/cfile.hpp @@ -329,6 +329,16 @@ class datastream : public fc::cfile { return true; } + bool read( char* d, size_t s ) { + cfile::read( d, s ); + return true; + } + + bool write(const char* d, size_t s) { + cfile::write(d, s); + return true; + } + fc::cfile& storage() { return *this; } const fc::cfile& storage() const { return *this; } diff --git a/libraries/libfc/include/fc/io/datastream_crc.hpp b/libraries/libfc/include/fc/io/datastream_crc.hpp new file mode 100644 index 0000000000..d447cc6379 --- /dev/null +++ b/libraries/libfc/include/fc/io/datastream_crc.hpp @@ -0,0 +1,109 @@ +#pragma once +#include +#include + +namespace fc { + +/** +* Provides a datasteam wrapper around another datastream for calculation of checksum. +* Example use: +* fc::datastream persist_cfile; +* fc::datastream_crc> persist_stream{persist_cfile}; +* +* persist_stream.seekp(0); +* fc::raw::pack(persist_stream, 'a'); +* uint32_t cs = persist_stream.check_sum(); +* fc::raw::pack(persist_stream, cs); // write checksum to file +* // ... +* persist_stream.seekp(0); +* char c; +* fc::raw::unpack(persist_stream, c); +* uint32_t calc_cs = persist_stream.check_sum(); +* uint32_t cs; +* fc::raw::unpack(persist_stream, cs); +* FC_ASSERT(calc_cs == cs, "checksum not equal"); +*/ +template +class datastream_crc { +public: + // ds must outlive datasteam_crc + explicit datastream_crc( DS& ds ) : ds_(ds) {} + + void skip(size_t s) { + ds_.skip(s); + } + + bool read(char* d, size_t s) { + bool r = ds_.read(d, s); + crc_.process_bytes(d, s); + return r; + } + + bool write(const char* d, size_t s) { + crc_.process_bytes(d, s); + return ds_.write(d, s); + } + + bool put(char c) { + crc_.process_byte(c); + return ds_.put(c); + } + + bool get(unsigned char& c) { + bool r = ds_.get(c); + crc_.process_byte(c); + return r; + } + + bool get(char& c) { + bool r = ds_.get(c); + crc_.process_byte(c); + return r; + } + + auto pos() const { + return ds_.pos(); + } + + bool valid() const { + return ds_.valid(); + } + + // only use with p==0, otherwise use seekp() below + bool seekp(size_t p) { + if (p == 0) { + crc_.reset(); + return ds_.seekp(0); + } + return false; + } + + size_t tellp() const { + return ds_.tellp(); + } + size_t remaining() const { + return ds_.remaining(); + } + + // extension to datastream + + bool seekp(size_t p, const CRC& crc) { + crc_ = crc; + return ds_.seekp(p); + } + + uint32_t checksum() const { + return crc_.checksum(); + } + + CRC crc() const { + return crc_; + } + +private: + DS& ds_; + CRC crc_; +}; + + +} // namespace fc diff --git a/unittests/finalizer_tests.cpp b/unittests/finalizer_tests.cpp index 392413c375..249a10660a 100644 --- a/unittests/finalizer_tests.cpp +++ b/unittests/finalizer_tests.cpp @@ -47,7 +47,7 @@ std::vector create_random_fsi(size_t count) { .lock = block_ref{sha256::hash("lock"s + std::to_string(i)), tstamp(i * 100), sha256::hash("lock_digest"s + std::to_string(i))}, - .votes_forked_since_latest_strong_vote = false + .other_branch_latest_time = {} }); if (i) assert(res.back() != res[0]); @@ -101,7 +101,7 @@ BOOST_AUTO_TEST_CASE( basic_finalizer_safety_file_io ) try { fsi_t fsi { .last_vote = proposals[6], .lock = proposals[2], - .votes_forked_since_latest_strong_vote = false }; + .other_branch_latest_time = {} }; bls_keys_t k("alice"_n); bls_pub_priv_key_map_t local_finalizers = { { k.pubkey_str, k.privkey_str } }; @@ -134,7 +134,7 @@ BOOST_AUTO_TEST_CASE( corrupt_finalizer_safety_file ) try { fsi_t fsi { .last_vote = proposals[6], .lock = proposals[2], - .votes_forked_since_latest_strong_vote = false }; + .other_branch_latest_time = {} }; bls_keys_t k("alice"_n); bls_pub_priv_key_map_t local_finalizers = { { k.pubkey_str, k.privkey_str } }; diff --git a/unittests/finalizer_vote_tests.cpp b/unittests/finalizer_vote_tests.cpp index 7499a16f90..a9fe942b02 100644 --- a/unittests/finalizer_vote_tests.cpp +++ b/unittests/finalizer_vote_tests.cpp @@ -143,7 +143,7 @@ struct simulator_t { forkdb.reset_root(genesis); block_ref genesis_ref(genesis->id(), genesis->timestamp(), genesis->id()); - my_finalizer.fsi = fsi_t{genesis_ref, genesis_ref, false}; + my_finalizer.fsi = fsi_t{genesis_ref, genesis_ref, {}}; } vote_result vote(const bsp& p) { diff --git a/unittests/test-data/fsi/safety_v1.dat b/unittests/test-data/fsi/safety_v1.dat new file mode 100644 index 0000000000..58c22860ca Binary files /dev/null and b/unittests/test-data/fsi/safety_v1.dat differ