// 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 "scanning_tools.h" //local headers #include "carrot_core/destination.h" #include "carrot_core/device_ram_borrowed.h" #include "carrot_core/enote_utils.h" #include "carrot_core/lazy_amount_commitment.h" #include "carrot_core/scan.h" #include "carrot_impl/format_utils.h" #include "common/container_helpers.h" #include "crypto/generators.h" #include "cryptonote_basic/cryptonote_format_utils.h" #include "ringct/rctOps.h" #include "ringct/rctSigs.h" //third party headers //standard headers #undef MONERO_DEFAULT_LOG_CATEGORY #define MONERO_DEFAULT_LOG_CATEGORY "wallet.scanning_tools" namespace tools { namespace wallet { //------------------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------------------- static bool parse_tx_extra_for_scanning(const std::vector &tx_extra, const std::size_t n_outputs, std::vector &main_tx_ephemeral_pubkeys_out, std::vector &additional_tx_ephemeral_pubkeys_out, cryptonote::blobdata &tx_extra_nonce_out) { // 1. parse extra fields std::vector tx_extra_fields; bool fully_parsed = cryptonote::parse_tx_extra(tx_extra, tx_extra_fields); // 2. extract main tx pubkey cryptonote::tx_extra_pub_key field_main_pubkey; size_t field_main_pubkey_index = 0; while (cryptonote::find_tx_extra_field_by_type(tx_extra_fields, field_main_pubkey, field_main_pubkey_index++)) main_tx_ephemeral_pubkeys_out.push_back(field_main_pubkey.pub_key); // 3. extract additional tx pubkeys cryptonote::tx_extra_additional_pub_keys field_additional_pubkeys; if (cryptonote::find_tx_extra_field_by_type(tx_extra_fields, field_additional_pubkeys)) { if (field_additional_pubkeys.data.size() == n_outputs) additional_tx_ephemeral_pubkeys_out = std::move(field_additional_pubkeys.data); else fully_parsed = false; } // 4. extract nonce string cryptonote::tx_extra_nonce field_extra_nonce; if (cryptonote::find_tx_extra_field_by_type(tx_extra_fields, field_extra_nonce)) tx_extra_nonce_out = std::move(field_extra_nonce.nonce); return fully_parsed; } //------------------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------------------- static bool try_load_pre_carrot_enote(const bool is_coinbase, const cryptonote::transaction &tx, const std::size_t local_output_index, PreCarrotEnote &enote_out) { CHECK_AND_ASSERT_MES(local_output_index < tx.vout.size(), false, "make_pre_carrot_enote: local_output_index out of range for vout"); const cryptonote::tx_out &o = tx.vout.at(local_output_index); CHECK_AND_ASSERT_MES(cryptonote::get_output_public_key(o, enote_out.onetime_address), false, "make_pre_carrot_enote: missing output public key"); CHECK_AND_ASSERT_MES(cryptonote::get_output_asset_type(o, enote_out.asset_type), false, "make_pre_carrot_enote: missing output asset type"); enote_out.view_tag = cryptonote::get_output_view_tag(o); enote_out.local_output_index = local_output_index; enote_out.encrypted_amount = tx.version == 2 && !is_coinbase; enote_out.short_amount = rct::is_rct_short_amount(tx.rct_signatures.type); if (enote_out.encrypted_amount) { CHECK_AND_ASSERT_MES(local_output_index < tx.rct_signatures.outPk.size(), false, "make_pre_carrot_enote: local_output_index out of range for outPk"); CHECK_AND_ASSERT_MES(local_output_index < tx.rct_signatures.ecdhInfo.size(), false, "make_pre_carrot_enote: local_output_index out of range for ecdhInfo"); enote_out.amount_commitment = tx.rct_signatures.outPk.at(local_output_index).mask; enote_out.amount = tx.rct_signatures.ecdhInfo.at(local_output_index); } else // !enote_out.encrypted_amount { enote_out.amount.amount = rct::d2h(o.amount); enote_out.amount.mask = rct::I; // technically the implicit amount commitment should be rct::zeroCommitVartime(o.amount), // but it isn't used during scanning for non-RingCT enotes, so we will leave it blank enote_out.amount_commitment = rct::Z; } return true; } //------------------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------------------- static std::optional view_incoming_scan_pre_carrot_enote( const PreCarrotEnote &enote, const std::optional &encrypted_payment_id, const epee::span main_derivations, const epee::span additional_derivations, const std::unordered_map &subaddress_map, hw::device &hwdev) { boost::optional receive_info; size_t main_deriv_idx; for (main_deriv_idx = 0; main_deriv_idx < std::max(1, main_derivations.size()); ++main_deriv_idx) { const crypto::key_derivation kd = main_deriv_idx < main_derivations.size() ? main_derivations[main_deriv_idx] : crypto::key_derivation{}; receive_info = cryptonote::is_out_to_acc_precomp(subaddress_map, enote.onetime_address, kd, main_deriv_idx ? epee::span{} : additional_derivations, enote.local_output_index, hwdev, enote.view_tag); if (receive_info) break; } if (!receive_info) return std::nullopt; crypto::ec_scalar amount_key; if (!hwdev.derivation_to_scalar(receive_info->derivation, enote.local_output_index, amount_key)) return std::nullopt; // a rct::xmr_amount amount = 0; // z rct::key amount_blinding_factor = rct::I; if (enote.encrypted_amount) { rct::ecdhTuple decoded_ecdh_tuple = enote.amount; if (!hwdev.ecdhDecode(decoded_ecdh_tuple, carrot::raw_byte_convert(amount_key), enote.short_amount)) return std::nullopt; // a amount = rct::h2d(decoded_ecdh_tuple.amount); // z amount_blinding_factor = decoded_ecdh_tuple.mask; // C' = z G + a H const rct::key recomputed_amount_commitment = rct::commit(amount, amount_blinding_factor); // C' ?= C if (!rct::equalKeys(enote.amount_commitment, recomputed_amount_commitment)) return std::nullopt; } else // !is_rct_amount { // a amount = rct::h2d(enote.amount.amount); // z amount_blinding_factor = enote.amount.mask; } // derive k^g_o crypto::secret_key sender_extension_g; if (!hwdev.derivation_to_scalar(receive_info->derivation, enote.local_output_index, sender_extension_g)) return std::nullopt; // K^j_s' crypto::public_key address_spend_pubkey; if (!hwdev.derive_subaddress_public_key(enote.onetime_address, receive_info->derivation, enote.local_output_index, address_spend_pubkey)) return std::nullopt; //pid crypto::hash payment_id = crypto::null_hash; if (encrypted_payment_id) { const crypto::secret_key inv_8 = rct::rct2sk(rct::INV_EIGHT); const crypto::public_key fake_pubkey = carrot::raw_byte_convert(receive_info->derivation); crypto::hash8 pid_hash8 = carrot::raw_byte_convert(*encrypted_payment_id); if (hwdev.decrypt_payment_id(pid_hash8, fake_pubkey, inv_8)) memcpy(&payment_id, &pid_hash8, sizeof(pid_hash8)); } //j const carrot::subaddress_index_extended subaddr_index{ .index = {receive_info->index.major, receive_info->index.minor}, .derive_type = carrot::AddressDeriveType::PreCarrot }; return enote_view_incoming_scan_info_t{ .sender_extension_g = sender_extension_g, .sender_extension_t = crypto::null_skey, .address_spend_pubkey = address_spend_pubkey, .payment_id = payment_id, .subaddr_index = subaddr_index, .amount = amount, .asset_type = enote.asset_type, .amount_blinding_factor = amount_blinding_factor, .main_tx_pubkey_index = main_deriv_idx }; } //------------------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------------------- static std::optional view_incoming_scan_carrot_coinbase_enote( const carrot::CarrotCoinbaseEnoteV1 &enote, const mx25519_pubkey &s_sender_receiver_unctx, const crypto::public_key &main_address_spend_pubkey) { enote_view_incoming_scan_info_t res; if (!carrot::try_scan_carrot_coinbase_enote_receiver(enote, s_sender_receiver_unctx, main_address_spend_pubkey, res.sender_extension_g, res.sender_extension_t)) return std::nullopt; res.address_spend_pubkey = main_address_spend_pubkey; res.payment_id = crypto::null_hash; res.subaddr_index = carrot::subaddress_index_extended{{0, 0}}; res.amount = enote.amount; res.asset_type = enote.asset_type; res.amount_blinding_factor = rct::I; res.main_tx_pubkey_index = 0; return res; } //------------------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------------------- static std::optional view_incoming_scan_carrot_enote_sender( const carrot::CarrotEnoteV1 &enote, const std::optional &encrypted_payment_id, const mx25519_pubkey &s_sender_receiver_unctx, const cryptonote::account_public_address &cn_addr) { for (unsigned is_subaddress = 0; is_subaddress < 2; ++is_subaddress) { const carrot::CarrotDestinationV1 destination{ .address_spend_pubkey = cn_addr.m_spend_public_key, .address_view_pubkey = cn_addr.m_view_public_key, .is_subaddress = bool(is_subaddress) }; enote_view_incoming_scan_info_t res; crypto::secret_key amount_blinding_factor_sk; carrot::payment_id_t payment_id; carrot::CarrotEnoteType dummy_enote_type; if (!carrot::try_scan_carrot_enote_external_sender(enote, encrypted_payment_id, destination, s_sender_receiver_unctx, res.sender_extension_g, res.sender_extension_t, res.amount, amount_blinding_factor_sk, dummy_enote_type, /*check_payment_id=*/false)) continue; memset(&res.payment_id, 0, sizeof(res.payment_id)); memcpy(&res.payment_id, &payment_id, sizeof(carrot::payment_id_t)); res.address_spend_pubkey= destination.address_spend_pubkey; res.subaddr_index.reset(); res.amount_blinding_factor = rct::sk2rct(amount_blinding_factor_sk); res.main_tx_pubkey_index = 0; res.asset_type = enote.asset_type; return res; } return std::nullopt; } //------------------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------------------- static std::optional view_incoming_scan_carrot_enote_receiver( const carrot::CarrotEnoteV1 &enote, const std::optional &encrypted_payment_id, const mx25519_pubkey &s_sender_receiver_unctx, const crypto::public_key &main_address_spend_pubkey, const carrot::view_incoming_key_device &k_view_dev, const std::unordered_map &subaddress_map) { enote_view_incoming_scan_info_t res; crypto::secret_key amount_blinding_factor_sk; carrot::payment_id_t payment_id; carrot::CarrotEnoteType dummy_enote_type; if (!carrot::try_scan_carrot_enote_external_receiver(enote, encrypted_payment_id, s_sender_receiver_unctx, {&main_address_spend_pubkey, 1}, k_view_dev, res.sender_extension_g, res.sender_extension_t, res.address_spend_pubkey, res.amount, amount_blinding_factor_sk, payment_id, dummy_enote_type)) return std::nullopt; const auto subaddr_it = subaddress_map.find(res.address_spend_pubkey); CHECK_AND_ASSERT_MES(subaddr_it != subaddress_map.cend(), std::nullopt, "view_incoming_scan_carrot_enote: carrot enote scanned successfully, " "but the recovered address spend pubkey was not found in the subaddress map"); const carrot::subaddress_index_extended subaddr_index = {{subaddr_it->second.major, subaddr_it->second.minor}}; memset(&res.payment_id, 0, sizeof(res.payment_id)); memcpy(&res.payment_id, &payment_id, sizeof(carrot::payment_id_t)); res.subaddr_index = subaddr_index; res.amount_blinding_factor = rct::sk2rct(amount_blinding_factor_sk); res.main_tx_pubkey_index = 0; res.asset_type = enote.asset_type; return res; } //------------------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------------------- static void perform_ecdh_derivations(const epee::span main_tx_ephemeral_pubkeys, const epee::span additional_tx_ephemeral_pubkeys, const crypto::secret_key &k_view_incoming, hw::device &hwdev, const bool is_carrot, std::vector &main_derivations_out, std::vector &additional_derivations_out) { main_derivations_out.clear(); additional_derivations_out.clear(); main_derivations_out.reserve(main_tx_ephemeral_pubkeys.size()); additional_derivations_out.reserve(additional_derivations_out.size()); if (is_carrot) { //! @TODO: HW device carrot::view_incoming_key_ram_borrowed_device k_view_dev(k_view_incoming); for (const crypto::public_key &main_tx_ephemeral_pubkey : main_tx_ephemeral_pubkeys) { mx25519_pubkey s_sender_receiver_unctx; k_view_dev.view_key_scalar_mult_x25519( carrot::raw_byte_convert(main_tx_ephemeral_pubkey), s_sender_receiver_unctx); main_derivations_out.push_back(carrot::raw_byte_convert(s_sender_receiver_unctx)); } for (const crypto::public_key &additional_tx_ephemeral_pubkey : additional_tx_ephemeral_pubkeys) { mx25519_pubkey s_sender_receiver_unctx; k_view_dev.view_key_scalar_mult_x25519( carrot::raw_byte_convert(additional_tx_ephemeral_pubkey), s_sender_receiver_unctx); additional_derivations_out.push_back(carrot::raw_byte_convert(s_sender_receiver_unctx)); } } else // !is_carrot { for (const crypto::public_key &main_tx_ephemeral_pubkey : main_tx_ephemeral_pubkeys) { hwdev.generate_key_derivation(main_tx_ephemeral_pubkey, k_view_incoming, tools::add_element(main_derivations_out)); } for (const crypto::public_key &additional_tx_ephemeral_pubkey : additional_tx_ephemeral_pubkeys) { hwdev.generate_key_derivation(additional_tx_ephemeral_pubkey, k_view_incoming, tools::add_element(additional_derivations_out)); } } } //------------------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------------------- bool try_load_pre_carrot_enote_from_transaction_prefix(const cryptonote::transaction_prefix &tx_prefix, const std::size_t local_output_index, const rct::xmr_amount amount, const rct::key &amount_blinding_factor, PreCarrotEnote &enote_out) { CHECK_AND_ASSERT_MES(local_output_index < tx_prefix.vout.size(), false, "make_pre_carrot_enote: local_output_index out of range for vout"); const cryptonote::tx_out &o = tx_prefix.vout.at(local_output_index); CHECK_AND_ASSERT_MES(cryptonote::get_output_public_key(o, enote_out.onetime_address), false, "make_pre_carrot_enote: missing output public key"); enote_out.view_tag = cryptonote::get_output_view_tag(o); enote_out.local_output_index = local_output_index; enote_out.encrypted_amount = false; enote_out.short_amount = false; enote_out.amount.amount = rct::d2h(amount); enote_out.amount.mask = amount_blinding_factor; return true; } //------------------------------------------------------------------------------------------------------------------- std::optional view_incoming_scan_enote( const MoneroEnoteVariant &enote, const std::size_t local_output_index, const cryptonote::blobdata &tx_extra_nonce, const epee::span main_derivations, const epee::span additional_derivations, const cryptonote::account_public_address &address, const carrot::view_incoming_key_device *k_view_dev, const std::unordered_map &subaddress_map, hw::device &hwdev) { CHECK_AND_ASSERT_MES(!main_derivations.empty() || !additional_derivations.empty(), std::nullopt, "view_incoming_scan_enote: no derivations provided"); CHECK_AND_ASSERT_MES(additional_derivations.empty() || local_output_index < additional_derivations.size(), std::nullopt, "view_incoming_scan_enote: additional derivations wrong size"); //pid_enc std::optional encrypted_payment_id; { crypto::hash8 pid_hash8; if (cryptonote::get_encrypted_payment_id_from_tx_extra_nonce(tx_extra_nonce, pid_hash8)) encrypted_payment_id = carrot::raw_byte_convert(pid_hash8); } struct view_incoming_scan_enote_visitor { std::optional operator()(const PreCarrotEnote &enote) const { auto res = view_incoming_scan_pre_carrot_enote(enote, encrypted_payment_id, main_derivations, additional_derivations, subaddress_map, hwdev); // copy long plaintext payment ID, if applicable const bool could_have_long_pid = res && res->payment_id == crypto::null_hash; if (could_have_long_pid && !cryptonote::get_payment_id_from_tx_extra_nonce(tx_extra_nonce, res->payment_id)) res->payment_id = crypto::hash{}; return res; } std::optional operator()(const carrot::CarrotCoinbaseEnoteV1 &enote) const { const crypto::key_derivation &kd = main_derivations.size() ? main_derivations[0] : additional_derivations[local_output_index]; const mx25519_pubkey s_sender_receiver_unctx = carrot::raw_byte_convert(kd); return view_incoming_scan_carrot_coinbase_enote(enote, s_sender_receiver_unctx, address.m_spend_public_key); } std::optional operator()(const carrot::CarrotEnoteV1 &enote) const { const crypto::key_derivation &kd = main_derivations.size() ? main_derivations[0] : additional_derivations[local_output_index]; const mx25519_pubkey s_sender_receiver_unctx = carrot::raw_byte_convert(kd); const bool scan_as_sender = k_view_dev == nullptr; if (scan_as_sender) { return view_incoming_scan_carrot_enote_sender(enote, encrypted_payment_id, s_sender_receiver_unctx, address); } else { return view_incoming_scan_carrot_enote_receiver(enote, encrypted_payment_id, s_sender_receiver_unctx, address.m_spend_public_key, *k_view_dev, subaddress_map); } } const std::size_t local_output_index; const cryptonote::blobdata &tx_extra_nonce; const std::optional encrypted_payment_id; const epee::span main_derivations; const epee::span additional_derivations; const cryptonote::account_public_address &address; const carrot::view_incoming_key_device *k_view_dev; const std::unordered_map &subaddress_map; hw::device &hwdev; }; return std::visit( view_incoming_scan_enote_visitor{local_output_index, tx_extra_nonce, encrypted_payment_id, main_derivations, additional_derivations, address, k_view_dev, subaddress_map, hwdev }, enote); } //------------------------------------------------------------------------------------------------------------------- std::optional view_incoming_scan_enote( const cryptonote::transaction &tx, const std::size_t local_output_index, const epee::span main_tx_ephemeral_pubkeys, const epee::span additional_tx_ephemeral_pubkeys, const cryptonote::blobdata &tx_extra_nonce, const epee::span main_derivations, const epee::span additional_derivations, const cryptonote::account_public_address &address, const carrot::view_incoming_key_device *k_view_dev, const std::unordered_map &subaddress_map, hw::device &hwdev) { MoneroEnoteVariant enote; const bool is_coinbase = cryptonote::is_coinbase(tx); const bool is_carrot = carrot::is_carrot_transaction_v1(tx); if (is_carrot) { const epee::span enote_ephemeral_pubkeys_pk = main_tx_ephemeral_pubkeys.empty() ? additional_tx_ephemeral_pubkeys : main_tx_ephemeral_pubkeys; //! @TODO: breaks strict aliasing rules const epee::span enote_ephemeral_pubkeys = { reinterpret_cast(enote_ephemeral_pubkeys_pk.data()), enote_ephemeral_pubkeys_pk.size()}; if (is_coinbase) { carrot::CarrotCoinbaseEnoteV1 coinbase_enote; if (!carrot::try_load_carrot_coinbase_enote_from_transaction_v1(tx, enote_ephemeral_pubkeys, local_output_index, coinbase_enote)) return std::nullopt; enote = coinbase_enote; } else { carrot::CarrotEnoteV1 carrot_enote; if (!carrot::try_load_carrot_enote_from_transaction_v1(tx, enote_ephemeral_pubkeys, local_output_index, carrot_enote)) return std::nullopt; enote = carrot_enote; } } else // !is_carrot { PreCarrotEnote pre_carrot_enote; if (!try_load_pre_carrot_enote(is_coinbase, tx, local_output_index, pre_carrot_enote)) return std::nullopt; enote = pre_carrot_enote; } return view_incoming_scan_enote(enote, local_output_index, tx_extra_nonce, main_derivations, additional_derivations, address, k_view_dev, subaddress_map, hwdev); } //------------------------------------------------------------------------------------------------------------------- std::optional view_incoming_scan_enote_from_prefix( const cryptonote::transaction_prefix &tx_prefix, const rct::xmr_amount amount, const rct::key &amount_blinding_factor, const std::size_t local_output_index, const cryptonote::account_public_address &address, const crypto::secret_key &k_view_incoming, const std::unordered_map &subaddress_map, hw::device &hwdev) { const bool is_carrot = carrot::is_carrot_transaction_v1(tx_prefix); CHECK_AND_ASSERT_MES(!is_carrot, std::nullopt, "view_incoming_scan_enote_from_prefix: carrot not yet supported"); // 1. parse enote at local_output_index using provided amount information PreCarrotEnote enote; CHECK_AND_ASSERT_MES(try_load_pre_carrot_enote_from_transaction_prefix(tx_prefix, local_output_index, amount, amount_blinding_factor, enote), std::nullopt, "view_incoming_scan_enote_from_prefix: failed to load enote from prefix"); // 2. parse tx_extra std::vector main_tx_ephemeral_pubkeys; std::vector additional_tx_ephemeral_pubkeys; cryptonote::blobdata tx_extra_nonce; if (!parse_tx_extra_for_scanning(tx_prefix.extra, tx_prefix.vout.size(), main_tx_ephemeral_pubkeys, additional_tx_ephemeral_pubkeys, tx_extra_nonce)) { MWARNING("view_incoming_scan_enote_from_prefix: tx extra has unsupported format"); } // 3. perform ECDH std::vector main_derivations; std::vector additional_derivations; perform_ecdh_derivations(epee::to_span(main_tx_ephemeral_pubkeys), epee::to_span(additional_tx_ephemeral_pubkeys), k_view_incoming, hwdev, is_carrot, main_derivations, additional_derivations); //! @TODO: HW device const carrot::view_incoming_key_ram_borrowed_device k_view_dev(k_view_incoming); // 4. view scan enote return view_incoming_scan_enote(enote, local_output_index, tx_extra_nonce, epee::to_span(main_derivations), epee::to_span(additional_derivations), address, &k_view_dev, subaddress_map, hwdev); } //------------------------------------------------------------------------------------------------------------------- void view_incoming_scan_transaction( const cryptonote::transaction &tx, const epee::span main_tx_ephemeral_pubkeys, const epee::span additional_tx_ephemeral_pubkeys, const cryptonote::blobdata &tx_extra_nonce, const epee::span main_derivations, const epee::span additional_derivations, const cryptonote::account_keys &acc, const std::unordered_map &subaddress_map, const epee::span> enote_scan_infos_out) { const size_t n_outputs = tx.vout.size(); CHECK_AND_ASSERT_THROW_MES(enote_scan_infos_out.size() == n_outputs, "view_incoming_scan_transaction: enote scan span wrong length"); //! @TODO: HW device carrot::view_incoming_key_ram_borrowed_device k_view_dev(acc.m_view_secret_key); // do view-incoming scan for each output enotes for (size_t local_output_index = 0; local_output_index < n_outputs; ++local_output_index) { auto &enote_scan_info = const_cast&>(enote_scan_infos_out[local_output_index]); enote_scan_info = view_incoming_scan_enote(tx, local_output_index, main_tx_ephemeral_pubkeys, additional_tx_ephemeral_pubkeys, tx_extra_nonce, main_derivations, additional_derivations, acc.m_account_address, &k_view_dev, subaddress_map, acc.get_device()); } } //------------------------------------------------------------------------------------------------------------------- void view_incoming_scan_transaction( const cryptonote::transaction &tx, const cryptonote::account_keys &acc, const std::unordered_map &subaddress_map, const epee::span> enote_scan_infos_out) { // 1. parse tx extra std::vector main_tx_ephemeral_pubkeys; std::vector additional_tx_ephemeral_pubkeys; cryptonote::blobdata tx_extra_nonce; if (!parse_tx_extra_for_scanning(tx.extra, tx.vout.size(), main_tx_ephemeral_pubkeys, additional_tx_ephemeral_pubkeys, tx_extra_nonce)) MWARNING("Transaction extra has unsupported format: " << cryptonote::get_transaction_hash(tx)); CHECK_AND_ASSERT_MES(!main_tx_ephemeral_pubkeys.empty() || !additional_tx_ephemeral_pubkeys.empty(),, "Transaction missing ephemeral pubkeys"); // 2. perform ECDH derivations std::vector main_derivations; std::vector additional_derivations; perform_ecdh_derivations(epee::to_span(main_tx_ephemeral_pubkeys), epee::to_span(additional_tx_ephemeral_pubkeys), acc.m_view_secret_key, acc.get_device(), carrot::is_carrot_transaction_v1(tx), main_derivations, additional_derivations); // 3. view-incoming scan output enotes view_incoming_scan_transaction(tx, epee::to_span(main_tx_ephemeral_pubkeys), epee::to_span(additional_tx_ephemeral_pubkeys), tx_extra_nonce, epee::to_span(main_derivations), epee::to_span(additional_derivations), acc, subaddress_map, enote_scan_infos_out); } //------------------------------------------------------------------------------------------------------------------- std::vector> view_incoming_scan_transaction( const cryptonote::transaction &tx, const cryptonote::account_keys &acc, const std::unordered_map &subaddress_map) { std::vector> res(tx.vout.size()); view_incoming_scan_transaction(tx, acc, subaddress_map, epee::to_mut_span(res)); return res; } //------------------------------------------------------------------------------------------------------------------- std::vector> view_incoming_scan_transaction_as_sender( const cryptonote::transaction &tx, const epee::span custom_main_derivations, const epee::span custom_additional_derivations, const cryptonote::account_public_address &address) { // 1. Resize output const size_t n_outputs = tx.vout.size(); std::vector> res(n_outputs); // 2. parse tx extra std::vector main_tx_ephemeral_pubkeys; std::vector additional_tx_ephemeral_pubkeys; cryptonote::blobdata tx_extra_nonce; if (!parse_tx_extra_for_scanning(tx.extra, tx.vout.size(), main_tx_ephemeral_pubkeys, additional_tx_ephemeral_pubkeys, tx_extra_nonce)) MWARNING("Transaction extra has unsupported format: " << cryptonote::get_transaction_hash(tx)); // 3. do view-incoming scan for each output enotes hw::device &hwdev = hw::get_device("default"); for (size_t local_output_index = 0; local_output_index < n_outputs; ++local_output_index) { auto &enote_scan_info = res[local_output_index]; enote_scan_info = view_incoming_scan_enote(tx, local_output_index, epee::to_span(main_tx_ephemeral_pubkeys), epee::to_span(additional_tx_ephemeral_pubkeys), tx_extra_nonce, custom_main_derivations, custom_additional_derivations, address, /*k_view_dev=*/nullptr, {{address.m_spend_public_key, {}}}, hwdev); } return res; } //------------------------------------------------------------------------------------------------------------------- bool is_long_payment_id(const crypto::hash &pid) { static_assert(sizeof(pid.data) / sizeof(pid.data[0]) == 32); char c = 0; for (size_t i = 8; i < 32; i++) c |= pid.data[i]; return c != 0; } //------------------------------------------------------------------------------------------------------------------- std::optional try_derive_enote_key_image( const enote_view_incoming_scan_info_t &enote_scan_info, const cryptonote::account_keys &acc) { if (!enote_scan_info.subaddr_index) return std::nullopt; // k^j_subext rct::key subaddress_extension; if (enote_scan_info.subaddr_index->index.is_subaddress()) { const cryptonote::subaddress_index subaddr_index_cn{enote_scan_info.subaddr_index->index.major, enote_scan_info.subaddr_index->index.minor}; subaddress_extension = rct::sk2rct( acc.get_device().get_subaddress_secret_key(acc.m_view_secret_key, subaddr_index_cn)); } else // !subaddr_index_cn.is_zero() { subaddress_extension = rct::Z; } // O = K^j_s + k^g_o G + k^t_o T rct::key onetime_address = rct::scalarmultKey(rct::pk2rct(crypto::get_T()), rct::sk2rct(enote_scan_info.sender_extension_t)); rct::addKeys1(onetime_address, rct::sk2rct(enote_scan_info.sender_extension_g), onetime_address); rct::addKeys(onetime_address, onetime_address, rct::pk2rct(enote_scan_info.address_spend_pubkey)); // I = Hp(O) crypto::ec_point ki_generator; crypto::derive_key_image_generator(rct::rct2pk(onetime_address), ki_generator); //! @TODO: HW devices // x = k_s + k^j_subext + k^g_o rct::key x; sc_add(x.bytes, to_bytes(acc.m_spend_secret_key), to_bytes(enote_scan_info.sender_extension_g)); sc_add(x.bytes, x.bytes, subaddress_extension.bytes); // L = x I = (k_s + k^j_subext + k^g_o) Hp(O) return rct::rct2ki(rct::scalarmultKey(rct::pt2rct(ki_generator), x)); } //------------------------------------------------------------------------------------------------------------------- } //namespace wallet } //namespace tools