diff --git a/src/wallet/tx_builder.cpp b/src/wallet/tx_builder.cpp index 7e5b384d5..e8ff67378 100644 --- a/src/wallet/tx_builder.cpp +++ b/src/wallet/tx_builder.cpp @@ -32,6 +32,7 @@ //local headers #include "carrot_core/config.h" #include "carrot_core/device_ram_borrowed.h" +#include "carrot_core/enote_utils.h" #include "carrot_impl/carrot_tx_builder_utils.h" #include "carrot_impl/input_selection.h" #include "cryptonote_basic/cryptonote_format_utils.h" @@ -466,6 +467,10 @@ std::vector make_carrot_transaction_proposa else tx_normal_payment_proposals.resize(sweep_outlay.n_tx_dests, normal_payment_proposal.at(0)); + // if a 2-selfsend, 2-out tx, flip one of the enote types to get unique derivations + if (tx_selfsend_payment_proposals.size() == 2) + tx_selfsend_payment_proposals.back().proposal.enote_type = carrot::CarrotEnoteType::CHANGE; + carrot::CarrotTransactionProposalV1 tx_proposal; carrot::make_carrot_transaction_proposal_v1_sweep(tx_normal_payment_proposals, tx_selfsend_payment_proposals, @@ -601,5 +606,129 @@ std::vector make_carrot_transaction_proposa w.get_account().get_keys()); } //------------------------------------------------------------------------------------------------------------------- +wallet2::pending_tx make_pending_carrot_tx(const carrot::CarrotTransactionProposalV1 &tx_proposal, + const wallet2::transfer_container &transfers, + const cryptonote::account_keys &acc_keys) +{ + 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; + + 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(acc_keys.m_view_secret_key); + + // 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; +} } //namespace wallet } //namespace tools diff --git a/tests/unit_tests/wallet_tx_builder.cpp b/tests/unit_tests/wallet_tx_builder.cpp index e3384424e..4b25fe36a 100644 --- a/tests/unit_tests/wallet_tx_builder.cpp +++ b/tests/unit_tests/wallet_tx_builder.cpp @@ -477,6 +477,86 @@ TEST(wallet_tx_builder, make_carrot_transaction_proposals_wallet2_sweep_5) EXPECT_EQ(n_dests, n_actual_dests); } //---------------------------------------------------------------------------------------------------------------------- +TEST(wallet_tx_builder, make_carrot_transaction_proposals_wallet2_sweep_6) +{ + // 2-dest, 2-out sweep to self + + cryptonote::account_base alice; + alice.generate(); + + // generate transfers list + static constexpr size_t n_transfers = 5; + tools::wallet2::transfer_container transfers; + transfers.reserve(n_transfers); + for (size_t i = 0; i < n_transfers; ++i) + transfers.push_back(gen_transfer_details()); + + // generate random indices into transfer list + static constexpr size_t n_selected_transfers = 3; + static_assert(n_selected_transfers < n_transfers); + std::set selected_transfer_indices; + while (selected_transfer_indices.size() < n_selected_transfers) + selected_transfer_indices.insert(crypto::rand_idx(n_transfers)); + + // generate map of amounts by key image, key image vector, and height of chain + std::vector selected_key_images; + std::unordered_map amounts_by_ki; + uint64_t top_block_index = 0; + for (const size_t selected_transfer_index : selected_transfer_indices) + { + const tools::wallet2::transfer_details &td = transfers.at(selected_transfer_index); + selected_key_images.push_back(td.m_key_image); + amounts_by_ki.emplace(td.m_key_image, td.amount()); + top_block_index = std::max(top_block_index, td.m_block_height); + } + top_block_index += CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE; + + ASSERT_EQ(n_selected_transfers, selected_key_images.size()); + ASSERT_EQ(n_selected_transfers, amounts_by_ki.size()); + + const size_t n_dests = 2; + + // make tx proposals + const std::vector tx_proposals = tools::wallet::make_carrot_transaction_proposals_wallet2_sweep( + transfers, + /*subaddress_map=*/{{alice.get_keys().m_account_address.m_spend_public_key, {}}}, + selected_key_images, + alice.get_keys().m_account_address, + /*is_subaddress=*/false, + /*n_dests=*/n_dests, + /*fee_per_weight=*/1, + /*extra=*/{}, + top_block_index, + alice.get_keys()); + ASSERT_EQ(1, tx_proposals.size()); + const carrot::CarrotTransactionProposalV1 &tx_proposal = tx_proposals.at(0); + + std::set actual_seen_kis; + + ASSERT_EQ(n_selected_transfers, tx_proposal.key_images_sorted.size()); + ASSERT_EQ(0, tx_proposal.normal_payment_proposals.size()); + ASSERT_EQ(2, tx_proposal.selfsend_payment_proposals.size()); + EXPECT_EQ(0, tx_proposal.extra.size()); + + rct::xmr_amount tx_inputs_amount = 0; + for (const crypto::key_image &ki : tx_proposal.key_images_sorted) + { + ASSERT_TRUE(amounts_by_ki.count(ki)); + ASSERT_FALSE(actual_seen_kis.count(ki)); + actual_seen_kis.insert(ki); + tx_inputs_amount += amounts_by_ki.at(ki); + } + const rct::xmr_amount output_amount_0 = tx_proposal.selfsend_payment_proposals.at(0).proposal.amount; + const rct::xmr_amount output_amount_1 = tx_proposal.selfsend_payment_proposals.at(1).proposal.amount; + const rct::xmr_amount tx_outputs_amount = tx_proposal.fee + output_amount_0 + output_amount_1; + ASSERT_EQ(tx_inputs_amount, tx_outputs_amount); + ASSERT_LE(std::max(output_amount_0, output_amount_1) - std::min(output_amount_0, output_amount_1), 1); + + const carrot::CarrotEnoteType enote_type_0 = tx_proposal.selfsend_payment_proposals.at(0).proposal.enote_type; + const carrot::CarrotEnoteType enote_type_1 = tx_proposal.selfsend_payment_proposals.at(1).proposal.enote_type; + ASSERT_NE(enote_type_0, enote_type_1); +} +//---------------------------------------------------------------------------------------------------------------------- TEST(wallet_tx_builder, wallet2_scan_propose_sign_prove_member_and_scan_1) { // 1. create fake blockchain