883 lines
37 KiB
C++
883 lines
37 KiB
C++
// 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<std::uint8_t> &tx_extra,
|
|
const std::size_t n_outputs,
|
|
std::vector<crypto::public_key> &main_tx_ephemeral_pubkeys_out,
|
|
std::vector<crypto::public_key> &additional_tx_ephemeral_pubkeys_out,
|
|
cryptonote::blobdata &tx_extra_nonce_out)
|
|
{
|
|
// 1. parse extra fields
|
|
std::vector<cryptonote::tx_extra_field> 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<enote_view_incoming_scan_info_t> view_incoming_scan_pre_carrot_enote(
|
|
const PreCarrotEnote &enote,
|
|
const std::optional<carrot::encrypted_payment_id_t> &encrypted_payment_id,
|
|
const epee::span<const crypto::key_derivation> main_derivations,
|
|
const epee::span<const crypto::key_derivation> additional_derivations,
|
|
const std::unordered_map<crypto::public_key, cryptonote::subaddress_index> &subaddress_map,
|
|
hw::device &hwdev)
|
|
{
|
|
boost::optional<cryptonote::subaddress_receive_info> receive_info;
|
|
size_t main_deriv_idx;
|
|
for (main_deriv_idx = 0; main_deriv_idx < std::max<size_t>(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<const crypto::key_derivation>{} : 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<rct::key>(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<crypto::public_key>(receive_info->derivation);
|
|
crypto::hash8 pid_hash8 = carrot::raw_byte_convert<crypto::hash8>(*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<enote_view_incoming_scan_info_t> 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<enote_view_incoming_scan_info_t> view_incoming_scan_carrot_enote_sender(
|
|
const carrot::CarrotEnoteV1 &enote,
|
|
const std::optional<carrot::encrypted_payment_id_t> &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<enote_view_incoming_scan_info_t> view_incoming_scan_carrot_enote_receiver(
|
|
const carrot::CarrotEnoteV1 &enote,
|
|
const std::optional<carrot::encrypted_payment_id_t> &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<crypto::public_key, cryptonote::subaddress_index> &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<const crypto::public_key> main_tx_ephemeral_pubkeys,
|
|
const epee::span<const crypto::public_key> additional_tx_ephemeral_pubkeys,
|
|
const crypto::secret_key &k_view_incoming,
|
|
hw::device &hwdev,
|
|
const bool is_carrot,
|
|
std::vector<crypto::key_derivation> &main_derivations_out,
|
|
std::vector<crypto::key_derivation> &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<mx25519_pubkey>(main_tx_ephemeral_pubkey),
|
|
s_sender_receiver_unctx);
|
|
main_derivations_out.push_back(carrot::raw_byte_convert<crypto::key_derivation>(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<mx25519_pubkey>(additional_tx_ephemeral_pubkey),
|
|
s_sender_receiver_unctx);
|
|
additional_derivations_out.push_back(carrot::raw_byte_convert<crypto::key_derivation>(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<enote_view_incoming_scan_info_t> view_incoming_scan_enote(
|
|
const MoneroEnoteVariant &enote,
|
|
const std::size_t local_output_index,
|
|
const cryptonote::blobdata &tx_extra_nonce,
|
|
const epee::span<const crypto::key_derivation> main_derivations,
|
|
const epee::span<const crypto::key_derivation> additional_derivations,
|
|
const cryptonote::account_public_address &address,
|
|
const carrot::view_incoming_key_device *k_view_dev,
|
|
const std::unordered_map<crypto::public_key, cryptonote::subaddress_index> &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<carrot::encrypted_payment_id_t> 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<carrot::encrypted_payment_id_t>(pid_hash8);
|
|
}
|
|
|
|
struct view_incoming_scan_enote_visitor
|
|
{
|
|
std::optional<enote_view_incoming_scan_info_t> 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<enote_view_incoming_scan_info_t> 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<mx25519_pubkey>(kd);
|
|
|
|
return view_incoming_scan_carrot_coinbase_enote(enote,
|
|
s_sender_receiver_unctx,
|
|
address.m_spend_public_key);
|
|
}
|
|
|
|
std::optional<enote_view_incoming_scan_info_t> 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<mx25519_pubkey>(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<carrot::encrypted_payment_id_t> encrypted_payment_id;
|
|
const epee::span<const crypto::key_derivation> main_derivations;
|
|
const epee::span<const crypto::key_derivation> additional_derivations;
|
|
const cryptonote::account_public_address &address;
|
|
const carrot::view_incoming_key_device *k_view_dev;
|
|
const std::unordered_map<crypto::public_key, cryptonote::subaddress_index> &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<enote_view_incoming_scan_info_t> view_incoming_scan_enote(
|
|
const cryptonote::transaction &tx,
|
|
const std::size_t local_output_index,
|
|
const epee::span<const crypto::public_key> main_tx_ephemeral_pubkeys,
|
|
const epee::span<const crypto::public_key> additional_tx_ephemeral_pubkeys,
|
|
const cryptonote::blobdata &tx_extra_nonce,
|
|
const epee::span<const crypto::key_derivation> main_derivations,
|
|
const epee::span<const crypto::key_derivation> additional_derivations,
|
|
const cryptonote::account_public_address &address,
|
|
const carrot::view_incoming_key_device *k_view_dev,
|
|
const std::unordered_map<crypto::public_key, cryptonote::subaddress_index> &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<const crypto::public_key> 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<const mx25519_pubkey> enote_ephemeral_pubkeys = {
|
|
reinterpret_cast<const mx25519_pubkey*>(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<enote_view_incoming_scan_info_t> 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<crypto::public_key, cryptonote::subaddress_index> &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<crypto::public_key> main_tx_ephemeral_pubkeys;
|
|
std::vector<crypto::public_key> 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<crypto::key_derivation> main_derivations;
|
|
std::vector<crypto::key_derivation> 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<const crypto::public_key> main_tx_ephemeral_pubkeys,
|
|
const epee::span<const crypto::public_key> additional_tx_ephemeral_pubkeys,
|
|
const cryptonote::blobdata &tx_extra_nonce,
|
|
const epee::span<const crypto::key_derivation> main_derivations,
|
|
const epee::span<const crypto::key_derivation> additional_derivations,
|
|
const cryptonote::account_keys &acc,
|
|
const std::unordered_map<crypto::public_key, cryptonote::subaddress_index> &subaddress_map,
|
|
const epee::span<std::optional<enote_view_incoming_scan_info_t>> 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<std::optional<enote_view_incoming_scan_info_t>&>(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<crypto::public_key, cryptonote::subaddress_index> &subaddress_map,
|
|
const epee::span<std::optional<enote_view_incoming_scan_info_t>> enote_scan_infos_out)
|
|
{
|
|
// 1. parse tx extra
|
|
std::vector<crypto::public_key> main_tx_ephemeral_pubkeys;
|
|
std::vector<crypto::public_key> 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<crypto::key_derivation> main_derivations;
|
|
std::vector<crypto::key_derivation> 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<std::optional<enote_view_incoming_scan_info_t>> view_incoming_scan_transaction(
|
|
const cryptonote::transaction &tx,
|
|
const cryptonote::account_keys &acc,
|
|
const std::unordered_map<crypto::public_key, cryptonote::subaddress_index> &subaddress_map)
|
|
{
|
|
std::vector<std::optional<enote_view_incoming_scan_info_t>> res(tx.vout.size());
|
|
view_incoming_scan_transaction(tx, acc, subaddress_map, epee::to_mut_span(res));
|
|
return res;
|
|
}
|
|
//-------------------------------------------------------------------------------------------------------------------
|
|
std::vector<std::optional<enote_view_incoming_scan_info_t>> view_incoming_scan_transaction_as_sender(
|
|
const cryptonote::transaction &tx,
|
|
const epee::span<const crypto::key_derivation> custom_main_derivations,
|
|
const epee::span<const crypto::key_derivation> custom_additional_derivations,
|
|
const cryptonote::account_public_address &address)
|
|
{
|
|
// 1. Resize output
|
|
const size_t n_outputs = tx.vout.size();
|
|
std::vector<std::optional<enote_view_incoming_scan_info_t>> res(n_outputs);
|
|
|
|
// 2. parse tx extra
|
|
std::vector<crypto::public_key> main_tx_ephemeral_pubkeys;
|
|
std::vector<crypto::public_key> 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<crypto::key_image> 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
|