carrot_impl: classify 2-out dummys as change and make unique derivs for 2-selfsend 2-outs

This commit is contained in:
jeffro256
2025-05-04 00:23:17 -05:00
committed by akildemir
parent 19593db002
commit 650b6b8bd4
2 changed files with 209 additions and 0 deletions

View File

@@ -32,6 +32,7 @@
//local headers
#include "carrot_core/config.h"
#include "carrot_core/device_ram_borrowed.h"
#include "carrot_core/enote_utils.h"
#include "carrot_impl/carrot_tx_builder_utils.h"
#include "carrot_impl/input_selection.h"
#include "cryptonote_basic/cryptonote_format_utils.h"
@@ -466,6 +467,10 @@ std::vector<carrot::CarrotTransactionProposalV1> make_carrot_transaction_proposa
else
tx_normal_payment_proposals.resize(sweep_outlay.n_tx_dests, normal_payment_proposal.at(0));
// if a 2-selfsend, 2-out tx, flip one of the enote types to get unique derivations
if (tx_selfsend_payment_proposals.size() == 2)
tx_selfsend_payment_proposals.back().proposal.enote_type = carrot::CarrotEnoteType::CHANGE;
carrot::CarrotTransactionProposalV1 tx_proposal;
carrot::make_carrot_transaction_proposal_v1_sweep(tx_normal_payment_proposals,
tx_selfsend_payment_proposals,
@@ -601,5 +606,129 @@ std::vector<carrot::CarrotTransactionProposalV1> make_carrot_transaction_proposa
w.get_account().get_keys());
}
//-------------------------------------------------------------------------------------------------------------------
wallet2::pending_tx make_pending_carrot_tx(const carrot::CarrotTransactionProposalV1 &tx_proposal,
const wallet2::transfer_container &transfers,
const cryptonote::account_keys &acc_keys)
{
const std::size_t n_inputs = tx_proposal.key_images_sorted.size();
const std::size_t n_outputs = tx_proposal.normal_payment_proposals.size() +
tx_proposal.selfsend_payment_proposals.size();
const bool shared_ephemeral_pubkey = n_outputs == 2;
const crypto::key_image &tx_first_key_image = tx_proposal.key_images_sorted.at(0);
// collect non-burned transfers
const std::unordered_map<crypto::key_image, std::size_t> unburned_transfers_by_key_image =
collect_non_burned_transfers_by_key_image(transfers);
// collect selected_transfers and key_images string
std::vector<std::size_t> selected_transfers;
selected_transfers.reserve(n_inputs);
std::stringstream key_images_string;
for (size_t i = 0; i < n_inputs; ++i)
{
const crypto::key_image &ki = tx_proposal.key_images_sorted.at(i);
const auto ki_it = unburned_transfers_by_key_image.find(ki);
CHECK_AND_ASSERT_THROW_MES(ki_it != unburned_transfers_by_key_image.cend(),
"make_pending_carrot_tx: unrecognized key image in transfers list");
selected_transfers.push_back(ki_it->second);
if (i)
key_images_string << ' ';
key_images_string << ki;
}
//! @TODO: HW device
carrot::view_incoming_key_ram_borrowed_device k_view_dev(acc_keys.m_view_secret_key);
// get order of payment proposals
std::vector<carrot::RCTOutputEnoteProposal> output_enote_proposals;
carrot::encrypted_payment_id_t encrypted_payment_id;
std::vector<std::pair<bool, std::size_t>> sorted_payment_proposal_indices;
carrot::get_output_enote_proposals_from_proposal_v1(tx_proposal,
/*s_view_balance_dev=*/nullptr,
&k_view_dev,
output_enote_proposals,
encrypted_payment_id,
&sorted_payment_proposal_indices);
// calculate change_dst index based whether 2-out tx has a dummy output
// change_dst is set to dummy in 2-out self-send, otherwise last self-send
const bool has_2out_dummy = n_outputs == 2
&& tx_proposal.normal_payment_proposals.size() == 1
&& tx_proposal.normal_payment_proposals.at(0).amount == 0;
CHECK_AND_ASSERT_THROW_MES(!tx_proposal.selfsend_payment_proposals.empty(),
"make_pending_carrot_tx: carrot tx proposal missing a self-send proposal");
const std::pair<bool, std::size_t> change_dst_index{!has_2out_dummy,
has_2out_dummy ? 0 : tx_proposal.selfsend_payment_proposals.size()-1};
// collect destinations and private tx keys for normal enotes
//! @TODO: payment proofs for special self-send, perhaps generate d_e deterministically
cryptonote::tx_destination_entry change_dts;
std::vector<cryptonote::tx_destination_entry> dests;
std::vector<crypto::secret_key> ephemeral_privkeys;
dests.reserve(n_outputs);
ephemeral_privkeys.reserve(n_outputs);
for (const std::pair<bool, std::size_t> &payment_idx : sorted_payment_proposal_indices)
{
cryptonote::tx_destination_entry dest;
const bool is_selfsend = payment_idx.first;
if (is_selfsend)
{
dest = make_tx_destination_entry(tx_proposal.selfsend_payment_proposals.at(payment_idx.second),
k_view_dev);
ephemeral_privkeys.push_back(crypto::null_skey);
}
else // !is_selfsend
{
const carrot::CarrotPaymentProposalV1 &normal_payment_proposal =
tx_proposal.normal_payment_proposals.at(payment_idx.second);
dest = make_tx_destination_entry(normal_payment_proposal);
ephemeral_privkeys.push_back(carrot::get_enote_ephemeral_privkey(normal_payment_proposal,
carrot::make_carrot_input_context(tx_first_key_image)));
}
if (payment_idx == change_dst_index)
change_dts = dest;
else
dests.push_back(dest);
}
// collect subaddr account and minor indices
const std::uint32_t subaddr_account = transfers.at(selected_transfers.at(0)).m_subaddr_index.major;
std::set<std::uint32_t> subaddr_indices;
for (const size_t selected_transfer : selected_transfers)
{
const wallet2::transfer_details &td = transfers.at(selected_transfer);
const std::uint32_t other_subaddr_account = td.m_subaddr_index.major;
if (other_subaddr_account != subaddr_account)
{
MWARNING("make_pending_carrot_tx: conflicting account indices: " << subaddr_account << " vs "
<< other_subaddr_account);
}
subaddr_indices.insert(td.m_subaddr_index.minor);
}
wallet2::pending_tx ptx;
ptx.tx.set_null();
ptx.dust = 0;
ptx.fee = tx_proposal.fee;
ptx.dust_added_to_fee = false;
ptx.change_dts = change_dts;
ptx.selected_transfers = std::move(selected_transfers);
ptx.key_images = key_images_string.str();
ptx.tx_key = shared_ephemeral_pubkey ? ephemeral_privkeys.at(0) : crypto::null_skey;
if (shared_ephemeral_pubkey)
ptx.additional_tx_keys = std::move(ephemeral_privkeys);
else
ptx.additional_tx_keys.clear();
ptx.dests = std::move(dests);
ptx.multisig_sigs = {};
ptx.multisig_tx_key_entropy = {};
ptx.subaddr_account = subaddr_account;
ptx.subaddr_indices = std::move(subaddr_indices);
ptx.construction_data = tx_proposal;
return ptx;
}
} //namespace wallet
} //namespace tools

View File

@@ -477,6 +477,86 @@ TEST(wallet_tx_builder, make_carrot_transaction_proposals_wallet2_sweep_5)
EXPECT_EQ(n_dests, n_actual_dests);
}
//----------------------------------------------------------------------------------------------------------------------
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 = 2;
// make tx proposals
const std::vector<carrot::CarrotTransactionProposalV1> tx_proposals = tools::wallet::make_carrot_transaction_proposals_wallet2_sweep(
transfers,
/*subaddress_map=*/{{alice.get_keys().m_account_address.m_spend_public_key, {}}},
selected_key_images,
alice.get_keys().m_account_address,
/*is_subaddress=*/false,
/*n_dests=*/n_dests,
/*fee_per_weight=*/1,
/*extra=*/{},
top_block_index,
alice.get_keys());
ASSERT_EQ(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