231 lines
9.7 KiB
C++
231 lines
9.7 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 "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()
|
|
{
|
|
return tools::wallet2::transfer_details{
|
|
.m_block_height = crypto::rand_idx<uint64_t>(CRYPTONOTE_MAX_BLOCK_NUMBER),
|
|
.m_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>(0, COIN), // [0, 1] XMR i.e. [0, 1e12] 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::int128_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 = tools::add_element(transfers);
|
|
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,
|
|
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_EQ(2, selected_inputs.size()); // assert two inputs selected
|
|
ASSERT_EQ(2, 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<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_proposal_wallet2_transfer_1)
|
|
{
|
|
cryptonote::account_base alice;
|
|
alice.generate();
|
|
cryptonote::account_base bob;
|
|
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);
|
|
|
|
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;
|
|
|
|
const std::vector<carrot::CarrotTransactionProposalV1> tx_proposals = tools::wallet::make_carrot_transaction_proposals_wallet2_transfer(
|
|
transfers,
|
|
/*subaddress_map=*/{},
|
|
dsts,
|
|
/*fee_per_weight=*/1,
|
|
/*extra=*/{},
|
|
/*subaddr_account=*/0,
|
|
/*subaddr_indices=*/{},
|
|
/*ignore_above=*/MONEY_SUPPLY,
|
|
/*ignore_below=*/0,
|
|
{},
|
|
top_block_index,
|
|
alice);
|
|
|
|
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,
|
|
transfers.back().m_key_image};
|
|
std::sort(expected_key_images.begin(),
|
|
expected_key_images.end(),
|
|
std::greater{});
|
|
|
|
// Assert basic length facts about tx proposal
|
|
ASSERT_EQ(2, 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() + transfers.back().amount());
|
|
}
|
|
//----------------------------------------------------------------------------------------------------------------------
|
|
TEST(wallet_tx_builder, make_carrot_transaction_proposal_wallet2_sweep_1)
|
|
{
|
|
cryptonote::account_base alice;
|
|
alice.generate();
|
|
cryptonote::account_base bob;
|
|
bob.generate();
|
|
|
|
const tools::wallet2::transfer_container transfers{gen_transfer_details()};
|
|
|
|
const carrot::CarrotTransactionProposalV1 tx_proposal = tools::wallet::make_carrot_transaction_proposal_wallet2_sweep(
|
|
transfers,
|
|
/*subaddress_map=*/{},
|
|
{transfers.front().m_key_image},
|
|
bob.get_keys().m_account_address,
|
|
/*is_subaddress=*/false,
|
|
/*n_dests=*/1,
|
|
/*fee_per_weight=*/1,
|
|
/*extra=*/{},
|
|
transfers.front().m_block_height + CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE,
|
|
alice);
|
|
|
|
// 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);
|
|
}
|
|
//----------------------------------------------------------------------------------------------------------------------
|