diff --git a/src/carrot_core/CMakeLists.txt b/src/carrot_core/CMakeLists.txt index e81cf1a46..299cc88b5 100644 --- a/src/carrot_core/CMakeLists.txt +++ b/src/carrot_core/CMakeLists.txt @@ -35,6 +35,7 @@ set(carrot_core_sources device_ram_borrowed.cpp enote_utils.cpp hash_functions.cpp + output_set_finalization.cpp payment_proposal.cpp) monero_find_all_headers(carrot_core_headers, "${CMAKE_CURRENT_SOURCE_DIR}") diff --git a/src/carrot_core/config.h b/src/carrot_core/config.h index bf398ecb0..c15100c56 100644 --- a/src/carrot_core/config.h +++ b/src/carrot_core/config.h @@ -66,4 +66,7 @@ static constexpr const unsigned char CARROT_DOMAIN_SEP_GENERATE_ADDRESS_SECRET[] static constexpr const unsigned char CARROT_DOMAIN_SEP_ADDRESS_INDEX_GEN[] = "Carrot address index generator"; static constexpr const unsigned char CARROT_DOMAIN_SEP_SUBADDRESS_SCALAR[] = "Carrot subaddress scalar"; +// Carrot misc constants +static constexpr const unsigned int CARROT_MIN_TX_OUTPUTS = 2; +static constexpr const unsigned int CARROT_MAX_TX_OUTPUTS = 16; } //namespace carrot diff --git a/src/carrot_core/output_set_finalization.cpp b/src/carrot_core/output_set_finalization.cpp new file mode 100644 index 000000000..7f39815a0 --- /dev/null +++ b/src/carrot_core/output_set_finalization.cpp @@ -0,0 +1,228 @@ +// 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 "output_set_finalization.h" + +//local headers +#include "common/container_helpers.h" +#include "enote_utils.h" +#include "misc_log_ex.h" + +//third party headers + +//standard headers + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "carrot" + +namespace carrot +{ +//------------------------------------------------------------------------------------------------------------------- +std::optional get_additional_output_type(const size_t num_outgoing, + const size_t num_selfsend, + const bool remaining_change, + const bool have_payment_type_selfsend) +{ + const size_t num_outputs = num_outgoing + num_selfsend; + const bool already_completed = num_outputs >= 2 && num_selfsend >= 1 && !remaining_change; + if (num_outputs == 0) + { + ASSERT_MES_AND_THROW("get additional output type: set contains 0 outputs"); + } + else if (already_completed) + { + return std::nullopt; + } + else if (num_outputs == 1) + { + if (num_selfsend == 0) + { + return AdditionalOutputType::CHANGE_SHARED; + } + else if (!remaining_change) + { + return AdditionalOutputType::DUMMY; + } + else // num_selfsend == 1 && remaining_change + { + if (have_payment_type_selfsend) + { + return AdditionalOutputType::CHANGE_SHARED; + } + else + { + return AdditionalOutputType::PAYMENT_SHARED; + } + } + } + else if (num_outputs < CARROT_MAX_TX_OUTPUTS) + { + return AdditionalOutputType::CHANGE_UNIQUE; + } + else // num_outputs >= CARROT_MAX_TX_OUTPUTS + { + ASSERT_MES_AND_THROW("get additional output type: " + "set needs finalization but already contains too many outputs"); + } +} +//------------------------------------------------------------------------------------------------------------------- +tools::optional_variant get_additional_output_proposal( + const size_t num_outgoing, + const size_t num_selfsend, + const rct::xmr_amount remaining_change, + const bool have_payment_type_selfsend, + const crypto::public_key &change_address_spend_pubkey, + const crypto::x25519_pubkey &other_enote_ephemeral_pubkey) +{ + const std::optional additional_output_type = get_additional_output_type( + num_outgoing, + num_selfsend, + remaining_change, + have_payment_type_selfsend + ); + + if (!additional_output_type) + return {}; + + switch (*additional_output_type) + { + case AdditionalOutputType::PAYMENT_SHARED: + return CarrotPaymentProposalSelfSendV1{ + .destination_address_spend_pubkey = change_address_spend_pubkey, + .amount = remaining_change, + .enote_type = CarrotEnoteType::PAYMENT, + .enote_ephemeral_pubkey = other_enote_ephemeral_pubkey + }; + case AdditionalOutputType::CHANGE_SHARED: + return CarrotPaymentProposalSelfSendV1{ + .destination_address_spend_pubkey = change_address_spend_pubkey, + .amount = remaining_change, + .enote_type = CarrotEnoteType::CHANGE, + .enote_ephemeral_pubkey = other_enote_ephemeral_pubkey + }; + case AdditionalOutputType::CHANGE_UNIQUE: + return CarrotPaymentProposalSelfSendV1{ + .destination_address_spend_pubkey = change_address_spend_pubkey, + .amount = remaining_change, + .enote_type = CarrotEnoteType::CHANGE, + .enote_ephemeral_pubkey = crypto::x25519_pubkey_gen() + }; + case AdditionalOutputType::DUMMY: + return CarrotPaymentProposalV1{ + .destination = gen_carrot_main_address_v1(), + .amount = 0, + .randomness = gen_janus_anchor() + }; + } + + ASSERT_MES_AND_THROW("get additional output proposal: unrecognized additional output type"); +} +//------------------------------------------------------------------------------------------------------------------- +void get_output_enote_proposals(std::vector &&normal_payment_proposals, + std::vector &&selfsend_payment_proposals, + const view_balance_secret_device &s_view_balance_dev, + const crypto::key_image &tx_first_key_image, + std::vector &output_enote_proposals_out, + encrypted_payment_id_t &encrypted_payment_id_out) +{ + output_enote_proposals_out.clear(); + encrypted_payment_id_out = null_payment_id; + + // assert payment proposals numbers + const size_t num_proposals = normal_payment_proposals.size() + selfsend_payment_proposals.size(); + CHECK_AND_ASSERT_THROW_MES(num_proposals >= CARROT_MIN_TX_OUTPUTS, + "get output enote proposals: too few payment proposals"); + CHECK_AND_ASSERT_THROW_MES(num_proposals <= CARROT_MAX_TX_OUTPUTS, + "get output enote proposals: too many payment proposals"); + CHECK_AND_ASSERT_THROW_MES(selfsend_payment_proposals.size(), + "get output enote proposals: no selfsend payment proposal"); + + // assert there is a max of 1 integrated address payment proposals + size_t num_integrated = 0; + for (const CarrotPaymentProposalV1 &normal_payment_proposal : normal_payment_proposals) + if (normal_payment_proposal.destination.payment_id != null_payment_id) + ++num_integrated; + CHECK_AND_ASSERT_THROW_MES(num_integrated <= 1, + "get output enote proposals: only one integrated address is allowed per tx output set"); + + // input_context = "R" || KI_1 + input_context_t input_context; + make_carrot_input_context(tx_first_key_image, input_context); + + // construct normal enotes + output_enote_proposals_out.reserve(num_proposals); + for (size_t i = 0; i < normal_payment_proposals.size(); ++i) + { + encrypted_payment_id_t encrypted_payment_id; + get_output_proposal_normal_v1(normal_payment_proposals[i], + tx_first_key_image, + tools::add_element(output_enote_proposals_out), + encrypted_payment_id); + + // set pid to the first payment proposal or only integrated proposal + const bool is_first = i == 0; + const bool is_integrated = normal_payment_proposals[i].destination.payment_id != null_payment_id; + if (is_first || is_integrated) + encrypted_payment_id_out = encrypted_payment_id; + } + + // in the case that the pid is ambiguous, set it to random bytes + const bool ambiguous_pid_destination = num_integrated == 0 && normal_payment_proposals.size() > 1; + if (ambiguous_pid_destination) + encrypted_payment_id_out = gen_payment_id(); + + // construct selfsend enotes + for (const CarrotPaymentProposalSelfSendV1 &selfsend_payment_proposal : selfsend_payment_proposals) + { + get_output_proposal_internal_v1(selfsend_payment_proposal, + s_view_balance_dev, + tx_first_key_image, + tools::add_element(output_enote_proposals_out)); + } + + // sort enotes by D_e and assert uniqueness properties of D_e + const auto sort_by_ephemeral_pubkey = [](const RCTOutputEnoteProposal &a, const RCTOutputEnoteProposal &b) -> bool + { + return memcmp(&a.enote.enote_ephemeral_pubkey, + &b.enote.enote_ephemeral_pubkey, + sizeof(crypto::x25519_pubkey)) < 0; + }; + std::sort(output_enote_proposals_out.begin(), output_enote_proposals_out.end(), sort_by_ephemeral_pubkey); + const bool has_unique_ephemeral_pubkeys = tools::is_sorted_and_unique(output_enote_proposals_out, + sort_by_ephemeral_pubkey); + CHECK_AND_ASSERT_THROW_MES(!(num_proposals == 2 && has_unique_ephemeral_pubkeys), + "get output enote proposals: a 2-out set needs to share an ephemeral pubkey, but this 2-out set doesn't"); + CHECK_AND_ASSERT_THROW_MES(!(num_proposals != 2 && !has_unique_ephemeral_pubkeys), + "get output enote proposals: this >2-out set contains duplicate enote ephemeral pubkeys"); + + // sort enotes by Ko + std::sort(output_enote_proposals_out.begin(), output_enote_proposals_out.end()); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace carrot diff --git a/src/carrot_core/output_set_finalization.h b/src/carrot_core/output_set_finalization.h new file mode 100644 index 000000000..90da62f75 --- /dev/null +++ b/src/carrot_core/output_set_finalization.h @@ -0,0 +1,99 @@ +// 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. + +//! @file Utilities for constructing output proposal sets that adhere to Carrot rules + +#pragma once + +//local headers +#include "carrot_enote_types.h" +#include "common/variant.h" +#include "config.h" +#include "payment_proposal.h" +#include "ringct/rctTypes.h" + +//third party headers + +//standard headers +#include + +//forward declarations + + +namespace carrot +{ +enum class AdditionalOutputType +{ + PAYMENT_SHARED, // selfsend proposal with enote_type="payment" with a shared D_e + CHANGE_SHARED, // selfsend proposal with enote_type="change" with a shared D_e + CHANGE_UNIQUE, // selfsend proposal with enote_type="change" with a unique D_e + DUMMY // outgoing proposal to a random address +}; + +/** + * brief: get_additional_output_type - get the type of the additional enote needed to finalize an output set + * param: num_outgoing - number of outgoing transfers + * param: num_selfsend - number of selfsend transfers + * param: remaining_change - whether there is any leftover change needed to be included + * param: have_payment_type_selfsend - true if the enote set has a selfsend enote with enote_type="payment" + * return: AdditionalOutputType if need an additional enote, else std::nullopt + * throw: std::runtime_error if the output set is in a state where it cannot be finalized + */ +std::optional get_additional_output_type(const size_t num_outgoing, + const size_t num_selfsend, + const bool remaining_change, + const bool have_payment_type_selfsend); +/** + * brief: get_additional_output_proposal - get an additional output proposal to complete an output set + * param: num_outgoing - number of outgoing transfers + * param: num_selfsend - number of selfsend transfers + * param: remaining_change - the amount of leftover change needed to be included + * param: have_payment_type_selfsend - true if the enote set has a selfsend enote with enote_type="payment" + * param: change_address_spend_pubkey - K^j_s of our change address + * param: other_enote_ephemeral_pubkey - D^other_e + * return: an output proposal if need an additional enote, else none + * throw: std::runtime_error if the output set is in a state where it cannot be finalized + */ +tools::optional_variant get_additional_output_proposal( + const size_t num_outgoing, + const size_t num_selfsend, + const rct::xmr_amount remaining_change, + const bool have_payment_type_selfsend, + const crypto::public_key &change_address_spend_pubkey, + const crypto::x25519_pubkey &other_enote_ephemeral_pubkey); +/** + * brief: get_output_enote_proposals - convert payment proposals into output enote proposals + */ +void get_output_enote_proposals(std::vector &&normal_payment_proposals, + std::vector &&selfsend_payment_proposals, + const view_balance_secret_device &s_view_balance_dev, + const crypto::key_image &tx_first_key_image, + std::vector &output_enote_proposals_out, + encrypted_payment_id_t &encrypted_payment_id_out); + +} //namespace carrot diff --git a/src/carrot_core/payment_proposal.cpp b/src/carrot_core/payment_proposal.cpp index 639cff960..e09f65f4b 100644 --- a/src/carrot_core/payment_proposal.cpp +++ b/src/carrot_core/payment_proposal.cpp @@ -84,7 +84,7 @@ static void get_normal_proposal_ecdh_parts(const CarrotPaymentProposalV1 &propos const crypto::secret_key enote_ephemeral_privkey = get_enote_ephemeral_privkey(proposal, input_context); // 2. make D_e - get_enote_ephemeral_pubkey(proposal, input_context, enote_ephemeral_pubkey_out); + enote_ephemeral_pubkey_out = get_enote_ephemeral_pubkey(proposal, input_context); // 3. s_sr = 8 d_e ConvertPointE(K^j_v) make_carrot_uncontextualized_shared_key_sender(enote_ephemeral_privkey, @@ -191,22 +191,29 @@ bool operator==(const CarrotPaymentProposalSelfSendV1 &a, const CarrotPaymentPro a.enote_ephemeral_pubkey == b.enote_ephemeral_pubkey; } //------------------------------------------------------------------------------------------------------------------- -void get_enote_ephemeral_pubkey(const CarrotPaymentProposalV1 &proposal, - const input_context_t &input_context, - crypto::x25519_pubkey &enote_ephemeral_pubkey_out) +bool operator<(const RCTOutputEnoteProposal &a, const RCTOutputEnoteProposal &b) +{ + return memcmp(&a.enote.onetime_address, &b.enote.onetime_address, sizeof(crypto::public_key)) < 0; +} +//------------------------------------------------------------------------------------------------------------------- +crypto::x25519_pubkey get_enote_ephemeral_pubkey(const CarrotPaymentProposalV1 &proposal, + const input_context_t &input_context) { // d_e = H_n(anchor_norm, input_context, K^j_s, K^j_v, pid)) const crypto::secret_key enote_ephemeral_privkey{get_enote_ephemeral_privkey(proposal, input_context)}; + crypto::x25519_pubkey enote_ephemeral_pubkey; if (proposal.destination.is_subaddress) // D_e = d_e ConvertPointE(K^j_s) make_carrot_enote_ephemeral_pubkey_subaddress(enote_ephemeral_privkey, proposal.destination.address_spend_pubkey, - enote_ephemeral_pubkey_out); + enote_ephemeral_pubkey); else // D_e = d_e B make_carrot_enote_ephemeral_pubkey_cryptonote(enote_ephemeral_privkey, - enote_ephemeral_pubkey_out); + enote_ephemeral_pubkey); + + return enote_ephemeral_pubkey; } //------------------------------------------------------------------------------------------------------------------- void get_coinbase_output_proposal_v1(const CarrotPaymentProposalV1 &proposal, @@ -266,10 +273,8 @@ void get_coinbase_output_proposal_v1(const CarrotPaymentProposalV1 &proposal, //------------------------------------------------------------------------------------------------------------------- void get_output_proposal_normal_v1(const CarrotPaymentProposalV1 &proposal, const crypto::key_image &tx_first_key_image, - CarrotEnoteV1 &output_enote_out, - encrypted_payment_id_t &encrypted_payment_id_out, - rct::xmr_amount &amount_out, - crypto::secret_key &amount_blinding_factor_out) + RCTOutputEnoteProposal &output_enote_out, + encrypted_payment_id_t &encrypted_payment_id_out) { // 1. sanity checks CHECK_AND_ASSERT_THROW_MES(proposal.randomness != null_anchor, @@ -283,7 +288,7 @@ void get_output_proposal_normal_v1(const CarrotPaymentProposalV1 &proposal, crypto::x25519_pubkey s_sender_receiver_unctx; auto dhe_wiper = auto_wiper(s_sender_receiver_unctx); get_normal_proposal_ecdh_parts(proposal, input_context, - output_enote_out.enote_ephemeral_pubkey, + output_enote_out.enote.enote_ephemeral_pubkey, s_sender_receiver_unctx); // 4. build the output enote address pieces @@ -293,34 +298,32 @@ void get_output_proposal_normal_v1(const CarrotPaymentProposalV1 &proposal, proposal.destination.payment_id, proposal.amount, CarrotEnoteType::PAYMENT, - output_enote_out.enote_ephemeral_pubkey, + output_enote_out.enote.enote_ephemeral_pubkey, input_context, false, // coinbase_amount_commitment s_sender_receiver, - amount_blinding_factor_out, - output_enote_out.amount_commitment, - output_enote_out.onetime_address, - output_enote_out.amount_enc, + output_enote_out.amount_blinding_factor, + output_enote_out.enote.amount_commitment, + output_enote_out.enote.onetime_address, + output_enote_out.enote.amount_enc, encrypted_payment_id_out, - output_enote_out.view_tag); + output_enote_out.enote.view_tag); // 5. anchor_enc = anchor XOR m_anchor - output_enote_out.anchor_enc = encrypt_carrot_anchor(proposal.randomness, + output_enote_out.enote.anchor_enc = encrypt_carrot_anchor(proposal.randomness, s_sender_receiver, - output_enote_out.onetime_address); + output_enote_out.enote.onetime_address); // 6. save the amount and first key image - amount_out = proposal.amount; - output_enote_out.tx_first_key_image = tx_first_key_image; + output_enote_out.amount = proposal.amount; + output_enote_out.enote.tx_first_key_image = tx_first_key_image; } //------------------------------------------------------------------------------------------------------------------- void get_output_proposal_special_v1(const CarrotPaymentProposalSelfSendV1 &proposal, const view_incoming_key_device &k_view_dev, const crypto::public_key &primary_address_spend_pubkey, const crypto::key_image &tx_first_key_image, - CarrotEnoteV1 &output_enote_out, - rct::xmr_amount &amount_out, - crypto::secret_key &amount_blinding_factor_out) + RCTOutputEnoteProposal &output_enote_out) { // 1. sanity checks // @TODO @@ -347,38 +350,36 @@ void get_output_proposal_special_v1(const CarrotPaymentProposalSelfSendV1 &propo input_context, false, // coinbase_amount_commitment s_sender_receiver, - amount_blinding_factor_out, - output_enote_out.amount_commitment, - output_enote_out.onetime_address, - output_enote_out.amount_enc, + output_enote_out.amount_blinding_factor, + output_enote_out.enote.amount_commitment, + output_enote_out.enote.onetime_address, + output_enote_out.enote.amount_enc, dummy_encrypted_payment_id, - output_enote_out.view_tag); + output_enote_out.enote.view_tag); // 5. make special janus anchor: anchor_sp = H_16(D_e, input_context, Ko, k_v, K_s) janus_anchor_t janus_anchor_special; k_view_dev.make_janus_anchor_special(proposal.enote_ephemeral_pubkey, input_context, - output_enote_out.onetime_address, + output_enote_out.enote.onetime_address, primary_address_spend_pubkey, janus_anchor_special); // 6. encrypt special anchor: anchor_enc = anchor XOR m_anchor - output_enote_out.anchor_enc = encrypt_carrot_anchor(janus_anchor_special, + output_enote_out.enote.anchor_enc = encrypt_carrot_anchor(janus_anchor_special, s_sender_receiver, - output_enote_out.onetime_address); + output_enote_out.enote.onetime_address); // 7. save the enote ephemeral pubkey, first tx key image, and amount - output_enote_out.enote_ephemeral_pubkey = proposal.enote_ephemeral_pubkey; - output_enote_out.tx_first_key_image = tx_first_key_image; - amount_out = proposal.amount; + output_enote_out.enote.enote_ephemeral_pubkey = proposal.enote_ephemeral_pubkey; + output_enote_out.enote.tx_first_key_image = tx_first_key_image; + output_enote_out.amount = proposal.amount; } //------------------------------------------------------------------------------------------------------------------- void get_output_proposal_internal_v1(const CarrotPaymentProposalSelfSendV1 &proposal, const view_balance_secret_device &s_view_balance_dev, const crypto::key_image &tx_first_key_image, - CarrotEnoteV1 &output_enote_out, - rct::xmr_amount &amount_out, - crypto::secret_key &amount_blinding_factor_out) + RCTOutputEnoteProposal &output_enote_out) { // 1. sanity checks // @TODO @@ -403,24 +404,24 @@ void get_output_proposal_internal_v1(const CarrotPaymentProposalSelfSendV1 &prop proposal.enote_ephemeral_pubkey, input_context, false, // coinbase_amount_commitment - amount_blinding_factor_out, - output_enote_out.amount_commitment, - output_enote_out.onetime_address, - output_enote_out.amount_enc, + output_enote_out.amount_blinding_factor, + output_enote_out.enote.amount_commitment, + output_enote_out.enote.onetime_address, + output_enote_out.enote.amount_enc, dummy_encrypted_payment_id); // 5. vt = H_3(s_vb || input_context || Ko) s_view_balance_dev.make_internal_view_tag(input_context, - output_enote_out.onetime_address, - output_enote_out.view_tag); + output_enote_out.enote.onetime_address, + output_enote_out.enote.view_tag); // 4. generate random encrypted anchor - output_enote_out.anchor_enc = gen_janus_anchor(); + output_enote_out.enote.anchor_enc = gen_janus_anchor(); // 5. save the enote ephemeral pubkey, first tx key image, and amount - output_enote_out.enote_ephemeral_pubkey = proposal.enote_ephemeral_pubkey; - output_enote_out.tx_first_key_image = tx_first_key_image; - amount_out = proposal.amount; + output_enote_out.enote.enote_ephemeral_pubkey = proposal.enote_ephemeral_pubkey; + output_enote_out.enote.tx_first_key_image = tx_first_key_image; + output_enote_out.amount = proposal.amount; } //------------------------------------------------------------------------------------------------------------------- CarrotPaymentProposalV1 gen_carrot_payment_proposal_v1(const bool is_subaddress, diff --git a/src/carrot_core/payment_proposal.h b/src/carrot_core/payment_proposal.h index 77544a6c0..04fa0bcdb 100644 --- a/src/carrot_core/payment_proposal.h +++ b/src/carrot_core/payment_proposal.h @@ -79,20 +79,31 @@ struct CarrotPaymentProposalSelfSendV1 final crypto::x25519_pubkey enote_ephemeral_pubkey; }; +struct RCTOutputEnoteProposal +{ + CarrotEnoteV1 enote; + + // we need this opening information to make amount range proofs + rct::xmr_amount amount; + crypto::secret_key amount_blinding_factor; +}; + /// equality operators bool operator==(const CarrotPaymentProposalV1 &a, const CarrotPaymentProposalV1 &b); /// equality operators bool operator==(const CarrotPaymentProposalSelfSendV1 &a, const CarrotPaymentProposalSelfSendV1 &b); +/// comparison operators +bool operator<(const RCTOutputEnoteProposal &a, const RCTOutputEnoteProposal &b); + /** * brief: get_enote_ephemeral_pubkey - get the proposal's enote ephemeral pubkey D_e * param: proposal - * param: input_context - -* outparam: enote_ephemeral_pubkey_out - +* return: D_e */ -void get_enote_ephemeral_pubkey(const CarrotPaymentProposalV1 &proposal, - const input_context_t &input_context, - crypto::x25519_pubkey &enote_ephemeral_pubkey_out); +crypto::x25519_pubkey get_enote_ephemeral_pubkey(const CarrotPaymentProposalV1 &proposal, + const input_context_t &input_context); /** * brief: get_coinbase_output_proposal_v1 - convert the carrot proposal to a coinbase output proposal * param: proposal - @@ -109,15 +120,11 @@ void get_coinbase_output_proposal_v1(const CarrotPaymentProposalV1 &proposal, * param: tx_first_key_image - * outparam: output_enote_out - * outparam: encrypted_payment_id_out - pid_enc -* outparam: amount_out - used to open commitment C_a -* outparam: amount_blinding_factor_out - used to open commitment C_a */ void get_output_proposal_normal_v1(const CarrotPaymentProposalV1 &proposal, const crypto::key_image &tx_first_key_image, - CarrotEnoteV1 &output_enote_out, - encrypted_payment_id_t &encrypted_payment_id_out, - rct::xmr_amount &amount_out, - crypto::secret_key &amount_blinding_factor_out); + RCTOutputEnoteProposal &output_enote_out, + encrypted_payment_id_t &encrypted_payment_id_out); /** * brief: get_output_proposal_v1 - convert the carrot proposal to an output proposal (external selfsend) * param: proposal - @@ -125,16 +132,12 @@ void get_output_proposal_normal_v1(const CarrotPaymentProposalV1 &proposal, * param: primary_address_spend_pubkey - * param: tx_first_key_image - * outparam: output_enote_out - -* outparam: amount_out - used to open commitment C_a -* outparam: amount_blinding_factor_out - used to open commitment C_a */ void get_output_proposal_special_v1(const CarrotPaymentProposalSelfSendV1 &proposal, const view_incoming_key_device &k_view_dev, const crypto::public_key &primary_address_spend_pubkey, const crypto::key_image &tx_first_key_image, - CarrotEnoteV1 &output_enote_out, - rct::xmr_amount &amount_out, - crypto::secret_key &amount_blinding_factor_out); + RCTOutputEnoteProposal &output_enote_out); /** * brief: get_output_proposal_internal_v1 - convert the carrot proposal to an output proposal (internal) * param: proposal - @@ -143,15 +146,11 @@ void get_output_proposal_special_v1(const CarrotPaymentProposalSelfSendV1 &propo * param: tx_first_key_image - * outparam: output_enote_out - * outparam: partial_memo_out - -* outparam: amount_out - used to open commitment C_a -* outparam: amount_blinding_factor_out - used to open commitment C_a */ void get_output_proposal_internal_v1(const CarrotPaymentProposalSelfSendV1 &proposal, const view_balance_secret_device &s_view_balance_dev, const crypto::key_image &tx_first_key_image, - CarrotEnoteV1 &output_enote_out, - rct::xmr_amount &amount_out, - crypto::secret_key &amount_blinding_factor_out); + RCTOutputEnoteProposal &output_enote_out); /** * brief: gen_jamtis_payment_proposal_v1 - generate a random proposal * param: is_subaddress - whether to generate a proposal to subaddress diff --git a/tests/unit_tests/carrot_core.cpp b/tests/unit_tests/carrot_core.cpp index 2482cd7d2..165aeb6ff 100644 --- a/tests/unit_tests/carrot_core.cpp +++ b/tests/unit_tests/carrot_core.cpp @@ -161,23 +161,20 @@ TEST(carrot_core, main_address_normal_scan_completeness) const crypto::key_image tx_first_key_image = rct::rct2ki(rct::pkGen()); - CarrotEnoteV1 enote; + RCTOutputEnoteProposal enote_proposal; encrypted_payment_id_t encrypted_payment_id; - rct::xmr_amount amount; - crypto::secret_key amount_blinding_factor; get_output_proposal_normal_v1(proposal, tx_first_key_image, - enote, - encrypted_payment_id, - amount, - amount_blinding_factor); + enote_proposal, + encrypted_payment_id); - ASSERT_EQ(proposal.amount, amount); - ASSERT_EQ(enote.amount_commitment, rct::commit(amount, rct::sk2rct(amount_blinding_factor))); + ASSERT_EQ(proposal.amount, enote_proposal.amount); + const rct::key recomputed_amount_commitment = rct::commit(enote_proposal.amount, rct::sk2rct(enote_proposal.amount_blinding_factor)); + ASSERT_EQ(enote_proposal.enote.amount_commitment, recomputed_amount_commitment); crypto::x25519_pubkey s_sender_receiver_unctx; make_carrot_uncontextualized_shared_key_receiver(keys.k_view, - enote.enote_ephemeral_pubkey, + enote_proposal.enote.enote_ephemeral_pubkey, s_sender_receiver_unctx); crypto::secret_key recovered_sender_extension_g; @@ -187,7 +184,7 @@ TEST(carrot_core, main_address_normal_scan_completeness) crypto::secret_key recovered_amount_blinding_factor; encrypted_payment_id_t recovered_payment_id; CarrotEnoteType recovered_enote_type; - const bool scan_success = try_scan_carrot_enote_external(enote, + const bool scan_success = try_scan_carrot_enote_external(enote_proposal.enote, encrypted_payment_id, s_sender_receiver_unctx, keys.k_view_dev, @@ -204,8 +201,8 @@ TEST(carrot_core, main_address_normal_scan_completeness) // check recovered data EXPECT_EQ(proposal.destination.address_spend_pubkey, recovered_address_spend_pubkey); - EXPECT_EQ(amount, recovered_amount); - EXPECT_EQ(amount_blinding_factor, recovered_amount_blinding_factor); + EXPECT_EQ(proposal.amount, recovered_amount); + EXPECT_EQ(enote_proposal.amount_blinding_factor, recovered_amount_blinding_factor); EXPECT_EQ(null_payment_id, recovered_payment_id); EXPECT_EQ(CarrotEnoteType::PAYMENT, recovered_enote_type); @@ -215,7 +212,7 @@ TEST(carrot_core, main_address_normal_scan_completeness) rct::rct2sk(rct::I), recovered_sender_extension_g, recovered_sender_extension_t, - enote.onetime_address)); + enote_proposal.enote.onetime_address)); } //---------------------------------------------------------------------------------------------------------------------- TEST(carrot_core, subaddress_normal_scan_completeness) @@ -241,23 +238,20 @@ TEST(carrot_core, subaddress_normal_scan_completeness) const crypto::key_image tx_first_key_image = rct::rct2ki(rct::pkGen()); - CarrotEnoteV1 enote; + RCTOutputEnoteProposal enote_proposal; encrypted_payment_id_t encrypted_payment_id; - rct::xmr_amount amount; - crypto::secret_key amount_blinding_factor; get_output_proposal_normal_v1(proposal, tx_first_key_image, - enote, - encrypted_payment_id, - amount, - amount_blinding_factor); + enote_proposal, + encrypted_payment_id); - ASSERT_EQ(proposal.amount, amount); - ASSERT_EQ(enote.amount_commitment, rct::commit(amount, rct::sk2rct(amount_blinding_factor))); + ASSERT_EQ(proposal.amount, enote_proposal.amount); + const rct::key recomputed_amount_commitment = rct::commit(enote_proposal.amount, rct::sk2rct(enote_proposal.amount_blinding_factor)); + ASSERT_EQ(enote_proposal.enote.amount_commitment, recomputed_amount_commitment); crypto::x25519_pubkey s_sender_receiver_unctx; make_carrot_uncontextualized_shared_key_receiver(keys.k_view, - enote.enote_ephemeral_pubkey, + enote_proposal.enote.enote_ephemeral_pubkey, s_sender_receiver_unctx); crypto::secret_key recovered_sender_extension_g; @@ -267,7 +261,7 @@ TEST(carrot_core, subaddress_normal_scan_completeness) crypto::secret_key recovered_amount_blinding_factor; encrypted_payment_id_t recovered_payment_id; CarrotEnoteType recovered_enote_type; - const bool scan_success = try_scan_carrot_enote_external(enote, + const bool scan_success = try_scan_carrot_enote_external(enote_proposal.enote, encrypted_payment_id, s_sender_receiver_unctx, keys.k_view_dev, @@ -284,8 +278,8 @@ TEST(carrot_core, subaddress_normal_scan_completeness) // check recovered data EXPECT_EQ(proposal.destination.address_spend_pubkey, recovered_address_spend_pubkey); - EXPECT_EQ(amount, recovered_amount); - EXPECT_EQ(amount_blinding_factor, recovered_amount_blinding_factor); + EXPECT_EQ(proposal.amount, recovered_amount); + EXPECT_EQ(enote_proposal.amount_blinding_factor, recovered_amount_blinding_factor); EXPECT_EQ(null_payment_id, recovered_payment_id); EXPECT_EQ(CarrotEnoteType::PAYMENT, recovered_enote_type); @@ -308,7 +302,7 @@ TEST(carrot_core, subaddress_normal_scan_completeness) subaddr_scalar, recovered_sender_extension_g, recovered_sender_extension_t, - enote.onetime_address)); + enote_proposal.enote.onetime_address)); } //---------------------------------------------------------------------------------------------------------------------- TEST(carrot_core, integrated_address_normal_scan_completeness) @@ -329,23 +323,20 @@ TEST(carrot_core, integrated_address_normal_scan_completeness) const crypto::key_image tx_first_key_image = rct::rct2ki(rct::pkGen()); - CarrotEnoteV1 enote; + RCTOutputEnoteProposal enote_proposal; encrypted_payment_id_t encrypted_payment_id; - rct::xmr_amount amount; - crypto::secret_key amount_blinding_factor; get_output_proposal_normal_v1(proposal, tx_first_key_image, - enote, - encrypted_payment_id, - amount, - amount_blinding_factor); + enote_proposal, + encrypted_payment_id); - ASSERT_EQ(proposal.amount, amount); - ASSERT_EQ(enote.amount_commitment, rct::commit(amount, rct::sk2rct(amount_blinding_factor))); + ASSERT_EQ(proposal.amount, enote_proposal.amount); + const rct::key recomputed_amount_commitment = rct::commit(enote_proposal.amount, rct::sk2rct(enote_proposal.amount_blinding_factor)); + ASSERT_EQ(enote_proposal.enote.amount_commitment, recomputed_amount_commitment); crypto::x25519_pubkey s_sender_receiver_unctx; make_carrot_uncontextualized_shared_key_receiver(keys.k_view, - enote.enote_ephemeral_pubkey, + enote_proposal.enote.enote_ephemeral_pubkey, s_sender_receiver_unctx); crypto::secret_key recovered_sender_extension_g; @@ -355,7 +346,7 @@ TEST(carrot_core, integrated_address_normal_scan_completeness) crypto::secret_key recovered_amount_blinding_factor; encrypted_payment_id_t recovered_payment_id; CarrotEnoteType recovered_enote_type; - const bool scan_success = try_scan_carrot_enote_external(enote, + const bool scan_success = try_scan_carrot_enote_external(enote_proposal.enote, encrypted_payment_id, s_sender_receiver_unctx, keys.k_view_dev, @@ -372,8 +363,8 @@ TEST(carrot_core, integrated_address_normal_scan_completeness) // check recovered data EXPECT_EQ(proposal.destination.address_spend_pubkey, recovered_address_spend_pubkey); - EXPECT_EQ(amount, recovered_amount); - EXPECT_EQ(amount_blinding_factor, recovered_amount_blinding_factor); + EXPECT_EQ(proposal.amount, recovered_amount); + EXPECT_EQ(enote_proposal.amount_blinding_factor, recovered_amount_blinding_factor); EXPECT_EQ(integrated_address.payment_id, recovered_payment_id); EXPECT_EQ(CarrotEnoteType::PAYMENT, recovered_enote_type); @@ -383,7 +374,7 @@ TEST(carrot_core, integrated_address_normal_scan_completeness) rct::rct2sk(rct::I), recovered_sender_extension_g, recovered_sender_extension_t, - enote.onetime_address)); + enote_proposal.enote.onetime_address)); } //---------------------------------------------------------------------------------------------------------------------- TEST(carrot_core, main_address_special_scan_completeness) @@ -407,23 +398,20 @@ TEST(carrot_core, main_address_special_scan_completeness) const crypto::key_image tx_first_key_image = rct::rct2ki(rct::pkGen()); - CarrotEnoteV1 enote; - rct::xmr_amount amount; - crypto::secret_key amount_blinding_factor; + RCTOutputEnoteProposal enote_proposal; get_output_proposal_special_v1(proposal, keys.k_view_dev, keys.account_spend_pubkey, tx_first_key_image, - enote, - amount, - amount_blinding_factor); + enote_proposal); - ASSERT_EQ(proposal.amount, amount); - ASSERT_EQ(enote.amount_commitment, rct::commit(amount, rct::sk2rct(amount_blinding_factor))); + ASSERT_EQ(proposal.amount, enote_proposal.amount); + const rct::key recomputed_amount_commitment = rct::commit(enote_proposal.amount, rct::sk2rct(enote_proposal.amount_blinding_factor)); + ASSERT_EQ(enote_proposal.enote.amount_commitment, recomputed_amount_commitment); crypto::x25519_pubkey s_sender_receiver_unctx; make_carrot_uncontextualized_shared_key_receiver(keys.k_view, - enote.enote_ephemeral_pubkey, + enote_proposal.enote.enote_ephemeral_pubkey, s_sender_receiver_unctx); crypto::secret_key recovered_sender_extension_g; @@ -433,7 +421,7 @@ TEST(carrot_core, main_address_special_scan_completeness) crypto::secret_key recovered_amount_blinding_factor; encrypted_payment_id_t recovered_payment_id; CarrotEnoteType recovered_enote_type; - const bool scan_success = try_scan_carrot_enote_external(enote, + const bool scan_success = try_scan_carrot_enote_external(enote_proposal.enote, std::nullopt, s_sender_receiver_unctx, keys.k_view_dev, @@ -450,8 +438,8 @@ TEST(carrot_core, main_address_special_scan_completeness) // check recovered data EXPECT_EQ(proposal.destination_address_spend_pubkey, recovered_address_spend_pubkey); - EXPECT_EQ(amount, recovered_amount); - EXPECT_EQ(amount_blinding_factor, recovered_amount_blinding_factor); + EXPECT_EQ(proposal.amount, recovered_amount); + EXPECT_EQ(enote_proposal.amount_blinding_factor, recovered_amount_blinding_factor); EXPECT_EQ(null_payment_id, recovered_payment_id); EXPECT_EQ(enote_type, recovered_enote_type); @@ -461,7 +449,7 @@ TEST(carrot_core, main_address_special_scan_completeness) rct::rct2sk(rct::I), recovered_sender_extension_g, recovered_sender_extension_t, - enote.onetime_address)); + enote_proposal.enote.onetime_address)); } } //---------------------------------------------------------------------------------------------------------------------- @@ -494,23 +482,20 @@ TEST(carrot_core, subaddress_special_scan_completeness) const crypto::key_image tx_first_key_image = rct::rct2ki(rct::pkGen()); - CarrotEnoteV1 enote; - rct::xmr_amount amount; - crypto::secret_key amount_blinding_factor; + RCTOutputEnoteProposal enote_proposal; get_output_proposal_special_v1(proposal, keys.k_view_dev, keys.account_spend_pubkey, tx_first_key_image, - enote, - amount, - amount_blinding_factor); + enote_proposal); - ASSERT_EQ(proposal.amount, amount); - ASSERT_EQ(enote.amount_commitment, rct::commit(amount, rct::sk2rct(amount_blinding_factor))); + ASSERT_EQ(proposal.amount, enote_proposal.amount); + const rct::key recomputed_amount_commitment = rct::commit(enote_proposal.amount, rct::sk2rct(enote_proposal.amount_blinding_factor)); + ASSERT_EQ(enote_proposal.enote.amount_commitment, recomputed_amount_commitment); crypto::x25519_pubkey s_sender_receiver_unctx; make_carrot_uncontextualized_shared_key_receiver(keys.k_view, - enote.enote_ephemeral_pubkey, + enote_proposal.enote.enote_ephemeral_pubkey, s_sender_receiver_unctx); crypto::secret_key recovered_sender_extension_g; @@ -520,7 +505,7 @@ TEST(carrot_core, subaddress_special_scan_completeness) crypto::secret_key recovered_amount_blinding_factor; encrypted_payment_id_t recovered_payment_id; CarrotEnoteType recovered_enote_type; - const bool scan_success = try_scan_carrot_enote_external(enote, + const bool scan_success = try_scan_carrot_enote_external(enote_proposal.enote, std::nullopt, s_sender_receiver_unctx, keys.k_view_dev, @@ -537,8 +522,8 @@ TEST(carrot_core, subaddress_special_scan_completeness) // check recovered data EXPECT_EQ(proposal.destination_address_spend_pubkey, recovered_address_spend_pubkey); - EXPECT_EQ(amount, recovered_amount); - EXPECT_EQ(amount_blinding_factor, recovered_amount_blinding_factor); + EXPECT_EQ(proposal.amount, recovered_amount); + EXPECT_EQ(enote_proposal.amount_blinding_factor, recovered_amount_blinding_factor); EXPECT_EQ(null_payment_id, recovered_payment_id); EXPECT_EQ(enote_type, recovered_enote_type); @@ -555,13 +540,13 @@ TEST(carrot_core, subaddress_special_scan_completeness) j_major, j_minor, subaddr_scalar); - + EXPECT_TRUE(can_open_fcmp_onetime_address(keys.k_prove_spend, keys.k_generate_image, subaddr_scalar, recovered_sender_extension_g, recovered_sender_extension_t, - enote.onetime_address)); + enote_proposal.enote.onetime_address)); } } //---------------------------------------------------------------------------------------------------------------------- @@ -586,18 +571,15 @@ TEST(carrot_core, main_address_internal_scan_completeness) const crypto::key_image tx_first_key_image = rct::rct2ki(rct::pkGen()); - CarrotEnoteV1 enote; - rct::xmr_amount amount; - crypto::secret_key amount_blinding_factor; + RCTOutputEnoteProposal enote_proposal; get_output_proposal_internal_v1(proposal, keys.s_view_balance_dev, tx_first_key_image, - enote, - amount, - amount_blinding_factor); + enote_proposal); - ASSERT_EQ(proposal.amount, amount); - ASSERT_EQ(enote.amount_commitment, rct::commit(amount, rct::sk2rct(amount_blinding_factor))); + ASSERT_EQ(proposal.amount, enote_proposal.amount); + const rct::key recomputed_amount_commitment = rct::commit(enote_proposal.amount, rct::sk2rct(enote_proposal.amount_blinding_factor)); + ASSERT_EQ(enote_proposal.enote.amount_commitment, recomputed_amount_commitment); crypto::secret_key recovered_sender_extension_g; crypto::secret_key recovered_sender_extension_t; @@ -605,7 +587,7 @@ TEST(carrot_core, main_address_internal_scan_completeness) rct::xmr_amount recovered_amount; crypto::secret_key recovered_amount_blinding_factor; CarrotEnoteType recovered_enote_type; - const bool scan_success = try_scan_carrot_enote_internal(enote, + const bool scan_success = try_scan_carrot_enote_internal(enote_proposal.enote, keys.s_view_balance_dev, recovered_sender_extension_g, recovered_sender_extension_t, @@ -618,8 +600,8 @@ TEST(carrot_core, main_address_internal_scan_completeness) // check recovered data EXPECT_EQ(proposal.destination_address_spend_pubkey, recovered_address_spend_pubkey); - EXPECT_EQ(amount, recovered_amount); - EXPECT_EQ(amount_blinding_factor, recovered_amount_blinding_factor); + EXPECT_EQ(proposal.amount, recovered_amount); + EXPECT_EQ(enote_proposal.amount_blinding_factor, recovered_amount_blinding_factor); EXPECT_EQ(enote_type, recovered_enote_type); // check spendability @@ -628,7 +610,7 @@ TEST(carrot_core, main_address_internal_scan_completeness) rct::rct2sk(rct::I), recovered_sender_extension_g, recovered_sender_extension_t, - enote.onetime_address)); + enote_proposal.enote.onetime_address)); } } //---------------------------------------------------------------------------------------------------------------------- @@ -661,18 +643,15 @@ TEST(carrot_core, subaddress_internal_scan_completeness) const crypto::key_image tx_first_key_image = rct::rct2ki(rct::pkGen()); - CarrotEnoteV1 enote; - rct::xmr_amount amount; - crypto::secret_key amount_blinding_factor; + RCTOutputEnoteProposal enote_proposal; get_output_proposal_internal_v1(proposal, keys.s_view_balance_dev, tx_first_key_image, - enote, - amount, - amount_blinding_factor); + enote_proposal); - ASSERT_EQ(proposal.amount, amount); - ASSERT_EQ(enote.amount_commitment, rct::commit(amount, rct::sk2rct(amount_blinding_factor))); + ASSERT_EQ(proposal.amount, enote_proposal.amount); + const rct::key recomputed_amount_commitment = rct::commit(enote_proposal.amount, rct::sk2rct(enote_proposal.amount_blinding_factor)); + ASSERT_EQ(enote_proposal.enote.amount_commitment, recomputed_amount_commitment); crypto::secret_key recovered_sender_extension_g; crypto::secret_key recovered_sender_extension_t; @@ -680,7 +659,7 @@ TEST(carrot_core, subaddress_internal_scan_completeness) rct::xmr_amount recovered_amount; crypto::secret_key recovered_amount_blinding_factor; CarrotEnoteType recovered_enote_type; - const bool scan_success = try_scan_carrot_enote_internal(enote, + const bool scan_success = try_scan_carrot_enote_internal(enote_proposal.enote, keys.s_view_balance_dev, recovered_sender_extension_g, recovered_sender_extension_t, @@ -693,8 +672,8 @@ TEST(carrot_core, subaddress_internal_scan_completeness) // check recovered data EXPECT_EQ(proposal.destination_address_spend_pubkey, recovered_address_spend_pubkey); - EXPECT_EQ(amount, recovered_amount); - EXPECT_EQ(amount_blinding_factor, recovered_amount_blinding_factor); + EXPECT_EQ(proposal.amount, recovered_amount); + EXPECT_EQ(enote_proposal.amount_blinding_factor, recovered_amount_blinding_factor); EXPECT_EQ(enote_type, recovered_enote_type); // check spendability @@ -716,7 +695,7 @@ TEST(carrot_core, subaddress_internal_scan_completeness) subaddr_scalar, recovered_sender_extension_g, recovered_sender_extension_t, - enote.onetime_address)); + enote_proposal.enote.onetime_address)); } } //----------------------------------------------------------------------------------------------------------------------