// Copyright (c) 2025, 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 "misc_log_ex.h" #define IN_UNIT_TESTS //paired header #include "fake_pruned_blockchain.h" //local headers #include "fcmp_pp/curve_trees.h" #include "ringct/rctOps.h" #include "ringct/rctTypes.h" #include "tx_construction_helpers.h" #undef MONERO_DEFAULT_LOG_CATEGORY #define MONERO_DEFAULT_LOG_CATEGORY "unit_tests.fake_pruned_bc" namespace mock { //---------------------------------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------------------------------- static constexpr std::size_t selene_chunk_width = fcmp_pp::curve_trees::SELENE_CHUNK_WIDTH; static constexpr std::size_t helios_chunk_width = fcmp_pp::curve_trees::HELIOS_CHUNK_WIDTH; const auto curve_trees = fcmp_pp::curve_trees::curve_trees_v1(selene_chunk_width, helios_chunk_width); //---------------------------------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------------------------------- template static bool compare_curve_point(const typename C::Point &p1, const typename C::Point &p2) { const crypto::ec_point p1_compressed = C().to_bytes(p1); const crypto::ec_point p2_compressed = C().to_bytes(p2); return p1_compressed == p2_compressed; } //---------------------------------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------------------------------- template static bool compare_curve_layer(const std::vector &p1s, const std::vector &p2s) { if (p1s.size() != p2s.size()) return false; for (size_t i = 0; i < p1s.size(); ++i) { if (!compare_curve_point(p1s.at(i), p2s.at(i))) return false; } return true; } //---------------------------------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------------------------------- template static bool compare_curve_chunks(const std::vector> &chunks1, const std::vector> &chunks2) { if (chunks1.size() != chunks2.size()) return false; for (size_t i = 0; i < chunks1.size(); ++i) { if (!compare_curve_layer(chunks1.at(i), chunks2.at(i))) return false; } return true; } //---------------------------------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------------------------------- static bool compare_leaf_layer(const std::vector &leaves1, const std::vector &leaves2) { MDEBUG("compare_leaf_layer: " << leaves1.size() << " vs " << leaves2.size()); if (leaves1.size() != leaves2.size()) return false; bool r = true; for (size_t i = 0; i < leaves1.size(); ++i) { MDEBUG(" Leaf O: " << leaves1.at(i).O << " vs " << leaves2.at(i).O); if (!(leaves1.at(i) == leaves2.at(i))) r = false; } return r; } //---------------------------------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------------------------------- static bool compare_paths_between_tree_cache_and_global_tree( const fcmp_pp::curve_trees::TreeCacheV1 &tree_cache, const CurveTreesGlobalTree &global_tree, const std::vector &leaves) { // this check compares the paths returned by tree_cache and global_tree against each other for a // given set of leaves CHECK_AND_ASSERT_MES(tree_cache.get_n_leaf_tuples() == global_tree.get_n_leaf_tuples(), false, "mismatch in number of leaf tuples"); for (const fcmp_pp::OutputPair &leaf : leaves) { CurveTreesV1::Path path_in_cache; CHECK_AND_ASSERT_THROW_MES(tree_cache.get_output_path(leaf, path_in_cache), "could not get path from tree cache"); CHECK_AND_ASSERT_THROW_MES(!path_in_cache.empty(), "tree cache path is empty for given output pair"); const std::set global_tree_leaf_idxs = global_tree.get_leaf_idxs_with_output_pair(leaf); CHECK_AND_ASSERT_THROW_MES(!global_tree_leaf_idxs.empty(), "global tree missing given output pair"); const CurveTreesV1::Path path_in_global = global_tree.get_path_at_leaf_idx(*global_tree_leaf_idxs.cbegin()); // use earliest leaf index CHECK_AND_ASSERT_THROW_MES(!path_in_cache.empty(), "global tree path is empty for given output pair"); CHECK_AND_ASSERT_MES(compare_leaf_layer(path_in_cache.leaves, path_in_global.leaves), false, "paths' leaves are not equal"); CHECK_AND_ASSERT_MES(compare_curve_chunks(path_in_cache.c1_layers, path_in_global.c1_layers), false, "paths' c1 layers are not equal"); CHECK_AND_ASSERT_MES(compare_curve_chunks(path_in_cache.c2_layers, path_in_global.c2_layers), false, "paths' c2 layers are not equal"); } return true; } //---------------------------------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------------------------------- static bool is_valid_output_pair_for_tree(const fcmp_pp::OutputPair &p) { return rct::isInMainSubgroup(rct::pk2rct(fcmp_pp::output_pubkey_cref(p))) && rct::isInMainSubgroup(rct::pt2rct(fcmp_pp::commitment_cref(p))); } //---------------------------------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------------------------------- fake_pruned_blockchain::fake_pruned_blockchain(const uint64_t start_block_index, const cryptonote::network_type nettype): m_start_block_index(start_block_index), m_nettype(nettype), m_num_outputs(0), m_global_curve_tree(*curve_trees) { add_starting_block(); } //---------------------------------------------------------------------------------------------------------------------- void fake_pruned_blockchain::add_block(const uint8_t hf_version, std::vector &&txs, const cryptonote::account_public_address &miner_address, const size_t num_miner_tx_outputs) { std::vector tx_prunable_hashes(txs.size()); std::vector tx_hashes(txs.size()); for (size_t i = 0; i < tx_hashes.size(); ++i) { const cryptonote::transaction &tx = txs.at(i); if (tx.pruned) { const crypto::hash &pruned_data_hash = tx_prunable_hashes[i] = crypto::rand(); CHECK_AND_ASSERT_THROW_MES(cryptonote::get_pruned_transaction_hash(tx, pruned_data_hash, tx_hashes[i]), "Failed to calculate pruned transaction hash"); } else // !tx.pruned { tx_prunable_hashes[i] = crypto::null_hash; tx_hashes[i] = cryptonote::get_transaction_hash(tx); } } CHECK_AND_ASSERT_THROW_MES(tx_hashes.size() == txs.size(), "wrong tx_hashes size"); cryptonote::transaction miner_tx = construct_miner_tx_fake_reward_1out(this->height(), miner_reward, miner_address, hf_version, num_miner_tx_outputs); cryptonote::block blk; blk.major_version = hf_version; blk.minor_version = hf_version; blk.timestamp = std::max(this->timestamp() + 120, time(NULL)); blk.prev_id = this->top_block_hash(); blk.nonce = crypto::rand(); blk.miner_tx = std::move(miner_tx); blk.tx_hashes = std::move(tx_hashes); add_block(std::move(blk), std::move(txs), std::move(tx_prunable_hashes)); } //---------------------------------------------------------------------------------------------------------------------- void fake_pruned_blockchain::add_block(cryptonote::block &&blk, std::vector &&pruned_txs, std::vector &&prunable_hashes) { // sanity checks and block rules assert_chain_count(); CHECK_AND_ASSERT_THROW_MES(blk.major_version >= this->hf_version(), "hf version too low"); CHECK_AND_ASSERT_THROW_MES(blk.miner_tx.vin.size() == 1, "miner tx wrong input count"); CHECK_AND_ASSERT_THROW_MES(blk.miner_tx.vin.front().type() == typeid(cryptonote::txin_gen), "miner tx wrong input type"); CHECK_AND_ASSERT_THROW_MES(boost::get(blk.miner_tx.vin.front()).height == this->height(), "miner tx wrong height"); CHECK_AND_ASSERT_THROW_MES(blk.miner_tx.unlock_time == this->height() + CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW, "miner tx wrong unlock time"); CHECK_AND_ASSERT_THROW_MES(blk.tx_hashes.size() == pruned_txs.size(), "wrong number of txs provided"); CHECK_AND_ASSERT_THROW_MES(blk.tx_hashes.size() == prunable_hashes.size(), "wrong number of prunable hashes provided"); CHECK_AND_ASSERT_THROW_MES(m_parsed_blocks.empty() || m_parsed_blocks.back().hash == blk.prev_id, "wrong block prev_id"); // calculate weight and num outputs in block size_t total_block_weight = 0; size_t num_new_block_outputs = blk.miner_tx.vout.size(); for (const cryptonote::transaction &tx : pruned_txs) { total_block_weight += tx.pruned ? cryptonote::get_pruned_transaction_weight(tx) : cryptonote::get_transaction_weight(tx); num_new_block_outputs += tx.vout.size(); } // make block_complete_entry cryptonote::block_complete_entry blk_entry; blk_entry.pruned = true; blk_entry.block = cryptonote::block_to_blob(blk); blk_entry.block_weight = total_block_weight; blk_entry.txs.reserve(pruned_txs.size()); for (size_t i = 0; i < pruned_txs.size(); ++i) { std::stringstream ss; binary_archive ar(ss); CHECK_AND_ASSERT_THROW_MES(pruned_txs.at(i).serialize_base(ar), "tx failed to serialize"); blk_entry.txs.push_back(cryptonote::tx_blob_entry(ss.str(), prunable_hashes.at(i))); } size_t running_num_chain_outputs = m_num_outputs; // make parsed_block tools::wallet2::parsed_block par_blk; par_blk.hash = cryptonote::get_block_hash(blk); par_blk.block = std::move(blk); par_blk.txes = std::move(pruned_txs); { auto &tx_o_indices = par_blk.o_indices.indices.emplace_back(); for (size_t i = 0; i < par_blk.block.miner_tx.vout.size(); ++i) { tx_o_indices.indices.push_back(running_num_chain_outputs); ++running_num_chain_outputs; } } for (const cryptonote::transaction &tx : par_blk.txes) { CHECK_AND_ASSERT_THROW_MES(tx.unlock_time == 0, "I don't want to code tree logic for custom unlock times plz"); auto &tx_o_indices = par_blk.o_indices.indices.emplace_back(); for (size_t i = 0; i < tx.vout.size(); ++i) { tx_o_indices.indices.push_back(running_num_chain_outputs); ++running_num_chain_outputs; } } par_blk.error = false; CHECK_AND_ASSERT_THROW_MES(running_num_chain_outputs - m_num_outputs == num_new_block_outputs, "new block output count mismatch (1)"); const size_t next_height = this->height() + 1; const bool should_try_expand_fcmp_tree = next_height >= CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE; if (should_try_expand_fcmp_tree) { // pull miner outputs from CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW blocks ago, // and non-miner outputs from CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE blocks ago const bool should_try_pull_coinbase_outs = next_height >= CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW; static_assert(CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW >= CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE, "Cannot assume that we should always try pulling non-miner outputs when we can pull miner outputs"); std::vector new_spendable_outputs; if (should_try_pull_coinbase_outs) { const tools::wallet2::parsed_block parsed_block_with_unlocked_miner_tx = this->get_parsed_block(next_height - CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW); const cryptonote::transaction &unlocked_miner_tx = parsed_block_with_unlocked_miner_tx.block.miner_tx; for (size_t local_output_index = 0; local_output_index < unlocked_miner_tx.vout.size(); ++local_output_index) { const cryptonote::tx_out &o = unlocked_miner_tx.vout.at(local_output_index); crypto::public_key output_pubkey; CHECK_AND_ASSERT_THROW_MES(cryptonote::get_output_public_key(o, output_pubkey), "cannot get output pubkey"); const rct::key amount_commitment = rct::zeroCommitVartime(o.amount); const auto output_pair = cryptonote::to_output_pair(o.target, amount_commitment); if (!is_valid_output_pair_for_tree(output_pair)) continue; const uint64_t unified_id = parsed_block_with_unlocked_miner_tx.o_indices.indices.at(0).indices.at(local_output_index); new_spendable_outputs.push_back(fcmp_pp::UnifiedOutput{ unified_id, output_pair }); } } const tools::wallet2::parsed_block parsed_block_with_unlocked_txs = this->get_parsed_block(next_height - CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE); for (size_t tx_index = 0; tx_index < parsed_block_with_unlocked_txs.txes.size(); ++tx_index) { const cryptonote::transaction &unlocked_tx = parsed_block_with_unlocked_txs.txes.at(tx_index); for (size_t local_output_index = 0; local_output_index < unlocked_tx.vout.size(); ++local_output_index) { const cryptonote::tx_out &o = unlocked_tx.vout.at(local_output_index); crypto::public_key output_pubkey; CHECK_AND_ASSERT_THROW_MES(cryptonote::get_output_public_key(o, output_pubkey), "cannot get output pubkey"); const rct::key amount_commitment = o.amount ? rct::zeroCommitVartime(o.amount) : unlocked_tx.rct_signatures.outPk.at(local_output_index).mask; const auto output_pair = cryptonote::to_output_pair(o.target, amount_commitment); if (!is_valid_output_pair_for_tree(output_pair)) continue; const uint64_t unified_id = parsed_block_with_unlocked_txs.o_indices.indices.at(tx_index + 1).indices.at(local_output_index); new_spendable_outputs.push_back(fcmp_pp::UnifiedOutput{ unified_id, output_pair }); } } if (!new_spendable_outputs.empty()) { const bool tree_grow_success = m_global_curve_tree.grow_tree( m_global_curve_tree.get_n_leaf_tuples(), new_spendable_outputs.size(), new_spendable_outputs); CHECK_AND_ASSERT_THROW_MES(tree_grow_success, "fake_pruned_blockchain::add_block: error while growing tree!!!"); } } // everything looks good! time to update fields m_num_outputs = running_num_chain_outputs; m_block_entries.emplace_back(std::move(blk_entry)); m_parsed_blocks.emplace_back(std::move(par_blk)); m_curve_tree_roots.emplace_back(m_global_curve_tree.get_tree_root()); } //---------------------------------------------------------------------------------------------------------------------- void fake_pruned_blockchain::get_blocks_data(const uint64_t start_block_index, const uint64_t stop_block_index, std::vector &block_entries_out, std::vector &parsed_blocks_out) const { block_entries_out.clear(); parsed_blocks_out.clear(); assert_chain_count(); if (start_block_index < m_start_block_index || stop_block_index >= this->height()) throw std::out_of_range("get_blocks_data requested block indices"); for (size_t block_index = start_block_index; block_index <= stop_block_index; ++block_index) { const size_t i = block_index - m_start_block_index; block_entries_out.push_back(m_block_entries.at(i)); parsed_blocks_out.push_back(m_parsed_blocks.at(i)); } } //---------------------------------------------------------------------------------------------------------------------- void fake_pruned_blockchain::init_wallet_for_starting_block(tools::wallet2 &w) const { assert_chain_count(); CHECK_AND_ASSERT_THROW_MES(!m_parsed_blocks.empty(), "blockchain missing starting block"); w.set_refresh_from_block_height(m_start_block_index); w.m_blockchain.set_top_block(m_parsed_blocks.front().hash, m_start_block_index); w.m_tree_cache.clear(); w.m_tree_cache.init(m_start_block_index, m_parsed_blocks.front().hash, /*n_leaf_tuples=*/0, /*last_path=*/{}, /*locked_outputs=*/{}); } //---------------------------------------------------------------------------------------------------------------------- uint64_t fake_pruned_blockchain::refresh_wallet(tools::wallet2 &w) const { const uint64_t start_height = w.get_blockchain_current_height(); // fetch blocks data std::vector blk_entries; std::vector parsed_blks; this->get_blocks_data(start_height, this->height()-1, blk_entries, parsed_blks); // wallet process blocks data uint64_t blocks_added; auto output_tracker_cache = w.create_output_tracker_cache(); w.process_parsed_blocks(start_height, 0/*start_parsed_block_i*/, blk_entries, parsed_blks, blocks_added, output_tracker_cache); // collect paths_to_check std::vector paths_to_check; paths_to_check.reserve(w.m_transfers.size()); for (const tools::wallet2::transfer_details &td : w.m_transfers) { const uint64_t unlock_block_index = td.m_tx.unlock_time ? td.m_tx.unlock_time : (td.m_block_height + CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE); if (unlock_block_index > this->height()) continue; paths_to_check.push_back(td.get_output_pair()); } // check effective FCMP tree equality between blockchain and wallet const bool tree_cache_and_global_tree_synced = compare_paths_between_tree_cache_and_global_tree( w.m_tree_cache, m_global_curve_tree, paths_to_check); CHECK_AND_ASSERT_THROW_MES(tree_cache_and_global_tree_synced, "fake_pruned_blockchain: mismatched wallet tree cache and blockchain global tree"); return blocks_added; } //---------------------------------------------------------------------------------------------------------------------- void fake_pruned_blockchain::assert_chain_count() const { CHECK_AND_ASSERT_THROW_MES(m_block_entries.size() == m_parsed_blocks.size(), "blockchain size mismatch"); CHECK_AND_ASSERT_THROW_MES(m_block_entries.size() == m_curve_tree_roots.size(), "curve tree roots size mismatch"); CHECK_AND_ASSERT_THROW_MES(m_block_entries.empty() || m_num_outputs > 0, "missing outputs in starting block"); CHECK_AND_ASSERT_THROW_MES(m_global_curve_tree.get_n_leaf_tuples() <= m_num_outputs, "tree has wrong number of leafs"); } //---------------------------------------------------------------------------------------------------------------------- void fake_pruned_blockchain::add_starting_block() { if (m_start_block_index == 0) { // add actual genesis block for this network type cryptonote::block genesis_blk; CHECK_AND_ASSERT_THROW_MES(cryptonote::generate_genesis_block(genesis_blk, get_config(m_nettype).GENESIS_TX, get_config(m_nettype).GENESIS_NONCE), "failed to generate genesis block"); add_block(std::move(genesis_blk), {}, {}); } else // m_start_block_index > 0 { // make up start block add_block(1, {}, null_addr); } } //---------------------------------------------------------------------------------------------------------------------- } //namespace mock