Skip to content

SHiP: Verify last log index entry matches last log entry #1680

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions libraries/state_history/include/eosio/state_history/log.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ class state_history_log {

check_log_on_init();
check_index_on_init();
check_log_and_index_on_init();

//check for conversions to/from pruned log, as long as log contains something
if(!empty()) {
Expand Down Expand Up @@ -419,6 +420,23 @@ class state_history_log {
ilog("${name} regeneration complete", ("name", index.display_path()));
}

void check_log_and_index_on_init() {
if(log.size() == 0)
return;

try {
log_header first_header = log.unpack_from<decltype(first_header)>(0);
FC_ASSERT(is_ship(first_header.magic) && is_ship_supported_version(first_header.magic), "Unexpected header magic");
bool is_pruned = is_ship_log_pruned(first_header.magic);

//fetch the last block header from the log solely using the log (i.e. not the index: so don't use get_pos()). This is a sanity check.
const uint64_t last_header_pos = log.unpack_from<std::decay_t<decltype(last_header_pos)>>(log.size() - sizeof(uint64_t) - (is_pruned ? sizeof(uint32_t) : 0));
//verify last index position matches last log entry
const uint64_t index_pos = get_pos(_end_block-1);
FC_ASSERT(index_pos == last_header_pos, "Last index position ${ip} does not match last entry in log ${lp}", ("ip", index_pos)("lp", last_header_pos));
} EOS_RETHROW_EXCEPTIONS(chain::plugin_exception, "${name} is corrupted and cannot be repaired, will be automatically regenerated if removed.", ("name", index.display_path()));
}

uint64_t get_pos(uint32_t block_num) {
assert(block_num >= _begin_block && block_num < _end_block);
return index.unpack_from<uint64_t>((block_num - _index_begin_block) * sizeof(uint64_t));
Expand Down
80 changes: 80 additions & 0 deletions unittests/state_history_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -876,6 +876,7 @@ BOOST_AUTO_TEST_CASE_TEMPLATE(test_corrupted_log_recovery, T, state_history_test
logfile.open("ab");
const char random_data[] = "12345678901231876983271649837";
logfile.write(random_data, sizeof(random_data));
logfile.close();

std::filesystem::remove_all(chain.get_config().blocks_dir/"reversible");

Expand All @@ -887,4 +888,83 @@ BOOST_AUTO_TEST_CASE_TEMPLATE(test_corrupted_log_recovery, T, state_history_test
BOOST_CHECK(get_decompressed_entry(new_chain.chain_state_log,10).size());
}

BOOST_AUTO_TEST_CASE_TEMPLATE(test_no_index_recovery, T, state_history_testers) {
fc::temp_directory state_history_dir;

eosio::state_history::partition_config config{};

T chain(state_history_dir.path(), config);
chain.produce_block();
chain.produce_blocks(21, true);
chain.close();

std::filesystem::remove(state_history_dir.path() / "trace_history.index");

T new_chain(state_history_dir.path(), config);
new_chain.produce_block();

BOOST_CHECK(get_traces(new_chain.traces_log, 10).size());
BOOST_CHECK(get_decompressed_entry(new_chain.chain_state_log,10).size());
}

BOOST_AUTO_TEST_CASE_TEMPLATE(test_curropted_index_recovery, T, state_history_testers) {
fc::temp_directory state_history_dir;

eosio::state_history::partition_config config{};

T chain(state_history_dir.path(), config);
chain.produce_block();
chain.produce_blocks(21, true);
chain.close();

// write a few random bytes to end of index log, size will not match and it will be auto-recreated
fc::cfile indexfile;
indexfile.set_file_path(state_history_dir.path() / "trace_history.index");
indexfile.open("ab");
const char random_data[] = "12345678901231876983271649837";
indexfile.seek_end(0);
indexfile.write(random_data, sizeof(random_data));
indexfile.close();

T new_chain(state_history_dir.path(), config);
new_chain.produce_block();

BOOST_CHECK(get_traces(new_chain.traces_log, 10).size());
BOOST_CHECK(get_decompressed_entry(new_chain.chain_state_log,10).size());
}

BOOST_AUTO_TEST_CASE_TEMPLATE(test_curropted_index_error, T, state_history_testers) {
fc::temp_directory state_history_dir;

eosio::state_history::partition_config config{};

T chain(state_history_dir.path(), config);
chain.produce_block();
chain.produce_blocks(21, true);
chain.close();

// write a few random bytes to end of index log, size will not match and it will be auto-recreated
fc::cfile indexfile;
indexfile.set_file_path(state_history_dir.path() / "trace_history.index");
indexfile.open("rb+");
const char random_data[] = "12345678901231876983271649837";
indexfile.seek_end(-(sizeof(random_data)));
indexfile.write(random_data, sizeof(random_data));
indexfile.close();

BOOST_CHECK_EXCEPTION(T(state_history_dir.path(), config),
plugin_exception, [](const plugin_exception& e) {
return e.to_detail_string().find("trace_history.index is corrupted and cannot be repaired, will be automatically regenerated if removed") != std::string::npos;
});

// remove index for auto recovery
std::filesystem::remove(state_history_dir.path() / "trace_history.index");

T new_chain(state_history_dir.path(), config);
new_chain.produce_block();

BOOST_CHECK(get_traces(new_chain.traces_log, 10).size());
BOOST_CHECK(get_decompressed_entry(new_chain.chain_state_log,10).size());
}

BOOST_AUTO_TEST_SUITE_END()