// 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_builder.h" //local headers #include "carrot_core/config.h" #include "carrot_core/device_ram_borrowed.h" #include "carrot_core/enote_utils.h" #include "carrot_core/exceptions.h" #include "carrot_core/output_set_finalization.h" #include "carrot_core/scan.h" #include "carrot_core/scan_unsafe.cpp" #include "carrot_core/address_utils.h" #include "carrot_core/core_types.h" #include "carrot_impl/address_device_ram_borrowed.h" #include "carrot_impl/tx_builder_outputs.h" #include "carrot_impl/format_utils.h" #include "carrot_impl/input_selection.h" #include "cryptonote_basic/cryptonote_format_utils.h" #include "ringct/bulletproofs_plus.h" #include "wallet/scanning_tools.cpp" #include "common/container_helpers.h" //third party headers //standard headers #undef MONERO_DEFAULT_LOG_CATEGORY #define MONERO_DEFAULT_LOG_CATEGORY "wallet.tx_builder" namespace tools { namespace wallet { //------------------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------------------- template static constexpr T div_ceil(T dividend, T divisor) { static_assert(std::is_unsigned_v, "T not unsigned int"); return (dividend + divisor - 1) / divisor; } //------------------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------------------- static bool is_transfer_usable_for_input_selection(const wallet2::transfer_details &td, const std::uint32_t from_account, const std::set from_subaddresses, const rct::xmr_amount ignore_above, const rct::xmr_amount ignore_below, const uint64_t top_block_index) { /** * This additional check appears to be for fcmp++. const uint64_t last_locked_block_index = cryptonote::get_last_locked_block_index( td.m_tx.unlock_time, td.m_block_height); */ return !td.m_spent && td.m_key_image_known && !td.m_key_image_partial && !td.m_frozen // && last_locked_block_index <= top_block_index && td.m_subaddr_index.major == from_account && (from_subaddresses.empty() || from_subaddresses.count(td.m_subaddr_index.minor) == 1) && td.amount() >= ignore_below && td.amount() <= ignore_above ; } //------------------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------------------- static bool build_payment_proposals(std::vector &normal_payment_proposals_inout, std::vector &selfsend_payment_proposals_inout, const cryptonote::tx_destination_entry &tx_dest_entry, const std::unordered_map &subaddress_map) { const auto subaddr_it = subaddress_map.find(tx_dest_entry.addr.m_spend_public_key); const bool is_selfsend_dest = subaddr_it != subaddress_map.cend(); // Make N destinations if (is_selfsend_dest) { const carrot::subaddress_index subaddr_index{subaddr_it->second.major, subaddr_it->second.minor}; selfsend_payment_proposals_inout.push_back(carrot::CarrotPaymentProposalVerifiableSelfSendV1{ .proposal = carrot::CarrotPaymentProposalSelfSendV1{ .destination_address_spend_pubkey = tx_dest_entry.addr.m_spend_public_key, .amount = tx_dest_entry.amount, .enote_type = carrot::CarrotEnoteType::PAYMENT }, .subaddr_index = {subaddr_index, carrot::AddressDeriveType::PreCarrot} // @TODO: handle carrot }); } else // not *known* self-send address { const carrot::CarrotDestinationV1 dest{ .address_spend_pubkey = tx_dest_entry.addr.m_spend_public_key, .address_view_pubkey = tx_dest_entry.addr.m_view_public_key, .is_subaddress = tx_dest_entry.is_subaddress //! @TODO: payment ID }; normal_payment_proposals_inout.push_back(carrot::CarrotPaymentProposalV1{ .destination = dest, .amount = tx_dest_entry.amount, .randomness = carrot::gen_janus_anchor() }); } return is_selfsend_dest; } //------------------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------------------- static cryptonote::tx_destination_entry make_tx_destination_entry( const carrot::CarrotPaymentProposalV1 &payment_proposal) { cryptonote::tx_destination_entry dest = cryptonote::tx_destination_entry(payment_proposal.amount, {payment_proposal.destination.address_spend_pubkey, payment_proposal.destination.address_view_pubkey}, payment_proposal.destination.is_subaddress); dest.is_integrated = payment_proposal.destination.payment_id != carrot::null_payment_id; return dest; } //------------------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------------------- static cryptonote::tx_destination_entry make_tx_destination_entry( const carrot::CarrotPaymentProposalVerifiableSelfSendV1 &payment_proposal, const carrot::view_incoming_key_device &k_view_dev) { crypto::public_key address_view_pubkey; CHECK_AND_ASSERT_THROW_MES(k_view_dev.view_key_scalar_mult_ed25519( payment_proposal.proposal.destination_address_spend_pubkey, address_view_pubkey), "make_tx_destination_entry: view-key multiplication failed"); return cryptonote::tx_destination_entry(payment_proposal.proposal.amount, {payment_proposal.proposal.destination_address_spend_pubkey, address_view_pubkey}, payment_proposal.subaddr_index.index.is_subaddress()); } //------------------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------------------- static crypto::public_key find_change_address_spend_pubkey( const std::unordered_map &subaddress_map, const std::uint32_t subaddr_account) { const auto change_it = std::find_if(subaddress_map.cbegin(), subaddress_map.cend(), [subaddr_account](const auto &p) { return p.second.major == subaddr_account && p.second.minor == 0; }); CHECK_AND_ASSERT_THROW_MES(change_it != subaddress_map.cend(), "find_change_address_spend_pubkey: missing change address (index " << subaddr_account << ",0) in subaddress map"); const auto change_it_2 = std::find_if(std::next(change_it), subaddress_map.cend(), [subaddr_account](const auto &p) { return p.second.major == subaddr_account && p.second.minor == 0; }); CHECK_AND_ASSERT_THROW_MES(change_it_2 == subaddress_map.cend(), "find_change_address_spend_pubkey: provided subaddress map is malformed!!! At least two spend pubkeys map to " "index " << subaddr_account << ",0 in the subaddress map!"); return change_it->first; } //------------------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------------------- std::unordered_map collect_non_burned_transfers_by_key_image( const wallet2::transfer_container &transfers) { std::unordered_map best_transfer_index_by_ki; for (size_t i = 0; i < transfers.size(); ++i) { const wallet2::transfer_details &td = transfers.at(i); if (!td.m_key_image_known || td.m_key_image_partial) continue; const auto it = best_transfer_index_by_ki.find(td.m_key_image); if (it == best_transfer_index_by_ki.end()) { best_transfer_index_by_ki.insert({td.m_key_image, i}); continue; } const wallet2::transfer_details &other_td = transfers.at(it->second); if (td.amount() < other_td.amount()) continue; else if (td.amount() > other_td.amount()) it->second = i; else if (td.m_global_output_index > other_td.m_global_output_index) continue; else if (td.m_global_output_index < other_td.m_global_output_index) it->second = i; } return best_transfer_index_by_ki; } //------------------------------------------------------------------------------------------------------------------- carrot::select_inputs_func_t make_wallet2_single_transfer_input_selector( const wallet2::transfer_container &transfers, const std::uint32_t from_account, const std::set &from_subaddresses, const rct::xmr_amount ignore_above, const rct::xmr_amount ignore_below, const std::uint64_t top_block_index, const bool allow_carrot_external_inputs_in_normal_transfers, const bool allow_pre_carrot_inputs_in_normal_transfers, std::set &selected_transfer_indices_out) { // Collect transfer_container into a `std::vector` for usable inputs std::vector input_candidates; std::vector input_candidates_transfer_indices; input_candidates.reserve(transfers.size()); input_candidates_transfer_indices.reserve(transfers.size()); for (size_t i = 0; i < transfers.size(); ++i) { const wallet2::transfer_details &td = transfers.at(i); if (is_transfer_usable_for_input_selection(td, from_account, from_subaddresses, ignore_above, ignore_below, top_block_index)) { input_candidates.push_back(carrot::InputCandidate{ .core = carrot::CarrotSelectedInput{ .amount = td.amount(), .key_image = td.m_key_image }, .is_pre_carrot = !carrot::is_carrot_transaction_v1(td.m_tx), .is_external = true, //! @TODO: derive this info from field in transfer_details .block_index = td.m_block_height }); input_candidates_transfer_indices.push_back(i); } } // Create wrapper around `make_single_transfer_input_selector` return [input_candidates = std::move(input_candidates), input_candidates_transfer_indices = std::move(input_candidates_transfer_indices), allow_carrot_external_inputs_in_normal_transfers, allow_pre_carrot_inputs_in_normal_transfers, &selected_transfer_indices_out ]( const boost::multiprecision::uint128_t &nominal_output_sum, const std::map &fee_by_input_count, const std::size_t num_normal_payment_proposals, const std::size_t num_selfsend_payment_proposals, std::vector &selected_inputs_outs ){ const std::vector policies{ &carrot::ispolicy::select_greedy_aging }; std::uint32_t flags = 0; if (allow_carrot_external_inputs_in_normal_transfers) flags |= carrot::InputSelectionFlags::ALLOW_EXTERNAL_INPUTS_IN_NORMAL_TRANSFERS; if (allow_pre_carrot_inputs_in_normal_transfers) flags |= carrot::InputSelectionFlags::ALLOW_PRE_CARROT_INPUTS_IN_NORMAL_TRANSFERS; // Make inner input selection functor std::set selected_input_indices; const carrot::select_inputs_func_t inner = carrot::make_single_transfer_input_selector( epee::to_span(input_candidates), epee::to_span(policies), flags, &selected_input_indices); // Call input selection inner(nominal_output_sum, fee_by_input_count, num_normal_payment_proposals, num_selfsend_payment_proposals, selected_inputs_outs); // Collect converted selected_input_indices -> selected_transfer_indices_out selected_transfer_indices_out.clear(); for (const size_t input_index : selected_input_indices) selected_transfer_indices_out.insert(input_candidates_transfer_indices.at(input_index)); }; } //------------------------------------------------------------------------------------------------------------------- std::vector make_carrot_transaction_proposals_wallet2_transfer( const wallet2::transfer_container &transfers, const std::unordered_map &subaddress_map, std::vector dsts, const rct::xmr_amount fee_per_weight, const std::vector &extra, const std::uint32_t subaddr_account, const std::set &subaddr_indices, const rct::xmr_amount ignore_above, const rct::xmr_amount ignore_below, wallet2::unique_index_container subtract_fee_from_outputs, const std::uint64_t top_block_index) { wallet2::transfer_container unused_transfers(transfers); std::vector tx_proposals; tx_proposals.reserve(dsts.size() / (FCMP_PLUS_PLUS_MAX_OUTPUTS - 1) + 1); const crypto::public_key change_address_spend_pubkey = find_change_address_spend_pubkey(subaddress_map, subaddr_account); while (!dsts.empty()) { const std::size_t num_dsts_to_complete = std::min(dsts.size(), FCMP_PLUS_PLUS_MAX_OUTPUTS - 1); // build payment proposals and subtractable info from last `num_dsts_to_complete` dsts std::vector normal_payment_proposals; std::vector selfsend_payment_proposals; std::set subtractable_normal_payment_proposals; std::set subtractable_selfsend_payment_proposals; for (size_t i = 0; i < num_dsts_to_complete && !dsts.empty(); ++i) { const cryptonote::tx_destination_entry &dst = dsts.back(); const bool is_selfsend = build_payment_proposals(normal_payment_proposals, selfsend_payment_proposals, dst, subaddress_map); if (subtract_fee_from_outputs.count(dsts.size() - 1)) { if (is_selfsend) subtractable_selfsend_payment_proposals.insert(selfsend_payment_proposals.size() - 1); else subtractable_normal_payment_proposals.insert(normal_payment_proposals.size() - 1); } dsts.pop_back(); } // make input selector std::set selected_transfer_indices; carrot::select_inputs_func_t select_inputs = make_wallet2_single_transfer_input_selector( unused_transfers, subaddr_account, subaddr_indices, ignore_above, ignore_below, top_block_index, /*allow_carrot_external_inputs_in_normal_transfers=*/true, /*allow_pre_carrot_inputs_in_normal_transfers=*/true, selected_transfer_indices); // make proposal carrot::CarrotTransactionProposalV1 tx_proposal; carrot::make_carrot_transaction_proposal_v1_transfer( normal_payment_proposals, selfsend_payment_proposals, fee_per_weight, extra, std::move(select_inputs), change_address_spend_pubkey, {{subaddr_account, 0}, carrot::AddressDeriveType::PreCarrot}, //! @TODO: handle Carrot keys subtractable_normal_payment_proposals, subtractable_selfsend_payment_proposals, tx_proposal); // update `unused_transfers` for next proposal by removing selected transfers tools::for_all_in_vector_erase_no_preserve_order_if(unused_transfers, [&tx_proposal](const wallet2::transfer_details &td) -> bool { const auto &used_kis = tx_proposal.key_images_sorted; const auto ki_it = std::find(used_kis.cbegin(), used_kis.cend(), td.m_key_image); return ki_it != used_kis.cend(); } ); tx_proposals.push_back(std::move(tx_proposal)); } return tx_proposals; } //------------------------------------------------------------------------------------------------------------------- std::vector make_carrot_transaction_proposals_wallet2_transfer( wallet2 &w, const std::vector &dsts, const std::uint32_t priority, const std::vector &extra, const std::uint32_t subaddr_account, const std::set &subaddr_indices, const wallet2::unique_index_container &subtract_fee_from_outputs) { wallet2::transfer_container transfers; w.get_transfers(transfers); const bool use_per_byte_fee = w.use_fork_rules(HF_VERSION_PER_BYTE_FEE, 0); CHECK_AND_ASSERT_THROW_MES(use_per_byte_fee, "make_carrot_transaction_proposals_wallet2_transfer: not using per-byte base fee"); const rct::xmr_amount fee_per_weight = w.get_base_fee(priority); MDEBUG("fee_per_weight = " << fee_per_weight << ", from priority = " << priority); const std::uint64_t current_chain_height = w.get_blockchain_current_height(); CHECK_AND_ASSERT_THROW_MES(current_chain_height > 0, "make_carrot_transaction_proposals_wallet2_transfer: chain height is 0, there is no top block"); const std::uint64_t top_block_index = current_chain_height - 1; return make_carrot_transaction_proposals_wallet2_transfer( transfers, w.get_subaddress_map_ref(), dsts, fee_per_weight, extra, subaddr_account, subaddr_indices, w.ignore_outputs_above(), w.ignore_outputs_below(), subtract_fee_from_outputs, top_block_index); } //------------------------------------------------------------------------------------------------------------------- std::vector make_carrot_transaction_proposals_wallet2_sweep( const wallet2::transfer_container &transfers, const std::unordered_map &subaddress_map, const std::vector &input_key_images, const cryptonote::account_public_address &address, const bool is_subaddress, const size_t n_dests_per_tx, const rct::xmr_amount fee_per_weight, const std::vector &extra, const std::uint64_t top_block_index) { const size_t n_inputs = input_key_images.size(); CARROT_CHECK_AND_THROW(n_inputs, carrot::too_few_inputs, "no key images provided"); CARROT_CHECK_AND_THROW(n_dests_per_tx, carrot::too_few_outputs, "sweep must have at least one destination"); CARROT_CHECK_AND_THROW(n_dests_per_tx <= FCMP_PLUS_PLUS_MAX_OUTPUTS, carrot::too_many_outputs, "too many sweep destinations per transaction"); // Check that the key image is usable and isn't spent, collect amounts, and get subaddress account index std::vector input_amounts; input_amounts.reserve(input_key_images.size()); std::uint32_t subaddr_account = std::numeric_limits::max(); const auto best_transfers_by_ki = collect_non_burned_transfers_by_key_image(transfers); for (const crypto::key_image &ki : input_key_images) { const auto ki_it = best_transfers_by_ki.find(ki); CHECK_AND_ASSERT_THROW_MES(ki_it != best_transfers_by_ki.cend(), __func__ << ": unknown key image"); const wallet2::transfer_details &td = transfers.at(ki_it->second); CHECK_AND_ASSERT_THROW_MES(is_transfer_usable_for_input_selection(td, td.m_subaddr_index.major, /*from_subaddresses=*/{}, /*ignore_above=*/MONEY_SUPPLY, /*ignore_below=*/0, top_block_index), __func__ << ": transfer not usable as an input"); input_amounts.push_back(td.amount()); subaddr_account = std::min(subaddr_account, td.m_subaddr_index.major); } const crypto::public_key change_address_spend_pubkey = find_change_address_spend_pubkey(subaddress_map, subaddr_account); // get 1 payment proposal corresponding to (address, is_subaddres) std::vector normal_payment_proposals; std::vector selfsend_payment_proposals; for (size_t i = 0; i < n_dests_per_tx; ++i) { const bool is_selfsend_dest = build_payment_proposals(normal_payment_proposals, selfsend_payment_proposals, cryptonote::tx_destination_entry(/*amount=*/0, address, is_subaddress), subaddress_map); CHECK_AND_ASSERT_THROW_MES((is_selfsend_dest && selfsend_payment_proposals.size() == i+1) || (!is_selfsend_dest && normal_payment_proposals.size() == i+1), __func__ << ": BUG in build_payment_proposals: incorrect count for payment proposal lists"); } CARROT_CHECK_AND_THROW(normal_payment_proposals.size() < FCMP_PLUS_PLUS_MAX_OUTPUTS, carrot::too_many_outputs, "too many *outgoing* sweep destinations per tx, we also need 1 self-send output"); // make `n_txs` tx proposals with `n_output` payment proposals each const size_t n_txs = div_ceil(n_inputs, FCMP_PLUS_PLUS_MAX_INPUTS); std::vector tx_proposals(n_txs); size_t ki_idx = 0; for (carrot::CarrotTransactionProposalV1 &tx_proposal : tx_proposals) { // if a 2-selfsend, 2-out tx, flip one of the enote types to get unique derivations if (selfsend_payment_proposals.size() == 2) selfsend_payment_proposals.back().proposal.enote_type = carrot::CarrotEnoteType::CHANGE; // collect inputs for this tx const size_t ki_idx_end = std::min(n_inputs, ki_idx + FCMP_PLUS_PLUS_MAX_INPUTS); std::vector selected_inputs; selected_inputs.reserve(n_inputs - ki_idx_end); for (; ki_idx < ki_idx_end; ++ki_idx) selected_inputs.push_back({input_amounts.at(ki_idx), input_key_images.at(ki_idx)}); carrot::make_carrot_transaction_proposal_v1_sweep(normal_payment_proposals, selfsend_payment_proposals, fee_per_weight, extra, std::move(selected_inputs), change_address_spend_pubkey, {{subaddr_account, 0}, carrot::AddressDeriveType::PreCarrot}, //! @TODO: handle Carrot keys tx_proposal); } CARROT_CHECK_AND_THROW(ki_idx == input_key_images.size(), carrot::carrot_logic_error, "BUG: sweep_all did not consume the correct num of key images while iterating"); return tx_proposals; } //------------------------------------------------------------------------------------------------------------------- std::vector make_carrot_transaction_proposals_wallet2_sweep( wallet2 &w, const std::vector &input_key_images, const cryptonote::account_public_address &address, const bool is_subaddress, const size_t n_dests_per_tx, const std::uint32_t priority, const std::vector &extra) { wallet2::transfer_container transfers; w.get_transfers(transfers); const rct::xmr_amount fee_per_weight = w.get_base_fee(priority); const std::uint64_t current_chain_height = w.get_blockchain_current_height(); CHECK_AND_ASSERT_THROW_MES(current_chain_height > 0, "make_carrot_transaction_proposals_wallet2_sweep: chain height is 0, there is no top block"); const std::uint64_t top_block_index = current_chain_height - 1; return make_carrot_transaction_proposals_wallet2_sweep( transfers, w.get_subaddress_map_ref(), input_key_images, address, is_subaddress, n_dests_per_tx, fee_per_weight, extra, top_block_index); } //------------------------------------------------------------------------------------------------------------------- std::vector make_carrot_transaction_proposals_wallet2_sweep_all( const wallet2::transfer_container &transfers, const std::unordered_map &subaddress_map, const rct::xmr_amount only_below, const cryptonote::account_public_address &address, const bool is_subaddress, const size_t n_dests_per_tx, const rct::xmr_amount fee_per_weight, const std::vector &extra, const std::uint32_t subaddr_account, const std::set &subaddr_indices, const std::uint64_t top_block_index) { const std::unordered_map unburned_transfers_by_key_image = collect_non_burned_transfers_by_key_image(transfers); std::vector input_key_images; input_key_images.reserve(transfers.size()); for (std::size_t transfer_idx = 0; transfer_idx < transfers.size(); ++transfer_idx) { const wallet2::transfer_details &td = transfers.at(transfer_idx); if (!is_transfer_usable_for_input_selection(td, subaddr_account, subaddr_indices, only_below ? only_below : MONEY_SUPPLY, 0, top_block_index)) continue; const auto ki_it = unburned_transfers_by_key_image.find(td.m_key_image); if (ki_it == unburned_transfers_by_key_image.cend()) continue; else if (ki_it->second != transfer_idx) continue; input_key_images.push_back(td.m_key_image); } CHECK_AND_ASSERT_THROW_MES(!input_key_images.empty(), __func__ << ": no usable transfers to sweep"); return make_carrot_transaction_proposals_wallet2_sweep( transfers, subaddress_map, input_key_images, address, is_subaddress, n_dests_per_tx, fee_per_weight, extra, top_block_index); } //------------------------------------------------------------------------------------------------------------------- std::vector make_carrot_transaction_proposals_wallet2_sweep_all( wallet2 &w, const rct::xmr_amount only_below, const cryptonote::account_public_address &address, const bool is_subaddress, const size_t n_dests_per_tx, const std::uint32_t priority, const std::vector &extra, const std::uint32_t subaddr_account, const std::set &subaddr_indices) { wallet2::transfer_container transfers; w.get_transfers(transfers); const rct::xmr_amount fee_per_weight = w.get_base_fee(priority); const std::uint64_t current_chain_height = w.get_blockchain_current_height(); CHECK_AND_ASSERT_THROW_MES(current_chain_height > 0, "make_carrot_transaction_proposals_wallet2_sweep: chain height is 0, there is no top block"); const std::uint64_t top_block_index = current_chain_height - 1; return make_carrot_transaction_proposals_wallet2_sweep_all( transfers, w.get_subaddress_map_ref(), only_below, address, is_subaddress, n_dests_per_tx, fee_per_weight, extra, subaddr_account, subaddr_indices, top_block_index); } std::vector get_sources( const wallet2::transfer_container &transfers, const std::vector &selected_transfers, const std::string &source_asset, const wallet2 &w ) { // get decoys const size_t fake_outputs_count = 15; std::vector> outs; std::unordered_set valid_public_keys_cache; wallet2 &w2 = const_cast(w); w2.get_outs(outs, selected_transfers, fake_outputs_count, true, valid_public_keys_cache); // may throw LOG_PRINT_L2("preparing outputs"); size_t i = 0, out_index = 0; std::vector sources; for(size_t idx: selected_transfers) { sources.resize(sources.size()+1); cryptonote::tx_source_entry& src = sources.back(); const wallet2::transfer_details& td = transfers[idx]; // Sanity check the asset_type for this TD is correct THROW_WALLET_EXCEPTION_IF(td.asset_type != source_asset, error::wallet_internal_error, "Input has wrong asset_type - expected " + source_asset + " but found " + td.asset_type); src.amount = td.amount(); src.rct = td.is_rct(); src.carrot = td.is_carrot(); src.asset_type = td.asset_type; // Create the origin TX data if (td.m_td_origin_idx != (uint64_t)-1) { THROW_WALLET_EXCEPTION_IF(td.m_td_origin_idx >= w.get_num_transfer_details(), error::wallet_internal_error, "cannot locate return_payment origin index in m_transfers"); const wallet2::transfer_details& td_origin = w.get_transfer_details(td.m_td_origin_idx); src.origin_tx_data.tx_type = td_origin.m_tx.type; src.origin_tx_data.tx_pub_key = cryptonote::get_tx_pub_key_from_extra(td_origin.m_tx); src.origin_tx_data.output_index = td_origin.m_internal_output_index; } //paste mixin transaction THROW_WALLET_EXCEPTION_IF(outs.size() < out_index + 1 , error::wallet_internal_error, "outs.size() < out_index + 1"); THROW_WALLET_EXCEPTION_IF(outs[out_index].size() < fake_outputs_count , error::wallet_internal_error, "fake_outputs_count > random outputs found"); typedef cryptonote::tx_source_entry::output_entry tx_output_entry; for (size_t n = 0; n < fake_outputs_count + 1; ++n) { tx_output_entry oe; oe.first = std::get<0>(outs[out_index][n]); oe.second.dest = rct::pk2rct(std::get<1>(outs[out_index][n])); oe.second.mask = std::get<2>(outs[out_index][n]); src.outputs.push_back(oe); } ++i; //paste real transaction to the random index auto it_to_replace = std::find_if(src.outputs.begin(), src.outputs.end(), [&](const tx_output_entry& a) { // HERE BE DRAGONS!!! // SRCG: ring tweak to indexed per asset_type - DO NOT COMMIT UNTIL IT IS ALL WORKING //return a.first == td.m_global_output_index; return a.first == td.m_asset_type_output_index; // LAND AHOY!!! }); THROW_WALLET_EXCEPTION_IF(it_to_replace == src.outputs.end(), error::wallet_internal_error, "real output not found"); tx_output_entry real_oe; // HERE BE DRAGONS!!! // SRCG: ring tweak to indexed per asset_type - DO NOT COMMIT UNTIL IT IS ALL WORKING //real_oe.first = td.m_global_output_index; real_oe.first = td.m_asset_type_output_index; // LAND AHOY!!! real_oe.second.dest = rct::pk2rct(td.get_public_key()); real_oe.second.mask = rct::commit(td.amount(), td.m_mask); *it_to_replace = real_oe; src.real_out_tx_key = get_tx_pub_key_from_extra(td.m_tx, td.m_pk_index); src.real_out_additional_tx_keys = get_additional_tx_pub_keys_from_extra(td.m_tx); src.real_output = it_to_replace - src.outputs.begin(); src.real_output_in_tx_index = td.m_internal_output_index; src.first_rct_key_image = boost::get(td.m_tx.vin[0]).k_image; src.mask = td.m_mask; src.address_spend_pubkey = td.m_recovered_spend_pubkey; if (false) // w.m_multisig // TODO: // note: multisig_kLRki is a legacy struct, currently only used as a key image shuttle into the multisig tx builder src.multisig_kLRki = {.k = {}, .L = {}, .R = {}, .ki = rct::ki2rct(td.m_key_image)}; else src.multisig_kLRki = rct::multisig_kLRki({rct::zero(), rct::zero(), rct::zero(), rct::zero()}); detail::print_source_entry(src); ++out_index; } LOG_PRINT_L2("outputs prepared"); return sources; } //------------------------------------------------------------------------------------------------------------------- cryptonote::transaction finalize_all_proofs_from_transfer_details( const carrot::CarrotTransactionProposalV1 &tx_proposal, const std::vector &selected_transfers, const cryptonote::transaction_type tx_type, const wallet2 &w) { const size_t n_inputs = tx_proposal.key_images_sorted.size(); const size_t n_outputs = tx_proposal.normal_payment_proposals.size() + tx_proposal.selfsend_payment_proposals.size(); CHECK_AND_ASSERT_THROW_MES(n_inputs, "finalize_all_proofs_from_transfer_details: no inputs"); LOG_PRINT_L2("finalize_all_proofs_from_transfer_details: make all proofs for transaction proposal: " << n_inputs << "-in " << n_outputs << "-out, with " << tx_proposal.normal_payment_proposals.size() << " normal payment proposals, " << tx_proposal.selfsend_payment_proposals.size() << " self-send payment proposals, and a fee of " << tx_proposal.fee << " pXMR"); wallet2::transfer_container transfers; w.get_transfers(transfers); cryptonote::account_keys acc_keys = w.get_account().get_keys(); // collect core selfsend proposals std::vector selfsend_payment_proposal_cores; selfsend_payment_proposal_cores.reserve(tx_proposal.selfsend_payment_proposals.size()); for (const auto &selfsend_payment_proposal : tx_proposal.selfsend_payment_proposals) selfsend_payment_proposal_cores.push_back(selfsend_payment_proposal.proposal); //! @TODO: HW device carrot::cryptonote_hierarchy_address_device_ram_borrowed addr_dev( acc_keys.m_account_address.m_spend_public_key, acc_keys.m_view_secret_key); // finalize enotes LOG_PRINT_L3("Getting output enote proposals"); std::vector output_enote_proposals; carrot::encrypted_payment_id_t encrypted_payment_id; size_t change_index; carrot::get_output_enote_proposals(tx_proposal.normal_payment_proposals, selfsend_payment_proposal_cores, tx_proposal.dummy_encrypted_payment_id, &w.get_carrot_account().s_view_balance_dev, &addr_dev, tx_proposal.key_images_sorted.at(0), output_enote_proposals, encrypted_payment_id, nullptr, change_index); CHECK_AND_ASSERT_THROW_MES(output_enote_proposals.size() == n_outputs, "finalize_all_proofs_from_transfer_details: unexpected number of output enote proposals"); // collect all non-burned inputs owned by wallet const std::unordered_map unburned_transfers_by_key_image = collect_non_burned_transfers_by_key_image(transfers); LOG_PRINT_L3("Did a burning bug pass, eliminated " << (transfers.size() - unburned_transfers_by_key_image.size()) << " eligible transfers"); // collect output amount blinding factors std::vector output_amount_blinding_factors; output_amount_blinding_factors.reserve(output_enote_proposals.size()); for (const carrot::RCTOutputEnoteProposal &output_enote_proposal : output_enote_proposals) output_amount_blinding_factors.push_back(rct::sk2rct(output_enote_proposal.amount_blinding_factor)); // collect enotes std::vector enotes(output_enote_proposals.size()); for (size_t i = 0; i < enotes.size(); ++i) enotes[i] = output_enote_proposals.at(i).enote; // serialize transaction cryptonote::transaction tx = carrot::store_carrot_to_transaction_v1(enotes, tx_proposal.key_images_sorted, tx_proposal.fee, encrypted_payment_id); // collect input key images and amounts hw::device &hwdev = acc_keys.get_device(); std::vector sources = get_sources(transfers, selected_transfers, "SAL1", w); // inputs uint64_t amount_in = 0; rct::carrot_ctkeyV inSk; inSk.reserve(sources.size()); std::vector inamounts; std::vector index; for (size_t i = 0; i < sources.size(); ++i) { amount_in += sources[i].amount; inamounts.push_back(sources[i].amount); index.push_back(sources[i].real_output); // inSk: (x, y, mask) rct::carrot_ctkey ctkey; ctkey.mask = sources[i].mask; if (sources[i].carrot) { const epee::span main_tx_ephemeral_pubkeys = epee::to_span(std::vector{sources[i].real_out_tx_key}); const epee::span additional_tx_ephemeral_pubkeys = epee::to_span(sources[i].real_out_additional_tx_keys); // 2. perform ECDH derivations std::vector main_derivations; std::vector additional_derivations; wallet::perform_ecdh_derivations( main_tx_ephemeral_pubkeys, additional_tx_ephemeral_pubkeys, acc_keys.m_view_secret_key, hwdev, carrot::is_carrot_transaction_v1(tx), main_derivations, additional_derivations ); crypto::hash s_sender_receiver; const crypto::key_derivation &kd = main_derivations.size() ? main_derivations[0] : additional_derivations[sources[i].real_output_in_tx_index]; const mx25519_pubkey s_sender_receiver_unctx = carrot::raw_byte_convert(kd); // ephemeral pubkeys const epee::span enote_ephemeral_pubkeys_pk = main_tx_ephemeral_pubkeys.empty() ? additional_tx_ephemeral_pubkeys : main_tx_ephemeral_pubkeys; const epee::span enote_ephemeral_pubkeys = { reinterpret_cast(enote_ephemeral_pubkeys_pk.data()), enote_ephemeral_pubkeys_pk.size() }; const bool shared_ephemeral_pubkey = enote_ephemeral_pubkeys.size() == 1; const size_t ephemeral_pubkey_index = shared_ephemeral_pubkey ? 0 : sources[i].real_output_in_tx_index; // input_context const carrot::input_context_t input_context = carrot::make_carrot_input_context(sources[i].first_rct_key_image); // s^ctx_sr = H_32(s_sr, D_e, input_context) make_carrot_sender_receiver_secret(s_sender_receiver_unctx.data, enote_ephemeral_pubkeys[ephemeral_pubkey_index], input_context, s_sender_receiver); // get the k_og and k_ot crypto::secret_key sender_extension_g_out; crypto::secret_key sender_extension_t_out; crypto::public_key address_spend_pubkey_out; carrot::payment_id_t nominal_payment_id_out; carrot::janus_anchor_t nominal_janus_anchor_out; carrot::encrypted_janus_anchor_t encrypted_janus_anchor; carrot::encrypted_payment_id_t encrypted_payment_id; carrot::scan_carrot_dest_info( rct::rct2pk(sources[i].outputs[sources[i].real_output].second.dest), sources[i].outputs[sources[i].real_output].second.mask, encrypted_janus_anchor, encrypted_payment_id, s_sender_receiver, sender_extension_g_out, sender_extension_t_out, address_spend_pubkey_out, nominal_payment_id_out, nominal_janus_anchor_out ); crypto::secret_key x, y; bool r = w.get_carrot_account().try_searching_for_opening_for_onetime_address( sources[i].address_spend_pubkey, sender_extension_g_out, sender_extension_t_out, x, y ); THROW_WALLET_EXCEPTION_IF(!r, error::wallet_internal_error, "Failed to search for opening for onetime address"); ctkey.x = rct::sk2rct(x); ctkey.y = rct::sk2rct(y); } else { // generate the secret key cryptonote::keypair in_ephemeral; crypto::key_image img; rct::salvium_input_data_t sid; const auto& out_key = reinterpret_cast(sources[i].outputs[sources[i].real_output].second.dest); bool use_origin_data = (sources[i].origin_tx_data.tx_type != cryptonote::transaction_type::UNSET); sid.origin_tx_type = sources[i].origin_tx_data.tx_type; bool r = cryptonote::generate_key_image_helper( w.get_account().get_keys(), w.get_subaddress_map_ref(), out_key, sources[i].real_out_tx_key, sources[i].real_out_additional_tx_keys, sources[i].real_output_in_tx_index, in_ephemeral, img, hwdev, use_origin_data, sources[i].origin_tx_data, sid ); THROW_WALLET_EXCEPTION_IF(!r, error::wallet_internal_error, "Failed to generate key image helper"); ctkey.x = rct::sk2rct(in_ephemeral.sec); ctkey.y = rct::zero(); // not used in non-carrot txes } inSk.push_back(ctkey); memwipe(&ctkey, sizeof(rct::carrot_ctkey)); // inPk: (public key, commitment) // will be done when filling in mixRing } // outputs uint64_t amount_out = 0; std::vector outamounts; rct::keyV destinations; std::vector destination_asset_types; rct::ctkeyV outSk; for (const auto &oep : output_enote_proposals) { destinations.push_back(rct::pk2rct(oep.enote.onetime_address)); destination_asset_types.push_back(oep.enote.asset_type); outamounts.push_back(oep.amount); amount_out += oep.amount; rct::ctkey key; key.mask = rct::sk2rct(oep.amount_blinding_factor); outSk.push_back(key); } // change output x, y crypto::public_key change_address_spend_pubkey; for (const auto &p :selfsend_payment_proposal_cores) { if (p.enote_type == carrot::CarrotEnoteType::CHANGE) { change_address_spend_pubkey = p.destination_address_spend_pubkey; } } const carrot::RCTOutputEnoteProposal &change_enote_proposal = output_enote_proposals.at(change_index); const carrot::input_context_t input_context = carrot::make_carrot_input_context(tx_proposal.key_images_sorted.at(0)); crypto::hash s_sender_receiver; w.get_carrot_account().s_view_balance_dev.make_internal_sender_receiver_secret( change_enote_proposal.enote.enote_ephemeral_pubkey, input_context, s_sender_receiver); crypto::secret_key sender_extension_g; carrot::make_carrot_onetime_address_extension_g(s_sender_receiver, change_enote_proposal.enote.amount_commitment, sender_extension_g); crypto::secret_key sender_extension_t; carrot::make_carrot_onetime_address_extension_t(s_sender_receiver, change_enote_proposal.enote.amount_commitment, sender_extension_t); crypto::secret_key change_x, change_y; bool r = w.get_carrot_account().try_searching_for_opening_for_onetime_address( change_address_spend_pubkey, sender_extension_g, sender_extension_t, change_x, change_y ); THROW_WALLET_EXCEPTION_IF(!r, error::wallet_internal_error, "Failed to search for opening for onetime address"); // mixRing indexing is done the other way round for simple rct::ctkeyM mixRing(sources.size()); for (size_t i = 0; i < sources.size(); ++i) { mixRing[i].resize(sources[i].outputs.size()); for (size_t n = 0; n < sources[i].outputs.size(); ++n) { mixRing[i][n] = sources[i].outputs[n].second; } } // fee uint64_t fee = amount_in - amount_out - tx.amount_burnt; rct::BulletproofPlus bpp; bpp = rct::bulletproof_plus_PROVE(outamounts, output_amount_blinding_factors); // store proofs crypto::hash tx_prefix_hash; get_transaction_prefix_hash(tx, tx_prefix_hash, hwdev); rct::salvium_data_t salvium_data; salvium_data.salvium_data_type = rct::SalviumOne; tx.rct_signatures = rct::genRctSimpleCarrot( rct::hash2rct(tx_prefix_hash), inSk, destinations, tx_type, "SAL1", destination_asset_types, inamounts, outamounts, fee, mixRing, index, outSk, rct::RCTConfig { rct::RangeProofType::RangeProofPaddedBulletproof, 6, }, hwdev, salvium_data, rct::sk2rct(change_x), rct::sk2rct(change_y), change_index ); tx.pruned = false; return tx; } //------------------------------------------------------------------------------------------------------------------- wallet2::pending_tx make_pending_carrot_tx(const carrot::CarrotTransactionProposalV1 &tx_proposal, const wallet2::transfer_container &transfers, const crypto::secret_key &k_view, hw::device &hwdev) { const std::size_t n_inputs = tx_proposal.key_images_sorted.size(); const std::size_t n_outputs = tx_proposal.normal_payment_proposals.size() + tx_proposal.selfsend_payment_proposals.size(); const bool shared_ephemeral_pubkey = n_outputs == 2; CARROT_CHECK_AND_THROW(n_inputs >= 1, carrot::too_few_inputs, "carrot tx proposal missing inputs"); CARROT_CHECK_AND_THROW(n_outputs >= 2, carrot::too_few_outputs, "carrot tx proposal missing outputs"); const crypto::key_image &tx_first_key_image = tx_proposal.key_images_sorted.at(0); // collect non-burned transfers const std::unordered_map unburned_transfers_by_key_image = collect_non_burned_transfers_by_key_image(transfers); // collect selected_transfers and key_images string std::vector selected_transfers; selected_transfers.reserve(n_inputs); std::stringstream key_images_string; for (size_t i = 0; i < n_inputs; ++i) { const crypto::key_image &ki = tx_proposal.key_images_sorted.at(i); const auto ki_it = unburned_transfers_by_key_image.find(ki); CHECK_AND_ASSERT_THROW_MES(ki_it != unburned_transfers_by_key_image.cend(), "make_pending_carrot_tx: unrecognized key image in transfers list"); selected_transfers.push_back(ki_it->second); if (i) key_images_string << ' '; key_images_string << ki; } //! @TODO: HW device carrot::view_incoming_key_ram_borrowed_device k_view_dev(k_view); // get order of payment proposals std::vector output_enote_proposals; carrot::encrypted_payment_id_t encrypted_payment_id; std::vector> sorted_payment_proposal_indices; carrot::get_output_enote_proposals_from_proposal_v1(tx_proposal, /*s_view_balance_dev=*/nullptr, &k_view_dev, output_enote_proposals, encrypted_payment_id, &sorted_payment_proposal_indices); // calculate change_dst index based whether 2-out tx has a dummy output // change_dst is set to dummy in 2-out self-send, otherwise last self-send const bool has_2out_dummy = n_outputs == 2 && tx_proposal.normal_payment_proposals.size() == 1 && tx_proposal.normal_payment_proposals.at(0).amount == 0; CHECK_AND_ASSERT_THROW_MES(!tx_proposal.selfsend_payment_proposals.empty(), "make_pending_carrot_tx: carrot tx proposal missing a self-send proposal"); const std::pair change_dst_index{!has_2out_dummy, has_2out_dummy ? 0 : tx_proposal.selfsend_payment_proposals.size()-1}; // collect destinations and private tx keys for normal enotes //! @TODO: payment proofs for special self-send, perhaps generate d_e deterministically cryptonote::tx_destination_entry change_dts; std::vector dests; std::vector ephemeral_privkeys; dests.reserve(n_outputs); ephemeral_privkeys.reserve(n_outputs); for (const std::pair &payment_idx : sorted_payment_proposal_indices) { cryptonote::tx_destination_entry dest; const bool is_selfsend = payment_idx.first; if (is_selfsend) { dest = make_tx_destination_entry(tx_proposal.selfsend_payment_proposals.at(payment_idx.second), k_view_dev); ephemeral_privkeys.push_back(crypto::null_skey); } else // !is_selfsend { const carrot::CarrotPaymentProposalV1 &normal_payment_proposal = tx_proposal.normal_payment_proposals.at(payment_idx.second); dest = make_tx_destination_entry(normal_payment_proposal); ephemeral_privkeys.push_back(carrot::get_enote_ephemeral_privkey(normal_payment_proposal, carrot::make_carrot_input_context(tx_first_key_image))); } if (payment_idx == change_dst_index) change_dts = dest; else dests.push_back(dest); } // collect subaddr account and minor indices const std::uint32_t subaddr_account = transfers.at(selected_transfers.at(0)).m_subaddr_index.major; std::set subaddr_indices; for (const size_t selected_transfer : selected_transfers) { const wallet2::transfer_details &td = transfers.at(selected_transfer); const std::uint32_t other_subaddr_account = td.m_subaddr_index.major; if (other_subaddr_account != subaddr_account) { MWARNING("make_pending_carrot_tx: conflicting account indices: " << subaddr_account << " vs " << other_subaddr_account); } subaddr_indices.insert(td.m_subaddr_index.minor); } wallet2::pending_tx ptx; ptx.tx.set_null(); ptx.dust = 0; ptx.fee = tx_proposal.fee; ptx.dust_added_to_fee = false; ptx.change_dts = change_dts; ptx.selected_transfers = std::move(selected_transfers); ptx.key_images = key_images_string.str(); ptx.tx_key = shared_ephemeral_pubkey ? ephemeral_privkeys.at(0) : crypto::null_skey; if (shared_ephemeral_pubkey) ptx.additional_tx_keys = std::move(ephemeral_privkeys); else ptx.additional_tx_keys.clear(); ptx.dests = std::move(dests); ptx.multisig_sigs = {}; ptx.multisig_tx_key_entropy = {}; ptx.subaddr_account = subaddr_account; ptx.subaddr_indices = std::move(subaddr_indices); ptx.construction_data = tx_proposal; return ptx; } //------------------------------------------------------------------------------------------------------------------- wallet2::pending_tx finalize_all_proofs_from_transfer_details_as_pending_tx( const carrot::CarrotTransactionProposalV1 &tx_proposal, const wallet2::transfer_container &transfers, const wallet2 &w) { const auto acc_keys = w.get_account().get_keys(); wallet2::pending_tx ptx = make_pending_carrot_tx(tx_proposal, transfers, acc_keys.m_view_secret_key, acc_keys.get_device()); ptx.tx = finalize_all_proofs_from_transfer_details( tx_proposal, ptx.selected_transfers, cryptonote::transaction_type::TRANSFER, // TODO:take this as a parameter w ); return ptx; } //------------------------------------------------------------------------------------------------------------------- wallet2::pending_tx finalize_all_proofs_from_transfer_details_as_pending_tx( const carrot::CarrotTransactionProposalV1 &tx_proposal, const wallet2 &w) { wallet2::transfer_container transfers; w.get_transfers(transfers); return finalize_all_proofs_from_transfer_details_as_pending_tx( tx_proposal, transfers, w); } //------------------------------------------------------------------------------------------------------------------- } //namespace wallet } //namespace tools