Files
salvium/src/wallet/tx_builder.cpp
2025-07-23 10:35:14 +01:00

1380 lines
61 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.
//paired header
#include "tx_builder.h"
//local headers
#include "carrot_core/config.h"
#include "carrot_core/device_ram_borrowed.h"
#include "carrot_core/enote_utils.h"
#include "carrot_core/exceptions.h"
#include "carrot_core/output_set_finalization.h"
#include "carrot_core/scan.h"
#include "carrot_core/scan_unsafe.cpp"
#include "carrot_core/address_utils.h"
#include "carrot_core/core_types.h"
#include "carrot_impl/address_device_ram_borrowed.h"
#include "carrot_impl/tx_builder_outputs.h"
#include "carrot_impl/format_utils.h"
#include "carrot_impl/input_selection.h"
#include "cryptonote_basic/cryptonote_format_utils.h"
#include "ringct/bulletproofs_plus.h"
#include "wallet/scanning_tools.cpp"
#include "common/container_helpers.h"
#include "carrot_core/payment_proposal.cpp"
//third party headers
//standard headers
#undef MONERO_DEFAULT_LOG_CATEGORY
#define MONERO_DEFAULT_LOG_CATEGORY "wallet.tx_builder"
namespace tools
{
namespace wallet
{
//-------------------------------------------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------------------------------------
template <typename T>
static constexpr T div_ceil(T dividend, T divisor)
{
static_assert(std::is_unsigned_v<T>, "T not unsigned int");
return (dividend + divisor - 1) / divisor;
}
//-------------------------------------------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------------------------------------
static bool is_transfer_usable_for_input_selection(const wallet2::transfer_details &td,
const std::uint32_t from_account,
const std::set<std::uint32_t> from_subaddresses,
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);
*/
// Reject locked outputs
size_t blocks_locked_for = CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE;
if (td.m_tx.type == cryptonote::transaction_type::MINER || td.m_tx.type == cryptonote::transaction_type::PROTOCOL)
blocks_locked_for = CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW;
return !td.m_spent
&& td.m_key_image_known
&& !td.m_key_image_partial
&& !td.m_frozen
&& (top_block_index >= td.m_block_height + blocks_locked_for)
// && 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
&& td.amount() <= ignore_above
&& td.asset_type == "SAL1"
;
}
//-------------------------------------------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------------------------------------
static bool build_payment_proposals(std::vector<carrot::CarrotPaymentProposalV1> &normal_payment_proposals_inout,
std::vector<carrot::CarrotPaymentProposalVerifiableSelfSendV1> &selfsend_payment_proposals_inout,
const cryptonote::tx_destination_entry &tx_dest_entry,
const std::unordered_map<crypto::public_key, cryptonote::subaddress_index> &subaddress_map)
{
const auto subaddr_it = subaddress_map.find(tx_dest_entry.addr.m_spend_public_key);
const bool is_selfsend_dest = subaddr_it != subaddress_map.cend();
// Make N destinations
if (is_selfsend_dest)
{
const carrot::subaddress_index subaddr_index{subaddr_it->second.major, subaddr_it->second.minor};
selfsend_payment_proposals_inout.push_back(carrot::CarrotPaymentProposalVerifiableSelfSendV1{
.proposal = carrot::CarrotPaymentProposalSelfSendV1{
.destination_address_spend_pubkey = tx_dest_entry.addr.m_spend_public_key,
.amount = tx_dest_entry.amount,
.enote_type = carrot::CarrotEnoteType::PAYMENT
},
.subaddr_index = {subaddr_index, carrot::AddressDeriveType::Carrot, false},
});
}
else // not *known* self-send address
{
const carrot::CarrotDestinationV1 dest{
.address_spend_pubkey = tx_dest_entry.addr.m_spend_public_key,
.address_view_pubkey = tx_dest_entry.addr.m_view_public_key,
.is_subaddress = tx_dest_entry.is_subaddress
//! @TODO: payment ID
};
normal_payment_proposals_inout.push_back(carrot::CarrotPaymentProposalV1{
.destination = dest,
.amount = tx_dest_entry.amount,
.asset_type = tx_dest_entry.asset_type,
.randomness = carrot::gen_janus_anchor(),
});
}
return is_selfsend_dest;
}
//-------------------------------------------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------------------------------------
static cryptonote::tx_destination_entry make_tx_destination_entry(
const carrot::CarrotPaymentProposalV1 &payment_proposal)
{
cryptonote::tx_destination_entry dest = cryptonote::tx_destination_entry(payment_proposal.amount,
{payment_proposal.destination.address_spend_pubkey, payment_proposal.destination.address_view_pubkey},
payment_proposal.destination.is_subaddress);
dest.is_integrated = payment_proposal.destination.payment_id != carrot::null_payment_id;
return dest;
}
//-------------------------------------------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------------------------------------
static cryptonote::tx_destination_entry make_tx_destination_entry(
const carrot::CarrotPaymentProposalVerifiableSelfSendV1 &payment_proposal,
const carrot::view_incoming_key_device &k_view_dev)
{
crypto::public_key address_view_pubkey;
CHECK_AND_ASSERT_THROW_MES(k_view_dev.view_key_scalar_mult_ed25519(
payment_proposal.proposal.destination_address_spend_pubkey,
address_view_pubkey),
"make_tx_destination_entry: view-key multiplication failed");
return cryptonote::tx_destination_entry(payment_proposal.proposal.amount,
{payment_proposal.proposal.destination_address_spend_pubkey, address_view_pubkey},
payment_proposal.subaddr_index.index.is_subaddress());
}
//-------------------------------------------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------------------------------------
static crypto::public_key find_change_address_spend_pubkey(
const std::unordered_map<crypto::public_key, cryptonote::subaddress_index> &subaddress_map,
const std::uint32_t subaddr_account)
{
const auto change_it = std::find_if(subaddress_map.cbegin(), subaddress_map.cend(),
[subaddr_account](const auto &p) { return p.second.major == subaddr_account && p.second.minor == 0; });
CHECK_AND_ASSERT_THROW_MES(change_it != subaddress_map.cend(),
"find_change_address_spend_pubkey: missing change address (index "
<< subaddr_account << ",0) in subaddress map");
const auto change_it_2 = std::find_if(std::next(change_it), subaddress_map.cend(),
[subaddr_account](const auto &p) { return p.second.major == subaddr_account && p.second.minor == 0; });
CHECK_AND_ASSERT_THROW_MES(change_it_2 == subaddress_map.cend(),
"find_change_address_spend_pubkey: provided CN subaddress map is malformed!!! At least two spend pubkeys map to "
"index " << subaddr_account << ",0 in the subaddress map!");
return change_it->first;
}
//-------------------------------------------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------------------------------------
static crypto::public_key find_change_address_spend_pubkey(
const std::unordered_map<crypto::public_key, carrot::subaddress_index_extended> &subaddress_map,
const std::uint32_t subaddr_account)
{
const auto change_it = std::find_if(subaddress_map.cbegin(), subaddress_map.cend(),
[subaddr_account](const auto &p) {
return p.second.index.major == subaddr_account &&
p.second.index.minor == 0 &&
p.second.derive_type == carrot::AddressDeriveType::Carrot &&
p.second.is_return_spend_key == false;
});
CHECK_AND_ASSERT_THROW_MES(change_it != subaddress_map.cend(),
"find_change_address_spend_pubkey: missing change address (index "
<< subaddr_account << ",0) in subaddress map");
const auto change_it_2 = std::find_if(std::next(change_it), subaddress_map.cend(),
[subaddr_account](const auto &p) {
return p.second.index.major == subaddr_account &&
p.second.index.minor == 0 &&
p.second.derive_type == carrot::AddressDeriveType::Carrot &&
p.second.is_return_spend_key == false;
});
CHECK_AND_ASSERT_THROW_MES(change_it_2 == subaddress_map.cend(),
"find_change_address_spend_pubkey: provided subaddress map is malformed!!! At least two spend pubkeys map to "
"index " << subaddr_account << ",0 in the subaddress map!");
return change_it->first;
}
//-------------------------------------------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------------------------------------
std::unordered_map<crypto::key_image, size_t> collect_non_burned_transfers_by_key_image(
const wallet2::transfer_container &transfers)
{
std::unordered_map<crypto::key_image, size_t> best_transfer_index_by_ki;
for (size_t i = 0; i < transfers.size(); ++i)
{
const wallet2::transfer_details &td = transfers.at(i);
if (!td.m_key_image_known || td.m_key_image_partial)
continue;
const auto it = best_transfer_index_by_ki.find(td.m_key_image);
if (it == best_transfer_index_by_ki.end())
{
best_transfer_index_by_ki.insert({td.m_key_image, i});
continue;
}
const wallet2::transfer_details &other_td = transfers.at(it->second);
if (td.amount() < other_td.amount())
continue;
else if (td.amount() > other_td.amount())
it->second = i;
else if (td.m_global_output_index > other_td.m_global_output_index)
continue;
else if (td.m_global_output_index < other_td.m_global_output_index)
it->second = i;
}
return best_transfer_index_by_ki;
}
//-------------------------------------------------------------------------------------------------------------------
carrot::select_inputs_func_t make_wallet2_single_transfer_input_selector(
const wallet2::transfer_container &transfers,
const std::uint32_t from_account,
const std::set<std::uint32_t> &from_subaddresses,
const rct::xmr_amount ignore_above,
const rct::xmr_amount ignore_below,
const std::uint64_t top_block_index,
const bool allow_carrot_external_inputs_in_normal_transfers,
const bool allow_pre_carrot_inputs_in_normal_transfers,
std::set<size_t> &selected_transfer_indices_out)
{
// Collect transfer_container into a `std::vector<carrot::InputCandidate>` for usable inputs
std::vector<carrot::InputCandidate> input_candidates;
std::vector<size_t> input_candidates_transfer_indices;
input_candidates.reserve(transfers.size());
input_candidates_transfer_indices.reserve(transfers.size());
for (size_t i = 0; i < transfers.size(); ++i)
{
const wallet2::transfer_details &td = transfers.at(i);
if (is_transfer_usable_for_input_selection(td,
from_account,
from_subaddresses,
ignore_above,
ignore_below,
top_block_index))
{
input_candidates.push_back(carrot::InputCandidate{
.core = carrot::CarrotSelectedInput{
.amount = td.amount(),
.key_image = td.m_key_image
},
.is_pre_carrot = !carrot::is_carrot_transaction_v1(td.m_tx),
.is_external = true, //! @TODO: derive this info from field in transfer_details
.block_index = td.m_block_height
});
input_candidates_transfer_indices.push_back(i);
}
}
// Create wrapper around `make_single_transfer_input_selector`
return [input_candidates = std::move(input_candidates),
input_candidates_transfer_indices = std::move(input_candidates_transfer_indices),
allow_carrot_external_inputs_in_normal_transfers,
allow_pre_carrot_inputs_in_normal_transfers,
&selected_transfer_indices_out
](
const boost::multiprecision::uint128_t &nominal_output_sum,
const std::map<std::size_t, rct::xmr_amount> &fee_by_input_count,
const std::size_t num_normal_payment_proposals,
const std::size_t num_selfsend_payment_proposals,
std::vector<carrot::CarrotSelectedInput> &selected_inputs_outs
){
const std::vector<carrot::input_selection_policy_t> policies{
&carrot::ispolicy::select_greedy_aging
};
std::uint32_t flags = 0;
if (allow_carrot_external_inputs_in_normal_transfers)
flags |= carrot::InputSelectionFlags::ALLOW_EXTERNAL_INPUTS_IN_NORMAL_TRANSFERS;
if (allow_pre_carrot_inputs_in_normal_transfers)
flags |= carrot::InputSelectionFlags::ALLOW_PRE_CARROT_INPUTS_IN_NORMAL_TRANSFERS;
// Make inner input selection functor
std::set<size_t> selected_input_indices;
const carrot::select_inputs_func_t inner = carrot::make_single_transfer_input_selector(
epee::to_span(input_candidates),
epee::to_span(policies),
flags,
&selected_input_indices);
// Call input selection
inner(nominal_output_sum,
fee_by_input_count,
num_normal_payment_proposals,
num_selfsend_payment_proposals,
selected_inputs_outs);
// Collect converted selected_input_indices -> selected_transfer_indices_out
selected_transfer_indices_out.clear();
for (const size_t input_index : selected_input_indices)
selected_transfer_indices_out.insert(input_candidates_transfer_indices.at(input_index));
};
}
//-------------------------------------------------------------------------------------------------------------------
std::vector<cryptonote::tx_source_entry> get_sources(
const wallet2::transfer_container &transfers,
const std::vector<std::size_t> &selected_transfers,
const std::string &source_asset,
wallet2 &w
) {
// get decoys
const size_t fake_outputs_count = 15;
std::vector<std::vector<tools::wallet2::get_outs_entry>> outs;
std::unordered_set<crypto::public_key> valid_public_keys_cache;
w.get_outs(outs, selected_transfers, fake_outputs_count, true, valid_public_keys_cache); // may throw
LOG_PRINT_L2("preparing outputs");
size_t i = 0, out_index = 0;
std::vector<cryptonote::tx_source_entry> sources;
for(size_t idx: selected_transfers)
{
sources.resize(sources.size()+1);
cryptonote::tx_source_entry& src = sources.back();
const wallet2::transfer_details& td = transfers[idx];
// Sanity check the asset_type for this TD is correct
THROW_WALLET_EXCEPTION_IF(td.asset_type != source_asset, error::wallet_internal_error, "Input has wrong asset_type - expected " + source_asset + " but found " + td.asset_type);
src.amount = td.amount();
src.rct = td.is_rct();
src.carrot = td.is_carrot();
src.coinbase = td.m_tx.vin[0].type() == typeid(cryptonote::txin_gen);
src.block_index = td.m_block_height;
src.asset_type = td.asset_type;
// Create the origin TX data
if (td.m_td_origin_idx != (uint64_t)-1) {
THROW_WALLET_EXCEPTION_IF(td.m_td_origin_idx >= w.get_num_transfer_details(), error::wallet_internal_error, "cannot locate return_payment origin index in m_transfers");
const wallet2::transfer_details& td_origin = w.get_transfer_details(td.m_td_origin_idx);
src.origin_tx_data.tx_type = td_origin.m_tx.type;
src.origin_tx_data.tx_pub_key = cryptonote::get_tx_pub_key_from_extra(td_origin.m_tx);
src.origin_tx_data.output_index = td_origin.m_internal_output_index;
}
//paste mixin transaction
THROW_WALLET_EXCEPTION_IF(outs.size() < out_index + 1 , error::wallet_internal_error, "outs.size() < out_index + 1");
THROW_WALLET_EXCEPTION_IF(outs[out_index].size() < fake_outputs_count , error::wallet_internal_error, "fake_outputs_count > random outputs found");
typedef cryptonote::tx_source_entry::output_entry tx_output_entry;
for (size_t n = 0; n < fake_outputs_count + 1; ++n)
{
tx_output_entry oe;
oe.first = std::get<0>(outs[out_index][n]);
oe.second.dest = rct::pk2rct(std::get<1>(outs[out_index][n]));
oe.second.mask = std::get<2>(outs[out_index][n]);
src.outputs.push_back(oe);
}
++i;
//paste real transaction to the random index
auto it_to_replace = std::find_if(src.outputs.begin(), src.outputs.end(), [&](const tx_output_entry& a)
{
// HERE BE DRAGONS!!!
// SRCG: ring tweak to indexed per asset_type - DO NOT COMMIT UNTIL IT IS ALL WORKING
//return a.first == td.m_global_output_index;
return a.first == td.m_asset_type_output_index;
// LAND AHOY!!!
});
THROW_WALLET_EXCEPTION_IF(it_to_replace == src.outputs.end(), error::wallet_internal_error,
"real output not found");
tx_output_entry real_oe;
// HERE BE DRAGONS!!!
// SRCG: ring tweak to indexed per asset_type - DO NOT COMMIT UNTIL IT IS ALL WORKING
//real_oe.first = td.m_global_output_index;
real_oe.first = td.m_asset_type_output_index;
// LAND AHOY!!!
real_oe.second.dest = rct::pk2rct(td.get_public_key());
real_oe.second.mask = rct::commit(td.amount(), td.m_mask);
*it_to_replace = real_oe;
src.real_out_tx_key = get_tx_pub_key_from_extra(td.m_tx, td.m_pk_index);
src.real_out_additional_tx_keys = get_additional_tx_pub_keys_from_extra(td.m_tx);
src.real_output = it_to_replace - src.outputs.begin();
src.real_output_in_tx_index = td.m_internal_output_index;
src.mask = td.m_mask;
src.address_spend_pubkey = td.m_recovered_spend_pubkey;
if (td.m_tx.vin[0].type() == typeid(cryptonote::txin_to_key)) {
src.first_rct_key_image = boost::get<cryptonote::txin_to_key>(td.m_tx.vin[0]).k_image;
}
if (false) // w.m_multisig // TODO:
// note: multisig_kLRki is a legacy struct, currently only used as a key image shuttle into the multisig tx builder
src.multisig_kLRki = {.k = {}, .L = {}, .R = {}, .ki = rct::ki2rct(td.m_key_image)};
else
src.multisig_kLRki = rct::multisig_kLRki({rct::zero(), rct::zero(), rct::zero(), rct::zero()});
detail::print_source_entry(src);
++out_index;
}
LOG_PRINT_L2("outputs prepared");
return sources;
}
//-------------------------------------------------------------------------------------------------------------------
std::vector<carrot::CarrotTransactionProposalV1> make_carrot_transaction_proposals_wallet2_transfer(
wallet2 &w,
std::vector<cryptonote::tx_destination_entry> dsts,
const rct::xmr_amount fee_per_weight,
const rct::xmr_amount fee_quantization_mask,
const std::vector<uint8_t> &extra,
const cryptonote::transaction_type tx_type,
const uint32_t subaddr_account,
const std::set<uint32_t> &subaddr_indices,
wallet2::unique_index_container subtract_fee_from_outputs,
const std::uint64_t top_block_index)
{
wallet2::transfer_container unused_transfers;
w.get_transfers(unused_transfers);
std::vector<carrot::CarrotTransactionProposalV1> tx_proposals;
tx_proposals.reserve(dsts.size() / (carrot::CARROT_MAX_TX_OUTPUTS - 1) + 1);
const crypto::public_key change_address_spend_pubkey
= find_change_address_spend_pubkey(w.get_account().get_subaddress_map_ref(), subaddr_account);
while (!dsts.empty())
{
const std::size_t num_dsts_to_complete = std::min<std::size_t>(dsts.size(), carrot::CARROT_MAX_TX_OUTPUTS - 1);
// build payment proposals and subtractable info from last `num_dsts_to_complete` dsts
std::vector<carrot::CarrotPaymentProposalV1> normal_payment_proposals;
std::vector<carrot::CarrotPaymentProposalVerifiableSelfSendV1> selfsend_payment_proposals;
std::set<std::size_t> subtractable_normal_payment_proposals;
std::set<std::size_t> subtractable_selfsend_payment_proposals;
for (size_t i = 0; i < num_dsts_to_complete && !dsts.empty(); ++i)
{
const cryptonote::tx_destination_entry &dst = dsts.back();
const bool is_selfsend = build_payment_proposals(normal_payment_proposals,
selfsend_payment_proposals,
dst,
w.get_account().get_subaddress_map_cn());
if (subtract_fee_from_outputs.count(dsts.size() - 1))
{
if (is_selfsend)
subtractable_selfsend_payment_proposals.insert(selfsend_payment_proposals.size() - 1);
else
subtractable_normal_payment_proposals.insert(normal_payment_proposals.size() - 1);
}
dsts.pop_back();
}
// make input selector
std::set<size_t> selected_transfer_indices;
carrot::select_inputs_func_t select_inputs = make_wallet2_single_transfer_input_selector(
unused_transfers,
subaddr_account,
subaddr_indices,
w.ignore_outputs_above(),
w.ignore_outputs_below(),
top_block_index,
/*allow_carrot_external_inputs_in_normal_transfers=*/true,
/*allow_pre_carrot_inputs_in_normal_transfers=*/true,
selected_transfer_indices);
// make proposal
carrot::CarrotTransactionProposalV1 tx_proposal;
carrot::make_carrot_transaction_proposal_v1_transfer(
normal_payment_proposals,
selfsend_payment_proposals,
fee_per_weight,
fee_quantization_mask,
extra,
tx_type,
std::move(select_inputs),
change_address_spend_pubkey,
{{subaddr_account, 0}, carrot::AddressDeriveType::Carrot, false},
subtractable_normal_payment_proposals,
subtractable_selfsend_payment_proposals,
tx_proposal);
// populate the sources
std::vector<size_t> selected_transfer_indices_sorted;
for (const auto &ki: tx_proposal.key_images_sorted) {
selected_transfer_indices_sorted.push_back(w.get_transfer_details(ki));
}
tx_proposal.sources = get_sources(unused_transfers, selected_transfer_indices_sorted, "SAL1", w);
// update `unused_transfers` for next proposal by removing selected transfers
tools::for_all_in_vector_erase_no_preserve_order_if(unused_transfers,
[&tx_proposal](const wallet2::transfer_details &td) -> bool {
const auto &used_kis = tx_proposal.key_images_sorted;
const auto ki_it = std::find(used_kis.cbegin(), used_kis.cend(), td.m_key_image);
return ki_it != used_kis.cend();
}
);
tx_proposals.push_back(std::move(tx_proposal));
}
return tx_proposals;
}
//-------------------------------------------------------------------------------------------------------------------
std::vector<carrot::CarrotTransactionProposalV1> make_carrot_transaction_proposals_wallet2_transfer(
wallet2 &w,
const std::vector<cryptonote::tx_destination_entry> &dsts,
const std::uint32_t priority,
const std::vector<uint8_t> &extra,
const cryptonote::transaction_type tx_type,
const std::uint32_t subaddr_account,
const std::set<uint32_t> &subaddr_indices,
const wallet2::unique_index_container &subtract_fee_from_outputs)
{
wallet2::transfer_container transfers;
w.get_transfers(transfers);
const bool use_per_byte_fee = w.use_fork_rules(HF_VERSION_PER_BYTE_FEE, 0);
CHECK_AND_ASSERT_THROW_MES(use_per_byte_fee,
"make_carrot_transaction_proposals_wallet2_transfer: not using per-byte base fee");
const rct::xmr_amount fee_per_weight = w.get_base_fee(priority);
MDEBUG("fee_per_weight = " << fee_per_weight << ", from priority = " << priority);
const rct::xmr_amount fee_quantization_mask = w.get_fee_quantization_mask();
MDEBUG("fee_quantization_mask = " << fee_quantization_mask << ", from priority = " << priority);
const std::uint64_t current_chain_height = w.get_blockchain_current_height();
CHECK_AND_ASSERT_THROW_MES(current_chain_height > 0,
"make_carrot_transaction_proposals_wallet2_transfer: chain height is 0, there is no top block");
const std::uint64_t top_block_index = current_chain_height - 1;
return make_carrot_transaction_proposals_wallet2_transfer(
w,
dsts,
fee_per_weight,
fee_quantization_mask,
extra,
tx_type,
subaddr_account,
subaddr_indices,
subtract_fee_from_outputs,
top_block_index);
}
//-------------------------------------------------------------------------------------------------------------------
std::vector<carrot::CarrotTransactionProposalV1> make_carrot_transaction_proposals_wallet2_sweep(
wallet2 &w,
const std::vector<crypto::key_image> &input_key_images,
const cryptonote::account_public_address &address,
const bool is_subaddress,
const size_t n_dests_per_tx,
const rct::xmr_amount fee_per_weight,
const rct::xmr_amount fee_quantization_mask,
const std::vector<uint8_t> &extra,
const cryptonote::transaction_type tx_type,
const std::uint64_t top_block_index)
{
wallet2::transfer_container transfers;
w.get_transfers(transfers);
const size_t n_inputs = input_key_images.size();
CARROT_CHECK_AND_THROW(n_inputs, carrot::too_few_inputs, "no key images provided");
CARROT_CHECK_AND_THROW(n_dests_per_tx, carrot::too_few_outputs, "sweep must have at least one destination");
CARROT_CHECK_AND_THROW(n_dests_per_tx <= carrot::CARROT_MAX_TX_OUTPUTS,
carrot::too_many_outputs, "too many sweep destinations per transaction");
// Check that the key image is usable and isn't spent, collect amounts, and get subaddress account index
std::vector<rct::xmr_amount> input_amounts;
input_amounts.reserve(input_key_images.size());
std::uint32_t subaddr_account = std::numeric_limits<std::uint32_t>::max();
const auto best_transfers_by_ki = collect_non_burned_transfers_by_key_image(transfers);
for (const crypto::key_image &ki : input_key_images)
{
const auto ki_it = best_transfers_by_ki.find(ki);
CHECK_AND_ASSERT_THROW_MES(ki_it != best_transfers_by_ki.cend(),
__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,
/*from_subaddresses=*/{},
/*ignore_above=*/MONEY_SUPPLY,
/*ignore_below=*/0,
top_block_index),
__func__ << ": transfer not usable as an input");
input_amounts.push_back(td.amount());
subaddr_account = std::min(subaddr_account, td.m_subaddr_index.major);
}
const crypto::public_key change_address_spend_pubkey
= find_change_address_spend_pubkey(w.get_account().get_subaddress_map_ref(), subaddr_account);
// get 1 payment proposal corresponding to (address, is_subaddres)
std::vector<carrot::CarrotPaymentProposalV1> normal_payment_proposals;
std::vector<carrot::CarrotPaymentProposalVerifiableSelfSendV1> selfsend_payment_proposals;
for (size_t i = 0; i < n_dests_per_tx; ++i)
{
cryptonote::tx_destination_entry de;
de.amount = 0;
de.addr = address;
de.is_subaddress = is_subaddress;
de.asset_type = "SAL1";
const bool is_selfsend_dest = build_payment_proposals(normal_payment_proposals,
selfsend_payment_proposals,
de,
w.get_account().get_subaddress_map_cn());
CHECK_AND_ASSERT_THROW_MES((is_selfsend_dest && selfsend_payment_proposals.size() == i+1)
|| (!is_selfsend_dest && normal_payment_proposals.size() == i+1),
__func__ << ": BUG in build_payment_proposals: incorrect count for payment proposal lists");
}
CARROT_CHECK_AND_THROW(normal_payment_proposals.size() < carrot::CARROT_MAX_TX_OUTPUTS,
carrot::too_many_outputs, "too many *outgoing* sweep destinations per tx, we also need 1 self-send output");
// make `n_txs` tx proposals with `n_output` payment proposals each
const size_t n_txs = div_ceil<size_t>(n_inputs, carrot::CARROT_MAX_TX_INPUTS);
std::vector<carrot::CarrotTransactionProposalV1> tx_proposals(n_txs);
size_t ki_idx = 0;
for (carrot::CarrotTransactionProposalV1 &tx_proposal : tx_proposals)
{
// if a 2-selfsend, 2-out tx, flip one of the enote types to get unique derivations
if (selfsend_payment_proposals.size() == 2)
selfsend_payment_proposals.back().proposal.enote_type = carrot::CarrotEnoteType::CHANGE;
// collect inputs for this tx
const size_t ki_idx_end = std::min<size_t>(n_inputs, ki_idx + carrot::CARROT_MAX_TX_INPUTS);
std::vector<carrot::CarrotSelectedInput> selected_inputs;
selected_inputs.reserve(n_inputs - ki_idx_end);
for (; ki_idx < ki_idx_end; ++ki_idx)
selected_inputs.push_back({input_amounts.at(ki_idx), input_key_images.at(ki_idx)});
carrot::make_carrot_transaction_proposal_v1_sweep(normal_payment_proposals,
selfsend_payment_proposals,
fee_per_weight,
fee_quantization_mask,
extra,
tx_type,
std::move(selected_inputs),
change_address_spend_pubkey,
{{subaddr_account, 0}, carrot::AddressDeriveType::PreCarrot}, //! @TODO: handle Carrot keys
tx_proposal);
// populate the sources
std::vector<size_t> selected_transfer_indices_sorted;
for (const auto &ki: tx_proposal.key_images_sorted) {
selected_transfer_indices_sorted.push_back(w.get_transfer_details(ki));
}
tx_proposal.sources = get_sources(transfers, selected_transfer_indices_sorted, "SAL1", w);
}
CARROT_CHECK_AND_THROW(ki_idx == input_key_images.size(),
carrot::carrot_logic_error, "BUG: sweep_all did not consume the correct num of key images while iterating");
return tx_proposals;
}
//-------------------------------------------------------------------------------------------------------------------
std::vector<carrot::CarrotTransactionProposalV1> make_carrot_transaction_proposals_wallet2_sweep(
wallet2 &w,
const std::vector<crypto::key_image> &input_key_images,
const cryptonote::account_public_address &address,
const bool is_subaddress,
const size_t n_dests_per_tx,
const std::uint32_t priority,
const std::vector<uint8_t> &extra,
const cryptonote::transaction_type tx_type)
{
const rct::xmr_amount fee_per_weight = w.get_base_fee(priority);
const rct::xmr_amount fee_quantization_mask = w.get_fee_quantization_mask();
const std::uint64_t current_chain_height = w.get_blockchain_current_height();
CHECK_AND_ASSERT_THROW_MES(current_chain_height > 0,
"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_proposals_wallet2_sweep(
w,
input_key_images,
address,
is_subaddress,
n_dests_per_tx,
fee_per_weight,
fee_quantization_mask,
extra,
tx_type,
top_block_index);
}
//-------------------------------------------------------------------------------------------------------------------
std::vector<carrot::CarrotTransactionProposalV1> make_carrot_transaction_proposals_wallet2_sweep_all(
wallet2 &w,
const rct::xmr_amount only_below,
const cryptonote::account_public_address &address,
const bool is_subaddress,
const size_t n_dests_per_tx,
const rct::xmr_amount fee_per_weight,
const rct::xmr_amount fee_quantization_mask,
const std::vector<uint8_t> &extra,
const cryptonote::transaction_type tx_type,
const std::uint32_t subaddr_account,
const std::set<uint32_t> &subaddr_indices,
const std::uint64_t top_block_index)
{
wallet2::transfer_container transfers;
w.get_transfers(transfers);
const std::unordered_map<crypto::key_image, size_t> unburned_transfers_by_key_image =
collect_non_burned_transfers_by_key_image(transfers);
std::vector<crypto::key_image> input_key_images;
input_key_images.reserve(transfers.size());
for (std::size_t transfer_idx = 0; transfer_idx < transfers.size(); ++transfer_idx)
{
const wallet2::transfer_details &td = transfers.at(transfer_idx);
if (!is_transfer_usable_for_input_selection(td,
subaddr_account,
subaddr_indices,
only_below ? only_below : MONEY_SUPPLY,
0,
top_block_index))
continue;
const auto ki_it = unburned_transfers_by_key_image.find(td.m_key_image);
if (ki_it == unburned_transfers_by_key_image.cend())
continue;
else if (ki_it->second != transfer_idx)
continue;
input_key_images.push_back(td.m_key_image);
}
CHECK_AND_ASSERT_THROW_MES(!input_key_images.empty(), __func__ << ": no usable transfers to sweep");
return make_carrot_transaction_proposals_wallet2_sweep(
w,
input_key_images,
address,
is_subaddress,
n_dests_per_tx,
fee_per_weight,
fee_quantization_mask,
extra,
tx_type,
top_block_index);
}
//-------------------------------------------------------------------------------------------------------------------
std::vector<carrot::CarrotTransactionProposalV1> make_carrot_transaction_proposals_wallet2_sweep_all(
wallet2 &w,
const rct::xmr_amount only_below,
const cryptonote::account_public_address &address,
const bool is_subaddress,
const size_t n_dests_per_tx,
const std::uint32_t priority,
const std::vector<uint8_t> &extra,
const cryptonote::transaction_type tx_type,
const std::uint32_t subaddr_account,
const std::set<uint32_t> &subaddr_indices)
{
const rct::xmr_amount fee_per_weight = w.get_base_fee(priority);
const rct::xmr_amount fee_quantization_mask = w.get_fee_quantization_mask();
const std::uint64_t current_chain_height = w.get_blockchain_current_height();
CHECK_AND_ASSERT_THROW_MES(current_chain_height > 0,
"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_proposals_wallet2_sweep_all(
w,
only_below,
address,
is_subaddress,
n_dests_per_tx,
fee_per_weight,
fee_quantization_mask,
extra,
tx_type,
subaddr_account,
subaddr_indices,
top_block_index);
}
//-------------------------------------------------------------------------------------------------------------------
bool get_address_openings_x_y(
const cryptonote::transaction &tx,
const cryptonote::tx_source_entry &src,
const wallet2 &w,
crypto::secret_key &x_out,
crypto::secret_key &y_out)
{
const std::vector<crypto::public_key> v_pubkeys{src.real_out_tx_key};
const std::vector<crypto::public_key> v_pubkeys_empty{};
const epee::span<const crypto::public_key> main_tx_ephemeral_pubkeys = (src.real_out_tx_key == crypto::null_pkey) ? epee::to_span(v_pubkeys_empty) : epee::to_span(v_pubkeys);
const epee::span<const crypto::public_key> additional_tx_ephemeral_pubkeys = epee::to_span(src.real_out_additional_tx_keys);
// 2. perform ECDH derivations
std::vector<crypto::key_derivation> main_derivations;
std::vector<crypto::key_derivation> additional_derivations;
bool is_carrot = carrot::is_carrot_transaction_v1(tx);
wallet::perform_ecdh_derivations(
main_tx_ephemeral_pubkeys,
additional_tx_ephemeral_pubkeys,
is_carrot ? w.get_account().get_keys().k_view_incoming : w.get_account().get_keys().m_view_secret_key,
w.get_account().get_keys().get_device(),
is_carrot,
main_derivations,
additional_derivations
);
crypto::hash s_sender_receiver;
const crypto::key_derivation &kd = main_derivations.size()
? main_derivations[0]
: additional_derivations[src.real_output_in_tx_index];
const mx25519_pubkey s_sender_receiver_unctx = carrot::raw_byte_convert<mx25519_pubkey>(kd);
// ephemeral pubkeys
const epee::span<const crypto::public_key> enote_ephemeral_pubkeys_pk =
main_tx_ephemeral_pubkeys.empty() ? additional_tx_ephemeral_pubkeys : main_tx_ephemeral_pubkeys;
const epee::span<const mx25519_pubkey> enote_ephemeral_pubkeys = {
reinterpret_cast<const mx25519_pubkey*>(enote_ephemeral_pubkeys_pk.data()),
enote_ephemeral_pubkeys_pk.size()
};
const bool shared_ephemeral_pubkey = enote_ephemeral_pubkeys.size() == 1;
const size_t ephemeral_pubkey_index = shared_ephemeral_pubkey ? 0 : src.real_output_in_tx_index;
// input_context
carrot::input_context_t input_context;
if (src.coinbase) {
input_context = carrot::make_carrot_input_context_coinbase(src.block_index);
} else {
input_context = carrot::make_carrot_input_context(src.first_rct_key_image);
}
// s^ctx_sr = H_32(s_sr, D_e, input_context)
make_carrot_sender_receiver_secret(s_sender_receiver_unctx.data,
enote_ephemeral_pubkeys[ephemeral_pubkey_index],
input_context,
s_sender_receiver);
// get the k_og and k_ot
crypto::secret_key sender_extension_g_out;
crypto::secret_key sender_extension_t_out;
crypto::public_key address_spend_pubkey_out;
carrot::payment_id_t nominal_payment_id_out;
carrot::janus_anchor_t nominal_janus_anchor_out;
carrot::encrypted_janus_anchor_t encrypted_janus_anchor;
carrot::encrypted_payment_id_t encrypted_payment_id;
carrot::scan_carrot_dest_info(
rct::rct2pk(src.outputs[src.real_output].second.dest),
src.outputs[src.real_output].second.mask,
encrypted_janus_anchor,
encrypted_payment_id,
s_sender_receiver,
sender_extension_g_out,
sender_extension_t_out,
address_spend_pubkey_out,
nominal_payment_id_out,
nominal_janus_anchor_out
);
bool r = w.get_account().try_searching_for_opening_for_onetime_address(
address_spend_pubkey_out,
sender_extension_g_out,
sender_extension_t_out,
x_out,
y_out
);
CHECK_AND_ASSERT_THROW_MES(r, "Failed to obtain openings for onetime address");
return true;
}
//-------------------------------------------------------------------------------------------------------------------
void encrypt_change_index(
const std::vector<carrot::CarrotPaymentProposalV1> &proposals,
const std::vector<carrot::CarrotPaymentProposalSelfSendV1> &selfsend_proposal_cores,
const crypto::key_image &tx_first_key_image,
const size_t change_index,
const std::unordered_map<crypto::public_key, size_t> &payments_indices,
std::vector<uint8_t> &change_masks_out
) {
// 1. input context: input_context = "R" || KI_1
const carrot::input_context_t input_context = carrot::make_carrot_input_context(tx_first_key_image);
// 2. collect proposals and selfsend proposals destinations
std::vector<std::tuple<crypto::public_key, size_t, bool>> destinations;
for (const auto &p : proposals) {
destinations.emplace_back(p.destination.address_spend_pubkey, payments_indices.at(p.destination.address_spend_pubkey), true);
}
for (const auto &p : selfsend_proposal_cores) {
destinations.emplace_back(p.destination_address_spend_pubkey, payments_indices.at(p.destination_address_spend_pubkey), false);
}
// 3. sort by indices
std::sort(destinations.begin(), destinations.end(),
[](const auto &a, const auto &b) {
return std::get<1>(a) < std::get<1>(b);
}
);
// 4. calculate change masks
for (const auto &d: destinations) {
// get shared secret
mx25519_pubkey eph_pubkey;
mx25519_pubkey s_sender_receiver_unctx;
if (std::get<2>(d)) {
// normal payment proposal
const auto it = std::find_if(proposals.begin(), proposals.end(),
[&d](const carrot::CarrotPaymentProposalV1 &p) {
return p.destination.address_spend_pubkey == std::get<0>(d);
});
CHECK_AND_ASSERT_THROW_MES(it != proposals.end(), "Failed to find normal payment proposal");
carrot::get_normal_proposal_ecdh_parts(
*it,
input_context,
eph_pubkey,
s_sender_receiver_unctx
);
} else {
s_sender_receiver_unctx = crypto::rand<mx25519_pubkey>();
}
// derive a scalar from the shared secret
crypto::secret_key output_index_key;
crypto::key_derivation output_index_derivation;
memcpy(output_index_derivation.data, s_sender_receiver_unctx.data, sizeof(output_index_derivation.data));
crypto::derivation_to_scalar(
output_index_derivation,
std::get<1>(d),
output_index_key
);
// Calculate the encrypted_change_index data for this output
struct {
char domain_separator[8];
crypto::secret_key output_index_key;
} eci_buf;
std::memset(eci_buf.domain_separator, 0x0, sizeof(eci_buf.domain_separator));
std::strncpy(eci_buf.domain_separator, "CHG_IDX", 8);
eci_buf.output_index_key = output_index_key;
crypto::secret_key eci_out;
keccak((uint8_t *)&eci_buf, sizeof(eci_buf), (uint8_t*)&eci_out, sizeof(eci_out));
uint8_t eci_data = change_index ^ eci_out.data[0];
change_masks_out.push_back(eci_data);
}
}
//-------------------------------------------------------------------------------------------------------------------
cryptonote::transaction finalize_all_proofs_from_transfer_details(
const carrot::CarrotTransactionProposalV1 &tx_proposal,
const wallet2 &w)
{
const size_t n_inputs = tx_proposal.key_images_sorted.size();
const size_t n_outputs = tx_proposal.normal_payment_proposals.size()
+ tx_proposal.selfsend_payment_proposals.size();
CHECK_AND_ASSERT_THROW_MES(n_inputs, "finalize_all_proofs_from_transfer_details: no inputs");
LOG_PRINT_L2("finalize_all_proofs_from_transfer_details: make all proofs for transaction proposal: "
<< n_inputs << "-in " << n_outputs << "-out, with "
<< tx_proposal.normal_payment_proposals.size() << " normal payment proposals, "
<< tx_proposal.selfsend_payment_proposals.size() << " self-send payment proposals, and a fee of "
<< cryptonote::print_money(tx_proposal.fee) << " SAL1");
wallet2::transfer_container transfers;
w.get_transfers(transfers);
cryptonote::account_keys acc_keys = w.get_account().get_keys();
// collect core selfsend proposals
std::vector<carrot::CarrotPaymentProposalSelfSendV1> selfsend_payment_proposal_cores;
selfsend_payment_proposal_cores.reserve(tx_proposal.selfsend_payment_proposals.size());
for (const auto &selfsend_payment_proposal : tx_proposal.selfsend_payment_proposals)
selfsend_payment_proposal_cores.push_back(selfsend_payment_proposal.proposal);
//! @TODO: HW device
carrot::cryptonote_hierarchy_address_device_ram_borrowed addr_dev(
acc_keys.m_carrot_account_address.m_spend_public_key,
acc_keys.k_view_incoming);
// finalize enotes
LOG_PRINT_L3("Getting output enote proposals");
std::vector<carrot::RCTOutputEnoteProposal> output_enote_proposals;
carrot::encrypted_payment_id_t encrypted_payment_id;
size_t change_index;
std::unordered_map<crypto::public_key, size_t> payments_indices;
carrot::get_output_enote_proposals(tx_proposal.normal_payment_proposals,
selfsend_payment_proposal_cores,
tx_proposal.dummy_encrypted_payment_id,
nullptr,
&addr_dev,
tx_proposal.key_images_sorted.at(0),
output_enote_proposals,
encrypted_payment_id,
tx_proposal.tx_type,
change_index,
payments_indices,
nullptr);
CHECK_AND_ASSERT_THROW_MES(output_enote_proposals.size() == n_outputs,
"finalize_all_proofs_from_transfer_details: unexpected number of output enote proposals");
// collect all non-burned inputs owned by wallet
const std::unordered_map<crypto::key_image, size_t> unburned_transfers_by_key_image =
collect_non_burned_transfers_by_key_image(transfers);
LOG_PRINT_L3("Did a burning bug pass, eliminated "
<< (transfers.size() - unburned_transfers_by_key_image.size())
<< " eligible transfers");
// collect output amount blinding factors
std::vector<rct::key> output_amount_blinding_factors;
output_amount_blinding_factors.reserve(output_enote_proposals.size());
for (const carrot::RCTOutputEnoteProposal &output_enote_proposal : output_enote_proposals)
output_amount_blinding_factors.push_back(rct::sk2rct(output_enote_proposal.amount_blinding_factor));
// collect enotes
std::vector<carrot::CarrotEnoteV1> enotes(output_enote_proposals.size());
for (size_t i = 0; i < enotes.size(); ++i)
enotes[i] = output_enote_proposals.at(i).enote;
// encrypt change index per output
std::vector<uint8_t> change_masks;
encrypt_change_index(
tx_proposal.normal_payment_proposals,
selfsend_payment_proposal_cores,
tx_proposal.key_images_sorted.at(0),
change_index,
payments_indices,
change_masks);
// serialize transaction
cryptonote::transaction tx = carrot::store_carrot_to_transaction_v1(enotes,
tx_proposal.key_images_sorted,
tx_proposal.sources,
tx_proposal.fee,
tx_proposal.tx_type,
change_masks,
encrypted_payment_id);
// aliases
hw::device &hwdev = acc_keys.get_device();
const auto &sources = tx_proposal.sources;
// inputs
uint64_t amount_in = 0;
rct::carrot_ctkeyV inSk;
inSk.reserve(sources.size());
std::vector<uint64_t> inamounts;
std::vector<unsigned int> index;
for (const auto& src: sources)
{
amount_in += src.amount;
inamounts.push_back(src.amount);
index.push_back(src.real_output);
// inSk: (x, y, mask)
rct::carrot_ctkey ctkey;
ctkey.mask = src.mask;
if (src.carrot) {
crypto::secret_key x, y;
THROW_WALLET_EXCEPTION_IF(!get_address_openings_x_y(tx, src, w, x, y),
error::wallet_internal_error, "Failed to get x and y for input");
ctkey.x = rct::sk2rct(x);
ctkey.y = rct::sk2rct(y);
} else {
// generate the secret key
cryptonote::keypair in_ephemeral;
crypto::key_image img;
rct::salvium_input_data_t sid;
const auto& out_key = reinterpret_cast<const crypto::public_key&>(src.outputs[src.real_output].second.dest);
bool use_origin_data = (src.origin_tx_data.tx_type != cryptonote::transaction_type::UNSET);
sid.origin_tx_type = src.origin_tx_data.tx_type;
bool r = cryptonote::generate_key_image_helper(
w.get_account().get_keys(),
w.get_account().get_subaddress_map_cn(),
out_key,
src.real_out_tx_key,
src.real_out_additional_tx_keys,
src.real_output_in_tx_index,
in_ephemeral,
img,
hwdev,
use_origin_data,
src.origin_tx_data, sid
);
THROW_WALLET_EXCEPTION_IF(!r, error::wallet_internal_error, "Failed to generate key image helper");
ctkey.x = rct::sk2rct(in_ephemeral.sec);
ctkey.y = rct::zero(); // not used in non-carrot txes
}
inSk.push_back(ctkey);
memwipe(&ctkey, sizeof(rct::carrot_ctkey));
// inPk: (public key, commitment)
// will be done when filling in mixRing
}
// outputs
uint64_t amount_out = 0;
std::vector<uint64_t> outamounts;
rct::keyV destinations;
std::vector<std::string> destination_asset_types;
rct::ctkeyV outSk;
for (const auto &oep : output_enote_proposals)
{
destinations.push_back(rct::pk2rct(oep.enote.onetime_address));
destination_asset_types.push_back(oep.enote.asset_type);
outamounts.push_back(oep.amount);
amount_out += oep.amount;
rct::ctkey key;
key.mask = rct::sk2rct(oep.amount_blinding_factor);
outSk.push_back(key);
}
// change output x, y
crypto::public_key change_address_spend_pubkey;
for (const auto &p :selfsend_payment_proposal_cores) {
if (p.enote_type == carrot::CarrotEnoteType::CHANGE) {
change_address_spend_pubkey = p.destination_address_spend_pubkey;
}
}
const carrot::RCTOutputEnoteProposal &change_enote_proposal = output_enote_proposals.at(change_index);
const carrot::input_context_t input_context = carrot::make_carrot_input_context(tx_proposal.key_images_sorted.at(0));
crypto::hash s_sender_receiver;
w.get_account().s_view_balance_dev.make_internal_sender_receiver_secret(
change_enote_proposal.enote.enote_ephemeral_pubkey,
input_context,
s_sender_receiver);
crypto::secret_key sender_extension_g;
carrot::make_carrot_onetime_address_extension_g(s_sender_receiver, change_enote_proposal.enote.amount_commitment, sender_extension_g);
crypto::secret_key sender_extension_t;
carrot::make_carrot_onetime_address_extension_t(s_sender_receiver, change_enote_proposal.enote.amount_commitment, sender_extension_t);
crypto::secret_key change_x, change_y;
bool r = w.get_account().try_searching_for_opening_for_onetime_address(
change_address_spend_pubkey,
sender_extension_g,
sender_extension_t,
change_x,
change_y
);
THROW_WALLET_EXCEPTION_IF(!r, error::wallet_internal_error,
"Failed to obtain opening for onetime change address");
// mixRing indexing is done the other way round for simple
rct::ctkeyM mixRing(sources.size());
for (size_t i = 0; i < sources.size(); ++i)
{
mixRing[i].resize(sources[i].outputs.size());
for (size_t n = 0; n < sources[i].outputs.size(); ++n)
{
mixRing[i][n] = sources[i].outputs[n].second;
}
}
// bpp
tx.rct_signatures.p.bulletproofs_plus.push_back(
rct::bulletproof_plus_PROVE(outamounts, output_amount_blinding_factors)
);
// store proofs
crypto::hash tx_prefix_hash;
get_transaction_prefix_hash(tx, tx_prefix_hash, hwdev);
rct::salvium_data_t salvium_data;
salvium_data.salvium_data_type = rct::SalviumOne;
rct::genRctSimpleCarrot(
rct::hash2rct(tx_prefix_hash),
inSk,
destinations,
tx_proposal.tx_type,
"SAL1",
destination_asset_types,
inamounts,
outamounts,
tx_proposal.fee,
mixRing,
index,
outSk,
rct::RCTConfig {
rct::RangeProofType::RangeProofPaddedBulletproof,
6,
},
hwdev,
salvium_data,
rct::sk2rct(change_x),
rct::sk2rct(change_y),
change_index,
tx.rct_signatures
);
tx.pruned = false;
return tx;
}
//-------------------------------------------------------------------------------------------------------------------
wallet2::pending_tx make_pending_carrot_tx(const carrot::CarrotTransactionProposalV1 &tx_proposal,
const wallet2::transfer_container &transfers,
const crypto::secret_key &k_view,
hw::device &hwdev)
{
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;
CARROT_CHECK_AND_THROW(n_inputs >= 1, carrot::too_few_inputs, "carrot tx proposal missing inputs");
CARROT_CHECK_AND_THROW(n_outputs >= 2, carrot::too_few_outputs, "carrot tx proposal missing outputs");
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(k_view);
// 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;
}
//-------------------------------------------------------------------------------------------------------------------
wallet2::pending_tx finalize_all_proofs_from_transfer_details_as_pending_tx(
const carrot::CarrotTransactionProposalV1 &tx_proposal,
const wallet2::transfer_container &transfers,
const wallet2 &w)
{
const auto acc_keys = w.get_account().get_keys();
wallet2::pending_tx ptx = make_pending_carrot_tx(tx_proposal,
transfers,
acc_keys.m_view_secret_key,
acc_keys.get_device());
ptx.tx = finalize_all_proofs_from_transfer_details(
tx_proposal,
w
);
return ptx;
}
//-------------------------------------------------------------------------------------------------------------------
wallet2::pending_tx finalize_all_proofs_from_transfer_details_as_pending_tx(
const carrot::CarrotTransactionProposalV1 &tx_proposal,
const wallet2 &w)
{
wallet2::transfer_container transfers;
w.get_transfers(transfers);
return finalize_all_proofs_from_transfer_details_as_pending_tx(
tx_proposal,
transfers,
w);
}
//-------------------------------------------------------------------------------------------------------------------
} //namespace wallet
} //namespace tools