// Copyright (c) 2019-2023, The Monero Project // // All rights reserved. // // Redistribution and use in source and binary forms, with or without modification, are // permitted provided that the following conditions are met: // // 1. Redistributions of source code must retain the above copyright notice, this list of // conditions and the following disclaimer. // // 2. Redistributions in binary form must reproduce the above copyright notice, this list // of conditions and the following disclaimer in the documentation and/or other // materials provided with the distribution. // // 3. Neither the name of the copyright holder nor the names of its contributors may be // used to endorse or promote products derived from this software without specific // prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY // EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL // THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, // PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, // STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF // THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #define IN_UNIT_TESTS #include "gtest/gtest.h" #include "cryptonote_core/blockchain.h" #include "cryptonote_core/tx_pool.h" #include "cryptonote_core/cryptonote_core.h" #include "blockchain_db/testdb.h" #define TEST_LONG_TERM_BLOCK_WEIGHT_WINDOW 5000 namespace { class TestDB: public cryptonote::BaseTestDB { private: struct block_t { size_t weight; uint64_t long_term_weight; }; public: TestDB() { m_open = true; } virtual void add_block( const cryptonote::block& blk , size_t block_weight , uint64_t long_term_block_weight , const cryptonote::difficulty_type& cumulative_difficulty , const uint64_t& coins_generated , uint64_t num_rct_outs , const crypto::hash& blk_hash ) override { blocks.push_back({block_weight, long_term_block_weight}); } virtual uint64_t height() const override { return blocks.size(); } virtual size_t get_block_weight(const uint64_t &h) const override { return blocks[h].weight; } virtual uint64_t get_block_long_term_weight(const uint64_t &h) const override { return blocks[h].long_term_weight; } virtual std::vector get_block_weights(uint64_t start_height, size_t count) const override { std::vector ret; ret.reserve(count); while (count-- && start_height < blocks.size()) ret.push_back(blocks[start_height++].weight); return ret; } virtual std::vector get_long_term_block_weights(uint64_t start_height, size_t count) const override { std::vector ret; ret.reserve(count); while (count-- && start_height < blocks.size()) ret.push_back(blocks[start_height++].long_term_weight); return ret; } virtual crypto::hash get_block_hash_from_height(const uint64_t &height) const override { crypto::hash hash = crypto::null_hash; *(uint64_t*)&hash = height; return hash; } virtual crypto::hash top_block_hash(uint64_t *block_height = NULL) const override { uint64_t h = height(); crypto::hash top = crypto::null_hash; if (h) *(uint64_t*)&top = h - 1; if (block_height) *block_height = h - 1; return top; } virtual void pop_block(cryptonote::block &blk, std::vector &txs) override { blocks.pop_back(); } private: std::vector blocks; }; static uint32_t lcg_seed = 0; static uint32_t lcg() { lcg_seed = (lcg_seed * 0x100000001b3 + 0xcbf29ce484222325) & 0xffffffff; return lcg_seed; } } struct BlockchainAndPool { cryptonote::tx_memory_pool txpool; cryptonote::Blockchain bc; BlockchainAndPool(): txpool(bc), bc(txpool) {} }; #define PREFIX_WINDOW(hf_version,window) \ BlockchainAndPool bap; \ cryptonote::Blockchain *bc = &bap.bc; \ struct get_test_options { \ const std::pair hard_forks[3]; \ const cryptonote::test_options test_options = { \ hard_forks, \ window, \ }; \ get_test_options(): hard_forks{std::make_pair(1, (uint64_t)0), std::make_pair((uint8_t)hf_version, (uint64_t)1), std::make_pair((uint8_t)0, (uint64_t)0)} {} \ } opts; \ bool r = bc->init(new TestDB(), cryptonote::FAKECHAIN, true, &opts.test_options, 0, NULL); \ ASSERT_TRUE(r) #define PREFIX(hf_version) PREFIX_WINDOW(hf_version, TEST_LONG_TERM_BLOCK_WEIGHT_WINDOW) TEST(long_term_block_weight, empty_short) { PREFIX(9); ASSERT_TRUE(bc->update_next_cumulative_weight_limit()); ASSERT_EQ(bc->get_current_cumulative_block_weight_median(), CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5); ASSERT_EQ(bc->get_current_cumulative_block_weight_limit(), CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5 * 2); } TEST(long_term_block_weight, identical_before_fork) { PREFIX(9); for (uint64_t h = 1; h < 10 * TEST_LONG_TERM_BLOCK_WEIGHT_WINDOW; ++h) { size_t w = h < CRYPTONOTE_REWARD_BLOCKS_WINDOW ? CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5 : bc->get_current_cumulative_block_weight_limit(); uint64_t ltw = bc->get_next_long_term_block_weight(w); bc->get_db().add_block(std::make_pair(cryptonote::block(), ""), w, ltw, h, h, {}); ASSERT_TRUE(bc->update_next_cumulative_weight_limit()); } for (uint64_t h = 0; h < 10 * TEST_LONG_TERM_BLOCK_WEIGHT_WINDOW; ++h) { ASSERT_EQ(bc->get_db().get_block_long_term_weight(h), bc->get_db().get_block_weight(h)); } } TEST(long_term_block_weight, identical_after_fork_before_long_term_window) { PREFIX(10); for (uint64_t h = 1; h <= TEST_LONG_TERM_BLOCK_WEIGHT_WINDOW; ++h) { size_t w = h < TEST_LONG_TERM_BLOCK_WEIGHT_WINDOW ? CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5 : bc->get_current_cumulative_block_weight_limit(); uint64_t ltw = bc->get_next_long_term_block_weight(w); bc->get_db().add_block(std::make_pair(cryptonote::block(), ""), w, ltw, h, h, {}); ASSERT_TRUE(bc->update_next_cumulative_weight_limit()); } for (uint64_t h = 0; h < TEST_LONG_TERM_BLOCK_WEIGHT_WINDOW; ++h) { ASSERT_EQ(bc->get_db().get_block_long_term_weight(h), bc->get_db().get_block_weight(h)); } } TEST(long_term_block_weight, ceiling_at_30000000) { PREFIX(10); for (uint64_t h = 0; h < TEST_LONG_TERM_BLOCK_WEIGHT_WINDOW + TEST_LONG_TERM_BLOCK_WEIGHT_WINDOW / 2 - 1; ++h) { size_t w = h < TEST_LONG_TERM_BLOCK_WEIGHT_WINDOW ? CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5 : bc->get_current_cumulative_block_weight_limit(); uint64_t ltw = bc->get_next_long_term_block_weight(w); bc->get_db().add_block(std::make_pair(cryptonote::block(), ""), w, ltw, h, h, {}); ASSERT_TRUE(bc->update_next_cumulative_weight_limit()); } ASSERT_EQ(bc->get_current_cumulative_block_weight_median(), 15000000); ASSERT_EQ(bc->get_current_cumulative_block_weight_limit(), 30000000); } TEST(long_term_block_weight, multi_pop) { PREFIX(10); for (uint64_t h = 1; h <= TEST_LONG_TERM_BLOCK_WEIGHT_WINDOW + 20; ++h) { size_t w = h < TEST_LONG_TERM_BLOCK_WEIGHT_WINDOW ? CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5 : bc->get_current_cumulative_block_weight_limit(); uint64_t ltw = bc->get_next_long_term_block_weight(w); bc->get_db().add_block(std::make_pair(cryptonote::block(), ""), w, ltw, h, h, {}); ASSERT_TRUE(bc->update_next_cumulative_weight_limit()); } const uint64_t effective_median = bc->get_current_cumulative_block_weight_median(); const uint64_t effective_limit = bc->get_current_cumulative_block_weight_limit(); const uint64_t num_pop = 4; for (uint64_t h = 0; h < num_pop; ++h) { size_t w = bc->get_current_cumulative_block_weight_limit(); uint64_t ltw = bc->get_next_long_term_block_weight(w); bc->get_db().add_block(std::make_pair(cryptonote::block(), ""), w, ltw, h, h, {}); ASSERT_TRUE(bc->update_next_cumulative_weight_limit()); } cryptonote::block b; std::vector txs; for (uint64_t h = 0; h < num_pop; ++h) bc->get_db().pop_block(b, txs); ASSERT_TRUE(bc->update_next_cumulative_weight_limit()); ASSERT_EQ(effective_median, bc->get_current_cumulative_block_weight_median()); ASSERT_EQ(effective_limit, bc->get_current_cumulative_block_weight_limit()); } TEST(long_term_block_weight, multiple_updates) { PREFIX(10); for (uint64_t h = 1; h <= 3 * TEST_LONG_TERM_BLOCK_WEIGHT_WINDOW; ++h) { size_t w = h < TEST_LONG_TERM_BLOCK_WEIGHT_WINDOW ? CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5 : bc->get_current_cumulative_block_weight_limit(); uint64_t ltw = bc->get_next_long_term_block_weight(w); bc->get_db().add_block(std::make_pair(cryptonote::block(), ""), w, ltw, h, h, {}); ASSERT_TRUE(bc->update_next_cumulative_weight_limit()); const uint64_t effective_median = bc->get_current_cumulative_block_weight_median(); const uint64_t effective_limit = bc->get_current_cumulative_block_weight_limit(); ASSERT_TRUE(bc->update_next_cumulative_weight_limit()); ASSERT_EQ(effective_median, bc->get_current_cumulative_block_weight_median()); ASSERT_EQ(effective_limit, bc->get_current_cumulative_block_weight_limit()); ASSERT_TRUE(bc->update_next_cumulative_weight_limit()); ASSERT_EQ(effective_median, bc->get_current_cumulative_block_weight_median()); ASSERT_EQ(effective_limit, bc->get_current_cumulative_block_weight_limit()); ASSERT_TRUE(bc->update_next_cumulative_weight_limit()); ASSERT_EQ(effective_median, bc->get_current_cumulative_block_weight_median()); ASSERT_EQ(effective_limit, bc->get_current_cumulative_block_weight_limit()); } } TEST(long_term_block_weight, pop_invariant_max) { PREFIX(10); for (uint64_t h = 1; h < TEST_LONG_TERM_BLOCK_WEIGHT_WINDOW - 10; ++h) { size_t w = bc->get_db().height() < TEST_LONG_TERM_BLOCK_WEIGHT_WINDOW ? CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5 : bc->get_current_cumulative_block_weight_limit(); uint64_t ltw = bc->get_next_long_term_block_weight(w); bc->get_db().add_block(std::make_pair(cryptonote::block(), ""), w, ltw, h, h, {}); ASSERT_TRUE(bc->update_next_cumulative_weight_limit()); } for (int n = 0; n < 1000; ++n) { // pop some blocks, then add some more int remove = 1 + (n * 17) % 8; int add = (n * 23) % 12; // save long term block weights we're about to remove uint64_t old_ltbw[16], h0 = bc->get_db().height() - remove - 1; for (int i = -2; i < remove; ++i) { old_ltbw[i + 2] = bc->get_db().get_block_long_term_weight(h0 + i); } for (int i = 0; i < remove; ++i) { cryptonote::block b; std::vector txs; bc->get_db().pop_block(b, txs); ASSERT_TRUE(bc->update_next_cumulative_weight_limit()); } for (int i = 0; i < add; ++i) { size_t w = bc->get_db().height() < TEST_LONG_TERM_BLOCK_WEIGHT_WINDOW ? CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5 : bc->get_current_cumulative_block_weight_limit(); uint64_t ltw = bc->get_next_long_term_block_weight(w); bc->get_db().add_block(std::make_pair(cryptonote::block(), ""), w, ltw, bc->get_db().height(), bc->get_db().height(), {}); ASSERT_TRUE(bc->update_next_cumulative_weight_limit()); } // check the new values are the same as the old ones for (int i = -2; i < std::min(add, remove); ++i) { ASSERT_EQ(bc->get_db().get_block_long_term_weight(h0 + i), old_ltbw[i + 2]); } } } TEST(long_term_block_weight, pop_invariant_random) { PREFIX(10); for (uint64_t h = 1; h < 2 * TEST_LONG_TERM_BLOCK_WEIGHT_WINDOW - 10; ++h) { lcg_seed = bc->get_db().height(); uint32_t r = lcg(); size_t w = bc->get_db().height() < TEST_LONG_TERM_BLOCK_WEIGHT_WINDOW ? CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5 : (r % bc->get_current_cumulative_block_weight_limit()); uint64_t ltw = bc->get_next_long_term_block_weight(w); bc->get_db().add_block(std::make_pair(cryptonote::block(), ""), w, ltw, h, h, {}); ASSERT_TRUE(bc->update_next_cumulative_weight_limit()); } for (int n = 0; n < 1000; ++n) { // pop some blocks, then add some more int remove = 1 + (n * 17) % 8; int add = (n * 23) % 123; // save long term block weights we're about to remove uint64_t old_ltbw[16], h0 = bc->get_db().height() - remove - 1; for (int i = -2; i < remove; ++i) { old_ltbw[i + 2] = bc->get_db().get_block_long_term_weight(h0 + i); } for (int i = 0; i < remove; ++i) { cryptonote::block b; std::vector txs; bc->get_db().pop_block(b, txs); ASSERT_TRUE(bc->update_next_cumulative_weight_limit()); const uint64_t effective_median = bc->get_current_cumulative_block_weight_median(); const uint64_t effective_limit = bc->get_current_cumulative_block_weight_limit(); ASSERT_TRUE(bc->update_next_cumulative_weight_limit()); ASSERT_EQ(effective_median, bc->get_current_cumulative_block_weight_median()); ASSERT_EQ(effective_limit, bc->get_current_cumulative_block_weight_limit()); } for (int i = 0; i < add; ++i) { lcg_seed = bc->get_db().height(); uint32_t r = lcg(); size_t w = bc->get_db().height() < TEST_LONG_TERM_BLOCK_WEIGHT_WINDOW ? CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5 : (r % bc->get_current_cumulative_block_weight_limit()); uint64_t ltw = bc->get_next_long_term_block_weight(w); bc->get_db().add_block(std::make_pair(cryptonote::block(), ""), w, ltw, bc->get_db().height(), bc->get_db().height(), {}); ASSERT_TRUE(bc->update_next_cumulative_weight_limit()); const uint64_t effective_median = bc->get_current_cumulative_block_weight_median(); const uint64_t effective_limit = bc->get_current_cumulative_block_weight_limit(); ASSERT_TRUE(bc->update_next_cumulative_weight_limit()); ASSERT_EQ(effective_median, bc->get_current_cumulative_block_weight_median()); ASSERT_EQ(effective_limit, bc->get_current_cumulative_block_weight_limit()); } // check the new values are the same as the old ones for (int i = -2; i < std::min(add, remove); ++i) { ASSERT_EQ(bc->get_db().get_block_long_term_weight(h0 + i), old_ltbw[i + 2]); } } } TEST(long_term_block_weight, long_growth_spike_and_drop) { PREFIX(10); uint64_t long_term_effective_median_block_weight; // constant init for (uint64_t h = 0; h < TEST_LONG_TERM_BLOCK_WEIGHT_WINDOW; ++h) { size_t w = CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5; uint64_t ltw = bc->get_next_long_term_block_weight(w); bc->get_db().add_block(std::make_pair(cryptonote::block(), ""), w, ltw, h, h, {}); ASSERT_TRUE(bc->update_next_cumulative_weight_limit(&long_term_effective_median_block_weight)); } ASSERT_EQ(long_term_effective_median_block_weight, 300000); // slow 10% yearly for a year (scaled down by 100000 / TEST_LONG_TERM_BLOCK_WEIGHT_WINDOW) -> 8% change for (uint64_t h = 0; h < 365 * 720 * TEST_LONG_TERM_BLOCK_WEIGHT_WINDOW / 100000; ++h) { //size_t w = bc->get_current_cumulative_block_weight_median() * rate; float t = h / float(365 * 720 * TEST_LONG_TERM_BLOCK_WEIGHT_WINDOW / 100000); size_t w = 300000 + t * 30000; uint64_t ltw = bc->get_next_long_term_block_weight(w); bc->get_db().add_block(std::make_pair(cryptonote::block(), ""), w, ltw, h, h, {}); ASSERT_TRUE(bc->update_next_cumulative_weight_limit(&long_term_effective_median_block_weight)); } ASSERT_GT(long_term_effective_median_block_weight, 300000 * 1.07); ASSERT_LT(long_term_effective_median_block_weight, 300000 * 1.09); // spike over three weeks - does not move much for (uint64_t h = 0; h < 21 * 720 * TEST_LONG_TERM_BLOCK_WEIGHT_WINDOW / 100000; ++h) { size_t w = bc->get_current_cumulative_block_weight_limit(); uint64_t ltw = bc->get_next_long_term_block_weight(w); bc->get_db().add_block(std::make_pair(cryptonote::block(), ""), w, ltw, h, h, {}); ASSERT_TRUE(bc->update_next_cumulative_weight_limit(&long_term_effective_median_block_weight)); } ASSERT_GT(long_term_effective_median_block_weight, 300000 * 1.07); ASSERT_LT(long_term_effective_median_block_weight, 300000 * 1.09); // drop - does not move much for (uint64_t h = 0; h < 21 * 720 * TEST_LONG_TERM_BLOCK_WEIGHT_WINDOW / 100000; ++h) { size_t w = bc->get_current_cumulative_block_weight_median() * .25; uint64_t ltw = bc->get_next_long_term_block_weight(w); bc->get_db().add_block(std::make_pair(cryptonote::block(), ""), w, ltw, h, h, {}); ASSERT_TRUE(bc->update_next_cumulative_weight_limit(&long_term_effective_median_block_weight)); } ASSERT_GT(long_term_effective_median_block_weight, 300000 * 1.07); ASSERT_LT(long_term_effective_median_block_weight, 300000 * 1.09); }