#include #include "gtest/gtest.h" #include "currency_core/currency_format_utils.h" #include "common/db_abstract_accessor.h" #include "common/median_db_cache.h" #include "common/db_backend_lmdb.h" #include "common/util.h" #include "misc_log_ex.h" #include "crypto/crypto.h" #include "../core_tests/random_helper.h" #include "serialization/serialization.h" #include "file_io_utils.h" inline uint64_t random_in_range(uint64_t from, uint64_t to) { if (from == to) return from; CHECK_AND_ASSERT_MES(from < to, 0, "Invalid arguments: from = " << from << ", to = " << to); return crypto::rand() % (to - from + 1) + from; } /* TEST(db_accessor_tests, cached_key_value_accessor_test) { epee::shared_recursive_mutex m_rw_lock; tools::db::basic_db_accessor m_db(std::shared_ptr(new tools::db::lmdb_db_backend), m_rw_lock); tools::db::cached_key_value_accessor m_container(m_db); const std::string folder_name = "./TEST_cached_key_value_accessor_test"; tools::create_directories_if_necessary(folder_name); uint64_t cache_size = CACHE_SIZE; ASSERT_TRUE(m_db.open(folder_name, cache_size)); ASSERT_TRUE(m_container.init("container")); ... TODO ... } */ TEST(db_accessor_tests_2, recoursive_tx_test) { epee::shared_recursive_mutex m_rw_lock; tools::db::basic_db_accessor m_db(std::shared_ptr(new tools::db::lmdb_db_backend), m_rw_lock); tools::db::cached_key_value_accessor m_container(m_db); const std::string folder_name = "./TEST_db_recursive_tx"; tools::create_directories_if_necessary(folder_name); uint64_t cache_size = CACHE_SIZE; ASSERT_TRUE(m_db.open(folder_name, cache_size)); ASSERT_TRUE(m_container.init("zzzz") ); bool tx_result = m_container.begin_transaction(); ASSERT_TRUE(tx_result); m_container.set(10, 10); m_container.set(11, 11); tx_result = m_container.begin_transaction(true); ASSERT_TRUE(tx_result); tx_result = m_container.begin_transaction(true); ASSERT_TRUE(tx_result); uint64_t r = *m_container.get(10); ASSERT_TRUE(r == 10); tx_result = m_container.begin_transaction(); m_container.set(13, 13); m_container.set(14, 14); r = *m_container.get(14); ASSERT_TRUE(r == 14); m_container.commit_transaction(); r = *m_container.get(14); ASSERT_TRUE(r == 14); m_container.commit_transaction(); m_container.commit_transaction(); m_container.commit_transaction(); } template struct naive_median { enum { WINDOW_SIZE = ALIAS_COAST_PERIOD }; naive_median() { clear(); } void push_item(const key_t& key, const data_t& data) { m_items.push_back(std::make_pair(key, data)); } void pop_item(uint64_t fee_to_check = UINT64_MAX /* default value means "don't check" */) { if (m_items.empty()) return; if (fee_to_check != UINT64_MAX) ASSERT_EQ(m_items.back().first, fee_to_check); m_items.pop_back(); } key_t get_median() const { if (m_items.empty() || m_items_in_window_count == 0) return (key_t)(0); items_t local_items(m_items.end() - m_items_in_window_count, m_items.end()); // copy last m_items_in_window_count items // calculate median in 'local_items' auto middle = local_items.begin() + local_items.size() / 2; std::nth_element(local_items.begin(), middle, local_items.end()); if (local_items.size() % 2 == 1) return middle->first; // 1, 3, 5... auto prev_middle = std::max_element(local_items.begin(), middle); // here we assume all the elements before 'middle' are not greater than middle, thus max_element === prev_middle (s.a. std::nth_element for details) return (prev_middle->first + middle->first) / 2; } // should be called after push/pop sequence but before get_median() template void scan_tems(cb_t cb) { // count items for which cb() returns true -- this mean 'item in the window' m_items_in_window_count = std::count_if(m_items.begin(), m_items.end(), [&cb](const item_t& x) { return cb(x.first, x.second); }); } void clear() { m_items.clear(); m_items_in_window_count = 0; } void print_to_file(const std::string& filename) { std::stringstream ss; ss << "m_items.size()() == " << m_items.size() << ", m_items_in_window_count == " << m_items_in_window_count << ENDL; for(size_t i = 0; i < m_items.size(); ++i) ss << i << "\t" << m_items[i].first << "\t" << m_items[i].second << ENDL; ss << "THE END"; epee::file_io_utils::save_string_to_file(filename, ss.str()); } typedef std::pair item_t; typedef std::vector items_t; items_t m_items; size_t m_items_in_window_count; }; // Emulate blockchain_storage a little bit struct bcs_stub_t { bcs_stub_t(tools::median_db_cache& tx_fee_median, naive_median& naive_median) : m_tx_fee_median(tx_fee_median) , m_naive_median(naive_median) {} bool is_pos_allowed() const { return true; } void update_naive_fee_median() { uint64_t period = is_pos_allowed() ? ALIAS_COAST_PERIOD : ALIAS_COAST_PERIOD / 2; int64_t starter_block_index = m_db_blocks.size() - period; m_naive_median.scan_tems([starter_block_index](uint64_t fee, uint64_t height) { return (int64_t)height >= starter_block_index; } ); } // almost copy-pasted from blockchain_storage bool update_tx_fee_median() { #define PERIOD_DISABLED 0xffffffffffffffffLL uint64_t period = is_pos_allowed() ? ALIAS_COAST_PERIOD : ALIAS_COAST_PERIOD / 2; if (period > m_db_blocks.size()) return true; // can cause problem when there's recent_items -- check this case! uint64_t starter_block_index = m_db_blocks.size() - period; uint64_t purge_recent_period = is_pos_allowed() ? ALIAS_COAST_RECENT_PERIOD : ALIAS_COAST_RECENT_PERIOD / 2; uint64_t purge_recent_block_index = 0; if (purge_recent_period >= m_db_blocks.size()) purge_recent_block_index = PERIOD_DISABLED; else purge_recent_block_index = m_db_blocks.size() - purge_recent_period; auto cb = [&](uint64_t fee, uint64_t height) { if (height >= starter_block_index) return true; return false; }; auto cb_final_eraser = [&](uint64_t fee, uint64_t height) { if (purge_recent_block_index == PERIOD_DISABLED) return true; if (height >= purge_recent_block_index) return true; return false; }; if (!m_tx_fee_median.scan_items(cb, cb_final_eraser)) { //recent items is over, need to rebuild m_tx_fee_median helper from scratch init_tx_fee_median(); // re-scan again to correctly populate recent items bool r = m_tx_fee_median.scan_items(cb, cb_final_eraser); CHECK_AND_ASSERT_THROW_MES(r, "internal error: second call of scan_items returned false"); } return true; } // almost directly copy-pasted from blockchain_storage void init_tx_fee_median() { LOG_PRINT_L0("Initializing tx fee median buffer..."); //uint64_t period = is_pos_allowed() ? ALIAS_COAST_PERIOD : ALIAS_COAST_PERIOD / 2; uint64_t purge_recent_period = is_pos_allowed() ? ALIAS_COAST_RECENT_PERIOD : ALIAS_COAST_RECENT_PERIOD / 2; CRITICAL_REGION_LOCAL(m_read_lock); m_tx_fee_median.clear(); if (purge_recent_period > m_db_blocks.size()) purge_recent_period = m_db_blocks.size(); std::vector tx_fee_list; size_t tx_count = 0, blocks_count = 0; for (uint64_t i = m_db_blocks.size() - purge_recent_period; i != m_db_blocks.size(); i++) { auto bei_ptr = m_db_blocks[i]; for (auto fee : bei_ptr.fees) { m_tx_fee_median.push_item(fee, i); ++tx_count; } ++blocks_count; } LOG_PRINT_L0("Tx fee median buffer initialized OK (using " << tx_count << " txs from " << blocks_count << " blocks), current fee median is " << m_tx_fee_median.get_median()); } void init_naive_median() { LOG_PRINT_L0("Initializing naive median buffer..."); //uint64_t period = is_pos_allowed() ? ALIAS_COAST_PERIOD : ALIAS_COAST_PERIOD / 2; m_naive_median.clear(); size_t tx_count = 0, blocks_count = 0; for (uint64_t i = 0; i != m_db_blocks.size(); ++i) { auto bei_ptr = m_db_blocks[i]; for (auto fee : bei_ptr.fees) { m_naive_median.push_item(fee, i); ++tx_count; } ++blocks_count; } LOG_PRINT_L0("Naive tx fee median buffer initialized OK (using " << tx_count << " txs from " << blocks_count << " blocks), current fee median is " << m_tx_fee_median.get_median()); } bool load_from_file(const std::string& filename) { std::string buffer; CHECK_AND_ASSERT_MES(epee::file_io_utils::load_file_to_string(filename, buffer), false, "load_file_to_string failed for " << filename); CHECK_AND_ASSERT_MES(t_unserializable_object_from_blob(*this, buffer), false, "t_unserializable_object_from_blob failed"); return true; } bool save_to_file(const std::string& filename) const { std::string buffer; CHECK_AND_ASSERT_MES(t_serializable_object_to_blob(*this, buffer), false, "t_serializable_object_to_blob failed"); CHECK_AND_ASSERT_MES(epee::file_io_utils::save_string_to_file(filename, buffer), false, "save_string_to_file failed for " << filename); return true; } // emulate block header with the list of txs struct block_t { uint64_t height; std::vector fees; BEGIN_SERIALIZE() FIELD(height) FIELD(fees) END_SERIALIZE() }; tools::median_db_cache& m_tx_fee_median; naive_median& m_naive_median; std::vector m_db_blocks; epee::dummy_critical_section m_read_lock; BEGIN_SERIALIZE_OBJECT() FIELD(m_db_blocks) END_SERIALIZE() }; TEST(db_accessor_tests, median_db_cache_test) { random_state_test_restorer::reset_random(); // make this test deterministic (the same crypto::rand() sequence) epee::shared_recursive_mutex m_rw_lock; tools::db::basic_db_accessor m_db(std::shared_ptr(new tools::db::lmdb_db_backend), m_rw_lock); tools::median_db_cache m_tx_fee_median(m_db); const std::string folder_name = "./TEST_median_db_cache"; const std::string naive_median_serialization_filename = folder_name + "/naive_median"; tools::create_directories_if_necessary(folder_name); uint64_t cache_size = CACHE_SIZE; ASSERT_TRUE(m_db.open(folder_name, cache_size)); ASSERT_TRUE(m_tx_fee_median.init("median_fee")); m_db.begin_transaction(); naive_median m_naive_median; bcs_stub_t bcs_stub(m_tx_fee_median, m_naive_median); bool bcs_loaded = bcs_stub.load_from_file(naive_median_serialization_filename); if (!bcs_loaded || bcs_stub.m_tx_fee_median.get_median() == 0) { // reject saved caches, init from scratch size_t initial_blocks_count = ALIAS_COAST_RECENT_PERIOD + 20; for (size_t i = 0; i < initial_blocks_count; ++i) { bcs_stub.m_db_blocks.push_back(bcs_stub_t::block_t()); bcs_stub_t::block_t &b = bcs_stub.m_db_blocks.back(); b.height = i; b.fees.resize(random_in_range(0, 3)); for (size_t j = 0; j < b.fees.size(); ++j) b.fees[j] = random_in_range(0, COIN); } bcs_stub.init_tx_fee_median(); // clear tx_fee_median and initialize by reading the 'blockchain' } bcs_stub.init_naive_median(); // naive median doesn't use db cache, so it always init from the 'blockchain' bcs_stub.update_tx_fee_median(); bcs_stub.update_naive_fee_median(); ASSERT_EQ(m_naive_median.get_median(), m_tx_fee_median.get_median()); m_naive_median.print_to_file(folder_name + "/naive_median_1.txt"); //epee::misc_utils::auto_scope_leave_caller scope_exit_handler = epee::misc_utils::create_scope_leave_handler([&] { m_db.commit_transaction(); bcs_stub.save_to_file(naive_median_serialization_filename); }); m_db.commit_transaction(); bcs_stub.save_to_file(naive_median_serialization_filename); m_db.begin_transaction(); size_t forward_steps_max_random_boundary = 20, rewind_steps_max_random_boundary = 7; size_t steps = 1000; for (size_t step = 0; step < steps; ++step) { LOG_PRINT_L0("Step " << step); if (step % 50 == 0) // once a bunch of steps make a dicision upon blockchain trend - insreasing or descreasing. This should result in a kind of oscillation { uint64_t normal_window = ALIAS_COAST_PERIOD; uint64_t recent_window = ALIAS_COAST_RECENT_PERIOD; if (bcs_stub.m_db_blocks.size() < normal_window) forward_steps_max_random_boundary = 20, rewind_steps_max_random_boundary = 7; if (bcs_stub.m_db_blocks.size() > recent_window) forward_steps_max_random_boundary = 7, rewind_steps_max_random_boundary = 20; // if blocks count lies between window's size -- keep the same } size_t rewind_steps = random_in_range(1, rewind_steps_max_random_boundary); for (size_t i = 0; i < rewind_steps; ++i) { auto& b = bcs_stub.m_db_blocks.back(); if (b.fees.size() > 0) { for (size_t j = b.fees.size() - 1; j < b.fees.size(); --j) { m_naive_median.pop_item(b.fees[j]); m_tx_fee_median.pop_item(); } } bcs_stub.m_db_blocks.pop_back(); bcs_stub.update_tx_fee_median(); bcs_stub.update_naive_fee_median(); ASSERT_EQ(m_naive_median.get_median(), m_tx_fee_median.get_median()); } uint64_t top_height = bcs_stub.m_db_blocks.back().height; size_t forward_steps = random_in_range(1, forward_steps_max_random_boundary); for (size_t h = top_height + 1; h < top_height + 1 + forward_steps; ++h) { bcs_stub.m_db_blocks.push_back(bcs_stub_t::block_t()); bcs_stub_t::block_t &b = bcs_stub.m_db_blocks.back(); b.height = h; b.fees.resize(random_in_range(0, 10)); for (size_t j = 0; j < b.fees.size(); ++j) { uint64_t fee = random_in_range(0, COIN); b.fees[j] = fee; m_tx_fee_median.push_item(fee, h); m_naive_median.push_item(fee, h); } bcs_stub.update_tx_fee_median(); bcs_stub.update_naive_fee_median(); ASSERT_EQ(m_naive_median.get_median(), m_tx_fee_median.get_median()); } } m_db.commit_transaction(); // save initial data for tx fee median cache to db bcs_stub.save_to_file(naive_median_serialization_filename); m_naive_median.print_to_file(folder_name + "/naive_median_2.txt"); }