mirror of
https://github.com/monero-project/monero.git
synced 2025-04-17 11:42:00 +00:00
carrot_impl 3/27/25 [WIP]
This commit is contained in:
parent
e85b76246f
commit
40b04ef372
45 changed files with 8560 additions and 1087 deletions
src
CMakeLists.txt
carrot_core
carrot_impl
CMakeLists.txtaddress_device.haddress_device_ram_borrowed.cppaddress_device_ram_borrowed.haddress_utils_compat.cppaddress_utils_compat.hcarrot_boost_serialization.hcarrot_chain_serialization.hcarrot_tx_builder_types.hcarrot_tx_builder_utils.cppcarrot_tx_builder_utils.hcarrot_tx_format_utils.cppcarrot_tx_format_utils.hinput_selection.cppinput_selection.h
crypto
cryptonote_basic
cryptonote_basic.hcryptonote_boost_serialization.hcryptonote_format_utils.cppcryptonote_format_utils.htx_extra.h
cryptonote_config.hcryptonote_core
ringct
serialization
wallet
tests
|
@ -84,6 +84,7 @@ include(Version)
|
|||
monero_add_library(version SOURCES ${CMAKE_BINARY_DIR}/version.cpp DEPENDS genversion)
|
||||
|
||||
add_subdirectory(carrot_core)
|
||||
add_subdirectory(carrot_impl)
|
||||
add_subdirectory(common)
|
||||
add_subdirectory(crypto)
|
||||
add_subdirectory(ringct)
|
||||
|
|
|
@ -213,33 +213,31 @@ bool make_carrot_uncontextualized_shared_key_receiver(
|
|||
return k_view_dev.view_key_scalar_mult_x25519(enote_ephemeral_pubkey, s_sender_receiver_unctx_out);
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
bool try_ecdh_and_scan_carrot_coinbase_enote(const CarrotCoinbaseEnoteV1 &enote,
|
||||
bool try_scan_carrot_coinbase_enote(
|
||||
const CarrotCoinbaseEnoteV1 &enote,
|
||||
const mx25519_pubkey &s_sender_receiver_unctx,
|
||||
const view_incoming_key_device &k_view_dev,
|
||||
const crypto::public_key &main_address_spend_pubkey,
|
||||
crypto::secret_key &sender_extension_g_out,
|
||||
crypto::secret_key &sender_extension_t_out)
|
||||
{
|
||||
return try_ecdh_and_scan_carrot_coinbase_enote(enote,
|
||||
return try_scan_carrot_coinbase_enote(
|
||||
enote,
|
||||
s_sender_receiver_unctx,
|
||||
k_view_dev,
|
||||
{&main_address_spend_pubkey, 1},
|
||||
sender_extension_g_out,
|
||||
sender_extension_t_out);
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
bool try_ecdh_and_scan_carrot_coinbase_enote(
|
||||
bool try_scan_carrot_coinbase_enote(
|
||||
const CarrotCoinbaseEnoteV1 &enote,
|
||||
const mx25519_pubkey &s_sender_receiver_unctx,
|
||||
const view_incoming_key_device &k_view_dev,
|
||||
const epee::span<const crypto::public_key> main_address_spend_pubkeys,
|
||||
crypto::secret_key &sender_extension_g_out,
|
||||
crypto::secret_key &sender_extension_t_out)
|
||||
{
|
||||
// s_sr = k_v D_e
|
||||
mx25519_pubkey s_sender_receiver_unctx;
|
||||
if (!make_carrot_uncontextualized_shared_key_receiver(k_view_dev,
|
||||
enote.enote_ephemeral_pubkey,
|
||||
s_sender_receiver_unctx))
|
||||
return false;
|
||||
|
||||
// input_context
|
||||
input_context_t input_context;
|
||||
make_carrot_input_context_coinbase(enote.block_index, input_context);
|
||||
|
@ -271,6 +269,41 @@ bool try_ecdh_and_scan_carrot_coinbase_enote(
|
|||
return is_main_address_spend_pubkey(nominal_address_spend_pubkey, main_address_spend_pubkeys);
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
bool try_ecdh_and_scan_carrot_coinbase_enote(const CarrotCoinbaseEnoteV1 &enote,
|
||||
const view_incoming_key_device &k_view_dev,
|
||||
const crypto::public_key &main_address_spend_pubkey,
|
||||
crypto::secret_key &sender_extension_g_out,
|
||||
crypto::secret_key &sender_extension_t_out)
|
||||
{
|
||||
return try_ecdh_and_scan_carrot_coinbase_enote(enote,
|
||||
k_view_dev,
|
||||
{&main_address_spend_pubkey, 1},
|
||||
sender_extension_g_out,
|
||||
sender_extension_t_out);
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
bool try_ecdh_and_scan_carrot_coinbase_enote(
|
||||
const CarrotCoinbaseEnoteV1 &enote,
|
||||
const view_incoming_key_device &k_view_dev,
|
||||
const epee::span<const crypto::public_key> main_address_spend_pubkeys,
|
||||
crypto::secret_key &sender_extension_g_out,
|
||||
crypto::secret_key &sender_extension_t_out)
|
||||
{
|
||||
// s_sr = k_v D_e
|
||||
mx25519_pubkey s_sender_receiver_unctx;
|
||||
if (!make_carrot_uncontextualized_shared_key_receiver(k_view_dev,
|
||||
enote.enote_ephemeral_pubkey,
|
||||
s_sender_receiver_unctx))
|
||||
return false;
|
||||
|
||||
return try_scan_carrot_coinbase_enote(enote,
|
||||
s_sender_receiver_unctx,
|
||||
k_view_dev,
|
||||
main_address_spend_pubkeys,
|
||||
sender_extension_g_out,
|
||||
sender_extension_t_out);
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
bool try_scan_carrot_enote_external(const CarrotEnoteV1 &enote,
|
||||
const std::optional<encrypted_payment_id_t> &encrypted_payment_id,
|
||||
const mx25519_pubkey &s_sender_receiver_unctx,
|
||||
|
@ -345,6 +378,64 @@ bool try_scan_carrot_enote_external(const CarrotEnoteV1 &enote,
|
|||
amount_blinding_factor_out);
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
bool try_ecdh_and_scan_carrot_enote_external(const CarrotEnoteV1 &enote,
|
||||
const std::optional<encrypted_payment_id_t> &encrypted_payment_id,
|
||||
const view_incoming_key_device &k_view_dev,
|
||||
const crypto::public_key &main_address_spend_pubkey,
|
||||
crypto::secret_key &sender_extension_g_out,
|
||||
crypto::secret_key &sender_extension_t_out,
|
||||
crypto::public_key &address_spend_pubkey_out,
|
||||
rct::xmr_amount &amount_out,
|
||||
crypto::secret_key &amount_blinding_factor_out,
|
||||
payment_id_t &payment_id_out,
|
||||
CarrotEnoteType &enote_type_out)
|
||||
{
|
||||
return try_ecdh_and_scan_carrot_enote_external(enote,
|
||||
encrypted_payment_id,
|
||||
k_view_dev,
|
||||
{&main_address_spend_pubkey, 1},
|
||||
sender_extension_g_out,
|
||||
sender_extension_t_out,
|
||||
address_spend_pubkey_out,
|
||||
amount_out,
|
||||
amount_blinding_factor_out,
|
||||
payment_id_out,
|
||||
enote_type_out);
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
bool try_ecdh_and_scan_carrot_enote_external(const CarrotEnoteV1 &enote,
|
||||
const std::optional<encrypted_payment_id_t> &encrypted_payment_id,
|
||||
const view_incoming_key_device &k_view_dev,
|
||||
const epee::span<const crypto::public_key> &main_address_spend_pubkeys,
|
||||
crypto::secret_key &sender_extension_g_out,
|
||||
crypto::secret_key &sender_extension_t_out,
|
||||
crypto::public_key &address_spend_pubkey_out,
|
||||
rct::xmr_amount &amount_out,
|
||||
crypto::secret_key &amount_blinding_factor_out,
|
||||
payment_id_t &payment_id_out,
|
||||
CarrotEnoteType &enote_type_out)
|
||||
{
|
||||
// s_sr = k_v D_e
|
||||
mx25519_pubkey s_sender_receiver_unctx;
|
||||
if (!make_carrot_uncontextualized_shared_key_receiver(k_view_dev,
|
||||
enote.enote_ephemeral_pubkey,
|
||||
s_sender_receiver_unctx))
|
||||
return false;
|
||||
|
||||
return try_scan_carrot_enote_external(enote,
|
||||
encrypted_payment_id,
|
||||
s_sender_receiver_unctx,
|
||||
k_view_dev,
|
||||
main_address_spend_pubkeys,
|
||||
sender_extension_g_out,
|
||||
sender_extension_t_out,
|
||||
address_spend_pubkey_out,
|
||||
amount_out,
|
||||
amount_blinding_factor_out,
|
||||
payment_id_out,
|
||||
enote_type_out);
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
bool try_scan_carrot_enote_internal(const CarrotEnoteV1 &enote,
|
||||
const view_balance_secret_device &s_view_balance_dev,
|
||||
crypto::secret_key &sender_extension_g_out,
|
||||
|
@ -399,38 +490,6 @@ bool try_scan_carrot_enote_internal(const CarrotEnoteV1 &enote,
|
|||
amount_blinding_factor_out);
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
bool try_ecdh_and_scan_carrot_enote_external(const CarrotEnoteV1 &enote,
|
||||
const std::optional<encrypted_payment_id_t> &encrypted_payment_id,
|
||||
const view_incoming_key_device &k_view_dev,
|
||||
const crypto::public_key &main_address_spend_pubkey,
|
||||
crypto::secret_key &sender_extension_g_out,
|
||||
crypto::secret_key &sender_extension_t_out,
|
||||
crypto::public_key &address_spend_pubkey_out,
|
||||
rct::xmr_amount &amount_out,
|
||||
crypto::secret_key &amount_blinding_factor_out,
|
||||
payment_id_t &payment_id_out,
|
||||
CarrotEnoteType &enote_type_out)
|
||||
{
|
||||
// s_sr = k_v D_e
|
||||
mx25519_pubkey s_sender_receiver_unctx;
|
||||
if (!k_view_dev.view_key_scalar_mult_x25519(enote.enote_ephemeral_pubkey,
|
||||
s_sender_receiver_unctx))
|
||||
return false;
|
||||
|
||||
return try_scan_carrot_enote_external(enote,
|
||||
encrypted_payment_id,
|
||||
s_sender_receiver_unctx,
|
||||
k_view_dev,
|
||||
main_address_spend_pubkey,
|
||||
sender_extension_g_out,
|
||||
sender_extension_t_out,
|
||||
address_spend_pubkey_out,
|
||||
amount_out,
|
||||
amount_blinding_factor_out,
|
||||
payment_id_out,
|
||||
enote_type_out);
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
bool try_scan_carrot_enote_external_destination_only(const CarrotEnoteV1 &enote,
|
||||
const std::optional<encrypted_payment_id_t> &encrypted_payment_id,
|
||||
const mx25519_pubkey &s_sender_receiver_unctx,
|
||||
|
|
|
@ -82,7 +82,7 @@ bool make_carrot_uncontextualized_shared_key_receiver(
|
|||
const mx25519_pubkey &enote_ephemeral_pubkey,
|
||||
mx25519_pubkey &s_sender_receiver_unctx_out);
|
||||
/**
|
||||
* brief: try_ecdh_and_scan_carrot_coinbase_enote - derive s_sr and attempt full scan process on coinbase enote
|
||||
* brief: try_scan_carrot_coinbase_enote - attempt full scan process on coinbase enote
|
||||
* param: enote -
|
||||
* param: k_view_dev -
|
||||
* param: main_address_spend_pubkey - K^0_s
|
||||
|
@ -91,6 +91,20 @@ bool make_carrot_uncontextualized_shared_key_receiver(
|
|||
* param: sender_extension_g_out - k^t_o
|
||||
* return: true iff the scan process succeeded
|
||||
*/
|
||||
bool try_scan_carrot_coinbase_enote(
|
||||
const CarrotCoinbaseEnoteV1 &enote,
|
||||
const mx25519_pubkey &s_sender_receiver_unctx,
|
||||
const view_incoming_key_device &k_view_dev,
|
||||
const crypto::public_key &main_address_spend_pubkey,
|
||||
crypto::secret_key &sender_extension_g_out,
|
||||
crypto::secret_key &sender_extension_t_out);
|
||||
bool try_scan_carrot_coinbase_enote(
|
||||
const CarrotCoinbaseEnoteV1 &enote,
|
||||
const mx25519_pubkey &s_sender_receiver_unctx,
|
||||
const view_incoming_key_device &k_view_dev,
|
||||
const epee::span<const crypto::public_key> main_address_spend_pubkeys,
|
||||
crypto::secret_key &sender_extension_g_out,
|
||||
crypto::secret_key &sender_extension_t_out);
|
||||
bool try_ecdh_and_scan_carrot_coinbase_enote(
|
||||
const CarrotCoinbaseEnoteV1 &enote,
|
||||
const view_incoming_key_device &k_view_dev,
|
||||
|
@ -144,6 +158,28 @@ bool try_scan_carrot_enote_external(const CarrotEnoteV1 &enote,
|
|||
crypto::secret_key &amount_blinding_factor_out,
|
||||
payment_id_t &payment_id_out,
|
||||
CarrotEnoteType &enote_type_out);
|
||||
bool try_ecdh_and_scan_carrot_enote_external(const CarrotEnoteV1 &enote,
|
||||
const std::optional<encrypted_payment_id_t> &encrypted_payment_id,
|
||||
const view_incoming_key_device &k_view_dev,
|
||||
const crypto::public_key &main_address_spend_pubkey,
|
||||
crypto::secret_key &sender_extension_g_out,
|
||||
crypto::secret_key &sender_extension_t_out,
|
||||
crypto::public_key &address_spend_pubkey_out,
|
||||
rct::xmr_amount &amount_out,
|
||||
crypto::secret_key &amount_blinding_factor_out,
|
||||
payment_id_t &payment_id_out,
|
||||
CarrotEnoteType &enote_type_out);
|
||||
bool try_ecdh_and_scan_carrot_enote_external(const CarrotEnoteV1 &enote,
|
||||
const std::optional<encrypted_payment_id_t> &encrypted_payment_id,
|
||||
const view_incoming_key_device &k_view_dev,
|
||||
const epee::span<const crypto::public_key> &main_address_spend_pubkeys,
|
||||
crypto::secret_key &sender_extension_g_out,
|
||||
crypto::secret_key &sender_extension_t_out,
|
||||
crypto::public_key &address_spend_pubkey_out,
|
||||
rct::xmr_amount &amount_out,
|
||||
crypto::secret_key &amount_blinding_factor_out,
|
||||
payment_id_t &payment_id_out,
|
||||
CarrotEnoteType &enote_type_out);
|
||||
/**
|
||||
* brief: try_scan_carrot_enote_internal - attempt full scan process on internal enote
|
||||
* param: enote -
|
||||
|
@ -166,22 +202,6 @@ bool try_scan_carrot_enote_internal(const CarrotEnoteV1 &enote,
|
|||
crypto::secret_key &amount_blinding_factor_out,
|
||||
CarrotEnoteType &enote_type_out,
|
||||
janus_anchor_t &internal_message_out);
|
||||
/**
|
||||
* brief: try_ecdh_and_scan_carrot_enote_external - derive s_sr and attempt full scan process on external enote
|
||||
*
|
||||
* Same as try_scan_carrot_enote_external() but paramater s_sender_receiver_unctx is calculated inside function
|
||||
*/
|
||||
bool try_ecdh_and_scan_carrot_enote_external(const CarrotEnoteV1 &enote,
|
||||
const std::optional<encrypted_payment_id_t> &encrypted_payment_id,
|
||||
const view_incoming_key_device &k_view_dev,
|
||||
const crypto::public_key &main_address_spend_pubkey,
|
||||
crypto::secret_key &sender_extension_g_out,
|
||||
crypto::secret_key &sender_extension_t_out,
|
||||
crypto::public_key &address_spend_pubkey_out,
|
||||
rct::xmr_amount &amount_out,
|
||||
crypto::secret_key &amount_blinding_factor_out,
|
||||
payment_id_t &payment_id_out,
|
||||
CarrotEnoteType &enote_type_out);
|
||||
/**
|
||||
* brief: try_scan_carrot_enote_external_destination_only - attempt external scan process, w/o decrypting
|
||||
* amount or recomputing the amount commitment
|
||||
|
|
54
src/carrot_impl/CMakeLists.txt
Normal file
54
src/carrot_impl/CMakeLists.txt
Normal file
|
@ -0,0 +1,54 @@
|
|||
# Copyright (c) 2024, The Monero Project
|
||||
#
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without modification, are
|
||||
# permitted provided that the following conditions are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright notice, this list of
|
||||
# conditions and the following disclaimer.
|
||||
#
|
||||
# 2. Redistributions in binary form must reproduce the above copyright notice, this list
|
||||
# of conditions and the following disclaimer in the documentation and/or other
|
||||
# materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its contributors may be
|
||||
# used to endorse or promote products derived from this software without specific
|
||||
# prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
|
||||
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
|
||||
# THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
||||
# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
|
||||
# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
set(carrot_impl_sources
|
||||
address_device_ram_borrowed.cpp
|
||||
address_utils_compat.cpp
|
||||
carrot_tx_builder_utils.cpp
|
||||
carrot_tx_format_utils.cpp
|
||||
input_selection.cpp
|
||||
)
|
||||
|
||||
monero_find_all_headers(carrot_impl_headers, "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
|
||||
monero_add_library(carrot_impl
|
||||
${carrot_impl_sources}
|
||||
${carrot_impl_headers})
|
||||
|
||||
target_link_libraries(carrot_impl
|
||||
PUBLIC
|
||||
carrot_core
|
||||
cryptonote_format_utils_basic
|
||||
PRIVATE
|
||||
${EXTRA_LIBRARIES})
|
||||
|
||||
target_include_directories(carrot_impl
|
||||
PUBLIC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}"
|
||||
PRIVATE
|
||||
${Boost_INCLUDE_DIRS})
|
105
src/carrot_impl/address_device.h
Normal file
105
src/carrot_impl/address_device.h
Normal file
|
@ -0,0 +1,105 @@
|
|||
// 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.
|
||||
|
||||
#pragma once
|
||||
|
||||
//local headers
|
||||
#include "carrot_core/device.h"
|
||||
#include "subaddress_index.h"
|
||||
|
||||
//third party headers
|
||||
|
||||
//standard headers
|
||||
|
||||
//forward declarations
|
||||
|
||||
namespace carrot
|
||||
{
|
||||
static constexpr const int E_UNSUPPORTED_ADDRESS_TYPE = 1;
|
||||
|
||||
struct cryptonote_hierarchy_address_device: virtual public view_incoming_key_device
|
||||
{
|
||||
/**
|
||||
* brief: get_cryptonote_account_spend_pubkey - K_s
|
||||
* return: K_s
|
||||
*/
|
||||
virtual crypto::public_key get_cryptonote_account_spend_pubkey() const = 0;
|
||||
|
||||
/**
|
||||
* brief: make_legacy_subaddress_extension - k^j_subext
|
||||
* k^j_subext = ScalarDeriveLegacy("SubAddr" || IntToBytes8(0) || k_v || IntToBytes32(j_major) || IntToBytes32(j_minor))
|
||||
* param: major_index - j_major
|
||||
* param: minor_index - j_minor
|
||||
* outparam: legacy_subaddress_extension_out - k^j_subext
|
||||
* throw: std::invalid_argument if major_index == minor_index == 0
|
||||
*/
|
||||
virtual void make_legacy_subaddress_extension(const std::uint32_t major_index,
|
||||
const std::uint32_t minor_index,
|
||||
crypto::secret_key &legacy_subaddress_extension_out) const = 0;
|
||||
};
|
||||
|
||||
struct carrot_hierarchy_address_device: public generate_address_secret_device
|
||||
{
|
||||
/**
|
||||
* brief: get_carrot_account_spend_pubkey - K_s = K^0_s
|
||||
* return: K_s = K^0_s
|
||||
*/
|
||||
virtual crypto::public_key get_carrot_account_spend_pubkey() const = 0;
|
||||
|
||||
/**
|
||||
* brief: get_carrot_account_view_pubkey - K_v
|
||||
* return: K_v
|
||||
*/
|
||||
virtual crypto::public_key get_carrot_account_view_pubkey() const = 0;
|
||||
|
||||
/**
|
||||
* brief: get_carrot_main_address_view_pubkey - K^0_v
|
||||
* return: K^0_v
|
||||
*/
|
||||
virtual crypto::public_key get_carrot_main_address_view_pubkey() const = 0;
|
||||
};
|
||||
|
||||
struct hybrid_hierarchy_address_device
|
||||
{
|
||||
virtual bool supports_address_derivation_type(AddressDeriveType derive_type) const = 0;
|
||||
|
||||
virtual cryptonote_hierarchy_address_device &access_cryptonote_hierarchy_device() const = 0;
|
||||
|
||||
virtual carrot_hierarchy_address_device &access_carrot_hierarchy_device() const = 0;
|
||||
|
||||
virtual ~hybrid_hierarchy_address_device() = default;
|
||||
};
|
||||
|
||||
struct interactive_address_device
|
||||
{
|
||||
virtual void request_explicit_on_device_address_confirmation(
|
||||
const subaddress_index_extended &index) = 0;
|
||||
|
||||
virtual ~interactive_address_device() = default;
|
||||
};
|
||||
} //namespace carrot
|
56
src/carrot_impl/address_device_ram_borrowed.cpp
Normal file
56
src/carrot_impl/address_device_ram_borrowed.cpp
Normal file
|
@ -0,0 +1,56 @@
|
|||
// Copyright (c) 2024, The Monero Project
|
||||
//
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without modification, are
|
||||
// permitted provided that the following conditions are met:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above copyright notice, this list of
|
||||
// conditions and the following disclaimer.
|
||||
//
|
||||
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
|
||||
// of conditions and the following disclaimer in the documentation and/or other
|
||||
// materials provided with the distribution.
|
||||
//
|
||||
// 3. Neither the name of the copyright holder nor the names of its contributors may be
|
||||
// used to endorse or promote products derived from this software without specific
|
||||
// prior written permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
|
||||
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
|
||||
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
||||
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
|
||||
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
//paired header
|
||||
#include "address_device_ram_borrowed.h"
|
||||
|
||||
//local headers
|
||||
#include "address_utils_compat.h"
|
||||
|
||||
//third party headers
|
||||
|
||||
//standard headers
|
||||
|
||||
#undef MONERO_DEFAULT_LOG_CATEGORY
|
||||
#define MONERO_DEFAULT_LOG_CATEGORY "carrot_impl"
|
||||
|
||||
namespace carrot
|
||||
{
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
void cryptonote_hierarchy_address_device_ram_borrowed::make_legacy_subaddress_extension(
|
||||
const std::uint32_t major_index,
|
||||
const std::uint32_t minor_index,
|
||||
crypto::secret_key &legacy_subaddress_extension_out) const
|
||||
{
|
||||
return carrot::make_legacy_subaddress_extension(m_k_view_incoming,
|
||||
major_index,
|
||||
minor_index,
|
||||
legacy_subaddress_extension_out);
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
} //namespace carrot
|
66
src/carrot_impl/address_device_ram_borrowed.h
Normal file
66
src/carrot_impl/address_device_ram_borrowed.h
Normal file
|
@ -0,0 +1,66 @@
|
|||
// 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.
|
||||
|
||||
#pragma once
|
||||
|
||||
//local headers
|
||||
#include "address_device.h"
|
||||
#include "carrot_core/device_ram_borrowed.h"
|
||||
|
||||
//third party headers
|
||||
|
||||
//standard headers
|
||||
|
||||
//forward declarations
|
||||
|
||||
namespace carrot
|
||||
{
|
||||
struct cryptonote_hierarchy_address_device_ram_borrowed:
|
||||
public cryptonote_hierarchy_address_device,
|
||||
public view_incoming_key_ram_borrowed_device
|
||||
{
|
||||
cryptonote_hierarchy_address_device_ram_borrowed(
|
||||
const crypto::public_key &cryptonote_account_spend_pubkey,
|
||||
const crypto::secret_key &k_view_incoming):
|
||||
view_incoming_key_ram_borrowed_device(k_view_incoming),
|
||||
m_cryptonote_account_spend_pubkey(cryptonote_account_spend_pubkey)
|
||||
{}
|
||||
|
||||
crypto::public_key get_cryptonote_account_spend_pubkey() const override
|
||||
{
|
||||
return m_cryptonote_account_spend_pubkey;
|
||||
}
|
||||
|
||||
void make_legacy_subaddress_extension(const std::uint32_t major_index,
|
||||
const std::uint32_t minor_index,
|
||||
crypto::secret_key &legacy_subaddress_extension_out) const override;
|
||||
|
||||
protected:
|
||||
const crypto::public_key &m_cryptonote_account_spend_pubkey;
|
||||
};
|
||||
} //namespace carrot
|
115
src/carrot_impl/address_utils_compat.cpp
Normal file
115
src/carrot_impl/address_utils_compat.cpp
Normal file
|
@ -0,0 +1,115 @@
|
|||
// Copyright (c) 2024, The Monero Project
|
||||
//
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without modification, are
|
||||
// permitted provided that the following conditions are met:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above copyright notice, this list of
|
||||
// conditions and the following disclaimer.
|
||||
//
|
||||
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
|
||||
// of conditions and the following disclaimer in the documentation and/or other
|
||||
// materials provided with the distribution.
|
||||
//
|
||||
// 3. Neither the name of the copyright holder nor the names of its contributors may be
|
||||
// used to endorse or promote products derived from this software without specific
|
||||
// prior written permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
|
||||
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
|
||||
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
||||
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
|
||||
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
//paired header
|
||||
#include "address_utils_compat.h"
|
||||
|
||||
//local headers
|
||||
#include "cryptonote_config.h"
|
||||
#include "int-util.h"
|
||||
#include "ringct/rctOps.h"
|
||||
|
||||
//third party headers
|
||||
|
||||
//standard headers
|
||||
|
||||
#undef MONERO_DEFAULT_LOG_CATEGORY
|
||||
#define MONERO_DEFAULT_LOG_CATEGORY "carrot_impl"
|
||||
|
||||
namespace carrot
|
||||
{
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
void make_legacy_subaddress_extension(const crypto::secret_key &k_view,
|
||||
const std::uint32_t major_index,
|
||||
const std::uint32_t minor_index,
|
||||
crypto::secret_key &legacy_subaddress_extension_out)
|
||||
{
|
||||
if (!major_index && !minor_index)
|
||||
{
|
||||
sc_0(to_bytes(legacy_subaddress_extension_out));
|
||||
return;
|
||||
}
|
||||
|
||||
char data[sizeof(config::HASH_KEY_SUBADDRESS) + sizeof(crypto::secret_key) + 2 * sizeof(uint32_t)];
|
||||
|
||||
// "Subaddr" || IntToBytes(0)
|
||||
memcpy(data, config::HASH_KEY_SUBADDRESS, sizeof(config::HASH_KEY_SUBADDRESS));
|
||||
|
||||
// ... || k_v
|
||||
memcpy(data + sizeof(config::HASH_KEY_SUBADDRESS), &k_view, sizeof(crypto::secret_key));
|
||||
|
||||
// ... || IntToBytes32(j_major)
|
||||
uint32_t idx = SWAP32LE(major_index);
|
||||
memcpy(data + sizeof(config::HASH_KEY_SUBADDRESS) + sizeof(crypto::secret_key), &idx, sizeof(uint32_t));
|
||||
|
||||
// ... || IntToBytes32(j_minor)
|
||||
idx = SWAP32LE(minor_index);
|
||||
memcpy(data + sizeof(config::HASH_KEY_SUBADDRESS) + sizeof(crypto::secret_key) + sizeof(uint32_t),
|
||||
&idx,
|
||||
sizeof(uint32_t));
|
||||
|
||||
// k^j_subext = ScalarDeriveLegacy("SubAddr" || IntToBytes8(0) || k_v || IntToBytes32(j_major) || IntToBytes32(j_minor))
|
||||
crypto::hash_to_scalar(data, sizeof(data), legacy_subaddress_extension_out);
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
void make_legacy_subaddress_spend_pubkey(const crypto::secret_key &k_view,
|
||||
const std::uint32_t major_index,
|
||||
const std::uint32_t minor_index,
|
||||
const crypto::public_key &account_spend_pubkey,
|
||||
crypto::public_key &legacy_subaddress_spend_pubkey_out)
|
||||
{
|
||||
// k^j_subext = ScalarDeriveLegacy("SubAddr" || IntToBytes8(0) || k_v || IntToBytes32(j_major) || IntToBytes32(j_minor))
|
||||
crypto::secret_key subaddress_extension;
|
||||
make_legacy_subaddress_extension(k_view, major_index, minor_index, subaddress_extension);
|
||||
|
||||
// K^j_s = K_s + k^j_subext G
|
||||
rct::key res_k;
|
||||
rct::addKeys1(res_k, rct::sk2rct(subaddress_extension), rct::pk2rct(account_spend_pubkey));
|
||||
legacy_subaddress_spend_pubkey_out = rct::rct2pk(res_k);
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
void make_legacy_subaddress_view_pubkey(const crypto::secret_key &k_view,
|
||||
const std::uint32_t major_index,
|
||||
const std::uint32_t minor_index,
|
||||
const crypto::public_key &account_spend_pubkey,
|
||||
crypto::public_key &legacy_subaddress_view_pubkey_out)
|
||||
{
|
||||
// K^j_s = K_s + k^j_subext G
|
||||
crypto::public_key address_spend_pubkey;
|
||||
make_legacy_subaddress_spend_pubkey(k_view,
|
||||
major_index,
|
||||
minor_index,
|
||||
account_spend_pubkey,
|
||||
address_spend_pubkey);
|
||||
|
||||
// K^j_v = k_v K^j_s
|
||||
legacy_subaddress_view_pubkey_out = rct::rct2pk(rct::scalarmultKey(rct::pk2rct(address_spend_pubkey),
|
||||
rct::sk2rct(k_view)));
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
} //namespace carrot
|
84
src/carrot_impl/address_utils_compat.h
Normal file
84
src/carrot_impl/address_utils_compat.h
Normal file
|
@ -0,0 +1,84 @@
|
|||
// 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.
|
||||
|
||||
#pragma once
|
||||
|
||||
//local headers
|
||||
#include "crypto/crypto.h"
|
||||
|
||||
//third party headers
|
||||
|
||||
//standard headers
|
||||
#include <cstdint>
|
||||
|
||||
//forward declarations
|
||||
|
||||
namespace carrot
|
||||
{
|
||||
/**
|
||||
* brief: make_legacy_subaddress_extension - k^j_subext
|
||||
* k^j_subext = ScalarDeriveLegacy("SubAddr" || IntToBytes8(0) || k_v || IntToBytes32(j_major) || IntToBytes32(j_minor))
|
||||
* param: k_view - k_v
|
||||
* param: major_index - j_major
|
||||
* param: minor_index - j_minor
|
||||
* outparam: legacy_subaddress_extension_out - k^j_subext
|
||||
*/
|
||||
void make_legacy_subaddress_extension(const crypto::secret_key &k_view,
|
||||
const std::uint32_t major_index,
|
||||
const std::uint32_t minor_index,
|
||||
crypto::secret_key &legacy_subaddress_extension_out);
|
||||
/**
|
||||
* brief: make_legacy_subaddress_spend_pubkey - K^j_s
|
||||
* K^j_s = K_s + k^j_subext G
|
||||
* param: k_view - k_v
|
||||
* param: major_index - j_major
|
||||
* param: minor_index - j_minor
|
||||
* param: account_spend_pubkey - K_s
|
||||
* outparam: legacy_subaddress_spend_pubkey_out - K^j_s
|
||||
*/
|
||||
void make_legacy_subaddress_spend_pubkey(const crypto::secret_key &k_view,
|
||||
const std::uint32_t major_index,
|
||||
const std::uint32_t minor_index,
|
||||
const crypto::public_key &account_spend_pubkey,
|
||||
crypto::public_key &legacy_subaddress_spend_pubkey_out);
|
||||
/**
|
||||
* brief: make_legacy_subaddress_view_pubkey - K^j_v
|
||||
* K^j_v = k_v K^j_s
|
||||
* param: k_view - k_v
|
||||
* param: major_index - j_major
|
||||
* param: minor_index - j_minor
|
||||
* param: account_spend_pubkey - K_s
|
||||
* outparam: legacy_subaddress_view_pubkey_out - K^j_v
|
||||
*/
|
||||
void make_legacy_subaddress_view_pubkey(const crypto::secret_key &k_view,
|
||||
const std::uint32_t major_index,
|
||||
const std::uint32_t minor_index,
|
||||
const crypto::public_key &account_spend_pubkey,
|
||||
crypto::public_key &legacy_subaddress_view_pubkey_out);
|
||||
|
||||
} //namespace carrot
|
61
src/carrot_impl/carrot_boost_serialization.h
Normal file
61
src/carrot_impl/carrot_boost_serialization.h
Normal file
|
@ -0,0 +1,61 @@
|
|||
// Copyright (c) 2024, The Monero Project
|
||||
//
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without modification, are
|
||||
// permitted provided that the following conditions are met:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above copyright notice, this list of
|
||||
// conditions and the following disclaimer.
|
||||
//
|
||||
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
|
||||
// of conditions and the following disclaimer in the documentation and/or other
|
||||
// materials provided with the distribution.
|
||||
//
|
||||
// 3. Neither the name of the copyright holder nor the names of its contributors may be
|
||||
// used to endorse or promote products derived from this software without specific
|
||||
// prior written permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
|
||||
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
|
||||
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
||||
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
|
||||
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
//
|
||||
// Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers
|
||||
|
||||
#pragma once
|
||||
|
||||
//local headers
|
||||
#include "carrot_core/core_types.h"
|
||||
|
||||
//third party headers
|
||||
#include <boost/serialization/utility.hpp>
|
||||
|
||||
//standard headers
|
||||
|
||||
//forward declarations
|
||||
|
||||
namespace boost
|
||||
{
|
||||
namespace serialization
|
||||
{
|
||||
//---------------------------------------------------
|
||||
template <class Archive>
|
||||
inline void serialize(Archive &a, carrot::view_tag_t &x, const boost::serialization::version_type ver)
|
||||
{
|
||||
a & x.bytes;
|
||||
}
|
||||
//---------------------------------------------------
|
||||
template <class Archive>
|
||||
inline void serialize(Archive &a, carrot::encrypted_janus_anchor_t &x, const boost::serialization::version_type ver)
|
||||
{
|
||||
a & x.bytes;
|
||||
}
|
||||
//---------------------------------------------------
|
||||
} //namespace serialization
|
||||
} //namespace boot
|
42
src/carrot_impl/carrot_chain_serialization.h
Normal file
42
src/carrot_impl/carrot_chain_serialization.h
Normal file
|
@ -0,0 +1,42 @@
|
|||
// Copyright (c) 2024, The Monero Project
|
||||
//
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without modification, are
|
||||
// permitted provided that the following conditions are met:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above copyright notice, this list of
|
||||
// conditions and the following disclaimer.
|
||||
//
|
||||
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
|
||||
// of conditions and the following disclaimer in the documentation and/or other
|
||||
// materials provided with the distribution.
|
||||
//
|
||||
// 3. Neither the name of the copyright holder nor the names of its contributors may be
|
||||
// used to endorse or promote products derived from this software without specific
|
||||
// prior written permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
|
||||
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
|
||||
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
||||
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
|
||||
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
#pragma once
|
||||
|
||||
//local headers
|
||||
#include "carrot_core/core_types.h"
|
||||
#include "serialization/serialization.h"
|
||||
|
||||
//third party headers
|
||||
|
||||
//standard headers
|
||||
|
||||
//forward declarations
|
||||
|
||||
BLOB_SERIALIZER(carrot::view_tag_t);
|
||||
BLOB_SERIALIZER(carrot::encrypted_janus_anchor_t);
|
98
src/carrot_impl/carrot_tx_builder_types.h
Normal file
98
src/carrot_impl/carrot_tx_builder_types.h
Normal file
|
@ -0,0 +1,98 @@
|
|||
// Copyright (c) 2024, The Monero Project
|
||||
//
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without modification, are
|
||||
// permitted provided that the following conditions are met:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above copyright notice, this list of
|
||||
// conditions and the following disclaimer.
|
||||
//
|
||||
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
|
||||
// of conditions and the following disclaimer in the documentation and/or other
|
||||
// materials provided with the distribution.
|
||||
//
|
||||
// 3. Neither the name of the copyright holder nor the names of its contributors may be
|
||||
// used to endorse or promote products derived from this software without specific
|
||||
// prior written permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
|
||||
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
|
||||
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
||||
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
|
||||
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
#pragma once
|
||||
|
||||
//local headers
|
||||
#include "carrot_core/payment_proposal.h"
|
||||
#include "crypto/crypto.h"
|
||||
#include "ringct/rctTypes.h"
|
||||
#include "subaddress_index.h"
|
||||
|
||||
//third party headers
|
||||
#include <boost/multiprecision/cpp_int.hpp>
|
||||
|
||||
//standard headers
|
||||
#include <functional>
|
||||
#include <map>
|
||||
|
||||
//forward declarations
|
||||
|
||||
namespace carrot
|
||||
{
|
||||
struct CarrotSelectedInput
|
||||
{
|
||||
rct::xmr_amount amount;
|
||||
crypto::key_image key_image;
|
||||
};
|
||||
static inline bool operator==(const CarrotSelectedInput &a, const CarrotSelectedInput &b)
|
||||
{
|
||||
return a.amount == b.amount && a.key_image == b.key_image;
|
||||
}
|
||||
static inline bool operator!=(const CarrotSelectedInput &a, const CarrotSelectedInput &b)
|
||||
{
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
struct CarrotPaymentProposalVerifiableSelfSendV1
|
||||
{
|
||||
CarrotPaymentProposalSelfSendV1 proposal;
|
||||
subaddress_index_extended subaddr_index;
|
||||
};
|
||||
|
||||
using select_inputs_func_t = std::function<void(
|
||||
const boost::multiprecision::int128_t&, // nominal output sum, w/o fee
|
||||
const std::map<std::size_t, rct::xmr_amount>&, // absolute fee per input count
|
||||
const std::size_t, // number of normal payment proposals
|
||||
const std::size_t, // number of self-send payment proposals
|
||||
std::vector<CarrotSelectedInput>& // selected inputs result
|
||||
)>;
|
||||
|
||||
using carve_fees_and_balance_func_t = std::function<void(
|
||||
const boost::multiprecision::int128_t&, // input sum amount
|
||||
const rct::xmr_amount, // fee
|
||||
std::vector<CarrotPaymentProposalV1>&, // normal payment proposals [inout]
|
||||
std::vector<CarrotPaymentProposalVerifiableSelfSendV1>& // selfsend payment proposals [inout]
|
||||
)>;
|
||||
|
||||
/**
|
||||
* brief: CarrotTransactionProposalV1
|
||||
*/
|
||||
struct CarrotTransactionProposalV1
|
||||
{
|
||||
std::vector<crypto::key_image> key_images_sorted;
|
||||
|
||||
std::vector<CarrotPaymentProposalV1> normal_payment_proposals;
|
||||
std::vector<CarrotPaymentProposalVerifiableSelfSendV1> selfsend_payment_proposals;
|
||||
encrypted_payment_id_t dummy_encrypted_payment_id;
|
||||
rct::xmr_amount fee;
|
||||
|
||||
std::vector<std::uint8_t> extra;
|
||||
};
|
||||
|
||||
} //namespace carrot
|
524
src/carrot_impl/carrot_tx_builder_utils.cpp
Normal file
524
src/carrot_impl/carrot_tx_builder_utils.cpp
Normal file
|
@ -0,0 +1,524 @@
|
|||
// Copyright (c) 2024, The Monero Project
|
||||
//
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without modification, are
|
||||
// permitted provided that the following conditions are met:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above copyright notice, this list of
|
||||
// conditions and the following disclaimer.
|
||||
//
|
||||
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
|
||||
// of conditions and the following disclaimer in the documentation and/or other
|
||||
// materials provided with the distribution.
|
||||
//
|
||||
// 3. Neither the name of the copyright holder nor the names of its contributors may be
|
||||
// used to endorse or promote products derived from this software without specific
|
||||
// prior written permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
|
||||
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
|
||||
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
||||
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
|
||||
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
//paired header
|
||||
#include "carrot_tx_builder_utils.h"
|
||||
|
||||
//local headers
|
||||
#include "carrot_core/output_set_finalization.h"
|
||||
#include "carrot_tx_format_utils.h"
|
||||
extern "C"
|
||||
{
|
||||
#include "crypto/crypto-ops.h"
|
||||
}
|
||||
#include "cryptonote_basic/cryptonote_format_utils.h"
|
||||
#include "ringct/rctOps.h"
|
||||
|
||||
//third party headers
|
||||
|
||||
//standard headers
|
||||
|
||||
#undef MONERO_DEFAULT_LOG_CATEGORY
|
||||
#define MONERO_DEFAULT_LOG_CATEGORY "carrot_impl"
|
||||
|
||||
namespace carrot
|
||||
{
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
static void append_additional_payment_proposal_if_necessary(
|
||||
std::vector<CarrotPaymentProposalV1>& normal_payment_proposals_inout,
|
||||
std::vector<CarrotPaymentProposalVerifiableSelfSendV1> &selfsend_payment_proposals_inout,
|
||||
const crypto::public_key &change_address_spend_pubkey,
|
||||
const subaddress_index_extended &change_subaddr_index)
|
||||
{
|
||||
struct append_additional_payment_proposal_if_necessary_visitor
|
||||
{
|
||||
void operator()(boost::blank) const {}
|
||||
void operator()(const CarrotPaymentProposalV1 &p) const { normal_proposals_inout.push_back(p); }
|
||||
void operator()(const CarrotPaymentProposalSelfSendV1 &p) const
|
||||
{
|
||||
selfsend_proposals_inout.push_back(CarrotPaymentProposalVerifiableSelfSendV1{
|
||||
.proposal = p,
|
||||
.subaddr_index = change_subaddr_index
|
||||
});
|
||||
}
|
||||
|
||||
std::vector<CarrotPaymentProposalV1>& normal_proposals_inout;
|
||||
std::vector<CarrotPaymentProposalVerifiableSelfSendV1> &selfsend_proposals_inout;
|
||||
const subaddress_index_extended &change_subaddr_index;
|
||||
};
|
||||
|
||||
bool have_payment_type_selfsend = false;
|
||||
for (const CarrotPaymentProposalVerifiableSelfSendV1 &selfsend_payment_proposal : selfsend_payment_proposals_inout)
|
||||
{
|
||||
if (selfsend_payment_proposal.proposal.enote_type == CarrotEnoteType::PAYMENT)
|
||||
{
|
||||
have_payment_type_selfsend = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const auto additional_output_proposal = get_additional_output_proposal(normal_payment_proposals_inout.size(),
|
||||
selfsend_payment_proposals_inout.size(),
|
||||
/*needed_change_amount=*/0,
|
||||
have_payment_type_selfsend,
|
||||
change_address_spend_pubkey);
|
||||
|
||||
additional_output_proposal.visit(append_additional_payment_proposal_if_necessary_visitor{
|
||||
normal_payment_proposals_inout,
|
||||
selfsend_payment_proposals_inout,
|
||||
change_subaddr_index
|
||||
});
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
void make_carrot_transaction_proposal_v1(const std::vector<CarrotPaymentProposalV1> &normal_payment_proposals_in,
|
||||
const std::vector<CarrotPaymentProposalVerifiableSelfSendV1> &selfsend_payment_proposals_in,
|
||||
const rct::xmr_amount fee_per_weight,
|
||||
const std::vector<uint8_t> &extra,
|
||||
select_inputs_func_t &&select_inputs,
|
||||
carve_fees_and_balance_func_t &&carve_fees_and_balance,
|
||||
const view_balance_secret_device *s_view_balance_dev,
|
||||
const view_incoming_key_device *k_view_dev,
|
||||
const crypto::public_key &account_spend_pubkey,
|
||||
CarrotTransactionProposalV1 &tx_proposal_out)
|
||||
{
|
||||
tx_proposal_out.extra = extra;
|
||||
|
||||
std::vector<CarrotPaymentProposalV1> &normal_payment_proposals
|
||||
= tx_proposal_out.normal_payment_proposals
|
||||
= normal_payment_proposals_in;
|
||||
std::vector<CarrotPaymentProposalVerifiableSelfSendV1> &selfsend_payment_proposals
|
||||
= tx_proposal_out.selfsend_payment_proposals
|
||||
= selfsend_payment_proposals_in;
|
||||
|
||||
// add an additional payment proposal to satisfy scanning/consensus rules, if applicable
|
||||
append_additional_payment_proposal_if_necessary(normal_payment_proposals,
|
||||
selfsend_payment_proposals,
|
||||
account_spend_pubkey,
|
||||
{0, 0, AddressDeriveType::Auto}); // @TODO: let callers pass AddressDeriveType as an argument
|
||||
|
||||
const size_t num_outs = normal_payment_proposals.size() + selfsend_payment_proposals.size();
|
||||
CHECK_AND_ASSERT_THROW_MES(num_outs >= CARROT_MIN_TX_OUTPUTS,
|
||||
"make_carrot_transaction_proposal_v1: too few outputs");
|
||||
|
||||
// generate random X25519 ephemeral pubkeys for selfsend proposals if:
|
||||
// a. not explicitly provided in a >2-out tx, OR
|
||||
// b. not explicitly provided in a 2-out 2-self-send tx and the other is also missing
|
||||
const bool should_gen_selfsend_ephemeral_pubkeys = num_outs != 2 ||
|
||||
(normal_payment_proposals.empty()
|
||||
&& !selfsend_payment_proposals.at(0).proposal.enote_ephemeral_pubkey
|
||||
&& !selfsend_payment_proposals.at(1).proposal.enote_ephemeral_pubkey);
|
||||
if (should_gen_selfsend_ephemeral_pubkeys)
|
||||
{
|
||||
for (size_t i = 0; i < selfsend_payment_proposals.size(); ++i)
|
||||
{
|
||||
// should not provide two different D_e in a 2-out tx, so skip the second D_e in a 2-out
|
||||
const bool should_skip_generating = num_outs == 2 && i == 1;
|
||||
if (should_skip_generating)
|
||||
continue;
|
||||
CarrotPaymentProposalVerifiableSelfSendV1 &selfsend_payment_proposal = selfsend_payment_proposals[i];
|
||||
if (!selfsend_payment_proposal.proposal.enote_ephemeral_pubkey)
|
||||
selfsend_payment_proposal.proposal.enote_ephemeral_pubkey = gen_x25519_pubkey();
|
||||
}
|
||||
}
|
||||
|
||||
// generate random dummy encrypted payment ID for if none of the normal payment proposals are integrated
|
||||
tx_proposal_out.dummy_encrypted_payment_id = gen_payment_id();
|
||||
|
||||
// calculate size of tx.extra
|
||||
const size_t tx_extra_size = get_carrot_default_tx_extra_size(num_outs);
|
||||
|
||||
// calculate the concrete fee for this transaction for each possible valid input count
|
||||
std::map<size_t, rct::xmr_amount> fee_per_input_count;
|
||||
for (size_t num_ins = 1; num_ins <= CARROT_MAX_TX_INPUTS; ++num_ins)
|
||||
{
|
||||
const size_t tx_weight = get_fcmppp_tx_weight(num_ins, num_outs, tx_extra_size);
|
||||
const rct::xmr_amount fee = tx_weight * fee_per_weight; // @TODO: check for overflow here
|
||||
fee_per_input_count.emplace(num_ins, fee);
|
||||
}
|
||||
|
||||
// calculate sum of payment proposal amounts before fee carving
|
||||
boost::multiprecision::int128_t nominal_output_amount_sum = 0;
|
||||
for (const CarrotPaymentProposalV1 &normal_proposal : normal_payment_proposals)
|
||||
nominal_output_amount_sum += normal_proposal.amount;
|
||||
for (const CarrotPaymentProposalVerifiableSelfSendV1 &selfsend_proposal : selfsend_payment_proposals)
|
||||
nominal_output_amount_sum += selfsend_proposal.proposal.amount;
|
||||
|
||||
// callback to select inputs given nominal output sum and fee per input count
|
||||
std::vector<CarrotSelectedInput> selected_inputs;
|
||||
select_inputs(nominal_output_amount_sum,
|
||||
fee_per_input_count,
|
||||
normal_payment_proposals.size(),
|
||||
selfsend_payment_proposals.size(),
|
||||
selected_inputs);
|
||||
|
||||
// get fee given the number of selected inputs
|
||||
// note: this will fail if input selection returned a bad number of inputs
|
||||
tx_proposal_out.fee = fee_per_input_count.at(selected_inputs.size());
|
||||
|
||||
// calculate input amount sum
|
||||
boost::multiprecision::int128_t input_amount_sum = 0;
|
||||
for (const CarrotSelectedInput &selected_input : selected_inputs)
|
||||
input_amount_sum += selected_input.amount;
|
||||
|
||||
// callback to balance the outputs with the fee and input sum
|
||||
carve_fees_and_balance(input_amount_sum, tx_proposal_out.fee, normal_payment_proposals, selfsend_payment_proposals);
|
||||
|
||||
// sanity check balance
|
||||
input_amount_sum -= tx_proposal_out.fee;
|
||||
for (const CarrotPaymentProposalV1 &normal_payment_proposal : normal_payment_proposals)
|
||||
input_amount_sum -= normal_payment_proposal.amount;
|
||||
for (const CarrotPaymentProposalVerifiableSelfSendV1 &selfsend_payment_proposal : selfsend_payment_proposals)
|
||||
input_amount_sum -= selfsend_payment_proposal.proposal.amount;
|
||||
CHECK_AND_ASSERT_THROW_MES(input_amount_sum == 0,
|
||||
"make_carrot_transaction_proposal_v1: post-carved transaction does not balance");
|
||||
|
||||
// collect and sort key images
|
||||
tx_proposal_out.key_images_sorted.reserve(selected_inputs.size());
|
||||
for (const CarrotSelectedInput &selected_input : selected_inputs)
|
||||
tx_proposal_out.key_images_sorted.push_back(selected_input.key_image);
|
||||
std::sort(tx_proposal_out.key_images_sorted.begin(),
|
||||
tx_proposal_out.key_images_sorted.end(),
|
||||
&compare_input_key_images);
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
void make_carrot_transaction_proposal_v1_transfer_subtractable(
|
||||
const std::vector<CarrotPaymentProposalV1> &normal_payment_proposals,
|
||||
const std::vector<CarrotPaymentProposalVerifiableSelfSendV1> &selfsend_payment_proposals_in,
|
||||
const rct::xmr_amount fee_per_weight,
|
||||
const std::vector<uint8_t> &extra,
|
||||
select_inputs_func_t &&select_inputs,
|
||||
const view_balance_secret_device *s_view_balance_dev,
|
||||
const view_incoming_key_device *k_view_dev,
|
||||
const crypto::public_key &account_spend_pubkey,
|
||||
const std::set<std::size_t> &subtractable_normal_payment_proposals,
|
||||
const std::set<std::size_t> &subtractable_selfsend_payment_proposals,
|
||||
CarrotTransactionProposalV1 &tx_proposal_out)
|
||||
{
|
||||
std::vector<CarrotPaymentProposalVerifiableSelfSendV1> selfsend_payment_proposals = selfsend_payment_proposals_in;
|
||||
|
||||
// always add implicit selfsend enote, so resultant enotes' amounts mirror given payments set close as possible
|
||||
// note: we always do this, even if the amount ends up being 0 and we already have a selfsend. this is because if we
|
||||
// realize later that the change output we added here has a 0 amount, and we try removing it, then the fee
|
||||
// would go down and then the change amount *wouldn't* be 0, so it must stay. Although technically,
|
||||
// the scenario could arise where a change in input selection changes the input sum amount and fee exactly
|
||||
// such that we could remove the implicit change output and it happens to balance. IMO, handling this edge
|
||||
// case isn't worth the additional code complexity, and may cause unexpected uniformity issues. The calling
|
||||
// code might expect that transfers to N destinations always produces a transaction with N+1 outputs
|
||||
const bool add_payment_type_selfsend = normal_payment_proposals.empty() &&
|
||||
selfsend_payment_proposals.size() == 1 &&
|
||||
selfsend_payment_proposals.at(0).proposal.enote_type == CarrotEnoteType::CHANGE;
|
||||
|
||||
selfsend_payment_proposals.push_back(CarrotPaymentProposalVerifiableSelfSendV1{
|
||||
.proposal = CarrotPaymentProposalSelfSendV1{
|
||||
.destination_address_spend_pubkey = account_spend_pubkey,
|
||||
.amount = 0,
|
||||
.enote_type = add_payment_type_selfsend ? CarrotEnoteType::PAYMENT : CarrotEnoteType::CHANGE
|
||||
},
|
||||
.subaddr_index = {0, 0, AddressDeriveType::Auto} // @TODO: let callers pass AddressDeriveType as an argument
|
||||
});
|
||||
|
||||
// define carves fees and balance callback
|
||||
carve_fees_and_balance_func_t carve_fees_and_balance =
|
||||
[
|
||||
&subtractable_normal_payment_proposals,
|
||||
&subtractable_selfsend_payment_proposals
|
||||
]
|
||||
(
|
||||
const boost::multiprecision::int128_t &input_sum_amount,
|
||||
const rct::xmr_amount fee,
|
||||
std::vector<CarrotPaymentProposalV1> &normal_payment_proposals,
|
||||
std::vector<CarrotPaymentProposalVerifiableSelfSendV1> &selfsend_payment_proposals
|
||||
)
|
||||
{
|
||||
const bool has_subbable_normal = !subtractable_normal_payment_proposals.empty();
|
||||
const bool has_subbable_selfsend = !subtractable_selfsend_payment_proposals.empty();
|
||||
const size_t num_normal = normal_payment_proposals.size();
|
||||
const size_t num_selfsend = selfsend_payment_proposals.size();
|
||||
|
||||
// check subbable indices invariants
|
||||
CHECK_AND_ASSERT_THROW_MES(
|
||||
!has_subbable_normal || *subtractable_normal_payment_proposals.crbegin() < num_normal,
|
||||
"make unsigned transaction transfer subtractable: subtractable normal proposal index out of bounds");
|
||||
CHECK_AND_ASSERT_THROW_MES(
|
||||
!has_subbable_selfsend || *subtractable_selfsend_payment_proposals.crbegin() < num_selfsend,
|
||||
"make unsigned transaction transfer subtractable: subtractable selfsend proposal index out of bounds");
|
||||
CHECK_AND_ASSERT_THROW_MES(has_subbable_normal || has_subbable_selfsend,
|
||||
"make unsigned transaction transfer subtractable: no subtractable indices");
|
||||
|
||||
// check selfsend proposal invariants
|
||||
CHECK_AND_ASSERT_THROW_MES(!selfsend_payment_proposals.empty(),
|
||||
"make unsigned transaction transfer subtractable: missing a selfsend proposal");
|
||||
CHECK_AND_ASSERT_THROW_MES(selfsend_payment_proposals.back().proposal.amount == 0,
|
||||
"make unsigned transaction transfer subtractable: bug: added implicit change output has non-zero amount");
|
||||
|
||||
// start by setting the last selfsend amount equal to (inputs - outputs), before fee
|
||||
boost::multiprecision::int128_t implicit_change_amount = input_sum_amount;
|
||||
for (const CarrotPaymentProposalV1 &normal_payment_proposal : normal_payment_proposals)
|
||||
implicit_change_amount -= normal_payment_proposal.amount;
|
||||
for (const CarrotPaymentProposalVerifiableSelfSendV1 &selfsend_payment_proposal : selfsend_payment_proposals)
|
||||
implicit_change_amount -= selfsend_payment_proposal.proposal.amount;
|
||||
|
||||
selfsend_payment_proposals.back().proposal.amount =
|
||||
boost::numeric_cast<rct::xmr_amount>(implicit_change_amount);
|
||||
|
||||
// deduct an even fee amount from all subtractable outputs
|
||||
const size_t num_subtractble_normal = subtractable_normal_payment_proposals.size();
|
||||
const size_t num_subtractable_selfsend = subtractable_selfsend_payment_proposals.size();
|
||||
const size_t num_subtractable = num_subtractble_normal + num_subtractable_selfsend;
|
||||
const rct::xmr_amount minimum_subtraction = fee / num_subtractable; // no div by 0 since we checked subtractable
|
||||
for (size_t normal_sub_idx : subtractable_normal_payment_proposals)
|
||||
{
|
||||
CarrotPaymentProposalV1 &normal_payment_proposal = normal_payment_proposals[normal_sub_idx];
|
||||
CHECK_AND_ASSERT_THROW_MES(normal_payment_proposal.amount >= minimum_subtraction,
|
||||
"make unsigned transaction transfer subtractable: not enough funds in subtractable payment");
|
||||
normal_payment_proposal.amount -= minimum_subtraction;
|
||||
}
|
||||
for (size_t selfsend_sub_idx : subtractable_selfsend_payment_proposals)
|
||||
{
|
||||
CarrotPaymentProposalSelfSendV1 &selfsend_payment_proposal =
|
||||
selfsend_payment_proposals[selfsend_sub_idx].proposal;
|
||||
CHECK_AND_ASSERT_THROW_MES(selfsend_payment_proposal.amount >= minimum_subtraction,
|
||||
"make unsigned transaction transfer subtractable: not enough funds in subtractable payment");
|
||||
selfsend_payment_proposal.amount -= minimum_subtraction;
|
||||
}
|
||||
|
||||
// deduct 1 at a time from selfsend proposals
|
||||
rct::xmr_amount fee_remainder = fee % num_subtractable;
|
||||
for (size_t selfsend_sub_idx : subtractable_selfsend_payment_proposals)
|
||||
{
|
||||
if (fee_remainder == 0)
|
||||
break;
|
||||
|
||||
CarrotPaymentProposalSelfSendV1 &selfsend_payment_proposal =
|
||||
selfsend_payment_proposals[selfsend_sub_idx].proposal;
|
||||
CHECK_AND_ASSERT_THROW_MES(selfsend_payment_proposal.amount >= 1,
|
||||
"make unsigned transaction transfer subtractable: not enough funds in subtractable payment");
|
||||
selfsend_payment_proposal.amount -= 1;
|
||||
fee_remainder -= 1;
|
||||
}
|
||||
|
||||
// now deduct 1 at a time from normal proposals, shuffled
|
||||
if (fee_remainder != 0)
|
||||
{
|
||||
// create vector of shuffled subtractble normal payment indices
|
||||
// note: we do this to hide the order that the normal payment proposals were described in this call, in case
|
||||
// the recipients collude
|
||||
std::vector<size_t> shuffled_normal_subtractable(subtractable_normal_payment_proposals.cbegin(),
|
||||
subtractable_normal_payment_proposals.cend());
|
||||
std::shuffle(shuffled_normal_subtractable.begin(),
|
||||
shuffled_normal_subtractable.end(),
|
||||
crypto::random_device{});
|
||||
|
||||
for (size_t normal_sub_idx : shuffled_normal_subtractable)
|
||||
{
|
||||
if (fee_remainder == 0)
|
||||
break;
|
||||
|
||||
CarrotPaymentProposalV1 &normal_payment_proposal = normal_payment_proposals[normal_sub_idx];
|
||||
CHECK_AND_ASSERT_THROW_MES(normal_payment_proposal.amount >= 1,
|
||||
"make unsigned transaction transfer subtractable: not enough funds in subtractable payment");
|
||||
normal_payment_proposal.amount -= 1;
|
||||
fee_remainder -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
CHECK_AND_ASSERT_THROW_MES(fee_remainder == 0,
|
||||
"make unsigned transaction transfer subtractable: bug: fee remainder at end of carve function");
|
||||
}; //end carve_fees_and_balance
|
||||
|
||||
// make unsigned transaction with fee carving callback
|
||||
make_carrot_transaction_proposal_v1(normal_payment_proposals,
|
||||
selfsend_payment_proposals,
|
||||
fee_per_weight,
|
||||
extra,
|
||||
std::forward<select_inputs_func_t>(select_inputs),
|
||||
std::move(carve_fees_and_balance),
|
||||
s_view_balance_dev,
|
||||
k_view_dev,
|
||||
account_spend_pubkey,
|
||||
tx_proposal_out);
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
void make_carrot_transaction_proposal_v1_transfer(
|
||||
const std::vector<CarrotPaymentProposalV1> &normal_payment_proposals,
|
||||
const std::vector<CarrotPaymentProposalVerifiableSelfSendV1> &selfsend_payment_proposals,
|
||||
const rct::xmr_amount fee_per_weight,
|
||||
const std::vector<uint8_t> &extra,
|
||||
select_inputs_func_t &&select_inputs,
|
||||
const view_balance_secret_device *s_view_balance_dev,
|
||||
const view_incoming_key_device *k_view_dev,
|
||||
const crypto::public_key &account_spend_pubkey,
|
||||
CarrotTransactionProposalV1 &tx_proposal_out)
|
||||
{
|
||||
make_carrot_transaction_proposal_v1_transfer_subtractable(
|
||||
normal_payment_proposals,
|
||||
selfsend_payment_proposals,
|
||||
fee_per_weight,
|
||||
extra,
|
||||
std::forward<select_inputs_func_t>(select_inputs),
|
||||
s_view_balance_dev,
|
||||
k_view_dev,
|
||||
account_spend_pubkey,
|
||||
/*subtractable_normal_payment_proposals=*/{},
|
||||
/*subtractable_selfsend_payment_proposals=*/{selfsend_payment_proposals.size()},
|
||||
tx_proposal_out);
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
void make_carrot_transaction_proposal_v1_sweep(
|
||||
const std::vector<CarrotPaymentProposalV1> &normal_payment_proposals,
|
||||
const std::vector<CarrotPaymentProposalVerifiableSelfSendV1> &selfsend_payment_proposals,
|
||||
const rct::xmr_amount fee_per_weight,
|
||||
const std::vector<uint8_t> &extra,
|
||||
std::vector<CarrotSelectedInput> &&selected_inputs,
|
||||
const view_balance_secret_device *s_view_balance_dev,
|
||||
const view_incoming_key_device *k_view_dev,
|
||||
const crypto::public_key &account_spend_pubkey,
|
||||
CarrotTransactionProposalV1 &tx_proposal_out)
|
||||
{
|
||||
// sanity check that all payment proposal amounts are 0
|
||||
for (const CarrotPaymentProposalV1 &normal_payment_proposal : normal_payment_proposals)
|
||||
{
|
||||
CHECK_AND_ASSERT_THROW_MES(normal_payment_proposal.amount == 0,
|
||||
"make carrot transaction proposal v1 sweep: payment proposal amount not 0");
|
||||
}
|
||||
for (const CarrotPaymentProposalVerifiableSelfSendV1 &selfsend_payment_proposal : selfsend_payment_proposals)
|
||||
{
|
||||
CHECK_AND_ASSERT_THROW_MES(selfsend_payment_proposal.proposal.amount == 0,
|
||||
"make carrot transaction proposal v1 sweep: payment proposal amount not 0");
|
||||
}
|
||||
|
||||
const bool is_selfsend_sweep = !selfsend_payment_proposals.empty();
|
||||
|
||||
// define input selection callback, which is just a shuttle for `selected_inputs`
|
||||
select_inputs_func_t select_inputs = [&selected_inputs]
|
||||
(
|
||||
const boost::multiprecision::int128_t&,
|
||||
const std::map<std::size_t, rct::xmr_amount>&,
|
||||
const std::size_t,
|
||||
const std::size_t,
|
||||
std::vector<CarrotSelectedInput> &selected_inputs_out
|
||||
)
|
||||
{
|
||||
selected_inputs_out = std::move(selected_inputs);
|
||||
}; //end select_inputs
|
||||
|
||||
// define carves fees and balance callback
|
||||
carve_fees_and_balance_func_t carve_fees_and_balance = [is_selfsend_sweep]
|
||||
(
|
||||
const boost::multiprecision::int128_t &input_sum_amount,
|
||||
const rct::xmr_amount fee,
|
||||
std::vector<CarrotPaymentProposalV1> &normal_payment_proposals,
|
||||
std::vector<CarrotPaymentProposalVerifiableSelfSendV1> &selfsend_payment_proposals
|
||||
)
|
||||
{
|
||||
// get pointers to proposal amounts and shuffle, excluding implicit selfsend
|
||||
const size_t n_outputs = normal_payment_proposals.size() + selfsend_payment_proposals.size();
|
||||
std::vector<rct::xmr_amount*> amount_ptrs;
|
||||
amount_ptrs.reserve(n_outputs);
|
||||
for (CarrotPaymentProposalV1 &normal_payment_proposal : normal_payment_proposals)
|
||||
amount_ptrs.push_back(&normal_payment_proposal.amount);
|
||||
if (is_selfsend_sweep)
|
||||
for (CarrotPaymentProposalVerifiableSelfSendV1 &selfsend_payment_proposal : selfsend_payment_proposals)
|
||||
amount_ptrs.push_back(&selfsend_payment_proposal.proposal.amount);
|
||||
std::shuffle(amount_ptrs.begin(), amount_ptrs.end(), crypto::random_device{});
|
||||
|
||||
// disburse amount equally amongst modifiable amounts
|
||||
const boost::multiprecision::int128_t output_sum_amount = input_sum_amount - fee;
|
||||
const rct::xmr_amount minimum_sweep_amount =
|
||||
boost::numeric_cast<rct::xmr_amount>(output_sum_amount / amount_ptrs.size());
|
||||
const size_t num_remaining =
|
||||
boost::numeric_cast<rct::xmr_amount>(output_sum_amount % amount_ptrs.size());
|
||||
CHECK_AND_ASSERT_THROW_MES(num_remaining < amount_ptrs.size(),
|
||||
"make carrot transaction proposal v1 sweep: bug: num_remaining >= n_outputs");
|
||||
for (size_t i = 0; i < amount_ptrs.size(); ++i)
|
||||
*amount_ptrs.at(i) = minimum_sweep_amount + (i < num_remaining ? 1 : 0);
|
||||
}; //end carve_fees_and_balance
|
||||
|
||||
// make unsigned transaction with sweep carving callback and selected inputs
|
||||
make_carrot_transaction_proposal_v1(normal_payment_proposals,
|
||||
selfsend_payment_proposals,
|
||||
fee_per_weight,
|
||||
extra,
|
||||
std::move(select_inputs),
|
||||
std::move(carve_fees_and_balance),
|
||||
s_view_balance_dev,
|
||||
k_view_dev,
|
||||
account_spend_pubkey,
|
||||
tx_proposal_out);
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
void make_pruned_transaction_from_carrot_proposal_v1(const CarrotTransactionProposalV1 &tx_proposal,
|
||||
const view_balance_secret_device *s_view_balance_dev,
|
||||
const view_incoming_key_device *k_view_dev,
|
||||
const crypto::public_key &account_spend_pubkey,
|
||||
cryptonote::transaction &pruned_tx_out)
|
||||
{
|
||||
// collect self-sends proposal cores
|
||||
std::vector<CarrotPaymentProposalSelfSendV1> selfsend_payment_proposal_cores;
|
||||
selfsend_payment_proposal_cores.reserve(tx_proposal.selfsend_payment_proposals.size());
|
||||
for (const auto &selfsend_payment_proposal : tx_proposal.selfsend_payment_proposals)
|
||||
selfsend_payment_proposal_cores.push_back(selfsend_payment_proposal.proposal);
|
||||
|
||||
// derive enote proposals
|
||||
std::vector<RCTOutputEnoteProposal> output_enote_proposals;
|
||||
encrypted_payment_id_t encrypted_payment_id;
|
||||
get_output_enote_proposals(tx_proposal.normal_payment_proposals,
|
||||
selfsend_payment_proposal_cores,
|
||||
tx_proposal.dummy_encrypted_payment_id,
|
||||
s_view_balance_dev,
|
||||
k_view_dev,
|
||||
tx_proposal.key_images_sorted.at(0),
|
||||
output_enote_proposals,
|
||||
encrypted_payment_id);
|
||||
|
||||
// collect enotes
|
||||
std::vector<CarrotEnoteV1> enotes;
|
||||
enotes.reserve(output_enote_proposals.size());
|
||||
for (const RCTOutputEnoteProposal &output_enote_proposal : output_enote_proposals)
|
||||
enotes.push_back(output_enote_proposal.enote);
|
||||
|
||||
// serialize tx
|
||||
pruned_tx_out = store_carrot_to_transaction_v1(enotes,
|
||||
tx_proposal.key_images_sorted,
|
||||
tx_proposal.fee,
|
||||
encrypted_payment_id);
|
||||
|
||||
// add extra payload and sort
|
||||
if (!tx_proposal.extra.empty())
|
||||
{
|
||||
std::vector<std::uint8_t> sorted_extra;
|
||||
pruned_tx_out.extra.insert(pruned_tx_out.extra.end(), tx_proposal.extra.cbegin(), tx_proposal.extra.cend());
|
||||
CHECK_AND_ASSERT_THROW_MES(cryptonote::sort_tx_extra(pruned_tx_out.extra, sorted_extra),
|
||||
"make_pruned_transaction_from_carrot_proposal_v1: failed to sort ");
|
||||
pruned_tx_out.extra = std::move(sorted_extra);
|
||||
}
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
} //namespace carrot
|
121
src/carrot_impl/carrot_tx_builder_utils.h
Normal file
121
src/carrot_impl/carrot_tx_builder_utils.h
Normal file
|
@ -0,0 +1,121 @@
|
|||
// Copyright (c) 2024, The Monero Project
|
||||
//
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without modification, are
|
||||
// permitted provided that the following conditions are met:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above copyright notice, this list of
|
||||
// conditions and the following disclaimer.
|
||||
//
|
||||
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
|
||||
// of conditions and the following disclaimer in the documentation and/or other
|
||||
// materials provided with the distribution.
|
||||
//
|
||||
// 3. Neither the name of the copyright holder nor the names of its contributors may be
|
||||
// used to endorse or promote products derived from this software without specific
|
||||
// prior written permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
|
||||
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
|
||||
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
||||
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
|
||||
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
#pragma once
|
||||
|
||||
//local headers
|
||||
#include "carrot_tx_builder_types.h"
|
||||
#include "cryptonote_basic/cryptonote_basic.h"
|
||||
|
||||
//third party headers
|
||||
|
||||
//standard headers
|
||||
#include <cstddef>
|
||||
|
||||
//forward declarations
|
||||
|
||||
namespace carrot
|
||||
{
|
||||
|
||||
static inline std::size_t get_carrot_default_tx_extra_size(const std::size_t num_outputs)
|
||||
{
|
||||
// @TODO: actually implement
|
||||
return num_outputs * (32 + 1) + (8 + 2);
|
||||
}
|
||||
|
||||
std::size_t get_carrot_coinbase_default_tx_extra_size(const std::size_t num_outputs);
|
||||
|
||||
static inline std::size_t get_fcmppp_tx_weight(const std::size_t num_inputs,
|
||||
const std::size_t num_outputs,
|
||||
const std::size_t tx_extra_size)
|
||||
{
|
||||
// @TODO: actually implement
|
||||
return 200 + num_inputs * 1000 + num_outputs * 100 + tx_extra_size;
|
||||
}
|
||||
|
||||
std::size_t get_fcmppp_coinbase_tx_weight(const std::size_t num_outputs,
|
||||
const std::size_t tx_extra_size);
|
||||
|
||||
static inline bool compare_input_key_images(const crypto::key_image &lhs, const crypto::key_image &rhs)
|
||||
{
|
||||
return memcmp(lhs.data, rhs.data, sizeof(crypto::key_image)) > 0;
|
||||
}
|
||||
|
||||
void make_carrot_transaction_proposal_v1(const std::vector<CarrotPaymentProposalV1> &normal_payment_proposals,
|
||||
const std::vector<CarrotPaymentProposalVerifiableSelfSendV1> &selfsend_payment_proposals,
|
||||
const rct::xmr_amount fee_per_weight,
|
||||
const std::vector<uint8_t> &extra,
|
||||
select_inputs_func_t &&select_inputs,
|
||||
carve_fees_and_balance_func_t &&carve_fees_and_balance,
|
||||
const view_balance_secret_device *s_view_balance_dev,
|
||||
const view_incoming_key_device *k_view_dev,
|
||||
const crypto::public_key &account_spend_pubkey,
|
||||
CarrotTransactionProposalV1 &tx_proposal_out);
|
||||
|
||||
void make_carrot_transaction_proposal_v1_transfer_subtractable(
|
||||
const std::vector<CarrotPaymentProposalV1> &normal_payment_proposals,
|
||||
const std::vector<CarrotPaymentProposalVerifiableSelfSendV1> &selfsend_payment_proposals,
|
||||
const rct::xmr_amount fee_per_weight,
|
||||
const std::vector<uint8_t> &extra,
|
||||
select_inputs_func_t &&select_inputs,
|
||||
const view_balance_secret_device *s_view_balance_dev,
|
||||
const view_incoming_key_device *k_view_dev,
|
||||
const crypto::public_key &account_spend_pubkey,
|
||||
const std::set<std::size_t> &subtractable_normal_payment_proposals,
|
||||
const std::set<std::size_t> &subtractable_selfsend_payment_proposals,
|
||||
CarrotTransactionProposalV1 &tx_proposal_out);
|
||||
|
||||
void make_carrot_transaction_proposal_v1_transfer(
|
||||
const std::vector<CarrotPaymentProposalV1> &normal_payment_proposals,
|
||||
const std::vector<CarrotPaymentProposalVerifiableSelfSendV1> &selfsend_payment_proposals,
|
||||
const rct::xmr_amount fee_per_weight,
|
||||
const std::vector<uint8_t> &extra,
|
||||
select_inputs_func_t &&select_inputs,
|
||||
const view_balance_secret_device *s_view_balance_dev,
|
||||
const view_incoming_key_device *k_view_dev,
|
||||
const crypto::public_key &account_spend_pubkey,
|
||||
CarrotTransactionProposalV1 &tx_proposal_out);
|
||||
|
||||
void make_carrot_transaction_proposal_v1_sweep(
|
||||
const std::vector<CarrotPaymentProposalV1> &normal_payment_proposals,
|
||||
const std::vector<CarrotPaymentProposalVerifiableSelfSendV1> &selfsend_payment_proposals,
|
||||
const rct::xmr_amount fee_per_weight,
|
||||
const std::vector<uint8_t> &extra,
|
||||
std::vector<CarrotSelectedInput> &&selected_inputs,
|
||||
const view_balance_secret_device *s_view_balance_dev,
|
||||
const view_incoming_key_device *k_view_dev,
|
||||
const crypto::public_key &account_spend_pubkey,
|
||||
CarrotTransactionProposalV1 &tx_proposal_out);
|
||||
|
||||
void make_pruned_transaction_from_carrot_proposal_v1(const CarrotTransactionProposalV1 &tx_proposal,
|
||||
const view_balance_secret_device *s_view_balance_dev,
|
||||
const view_incoming_key_device *k_view_dev,
|
||||
const crypto::public_key &account_spend_pubkey,
|
||||
cryptonote::transaction &pruned_tx_out);
|
||||
|
||||
} //namespace carrot
|
380
src/carrot_impl/carrot_tx_format_utils.cpp
Normal file
380
src/carrot_impl/carrot_tx_format_utils.cpp
Normal file
|
@ -0,0 +1,380 @@
|
|||
// Copyright (c) 2024, The Monero Project
|
||||
//
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without modification, are
|
||||
// permitted provided that the following conditions are met:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above copyright notice, this list of
|
||||
// conditions and the following disclaimer.
|
||||
//
|
||||
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
|
||||
// of conditions and the following disclaimer in the documentation and/or other
|
||||
// materials provided with the distribution.
|
||||
//
|
||||
// 3. Neither the name of the copyright holder nor the names of its contributors may be
|
||||
// used to endorse or promote products derived from this software without specific
|
||||
// prior written permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
|
||||
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
|
||||
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
||||
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
|
||||
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
//paired header
|
||||
#include "carrot_tx_format_utils.h"
|
||||
|
||||
//local headers
|
||||
#include "common/container_helpers.h"
|
||||
#include "cryptonote_basic/cryptonote_format_utils.h"
|
||||
#include "cryptonote_config.h"
|
||||
|
||||
//third party headers
|
||||
|
||||
//standard headers
|
||||
|
||||
#undef MONERO_DEFAULT_LOG_CATEGORY
|
||||
#define MONERO_DEFAULT_LOG_CATEGORY "carrot_impl"
|
||||
|
||||
static_assert(sizeof(mx25519_pubkey) == sizeof(crypto::public_key),
|
||||
"cannot use crypto::public_key as storage for X25519 keys since size is different");
|
||||
|
||||
namespace carrot
|
||||
{
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
template <class EnoteContainer>
|
||||
static void store_carrot_ephemeral_pubkeys_to_extra(const EnoteContainer &enotes, std::vector<uint8_t> &extra_inout)
|
||||
{
|
||||
const size_t nouts = enotes.size();
|
||||
const bool use_shared_ephemeral_pubkey = nouts == 2 && 0 == memcmp(
|
||||
&enotes.front().enote_ephemeral_pubkey,
|
||||
&enotes.back().enote_ephemeral_pubkey,
|
||||
sizeof(mx25519_pubkey));
|
||||
bool success = true;
|
||||
if (use_shared_ephemeral_pubkey)
|
||||
{
|
||||
const mx25519_pubkey &enote_ephemeral_pubkey = enotes.at(0).enote_ephemeral_pubkey;
|
||||
const crypto::public_key tx_pubkey = raw_byte_convert<crypto::public_key>(enote_ephemeral_pubkey);
|
||||
success = success && cryptonote::add_tx_pub_key_to_extra(extra_inout, tx_pubkey);
|
||||
}
|
||||
else // !use_shared_ephemeral_pubkey
|
||||
{
|
||||
std::vector<crypto::public_key> tx_pubkeys(nouts);
|
||||
for (size_t i = 0; i < nouts; ++i)
|
||||
{
|
||||
const mx25519_pubkey &enote_ephemeral_pubkey = enotes.at(i).enote_ephemeral_pubkey;
|
||||
tx_pubkeys[i] = raw_byte_convert<crypto::public_key>(enote_ephemeral_pubkey);
|
||||
}
|
||||
success = success && cryptonote::add_additional_tx_pub_keys_to_extra(extra_inout, tx_pubkeys);
|
||||
}
|
||||
CHECK_AND_ASSERT_THROW_MES(success, "add carrot ephemeral pubkeys to extra: failed to add tx_extra fields");
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
static mx25519_pubkey &De_ref(CarrotCoinbaseEnoteV1 &e) { return e.enote_ephemeral_pubkey; }
|
||||
static mx25519_pubkey &De_ref(CarrotEnoteV1 &e) { return e.enote_ephemeral_pubkey; }
|
||||
static mx25519_pubkey &De_ref(mx25519_pubkey &e) { return e; }
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
template <typename EnoteType>
|
||||
static bool try_load_carrot_ephemeral_pubkeys_from_extra(const std::vector<cryptonote::tx_extra_field> &extra_fields,
|
||||
std::vector<EnoteType> &enotes_inout)
|
||||
{
|
||||
cryptonote::tx_extra_pub_key tx_pubkey;
|
||||
cryptonote::tx_extra_additional_pub_keys tx_pubkeys;
|
||||
if (cryptonote::find_tx_extra_field_by_type(extra_fields, tx_pubkey))
|
||||
{
|
||||
De_ref(enotes_inout.front()) = raw_byte_convert<mx25519_pubkey>(tx_pubkey.pub_key);
|
||||
De_ref(enotes_inout.back()) = raw_byte_convert<mx25519_pubkey>(tx_pubkey.pub_key);
|
||||
return true;
|
||||
}
|
||||
else if (cryptonote::find_tx_extra_field_by_type(extra_fields, tx_pubkeys))
|
||||
{
|
||||
if (tx_pubkeys.data.size() != enotes_inout.size())
|
||||
return false;
|
||||
|
||||
for (size_t i = 0; i < enotes_inout.size(); ++i)
|
||||
De_ref(enotes_inout[i]) = raw_byte_convert<mx25519_pubkey>(tx_pubkeys.data.at(i));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
bool is_carrot_transaction_v1(const cryptonote::transaction_prefix &tx_prefix)
|
||||
{
|
||||
return tx_prefix.vout.at(0).target.type() == typeid(cryptonote::txout_to_carrot_v1);
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
bool try_load_carrot_extra_v1(
|
||||
const std::vector<cryptonote::tx_extra_field> &tx_extra_fields,
|
||||
std::vector<mx25519_pubkey> &enote_ephemeral_pubkeys_out,
|
||||
std::optional<encrypted_payment_id_t> &encrypted_payment_id_out)
|
||||
{
|
||||
//ephemeral pubkeys: D_e
|
||||
if (!try_load_carrot_ephemeral_pubkeys_from_extra(tx_extra_fields, enote_ephemeral_pubkeys_out))
|
||||
return false;
|
||||
|
||||
//encrypted payment ID: pid_enc
|
||||
encrypted_payment_id_out = std::nullopt;
|
||||
cryptonote::tx_extra_nonce extra_nonce;
|
||||
if (cryptonote::find_tx_extra_field_by_type(tx_extra_fields, extra_nonce))
|
||||
{
|
||||
crypto::hash8 pid_enc_8;
|
||||
if (cryptonote::get_encrypted_payment_id_from_tx_extra_nonce(extra_nonce.nonce, pid_enc_8))
|
||||
{
|
||||
encrypted_payment_id_t &pid_enc = encrypted_payment_id_out.emplace();
|
||||
pid_enc = raw_byte_convert<encrypted_payment_id_t>(pid_enc_8);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
cryptonote::transaction store_carrot_to_transaction_v1(const std::vector<CarrotEnoteV1> &enotes,
|
||||
const std::vector<crypto::key_image> &key_images,
|
||||
const rct::xmr_amount fee,
|
||||
const encrypted_payment_id_t encrypted_payment_id)
|
||||
{
|
||||
const size_t nins = key_images.size();
|
||||
const size_t nouts = enotes.size();
|
||||
|
||||
cryptonote::transaction tx;
|
||||
tx.pruned = true;
|
||||
tx.version = 2;
|
||||
tx.unlock_time = 0;
|
||||
tx.vin.reserve(nins);
|
||||
tx.vout.reserve(nouts);
|
||||
tx.extra.reserve(MAX_TX_EXTRA_SIZE);
|
||||
tx.rct_signatures.type = carrot_v1_rct_type;
|
||||
tx.rct_signatures.txnFee = fee;
|
||||
tx.rct_signatures.ecdhInfo.reserve(nouts);
|
||||
tx.rct_signatures.outPk.reserve(nouts);
|
||||
|
||||
//inputs
|
||||
for (const crypto::key_image &ki : key_images)
|
||||
{
|
||||
//L
|
||||
tx.vin.emplace_back(cryptonote::txin_to_key{ //@TODO: can save 2 bytes by using slim input type
|
||||
.amount = 0,
|
||||
.key_offsets = {},
|
||||
.k_image = ki
|
||||
});
|
||||
}
|
||||
|
||||
//outputs
|
||||
for (const CarrotEnoteV1 &enote : enotes)
|
||||
{
|
||||
//K_o,vt,anchor_enc
|
||||
tx.vout.push_back(cryptonote::tx_out{0, cryptonote::txout_to_carrot_v1{
|
||||
.key = enote.onetime_address,
|
||||
.view_tag = enote.view_tag,
|
||||
.encrypted_janus_anchor = enote.anchor_enc
|
||||
}});
|
||||
|
||||
//a_enc
|
||||
rct::ecdhTuple &ecdh_tuple = tools::add_element(tx.rct_signatures.ecdhInfo);
|
||||
memcpy(ecdh_tuple.amount.bytes, enote.amount_enc.bytes, sizeof(ecdh_tuple.amount));
|
||||
|
||||
//C_a
|
||||
tx.rct_signatures.outPk.push_back(rct::ctkey{rct::key{}, enote.amount_commitment});
|
||||
}
|
||||
|
||||
//ephemeral pubkeys: D_e
|
||||
store_carrot_ephemeral_pubkeys_to_extra(enotes, tx.extra);
|
||||
|
||||
//encrypted payment id: pid_enc
|
||||
const crypto::hash8 pid_enc_8 = raw_byte_convert<crypto::hash8>(encrypted_payment_id);
|
||||
cryptonote::blobdata extra_nonce;
|
||||
cryptonote::set_encrypted_payment_id_to_tx_extra_nonce(extra_nonce, pid_enc_8);
|
||||
CHECK_AND_ASSERT_THROW_MES(cryptonote::add_extra_nonce_to_tx_extra(tx.extra, extra_nonce),
|
||||
"store carrot to transaction v1: failed to add encrypted payment ID to tx_extra");
|
||||
|
||||
//finalize tx_extra
|
||||
CHECK_AND_ASSERT_THROW_MES(cryptonote::sort_tx_extra(tx.extra, tx.extra, /*allow_partial=*/false),
|
||||
"store carrot to transaction v1: failed to sort tx_extra");
|
||||
|
||||
return tx;
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
bool try_load_carrot_from_transaction_v1(const cryptonote::transaction &tx,
|
||||
std::vector<CarrotEnoteV1> &enotes_out,
|
||||
std::vector<crypto::key_image> &key_images_out,
|
||||
rct::xmr_amount &fee_out,
|
||||
std::optional<encrypted_payment_id_t> &encrypted_payment_id_out)
|
||||
{
|
||||
const rct::rctSigBase &rv = tx.rct_signatures;
|
||||
fee_out = rv.txnFee;
|
||||
|
||||
const size_t nins = tx.vin.size();
|
||||
const size_t nouts = tx.vout.size();
|
||||
|
||||
if (0 == nins)
|
||||
return false; // no input_context
|
||||
else if (nouts != rv.ecdhInfo.size())
|
||||
return false; // incorrect # of encrypted amounts
|
||||
else if (nouts != rv.outPk.size())
|
||||
return false; // incorrect # of amount commitments
|
||||
|
||||
//inputs
|
||||
key_images_out.resize(nins);
|
||||
for (size_t i = 0; i < nins; ++i)
|
||||
{
|
||||
const cryptonote::txin_to_key * const k = boost::strict_get<cryptonote::txin_to_key>(&tx.vin.at(i));
|
||||
if (nullptr == k)
|
||||
return false;
|
||||
|
||||
//L
|
||||
key_images_out[i] = k->k_image;
|
||||
}
|
||||
|
||||
//outputs
|
||||
enotes_out.resize(nouts);
|
||||
for (size_t i = 0; i < nouts; ++i)
|
||||
{
|
||||
const cryptonote::txout_target_v &t = tx.vout.at(i).target;
|
||||
const cryptonote::txout_to_carrot_v1 * const c = boost::strict_get<cryptonote::txout_to_carrot_v1>(&t);
|
||||
if (nullptr == c)
|
||||
return false;
|
||||
|
||||
//K_o
|
||||
enotes_out[i].onetime_address = c->key;
|
||||
|
||||
//vt
|
||||
enotes_out[i].view_tag = c->view_tag;
|
||||
|
||||
//anchor_enc
|
||||
enotes_out[i].anchor_enc = c->encrypted_janus_anchor;
|
||||
|
||||
//L_1
|
||||
enotes_out[i].tx_first_key_image = key_images_out.at(0);
|
||||
|
||||
//a_enc
|
||||
memcpy(enotes_out[i].amount_enc.bytes, rv.ecdhInfo.at(i).amount.bytes, sizeof(encrypted_amount_t));
|
||||
|
||||
//C_a
|
||||
enotes_out[i].amount_commitment = rv.outPk.at(i).mask;
|
||||
}
|
||||
|
||||
//parse tx_extra
|
||||
std::vector<cryptonote::tx_extra_field> extra_fields;
|
||||
if (!cryptonote::parse_tx_extra(tx.extra, extra_fields))
|
||||
return false;
|
||||
|
||||
//ephemeral pubkeys: D_e
|
||||
if (!try_load_carrot_ephemeral_pubkeys_from_extra(extra_fields, enotes_out))
|
||||
return false;
|
||||
|
||||
//encrypted payment ID: pid_enc
|
||||
encrypted_payment_id_out = std::nullopt;
|
||||
cryptonote::tx_extra_nonce extra_nonce;
|
||||
if (cryptonote::find_tx_extra_field_by_type(extra_fields, extra_nonce))
|
||||
{
|
||||
crypto::hash8 pid_enc_8;
|
||||
if (cryptonote::get_encrypted_payment_id_from_tx_extra_nonce(extra_nonce.nonce, pid_enc_8))
|
||||
encrypted_payment_id_out.emplace() = raw_byte_convert<encrypted_payment_id_t>(pid_enc_8);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
cryptonote::transaction store_carrot_to_coinbase_transaction_v1(
|
||||
const std::vector<CarrotCoinbaseEnoteV1> &enotes)
|
||||
{
|
||||
const size_t nouts = enotes.size();
|
||||
const std::uint64_t block_index = enotes.at(0).block_index;
|
||||
|
||||
cryptonote::transaction tx;
|
||||
tx.pruned = false;
|
||||
tx.version = 2;
|
||||
tx.unlock_time = block_index + CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW;
|
||||
tx.vin.reserve(1);
|
||||
tx.vout.reserve(nouts);
|
||||
tx.extra.reserve(MAX_TX_EXTRA_SIZE);
|
||||
tx.rct_signatures.type = rct::RCTTypeNull;
|
||||
|
||||
//input
|
||||
tx.vin.emplace_back(cryptonote::txin_gen{.height = static_cast<size_t>(block_index)});
|
||||
|
||||
//outputs
|
||||
for (const CarrotCoinbaseEnoteV1 &enote : enotes)
|
||||
{
|
||||
//K_o,vt,anchor_enc,a
|
||||
tx.vout.push_back(cryptonote::tx_out{enote.amount,
|
||||
cryptonote::txout_to_carrot_v1{
|
||||
.key = enote.onetime_address,
|
||||
.view_tag = enote.view_tag,
|
||||
.encrypted_janus_anchor = enote.anchor_enc
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//ephemeral pubkeys: D_e
|
||||
store_carrot_ephemeral_pubkeys_to_extra(enotes, tx.extra);
|
||||
|
||||
//we don't need to sort tx_extra since we only added one field
|
||||
//if you add more tx_extra fields here in the future, then please sort <3
|
||||
|
||||
return tx;
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
bool try_load_carrot_from_coinbase_transaction_v1(const cryptonote::transaction &tx,
|
||||
std::vector<CarrotCoinbaseEnoteV1> &enotes_out)
|
||||
{
|
||||
const size_t nins = tx.vin.size();
|
||||
const size_t nouts = tx.vout.size();
|
||||
|
||||
if (1 == nins)
|
||||
return false; // not coinbase
|
||||
|
||||
//input
|
||||
const cryptonote::txin_gen * const h = boost::strict_get<cryptonote::txin_gen>(&tx.vin.front());
|
||||
if (nullptr == h)
|
||||
return false;
|
||||
|
||||
//outputs
|
||||
enotes_out.resize(nouts);
|
||||
for (size_t i = 0; i < nouts; ++i)
|
||||
{
|
||||
//a
|
||||
enotes_out[i].amount = tx.vout.at(i).amount;
|
||||
|
||||
const cryptonote::txout_target_v &t = tx.vout.at(i).target;
|
||||
const cryptonote::txout_to_carrot_v1 * const c = boost::strict_get<cryptonote::txout_to_carrot_v1>(&t);
|
||||
if (nullptr == c)
|
||||
return false;
|
||||
|
||||
//K_o
|
||||
enotes_out[i].onetime_address = c->key;
|
||||
|
||||
//vt
|
||||
enotes_out[i].view_tag = c->view_tag;
|
||||
|
||||
//anchor_enc
|
||||
enotes_out[i].anchor_enc = c->encrypted_janus_anchor;
|
||||
|
||||
//block_index
|
||||
enotes_out[i].block_index = h->height;
|
||||
}
|
||||
|
||||
//parse tx_extra
|
||||
std::vector<cryptonote::tx_extra_field> extra_fields;
|
||||
if (!cryptonote::parse_tx_extra(tx.extra, extra_fields))
|
||||
return false;
|
||||
|
||||
//ephemeral pubkeys: D_e
|
||||
if (!try_load_carrot_ephemeral_pubkeys_from_extra(extra_fields, enotes_out))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
} //namespace carrot
|
122
src/carrot_impl/carrot_tx_format_utils.h
Normal file
122
src/carrot_impl/carrot_tx_format_utils.h
Normal file
|
@ -0,0 +1,122 @@
|
|||
// Copyright (c) 2024, The Monero Project
|
||||
//
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without modification, are
|
||||
// permitted provided that the following conditions are met:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above copyright notice, this list of
|
||||
// conditions and the following disclaimer.
|
||||
//
|
||||
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
|
||||
// of conditions and the following disclaimer in the documentation and/or other
|
||||
// materials provided with the distribution.
|
||||
//
|
||||
// 3. Neither the name of the copyright holder nor the names of its contributors may be
|
||||
// used to endorse or promote products derived from this software without specific
|
||||
// prior written permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
|
||||
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
|
||||
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
||||
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
|
||||
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
#pragma once
|
||||
|
||||
//local headers
|
||||
#include "carrot_core/carrot_enote_types.h"
|
||||
#include "cryptonote_basic/cryptonote_basic.h"
|
||||
#include "cryptonote_basic/tx_extra.h"
|
||||
|
||||
//third party headers
|
||||
|
||||
//standard headers
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <type_traits>
|
||||
|
||||
//forward declarations
|
||||
|
||||
namespace carrot
|
||||
{
|
||||
static constexpr std::uint8_t carrot_v1_rct_type = rct::RCTTypeFcmpPlusPlus;
|
||||
|
||||
template <typename T, typename U>
|
||||
static inline T raw_byte_convert(const U &u)
|
||||
{
|
||||
static_assert(sizeof(T) == sizeof(U));
|
||||
static_assert(std::is_trivially_copyable_v<T>);
|
||||
static_assert(std::has_unique_object_representations_v<T>);
|
||||
static_assert(std::has_unique_object_representations_v<U>);
|
||||
static_assert(alignof(T) == 1);
|
||||
static_assert(alignof(U) == 1);
|
||||
|
||||
T t;
|
||||
memcpy(&t, &u, sizeof(T));
|
||||
return t;
|
||||
}
|
||||
|
||||
/**
|
||||
* is_carrot_transaction_v1 - determine whether a transaction uses the Carrot addressing protocol
|
||||
*/
|
||||
bool is_carrot_transaction_v1(const cryptonote::transaction_prefix &tx_prefix);
|
||||
/**
|
||||
* try_load_carrot_extra_v1 - load Carrot info which is stored in tx_extra
|
||||
* param: tx_extra_fields -
|
||||
* outparam: enote_ephemeral_pubkeys_out - D_e
|
||||
* outparam: encrypted_payment_id_out - pid_enc
|
||||
*/
|
||||
bool try_load_carrot_extra_v1(
|
||||
const std::vector<cryptonote::tx_extra_field> &tx_extra_fields,
|
||||
std::vector<mx25519_pubkey> &enote_ephemeral_pubkeys_out,
|
||||
std::optional<encrypted_payment_id_t> &encrypted_payment_id_out);
|
||||
/**
|
||||
* brief: store_carrot_to_transaction_v1 - store non-coinbase Carrot info to a cryptonote::transaction
|
||||
* param: enotes -
|
||||
* param: key_images -
|
||||
* param: fee -
|
||||
* param: encrypted_payment_id - pid_enc
|
||||
* return: a fully populated, pruned, non-coinbase transaction containing given Carrot information
|
||||
*/
|
||||
cryptonote::transaction store_carrot_to_transaction_v1(const std::vector<CarrotEnoteV1> &enotes,
|
||||
const std::vector<crypto::key_image> &key_images,
|
||||
const rct::xmr_amount fee,
|
||||
const encrypted_payment_id_t encrypted_payment_id);
|
||||
/**
|
||||
* brief: load_carrot_from_transaction_v1 - load non-coinbase Carrot info from a cryptonote::transaction
|
||||
* param: tx -
|
||||
* outparam: enotes_out -
|
||||
* outparam: key_images_out -
|
||||
* outparam: fee_out -
|
||||
* outparam: encrypted_payment_id_out -
|
||||
* return: Carrot enotes, key images, fee, and encrypted pid contained within a non-coinbase transaction
|
||||
*/
|
||||
bool try_load_carrot_from_transaction_v1(const cryptonote::transaction &tx,
|
||||
std::vector<CarrotEnoteV1> &enotes_out,
|
||||
std::vector<crypto::key_image> &key_images_out,
|
||||
rct::xmr_amount &fee_out,
|
||||
std::optional<encrypted_payment_id_t> &encrypted_payment_id_out);
|
||||
/**
|
||||
* brief: store_carrot_to_coinbase_transaction_v1 - store coinbase Carrot info to a cryptonote::transaction
|
||||
* param: enotes -
|
||||
* param: block_index -
|
||||
* return: a full coinbase transaction containing given Carrot information
|
||||
*/
|
||||
cryptonote::transaction store_carrot_to_coinbase_transaction_v1(
|
||||
const std::vector<CarrotCoinbaseEnoteV1> &enotes);
|
||||
/**
|
||||
* brief: try_load_carrot_from_coinbase_transaction_v1 - load coinbase Carrot info from a cryptonote::transaction
|
||||
* param: tx -
|
||||
* outparam: enotes_out -
|
||||
* outparam: block_index_out -
|
||||
* return: Carrot coinbase enotes and block index contained within a coinbase transaction
|
||||
*/
|
||||
bool try_load_carrot_from_coinbase_transaction_v1(const cryptonote::transaction &tx,
|
||||
std::vector<CarrotCoinbaseEnoteV1> &enotes_out);
|
||||
|
||||
} //namespace carrot
|
441
src/carrot_impl/input_selection.cpp
Normal file
441
src/carrot_impl/input_selection.cpp
Normal file
|
@ -0,0 +1,441 @@
|
|||
// Copyright (c) 2024, The Monero Project
|
||||
//
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without modification, are
|
||||
// permitted provided that the following conditions are met:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above copyright notice, this list of
|
||||
// conditions and the following disclaimer.
|
||||
//
|
||||
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
|
||||
// of conditions and the following disclaimer in the documentation and/or other
|
||||
// materials provided with the distribution.
|
||||
//
|
||||
// 3. Neither the name of the copyright holder nor the names of its contributors may be
|
||||
// used to endorse or promote products derived from this software without specific
|
||||
// prior written permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
|
||||
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
|
||||
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
||||
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
|
||||
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
//paired header
|
||||
#include "input_selection.h"
|
||||
|
||||
//local headers
|
||||
#include "carrot_core/config.h"
|
||||
#include "misc_log_ex.h"
|
||||
|
||||
//third party headers
|
||||
|
||||
//standard headers
|
||||
#include <algorithm>
|
||||
|
||||
#undef MONERO_DEFAULT_LOG_CATEGORY
|
||||
#define MONERO_DEFAULT_LOG_CATEGORY "carrot_impl"
|
||||
|
||||
namespace carrot
|
||||
{
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
static int compare_input_candidate_same_ki(const CarrotPreSelectedInput &lhs, const CarrotPreSelectedInput &rhs)
|
||||
{
|
||||
CHECK_AND_ASSERT_THROW_MES(lhs.core.key_image == rhs.core.key_image,
|
||||
"compare_input_candidate_same_ki: this function is not meant to compare inputs of different key images");
|
||||
|
||||
// first prefer the higher amount
|
||||
if (lhs.core.amount < rhs.core.amount)
|
||||
return -1;
|
||||
else if (lhs.core.amount > rhs.core.amount)
|
||||
return 1;
|
||||
|
||||
// then prefer older
|
||||
if (lhs.block_index < rhs.block_index)
|
||||
return 1;
|
||||
else if (lhs.block_index > rhs.block_index)
|
||||
return -1;
|
||||
|
||||
// It should be computationally intractable for lhs.is_external != rhs.is_external, but I haven't
|
||||
// looked into it too deeply. I guess you would want to prefer whichever one !is_external.
|
||||
|
||||
return 0;
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
static std::set<size_t> set_union(const std::set<size_t> &a, const std::set<size_t> &b)
|
||||
{
|
||||
std::set<size_t> c = a;
|
||||
c.merge(std::set<size_t>(b));
|
||||
return c;
|
||||
};
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
static void stable_sort_indices_by_amount(const epee::span<const CarrotPreSelectedInput> input_candidates,
|
||||
std::vector<size_t> &indices_inout)
|
||||
{
|
||||
std::stable_sort(indices_inout.begin(), indices_inout.end(),
|
||||
[input_candidates](const size_t a, const size_t b) -> bool
|
||||
{
|
||||
CHECK_AND_ASSERT_THROW_MES(a < input_candidates.size() && b < input_candidates.size(),
|
||||
"input candidate index out of range");
|
||||
return input_candidates[a].core.amount < input_candidates[b].core.amount;
|
||||
});
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
static void stable_sort_indices_by_block_index(const epee::span<const CarrotPreSelectedInput> input_candidates,
|
||||
std::vector<size_t> &indices_inout)
|
||||
{
|
||||
std::stable_sort(indices_inout.begin(), indices_inout.end(),
|
||||
[input_candidates](const size_t a, const size_t b) -> bool
|
||||
{
|
||||
CHECK_AND_ASSERT_THROW_MES(a < input_candidates.size() && b < input_candidates.size(),
|
||||
"input candidate index out of range");
|
||||
return input_candidates[a].block_index < input_candidates[b].block_index;
|
||||
});
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
static std::pair<size_t, boost::multiprecision::int128_t> input_count_for_max_usable_money(
|
||||
const epee::span<const CarrotPreSelectedInput> input_candidates,
|
||||
const std::set<size_t> selectable_inputs,
|
||||
const std::map<size_t, boost::multiprecision::int128_t> &required_money_by_input_count)
|
||||
{
|
||||
// Returns (N, X) where the X is the sum of the amounts of the greatest N <= CARROT_MAX_TX_INPUTS
|
||||
// inputs from selectable_inputs, maximizing X - R(N). R(N) is the required money for this
|
||||
// transaction, including fee, for given input count N. This should correctly handle "almost-dust":
|
||||
// inputs which are less than the fee, but greater than or equal to the difference of the fee
|
||||
// compared to excluding that input. If this function returns N == 0, then there aren't enough
|
||||
// usable funds, i.e. no N exists such that X - R(N) > 0.
|
||||
|
||||
size_t num_ins = 0;
|
||||
boost::multiprecision::int128_t cumulative_input_sum = 0;
|
||||
boost::multiprecision::int128_t last_required_money = 0;
|
||||
|
||||
std::vector<size_t> selectable_inputs_vec(selectable_inputs.cbegin(), selectable_inputs.cend());
|
||||
stable_sort_indices_by_amount(input_candidates, selectable_inputs_vec);
|
||||
|
||||
// for selectable indices in descending amount...
|
||||
for (auto it = selectable_inputs_vec.crbegin(); it != selectable_inputs_vec.crend(); ++it)
|
||||
{
|
||||
++num_ins;
|
||||
if (num_ins > CARROT_MAX_TX_INPUTS)
|
||||
break;
|
||||
|
||||
const rct::xmr_amount amount = input_candidates[*it].core.amount;
|
||||
|
||||
if (amount < required_money_by_input_count.at(num_ins) - last_required_money)
|
||||
{
|
||||
// then this input doesn't pay for itself, rollback previous state and break
|
||||
// since all next inputs will have same amount or less
|
||||
--num_ins;
|
||||
break;
|
||||
}
|
||||
|
||||
cumulative_input_sum += amount;
|
||||
}
|
||||
|
||||
return {num_ins, cumulative_input_sum};
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
select_inputs_func_t make_single_transfer_input_selector(
|
||||
const epee::span<const CarrotPreSelectedInput> input_candidates,
|
||||
const epee::span<const input_selection_policy_t> policies,
|
||||
const std::uint32_t flags,
|
||||
std::set<size_t> *selected_input_indices_out)
|
||||
{
|
||||
using namespace InputSelectionFlags;
|
||||
|
||||
CHECK_AND_ASSERT_THROW_MES(!policies.empty(),
|
||||
"make_single_transfer_input_selector: no input selection policies provided");
|
||||
|
||||
// Sanity check flags
|
||||
const bool confused_qfs = (flags & ALLOW_PRE_CARROT_INPUTS_IN_NORMAL_TRANSFERS) &&
|
||||
!(flags & ALLOW_EXTERNAL_INPUTS_IN_NORMAL_TRANSFERS);
|
||||
CHECK_AND_ASSERT_THROW_MES(!confused_qfs,
|
||||
"make single transfer input selector: It does not make sense to allow pre-carrot inputs in normal transfers, "
|
||||
"but not external carrot inputs.");
|
||||
|
||||
// input selector :)
|
||||
return [=](const boost::multiprecision::int128_t &nominal_output_sum,
|
||||
const std::map<std::size_t, rct::xmr_amount> &fee_by_input_count,
|
||||
const std::size_t num_normal_payment_proposals,
|
||||
const std::size_t num_selfsend_payment_proposals,
|
||||
std::vector<CarrotSelectedInput> &selected_inputs_out)
|
||||
{
|
||||
// 1. Compile map of best input candidates by key image to mitigate the "burning bug" for legacy enotes
|
||||
std::unordered_map<crypto::key_image, size_t> best_input_by_key_image;
|
||||
for (size_t i = 0; i < input_candidates.size(); ++i)
|
||||
{
|
||||
const CarrotPreSelectedInput &input_candidate = input_candidates[i];
|
||||
auto it = best_input_by_key_image.find(input_candidate.core.key_image);
|
||||
if (it == best_input_by_key_image.end())
|
||||
{
|
||||
best_input_by_key_image[input_candidate.core.key_image] = i;
|
||||
}
|
||||
else
|
||||
{
|
||||
const CarrotPreSelectedInput &other_input_candidate = input_candidates[it->second];
|
||||
if (compare_input_candidate_same_ki(other_input_candidate, input_candidate) < 0)
|
||||
it->second = i;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Collect set of non-burned inputs
|
||||
std::set<size_t> all_non_burned_inputs;
|
||||
for (const auto &best_input : best_input_by_key_image)
|
||||
all_non_burned_inputs.insert(best_input.second);
|
||||
|
||||
// 3. Partition into:
|
||||
// a) Pre-carrot (no quantum forward secrecy)
|
||||
// b) External carrot (quantum forward secret if public address not known)
|
||||
// c) Internal carrot (always quantum forward secret unless secret keys known)
|
||||
std::set<size_t> pre_carrot_inputs;
|
||||
std::set<size_t> external_carrot_inputs;
|
||||
std::set<size_t> internal_inputs;
|
||||
for (size_t candidate_idx : all_non_burned_inputs)
|
||||
{
|
||||
if (input_candidates[candidate_idx].is_pre_carrot)
|
||||
pre_carrot_inputs.insert(candidate_idx);
|
||||
else if (input_candidates[candidate_idx].is_external)
|
||||
external_carrot_inputs.insert(candidate_idx);
|
||||
else
|
||||
internal_inputs.insert(candidate_idx);
|
||||
}
|
||||
|
||||
// 4. Calculate minimum required input money sum for a given input count
|
||||
const bool subtract_fee = flags & IS_KNOWN_FEE_SUBTRACTABLE;
|
||||
std::map<size_t, boost::multiprecision::int128_t> required_money_by_input_count;
|
||||
for (const auto &fee_and_input_count : fee_by_input_count)
|
||||
{
|
||||
required_money_by_input_count[fee_and_input_count.first] =
|
||||
nominal_output_sum + (subtract_fee ? 0 : fee_and_input_count.second);
|
||||
}
|
||||
|
||||
// 5. Calculate misc features
|
||||
const bool must_use_internal = !(flags & ALLOW_EXTERNAL_INPUTS_IN_NORMAL_TRANSFERS) &&
|
||||
(num_normal_payment_proposals != 0);
|
||||
const bool allow_mixed_externality = (flags & ALLOW_MIXED_INTERNAL_EXTERNAL) &&
|
||||
!must_use_internal;
|
||||
const bool must_use_carrot = !(flags & ALLOW_PRE_CARROT_INPUTS_IN_NORMAL_TRANSFERS) &&
|
||||
(num_normal_payment_proposals != 0);
|
||||
const bool allow_mixed_carrotness = (flags & ALLOW_MIXED_CARROT_PRE_CARROT) &&
|
||||
!must_use_carrot;
|
||||
|
||||
// We should prefer to spend non-forward-secret enotes in transactions where all the outputs are going back to
|
||||
// ourself. Otherwise, if we spend these enotes while transferring money to another entity, an external observer
|
||||
// who A) has a quantum computer, and B) knows one of their public addresses, will be able to trace the money
|
||||
// transfer. Such an observer will always be able to tell which view-incoming keys / accounts these
|
||||
// non-forward-secrets enotes belong to, their amounts, and where they're spent. So since they already know that
|
||||
// information, churning back to oneself doesn't actually reveal that much more additional information.
|
||||
const bool prefer_non_fs = num_normal_payment_proposals == 0;
|
||||
CHECK_AND_ASSERT_THROW_MES(!must_use_internal || !prefer_non_fs,
|
||||
"make_single_transfer_input_selector: bug: must_use_internal AND prefer_non_fs are true");
|
||||
|
||||
// There is no "prefer pre-carrot" variable since in the case that we prefer spending non-forward-secret, we
|
||||
// always prefer first spending pre-carrot over carrot, if it is allowed
|
||||
|
||||
// 6. Short-hand functor for dispatching input selection on a subset of inputs
|
||||
// Note: Result goes into `selected_inputs_indices`. If already populated, then this functor does nothing
|
||||
std::set<size_t> selected_inputs_indices;
|
||||
const auto try_dispatch_input_selection =
|
||||
[&](const std::set<size_t> &selectable_indices)
|
||||
{
|
||||
// Return early if already selected inputs or no available selectable
|
||||
const bool already_selected = !selected_inputs_indices.empty();
|
||||
if (already_selected || selectable_indices.empty())
|
||||
return;
|
||||
|
||||
// Return early if not enough money in this selectable set...
|
||||
const auto max_usable_money = input_count_for_max_usable_money(input_candidates,
|
||||
selectable_indices,
|
||||
required_money_by_input_count);
|
||||
const bool enough_money = max_usable_money.first > 0;
|
||||
if (!enough_money)
|
||||
return;
|
||||
|
||||
// For each passed policy and while not already selected inputs, dispatch policy...
|
||||
for (size_t policy_idx = 0; policy_idx < policies.size() && selected_inputs_indices.empty(); ++policy_idx)
|
||||
policies[policy_idx](input_candidates,
|
||||
selectable_indices,
|
||||
required_money_by_input_count,
|
||||
selected_inputs_indices);
|
||||
|
||||
// Check that returned selected indices were actually selectable
|
||||
for (const size_t selected_inputs_index : selected_inputs_indices)
|
||||
CHECK_AND_ASSERT_THROW_MES(selectable_indices.count(selected_inputs_index),
|
||||
"make_single_transfer_input_selector: bug in policy: returned unselectable index");
|
||||
};
|
||||
|
||||
// 8. Try dispatching for non-forward-secret input subsets, if preferred in this context
|
||||
if (prefer_non_fs)
|
||||
{
|
||||
// try getting rid of pre-carrot enotes first, if allowed
|
||||
if (!must_use_carrot)
|
||||
try_dispatch_input_selection(pre_carrot_inputs);
|
||||
|
||||
// ... then external carrot
|
||||
try_dispatch_input_selection(external_carrot_inputs);
|
||||
}
|
||||
|
||||
// 9. Try dispatching for internal
|
||||
try_dispatch_input_selection(internal_inputs);
|
||||
|
||||
// 10. Try dispatching for non-FS *after* internal, if allowed and not already tried
|
||||
if (!must_use_internal || !prefer_non_fs)
|
||||
{
|
||||
// Spending non-FS inputs in a normal transfer transaction is not ideal, but at least
|
||||
// when partition it like this, we aren't "dirtying" the carrot with the pre-carrot, and
|
||||
// the internal with the external
|
||||
if (!must_use_carrot)
|
||||
try_dispatch_input_selection(pre_carrot_inputs);
|
||||
try_dispatch_input_selection(external_carrot_inputs);
|
||||
}
|
||||
|
||||
// 11. Try dispatching for all non-FS (mixed pre-carrot & carrot external), if allowed
|
||||
if (allow_mixed_carrotness)
|
||||
{
|
||||
// We're mixing carrot/pre-carrot spends here, but avoiding "dirtying" the internal
|
||||
try_dispatch_input_selection(set_union(pre_carrot_inputs, external_carrot_inputs));
|
||||
}
|
||||
|
||||
// 12. Try dispatching for all carrot, if allowed
|
||||
if (allow_mixed_externality)
|
||||
{
|
||||
// We're mixing internal & external carrot spends here, but avoiding "dirtying" the
|
||||
// carrot spends with pre-carrot spends. This will be quantum forward secret iff the
|
||||
// adversary doesn't know one of your public addresses
|
||||
try_dispatch_input_selection(set_union(external_carrot_inputs, internal_inputs));
|
||||
}
|
||||
|
||||
//! @TODO: MRL discussion about whether step 11 or step 12 should go first. In other words,
|
||||
// do we prefer to avoid dirtying internal, and protect against quantum adversaries
|
||||
// who know your public addresses? Or do we prefer to avoid dirtying w/ pre-carrot,
|
||||
// and protect against quantum adversaries with no special knowledge of your public
|
||||
// addresses, but whose attacks are only relevant when spending pre-FCMP++ enotes?
|
||||
|
||||
// 13. Try dispatching for everything, if allowed
|
||||
if (allow_mixed_carrotness && allow_mixed_externality)
|
||||
try_dispatch_input_selection(all_non_burned_inputs);
|
||||
|
||||
// Notice that we don't combine just the pre_carrot_inputs and internal_inputs by themselves
|
||||
|
||||
// 14. Sanity check indices
|
||||
CHECK_AND_ASSERT_THROW_MES(!selected_inputs_indices.empty(),
|
||||
"make_single_transfer_input_selector: input selection failed");
|
||||
CHECK_AND_ASSERT_THROW_MES(*selected_inputs_indices.crbegin() < input_candidates.size(),
|
||||
"make_single_transfer_input_selector: bug: selected inputs index out of range");
|
||||
|
||||
// 15. Do a greedy search for inputs whose amount doesn't pay for itself and drop them, logging debug messages
|
||||
// Note: this also happens to be optimal if the fee difference between each input count is constant
|
||||
bool should_search_for_dust = !(flags & ALLOW_DUST);
|
||||
while (should_search_for_dust && selected_inputs_indices.size() > CARROT_MIN_TX_INPUTS)
|
||||
{
|
||||
should_search_for_dust = false; // only loop again if we remove an input below
|
||||
const boost::multiprecision::int128_t fee_diff =
|
||||
required_money_by_input_count.at(selected_inputs_indices.size()) -
|
||||
required_money_by_input_count.at(selected_inputs_indices.size() - 1);
|
||||
CHECK_AND_ASSERT_THROW_MES(fee_diff >= 0,
|
||||
"make_single_transfer_input_selector: bug: fee is expected to be higher with fewer inputs");
|
||||
for (auto it = selected_inputs_indices.begin(); it != selected_inputs_indices.end(); ++it)
|
||||
{
|
||||
const CarrotPreSelectedInput &input_candidate = input_candidates[*it];
|
||||
if (input_candidate.core.amount < fee_diff)
|
||||
{
|
||||
MDEBUG("make_single_transfer_input_selector: dropping dusty input "
|
||||
<< input_candidate.core.key_image << " with amount " << input_candidate.core.amount
|
||||
<< ", which is less than the difference in fee of this transaction with it: " << fee_diff);
|
||||
selected_inputs_indices.erase(it);
|
||||
should_search_for_dust = true;
|
||||
break; // break out of inner `for` loop so we can recalculate `fee_diff`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 16. Check the sum of input amounts is great enough
|
||||
const size_t num_selected = selected_inputs_indices.size();
|
||||
const boost::multiprecision::int128_t required_money = required_money_by_input_count.at(num_selected);
|
||||
boost::multiprecision::int128_t input_amount_sum = 0;
|
||||
for (const size_t idx : selected_inputs_indices)
|
||||
input_amount_sum += input_candidates[idx].core.amount;
|
||||
CHECK_AND_ASSERT_THROW_MES(input_amount_sum >= required_money,
|
||||
"make_single_transfer_input_selector: bug: input selection returned successful without enough funds");
|
||||
|
||||
// 17. Collect selected inputs
|
||||
selected_inputs_out.clear();
|
||||
selected_inputs_out.reserve(num_selected);
|
||||
for (size_t selected_input_index : selected_inputs_indices)
|
||||
selected_inputs_out.push_back(input_candidates[selected_input_index].core);
|
||||
|
||||
if (selected_input_indices_out != nullptr)
|
||||
*selected_input_indices_out = std::move(selected_inputs_indices);
|
||||
};
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
namespace ispolicy
|
||||
{
|
||||
void select_two_inputs_prefer_oldest(const epee::span<const CarrotPreSelectedInput> input_candidates,
|
||||
const std::set<size_t> &selectable_inputs,
|
||||
const std::map<size_t, boost::multiprecision::int128_t> &required_money_by_input_count,
|
||||
std::set<size_t> &selected_inputs_indices_out)
|
||||
{
|
||||
// calculate required money and fee diff from one to two inputs
|
||||
const boost::multiprecision::int128_t required_money = required_money_by_input_count.at(2);
|
||||
const rct::xmr_amount fee_diff = boost::numeric_cast<rct::xmr_amount>(required_money -
|
||||
required_money_by_input_count.at(1));
|
||||
|
||||
// copy selectable_inputs, excluding dust, then sort by ascending block index
|
||||
std::vector<size_t> selectable_inputs_by_bi;
|
||||
selectable_inputs_by_bi.reserve(selectable_inputs.size());
|
||||
for (size_t idx : selectable_inputs)
|
||||
if (input_candidates[idx].core.amount > fee_diff)
|
||||
selectable_inputs_by_bi.push_back(idx);
|
||||
stable_sort_indices_by_block_index(input_candidates, selectable_inputs_by_bi);
|
||||
|
||||
// then copy again and *stable* sort by amount
|
||||
std::vector<size_t> selectable_inputs_by_amount_bi = selectable_inputs_by_bi;
|
||||
stable_sort_indices_by_amount(input_candidates, selectable_inputs_by_amount_bi);
|
||||
|
||||
// for each input in ascending block index order...
|
||||
for (size_t low_bi_input : selectable_inputs_by_bi)
|
||||
{
|
||||
// calculate how much we need in a corresponding input to this one
|
||||
const rct::xmr_amount old_amount = input_candidates[low_bi_input].core.amount;
|
||||
const boost::multiprecision::int128_t required_money_in_other_128 = (required_money > old_amount)
|
||||
? (required_money - old_amount) : 0;
|
||||
if (required_money_in_other_128 >= std::numeric_limits<rct::xmr_amount>::max())
|
||||
continue;
|
||||
const rct::xmr_amount required_money_in_other =
|
||||
boost::numeric_cast<rct::xmr_amount>(required_money_in_other_128);
|
||||
|
||||
// do a binary search for an input with at least that amount
|
||||
auto other_it = std::lower_bound(selectable_inputs_by_amount_bi.cbegin(),
|
||||
selectable_inputs_by_amount_bi.cend(),
|
||||
required_money_in_other,
|
||||
[input_candidates](size_t selectable_index, rct::xmr_amount required_money_in_other) -> bool
|
||||
{ return input_candidates[selectable_index].core.amount < required_money_in_other; });
|
||||
|
||||
// check that the iterator is in bounds and the complementary input isn't equal to the first
|
||||
if (other_it == selectable_inputs_by_amount_bi.cend())
|
||||
continue;
|
||||
else if (*other_it == low_bi_input)
|
||||
++other_it; // can't choose same input twice
|
||||
|
||||
if (other_it == selectable_inputs_by_amount_bi.cend())
|
||||
continue;
|
||||
|
||||
// we found a match !
|
||||
selected_inputs_indices_out = {low_bi_input, *other_it};
|
||||
return;
|
||||
}
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
} //namespace ispolicy
|
||||
} //namespace carrot
|
87
src/carrot_impl/input_selection.h
Normal file
87
src/carrot_impl/input_selection.h
Normal file
|
@ -0,0 +1,87 @@
|
|||
// 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.
|
||||
|
||||
#pragma once
|
||||
|
||||
//local headers
|
||||
#include "carrot_tx_builder_types.h"
|
||||
|
||||
//third party headers
|
||||
|
||||
//standard headers
|
||||
#include <set>
|
||||
|
||||
//forward declarations
|
||||
|
||||
namespace carrot
|
||||
{
|
||||
struct CarrotPreSelectedInput
|
||||
{
|
||||
CarrotSelectedInput core;
|
||||
|
||||
bool is_pre_carrot;
|
||||
bool is_external;
|
||||
uint64_t block_index;
|
||||
};
|
||||
|
||||
namespace InputSelectionFlags
|
||||
{
|
||||
// Quantum forward secrecy (ON = unsafe)
|
||||
static constexpr std::uint32_t ALLOW_EXTERNAL_INPUTS_IN_NORMAL_TRANSFERS = 1 << 0;
|
||||
static constexpr std::uint32_t ALLOW_PRE_CARROT_INPUTS_IN_NORMAL_TRANSFERS = 1 << 1;
|
||||
static constexpr std::uint32_t ALLOW_MIXED_INTERNAL_EXTERNAL = 1 << 2;
|
||||
static constexpr std::uint32_t ALLOW_MIXED_CARROT_PRE_CARROT = 1 << 3;
|
||||
|
||||
// Amount handling
|
||||
static constexpr std::uint32_t IS_KNOWN_FEE_SUBTRACTABLE = 1 << 4;
|
||||
static constexpr std::uint32_t ALLOW_DUST = 1 << 5;
|
||||
}
|
||||
|
||||
using input_selection_policy_t = std::function<void(
|
||||
const epee::span<const CarrotPreSelectedInput>, // input candidates
|
||||
const std::set<std::size_t>&, // selectable subset indices
|
||||
const std::map<size_t, boost::multiprecision::int128_t>&, // required money by input count
|
||||
std::set<std::size_t>& // selected indices
|
||||
)>;
|
||||
|
||||
select_inputs_func_t make_single_transfer_input_selector(
|
||||
const epee::span<const CarrotPreSelectedInput> input_candidates,
|
||||
const epee::span<const input_selection_policy_t> policies,
|
||||
const std::uint32_t flags,
|
||||
std::set<size_t> *selected_input_indices_out);
|
||||
|
||||
namespace ispolicy
|
||||
{
|
||||
void select_two_inputs_prefer_oldest(
|
||||
const epee::span<const CarrotPreSelectedInput>,
|
||||
const std::set<std::size_t>&,
|
||||
const std::map<size_t, boost::multiprecision::int128_t>&,
|
||||
std::set<std::size_t>&);
|
||||
|
||||
} //namespace ispolicy
|
||||
} //namespace carrot
|
|
@ -618,6 +618,12 @@ namespace crypto {
|
|||
ge_p1p1_to_p3(&res, &point2);
|
||||
}
|
||||
|
||||
void crypto_ops::derive_key_image_generator(const public_key &pub, ec_point &ki_gen) {
|
||||
ge_p3 point;
|
||||
hash_to_ec(pub, point);
|
||||
ge_p3_tobytes(&ki_gen, &point);
|
||||
}
|
||||
|
||||
void crypto_ops::generate_key_image(const public_key &pub, const secret_key &sec, key_image &image) {
|
||||
ge_p3 point;
|
||||
ge_p2 point2;
|
||||
|
|
|
@ -79,6 +79,10 @@ namespace crypto {
|
|||
friend class crypto_ops;
|
||||
};
|
||||
|
||||
POD_CLASS key_image_y: ec_point {
|
||||
friend class crypto_ops;
|
||||
};
|
||||
|
||||
POD_CLASS signature {
|
||||
ec_scalar c, r;
|
||||
friend class crypto_ops;
|
||||
|
@ -129,6 +133,8 @@ namespace crypto {
|
|||
friend void generate_tx_proof_v1(const hash &, const public_key &, const public_key &, const boost::optional<public_key> &, const public_key &, const secret_key &, signature &);
|
||||
static bool check_tx_proof(const hash &, const public_key &, const public_key &, const boost::optional<public_key> &, const public_key &, const signature &, const int);
|
||||
friend bool check_tx_proof(const hash &, const public_key &, const public_key &, const boost::optional<public_key> &, const public_key &, const signature &, const int);
|
||||
static void derive_key_image_generator(const public_key &, ec_point &);
|
||||
friend void derive_key_image_generator(const public_key &, ec_point &);
|
||||
static void generate_key_image(const public_key &, const secret_key &, key_image &);
|
||||
friend void generate_key_image(const public_key &, const secret_key &, key_image &);
|
||||
static void generate_ring_signature(const hash &, const key_image &,
|
||||
|
@ -254,6 +260,10 @@ namespace crypto {
|
|||
return crypto_ops::check_tx_proof(prefix_hash, R, A, B, D, sig, version);
|
||||
}
|
||||
|
||||
inline void derive_key_image_generator(const public_key &pub, ec_point &ki_gen) {
|
||||
crypto_ops::derive_key_image_generator(pub, ki_gen);
|
||||
}
|
||||
|
||||
/* To send money to a key:
|
||||
* * The sender generates an ephemeral key and includes it in transaction output.
|
||||
* * To spend the money, the receiver generates a key image from it.
|
||||
|
|
|
@ -43,6 +43,8 @@
|
|||
#include "serialization/debug_archive.h"
|
||||
#include "serialization/crypto.h"
|
||||
#include "serialization/keyvalue_serialization.h" // eepe named serialization
|
||||
#include "carrot_core/core_types.h"
|
||||
#include "carrot_impl/carrot_chain_serialization.h"
|
||||
#include "cryptonote_config.h"
|
||||
#include "crypto/crypto.h"
|
||||
#include "crypto/hash.h"
|
||||
|
@ -58,14 +60,19 @@ namespace cryptonote
|
|||
|
||||
/* outputs */
|
||||
|
||||
struct txout_to_script
|
||||
struct txout_to_carrot_v1
|
||||
{
|
||||
std::vector<crypto::public_key> keys;
|
||||
std::vector<uint8_t> script;
|
||||
crypto::public_key key; // K_o
|
||||
carrot::view_tag_t view_tag; // vt
|
||||
carrot::encrypted_janus_anchor_t encrypted_janus_anchor; // anchor_enc
|
||||
|
||||
// Encrypted amount a_enc and amount commitment C_a are stored in rct::rctSigBase
|
||||
// This allows for reuse of this output type between coinbase and non-coinbase txs
|
||||
|
||||
BEGIN_SERIALIZE_OBJECT()
|
||||
FIELD(keys)
|
||||
FIELD(script)
|
||||
FIELD(key)
|
||||
FIELD(view_tag)
|
||||
FIELD(encrypted_janus_anchor)
|
||||
END_SERIALIZE()
|
||||
};
|
||||
|
||||
|
@ -122,16 +129,7 @@ namespace cryptonote
|
|||
|
||||
struct txin_to_scripthash
|
||||
{
|
||||
crypto::hash prev;
|
||||
size_t prevout;
|
||||
txout_to_script script;
|
||||
std::vector<uint8_t> sigset;
|
||||
|
||||
BEGIN_SERIALIZE_OBJECT()
|
||||
FIELD(prev)
|
||||
VARINT_FIELD(prevout)
|
||||
FIELD(script)
|
||||
FIELD(sigset)
|
||||
END_SERIALIZE()
|
||||
};
|
||||
|
||||
|
@ -151,7 +149,7 @@ namespace cryptonote
|
|||
|
||||
typedef boost::variant<txin_gen, txin_to_script, txin_to_scripthash, txin_to_key> txin_v;
|
||||
|
||||
typedef boost::variant<txout_to_script, txout_to_scripthash, txout_to_key, txout_to_tagged_key> txout_target_v;
|
||||
typedef boost::variant<txout_to_carrot_v1, txout_to_scripthash, txout_to_key, txout_to_tagged_key> txout_target_v;
|
||||
|
||||
//typedef std::pair<uint64_t, txout> out_t;
|
||||
struct tx_out
|
||||
|
@ -573,7 +571,7 @@ VARIANT_TAG(binary_archive, cryptonote::txin_gen, 0xff);
|
|||
VARIANT_TAG(binary_archive, cryptonote::txin_to_script, 0x0);
|
||||
VARIANT_TAG(binary_archive, cryptonote::txin_to_scripthash, 0x1);
|
||||
VARIANT_TAG(binary_archive, cryptonote::txin_to_key, 0x2);
|
||||
VARIANT_TAG(binary_archive, cryptonote::txout_to_script, 0x0);
|
||||
VARIANT_TAG(binary_archive, cryptonote::txout_to_carrot_v1, 0x0);
|
||||
VARIANT_TAG(binary_archive, cryptonote::txout_to_scripthash, 0x1);
|
||||
VARIANT_TAG(binary_archive, cryptonote::txout_to_key, 0x2);
|
||||
VARIANT_TAG(binary_archive, cryptonote::txout_to_tagged_key, 0x3);
|
||||
|
@ -584,7 +582,7 @@ VARIANT_TAG(json_archive, cryptonote::txin_gen, "gen");
|
|||
VARIANT_TAG(json_archive, cryptonote::txin_to_script, "script");
|
||||
VARIANT_TAG(json_archive, cryptonote::txin_to_scripthash, "scripthash");
|
||||
VARIANT_TAG(json_archive, cryptonote::txin_to_key, "key");
|
||||
VARIANT_TAG(json_archive, cryptonote::txout_to_script, "script");
|
||||
VARIANT_TAG(json_archive, cryptonote::txout_to_carrot_v1, "carrot_v1");
|
||||
VARIANT_TAG(json_archive, cryptonote::txout_to_scripthash, "scripthash");
|
||||
VARIANT_TAG(json_archive, cryptonote::txout_to_key, "key");
|
||||
VARIANT_TAG(json_archive, cryptonote::txout_to_tagged_key, "tagged_key");
|
||||
|
@ -595,7 +593,7 @@ VARIANT_TAG(debug_archive, cryptonote::txin_gen, "gen");
|
|||
VARIANT_TAG(debug_archive, cryptonote::txin_to_script, "script");
|
||||
VARIANT_TAG(debug_archive, cryptonote::txin_to_scripthash, "scripthash");
|
||||
VARIANT_TAG(debug_archive, cryptonote::txin_to_key, "key");
|
||||
VARIANT_TAG(debug_archive, cryptonote::txout_to_script, "script");
|
||||
VARIANT_TAG(debug_archive, cryptonote::txout_to_carrot_v1, "carrot_v1");
|
||||
VARIANT_TAG(debug_archive, cryptonote::txout_to_scripthash, "scripthash");
|
||||
VARIANT_TAG(debug_archive, cryptonote::txout_to_key, "key");
|
||||
VARIANT_TAG(debug_archive, cryptonote::txout_to_tagged_key, "tagged_key");
|
||||
|
|
|
@ -38,6 +38,7 @@
|
|||
#include <boost/serialization/is_bitwise_serializable.hpp>
|
||||
#include <boost/archive/portable_binary_iarchive.hpp>
|
||||
#include <boost/archive/portable_binary_oarchive.hpp>
|
||||
#include "carrot_impl/carrot_boost_serialization.h"
|
||||
#include "cryptonote_basic.h"
|
||||
#include "difficulty.h"
|
||||
#include "common/unordered_containers_boost_serialization.h"
|
||||
|
@ -93,10 +94,11 @@ namespace boost
|
|||
}
|
||||
|
||||
template <class Archive>
|
||||
inline void serialize(Archive &a, cryptonote::txout_to_script &x, const boost::serialization::version_type ver)
|
||||
inline void serialize(Archive &a, cryptonote::txout_to_carrot_v1 &x, const boost::serialization::version_type ver)
|
||||
{
|
||||
a & x.keys;
|
||||
a & x.script;
|
||||
a & x.key;
|
||||
a & x.view_tag;
|
||||
a & x.encrypted_janus_anchor;
|
||||
}
|
||||
|
||||
|
||||
|
@ -136,10 +138,6 @@ namespace boost
|
|||
template <class Archive>
|
||||
inline void serialize(Archive &a, cryptonote::txin_to_scripthash &x, const boost::serialization::version_type ver)
|
||||
{
|
||||
a & x.prev;
|
||||
a & x.prevout;
|
||||
a & x.script;
|
||||
a & x.sigset;
|
||||
}
|
||||
|
||||
template <class Archive>
|
||||
|
|
|
@ -907,10 +907,13 @@ namespace cryptonote
|
|||
{
|
||||
// before HF_VERSION_VIEW_TAGS, outputs with public keys are of type txout_to_key
|
||||
// after HF_VERSION_VIEW_TAGS, outputs with public keys are of type txout_to_tagged_key
|
||||
// after HF_VERSION_FCMP_PLUS_PLUS, outputs with public keys are of type txout_to_carrot_v1
|
||||
if (out.target.type() == typeid(txout_to_key))
|
||||
output_public_key = boost::get< txout_to_key >(out.target).key;
|
||||
else if (out.target.type() == typeid(txout_to_tagged_key))
|
||||
output_public_key = boost::get< txout_to_tagged_key >(out.target).key;
|
||||
else if (out.target.type() == typeid(txout_to_carrot_v1))
|
||||
output_public_key = boost::get< txout_to_carrot_v1 >(out.target).key;
|
||||
else
|
||||
{
|
||||
LOG_ERROR("Unexpected output target type found: " << out.target.type().name());
|
||||
|
@ -956,11 +959,33 @@ namespace cryptonote
|
|||
//---------------------------------------------------------------
|
||||
bool check_output_types(const transaction& tx, const uint8_t hf_version)
|
||||
{
|
||||
CHECK_AND_ASSERT_MES(tx.vout.size() > 0, false, "no outputs in transaction");
|
||||
|
||||
for (const auto &o: tx.vout)
|
||||
{
|
||||
if (hf_version > HF_VERSION_VIEW_TAGS)
|
||||
if (hf_version > HF_VERSION_CARROT)
|
||||
{
|
||||
// from v15, require outputs have view tags
|
||||
// from v18, require outputs be carrot outputs
|
||||
CHECK_AND_ASSERT_MES(o.target.type() == typeid(txout_to_carrot_v1), false, "wrong variant type: "
|
||||
<< o.target.type().name() << ", expected txout_to_carrot_v1 in transaction id=" << get_transaction_hash(tx));
|
||||
}
|
||||
else if (hf_version == HF_VERSION_CARROT)
|
||||
{
|
||||
// during v17, require outputs be of type txout_to_tagged_key OR txout_to_carrot_v1
|
||||
// to allow grace period before requiring all to be txout_to_carrot_v1
|
||||
CHECK_AND_ASSERT_MES(
|
||||
o.target.type() == typeid(txout_to_carrot_v1) || o.target.type() == typeid(txout_to_tagged_key),
|
||||
false, "wrong variant type: " << o.target.type().name()
|
||||
<< ", expected txout_to_key or txout_to_tagged_key in transaction id=" << get_transaction_hash(tx));
|
||||
|
||||
// require all outputs in a tx be of the same type
|
||||
CHECK_AND_ASSERT_MES(o.target.type() == tx.vout[0].target.type(), false, "non-matching variant types: "
|
||||
<< o.target.type().name() << " and " << tx.vout[0].target.type().name() << ", "
|
||||
<< "expected matching variant types in transaction id=" << get_transaction_hash(tx));
|
||||
}
|
||||
else if (hf_version > HF_VERSION_VIEW_TAGS)
|
||||
{
|
||||
// from v16, require outputs have view tags
|
||||
CHECK_AND_ASSERT_MES(o.target.type() == typeid(txout_to_tagged_key), false, "wrong variant type: "
|
||||
<< o.target.type().name() << ", expected txout_to_tagged_key in transaction id=" << get_transaction_hash(tx));
|
||||
}
|
||||
|
|
|
@ -53,6 +53,7 @@ namespace cryptonote
|
|||
void get_transaction_prefix_hash(const transaction_prefix& tx, crypto::hash& h);
|
||||
crypto::hash get_transaction_prefix_hash(const transaction_prefix& tx);
|
||||
bool parse_and_validate_tx_prefix_from_blob(const blobdata_ref& tx_blob, transaction_prefix& tx);
|
||||
bool expand_transaction_1(transaction &tx, bool base_only);
|
||||
bool parse_and_validate_tx_from_blob(const blobdata_ref& tx_blob, transaction& tx, crypto::hash& tx_hash, crypto::hash& tx_prefix_hash);
|
||||
bool parse_and_validate_tx_from_blob(const blobdata_ref& tx_blob, transaction& tx, crypto::hash& tx_hash);
|
||||
bool parse_and_validate_tx_from_blob(const blobdata_ref& tx_blob, transaction& tx);
|
||||
|
|
|
@ -91,6 +91,7 @@ namespace cryptonote
|
|||
|
||||
struct tx_extra_pub_key
|
||||
{
|
||||
// while marked `crypto::public_key`, which usually means Ed25519, this will hold an X25519 pubkey in Carrot txs
|
||||
crypto::public_key pub_key;
|
||||
|
||||
BEGIN_SERIALIZE()
|
||||
|
@ -158,6 +159,7 @@ namespace cryptonote
|
|||
// per-output additional tx pubkey for multi-destination transfers involving at least one subaddress
|
||||
struct tx_extra_additional_pub_keys
|
||||
{
|
||||
// same as tx_extra_pub_key, this is a vector of X25519 pubkeys in Carrot txs
|
||||
std::vector<crypto::public_key> data;
|
||||
|
||||
BEGIN_SERIALIZE()
|
||||
|
|
|
@ -192,6 +192,8 @@
|
|||
#define HF_VERSION_BULLETPROOF_PLUS 15
|
||||
#define HF_VERSION_VIEW_TAGS 15
|
||||
#define HF_VERSION_2021_SCALING 15
|
||||
#define HF_VERSION_FCMP_PLUS_PLUS 17
|
||||
#define HF_VERSION_CARROT 17
|
||||
|
||||
#define PER_KB_FEE_QUANTIZATION_DECIMALS 8
|
||||
#define CRYPTONOTE_SCALING_2021_FEE_ROUNDING_PLACES 2
|
||||
|
|
|
@ -610,7 +610,10 @@ namespace cryptonote
|
|||
{
|
||||
hw::device &hwdev = sender_account_keys.get_device();
|
||||
hwdev.open_tx(tx_key);
|
||||
try {
|
||||
auto auto_close_tx = epee::misc_utils::create_scope_leave_handler([&hwdev](){
|
||||
hwdev.close_tx();
|
||||
});
|
||||
{
|
||||
// figure out if we need to make additional tx pubkeys
|
||||
size_t num_stdaddresses = 0;
|
||||
size_t num_subaddresses = 0;
|
||||
|
@ -628,11 +631,7 @@ namespace cryptonote
|
|||
|
||||
bool shuffle_outs = true;
|
||||
bool r = construct_tx_with_tx_key(sender_account_keys, subaddresses, sources, destinations, change_addr, extra, tx, tx_key, additional_tx_keys, rct, rct_config, shuffle_outs, use_view_tags);
|
||||
hwdev.close_tx();
|
||||
return r;
|
||||
} catch(...) {
|
||||
hwdev.close_tx();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
//---------------------------------------------------------------
|
||||
|
|
|
@ -304,6 +304,7 @@ namespace rct {
|
|||
RCTTypeBulletproof2 = 4,
|
||||
RCTTypeCLSAG = 5,
|
||||
RCTTypeBulletproofPlus = 6,
|
||||
RCTTypeFcmpPlusPlus = 7,
|
||||
};
|
||||
enum RangeProofType { RangeProofBorromean, RangeProofPaddedBulletproof };
|
||||
struct RCTConfig {
|
||||
|
@ -336,7 +337,7 @@ namespace rct {
|
|||
FIELD(type)
|
||||
if (type == RCTTypeNull)
|
||||
return ar.good();
|
||||
if (type != RCTTypeFull && type != RCTTypeSimple && type != RCTTypeBulletproof && type != RCTTypeBulletproof2 && type != RCTTypeCLSAG && type != RCTTypeBulletproofPlus)
|
||||
if (type != RCTTypeFull && type != RCTTypeSimple && type != RCTTypeBulletproof && type != RCTTypeBulletproof2 && type != RCTTypeCLSAG && type != RCTTypeBulletproofPlus && type != RCTTypeFcmpPlusPlus)
|
||||
return false;
|
||||
VARINT_FIELD(txnFee)
|
||||
// inputs/outputs not saved, only here for serialization help
|
||||
|
@ -365,7 +366,7 @@ namespace rct {
|
|||
return false;
|
||||
for (size_t i = 0; i < outputs; ++i)
|
||||
{
|
||||
if (type == RCTTypeBulletproof2 || type == RCTTypeCLSAG || type == RCTTypeBulletproofPlus)
|
||||
if (type == RCTTypeBulletproof2 || type == RCTTypeCLSAG || type == RCTTypeBulletproofPlus || type == RCTTypeFcmpPlusPlus)
|
||||
{
|
||||
// Since RCTTypeBulletproof2 enote types, we don't serialize the blinding factor, and only serialize the
|
||||
// first 8 bytes of ecdhInfo[i].amount
|
||||
|
@ -741,10 +742,12 @@ namespace rct {
|
|||
static inline const rct::key &sk2rct(const crypto::secret_key &sk) { return (const rct::key&)sk; }
|
||||
static inline const rct::key &ki2rct(const crypto::key_image &ki) { return (const rct::key&)ki; }
|
||||
static inline const rct::key &hash2rct(const crypto::hash &h) { return (const rct::key&)h; }
|
||||
static inline const rct::key &pt2rct(const crypto::ec_point &pt) { return (const rct::key&)pt; }
|
||||
static inline const crypto::public_key &rct2pk(const rct::key &k) { return (const crypto::public_key&)k; }
|
||||
static inline const crypto::secret_key &rct2sk(const rct::key &k) { return (const crypto::secret_key&)k; }
|
||||
static inline const crypto::key_image &rct2ki(const rct::key &k) { return (const crypto::key_image&)k; }
|
||||
static inline const crypto::hash &rct2hash(const rct::key &k) { return (const crypto::hash&)k; }
|
||||
static inline const crypto::ec_point &rct2pt(const rct::key &k) { return (const crypto::ec_point&)k; }
|
||||
static inline bool operator==(const rct::key &k0, const crypto::public_key &k1) { return !crypto_verify_32(k0.bytes, (const unsigned char*)&k1); }
|
||||
static inline bool operator!=(const rct::key &k0, const crypto::public_key &k1) { return crypto_verify_32(k0.bytes, (const unsigned char*)&k1); }
|
||||
}
|
||||
|
|
|
@ -458,12 +458,6 @@ void fromJsonValue(const rapidjson::Value& val, cryptonote::txin_to_script& txin
|
|||
void toJsonValue(rapidjson::Writer<epee::byte_stream>& dest, const cryptonote::txin_to_scripthash& txin)
|
||||
{
|
||||
dest.StartObject();
|
||||
|
||||
INSERT_INTO_JSON_OBJECT(dest, prev, txin.prev);
|
||||
INSERT_INTO_JSON_OBJECT(dest, prevout, txin.prevout);
|
||||
INSERT_INTO_JSON_OBJECT(dest, script, txin.script);
|
||||
INSERT_INTO_JSON_OBJECT(dest, sigset, txin.sigset);
|
||||
|
||||
dest.EndObject();
|
||||
}
|
||||
|
||||
|
@ -474,11 +468,6 @@ void fromJsonValue(const rapidjson::Value& val, cryptonote::txin_to_scripthash&
|
|||
{
|
||||
throw WRONG_TYPE("json object");
|
||||
}
|
||||
|
||||
GET_FROM_JSON_OBJECT(val, txin.prev, prev);
|
||||
GET_FROM_JSON_OBJECT(val, txin.prevout, prevout);
|
||||
GET_FROM_JSON_OBJECT(val, txin.script, script);
|
||||
GET_FROM_JSON_OBJECT(val, txin.sigset, sigset);
|
||||
}
|
||||
|
||||
void toJsonValue(rapidjson::Writer<epee::byte_stream>& dest, const cryptonote::txin_to_key& txin)
|
||||
|
@ -505,25 +494,27 @@ void fromJsonValue(const rapidjson::Value& val, cryptonote::txin_to_key& txin)
|
|||
}
|
||||
|
||||
|
||||
void toJsonValue(rapidjson::Writer<epee::byte_stream>& dest, const cryptonote::txout_to_script& txout)
|
||||
void toJsonValue(rapidjson::Writer<epee::byte_stream>& dest, const cryptonote::txout_to_carrot_v1& txout)
|
||||
{
|
||||
dest.StartObject();
|
||||
|
||||
INSERT_INTO_JSON_OBJECT(dest, keys, txout.keys);
|
||||
INSERT_INTO_JSON_OBJECT(dest, script, txout.script);
|
||||
INSERT_INTO_JSON_OBJECT(dest, key, txout.key);
|
||||
INSERT_INTO_JSON_OBJECT(dest, view_tag, txout.view_tag);
|
||||
INSERT_INTO_JSON_OBJECT(dest, encrypted_janus_anchor, txout.encrypted_janus_anchor);
|
||||
|
||||
dest.EndObject();
|
||||
}
|
||||
|
||||
void fromJsonValue(const rapidjson::Value& val, cryptonote::txout_to_script& txout)
|
||||
void fromJsonValue(const rapidjson::Value& val, cryptonote::txout_to_carrot_v1& txout)
|
||||
{
|
||||
if (!val.IsObject())
|
||||
{
|
||||
throw WRONG_TYPE("json object");
|
||||
}
|
||||
|
||||
GET_FROM_JSON_OBJECT(val, txout.keys, keys);
|
||||
GET_FROM_JSON_OBJECT(val, txout.script, script);
|
||||
GET_FROM_JSON_OBJECT(val, txout.key, key);
|
||||
GET_FROM_JSON_OBJECT(val, txout.view_tag, view_tag);
|
||||
GET_FROM_JSON_OBJECT(val, txout.encrypted_janus_anchor, encrypted_janus_anchor);
|
||||
}
|
||||
|
||||
|
||||
|
@ -606,9 +597,9 @@ void toJsonValue(rapidjson::Writer<epee::byte_stream>& dest, const cryptonote::t
|
|||
{
|
||||
INSERT_INTO_JSON_OBJECT(dest, to_tagged_key, output);
|
||||
}
|
||||
void operator()(cryptonote::txout_to_script const& output) const
|
||||
void operator()(cryptonote::txout_to_carrot_v1 const& output) const
|
||||
{
|
||||
INSERT_INTO_JSON_OBJECT(dest, to_script, output);
|
||||
INSERT_INTO_JSON_OBJECT(dest, to_carrot_v1, output);
|
||||
}
|
||||
void operator()(cryptonote::txout_to_scripthash const& output) const
|
||||
{
|
||||
|
@ -650,9 +641,9 @@ void fromJsonValue(const rapidjson::Value& val, cryptonote::tx_out& txout)
|
|||
fromJsonValue(elem.value, tmpVal);
|
||||
txout.target = std::move(tmpVal);
|
||||
}
|
||||
else if (elem.name == "to_script")
|
||||
else if (elem.name == "to_carrot_v1")
|
||||
{
|
||||
cryptonote::txout_to_script tmpVal;
|
||||
cryptonote::txout_to_carrot_v1 tmpVal;
|
||||
fromJsonValue(elem.value, tmpVal);
|
||||
txout.target = std::move(tmpVal);
|
||||
}
|
||||
|
|
|
@ -221,8 +221,8 @@ void fromJsonValue(const rapidjson::Value& val, cryptonote::txin_to_key& txin);
|
|||
void toJsonValue(rapidjson::Writer<epee::byte_stream>& dest, const cryptonote::txout_target_v& txout);
|
||||
void fromJsonValue(const rapidjson::Value& val, cryptonote::txout_target_v& txout);
|
||||
|
||||
void toJsonValue(rapidjson::Writer<epee::byte_stream>& dest, const cryptonote::txout_to_script& txout);
|
||||
void fromJsonValue(const rapidjson::Value& val, cryptonote::txout_to_script& txout);
|
||||
void toJsonValue(rapidjson::Writer<epee::byte_stream>& dest, const cryptonote::txout_to_carrot_v1& txout);
|
||||
void fromJsonValue(const rapidjson::Value& val, cryptonote::txout_to_carrot_v1& txout);
|
||||
|
||||
void toJsonValue(rapidjson::Writer<epee::byte_stream>& dest, const cryptonote::txout_to_scripthash& txout);
|
||||
void fromJsonValue(const rapidjson::Value& val, cryptonote::txout_to_scripthash& txout);
|
||||
|
|
|
@ -37,6 +37,8 @@ set(wallet_sources
|
|||
node_rpc_proxy.cpp
|
||||
message_store.cpp
|
||||
message_transporter.cpp
|
||||
scanning_tools.cpp
|
||||
tx_builder.cpp
|
||||
)
|
||||
|
||||
monero_find_all_headers(wallet_private_headers "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
|
@ -50,6 +52,7 @@ target_link_libraries(wallet
|
|||
PUBLIC
|
||||
rpc_base
|
||||
multisig
|
||||
carrot_impl
|
||||
common
|
||||
cryptonote_core
|
||||
mnemonics
|
||||
|
|
671
src/wallet/scanning_tools.cpp
Normal file
671
src/wallet/scanning_tools.cpp
Normal file
|
@ -0,0 +1,671 @@
|
|||
// 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/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 "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_ephemeral_pubkeys_out,
|
||||
std::vector<crypto::public_key> &additional_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_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_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))
|
||||
field_extra_nonce.nonce.clear();
|
||||
|
||||
return fully_parsed;
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
static bool parse_tx_extra_for_scanning(const cryptonote::transaction_prefix &tx_prefix,
|
||||
std::vector<crypto::public_key> &main_ephemeral_pubkeys_out,
|
||||
std::vector<crypto::public_key> &additional_ephemeral_pubkeys_out,
|
||||
cryptonote::blobdata &tx_extra_nonce_out)
|
||||
{
|
||||
return parse_tx_extra_for_scanning(tx_prefix.extra,
|
||||
tx_prefix.vout.size(),
|
||||
main_ephemeral_pubkeys_out,
|
||||
additional_ephemeral_pubkeys_out,
|
||||
tx_extra_nonce_out);
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
static void perform_ecdh_derivations(const epee::span<const crypto::public_key> main_ephemeral_pubkeys,
|
||||
const epee::span<const crypto::public_key> additional_ephemeral_pubkeys,
|
||||
const cryptonote::account_keys &acc,
|
||||
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_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(acc.m_view_secret_key);
|
||||
|
||||
for (const crypto::public_key &main_ephemeral_pubkey : main_ephemeral_pubkeys)
|
||||
{
|
||||
mx25519_pubkey s_sender_receiver_unctx;
|
||||
k_view_dev.view_key_scalar_mult_x25519(
|
||||
carrot::raw_byte_convert<mx25519_pubkey>(main_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_ephemeral_pubkey : additional_ephemeral_pubkeys)
|
||||
{
|
||||
mx25519_pubkey s_sender_receiver_unctx;
|
||||
k_view_dev.view_key_scalar_mult_x25519(
|
||||
carrot::raw_byte_convert<mx25519_pubkey>(additional_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_ephemeral_pubkey : main_ephemeral_pubkeys)
|
||||
{
|
||||
acc.get_device().generate_key_derivation(main_ephemeral_pubkey,
|
||||
acc.m_view_secret_key,
|
||||
tools::add_element(main_derivations_out));
|
||||
}
|
||||
|
||||
for (const crypto::public_key &additional_ephemeral_pubkey : additional_ephemeral_pubkeys)
|
||||
{
|
||||
acc.get_device().generate_key_derivation(additional_ephemeral_pubkey,
|
||||
acc.m_view_secret_key,
|
||||
tools::add_element(additional_derivations_out));
|
||||
}
|
||||
}
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
std::optional<enote_view_incoming_scan_info_t> try_view_incoming_scan_enote_destination(
|
||||
const cryptonote::tx_out &enote_destination,
|
||||
const carrot::lazy_amount_commitment_t &amount_commitment,
|
||||
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 cryptonote::txin_v &first_tx_input,
|
||||
const std::size_t local_output_index,
|
||||
const epee::span<const crypto::key_derivation> main_derivations,
|
||||
const std::vector<crypto::key_derivation> &additional_derivations,
|
||||
const cryptonote::account_keys &acc,
|
||||
const std::unordered_map<crypto::public_key, cryptonote::subaddress_index> &subaddress_map)
|
||||
{
|
||||
// 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;
|
||||
|
||||
// 2. setup default values
|
||||
res.amount = enote_destination.amount;
|
||||
res.amount_blinding_factor = rct::I;
|
||||
res.local_output_index = local_output_index;
|
||||
|
||||
boost::optional<cryptonote::subaddress_receive_info> 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<mx25519_pubkey>(enote_ephemeral_pubkey_pk);
|
||||
|
||||
const auto &txout_carrot = boost::get<cryptonote::txout_to_carrot_v1>(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<cryptonote::txin_gen>(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<cryptonote::txin_to_key>(first_tx_input).k_image
|
||||
};
|
||||
|
||||
// convert pid_enc
|
||||
std::optional<carrot::encrypted_payment_id_t> 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<carrot::encrypted_payment_id_t>(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<cryptonote::txin_gen>(first_tx_input).height,
|
||||
input_context);
|
||||
}
|
||||
else
|
||||
{
|
||||
// input_context = "R" || KI_1
|
||||
carrot::make_carrot_input_context(boost::get<cryptonote::txin_to_key>(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<crypto::key_derivation>(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<crypto::view_tag> view_tag = cryptonote::get_output_view_tag(enote_destination);
|
||||
for (size_t i = 0; i < std::max<size_t>(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<crypto::key_derivation>{},
|
||||
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<enote_view_incoming_scan_info_t> 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<crypto::public_key, cryptonote::subaddress_index> &subaddress_map)
|
||||
{
|
||||
// 1. parse tx extra
|
||||
std::vector<crypto::public_key> main_ephemeral_pubkeys;
|
||||
std::vector<crypto::public_key> additional_ephemeral_pubkeys;
|
||||
cryptonote::blobdata tx_extra_nonce;
|
||||
parse_tx_extra_for_scanning(tx_prefix, main_ephemeral_pubkeys, additional_ephemeral_pubkeys, tx_extra_nonce);
|
||||
|
||||
// 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_ephemeral_pubkeys),
|
||||
epee::to_span(additional_ephemeral_pubkeys),
|
||||
acc,
|
||||
carrot::is_carrot_transaction_v1(tx_prefix),
|
||||
main_derivations,
|
||||
additional_derivations);
|
||||
|
||||
// 3. view-scan enote destination
|
||||
return try_view_incoming_scan_enote_destination(tx_prefix.vout.at(local_output_index),
|
||||
amount_commitment,
|
||||
epee::to_span(main_ephemeral_pubkeys),
|
||||
epee::to_span(additional_ephemeral_pubkeys),
|
||||
tx_extra_nonce,
|
||||
tx_prefix.vin.at(0),
|
||||
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<crypto::hash>(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<rct::key>(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;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
std::optional<enote_view_incoming_scan_info_t> try_view_incoming_scan_enote(
|
||||
const cryptonote::tx_out &enote_destination,
|
||||
const rct::rctSigBase &rct_sig,
|
||||
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 cryptonote::txin_v &first_tx_input,
|
||||
const std::size_t local_output_index,
|
||||
const epee::span<const crypto::key_derivation> main_derivations,
|
||||
const std::vector<crypto::key_derivation> &additional_derivations,
|
||||
const cryptonote::account_keys &acc,
|
||||
const std::unordered_map<crypto::public_key, cryptonote::subaddress_index> &subaddress_map)
|
||||
{
|
||||
const bool is_rct = rct_sig.type != rct::RCTTypeNull;
|
||||
|
||||
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;
|
||||
|
||||
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,
|
||||
local_output_index,
|
||||
main_derivations,
|
||||
additional_derivations,
|
||||
acc,
|
||||
subaddress_map);
|
||||
|
||||
if (!res)
|
||||
return res;
|
||||
|
||||
// a, z
|
||||
if (is_rct)
|
||||
{
|
||||
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);
|
||||
|
||||
if (!decrypted_amount)
|
||||
res.reset();
|
||||
}
|
||||
else // !is_rct
|
||||
{
|
||||
res->amount = enote_destination.amount;
|
||||
res->amount_blinding_factor = rct::I;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
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)
|
||||
{
|
||||
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");
|
||||
|
||||
// 1. parse tx extra
|
||||
std::vector<crypto::public_key> main_ephemeral_pubkeys;
|
||||
std::vector<crypto::public_key> additional_ephemeral_pubkeys;
|
||||
cryptonote::blobdata tx_extra_nonce;
|
||||
if (!parse_tx_extra_for_scanning(tx, main_ephemeral_pubkeys, additional_ephemeral_pubkeys, tx_extra_nonce))
|
||||
MWARNING("Transaction extra has unsupported format: " << cryptonote::get_transaction_hash(tx));
|
||||
|
||||
// 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_ephemeral_pubkeys),
|
||||
epee::to_span(additional_ephemeral_pubkeys),
|
||||
acc,
|
||||
carrot::is_carrot_transaction_v1(tx),
|
||||
main_derivations,
|
||||
additional_derivations);
|
||||
|
||||
// 3. view-incoming scan output enotes
|
||||
for (size_t local_output_index = 0; local_output_index < n_outputs; ++local_output_index)
|
||||
{
|
||||
auto &enote_scan_info = enote_scan_infos_out[local_output_index];
|
||||
|
||||
const cryptonote::tx_out &enote_destination = tx.vout.at(local_output_index);
|
||||
|
||||
enote_scan_info = try_view_incoming_scan_enote(enote_destination,
|
||||
tx.rct_signatures,
|
||||
epee::to_span(main_ephemeral_pubkeys),
|
||||
epee::to_span(additional_ephemeral_pubkeys),
|
||||
tx_extra_nonce,
|
||||
tx.vin.at(0),
|
||||
local_output_index,
|
||||
epee::to_span(main_derivations),
|
||||
additional_derivations,
|
||||
acc,
|
||||
subaddress_map);
|
||||
}
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
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;
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
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;
|
||||
|
||||
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)
|
||||
{
|
||||
//! @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));
|
||||
}
|
||||
else // !is_carrot
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
} //namespace wallet
|
||||
} //namespace tools
|
138
src/wallet/scanning_tools.h
Normal file
138
src/wallet/scanning_tools.h
Normal file
|
@ -0,0 +1,138 @@
|
|||
// 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.
|
||||
|
||||
#pragma once
|
||||
|
||||
//local headers
|
||||
#include "carrot_core/lazy_amount_commitment.h"
|
||||
#include "carrot_impl/subaddress_index.h"
|
||||
#include "cryptonote_basic/account.h"
|
||||
#include "cryptonote_basic/blobdatatype.h"
|
||||
#include "cryptonote_basic/subaddress_index.h"
|
||||
#include "cryptonote_basic/tx_extra.h"
|
||||
|
||||
//third party headers
|
||||
|
||||
//standard headers
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
//forward declarations
|
||||
|
||||
namespace tools
|
||||
{
|
||||
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
|
||||
crypto::secret_key sender_extension_t;
|
||||
// K^j_s
|
||||
crypto::public_key address_spend_pubkey;
|
||||
// pid
|
||||
crypto::hash payment_id;
|
||||
// j
|
||||
std::optional<carrot::subaddress_index_extended> subaddr_index;
|
||||
|
||||
// a
|
||||
rct::xmr_amount amount;
|
||||
// z
|
||||
rct::key amount_blinding_factor;
|
||||
|
||||
// legacy: 8 k_v R, carrot: s^ctx_sr
|
||||
crypto::key_derivation derivation;
|
||||
// i
|
||||
std::size_t local_output_index;
|
||||
//
|
||||
std::size_t main_tx_pubkey_index;
|
||||
};
|
||||
|
||||
std::optional<enote_view_incoming_scan_info_t> try_view_incoming_scan_enote_destination(
|
||||
const cryptonote::tx_out &enote_destination,
|
||||
const carrot::lazy_amount_commitment_t &lazy_amount_commitment,
|
||||
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 cryptonote::txin_v &first_tx_input,
|
||||
const std::size_t local_output_index,
|
||||
const epee::span<const crypto::key_derivation> main_derivations,
|
||||
const std::vector<crypto::key_derivation> &additional_derivations,
|
||||
const cryptonote::account_keys &acc,
|
||||
const std::unordered_map<crypto::public_key, cryptonote::subaddress_index> &subaddress_map);
|
||||
std::optional<enote_view_incoming_scan_info_t> try_view_incoming_scan_enote_destination(
|
||||
const cryptonote::transaction_prefix &tx_prefix,
|
||||
const carrot::lazy_amount_commitment_t &lazy_amount_commitment,
|
||||
const std::size_t local_output_index,
|
||||
const cryptonote::account_keys &acc,
|
||||
const std::unordered_map<crypto::public_key, cryptonote::subaddress_index> &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<enote_view_incoming_scan_info_t> try_view_incoming_scan_enote(
|
||||
const cryptonote::tx_out &enote_destination,
|
||||
const rct::rctSigBase &rct_sig,
|
||||
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 cryptonote::txin_v &first_tx_input,
|
||||
const std::size_t local_output_index,
|
||||
const epee::span<const crypto::key_derivation> main_derivations,
|
||||
const std::vector<crypto::key_derivation> &additional_derivations,
|
||||
const cryptonote::account_keys &acc,
|
||||
const std::unordered_map<crypto::public_key, cryptonote::subaddress_index> &subaddress_map);
|
||||
|
||||
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);
|
||||
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);
|
||||
|
||||
bool is_long_payment_id(const crypto::hash &pid);
|
||||
|
||||
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);
|
||||
} //namespace wallet
|
||||
} //namespace tools
|
428
src/wallet/tx_builder.cpp
Normal file
428
src/wallet/tx_builder.cpp
Normal file
|
@ -0,0 +1,428 @@
|
|||
// 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 "tx_builder.h"
|
||||
|
||||
//local headers
|
||||
#include "carrot_core/config.h"
|
||||
#include "carrot_core/device_ram_borrowed.h"
|
||||
#include "carrot_impl/carrot_tx_builder_utils.h"
|
||||
#include "carrot_impl/input_selection.h"
|
||||
#include "cryptonote_basic/cryptonote_format_utils.h"
|
||||
|
||||
//third party headers
|
||||
|
||||
//standard headers
|
||||
|
||||
#undef MONERO_DEFAULT_LOG_CATEGORY
|
||||
#define MONERO_DEFAULT_LOG_CATEGORY "wallet.tx_builder"
|
||||
|
||||
namespace tools
|
||||
{
|
||||
namespace wallet
|
||||
{
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
static bool is_transfer_unlocked_for_next_fcmp_pp_block(const wallet2::transfer_details &td,
|
||||
const uint64_t top_block_index)
|
||||
{
|
||||
const uint64_t next_block_index = top_block_index + 1;
|
||||
|
||||
// @TODO: handle FCMP++ conversion of UNIX unlock time to block index number
|
||||
|
||||
if (td.m_block_height + CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE > next_block_index)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
static bool is_transfer_usable_for_input_selection(const wallet2::transfer_details &td,
|
||||
const std::uint32_t from_account,
|
||||
const std::set<std::uint32_t> from_subaddresses,
|
||||
const rct::xmr_amount ignore_above,
|
||||
const rct::xmr_amount ignore_below,
|
||||
const uint64_t top_block_index)
|
||||
{
|
||||
return !td.m_spent
|
||||
&& td.m_key_image_known
|
||||
&& !td.m_key_image_partial
|
||||
&& !td.m_frozen
|
||||
&& is_transfer_unlocked_for_next_fcmp_pp_block(td, top_block_index)
|
||||
&& td.m_subaddr_index.major == from_account
|
||||
&& (from_subaddresses.empty() || from_subaddresses.count(td.m_subaddr_index.minor) == 1)
|
||||
&& td.amount() >= ignore_below
|
||||
&& td.amount() <= ignore_above
|
||||
;
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
static bool build_payment_proposals(std::vector<carrot::CarrotPaymentProposalV1> &normal_payment_proposals_inout,
|
||||
std::vector<carrot::CarrotPaymentProposalVerifiableSelfSendV1> &selfsend_payment_proposals_inout,
|
||||
const cryptonote::tx_destination_entry &tx_dest_entry,
|
||||
const std::unordered_map<crypto::public_key, cryptonote::subaddress_index> &subaddress_map)
|
||||
{
|
||||
const auto subaddr_it = subaddress_map.find(tx_dest_entry.addr.m_spend_public_key);
|
||||
const bool is_selfsend_dest = subaddr_it != subaddress_map.cend();
|
||||
|
||||
// Make N destinations
|
||||
if (is_selfsend_dest)
|
||||
{
|
||||
const carrot::subaddress_index subaddr_index{subaddr_it->second.major, subaddr_it->second.minor};
|
||||
selfsend_payment_proposals_inout.push_back(carrot::CarrotPaymentProposalVerifiableSelfSendV1{
|
||||
.proposal = carrot::CarrotPaymentProposalSelfSendV1{
|
||||
.destination_address_spend_pubkey = tx_dest_entry.addr.m_spend_public_key,
|
||||
.amount = tx_dest_entry.amount,
|
||||
.enote_type = carrot::CarrotEnoteType::PAYMENT
|
||||
},
|
||||
.subaddr_index = {subaddr_index, carrot::AddressDeriveType::PreCarrot} // @TODO: handle carrot
|
||||
});
|
||||
}
|
||||
else // not *known* self-send address
|
||||
{
|
||||
const carrot::CarrotDestinationV1 dest{
|
||||
.address_spend_pubkey = tx_dest_entry.addr.m_spend_public_key,
|
||||
.address_view_pubkey = tx_dest_entry.addr.m_view_public_key,
|
||||
.is_subaddress = tx_dest_entry.is_subaddress
|
||||
//! @TODO: payment ID
|
||||
};
|
||||
|
||||
normal_payment_proposals_inout.push_back(carrot::CarrotPaymentProposalV1{
|
||||
.destination = dest,
|
||||
.amount = tx_dest_entry.amount,
|
||||
.randomness = carrot::gen_janus_anchor()
|
||||
});
|
||||
}
|
||||
|
||||
return is_selfsend_dest;
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
std::unordered_map<crypto::key_image, size_t> collect_non_burned_transfers_by_key_image(
|
||||
const wallet2::transfer_container &transfers)
|
||||
{
|
||||
std::unordered_map<crypto::key_image, size_t> best_transfer_index_by_ki;
|
||||
for (size_t i = 0; i < transfers.size(); ++i)
|
||||
{
|
||||
const wallet2::transfer_details &td = transfers.at(i);
|
||||
if (!td.m_key_image_known || td.m_key_image_partial)
|
||||
continue;
|
||||
const auto it = best_transfer_index_by_ki.find(td.m_key_image);
|
||||
if (it == best_transfer_index_by_ki.end())
|
||||
{
|
||||
best_transfer_index_by_ki.insert({td.m_key_image, i});
|
||||
break;
|
||||
}
|
||||
const wallet2::transfer_details &other_td = transfers.at(it->second);
|
||||
if (td.amount() < other_td.amount())
|
||||
continue;
|
||||
else if (td.amount() > other_td.amount())
|
||||
it->second = i;
|
||||
else if (td.m_global_output_index > other_td.m_global_output_index)
|
||||
continue;
|
||||
else if (td.m_global_output_index < other_td.m_global_output_index)
|
||||
it->second = i;
|
||||
}
|
||||
return best_transfer_index_by_ki;
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
carrot::select_inputs_func_t make_wallet2_single_transfer_input_selector(
|
||||
const wallet2::transfer_container &transfers,
|
||||
const std::uint32_t from_account,
|
||||
const std::set<std::uint32_t> &from_subaddresses,
|
||||
const rct::xmr_amount ignore_above,
|
||||
const rct::xmr_amount ignore_below,
|
||||
const std::uint64_t top_block_index,
|
||||
const bool allow_carrot_external_inputs_in_normal_transfers,
|
||||
const bool allow_pre_carrot_inputs_in_normal_transfers,
|
||||
std::set<size_t> &selected_transfer_indices_out)
|
||||
{
|
||||
// Collect transfer_container into a `std::vector<carrot::CarrotPreSelectedInput>` for usable inputs
|
||||
std::vector<carrot::CarrotPreSelectedInput> input_candidates;
|
||||
std::vector<size_t> input_candidates_transfer_indices;
|
||||
input_candidates.reserve(transfers.size());
|
||||
input_candidates_transfer_indices.reserve(transfers.size());
|
||||
for (size_t i = 0; i < transfers.size(); ++i)
|
||||
{
|
||||
const wallet2::transfer_details& td = transfers.at(i);
|
||||
if (is_transfer_usable_for_input_selection(td,
|
||||
from_account,
|
||||
from_subaddresses,
|
||||
ignore_above,
|
||||
ignore_below,
|
||||
top_block_index))
|
||||
{
|
||||
input_candidates.push_back(carrot::CarrotPreSelectedInput{
|
||||
.core = carrot::CarrotSelectedInput{
|
||||
.amount = td.amount(),
|
||||
.key_image = td.m_key_image
|
||||
},
|
||||
.is_pre_carrot = true, //! @TODO: handle post-Carrot enotes in transfer_details
|
||||
.is_external = true, //! @TODO: derive this info from field in transfer_details
|
||||
.block_index = td.m_block_height
|
||||
});
|
||||
input_candidates_transfer_indices.push_back(i);
|
||||
}
|
||||
}
|
||||
|
||||
// Create wrapper around `make_single_transfer_input_selector`
|
||||
return [input_candidates = std::move(input_candidates),
|
||||
input_candidates_transfer_indices = std::move(input_candidates_transfer_indices),
|
||||
allow_carrot_external_inputs_in_normal_transfers,
|
||||
allow_pre_carrot_inputs_in_normal_transfers,
|
||||
&selected_transfer_indices_out
|
||||
](
|
||||
const boost::multiprecision::int128_t& nominal_output_sum,
|
||||
const std::map<std::size_t, rct::xmr_amount> &fee_by_input_count,
|
||||
const std::size_t num_normal_payment_proposals,
|
||||
const std::size_t num_selfsend_payment_proposals,
|
||||
std::vector<carrot::CarrotSelectedInput> &selected_inputs_outs
|
||||
){
|
||||
const std::vector<carrot::input_selection_policy_t> policies{
|
||||
&carrot::ispolicy::select_two_inputs_prefer_oldest
|
||||
}; // @TODO
|
||||
|
||||
// TODO: not all carrot is internal
|
||||
std::uint32_t flags = 0;
|
||||
if (allow_carrot_external_inputs_in_normal_transfers)
|
||||
flags |= carrot::InputSelectionFlags::ALLOW_EXTERNAL_INPUTS_IN_NORMAL_TRANSFERS;
|
||||
if (allow_pre_carrot_inputs_in_normal_transfers)
|
||||
flags |= carrot::InputSelectionFlags::ALLOW_PRE_CARROT_INPUTS_IN_NORMAL_TRANSFERS;
|
||||
|
||||
// Make inner input selection functor
|
||||
std::set<size_t> selected_input_indices;
|
||||
const carrot::select_inputs_func_t inner = carrot::make_single_transfer_input_selector(
|
||||
epee::to_span(input_candidates),
|
||||
epee::to_span(policies),
|
||||
flags,
|
||||
&selected_input_indices);
|
||||
|
||||
// Call input selection
|
||||
inner(nominal_output_sum,
|
||||
fee_by_input_count,
|
||||
num_normal_payment_proposals,
|
||||
num_selfsend_payment_proposals,
|
||||
selected_inputs_outs);
|
||||
|
||||
// Collect converted selected_input_indices -> selected_transfer_indices_out
|
||||
selected_transfer_indices_out.clear();
|
||||
for (const size_t input_index : selected_input_indices)
|
||||
selected_transfer_indices_out.insert(input_candidates_transfer_indices.at(input_index));
|
||||
};
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
carrot::CarrotTransactionProposalV1 make_carrot_transaction_proposal_wallet2_transfer_subtractable(
|
||||
const wallet2::transfer_container &transfers,
|
||||
const std::unordered_map<crypto::public_key, cryptonote::subaddress_index> &subaddress_map,
|
||||
const std::vector<cryptonote::tx_destination_entry> &dsts,
|
||||
const rct::xmr_amount fee_per_weight,
|
||||
const std::vector<uint8_t> &extra,
|
||||
const uint32_t subaddr_account,
|
||||
const std::set<uint32_t> &subaddr_indices,
|
||||
const rct::xmr_amount ignore_above,
|
||||
const rct::xmr_amount ignore_below,
|
||||
const wallet2::unique_index_container& subtract_fee_from_outputs,
|
||||
const std::uint64_t top_block_index,
|
||||
const cryptonote::account_base &acb)
|
||||
{
|
||||
// build payment proposals and subtractable info
|
||||
std::vector<carrot::CarrotPaymentProposalV1> normal_payment_proposals;
|
||||
std::vector<carrot::CarrotPaymentProposalVerifiableSelfSendV1> selfsend_payment_proposals;
|
||||
std::set<std::size_t> subtractable_normal_payment_proposals;
|
||||
std::set<std::size_t> subtractable_selfsend_payment_proposals;
|
||||
for (size_t i = 0; i < dsts.size(); ++i)
|
||||
{
|
||||
const cryptonote::tx_destination_entry &dst = dsts.at(i);
|
||||
const bool is_selfsend = build_payment_proposals(normal_payment_proposals,
|
||||
selfsend_payment_proposals,
|
||||
dst,
|
||||
subaddress_map);
|
||||
if (subtract_fee_from_outputs.count(i))
|
||||
{
|
||||
if (is_selfsend)
|
||||
subtractable_selfsend_payment_proposals.insert(selfsend_payment_proposals.size() - 1);
|
||||
else
|
||||
subtractable_normal_payment_proposals.insert(normal_payment_proposals.size() - 1);
|
||||
}
|
||||
}
|
||||
|
||||
// make input selector
|
||||
std::set<size_t> selected_transfer_indices;
|
||||
carrot::select_inputs_func_t select_inputs = make_wallet2_single_transfer_input_selector(
|
||||
transfers,
|
||||
subaddr_account,
|
||||
subaddr_indices,
|
||||
ignore_above,
|
||||
ignore_below,
|
||||
top_block_index,
|
||||
/*allow_carrot_external_inputs_in_normal_transfers=*/true,
|
||||
/*allow_pre_carrot_inputs_in_normal_transfers=*/true,
|
||||
selected_transfer_indices);
|
||||
|
||||
//! @TODO: handle HW devices
|
||||
carrot::view_incoming_key_ram_borrowed_device k_view_incoming_dev(acb.get_keys().m_view_secret_key);
|
||||
|
||||
carrot::CarrotTransactionProposalV1 tx_proposal;
|
||||
if (subtract_fee_from_outputs.size())
|
||||
{
|
||||
carrot::make_carrot_transaction_proposal_v1_transfer_subtractable(
|
||||
normal_payment_proposals,
|
||||
selfsend_payment_proposals,
|
||||
fee_per_weight,
|
||||
extra,
|
||||
std::move(select_inputs),
|
||||
/*s_view_balance_dev=*/nullptr, //! @TODO: handle carrot
|
||||
&k_view_incoming_dev,
|
||||
acb.get_keys().m_account_address.m_spend_public_key,
|
||||
subtractable_normal_payment_proposals,
|
||||
subtractable_selfsend_payment_proposals,
|
||||
tx_proposal);
|
||||
}
|
||||
else // non-subtractable
|
||||
{
|
||||
carrot::make_carrot_transaction_proposal_v1_transfer(
|
||||
normal_payment_proposals,
|
||||
selfsend_payment_proposals,
|
||||
fee_per_weight,
|
||||
extra,
|
||||
std::move(select_inputs),
|
||||
/*s_view_balance_dev=*/nullptr, //! @TODO: handle carrot
|
||||
&k_view_incoming_dev,
|
||||
acb.get_keys().m_account_address.m_spend_public_key,
|
||||
tx_proposal);
|
||||
}
|
||||
|
||||
return tx_proposal;
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
carrot::CarrotTransactionProposalV1 make_carrot_transaction_proposal_wallet2_transfer(
|
||||
const wallet2::transfer_container &transfers,
|
||||
const std::unordered_map<crypto::public_key, cryptonote::subaddress_index> &subaddress_map,
|
||||
const std::vector<cryptonote::tx_destination_entry> &dsts,
|
||||
const rct::xmr_amount fee_per_weight,
|
||||
const std::vector<uint8_t> &extra,
|
||||
const uint32_t subaddr_account,
|
||||
const std::set<uint32_t> &subaddr_indices,
|
||||
const rct::xmr_amount ignore_above,
|
||||
const rct::xmr_amount ignore_below,
|
||||
const std::uint64_t top_block_index,
|
||||
const cryptonote::account_base &acb)
|
||||
{
|
||||
return make_carrot_transaction_proposal_wallet2_transfer_subtractable(
|
||||
transfers,
|
||||
subaddress_map,
|
||||
dsts,
|
||||
fee_per_weight,
|
||||
extra,
|
||||
subaddr_account,
|
||||
subaddr_indices,
|
||||
ignore_above,
|
||||
ignore_below,
|
||||
/*subtract_fee_from_outputs=*/{},
|
||||
top_block_index,
|
||||
acb);
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
carrot::CarrotTransactionProposalV1 make_carrot_transaction_proposal_wallet2_sweep(
|
||||
const wallet2::transfer_container &transfers,
|
||||
const std::unordered_map<crypto::public_key, cryptonote::subaddress_index> &subaddress_map,
|
||||
const std::vector<crypto::key_image> &input_key_images,
|
||||
const cryptonote::account_public_address &address,
|
||||
const bool is_subaddress,
|
||||
const size_t n_dests,
|
||||
const rct::xmr_amount fee_per_weight,
|
||||
const std::vector<uint8_t>& extra,
|
||||
const std::uint64_t top_block_index,
|
||||
const cryptonote::account_base &acb)
|
||||
{
|
||||
CHECK_AND_ASSERT_THROW_MES(!input_key_images.empty(),
|
||||
"make carrot transaction proposal wallet2 sweep: no key images provided");
|
||||
CHECK_AND_ASSERT_THROW_MES(n_dests <= carrot::CARROT_MAX_TX_INPUTS,
|
||||
"make carrot transaction proposal wallet2 sweep: too many destinations");
|
||||
|
||||
// Check that the key image is available and isn't spent, and collect amounts
|
||||
std::vector<rct::xmr_amount> input_amounts;
|
||||
input_amounts.reserve(input_key_images.size());
|
||||
const auto best_transfers_by_ki = collect_non_burned_transfers_by_key_image(transfers);
|
||||
for (const crypto::key_image &ki : input_key_images)
|
||||
{
|
||||
const auto ki_it = best_transfers_by_ki.find(ki);
|
||||
CHECK_AND_ASSERT_THROW_MES(ki_it != best_transfers_by_ki.cend(),
|
||||
"make carrot transaction proposal wallet2 sweep: unknown key image");
|
||||
const wallet2::transfer_details &td = transfers.at(ki_it->second);
|
||||
CHECK_AND_ASSERT_THROW_MES(is_transfer_usable_for_input_selection(td,
|
||||
td.m_subaddr_index.major,
|
||||
/*from_subaddresses=*/{},
|
||||
/*ignore_above=*/MONEY_SUPPLY,
|
||||
/*ignore_below=*/0,
|
||||
top_block_index),
|
||||
"make carrot transaction proposal wallet2 sweep: transfer not usable as an input");
|
||||
input_amounts.push_back(td.amount());
|
||||
}
|
||||
|
||||
// build n_dests payment proposals
|
||||
std::vector<carrot::CarrotPaymentProposalV1> normal_payment_proposals;
|
||||
std::vector<carrot::CarrotPaymentProposalVerifiableSelfSendV1> selfsend_payment_proposals;
|
||||
for (size_t i = 0; i < n_dests; ++i)
|
||||
{
|
||||
build_payment_proposals(normal_payment_proposals,
|
||||
selfsend_payment_proposals,
|
||||
cryptonote::tx_destination_entry(/*amount=*/0, address, is_subaddress),
|
||||
subaddress_map);
|
||||
}
|
||||
|
||||
// Collect CarrotSelectedInput
|
||||
std::vector<carrot::CarrotSelectedInput> selected_inputs(input_key_images.size());
|
||||
for (size_t i = 0; i < input_key_images.size(); ++i)
|
||||
{
|
||||
selected_inputs[i] = carrot::CarrotSelectedInput{
|
||||
.amount = input_amounts.at(i),
|
||||
.key_image = input_key_images.at(i)
|
||||
};
|
||||
}
|
||||
|
||||
//! @TODO: handle HW devices
|
||||
carrot::view_incoming_key_ram_borrowed_device k_view_incoming_dev(acb.get_keys().m_view_secret_key);
|
||||
|
||||
carrot::CarrotTransactionProposalV1 tx_proposal;
|
||||
carrot::make_carrot_transaction_proposal_v1_sweep(normal_payment_proposals,
|
||||
selfsend_payment_proposals,
|
||||
fee_per_weight,
|
||||
extra,
|
||||
std::move(selected_inputs),
|
||||
/*s_view_balance_dev=*/nullptr, //! @TODO: handle carrot
|
||||
&k_view_incoming_dev,
|
||||
acb.get_keys().m_account_address.m_spend_public_key,
|
||||
tx_proposal);
|
||||
|
||||
return tx_proposal;
|
||||
}
|
||||
//-------------------------------------------------------------------------------------------------------------------
|
||||
} //namespace wallet
|
||||
} //namespace tools
|
98
src/wallet/tx_builder.h
Normal file
98
src/wallet/tx_builder.h
Normal file
|
@ -0,0 +1,98 @@
|
|||
// 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.
|
||||
|
||||
#pragma once
|
||||
|
||||
//local headers
|
||||
#include "carrot_impl/carrot_tx_builder_types.h"
|
||||
#include "wallet2.h"
|
||||
|
||||
//third party headers
|
||||
|
||||
//standard headers
|
||||
|
||||
//forward declarations
|
||||
|
||||
namespace tools
|
||||
{
|
||||
namespace wallet
|
||||
{
|
||||
std::unordered_map<crypto::key_image, size_t> collect_non_burned_transfers_by_key_image(
|
||||
const wallet2::transfer_container &transfers);
|
||||
|
||||
carrot::select_inputs_func_t make_wallet2_single_transfer_input_selector(
|
||||
const wallet2::transfer_container &transfers,
|
||||
const std::uint32_t from_account,
|
||||
const std::set<std::uint32_t> &from_subaddresses,
|
||||
const rct::xmr_amount ignore_above,
|
||||
const rct::xmr_amount ignore_below,
|
||||
const std::uint64_t top_block_index,
|
||||
const bool allow_carrot_external_inputs_in_normal_transfers,
|
||||
const bool allow_pre_carrot_inputs_in_normal_transfers,
|
||||
std::set<size_t> &selected_transfer_indices_out);
|
||||
|
||||
carrot::CarrotTransactionProposalV1 make_carrot_transaction_proposal_wallet2_transfer_subtractable(
|
||||
const wallet2::transfer_container &transfers,
|
||||
const std::unordered_map<crypto::public_key, cryptonote::subaddress_index> &subaddress_map,
|
||||
const std::vector<cryptonote::tx_destination_entry> &dsts,
|
||||
const rct::xmr_amount fee_per_weight,
|
||||
const std::vector<uint8_t> &extra,
|
||||
const uint32_t subaddr_account,
|
||||
const std::set<uint32_t> &subaddr_indices,
|
||||
const rct::xmr_amount ignore_above,
|
||||
const rct::xmr_amount ignore_below,
|
||||
const wallet2::unique_index_container& subtract_fee_from_outputs,
|
||||
const std::uint64_t top_block_index,
|
||||
const cryptonote::account_base &acb);
|
||||
|
||||
carrot::CarrotTransactionProposalV1 make_carrot_transaction_proposal_wallet2_transfer(
|
||||
const wallet2::transfer_container &transfers,
|
||||
const std::unordered_map<crypto::public_key, cryptonote::subaddress_index> &subaddress_map,
|
||||
const std::vector<cryptonote::tx_destination_entry> &dsts,
|
||||
const rct::xmr_amount fee_per_weight,
|
||||
const std::vector<uint8_t> &extra,
|
||||
const uint32_t subaddr_account,
|
||||
const std::set<uint32_t> &subaddr_indices,
|
||||
const rct::xmr_amount ignore_above,
|
||||
const rct::xmr_amount ignore_below,
|
||||
const std::uint64_t top_block_index,
|
||||
const cryptonote::account_base &acb);
|
||||
|
||||
carrot::CarrotTransactionProposalV1 make_carrot_transaction_proposal_wallet2_sweep(
|
||||
const wallet2::transfer_container &transfers,
|
||||
const std::unordered_map<crypto::public_key, cryptonote::subaddress_index> &subaddress_map,
|
||||
const std::vector<crypto::key_image> &input_key_images,
|
||||
const cryptonote::account_public_address &address,
|
||||
const bool is_subaddress,
|
||||
const size_t n_dests,
|
||||
const rct::xmr_amount fee_per_weight,
|
||||
const std::vector<uint8_t>& extra,
|
||||
const std::uint64_t top_block_index,
|
||||
const cryptonote::account_base &acb);
|
||||
} //namespace wallet
|
||||
} //namespace tools
|
File diff suppressed because it is too large
Load diff
|
@ -66,6 +66,7 @@
|
|||
#include "serialization/pair.h"
|
||||
#include "serialization/tuple.h"
|
||||
#include "serialization/containers.h"
|
||||
#include "scanning_tools.h"
|
||||
|
||||
#include "wallet_errors.h"
|
||||
#include "common/password.h"
|
||||
|
@ -853,23 +854,6 @@ private:
|
|||
cryptonote::COMMAND_RPC_GET_BLOCKS_FAST::block_output_indices o_indices;
|
||||
bool error;
|
||||
};
|
||||
|
||||
struct is_out_data
|
||||
{
|
||||
crypto::public_key pkey;
|
||||
crypto::key_derivation derivation;
|
||||
std::vector<boost::optional<cryptonote::subaddress_receive_info>> received;
|
||||
};
|
||||
|
||||
struct tx_cache_data
|
||||
{
|
||||
std::vector<cryptonote::tx_extra_field> tx_extra_fields;
|
||||
std::vector<is_out_data> primary;
|
||||
std::vector<is_out_data> additional;
|
||||
|
||||
bool empty() const { return tx_extra_fields.empty() && primary.empty() && additional.empty(); }
|
||||
};
|
||||
|
||||
struct detached_blockchain_data
|
||||
{
|
||||
hashchain detached_blockchain;
|
||||
|
@ -1493,7 +1477,6 @@ private:
|
|||
void check_tx_key(const crypto::hash &txid, const crypto::secret_key &tx_key, const std::vector<crypto::secret_key> &additional_tx_keys, const cryptonote::account_public_address &address, uint64_t &received, bool &in_pool, uint64_t &confirmations);
|
||||
void check_tx_key_helper(const crypto::hash &txid, const crypto::key_derivation &derivation, const std::vector<crypto::key_derivation> &additional_derivations, const cryptonote::account_public_address &address, uint64_t &received, bool &in_pool, uint64_t &confirmations);
|
||||
void check_tx_key_helper(const cryptonote::transaction &tx, const crypto::key_derivation &derivation, const std::vector<crypto::key_derivation> &additional_derivations, const cryptonote::account_public_address &address, uint64_t &received) const;
|
||||
bool is_out_to_acc(const cryptonote::account_public_address &address, const crypto::public_key& out_key, const crypto::key_derivation &derivation, const std::vector<crypto::key_derivation> &additional_derivations, const size_t output_index, const boost::optional<crypto::view_tag> &view_tag_opt, crypto::key_derivation &found_derivation) const;
|
||||
std::string get_tx_proof(const crypto::hash &txid, const cryptonote::account_public_address &address, bool is_subaddress, const std::string &message);
|
||||
std::string get_tx_proof(const cryptonote::transaction &tx, const crypto::secret_key &tx_key, const std::vector<crypto::secret_key> &additional_tx_keys, const cryptonote::account_public_address &address, bool is_subaddress, const std::string &message) const;
|
||||
bool check_tx_proof(const crypto::hash &txid, const cryptonote::account_public_address &address, bool is_subaddress, const std::string &message, const std::string &sig_str, uint64_t &received, bool &in_pool, uint64_t &confirmations);
|
||||
|
@ -1662,6 +1645,8 @@ private:
|
|||
uint32_t adjust_priority(uint32_t priority);
|
||||
|
||||
bool is_unattended() const { return m_unattended; }
|
||||
bool is_spendkey_encryption_enabled() const
|
||||
{ return m_ask_password == AskPasswordToDecrypt && !m_unattended && !m_watch_only && !m_multisig && !m_is_background_wallet; }
|
||||
|
||||
std::pair<size_t, uint64_t> estimate_tx_size_and_weight(bool use_rct, int n_inputs, int ring_size, int n_outputs, size_t extra_size);
|
||||
|
||||
|
@ -1768,7 +1753,9 @@ private:
|
|||
|
||||
static std::string get_default_daemon_address() { CRITICAL_REGION_LOCAL(default_daemon_address_lock); return default_daemon_address; }
|
||||
|
||||
#ifndef IN_UNIT_TESTS
|
||||
private:
|
||||
#endif
|
||||
/*!
|
||||
* \brief Stores wallet information to wallet file.
|
||||
* \param keys_file_name Name of wallet file
|
||||
|
@ -1794,11 +1781,44 @@ private:
|
|||
bool load_keys_buf(const std::string& keys_buf, const epee::wipeable_string& password);
|
||||
bool load_keys_buf(const std::string& keys_buf, const epee::wipeable_string& password, boost::optional<crypto::chacha_key>& keys_to_encrypt);
|
||||
void load_wallet_cache(const bool use_fs, const std::string& cache_buf = "");
|
||||
void process_new_transaction(const crypto::hash &txid, const cryptonote::transaction& tx, const std::vector<uint64_t> &o_indices, uint64_t height, uint8_t block_version, uint64_t ts, bool miner_tx, bool pool, bool double_spend_seen, const tx_cache_data &tx_cache_data, std::map<std::pair<uint64_t, uint64_t>, size_t> *output_tracker_cache = NULL, bool ignore_callbacks = false);
|
||||
void scan_key_image(const wallet::enote_view_incoming_scan_info_t &enote_scan_info, bool pool, std::optional<crypto::key_image> &ki_out);
|
||||
void process_new_transaction(
|
||||
const crypto::hash &txid,
|
||||
const cryptonote::transaction& tx,
|
||||
const std::vector<uint64_t> &o_indices,
|
||||
const uint64_t height,
|
||||
const uint8_t block_version,
|
||||
const uint64_t ts,
|
||||
const bool miner_tx,
|
||||
const bool pool,
|
||||
const bool double_spend_seen,
|
||||
const bool ignore_callbacks = false);
|
||||
void process_new_scanned_transaction(
|
||||
const crypto::hash &txid,
|
||||
const cryptonote::transaction& tx,
|
||||
const epee::span<const std::optional<wallet::enote_view_incoming_scan_info_t>> enote_scan_infos,
|
||||
const epee::span<const std::optional<crypto::key_image>> output_key_images,
|
||||
const std::vector<uint64_t> &o_indices,
|
||||
const uint64_t height,
|
||||
const uint8_t block_version,
|
||||
const uint64_t ts,
|
||||
const bool miner_tx,
|
||||
const bool pool,
|
||||
const bool double_spend_seen,
|
||||
std::map<std::pair<uint64_t, uint64_t>, size_t> &output_tracker_cache,
|
||||
const bool ignore_callbacks = false);
|
||||
bool should_skip_block(const cryptonote::block &b, uint64_t height) const;
|
||||
void process_new_blockchain_entry(const cryptonote::block& b, const cryptonote::block_complete_entry& bche, const parsed_block &parsed_block, const crypto::hash& bl_id, uint64_t height, const std::vector<tx_cache_data> &tx_cache_data, size_t tx_cache_data_offset, std::map<std::pair<uint64_t, uint64_t>, size_t> *output_tracker_cache = NULL);
|
||||
detached_blockchain_data detach_blockchain(uint64_t height, std::map<std::pair<uint64_t, uint64_t>, size_t> *output_tracker_cache = NULL);
|
||||
void handle_reorg(uint64_t height, std::map<std::pair<uint64_t, uint64_t>, size_t> *output_tracker_cache = NULL);
|
||||
void process_new_blockchain_entry(const cryptonote::block& b,
|
||||
const cryptonote::block_complete_entry& bche,
|
||||
const parsed_block &parsed_block,
|
||||
const crypto::hash& bl_id,
|
||||
const uint64_t height,
|
||||
epee::span<const std::optional<wallet::enote_view_incoming_scan_info_t>> enote_scan_infos,
|
||||
epee::span<const std::optional<crypto::key_image>> output_key_images,
|
||||
std::map<std::pair<uint64_t, uint64_t>, size_t> &output_tracker_cache);
|
||||
detached_blockchain_data detach_blockchain(uint64_t height,
|
||||
std::map<std::pair<uint64_t, uint64_t>, size_t> &output_tracker_cache);
|
||||
void handle_reorg(uint64_t height, std::map<std::pair<uint64_t, uint64_t>, size_t> &output_tracker_cache);
|
||||
void get_short_chain_history(std::list<crypto::hash>& ids, uint64_t granularity = 1) const;
|
||||
bool clear();
|
||||
void clear_soft(bool keep_key_images=false);
|
||||
|
@ -1815,7 +1835,7 @@ private:
|
|||
void pull_hashes(uint64_t start_height, uint64_t& blocks_start_height, const std::list<crypto::hash> &short_chain_history, std::vector<crypto::hash> &hashes);
|
||||
void fast_refresh(uint64_t stop_height, uint64_t &blocks_start_height, std::list<crypto::hash> &short_chain_history, bool force = false);
|
||||
void pull_and_parse_next_blocks(bool first, bool try_incremental, uint64_t start_height, uint64_t &blocks_start_height, std::list<crypto::hash> &short_chain_history, const std::vector<cryptonote::block_complete_entry> &prev_blocks, const std::vector<parsed_block> &prev_parsed_blocks, std::vector<cryptonote::block_complete_entry> &blocks, std::vector<parsed_block> &parsed_blocks, std::vector<std::tuple<cryptonote::transaction, crypto::hash, bool>>& process_pool_txs, bool &last, bool &error, std::exception_ptr &exception);
|
||||
void process_parsed_blocks(const uint64_t start_height, const std::vector<cryptonote::block_complete_entry> &blocks, const std::vector<parsed_block> &parsed_blocks, uint64_t& blocks_added, std::map<std::pair<uint64_t, uint64_t>, size_t> *output_tracker_cache = NULL);
|
||||
void process_parsed_blocks(const uint64_t start_height, const std::vector<cryptonote::block_complete_entry> &blocks, const std::vector<parsed_block> &parsed_blocks, uint64_t& blocks_added, std::map<std::pair<uint64_t, uint64_t>, size_t> &output_tracker_cache);
|
||||
bool accept_pool_tx_for_processing(const crypto::hash &txid);
|
||||
void process_unconfirmed_transfer(bool incremental, const crypto::hash &txid, wallet2::unconfirmed_transfer_details &tx_details, bool seen_in_pool, std::chrono::system_clock::time_point now, bool refreshed);
|
||||
void process_pool_info_extent(const cryptonote::COMMAND_RPC_GET_BLOCKS_FAST::response &res, std::vector<std::tuple<cryptonote::transaction, crypto::hash, bool>> &process_txs, bool refreshed);
|
||||
|
@ -1831,9 +1851,6 @@ private:
|
|||
bool generate_chacha_key_from_secret_keys(crypto::chacha_key &key) const;
|
||||
void generate_chacha_key_from_password(const epee::wipeable_string &pass, crypto::chacha_key &key) const;
|
||||
crypto::hash get_payment_id(const pending_tx &ptx) const;
|
||||
void check_acc_out_precomp(const cryptonote::tx_out &o, const crypto::key_derivation &derivation, const std::vector<crypto::key_derivation> &additional_derivations, size_t i, tx_scan_info_t &tx_scan_info) const;
|
||||
void check_acc_out_precomp(const cryptonote::tx_out &o, const crypto::key_derivation &derivation, const std::vector<crypto::key_derivation> &additional_derivations, size_t i, const is_out_data *is_out_data, tx_scan_info_t &tx_scan_info) const;
|
||||
void check_acc_out_precomp_once(const cryptonote::tx_out &o, const crypto::key_derivation &derivation, const std::vector<crypto::key_derivation> &additional_derivations, size_t i, const is_out_data *is_out_data, tx_scan_info_t &tx_scan_info, bool &already_seen) const;
|
||||
void parse_block_round(const cryptonote::blobdata &blob, cryptonote::block &bl, crypto::hash &bl_id, bool &error) const;
|
||||
uint64_t get_upper_transaction_weight_limit();
|
||||
std::vector<uint64_t> get_unspent_amounts_vector(bool strict);
|
||||
|
@ -1849,7 +1866,6 @@ private:
|
|||
bool tx_add_fake_output(std::vector<std::vector<tools::wallet2::get_outs_entry>> &outs, uint64_t global_index, const crypto::public_key& tx_public_key, const rct::key& mask, uint64_t real_index, bool unlocked, std::unordered_set<crypto::public_key> &valid_public_keys_cache) const;
|
||||
bool should_pick_a_second_output(bool use_rct, size_t n_transfers, const std::vector<size_t> &unused_transfers_indices, const std::vector<size_t> &unused_dust_indices) const;
|
||||
std::vector<size_t> get_only_rct(const std::vector<size_t> &unused_dust_indices, const std::vector<size_t> &unused_transfers_indices) const;
|
||||
void scan_output(const cryptonote::transaction &tx, bool miner_tx, const crypto::public_key &tx_pub_key, size_t i, tx_scan_info_t &tx_scan_info, int &num_vouts_received, std::unordered_map<cryptonote::subaddress_index, uint64_t> &tx_money_got_in_outs, std::vector<size_t> &outs, bool pool);
|
||||
void trim_hashchain();
|
||||
crypto::key_image get_multisig_composite_key_image(size_t n) const;
|
||||
rct::multisig_kLRki get_multisig_composite_kLRki(size_t n, const std::unordered_set<crypto::public_key> &ignore_set, std::unordered_set<rct::key> &used_L, std::unordered_set<rct::key> &new_used_L) const;
|
||||
|
@ -1887,8 +1903,7 @@ private:
|
|||
|
||||
uint64_t get_segregation_fork_height() const;
|
||||
|
||||
void cache_tx_data(const cryptonote::transaction& tx, const crypto::hash &txid, tx_cache_data &tx_cache_data) const;
|
||||
std::shared_ptr<std::map<std::pair<uint64_t, uint64_t>, size_t>> create_output_tracker_cache() const;
|
||||
std::map<std::pair<uint64_t, uint64_t>, size_t> create_output_tracker_cache() const;
|
||||
|
||||
void init_type(hw::device::device_type device_type);
|
||||
void setup_new_blockchain();
|
||||
|
|
|
@ -731,19 +731,6 @@ bool gen_tx_output_is_not_txout_to_key::generate(std::vector<test_event_entry>&
|
|||
builder.step1_init();
|
||||
builder.step2_fill_inputs(miner_account.get_keys(), sources);
|
||||
|
||||
builder.m_tx.vout.push_back(tx_out());
|
||||
builder.m_tx.vout.back().amount = 1;
|
||||
builder.m_tx.vout.back().target = txout_to_script();
|
||||
|
||||
builder.step4_calc_hash();
|
||||
builder.step5_sign(sources);
|
||||
|
||||
DO_CALLBACK(events, "mark_invalid_tx");
|
||||
events.push_back(builder.m_tx);
|
||||
|
||||
builder.step1_init();
|
||||
builder.step2_fill_inputs(miner_account.get_keys(), sources);
|
||||
|
||||
builder.m_tx.vout.push_back(tx_out());
|
||||
builder.m_tx.vout.back().amount = 1;
|
||||
builder.m_tx.vout.back().target = txout_to_scripthash();
|
||||
|
|
|
@ -32,7 +32,8 @@ void wallet_accessor_test::set_account(tools::wallet2 * wallet, cryptonote::acco
|
|||
void wallet_accessor_test::process_parsed_blocks(tools::wallet2 * wallet, uint64_t start_height, const std::vector<cryptonote::block_complete_entry> &blocks, const std::vector<tools::wallet2::parsed_block> &parsed_blocks, uint64_t& blocks_added)
|
||||
{
|
||||
if (wallet != nullptr) {
|
||||
wallet->process_parsed_blocks(start_height, blocks, parsed_blocks, blocks_added);
|
||||
auto output_tracker_cache = wallet->create_output_tracker_cache();
|
||||
wallet->process_parsed_blocks(start_height, blocks, parsed_blocks, blocks_added, output_tracker_cache);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -39,6 +39,7 @@ set(unit_tests_sources
|
|||
bulletproofs_plus.cpp
|
||||
canonical_amounts.cpp
|
||||
carrot_core.cpp
|
||||
carrot_impl.cpp
|
||||
carrot_legacy.cpp
|
||||
carrot_mock_helpers.cpp
|
||||
carrot_transcript_fixed.cpp
|
||||
|
@ -102,7 +103,9 @@ set(unit_tests_sources
|
|||
output_selection.cpp
|
||||
vercmp.cpp
|
||||
ringdb.cpp
|
||||
wallet_scanning.cpp
|
||||
wallet_storage.cpp
|
||||
wallet_tx_builder.cpp
|
||||
wipeable_string.cpp
|
||||
is_hdd.cpp
|
||||
aligned.cpp
|
||||
|
@ -120,6 +123,7 @@ target_link_libraries(unit_tests
|
|||
PRIVATE
|
||||
ringct
|
||||
carrot_core
|
||||
carrot_impl
|
||||
cryptonote_protocol
|
||||
cryptonote_core
|
||||
daemon_messages
|
||||
|
|
670
tests/unit_tests/carrot_fcmp.cpp
Normal file
670
tests/unit_tests/carrot_fcmp.cpp
Normal file
|
@ -0,0 +1,670 @@
|
|||
// 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.
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
#include "carrot_core/output_set_finalization.h"
|
||||
#include "carrot_core/payment_proposal.h"
|
||||
#include "carrot_impl/carrot_tx_builder_inputs.h"
|
||||
#include "carrot_impl/carrot_tx_builder_utils.h"
|
||||
#include "carrot_impl/carrot_tx_format_utils.h"
|
||||
#include "carrot_impl/input_selection.h"
|
||||
#include "carrot_mock_helpers.h"
|
||||
#include "common/container_helpers.h"
|
||||
#include "crypto/generators.h"
|
||||
#include "cryptonote_basic/account.h"
|
||||
#include "cryptonote_basic/subaddress_index.h"
|
||||
#include "cryptonote_basic/cryptonote_format_utils.h"
|
||||
#include "cryptonote_core/blockchain.h"
|
||||
#include "curve_trees.h"
|
||||
#include "fcmp_pp/prove.h"
|
||||
#include "ringct/bulletproofs_plus.h"
|
||||
#include "ringct/rctOps.h"
|
||||
#include "ringct/rctSigs.h"
|
||||
|
||||
using namespace carrot;
|
||||
|
||||
namespace
|
||||
{
|
||||
//----------------------------------------------------------------------------------------------------------------------
|
||||
//----------------------------------------------------------------------------------------------------------------------
|
||||
static constexpr rct::xmr_amount MAX_AMOUNT_FCMP_PP = MONEY_SUPPLY /
|
||||
(FCMP_PLUS_PLUS_MAX_INPUTS + FCMP_PLUS_PLUS_MAX_OUTPUTS + 1);
|
||||
//----------------------------------------------------------------------------------------------------------------------
|
||||
//----------------------------------------------------------------------------------------------------------------------
|
||||
using CarrotEnoteVariant = tools::variant<CarrotCoinbaseEnoteV1, CarrotEnoteV1>;
|
||||
//----------------------------------------------------------------------------------------------------------------------
|
||||
//----------------------------------------------------------------------------------------------------------------------
|
||||
struct CarrotOutputContextsAndKeys
|
||||
{
|
||||
std::vector<CarrotEnoteVariant> enotes;
|
||||
std::vector<encrypted_payment_id_t> encrypted_payment_ids;
|
||||
std::vector<fcmp_pp::curve_trees::OutputContext> output_pairs;
|
||||
};
|
||||
//----------------------------------------------------------------------------------------------------------------------
|
||||
//----------------------------------------------------------------------------------------------------------------------
|
||||
static const CarrotOutputContextsAndKeys generate_random_carrot_outputs(
|
||||
const mock::mock_carrot_and_legacy_keys &keys,
|
||||
const std::size_t old_n_leaf_tuples,
|
||||
const std::size_t new_n_leaf_tuples)
|
||||
{
|
||||
CarrotOutputContextsAndKeys outs;
|
||||
outs.enotes.reserve(new_n_leaf_tuples);
|
||||
outs.encrypted_payment_ids.reserve(new_n_leaf_tuples);
|
||||
outs.output_pairs.reserve(new_n_leaf_tuples);
|
||||
|
||||
for (std::size_t i = 0; i < new_n_leaf_tuples; ++i)
|
||||
{
|
||||
const std::uint64_t output_id = old_n_leaf_tuples + i;
|
||||
fcmp_pp::curve_trees::OutputContext output_pair{
|
||||
.output_id = output_id
|
||||
};
|
||||
|
||||
CarrotPaymentProposalV1 normal_payment_proposal{
|
||||
.destination = keys.cryptonote_address(),
|
||||
.amount = rct::randXmrAmount(MAX_AMOUNT_FCMP_PP),
|
||||
.randomness = gen_janus_anchor()
|
||||
};
|
||||
CarrotPaymentProposalVerifiableSelfSendV1 selfsend_payment_proposal{
|
||||
.proposal = CarrotPaymentProposalSelfSendV1{
|
||||
.destination_address_spend_pubkey = keys.cryptonote_address().address_spend_pubkey,
|
||||
.amount = rct::randXmrAmount(MAX_AMOUNT_FCMP_PP),
|
||||
.enote_type = i % 2 ? CarrotEnoteType::CHANGE : CarrotEnoteType::PAYMENT,
|
||||
.enote_ephemeral_pubkey = gen_x25519_pubkey()
|
||||
},
|
||||
.subaddr_index = {0, 0}
|
||||
};
|
||||
|
||||
bool push_coinbase = false;
|
||||
CarrotCoinbaseEnoteV1 coinbase_enote;
|
||||
RCTOutputEnoteProposal rct_output_enote_proposal;
|
||||
encrypted_payment_id_t encrypted_payment_id = null_payment_id;
|
||||
|
||||
const unsigned int enote_derive_type = i % 7;
|
||||
switch (enote_derive_type)
|
||||
{
|
||||
case 0: // coinbase enote
|
||||
get_coinbase_output_proposal_v1(normal_payment_proposal,
|
||||
mock::gen_block_index(),
|
||||
coinbase_enote);
|
||||
push_coinbase = true;
|
||||
break;
|
||||
case 1: // normal enote main address
|
||||
get_output_proposal_normal_v1(normal_payment_proposal,
|
||||
mock::gen_key_image(),
|
||||
rct_output_enote_proposal,
|
||||
encrypted_payment_id);
|
||||
break;
|
||||
case 2: // normal enote subaddress
|
||||
normal_payment_proposal.destination = keys.subaddress({mock::gen_subaddress_index()});
|
||||
get_output_proposal_normal_v1(normal_payment_proposal,
|
||||
mock::gen_key_image(),
|
||||
rct_output_enote_proposal,
|
||||
encrypted_payment_id);
|
||||
break;
|
||||
case 3: // special enote main address
|
||||
get_output_proposal_special_v1(selfsend_payment_proposal.proposal,
|
||||
keys.k_view_incoming_dev,
|
||||
keys.cryptonote_address().address_spend_pubkey,
|
||||
mock::gen_key_image(),
|
||||
std::nullopt,
|
||||
rct_output_enote_proposal);
|
||||
break;
|
||||
case 4: // special enote subaddress
|
||||
selfsend_payment_proposal.subaddr_index.index = mock::gen_subaddress_index();
|
||||
selfsend_payment_proposal.proposal.destination_address_spend_pubkey
|
||||
= keys.subaddress(selfsend_payment_proposal.subaddr_index).address_spend_pubkey;
|
||||
get_output_proposal_special_v1(selfsend_payment_proposal.proposal,
|
||||
keys.k_view_incoming_dev,
|
||||
keys.cryptonote_address().address_spend_pubkey,
|
||||
mock::gen_key_image(),
|
||||
std::nullopt,
|
||||
rct_output_enote_proposal);
|
||||
break;
|
||||
case 5: // internal main address
|
||||
get_output_proposal_internal_v1(selfsend_payment_proposal.proposal,
|
||||
keys.s_view_balance_dev,
|
||||
mock::gen_key_image(),
|
||||
std::nullopt,
|
||||
rct_output_enote_proposal);
|
||||
break;
|
||||
case 6: // internal subaddress
|
||||
selfsend_payment_proposal.subaddr_index.index = mock::gen_subaddress_index();
|
||||
selfsend_payment_proposal.proposal.destination_address_spend_pubkey
|
||||
= keys.subaddress(selfsend_payment_proposal.subaddr_index).address_spend_pubkey;
|
||||
get_output_proposal_internal_v1(selfsend_payment_proposal.proposal,
|
||||
keys.s_view_balance_dev,
|
||||
mock::gen_key_image(),
|
||||
std::nullopt,
|
||||
rct_output_enote_proposal);
|
||||
break;
|
||||
}
|
||||
|
||||
if (push_coinbase)
|
||||
{
|
||||
output_pair.output_pair.output_pubkey = coinbase_enote.onetime_address;
|
||||
output_pair.output_pair.commitment = rct::zeroCommitVartime(coinbase_enote.amount);
|
||||
outs.enotes.push_back(coinbase_enote);
|
||||
outs.encrypted_payment_ids.push_back(null_payment_id);
|
||||
}
|
||||
else
|
||||
{
|
||||
output_pair.output_pair.output_pubkey = rct_output_enote_proposal.enote.onetime_address;
|
||||
output_pair.output_pair.commitment = rct_output_enote_proposal.enote.amount_commitment;
|
||||
outs.enotes.push_back(rct_output_enote_proposal.enote);
|
||||
}
|
||||
|
||||
outs.encrypted_payment_ids.push_back(encrypted_payment_id);
|
||||
outs.output_pairs.push_back(output_pair);
|
||||
}
|
||||
|
||||
return outs;
|
||||
}
|
||||
} //anonymous namespace
|
||||
//----------------------------------------------------------------------------------------------------------------------
|
||||
//----------------------------------------------------------------------------------------------------------------------
|
||||
TEST(carrot_fcmp, receive_scan_spend_and_verify_serialized_carrot_tx)
|
||||
{
|
||||
// In this test we:
|
||||
// 1. Populate a curve tree with Carrot-derived enotes to Alice
|
||||
// 2. Scan those enotes and construct a transfer-style tx to Bob
|
||||
// 3. Serialize that tx, then deserialize it
|
||||
// 4. Verify non-input consensus rules on the deserialized tx
|
||||
// 5. Verify FCMP membership in the curve tree on the deserialized tx
|
||||
// 6. Scan the deserialized tx to Bob
|
||||
|
||||
mock::mock_carrot_and_legacy_keys alice;
|
||||
mock::mock_carrot_and_legacy_keys bob;
|
||||
alice.generate();
|
||||
bob.generate();
|
||||
|
||||
const size_t n_inputs = crypto::rand_range<size_t>(CARROT_MIN_TX_INPUTS, FCMP_PLUS_PLUS_MAX_INPUTS);
|
||||
const size_t n_outputs = crypto::rand_range<size_t>(CARROT_MIN_TX_OUTPUTS, FCMP_PLUS_PLUS_MAX_OUTPUTS);
|
||||
|
||||
const std::size_t selene_chunk_width = fcmp_pp::curve_trees::SELENE_CHUNK_WIDTH;
|
||||
const std::size_t helios_chunk_width = fcmp_pp::curve_trees::HELIOS_CHUNK_WIDTH;
|
||||
const std::size_t tree_depth = 3;
|
||||
const std::size_t n_tree_layers = tree_depth + 1;
|
||||
const size_t expected_num_selene_branch_blinds = (tree_depth + 1) / 2;
|
||||
const size_t expected_num_helios_branch_blinds = tree_depth / 2;
|
||||
|
||||
LOG_PRINT_L1("Test carrot_impl.receive_scan_spend_and_verify_serialized_carrot_tx with selene chunk width "
|
||||
<< selene_chunk_width << ", helios chunk width " << helios_chunk_width << ", tree depth " << tree_depth
|
||||
<< ", number of inputs " << n_inputs << ", number of outputs " << n_outputs);
|
||||
|
||||
// Tree params
|
||||
uint64_t min_leaves_needed_for_tree_depth = 0;
|
||||
const auto curve_trees = test::init_curve_trees_test(selene_chunk_width,
|
||||
helios_chunk_width,
|
||||
tree_depth,
|
||||
min_leaves_needed_for_tree_depth);
|
||||
|
||||
// Generate enotes...
|
||||
LOG_PRINT_L1("Generating carrot-derived enotes to Alice");
|
||||
const auto new_outputs = generate_random_carrot_outputs(alice,
|
||||
0,
|
||||
min_leaves_needed_for_tree_depth
|
||||
);
|
||||
ASSERT_GT(min_leaves_needed_for_tree_depth, n_inputs);
|
||||
|
||||
// generate output ids to use as inputs...
|
||||
std::set<size_t> picked_output_ids_set;
|
||||
while (picked_output_ids_set.size() < n_inputs)
|
||||
picked_output_ids_set.insert(crypto::rand_idx(min_leaves_needed_for_tree_depth));
|
||||
std::vector<size_t> picked_output_ids(picked_output_ids_set.cbegin(), picked_output_ids_set.cend());
|
||||
std::shuffle(picked_output_ids.begin(), picked_output_ids.end(), crypto::random_device{});
|
||||
|
||||
// scan inputs and make key images and opening hints...
|
||||
// a z C_a K_o opening hint output id
|
||||
using input_info_t = std::tuple<rct::xmr_amount, rct::key, rct::key, crypto::public_key, OutputOpeningHintVariant, std::uint64_t>;
|
||||
LOG_PRINT_L1("Alice scanning inputs");
|
||||
std::unordered_map<crypto::key_image, input_info_t> input_info_by_ki;
|
||||
rct::xmr_amount input_amount_sum = 0;
|
||||
for (const size_t picked_output_id : picked_output_ids)
|
||||
{
|
||||
// find index into new_outputs based on picked_output_id
|
||||
size_t new_outputs_idx;
|
||||
for (new_outputs_idx = 0; new_outputs_idx < new_outputs.output_pairs.size(); ++new_outputs_idx)
|
||||
{
|
||||
if (new_outputs.output_pairs.at(new_outputs_idx).output_id == picked_output_id)
|
||||
break;
|
||||
}
|
||||
ASSERT_LT(new_outputs_idx, new_outputs.enotes.size());
|
||||
|
||||
// compile information about this enote
|
||||
const CarrotEnoteVariant &enote_v = new_outputs.enotes.at(new_outputs_idx);
|
||||
OutputOpeningHintVariant opening_hint;
|
||||
std::vector<mock::mock_scan_result_t> scan_results;
|
||||
if (enote_v.is_type<CarrotEnoteV1>())
|
||||
{
|
||||
const CarrotEnoteV1 &enote = enote_v.unwrap<CarrotEnoteV1>();
|
||||
const encrypted_payment_id_t encrypted_payment_id = new_outputs.encrypted_payment_ids.at(new_outputs_idx);
|
||||
mock::mock_scan_enote_set({enote},
|
||||
encrypted_payment_id,
|
||||
alice,
|
||||
scan_results);
|
||||
ASSERT_EQ(1, scan_results.size());
|
||||
const mock::mock_scan_result_t &scan_result = scan_results.front();
|
||||
const auto subaddr_it = alice.subaddress_map.find(scan_result.address_spend_pubkey);
|
||||
ASSERT_NE(alice.subaddress_map.cend(), subaddr_it);
|
||||
opening_hint = CarrotOutputOpeningHintV1{
|
||||
.source_enote = enote,
|
||||
.encrypted_payment_id = encrypted_payment_id,
|
||||
.subaddr_index = subaddr_it->second
|
||||
};
|
||||
}
|
||||
else // is coinbase
|
||||
{
|
||||
const CarrotCoinbaseEnoteV1 &enote = enote_v.unwrap<CarrotCoinbaseEnoteV1>();
|
||||
mock::mock_scan_coinbase_enote_set({enote},
|
||||
alice,
|
||||
scan_results);
|
||||
ASSERT_EQ(1, scan_results.size());
|
||||
const mock::mock_scan_result_t &scan_result = scan_results.front();
|
||||
ASSERT_EQ(alice.cryptonote_address().address_spend_pubkey, scan_result.address_spend_pubkey);
|
||||
opening_hint = CarrotCoinbaseOutputOpeningHintV1{
|
||||
.source_enote = enote,
|
||||
.derive_type = AddressDeriveType::Carrot
|
||||
};
|
||||
}
|
||||
ASSERT_EQ(1, scan_results.size());
|
||||
const mock::mock_scan_result_t &scan_result = scan_results.front();
|
||||
const fcmp_pp::curve_trees::OutputPair &output_pair = new_outputs.output_pairs.at(new_outputs_idx).output_pair;
|
||||
const crypto::key_image ki = alice.derive_key_image(scan_result.address_spend_pubkey,
|
||||
scan_result.sender_extension_g,
|
||||
scan_result.sender_extension_t,
|
||||
output_pair.output_pubkey);
|
||||
|
||||
ASSERT_EQ(0, input_info_by_ki.count(ki));
|
||||
|
||||
input_info_by_ki[ki] = {scan_result.amount,
|
||||
rct::sk2rct(scan_result.amount_blinding_factor),
|
||||
output_pair.commitment,
|
||||
output_pair.output_pubkey,
|
||||
opening_hint,
|
||||
new_outputs.output_pairs.at(new_outputs_idx).output_id};
|
||||
input_amount_sum += scan_result.amount;
|
||||
}
|
||||
|
||||
// generate n_outputs-1 payment proposals to bob ...
|
||||
LOG_PRINT_L1("Generating payment proposals to Bob");
|
||||
rct::xmr_amount output_amount_remaining = rct::randXmrAmount(input_amount_sum);
|
||||
std::vector<CarrotPaymentProposalV1> bob_payment_proposals;
|
||||
for (size_t i = 0; i < n_outputs - 1; ++i)
|
||||
{
|
||||
const bool use_subaddress = i % 2 == 1;
|
||||
const CarrotDestinationV1 addr = use_subaddress ?
|
||||
bob.subaddress({mock::gen_subaddress_index()}) :
|
||||
bob.cryptonote_address();
|
||||
const rct::xmr_amount amount = rct::randXmrAmount(output_amount_remaining);
|
||||
bob_payment_proposals.push_back(CarrotPaymentProposalV1{
|
||||
.destination = addr,
|
||||
.amount = amount,
|
||||
.randomness = gen_janus_anchor()
|
||||
});
|
||||
output_amount_remaining -= amount;
|
||||
}
|
||||
|
||||
// make a transfer-type tx proposal
|
||||
// @TODO: this can fail sporadically if fee exceeds remaining funds
|
||||
LOG_PRINT_L1("Creating transaction proposal");
|
||||
const rct::xmr_amount fee_per_weight = 1;
|
||||
CarrotTransactionProposalV1 tx_proposal;
|
||||
make_carrot_transaction_proposal_v1_transfer(bob_payment_proposals,
|
||||
/*selfsend_payment_proposals=*/{},
|
||||
fee_per_weight,
|
||||
/*extra=*/{},
|
||||
[&input_info_by_ki]
|
||||
(
|
||||
const boost::multiprecision::int128_t&,
|
||||
const std::map<std::size_t, rct::xmr_amount>&,
|
||||
const std::size_t,
|
||||
const std::size_t,
|
||||
std::vector<CarrotSelectedInput>& key_images_out)
|
||||
{
|
||||
key_images_out.clear();
|
||||
key_images_out.reserve(input_info_by_ki.size());
|
||||
for (const auto &info : input_info_by_ki)
|
||||
{
|
||||
key_images_out.push_back(CarrotSelectedInput{
|
||||
.amount = std::get<0>(info.second),
|
||||
.key_image = info.first
|
||||
});
|
||||
}
|
||||
},
|
||||
&alice.s_view_balance_dev,
|
||||
&alice.k_view_incoming_dev,
|
||||
alice.carrot_account_spend_pubkey,
|
||||
tx_proposal);
|
||||
|
||||
ASSERT_EQ(n_outputs, tx_proposal.normal_payment_proposals.size() + tx_proposal.selfsend_payment_proposals.size());
|
||||
|
||||
// collect core selfsend proposals
|
||||
std::vector<CarrotPaymentProposalSelfSendV1> selfsend_payment_proposal_cores;
|
||||
for (const CarrotPaymentProposalVerifiableSelfSendV1 &selfsend_payment_proposal : tx_proposal.selfsend_payment_proposals)
|
||||
selfsend_payment_proposal_cores.push_back(selfsend_payment_proposal.proposal);
|
||||
|
||||
// derive output enote set
|
||||
LOG_PRINT_L1("Deriving enotes");
|
||||
std::vector<RCTOutputEnoteProposal> output_enote_proposals;
|
||||
encrypted_payment_id_t encrypted_payment_id;
|
||||
get_output_enote_proposals(tx_proposal.normal_payment_proposals,
|
||||
selfsend_payment_proposal_cores,
|
||||
tx_proposal.dummy_encrypted_payment_id,
|
||||
&alice.s_view_balance_dev,
|
||||
&alice.k_view_incoming_dev,
|
||||
alice.carrot_account_spend_pubkey,
|
||||
tx_proposal.key_images_sorted.at(0),
|
||||
output_enote_proposals,
|
||||
encrypted_payment_id);
|
||||
|
||||
// Collect balance info and enotes
|
||||
std::vector<crypto::public_key> input_onetime_addresses;
|
||||
std::vector<rct::key> input_amount_commitments;
|
||||
std::vector<rct::key> input_amount_blinding_factors;
|
||||
std::vector<rct::xmr_amount> output_amounts;
|
||||
std::vector<rct::key> output_amount_blinding_factors;
|
||||
std::vector<CarrotEnoteV1> output_enotes;
|
||||
for (size_t i = 0; i < n_inputs; ++i)
|
||||
{
|
||||
const input_info_t &input_info = input_info_by_ki.at(tx_proposal.key_images_sorted.at(i));
|
||||
input_onetime_addresses.push_back(std::get<3>(input_info));
|
||||
input_amount_commitments.push_back(std::get<2>(input_info));
|
||||
input_amount_blinding_factors.push_back(std::get<1>(input_info));
|
||||
}
|
||||
for (const RCTOutputEnoteProposal &output_enote_proposal : output_enote_proposals)
|
||||
{
|
||||
output_amounts.push_back(output_enote_proposal.amount);
|
||||
output_amount_blinding_factors.push_back(rct::sk2rct(output_enote_proposal.amount_blinding_factor));
|
||||
output_enotes.push_back(output_enote_proposal.enote);
|
||||
}
|
||||
|
||||
// make pruned tx
|
||||
LOG_PRINT_L1("Storing carrot to transaction");
|
||||
cryptonote::transaction tx = store_carrot_to_transaction_v1(output_enotes,
|
||||
tx_proposal.key_images_sorted,
|
||||
tx_proposal.fee,
|
||||
encrypted_payment_id);
|
||||
|
||||
ASSERT_EQ(2, tx.version);
|
||||
ASSERT_EQ(0, tx.unlock_time);
|
||||
ASSERT_EQ(n_inputs, tx.vin.size());
|
||||
ASSERT_EQ(n_outputs, tx.vout.size());
|
||||
ASSERT_EQ(n_outputs, tx.rct_signatures.outPk.size());
|
||||
|
||||
// Generate bulletproof+
|
||||
LOG_PRINT_L1("Generating Bulletproof+");
|
||||
tx.rct_signatures.p.bulletproofs_plus.push_back(rct::bulletproof_plus_PROVE(output_amounts, output_amount_blinding_factors));
|
||||
ASSERT_EQ(n_outputs, tx.rct_signatures.p.bulletproofs_plus.at(0).V.size());
|
||||
|
||||
// expand tx and calculate signable tx hash
|
||||
LOG_PRINT_L1("Calculating signable tx hash");
|
||||
hw::device &hwdev = hw::get_device("default");
|
||||
ASSERT_TRUE(cryptonote::expand_transaction_1(tx, /*base_only=*/false));
|
||||
const crypto::hash tx_prefix_hash = cryptonote::get_transaction_prefix_hash(tx);
|
||||
tx.rct_signatures.message = rct::hash2rct(tx_prefix_hash);
|
||||
tx.rct_signatures.p.pseudoOuts.resize(n_inputs); // @TODO: make this not necessary to call get_mlsag_hash
|
||||
const crypto::hash signable_tx_hash = rct::rct2hash(rct::get_pre_mlsag_hash(tx.rct_signatures, hwdev));
|
||||
|
||||
// rerandomize inputs
|
||||
LOG_PRINT_L1("Making rerandomized inputs");
|
||||
std::vector<FcmpRerandomizedOutputCompressed> rerandomized_outputs;
|
||||
make_carrot_rerandomized_outputs_nonrefundable(input_onetime_addresses,
|
||||
input_amount_commitments,
|
||||
input_amount_blinding_factors,
|
||||
output_amount_blinding_factors,
|
||||
rerandomized_outputs);
|
||||
|
||||
// Make SA/L proofs
|
||||
LOG_PRINT_L1("Generating FCMP++ SA/L proofs");
|
||||
std::vector<crypto::key_image> actual_key_images;
|
||||
std::vector<fcmp_pp::FcmpPpSalProof> sal_proofs;
|
||||
for (size_t i = 0; i < n_inputs; ++i)
|
||||
{
|
||||
const CarrotOpenableRerandomizedOutputV1 openable_opening_hint{
|
||||
.rerandomized_output = rerandomized_outputs.at(i),
|
||||
.opening_hint = std::get<4>(input_info_by_ki.at(tx_proposal.key_images_sorted.at(i)))
|
||||
};
|
||||
|
||||
make_sal_proof_any_to_carrot_v1(signable_tx_hash,
|
||||
openable_opening_hint,
|
||||
alice.k_prove_spend,
|
||||
alice.k_generate_image,
|
||||
alice.s_view_balance_dev,
|
||||
alice.k_view_incoming_dev,
|
||||
alice.s_generate_address_dev,
|
||||
tools::add_element(sal_proofs),
|
||||
tools::add_element(actual_key_images));
|
||||
}
|
||||
|
||||
// Init tree in memory
|
||||
LOG_PRINT_L1("Initializing tree with " << min_leaves_needed_for_tree_depth << " leaves");
|
||||
CurveTreesGlobalTree global_tree(*curve_trees);
|
||||
ASSERT_TRUE(global_tree.grow_tree(0, min_leaves_needed_for_tree_depth, new_outputs.output_pairs));
|
||||
LOG_PRINT_L1("Finished initializing tree with " << min_leaves_needed_for_tree_depth << " leaves");
|
||||
|
||||
// Make FCMP paths
|
||||
LOG_PRINT_L1("Calculating FCMP paths");
|
||||
std::vector<fcmp_pp::ProofInput> fcmp_proof_inputs(n_inputs);
|
||||
for (size_t i = 0; i < n_inputs; ++i)
|
||||
{
|
||||
const size_t leaf_idx = std::get<5>(input_info_by_ki.at(tx_proposal.key_images_sorted.at(i)));
|
||||
const auto path = global_tree.get_path_at_leaf_idx(leaf_idx);
|
||||
const std::size_t path_leaf_idx = leaf_idx % curve_trees->m_c1_width;
|
||||
|
||||
const fcmp_pp::curve_trees::OutputPair output_pair = {rct::rct2pk(path.leaves[path_leaf_idx].O),
|
||||
path.leaves[path_leaf_idx].C};
|
||||
const auto output_tuple = fcmp_pp::curve_trees::output_to_tuple(output_pair);
|
||||
|
||||
const auto path_for_proof = curve_trees->path_for_proof(path, output_tuple);
|
||||
|
||||
const auto helios_scalar_chunks = fcmp_pp::tower_cycle::scalar_chunks_to_chunk_vector<fcmp_pp::HeliosT>(
|
||||
path_for_proof.c2_scalar_chunks);
|
||||
const auto selene_scalar_chunks = fcmp_pp::tower_cycle::scalar_chunks_to_chunk_vector<fcmp_pp::SeleneT>(
|
||||
path_for_proof.c1_scalar_chunks);
|
||||
|
||||
const auto path_rust = fcmp_pp::path_new({path_for_proof.leaves.data(), path_for_proof.leaves.size()},
|
||||
path_for_proof.output_idx,
|
||||
{helios_scalar_chunks.data(), helios_scalar_chunks.size()},
|
||||
{selene_scalar_chunks.data(), selene_scalar_chunks.size()});
|
||||
|
||||
fcmp_proof_inputs[i].path = path_rust;
|
||||
}
|
||||
|
||||
// make FCMP blinds
|
||||
LOG_PRINT_L1("Calculating branch and output blinds");
|
||||
for (size_t i = 0; i < n_inputs; ++i)
|
||||
{
|
||||
fcmp_pp::ProofInput &proof_input = fcmp_proof_inputs[i];
|
||||
const FcmpRerandomizedOutputCompressed &rerandomized_output = rerandomized_outputs.at(i);
|
||||
|
||||
// calculate individual blinds
|
||||
uint8_t *blinded_o_blind = fcmp_pp::blind_o_blind(fcmp_pp::o_blind(rerandomized_output));
|
||||
uint8_t *blinded_i_blind = fcmp_pp::blind_i_blind(fcmp_pp::i_blind(rerandomized_output));
|
||||
uint8_t *blinded_i_blind_blind = fcmp_pp::blind_i_blind_blind(fcmp_pp::i_blind_blind(rerandomized_output));
|
||||
uint8_t *blinded_c_blind = fcmp_pp::blind_c_blind(fcmp_pp::c_blind(rerandomized_output));
|
||||
|
||||
// make output blinds
|
||||
proof_input.output_blinds = fcmp_pp::output_blinds_new(
|
||||
blinded_o_blind, blinded_i_blind, blinded_i_blind_blind, blinded_c_blind);
|
||||
|
||||
// generate selene blinds
|
||||
proof_input.selene_branch_blinds.reserve(expected_num_selene_branch_blinds);
|
||||
for (size_t j = 0; j < expected_num_selene_branch_blinds; ++j)
|
||||
proof_input.selene_branch_blinds.push_back(fcmp_pp::selene_branch_blind());
|
||||
|
||||
// generate helios blinds
|
||||
proof_input.helios_branch_blinds.reserve(expected_num_helios_branch_blinds);
|
||||
for (size_t j = 0; j < expected_num_helios_branch_blinds; ++j)
|
||||
proof_input.helios_branch_blinds.push_back(fcmp_pp::helios_branch_blind());
|
||||
|
||||
// dealloc individual blinds
|
||||
free(blinded_o_blind);
|
||||
free(blinded_i_blind);
|
||||
free(blinded_i_blind_blind);
|
||||
free(blinded_c_blind);
|
||||
}
|
||||
|
||||
// Make FCMP membership proof
|
||||
LOG_PRINT_L1("Generating FCMP++ membership proofs");
|
||||
std::vector<const uint8_t*> fcmp_proof_inputs_rust;
|
||||
for (size_t i = 0; i < n_inputs; ++i)
|
||||
{
|
||||
fcmp_pp::ProofInput &proof_input = fcmp_proof_inputs.at(i);
|
||||
fcmp_proof_inputs_rust.push_back(fcmp_pp::fcmp_prove_input_new(
|
||||
rerandomized_outputs.at(i),
|
||||
proof_input.path,
|
||||
proof_input.output_blinds,
|
||||
proof_input.selene_branch_blinds,
|
||||
proof_input.helios_branch_blinds));
|
||||
free(proof_input.path);
|
||||
free(proof_input.output_blinds);
|
||||
for (const uint8_t *branch_blind : proof_input.selene_branch_blinds)
|
||||
free(const_cast<uint8_t*>(branch_blind));
|
||||
for (const uint8_t *branch_blind : proof_input.helios_branch_blinds)
|
||||
free(const_cast<uint8_t*>(branch_blind));
|
||||
}
|
||||
const fcmp_pp::FcmpMembershipProof membership_proof = fcmp_pp::prove_membership(fcmp_proof_inputs_rust,
|
||||
n_tree_layers);
|
||||
|
||||
// Dealloc FCMP proof inputs
|
||||
for (const uint8_t *proof_input : fcmp_proof_inputs_rust)
|
||||
free(const_cast<uint8_t*>(proof_input));
|
||||
|
||||
// Attach rctSigPrunable to tx
|
||||
LOG_PRINT_L1("Storing rctSig prunable");
|
||||
const std::uint64_t fcmp_block_reference_index = mock::gen_block_index();
|
||||
tx.rct_signatures.p = store_fcmp_proofs_to_rct_prunable_v1(std::move(tx.rct_signatures.p.bulletproofs_plus),
|
||||
rerandomized_outputs,
|
||||
sal_proofs,
|
||||
membership_proof,
|
||||
fcmp_block_reference_index,
|
||||
n_tree_layers);
|
||||
tx.pruned = false;
|
||||
|
||||
// Serialize tx to bytes
|
||||
LOG_PRINT_L1("Serializing & deserializing transaction");
|
||||
const cryptonote::blobdata tx_blob = cryptonote::tx_to_blob(tx);
|
||||
|
||||
// Deserialize tx
|
||||
cryptonote::transaction deserialized_tx;
|
||||
ASSERT_TRUE(cryptonote::parse_and_validate_tx_from_blob(tx_blob, deserialized_tx));
|
||||
|
||||
// Expand tx
|
||||
auto tree_root = global_tree.get_tree_root();
|
||||
const crypto::hash tx_prefix_hash_2 = cryptonote::get_transaction_prefix_hash(deserialized_tx);
|
||||
ASSERT_TRUE(cryptonote::Blockchain::expand_transaction_2(deserialized_tx, tx_prefix_hash_2, {}, tree_root));
|
||||
|
||||
// Verify non-input consensus rules on tx
|
||||
LOG_PRINT_L1("Verifying non-input consensus rules");
|
||||
cryptonote::tx_verification_context tvc{};
|
||||
ASSERT_TRUE(cryptonote::ver_non_input_consensus(deserialized_tx, tvc, HF_VERSION_FCMP_PLUS_PLUS));
|
||||
ASSERT_FALSE(tvc.m_verifivation_failed);
|
||||
ASSERT_FALSE(tvc.m_verifivation_impossible);
|
||||
ASSERT_FALSE(tvc.m_added_to_pool);
|
||||
ASSERT_FALSE(tvc.m_low_mixin);
|
||||
ASSERT_FALSE(tvc.m_double_spend);
|
||||
ASSERT_FALSE(tvc.m_invalid_input);
|
||||
ASSERT_FALSE(tvc.m_invalid_output);
|
||||
ASSERT_FALSE(tvc.m_too_big);
|
||||
ASSERT_FALSE(tvc.m_overspend);
|
||||
ASSERT_FALSE(tvc.m_fee_too_low);
|
||||
ASSERT_FALSE(tvc.m_too_few_outputs);
|
||||
ASSERT_FALSE(tvc.m_tx_extra_too_big);
|
||||
ASSERT_FALSE(tvc.m_nonzero_unlock_time);
|
||||
|
||||
// Recalculate signable tx hash from deserialized tx and check
|
||||
const crypto::hash signable_tx_hash_2 = rct::rct2hash(rct::get_pre_mlsag_hash(deserialized_tx.rct_signatures, hwdev));
|
||||
ASSERT_EQ(signable_tx_hash, signable_tx_hash_2);
|
||||
|
||||
// Pre-verify SAL proofs
|
||||
LOG_PRINT_L1("Verify SA/L proofs");
|
||||
ASSERT_EQ(deserialized_tx.vin.size(), n_inputs);
|
||||
ASSERT_EQ(deserialized_tx.vin.size(), deserialized_tx.rct_signatures.p.fcmp_ver_helper_data.key_images.size());
|
||||
ASSERT_EQ(deserialized_tx.vin.size(), deserialized_tx.rct_signatures.p.pseudoOuts.size());
|
||||
ASSERT_GT(deserialized_tx.rct_signatures.p.fcmp_pp.size(), (3*32 + FCMP_PP_SAL_PROOF_SIZE_V1) * n_inputs);
|
||||
for (size_t i = 0; i < n_inputs; ++i)
|
||||
{
|
||||
const uint8_t * const pbytes = deserialized_tx.rct_signatures.p.fcmp_pp.data() +
|
||||
(3*32 + FCMP_PP_SAL_PROOF_SIZE_V1) * i;
|
||||
FcmpInputCompressed input;
|
||||
fcmp_pp::FcmpPpSalProof sal_proof(FCMP_PP_SAL_PROOF_SIZE_V1);
|
||||
memcpy(&input, pbytes, 3*32);
|
||||
memcpy(&sal_proof[0], pbytes + 3*32, FCMP_PP_SAL_PROOF_SIZE_V1);
|
||||
memcpy(input.C_tilde, deserialized_tx.rct_signatures.p.pseudoOuts.at(i).bytes, 32);
|
||||
const crypto::key_image &ki = deserialized_tx.rct_signatures.p.fcmp_ver_helper_data.key_images.at(i);
|
||||
ASSERT_TRUE(fcmp_pp::verify_sal(signable_tx_hash_2, input, ki, sal_proof));
|
||||
}
|
||||
|
||||
// Verify all RingCT non-semantics
|
||||
LOG_PRINT_L1("Verify RingCT non-semantics consensus rules");
|
||||
ASSERT_TRUE(rct::verRctNonSemanticsSimple(deserialized_tx.rct_signatures));
|
||||
free(tree_root);
|
||||
|
||||
// Load carrot from tx
|
||||
LOG_PRINT_L1("Parsing carrot info from deserialized transaction");
|
||||
std::vector<CarrotEnoteV1> parsed_enotes;
|
||||
std::vector<crypto::key_image> parsed_key_images;
|
||||
rct::xmr_amount parsed_fee;
|
||||
std::optional<encrypted_payment_id_t> parsed_encrypted_payment_id;
|
||||
ASSERT_TRUE(try_load_carrot_from_transaction_v1(deserialized_tx,
|
||||
parsed_enotes,
|
||||
parsed_key_images,
|
||||
parsed_fee,
|
||||
parsed_encrypted_payment_id));
|
||||
|
||||
// Bob scan
|
||||
LOG_PRINT_L1("Bob scanning");
|
||||
std::vector<mock::mock_scan_result_t> bob_scan_results;
|
||||
mock::mock_scan_enote_set(parsed_enotes,
|
||||
parsed_encrypted_payment_id.value_or(null_payment_id),
|
||||
bob,
|
||||
bob_scan_results);
|
||||
ASSERT_EQ(bob_payment_proposals.size(), bob_scan_results.size());
|
||||
|
||||
// Compare bob scan results to bob payment proposals
|
||||
std::unordered_set<size_t> matched_scan_results;
|
||||
for (size_t i = 0; i < bob_payment_proposals.size(); ++i)
|
||||
{
|
||||
bool matched = false;
|
||||
for (size_t j = 0; j < bob_scan_results.size(); ++j)
|
||||
{
|
||||
if (matched_scan_results.count(j))
|
||||
continue;
|
||||
else if (compare_scan_result(bob_scan_results.at(j),
|
||||
bob_payment_proposals.at(i)))
|
||||
{
|
||||
matched = true;
|
||||
matched_scan_results.insert(j);
|
||||
break;
|
||||
}
|
||||
}
|
||||
ASSERT_TRUE(matched);
|
||||
}
|
||||
}
|
||||
//----------------------------------------------------------------------------------------------------------------------
|
1224
tests/unit_tests/carrot_impl.cpp
Normal file
1224
tests/unit_tests/carrot_impl.cpp
Normal file
File diff suppressed because it is too large
Load diff
1232
tests/unit_tests/carrot_tx_builder.cpp
Normal file
1232
tests/unit_tests/carrot_tx_builder.cpp
Normal file
File diff suppressed because it is too large
Load diff
936
tests/unit_tests/wallet_scanning.cpp
Normal file
936
tests/unit_tests/wallet_scanning.cpp
Normal file
|
@ -0,0 +1,936 @@
|
|||
// Copyright (c) 2023-2024, The Monero Project
|
||||
//
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without modification, are
|
||||
// permitted provided that the following conditions are met:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above copyright notice, this list of
|
||||
// conditions and the following disclaimer.
|
||||
//
|
||||
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
|
||||
// of conditions and the following disclaimer in the documentation and/or other
|
||||
// materials provided with the distribution.
|
||||
//
|
||||
// 3. Neither the name of the copyright holder nor the names of its contributors may be
|
||||
// used to endorse or promote products derived from this software without specific
|
||||
// prior written permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
|
||||
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
|
||||
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
||||
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
|
||||
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
#define IN_UNIT_TESTS
|
||||
|
||||
#include "unit_tests_utils.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
#include "carrot_core/device_ram_borrowed.h"
|
||||
#include "carrot_core/output_set_finalization.h"
|
||||
#include "carrot_impl/carrot_tx_builder_utils.h"
|
||||
#include "carrot_impl/carrot_tx_format_utils.h"
|
||||
#include "carrot_mock_helpers.h"
|
||||
#include "common/container_helpers.h"
|
||||
#include "crypto/generators.h"
|
||||
#include "cryptonote_core/cryptonote_tx_utils.h"
|
||||
#include "wallet/wallet2.h"
|
||||
|
||||
#undef MONERO_DEFAULT_LOG_CATEGORY
|
||||
#define MONERO_DEFAULT_LOG_CATEGORY "unit_tests.wallet_scanning"
|
||||
|
||||
namespace
|
||||
{
|
||||
//----------------------------------------------------------------------------------------------------------------------
|
||||
//----------------------------------------------------------------------------------------------------------------------
|
||||
static bool construct_miner_tx_fake_reward_1out(const size_t height,
|
||||
const rct::xmr_amount reward,
|
||||
const cryptonote::account_public_address &miner_address,
|
||||
cryptonote::transaction& tx,
|
||||
const uint8_t hf_version)
|
||||
{
|
||||
const bool is_carrot = hf_version >= HF_VERSION_CARROT;
|
||||
if (is_carrot)
|
||||
{
|
||||
carrot::CarrotDestinationV1 miner_destination;
|
||||
make_carrot_main_address_v1(miner_address.m_spend_public_key,
|
||||
miner_address.m_view_public_key,
|
||||
miner_destination);
|
||||
|
||||
const carrot::CarrotPaymentProposalV1 normal_payment_proposal{
|
||||
.destination = miner_destination,
|
||||
.amount = reward,
|
||||
.randomness = carrot::gen_janus_anchor()
|
||||
};
|
||||
|
||||
std::vector<carrot::CarrotCoinbaseEnoteV1> coinbase_enotes;
|
||||
carrot::get_coinbase_output_enotes({normal_payment_proposal},
|
||||
height,
|
||||
coinbase_enotes);
|
||||
|
||||
tx = carrot::store_carrot_to_coinbase_transaction_v1(coinbase_enotes);
|
||||
}
|
||||
else // !is_carrot
|
||||
{
|
||||
tx.vin.clear();
|
||||
tx.vout.clear();
|
||||
tx.extra.clear();
|
||||
|
||||
cryptonote::txin_gen in;
|
||||
in.height = height;
|
||||
|
||||
cryptonote::keypair txkey = cryptonote::keypair::generate(hw::get_device("default"));
|
||||
cryptonote::add_tx_pub_key_to_extra(tx, txkey.pub);
|
||||
if (!cryptonote::sort_tx_extra(tx.extra, tx.extra))
|
||||
return false;
|
||||
|
||||
crypto::key_derivation derivation;
|
||||
crypto::public_key out_eph_public_key;
|
||||
bool r = crypto::generate_key_derivation(miner_address.m_view_public_key, txkey.sec, derivation);
|
||||
CHECK_AND_ASSERT_MES(r, false,
|
||||
"while creating outs: failed to generate_key_derivation(" << miner_address.m_view_public_key << ", "
|
||||
<< crypto::secret_key_explicit_print_ref{txkey.sec} << ")");
|
||||
|
||||
const size_t local_output_index = 0;
|
||||
r = crypto::derive_public_key(derivation, local_output_index, miner_address.m_spend_public_key, out_eph_public_key);
|
||||
CHECK_AND_ASSERT_MES(r, false,
|
||||
"while creating outs: failed to derive_public_key(" << derivation << ", "
|
||||
<< local_output_index << ", "<< miner_address.m_spend_public_key << ")");
|
||||
|
||||
const bool use_view_tags = hf_version >= HF_VERSION_VIEW_TAGS;
|
||||
crypto::view_tag view_tag;
|
||||
if (use_view_tags)
|
||||
crypto::derive_view_tag(derivation, local_output_index, view_tag);
|
||||
|
||||
cryptonote::tx_out out;
|
||||
cryptonote::set_tx_out(reward, out_eph_public_key, use_view_tags, view_tag, out);
|
||||
|
||||
tx.vout.push_back(out);
|
||||
|
||||
if (hf_version >= 4)
|
||||
tx.version = 2;
|
||||
else
|
||||
tx.version = 1;
|
||||
|
||||
tx.unlock_time = height + CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW;
|
||||
tx.vin.push_back(in);
|
||||
|
||||
tx.invalidate_hashes();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
//----------------------------------------------------------------------------------------------------------------------
|
||||
//----------------------------------------------------------------------------------------------------------------------
|
||||
static cryptonote::transaction construct_miner_tx_fake_reward_1out(const size_t height,
|
||||
const rct::xmr_amount reward,
|
||||
const cryptonote::account_public_address &miner_address,
|
||||
const uint8_t hf_version)
|
||||
{
|
||||
cryptonote::transaction tx;
|
||||
const bool r = construct_miner_tx_fake_reward_1out(height, reward, miner_address, tx, hf_version);
|
||||
CHECK_AND_ASSERT_THROW_MES(r, "failed to construct miner tx");
|
||||
return tx;
|
||||
}
|
||||
//----------------------------------------------------------------------------------------------------------------------
|
||||
//----------------------------------------------------------------------------------------------------------------------
|
||||
struct stripped_down_tx_source_entry_t
|
||||
{
|
||||
uint64_t global_output_index;
|
||||
crypto::public_key onetime_address;
|
||||
crypto::public_key real_out_tx_key;
|
||||
std::vector<crypto::public_key> real_out_additional_tx_keys;
|
||||
size_t local_output_index;
|
||||
rct::xmr_amount amount;
|
||||
rct::key mask;
|
||||
};
|
||||
//----------------------------------------------------------------------------------------------------------------------
|
||||
//----------------------------------------------------------------------------------------------------------------------
|
||||
static cryptonote::tx_source_entry gen_tx_source_entry_fake_members(
|
||||
const stripped_down_tx_source_entry_t &in,
|
||||
const size_t mixin,
|
||||
const uint64_t max_global_output_index)
|
||||
{
|
||||
const size_t ring_size = mixin + 1;
|
||||
const bool is_rct = in.mask == rct::I;
|
||||
|
||||
CHECK_AND_ASSERT_THROW_MES(in.global_output_index <= max_global_output_index,
|
||||
"real global output index too low");
|
||||
CHECK_AND_ASSERT_THROW_MES(max_global_output_index >= ring_size,
|
||||
"not enough global output indices for mixin");
|
||||
|
||||
cryptonote::tx_source_entry res;
|
||||
|
||||
// populate ring with fake data
|
||||
std::unordered_set<uint64_t> used_indices;
|
||||
res.outputs.reserve(mixin + 1);
|
||||
res.outputs.push_back(
|
||||
{in.global_output_index,
|
||||
{ rct::pk2rct(in.onetime_address), rct::commit(in.amount, in.mask) }});
|
||||
used_indices.insert(in.global_output_index);
|
||||
while (res.outputs.size() < ring_size)
|
||||
{
|
||||
const uint64_t global_output_index = crypto::rand_range<uint64_t>(0, max_global_output_index);
|
||||
if (used_indices.count(global_output_index))
|
||||
continue;
|
||||
used_indices.insert(global_output_index);
|
||||
const rct::ctkey output_pair{rct::pkGen(),
|
||||
is_rct ? rct::pkGen() : rct::zeroCommit(in.amount)};
|
||||
res.outputs.push_back({global_output_index, output_pair});
|
||||
}
|
||||
// sort by index
|
||||
std::sort(res.outputs.begin(), res.outputs.end(), [](const auto &a, const auto &b) -> bool {
|
||||
return a.first < b.first;
|
||||
});
|
||||
|
||||
// real_output
|
||||
res.real_output = 0;
|
||||
while (res.outputs.at(res.real_output).second.dest != in.onetime_address)
|
||||
++res.real_output;
|
||||
|
||||
// copy from in
|
||||
res.real_out_tx_key = in.real_out_tx_key;
|
||||
res.real_out_additional_tx_keys = in.real_out_additional_tx_keys;
|
||||
res.real_output_in_tx_index = in.local_output_index;
|
||||
res.amount = in.amount;
|
||||
res.rct = is_rct;
|
||||
res.mask = in.mask;
|
||||
|
||||
return res;
|
||||
}
|
||||
//----------------------------------------------------------------------------------------------------------------------
|
||||
//----------------------------------------------------------------------------------------------------------------------
|
||||
static cryptonote::transaction construct_pre_carrot_tx_with_fake_inputs(
|
||||
const cryptonote::account_keys &sender_account_keys,
|
||||
const std::unordered_map<crypto::public_key, cryptonote::subaddress_index> &subaddresses,
|
||||
std::vector<stripped_down_tx_source_entry_t> &&stripped_sources,
|
||||
std::vector<cryptonote::tx_destination_entry> &destinations,
|
||||
const boost::optional<cryptonote::account_public_address> &change_addr,
|
||||
const rct::xmr_amount fee,
|
||||
const uint8_t hf_version,
|
||||
const bool sweep_unmixable_override = false)
|
||||
{
|
||||
// derive config from hf version
|
||||
const bool rct = hf_version >= HF_VERSION_DYNAMIC_FEE && !sweep_unmixable_override;
|
||||
rct::RCTConfig rct_config;
|
||||
switch (hf_version)
|
||||
{
|
||||
case 1:
|
||||
case 2:
|
||||
case 3:
|
||||
case HF_VERSION_DYNAMIC_FEE:
|
||||
case 5:
|
||||
case HF_VERSION_MIN_MIXIN_4:
|
||||
case 7:
|
||||
rct_config = { rct::RangeProofBorromean, 0 };
|
||||
break;
|
||||
case HF_VERSION_PER_BYTE_FEE:
|
||||
case 9:
|
||||
rct_config = { rct::RangeProofPaddedBulletproof, 1 };
|
||||
break;
|
||||
case HF_VERSION_SMALLER_BP:
|
||||
case 11:
|
||||
case HF_VERSION_MIN_2_OUTPUTS:
|
||||
rct_config = { rct::RangeProofPaddedBulletproof, 2 };
|
||||
break;
|
||||
case HF_VERSION_CLSAG:
|
||||
case 14:
|
||||
rct_config = { rct::RangeProofPaddedBulletproof, 3 };
|
||||
break;
|
||||
case HF_VERSION_BULLETPROOF_PLUS:
|
||||
case 16:
|
||||
rct_config = { rct::RangeProofPaddedBulletproof, 4 };
|
||||
break;
|
||||
default:
|
||||
ASSERT_MES_AND_THROW("unrecognized hf version");
|
||||
}
|
||||
const bool use_view_tags = hf_version >= HF_VERSION_VIEW_TAGS;
|
||||
const size_t mixin = 15;
|
||||
const uint64_t max_global_output_index = 1000000;
|
||||
|
||||
// count missing money and balance if necessary
|
||||
boost::multiprecision::int128_t missing_money = fee;
|
||||
for (const cryptonote::tx_destination_entry &destination : destinations)
|
||||
missing_money += destination.amount;
|
||||
for (const stripped_down_tx_source_entry_t &stripped_source : stripped_sources)
|
||||
missing_money -= stripped_source.amount;
|
||||
if (missing_money > 0)
|
||||
{
|
||||
const rct::xmr_amount missing_money64 = boost::numeric_cast<rct::xmr_amount>(missing_money);
|
||||
|
||||
hw::device &hwdev = hw::get_device("default");
|
||||
cryptonote::keypair main_tx_keypair = cryptonote::keypair::generate(hwdev);
|
||||
|
||||
std::vector<crypto::public_key> dummy_additional_tx_public_keys;
|
||||
std::vector<rct::key> amount_keys;
|
||||
crypto::public_key input_onetime_address;
|
||||
crypto::view_tag vt;
|
||||
const bool r = hwdev.generate_output_ephemeral_keys(rct ? 2 : 1,
|
||||
cryptonote::account_keys(),
|
||||
main_tx_keypair.pub,
|
||||
main_tx_keypair.sec,
|
||||
cryptonote::tx_destination_entry(missing_money64, sender_account_keys.m_account_address, false),
|
||||
boost::none,
|
||||
/*output_index=*/0,
|
||||
/*need_additional_txkeys=*/false,
|
||||
/*additional_tx_keys=*/{},
|
||||
dummy_additional_tx_public_keys,
|
||||
amount_keys,
|
||||
input_onetime_address,
|
||||
use_view_tags,
|
||||
vt);
|
||||
CHECK_AND_ASSERT_THROW_MES(r, "failed to generate balancing input");
|
||||
|
||||
const stripped_down_tx_source_entry_t balancing_in{
|
||||
.global_output_index = crypto::rand_range<uint64_t>(0, max_global_output_index),
|
||||
.onetime_address = input_onetime_address,
|
||||
.real_out_tx_key = main_tx_keypair.pub,
|
||||
.real_out_additional_tx_keys = {},
|
||||
.local_output_index = 0,
|
||||
.amount = missing_money64,
|
||||
.mask = rct ? rct::genCommitmentMask(amount_keys.at(0)) : rct::I
|
||||
};
|
||||
|
||||
stripped_sources.push_back(balancing_in);
|
||||
}
|
||||
|
||||
// populate random sources
|
||||
std::vector<cryptonote::tx_source_entry> sources;
|
||||
sources.reserve(stripped_sources.size());
|
||||
for (const auto &stripped_source : stripped_sources)
|
||||
sources.push_back(gen_tx_source_entry_fake_members(stripped_source,
|
||||
mixin,
|
||||
max_global_output_index));
|
||||
|
||||
// construct tx
|
||||
cryptonote::transaction tx;
|
||||
crypto::secret_key tx_key;
|
||||
std::vector<crypto::secret_key> additional_tx_keys;
|
||||
const bool r = cryptonote::construct_tx_and_get_tx_key(
|
||||
sender_account_keys,
|
||||
subaddresses,
|
||||
sources,
|
||||
destinations,
|
||||
change_addr,
|
||||
/*extra=*/{},
|
||||
tx,
|
||||
tx_key,
|
||||
additional_tx_keys,
|
||||
rct,
|
||||
rct_config,
|
||||
use_view_tags);
|
||||
CHECK_AND_ASSERT_THROW_MES(r, "failed to construct tx");
|
||||
return tx;
|
||||
}
|
||||
//----------------------------------------------------------------------------------------------------------------------
|
||||
//----------------------------------------------------------------------------------------------------------------------
|
||||
static constexpr rct::xmr_amount fake_fee_per_weight = 2023;
|
||||
//----------------------------------------------------------------------------------------------------------------------
|
||||
//----------------------------------------------------------------------------------------------------------------------
|
||||
static cryptonote::transaction construct_carrot_pruned_transaction_fake_inputs(
|
||||
const std::vector<carrot::CarrotPaymentProposalV1> &normal_payment_proposals,
|
||||
const std::vector<carrot::CarrotPaymentProposalVerifiableSelfSendV1> &selfsend_payment_proposals,
|
||||
const cryptonote::account_keys &acc_keys)
|
||||
{
|
||||
carrot::select_inputs_func_t select_inputs = [](
|
||||
const boost::multiprecision::int128_t &nominal_output_sum,
|
||||
const std::map<std::size_t, rct::xmr_amount> &fee_by_input_count,
|
||||
const std::size_t,
|
||||
const std::size_t,
|
||||
std::vector<carrot::CarrotSelectedInput> &select_inputs_out
|
||||
)
|
||||
{
|
||||
const auto in_amount = boost::numeric_cast<rct::xmr_amount>(nominal_output_sum + fee_by_input_count.at(1));
|
||||
const crypto::key_image ki = rct::rct2ki(rct::pkGen());
|
||||
select_inputs_out = {carrot::CarrotSelectedInput{.amount = in_amount, .key_image = ki}};
|
||||
};
|
||||
|
||||
const carrot::view_incoming_key_ram_borrowed_device k_view_dev(acc_keys.m_view_secret_key);
|
||||
|
||||
carrot::CarrotTransactionProposalV1 tx_proposal;
|
||||
carrot::make_carrot_transaction_proposal_v1_transfer(
|
||||
normal_payment_proposals,
|
||||
selfsend_payment_proposals,
|
||||
fake_fee_per_weight,
|
||||
/*extra=*/{},
|
||||
std::move(select_inputs),
|
||||
/*s_view_balance_dev=*/nullptr,
|
||||
&k_view_dev,
|
||||
acc_keys.m_account_address.m_spend_public_key,
|
||||
tx_proposal);
|
||||
|
||||
cryptonote::transaction tx;
|
||||
carrot::make_pruned_transaction_from_carrot_proposal_v1(tx_proposal,
|
||||
/*s_view_balance_dev=*/nullptr,
|
||||
&k_view_dev,
|
||||
acc_keys.m_account_address.m_spend_public_key,
|
||||
tx);
|
||||
|
||||
return tx;
|
||||
}
|
||||
//----------------------------------------------------------------------------------------------------------------------
|
||||
//----------------------------------------------------------------------------------------------------------------------
|
||||
static const cryptonote::account_public_address null_addr{
|
||||
.m_spend_public_key = crypto::get_G(),
|
||||
.m_view_public_key = crypto::get_G()
|
||||
};
|
||||
//----------------------------------------------------------------------------------------------------------------------
|
||||
//----------------------------------------------------------------------------------------------------------------------
|
||||
class fake_pruned_blockchain
|
||||
{
|
||||
public:
|
||||
static constexpr rct::xmr_amount miner_reward = 600000000000; // 0.6 XMR
|
||||
|
||||
fake_pruned_blockchain(const uint64_t start_block_index = 0,
|
||||
const cryptonote::network_type nettype = cryptonote::MAINNET):
|
||||
m_start_block_index(start_block_index),
|
||||
m_nettype(nettype),
|
||||
m_output_index()
|
||||
{
|
||||
add_starting_block();
|
||||
}
|
||||
|
||||
void add_block(const uint8_t hf_version,
|
||||
std::vector<cryptonote::transaction> &&txs,
|
||||
const cryptonote::account_public_address &miner_address)
|
||||
{
|
||||
std::vector<crypto::hash> tx_prunable_hashes(txs.size());
|
||||
std::vector<crypto::hash> tx_hashes(txs.size());
|
||||
for (size_t i = 0; i < tx_hashes.size(); ++i)
|
||||
{
|
||||
const cryptonote::transaction &tx = txs.at(i);
|
||||
if (tx.pruned)
|
||||
{
|
||||
tx_prunable_hashes[i] = crypto::rand<crypto::hash>();
|
||||
tx_hashes[i] = cryptonote::get_pruned_transaction_hash(tx, tx_prunable_hashes.at(i));
|
||||
}
|
||||
else // !tx.pruned
|
||||
{
|
||||
tx_prunable_hashes[i] = crypto::null_hash;
|
||||
tx_hashes[i] = cryptonote::get_transaction_hash(tx);
|
||||
}
|
||||
}
|
||||
|
||||
CHECK_AND_ASSERT_THROW_MES(tx_hashes.size() == txs.size(), "wrong tx_hashes size");
|
||||
|
||||
cryptonote::transaction miner_tx = construct_miner_tx_fake_reward_1out(this->height(),
|
||||
miner_reward,
|
||||
miner_address,
|
||||
hf_version);
|
||||
|
||||
cryptonote::block blk;
|
||||
blk.major_version = hf_version;
|
||||
blk.minor_version = hf_version;
|
||||
blk.timestamp = std::max<uint64_t>(this->timestamp() + 120, time(NULL));
|
||||
blk.prev_id = this->top_block_hash();
|
||||
blk.nonce = crypto::rand<uint32_t>();
|
||||
blk.miner_tx = std::move(miner_tx);
|
||||
blk.tx_hashes = std::move(tx_hashes);
|
||||
|
||||
add_block(std::move(blk), std::move(txs), std::move(tx_prunable_hashes));
|
||||
}
|
||||
|
||||
void add_block(cryptonote::block &&blk,
|
||||
std::vector<cryptonote::transaction> &&pruned_txs,
|
||||
std::vector<crypto::hash> &&prunable_hashes)
|
||||
{
|
||||
assert_chain_count();
|
||||
CHECK_AND_ASSERT_THROW_MES(blk.major_version >= this->hf_version(),
|
||||
"hf version too low");
|
||||
CHECK_AND_ASSERT_THROW_MES(blk.tx_hashes.size() == pruned_txs.size(),
|
||||
"wrong number of txs provided");
|
||||
CHECK_AND_ASSERT_THROW_MES(blk.tx_hashes.size() == prunable_hashes.size(),
|
||||
"wrong number of prunable hashes provided");
|
||||
|
||||
size_t total_block_weight = 0;
|
||||
for (const cryptonote::transaction &tx : pruned_txs)
|
||||
{
|
||||
total_block_weight += tx.pruned
|
||||
? cryptonote::get_pruned_transaction_weight(tx)
|
||||
: cryptonote::get_transaction_weight(tx);
|
||||
}
|
||||
|
||||
cryptonote::block_complete_entry blk_entry;
|
||||
blk_entry.pruned = true;
|
||||
blk_entry.block = cryptonote::block_to_blob(blk);
|
||||
blk_entry.block_weight = total_block_weight;
|
||||
blk_entry.txs.reserve(pruned_txs.size());
|
||||
for (size_t i = 0; i < pruned_txs.size(); ++i)
|
||||
{
|
||||
std::stringstream ss;
|
||||
binary_archive<true> ar(ss);
|
||||
CHECK_AND_ASSERT_THROW_MES(pruned_txs.at(i).serialize_base(ar), "tx failed to serialize");
|
||||
blk_entry.txs.push_back(cryptonote::tx_blob_entry(ss.str(), prunable_hashes.at(i)));
|
||||
}
|
||||
|
||||
tools::wallet2::parsed_block par_blk;
|
||||
par_blk.hash = cryptonote::get_block_hash(blk);
|
||||
par_blk.block = std::move(blk);
|
||||
par_blk.txes = std::move(pruned_txs);
|
||||
{
|
||||
auto &tx_o_indices = tools::add_element(par_blk.o_indices.indices);
|
||||
for (size_t n = 0; n < par_blk.block.miner_tx.vout.size(); ++n)
|
||||
tx_o_indices.indices.push_back(m_output_index++);
|
||||
}
|
||||
for (const cryptonote::transaction &tx : par_blk.txes)
|
||||
{
|
||||
auto &tx_o_indices = tools::add_element(par_blk.o_indices.indices);
|
||||
for (size_t n = 0; n < tx.vout.size(); ++n)
|
||||
tx_o_indices.indices.push_back(m_output_index++);
|
||||
}
|
||||
par_blk.error = false;
|
||||
|
||||
m_block_entries.emplace_back(std::move(blk_entry));
|
||||
m_parsed_blocks.emplace_back(std::move(par_blk));
|
||||
}
|
||||
|
||||
void pop_block()
|
||||
{
|
||||
assert_chain_count();
|
||||
|
||||
CHECK_AND_ASSERT_THROW_MES(m_block_entries.size() >= 2, "Cannot pop starting block");
|
||||
|
||||
m_block_entries.pop_back();
|
||||
m_parsed_blocks.pop_back();
|
||||
}
|
||||
|
||||
void get_blocks_data(const uint64_t start_block_index,
|
||||
const uint64_t stop_block_index,
|
||||
std::vector<cryptonote::block_complete_entry> &block_entries_out,
|
||||
std::vector<tools::wallet2::parsed_block> &parsed_blocks_out) const
|
||||
{
|
||||
block_entries_out.clear();
|
||||
parsed_blocks_out.clear();
|
||||
|
||||
assert_chain_count();
|
||||
|
||||
if (start_block_index < m_start_block_index || stop_block_index >= this->height())
|
||||
throw std::out_of_range("get_blocks_data requested block indices");
|
||||
|
||||
for (size_t block_index = start_block_index; block_index <= stop_block_index; ++block_index)
|
||||
{
|
||||
const size_t i = block_index - m_start_block_index;
|
||||
block_entries_out.push_back(m_block_entries.at(i));
|
||||
parsed_blocks_out.push_back(m_parsed_blocks.at(i));
|
||||
}
|
||||
}
|
||||
|
||||
void init_wallet_for_starting_block(tools::wallet2 &w) const
|
||||
{
|
||||
assert_chain_count();
|
||||
CHECK_AND_ASSERT_THROW_MES(!m_block_entries.empty(), "blockchain missing starting block");
|
||||
|
||||
w.set_refresh_from_block_height(m_start_block_index);
|
||||
|
||||
w.m_blockchain.clear();
|
||||
for (size_t i = 0; i < m_start_block_index; ++i)
|
||||
w.m_blockchain.push_back(crypto::null_hash);
|
||||
w.m_blockchain.push_back(m_parsed_blocks.front().hash);
|
||||
w.m_blockchain.trim(m_start_block_index);
|
||||
|
||||
//! TODO: uncomment for FCMP++ integration
|
||||
//w.m_tree_cache.clear();
|
||||
//w.m_tree_cache.init(m_start_block_index,
|
||||
// m_parsed_blocks.front().hash,
|
||||
// /*n_leaf_tuples=*/0,
|
||||
// /*last_path=*/{},
|
||||
// /*locked_outputs=*/{});
|
||||
}
|
||||
|
||||
uint8_t hf_version() const
|
||||
{
|
||||
return m_parsed_blocks.empty() ? 1 : m_parsed_blocks.back().block.major_version;
|
||||
}
|
||||
|
||||
uint64_t start_block_index() const
|
||||
{
|
||||
return m_start_block_index;
|
||||
}
|
||||
|
||||
uint64_t height() const
|
||||
{
|
||||
return m_start_block_index + m_block_entries.size();
|
||||
}
|
||||
|
||||
uint64_t timestamp() const
|
||||
{
|
||||
return m_parsed_blocks.empty() ? 0 : m_parsed_blocks.back().block.timestamp;
|
||||
}
|
||||
|
||||
crypto::hash top_block_hash() const
|
||||
{
|
||||
return m_parsed_blocks.empty() ? crypto::null_hash : m_parsed_blocks.back().hash;
|
||||
}
|
||||
|
||||
tools::wallet2::parsed_block get_parsed_block(const uint64_t block_index) const
|
||||
{
|
||||
if (block_index >= this->height() || block_index < this->m_start_block_index)
|
||||
throw std::out_of_range("get_block requested block index");
|
||||
|
||||
return m_parsed_blocks.at(block_index - this->m_start_block_index);
|
||||
}
|
||||
|
||||
private:
|
||||
void assert_chain_count() const
|
||||
{
|
||||
CHECK_AND_ASSERT_THROW_MES(m_block_entries.size() == m_parsed_blocks.size(), "blockchain size mismatch");
|
||||
}
|
||||
|
||||
void add_starting_block()
|
||||
{
|
||||
if (m_start_block_index == 0)
|
||||
{
|
||||
// add actual genesis block for this network type
|
||||
cryptonote::block genesis_blk;
|
||||
CHECK_AND_ASSERT_THROW_MES(cryptonote::generate_genesis_block(genesis_blk,
|
||||
get_config(m_nettype).GENESIS_TX,
|
||||
get_config(m_nettype).GENESIS_NONCE),
|
||||
"failed to generate genesis block");
|
||||
add_block(std::move(genesis_blk), {}, {});
|
||||
}
|
||||
else // m_start_block_index > 0
|
||||
{
|
||||
// make up start block
|
||||
add_block(1, {}, null_addr);
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t m_start_block_index;
|
||||
cryptonote::network_type m_nettype;
|
||||
uint64_t m_output_index;
|
||||
std::vector<cryptonote::block_complete_entry> m_block_entries;
|
||||
std::vector<tools::wallet2::parsed_block> m_parsed_blocks;
|
||||
};
|
||||
//----------------------------------------------------------------------------------------------------------------------
|
||||
//----------------------------------------------------------------------------------------------------------------------
|
||||
} //anonymous namespace
|
||||
//----------------------------------------------------------------------------------------------------------------------
|
||||
TEST(wallet_scanning, view_scan_special_offline)
|
||||
{
|
||||
cryptonote::account_base acb;
|
||||
acb.generate();
|
||||
const cryptonote::account_keys &acc_keys = acb.get_keys();
|
||||
|
||||
const rct::xmr_amount amount_a = rct::randXmrAmount(COIN);
|
||||
std::vector<cryptonote::tx_destination_entry> dests = {
|
||||
cryptonote::tx_destination_entry(amount_a, acc_keys.m_account_address, false)};
|
||||
cryptonote::transaction curr_tx = construct_carrot_pruned_transaction_fake_inputs(
|
||||
/*normal_payment_proposals=*/{},
|
||||
{{carrot::mock::convert_selfsend_payment_proposal_v1(dests.front()), {/*main*/}}},
|
||||
acc_keys);
|
||||
ASSERT_FALSE(cryptonote::is_coinbase(curr_tx));
|
||||
ASSERT_EQ(2, curr_tx.version);
|
||||
ASSERT_EQ(rct::RCTTypeFcmpPlusPlus, curr_tx.rct_signatures.type);
|
||||
ASSERT_EQ(2, curr_tx.vout.size());
|
||||
ASSERT_EQ(typeid(cryptonote::txout_to_carrot_v1), curr_tx.vout.at(0).target.type());
|
||||
ASSERT_EQ(0, curr_tx.vout.at(0).amount);
|
||||
|
||||
const std::vector<std::optional<tools::wallet::enote_view_incoming_scan_info_t>> enote_scan_infos =
|
||||
tools::wallet::view_incoming_scan_transaction(
|
||||
curr_tx,
|
||||
acc_keys,
|
||||
{{acc_keys.m_account_address.m_spend_public_key, {/*main*/}}});
|
||||
ASSERT_EQ(2, enote_scan_infos.size());
|
||||
ASSERT_TRUE(enote_scan_infos.front() || enote_scan_infos.back());
|
||||
const bool match_first = bool(enote_scan_infos.front()) && enote_scan_infos.front()->amount;
|
||||
const auto &enote_scan_info = match_first ? *enote_scan_infos.front() : *enote_scan_infos.back();
|
||||
crypto::public_key onetime_address;
|
||||
ASSERT_TRUE(cryptonote::get_output_public_key(curr_tx.vout.at(match_first ? 0 : 1), onetime_address));
|
||||
const rct::key &amount_commitment = curr_tx.rct_signatures.outPk.at(match_first ? 0 : 1).mask;
|
||||
EXPECT_EQ(amount_a, enote_scan_info.amount);
|
||||
EXPECT_EQ(onetime_address, enote_scan_info.onetime_address);
|
||||
EXPECT_EQ(amount_commitment, rct::commit(enote_scan_info.amount, enote_scan_info.amount_blinding_factor));
|
||||
ASSERT_TRUE(enote_scan_info.subaddr_index);
|
||||
EXPECT_EQ(carrot::subaddress_index{}, enote_scan_info.subaddr_index->index);
|
||||
EXPECT_EQ(acc_keys.m_account_address.m_spend_public_key, enote_scan_info.address_spend_pubkey);
|
||||
EXPECT_EQ(match_first ? 0 : 1, enote_scan_info.local_output_index);
|
||||
EXPECT_EQ(0, enote_scan_info.main_tx_pubkey_index);
|
||||
}
|
||||
//----------------------------------------------------------------------------------------------------------------------
|
||||
TEST(wallet_scanning, positive_smallout_main_addr_all_types_outputs)
|
||||
{
|
||||
// Test that wallet can scan and recover enotes of following type:
|
||||
// a. pre-ringct coinbase
|
||||
// b. pre-ringct
|
||||
// c. ringct coinbase
|
||||
// d. ringct long-amount
|
||||
// e. ringct short-amount
|
||||
// f. view-tagged ringct coinbase
|
||||
// g. view-tagged pre-ringct (only possible in unmixable sweep txs)
|
||||
// h. view-tagged ringct
|
||||
// i. carrot v1 coinbase
|
||||
// j. carrot v1 normal
|
||||
// k. carrot v1 special
|
||||
// l. carrot v1 internal (@TODO)
|
||||
//
|
||||
// All enotes are addressed to the main address in 2-out noin-coinbase txs or 1-out coinbase txs.
|
||||
// We also don't test reorgs here.
|
||||
|
||||
// init blockchain
|
||||
fake_pruned_blockchain bc(0);
|
||||
|
||||
// generate wallet
|
||||
tools::wallet2 w(cryptonote::MAINNET, /*kdf_rounds=*/1, /*unattended=*/true);
|
||||
w.generate("", "");
|
||||
const cryptonote::account_keys &acc_keys = w.get_account().get_keys();
|
||||
const cryptonote::account_public_address main_addr = w.get_account().get_keys().m_account_address;
|
||||
ASSERT_EQ(0, w.balance(0, true));
|
||||
bc.init_wallet_for_starting_block(w); // needed b/c internal logic
|
||||
|
||||
uint64_t refresh_height = 0;
|
||||
const auto wallet_process_new_blocks = [&w, &bc, &refresh_height]() -> boost::multiprecision::int128_t
|
||||
{
|
||||
const boost::multiprecision::int128_t old_balance = w.balance(0, true);
|
||||
|
||||
// note: doesn't handle reorgs
|
||||
std::vector<cryptonote::block_complete_entry> block_entries;
|
||||
std::vector<tools::wallet2::parsed_block> parsed_blocks;
|
||||
bc.get_blocks_data(0, bc.height()-1, block_entries, parsed_blocks); //! @TODO: figure out why starting from refresh_height doesn't work
|
||||
uint64_t blocks_added{};
|
||||
auto output_tracker_cache = w.create_output_tracker_cache();
|
||||
w.process_parsed_blocks(0, block_entries, parsed_blocks, blocks_added, output_tracker_cache);
|
||||
|
||||
// update refresh_height
|
||||
refresh_height = bc.height();
|
||||
|
||||
// return amount of money received
|
||||
return boost::multiprecision::int128_t(w.balance(0, true)) - old_balance;
|
||||
};
|
||||
|
||||
// a. push block containing a pre-ringct coinbase output to wallet
|
||||
bc.add_block(1, {}, main_addr);
|
||||
|
||||
// a. scan pre-ringct coinbase tx
|
||||
auto balance_diff = wallet_process_new_blocks();
|
||||
EXPECT_EQ(fake_pruned_blockchain::miner_reward, balance_diff);
|
||||
|
||||
// b. construct and push a pre-ringct tx
|
||||
const rct::xmr_amount amount_b = rct::randXmrAmount(COIN);
|
||||
{
|
||||
const rct::xmr_amount fee = rct::randXmrAmount(COIN);
|
||||
std::vector<cryptonote::tx_destination_entry> dests = {
|
||||
cryptonote::tx_destination_entry(amount_b, acc_keys.m_account_address, false)};
|
||||
cryptonote::transaction curr_tx = construct_pre_carrot_tx_with_fake_inputs(
|
||||
acc_keys,
|
||||
w.m_subaddresses,
|
||||
/*stripped_sources=*/{},
|
||||
dests,
|
||||
acc_keys.m_account_address,
|
||||
fee,
|
||||
/*hf_version=*/1);
|
||||
ASSERT_FALSE(cryptonote::is_coinbase(curr_tx));
|
||||
ASSERT_EQ(1, curr_tx.version);
|
||||
ASSERT_EQ(rct::RCTTypeNull, curr_tx.rct_signatures.type);
|
||||
ASSERT_EQ(typeid(cryptonote::txout_to_key), curr_tx.vout.at(0).target.type());
|
||||
ASSERT_EQ(amount_b, curr_tx.vout.at(0).amount);
|
||||
bc.add_block(1, {std::move(curr_tx)}, null_addr);
|
||||
}
|
||||
|
||||
// b. scan pre-ringct tx
|
||||
balance_diff = wallet_process_new_blocks();
|
||||
EXPECT_EQ(amount_b, balance_diff);
|
||||
|
||||
// c. construct and push a ringct coinbase tx
|
||||
bc.add_block(HF_VERSION_DYNAMIC_FEE, {}, main_addr);
|
||||
{
|
||||
auto top_block = bc.get_parsed_block(bc.height() - 1);
|
||||
const cryptonote::transaction &top_miner_tx = top_block.block.miner_tx;
|
||||
ASSERT_EQ(2, top_miner_tx.version);
|
||||
ASSERT_NE(0, top_miner_tx.vout.size());
|
||||
ASSERT_EQ(rct::RCTTypeNull, top_miner_tx.rct_signatures.type);
|
||||
ASSERT_EQ(0, top_miner_tx.signatures.size());
|
||||
ASSERT_EQ(fake_pruned_blockchain::miner_reward, top_miner_tx.vout.at(0).amount);
|
||||
}
|
||||
|
||||
// c. scan ringct coinbase tx
|
||||
balance_diff = wallet_process_new_blocks();
|
||||
EXPECT_EQ(fake_pruned_blockchain::miner_reward, balance_diff);
|
||||
|
||||
// d. construct and push a ringct long-amount tx
|
||||
const rct::xmr_amount amount_d = rct::randXmrAmount(COIN);
|
||||
{
|
||||
const rct::xmr_amount fee = rct::randXmrAmount(COIN);
|
||||
std::vector<cryptonote::tx_destination_entry> dests = {
|
||||
cryptonote::tx_destination_entry(amount_d, acc_keys.m_account_address, false)};
|
||||
cryptonote::transaction curr_tx = construct_pre_carrot_tx_with_fake_inputs(
|
||||
acc_keys,
|
||||
w.m_subaddresses,
|
||||
/*stripped_sources=*/{},
|
||||
dests,
|
||||
acc_keys.m_account_address,
|
||||
fee,
|
||||
HF_VERSION_DYNAMIC_FEE);
|
||||
ASSERT_FALSE(cryptonote::is_coinbase(curr_tx));
|
||||
ASSERT_EQ(2, curr_tx.version);
|
||||
ASSERT_EQ(rct::RCTTypeFull, curr_tx.rct_signatures.type);
|
||||
ASSERT_EQ(typeid(cryptonote::txout_to_key), curr_tx.vout.at(0).target.type());
|
||||
ASSERT_EQ(0, curr_tx.vout.at(0).amount);
|
||||
bc.add_block(HF_VERSION_DYNAMIC_FEE, {std::move(curr_tx)}, null_addr);
|
||||
}
|
||||
|
||||
// d. scan ringct long-amount tx
|
||||
balance_diff = wallet_process_new_blocks();
|
||||
EXPECT_EQ(amount_d, balance_diff);
|
||||
|
||||
// e. construct and push a ringct short-amount tx
|
||||
const rct::xmr_amount amount_e = rct::randXmrAmount(COIN);
|
||||
{
|
||||
const rct::xmr_amount fee = rct::randXmrAmount(COIN);
|
||||
std::vector<cryptonote::tx_destination_entry> dests = {
|
||||
cryptonote::tx_destination_entry(amount_e, acc_keys.m_account_address, false)};
|
||||
cryptonote::transaction curr_tx = construct_pre_carrot_tx_with_fake_inputs(
|
||||
acc_keys,
|
||||
w.m_subaddresses,
|
||||
/*stripped_sources=*/{},
|
||||
dests,
|
||||
acc_keys.m_account_address,
|
||||
fee,
|
||||
HF_VERSION_SMALLER_BP);
|
||||
ASSERT_FALSE(cryptonote::is_coinbase(curr_tx));
|
||||
ASSERT_EQ(2, curr_tx.version);
|
||||
ASSERT_EQ(rct::RCTTypeBulletproof2, curr_tx.rct_signatures.type);
|
||||
ASSERT_EQ(typeid(cryptonote::txout_to_key), curr_tx.vout.at(0).target.type());
|
||||
ASSERT_EQ(0, curr_tx.vout.at(0).amount);
|
||||
bc.add_block(HF_VERSION_SMALLER_BP, {std::move(curr_tx)}, null_addr);
|
||||
}
|
||||
|
||||
// e. scan ringct short-amount tx
|
||||
balance_diff = wallet_process_new_blocks();
|
||||
EXPECT_EQ(amount_e, balance_diff);
|
||||
|
||||
// f. construct and push a view-tagged ringct coinbase tx
|
||||
bc.add_block(HF_VERSION_VIEW_TAGS, {}, main_addr);
|
||||
{
|
||||
auto top_block = bc.get_parsed_block(bc.height() - 1);
|
||||
const cryptonote::transaction &top_miner_tx = top_block.block.miner_tx;
|
||||
ASSERT_EQ(2, top_miner_tx.version);
|
||||
ASSERT_EQ(1, top_miner_tx.vout.size());
|
||||
ASSERT_EQ(rct::RCTTypeNull, top_miner_tx.rct_signatures.type);
|
||||
ASSERT_EQ(0, top_miner_tx.signatures.size());
|
||||
ASSERT_EQ(typeid(cryptonote::txout_to_tagged_key), top_miner_tx.vout.at(0).target.type());
|
||||
ASSERT_EQ(fake_pruned_blockchain::miner_reward, top_miner_tx.vout.at(0).amount);
|
||||
}
|
||||
|
||||
// f. scan view-tagged ringct coinbase tx
|
||||
balance_diff = wallet_process_new_blocks();
|
||||
EXPECT_EQ(fake_pruned_blockchain::miner_reward, balance_diff);
|
||||
|
||||
// g. construct and push a view-tagged pre-ringct (only possible in unmixable sweep txs) tx
|
||||
const rct::xmr_amount amount_g = rct::randXmrAmount(COIN);
|
||||
{
|
||||
const rct::xmr_amount fee = rct::randXmrAmount(COIN);
|
||||
std::vector<cryptonote::tx_destination_entry> dests = {
|
||||
cryptonote::tx_destination_entry(amount_g, acc_keys.m_account_address, false)};
|
||||
cryptonote::transaction curr_tx = construct_pre_carrot_tx_with_fake_inputs(
|
||||
acc_keys,
|
||||
w.m_subaddresses,
|
||||
/*stripped_sources=*/{},
|
||||
dests,
|
||||
acc_keys.m_account_address,
|
||||
fee,
|
||||
HF_VERSION_VIEW_TAGS,
|
||||
/*sweep_unmixable_override=*/true);
|
||||
ASSERT_FALSE(cryptonote::is_coinbase(curr_tx));
|
||||
ASSERT_EQ(1, curr_tx.version);
|
||||
ASSERT_EQ(rct::RCTTypeNull, curr_tx.rct_signatures.type);
|
||||
ASSERT_EQ(typeid(cryptonote::txout_to_tagged_key), curr_tx.vout.at(0).target.type());
|
||||
ASSERT_EQ(amount_g, curr_tx.vout.at(0).amount);
|
||||
bc.add_block(HF_VERSION_VIEW_TAGS, {std::move(curr_tx)}, null_addr);
|
||||
}
|
||||
|
||||
// g. scan view-tagged pre-ringct (only possible in unmixable sweep txs) tx
|
||||
balance_diff = wallet_process_new_blocks();
|
||||
EXPECT_EQ(amount_g, balance_diff);
|
||||
|
||||
// h. construct and push a view-tagged ringct tx
|
||||
const rct::xmr_amount amount_h = rct::randXmrAmount(COIN);
|
||||
{
|
||||
const rct::xmr_amount fee = rct::randXmrAmount(COIN);
|
||||
std::vector<cryptonote::tx_destination_entry> dests = {
|
||||
cryptonote::tx_destination_entry(amount_h, acc_keys.m_account_address, false)};
|
||||
cryptonote::transaction curr_tx = construct_pre_carrot_tx_with_fake_inputs(
|
||||
acc_keys,
|
||||
w.m_subaddresses,
|
||||
/*stripped_sources=*/{},
|
||||
dests,
|
||||
acc_keys.m_account_address,
|
||||
fee,
|
||||
HF_VERSION_VIEW_TAGS);
|
||||
ASSERT_FALSE(cryptonote::is_coinbase(curr_tx));
|
||||
ASSERT_EQ(2, curr_tx.version);
|
||||
ASSERT_EQ(rct::RCTTypeBulletproofPlus, curr_tx.rct_signatures.type);
|
||||
ASSERT_EQ(typeid(cryptonote::txout_to_tagged_key), curr_tx.vout.at(0).target.type());
|
||||
ASSERT_EQ(0, curr_tx.vout.at(0).amount);
|
||||
bc.add_block(HF_VERSION_VIEW_TAGS, {std::move(curr_tx)}, null_addr);
|
||||
}
|
||||
|
||||
// h. scan ringct view-tagged ringct tx
|
||||
balance_diff = wallet_process_new_blocks();
|
||||
EXPECT_EQ(amount_h, balance_diff);
|
||||
|
||||
// i. construct and push a carrot v1 coinbase tx
|
||||
bc.add_block(HF_VERSION_CARROT, {}, main_addr);
|
||||
{
|
||||
auto top_block = bc.get_parsed_block(bc.height() - 1);
|
||||
const cryptonote::transaction &top_miner_tx = top_block.block.miner_tx;
|
||||
ASSERT_EQ(2, top_miner_tx.version);
|
||||
ASSERT_EQ(1, top_miner_tx.vout.size());
|
||||
ASSERT_EQ(rct::RCTTypeNull, top_miner_tx.rct_signatures.type);
|
||||
ASSERT_EQ(0, top_miner_tx.signatures.size());
|
||||
ASSERT_EQ(typeid(cryptonote::txout_to_carrot_v1), top_miner_tx.vout.at(0).target.type());
|
||||
ASSERT_EQ(fake_pruned_blockchain::miner_reward, top_miner_tx.vout.at(0).amount);
|
||||
}
|
||||
|
||||
// i. scan carrot v1 coinbase tx
|
||||
balance_diff = wallet_process_new_blocks();
|
||||
EXPECT_EQ(fake_pruned_blockchain::miner_reward, balance_diff);
|
||||
|
||||
// j. construct and push a carrot v1 normal tx
|
||||
const rct::xmr_amount amount_j = rct::randXmrAmount(COIN);
|
||||
{
|
||||
std::vector<cryptonote::tx_destination_entry> dests = {
|
||||
cryptonote::tx_destination_entry(amount_j, acc_keys.m_account_address, false)};
|
||||
cryptonote::transaction curr_tx = construct_carrot_pruned_transaction_fake_inputs(
|
||||
{carrot::mock::convert_normal_payment_proposal_v1(dests.front())},
|
||||
/*selfsend_payment_proposals=*/{},
|
||||
acc_keys);
|
||||
ASSERT_FALSE(cryptonote::is_coinbase(curr_tx));
|
||||
ASSERT_EQ(2, curr_tx.version);
|
||||
ASSERT_EQ(rct::RCTTypeFcmpPlusPlus, curr_tx.rct_signatures.type);
|
||||
ASSERT_EQ(typeid(cryptonote::txout_to_carrot_v1), curr_tx.vout.at(0).target.type());
|
||||
ASSERT_EQ(0, curr_tx.vout.at(0).amount);
|
||||
bc.add_block(HF_VERSION_CARROT, {std::move(curr_tx)}, null_addr);
|
||||
}
|
||||
|
||||
// j. scan carrot v1 normal tx
|
||||
balance_diff = wallet_process_new_blocks();
|
||||
EXPECT_EQ(amount_j, balance_diff);
|
||||
|
||||
// k. construct and push a carrot v1 special tx
|
||||
const rct::xmr_amount amount_k = rct::randXmrAmount(COIN);
|
||||
{
|
||||
std::vector<cryptonote::tx_destination_entry> dests = {
|
||||
cryptonote::tx_destination_entry(amount_k, acc_keys.m_account_address, false)};
|
||||
cryptonote::transaction curr_tx = construct_carrot_pruned_transaction_fake_inputs(
|
||||
/*normal_payment_proposals=*/{},
|
||||
{{carrot::mock::convert_selfsend_payment_proposal_v1(dests.front()), {/*main*/}}},
|
||||
acc_keys);
|
||||
ASSERT_FALSE(cryptonote::is_coinbase(curr_tx));
|
||||
ASSERT_EQ(2, curr_tx.version);
|
||||
ASSERT_EQ(rct::RCTTypeFcmpPlusPlus, curr_tx.rct_signatures.type);
|
||||
ASSERT_EQ(2, curr_tx.vout.size());
|
||||
ASSERT_EQ(typeid(cryptonote::txout_to_carrot_v1), curr_tx.vout.at(0).target.type());
|
||||
ASSERT_EQ(0, curr_tx.vout.at(0).amount);
|
||||
bc.add_block(HF_VERSION_CARROT, {std::move(curr_tx)}, null_addr);
|
||||
}
|
||||
|
||||
// k. scan carrot v1 special tx
|
||||
balance_diff = wallet_process_new_blocks();
|
||||
EXPECT_EQ(amount_k, balance_diff);
|
||||
}
|
||||
//----------------------------------------------------------------------------------------------------------------------
|
137
tests/unit_tests/wallet_tx_builder.cpp
Normal file
137
tests/unit_tests/wallet_tx_builder.cpp
Normal file
|
@ -0,0 +1,137 @@
|
|||
// 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.
|
||||
|
||||
#include "unit_tests_utils.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
#include "carrot_core/config.h"
|
||||
#include "common/container_helpers.h"
|
||||
#include "ringct/rctOps.h"
|
||||
#include "wallet/tx_builder.h"
|
||||
|
||||
static tools::wallet2::transfer_details gen_transfer_details()
|
||||
{
|
||||
return tools::wallet2::transfer_details{
|
||||
.m_block_height = crypto::rand_idx<uint64_t>(CRYPTONOTE_MAX_BLOCK_NUMBER),
|
||||
.m_tx = {},
|
||||
.m_txid = crypto::rand<crypto::hash>(),
|
||||
.m_internal_output_index = crypto::rand_idx<uint64_t>(carrot::CARROT_MAX_TX_OUTPUTS),
|
||||
.m_global_output_index = crypto::rand_idx<uint64_t>(CRYPTONOTE_MAX_BLOCK_NUMBER * 1000ull),
|
||||
.m_spent = false,
|
||||
.m_frozen = false,
|
||||
.m_spent_height = 0,
|
||||
.m_key_image = rct::rct2pk(rct::pkGen()),
|
||||
.m_mask = rct::skGen(),
|
||||
.m_amount = crypto::rand_range<rct::xmr_amount>(0, COIN), // [0, 1] XMR i.e. [0, 1e12] pXMR
|
||||
.m_rct = true,
|
||||
.m_key_image_known = true,
|
||||
.m_key_image_request = false,
|
||||
.m_pk_index = 1,
|
||||
.m_subaddr_index = {},
|
||||
.m_key_image_partial = false,
|
||||
.m_multisig_k = {},
|
||||
.m_multisig_info = {},
|
||||
.m_uses = {},
|
||||
};
|
||||
}
|
||||
|
||||
static bool compare_transfer_to_selected_input(const tools::wallet2::transfer_details &td,
|
||||
const carrot::CarrotSelectedInput &input)
|
||||
{
|
||||
return td.m_amount == input.amount && td.m_key_image == input.key_image;
|
||||
}
|
||||
|
||||
TEST(wallet_tx_builder, input_selection_basic)
|
||||
{
|
||||
std::map<std::size_t, rct::xmr_amount> fee_by_input_count;
|
||||
for (size_t i = carrot::CARROT_MIN_TX_INPUTS; i <= carrot::CARROT_MAX_TX_INPUTS; ++i)
|
||||
fee_by_input_count[i] = 30680000 * i - i*i;
|
||||
|
||||
const boost::multiprecision::int128_t nominal_output_sum = 4444444444444; // 4.444... XMR
|
||||
|
||||
// add 10 random transfers
|
||||
tools::wallet2::transfer_container transfers;
|
||||
for (size_t i = 0; i < 10; ++i)
|
||||
{
|
||||
tools::wallet2::transfer_details &td = tools::add_element(transfers);
|
||||
td = gen_transfer_details();
|
||||
td.m_block_height = transfers.size(); // small ascending block heights
|
||||
}
|
||||
|
||||
// modify one so that it funds the transfer all by itself
|
||||
const size_t rand_idx = crypto::rand_idx(transfers.size());
|
||||
transfers[rand_idx].m_amount = boost::numeric_cast<rct::xmr_amount>(nominal_output_sum +
|
||||
fee_by_input_count.crbegin()->second +
|
||||
crypto::rand_range<rct::xmr_amount>(0, COIN));
|
||||
|
||||
// set such that all transfers are unlocked
|
||||
const std::uint64_t top_block_index = transfers.size() + CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE;
|
||||
|
||||
// make input selector
|
||||
std::set<size_t> selected_transfer_indices;
|
||||
const carrot::select_inputs_func_t input_selector = tools::wallet::make_wallet2_single_transfer_input_selector(
|
||||
transfers,
|
||||
/*from_account=*/0,
|
||||
/*from_subaddresses=*/{},
|
||||
/*ignore_above=*/std::numeric_limits<rct::xmr_amount>::max(),
|
||||
/*ignore_below=*/0,
|
||||
top_block_index,
|
||||
/*allow_carrot_external_inputs_in_normal_transfers=*/true,
|
||||
/*allow_pre_carrot_inputs_in_normal_transfers=*/true,
|
||||
selected_transfer_indices
|
||||
);
|
||||
|
||||
// select inputs
|
||||
std::vector<carrot::CarrotSelectedInput> selected_inputs;
|
||||
input_selector(nominal_output_sum,
|
||||
fee_by_input_count,
|
||||
1, // number of normal payment proposals
|
||||
1, // number of self-send payment proposals
|
||||
selected_inputs);
|
||||
|
||||
ASSERT_EQ(2, selected_inputs.size()); // assert two inputs selected
|
||||
ASSERT_EQ(2, selected_transfer_indices.size());
|
||||
ASSERT_LT(*selected_transfer_indices.crbegin(), transfers.size());
|
||||
ASSERT_NE(selected_inputs.front().key_image, selected_inputs.back().key_image);
|
||||
|
||||
// Assert content of selected inputs matches the content in `transfers`
|
||||
std::set<size_t> matched_transfer_indices;
|
||||
for (const carrot::CarrotSelectedInput &selected_input : selected_inputs)
|
||||
{
|
||||
for (const size_t selected_transfer_index : selected_transfer_indices)
|
||||
{
|
||||
if (compare_transfer_to_selected_input(transfers.at(selected_transfer_index), selected_input))
|
||||
{
|
||||
const auto insert_res = matched_transfer_indices.insert(selected_transfer_index);
|
||||
if (insert_res.second)
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
ASSERT_EQ(selected_transfer_indices.size(), matched_transfer_indices.size());
|
||||
}
|
Loading…
Reference in a new issue