diff --git a/libraries/chain/block_log.cpp b/libraries/chain/block_log.cpp index eeacf5debf..a4ea1b7d93 100644 --- a/libraries/chain/block_log.cpp +++ b/libraries/chain/block_log.cpp @@ -15,6 +15,12 @@ static_assert(__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__); namespace sysio { namespace chain { + namespace { + constexpr auto empty_active_block_log_message = + "{} is empty: the launch that created it did not complete startup. " + "Rerun once with --delete-all-blocks."; + } + enum versions { initial_version = 1, ///< complete block log from genesis block_x_start_version = 2, ///< adds optional partial block log, cannot be used for replay without snapshot @@ -1565,8 +1571,11 @@ namespace sysio { namespace chain { }); } - if (first_block_file.empty() && std::filesystem::exists(block_dir / "blocks.log")) { - first_block_file = block_dir / "blocks.log"; + const auto active_log = block_dir / "blocks.log"; + if (first_block_file.empty() && std::filesystem::exists(active_log)) { + SYS_ASSERT(std::filesystem::file_size(active_log) > 0, block_log_exception, + empty_active_block_log_message, active_log.generic_string()); + first_block_file = active_log; } if (!first_block_file.empty()) { diff --git a/libraries/chain/controller.cpp b/libraries/chain/controller.cpp index 02f446eafb..39c6e2a6f6 100644 --- a/libraries/chain/controller.cpp +++ b/libraries/chain/controller.cpp @@ -51,6 +51,12 @@ namespace sysio::chain { +namespace { +constexpr auto missing_database_header_message = + "State database has no version header: the launch that created it did not complete startup. " + "Rerun once with --delete-all-blocks."; +} + using resource_limits::resource_limits_manager; using controller_index_set = index_set< @@ -1481,7 +1487,7 @@ struct controller_impl { const auto& header_idx = db.get_index().indices().get(); SYS_ASSERT(header_idx.begin() != header_idx.end(), bad_database_version_exception, - "state database version pre-dates versioning, please restore from a compatible snapshot or replay!"); + missing_database_header_message); auto header_itr = header_idx.begin(); header_itr->validate(); diff --git a/unittests/block_log_extract.cpp b/unittests/block_log_extract.cpp index 984a88d2a5..6c84720d6e 100644 --- a/unittests/block_log_extract.cpp +++ b/unittests/block_log_extract.cpp @@ -4,6 +4,7 @@ #include #include +#include #include using namespace sysio::chain; @@ -49,6 +50,29 @@ struct block_log_extract_fixture { BOOST_AUTO_TEST_SUITE(block_log_extraction_tests) +/** An active log created before interrupted startup has no preamble to read. */ +BOOST_AUTO_TEST_CASE(empty_active_log_reports_interrupted_initialization) { + fc::temp_directory active_dir; + { + block_log uninitialized(active_dir.path()); + } + const auto expected = (active_dir.path() / "blocks.log").generic_string() + + " is empty: the launch that created it did not complete startup. " + "Rerun once with --delete-all-blocks."; + BOOST_REQUIRE_EXCEPTION(block_log::extract_chain_context(active_dir.path(), {}), block_log_exception, + [&](const auto& ex) { return ex.top_message() == expected; }); +} + +BOOST_AUTO_TEST_CASE(empty_retained_log_is_rejected) { + fc::temp_directory active_dir; + fc::temp_directory retained_dir; + { + std::ofstream log(retained_dir.path() / "blocks-3-7.log", std::ios::binary); + } + BOOST_CHECK_THROW(block_log::extract_chain_context(active_dir.path(), retained_dir.path()), + std::ios_base::failure); +} + BOOST_FIXTURE_TEST_CASE(extract_from_middle, block_log_extract_fixture) try { fc::temp_directory output_dir; diff --git a/unittests/restart_chain_tests.cpp b/unittests/restart_chain_tests.cpp index 95f9744165..2fd40d9571 100644 --- a/unittests/restart_chain_tests.cpp +++ b/unittests/restart_chain_tests.cpp @@ -1,5 +1,5 @@ -#include #include +#include #include #include @@ -89,6 +89,36 @@ class replay_tester : public base_tester { BOOST_AUTO_TEST_SUITE(restart_chain_tests) +/** An interrupted initialization leaves a state database without a version header. */ +BOOST_AUTO_TEST_CASE(interrupted_initialization_reports_missing_database_header) { + tester reference; + auto cfg = reference.get_config(); + fc::temp_directory directory; + cfg.state_dir = directory.path() / "state"; + cfg.blocks_dir = directory.path() / "blocks"; + cfg.finalizers_dir = directory.path() / "finalizers"; + { + controller interrupted(cfg, reference.control->get_chain_id()); + interrupted.add_indices(); + } + BOOST_REQUIRE_EXCEPTION(controller::extract_chain_id_from_db(cfg.state_dir), bad_database_version_exception, + [](const auto& ex) { + return ex.top_message() == + "State database has no version header: the launch that created it did not complete startup. " + "Rerun once with --delete-all-blocks."; + }); +} + +/** A partially written active log still reports its original read failure. */ +BOOST_AUTO_TEST_CASE(restart_rejects_truncated_block_log) { + fc::temp_directory directory; + { + std::ofstream log(directory.path() / "blocks.log", std::ios::binary); + log.put('\0'); + } + BOOST_CHECK_THROW(block_log::extract_chain_context(directory.path(), {}), std::ios_base::failure); +} + /// Existing chain state must never restart with missing, corrupt, unreadable, or mismatched replay protection. BOOST_AUTO_TEST_CASE(dedup_state_required_on_restart) { for (const std::string damage : {"missing", "corrupt", "unreadable", "mismatched"}) {