// 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 "format_utils.h" //local headers #include "carrot_core/enote_utils.h" #include "carrot_core/exceptions.h" #include "carrot_core/payment_proposal.h" #include "cryptonote_basic/cryptonote_format_utils.h" #include "cryptonote_core/cryptonote_tx_utils.h" #include "cryptonote_config.h" //third party headers //standard headers #undef MONERO_DEFAULT_LOG_CATEGORY #define MONERO_DEFAULT_LOG_CATEGORY "carrot_impl.format_utils" static_assert(sizeof(mx25519_pubkey) == sizeof(crypto::public_key), "cannot use crypto::public_key as storage for X25519 keys since size is different"); namespace carrot { //------------------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------------------- template static void store_carrot_ephemeral_pubkeys_to_extra(const EnoteContainer &enotes, std::vector &extra_inout) { const size_t nouts = enotes.size(); if (nouts == 0) return; const bool use_shared_ephemeral_pubkey = nouts == 1 || (nouts == 2 && 0 == memcmp( &enotes.front().enote_ephemeral_pubkey, &enotes.back().enote_ephemeral_pubkey, sizeof(mx25519_pubkey))); bool success = true; if (use_shared_ephemeral_pubkey) { const mx25519_pubkey &enote_ephemeral_pubkey = enotes.at(0).enote_ephemeral_pubkey; const crypto::public_key tx_pubkey = raw_byte_convert(enote_ephemeral_pubkey); success = success && cryptonote::add_tx_pub_key_to_extra(extra_inout, tx_pubkey); } else // !use_shared_ephemeral_pubkey { std::vector tx_pubkeys(nouts); for (size_t i = 0; i < nouts; ++i) { const mx25519_pubkey &enote_ephemeral_pubkey = enotes.at(i).enote_ephemeral_pubkey; tx_pubkeys[i] = raw_byte_convert(enote_ephemeral_pubkey); } success = success && cryptonote::add_additional_tx_pub_keys_to_extra(extra_inout, tx_pubkeys); } CHECK_AND_ASSERT_THROW_MES(success, "add carrot ephemeral pubkeys to extra: failed to add tx_extra fields"); } //------------------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------------------- static bool try_load_carrot_ephemeral_pubkeys_from_extra(const std::vector &extra_fields, std::vector &enote_ephemeral_pubkeys_out) { //! @TODO: skip extra vector allocation for tx_pubkeys and copy directly //! @TODO: longterm: skip allocating into std::vector before copying cryptonote::tx_extra_pub_key tx_pubkey; cryptonote::tx_extra_additional_pub_keys tx_pubkeys; if (cryptonote::find_tx_extra_field_by_type(extra_fields, tx_pubkey)) { enote_ephemeral_pubkeys_out = {raw_byte_convert(tx_pubkey.pub_key)}; return true; } else if (cryptonote::find_tx_extra_field_by_type(extra_fields, tx_pubkeys)) { enote_ephemeral_pubkeys_out.resize(tx_pubkeys.data.size()); static_assert(sizeof(mx25519_pubkey) == sizeof(crypto::public_key)); static_assert(std::is_trivially_copyable_v); static_assert(std::is_trivially_copyable_v); memcpy(enote_ephemeral_pubkeys_out.data(), tx_pubkeys.data.data(), tx_pubkeys.data.size() * sizeof(mx25519_pubkey)); return true; } return false; } //------------------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------------------- bool is_carrot_transaction_v1(const cryptonote::transaction_prefix &tx_prefix) { if (tx_prefix.type == cryptonote::transaction_type::PROTOCOL && tx_prefix.vout.size() == 0) return false; CARROT_CHECK_AND_THROW(tx_prefix.vout.size(), too_few_outputs, "transaction prefix contains no outputs"); return tx_prefix.vout.at(0).target.type() == typeid(cryptonote::txout_to_carrot_v1); } //------------------------------------------------------------------------------------------------------------------- input_context_t parse_carrot_input_context(const cryptonote::txin_gen &txin) { return make_carrot_input_context_coinbase(txin.height); } //------------------------------------------------------------------------------------------------------------------- input_context_t parse_carrot_input_context(const cryptonote::txin_to_key &txin) { return make_carrot_input_context(txin.k_image); } //------------------------------------------------------------------------------------------------------------------- bool parse_carrot_input_context(const cryptonote::txin_v &txin, input_context_t &input_context_out) { struct parse_carrot_input_context_v_visitor { bool operator()(const cryptonote::txin_gen &txin) const { input_context_out = parse_carrot_input_context(txin); return true; } bool operator()(const cryptonote::txin_to_key &txin) const { input_context_out = parse_carrot_input_context(txin); return true; } bool operator()(const cryptonote::txin_to_script&) const { return false; } bool operator()(const cryptonote::txin_to_scripthash&) const { return false; } input_context_t &input_context_out; }; return boost::apply_visitor(parse_carrot_input_context_v_visitor{input_context_out}, txin); } //------------------------------------------------------------------------------------------------------------------- bool parse_carrot_input_context(const cryptonote::transaction_prefix &tx_prefix, input_context_t &input_context_out) { CHECK_AND_ASSERT_MES(!tx_prefix.vin.empty(), false, "parse_carrot_input_context: no input available"); return parse_carrot_input_context(tx_prefix.vin.at(0), input_context_out); } //------------------------------------------------------------------------------------------------------------------- bool try_load_carrot_extra_v1( const std::vector &tx_extra, std::vector &enote_ephemeral_pubkeys_out, std::optional &encrypted_payment_id_out) { std::vector tx_extra_fields; cryptonote::parse_tx_extra(tx_extra, tx_extra_fields); return try_load_carrot_extra_v1(tx_extra_fields, enote_ephemeral_pubkeys_out, encrypted_payment_id_out); } //------------------------------------------------------------------------------------------------------------------- bool try_load_carrot_extra_v1( const std::vector &tx_extra_fields, std::vector &enote_ephemeral_pubkeys_out, std::optional &encrypted_payment_id_out) { //ephemeral pubkeys: D_e if (!try_load_carrot_ephemeral_pubkeys_from_extra(tx_extra_fields, enote_ephemeral_pubkeys_out)) return false; //encrypted payment ID: pid_enc encrypted_payment_id_out = std::nullopt; cryptonote::tx_extra_nonce extra_nonce; if (cryptonote::find_tx_extra_field_by_type(tx_extra_fields, extra_nonce)) { crypto::hash8 pid_enc_8; if (cryptonote::get_encrypted_payment_id_from_tx_extra_nonce(extra_nonce.nonce, pid_enc_8)) { encrypted_payment_id_t &pid_enc = encrypted_payment_id_out.emplace(); pid_enc = raw_byte_convert(pid_enc_8); } } return true; } //------------------------------------------------------------------------------------------------------------------- cryptonote::transaction store_carrot_to_transaction_v1(const std::vector &enotes, const std::vector &key_images, const std::vector &sources, const rct::xmr_amount fee, const cryptonote::transaction_type tx_type, const rct::xmr_amount tx_amount_burnt, const std::vector &change_masks, const carrot::RCTOutputEnoteProposal &return_enote, const encrypted_payment_id_t encrypted_payment_id) { const size_t nins = key_images.size(); const size_t nouts = enotes.size(); CHECK_AND_ASSERT_THROW_MES(nins == sources.size(), "invalid inputs/sources size"); CHECK_AND_ASSERT_THROW_MES(change_masks.size() == nouts, "invalid change masks size. Expected: " << nouts - 1 << " got: " << change_masks.size()); cryptonote::transaction tx; tx.pruned = true; tx.unlock_time = 0; tx.source_asset_type = "SAL1"; tx.destination_asset_type = "SAL1"; tx.version = TRANSACTION_VERSION_CARROT; tx.type = tx_type == cryptonote::transaction_type::RETURN ? cryptonote::transaction_type::TRANSFER : tx_type; tx.amount_burnt = ( tx.type == cryptonote::transaction_type::STAKE || tx.type == cryptonote::transaction_type::BURN ) ? tx_amount_burnt : 0; tx.return_address_change_mask.assign(change_masks.begin(), change_masks.end()); tx.vin.reserve(nins); tx.vout.reserve(nouts); tx.return_address_list.reserve(change_masks.size()); tx.extra.reserve(MAX_TX_EXTRA_SIZE); tx.rct_signatures.type = carrot_v1_rct_type; tx.rct_signatures.txnFee = fee; tx.rct_signatures.ecdhInfo.reserve(nouts); tx.rct_signatures.outPk.reserve(nouts); //inputs for (size_t i = 0; i < nins; ++i) { // collect key offsets std::vector key_offsets; for(const auto &out_entry: sources[i].outputs) key_offsets.push_back(out_entry.first); //L tx.vin.emplace_back(cryptonote::txin_to_key{ //@TODO: can save 2 bytes by using slim input type .amount = 0, .asset_type = "SAL1", .key_offsets = cryptonote::absolute_output_offsets_to_relative(key_offsets), .k_image = key_images.at(i) }); } //outputs for (const CarrotEnoteV1 &enote : enotes) { //K_o,vt,anchor_enc tx.vout.push_back(cryptonote::tx_out{0, cryptonote::txout_to_carrot_v1{ .key = enote.onetime_address, .asset_type = enote.asset_type, .view_tag = enote.view_tag, .encrypted_janus_anchor = enote.anchor_enc, }}); //a_enc rct::ecdhTuple &ecdh_tuple = tx.rct_signatures.ecdhInfo.emplace_back(); memcpy(ecdh_tuple.amount.bytes, enote.amount_enc.bytes, sizeof(ecdh_tuple.amount)); //C_a tx.rct_signatures.outPk.push_back(rct::ctkey{rct::key{}, enote.amount_commitment}); //K_return if (tx_type != cryptonote::transaction_type::STAKE) { crypto::public_key K_return; memcpy(K_return.data, enote.return_enc.bytes, sizeof(crypto::public_key)); tx.return_address_list.push_back(K_return); } } // store the return pubkey for stake txs if (tx_type == cryptonote::transaction_type::STAKE) { tx.protocol_tx_data.version = 1; memcpy(tx.protocol_tx_data.return_address.data, return_enote.enote.onetime_address.data, sizeof(crypto::public_key)); memcpy(tx.protocol_tx_data.return_pubkey.data, return_enote.enote.enote_ephemeral_pubkey.data, sizeof(crypto::public_key)); tx.protocol_tx_data.return_view_tag = return_enote.enote.view_tag; tx.protocol_tx_data.return_anchor_enc = return_enote.enote.anchor_enc; } //ephemeral pubkeys: D_e store_carrot_ephemeral_pubkeys_to_extra(enotes, tx.extra); //encrypted payment id: pid_enc const crypto::hash8 pid_enc_8 = raw_byte_convert(encrypted_payment_id); cryptonote::blobdata extra_nonce; cryptonote::set_encrypted_payment_id_to_tx_extra_nonce(extra_nonce, pid_enc_8); CHECK_AND_ASSERT_THROW_MES(cryptonote::add_extra_nonce_to_tx_extra(tx.extra, extra_nonce), "store carrot to transaction v1: failed to add encrypted payment ID to tx_extra"); //finalize tx_extra CHECK_AND_ASSERT_THROW_MES(cryptonote::sort_tx_extra(tx.extra, tx.extra, /*allow_partial=*/false), "store carrot to transaction v1: failed to sort tx_extra"); return tx; } //------------------------------------------------------------------------------------------------------------------- bool try_load_carrot_enote_from_transaction_v1(const cryptonote::transaction &tx, const epee::span enote_ephemeral_pubkeys, const std::size_t local_output_index, CarrotEnoteV1 &enote_out) { const rct::rctSigBase &rv = tx.rct_signatures; const size_t nins = tx.vin.size(); const size_t nouts = tx.vout.size(); const bool shared_ephemeral_pubkey = enote_ephemeral_pubkeys.size() == 1; const size_t ephemeral_pubkey_index = shared_ephemeral_pubkey ? 0 : local_output_index; CHECK_AND_ASSERT_MES(nins, false, "try_load_carrot_enote_from_transaction_v1: no inputs"); CHECK_AND_ASSERT_MES(ephemeral_pubkey_index < enote_ephemeral_pubkeys.size(), false, "try_load_carrot_enote_from_transaction_v1: not enough ephemeral pubkeys"); CHECK_AND_ASSERT_MES(local_output_index < nouts, false, "try_load_carrot_enote_from_transaction_v1: not enough outputs"); CHECK_AND_ASSERT_MES(nouts == rv.ecdhInfo.size(), false, "try_load_carrot_enote_from_transaction_v1: ecdhInfo wrong size"); CHECK_AND_ASSERT_MES(nouts == rv.outPk.size(), false, "try_load_carrot_enote_from_transaction_v1: outPk wrong size"); const cryptonote::txout_target_v &t = tx.vout.at(local_output_index).target; const cryptonote::txout_to_carrot_v1 * const c = boost::strict_get(&t); CHECK_AND_ASSERT_MES(c, false, "try_load_carrot_enote_from_transaction_v1: wrong output type"); const cryptonote::txin_to_key * const inp = boost::strict_get(&tx.vin.at(0)); CHECK_AND_ASSERT_MES(inp, false, "try_load_carrot_enote_from_transaction_v1: wrong input type"); //K_o enote_out.onetime_address = c->key; // asset_type enote_out.asset_type = c->asset_type; //vt enote_out.view_tag = c->view_tag; //anchor_enc enote_out.anchor_enc = c->encrypted_janus_anchor; //L_1 enote_out.tx_first_key_image = inp->k_image; //a_enc memcpy(enote_out.amount_enc.bytes, rv.ecdhInfo.at(local_output_index).amount.bytes, sizeof(encrypted_amount_t)); //C_a enote_out.amount_commitment = rv.outPk.at(local_output_index).mask; //D_e enote_out.enote_ephemeral_pubkey = enote_ephemeral_pubkeys[ephemeral_pubkey_index]; // save all output keys in order to calculate Kr values. for (const auto& out: tx.vout) { const cryptonote::txout_to_carrot_v1 * const carrot_out = boost::strict_get(&out.target); if (carrot_out) { enote_out.tx_output_keys.push_back(carrot_out->key); } } return true; } //------------------------------------------------------------------------------------------------------------------- bool try_load_carrot_from_transaction_v1(const cryptonote::transaction &tx, std::vector &enotes_out, std::vector &key_images_out, rct::xmr_amount &fee_out, std::optional &encrypted_payment_id_out) { const rct::rctSigBase &rv = tx.rct_signatures; fee_out = rv.txnFee; const size_t nins = tx.vin.size(); const size_t nouts = tx.vout.size(); //inputs key_images_out.resize(nins); for (size_t i = 0; i < nins; ++i) { const cryptonote::txin_to_key * const k = boost::strict_get(&tx.vin.at(i)); if (nullptr == k) return false; //L key_images_out[i] = k->k_image; } //D_e, pid_enc std::vector enote_ephemeral_pubkeys; if (!try_load_carrot_extra_v1(tx.extra, enote_ephemeral_pubkeys, encrypted_payment_id_out)) return false; const size_t n_ephemeral = enote_ephemeral_pubkeys.size(); if (n_ephemeral == 0 || n_ephemeral > nouts) return false; //outputs enotes_out.resize(nouts); for (size_t i = 0; i < nouts; ++i) if (!try_load_carrot_enote_from_transaction_v1(tx, epee::to_span(enote_ephemeral_pubkeys), i, enotes_out[i])) return false; return true; } //------------------------------------------------------------------------------------------------------------------- cryptonote::transaction store_carrot_to_coinbase_transaction_v1( const std::vector &enotes, const cryptonote::blobdata &extra_nonce, const cryptonote::transaction_type &tx_type, const std::uint64_t block_index) { CARROT_CHECK_AND_THROW(tx_type == cryptonote::transaction_type::MINER || tx_type == cryptonote::transaction_type::PROTOCOL, invalid_tx_type, "invalid tx_type : is not MINER or PROTOCOL"); const size_t nouts = enotes.size(); cryptonote::transaction tx; tx.type = tx_type; tx.pruned = false; tx.version = TRANSACTION_VERSION_CARROT; tx.unlock_time = CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW; tx.vin.reserve(1); tx.vout.reserve(nouts); tx.extra.reserve(MAX_TX_EXTRA_SIZE); tx.rct_signatures.type = rct::RCTTypeNull; //input tx.vin.emplace_back(cryptonote::txin_gen{.height = static_cast(block_index)}); //outputs for (const CarrotCoinbaseEnoteV1 &enote : enotes) { //K_o,vt,anchor_enc,a tx.vout.push_back(cryptonote::tx_out{enote.amount, cryptonote::txout_to_carrot_v1{ .key = enote.onetime_address, .asset_type = enote.asset_type, .view_tag = enote.view_tag, .encrypted_janus_anchor = enote.anchor_enc } }); } //ephemeral pubkeys: D_e store_carrot_ephemeral_pubkeys_to_extra(enotes, tx.extra); //add extra_nonce to tx_extra CHECK_AND_ASSERT_THROW_MES(cryptonote::add_extra_nonce_to_tx_extra(tx.extra, extra_nonce), "store_carrot_to_coinbase_transaction_v1: failed to add extra nonce to tx_extra"); // sort tx_extra CHECK_AND_ASSERT_THROW_MES(cryptonote::sort_tx_extra(tx.extra, tx.extra), "store_carrot_to_coinbase_transaction_v1: failed to sort tx_extra"); return tx; } //------------------------------------------------------------------------------------------------------------------- cryptonote::transaction make_single_enote_carrot_coinbase_transaction_v1(const CarrotDestinationV1 &destination, const rct::xmr_amount block_reward, const std::uint64_t block_index, const cryptonote::blobdata &extra_nonce) { CHECK_AND_ASSERT_THROW_MES(!destination.is_subaddress, "make_single_enote_carrot_coinbase_transaction_v1: subaddress are not allowed in miner transactions"); CHECK_AND_ASSERT_THROW_MES(destination.payment_id == null_payment_id, "make_single_enote_carrot_coinbase_transaction_v1: integrated addresses are not allowed in miner transactions"); const CarrotPaymentProposalV1 payment_proposal{ .destination = destination, .amount = block_reward, .asset_type = "SAL1", .randomness = gen_janus_anchor() }; std::vector enotes(1); get_coinbase_output_proposal_v1(payment_proposal, block_index, enotes.front()); return store_carrot_to_coinbase_transaction_v1(enotes, extra_nonce, cryptonote::transaction_type::MINER, block_index); } //------------------------------------------------------------------------------------------------------------------- bool try_load_carrot_coinbase_enote_from_transaction_v1(const cryptonote::transaction &tx, const epee::span enote_ephemeral_pubkeys, const std::size_t local_output_index, CarrotCoinbaseEnoteV1 &enote_out) { CHECK_AND_ASSERT_MES(!tx.vin.empty(), false, "try_load_carrot_coinbase_enote_from_transaction_v1: no inputs"); const cryptonote::txin_gen * const inp = boost::strict_get(&tx.vin.at(0)); CHECK_AND_ASSERT_MES(inp, false, "try_load_carrot_coinbase_enote_from_transaction_v1: wrong input type"); //block_index enote_out.block_index = inp->height; CHECK_AND_ASSERT_MES(local_output_index < tx.vout.size(), false, "try_load_carrot_coinbase_enote_from_transaction_v1: not enough outputs"); const cryptonote::tx_out &o = tx.vout.at(local_output_index); //a enote_out.amount = o.amount; const cryptonote::txout_to_carrot_v1 * const c = boost::strict_get(&o.target); CHECK_AND_ASSERT_MES(c, false, "try_load_carrot_coinbase_enote_from_transaction_v1: wrong output type"); // asset type enote_out.asset_type = c->asset_type; //K_o enote_out.onetime_address = c->key; //vt enote_out.view_tag = c->view_tag; //anchor_enc enote_out.anchor_enc = c->encrypted_janus_anchor; CHECK_AND_ASSERT_MES(local_output_index < enote_ephemeral_pubkeys.size(), false, "try_load_carrot_coinbase_enote_from_transaction_v1: no enough ephemeral pubkeys"); //D_e enote_out.enote_ephemeral_pubkey = enote_ephemeral_pubkeys[local_output_index]; return true; } //------------------------------------------------------------------------------------------------------------------- bool try_load_carrot_from_coinbase_transaction_v1(const cryptonote::transaction &tx, std::vector &enotes_out) { const size_t nouts = tx.vout.size(); //D_e, pid_enc std::vector enote_ephemeral_pubkeys; std::optional dummy_encrypted_payment_id; if (!try_load_carrot_extra_v1(tx.extra, enote_ephemeral_pubkeys, dummy_encrypted_payment_id)) return false; else if (enote_ephemeral_pubkeys.size() != nouts) return false; //outputs enotes_out.resize(nouts); for (size_t i = 0; i < nouts; ++i) if (!try_load_carrot_coinbase_enote_from_transaction_v1(tx, epee::to_span(enote_ephemeral_pubkeys), i, enotes_out[i])) return false; return true; } //------------------------------------------------------------------------------------------------------------------- } //namespace carrot