// Copyright (c) 2014, 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. #include "gtest/gtest.h" #include "crypto/crypto.h" #include "cryptonote_basic/cryptonote_format_utils.h" #include "curve_trees.h" #include "fcmp_pp/curve_trees.h" #include "fcmp_pp/fcmp_pp_types.h" #include "fcmp_pp/tree_cache.h" #include "serialization/binary_utils.h" #include "string_tools.h" #include //---------------------------------------------------------------------------------------------------------------------- #define INIT_SYNC_TEST(tree_depth) \ static const std::size_t selene_chunk_width = 2; \ static const std::size_t helios_chunk_width = 3; \ \ uint64_t n_leaves_needed = 0; \ auto curve_trees = test::init_curve_trees_test(selene_chunk_width, \ helios_chunk_width, \ tree_depth, \ n_leaves_needed); \ \ /* Each block, we'll sync a max of just over 2 full chunks, to make sure we're saving all path elems even \ when the data we need is not in the last chunk */ \ static const std::size_t max_outputs_per_block = (2*selene_chunk_width)+1; \ //---------------------------------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------------------------------- TEST(tree_cache, register_output) { using namespace fcmp_pp; // 1. Init auto curve_trees = fcmp_pp::curve_trees::curve_trees_v1(); auto tree_cache = new fcmp_pp::curve_trees::TreeCache(curve_trees); const std::size_t INIT_LEAVES = 10; auto outputs = test::generate_random_outputs(0, INIT_LEAVES); CHECK_AND_ASSERT_THROW_MES(outputs.size() == INIT_LEAVES, "unexpected size of outputs"); // Mock values const auto output = outputs.at(0).output_pair; // 2. Register output - valid ASSERT_TRUE(tree_cache->register_output(output)); // 3. Register same output again - already registered ASSERT_FALSE(tree_cache->register_output(output)); // 3a. Register a copy of the same output again - already registered const auto output_copy = output; ASSERT_FALSE(tree_cache->register_output(output_copy)); // 4. Register another output with the same output pubkey as existing, different commitment - valid const OutputPair output_new_commitment = LegacyOutputPair{{ output_pubkey_cref(output), commitment_cref(outputs.at(1).output_pair) }}; ASSERT_EQ(output_pubkey_cref(output_new_commitment), output_pubkey_cref(output)); ASSERT_NE(commitment_cref(output_new_commitment), commitment_cref(output)); ASSERT_TRUE(tree_cache->register_output(output_new_commitment)); // 5. Register another output with same output pubkey and commitment, but different output pair type - valid ASSERT_TRUE(std::holds_alternative(output)); const OutputPair output_diff_type = CarrotOutputPairV1{{ output_pubkey_cref(output), commitment_cref(output) }}; ASSERT_EQ(output_pubkey_cref(output_diff_type), output_pubkey_cref(output)); ASSERT_EQ(commitment_cref(output_diff_type), commitment_cref(output)); ASSERT_TRUE(!(output == output_diff_type)); ASSERT_TRUE(tree_cache->register_output(output_diff_type)); } //---------------------------------------------------------------------------------------------------------------------- TEST(tree_cache, sync_block_simple) { // 1. Init static const std::size_t INIT_LEAVES = 10; auto curve_trees = fcmp_pp::curve_trees::curve_trees_v1(); auto tree_cache = new fcmp_pp::curve_trees::TreeCache(curve_trees); auto outputs = test::generate_random_outputs(0, INIT_LEAVES); CHECK_AND_ASSERT_THROW_MES(outputs.size() == INIT_LEAVES, "unexpected size of outputs"); // Mock values const uint64_t last_locked_block_idx = 1; const auto output = outputs[0].output_pair; // 2. Register output ASSERT_TRUE(tree_cache->register_output(output)); // 3. Sync the block of outputs crypto::hash block_hash{0x01}; crypto::hash prev_block_hash{}; tree_cache->sync_block(0, block_hash, prev_block_hash, {{ last_locked_block_idx, outputs }}); // 4. Sync 1 more block so the outputs unlock and enter the tree prev_block_hash = block_hash; block_hash = crypto::hash{0x02}; tree_cache->sync_block(last_locked_block_idx, block_hash, prev_block_hash, {}); // 5. Get the output's path in the tree CurveTreesV1::Path output_path; ASSERT_TRUE(tree_cache->get_output_path(output, output_path)); // If the c1 layer chunk width < INIT_LEAVES, the test won't use expected values below ASSERT_GE(curve_trees->m_c1_width, INIT_LEAVES); ASSERT_EQ(output_path.leaves.size(), INIT_LEAVES); ASSERT_TRUE(curve_trees->audit_path(output_path, output, INIT_LEAVES)); } //---------------------------------------------------------------------------------------------------------------------- TEST(tree_cache, sync_n_chunks_of_blocks) { // 1. Init static const std::size_t INIT_LEAVES = 10; static const std::size_t N_CHUNKS = 10; static const std::size_t N_BLOCKS_PER_CHUNK = 100; const std::size_t rand_chunk = crypto::rand_idx(N_CHUNKS); const std::size_t rand_block = crypto::rand_idx(N_BLOCKS_PER_CHUNK - 1); const std::size_t rand_leaf = crypto::rand_idx(INIT_LEAVES); auto curve_trees = fcmp_pp::curve_trees::curve_trees_v1(); auto tree_cache = new fcmp_pp::curve_trees::TreeCache(curve_trees); const crypto::hash mock_block_hash{}; // 2. Make chunks of blocks std::vector> chunks_of_block_hashes; std::vector> chunks_of_outputs; std::size_t leaf_count = 0; uint64_t last_locked_block_idx = 0; fcmp_pp::OutputPair output; for (std::size_t i = 0; i < N_CHUNKS; ++i) { std::vector block_hashes; std::vector outs; for (std::size_t j = 0; j < N_BLOCKS_PER_CHUNK; ++j) { auto outputs = test::generate_random_outputs(leaf_count, INIT_LEAVES); leaf_count += INIT_LEAVES; const auto last_locked_block = 1 + i * N_BLOCKS_PER_CHUNK + j; if (i == rand_chunk && j == rand_block) { last_locked_block_idx = last_locked_block; output = outputs[rand_leaf].output_pair; } outs.push_back({{ last_locked_block, std::move(outputs) }}); block_hashes.push_back(mock_block_hash); } chunks_of_block_hashes.push_back(std::move(block_hashes)); chunks_of_outputs.push_back(std::move(outs)); } ASSERT_TRUE(last_locked_block_idx > 0); // 3. Register output ASSERT_TRUE(tree_cache->register_output(output)); // 4. Sync the chunks of blocks for (std::size_t i = 0; i < N_CHUNKS; ++i) { const uint64_t start_block_idx = i * N_BLOCKS_PER_CHUNK; fcmp_pp::curve_trees::TreeCache::CacheStateChange cache_state_change; tree_cache->prepare_to_grow_cache(start_block_idx, mock_block_hash, chunks_of_block_hashes[i], chunks_of_outputs[i], cache_state_change); tree_cache->grow_cache(start_block_idx, chunks_of_block_hashes[i], std::move(cache_state_change)); // Audit the output's path in the tree CurveTreesV1::Path output_path; ASSERT_TRUE(tree_cache->get_output_path(output, output_path)); if (!output_path.empty()) { ASSERT_TRUE(curve_trees->audit_path(output_path, output, tree_cache->get_n_leaf_tuples())); } } // 5. Make sure the output was actually added to the tree CurveTreesV1::Path output_path; ASSERT_TRUE(tree_cache->get_output_path(output, output_path)); ASSERT_TRUE(!output_path.empty()); } //---------------------------------------------------------------------------------------------------------------------- TEST(tree_cache, sync_n_blocks_register_n_outputs) { // Init static const std::size_t tree_depth = 6; INIT_SYNC_TEST(tree_depth); // Sync until we've synced all the leaves needed to get to the desired tree depth auto tree_cache = new fcmp_pp::curve_trees::TreeCache(curve_trees); uint64_t block_idx = 0; uint64_t n_outputs = 0; uint64_t n_unlocked_outputs = 0; crypto::hash prev_block_hash = crypto::hash{}; // Keep track of all registered outputs so that we can make sure ALL output paths update correctly every block std::vector registered_outputs; registered_outputs.reserve(n_leaves_needed); while (n_unlocked_outputs < n_leaves_needed) { const auto sync_n_outputs = (block_idx % max_outputs_per_block) + 1; LOG_PRINT_L1("Syncing "<< sync_n_outputs << " outputs in block " << (block_idx+1) << " (" << n_unlocked_outputs << " unlocked / " << n_leaves_needed << " outputs)"); auto outputs = test::generate_random_outputs(n_outputs, sync_n_outputs); CHECK_AND_ASSERT_THROW_MES(outputs.size() == sync_n_outputs, "unexpected size of outputs"); // Pick an output to register auto output_to_register = block_idx % sync_n_outputs; const auto output = outputs[output_to_register].output_pair; MDEBUG("Registering output " << n_outputs + output_to_register); // Register the output const uint64_t last_locked_block_idx = block_idx + 1; ASSERT_TRUE(tree_cache->register_output(output)); // Sync the outputs generated above crypto::hash block_hash; crypto::cn_fast_hash(&block_idx, sizeof(std::size_t), block_hash); tree_cache->sync_block(block_idx, block_hash, prev_block_hash, {{ last_locked_block_idx, outputs }}); n_unlocked_outputs = n_outputs; n_outputs += sync_n_outputs; // Audit all registered output paths for (const auto &o : registered_outputs) { CurveTreesV1::Path output_path; ASSERT_TRUE(tree_cache->get_output_path(o, output_path)); ASSERT_TRUE(curve_trees->audit_path(output_path, o, n_unlocked_outputs)); } // Update for next iteration registered_outputs.push_back(output); prev_block_hash = block_hash; ++block_idx; } // Sync 1 more empty block so all outputs unlock crypto::hash block_hash; crypto::cn_fast_hash(&block_idx, sizeof(std::size_t), block_hash); tree_cache->sync_block(block_idx, block_hash, prev_block_hash, {}); // Check all registered output paths for (const auto &o : registered_outputs) { CurveTreesV1::Path output_path; ASSERT_TRUE(tree_cache->get_output_path(o, output_path)); ASSERT_TRUE(curve_trees->audit_path(output_path, o, n_unlocked_outputs)); } } //---------------------------------------------------------------------------------------------------------------------- TEST(tree_cache, sync_n_blocks_register_one_output) { // Init static const std::size_t tree_depth = 5; INIT_SYNC_TEST(tree_depth); // For every output, sync until the tree reaches the expected tree depth, registering 1 unique output each separate // sync. We audit the output path every block while syncing for (std::size_t i = 0; i < n_leaves_needed; ++i) { LOG_PRINT_L1("Register output " << (i+1) << " / " << n_leaves_needed); auto tree_cache = new fcmp_pp::curve_trees::TreeCache(curve_trees); fcmp_pp::OutputPair registered_output; bool registered = false; crypto::hash prev_block_hash = crypto::hash{}; uint64_t block_idx = 0; uint64_t n_outputs = 0; uint64_t n_unlocked_outputs = 0; while (n_unlocked_outputs < n_leaves_needed) { const auto sync_n_outputs = (block_idx % max_outputs_per_block) + 1; MDEBUG("Syncing "<< sync_n_outputs << " outputs in block " << (block_idx+1) << " (" << n_unlocked_outputs << " unlocked / " << n_leaves_needed << " outputs)"); auto outputs = test::generate_random_outputs(n_outputs, sync_n_outputs); CHECK_AND_ASSERT_THROW_MES(outputs.size() == sync_n_outputs, "unexpected size of outputs"); // Check if this chunk includes the output we're supposed to register const uint64_t last_locked_block_idx = block_idx + 1; bool just_registered = n_outputs <= i && i < (n_outputs + sync_n_outputs); if (just_registered) { MDEBUG("Registering output"); ASSERT_FALSE(registered); auto output_to_register = i - n_outputs; const auto output = outputs[output_to_register].output_pair; ASSERT_TRUE(tree_cache->register_output(output)); registered = true; registered_output = output; } // Sync the outputs generated above crypto::hash block_hash; crypto::cn_fast_hash(&block_idx, sizeof(uint64_t), block_hash); tree_cache->sync_block(block_idx, block_hash, prev_block_hash, {{ last_locked_block_idx, outputs }}); n_unlocked_outputs = n_outputs; n_outputs += sync_n_outputs; // Audit registered output path if (registered && !just_registered) { CurveTreesV1::Path output_path; ASSERT_TRUE(tree_cache->get_output_path(registered_output, output_path)); ASSERT_TRUE(curve_trees->audit_path(output_path, registered_output, n_unlocked_outputs)); } // Update for next iteration prev_block_hash = block_hash; ++block_idx; } ASSERT_TRUE(registered); // Sync 1 more empty block so all outputs unlock crypto::hash block_hash; crypto::cn_fast_hash(&block_idx, sizeof(std::size_t), block_hash); tree_cache->sync_block(block_idx, block_hash, prev_block_hash, {}); CurveTreesV1::Path output_path; ASSERT_TRUE(tree_cache->get_output_path(registered_output, output_path)); ASSERT_TRUE(curve_trees->audit_path(output_path, registered_output, n_unlocked_outputs)); } } //---------------------------------------------------------------------------------------------------------------------- TEST(tree_cache, sync_past_max_reorg_depth) { // This test is useful for making sure TreeSync syncs correctly even after syncing past the reorg depth. Internally // the class drops cached values from beyond the reorg depth. This test makes sure registered output paths still // update correctly even after cached values from the tree get dropped. // Init static const std::size_t max_reorg_depth = 1; static const std::size_t tree_depth = 5; INIT_SYNC_TEST(tree_depth); // For every output, sync until the tree reaches the expected tree depth AND we sync past the max reorg depth, // registering 1 unique output each separate sync. We audit the output path every block while syncing for (std::size_t i = 0; i < n_leaves_needed; ++i) { LOG_PRINT_L1("Register output " << (i+1) << " / " << n_leaves_needed); // Sync until we've synced all the leaves needed to get to the desired tree depth auto tree_cache = new fcmp_pp::curve_trees::TreeCache(curve_trees, max_reorg_depth); uint64_t block_idx = 0; uint64_t n_outputs = 0; uint64_t n_unlocked_outputs = 0; crypto::hash prev_block_hash = crypto::hash{}; fcmp_pp::OutputPair registered_output; bool registered = false; while (n_unlocked_outputs < n_leaves_needed || block_idx <= max_reorg_depth) { // TODO: de-dup this code with above test const auto sync_n_outputs = (block_idx % max_outputs_per_block) + 1; MDEBUG("Syncing "<< sync_n_outputs << " outputs in block " << block_idx); auto outputs = test::generate_random_outputs(n_outputs, sync_n_outputs); CHECK_AND_ASSERT_THROW_MES(outputs.size() == sync_n_outputs, "unexpected size of outputs"); // Check if this chunk includes the output we're supposed to register const uint64_t last_locked_block_idx = block_idx + 1; const bool just_registered = n_outputs <= i && i < (n_outputs + sync_n_outputs); if (just_registered) { ASSERT_FALSE(registered); auto output_to_register = i - n_outputs; const auto output = outputs[output_to_register].output_pair; ASSERT_TRUE(tree_cache->register_output(output)); registered = true; registered_output = output; } // Sync the outputs generated above crypto::hash block_hash; crypto::cn_fast_hash(&block_idx, sizeof(uint64_t), block_hash); tree_cache->sync_block(block_idx, block_hash, prev_block_hash, {{ last_locked_block_idx, outputs }}); n_unlocked_outputs = n_outputs; n_outputs += sync_n_outputs; // Audit registered output path if (registered && !just_registered) { CurveTreesV1::Path output_path; ASSERT_TRUE(tree_cache->get_output_path(registered_output, output_path)); ASSERT_TRUE(curve_trees->audit_path(output_path, registered_output, n_unlocked_outputs)); } // Update for next iteration prev_block_hash = block_hash; ++block_idx; } ASSERT_TRUE(registered); // Sync 1 more empty block so all outputs unlock crypto::hash block_hash; crypto::cn_fast_hash(&block_idx, sizeof(std::size_t), block_hash); tree_cache->sync_block(block_idx, block_hash, prev_block_hash, {}); CurveTreesV1::Path output_path; ASSERT_TRUE(tree_cache->get_output_path(registered_output, output_path)); ASSERT_TRUE(curve_trees->audit_path(output_path, registered_output, n_unlocked_outputs)); } } //---------------------------------------------------------------------------------------------------------------------- TEST(tree_cache, reorg_after_register) { // Init static const std::size_t tree_depth = 5; INIT_SYNC_TEST(tree_depth); // For every output, sync until the tree reaches the expected tree depth, registering 1 unique output each separate // sync. We audit the output path every block while syncing for (std::size_t i = 0; i < n_leaves_needed; ++i) { LOG_PRINT_L1("Register output " << (i+1) << " / " << n_leaves_needed); auto tree_cache = new fcmp_pp::curve_trees::TreeCache(curve_trees); fcmp_pp::OutputPair registered_output; bool registered = false; uint64_t last_locked_block_idx = 0; crypto::hash last_locked_block_hash; crypto::hash prev_block_hash = crypto::hash{}; uint64_t block_idx = 0; uint64_t n_outputs = 0; uint64_t n_unlocked_outputs = 0; std::vector n_outputs_synced_by_block; while (n_unlocked_outputs < n_leaves_needed) { if (registered && block_idx > (last_locked_block_idx + 1)) { uint64_t cur_block_idx = block_idx; ASSERT_EQ(n_outputs_synced_by_block.size(), block_idx); LOG_PRINT_L1("Popping blocks back to block " << last_locked_block_idx + 1 << " , then re-syncing"); auto n_outputs_unlocked = [&n_outputs_synced_by_block](uint64_t blk_idx) -> uint64_t { return blk_idx < 2 ? 0 : n_outputs_synced_by_block[blk_idx - 2]; }; // Pop blocks until the included block is the top block while (cur_block_idx > (last_locked_block_idx + 1)) { ASSERT_TRUE(tree_cache->pop_block()); --cur_block_idx; MDEBUG("cur_block_idx: " << cur_block_idx << " , n_outputs_unlocked(cur_block_idx): " << n_outputs_unlocked(cur_block_idx)); CurveTreesV1::Path output_path; ASSERT_TRUE(tree_cache->get_output_path(registered_output, output_path)); ASSERT_TRUE(curve_trees->audit_path(output_path, registered_output, n_outputs_unlocked(cur_block_idx))); } // Sync back up again until cur_block_idx == block_idx prev_block_hash = last_locked_block_hash; while (cur_block_idx < block_idx) { const auto sync_n_outputs = (cur_block_idx % max_outputs_per_block) + 1; MDEBUG("Re-syncing "<< sync_n_outputs << " outputs in block " << (cur_block_idx+1) << " (" << (n_outputs_unlocked(cur_block_idx)) << " unlocked / " << n_leaves_needed << " outputs)"); auto outputs = test::generate_random_outputs(n_outputs_unlocked(cur_block_idx+1), sync_n_outputs); CHECK_AND_ASSERT_THROW_MES(outputs.size() == sync_n_outputs, "unexpected size of outputs"); // Sync the outputs generated above crypto::hash block_hash; crypto::cn_fast_hash(&cur_block_idx, sizeof(uint64_t), block_hash); tree_cache->sync_block(cur_block_idx, block_hash, prev_block_hash, {{ cur_block_idx + 1, outputs }}); ++cur_block_idx; CurveTreesV1::Path output_path; ASSERT_TRUE(tree_cache->get_output_path(registered_output, output_path)); ASSERT_TRUE(curve_trees->audit_path(output_path, registered_output, n_outputs_unlocked(cur_block_idx))); prev_block_hash = block_hash; } } const auto sync_n_outputs = (block_idx % max_outputs_per_block) + 1; MDEBUG("Syncing "<< sync_n_outputs << " outputs in block " << (block_idx+1) << " (" << n_unlocked_outputs << " unlocked / " << n_leaves_needed << " outputs)"); auto outputs = test::generate_random_outputs(n_outputs, sync_n_outputs); CHECK_AND_ASSERT_THROW_MES(outputs.size() == sync_n_outputs, "unexpected size of outputs"); // Block metadata crypto::hash block_hash; crypto::cn_fast_hash(&block_idx, sizeof(uint64_t), block_hash); // Check if this chunk includes the output we're supposed to register const bool just_registered = n_outputs <= i && i < (n_outputs + sync_n_outputs); if (just_registered) { ASSERT_FALSE(registered); auto output_to_register = i - n_outputs; const auto output = outputs[output_to_register].output_pair; ASSERT_TRUE(tree_cache->register_output(output)); registered = true; registered_output = output; last_locked_block_idx = block_idx + 1; } if (registered && block_idx == last_locked_block_idx) last_locked_block_hash = block_hash; // Sync the outputs generated above tree_cache->sync_block(block_idx, block_hash, prev_block_hash, {{ block_idx + 1, outputs }}); n_unlocked_outputs = n_outputs; n_outputs += sync_n_outputs; n_outputs_synced_by_block.push_back(n_outputs); // Audit registered output path if (registered && !just_registered) { CurveTreesV1::Path output_path; ASSERT_TRUE(tree_cache->get_output_path(registered_output, output_path)); ASSERT_TRUE(curve_trees->audit_path(output_path, registered_output, n_unlocked_outputs)); } // Update for next iteration prev_block_hash = block_hash; ++block_idx; } ASSERT_TRUE(registered); } } //---------------------------------------------------------------------------------------------------------------------- TEST(tree_cache, register_after_reorg) { // Init static const std::size_t tree_depth = 5; INIT_SYNC_TEST(tree_depth); auto tree_cache = new fcmp_pp::curve_trees::TreeCache(curve_trees); // Sync until we reach expected tree depth uint64_t block_idx = 0; uint64_t n_outputs = 0; uint64_t n_unlocked_outputs = 0; std::vector n_outputs_synced_by_block; std::vector block_hashes; while (n_unlocked_outputs < n_leaves_needed) { const auto sync_n_outputs = (block_idx % max_outputs_per_block) + 1; LOG_PRINT_L1("Syncing "<< sync_n_outputs << " outputs in block " << (block_idx+1) << " (" << n_unlocked_outputs << " unlocked / " << n_leaves_needed << " outputs)"); auto outputs = test::generate_random_outputs(n_outputs, sync_n_outputs); CHECK_AND_ASSERT_THROW_MES(outputs.size() == sync_n_outputs, "unexpected size of outputs"); // Block metadata crypto::hash block_hash; crypto::cn_fast_hash(&block_idx, sizeof(uint64_t), block_hash); // Sync the outputs generated above const uint64_t last_locked_block_idx = block_idx + 1; tree_cache->sync_block(block_idx, block_hash, block_hashes.empty() ? crypto::hash{} : block_hashes.back(), {{ last_locked_block_idx, outputs }}); n_unlocked_outputs = n_outputs; n_outputs += sync_n_outputs; n_outputs_synced_by_block.push_back(n_outputs); block_hashes.push_back(block_hash); // Update for next iteration ++block_idx; } // Reorg 1 block // TODO: test reorg 1 block to block_idx blocks LOG_PRINT_L1("Popping 1 block"); ASSERT_TRUE(tree_cache->pop_block()); --block_idx; block_hashes.pop_back(); n_outputs_synced_by_block.pop_back(); n_outputs = n_outputs_synced_by_block.back(); n_unlocked_outputs = n_outputs_synced_by_block[n_outputs_synced_by_block.size() - 2]; // Register output and sync it in the next block LOG_PRINT_L1("Registering 1 output and syncing in next block"); auto outputs = test::generate_random_outputs(n_unlocked_outputs, 1); CHECK_AND_ASSERT_THROW_MES(outputs.size() == 1, "unexpected size of outputs"); const auto output = outputs[0].output_pair; ASSERT_TRUE(tree_cache->register_output(output)); // Block metadata crypto::hash block_hash; crypto::cn_fast_hash(&block_idx, sizeof(uint64_t), block_hash); // Sync the output generated above const uint64_t last_locked_block_idx = block_idx + 1; tree_cache->sync_block(block_idx, block_hash, block_hashes.empty() ? crypto::hash{} : block_hashes.back(), {{ last_locked_block_idx, outputs }}); block_hashes.push_back(block_hash); n_unlocked_outputs = n_outputs; // Output is not expected to enter the tree until next block CurveTreesV1::Path output_path; ASSERT_TRUE(tree_cache->get_output_path(output, output_path)); ASSERT_TRUE(output_path.leaves.empty() && output_path.c1_layers.empty() && output_path.c2_layers.empty()); // Sync 1 more block so the output unlocks and enters the tree crypto::cn_fast_hash(&last_locked_block_idx, sizeof(uint64_t), block_hash); tree_cache->sync_block(last_locked_block_idx, block_hash, block_hashes.back(), {}); n_unlocked_outputs += outputs.size(); ASSERT_TRUE(tree_cache->get_output_path(output, output_path)); ASSERT_TRUE(curve_trees->audit_path(output_path, output, n_unlocked_outputs)); } //---------------------------------------------------------------------------------------------------------------------- TEST(tree_cache, serialization) { // 1. Grow the tree with a registered output static const std::size_t INIT_LEAVES = 10; auto curve_trees = fcmp_pp::curve_trees::curve_trees_v1(); auto tree_cache = new fcmp_pp::curve_trees::TreeCache(curve_trees); auto outputs = test::generate_random_outputs(0, INIT_LEAVES); CHECK_AND_ASSERT_THROW_MES(outputs.size() == INIT_LEAVES, "unexpected size of outputs"); const uint64_t block_idx = 0; const uint64_t last_locked_block_idx = 1; const auto output = outputs[0].output_pair; ASSERT_TRUE(tree_cache->register_output(output)); crypto::hash block_hash{0x01}; crypto::hash prev_block_hash{}; tree_cache->sync_block(block_idx, block_hash, prev_block_hash, {{ last_locked_block_idx, outputs }}); // 2. Serialize the tree_cache object std::string blob; ASSERT_TRUE(serialization::dump_binary(*tree_cache, blob)); // 3. Make sure the output is present in the serialized string const std::string blob_hex = epee::string_tools::buff_to_hex_nodelimer(blob); ASSERT_TRUE(blob_hex.find(epee::string_tools::pod_to_hex(fcmp_pp::output_pubkey_cref(output))) != std::string::npos); ASSERT_TRUE(blob_hex.find(epee::string_tools::pod_to_hex(fcmp_pp::commitment_cref(output))) != std::string::npos); // 4. De-serialize the string into a new tree_cache2 object auto tree_cache2 = fcmp_pp::curve_trees::TreeCache(curve_trees); ASSERT_TRUE(serialization::parse_binary(blob, tree_cache2)); // 5. Sync 1 more block in tree_cache and tree_cache2 so the init outputs unlock and enter the trees prev_block_hash = block_hash; block_hash = crypto::hash{0x02}; tree_cache->sync_block(last_locked_block_idx, block_hash, prev_block_hash, {}); tree_cache2.sync_block(last_locked_block_idx, block_hash, prev_block_hash, {}); // 6. Make sure output's path is the same across both tree_cache and tree_cache2 CurveTreesV1::Path output_path; ASSERT_TRUE(tree_cache->get_output_path(output, output_path)); ASSERT_TRUE(curve_trees->audit_path(output_path, output, INIT_LEAVES)); CurveTreesV1::Path output_path2; ASSERT_TRUE(tree_cache2.get_output_path(output, output_path2)); // Checking the roots match and auditing path suffices for now, TODO: implement constant time eq // ASSERT_EQ(output_path, output_path2); auto get_root_str = [&curve_trees](const CurveTreesV1::Path &path) -> std::string { return path.c1_layers.size() > path.c2_layers.size() ? curve_trees->m_c1->to_string(path.c1_layers.back().front()) : curve_trees->m_c2->to_string(path.c2_layers.back().front()); }; const auto root_str = get_root_str(output_path); const auto root_str2 = get_root_str(output_path2); ASSERT_EQ(root_str, root_str2); ASSERT_TRUE(curve_trees->audit_path(output_path2, output, INIT_LEAVES)); // 7. Validate output counts ASSERT_EQ(tree_cache->get_output_count(), INIT_LEAVES); ASSERT_EQ(tree_cache2.get_output_count(), INIT_LEAVES); } //---------------------------------------------------------------------------------------------------------------------- // TODO: test edge cases: duplicate output when syncing, mismatched prev block hash in sync_block // TODO: clean up code // TODO: test that there are no dangling refs, refs are gc'd as expected //----------------------------------------------------------------------------------------------------------------------