// Copyright (c) 2024, 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_proposal_utils.h" //local headers #include "carrot_core/exceptions.h" #include "carrot_core/output_set_finalization.h" #include "cryptonote_basic/cryptonote_format_utils.h" #include "misc_log_ex.h" //third party headers //standard headers #undef MONERO_DEFAULT_LOG_CATEGORY #define MONERO_DEFAULT_LOG_CATEGORY "carrot_impl.tpu" namespace carrot { //------------------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------------------- static void append_additional_payment_proposal_if_necessary( std::vector& normal_payment_proposals_inout, std::vector &selfsend_payment_proposals_inout, const crypto::public_key &change_address_spend_pubkey, const subaddress_index_extended &change_subaddr_index) { struct append_additional_payment_proposal_if_necessary_visitor { void operator()(boost::blank) const {} void operator()(const CarrotPaymentProposalV1 &p) const { normal_proposals_inout.push_back(p); } void operator()(const CarrotPaymentProposalSelfSendV1 &p) const { selfsend_proposals_inout.push_back(CarrotPaymentProposalVerifiableSelfSendV1{ .proposal = p, .subaddr_index = change_subaddr_index }); } std::vector& normal_proposals_inout; std::vector &selfsend_proposals_inout; const subaddress_index_extended &change_subaddr_index; }; bool have_payment_type_selfsend = false; for (const CarrotPaymentProposalVerifiableSelfSendV1 &selfsend_payment_proposal : selfsend_payment_proposals_inout) if (selfsend_payment_proposal.proposal.enote_type == CarrotEnoteType::PAYMENT) have_payment_type_selfsend = true; const auto additional_output_proposal = get_additional_output_proposal(normal_payment_proposals_inout.size(), selfsend_payment_proposals_inout.size(), /*needed_change_amount=*/0, have_payment_type_selfsend, change_address_spend_pubkey); additional_output_proposal.visit(append_additional_payment_proposal_if_necessary_visitor{ normal_payment_proposals_inout, selfsend_payment_proposals_inout, change_subaddr_index }); } //------------------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------------------- std::uint64_t get_carrot_default_tx_extra_size(const std::size_t n_outputs) { CHECK_AND_ASSERT_THROW_MES(n_outputs <= CARROT_MAX_TX_OUTPUTS, "get_carrot_default_tx_extra_size: n_outputs too high: " << n_outputs); CHECK_AND_ASSERT_THROW_MES(n_outputs >= CARROT_MIN_TX_OUTPUTS, "get_carrot_default_tx_extra_size: n_outputs too low: " << n_outputs); static constexpr std::uint64_t enc_pid_extra_field_size = 8 /*pid*/ + 1 /*TX_EXTRA_NONCE_ENCRYPTED_PAYMENT_ID*/ + 1 /*nonce.size()*/ + 1 /*tx_extra_field variant tag*/; static constexpr std::uint64_t x25519_pubkey_size = 32; const std::size_t n_ephemeral = (n_outputs == 2) ? 1 : n_outputs; const bool use_additional = n_ephemeral > 1; const std::uint64_t ephemeral_pubkeys_field_size = 1 /*tx_extra_field variant tag*/ + (use_additional ? 1 : 0) /*vector size if applicable*/ + (n_ephemeral * x25519_pubkey_size) /*actual pubkeys*/; return enc_pid_extra_field_size + ephemeral_pubkeys_field_size; } //------------------------------------------------------------------------------------------------------------------- void make_carrot_transaction_proposal_v1(const std::vector &normal_payment_proposals_in, const std::vector &selfsend_payment_proposals_in, const rct::xmr_amount fee_per_weight, const rct::xmr_amount fee_quantization_mask, const std::vector &extra, const cryptonote::transaction_type tx_type, select_inputs_func_t &&select_inputs, carve_fees_and_balance_func_t &&carve_fees_and_balance, const crypto::public_key &change_address_spend_pubkey, const subaddress_index_extended &change_address_index, CarrotTransactionProposalV1 &tx_proposal_out) { tx_proposal_out.extra = extra; std::vector &normal_payment_proposals = tx_proposal_out.normal_payment_proposals = normal_payment_proposals_in; std::vector &selfsend_payment_proposals = tx_proposal_out.selfsend_payment_proposals = selfsend_payment_proposals_in; // add an additional payment proposal to satisfy scanning/consensus rules, if applicable append_additional_payment_proposal_if_necessary(normal_payment_proposals, selfsend_payment_proposals, change_address_spend_pubkey, change_address_index); const size_t num_outs = normal_payment_proposals.size() + selfsend_payment_proposals.size(); CHECK_AND_ASSERT_THROW_MES(num_outs >= CARROT_MIN_TX_OUTPUTS, "make_carrot_transaction_proposal_v1: too few outputs"); // generate random X25519 ephemeral pubkeys for selfsend proposals if: // a. not explicitly provided in a >2-out tx, OR // b. not explicitly provided in a 2-out 2-self-send tx and the other is also missing const bool should_gen_selfsend_ephemeral_pubkeys = num_outs != 2 || (normal_payment_proposals.empty() && !selfsend_payment_proposals.at(0).proposal.enote_ephemeral_pubkey && !selfsend_payment_proposals.at(1).proposal.enote_ephemeral_pubkey); if (should_gen_selfsend_ephemeral_pubkeys) { for (size_t i = 0; i < selfsend_payment_proposals.size(); ++i) { // should not provide two different D_e in a 2-out tx, so skip the second D_e in a 2-out const bool should_skip_generating = num_outs == 2 && i == 1; if (should_skip_generating) continue; CarrotPaymentProposalVerifiableSelfSendV1 &selfsend_payment_proposal = selfsend_payment_proposals[i]; if (!selfsend_payment_proposal.proposal.enote_ephemeral_pubkey) selfsend_payment_proposal.proposal.enote_ephemeral_pubkey = gen_x25519_pubkey(); } } // generate random dummy encrypted payment ID for if none of the normal payment proposals are integrated tx_proposal_out.dummy_encrypted_payment_id = gen_encrypted_payment_id(); // calculate the final size of tx.extra const size_t tx_extra_size = get_carrot_default_tx_extra_size(num_outs) + extra.size(); // calculate the concrete fee for this transaction for each possible valid input count std::map fee_per_input_count; for (size_t num_ins = CARROT_MIN_TX_INPUTS; num_ins <= CARROT_MAX_TX_INPUTS; ++num_ins) { const rct::xmr_amount fee = estimate_fee_carrot(num_ins, 15, num_outs, tx_extra_size, true, true, true, true, fee_per_weight, fee_quantization_mask); fee_per_input_count.emplace(num_ins, fee); } // calculate sum of payment proposal amounts before fee carving boost::multiprecision::uint128_t nominal_output_amount_sum = 0; for (const CarrotPaymentProposalV1 &normal_proposal : normal_payment_proposals) nominal_output_amount_sum += normal_proposal.amount; for (const CarrotPaymentProposalVerifiableSelfSendV1 &selfsend_proposal : selfsend_payment_proposals) nominal_output_amount_sum += selfsend_proposal.proposal.amount; // callback to select inputs given nominal output sum and fee per input count std::vector selected_inputs; select_inputs(nominal_output_amount_sum, fee_per_input_count, normal_payment_proposals.size(), selfsend_payment_proposals.size(), selected_inputs); // get fee given the number of selected inputs const std::size_t n_inputs = selected_inputs.size(); CARROT_CHECK_AND_THROW(n_inputs >= CARROT_MIN_TX_INPUTS, too_few_inputs, "input selection returned too few inputs: " << n_inputs); CARROT_CHECK_AND_THROW(n_inputs <= CARROT_MAX_TX_INPUTS, too_few_inputs, "input selection returned too many inputs: " << n_inputs); CARROT_CHECK_AND_THROW(fee_per_input_count.count(n_inputs), carrot_logic_error, "BUG: fee_per_input_count populated with holes, missing: " << n_inputs); tx_proposal_out.fee = fee_per_input_count.at(n_inputs); // calculate input amount sum boost::multiprecision::uint128_t input_amount_sum = 0; for (const CarrotSelectedInput &selected_input : selected_inputs) input_amount_sum += selected_input.amount; // callback to balance the outputs with the fee and input sum carve_fees_and_balance(input_amount_sum, tx_proposal_out.fee, normal_payment_proposals, selfsend_payment_proposals); // sanity check balance input_amount_sum -= tx_proposal_out.fee; for (const CarrotPaymentProposalV1 &normal_payment_proposal : normal_payment_proposals) input_amount_sum -= normal_payment_proposal.amount; for (const CarrotPaymentProposalVerifiableSelfSendV1 &selfsend_payment_proposal : selfsend_payment_proposals) input_amount_sum -= selfsend_payment_proposal.proposal.amount; if (tx_type != cryptonote::transaction_type::STAKE && tx_type != cryptonote::transaction_type::BURN) { CHECK_AND_ASSERT_THROW_MES(input_amount_sum == 0, "make_carrot_transaction_proposal_v1: post-carved transaction does not balance"); } else { tx_proposal_out.amount_burnt = input_amount_sum.convert_to(); CHECK_AND_ASSERT_THROW_MES(tx_proposal_out.amount_burnt >= 0, "make_carrot_transaction_proposal_v1: post-carved transaction burnt amount is negative: " << tx_proposal_out.amount_burnt); input_amount_sum -= tx_proposal_out.amount_burnt; } // collect and sort key images tx_proposal_out.key_images_sorted.reserve(selected_inputs.size()); for (const CarrotSelectedInput &selected_input : selected_inputs) tx_proposal_out.key_images_sorted.push_back(selected_input.key_image); std::sort(tx_proposal_out.key_images_sorted.begin(), tx_proposal_out.key_images_sorted.end(), std::greater{}); // consensus rules dictate inputs sorted in *reverse* lexicographical order since v7 // set the transaction type tx_proposal_out.tx_type = tx_type; } //------------------------------------------------------------------------------------------------------------------- void make_carrot_transaction_proposal_v1_transfer( const std::vector &normal_payment_proposals, const std::vector &selfsend_payment_proposals_in, const rct::xmr_amount fee_per_weight, const rct::xmr_amount fee_quantization_mask, const std::vector &extra, const cryptonote::transaction_type tx_type, select_inputs_func_t &&select_inputs, const crypto::public_key &change_address_spend_pubkey, const subaddress_index_extended &change_address_index, const std::set &subtractable_normal_payment_proposals, const std::set &subtractable_selfsend_payment_proposals, CarrotTransactionProposalV1 &tx_proposal_out) { std::vector selfsend_payment_proposals = selfsend_payment_proposals_in; // always add implicit selfsend enote, so resultant enotes' amounts mirror given payments set close as possible // note: we always do this, even if the amount ends up being 0 and we already have a selfsend. this is because if we // realize later that the change output we added here has a 0 amount, and we try removing it, then the fee // would go down and then the change amount *wouldn't* be 0, so it must stay. Although technically, // the scenario could arise where a change in input selection changes the input sum amount and fee exactly // such that we could remove the implicit change output and it happens to balance. IMO, handling this edge // case isn't worth the additional code complexity, and may cause unexpected uniformity issues. The calling // code might expect that transfers to N destinations always produces a transaction with N+1 outputs const bool add_payment_type_selfsend = normal_payment_proposals.empty() && selfsend_payment_proposals.size() == 1 && selfsend_payment_proposals.at(0).proposal.enote_type == CarrotEnoteType::CHANGE; selfsend_payment_proposals.push_back(CarrotPaymentProposalVerifiableSelfSendV1{ .proposal = CarrotPaymentProposalSelfSendV1{ .destination_address_spend_pubkey = change_address_spend_pubkey, .amount = 0, .enote_type = add_payment_type_selfsend ? CarrotEnoteType::PAYMENT : CarrotEnoteType::CHANGE, .asset_type = "SAL1" }, .subaddr_index = change_address_index }); // define carves fees and balance callback carve_fees_and_balance_func_t carve_fees_and_balance = [ &subtractable_normal_payment_proposals, &subtractable_selfsend_payment_proposals, &tx_type ] ( const boost::multiprecision::uint128_t &input_sum_amount, const rct::xmr_amount fee, std::vector &normal_payment_proposals, std::vector &selfsend_payment_proposals ) { // shadow subtractable_selfsend_payment_proposals and adjust for default case (no subtractable provided) const auto &subtractable_selfsend_payment_proposals_ref = subtractable_selfsend_payment_proposals; std::set subtractable_selfsend_payment_proposals = subtractable_selfsend_payment_proposals_ref; if (subtractable_selfsend_payment_proposals.empty()) subtractable_selfsend_payment_proposals.insert(selfsend_payment_proposals.size() - 1); const bool has_subbable_normal = !subtractable_normal_payment_proposals.empty(); const bool has_subbable_selfsend = !subtractable_selfsend_payment_proposals.empty(); const size_t num_normal = normal_payment_proposals.size(); const size_t num_selfsend = selfsend_payment_proposals.size(); // check subbable indices invariants CHECK_AND_ASSERT_THROW_MES( !has_subbable_normal || *subtractable_normal_payment_proposals.crbegin() < num_normal, "make unsigned transaction transfer subtractable: subtractable normal proposal index out of bounds"); CHECK_AND_ASSERT_THROW_MES( !has_subbable_selfsend || *subtractable_selfsend_payment_proposals.crbegin() < num_selfsend, "make unsigned transaction transfer subtractable: subtractable selfsend proposal index out of bounds"); CHECK_AND_ASSERT_THROW_MES(has_subbable_normal || has_subbable_selfsend, "make unsigned transaction transfer subtractable: no subtractable indices"); // check selfsend proposal invariants CHECK_AND_ASSERT_THROW_MES(!selfsend_payment_proposals.empty(), "make unsigned transaction transfer subtractable: missing a selfsend proposal"); CHECK_AND_ASSERT_THROW_MES(selfsend_payment_proposals.back().proposal.amount == 0, "make unsigned transaction transfer subtractable: bug: added implicit change output has non-zero amount"); // start by setting the last selfsend amount equal to (inputs - outputs), before fee boost::multiprecision::uint128_t implicit_change_amount = input_sum_amount; for (const CarrotPaymentProposalV1 &normal_payment_proposal : normal_payment_proposals) implicit_change_amount -= normal_payment_proposal.amount; for (const CarrotPaymentProposalVerifiableSelfSendV1 &selfsend_payment_proposal : selfsend_payment_proposals) implicit_change_amount -= selfsend_payment_proposal.proposal.amount; selfsend_payment_proposals.back().proposal.amount = boost::numeric_cast(implicit_change_amount); // deduct an even fee amount from all subtractable outputs const size_t num_subtractble_normal = subtractable_normal_payment_proposals.size(); const size_t num_subtractable_selfsend = subtractable_selfsend_payment_proposals.size(); const size_t num_subtractable = num_subtractble_normal + num_subtractable_selfsend; const rct::xmr_amount minimum_subtraction = fee / num_subtractable; // no div by 0 since we checked subtractable for (size_t normal_sub_idx : subtractable_normal_payment_proposals) { CarrotPaymentProposalV1 &normal_payment_proposal = normal_payment_proposals[normal_sub_idx]; CHECK_AND_ASSERT_THROW_MES(normal_payment_proposal.amount >= minimum_subtraction, "make unsigned transaction transfer subtractable: not enough funds in subtractable payment"); normal_payment_proposal.amount -= minimum_subtraction; } for (size_t selfsend_sub_idx : subtractable_selfsend_payment_proposals) { CarrotPaymentProposalSelfSendV1 &selfsend_payment_proposal = selfsend_payment_proposals[selfsend_sub_idx].proposal; CHECK_AND_ASSERT_THROW_MES(selfsend_payment_proposal.amount >= minimum_subtraction, "make unsigned transaction transfer subtractable: not enough funds in subtractable payment"); selfsend_payment_proposal.amount -= minimum_subtraction; } // deduct 1 at a time from selfsend proposals rct::xmr_amount fee_remainder = fee % num_subtractable; for (size_t selfsend_sub_idx : subtractable_selfsend_payment_proposals) { if (fee_remainder == 0) break; CarrotPaymentProposalSelfSendV1 &selfsend_payment_proposal = selfsend_payment_proposals[selfsend_sub_idx].proposal; CHECK_AND_ASSERT_THROW_MES(selfsend_payment_proposal.amount >= 1, "make unsigned transaction transfer subtractable: not enough funds in subtractable payment"); selfsend_payment_proposal.amount -= 1; fee_remainder -= 1; } // now deduct 1 at a time from normal proposals, shuffled if (fee_remainder != 0) { // create vector of shuffled subtractble normal payment indices // note: we do this to hide the order that the normal payment proposals were described in this call, in case // the recipients collude std::vector shuffled_normal_subtractable(subtractable_normal_payment_proposals.cbegin(), subtractable_normal_payment_proposals.cend()); std::shuffle(shuffled_normal_subtractable.begin(), shuffled_normal_subtractable.end(), crypto::random_device{}); for (size_t normal_sub_idx : shuffled_normal_subtractable) { if (fee_remainder == 0) break; CarrotPaymentProposalV1 &normal_payment_proposal = normal_payment_proposals[normal_sub_idx]; CHECK_AND_ASSERT_THROW_MES(normal_payment_proposal.amount >= 1, "make unsigned transaction transfer subtractable: not enough funds in subtractable payment"); normal_payment_proposal.amount -= 1; fee_remainder -= 1; } } CHECK_AND_ASSERT_THROW_MES(fee_remainder == 0, "make unsigned transaction transfer subtractable: bug: fee remainder at end of carve function"); // remove the self send payment we have made to ourself now that we have our change payment. if (tx_type == cryptonote::transaction_type::STAKE || tx_type == cryptonote::transaction_type::BURN) { selfsend_payment_proposals.back().proposal.enote_ephemeral_pubkey = selfsend_payment_proposals.front().proposal.enote_ephemeral_pubkey; selfsend_payment_proposals.erase(selfsend_payment_proposals.begin()); } }; //end carve_fees_and_balance // make unsigned transaction with fee carving callback make_carrot_transaction_proposal_v1(normal_payment_proposals, selfsend_payment_proposals, fee_per_weight, fee_quantization_mask, extra, tx_type, std::forward(select_inputs), std::move(carve_fees_and_balance), change_address_spend_pubkey, change_address_index, tx_proposal_out); } //------------------------------------------------------------------------------------------------------------------- void make_carrot_transaction_proposal_v1_sweep( const std::vector &normal_payment_proposals, const std::vector &selfsend_payment_proposals, const rct::xmr_amount fee_per_weight, const rct::xmr_amount fee_quantization_mask, const std::vector &extra, const cryptonote::transaction_type tx_type, std::vector &&selected_inputs, const crypto::public_key &change_address_spend_pubkey, const subaddress_index_extended &change_address_index, CarrotTransactionProposalV1 &tx_proposal_out) { // sanity check payment proposals are provided CHECK_AND_ASSERT_THROW_MES(normal_payment_proposals.size() || selfsend_payment_proposals.size(), "make carrot transaction proposal v1 sweep: no payment proposals provided"); // sanity check that all payment proposal amounts are 0 for (const CarrotPaymentProposalV1 &normal_payment_proposal : normal_payment_proposals) { CHECK_AND_ASSERT_THROW_MES(normal_payment_proposal.amount == 0, "make carrot transaction proposal v1 sweep: payment proposal amount not 0"); } for (const CarrotPaymentProposalVerifiableSelfSendV1 &selfsend_payment_proposal : selfsend_payment_proposals) { CHECK_AND_ASSERT_THROW_MES(selfsend_payment_proposal.proposal.amount == 0, "make carrot transaction proposal v1 sweep: payment proposal amount not 0"); } // sanity check that either normal payment proposals XOR selfsend are provided, not both CHECK_AND_ASSERT_THROW_MES(bool(normal_payment_proposals.size()) ^ bool(selfsend_payment_proposals.size()), "make carrot transaction proposal v1 sweep: both normal and self-send payment proposals are provided"); const bool is_selfsend_sweep = !selfsend_payment_proposals.empty(); // define input selection callback, which is just a shuttle for `selected_inputs` select_inputs_func_t select_inputs = [&selected_inputs] ( const boost::multiprecision::uint128_t&, const std::map&, const std::size_t, const std::size_t, std::vector &selected_inputs_out ) { selected_inputs_out = std::move(selected_inputs); }; //end select_inputs // define carves fees and balance callback carve_fees_and_balance_func_t carve_fees_and_balance = [is_selfsend_sweep] ( const boost::multiprecision::uint128_t &input_sum_amount, const rct::xmr_amount fee, std::vector &normal_payment_proposals, std::vector &selfsend_payment_proposals ) { // get pointers to proposal amounts and shuffle, excluding implicit selfsend const size_t n_outputs = normal_payment_proposals.size() + selfsend_payment_proposals.size(); std::vector amount_ptrs; amount_ptrs.reserve(n_outputs); if (is_selfsend_sweep) for (CarrotPaymentProposalVerifiableSelfSendV1 &selfsend_payment_proposal : selfsend_payment_proposals) amount_ptrs.push_back(&selfsend_payment_proposal.proposal.amount); else for (CarrotPaymentProposalV1 &normal_payment_proposal : normal_payment_proposals) amount_ptrs.push_back(&normal_payment_proposal.amount); std::shuffle(amount_ptrs.begin(), amount_ptrs.end(), crypto::random_device{}); // disburse amount equally amongst modifiable amounts const boost::multiprecision::uint128_t output_sum_amount = input_sum_amount - fee; const rct::xmr_amount minimum_sweep_amount = boost::numeric_cast(output_sum_amount / amount_ptrs.size()); const size_t num_remaining = boost::numeric_cast(output_sum_amount % amount_ptrs.size()); CHECK_AND_ASSERT_THROW_MES(num_remaining < amount_ptrs.size(), "make carrot transaction proposal v1 sweep: bug: num_remaining >= n_outputs"); for (size_t i = 0; i < amount_ptrs.size(); ++i) *amount_ptrs.at(i) = minimum_sweep_amount + (i < num_remaining ? 1 : 0); }; //end carve_fees_and_balance // make unsigned transaction with sweep carving callback and selected inputs make_carrot_transaction_proposal_v1(normal_payment_proposals, selfsend_payment_proposals, fee_per_weight, fee_quantization_mask, extra, tx_type, std::move(select_inputs), std::move(carve_fees_and_balance), change_address_spend_pubkey, change_address_index, tx_proposal_out); } //------------------------------------------------------------------------------------------------------------------- } //namespace carrot