From b529887e88f573bce42c3fac3f8a4174250910c3 Mon Sep 17 00:00:00 2001 From: jeffro256 Date: Wed, 16 Apr 2025 18:41:10 -0500 Subject: [PATCH] carrot+fcmp: rework for Carrot scanning changes --- src/ringct/rctTypes.cpp | 19 + src/ringct/rctTypes.h | 1 + src/wallet/scanning_tools.cpp | 1085 +++++++++++++++----------- src/wallet/scanning_tools.h | 101 +-- src/wallet/wallet2.cpp | 54 +- src/wallet/wallet2.h | 1 + tests/unit_tests/wallet_scanning.cpp | 8 +- 7 files changed, 718 insertions(+), 551 deletions(-) diff --git a/src/ringct/rctTypes.cpp b/src/ringct/rctTypes.cpp index d379b1c77..c3328737a 100644 --- a/src/ringct/rctTypes.cpp +++ b/src/ringct/rctTypes.cpp @@ -257,6 +257,25 @@ namespace rct { } } + bool is_rct_short_amount(int type) + { + switch (type) + { + case RCTTypeBulletproof2: + case RCTTypeCLSAG: + case RCTTypeBulletproofPlus: + case RCTTypeFcmpPlusPlus: + return true; + case RCTTypeNull: + case RCTTypeFull: + case RCTTypeSimple: + case RCTTypeBulletproof: + return false; + default: + CHECK_AND_ASSERT_THROW_MES(false, "unknown rct type = " << type); + } + } + static size_t n_bulletproof_amounts_base(const size_t L_size, const size_t R_size, const size_t V_size, const size_t max_outputs) { CHECK_AND_ASSERT_MES(L_size >= 6, 0, "Invalid bulletproof L size"); diff --git a/src/ringct/rctTypes.h b/src/ringct/rctTypes.h index 17b390c59..be737cd3c 100644 --- a/src/ringct/rctTypes.h +++ b/src/ringct/rctTypes.h @@ -818,6 +818,7 @@ namespace rct { bool is_rct_bulletproof_plus(int type); bool is_rct_borromean(int type); bool is_rct_clsag(int type); + bool is_rct_short_amount(int type); static inline const rct::key &pk2rct(const crypto::public_key &pk) { return (const rct::key&)pk; } static inline const rct::key &sk2rct(const crypto::secret_key &sk) { return (const rct::key&)sk; } diff --git a/src/wallet/scanning_tools.cpp b/src/wallet/scanning_tools.cpp index a5779b681..fcdc24824 100644 --- a/src/wallet/scanning_tools.cpp +++ b/src/wallet/scanning_tools.cpp @@ -30,12 +30,14 @@ #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/carrot_tx_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" @@ -88,22 +90,283 @@ static bool parse_tx_extra_for_scanning(const std::vector &tx_extr } //------------------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------------------- -static bool parse_tx_extra_for_scanning(const cryptonote::transaction_prefix &tx_prefix, - std::vector &main_tx_ephemeral_pubkeys_out, - std::vector &additional_tx_ephemeral_pubkeys_out, - cryptonote::blobdata &tx_extra_nonce_out) +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) { - return parse_tx_extra_for_scanning(tx_prefix.extra, - tx_prefix.vout.size(), - main_tx_ephemeral_pubkeys_out, - additional_tx_ephemeral_pubkeys_out, - tx_extra_nonce_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"); + 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, + .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.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; + + 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; + + return res; } //------------------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------------------- static void perform_ecdh_derivations(const epee::span main_tx_ephemeral_pubkeys, const epee::span additional_tx_ephemeral_pubkeys, - const cryptonote::account_keys &acc, + const crypto::secret_key &k_view_incoming, + hw::device &hwdev, const bool is_carrot, std::vector &main_derivations_out, std::vector &additional_derivations_out) @@ -116,8 +379,8 @@ static void perform_ecdh_derivations(const epee::span if (is_carrot) { //! @TODO: HW device - carrot::view_incoming_key_ram_borrowed_device k_view_dev(acc.m_view_secret_key); - + 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; @@ -140,410 +403,284 @@ static void perform_ecdh_derivations(const epee::span { for (const crypto::public_key &main_tx_ephemeral_pubkey : main_tx_ephemeral_pubkeys) { - acc.get_device().generate_key_derivation(main_tx_ephemeral_pubkey, - acc.m_view_secret_key, + 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) { - acc.get_device().generate_key_derivation(additional_tx_ephemeral_pubkey, - acc.m_view_secret_key, + hwdev.generate_key_derivation(additional_tx_ephemeral_pubkey, + k_view_incoming, tools::add_element(additional_derivations_out)); } } } //------------------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------------------- -std::optional try_view_incoming_scan_enote_destination( - const cryptonote::tx_out &enote_destination, - const carrot::lazy_amount_commitment_t &amount_commitment, - const epee::span main_tx_ephemeral_pubkeys, - const epee::span additional_tx_ephemeral_pubkeys, - const cryptonote::blobdata &tx_extra_nonce, - const cryptonote::txin_v &first_tx_input, +bool try_load_pre_carrot_enote_from_transaction_prefix(const cryptonote::transaction_prefix &tx_prefix, const std::size_t local_output_index, - const epee::span main_derivations, - const std::vector &additional_derivations, - const cryptonote::account_keys &acc, - const std::unordered_map &subaddress_map) + const rct::xmr_amount amount, + const rct::key &amount_blinding_factor, + PreCarrotEnote &enote_out) { - // 1. check that: - // a) has main or additional ephemeral pubkey, and - // b) main derivations match with main ephemeral pubkeys, and - // c) additional derivations match with additional pubkeys, and - // d) output_index isn't out of range for a non-empty additional ephemeral pubkeys, and - // e) main ephemeral pubkeys count is not greater than two, and - // f) we can extract an output public key from the enote destination - enote_view_incoming_scan_info_t res{}; - if (main_tx_ephemeral_pubkeys.empty() && additional_tx_ephemeral_pubkeys.empty()) - return std::nullopt; - else if (main_derivations.size() != main_tx_ephemeral_pubkeys.size()) - return std::nullopt; - else if (additional_derivations.size() != additional_tx_ephemeral_pubkeys.size()) - return std::nullopt; - else if (!additional_derivations.empty() && local_output_index >= additional_derivations.size()) - return std::nullopt; - else if (main_derivations.size() > 2) - return std::nullopt; - else if (!cryptonote::get_output_public_key(enote_destination, res.onetime_address)) - return std::nullopt; + 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); - // 2. setup default values - res.amount = enote_destination.amount; - res.amount_blinding_factor = rct::I; - res.local_output_index = local_output_index; + enote_out.local_output_index = local_output_index; - boost::optional subaddr_receive_info; - - // 3. copy long plaintext payment ID, if applicable - if (!tx_extra_nonce.empty() && !cryptonote::get_payment_id_from_tx_extra_nonce(tx_extra_nonce, res.payment_id)) - res.payment_id = crypto::hash{}; - - // 4. view-incoming scan - const bool is_carrot = enote_destination.target.type() == typeid(cryptonote::txout_to_carrot_v1); - const bool is_coinbase = first_tx_input.type() == typeid(cryptonote::txin_gen); - if (is_carrot) - { - const crypto::public_key &enote_ephemeral_pubkey_pk = main_tx_ephemeral_pubkeys.empty() - ? additional_tx_ephemeral_pubkeys[local_output_index] : main_tx_ephemeral_pubkeys[0]; - const auto enote_ephemeral_pubkey = carrot::raw_byte_convert(enote_ephemeral_pubkey_pk); - - const auto &txout_carrot = boost::get(enote_destination.target); - - //! @TODO: HW device - const carrot::view_incoming_key_ram_borrowed_device k_view_dev(acc.m_view_secret_key); - mx25519_pubkey s_sender_receiver_unctx; - if (!k_view_dev.view_key_scalar_mult_x25519(enote_ephemeral_pubkey, s_sender_receiver_unctx)) - return std::nullopt; - - if (is_coinbase) - { - const carrot::CarrotCoinbaseEnoteV1 enote{ - .onetime_address = txout_carrot.key, - .amount = enote_destination.amount, - .anchor_enc = txout_carrot.encrypted_janus_anchor, - .view_tag = txout_carrot.view_tag, - .enote_ephemeral_pubkey = enote_ephemeral_pubkey, - .block_index = boost::get(first_tx_input).height - }; - - if (!carrot::try_scan_carrot_coinbase_enote(enote, - s_sender_receiver_unctx, - k_view_dev, - acc.m_account_address.m_spend_public_key, - res.sender_extension_g, - res.sender_extension_t)) - return std::nullopt; - - res.address_spend_pubkey = acc.m_account_address.m_spend_public_key; - } - else // !is_coinbase - { - const carrot::CarrotEnoteV1 enote{ - .onetime_address = txout_carrot.key, - .amount_commitment = carrot::calculate_amount_commitment(amount_commitment), - //.anchor_enc = ... doesn't matter for try_scan_carrot_enote_external_destination_only() ... - .anchor_enc = txout_carrot.encrypted_janus_anchor, - .view_tag = txout_carrot.view_tag, - .enote_ephemeral_pubkey = enote_ephemeral_pubkey, - .tx_first_key_image = boost::get(first_tx_input).k_image - }; - - // convert pid_enc - std::optional encrypted_carrot_payment_id; - if (!tx_extra_nonce.empty()) - { - crypto::hash8 pid_enc_8; - if (cryptonote::get_encrypted_payment_id_from_tx_extra_nonce(tx_extra_nonce, pid_enc_8)) - encrypted_carrot_payment_id = carrot::raw_byte_convert(pid_enc_8); - } - - // try Carrot view-scan w/o amount commitment recomputation - carrot::payment_id_t carrot_payment_id; - if (!carrot::try_scan_carrot_enote_external_destination_only(enote, - encrypted_carrot_payment_id, - s_sender_receiver_unctx, - k_view_dev, - acc.m_account_address.m_spend_public_key, - res.sender_extension_g, - res.sender_extension_t, - res.address_spend_pubkey, - carrot_payment_id)) - return std::nullopt; - - memcpy(&res.payment_id, &carrot_payment_id, sizeof(carrot_payment_id)); - } - - // find K^j_s in subaddress map - const auto subaddr_it = subaddress_map.find(res.address_spend_pubkey); - if (subaddr_it == subaddress_map.cend()) - return std::nullopt; - - carrot::input_context_t input_context; - if (is_coinbase) - { - // input_context = "C" || IntToBytes256(block_index) - carrot::make_carrot_input_context_coinbase(boost::get(first_tx_input).height, - input_context); - } - else - { - // input_context = "R" || KI_1 - carrot::make_carrot_input_context(boost::get(first_tx_input).k_image, - input_context); - } - - //! TODO: return s^ctx_sr from scan function - // s^ctx_sr = H_32(s_sr, D_e, input_context) - crypto::hash s_sender_receiver; - carrot::make_carrot_sender_receiver_secret(s_sender_receiver_unctx.data, - enote_ephemeral_pubkey, - input_context, - s_sender_receiver); - - // j - subaddr_receive_info = cryptonote::subaddress_receive_info{ - .index = subaddr_it->second, - .derivation = carrot::raw_byte_convert(s_sender_receiver) - }; - - // we don't have the extra main tx pubkey bug in Carrot - res.main_tx_pubkey_index = 0; - } - else // !is_carrot - { - const boost::optional view_tag = cryptonote::get_output_view_tag(enote_destination); - for (size_t i = 0; i < std::max(main_derivations.size(), 1); ++i) - { - // try view-scan, testing view tag if applicable - subaddr_receive_info = cryptonote::is_out_to_acc_precomp(subaddress_map, - res.onetime_address, - (i < main_derivations.size()) ? main_derivations[i] : crypto::key_derivation{}, - (i == 0) ? additional_derivations : std::vector{}, - local_output_index, - acc.get_device(), - view_tag); - - if (!subaddr_receive_info) - continue; - - // derive k^g_o - if (!acc.get_device().derivation_to_scalar(subaddr_receive_info->derivation, - local_output_index, - res.sender_extension_g)) - continue; - - // k^t_o = 0 - res.sender_extension_t = crypto::null_skey; - - // K^j_s' - if (!acc.get_device().derive_subaddress_public_key(res.onetime_address, - subaddr_receive_info->derivation, - local_output_index, - res.address_spend_pubkey)) - continue; - - res.main_tx_pubkey_index = i; - } - - // decrypt pid - const bool should_try_decrypt_pid = !tx_extra_nonce.empty() - && res.main_tx_pubkey_index < main_tx_ephemeral_pubkeys.size(); - if (should_try_decrypt_pid) - { - crypto::hash8 pid_8; - if (cryptonote::get_encrypted_payment_id_from_tx_extra_nonce(tx_extra_nonce, pid_8)) - { - if (acc.get_device().decrypt_payment_id(pid_8, - main_tx_ephemeral_pubkeys[res.main_tx_pubkey_index], - acc.m_view_secret_key)) - memcpy(&res.payment_id, &pid_8, sizeof(pid_8)); - } - } - } - - if (!subaddr_receive_info) - return std::nullopt; - - res.subaddr_index = carrot::subaddress_index_extended{ - .index = { subaddr_receive_info->index.major, subaddr_receive_info->index.minor }, - .derive_type = carrot::AddressDeriveType::Auto //! @TODO: handle hybrid devices - }; - - res.derivation = subaddr_receive_info->derivation; - - return res; -} -//------------------------------------------------------------------------------------------------------------------- -std::optional try_view_incoming_scan_enote_destination( - const cryptonote::transaction_prefix &tx_prefix, - const carrot::lazy_amount_commitment_t &amount_commitment, - const std::size_t local_output_index, - const cryptonote::account_keys &acc, - const std::unordered_map &subaddress_map) -{ - // 1. parse tx extra - std::vector main_tx_ephemeral_pubkeys; - std::vector additional_tx_ephemeral_pubkeys; - cryptonote::blobdata tx_extra_nonce; - parse_tx_extra_for_scanning(tx_prefix, main_tx_ephemeral_pubkeys, additional_tx_ephemeral_pubkeys, tx_extra_nonce); - - // 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, - carrot::is_carrot_transaction_v1(tx_prefix), - main_derivations, - additional_derivations); - - // 3. get first transaction input with a default value - const cryptonote::txin_v tx_first_input = !tx_prefix.vin.empty() - ? tx_prefix.vin.at(0) - : cryptonote::txin_v(cryptonote::txin_to_key{}); - - // 4. view-scan enote destination - return try_view_incoming_scan_enote_destination(tx_prefix.vout.at(local_output_index), - amount_commitment, - epee::to_span(main_tx_ephemeral_pubkeys), - epee::to_span(additional_tx_ephemeral_pubkeys), - tx_extra_nonce, - tx_first_input, - local_output_index, - epee::to_span(main_derivations), - additional_derivations, - acc, - subaddress_map); -} -//------------------------------------------------------------------------------------------------------------------- -bool try_decrypt_enote_amount(const crypto::public_key &onetime_address, - const rct::key &enote_amount_commitment, - const std::uint8_t rct_type, - const rct::ecdhTuple &rct_ecdh_tuple, - const crypto::key_derivation &derivation, - const std::size_t local_output_index, - const crypto::public_key &address_spend_pubkey, - hw::device &hwdev, - rct::xmr_amount &amount_out, - rct::key &amount_blinding_factor_out) -{ - const bool is_carrot = rct_type >= carrot::carrot_v1_rct_type; - if (is_carrot) - { - //! @TODO: put this into format utils - carrot::encrypted_amount_t encrypted_amount; - memcpy(&encrypted_amount, &rct_ecdh_tuple.amount, sizeof(encrypted_amount)); - - const crypto::hash s_sender_receiver = carrot::raw_byte_convert(derivation); - - carrot::CarrotEnoteType dummy_enote_type; - crypto::secret_key amount_blinding_factor_sk; - if (!carrot::try_get_carrot_amount(s_sender_receiver, - encrypted_amount, - onetime_address, - address_spend_pubkey, - enote_amount_commitment, - dummy_enote_type, - amount_out, // a - amount_blinding_factor_sk)) - return false; - // z - amount_blinding_factor_out = rct::sk2rct(amount_blinding_factor_sk); - } - else // !is_carrot - { - crypto::ec_scalar amount_key; - if (!hwdev.derivation_to_scalar(derivation, - local_output_index, - amount_key)) - return false; - - const bool is_short_amount = rct_type >= rct::RCTTypeBulletproof2; - rct::ecdhTuple decoded_ecdh_tuple = rct_ecdh_tuple; - if (!hwdev.ecdhDecode(decoded_ecdh_tuple, - carrot::raw_byte_convert(amount_key), - is_short_amount)) - return false; - - // a - amount_out = rct::h2d(decoded_ecdh_tuple.amount); - // z - amount_blinding_factor_out = decoded_ecdh_tuple.mask; - - // C' = z G + a H - const rct::key recomputed_amount_commitment = rct::commit(amount_out, - amount_blinding_factor_out); - - // C' ?= C - if (!rct::equalKeys(enote_amount_commitment, recomputed_amount_commitment)) - return false; - } + 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 try_view_incoming_scan_enote( - const cryptonote::tx_out &enote_destination, - const rct::rctSigBase &rct_sig, +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 enote.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}); +} +//------------------------------------------------------------------------------------------------------------------- +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 cryptonote::txin_v &first_tx_input, - const std::size_t local_output_index, const epee::span main_derivations, - const std::vector &additional_derivations, - const cryptonote::account_keys &acc, - const std::unordered_map &subaddress_map) + 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) { - const bool is_rct = rct_sig.type != rct::RCTTypeNull; + MoneroEnoteVariant enote; + const bool is_coinbase = cryptonote::is_coinbase(tx); + const bool is_carrot = carrot::is_carrot_transaction_v1(tx); - carrot::lazy_amount_commitment_t amount_commitment; - if (is_rct) amount_commitment = rct_sig.outPk.at(local_output_index).mask; - else amount_commitment = enote_destination.amount; + if (is_carrot) + { + const epee::span enote_ephemeral_pubkeys_pk = main_tx_ephemeral_pubkeys.empty() + ? additional_tx_ephemeral_pubkeys + : main_tx_ephemeral_pubkeys; - auto res = try_view_incoming_scan_enote_destination( - enote_destination, - amount_commitment, - main_tx_ephemeral_pubkeys, - additional_tx_ephemeral_pubkeys, - tx_extra_nonce, - first_tx_input, + //! @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, - acc, - subaddress_map); + 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"); - if (!res) - return res; + // 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"); - // a, z - if (is_rct) + // 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)) { - const bool decrypted_amount = try_decrypt_enote_amount( - res->onetime_address, - rct_sig.outPk.at(local_output_index).mask, - rct_sig.type, - rct_sig.ecdhInfo.at(local_output_index), - res->derivation, - res->local_output_index, - res->address_spend_pubkey, - acc.get_device(), - res->amount, - res->amount_blinding_factor); + MWARNING("view_incoming_scan_enote_from_prefix: tx extra has unsupported format"); + } - if (!decrypted_amount) - res.reset(); - } - else // !is_rct - { - res->amount = enote_destination.amount; - res->amount_blinding_factor = rct::I; - } - - return res; + // 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( @@ -552,7 +689,7 @@ void view_incoming_scan_transaction( const epee::span additional_tx_ephemeral_pubkeys, const cryptonote::blobdata &tx_extra_nonce, const epee::span main_derivations, - const std::vector &additional_derivations, + const epee::span additional_derivations, const cryptonote::account_keys &acc, const std::unordered_map &subaddress_map, const epee::span> enote_scan_infos_out) @@ -562,36 +699,29 @@ void view_incoming_scan_transaction( 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]); - - const cryptonote::tx_out &enote_destination = tx.vout.at(local_output_index); - - // get first transaction input with a default value - const cryptonote::txin_v tx_first_input = !tx.vin.empty() - ? tx.vin.at(0) - : cryptonote::txin_v(cryptonote::txin_to_key{}); - - enote_scan_info = try_view_incoming_scan_enote(enote_destination, - tx.rct_signatures, + enote_scan_info = view_incoming_scan_enote(tx, + local_output_index, main_tx_ephemeral_pubkeys, additional_tx_ephemeral_pubkeys, tx_extra_nonce, - tx_first_input, - local_output_index, main_derivations, additional_derivations, - acc, - subaddress_map); + acc.m_account_address, + &k_view_dev, + subaddress_map, + acc.get_device()); } } //------------------------------------------------------------------------------------------------------------------- void view_incoming_scan_transaction( const cryptonote::transaction &tx, - const epee::span custom_main_derivations, - const std::vector &custom_additional_derivations, const cryptonote::account_keys &acc, const std::unordered_map &subaddress_map, const epee::span> enote_scan_infos_out) @@ -600,40 +730,23 @@ void view_incoming_scan_transaction( std::vector main_tx_ephemeral_pubkeys; std::vector additional_tx_ephemeral_pubkeys; cryptonote::blobdata tx_extra_nonce; - if (!parse_tx_extra_for_scanning(tx, main_tx_ephemeral_pubkeys, additional_tx_ephemeral_pubkeys, 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)); - // 2. 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, - custom_main_derivations, - custom_additional_derivations, - acc, - subaddress_map, - enote_scan_infos_out); -} -//------------------------------------------------------------------------------------------------------------------- -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, 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, + acc.m_view_secret_key, + acc.get_device(), carrot::is_carrot_transaction_v1(tx), main_derivations, additional_derivations); @@ -644,7 +757,7 @@ void view_incoming_scan_transaction( epee::to_span(additional_tx_ephemeral_pubkeys), tx_extra_nonce, epee::to_span(main_derivations), - additional_derivations, + epee::to_span(additional_derivations), acc, subaddress_map, enote_scan_infos_out); @@ -660,6 +773,49 @@ std::vector> view_incoming_scan_t 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); @@ -676,55 +832,40 @@ std::optional try_derive_enote_key_image( if (!enote_scan_info.subaddr_index) return std::nullopt; - const cryptonote::subaddress_index subaddr_index_cn{ - enote_scan_info.subaddr_index->index.major, - enote_scan_info.subaddr_index->index.minor - }; - - const bool is_carrot = enote_scan_info.sender_extension_t != crypto::null_skey; - if (is_carrot) + // k^j_subext + rct::key subaddress_extension; + if (enote_scan_info.subaddr_index->index.is_subaddress()) { - //! @TODO: compact helper function like generate_key_image_helper_precomp() - //! @TODO: verify x G + y T = O to prevent downstream debugging issues - - // I = Hp(O) - crypto::ec_point ki_generator; - crypto::derive_key_image_generator(enote_scan_info.onetime_address, ki_generator); - - //! @TODO: HW devices - - rct::key subaddress_extension; - if (subaddr_index_cn.is_zero()) - subaddress_extension = rct::Z; - else // !subaddr_index_cn.is_zero() - subaddress_extension = rct::sk2rct( - acc.get_device().get_subaddress_secret_key(acc.m_view_secret_key, subaddr_index_cn)); - - // x = k_s + k^j_subext + k_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_o) Hp(O) - return rct::rct2ki(rct::scalarmultKey(rct::pt2rct(ki_generator), x)); + 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 // !is_carrot + else // !subaddr_index_cn.is_zero() { - crypto::key_image ki; - cryptonote::keypair ota_kp; - if (!cryptonote::generate_key_image_helper_precomp(acc, - enote_scan_info.onetime_address, - enote_scan_info.derivation, - enote_scan_info.local_output_index, - subaddr_index_cn, - ota_kp, - ki, - acc.get_device())) - return std::nullopt; - return ki; + 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 diff --git a/src/wallet/scanning_tools.h b/src/wallet/scanning_tools.h index 894ffbb66..fda198387 100644 --- a/src/wallet/scanning_tools.h +++ b/src/wallet/scanning_tools.h @@ -29,16 +29,20 @@ #pragma once //local headers -#include "carrot_core/lazy_amount_commitment.h" +#include "carrot_core/carrot_enote_types.h" +#include "carrot_core/device.h" #include "carrot_impl/subaddress_index.h" +#include "crypto/crypto.h" #include "cryptonote_basic/account.h" #include "cryptonote_basic/blobdatatype.h" #include "cryptonote_basic/subaddress_index.h" #include "cryptonote_basic/tx_extra.h" +#include "common/variant.h" //third party headers //standard headers +#include #include #include @@ -50,8 +54,6 @@ namespace wallet { struct enote_view_incoming_scan_info_t { - // K_o - crypto::public_key onetime_address; // k^g_o crypto::secret_key sender_extension_g; // k^t_o @@ -70,71 +72,44 @@ struct enote_view_incoming_scan_info_t // asset type std::string asset_type; - // legacy: 8 k_v R, carrot: s^ctx_sr - crypto::key_derivation derivation; - // i - std::size_t local_output_index; - // + // the cold signing code used to have a bug which added multiple main tx pubkeys to extra std::size_t main_tx_pubkey_index; }; -std::optional try_view_incoming_scan_enote_destination( - const cryptonote::tx_out &enote_destination, - const carrot::lazy_amount_commitment_t &lazy_amount_commitment, - const epee::span main_tx_ephemeral_pubkeys, - const epee::span additional_tx_ephemeral_pubkeys, - const cryptonote::blobdata &tx_extra_nonce, - const cryptonote::txin_v &first_tx_input, - const std::size_t local_output_index, - const epee::span main_derivations, - const std::vector &additional_derivations, - const cryptonote::account_keys &acc, - const std::unordered_map &subaddress_map); -std::optional try_view_incoming_scan_enote_destination( +struct PreCarrotEnote +{ + crypto::public_key onetime_address; + boost::optional view_tag; + + std::size_t local_output_index; + + bool encrypted_amount; + bool short_amount; + rct::key amount_commitment; + rct::ecdhTuple amount; +}; + +using MoneroEnoteVariant = tools::variant; + +std::optional view_incoming_scan_enote_from_prefix( const cryptonote::transaction_prefix &tx_prefix, - const carrot::lazy_amount_commitment_t &lazy_amount_commitment, + const rct::xmr_amount amount, + const rct::key &amount_blinding_factor, const std::size_t local_output_index, - const cryptonote::account_keys &acc, - const std::unordered_map &subaddress_map); - -bool try_decrypt_enote_amount(const crypto::public_key &onetime_address, - const rct::key &enote_amount_commitment, - const std::uint8_t rct_type, - const rct::ecdhTuple &rct_ecdh_tuple, - const crypto::key_derivation &derivation, - const std::size_t local_output_index, - const crypto::public_key &address_spend_pubkey, - hw::device &hwdev, - rct::xmr_amount &amount_out, - rct::key &amount_blinding_factor_out); - -std::optional try_view_incoming_scan_enote( - const cryptonote::tx_out &enote_destination, - const rct::rctSigBase &rct_sig, - const epee::span main_tx_ephemeral_pubkeys, - const epee::span additional_tx_ephemeral_pubkeys, - const cryptonote::blobdata &tx_extra_nonce, - const cryptonote::txin_v &first_tx_input, - const std::size_t local_output_index, - const epee::span main_derivations, - const std::vector &additional_derivations, - const cryptonote::account_keys &acc, - const std::unordered_map &subaddress_map); - -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 std::vector &additional_derivations, - const cryptonote::account_keys &acc, + const cryptonote::account_public_address &address, + const crypto::secret_key &k_view_incoming, const std::unordered_map &subaddress_map, - const epee::span> enote_scan_infos_out); + hw::device &hwdev); + void view_incoming_scan_transaction( const cryptonote::transaction &tx, - const epee::span custom_main_derivations, - const std::vector &custom_additional_derivations, + 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); @@ -148,6 +123,12 @@ std::vector> view_incoming_scan_t const cryptonote::account_keys &acc, const std::unordered_map &subaddress_map); +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); + bool is_long_payment_id(const crypto::hash &pid); std::optional try_derive_enote_key_image( diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp index 8dfde09cc..b4ea6ce28 100644 --- a/src/wallet/wallet2.cpp +++ b/src/wallet/wallet2.cpp @@ -95,6 +95,7 @@ using namespace epee; #include "device/device_cold.hpp" #include "device_trezor/device_trezor.hpp" #include "net/socks_connect.h" +#include "carrot_impl/carrot_tx_format_utils.h" extern "C" { @@ -1979,6 +1980,19 @@ void wallet2::set_spent(size_t idx, uint64_t height) td.m_spent_height = height; } //---------------------------------------------------------------------------------------------------- +void wallet2::set_spent(const crypto::key_image &ki, const uint64_t height) +{ + // NOTE: There may be multiple transfers which share the same key image!!! However, because we + // mark inferior older outputs as spent when a new, better one comes along inside of + // process_new_scanned_transaction(), then the only transfer which shouldn't be marked as + // spent is the one pointed to in m_key_images. So we should be safe from not marking + // transfers as spent when they should be. + + const auto ki_it = m_key_images.find(ki); + CHECK_AND_ASSERT_THROW_MES(ki_it != m_key_images.cend(), "Key image not found in transfers list"); + set_spent(ki_it->second, height); +} +//---------------------------------------------------------------------------------------------------- void wallet2::set_unspent(size_t idx) { CHECK_AND_ASSERT_THROW_MES(idx < m_transfers.size(), "Invalid index"); @@ -2511,10 +2525,18 @@ void wallet2::process_new_scanned_transaction( received_an_output = true; + crypto::public_key onetime_address; + if (!cryptonote::get_output_public_key(tx.vout.at(local_output_index), onetime_address)) + { + MERROR("Cannot get onetime address for view-scanned enote???"); + continue; + } + // check for burning bug - const auto ot_it = m_pub_keys.find(enote_scan_info->onetime_address); + const auto ot_it = m_pub_keys.find(onetime_address); const transfer_details *burning_td = ot_it != m_pub_keys.cend() ? &m_transfers.at(ot_it->second) : nullptr; - const bool should_treat_as_burned = burning_td && burning_td->amount() >= enote_scan_info->amount; + const bool should_treat_as_burned = burning_td && + (burning_td->amount() >= enote_scan_info->amount || burning_td->m_spent); if (should_treat_as_burned) { // We already have an older received transfer with a greater-than-or-equal amount @@ -2586,7 +2608,7 @@ void wallet2::process_new_scanned_transaction( if (!td.m_key_image_known) { // we might have cold signed, and have a mapping to key images - const auto i = m_cold_key_images.find(enote_scan_info->onetime_address); + const auto i = m_cold_key_images.find(onetime_address); if (i != m_cold_key_images.end()) { td.m_key_image = i->second; @@ -2599,7 +2621,7 @@ void wallet2::process_new_scanned_transaction( // update m_key_images, m_pub_keys, and output_tracker_cache if (td.m_key_image_known) m_key_images[td.m_key_image] = m_transfers.size() - 1; - m_pub_keys[enote_scan_info->onetime_address] = m_transfers.size() - 1; + m_pub_keys[onetime_address] = m_transfers.size() - 1; output_tracker_cache[std::make_pair(tx.vout.at(td.m_internal_output_index).amount, td.m_global_output_index)] = m_transfers.size() - 1; @@ -12410,13 +12432,10 @@ void wallet2::check_tx_key_helper(const cryptonote::transaction &tx, const crypt { received = 0; - std::vector> enote_scan_infos(tx.vout.size()); - wallet::view_incoming_scan_transaction(tx, + const auto enote_scan_infos = wallet::view_incoming_scan_transaction_as_sender(tx, {&derivation, 1}, - additional_derivations, - m_account.get_keys(), - {{address.m_spend_public_key, {}}}, // use a fake subaddress map with just the provided address in it - epee::to_mut_span(enote_scan_infos)); + epee::to_span(additional_derivations), + address); for (const auto &enote_scan_info : enote_scan_infos) if (enote_scan_info && enote_scan_info->address_spend_pubkey == address.m_spend_public_key) @@ -13413,12 +13432,19 @@ bool wallet2::verify_with_public_key(const std::string &data, const crypto::publ //---------------------------------------------------------------------------------------------------- crypto::public_key wallet2::get_tx_pub_key_from_received_outs(const tools::wallet2::transfer_details &td) const { - const auto enote_scan_info = wallet::try_view_incoming_scan_enote_destination( + THROW_WALLET_EXCEPTION_IF(carrot::is_carrot_transaction_v1(td.m_tx), + error::wallet_internal_error, + "get_tx_pub_key_from_received_outs is not relevant for Carrot txs"); + + const auto enote_scan_info = wallet::view_incoming_scan_enote_from_prefix( td.m_tx, - rct::commit(td.amount(), td.m_mask), + td.amount(), + td.m_mask, td.m_internal_output_index, - m_account.get_keys(), - m_subaddresses); + m_account_public_address, + m_account.get_keys().m_view_secret_key, + m_subaddresses, + m_account.get_device()); const size_t main_tx_pubkey_index = enote_scan_info ? enote_scan_info->main_tx_pubkey_index : 0; diff --git a/src/wallet/wallet2.h b/src/wallet/wallet2.h index 90094dd2c..2933d441c 100644 --- a/src/wallet/wallet2.h +++ b/src/wallet/wallet2.h @@ -1951,6 +1951,7 @@ private: float get_output_relatedness(const transfer_details &td0, const transfer_details &td1) const; std::vector pick_preferred_rct_inputs(uint64_t needed_money, uint32_t subaddr_account, const std::set &subaddr_indices, const std::string& asset_type); void set_spent(size_t idx, uint64_t height); + void set_spent(const crypto::key_image &ki, const uint64_t height); void set_unspent(size_t idx); bool is_spent(const transfer_details &td, bool strict = true) const; bool is_spent(size_t idx, bool strict = true) const; diff --git a/tests/unit_tests/wallet_scanning.cpp b/tests/unit_tests/wallet_scanning.cpp index 60d94ad19..2ca53db40 100644 --- a/tests/unit_tests/wallet_scanning.cpp +++ b/tests/unit_tests/wallet_scanning.cpp @@ -88,13 +88,10 @@ TEST(wallet_scanning, view_scan_as_sender_mainaddr) // call view_incoming_scan_transaction with no meaningful key nor subaddresses maps, // just with the proper ECDH - std::vector> enote_scan_infos(tx.vout.size()); - tools::wallet::view_incoming_scan_transaction(tx, + const auto enote_scan_infos = tools::wallet::view_incoming_scan_transaction_as_sender(tx, {&main_derivation, 1}, {}, - aether.get_keys(), - {{bob_main_spend_pubkey, {}}}, // use a fake subaddress map with just the provided address in it - epee::to_mut_span(enote_scan_infos)); + bob_main_addr); bool matched = false; for (const auto &enote_scan_info : enote_scan_infos) @@ -203,6 +200,7 @@ TEST(wallet_scanning, view_scan_short_payment_id) memcpy(&payment_id, &pid_8, sizeof(pid_8)); ASSERT_FALSE(tools::wallet::is_long_payment_id(payment_id)); + ASSERT_NE(crypto::null_hash, payment_id); for (uint8_t hf_version = 1; hf_version < HF_VERSION_FCMP_PLUS_PLUS; ++hf_version) {