diff --git a/src/carrot_impl/carrot_tx_builder_utils.cpp b/src/carrot_impl/carrot_tx_builder_utils.cpp index 2797bfd04..609a545ef 100644 --- a/src/carrot_impl/carrot_tx_builder_utils.cpp +++ b/src/carrot_impl/carrot_tx_builder_utils.cpp @@ -405,6 +405,10 @@ void make_carrot_transaction_proposal_v1_sweep( const crypto::public_key &account_spend_pubkey, CarrotTransactionProposalV1 &tx_proposal_out) { + // sanity check payment proposals are provided + CHECK_AND_ASSERT_THROW_MES(normal_payment_proposals.size() || selfsend_payment_proposals.size(), + "make carrot transaction proposal v1 sweep: no payment proposals provided"); + // sanity check that all payment proposal amounts are 0 for (const CarrotPaymentProposalV1 &normal_payment_proposal : normal_payment_proposals) { @@ -417,6 +421,10 @@ void make_carrot_transaction_proposal_v1_sweep( "make carrot transaction proposal v1 sweep: payment proposal amount not 0"); } + // sanity check that either normal payment proposals XOR selfsend are provided, not both + CHECK_AND_ASSERT_THROW_MES(bool(normal_payment_proposals.size()) ^ bool(selfsend_payment_proposals.size()), + "make carrot transaction proposal v1 sweep: both normal and self-send payment proposals are provided"); + const bool is_selfsend_sweep = !selfsend_payment_proposals.empty(); // define input selection callback, which is just a shuttle for `selected_inputs` @@ -445,11 +453,12 @@ void make_carrot_transaction_proposal_v1_sweep( const size_t n_outputs = normal_payment_proposals.size() + selfsend_payment_proposals.size(); std::vector amount_ptrs; amount_ptrs.reserve(n_outputs); - for (CarrotPaymentProposalV1 &normal_payment_proposal : normal_payment_proposals) - amount_ptrs.push_back(&normal_payment_proposal.amount); if (is_selfsend_sweep) for (CarrotPaymentProposalVerifiableSelfSendV1 &selfsend_payment_proposal : selfsend_payment_proposals) amount_ptrs.push_back(&selfsend_payment_proposal.proposal.amount); + else + for (CarrotPaymentProposalV1 &normal_payment_proposal : normal_payment_proposals) + amount_ptrs.push_back(&normal_payment_proposal.amount); std::shuffle(amount_ptrs.begin(), amount_ptrs.end(), crypto::random_device{}); // disburse amount equally amongst modifiable amounts diff --git a/tests/unit_tests/wallet_tx_builder.cpp b/tests/unit_tests/wallet_tx_builder.cpp index affe5ce44..e3384424e 100644 --- a/tests/unit_tests/wallet_tx_builder.cpp +++ b/tests/unit_tests/wallet_tx_builder.cpp @@ -372,8 +372,6 @@ TEST(wallet_tx_builder, make_carrot_transaction_proposals_wallet2_sweep_4) ASSERT_LE(tx_proposal.key_images_sorted.size(), FCMP_PLUS_PLUS_MAX_INPUTS); ASSERT_EQ(1, tx_proposal.normal_payment_proposals.size()); ASSERT_EQ(1, tx_proposal.selfsend_payment_proposals.size()); - ASSERT_EQ(1, tx_proposal.normal_payment_proposals.size()); - ASSERT_EQ(1, tx_proposal.selfsend_payment_proposals.size()); ASSERT_EQ(0, tx_proposal.selfsend_payment_proposals.at(0).proposal.amount); EXPECT_EQ(0, tx_proposal.extra.size()); @@ -396,6 +394,89 @@ TEST(wallet_tx_builder, make_carrot_transaction_proposals_wallet2_sweep_4) EXPECT_EQ(n_dests, n_actual_dests); } //---------------------------------------------------------------------------------------------------------------------- +TEST(wallet_tx_builder, make_carrot_transaction_proposals_wallet2_sweep_5) +{ + // output-limited sweep to self + + cryptonote::account_base alice; + alice.generate(); + + // generate transfers list + static constexpr size_t n_transfers = 71; + 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 = FCMP_PLUS_PLUS_MAX_INPUTS * 8; + 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 = 8; + + // 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(8, tx_proposals.size()); + + std::set actual_seen_kis; + size_t n_actual_inputs = 0; + size_t n_actual_dests = 0; + for (const carrot::CarrotTransactionProposalV1 &tx_proposal : tx_proposals) + { + ASSERT_LE(tx_proposal.key_images_sorted.size(), FCMP_PLUS_PLUS_MAX_INPUTS); + ASSERT_EQ(1, tx_proposal.normal_payment_proposals.size()); + ASSERT_EQ(1, tx_proposal.selfsend_payment_proposals.size()); + ASSERT_EQ(0, tx_proposal.normal_payment_proposals.at(0).amount); + 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 tx_outputs_amount = tx_proposal.fee + tx_proposal.selfsend_payment_proposals.at(0).proposal.amount; + ASSERT_EQ(tx_inputs_amount, tx_outputs_amount); + + n_actual_inputs += tx_proposal.key_images_sorted.size(); + n_actual_dests += tx_proposal.selfsend_payment_proposals.size(); + } + + EXPECT_EQ(n_selected_transfers, n_actual_inputs); + EXPECT_EQ(n_dests, n_actual_dests); +} +//---------------------------------------------------------------------------------------------------------------------- TEST(wallet_tx_builder, wallet2_scan_propose_sign_prove_member_and_scan_1) { // 1. create fake blockchain