Files
wownero/tests/unit_tests/wallet_tx_builder.cpp
+3 a9145f9d38 FCMP++/Carrot mega commit [9/14/26]
Co-authored-by: jeffro256 <jeffro256@tutanota.com>
Co-authored-by: Luke Parker <lukeparker5132@gmail.com>
Co-authored-by: Lee *!* Clagett <code@leeclagett.com>
Co-authored-by: SyntheticBird45 <someoneelse.is_on.github.rio7x@simplelogin.com>
Co-authored-by: jeetrex17 <jeetrex@tutamail.com>
Co-authored-by: UkoeHB <ukoehb@users.noreply.github.com>
Co-authored-by: MrCyjaneK
Co-authored-by: tobtoht
Co-authored-by: selsta
Co-authored-by: Thomas <thomas.giudici@proton.me>
Co-authored-by: navidR
Co-authored-by: Boog900 <boog900@tutanota.com>
2026-09-17 18:33:24 -07:00

858 lines
40 KiB
C++

// Copyright (c) 2025-2026, 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 "gtest/gtest.h"
#include "carrot_core/config.h"
#include "carrot_core/exceptions.h"
#include "carrot_impl/format_utils.h"
#include "carrot_impl/subaddress_map_legacy.h"
#include "carrot_mock_helpers.h"
#include "cryptonote_core/blockchain.h"
#include "cryptonote_core/tx_verification_utils.h"
#include "fake_pruned_blockchain.h"
#include "fcmp_pp/prove.h"
#include "ringct/rctOps.h"
#include "ringct/rctSigs.h"
#include "tx_construction_helpers.h"
#include "wallet/tx_builder.h"
#undef MONERO_DEFAULT_LOG_CATEGORY
#define MONERO_DEFAULT_LOG_CATEGORY "unit_tests.wallet_scanning"
//----------------------------------------------------------------------------------------------------------------------
//----------------------------------------------------------------------------------------------------------------------
static tools::wallet2::transfer_details gen_transfer_details()
{
cryptonote::transaction carrot_tx{};
carrot_tx.version = 2;
carrot_tx.vin.push_back(cryptonote::txin_to_key{.k_image = carrot::mock::gen_key_image()});
carrot_tx.vout.push_back(cryptonote::tx_out{.target = cryptonote::txout_to_carrot_v1{
.key = carrot::mock::gen_public_key()}});
cryptonote::add_tx_pub_key_to_extra(carrot_tx.extra, {});
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(carrot_tx.vout.size()),
.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 = {},
};
}
//----------------------------------------------------------------------------------------------------------------------
//----------------------------------------------------------------------------------------------------------------------
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;
const std::vector<carrot::CarrotTransactionProposalV1> tx_proposals = tools::wallet::make_carrot_transaction_proposals_wallet2_transfer(
transfers,
carrot::subaddress_map_legacy{{{alice.get_keys().m_account_address.m_spend_public_key, {}}}},
dsts,
/*fee_per_weight=*/1,
/*extra=*/{},
/*subaddr_account=*/0,
/*subaddr_indices=*/{},
/*ignore_above=*/MONEY_SUPPLY,
/*ignore_below=*/0,
/*max_n_inputs=*/0,
{},
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_EQ(1, tx_proposal.input_proposals.size()); // we always try 2 when available
EXPECT_EQ(transfers.front().get_public_key(), onetime_address_ref(tx_proposal.input_proposals.at(0)));
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().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::public_key, 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.get_public_key(), 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)
};
const std::vector<carrot::CarrotTransactionProposalV1> tx_proposals = tools::wallet::make_carrot_transaction_proposals_wallet2_transfer(
transfers,
alice.subaddress_map,
dsts,
/*fee_per_weight=*/1,
/*extra=*/{},
/*subaddr_account=*/spending_subaddr_account,
/*subaddr_indices=*/{},
/*ignore_above=*/MONEY_SUPPLY,
/*ignore_below=*/0,
/*max_n_inputs=*/0,
{},
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.input_proposals.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.input_proposals.size(); ++in_idx)
{
const crypto::public_key &ota = onetime_address_ref(tx_proposal.input_proposals.at(in_idx));
ASSERT_EQ(1, allowed_transfers.count(ota));
const tools::wallet2::transfer_details &td = transfers.at(allowed_transfers.at(ota));
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.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::Carrot}).address_spend_pubkey,
selfsend_payment_proposal.destination_address_spend_pubkey);
EXPECT_EQ(carrot::CarrotEnoteType::CHANGE, selfsend_payment_proposal.enote_type);
EXPECT_FALSE(selfsend_payment_proposal.internal_message);
EXPECT_FALSE(selfsend_payment_proposal.enote_ephemeral_privkey);
}
//----------------------------------------------------------------------------------------------------------------------
TEST(wallet_tx_builder, make_carrot_transaction_proposals_wallet2_transfer_3)
{
// 1 input candidate, fee subtractable
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;
const std::vector<carrot::CarrotTransactionProposalV1> tx_proposals = tools::wallet::make_carrot_transaction_proposals_wallet2_transfer(
transfers,
carrot::subaddress_map_legacy{{{alice.get_keys().m_account_address.m_spend_public_key, {}}}},
dsts,
/*fee_per_weight=*/1,
/*extra=*/{},
/*subaddr_account=*/0,
/*subaddr_indices=*/{},
/*ignore_above=*/MONEY_SUPPLY,
/*ignore_below=*/0,
/*max_n_inputs=*/0,
/*subtract_fee_from_outputs=*/{0},
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_EQ(1, tx_proposal.input_proposals.size()); // we always try 2 when available
EXPECT_EQ(transfers.front().get_public_key(), onetime_address_ref(tx_proposal.input_proposals.at(0)));
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 + tx_proposal.fee);
EXPECT_EQ(out_amount + tx_proposal.selfsend_payment_proposals.front().amount,
transfers.front().amount());
}
//----------------------------------------------------------------------------------------------------------------------
TEST(wallet_tx_builder, make_carrot_transaction_proposals_wallet2_transfer_4)
{
// 1 input candidate, fee subtractable, but not possible if it wasn't fee subtractable
cryptonote::account_base alice;
alice.generate();
cryptonote::account_base bob;
bob.generate();
const tools::wallet2::transfer_container transfers{gen_transfer_details()};
// Set output amount as generated input amount - fee * x, where x sampled in [0, 1)
// This makes a non-fee-subtractable tx impossible, b/c the required money is the generated input amount + fee
const rct::xmr_amount fee_per_weight = mock::fake_fee_per_weight;
const rct::xmr_amount expected_fee = carrot::get_fee_by_input_count(2, 0, fee_per_weight).at(1);
EXPECT_GT(expected_fee, 1);
const rct::xmr_amount out_amount = transfers.at(0).amount() - rct::randXmrAmount(expected_fee);
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,
carrot::subaddress_map_legacy{{{alice.get_keys().m_account_address.m_spend_public_key, {}}}},
dsts,
fee_per_weight,
/*extra=*/{},
/*subaddr_account=*/0,
/*subaddr_indices=*/{},
/*ignore_above=*/MONEY_SUPPLY,
/*ignore_below=*/0,
/*max_n_inputs=*/0,
/*subtract_fee_from_outputs=*/{0},
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_EQ(1, tx_proposal.input_proposals.size()); // we always try 2 when available
EXPECT_EQ(transfers.front().get_public_key(), onetime_address_ref(tx_proposal.input_proposals.at(0)));
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());
EXPECT_EQ(expected_fee, tx_proposal.fee);
// Assert amounts
EXPECT_EQ(out_amount, tx_proposal.normal_payment_proposals.front().amount + tx_proposal.fee);
EXPECT_EQ(out_amount + tx_proposal.selfsend_payment_proposals.front().amount,
transfers.front().amount());
// Check that non-fee-subtractable fails
EXPECT_THROW(tools::wallet::make_carrot_transaction_proposals_wallet2_transfer(
transfers,
carrot::subaddress_map_legacy{{{alice.get_keys().m_account_address.m_spend_public_key, {}}}},
dsts,
fee_per_weight,
/*extra=*/{},
/*subaddr_account=*/0,
/*subaddr_indices=*/{},
/*ignore_above=*/MONEY_SUPPLY,
/*ignore_below=*/0,
/*max_n_inputs=*/0,
/*subtract_fee_from_outputs=*/{},
top_block_index), carrot::not_enough_money);
}
//----------------------------------------------------------------------------------------------------------------------
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()};
const std::vector<carrot::CarrotTransactionProposalV1> tx_proposals = tools::wallet::make_carrot_transaction_proposals_wallet2_sweep(
transfers,
carrot::subaddress_map_legacy{{{alice.get_keys().m_account_address.m_spend_public_key, {}}}},
{transfers.front().m_key_image},
bob.get_keys().m_account_address,
/*is_subaddress=*/false,
/*n_dests_per_tx=*/1,
/*fee_per_weight=*/1,
/*extra=*/{},
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.input_proposals.size());
EXPECT_EQ(transfers.front().get_public_key(), onetime_address_ref(tx_proposal.input_proposals.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().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()};
const std::vector<carrot::CarrotTransactionProposalV1> tx_proposals = tools::wallet::make_carrot_transaction_proposals_wallet2_sweep(
transfers,
carrot::subaddress_map_legacy{{{alice.get_keys().m_account_address.m_spend_public_key, {}}}},
{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,
/*extra=*/{},
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.input_proposals.size());
EXPECT_EQ(transfers.front().get_public_key(), onetime_address_ref(tx_proposal.input_proposals.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().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<carrot::CarrotTransactionProposalV1> tx_proposals = tools::wallet::make_carrot_transaction_proposals_wallet2_sweep(
transfers,
carrot::subaddress_map_legacy{{{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_per_tx=*/FCMP_PLUS_PLUS_MAX_OUTPUTS,
/*fee_per_weight=*/1,
/*extra=*/{},
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.input_proposals.size());
EXPECT_EQ(transfers.front().get_public_key(), onetime_address_ref(tx_proposal.input_proposals.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).amount;
for (const auto &selfsend_payment_proposal : tx_proposal.selfsend_payment_proposals)
{
const rct::xmr_amount amount = selfsend_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_4)
{
// output-limited sweep
cryptonote::account_base alice;
alice.generate();
cryptonote::account_base bob;
bob.generate();
// generate transfers list
static constexpr size_t n_transfers = FCMP_PLUS_PLUS_MAX_INPUTS * 2;
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 + 1;
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::public_key, rct::xmr_amount> amounts_by_ota;
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_ota.emplace(td.get_public_key(), 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_ota.size());
const size_t n_dests_per_tx = 4;
// make tx proposals
const std::vector<carrot::CarrotTransactionProposalV1> tx_proposals = tools::wallet::make_carrot_transaction_proposals_wallet2_sweep(
transfers,
carrot::subaddress_map_legacy{{{alice.get_keys().m_account_address.m_spend_public_key, {}}}},
selected_key_images,
bob.get_keys().m_account_address,
/*is_subaddress=*/false,
/*n_dests_per_tx=*/n_dests_per_tx,
/*fee_per_weight=*/1,
/*extra=*/{},
top_block_index);
static constexpr size_t n_txs = (n_selected_transfers+FCMP_PLUS_PLUS_MAX_INPUTS-1) / FCMP_PLUS_PLUS_MAX_INPUTS;
static_assert(n_txs > 1);
ASSERT_EQ(n_txs, tx_proposals.size());
std::set<crypto::public_key> actual_seen_otas;
size_t n_actual_inputs = 0;
for (const carrot::CarrotTransactionProposalV1 &tx_proposal : tx_proposals)
{
ASSERT_LE(tx_proposal.input_proposals.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).amount);
EXPECT_EQ(0, tx_proposal.extra.size());
rct::xmr_amount tx_inputs_amount = 0;
for (const auto &input_proposal : tx_proposal.input_proposals)
{
const crypto::public_key ota = onetime_address_ref(input_proposal);
ASSERT_TRUE(amounts_by_ota.count(ota));
ASSERT_FALSE(actual_seen_otas.count(ota));
actual_seen_otas.insert(ota);
tx_inputs_amount += amounts_by_ota.at(ota);
}
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.input_proposals.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 = FCMP_PLUS_PLUS_MAX_INPUTS * 2;
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 + 1;
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::public_key, rct::xmr_amount> amounts_by_ota;
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_ota.emplace(td.get_public_key(), 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_ota.size());
const size_t n_dests_per_tx = 8;
// make tx proposals
const std::vector<carrot::CarrotTransactionProposalV1> tx_proposals = tools::wallet::make_carrot_transaction_proposals_wallet2_sweep(
transfers,
carrot::subaddress_map_legacy{{{alice.get_keys().m_account_address.m_spend_public_key, {}}}},
selected_key_images,
alice.get_keys().m_account_address,
/*is_subaddress=*/false,
/*n_dests_per_tx=*/n_dests_per_tx,
/*fee_per_weight=*/1,
/*extra=*/{},
top_block_index);
static constexpr size_t n_txs = (n_selected_transfers+FCMP_PLUS_PLUS_MAX_INPUTS-1) / FCMP_PLUS_PLUS_MAX_INPUTS;
static_assert(n_txs > 1);
ASSERT_EQ(n_txs, tx_proposals.size());
std::set<crypto::public_key> actual_seen_otas;
size_t n_actual_inputs = 0;
for (const carrot::CarrotTransactionProposalV1 &tx_proposal : tx_proposals)
{
ASSERT_LE(tx_proposal.input_proposals.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 carrot::InputProposalV1 &input_proposal : tx_proposal.input_proposals)
{
const crypto::public_key ota = onetime_address_ref(input_proposal);
ASSERT_TRUE(amounts_by_ota.count(ota));
ASSERT_FALSE(actual_seen_otas.count(ota));
actual_seen_otas.insert(ota);
tx_inputs_amount += amounts_by_ota.at(ota);
}
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.amount;
ASSERT_EQ(tx_inputs_amount, tx_outputs_amount);
n_actual_inputs += tx_proposal.input_proposals.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::public_key, rct::xmr_amount> amounts_by_ota;
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_ota.emplace(td.get_public_key(), 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_ota.size());
const size_t n_dests_per_tx = 2;
// make tx proposals
const std::vector<carrot::CarrotTransactionProposalV1> tx_proposals = tools::wallet::make_carrot_transaction_proposals_wallet2_sweep(
transfers,
carrot::subaddress_map_legacy{{{alice.get_keys().m_account_address.m_spend_public_key, {}}}},
selected_key_images,
alice.get_keys().m_account_address,
/*is_subaddress=*/false,
/*n_dests_per_tx=*/n_dests_per_tx,
/*fee_per_weight=*/1,
/*extra=*/{},
top_block_index);
ASSERT_EQ(1, tx_proposals.size());
const carrot::CarrotTransactionProposalV1 &tx_proposal = tx_proposals.at(0);
std::set<crypto::public_key> actual_seen_otas;
ASSERT_EQ(n_selected_transfers, tx_proposal.input_proposals.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 carrot::InputProposalV1 &input_proposal : tx_proposal.input_proposals)
{
const crypto::public_key ota = onetime_address_ref(input_proposal);
ASSERT_TRUE(amounts_by_ota.count(ota));
ASSERT_FALSE(actual_seen_otas.count(ota));
actual_seen_otas.insert(ota);
tx_inputs_amount += amounts_by_ota.at(ota);
}
const rct::xmr_amount output_amount_0 = tx_proposal.selfsend_payment_proposals.at(0).amount;
const rct::xmr_amount output_amount_1 = tx_proposal.selfsend_payment_proposals.at(1).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).enote_type;
const carrot::CarrotEnoteType enote_type_1 = tx_proposal.selfsend_payment_proposals.at(1).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_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);
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) / 2; // divide by 2 to make sure Alice has enough for the fee
const std::vector<carrot::CarrotTransactionProposalV1> tx_proposals =
tools::wallet::make_carrot_transaction_proposals_wallet2_transfer( // stupidly long function name ;(
alice.m_transfers,
carrot::subaddress_map_legacy(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,
/*max_n_inputs=*/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_fcmp_pp_proofs(tx_proposal,
alice.m_tree_cache,
*alice.m_curve_trees,
*alice.get_address_device(),
*alice.get_view_incoming_key_device(),
alice.get_view_balance_secret_device().get(),
*alice.get_spend_device());
// 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 auto 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);
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);
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);
}
//----------------------------------------------------------------------------------------------------------------------