carrot_impl 3/27/25 [WIP]

This commit is contained in:
jeffro256 2025-01-10 14:59:27 -06:00
parent e85b76246f
commit 40b04ef372
No known key found for this signature in database
GPG key ID: 6F79797A6E392442
45 changed files with 8560 additions and 1087 deletions

View file

@ -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)

View file

@ -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,

View file

@ -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

View 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})

View 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

View 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

View 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

View 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

View 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

View 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

View 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);

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View file

@ -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;

View file

@ -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.

View file

@ -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");

View file

@ -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>

View file

@ -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));
}

View file

@ -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);

View file

@ -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()

View file

@ -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

View file

@ -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;
}
}
//---------------------------------------------------------------

View file

@ -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); }
}

View file

@ -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);
}

View file

@ -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);

View file

@ -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

View 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
View 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
View 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
View 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

View file

@ -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();

View file

@ -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();

View file

@ -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);
}
}

View file

@ -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

View 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);
}
}
//----------------------------------------------------------------------------------------------------------------------

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View 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);
}
//----------------------------------------------------------------------------------------------------------------------

View 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());
}