diff --git a/src/wallet/tx_builder.cpp b/src/wallet/tx_builder.cpp index 8dadd72f1..4bcd7d9b4 100644 --- a/src/wallet/tx_builder.cpp +++ b/src/wallet/tx_builder.cpp @@ -50,17 +50,11 @@ namespace wallet { //------------------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------------------- -static bool is_transfer_unlocked_for_next_fcmp_pp_block(const wallet2::transfer_details &td, - const uint64_t top_block_index) +template +static constexpr T div_ceil(T dividend, T divisor) { - const uint64_t next_block_index = top_block_index + 1; - - // @TODO: handle FCMP++ conversion of UNIX unlock time to block index number - - if (td.m_block_height + CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE > next_block_index) - return false; - - return true; + static_assert(std::is_unsigned_v, "T not unsigned int"); + return (dividend + divisor - 1) / divisor; } //------------------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------------------- @@ -70,12 +64,18 @@ static bool is_transfer_usable_for_input_selection(const wallet2::transfer_detai 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 - && is_transfer_unlocked_for_next_fcmp_pp_block(td, top_block_index) + // && 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 @@ -254,9 +254,6 @@ std::vector make_carrot_transaction_proposa { wallet2::transfer_container unused_transfers(transfers); - //! @TODO: handle HW devices - carrot::view_incoming_key_ram_borrowed_device k_view_incoming_dev(acc_keys.m_view_secret_key); - std::vector tx_proposals; tx_proposals.reserve(dsts.size() / (FCMP_PLUS_PLUS_MAX_OUTPUTS - 1) + 1); @@ -307,8 +304,6 @@ std::vector make_carrot_transaction_proposa fee_per_weight, extra, std::move(select_inputs), - /*s_view_balance_dev=*/nullptr, //! @TODO: handle carrot - &k_view_incoming_dev, acc_keys.m_account_address.m_spend_public_key, subtractable_normal_payment_proposals, subtractable_selfsend_payment_proposals, @@ -369,7 +364,7 @@ std::vector make_carrot_transaction_proposa w.get_account().get_keys()); } //------------------------------------------------------------------------------------------------------------------- -carrot::CarrotTransactionProposalV1 make_carrot_transaction_proposal_wallet2_sweep( +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, @@ -381,10 +376,11 @@ carrot::CarrotTransactionProposalV1 make_carrot_transaction_proposal_wallet2_swe const std::uint64_t top_block_index, const cryptonote::account_keys &acc_keys) { - CHECK_AND_ASSERT_THROW_MES(!input_key_images.empty(), - "make carrot transaction proposal wallet2 sweep: no key images provided"); - CHECK_AND_ASSERT_THROW_MES(n_dests <= carrot::CARROT_MAX_TX_INPUTS, - "make carrot transaction proposal wallet2 sweep: too many destinations"); + const size_t n_inputs = input_key_images.size(); + CHECK_AND_ASSERT_THROW_MES(n_inputs, + __func__ << ": no key images provided"); + CHECK_AND_ASSERT_THROW_MES(n_dests, + __func__ << ": n_dests is zero"); // Check that the key image is available and isn't spent, and collect amounts std::vector input_amounts; @@ -394,7 +390,7 @@ carrot::CarrotTransactionProposalV1 make_carrot_transaction_proposal_wallet2_swe { const auto ki_it = best_transfers_by_ki.find(ki); CHECK_AND_ASSERT_THROW_MES(ki_it != best_transfers_by_ki.cend(), - "make carrot transaction proposal wallet2 sweep: unknown key image"); + __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, @@ -402,49 +398,90 @@ carrot::CarrotTransactionProposalV1 make_carrot_transaction_proposal_wallet2_swe /*ignore_above=*/MONEY_SUPPLY, /*ignore_below=*/0, top_block_index), - "make carrot transaction proposal wallet2 sweep: transfer not usable as an input"); + __func__ << ": transfer not usable as an input"); input_amounts.push_back(td.amount()); } - // build n_dests payment proposals - std::vector normal_payment_proposals; - std::vector selfsend_payment_proposals; - for (size_t i = 0; i < n_dests; ++i) + // get 1 payment proposal corresponding to (address, is_subaddres) + std::vector normal_payment_proposal; + std::vector selfsend_payment_proposal; + const bool is_selfsend_dest = build_payment_proposals(normal_payment_proposal, + selfsend_payment_proposal, + cryptonote::tx_destination_entry(/*amount=*/0, address, is_subaddress), + subaddress_map); + CHECK_AND_ASSERT_THROW_MES((is_selfsend_dest && selfsend_payment_proposal.size() == 1) + || (!is_selfsend_dest && normal_payment_proposal.size() == 1), + __func__ << ": BUG in build_payment_proposals: incorrect count for payment proposal lists"); + + // in/out/tx count calculations + const size_t max_dsts_per_tx = FCMP_PLUS_PLUS_MAX_OUTPUTS - (size_t(!is_selfsend_dest)); + const size_t min_n_dests = div_ceil(n_inputs, FCMP_PLUS_PLUS_MAX_INPUTS); + const size_t max_n_dests = n_inputs * max_dsts_per_tx; + CHECK_AND_ASSERT_THROW_MES(n_dests >= min_n_dests, + __func__ << ": not enough destinations (" << n_dests << ") for number of inputs to be spent (" + << n_inputs << ")"); + CHECK_AND_ASSERT_THROW_MES(n_dests <= max_n_dests, + __func__ << ": too many destinations (" << n_dests << ") for number of inputs to be spent (" + << n_inputs << ")"); + + const size_t n_txs = std::max(div_ceil(n_dests, max_dsts_per_tx), min_n_dests); + CHECK_AND_ASSERT_THROW_MES(n_txs, __func__ << ": BUG: calculated target num of txs to be 0"); + + struct sweep_tx_outlay_t { - build_payment_proposals(normal_payment_proposals, - selfsend_payment_proposals, - cryptonote::tx_destination_entry(/*amount=*/0, address, is_subaddress), - subaddress_map); + std::vector selected_inputs; + size_t n_tx_dests; + }; + + // build list of sweep_tx_outlay_t's + std::vector tx_outlays(n_txs); + size_t input_idx = 0; + for (size_t tx_idx = 0; tx_idx < tx_outlays.size(); ++tx_idx) + { + sweep_tx_outlay_t &tx_outlay = tx_outlays[tx_idx]; + + const size_t n_remaining_inputs = n_inputs - input_idx; + const size_t n_tx_inputs = std::min(div_ceil(n_inputs, n_txs), n_remaining_inputs); + const size_t n_tx_dests = n_dests / n_txs + ((tx_idx < (n_dests % n_txs)) ? 1 : 0); + + const size_t max_input_idx = input_idx + n_tx_inputs; + tx_outlay.selected_inputs.reserve(n_tx_inputs); + for (; input_idx < max_input_idx; ++input_idx) + tx_outlay.selected_inputs.push_back({input_amounts.at(input_idx), input_key_images.at(input_idx)}); + + tx_outlay.n_tx_dests = n_tx_dests; } - // Collect CarrotSelectedInput - std::vector selected_inputs(input_key_images.size()); - for (size_t i = 0; i < input_key_images.size(); ++i) + //! @TODO: sanity check tx_outlays + + // convert sweep outlays into transaction proposals + std::vector tx_proposals; + tx_proposals.reserve(tx_outlays.size()); + for (sweep_tx_outlay_t &sweep_outlay : tx_outlays) { - selected_inputs[i] = carrot::CarrotSelectedInput{ - .amount = input_amounts.at(i), - .key_image = input_key_images.at(i) - }; + std::vector tx_normal_payment_proposals; + std::vector tx_selfsend_payment_proposals; + if (is_selfsend_dest) + tx_selfsend_payment_proposals.resize(sweep_outlay.n_tx_dests, selfsend_payment_proposal.at(0)); + else + tx_normal_payment_proposals.resize(sweep_outlay.n_tx_dests, normal_payment_proposal.at(0)); + + carrot::CarrotTransactionProposalV1 tx_proposal; + carrot::make_carrot_transaction_proposal_v1_sweep(tx_normal_payment_proposals, + tx_selfsend_payment_proposals, + fee_per_weight, + extra, + std::move(sweep_outlay.selected_inputs), + acc_keys.m_account_address.m_spend_public_key, + tx_proposal); + + tx_proposals.push_back(std::move(tx_proposal)); } - //! @TODO: handle HW devices - carrot::view_incoming_key_ram_borrowed_device k_view_incoming_dev(acc_keys.m_view_secret_key); - - carrot::CarrotTransactionProposalV1 tx_proposal; - carrot::make_carrot_transaction_proposal_v1_sweep(normal_payment_proposals, - selfsend_payment_proposals, - fee_per_weight, - extra, - std::move(selected_inputs), - /*s_view_balance_dev=*/nullptr, //! @TODO: handle carrot - &k_view_incoming_dev, - acc_keys.m_account_address.m_spend_public_key, - tx_proposal); - - return tx_proposal; + return tx_proposals; } //------------------------------------------------------------------------------------------------------------------- -carrot::CarrotTransactionProposalV1 make_carrot_transaction_proposal_wallet2_sweep( +std::vector make_carrot_transaction_proposals_wallet2_sweep( wallet2 &w, const std::vector &input_key_images, const cryptonote::account_public_address &address, @@ -460,10 +497,10 @@ carrot::CarrotTransactionProposalV1 make_carrot_transaction_proposal_wallet2_swe const std::uint64_t current_chain_height = w.get_blockchain_current_height(); CHECK_AND_ASSERT_THROW_MES(current_chain_height > 0, - "make_carrot_transaction_proposal_wallet2_transfer: chain height is 0, there is no top block"); + "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_proposal_wallet2_sweep( + return make_carrot_transaction_proposals_wallet2_sweep( transfers, w.get_subaddress_map_ref(), input_key_images, diff --git a/src/wallet/tx_builder.h b/src/wallet/tx_builder.h index 98c74352c..eb5d918cc 100644 --- a/src/wallet/tx_builder.h +++ b/src/wallet/tx_builder.h @@ -80,7 +80,7 @@ std::vector make_carrot_transaction_proposa const rct::xmr_amount ignore_below, const wallet2::unique_index_container &subtract_fee_from_outputs); -carrot::CarrotTransactionProposalV1 make_carrot_transaction_proposal_wallet2_sweep( +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, @@ -91,7 +91,7 @@ carrot::CarrotTransactionProposalV1 make_carrot_transaction_proposal_wallet2_swe const std::vector& extra, const std::uint64_t top_block_index, const cryptonote::account_keys &acc_keys); -carrot::CarrotTransactionProposalV1 make_carrot_transaction_proposal_wallet2_sweep( + std::vector make_carrot_transaction_proposals_wallet2_sweep( wallet2 &w, const std::vector &input_key_images, const cryptonote::account_public_address &address, diff --git a/tests/unit_tests/wallet_tx_builder.cpp b/tests/unit_tests/wallet_tx_builder.cpp index e946ae8ea..affe5ce44 100644 --- a/tests/unit_tests/wallet_tx_builder.cpp +++ b/tests/unit_tests/wallet_tx_builder.cpp @@ -116,10 +116,9 @@ TEST(wallet_tx_builder, input_selection_basic) 1, // number of self-send payment proposals selected_inputs); - ASSERT_EQ(2, selected_inputs.size()); // assert two inputs selected - ASSERT_EQ(2, selected_transfer_indices.size()); + ASSERT_TRUE(1 == selected_inputs.size() || 2 == selected_inputs.size()); // assert one or two inputs selected + ASSERT_EQ(selected_inputs.size(), selected_transfer_indices.size()); ASSERT_LT(*selected_transfer_indices.crbegin(), transfers.size()); - ASSERT_NE(selected_inputs.front().key_image, selected_inputs.back().key_image); // Assert content of selected inputs matches the content in `transfers` std::set matched_transfer_indices; @@ -138,7 +137,7 @@ TEST(wallet_tx_builder, input_selection_basic) ASSERT_EQ(selected_transfer_indices.size(), matched_transfer_indices.size()); } //---------------------------------------------------------------------------------------------------------------------- -TEST(wallet_tx_builder, make_carrot_transaction_proposal_wallet2_transfer_1) +TEST(wallet_tx_builder, make_carrot_transaction_proposals_wallet2_transfer_1) { cryptonote::account_base alice; alice.generate(); @@ -146,7 +145,6 @@ TEST(wallet_tx_builder, make_carrot_transaction_proposal_wallet2_transfer_1) bob.generate(); const tools::wallet2::transfer_container transfers{ - gen_transfer_details(), gen_transfer_details()}; const rct::xmr_amount out_amount = rct::randXmrAmount(transfers.front().amount() / 2); @@ -175,15 +173,10 @@ TEST(wallet_tx_builder, make_carrot_transaction_proposal_wallet2_transfer_1) ASSERT_EQ(1, tx_proposals.size()); const carrot::CarrotTransactionProposalV1 tx_proposal = tx_proposals.at(0); - std::vector expected_key_images{ - transfers.front().m_key_image, - transfers.back().m_key_image}; - std::sort(expected_key_images.begin(), - expected_key_images.end(), - std::greater{}); + std::vector expected_key_images{transfers.front().m_key_image}; // Assert basic length facts about tx proposal - ASSERT_EQ(2, tx_proposal.key_images_sorted.size()); // we always try 2 when available + ASSERT_EQ(1, tx_proposal.key_images_sorted.size()); // we always try 2 when available EXPECT_EQ(expected_key_images, tx_proposal.key_images_sorted); ASSERT_EQ(1, tx_proposal.normal_payment_proposals.size()); ASSERT_EQ(1, tx_proposal.selfsend_payment_proposals.size()); @@ -192,10 +185,10 @@ TEST(wallet_tx_builder, make_carrot_transaction_proposal_wallet2_transfer_1) // Assert amounts EXPECT_EQ(out_amount, tx_proposal.normal_payment_proposals.front().amount); EXPECT_EQ(out_amount + tx_proposal.selfsend_payment_proposals.front().proposal.amount + tx_proposal.fee, - transfers.front().amount() + transfers.back().amount()); + transfers.front().amount()); } //---------------------------------------------------------------------------------------------------------------------- -TEST(wallet_tx_builder, make_carrot_transaction_proposal_wallet2_sweep_1) +TEST(wallet_tx_builder, make_carrot_transaction_proposals_wallet2_sweep_1) { cryptonote::account_base alice; alice.generate(); @@ -204,7 +197,7 @@ TEST(wallet_tx_builder, make_carrot_transaction_proposal_wallet2_sweep_1) const tools::wallet2::transfer_container transfers{gen_transfer_details()}; - const carrot::CarrotTransactionProposalV1 tx_proposal = tools::wallet::make_carrot_transaction_proposal_wallet2_sweep( + const std::vector tx_proposals = tools::wallet::make_carrot_transaction_proposals_wallet2_sweep( transfers, /*subaddress_map=*/{}, {transfers.front().m_key_image}, @@ -214,7 +207,9 @@ TEST(wallet_tx_builder, make_carrot_transaction_proposal_wallet2_sweep_1) /*fee_per_weight=*/1, /*extra=*/{}, transfers.front().m_block_height + CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE, - alice); + alice.get_keys()); + ASSERT_EQ(1, tx_proposals.size()); + const carrot::CarrotTransactionProposalV1 &tx_proposal = tx_proposals.at(0); // Assert basic length facts about tx proposal ASSERT_EQ(1, tx_proposal.key_images_sorted.size()); @@ -228,3 +223,320 @@ TEST(wallet_tx_builder, make_carrot_transaction_proposal_wallet2_sweep_1) EXPECT_EQ(transfers.front().amount(), tx_proposal.fee + tx_proposal.normal_payment_proposals.front().amount); } //---------------------------------------------------------------------------------------------------------------------- +TEST(wallet_tx_builder, make_carrot_transaction_proposals_wallet2_sweep_2) +{ + cryptonote::account_base alice; + alice.generate(); + cryptonote::account_base bob; + bob.generate(); + + const tools::wallet2::transfer_container transfers{gen_transfer_details()}; + + const std::vector tx_proposals = tools::wallet::make_carrot_transaction_proposals_wallet2_sweep( + transfers, + /*subaddress_map=*/{}, + {transfers.front().m_key_image}, + bob.get_keys().m_account_address, + /*is_subaddress=*/false, + /*n_dests=*/FCMP_PLUS_PLUS_MAX_OUTPUTS - 1, + /*fee_per_weight=*/1, + /*extra=*/{}, + transfers.front().m_block_height + CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE, + alice.get_keys()); + ASSERT_EQ(1, tx_proposals.size()); + const carrot::CarrotTransactionProposalV1 &tx_proposal = tx_proposals.at(0); + + // Assert basic length facts about tx proposal + ASSERT_EQ(1, tx_proposal.key_images_sorted.size()); + EXPECT_EQ(transfers.front().m_key_image, tx_proposal.key_images_sorted.front()); + ASSERT_EQ(FCMP_PLUS_PLUS_MAX_OUTPUTS - 1, tx_proposal.normal_payment_proposals.size()); + ASSERT_EQ(1, tx_proposal.selfsend_payment_proposals.size()); + EXPECT_EQ(0, tx_proposal.extra.size()); + + // Assert amounts + EXPECT_EQ(0, tx_proposal.selfsend_payment_proposals.front().proposal.amount); + rct::xmr_amount total_output_amount = tx_proposal.fee; + const rct::xmr_amount first_output_amount = tx_proposal.normal_payment_proposals.at(0).amount; + for (const auto &normal_payment_proposal : tx_proposal.normal_payment_proposals) + { + const rct::xmr_amount amount = normal_payment_proposal.amount; + const rct::xmr_amount max_amount = std::max(amount, first_output_amount); + const rct::xmr_amount min_amount = std::min(amount, first_output_amount); + EXPECT_LE(max_amount - min_amount, 1); + total_output_amount += amount; + } + EXPECT_EQ(transfers.front().amount(), total_output_amount); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(wallet_tx_builder, make_carrot_transaction_proposals_wallet2_sweep_3) +{ + cryptonote::account_base alice; + alice.generate(); + + const tools::wallet2::transfer_container transfers{gen_transfer_details()}; + + 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, {}}}, + {transfers.front().m_key_image}, + alice.get_keys().m_account_address, + /*is_subaddress=*/false, + /*n_dests=*/FCMP_PLUS_PLUS_MAX_OUTPUTS, + /*fee_per_weight=*/1, + /*extra=*/{}, + transfers.front().m_block_height + CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE, + alice.get_keys()); + ASSERT_EQ(1, tx_proposals.size()); + const carrot::CarrotTransactionProposalV1 &tx_proposal = tx_proposals.at(0); + + // Assert basic length facts about tx proposal + ASSERT_EQ(1, tx_proposal.key_images_sorted.size()); + EXPECT_EQ(transfers.front().m_key_image, tx_proposal.key_images_sorted.front()); + ASSERT_EQ(0, tx_proposal.normal_payment_proposals.size()); + ASSERT_EQ(FCMP_PLUS_PLUS_MAX_OUTPUTS, tx_proposal.selfsend_payment_proposals.size()); + EXPECT_EQ(0, tx_proposal.extra.size()); + + // Assert amounts + rct::xmr_amount total_output_amount = tx_proposal.fee; + const rct::xmr_amount first_output_amount = tx_proposal.selfsend_payment_proposals.at(0).proposal.amount; + for (const auto &selfsend_payment_proposal : tx_proposal.selfsend_payment_proposals) + { + const rct::xmr_amount amount = selfsend_payment_proposal.proposal.amount; + const rct::xmr_amount max_amount = std::max(amount, first_output_amount); + const rct::xmr_amount min_amount = std::min(amount, first_output_amount); + EXPECT_LE(max_amount - min_amount, 1); + total_output_amount += amount; + } + EXPECT_EQ(transfers.front().amount(), total_output_amount); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(wallet_tx_builder, make_carrot_transaction_proposals_wallet2_sweep_4) +{ + // output-limited sweep + + cryptonote::account_base alice; + alice.generate(); + cryptonote::account_base bob; + bob.generate(); + + // generate transfers list + const size_t n_transfers = 35; + 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 + const size_t n_selected_transfers = 31; + 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 = 4; + + // make tx proposals + const std::vector tx_proposals = tools::wallet::make_carrot_transaction_proposals_wallet2_sweep( + transfers, + /*subaddress_map=*/{}, + selected_key_images, + bob.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(4, 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(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()); + + 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.normal_payment_proposals.at(0).amount; + ASSERT_EQ(tx_inputs_amount, tx_outputs_amount); + + n_actual_inputs += tx_proposal.key_images_sorted.size(); + n_actual_dests += tx_proposal.normal_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 + // 2. create Alice, Bob wallet2 instance + // 3. send a mix of fake-input legacy and carrot txs to Alice + // 4. step blockchain forward 10 blocks + // 5. scan blockchain with Alice wallet + // 6. create carrot transaction proposal + // 7. construct proofs for transaction + // 8. serialize tx + // 9. deserialize tx + // 10. check ver_non_input_consensus() + // 11. check verRctNonSemanticsSimple() + // 12. add Alice's transaction to blockchain + // 13. scan blockchain with Bob's wallet and assert money received + // 14. scan blockchain with Alice's wallet and assert money left + + // 1. + LOG_PRINT_L2("Initiating my imaginary, friendly chain of blocks"); + mock::fake_pruned_blockchain bc(0); + + // 2. + LOG_PRINT_L2("Generating wallets for Alice and Bob, the usual suspects"); + tools::wallet2 alice(cryptonote::MAINNET, /*kdf_rounds=*/1, /*unattended=*/true); + tools::wallet2 bob(cryptonote::MAINNET, /*kdf_rounds=*/1, /*unattended=*/true); + alice.set_offline(true); + bob.set_offline(true); + alice.generate("", ""); + bob.generate("", ""); + const cryptonote::account_keys &alice_keys = alice.get_account().get_keys(); + const cryptonote::account_public_address alice_main_addr = alice.get_account().get_keys().m_account_address; + const cryptonote::account_public_address bob_main_addr = bob.get_account().get_keys().m_account_address; + bc.init_wallet_for_starting_block(alice); + bc.init_wallet_for_starting_block(bob); + + // 3. + LOG_PRINT_L2("Sending transactions from the aether to Alice (0)"); + const rct::xmr_amount amount0 = rct::randXmrAmount(COIN); + std::vector dests0{cryptonote::tx_destination_entry(amount0, alice_main_addr, false)}; + cryptonote::transaction tx = mock::construct_pre_carrot_tx_with_fake_inputs(dests0, /*fee=*/1234, /*hf_version=*/2); + bc.add_block(2, {std::move(tx)}, mock::null_addr); + LOG_PRINT_L2("Sending transactions from the aether to Alice (1)"); + const rct::xmr_amount amount1 = rct::randXmrAmount(COIN); + std::vector dests1{cryptonote::tx_destination_entry(amount1, alice.get_subaddress({0, 13}), true)}; + cryptonote::account_base aether; + aether.generate(); + tx = mock::construct_carrot_pruned_transaction_fake_inputs({carrot::mock::convert_normal_payment_proposal_v1(dests1.front())}, {}, aether.get_keys()); + bc.add_block(HF_VERSION_CARROT, {std::move(tx)}, mock::null_addr); + + // 4. + //!@TODO: figure out why membership proving fails if there's fewer leaves than the curve1 width + const size_t target_num_outputs = fcmp_pp::curve_trees::SELENE_CHUNK_WIDTH * fcmp_pp::curve_trees::HELIOS_CHUNK_WIDTH + 7; + while (bc.num_outputs() < target_num_outputs) + bc.add_block(HF_VERSION_CARROT, {}, mock::null_addr, target_num_outputs - bc.num_outputs()); + + LOG_PRINT_L2("Twiddling thumbs"); + for (size_t i = 0; i < CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW; ++i) + bc.add_block(HF_VERSION_CARROT, {}, mock::null_addr); + + // 5. + LOG_PRINT_L2("Alice's vision is filled with shadowy keys, hashes, points, rings, trees, curves, chains, all flowing in and out of one another"); + uint64_t blocks_added = bc.refresh_wallet(alice, 0); + ASSERT_EQ(bc.height()-1, blocks_added); + ASSERT_EQ(2, alice.m_transfers.size()); + ASSERT_EQ(amount0 + amount1, alice.balance_all(true)); // really, we care about unlocked_balance_all() for sending, but that call uses RPC + + // 6. + LOG_PRINT_L2("Alice feels pity on Bob and proposes to send his broke ass some dough"); + const rct::xmr_amount out_amount = rct::randXmrAmount(amount0 + amount1); + const std::vector tx_proposals = + tools::wallet::make_carrot_transaction_proposals_wallet2_transfer( // stupidly long function name ;( + alice.m_transfers, + alice.m_subaddresses, + {cryptonote::tx_destination_entry(out_amount, bob_main_addr, false)}, + /*fee_per_weight=*/1, + /*extra=*/{}, + /*subaddr_account=*/0, + /*subaddr_indices=*/{}, + /*ignore_above=*/std::numeric_limits::max(), + /*ignore_below=*/0, + {}, + /*top_block_index=*/bc.height()-1, + alice_keys); + + ASSERT_EQ(1, tx_proposals.size()); + const carrot::CarrotTransactionProposalV1 tx_proposal = tx_proposals.at(0); + + // 7. + LOG_PRINT_L2("Alice has something to prove"); + tx = tools::wallet::finalize_all_proofs_from_transfer_details(tx_proposal, + alice.m_transfers, + alice.m_tree_cache, + *alice.m_curve_trees, + alice_keys); + + // 8. + LOG_PRINT_L2("Hello, Mr. Blobby"); + const cryptonote::blobdata alicebob_tx_blob = cryptonote::tx_to_blob(tx); + + // 9. + LOG_PRINT_L2("Goodbye, Mr. Blobby"); + cryptonote::transaction alicebob_tx; + ASSERT_TRUE(cryptonote::parse_and_validate_tx_from_blob(alicebob_tx_blob, alicebob_tx)); + + // 10. + LOG_PRINT_L2("Bob couldn't believe someone to be so generous in his time of need, so he verifies"); + ASSERT_GE(bc.hf_version(), HF_VERSION_FCMP_PLUS_PLUS); + cryptonote::tx_verification_context tvc{}; + ASSERT_TRUE(cryptonote::ver_non_input_consensus(alicebob_tx, tvc, bc.hf_version())); + EXPECT_FALSE(tvc.m_verifivation_failed); + + // 11. + LOG_PRINT_L2("'Perhaps this is valid money that belongs to another chain', Bob postulates"); + const uint8_t *tree_root = bc.get_fcmp_tree_root_at(bc.height() - 1); + ASSERT_TRUE(cryptonote::Blockchain::expand_transaction_2(alicebob_tx, + cryptonote::get_transaction_prefix_hash(alicebob_tx), + /*pubkeys=*/{}, + tree_root)); + EXPECT_TRUE(rct::verRctNonSemanticsSimple(alicebob_tx.rct_signatures)); + + // 12. + LOG_PRINT_L2("'Chain, chain, chain (Chain, chain, chain)' - Aretha Franklin"); + const rct::xmr_amount alicebob_tx_fee = alicebob_tx.rct_signatures.txnFee; + bc.add_block(HF_VERSION_CARROT, {std::move(alicebob_tx)}, mock::null_addr); + + // 13. + LOG_PRINT_L2("A great day for Bob"); + ASSERT_EQ(0, bob.balance_all(true)); + blocks_added = bc.refresh_wallet(bob, 0); + ASSERT_EQ(bc.height()-1, blocks_added); + ASSERT_EQ(1, bob.m_transfers.size()); + EXPECT_EQ(out_amount, bob.balance_all(true)); + + // 14. + LOG_PRINT_L2("Alice obtains the fulfillment that only stems from selfless generosity"); + const rct::xmr_amount alice_old_balance = alice.balance_all(true); + ASSERT_GE(alice_old_balance, out_amount + alicebob_tx_fee); + blocks_added = bc.refresh_wallet(alice, 0); + ASSERT_EQ(1, blocks_added); + const rct::xmr_amount alice_new_balance = alice.balance_all(true); + ASSERT_LT(alice_new_balance, alice_old_balance); + EXPECT_EQ(alice_new_balance + out_amount + alicebob_tx_fee, alice_old_balance); +} +//----------------------------------------------------------------------------------------------------------------------