mirror of
https://github.com/AntelopeIO/spring.git
synced 2026-07-23 15:03:39 +00:00
1517 lines
69 KiB
C++
1517 lines
69 KiB
C++
#include <boost/test/unit_test.hpp>
|
|
#include <boost/test/data/test_case.hpp>
|
|
#include <boost/test/data/monomorphic.hpp>
|
|
#include <boost/test/data/monomorphic/generators/xrange.hpp>
|
|
|
|
#include <fc/io/raw.hpp>
|
|
#include <fc/io/cfile.hpp>
|
|
#include <fc/bitutil.hpp>
|
|
#include <fc/crypto/rand.hpp>
|
|
#include <fc/crypto/sha256.hpp>
|
|
#include <fc/io/fstream.hpp>
|
|
|
|
#include <eosio/state_history/log_catalog.hpp>
|
|
#include <boost/iostreams/copy.hpp>
|
|
#include <boost/iostreams/device/back_inserter.hpp>
|
|
|
|
|
|
namespace bdata = boost::unit_test::data;
|
|
namespace bio = boost::iostreams;
|
|
using namespace eosio;
|
|
using namespace eosio::chain;
|
|
using namespace fc;
|
|
using namespace std::literals;
|
|
|
|
static block_id_type fake_blockid_for_num(const block_num_type block_num, const uint64_t salt = 0u) {
|
|
block_id_type ret;
|
|
ret._hash[0] = ret._hash[1] = ret._hash[2] = ret._hash[3] = salt;
|
|
ret._hash[0] &= 0xffffffff00000000;
|
|
ret._hash[0] += fc::endian_reverse_u32(block_num);
|
|
return ret;
|
|
}
|
|
|
|
struct random_source {
|
|
typedef char char_type;
|
|
struct category : bio::seekable_device_tag {};
|
|
std::streamsize read(char* s, std::streamsize n) {
|
|
if(n == 0) //why doesn't restrict() do this for us?
|
|
return -1;
|
|
rand_bytes(s, n);
|
|
return n;
|
|
}
|
|
std::streamsize write(const char_type* s, std::streamsize n) {
|
|
FC_ASSERT(false, "not supported");
|
|
}
|
|
//this isn't valid but Device needs to be Seekable for restrict()
|
|
std::streampos seek(bio::stream_offset off, std::ios_base::seekdir) {
|
|
return off;
|
|
}
|
|
};
|
|
BOOST_IOSTREAMS_PIPABLE(random_source, 0)
|
|
|
|
struct sha256_filter {
|
|
typedef char char_type;
|
|
struct category : bio::dual_use, bio::filter_tag, bio::multichar_tag, bio::optimally_buffered_tag {};
|
|
|
|
std::streamsize optimal_buffer_size() const {return 4*1024;}
|
|
|
|
template<typename Source>
|
|
std::streamsize read(Source& src, char_type* s, std::streamsize n) {
|
|
std::streamsize result = bio::read(src, s, n);
|
|
if(result == -1)
|
|
return -1;
|
|
enc->write(s, result);
|
|
return result;
|
|
}
|
|
|
|
template<typename Sink>
|
|
std::streamsize write(Sink& snk, const char_type* s, std::streamsize n) {
|
|
std::streamsize result = bio::write(snk, s, n);
|
|
enc->write(s, result);
|
|
return result;
|
|
}
|
|
|
|
//sha256::encoder is not copyable which is a requirement for a Filter when used in a pipeline like done below. for this trivial
|
|
// non-production use case let's just hack the limitation by stuffing it in shared_ptr so it becomes "copyable"
|
|
std::shared_ptr<sha256::encoder> enc = std::make_shared<sha256::encoder>();
|
|
};
|
|
BOOST_IOSTREAMS_PIPABLE(sha256_filter, 0)
|
|
|
|
struct ship_log_fixture {
|
|
ship_log_fixture(bool enable_read, bool reopen_on_mark, bool remove_index_on_reopen, bool vacuum_on_exit_if_small, std::optional<uint32_t> prune_blocks) :
|
|
enable_read(enable_read), reopen_on_mark(reopen_on_mark),
|
|
remove_index_on_reopen(remove_index_on_reopen), vacuum_on_exit_if_small(vacuum_on_exit_if_small){
|
|
if (prune_blocks)
|
|
conf = eosio::state_history::prune_config{ .prune_blocks = *prune_blocks };
|
|
bounce();
|
|
}
|
|
|
|
void add(uint32_t index, size_t size, char fillchar, char prevchar) {
|
|
std::vector<char> a;
|
|
a.assign(size, fillchar);
|
|
|
|
auto block_for_id = [](const uint32_t bnum, const char fillc) {
|
|
fc::sha256 m = fc::sha256::hash(fc::sha256::hash(std::to_string(bnum)+fillc));
|
|
m._hash[0] = fc::endian_reverse_u32(bnum);
|
|
return m;
|
|
};
|
|
|
|
log->pack_and_write_entry(block_for_id(index, fillchar), block_for_id(index-1, prevchar), [&](auto& f) {
|
|
bio::write(f, a.data(), a.size());
|
|
});
|
|
|
|
if(index + 1 > written_data.size())
|
|
written_data.resize(index + 1);
|
|
written_data.at(index) = a;
|
|
}
|
|
|
|
void check_range_present(uint32_t first, uint32_t last) {
|
|
auto r = log->block_range();
|
|
BOOST_REQUIRE_EQUAL(r.first, first);
|
|
BOOST_REQUIRE_EQUAL(r.second-1, last);
|
|
if(enable_read) {
|
|
for(auto i = first; i <= last; i++) {
|
|
std::optional<eosio::state_history::ship_log_entry> entry = log->get_entry(i);
|
|
BOOST_REQUIRE(!!entry);
|
|
bio::filtering_istreambuf istream = entry->get_stream();
|
|
std::vector<char> buff;
|
|
bio::copy(istream, bio::back_inserter(buff));
|
|
BOOST_REQUIRE(buff == written_data.at(i));
|
|
}
|
|
}
|
|
}
|
|
|
|
void check_not_present(uint32_t index) {
|
|
BOOST_REQUIRE(!log->get_entry(index));;
|
|
}
|
|
|
|
void check_empty() {
|
|
BOOST_REQUIRE(log->empty());
|
|
}
|
|
|
|
//double the fun
|
|
template <typename F>
|
|
void check_n_bounce(F&& f) {
|
|
f();
|
|
if(reopen_on_mark) {
|
|
bounce();
|
|
f();
|
|
}
|
|
}
|
|
|
|
bool enable_read, reopen_on_mark, remove_index_on_reopen, vacuum_on_exit_if_small;
|
|
eosio::state_history::state_history_log_config conf;
|
|
fc::temp_directory log_dir;
|
|
|
|
std::optional<eosio::state_history::log_catalog> log;
|
|
|
|
std::vector<std::vector<char>> written_data;
|
|
|
|
private:
|
|
void bounce() {
|
|
log.reset();
|
|
if(remove_index_on_reopen)
|
|
std::filesystem::remove(log_dir.path()/ (std::string("shipit") + ".index"));
|
|
auto prune_conf = std::get_if<eosio::state_history::prune_config>(&conf);
|
|
if(prune_conf) {
|
|
prune_conf->prune_threshold = 8; //every 8 bytes check in and see if to prune. should make it always check after each entry for us
|
|
if(vacuum_on_exit_if_small)
|
|
prune_conf->vacuum_on_close = 1024*1024*1024; //something large: always vacuum on close for these tests
|
|
}
|
|
log.emplace(log_dir.path(), conf, "shipit");
|
|
}
|
|
};
|
|
|
|
//can only punch holes on filesystem block boundaries. let's make sure the entries we add are larger than that
|
|
static size_t larger_than_tmpfile_blocksize() {
|
|
fc::temp_cfile tf;
|
|
auto& cf = tf.file();
|
|
return cf.filesystem_block_size() + cf.filesystem_block_size()/2;
|
|
}
|
|
|
|
BOOST_AUTO_TEST_SUITE(ship_file_tests)
|
|
|
|
BOOST_DATA_TEST_CASE(basic_prune_test, bdata::xrange(2) * bdata::xrange(2) * bdata::xrange(2) * bdata::xrange(2), enable_read, reopen_on_mark, remove_index_on_reopen, vacuum_on_exit_if_small) { try {
|
|
ship_log_fixture t(enable_read, reopen_on_mark, remove_index_on_reopen, vacuum_on_exit_if_small, 4);
|
|
|
|
t.check_empty();
|
|
|
|
//with a small prune blocks value, the log will attempt to prune every filesystem block size. So let's just make
|
|
// every entry be greater than that size
|
|
size_t payload_size = larger_than_tmpfile_blocksize();
|
|
|
|
//we'll start at 2 here, since that's what you'd get from starting from genesis, but it really doesn't matter
|
|
// one way or another for the ship log logic
|
|
t.add(2, payload_size, 'A', 'A');
|
|
t.add(3, payload_size, 'B', 'A');
|
|
t.add(4, payload_size, 'C', 'B');
|
|
t.check_n_bounce([&]() {
|
|
t.check_range_present(2, 4);
|
|
});
|
|
|
|
t.add(5, payload_size, 'D', 'C');
|
|
t.check_n_bounce([&]() {
|
|
t.check_range_present(2, 5);
|
|
});
|
|
|
|
t.add(6, payload_size, 'E', 'D');
|
|
t.check_n_bounce([&]() {
|
|
t.check_not_present(2);
|
|
t.check_range_present(3, 6);
|
|
});
|
|
|
|
t.add(7, payload_size, 'F', 'E');
|
|
t.check_n_bounce([&]() {
|
|
t.check_not_present(2);
|
|
t.check_not_present(3);
|
|
t.check_range_present(4, 7);
|
|
});
|
|
|
|
//undo 6 & 7 and reapply 6
|
|
t.add(6, payload_size, 'G', 'D');
|
|
t.check_n_bounce([&]() {
|
|
t.check_not_present(2);
|
|
t.check_not_present(3);
|
|
t.check_not_present(7);
|
|
t.check_range_present(4, 6);
|
|
});
|
|
|
|
t.add(7, payload_size, 'H', 'G');
|
|
t.check_n_bounce([&]() {
|
|
t.check_not_present(2);
|
|
t.check_not_present(3);
|
|
t.check_range_present(4, 7);
|
|
});
|
|
|
|
t.add(8, payload_size, 'I', 'H');
|
|
t.add(9, payload_size, 'J', 'I');
|
|
t.add(10, payload_size, 'K', 'J');
|
|
t.check_n_bounce([&]() {
|
|
t.check_range_present(7, 10);
|
|
});
|
|
|
|
//undo back to the first stored block
|
|
t.add(7, payload_size, 'L', 'G');
|
|
t.check_n_bounce([&]() {
|
|
t.check_range_present(7, 7);
|
|
t.check_not_present(6);
|
|
t.check_not_present(8);
|
|
});
|
|
|
|
t.add(8, payload_size, 'M', 'L');
|
|
t.add(9, payload_size, 'N', 'M');
|
|
t.add(10, payload_size, 'O', 'N');
|
|
t.add(11, payload_size, 'P', 'O');
|
|
t.check_n_bounce([&]() {
|
|
t.check_range_present(8, 11);
|
|
t.check_not_present(6);
|
|
t.check_not_present(7);
|
|
});
|
|
|
|
//pile up more
|
|
t.add(12, payload_size, 'W', 'P');
|
|
t.add(13, payload_size, 'X', 'W');
|
|
t.add(14, payload_size, 'Y', 'X');
|
|
t.add(15, payload_size, 'Z', 'Y');
|
|
t.add(16, payload_size, '1', 'Z');
|
|
t.check_n_bounce([&]() {
|
|
t.check_range_present(13, 16);
|
|
t.check_not_present(12);
|
|
t.check_not_present(17);
|
|
});
|
|
|
|
//invalid fork, previous should be 'X'
|
|
BOOST_REQUIRE_EXCEPTION(t.add(14, payload_size, '*', 'W' ), eosio::chain::plugin_exception, [](const eosio::chain::plugin_exception& e) {
|
|
return e.to_detail_string().find("missed a fork change") != std::string::npos;
|
|
});
|
|
|
|
} FC_LOG_AND_RETHROW() }
|
|
|
|
BOOST_DATA_TEST_CASE(basic_test, bdata::xrange(2) * bdata::xrange(2) * bdata::xrange(2), enable_read, reopen_on_mark, remove_index_on_reopen) { try {
|
|
ship_log_fixture t(enable_read, reopen_on_mark, remove_index_on_reopen, false, std::optional<uint32_t>());
|
|
|
|
t.check_empty();
|
|
size_t payload_size = larger_than_tmpfile_blocksize();
|
|
|
|
//we'll start off with a high number; but it really doesn't matter for ship's logs
|
|
t.add(200, payload_size, 'A', 'A');
|
|
t.add(201, payload_size, 'B', 'A');
|
|
t.add(202, payload_size, 'C', 'B');
|
|
t.check_n_bounce([&]() {
|
|
t.check_range_present(200, 202);
|
|
});
|
|
t.add(203, payload_size, 'D', 'C');
|
|
t.add(204, payload_size, 'E', 'D');
|
|
t.add(205, payload_size, 'F', 'E');
|
|
t.add(206, payload_size, 'G', 'F');
|
|
t.add(207, payload_size, 'H', 'G');
|
|
t.check_n_bounce([&]() {
|
|
t.check_range_present(200, 207);
|
|
});
|
|
|
|
//fork off G & H
|
|
t.add(206, payload_size, 'I', 'F');
|
|
t.add(207, payload_size, 'J', 'I');
|
|
t.check_n_bounce([&]() {
|
|
t.check_range_present(200, 207);
|
|
});
|
|
|
|
t.add(208, payload_size, 'K', 'J');
|
|
t.add(209, payload_size, 'L', 'K');
|
|
t.check_n_bounce([&]() {
|
|
t.check_range_present(200, 209);
|
|
t.check_not_present(199);
|
|
t.check_not_present(210);
|
|
});
|
|
|
|
} FC_LOG_AND_RETHROW() }
|
|
|
|
BOOST_AUTO_TEST_CASE(empty) { try {
|
|
fc::temp_directory log_dir;
|
|
|
|
{
|
|
eosio::state_history::state_history_log log(log_dir.path()/ "empty");
|
|
BOOST_REQUIRE(log.empty());
|
|
}
|
|
//reopen
|
|
{
|
|
eosio::state_history::state_history_log log(log_dir.path() / "empty");
|
|
BOOST_REQUIRE(log.empty());
|
|
}
|
|
//reopen but prunned set
|
|
const eosio::state_history::prune_config simple_prune_conf = {
|
|
.prune_blocks = 4
|
|
};
|
|
{
|
|
eosio::state_history::state_history_log log(log_dir.path() / "empty", state_history::state_history_log::no_non_local_get_block_id_func, simple_prune_conf);
|
|
BOOST_REQUIRE(log.empty());
|
|
}
|
|
{
|
|
eosio::state_history::state_history_log log(log_dir.path() / "empty", state_history::state_history_log::no_non_local_get_block_id_func, simple_prune_conf);
|
|
BOOST_REQUIRE(log.empty());
|
|
}
|
|
//back to non pruned
|
|
{
|
|
eosio::state_history::state_history_log log(log_dir.path() / "empty");
|
|
BOOST_REQUIRE(log.empty());
|
|
}
|
|
{
|
|
eosio::state_history::state_history_log log(log_dir.path() / "empty");
|
|
BOOST_REQUIRE(log.empty());
|
|
}
|
|
|
|
auto log_file = (log_dir.path()/ (std::string("empty") + ".log")).string();
|
|
auto index_file = (log_dir.path()/ (std::string("empty") + ".index")).string();
|
|
|
|
BOOST_REQUIRE(std::filesystem::file_size(log_file.c_str()) == 0);
|
|
BOOST_REQUIRE(std::filesystem::file_size(index_file.c_str()) == 0);
|
|
|
|
//one more time to pruned, just to make sure
|
|
{
|
|
eosio::state_history::state_history_log log(log_dir.path()/ "empty", state_history::state_history_log::no_non_local_get_block_id_func, simple_prune_conf);
|
|
BOOST_REQUIRE(log.empty());
|
|
}
|
|
BOOST_REQUIRE(std::filesystem::file_size(log_file.c_str()) == 0);
|
|
BOOST_REQUIRE(std::filesystem::file_size(index_file.c_str()) == 0);
|
|
} FC_LOG_AND_RETHROW() }
|
|
|
|
BOOST_DATA_TEST_CASE(non_prune_to_prune, bdata::xrange(2) * bdata::xrange(2), enable_read, remove_index_on_reopen) { try {
|
|
ship_log_fixture t(enable_read, true, remove_index_on_reopen, false, std::optional<uint32_t>());
|
|
|
|
t.check_empty();
|
|
size_t payload_size = larger_than_tmpfile_blocksize();
|
|
|
|
t.add(2, payload_size, 'A', 'A');
|
|
t.add(3, payload_size, 'B', 'A');
|
|
t.add(4, payload_size, 'C', 'B');
|
|
t.add(5, payload_size, 'D', 'C');
|
|
t.add(6, payload_size, 'E', 'D');
|
|
t.add(7, payload_size, 'F', 'E');
|
|
t.add(8, payload_size, 'G', 'F');
|
|
t.add(9, payload_size, 'H', 'G');
|
|
t.check_n_bounce([&]() {
|
|
t.check_range_present(2, 9);
|
|
});
|
|
|
|
//upgrade to pruned...
|
|
t.conf = eosio::state_history::prune_config{ .prune_blocks = 4 };
|
|
t.check_n_bounce([]() {});
|
|
|
|
t.check_n_bounce([&]() {
|
|
t.check_range_present(6, 9);
|
|
});
|
|
t.add(10, payload_size, 'I', 'H');
|
|
t.add(11, payload_size, 'J', 'I');
|
|
t.add(12, payload_size, 'K', 'J');
|
|
t.add(13, payload_size, 'L', 'K');
|
|
t.check_n_bounce([&]() {
|
|
t.check_range_present(10, 13);
|
|
});
|
|
|
|
} FC_LOG_AND_RETHROW() }
|
|
|
|
BOOST_DATA_TEST_CASE(prune_to_non_prune, bdata::xrange(2) * bdata::xrange(2), enable_read, remove_index_on_reopen) { try {
|
|
ship_log_fixture t(enable_read, true, remove_index_on_reopen, false, 4);
|
|
|
|
t.check_empty();
|
|
size_t payload_size = larger_than_tmpfile_blocksize();
|
|
|
|
t.add(2, payload_size, 'A', 'X');
|
|
t.add(3, payload_size, 'B', 'A');
|
|
t.add(4, payload_size, 'C', 'B');
|
|
t.add(5, payload_size, 'D', 'C');
|
|
t.add(6, payload_size, 'E', 'D');
|
|
t.add(7, payload_size, 'F', 'E');
|
|
t.add(8, payload_size, 'G', 'F');
|
|
t.add(9, payload_size, 'H', 'G');
|
|
t.check_n_bounce([&]() {
|
|
t.check_range_present(6, 9);
|
|
});
|
|
|
|
//no more pruned
|
|
t.conf = std::monostate{};
|
|
t.check_n_bounce([]() {});
|
|
|
|
t.check_n_bounce([&]() {
|
|
t.check_range_present(6, 9);
|
|
});
|
|
t.add(10, payload_size, 'I', 'H');
|
|
t.add(11, payload_size, 'J', 'I');
|
|
t.add(12, payload_size, 'K', 'J');
|
|
t.add(13, payload_size, 'L', 'K');
|
|
t.add(14, payload_size, 'M', 'L');
|
|
t.add(15, payload_size, 'N', 'M');
|
|
t.check_n_bounce([&]() {
|
|
t.check_range_present(6, 15);
|
|
});
|
|
|
|
} FC_LOG_AND_RETHROW() }
|
|
|
|
BOOST_DATA_TEST_CASE(prune_to_partitioned, bdata::xrange(2) * bdata::xrange(2), enable_read, remove_index_on_reopen) { try {
|
|
ship_log_fixture t(enable_read, true, remove_index_on_reopen, false, 4);
|
|
|
|
t.check_empty();
|
|
size_t payload_size = larger_than_tmpfile_blocksize();
|
|
|
|
t.add(2, payload_size, 'A', 'A');
|
|
t.add(3, payload_size, 'B', 'A');
|
|
t.add(4, payload_size, 'C', 'B');
|
|
t.add(5, payload_size, 'D', 'C');
|
|
t.add(6, payload_size, 'E', 'D');
|
|
t.add(7, payload_size, 'F', 'E');
|
|
t.add(8, payload_size, 'G', 'F');
|
|
t.add(9, payload_size, 'H', 'G');
|
|
t.check_n_bounce([&]() {
|
|
t.check_range_present(6, 9);
|
|
});
|
|
|
|
//no more pruned
|
|
t.conf = eosio::state_history::partition_config{
|
|
.stride = 5
|
|
};
|
|
|
|
t.check_n_bounce([]() {});
|
|
|
|
t.check_n_bounce([&]() {
|
|
t.check_range_present(6, 9);
|
|
});
|
|
t.add(10, payload_size, 'I', 'H');
|
|
t.add(11, payload_size, 'J', 'I');
|
|
t.add(12, payload_size, 'K', 'J');
|
|
t.add(13, payload_size, 'L', 'K');
|
|
t.add(14, payload_size, 'M', 'L');
|
|
t.add(15, payload_size, 'N', 'M');
|
|
t.check_n_bounce([&]() {
|
|
t.check_range_present(6, 15);
|
|
});
|
|
|
|
} FC_LOG_AND_RETHROW() }
|
|
|
|
BOOST_DATA_TEST_CASE(basic, bdata::make({2u, 333u, 578'000u, 3'123'456'789u}) ^ bdata::make({102u, 400u, 578'111u, 3'123'456'900u}), start, end) try {
|
|
const fc::temp_directory tmpdir;
|
|
|
|
eosio::state_history::log_catalog lc(tmpdir.path(), std::monostate(), "testlog");
|
|
BOOST_REQUIRE(lc.empty());
|
|
|
|
std::map<block_num_type, sha256> wrote_data_for_blocknum;
|
|
std::mt19937 mt_random(0xbeefbeefu * start);
|
|
|
|
//write some blocks in order
|
|
for(unsigned i = start; i < end; ++i)
|
|
lc.pack_and_write_entry(fake_blockid_for_num(i), fake_blockid_for_num(i-1), [&](bio::filtering_ostreambuf& obuf) {
|
|
bio::filtering_istreambuf hashed_randomness(sha256_filter() | bio::restrict(random_source(), 0, mt_random()%16*1024*1024));
|
|
bio::copy(hashed_randomness, obuf);
|
|
wrote_data_for_blocknum[i] = hashed_randomness.component<sha256_filter>(0)->enc->result();
|
|
});
|
|
|
|
BOOST_REQUIRE_EQUAL(lc.block_range().first, start);
|
|
BOOST_REQUIRE_EQUAL(lc.block_range().second, end);
|
|
|
|
//pick some random blocks and read their content back; make sure it matches.
|
|
for(unsigned i = start; i < end; i+=mt_random()%10) {
|
|
std::optional<state_history::ship_log_entry> entry = lc.get_entry(i);
|
|
BOOST_REQUIRE(!!entry);
|
|
|
|
std::optional<chain::block_id_type> bid = lc.get_block_id(i);
|
|
BOOST_REQUIRE(!!bid);
|
|
BOOST_REQUIRE_EQUAL(*bid, fake_blockid_for_num(i));
|
|
|
|
bio::filtering_ostreambuf hashed_null(sha256_filter() | bio::null_sink());
|
|
bio::filtering_istreambuf log_stream = entry->get_stream();
|
|
bio::copy(log_stream, hashed_null);
|
|
BOOST_REQUIRE_EQUAL(hashed_null.component<sha256_filter>(0)->enc->result(), wrote_data_for_blocknum[i]);
|
|
}
|
|
|
|
//pick some blocks outside the range of blocks we wrote and make sure we cannot read them
|
|
for(const unsigned i : {1u, start-34, start-1, end, end+56, end+1004}) { //start-34 might roll over; no big deal
|
|
std::optional<chain::block_id_type> bid = lc.get_block_id(i);
|
|
std::optional<state_history::ship_log_entry> entry = lc.get_entry(i);
|
|
BOOST_REQUIRE(!bid);
|
|
BOOST_REQUIRE(!entry);
|
|
}
|
|
|
|
//"end" would be the next block to be appended
|
|
//attempt to "skip" a block
|
|
{
|
|
unsigned skipped_block = end + 1;
|
|
BOOST_REQUIRE_EXCEPTION(lc.pack_and_write_entry(fake_blockid_for_num(skipped_block), fake_blockid_for_num(skipped_block-1), [&](bio::filtering_ostreambuf& obuf) {
|
|
FC_ASSERT(false, "should not reach here");
|
|
}),
|
|
plugin_exception,
|
|
[](const plugin_exception& e) {return e.to_detail_string().find("skips over block") != std::string::npos;});
|
|
}
|
|
|
|
//now let's try appending a block that doesn't have the right previous block id
|
|
{
|
|
BOOST_REQUIRE_EXCEPTION(lc.pack_and_write_entry(fake_blockid_for_num(end), fake_blockid_for_num(end-1, 0xbeefUL), [&](bio::filtering_ostreambuf& obuf) {
|
|
FC_ASSERT(false, "should not reach here");
|
|
}),
|
|
plugin_exception,
|
|
[](const plugin_exception& e) {return e.to_detail_string().find("missed a fork change") != std::string::npos;});
|
|
}
|
|
|
|
//now we're going to try writing identical blockids to the log. These should be silently swallowed as no-ops
|
|
for(unsigned i : {start, start+6, end-5, end-1}) {
|
|
//but block 2 is special. Writing block 2 on a non empty log will fail if the blockid is different (instead of treated it like a fork), but a
|
|
// no-op otherwise. So try a different blockid here to test that
|
|
if(i == 2u)
|
|
//different blockid
|
|
BOOST_REQUIRE_EXCEPTION(lc.pack_and_write_entry(fake_blockid_for_num(i, 0xbeef), fake_blockid_for_num(i-1), [&](bio::filtering_ostreambuf& obuf) {
|
|
FC_ASSERT(false, "should not reach here");
|
|
}),
|
|
plugin_exception,
|
|
[](const plugin_exception& e) {return e.to_detail_string().find("when starting from genesis block 2") != std::string::npos;});
|
|
|
|
lc.pack_and_write_entry(fake_blockid_for_num(i), fake_blockid_for_num(i-1), [&](bio::filtering_ostreambuf& obuf) {
|
|
FC_ASSERT(false, "should not reach here");
|
|
});
|
|
}
|
|
|
|
BOOST_REQUIRE_EQUAL(lc.block_range().first, start);
|
|
BOOST_REQUIRE_EQUAL(lc.block_range().second, end);
|
|
|
|
//time for a "fork": we're going to rewrite the last 4 blocks and add 2 new ones as well. But we're going to ensure that old data remains intact during this
|
|
//"overwrite" (as long as the ship_log_entry is alive)
|
|
std::array<std::pair<std::optional<state_history::ship_log_entry>, sha256>, 4> pre_fork_entries_and_expected_hashes = {
|
|
std::make_pair(lc.get_entry(end-1), wrote_data_for_blocknum[end-1]),
|
|
std::make_pair(lc.get_entry(end-3), wrote_data_for_blocknum[end-3]), //out of order for fun
|
|
std::make_pair(lc.get_entry(end-2), wrote_data_for_blocknum[end-2]),
|
|
std::make_pair(lc.get_entry(end-4), wrote_data_for_blocknum[end-4]),
|
|
};
|
|
|
|
lc.pack_and_write_entry(fake_blockid_for_num(end-4, 0xdeadUL), fake_blockid_for_num(end-4-1), [&](bio::filtering_ostreambuf& obuf) {
|
|
bio::filtering_istreambuf hashed_randomness(sha256_filter() | bio::restrict(random_source(), 0, mt_random()%16*1024*1024));
|
|
bio::copy(hashed_randomness, obuf);
|
|
wrote_data_for_blocknum[end-4] = hashed_randomness.component<sha256_filter>(0)->enc->result();
|
|
});
|
|
for(const unsigned i : {end-3, end-2, end-1, end, end+1})
|
|
lc.pack_and_write_entry(fake_blockid_for_num(i, 0xdeadUL), fake_blockid_for_num(i-1, 0xdeadUL), [&](bio::filtering_ostreambuf& obuf) {
|
|
bio::filtering_istreambuf hashed_randomness(sha256_filter() | bio::restrict(random_source(), 0, mt_random()%16*1024*1024));
|
|
bio::copy(hashed_randomness, obuf);
|
|
wrote_data_for_blocknum[i] = hashed_randomness.component<sha256_filter>(0)->enc->result();
|
|
});
|
|
|
|
//first, check that the pre-fork entries still read their pre-fork data
|
|
///XXX can we const this please?
|
|
for(std::pair<std::optional<state_history::ship_log_entry>, sha256>& prefork_entry : pre_fork_entries_and_expected_hashes) {
|
|
BOOST_REQUIRE(!!prefork_entry.first);
|
|
bio::filtering_ostreambuf hashed_null(sha256_filter() | bio::null_sink());
|
|
bio::filtering_istreambuf log_stream = prefork_entry.first->get_stream();
|
|
bio::copy(log_stream, hashed_null);
|
|
BOOST_REQUIRE_EQUAL(hashed_null.component<sha256_filter>(0)->enc->result(), prefork_entry.second);
|
|
}
|
|
//now let's check all of the just added blocks; and a couple earlier ones
|
|
for(unsigned i : {end-6, end-5, /*"new fork" blocks:*/end-4, end-3, end-2, end-1, end, end+1}) {
|
|
std::optional<state_history::ship_log_entry> entry = lc.get_entry(i);
|
|
BOOST_REQUIRE(!!entry);
|
|
|
|
bio::filtering_ostreambuf hashed_null(sha256_filter() | bio::null_sink());
|
|
bio::filtering_istreambuf log_stream = entry->get_stream();
|
|
bio::copy(log_stream, hashed_null);
|
|
BOOST_REQUIRE_EQUAL(hashed_null.component<sha256_filter>(0)->enc->result(), wrote_data_for_blocknum[i]);
|
|
}
|
|
|
|
BOOST_REQUIRE_EQUAL(lc.block_range().first, start);
|
|
BOOST_REQUIRE_EQUAL(lc.block_range().second, end+2);
|
|
|
|
} FC_LOG_AND_RETHROW();
|
|
|
|
|
|
BOOST_AUTO_TEST_CASE(regen_index) try {
|
|
const fc::temp_directory tmpdir;
|
|
|
|
//try recreating the index for an empty log
|
|
{
|
|
eosio::state_history::log_catalog lc(tmpdir.path(), std::monostate(), "empty");
|
|
BOOST_REQUIRE(lc.empty());
|
|
}
|
|
BOOST_REQUIRE(std::filesystem::exists(tmpdir.path() / "empty.index"));
|
|
std::filesystem::remove(tmpdir.path() / "empty.index");
|
|
BOOST_REQUIRE(!std::filesystem::exists(tmpdir.path() / "empty.index"));
|
|
{
|
|
eosio::state_history::log_catalog lc(tmpdir.path(), std::monostate(), "empty");
|
|
BOOST_REQUIRE(lc.empty());
|
|
}
|
|
|
|
//fill up a log with a handful of blocks
|
|
{
|
|
eosio::state_history::log_catalog lc(tmpdir.path(), std::monostate(), "newlog");
|
|
BOOST_REQUIRE(lc.empty());
|
|
|
|
for(unsigned i = 2; i < 34; ++i)
|
|
lc.pack_and_write_entry(fake_blockid_for_num(i), fake_blockid_for_num(i-1), [&](bio::filtering_ostreambuf& obuf) {
|
|
{
|
|
fc::datastream<bio::filtering_ostreambuf&> ds(obuf);
|
|
fc::raw::pack(ds, i);
|
|
}
|
|
bio::copy(bio::restrict(random_source(), 0, 77777), obuf);
|
|
});
|
|
|
|
BOOST_REQUIRE_EQUAL(lc.block_range().first, 2u);
|
|
BOOST_REQUIRE_EQUAL(lc.block_range().second, 34u);
|
|
}
|
|
BOOST_REQUIRE(std::filesystem::exists(tmpdir.path() / "newlog.index"));
|
|
const uintmax_t prev_index_size = std::filesystem::file_size(tmpdir.path() / "newlog.index");
|
|
std::string old_index_contents;
|
|
read_file_contents(tmpdir.path() / "newlog.index", old_index_contents);
|
|
BOOST_REQUIRE_EQUAL(prev_index_size, old_index_contents.size());
|
|
|
|
//now remove the index and make sure the recreated index works
|
|
std::filesystem::remove(tmpdir.path() / "newlog.index");
|
|
BOOST_REQUIRE(!std::filesystem::exists(tmpdir.path() / "newlog.index"));
|
|
{
|
|
eosio::state_history::log_catalog lc(tmpdir.path(), std::monostate(), "newlog");
|
|
BOOST_REQUIRE_EQUAL(lc.block_range().first, 2u);
|
|
BOOST_REQUIRE_EQUAL(lc.block_range().second, 34u);
|
|
|
|
//read some blocks back
|
|
for(const unsigned i : {2u, 10u, 22u, 33u}) {
|
|
std::optional<state_history::ship_log_entry> entry = lc.get_entry(i);
|
|
BOOST_REQUIRE(!!entry);
|
|
bio::filtering_istreambuf log_stream = entry->get_stream();
|
|
fc::datastream<bio::filtering_istreambuf&> ds(log_stream);
|
|
unsigned red;
|
|
fc::raw::unpack(ds, red);
|
|
BOOST_REQUIRE_EQUAL(red, i);
|
|
}
|
|
}
|
|
|
|
//also compare the index contents; should be exactly the same
|
|
std::string new_index_contents;
|
|
read_file_contents(tmpdir.path() / "newlog.index", new_index_contents);
|
|
BOOST_REQUIRE_EQUAL(new_index_contents.size(), old_index_contents.size());
|
|
BOOST_REQUIRE_EQUAL(new_index_contents, old_index_contents);
|
|
} FC_LOG_AND_RETHROW();
|
|
|
|
BOOST_AUTO_TEST_CASE(empty_empty_empty) try {
|
|
//just opens and closes an empty log a few times
|
|
const fc::temp_directory tmpdir;
|
|
|
|
for(unsigned i = 0; i < 4; ++i) {
|
|
eosio::state_history::log_catalog lc(tmpdir.path(), std::monostate(), "empty");
|
|
BOOST_REQUIRE(lc.empty());
|
|
}
|
|
BOOST_REQUIRE(std::filesystem::exists(tmpdir.path() / "empty.log"));
|
|
BOOST_REQUIRE(std::filesystem::exists(tmpdir.path() / "empty.index"));
|
|
BOOST_REQUIRE_EQUAL(std::filesystem::file_size(tmpdir.path() / "empty.log"), 0u);
|
|
BOOST_REQUIRE_EQUAL(std::filesystem::file_size(tmpdir.path() / "empty.index"), 0u);
|
|
} FC_LOG_AND_RETHROW();
|
|
|
|
BOOST_DATA_TEST_CASE(basic_split, boost::unit_test::data::make({5u, 6u, 7u, 8u, 9u, 10u, 578'000u, 3'123'456'789u}) *
|
|
boost::unit_test::data::make({5u, 10u}) *
|
|
boost::unit_test::data::make({"保留", ""})
|
|
, start, stride, retained_dir) try {
|
|
const fc::temp_directory tmpdir;
|
|
|
|
state_history::partition_config conf = {
|
|
.retained_dir = retained_dir,
|
|
.archive_dir = "档案",
|
|
.stride = stride,
|
|
.max_retained_files = UINT32_MAX
|
|
};
|
|
|
|
const unsigned initial_blocks_to_append = 50;
|
|
unsigned end = start+initial_blocks_to_append+1;
|
|
std::map<block_num_type, sha256> wrote_data_for_blocknum;
|
|
|
|
{
|
|
eosio::state_history::log_catalog lc(tmpdir.path(), conf, "splitit");
|
|
BOOST_REQUIRE(lc.empty());
|
|
|
|
std::mt19937 mt_random(0xbeefbeefu * start);
|
|
|
|
for(unsigned i = start; i < end; ++i)
|
|
lc.pack_and_write_entry(fake_blockid_for_num(i), fake_blockid_for_num(i-1), [&](bio::filtering_ostreambuf& obuf) {
|
|
bio::filtering_istreambuf hashed_randomness(sha256_filter() | bio::restrict(random_source(), 0, mt_random()%1024*1024));
|
|
bio::copy(hashed_randomness, obuf);
|
|
wrote_data_for_blocknum[i] = hashed_randomness.component<sha256_filter>(0)->enc->result();
|
|
});
|
|
|
|
BOOST_REQUIRE_EQUAL(lc.block_range().first, start);
|
|
BOOST_REQUIRE_EQUAL(lc.block_range().second, end);
|
|
}
|
|
|
|
const unsigned expected_log_parts = initial_blocks_to_append/stride + (start%stride == 0);
|
|
|
|
for(const std::string& suffix : {"log"s, "index"s}) {
|
|
const std::regex retained_logfile_regex(R"(^splitit-\d+-\d+\.)" + suffix + "$");
|
|
|
|
unsigned found = 0;
|
|
for(const std::filesystem::directory_entry& dir_entry : std::filesystem::directory_iterator(tmpdir.path() / conf.retained_dir))
|
|
found += std::regex_search(dir_entry.path().filename().string(), retained_logfile_regex);
|
|
BOOST_REQUIRE_EQUAL(found, expected_log_parts);
|
|
BOOST_REQUIRE(std::filesystem::exists(tmpdir.path() / ("splitit."+suffix)));
|
|
}
|
|
|
|
//load the catalog back up and read through all the blocks
|
|
{
|
|
eosio::state_history::log_catalog lc(tmpdir.path(), conf, "splitit");
|
|
BOOST_REQUIRE_EQUAL(lc.block_range().first, start);
|
|
BOOST_REQUIRE_EQUAL(lc.block_range().second, end);
|
|
|
|
for(unsigned i = start; i < end; i++) {
|
|
std::optional<state_history::ship_log_entry> entry = lc.get_entry(i);
|
|
BOOST_REQUIRE(!!entry);
|
|
|
|
std::optional<chain::block_id_type> bid = lc.get_block_id(i);
|
|
BOOST_REQUIRE(!!bid);
|
|
BOOST_REQUIRE_EQUAL(*bid, fake_blockid_for_num(i));
|
|
|
|
bio::filtering_ostreambuf hashed_null(sha256_filter() | bio::null_sink());
|
|
bio::filtering_istreambuf log_stream = entry->get_stream();
|
|
bio::copy(log_stream, hashed_null);
|
|
BOOST_REQUIRE_EQUAL(hashed_null.component<sha256_filter>(0)->enc->result(), wrote_data_for_blocknum[i]);
|
|
}
|
|
}
|
|
|
|
//find a log & index file and copy it to a name that does not match expected; it should be silently ignored
|
|
{
|
|
const std::regex regex(R"(^splitit-\d+-\d+\.log$)");
|
|
|
|
for(const std::filesystem::directory_entry& dir_entry : std::filesystem::directory_iterator(tmpdir.path() / conf.retained_dir))
|
|
if(std::regex_search(dir_entry.path().filename().string(), regex)) {
|
|
std::filesystem::copy_file(dir_entry.path(), std::filesystem::path(dir_entry.path()).replace_filename("yeeeeehaw-1234.log"));
|
|
std::filesystem::copy_file(std::filesystem::path(dir_entry.path()).replace_extension("index"), std::filesystem::path(dir_entry.path()).replace_filename("yeeeeehaw-1234.index"));
|
|
break;
|
|
}
|
|
}
|
|
{
|
|
eosio::state_history::log_catalog lc(tmpdir.path(), conf, "splitit");
|
|
BOOST_REQUIRE_EQUAL(lc.block_range().first, start);
|
|
BOOST_REQUIRE_EQUAL(lc.block_range().second, end);
|
|
}
|
|
|
|
//delete every other .index file. indexes will be recreated for all log parts on construction of the ship_log_catalog
|
|
{
|
|
const std::regex regex(R"(^splitit-\d+-\d+\.index)");
|
|
|
|
bool do_this_one = false;
|
|
for(const std::filesystem::directory_entry& dir_entry : std::filesystem::directory_iterator(tmpdir.path() / conf.retained_dir))
|
|
if(std::regex_search(dir_entry.path().filename().string(), regex)) {
|
|
if(do_this_one)
|
|
std::filesystem::remove(dir_entry.path());
|
|
do_this_one = !do_this_one;
|
|
}
|
|
}
|
|
//and we'll go through the process of reading all blocks after the indexes have been recreated
|
|
{
|
|
eosio::state_history::log_catalog lc(tmpdir.path(), conf, "splitit");
|
|
BOOST_REQUIRE_EQUAL(lc.block_range().first, start);
|
|
BOOST_REQUIRE_EQUAL(lc.block_range().second, end);
|
|
|
|
for(unsigned i = start; i < end; i++) {
|
|
std::optional<state_history::ship_log_entry> entry = lc.get_entry(i);
|
|
BOOST_REQUIRE(!!entry);
|
|
|
|
std::optional<chain::block_id_type> bid = lc.get_block_id(i);
|
|
BOOST_REQUIRE(!!bid);
|
|
BOOST_REQUIRE_EQUAL(*bid, fake_blockid_for_num(i));
|
|
|
|
bio::filtering_ostreambuf hashed_null(sha256_filter() | bio::null_sink());
|
|
bio::filtering_istreambuf log_stream = entry->get_stream();
|
|
bio::copy(log_stream, hashed_null);
|
|
BOOST_REQUIRE_EQUAL(hashed_null.component<sha256_filter>(0)->enc->result(), wrote_data_for_blocknum[i]);
|
|
}
|
|
}
|
|
|
|
//now switch over to no splitting. this is allowed but old split logs will not be "visible" when configured this way
|
|
{
|
|
eosio::state_history::log_catalog lc(tmpdir.path(), std::monostate(), "splitit");
|
|
if(start % conf.stride == 0) { //"head log" will be empty in this case
|
|
BOOST_REQUIRE(lc.empty());
|
|
}
|
|
else {
|
|
BOOST_REQUIRE(lc.block_range().first % conf.stride == 1);
|
|
BOOST_REQUIRE_EQUAL(lc.block_range().second, end);
|
|
}
|
|
|
|
//let's go create another 100 blocks too!
|
|
std::mt19937 mt_random(0xbeefbeefu * end);
|
|
const unsigned new_end = end + 100;
|
|
|
|
for(unsigned i = end; i < new_end; ++i)
|
|
lc.pack_and_write_entry(fake_blockid_for_num(i), fake_blockid_for_num(i-1), [&](bio::filtering_ostreambuf& obuf) {
|
|
bio::filtering_istreambuf hashed_randomness(sha256_filter() | bio::restrict(random_source(), 0, mt_random()%1024*1024));
|
|
bio::copy(hashed_randomness, obuf);
|
|
wrote_data_for_blocknum[i] = hashed_randomness.component<sha256_filter>(0)->enc->result();
|
|
});
|
|
|
|
end = new_end;
|
|
BOOST_REQUIRE_EQUAL(lc.block_range().second, end);
|
|
}
|
|
|
|
//and back to split log mode. all those retained logs will be visible again
|
|
{
|
|
eosio::state_history::log_catalog lc(tmpdir.path(), conf, "splitit");
|
|
BOOST_REQUIRE_EQUAL(lc.block_range().first, start);
|
|
BOOST_REQUIRE_EQUAL(lc.block_range().second, end);
|
|
|
|
//but now let's add enough blocks to trigger a rotation again. This will give us a retained log that is a different span
|
|
// size than all the previous spans
|
|
std::mt19937 mt_random(0xbeefbeefu * end);
|
|
for(unsigned i = end; i < end+conf.stride; ++i)
|
|
lc.pack_and_write_entry(fake_blockid_for_num(i), fake_blockid_for_num(i-1), [&](bio::filtering_ostreambuf& obuf) {
|
|
bio::filtering_istreambuf hashed_randomness(sha256_filter() | bio::restrict(random_source(), 0, mt_random()%1024*1024));
|
|
bio::copy(hashed_randomness, obuf);
|
|
wrote_data_for_blocknum[i] = hashed_randomness.component<sha256_filter>(0)->enc->result();
|
|
});
|
|
end += conf.stride;
|
|
BOOST_REQUIRE_EQUAL(lc.block_range().first, start);
|
|
BOOST_REQUIRE_EQUAL(lc.block_range().second, end);
|
|
}
|
|
|
|
//one more time where we read through everything
|
|
{
|
|
eosio::state_history::log_catalog lc(tmpdir.path(), conf, "splitit");
|
|
BOOST_REQUIRE_EQUAL(lc.block_range().first, start);
|
|
BOOST_REQUIRE_EQUAL(lc.block_range().second, end);
|
|
|
|
for(unsigned i = start; i < end; i++) {
|
|
std::optional<state_history::ship_log_entry> entry = lc.get_entry(i);
|
|
BOOST_REQUIRE(!!entry);
|
|
|
|
std::optional<chain::block_id_type> bid = lc.get_block_id(i);
|
|
BOOST_REQUIRE(!!bid);
|
|
BOOST_REQUIRE_EQUAL(*bid, fake_blockid_for_num(i));
|
|
|
|
bio::filtering_ostreambuf hashed_null(sha256_filter() | bio::null_sink());
|
|
bio::filtering_istreambuf log_stream = entry->get_stream();
|
|
bio::copy(log_stream, hashed_null);
|
|
BOOST_REQUIRE_EQUAL(hashed_null.component<sha256_filter>(0)->enc->result(), wrote_data_for_blocknum[i]);
|
|
}
|
|
}
|
|
|
|
//set the number of retained logs to 4
|
|
conf.max_retained_files = 4u;
|
|
//and go generate enough blocks to cause a rotation which will move old logs to the archive directory
|
|
{
|
|
eosio::state_history::log_catalog lc(tmpdir.path(), conf, "splitit");
|
|
BOOST_REQUIRE_EQUAL(lc.block_range().first, start);
|
|
BOOST_REQUIRE_EQUAL(lc.block_range().second, end);
|
|
|
|
//but now let's add enough blocks to trigger a rotation again. This will give us a retained log that is a different span
|
|
// size than all the previous spans
|
|
std::mt19937 mt_random(0xbeefbeefu * end);
|
|
for(unsigned i = end; i < end+conf.stride; ++i)
|
|
lc.pack_and_write_entry(fake_blockid_for_num(i), fake_blockid_for_num(i-1), [&](bio::filtering_ostreambuf& obuf) {
|
|
bio::filtering_istreambuf hashed_randomness(sha256_filter() | bio::restrict(random_source(), 0, mt_random()%1024*1024));
|
|
bio::copy(hashed_randomness, obuf);
|
|
wrote_data_for_blocknum[i] = hashed_randomness.component<sha256_filter>(0)->enc->result();
|
|
});
|
|
end += conf.stride;
|
|
|
|
BOOST_REQUIRE_NE(lc.block_range().first, 2u);
|
|
BOOST_REQUIRE_EQUAL(lc.block_range().second, end);
|
|
}
|
|
|
|
//make sure we have only 4 retained logs in the retained dir; and note how many in archived dir
|
|
std::unordered_map<std::string, unsigned> last_archive_dir_count;
|
|
{
|
|
for(const std::string& suffix : {"log"s, "index"s}) {
|
|
const std::regex retained_logfile_regex(R"(^splitit-\d+-\d+\.)" + suffix + "$");
|
|
|
|
const unsigned found = std::ranges::count_if(std::filesystem::directory_iterator(tmpdir.path() / conf.retained_dir), [&](const std::filesystem::directory_entry& dir_entry) {
|
|
return std::regex_search(dir_entry.path().filename().string(), retained_logfile_regex);
|
|
});
|
|
BOOST_REQUIRE_EQUAL(found, 4u);
|
|
}
|
|
for(const std::string& suffix : {"log"s, "index"s}) {
|
|
const std::regex retained_logfile_regex(R"(^splitit-\d+-\d+\.)" + suffix + "$");
|
|
|
|
last_archive_dir_count[suffix] = std::ranges::count_if(std::filesystem::directory_iterator(tmpdir.path() / conf.archive_dir), [&](const std::filesystem::directory_entry& dir_entry) {
|
|
return std::regex_search(dir_entry.path().filename().string(), retained_logfile_regex);
|
|
});
|
|
BOOST_REQUIRE_NE(last_archive_dir_count[suffix], 0u);
|
|
}
|
|
BOOST_REQUIRE_EQUAL(last_archive_dir_count["log"], last_archive_dir_count["index"]);
|
|
}
|
|
|
|
//clear the archive directory. This will cause logs to be removed; reduce max_retain to 3 to make it easier to spot proper behavior
|
|
const std::filesystem::path previous_archive_dir = conf.archive_dir;
|
|
conf.archive_dir.clear();
|
|
conf.max_retained_files = 3u;
|
|
//generate enough blocks for a rotation...
|
|
{
|
|
eosio::state_history::log_catalog lc(tmpdir.path(), conf, "splitit");
|
|
BOOST_REQUIRE_EQUAL(lc.block_range().second, end);
|
|
|
|
std::mt19937 mt_random(0xbeefbeefu * end);
|
|
for(unsigned i = end; i < end+conf.stride; ++i)
|
|
lc.pack_and_write_entry(fake_blockid_for_num(i), fake_blockid_for_num(i-1), [&](bio::filtering_ostreambuf& obuf) {
|
|
bio::filtering_istreambuf hashed_randomness(sha256_filter() | bio::restrict(random_source(), 0, mt_random()%1024*1024));
|
|
bio::copy(hashed_randomness, obuf);
|
|
wrote_data_for_blocknum[i] = hashed_randomness.component<sha256_filter>(0)->enc->result();
|
|
});
|
|
end += conf.stride;
|
|
|
|
BOOST_REQUIRE_EQUAL(lc.block_range().second, end);
|
|
}
|
|
|
|
//check filesystem..
|
|
{
|
|
//should only find 3 logs in retain dir
|
|
for(const std::string& suffix : {"log"s, "index"s}) {
|
|
const std::regex retained_logfile_regex(R"(^splitit-\d+-\d+\.)" + suffix + "$");
|
|
|
|
const unsigned found = std::ranges::count_if(std::filesystem::directory_iterator(tmpdir.path() / conf.retained_dir), [&](const std::filesystem::directory_entry& dir_entry) {
|
|
return std::regex_search(dir_entry.path().filename().string(), retained_logfile_regex);
|
|
});
|
|
BOOST_REQUIRE_EQUAL(found, 3u);
|
|
}
|
|
//archive dir should still have same number of files
|
|
for(const std::string& suffix : {"log"s, "index"s}) {
|
|
const std::regex retained_logfile_regex(R"(^splitit-\d+-\d+\.)" + suffix + "$");
|
|
|
|
const unsigned found = std::ranges::count_if(std::filesystem::directory_iterator(tmpdir.path() / previous_archive_dir), [&](const std::filesystem::directory_entry& dir_entry) {
|
|
return std::regex_search(dir_entry.path().filename().string(), retained_logfile_regex);
|
|
});
|
|
BOOST_REQUIRE_EQUAL(found, last_archive_dir_count[suffix]);
|
|
}
|
|
}
|
|
|
|
//one more pass through all the blocks
|
|
{
|
|
eosio::state_history::log_catalog lc(tmpdir.path(), conf, "splitit");
|
|
BOOST_REQUIRE_EQUAL(lc.block_range().second, end);
|
|
|
|
for(unsigned i = lc.block_range().first; i < end; i++) {
|
|
std::optional<state_history::ship_log_entry> entry = lc.get_entry(i);
|
|
BOOST_REQUIRE(!!entry);
|
|
|
|
std::optional<chain::block_id_type> bid = lc.get_block_id(i);
|
|
BOOST_REQUIRE(!!bid);
|
|
BOOST_REQUIRE_EQUAL(*bid, fake_blockid_for_num(i));
|
|
|
|
bio::filtering_ostreambuf hashed_null(sha256_filter() | bio::null_sink());
|
|
bio::filtering_istreambuf log_stream = entry->get_stream();
|
|
bio::copy(log_stream, hashed_null);
|
|
BOOST_REQUIRE_EQUAL(hashed_null.component<sha256_filter>(0)->enc->result(), wrote_data_for_blocknum[i]);
|
|
}
|
|
}
|
|
|
|
//remove one of the reained logs causing a "hole" which is disallowed. to do this reliably and for full coverage, we'll first delete the
|
|
// second newest retained log, and then delete the newest retained log
|
|
std::map<unsigned, std::filesystem::path> found;
|
|
{
|
|
const std::regex retained_logfile_regex(R"(^splitit-\d+-\d+\.log$)");
|
|
|
|
for(const std::filesystem::directory_entry& dir_entry : std::filesystem::directory_iterator(tmpdir.path() / conf.retained_dir)) {
|
|
if(!std::regex_search(dir_entry.path().filename().string(), retained_logfile_regex))
|
|
continue;
|
|
found[state_history::state_history_log(std::filesystem::path(dir_entry.path()).replace_extension("")).block_range().second] = dir_entry.path();
|
|
}
|
|
BOOST_REQUIRE_GT(found.size(), 1u);
|
|
}
|
|
std::filesystem::remove(std::next(found.rbegin())->second);
|
|
BOOST_REQUIRE_EXCEPTION(eosio::state_history::log_catalog(tmpdir.path(), conf, "splitit"),
|
|
plugin_exception,
|
|
[](const plugin_exception& e) {return e.to_detail_string().find("which results in a hole") != std::string::npos;});
|
|
std::filesystem::remove(found.rbegin()->second);
|
|
|
|
//only perform this check if we expect the "head log" to be non-empty
|
|
if(start % conf.stride)
|
|
BOOST_REQUIRE_EXCEPTION(eosio::state_history::log_catalog(tmpdir.path(), conf, "splitit"),
|
|
plugin_exception,
|
|
[](const plugin_exception& e) {return e.to_detail_string().find("which results in a hole") != std::string::npos;});
|
|
//unfortuately if the "head log" _is_ empty we're in quite a problem since we won't be able to detect the hole until a block is appended
|
|
//TODO: *is* the above checked?
|
|
} FC_LOG_AND_RETHROW();
|
|
|
|
BOOST_DATA_TEST_CASE(split_forks, bdata::xrange(1u, 6u), fork_size) try {
|
|
const fc::temp_directory tmpdir;
|
|
|
|
state_history::partition_config conf = {
|
|
.retained_dir = "retained",
|
|
.archive_dir = {},
|
|
.stride = 10,
|
|
.max_retained_files = UINT32_MAX
|
|
};
|
|
|
|
//fill up 50 blocks
|
|
const unsigned start = 2;
|
|
const unsigned end = 53;
|
|
std::map<block_num_type, sha256> wrote_data_for_blocknum;
|
|
std::mt19937 mt_random(0xbeefbeefu * start);
|
|
|
|
{
|
|
eosio::state_history::log_catalog lc(tmpdir.path(), conf, "logz");
|
|
BOOST_REQUIRE(lc.empty());
|
|
|
|
for(unsigned i = start; i < end; ++i)
|
|
lc.pack_and_write_entry(fake_blockid_for_num(i), fake_blockid_for_num(i-1), [&](bio::filtering_ostreambuf& obuf) {
|
|
bio::filtering_istreambuf hashed_randomness(sha256_filter() | bio::restrict(random_source(), 0, mt_random()%1024*1024));
|
|
bio::copy(hashed_randomness, obuf);
|
|
wrote_data_for_blocknum[i] = hashed_randomness.component<sha256_filter>(0)->enc->result();
|
|
});
|
|
|
|
BOOST_REQUIRE_EQUAL(lc.block_range().first, start);
|
|
BOOST_REQUIRE_EQUAL(lc.block_range().second, end);
|
|
}
|
|
|
|
//discover the filenames for:
|
|
// head
|
|
// 41-50
|
|
// 31-40
|
|
const std::filesystem::path head_log_path = tmpdir.path() / "logz";
|
|
std::filesystem::path path_31to40;
|
|
std::filesystem::path path_41to50;
|
|
{
|
|
const std::regex retained_logfile_regex(R"(^logz-\d+-\d+\.log$)");
|
|
|
|
for(const std::filesystem::directory_entry& dir_entry : std::filesystem::directory_iterator(tmpdir.path() / conf.retained_dir)) {
|
|
if(!std::regex_search(dir_entry.path().filename().string(), retained_logfile_regex))
|
|
continue;
|
|
const std::filesystem::path path_no_ext = std::filesystem::path(dir_entry.path()).replace_extension("");
|
|
const unsigned start_block = state_history::state_history_log(path_no_ext).block_range().first;
|
|
if(start_block == 31)
|
|
path_31to40 = dir_entry.path();
|
|
else if(start_block == 41)
|
|
path_41to50 = dir_entry.path();
|
|
}
|
|
BOOST_REQUIRE(!path_31to40.empty() && !path_41to50.empty());
|
|
}
|
|
|
|
const size_t before_head_log_size = std::filesystem::file_size(std::filesystem::path(head_log_path).replace_extension("log"));
|
|
const size_t before_head_index_size = std::filesystem::file_size(std::filesystem::path(head_log_path).replace_extension("index"));
|
|
const size_t before_31to40_log_size = std::filesystem::file_size(std::filesystem::path(path_31to40).replace_extension("log"));
|
|
const size_t before_31to40_index_size = std::filesystem::file_size(std::filesystem::path(path_31to40).replace_extension("index"));
|
|
const size_t before_41to50_log_size = std::filesystem::file_size(std::filesystem::path(path_41to50).replace_extension("log"));
|
|
const size_t before_41to50_index_size = std::filesystem::file_size(std::filesystem::path(path_41to50).replace_extension("index"));
|
|
|
|
const unsigned start_fork_at = end-fork_size;
|
|
{
|
|
eosio::state_history::log_catalog lc(tmpdir.path(), conf, "logz");
|
|
BOOST_REQUIRE_EQUAL(lc.block_range().first, start);
|
|
BOOST_REQUIRE_EQUAL(lc.block_range().second, end);
|
|
|
|
lc.pack_and_write_entry(fake_blockid_for_num(start_fork_at, 0xdeadUL), fake_blockid_for_num(start_fork_at-1), [&](bio::filtering_ostreambuf& obuf) {
|
|
bio::filtering_istreambuf hashed_randomness(sha256_filter() | bio::restrict(random_source(), 0, mt_random()%1024*1024));
|
|
bio::copy(hashed_randomness, obuf);
|
|
wrote_data_for_blocknum[start_fork_at] = hashed_randomness.component<sha256_filter>(0)->enc->result();
|
|
});
|
|
}
|
|
|
|
if(fork_size == 1) {
|
|
//in this case we're just overwriting the last block
|
|
//all indexes should remain the same size
|
|
BOOST_REQUIRE_EQUAL(before_head_index_size, std::filesystem::file_size(std::filesystem::path(head_log_path).replace_extension("index")));
|
|
BOOST_REQUIRE_EQUAL(before_31to40_index_size, std::filesystem::file_size(std::filesystem::path(path_31to40).replace_extension("index")));
|
|
BOOST_REQUIRE_EQUAL(before_41to50_index_size, std::filesystem::file_size(std::filesystem::path(path_41to50).replace_extension("index")));
|
|
//all logs should remain the same size, except the head log which will have grown
|
|
BOOST_REQUIRE_EQUAL(before_31to40_log_size, std::filesystem::file_size(std::filesystem::path(path_31to40).replace_extension("log")));
|
|
BOOST_REQUIRE_EQUAL(before_41to50_log_size, std::filesystem::file_size(std::filesystem::path(path_41to50).replace_extension("log")));
|
|
BOOST_REQUIRE_LT(before_head_log_size, std::filesystem::file_size(std::filesystem::path(head_log_path).replace_extension("log")));
|
|
}
|
|
else if(start_fork_at >= 51) {
|
|
//in this case only the head log will have been modified
|
|
//retained indexes will remain the same size
|
|
BOOST_REQUIRE_EQUAL(before_31to40_index_size, std::filesystem::file_size(std::filesystem::path(path_31to40).replace_extension("index")));
|
|
BOOST_REQUIRE_EQUAL(before_41to50_index_size, std::filesystem::file_size(std::filesystem::path(path_41to50).replace_extension("index")));
|
|
//head index will be smaller
|
|
BOOST_REQUIRE_GT(before_head_index_size, std::filesystem::file_size(std::filesystem::path(head_log_path).replace_extension("index")));
|
|
//retained logs will remain the same size, except head block which will have grown
|
|
BOOST_REQUIRE_EQUAL(before_31to40_log_size, std::filesystem::file_size(std::filesystem::path(path_31to40).replace_extension("log")));
|
|
BOOST_REQUIRE_EQUAL(before_41to50_log_size, std::filesystem::file_size(std::filesystem::path(path_41to50).replace_extension("log")));
|
|
BOOST_REQUIRE_LT(before_head_log_size, std::filesystem::file_size(std::filesystem::path(head_log_path).replace_extension("log")));
|
|
}
|
|
else {
|
|
//in this case we will have "unrotated" a retained log
|
|
//check that 31-40 log and index remains unchanged
|
|
BOOST_REQUIRE_EQUAL(before_31to40_log_size, std::filesystem::file_size(std::filesystem::path(path_31to40).replace_extension("log")));
|
|
BOOST_REQUIRE_EQUAL(before_31to40_index_size, std::filesystem::file_size(std::filesystem::path(path_31to40).replace_extension("index")));
|
|
if(start_fork_at == 50) {
|
|
//if the fork was at 50, we actually both unrotated and then rotated
|
|
//so check that index size for 41-50 is the same, and that its log is larger
|
|
BOOST_REQUIRE_LT(before_41to50_log_size, std::filesystem::file_size(std::filesystem::path(path_41to50).replace_extension("log")));
|
|
BOOST_REQUIRE_EQUAL(before_41to50_index_size, std::filesystem::file_size(std::filesystem::path(path_41to50).replace_extension("index")));
|
|
//and only empty head log is present
|
|
BOOST_REQUIRE_EQUAL(0u, std::filesystem::file_size(std::filesystem::path(head_log_path).replace_extension("log")));
|
|
BOOST_REQUIRE_EQUAL(0u, std::filesystem::file_size(std::filesystem::path(head_log_path).replace_extension("index")));
|
|
}
|
|
else {
|
|
//check that the 41-50 index and log do not exist
|
|
BOOST_REQUIRE(!std::filesystem::exists(std::filesystem::path(path_41to50).replace_extension("log")));
|
|
BOOST_REQUIRE(!std::filesystem::exists(std::filesystem::path(path_41to50).replace_extension("index")));
|
|
//check that the head index is smaller than what 41-50 index previously was
|
|
BOOST_REQUIRE_GT(before_41to50_index_size, std::filesystem::file_size(std::filesystem::path(head_log_path).replace_extension("index")));
|
|
//and that the head log is larger than what 41-50 log previously was
|
|
BOOST_REQUIRE_LT(before_41to50_log_size, std::filesystem::file_size(std::filesystem::path(head_log_path).replace_extension("log")));
|
|
}
|
|
}
|
|
|
|
//reopen the log while we're in this shortened fork state
|
|
{
|
|
eosio::state_history::log_catalog lc(tmpdir.path(), conf, "logz");
|
|
BOOST_REQUIRE_EQUAL(lc.block_range().first, start);
|
|
BOOST_REQUIRE_EQUAL(lc.block_range().second, start_fork_at+1);
|
|
}
|
|
|
|
//continue on writing to the log replacing all blocks after the fork block
|
|
{
|
|
eosio::state_history::log_catalog lc(tmpdir.path(), conf, "logz");
|
|
for(unsigned i = start_fork_at+1; i < end; ++i)
|
|
lc.pack_and_write_entry(fake_blockid_for_num(i, 0xdeadUL), fake_blockid_for_num(i-1, 0xdeadUL), [&](bio::filtering_ostreambuf& obuf) {
|
|
bio::filtering_istreambuf hashed_randomness(sha256_filter() | bio::restrict(random_source(), 0, mt_random()%1024*1024));
|
|
bio::copy(hashed_randomness, obuf);
|
|
wrote_data_for_blocknum[i] = hashed_randomness.component<sha256_filter>(0)->enc->result();
|
|
});
|
|
|
|
BOOST_REQUIRE_EQUAL(lc.block_range().first, start);
|
|
BOOST_REQUIRE_EQUAL(lc.block_range().second, end);
|
|
}
|
|
|
|
//check sizes of everything: all index sizes should have been the same as we originally started with
|
|
BOOST_REQUIRE_EQUAL(before_head_index_size, std::filesystem::file_size(std::filesystem::path(head_log_path).replace_extension("index")));
|
|
BOOST_REQUIRE_EQUAL(before_31to40_index_size, std::filesystem::file_size(std::filesystem::path(path_31to40).replace_extension("index")));
|
|
BOOST_REQUIRE_EQUAL(before_41to50_index_size, std::filesystem::file_size(std::filesystem::path(path_41to50).replace_extension("index")));
|
|
BOOST_REQUIRE_LT(before_head_log_size, std::filesystem::file_size(std::filesystem::path(head_log_path).replace_extension("log")));
|
|
if(start_fork_at >= 51)
|
|
BOOST_REQUIRE_EQUAL(before_41to50_log_size, std::filesystem::file_size(std::filesystem::path(path_41to50).replace_extension("log")));
|
|
//don't do this else for now: randomness in the data written may cause this to fail
|
|
//else BOOST_REQUIRE_LT(before_41to50_log_size, std::filesystem::file_size(std::filesystem::path(path_41to50).replace_extension("log")));
|
|
BOOST_REQUIRE_EQUAL(before_31to40_log_size, std::filesystem::file_size(std::filesystem::path(path_31to40).replace_extension("log")));
|
|
|
|
//read through all the blocks and validate contents
|
|
{
|
|
eosio::state_history::log_catalog lc(tmpdir.path(), conf, "logz");
|
|
BOOST_REQUIRE_EQUAL(lc.block_range().first, start);
|
|
BOOST_REQUIRE_EQUAL(lc.block_range().second, end);
|
|
|
|
for(unsigned i = start; i < end; ++i) {
|
|
std::optional<state_history::ship_log_entry> entry = lc.get_entry(i);
|
|
BOOST_REQUIRE(!!entry);
|
|
|
|
bio::filtering_ostreambuf hashed_null(sha256_filter() | bio::null_sink());
|
|
bio::filtering_istreambuf log_stream = entry->get_stream();
|
|
bio::copy(log_stream, hashed_null);
|
|
BOOST_REQUIRE_EQUAL(hashed_null.component<sha256_filter>(0)->enc->result(), wrote_data_for_blocknum[i]);
|
|
}
|
|
}
|
|
} FC_LOG_AND_RETHROW();
|
|
|
|
//(manually) fabricate a leap 3.x ship log format and make sure it's readable
|
|
BOOST_AUTO_TEST_CASE(old_log_format) try {
|
|
const temp_directory tmpdir;
|
|
|
|
const unsigned begin_block = 2;
|
|
const unsigned end_block = 45;
|
|
|
|
std::map<block_num_type, sha256> wrote_data_for_blocknum;
|
|
|
|
{
|
|
random_access_file file(tmpdir.path() / "old.log");
|
|
for(unsigned blocknum = begin_block; blocknum < end_block; ++blocknum) {
|
|
const size_t insertpos = file.size();
|
|
std::pair<state_history::log_header, uint32_t> legacy_header = {};
|
|
legacy_header.first.block_id = fake_blockid_for_num(blocknum);
|
|
|
|
bio::filtering_istreambuf hashed_randomness(sha256_filter() | bio::restrict(random_source(), 0, 128*1024));
|
|
bio::filtering_ostreambuf output(bio::zlib_compressor() | eosio::detail::counter() | bio::restrict(file.seekable_device(), insertpos + raw::pack_size(legacy_header)));
|
|
bio::copy(hashed_randomness, output);
|
|
wrote_data_for_blocknum[blocknum] = hashed_randomness.component<sha256_filter>(0)->enc->result();
|
|
legacy_header.first.payload_size = output.component<eosio::detail::counter>(1)->characters() + sizeof(decltype(legacy_header.second));
|
|
|
|
file.pack_to(legacy_header, insertpos);
|
|
file.pack_to_end(insertpos);
|
|
}
|
|
}
|
|
|
|
{
|
|
//will regenerate index too
|
|
eosio::state_history::log_catalog lc(tmpdir.path(), std::monostate(), "old");
|
|
|
|
BOOST_REQUIRE_EQUAL(begin_block, lc.block_range().first);
|
|
BOOST_REQUIRE_EQUAL(end_block, lc.block_range().second);
|
|
|
|
for(unsigned i = begin_block; i < end_block; ++i) {
|
|
std::optional<state_history::ship_log_entry> entry = lc.get_entry(i);
|
|
BOOST_REQUIRE(!!entry);
|
|
|
|
bio::filtering_ostreambuf hashed_null(sha256_filter() | bio::null_sink());
|
|
bio::filtering_istreambuf log_stream = entry->get_stream();
|
|
bio::copy(log_stream, hashed_null);
|
|
BOOST_REQUIRE_EQUAL(hashed_null.component<sha256_filter>(0)->enc->result(), wrote_data_for_blocknum[i]);
|
|
}
|
|
}
|
|
} FC_LOG_AND_RETHROW();
|
|
|
|
//writes a bunch of a blocks, and then writes a bunch of the same blocks (block ids) all over again. this is similar to
|
|
// what would occur on a replay or loading a snapshot older than what was the prior head.
|
|
const state_history::state_history_log_config log_configs_for_rewrite_same[] = {
|
|
{std::monostate()},
|
|
{state_history::partition_config{
|
|
.retained_dir = "retain here pls",
|
|
.archive_dir = "archive here pls",
|
|
.stride = 10
|
|
}}
|
|
};
|
|
BOOST_DATA_TEST_CASE(rewrite_same, bdata::make(log_configs_for_rewrite_same), config) try {
|
|
const fc::temp_directory tmpdir;
|
|
|
|
std::map<block_num_type, sha256> wrote_data_for_blocknum;
|
|
const unsigned begin_block = 10;
|
|
const unsigned end_block = 105;
|
|
|
|
{
|
|
eosio::state_history::log_catalog lc(tmpdir.path(), config, "mr,log");
|
|
for(unsigned i = begin_block; i < end_block; ++i)
|
|
lc.pack_and_write_entry(fake_blockid_for_num(i), fake_blockid_for_num(i-1), [&](bio::filtering_ostreambuf& obuf) {
|
|
bio::filtering_istreambuf hashed_randomness(sha256_filter() | bio::restrict(random_source(), 0, 64*1024));
|
|
bio::copy(hashed_randomness, obuf);
|
|
wrote_data_for_blocknum[i] = hashed_randomness.component<sha256_filter>(0)->enc->result();
|
|
});
|
|
}
|
|
|
|
const size_t before_log_size = std::filesystem::file_size(tmpdir.path() / "mr,log.log");
|
|
const size_t before_index_size = std::filesystem::file_size(tmpdir.path() / "mr,log.index");
|
|
|
|
//reopen and write different data for each block id. This should silently be swallowed
|
|
{
|
|
eosio::state_history::log_catalog lc(tmpdir.path(), config, "mr,log");
|
|
for(unsigned i = begin_block; i < end_block; ++i)
|
|
lc.pack_and_write_entry(fake_blockid_for_num(i), fake_blockid_for_num(i-1), [&](bio::filtering_ostreambuf& obuf) {
|
|
bio::filtering_istreambuf hashed_randomness(sha256_filter() | bio::restrict(random_source(), 0, 64*1024));
|
|
bio::copy(hashed_randomness, obuf);
|
|
});
|
|
}
|
|
|
|
//read the blocks back, making sure the hash of data is what was originally written and that the size of the log remained equal
|
|
{
|
|
eosio::state_history::log_catalog lc(tmpdir.path(), config, "mr,log");
|
|
BOOST_REQUIRE_EQUAL(lc.block_range().first, begin_block);
|
|
BOOST_REQUIRE_EQUAL(lc.block_range().second, end_block);
|
|
|
|
for(unsigned i = begin_block; i < end_block; ++i) {
|
|
std::optional<state_history::ship_log_entry> entry = lc.get_entry(i);
|
|
BOOST_REQUIRE(!!entry);
|
|
|
|
bio::filtering_ostreambuf hashed_null(sha256_filter() | bio::null_sink());
|
|
bio::filtering_istreambuf log_stream = entry->get_stream();
|
|
bio::copy(log_stream, hashed_null);
|
|
BOOST_REQUIRE_EQUAL(hashed_null.component<sha256_filter>(0)->enc->result(), wrote_data_for_blocknum[i]);
|
|
}
|
|
}
|
|
BOOST_REQUIRE_EQUAL(before_log_size, std::filesystem::file_size(tmpdir.path() / "mr,log.log"));
|
|
BOOST_REQUIRE_EQUAL(before_index_size, std::filesystem::file_size(tmpdir.path() / "mr,log.index"));
|
|
|
|
} FC_LOG_AND_RETHROW();
|
|
|
|
//similar to above, but this time configure the logs such that it forgets (prunes/rotates out) early blocks, but then the rewrite of blocks occurs in the range
|
|
// of still "remembered" blocks
|
|
const state_history::state_history_log_config log_configs_for_rewrite_remembered[] = {
|
|
{state_history::partition_config{.retained_dir = "retain here pls", .archive_dir = "archive here pls", .stride = 10, .max_retained_files = 4}},
|
|
{state_history::prune_config{.prune_blocks = 40, .prune_threshold = 2}}
|
|
};
|
|
BOOST_DATA_TEST_CASE(rewrite_same_remembered, bdata::make(log_configs_for_rewrite_remembered), config) try {
|
|
const fc::temp_directory tmpdir;
|
|
|
|
std::map<block_num_type, sha256> wrote_data_for_blocknum;
|
|
const unsigned begin_block = 10;
|
|
const unsigned end_block = 105;
|
|
|
|
//fill up blocks 10 through 104, but many of the early blocks are forgotten
|
|
{
|
|
eosio::state_history::log_catalog lc(tmpdir.path(), config, "huh");
|
|
for(unsigned i = begin_block; i < end_block; ++i)
|
|
lc.pack_and_write_entry(fake_blockid_for_num(i), fake_blockid_for_num(i-1), [&](bio::filtering_ostreambuf& obuf) {
|
|
bio::filtering_istreambuf hashed_randomness(sha256_filter() | bio::restrict(random_source(), 0, 64*1024));
|
|
bio::copy(hashed_randomness, obuf);
|
|
wrote_data_for_blocknum[i] = hashed_randomness.component<sha256_filter>(0)->enc->result();
|
|
});
|
|
}
|
|
|
|
const unsigned start_rewrite_block = 70;
|
|
|
|
const size_t before_log_size = std::filesystem::file_size(tmpdir.path() / "huh.log");
|
|
const size_t before_index_size = std::filesystem::file_size(tmpdir.path() / "huh.index");
|
|
|
|
//rewrite blocks 70-104
|
|
{
|
|
eosio::state_history::log_catalog lc(tmpdir.path(), config, "huh");
|
|
for(unsigned i = start_rewrite_block; i < end_block; ++i)
|
|
lc.pack_and_write_entry(fake_blockid_for_num(i), fake_blockid_for_num(i-1), [&](bio::filtering_ostreambuf& obuf) {
|
|
bio::filtering_istreambuf hashed_randomness(sha256_filter() | bio::restrict(random_source(), 0, 64*1024));
|
|
bio::copy(hashed_randomness, obuf);
|
|
});
|
|
}
|
|
|
|
//read the blocks back, making sure the hash of data is what was originally written and that the size of the log remained equal
|
|
{
|
|
eosio::state_history::log_catalog lc(tmpdir.path(), config, "huh");
|
|
BOOST_REQUIRE_LT(lc.block_range().first, start_rewrite_block);
|
|
BOOST_REQUIRE_EQUAL(lc.block_range().second, end_block);
|
|
|
|
for(unsigned i = start_rewrite_block; i < end_block; ++i) {
|
|
std::optional<state_history::ship_log_entry> entry = lc.get_entry(i);
|
|
BOOST_REQUIRE(!!entry);
|
|
|
|
bio::filtering_ostreambuf hashed_null(sha256_filter() | bio::null_sink());
|
|
bio::filtering_istreambuf log_stream = entry->get_stream();
|
|
bio::copy(log_stream, hashed_null);
|
|
BOOST_REQUIRE_EQUAL(hashed_null.component<sha256_filter>(0)->enc->result(), wrote_data_for_blocknum[i]);
|
|
}
|
|
|
|
BOOST_REQUIRE_EQUAL(before_log_size, std::filesystem::file_size(tmpdir.path() / "huh.log"));
|
|
BOOST_REQUIRE_EQUAL(before_index_size, std::filesystem::file_size(tmpdir.path() / "huh.index"));
|
|
}
|
|
|
|
} FC_LOG_AND_RETHROW();
|
|
|
|
//similar to above, but this time configure the logs such that it forgets (rotates out) early blocks, and then rewrite a block in that forgotten range
|
|
BOOST_AUTO_TEST_CASE(rewrite_same_forgotten) try {
|
|
const fc::temp_directory tmpdir;
|
|
|
|
const state_history::state_history_log_config config = {state_history::partition_config{.retained_dir = "retain here pls", .archive_dir = "archive here pls", .stride = 10, .max_retained_files = 4}};
|
|
|
|
std::map<block_num_type, sha256> wrote_data_for_blocknum;
|
|
const unsigned begin_block = 10;
|
|
const unsigned end_block = 105;
|
|
|
|
//fill up blocks 10 through 104, but many of the early blocks are forgotten
|
|
{
|
|
eosio::state_history::log_catalog lc(tmpdir.path(), config, "huh");
|
|
for(unsigned i = begin_block; i < end_block; ++i)
|
|
lc.pack_and_write_entry(fake_blockid_for_num(i), fake_blockid_for_num(i-1), [&](bio::filtering_ostreambuf& obuf) {
|
|
bio::filtering_istreambuf hashed_randomness(sha256_filter() | bio::restrict(random_source(), 0, 64*1024));
|
|
bio::copy(hashed_randomness, obuf);
|
|
wrote_data_for_blocknum[i] = hashed_randomness.component<sha256_filter>(0)->enc->result();
|
|
});
|
|
}
|
|
|
|
const unsigned start_rewrite_block = 30;
|
|
|
|
//try and rewrite block 30. This is a forgotten block that is before the first block in the catalog, so it will fail
|
|
eosio::state_history::log_catalog lc(tmpdir.path(), config, "huh");
|
|
BOOST_REQUIRE_EXCEPTION(lc.pack_and_write_entry(fake_blockid_for_num(start_rewrite_block), fake_blockid_for_num(start_rewrite_block-1), [&](bio::filtering_ostreambuf& obuf) {}), chain::plugin_exception,
|
|
[](const chain::plugin_exception& e) {return e.get_log().at(0).get_message().find("is before first block") != std::string::npos;});
|
|
} FC_LOG_AND_RETHROW();
|
|
|
|
//similar to above, but with pruning. surprise! it's allowed! rewriting pruned blocks is allowed as long as they are after the index's start block (the first block ever written to
|
|
// the log)
|
|
BOOST_AUTO_TEST_CASE(rewrite_same_forgotten_pruned_range) try {
|
|
const fc::temp_directory tmpdir;
|
|
|
|
const state_history::state_history_log_config config = {state_history::prune_config{.prune_blocks = 40, .prune_threshold = 2}};
|
|
|
|
std::map<block_num_type, sha256> wrote_data_for_blocknum;
|
|
const unsigned begin_block = 10;
|
|
const unsigned end_block = 105;
|
|
|
|
//fill up blocks 10 through 104, but many of the early blocks are forgotten
|
|
{
|
|
eosio::state_history::log_catalog lc(tmpdir.path(), config, "huh");
|
|
for(unsigned i = begin_block; i < end_block; ++i)
|
|
lc.pack_and_write_entry(fake_blockid_for_num(i), fake_blockid_for_num(i-1), [&](bio::filtering_ostreambuf& obuf) {
|
|
bio::filtering_istreambuf hashed_randomness(sha256_filter() | bio::restrict(random_source(), 0, 64*1024));
|
|
bio::copy(hashed_randomness, obuf);
|
|
wrote_data_for_blocknum[i] = hashed_randomness.component<sha256_filter>(0)->enc->result();
|
|
});
|
|
}
|
|
|
|
const unsigned start_rewrite_block = 30;
|
|
|
|
const size_t before_log_size = std::filesystem::file_size(tmpdir.path() / "huh.log");
|
|
const size_t before_index_size = std::filesystem::file_size(tmpdir.path() / "huh.index");
|
|
|
|
//rewrite block 30, and check that the log state seems sane
|
|
{
|
|
eosio::state_history::log_catalog lc(tmpdir.path(), config, "huh");
|
|
lc.pack_and_write_entry(fake_blockid_for_num(start_rewrite_block), fake_blockid_for_num(start_rewrite_block-1), [&](bio::filtering_ostreambuf& obuf) {});
|
|
const auto [after_begin_block, after_end_block] = lc.block_range();
|
|
|
|
BOOST_REQUIRE_EQUAL(after_begin_block, start_rewrite_block);
|
|
BOOST_REQUIRE_EQUAL(after_end_block, start_rewrite_block+1u);
|
|
}
|
|
//open again just in case
|
|
{
|
|
eosio::state_history::log_catalog lc(tmpdir.path(), config, "huh");
|
|
const auto [after_begin_block, after_end_block] = lc.block_range();
|
|
BOOST_REQUIRE_EQUAL(after_begin_block, start_rewrite_block);
|
|
BOOST_REQUIRE_EQUAL(after_end_block, start_rewrite_block+1u);
|
|
}
|
|
|
|
//and check the file sizes, index should have shrunk, log grown
|
|
BOOST_REQUIRE_LT(before_log_size, std::filesystem::file_size(tmpdir.path() / "huh.log"));
|
|
BOOST_REQUIRE_GT(before_index_size, std::filesystem::file_size(tmpdir.path() / "huh.index"));
|
|
|
|
} FC_LOG_AND_RETHROW();
|
|
|
|
//similar to above, but this time write a block that is before the first ever block of the pruned log (index_start_block). this isn't allowed
|
|
BOOST_AUTO_TEST_CASE(rewrite_too_old_pruned_block) try {
|
|
const fc::temp_directory tmpdir;
|
|
|
|
const state_history::state_history_log_config config = {state_history::prune_config{.prune_blocks = 40, .prune_threshold = 2}};
|
|
|
|
std::map<block_num_type, sha256> wrote_data_for_blocknum;
|
|
const unsigned begin_block = 10;
|
|
const unsigned end_block = 105;
|
|
|
|
//fill up blocks 10 through 104, but many of the early blocks are forgotten
|
|
{
|
|
eosio::state_history::log_catalog lc(tmpdir.path(), config, "huh");
|
|
for(unsigned i = begin_block; i < end_block; ++i)
|
|
lc.pack_and_write_entry(fake_blockid_for_num(i), fake_blockid_for_num(i-1), [&](bio::filtering_ostreambuf& obuf) {
|
|
bio::filtering_istreambuf hashed_randomness(sha256_filter() | bio::restrict(random_source(), 0, 64*1024));
|
|
bio::copy(hashed_randomness, obuf);
|
|
wrote_data_for_blocknum[i] = hashed_randomness.component<sha256_filter>(0)->enc->result();
|
|
});
|
|
}
|
|
|
|
const unsigned start_rewrite_block = 9;
|
|
|
|
eosio::state_history::log_catalog lc(tmpdir.path(), config, "huh");
|
|
BOOST_REQUIRE_EXCEPTION(lc.pack_and_write_entry(fake_blockid_for_num(start_rewrite_block), fake_blockid_for_num(start_rewrite_block-1), [&](bio::filtering_ostreambuf& obuf) {}), chain::plugin_exception,
|
|
[](const chain::plugin_exception& e) {return e.get_log().at(0).get_message().find("is before start block") != std::string::npos;});
|
|
|
|
} FC_LOG_AND_RETHROW();
|
|
|
|
//verificaiton of clear()
|
|
const state_history::state_history_log_config log_configs_for_clear[] = {
|
|
{std::monostate()},
|
|
{state_history::partition_config{
|
|
.retained_dir = {},
|
|
.archive_dir = {},
|
|
.stride = 5
|
|
}},
|
|
{state_history::partition_config{
|
|
.retained_dir = {},
|
|
.archive_dir = {},
|
|
.stride = 5,
|
|
.max_retained_files = 2
|
|
}},
|
|
{state_history::prune_config{
|
|
.prune_blocks = 5,
|
|
.prune_threshold = 2
|
|
}}
|
|
};
|
|
BOOST_DATA_TEST_CASE(clear, bdata::make(log_configs_for_clear) * bdata::make({9u, 10u, 11u}), config, after_clear_begin_block) try {
|
|
const fc::temp_directory tmpdir;
|
|
|
|
const unsigned before_clear_begin_block = 10;
|
|
const unsigned before_clear_end_block = 42;
|
|
|
|
const unsigned after_clear_end_block = after_clear_begin_block+4;
|
|
|
|
{
|
|
eosio::state_history::log_catalog lc(tmpdir.path(), config, "clearme");
|
|
for(unsigned i = before_clear_begin_block; i < before_clear_end_block; ++i)
|
|
lc.pack_and_write_entry(fake_blockid_for_num(i), fake_blockid_for_num(i-1), [&](bio::filtering_ostreambuf& obuf) {});
|
|
|
|
auto [begin_block, end_block] = lc.block_range();
|
|
//not checking begin_block because logs could have been rotated or pruned depending on test case
|
|
BOOST_REQUIRE_EQUAL(end_block, before_clear_end_block);
|
|
|
|
lc.clear();
|
|
BOOST_REQUIRE(lc.empty());
|
|
//head log should be empty
|
|
BOOST_REQUIRE_EQUAL(0u, std::filesystem::file_size(tmpdir.path() / "clearme.log"));
|
|
BOOST_REQUIRE_EQUAL(0u, std::filesystem::file_size(tmpdir.path() / "clearme.index"));
|
|
//make sure no retained logs exist
|
|
for(const std::string& suffix : {"log"s, "index"s}) {
|
|
const std::regex retained_logfile_regex(R"(^clearme-\d+-\d+\.)" + suffix + "$");
|
|
|
|
unsigned found = 0;
|
|
for(const std::filesystem::directory_entry& dir_entry : std::filesystem::directory_iterator(tmpdir.path()))
|
|
found += std::regex_search(dir_entry.path().filename().string(), retained_logfile_regex);
|
|
BOOST_REQUIRE_EQUAL(found, 0u);
|
|
}
|
|
|
|
for(unsigned i = after_clear_begin_block; i < after_clear_end_block; ++i)
|
|
lc.pack_and_write_entry(fake_blockid_for_num(i), fake_blockid_for_num(i-1), [&](bio::filtering_ostreambuf& obuf) {});
|
|
|
|
std::tie(begin_block, end_block) = lc.block_range();
|
|
BOOST_REQUIRE_EQUAL(begin_block, after_clear_begin_block);
|
|
BOOST_REQUIRE_EQUAL(end_block, after_clear_end_block);
|
|
}
|
|
|
|
//reopen for sanity check
|
|
{
|
|
eosio::state_history::log_catalog lc(tmpdir.path(), config, "clearme");
|
|
const auto [begin_block, end_block] = lc.block_range();
|
|
BOOST_REQUIRE_EQUAL(begin_block, after_clear_begin_block);
|
|
BOOST_REQUIRE_EQUAL(end_block, after_clear_end_block);
|
|
}
|
|
} FC_LOG_AND_RETHROW();
|
|
|
|
BOOST_AUTO_TEST_SUITE_END()
|