diff --git a/src/wallet/scanning_tools.cpp b/src/wallet/scanning_tools.cpp index b9adb61b8..8fe899bd2 100644 --- a/src/wallet/scanning_tools.cpp +++ b/src/wallet/scanning_tools.cpp @@ -55,8 +55,8 @@ namespace wallet //------------------------------------------------------------------------------------------------------------------- static bool parse_tx_extra_for_scanning(const std::vector &tx_extra, const std::size_t n_outputs, - std::vector &main_ephemeral_pubkeys_out, - std::vector &additional_ephemeral_pubkeys_out, + std::vector &main_tx_ephemeral_pubkeys_out, + std::vector &additional_tx_ephemeral_pubkeys_out, cryptonote::blobdata &tx_extra_nonce_out) { // 1. parse extra fields @@ -67,14 +67,14 @@ static bool parse_tx_extra_for_scanning(const std::vector &tx_extr cryptonote::tx_extra_pub_key field_main_pubkey; size_t field_main_pubkey_index = 0; while (cryptonote::find_tx_extra_field_by_type(tx_extra_fields, field_main_pubkey, field_main_pubkey_index++)) - main_ephemeral_pubkeys_out.push_back(field_main_pubkey.pub_key); + main_tx_ephemeral_pubkeys_out.push_back(field_main_pubkey.pub_key); // 3. extract additional tx pubkeys cryptonote::tx_extra_additional_pub_keys field_additional_pubkeys; if (cryptonote::find_tx_extra_field_by_type(tx_extra_fields, field_additional_pubkeys)) { if (field_additional_pubkeys.data.size() == n_outputs) - additional_ephemeral_pubkeys_out = std::move(field_additional_pubkeys.data); + additional_tx_ephemeral_pubkeys_out = std::move(field_additional_pubkeys.data); else fully_parsed = false; } @@ -89,20 +89,20 @@ static bool parse_tx_extra_for_scanning(const std::vector &tx_extr //------------------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------------------- static bool parse_tx_extra_for_scanning(const cryptonote::transaction_prefix &tx_prefix, - std::vector &main_ephemeral_pubkeys_out, - std::vector &additional_ephemeral_pubkeys_out, + std::vector &main_tx_ephemeral_pubkeys_out, + std::vector &additional_tx_ephemeral_pubkeys_out, cryptonote::blobdata &tx_extra_nonce_out) { return parse_tx_extra_for_scanning(tx_prefix.extra, tx_prefix.vout.size(), - main_ephemeral_pubkeys_out, - additional_ephemeral_pubkeys_out, + main_tx_ephemeral_pubkeys_out, + additional_tx_ephemeral_pubkeys_out, tx_extra_nonce_out); } //------------------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------------------- -static void perform_ecdh_derivations(const epee::span main_ephemeral_pubkeys, - const epee::span additional_ephemeral_pubkeys, +static void perform_ecdh_derivations(const epee::span main_tx_ephemeral_pubkeys, + const epee::span additional_tx_ephemeral_pubkeys, const cryptonote::account_keys &acc, const bool is_carrot, std::vector &main_derivations_out, @@ -110,7 +110,7 @@ static void perform_ecdh_derivations(const epee::span { main_derivations_out.clear(); additional_derivations_out.clear(); - main_derivations_out.reserve(main_ephemeral_pubkeys.size()); + main_derivations_out.reserve(main_tx_ephemeral_pubkeys.size()); additional_derivations_out.reserve(additional_derivations_out.size()); if (is_carrot) @@ -118,36 +118,36 @@ static void perform_ecdh_derivations(const epee::span //! @TODO: HW device carrot::view_incoming_key_ram_borrowed_device k_view_dev(acc.m_view_secret_key); - for (const crypto::public_key &main_ephemeral_pubkey : main_ephemeral_pubkeys) + for (const crypto::public_key &main_tx_ephemeral_pubkey : main_tx_ephemeral_pubkeys) { mx25519_pubkey s_sender_receiver_unctx; k_view_dev.view_key_scalar_mult_x25519( - carrot::raw_byte_convert(main_ephemeral_pubkey), + carrot::raw_byte_convert(main_tx_ephemeral_pubkey), s_sender_receiver_unctx); main_derivations_out.push_back(carrot::raw_byte_convert(s_sender_receiver_unctx)); } - for (const crypto::public_key &additional_ephemeral_pubkey : additional_ephemeral_pubkeys) + for (const crypto::public_key &additional_tx_ephemeral_pubkey : additional_tx_ephemeral_pubkeys) { mx25519_pubkey s_sender_receiver_unctx; k_view_dev.view_key_scalar_mult_x25519( - carrot::raw_byte_convert(additional_ephemeral_pubkey), + carrot::raw_byte_convert(additional_tx_ephemeral_pubkey), s_sender_receiver_unctx); additional_derivations_out.push_back(carrot::raw_byte_convert(s_sender_receiver_unctx)); } } else // !is_carrot { - for (const crypto::public_key &main_ephemeral_pubkey : main_ephemeral_pubkeys) + for (const crypto::public_key &main_tx_ephemeral_pubkey : main_tx_ephemeral_pubkeys) { - acc.get_device().generate_key_derivation(main_ephemeral_pubkey, + acc.get_device().generate_key_derivation(main_tx_ephemeral_pubkey, acc.m_view_secret_key, tools::add_element(main_derivations_out)); } - for (const crypto::public_key &additional_ephemeral_pubkey : additional_ephemeral_pubkeys) + for (const crypto::public_key &additional_tx_ephemeral_pubkey : additional_tx_ephemeral_pubkeys) { - acc.get_device().generate_key_derivation(additional_ephemeral_pubkey, + acc.get_device().generate_key_derivation(additional_tx_ephemeral_pubkey, acc.m_view_secret_key, tools::add_element(additional_derivations_out)); } @@ -384,16 +384,16 @@ std::optional try_view_incoming_scan_enote_dest const std::unordered_map &subaddress_map) { // 1. parse tx extra - std::vector main_ephemeral_pubkeys; - std::vector additional_ephemeral_pubkeys; + std::vector main_tx_ephemeral_pubkeys; + std::vector additional_tx_ephemeral_pubkeys; cryptonote::blobdata tx_extra_nonce; - parse_tx_extra_for_scanning(tx_prefix, main_ephemeral_pubkeys, additional_ephemeral_pubkeys, tx_extra_nonce); + parse_tx_extra_for_scanning(tx_prefix, main_tx_ephemeral_pubkeys, additional_tx_ephemeral_pubkeys, tx_extra_nonce); // 2. perform ECDH derivations std::vector main_derivations; std::vector additional_derivations; - perform_ecdh_derivations(epee::to_span(main_ephemeral_pubkeys), - epee::to_span(additional_ephemeral_pubkeys), + perform_ecdh_derivations(epee::to_span(main_tx_ephemeral_pubkeys), + epee::to_span(additional_tx_ephemeral_pubkeys), acc, carrot::is_carrot_transaction_v1(tx_prefix), main_derivations, @@ -402,8 +402,8 @@ std::optional try_view_incoming_scan_enote_dest // 3. view-scan enote destination return try_view_incoming_scan_enote_destination(tx_prefix.vout.at(local_output_index), amount_commitment, - epee::to_span(main_ephemeral_pubkeys), - epee::to_span(additional_ephemeral_pubkeys), + epee::to_span(main_tx_ephemeral_pubkeys), + epee::to_span(additional_tx_ephemeral_pubkeys), tx_extra_nonce, tx_prefix.vin.at(0), local_output_index, @@ -543,33 +543,21 @@ std::optional try_view_incoming_scan_enote( //------------------------------------------------------------------------------------------------------------------- void view_incoming_scan_transaction( const cryptonote::transaction &tx, + const epee::span main_tx_ephemeral_pubkeys, + const epee::span additional_tx_ephemeral_pubkeys, + const cryptonote::blobdata &tx_extra_nonce, + const epee::span main_derivations, + const std::vector &additional_derivations, const cryptonote::account_keys &acc, const std::unordered_map &subaddress_map, - const epee::span> &enote_scan_infos_out) + const epee::span> enote_scan_infos_out) { const size_t n_outputs = tx.vout.size(); CHECK_AND_ASSERT_THROW_MES(enote_scan_infos_out.size() == n_outputs, "view_incoming_scan_transaction: enote scan span wrong length"); - // 1. parse tx extra - std::vector main_ephemeral_pubkeys; - std::vector additional_ephemeral_pubkeys; - cryptonote::blobdata tx_extra_nonce; - if (!parse_tx_extra_for_scanning(tx, main_ephemeral_pubkeys, additional_ephemeral_pubkeys, tx_extra_nonce)) - MWARNING("Transaction extra has unsupported format: " << cryptonote::get_transaction_hash(tx)); - - // 2. perform ECDH derivations - std::vector main_derivations; - std::vector additional_derivations; - perform_ecdh_derivations(epee::to_span(main_ephemeral_pubkeys), - epee::to_span(additional_ephemeral_pubkeys), - acc, - carrot::is_carrot_transaction_v1(tx), - main_derivations, - additional_derivations); - - // 3. view-incoming scan output enotes + // do view-incoming scan for each output enotes for (size_t local_output_index = 0; local_output_index < n_outputs; ++local_output_index) { auto &enote_scan_info = const_cast&>(enote_scan_infos_out[local_output_index]); @@ -578,18 +566,80 @@ void view_incoming_scan_transaction( enote_scan_info = try_view_incoming_scan_enote(enote_destination, tx.rct_signatures, - epee::to_span(main_ephemeral_pubkeys), - epee::to_span(additional_ephemeral_pubkeys), + main_tx_ephemeral_pubkeys, + additional_tx_ephemeral_pubkeys, tx_extra_nonce, tx.vin.at(0), local_output_index, - epee::to_span(main_derivations), + main_derivations, additional_derivations, acc, subaddress_map); } } //------------------------------------------------------------------------------------------------------------------- +void view_incoming_scan_transaction( + const cryptonote::transaction &tx, + const epee::span custom_main_derivations, + const std::vector &custom_additional_derivations, + const cryptonote::account_keys &acc, + const std::unordered_map &subaddress_map, + const epee::span> enote_scan_infos_out) +{ + // 1. parse tx extra + std::vector main_tx_ephemeral_pubkeys; + std::vector additional_tx_ephemeral_pubkeys; + cryptonote::blobdata tx_extra_nonce; + if (!parse_tx_extra_for_scanning(tx, main_tx_ephemeral_pubkeys, additional_tx_ephemeral_pubkeys, tx_extra_nonce)) + MWARNING("Transaction extra has unsupported format: " << cryptonote::get_transaction_hash(tx)); + + // 2. view-incoming scan output enotes + view_incoming_scan_transaction(tx, + epee::to_span(main_tx_ephemeral_pubkeys), + epee::to_span(additional_tx_ephemeral_pubkeys), + tx_extra_nonce, + custom_main_derivations, + custom_additional_derivations, + acc, + subaddress_map, + enote_scan_infos_out); +} +//------------------------------------------------------------------------------------------------------------------- +void view_incoming_scan_transaction( + const cryptonote::transaction &tx, + const cryptonote::account_keys &acc, + const std::unordered_map &subaddress_map, + const epee::span> enote_scan_infos_out) +{ + // 1. parse tx extra + std::vector main_tx_ephemeral_pubkeys; + std::vector additional_tx_ephemeral_pubkeys; + cryptonote::blobdata tx_extra_nonce; + if (!parse_tx_extra_for_scanning(tx, main_tx_ephemeral_pubkeys, additional_tx_ephemeral_pubkeys, tx_extra_nonce)) + MWARNING("Transaction extra has unsupported format: " << cryptonote::get_transaction_hash(tx)); + + // 2. perform ECDH derivations + std::vector main_derivations; + std::vector additional_derivations; + perform_ecdh_derivations(epee::to_span(main_tx_ephemeral_pubkeys), + epee::to_span(additional_tx_ephemeral_pubkeys), + acc, + carrot::is_carrot_transaction_v1(tx), + main_derivations, + additional_derivations); + + // 3. view-incoming scan output enotes + view_incoming_scan_transaction(tx, + epee::to_span(main_tx_ephemeral_pubkeys), + epee::to_span(additional_tx_ephemeral_pubkeys), + tx_extra_nonce, + epee::to_span(main_derivations), + additional_derivations, + acc, + subaddress_map, + enote_scan_infos_out); +} +//------------------------------------------------------------------------------------------------------------------- std::vector> view_incoming_scan_transaction( const cryptonote::transaction &tx, const cryptonote::account_keys &acc, diff --git a/src/wallet/scanning_tools.h b/src/wallet/scanning_tools.h index eb0a541e5..894ffbb66 100644 --- a/src/wallet/scanning_tools.h +++ b/src/wallet/scanning_tools.h @@ -123,9 +123,26 @@ std::optional try_view_incoming_scan_enote( void view_incoming_scan_transaction( const cryptonote::transaction &tx, + const epee::span main_tx_ephemeral_pubkeys, + const epee::span additional_tx_ephemeral_pubkeys, + const cryptonote::blobdata &tx_extra_nonce, + const epee::span main_derivations, + const std::vector &additional_derivations, const cryptonote::account_keys &acc, const std::unordered_map &subaddress_map, - const epee::span> &enote_scan_infos_out); + const epee::span> enote_scan_infos_out); +void view_incoming_scan_transaction( + const cryptonote::transaction &tx, + const epee::span custom_main_derivations, + const std::vector &custom_additional_derivations, + const cryptonote::account_keys &acc, + const std::unordered_map &subaddress_map, + const epee::span> enote_scan_infos_out); +void view_incoming_scan_transaction( + const cryptonote::transaction &tx, + const cryptonote::account_keys &acc, + const std::unordered_map &subaddress_map, + const epee::span> enote_scan_infos_out); std::vector> view_incoming_scan_transaction( const cryptonote::transaction &tx, const cryptonote::account_keys &acc, diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp index e64199bfe..ee6d5c742 100644 --- a/src/wallet/wallet2.cpp +++ b/src/wallet/wallet2.cpp @@ -12401,7 +12401,14 @@ void wallet2::check_tx_key_helper(const cryptonote::transaction &tx, const crypt { received = 0; - const auto enote_scan_infos = wallet::view_incoming_scan_transaction(tx, m_account.get_keys(), m_subaddresses); + std::vector> enote_scan_infos(tx.vout.size()); + wallet::view_incoming_scan_transaction(tx, + {&derivation, 1}, + additional_derivations, + m_account.get_keys(), + {{address.m_spend_public_key, {}}}, // use a fake subaddress map with just the provided address in it + epee::to_mut_span(enote_scan_infos)); + for (const auto &enote_scan_info : enote_scan_infos) if (enote_scan_info && enote_scan_info->address_spend_pubkey == address.m_spend_public_key) received += enote_scan_info->amount; //! @TODO: check overflow diff --git a/tests/unit_tests/CMakeLists.txt b/tests/unit_tests/CMakeLists.txt index 34acb6147..41b3d4873 100644 --- a/tests/unit_tests/CMakeLists.txt +++ b/tests/unit_tests/CMakeLists.txt @@ -95,6 +95,7 @@ set(unit_tests_sources test_peerlist.cpp test_protocol_pack.cpp threadpool.cpp + tx_construction_helpers.cpp tx_proof.cpp hardfork.cpp unbound.cpp diff --git a/tests/unit_tests/fake_pruned_blockchain.cpp b/tests/unit_tests/fake_pruned_blockchain.cpp index 88f104f45..8baca70d5 100644 --- a/tests/unit_tests/fake_pruned_blockchain.cpp +++ b/tests/unit_tests/fake_pruned_blockchain.cpp @@ -32,12 +32,9 @@ #include "fake_pruned_blockchain.h" //local headers -#include "carrot_core/device_ram_borrowed.h" -#include "carrot_core/output_set_finalization.h" -#include "carrot_impl/carrot_tx_builder_utils.h" -#include "carrot_impl/carrot_tx_format_utils.h" #include "common/container_helpers.h" -#include "crypto/generators.h" +#include "ringct/rctOps.h" +#include "tx_construction_helpers.h" #undef MONERO_DEFAULT_LOG_CATEGORY #define MONERO_DEFAULT_LOG_CATEGORY "unit_tests.fake_pruned_bc" @@ -45,318 +42,122 @@ namespace mock { //---------------------------------------------------------------------------------------------------------------------- -bool construct_miner_tx_fake_reward_1out(const size_t height, - const rct::xmr_amount reward, - const cryptonote::account_public_address &miner_address, - cryptonote::transaction& tx, - const uint8_t hf_version) +//---------------------------------------------------------------------------------------------------------------------- +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 bool is_carrot = hf_version >= HF_VERSION_CARROT; - if (is_carrot) + 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) { - carrot::CarrotDestinationV1 miner_destination; - make_carrot_main_address_v1(miner_address.m_spend_public_key, - miner_address.m_view_public_key, - miner_destination); - - const carrot::CarrotPaymentProposalV1 normal_payment_proposal{ - .destination = miner_destination, - .amount = reward, - .randomness = carrot::gen_janus_anchor() - }; - - std::vector coinbase_enotes; - carrot::get_coinbase_output_enotes({normal_payment_proposal}, - height, - coinbase_enotes); - - tx = carrot::store_carrot_to_coinbase_transaction_v1(coinbase_enotes); - } - else // !is_carrot - { - tx.vin.clear(); - tx.vout.clear(); - tx.extra.clear(); - - cryptonote::txin_gen in; - in.height = height; - - cryptonote::keypair txkey = cryptonote::keypair::generate(hw::get_device("default")); - cryptonote::add_tx_pub_key_to_extra(tx, txkey.pub); - if (!cryptonote::sort_tx_extra(tx.extra, tx.extra)) + if (!compare_curve_point(p1s.at(i), p2s.at(i))) return false; - - crypto::key_derivation derivation; - crypto::public_key out_eph_public_key; - bool r = crypto::generate_key_derivation(miner_address.m_view_public_key, txkey.sec, derivation); - CHECK_AND_ASSERT_MES(r, false, - "while creating outs: failed to generate_key_derivation(" << miner_address.m_view_public_key << ", " - << crypto::secret_key_explicit_print_ref{txkey.sec} << ")"); - - const size_t local_output_index = 0; - r = crypto::derive_public_key(derivation, local_output_index, miner_address.m_spend_public_key, out_eph_public_key); - CHECK_AND_ASSERT_MES(r, false, - "while creating outs: failed to derive_public_key(" << derivation << ", " - << local_output_index << ", "<< miner_address.m_spend_public_key << ")"); - - const bool use_view_tags = hf_version >= HF_VERSION_VIEW_TAGS; - crypto::view_tag view_tag; - if (use_view_tags) - crypto::derive_view_tag(derivation, local_output_index, view_tag); - - cryptonote::tx_out out; - cryptonote::set_tx_out(reward, out_eph_public_key, use_view_tags, view_tag, out); - - tx.vout.push_back(out); - - if (hf_version >= 4) - tx.version = 2; - else - tx.version = 1; - - tx.unlock_time = height + CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW; - tx.vin.push_back(in); - - tx.invalidate_hashes(); } - return true; } //---------------------------------------------------------------------------------------------------------------------- -cryptonote::transaction construct_miner_tx_fake_reward_1out(const size_t height, - const rct::xmr_amount reward, - const cryptonote::account_public_address &miner_address, - const uint8_t hf_version) -{ - cryptonote::transaction tx; - const bool r = construct_miner_tx_fake_reward_1out(height, reward, miner_address, tx, hf_version); - CHECK_AND_ASSERT_THROW_MES(r, "failed to construct miner tx"); - return tx; -} //---------------------------------------------------------------------------------------------------------------------- -cryptonote::tx_source_entry gen_tx_source_entry_fake_members( - const stripped_down_tx_source_entry_t &in, - const size_t mixin, - const uint64_t max_global_output_index) +template +static bool compare_curve_chunks(const std::vector> &chunks1, + const std::vector> &chunks2) { - const size_t ring_size = mixin + 1; - const bool is_rct = in.mask == rct::I; - - CHECK_AND_ASSERT_THROW_MES(in.global_output_index <= max_global_output_index, - "real global output index too low"); - CHECK_AND_ASSERT_THROW_MES(max_global_output_index >= ring_size, - "not enough global output indices for mixin"); - - cryptonote::tx_source_entry res; - - // populate ring with fake data - std::unordered_set used_indices; - res.outputs.reserve(mixin + 1); - res.outputs.push_back( - {in.global_output_index, - { rct::pk2rct(in.onetime_address), rct::commit(in.amount, in.mask) }}); - used_indices.insert(in.global_output_index); - while (res.outputs.size() < ring_size) + if (chunks1.size() != chunks2.size()) + return false; + for (size_t i = 0; i < chunks1.size(); ++i) { - const uint64_t global_output_index = crypto::rand_range(0, max_global_output_index); - if (used_indices.count(global_output_index)) - continue; - used_indices.insert(global_output_index); - const rct::ctkey output_pair{rct::pkGen(), - is_rct ? rct::pkGen() : rct::zeroCommitVartime(in.amount)}; - res.outputs.push_back({global_output_index, output_pair}); + if (!compare_curve_layer(chunks1.at(i), chunks2.at(i))) + return false; } - // sort by index - std::sort(res.outputs.begin(), res.outputs.end(), [](const auto &a, const auto &b) -> bool { - return a.first < b.first; - }); - - // real_output - res.real_output = 0; - while (res.outputs.at(res.real_output).second.dest != in.onetime_address) - ++res.real_output; - - // copy from in - res.real_out_tx_key = in.real_out_tx_key; - res.real_out_additional_tx_keys = in.real_out_additional_tx_keys; - res.real_output_in_tx_index = in.local_output_index; - res.amount = in.amount; - res.rct = is_rct; - res.mask = in.mask; - - return res; + return true; } //---------------------------------------------------------------------------------------------------------------------- -cryptonote::transaction construct_pre_carrot_tx_with_fake_inputs( - const cryptonote::account_keys &sender_account_keys, - const std::unordered_map &subaddresses, - std::vector &&stripped_sources, - std::vector &destinations, - const boost::optional &change_addr, - const rct::xmr_amount fee, - const uint8_t hf_version, - const bool sweep_unmixable_override) +//---------------------------------------------------------------------------------------------------------------------- +static bool compare_output_tuple(const fcmp_pp::curve_trees::OutputTuple &tup1, + const fcmp_pp::curve_trees::OutputTuple &tup2) { - // derive config from hf version - const bool rct = hf_version >= HF_VERSION_DYNAMIC_FEE && !sweep_unmixable_override; - rct::RCTConfig rct_config; - switch (hf_version) - { - case 1: - case 2: - case 3: - case HF_VERSION_DYNAMIC_FEE: - case 5: - case HF_VERSION_MIN_MIXIN_4: - case 7: - rct_config = { rct::RangeProofBorromean, 0 }; - break; - case HF_VERSION_PER_BYTE_FEE: - case 9: - rct_config = { rct::RangeProofPaddedBulletproof, 1 }; - break; - case HF_VERSION_SMALLER_BP: - case 11: - case HF_VERSION_MIN_2_OUTPUTS: - rct_config = { rct::RangeProofPaddedBulletproof, 2 }; - break; - case HF_VERSION_CLSAG: - case 14: - rct_config = { rct::RangeProofPaddedBulletproof, 3 }; - break; - case HF_VERSION_BULLETPROOF_PLUS: - case 16: - rct_config = { rct::RangeProofPaddedBulletproof, 4 }; - break; - default: - ASSERT_MES_AND_THROW("unrecognized hf version"); - } - const bool use_view_tags = hf_version >= HF_VERSION_VIEW_TAGS; - const size_t mixin = 15; - const uint64_t max_global_output_index = 1000000; - - // count missing money and balance if necessary - boost::multiprecision::int128_t missing_money = fee; - for (const cryptonote::tx_destination_entry &destination : destinations) - missing_money += destination.amount; - for (const stripped_down_tx_source_entry_t &stripped_source : stripped_sources) - missing_money -= stripped_source.amount; - if (missing_money > 0) - { - const rct::xmr_amount missing_money64 = boost::numeric_cast(missing_money); - - hw::device &hwdev = hw::get_device("default"); - cryptonote::keypair main_tx_keypair = cryptonote::keypair::generate(hwdev); - - std::vector dummy_additional_tx_public_keys; - std::vector amount_keys; - crypto::public_key input_onetime_address; - crypto::view_tag vt; - const bool r = hwdev.generate_output_ephemeral_keys(rct ? 2 : 1, - cryptonote::account_keys(), - main_tx_keypair.pub, - main_tx_keypair.sec, - cryptonote::tx_destination_entry(missing_money64, sender_account_keys.m_account_address, false), - boost::none, - /*output_index=*/0, - /*need_additional_txkeys=*/false, - /*additional_tx_keys=*/{}, - dummy_additional_tx_public_keys, - amount_keys, - input_onetime_address, - use_view_tags, - vt); - CHECK_AND_ASSERT_THROW_MES(r, "failed to generate balancing input"); - - const stripped_down_tx_source_entry_t balancing_in{ - .global_output_index = crypto::rand_range(0, max_global_output_index), - .onetime_address = input_onetime_address, - .real_out_tx_key = main_tx_keypair.pub, - .real_out_additional_tx_keys = {}, - .local_output_index = 0, - .amount = missing_money64, - .mask = rct ? rct::genCommitmentMask(amount_keys.at(0)) : rct::I - }; - - stripped_sources.push_back(balancing_in); - } - - // populate random sources - std::vector sources; - sources.reserve(stripped_sources.size()); - for (const auto &stripped_source : stripped_sources) - sources.push_back(gen_tx_source_entry_fake_members(stripped_source, - mixin, - max_global_output_index)); - - // construct tx - cryptonote::transaction tx; - crypto::secret_key tx_key; - std::vector additional_tx_keys; - fcmp_pp::ProofParams dummy_fcmp_params; - const bool r = cryptonote::construct_tx_and_get_tx_key( - sender_account_keys, - subaddresses, - sources, - destinations, - change_addr, - /*extra=*/{}, - tx, - tx_key, - additional_tx_keys, - dummy_fcmp_params, - rct, - rct_config, - use_view_tags); - CHECK_AND_ASSERT_THROW_MES(r, "failed to construct tx"); - return tx; + return tup1.O == tup2.O && tup1.I == tup2.I && tup1.C == tup2.C; } //---------------------------------------------------------------------------------------------------------------------- -cryptonote::transaction construct_carrot_pruned_transaction_fake_inputs( - const std::vector &normal_payment_proposals, - const std::vector &selfsend_payment_proposals, - const cryptonote::account_keys &acc_keys) +//---------------------------------------------------------------------------------------------------------------------- +static bool compare_leaf_layer(const std::vector &leaves1, + const std::vector &leaves2) { - carrot::select_inputs_func_t select_inputs = []( - const boost::multiprecision::int128_t &nominal_output_sum, - const std::map &fee_by_input_count, - const std::size_t, - const std::size_t, - std::vector &select_inputs_out - ) + 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) { - const auto in_amount = boost::numeric_cast(nominal_output_sum + fee_by_input_count.at(1)); - const crypto::key_image ki = rct::rct2ki(rct::pkGen()); - select_inputs_out = {carrot::CarrotSelectedInput{.amount = in_amount, .key_image = ki}}; - }; - - const carrot::view_incoming_key_ram_borrowed_device k_view_dev(acc_keys.m_view_secret_key); - - carrot::CarrotTransactionProposalV1 tx_proposal; - carrot::make_carrot_transaction_proposal_v1_transfer( - normal_payment_proposals, - selfsend_payment_proposals, - fake_fee_per_weight, - /*extra=*/{}, - std::move(select_inputs), - /*s_view_balance_dev=*/nullptr, - &k_view_dev, - acc_keys.m_account_address.m_spend_public_key, - tx_proposal); - - cryptonote::transaction tx; - carrot::make_pruned_transaction_from_carrot_proposal_v1(tx_proposal, - /*s_view_balance_dev=*/nullptr, - &k_view_dev, - tx); - - return tx; + MDEBUG(" Leaf O: " << leaves1.at(i).O << " vs " << leaves2.at(i).O); + if (!compare_output_tuple(leaves1.at(i), leaves2.at(i))) + r = false; + } + return r; } //---------------------------------------------------------------------------------------------------------------------- -const cryptonote::account_public_address null_addr{ - .m_spend_public_key = crypto::get_G(), - .m_view_public_key = crypto::get_G() -}; +//---------------------------------------------------------------------------------------------------------------------- +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 + + using namespace fcmp_pp::curve_trees; + + 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 OutputContext &leaf : leaves) + { + CurveTreesV1::Path path_in_cache; + CHECK_AND_ASSERT_THROW_MES(tree_cache.get_output_path(leaf.output_pair, path_in_cache), + "could not get path from tree cache"); + const CurveTreesV1::Path path_in_global = + global_tree.get_path_at_leaf_idx(leaf.output_id); + 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::curve_trees::OutputPair &p) +{ + return rct::isInMainSubgroup(rct::pk2rct(p.output_pubkey)) && rct::isInMainSubgroup(p.commitment); +} +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +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, diff --git a/tests/unit_tests/fake_pruned_blockchain.h b/tests/unit_tests/fake_pruned_blockchain.h index c591fe57e..7257f0fd6 100644 --- a/tests/unit_tests/fake_pruned_blockchain.h +++ b/tests/unit_tests/fake_pruned_blockchain.h @@ -30,65 +30,14 @@ #define IN_UNIT_TESTS -#include "carrot_impl/carrot_tx_builder_types.h" #include "wallet/wallet2.h" namespace mock { //---------------------------------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------------------------------- -bool construct_miner_tx_fake_reward_1out(const size_t height, - const rct::xmr_amount reward, - const cryptonote::account_public_address &miner_address, - cryptonote::transaction& tx, - const uint8_t hf_version); -//---------------------------------------------------------------------------------------------------------------------- -//---------------------------------------------------------------------------------------------------------------------- -cryptonote::transaction construct_miner_tx_fake_reward_1out(const size_t height, - const rct::xmr_amount reward, - const cryptonote::account_public_address &miner_address, - const uint8_t hf_version); -//---------------------------------------------------------------------------------------------------------------------- -//---------------------------------------------------------------------------------------------------------------------- -struct stripped_down_tx_source_entry_t -{ - uint64_t global_output_index; - crypto::public_key onetime_address; - crypto::public_key real_out_tx_key; - std::vector real_out_additional_tx_keys; - size_t local_output_index; - rct::xmr_amount amount; - rct::key mask; -}; -//---------------------------------------------------------------------------------------------------------------------- -//---------------------------------------------------------------------------------------------------------------------- -cryptonote::tx_source_entry gen_tx_source_entry_fake_members( - const stripped_down_tx_source_entry_t &in, - const size_t mixin, - const uint64_t max_global_output_index); -//---------------------------------------------------------------------------------------------------------------------- -//---------------------------------------------------------------------------------------------------------------------- -cryptonote::transaction construct_pre_carrot_tx_with_fake_inputs( - const cryptonote::account_keys &sender_account_keys, - const std::unordered_map &subaddresses, - std::vector &&stripped_sources, - std::vector &destinations, - const boost::optional &change_addr, - const rct::xmr_amount fee, - const uint8_t hf_version, - const bool sweep_unmixable_override = false); -//---------------------------------------------------------------------------------------------------------------------- -//---------------------------------------------------------------------------------------------------------------------- -static constexpr rct::xmr_amount fake_fee_per_weight = 2023; -//---------------------------------------------------------------------------------------------------------------------- -//---------------------------------------------------------------------------------------------------------------------- -cryptonote::transaction construct_carrot_pruned_transaction_fake_inputs( - const std::vector &normal_payment_proposals, - const std::vector &selfsend_payment_proposals, - const cryptonote::account_keys &acc_keys); -//---------------------------------------------------------------------------------------------------------------------- -//---------------------------------------------------------------------------------------------------------------------- -extern const cryptonote::account_public_address null_addr; +using fcmp_generic_object_t = std::unique_ptr; +static inline fcmp_generic_object_t make_fcmp_generic_object(void *p) { return fcmp_generic_object_t(p, &free); } //---------------------------------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------------------------------- class fake_pruned_blockchain diff --git a/tests/unit_tests/tx_construction_helpers.cpp b/tests/unit_tests/tx_construction_helpers.cpp new file mode 100644 index 000000000..47eda07da --- /dev/null +++ b/tests/unit_tests/tx_construction_helpers.cpp @@ -0,0 +1,415 @@ +// 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. + +//paired header +#include "tx_construction_helpers.h" + +//local headers +#include "carrot_core/device_ram_borrowed.h" +#include "carrot_core/output_set_finalization.h" +#include "carrot_impl/carrot_tx_builder_utils.h" +#include "carrot_impl/carrot_tx_format_utils.h" +#include "crypto/generators.h" + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "unit_tests.tx_construction_helpers" + +namespace mock +{ +//---------------------------------------------------------------------------------------------------------------------- +bool construct_miner_tx_fake_reward_1out(const size_t height, + const rct::xmr_amount reward, + const cryptonote::account_public_address &miner_address, + cryptonote::transaction& tx, + const uint8_t hf_version, + const size_t num_tx_outputs) +{ + const bool is_carrot = hf_version >= HF_VERSION_CARROT; + if (is_carrot) + { + carrot::CarrotDestinationV1 miner_destination; + make_carrot_main_address_v1(miner_address.m_spend_public_key, + miner_address.m_view_public_key, + miner_destination); + + std::vector normal_payment_proposals; + normal_payment_proposals.reserve(num_tx_outputs); + for (size_t i = 0; i < num_tx_outputs; ++i) + { + normal_payment_proposals.push_back(carrot::CarrotPaymentProposalV1{ + .destination = miner_destination, + .amount = reward, + .randomness = carrot::gen_janus_anchor() + }); + } + + std::vector coinbase_enotes; + carrot::get_coinbase_output_enotes(normal_payment_proposals, + height, + coinbase_enotes); + + tx = carrot::store_carrot_to_coinbase_transaction_v1(coinbase_enotes); + } + else // !is_carrot + { + tx.vin.clear(); + tx.vout.clear(); + tx.extra.clear(); + + cryptonote::txin_gen in; + in.height = height; + + if (hf_version >= 4) + tx.version = 2; + else + tx.version = 1; + + tx.unlock_time = height + CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW; + tx.vin.push_back(in); + + for (size_t i = 0; i < num_tx_outputs; ++i) + { + cryptonote::keypair txkey = cryptonote::keypair::generate(hw::get_device("default")); + cryptonote::add_tx_pub_key_to_extra(tx, txkey.pub); + if (!cryptonote::sort_tx_extra(tx.extra, tx.extra)) + return false; + + crypto::key_derivation derivation; + crypto::public_key out_eph_public_key; + bool r = crypto::generate_key_derivation(miner_address.m_view_public_key, txkey.sec, derivation); + CHECK_AND_ASSERT_MES(r, false, + "while creating outs: failed to generate_key_derivation(" << miner_address.m_view_public_key << ", " + << crypto::secret_key_explicit_print_ref{txkey.sec} << ")"); + + const size_t local_output_index = 0; + r = crypto::derive_public_key(derivation, local_output_index, miner_address.m_spend_public_key, out_eph_public_key); + CHECK_AND_ASSERT_MES(r, false, + "while creating outs: failed to derive_public_key(" << derivation << ", " + << local_output_index << ", "<< miner_address.m_spend_public_key << ")"); + + const bool use_view_tags = hf_version >= HF_VERSION_VIEW_TAGS; + crypto::view_tag view_tag; + if (use_view_tags) + crypto::derive_view_tag(derivation, local_output_index, view_tag); + + cryptonote::tx_out out; + cryptonote::set_tx_out(reward, out_eph_public_key, use_view_tags, view_tag, out); + + tx.vout.push_back(out); + } + + tx.invalidate_hashes(); + } + + return true; +} +//---------------------------------------------------------------------------------------------------------------------- +cryptonote::transaction construct_miner_tx_fake_reward_1out(const size_t height, + const rct::xmr_amount reward, + const cryptonote::account_public_address &miner_address, + const uint8_t hf_version, + const size_t num_tx_outputs) +{ + cryptonote::transaction tx; + const bool r = construct_miner_tx_fake_reward_1out(height, reward, miner_address, tx, hf_version, num_tx_outputs); + CHECK_AND_ASSERT_THROW_MES(r, "failed to construct miner tx"); + return tx; +} +//---------------------------------------------------------------------------------------------------------------------- +cryptonote::tx_source_entry gen_tx_source_entry_fake_members( + const stripped_down_tx_source_entry_t &in, + const size_t mixin, + const uint64_t max_global_output_index) +{ + const size_t ring_size = mixin + 1; + const bool is_rct = in.mask == rct::I; + + CHECK_AND_ASSERT_THROW_MES(in.global_output_index <= max_global_output_index, + "real global output index too low"); + CHECK_AND_ASSERT_THROW_MES(max_global_output_index >= ring_size, + "not enough global output indices for mixin"); + + cryptonote::tx_source_entry res; + + // populate ring with fake data + std::unordered_set used_indices; + res.outputs.reserve(mixin + 1); + res.outputs.push_back( + {in.global_output_index, + { rct::pk2rct(in.onetime_address), rct::commit(in.amount, in.mask) }}); + used_indices.insert(in.global_output_index); + while (res.outputs.size() < ring_size) + { + const uint64_t global_output_index = crypto::rand_range(0, max_global_output_index); + if (used_indices.count(global_output_index)) + continue; + used_indices.insert(global_output_index); + const rct::ctkey output_pair{rct::pkGen(), + is_rct ? rct::pkGen() : rct::zeroCommitVartime(in.amount)}; + res.outputs.push_back({global_output_index, output_pair}); + } + // sort by index + std::sort(res.outputs.begin(), res.outputs.end(), [](const auto &a, const auto &b) -> bool { + return a.first < b.first; + }); + + // real_output + res.real_output = 0; + while (res.outputs.at(res.real_output).second.dest != in.onetime_address) + ++res.real_output; + + // copy from in + res.real_out_tx_key = in.real_out_tx_key; + res.real_out_additional_tx_keys = in.real_out_additional_tx_keys; + res.real_output_in_tx_index = in.local_output_index; + res.amount = in.amount; + res.rct = is_rct; + res.mask = in.mask; + + return res; +} +//---------------------------------------------------------------------------------------------------------------------- +cryptonote::transaction construct_pre_carrot_tx_with_fake_inputs( + const cryptonote::account_keys &sender_account_keys, + const std::unordered_map &sender_subaddress_map, + std::vector &&stripped_sources, + std::vector &destinations, + const boost::optional &change_addr, + const rct::xmr_amount fee, + const uint8_t hf_version, + crypto::secret_key &main_tx_privkey_out, + std::vector &additional_tx_privkeys_out, + const bool sweep_unmixable_override) +{ + // derive config from hf version + const bool rct = hf_version >= HF_VERSION_DYNAMIC_FEE && !sweep_unmixable_override; + rct::RCTConfig rct_config; + switch (hf_version) + { + case 1: + case 2: + case 3: + case HF_VERSION_DYNAMIC_FEE: + case 5: + case HF_VERSION_MIN_MIXIN_4: + case 7: + rct_config = { rct::RangeProofBorromean, 0 }; + break; + case HF_VERSION_PER_BYTE_FEE: + case 9: + rct_config = { rct::RangeProofPaddedBulletproof, 1 }; + break; + case HF_VERSION_SMALLER_BP: + case 11: + case HF_VERSION_MIN_2_OUTPUTS: + rct_config = { rct::RangeProofPaddedBulletproof, 2 }; + break; + case HF_VERSION_CLSAG: + case 14: + rct_config = { rct::RangeProofPaddedBulletproof, 3 }; + break; + case HF_VERSION_BULLETPROOF_PLUS: + case 16: + rct_config = { rct::RangeProofPaddedBulletproof, 4 }; + break; + default: + ASSERT_MES_AND_THROW("unrecognized hf version"); + } + const bool use_view_tags = hf_version >= HF_VERSION_VIEW_TAGS; + const size_t mixin = 15; + const uint64_t max_global_output_index = 1000000; + + // count missing money and balance if necessary + boost::multiprecision::int128_t missing_money = fee; + for (const cryptonote::tx_destination_entry &destination : destinations) + missing_money += destination.amount; + for (const stripped_down_tx_source_entry_t &stripped_source : stripped_sources) + missing_money -= stripped_source.amount; + if (missing_money > 0) + { + const rct::xmr_amount missing_money64 = boost::numeric_cast(missing_money); + + hw::device &hwdev = hw::get_device("default"); + cryptonote::keypair main_tx_keypair = cryptonote::keypair::generate(hwdev); + + std::vector dummy_additional_tx_public_keys; + std::vector amount_keys; + crypto::public_key input_onetime_address; + crypto::view_tag vt; + const bool r = hwdev.generate_output_ephemeral_keys(rct ? 2 : 1, + cryptonote::account_keys(), + main_tx_keypair.pub, + main_tx_keypair.sec, + cryptonote::tx_destination_entry(missing_money64, sender_account_keys.m_account_address, false), + boost::none, + /*output_index=*/0, + /*need_additional_txkeys=*/false, + /*additional_tx_keys=*/{}, + dummy_additional_tx_public_keys, + amount_keys, + input_onetime_address, + use_view_tags, + vt); + CHECK_AND_ASSERT_THROW_MES(r, "failed to generate balancing input"); + + const stripped_down_tx_source_entry_t balancing_in{ + .global_output_index = crypto::rand_range(0, max_global_output_index), + .onetime_address = input_onetime_address, + .real_out_tx_key = main_tx_keypair.pub, + .real_out_additional_tx_keys = {}, + .local_output_index = 0, + .amount = missing_money64, + .mask = rct ? rct::genCommitmentMask(amount_keys.at(0)) : rct::I + }; + + stripped_sources.push_back(balancing_in); + } + + // populate random sources + std::vector sources; + sources.reserve(stripped_sources.size()); + for (const auto &stripped_source : stripped_sources) + sources.push_back(gen_tx_source_entry_fake_members(stripped_source, + mixin, + max_global_output_index)); + + // construct tx + cryptonote::transaction tx; + + fcmp_pp::ProofParams dummy_fcmp_params; + const bool r = cryptonote::construct_tx_and_get_tx_key( + sender_account_keys, + sender_subaddress_map, + sources, + destinations, + change_addr, + /*extra=*/{}, + tx, + main_tx_privkey_out, + additional_tx_privkeys_out, + dummy_fcmp_params, + rct, + rct_config, + use_view_tags); + CHECK_AND_ASSERT_THROW_MES(r, "failed to construct tx"); + return tx; +} +//---------------------------------------------------------------------------------------------------------------------- +cryptonote::transaction construct_pre_carrot_tx_with_fake_inputs( + const cryptonote::account_keys &sender_account_keys, + const std::unordered_map &sender_subaddress_map, + std::vector &&stripped_sources, + std::vector &destinations, + const boost::optional &change_addr, + const rct::xmr_amount fee, + const uint8_t hf_version, + const bool sweep_unmixable_override) +{ + crypto::secret_key dummy_main_tx_privkey; + std::vector dummy_additional_tx_privkeys; + return construct_pre_carrot_tx_with_fake_inputs(sender_account_keys, + sender_subaddress_map, + std::forward>(stripped_sources), + destinations, + change_addr, + fee, + hf_version, + dummy_main_tx_privkey, + dummy_additional_tx_privkeys, + sweep_unmixable_override); +} +//---------------------------------------------------------------------------------------------------------------------- +cryptonote::transaction construct_pre_carrot_tx_with_fake_inputs( + std::vector &destinations, + const rct::xmr_amount fee, + const uint8_t hf_version, + const bool sweep_unmixable_override) +{ + cryptonote::account_base aether_acb; + aether_acb.generate(); + const std::unordered_map sender_subaddress_map = { + { aether_acb.get_keys().m_account_address.m_spend_public_key, {0, 0 } } + }; + + return construct_pre_carrot_tx_with_fake_inputs(aether_acb.get_keys(), + sender_subaddress_map, + /*stripped_sources=*/{}, + destinations, + /*change_addr*/boost::none, + fee, + hf_version, + sweep_unmixable_override); +} +//---------------------------------------------------------------------------------------------------------------------- +cryptonote::transaction construct_carrot_pruned_transaction_fake_inputs( + const std::vector &normal_payment_proposals, + const std::vector &selfsend_payment_proposals, + const cryptonote::account_keys &acc_keys) +{ + carrot::select_inputs_func_t select_inputs = []( + const boost::multiprecision::int128_t &nominal_output_sum, + const std::map &fee_by_input_count, + const std::size_t, + const std::size_t, + std::vector &select_inputs_out + ) + { + const auto in_amount = boost::numeric_cast(nominal_output_sum + fee_by_input_count.at(1)); + const crypto::key_image ki = rct::rct2ki(rct::pkGen()); + select_inputs_out = {carrot::CarrotSelectedInput{.amount = in_amount, .key_image = ki}}; + }; + + const carrot::view_incoming_key_ram_borrowed_device k_view_dev(acc_keys.m_view_secret_key); + + carrot::CarrotTransactionProposalV1 tx_proposal; + carrot::make_carrot_transaction_proposal_v1_transfer( + normal_payment_proposals, + selfsend_payment_proposals, + fake_fee_per_weight, + /*extra=*/{}, + std::move(select_inputs), + /*s_view_balance_dev=*/nullptr, + &k_view_dev, + acc_keys.m_account_address.m_spend_public_key, + tx_proposal); + + cryptonote::transaction tx; + carrot::make_pruned_transaction_from_carrot_proposal_v1(tx_proposal, + /*s_view_balance_dev=*/nullptr, + &k_view_dev, + tx); + + return tx; +} +//---------------------------------------------------------------------------------------------------------------------- +const cryptonote::account_public_address null_addr{ + .m_spend_public_key = crypto::get_G(), + .m_view_public_key = crypto::get_G() +}; +//---------------------------------------------------------------------------------------------------------------------- +} //namespace mock diff --git a/tests/unit_tests/tx_construction_helpers.h b/tests/unit_tests/tx_construction_helpers.h new file mode 100644 index 000000000..1548eb080 --- /dev/null +++ b/tests/unit_tests/tx_construction_helpers.h @@ -0,0 +1,121 @@ +// 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. + +#pragma once + +//local headers +#include "carrot_impl/carrot_tx_builder_types.h" +#include "cryptonote_core/cryptonote_tx_utils.h" + +//third party headers + +//standard headers + +//forward declarations + +namespace mock +{ +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +bool construct_miner_tx_fake_reward_1out(const size_t height, + const rct::xmr_amount reward, + const cryptonote::account_public_address &miner_address, + cryptonote::transaction& tx, + const uint8_t hf_version, + const size_t num_tx_outputs = 1); +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +cryptonote::transaction construct_miner_tx_fake_reward_1out(const size_t height, + const rct::xmr_amount reward, + const cryptonote::account_public_address &miner_address, + const uint8_t hf_version, + const size_t num_tx_outputs = 1); +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +struct stripped_down_tx_source_entry_t +{ + uint64_t global_output_index; + crypto::public_key onetime_address; + crypto::public_key real_out_tx_key; + std::vector real_out_additional_tx_keys; + size_t local_output_index; + rct::xmr_amount amount; + rct::key mask; +}; +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +cryptonote::tx_source_entry gen_tx_source_entry_fake_members( + const stripped_down_tx_source_entry_t &in, + const size_t mixin, + const uint64_t max_global_output_index); +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +cryptonote::transaction construct_pre_carrot_tx_with_fake_inputs( + const cryptonote::account_keys &sender_account_keys, + const std::unordered_map &sender_subaddress_map, + std::vector &&stripped_sources, + std::vector &destinations, + const boost::optional &change_addr, + const rct::xmr_amount fee, + const uint8_t hf_version, + crypto::secret_key &main_tx_privkey_out, + std::vector &additional_tx_privkeys_out, + const bool sweep_unmixable_override = false); +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +cryptonote::transaction construct_pre_carrot_tx_with_fake_inputs( + const cryptonote::account_keys &sender_account_keys, + const std::unordered_map &sender_subaddress_map, + std::vector &&stripped_sources, + std::vector &destinations, + const boost::optional &change_addr, + const rct::xmr_amount fee, + const uint8_t hf_version, + const bool sweep_unmixable_override = false); +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +cryptonote::transaction construct_pre_carrot_tx_with_fake_inputs( + std::vector &destinations, + const rct::xmr_amount fee, + const uint8_t hf_version, + const bool sweep_unmixable_override = false); +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +static constexpr rct::xmr_amount fake_fee_per_weight = 2023; +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +cryptonote::transaction construct_carrot_pruned_transaction_fake_inputs( + const std::vector &normal_payment_proposals, + const std::vector &selfsend_payment_proposals, + const cryptonote::account_keys &acc_keys); +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +extern const cryptonote::account_public_address null_addr; +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +} //namespace mock diff --git a/tests/unit_tests/wallet_scanning.cpp b/tests/unit_tests/wallet_scanning.cpp index c45942d98..fbccfcea0 100644 --- a/tests/unit_tests/wallet_scanning.cpp +++ b/tests/unit_tests/wallet_scanning.cpp @@ -31,13 +31,84 @@ #include "carrot_impl/address_device_ram_borrowed.h" #include "carrot_impl/carrot_tx_builder_inputs.h" #include "carrot_mock_helpers.h" +#include "cryptonote_basic/cryptonote_basic_impl.h" #include "fake_pruned_blockchain.h" #include "fcmp_pp/prove.h" +#include "tx_construction_helpers.h" #include "wallet/tx_builder.h" #undef MONERO_DEFAULT_LOG_CATEGORY #define MONERO_DEFAULT_LOG_CATEGORY "unit_tests.wallet_scanning" +//---------------------------------------------------------------------------------------------------------------------- +TEST(wallet_scanning, view_scan_as_sender_mainaddr) +{ + cryptonote::account_base aether; + aether.generate(); + + cryptonote::account_base bob; + bob.generate(); + const cryptonote::account_public_address bob_main_addr = bob.get_keys().m_account_address; + const crypto::public_key bob_main_spend_pubkey = bob_main_addr.m_spend_public_key; + + const rct::xmr_amount amount = rct::randXmrAmount(10 * COIN); + + const rct::xmr_amount fee = 565678; + + for (uint8_t hf_version = 1; hf_version < HF_VERSION_FCMP_PLUS_PLUS; ++hf_version) + { + MDEBUG("view_scan_as_sender_mainaddr: hf_version=" << static_cast(hf_version)); + + std::vector destinations{ + cryptonote::tx_destination_entry(amount, bob_main_addr, false)}; + + crypto::secret_key main_tx_privkey; + std::vector additional_tx_privkeys; + const cryptonote::transaction tx = mock::construct_pre_carrot_tx_with_fake_inputs(aether.get_keys(), + {{aether.get_keys().m_account_address.m_spend_public_key, {0, 0}}}, + {}, + destinations, + {}, + fee, + hf_version, + main_tx_privkey, + additional_tx_privkeys); + + ASSERT_EQ(0, additional_tx_privkeys.size()); + ASSERT_GT(tx.vout.size(), 0); + + // do K_d = 8 * r * K^j_v + crypto::key_derivation main_derivation; + ASSERT_TRUE(crypto::generate_key_derivation(bob_main_addr.m_view_public_key, + main_tx_privkey, + main_derivation)); + + aether.generate(); + + // call view_incoming_scan_transaction with no meaningful key nor subaddresses maps, + // just with the proper ECDH + std::vector> enote_scan_infos(tx.vout.size()); + tools::wallet::view_incoming_scan_transaction(tx, + {&main_derivation, 1}, + {}, + aether.get_keys(), + {{bob_main_spend_pubkey, {}}}, // use a fake subaddress map with just the provided address in it + epee::to_mut_span(enote_scan_infos)); + + bool matched = false; + for (const auto &enote_scan_info : enote_scan_infos) + { + if (enote_scan_info) + { + ASSERT_FALSE(matched); + ASSERT_EQ(amount, enote_scan_info->amount); + ASSERT_EQ(bob_main_spend_pubkey, enote_scan_info->address_spend_pubkey); + matched = true; + } + } + ASSERT_TRUE(matched); + } +} //---------------------------------------------------------------------------------------------------------------------- TEST(wallet_scanning, positive_smallout_main_addr_all_types_outputs) { diff --git a/tests/unit_tests/wallet_tx_builder.cpp b/tests/unit_tests/wallet_tx_builder.cpp index e6e50f12e..d8c40a93b 100644 --- a/tests/unit_tests/wallet_tx_builder.cpp +++ b/tests/unit_tests/wallet_tx_builder.cpp @@ -32,6 +32,8 @@ #include "carrot_core/config.h" #include "common/container_helpers.h" #include "ringct/rctOps.h" +#include "ringct/rctSigs.h" +#include "tx_construction_helpers.h" #include "wallet/tx_builder.h" static tools::wallet2::transfer_details gen_transfer_details()