Files
Peya/tests/unit_tests/wallet_tx_builder.cpp
Some Random Crypto Guy a4e7ebc591 added missing test code
2026-03-05 14:10:49 +00:00

814 lines
37 KiB
C++

// 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.
#include "unit_tests_utils.h"
#include "gtest/gtest.h"
#include "carrot_core/config.h"
#include "carrot_mock_helpers.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()
{
cryptonote::transaction carrot_tx;
carrot_tx.vout.push_back(cryptonote::tx_out{.target = cryptonote::txout_to_carrot_v1{}});
return tools::wallet2::transfer_details{
.m_block_height = crypto::rand_idx<uint64_t>(CRYPTONOTE_MAX_BLOCK_NUMBER),
.m_tx = carrot_tx,
.m_txid = crypto::rand<crypto::hash>(),
.m_internal_output_index = crypto::rand_idx<uint64_t>(carrot::CARROT_MAX_TX_OUTPUTS),
.m_global_output_index = crypto::rand_idx<uint64_t>(CRYPTONOTE_MAX_BLOCK_NUMBER * 1000ull),
.m_spent = false,
.m_frozen = false,
.m_spent_height = 0,
.m_key_image = crypto::key_image{rct::rct2pk(rct::pkGen())},
.m_mask = rct::skGen(),
.m_amount = crypto::rand_range<rct::xmr_amount>(COIN, 2 * COIN), // [1, 2] XMR i.e. [1e12, 2e12] pXMR
.m_rct = true,
.m_key_image_known = true,
.m_key_image_request = false,
.m_pk_index = 1,
.m_subaddr_index = {},
.m_key_image_partial = false,
.m_multisig_k = {},
.m_multisig_info = {},
.m_uses = {},
};
}
static bool compare_transfer_to_selected_input(const tools::wallet2::transfer_details &td,
const carrot::CarrotSelectedInput &input)
{
return td.m_amount == input.amount && td.m_key_image == input.key_image;
}
TEST(wallet_tx_builder, input_selection_basic)
{
std::map<std::size_t, rct::xmr_amount> fee_by_input_count;
for (size_t i = carrot::CARROT_MIN_TX_INPUTS; i <= carrot::CARROT_MAX_TX_INPUTS; ++i)
fee_by_input_count[i] = 30680000 * i - i*i;
const boost::multiprecision::uint128_t nominal_output_sum = 4444444444444; // 4.444... XMR
// add 10 random transfers
tools::wallet2::transfer_container transfers;
for (size_t i = 0; i < 10; ++i)
{
tools::wallet2::transfer_details &td = transfers.emplace_back();
td = gen_transfer_details();
td.m_block_height = transfers.size(); // small ascending block heights
}
// modify one so that it funds the transfer all by itself
const size_t rand_idx = crypto::rand_idx(transfers.size());
transfers[rand_idx].m_amount = boost::numeric_cast<rct::xmr_amount>(nominal_output_sum +
fee_by_input_count.crbegin()->second +
crypto::rand_range<rct::xmr_amount>(0, COIN));
// set such that all transfers are unlocked
const std::uint64_t top_block_index = transfers.size() + CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE;
// make input selector
std::set<size_t> selected_transfer_indices;
const carrot::select_inputs_func_t input_selector = tools::wallet::make_wallet2_single_transfer_input_selector(
transfers,
/*from_account=*/0,
/*from_subaddresses=*/{},
/*ignore_above=*/std::numeric_limits<rct::xmr_amount>::max(),
/*ignore_below=*/0,
top_block_index,
/*allow_carrot_external_inputs_in_normal_transfers=*/true,
/*allow_pre_carrot_inputs_in_normal_transfers=*/true,
/*asset_type=*/"",
selected_transfer_indices
);
// select inputs
std::vector<carrot::CarrotSelectedInput> selected_inputs;
input_selector(nominal_output_sum,
fee_by_input_count,
1, // number of normal payment proposals
1, // number of self-send payment proposals
selected_inputs);
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 content of selected inputs matches the content in `transfers`
std::set<size_t> matched_transfer_indices;
for (const carrot::CarrotSelectedInput &selected_input : selected_inputs)
{
for (const size_t selected_transfer_index : selected_transfer_indices)
{
if (compare_transfer_to_selected_input(transfers.at(selected_transfer_index), selected_input))
{
const auto insert_res = matched_transfer_indices.insert(selected_transfer_index);
if (insert_res.second)
break;
}
}
}
ASSERT_EQ(selected_transfer_indices.size(), matched_transfer_indices.size());
}
//----------------------------------------------------------------------------------------------------------------------
TEST(wallet_tx_builder, make_carrot_transaction_proposals_wallet2_transfer_1)
{
cryptonote::account_base alice;
alice.generate();
cryptonote::account_base bob;
bob.generate();
const tools::wallet2::transfer_container transfers{
gen_transfer_details()};
const rct::xmr_amount out_amount = rct::randXmrAmount(transfers.front().amount() / 2);
const std::vector<cryptonote::tx_destination_entry> dsts{
cryptonote::tx_destination_entry(out_amount, bob.get_keys().m_account_address, false)
};
const uint64_t top_block_index = std::max(transfers.front().m_block_height, transfers.back().m_block_height)
+ CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE;
tools::wallet2 w;
const std::vector<carrot::CarrotTransactionProposalV1> tx_proposals = tools::wallet::make_carrot_transaction_proposals_wallet2_transfer(
w,
dsts,
/*fee_per_weight=*/1,
5,
/*extra=*/{},
/*tx_type=*/cryptonote::transaction_type::TRANSFER,
/*subaddr_account=*/0,
/*subaddr_indices=*/{},
{},
top_block_index);
ASSERT_EQ(1, tx_proposals.size());
const carrot::CarrotTransactionProposalV1 tx_proposal = tx_proposals.at(0);
std::vector<crypto::key_image> expected_key_images{transfers.front().m_key_image};
// Assert basic length facts about tx proposal
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());
EXPECT_EQ(0, tx_proposal.extra.size());
// 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());
}
//----------------------------------------------------------------------------------------------------------------------
TEST(wallet_tx_builder, make_carrot_transaction_proposals_wallet2_transfer_2)
{
carrot::mock::mock_carrot_and_legacy_keys alice;
alice.generate();
carrot::mock::mock_carrot_and_legacy_keys bob;
bob.generate();
static constexpr uint32_t spending_subaddr_account = 2;
static_assert(spending_subaddr_account);
tools::wallet2::transfer_container transfers;
std::uint64_t top_block_index = 0;
std::unordered_map<crypto::key_image, std::size_t> allowed_transfers;
for (size_t i = 0; i < FCMP_PLUS_PLUS_MAX_INPUTS + 2; ++i)
{
tools::wallet2::transfer_details &td = transfers.emplace_back();
td = gen_transfer_details();
td.m_subaddr_index.major = (i % 2 == 0) ? spending_subaddr_account : (spending_subaddr_account - 1);
td.m_subaddr_index.minor = crypto::rand_range<std::uint32_t>(0, carrot::mock::MAX_SUBADDRESS_MINOR_INDEX);
top_block_index = std::max(top_block_index, td.m_block_height);
if (td.m_subaddr_index.major == spending_subaddr_account)
allowed_transfers.emplace(td.m_key_image, i);
}
top_block_index += CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE;
const rct::xmr_amount out_amount = COIN * 3 / 4;
const std::vector<cryptonote::tx_destination_entry> dsts{
carrot::mock::convert_destination_v1(bob.cryptonote_address(), out_amount)
};
tools::wallet2 w;
const std::vector<carrot::CarrotTransactionProposalV1> tx_proposals = tools::wallet::make_carrot_transaction_proposals_wallet2_transfer(
w,
dsts,
/*fee_per_weight=*/1,
5,
/*extra=*/{},
/*tx_type=*/cryptonote::transaction_type::TRANSFER,
/*subaddr_account=*/spending_subaddr_account,
/*subaddr_indices=*/{},
{},
top_block_index);
ASSERT_EQ(1, tx_proposals.size());
const carrot::CarrotTransactionProposalV1 &tx_proposal = tx_proposals.at(0);
// Assert basic length facts about tx proposal
ASSERT_LE(tx_proposal.key_images_sorted.size(), 2);
ASSERT_EQ(1, tx_proposal.normal_payment_proposals.size());
ASSERT_EQ(1, tx_proposal.selfsend_payment_proposals.size());
EXPECT_EQ(0, tx_proposal.extra.size());
const carrot::CarrotPaymentProposalV1 &normal_payment_proposal = tx_proposal.normal_payment_proposals.at(0);
const carrot::CarrotPaymentProposalVerifiableSelfSendV1 &selfsend_payment_proposal = tx_proposal.selfsend_payment_proposals.at(0);
// Assert that selected transfers have spending_subaddr_account subaddr major index
boost::multiprecision::uint128_t in_sum = 0;
for (std::size_t in_idx = 0; in_idx < tx_proposal.key_images_sorted.size(); ++in_idx)
{
const crypto::key_image &ki = tx_proposal.key_images_sorted.at(in_idx);
if (in_idx > 0)
{
ASSERT_LT(ki, tx_proposal.key_images_sorted.at(in_idx - 1));
}
ASSERT_EQ(1, allowed_transfers.count(ki));
const tools::wallet2::transfer_details &td = transfers.at(allowed_transfers.at(ki));
ASSERT_EQ(spending_subaddr_account, td.m_subaddr_index.major);
in_sum += td.amount();
}
// Assert balanced amounts
boost::multiprecision::uint128_t out_sum = tx_proposal.fee;
out_sum += normal_payment_proposal.amount;
out_sum += selfsend_payment_proposal.proposal.amount;
ASSERT_EQ(in_sum, out_sum);
// Assert pubkeys/subaddr indices/amounts of payment proposals
EXPECT_EQ(carrot::mock::convert_normal_payment_proposal_v1(dsts.at(0), normal_payment_proposal.randomness), normal_payment_proposal);
EXPECT_NE(normal_payment_proposal.randomness, carrot::janus_anchor_t{});
EXPECT_EQ(spending_subaddr_account, selfsend_payment_proposal.subaddr_index.index.major);
EXPECT_EQ(0, selfsend_payment_proposal.subaddr_index.index.minor);
EXPECT_EQ(alice.subaddress({{spending_subaddr_account, 0}, carrot::AddressDeriveType::PreCarrot}).address_spend_pubkey,
selfsend_payment_proposal.proposal.destination_address_spend_pubkey);
EXPECT_EQ(carrot::CarrotEnoteType::CHANGE, selfsend_payment_proposal.proposal.enote_type);
EXPECT_FALSE(selfsend_payment_proposal.proposal.internal_message);
EXPECT_FALSE(selfsend_payment_proposal.proposal.enote_ephemeral_pubkey);
}
//----------------------------------------------------------------------------------------------------------------------
TEST(wallet_tx_builder, make_carrot_transaction_proposals_wallet2_sweep_1)
{
cryptonote::account_base alice;
alice.generate();
cryptonote::account_base bob;
bob.generate();
const tools::wallet2::transfer_container transfers{gen_transfer_details()};
tools::wallet2 w;
const std::vector<carrot::CarrotTransactionProposalV1> tx_proposals = tools::wallet::make_carrot_transaction_proposals_wallet2_sweep(
// transfers,
// {{alice.get_keys().m_account_address.m_spend_public_key, {}}},
w,
{transfers.front().m_key_image},
bob.get_keys().m_account_address,
/*is_subaddress=*/false,
/*n_dests_per_tx=*/1,
/*fee_per_weight=*/1,
5,
/*extra=*/{},
/*tx_type=*/cryptonote::transaction_type::TRANSFER,
transfers.front().m_block_height + CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE);
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(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);
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()};
tools::wallet2 w;
const std::vector<carrot::CarrotTransactionProposalV1> tx_proposals = tools::wallet::make_carrot_transaction_proposals_wallet2_sweep(
// transfers,
// {{alice.get_keys().m_account_address.m_spend_public_key, {}}},
w,
{transfers.front().m_key_image},
bob.get_keys().m_account_address,
/*is_subaddress=*/false,
/*n_dests_per_tx=*/FCMP_PLUS_PLUS_MAX_OUTPUTS - 1,
/*fee_per_weight=*/1,
5,
/*extra=*/{},
/*tx_type=*/cryptonote::transaction_type::TRANSFER,
transfers.front().m_block_height + CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE);
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()};
tools::wallet2 w;
const std::vector<carrot::CarrotTransactionProposalV1> tx_proposals = tools::wallet::make_carrot_transaction_proposals_wallet2_sweep(
// transfers,
// {{alice.get_keys().m_account_address.m_spend_public_key, {}}},
w,
{transfers.front().m_key_image},
alice.get_keys().m_account_address,
/*is_subaddress=*/false,
/*n_dests_per_tx=*/FCMP_PLUS_PLUS_MAX_OUTPUTS,
/*fee_per_weight=*/1,
5,
/*extra=*/{},
/*tx_type=*/cryptonote::transaction_type::TRANSFER,
transfers.front().m_block_height + CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE);
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<size_t> 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<crypto::key_image> selected_key_images;
std::unordered_map<crypto::key_image, rct::xmr_amount> 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_per_tx = 4;
// make tx proposals
tools::wallet2 w;
const std::vector<carrot::CarrotTransactionProposalV1> tx_proposals = tools::wallet::make_carrot_transaction_proposals_wallet2_sweep(
// transfers,
// {{alice.get_keys().m_account_address.m_spend_public_key, {}}},
w,
selected_key_images,
bob.get_keys().m_account_address,
/*is_subaddress=*/false,
/*n_dests_per_tx=*/n_dests_per_tx,
/*fee_per_weight=*/1,
5,
/*extra=*/{},
/*tx_type=*/cryptonote::transaction_type::TRANSFER,
top_block_index);
ASSERT_EQ(4, tx_proposals.size());
std::set<crypto::key_image> actual_seen_kis;
size_t n_actual_inputs = 0;
for (const carrot::CarrotTransactionProposalV1 &tx_proposal : tx_proposals)
{
ASSERT_LE(tx_proposal.key_images_sorted.size(), FCMP_PLUS_PLUS_MAX_INPUTS);
ASSERT_EQ(n_dests_per_tx, 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);
}
rct::xmr_amount tx_outputs_amount = tx_proposal.fee;
for (const carrot::CarrotPaymentProposalV1 &normal_payment_proposal : tx_proposal.normal_payment_proposals)
tx_outputs_amount += normal_payment_proposal.amount;
ASSERT_EQ(tx_inputs_amount, tx_outputs_amount);
n_actual_inputs += tx_proposal.key_images_sorted.size();
}
EXPECT_EQ(n_selected_transfers, n_actual_inputs);
}
//----------------------------------------------------------------------------------------------------------------------
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<size_t> 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<crypto::key_image> selected_key_images;
std::unordered_map<crypto::key_image, rct::xmr_amount> 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_per_tx = 8;
// make tx proposals
tools::wallet2 w;
const std::vector<carrot::CarrotTransactionProposalV1> tx_proposals = tools::wallet::make_carrot_transaction_proposals_wallet2_sweep(
// transfers,
// {{alice.get_keys().m_account_address.m_spend_public_key, {}}},
w,
selected_key_images,
alice.get_keys().m_account_address,
/*is_subaddress=*/false,
/*n_dests_per_tx=*/n_dests_per_tx,
/*fee_per_weight=*/1,
5,
/*extra=*/{},
/*tx_type=*/cryptonote::transaction_type::TRANSFER,
top_block_index);
ASSERT_EQ(8, tx_proposals.size());
std::set<crypto::key_image> actual_seen_kis;
size_t n_actual_inputs = 0;
for (const carrot::CarrotTransactionProposalV1 &tx_proposal : tx_proposals)
{
ASSERT_LE(tx_proposal.key_images_sorted.size(), FCMP_PLUS_PLUS_MAX_INPUTS);
ASSERT_EQ(n_dests_per_tx == 1 ? 1 : 0, tx_proposal.normal_payment_proposals.size());
ASSERT_EQ(n_dests_per_tx, tx_proposal.selfsend_payment_proposals.size());
if (!tx_proposal.normal_payment_proposals.empty())
{
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);
}
rct::xmr_amount tx_outputs_amount = tx_proposal.fee;
for (const carrot::CarrotPaymentProposalVerifiableSelfSendV1 &selfsend_payment_proposal : tx_proposal.selfsend_payment_proposals)
tx_outputs_amount += selfsend_payment_proposal.proposal.amount;
ASSERT_EQ(tx_inputs_amount, tx_outputs_amount);
n_actual_inputs += tx_proposal.key_images_sorted.size();
}
EXPECT_EQ(n_selected_transfers, n_actual_inputs);
}
//----------------------------------------------------------------------------------------------------------------------
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<size_t> 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<crypto::key_image> selected_key_images;
std::unordered_map<crypto::key_image, rct::xmr_amount> 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_per_tx = 2;
// make tx proposals
tools::wallet2 w;
const std::vector<carrot::CarrotTransactionProposalV1> tx_proposals = tools::wallet::make_carrot_transaction_proposals_wallet2_sweep(
// transfers,
// {{alice.get_keys().m_account_address.m_spend_public_key, {}}},
w,
selected_key_images,
alice.get_keys().m_account_address,
/*is_subaddress=*/false,
/*n_dests_per_tx=*/n_dests_per_tx,
/*fee_per_weight=*/1,
5,
/*extra=*/{},
/*tx_type=*/cryptonote::transaction_type::TRANSFER,
top_block_index);
ASSERT_EQ(1, tx_proposals.size());
const carrot::CarrotTransactionProposalV1 &tx_proposal = tx_proposals.at(0);
std::set<crypto::key_image> 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
// // 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<cryptonote::tx_destination_entry> 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<cryptonote::tx_destination_entry> 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<carrot::CarrotTransactionProposalV1> 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<rct::xmr_amount>::max(),
// /*ignore_below=*/0,
// {},
// /*top_block_index=*/bc.height()-1);
// 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);
// }
//----------------------------------------------------------------------------------------------------------------------