From ba00cd282e2033b1d37c9d889cf5a7d793adfb72 Mon Sep 17 00:00:00 2001 From: jeffro256 Date: Fri, 8 Nov 2024 14:33:54 -0600 Subject: [PATCH 01/15] carrot_core [27/1/2025] --- .gitmodules | 4 + CMakeLists.txt | 4 +- external/CMakeLists.txt | 1 + external/mx25519 | 1 + src/CMakeLists.txt | 1 + src/carrot_core/CMakeLists.txt | 62 + src/carrot_core/account_secrets.cpp | 102 ++ src/carrot_core/account_secrets.h | 103 ++ src/carrot_core/address_utils.cpp | 85 ++ src/carrot_core/address_utils.h | 95 ++ src/carrot_core/carrot_enote_scan.cpp | 334 +++++ src/carrot_core/carrot_enote_scan.h | 87 ++ src/carrot_core/carrot_enote_types.cpp | 73 ++ src/carrot_core/carrot_enote_types.h | 109 ++ src/carrot_core/config.h | 73 ++ src/carrot_core/core_types.cpp | 137 +++ src/carrot_core/core_types.h | 133 ++ src/carrot_core/destination.cpp | 149 +++ src/carrot_core/destination.h | 116 ++ src/carrot_core/device.h | 165 +++ src/carrot_core/device_ram_borrowed.cpp | 90 ++ src/carrot_core/device_ram_borrowed.h | 86 ++ src/carrot_core/enote_utils.cpp | 499 ++++++++ src/carrot_core/enote_utils.h | 392 ++++++ src/carrot_core/hash_functions.cpp | 109 ++ src/carrot_core/hash_functions.h | 59 + src/carrot_core/output_set_finalization.cpp | 268 ++++ src/carrot_core/output_set_finalization.h | 111 ++ src/carrot_core/payment_proposal.cpp | 489 ++++++++ src/carrot_core/payment_proposal.h | 171 +++ src/carrot_core/transcript_fixed.h | 185 +++ src/crypto/CMakeLists.txt | 3 + src/crypto/crypto-ops.c | 86 ++ src/crypto/crypto-ops.h | 6 + src/crypto/generators.cpp | 56 + src/crypto/generators.h | 3 + tests/performance_tests/CMakeLists.txt | 4 +- tests/performance_tests/main.cpp | 13 + tests/performance_tests/view_scan.h | 307 +++++ tests/unit_tests/CMakeLists.txt | 5 + tests/unit_tests/carrot_core.cpp | 1151 ++++++++++++++++++ tests/unit_tests/carrot_legacy.cpp | 291 +++++ tests/unit_tests/carrot_transcript_fixed.cpp | 61 + tests/unit_tests/x25519.cpp | 205 ++++ 44 files changed, 6482 insertions(+), 2 deletions(-) create mode 160000 external/mx25519 create mode 100644 src/carrot_core/CMakeLists.txt create mode 100644 src/carrot_core/account_secrets.cpp create mode 100644 src/carrot_core/account_secrets.h create mode 100644 src/carrot_core/address_utils.cpp create mode 100644 src/carrot_core/address_utils.h create mode 100644 src/carrot_core/carrot_enote_scan.cpp create mode 100644 src/carrot_core/carrot_enote_scan.h create mode 100644 src/carrot_core/carrot_enote_types.cpp create mode 100644 src/carrot_core/carrot_enote_types.h create mode 100644 src/carrot_core/config.h create mode 100644 src/carrot_core/core_types.cpp create mode 100644 src/carrot_core/core_types.h create mode 100644 src/carrot_core/destination.cpp create mode 100644 src/carrot_core/destination.h create mode 100644 src/carrot_core/device.h create mode 100644 src/carrot_core/device_ram_borrowed.cpp create mode 100644 src/carrot_core/device_ram_borrowed.h create mode 100644 src/carrot_core/enote_utils.cpp create mode 100644 src/carrot_core/enote_utils.h create mode 100644 src/carrot_core/hash_functions.cpp create mode 100644 src/carrot_core/hash_functions.h create mode 100644 src/carrot_core/output_set_finalization.cpp create mode 100644 src/carrot_core/output_set_finalization.h create mode 100644 src/carrot_core/payment_proposal.cpp create mode 100644 src/carrot_core/payment_proposal.h create mode 100644 src/carrot_core/transcript_fixed.h create mode 100644 tests/performance_tests/view_scan.h create mode 100644 tests/unit_tests/carrot_core.cpp create mode 100644 tests/unit_tests/carrot_legacy.cpp create mode 100644 tests/unit_tests/carrot_transcript_fixed.cpp create mode 100644 tests/unit_tests/x25519.cpp diff --git a/.gitmodules b/.gitmodules index f711a555bb5..2015f8d4661 100644 --- a/.gitmodules +++ b/.gitmodules @@ -14,3 +14,7 @@ [submodule "external/gtest"] path = external/gtest url = https://github.com/google/googletest.git +[submodule "external/mx25519"] + path = external/mx25519 + url = https://github.com/jeffro256/mx25519 + branch = unclamped diff --git a/CMakeLists.txt b/CMakeLists.txt index 1e1bf1a9423..835f5119b65 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -360,6 +360,7 @@ else() message(STATUS "Building without build tag") endif() +option(MANUAL_SUBMODULES "Skip submodule up-to-date checks" OFF) if(NOT MANUAL_SUBMODULES) find_package(Git) if(GIT_FOUND) @@ -379,6 +380,7 @@ if(NOT MANUAL_SUBMODULES) check_submodule(external/rapidjson) check_submodule(external/randomx) check_submodule(external/supercop) + check_submodule(external/mx25519) endif() endif() @@ -460,7 +462,7 @@ elseif(CMAKE_SYSTEM_NAME MATCHES ".*BSDI.*") set(BSDI TRUE) endif() -include_directories(external/rapidjson/include external/easylogging++ src contrib/epee/include external external/supercop/include) +include_directories(external/rapidjson/include external/easylogging++ src contrib/epee/include external external/supercop/include external/mx25519/include) if(APPLE) cmake_policy(SET CMP0042 NEW) diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index f414fb85bef..1a64a681aae 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -71,3 +71,4 @@ add_subdirectory(db_drivers) add_subdirectory(easylogging++) add_subdirectory(qrcodegen) add_subdirectory(randomx EXCLUDE_FROM_ALL) +add_subdirectory(mx25519) diff --git a/external/mx25519 b/external/mx25519 new file mode 160000 index 00000000000..3c3a36d77d7 --- /dev/null +++ b/external/mx25519 @@ -0,0 +1 @@ +Subproject commit 3c3a36d77d7a10e328cbffc2cf2c2bb59ced9d9a diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6190b40f830..6c53b115d6f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -83,6 +83,7 @@ endfunction () include(Version) monero_add_library(version SOURCES ${CMAKE_BINARY_DIR}/version.cpp DEPENDS genversion) +add_subdirectory(carrot_core) add_subdirectory(common) add_subdirectory(crypto) add_subdirectory(ringct) diff --git a/src/carrot_core/CMakeLists.txt b/src/carrot_core/CMakeLists.txt new file mode 100644 index 00000000000..e6e63c8021e --- /dev/null +++ b/src/carrot_core/CMakeLists.txt @@ -0,0 +1,62 @@ +# 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_core_sources + account_secrets.cpp + address_utils.cpp + carrot_enote_scan.cpp + carrot_enote_types.cpp + core_types.cpp + destination.cpp + device_ram_borrowed.cpp + enote_utils.cpp + hash_functions.cpp + output_set_finalization.cpp + payment_proposal.cpp) + +monero_find_all_headers(carrot_core_headers, "${CMAKE_CURRENT_SOURCE_DIR}") + +monero_add_library(carrot_core + ${carrot_core_sources} + ${carrot_core_headers}) + +target_link_libraries(carrot_core + PUBLIC + cncrypto + epee + mx25519_static + ringct + seraphis_crypto + PRIVATE + ${EXTRA_LIBRARIES}) + +target_include_directories(carrot_core + PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}" + PRIVATE + ${Boost_INCLUDE_DIRS}) diff --git a/src/carrot_core/account_secrets.cpp b/src/carrot_core/account_secrets.cpp new file mode 100644 index 00000000000..c7c8652fbab --- /dev/null +++ b/src/carrot_core/account_secrets.cpp @@ -0,0 +1,102 @@ +// 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 "account_secrets.h" + +//local headers +#include "config.h" +#include "crypto/generators.h" +#include "hash_functions.h" +#include "ringct/rctOps.h" +#include "transcript_fixed.h" + +//third party headers + +//standard headers + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "carrot" + +namespace carrot +{ +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_provespend_key(const crypto::secret_key &s_master, + crypto::secret_key &k_prove_spend_out) +{ + // k_ps = H_n(s_m) + const auto transcript = sp::make_fixed_transcript(); + derive_scalar(transcript.data(), transcript.size(), &s_master, to_bytes(k_prove_spend_out)); +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_viewbalance_secret(const crypto::secret_key &s_master, + crypto::secret_key &s_view_balance_out) +{ + // s_vb = H_32(s_m) + const auto transcript = sp::make_fixed_transcript(); + derive_bytes_32(transcript.data(), transcript.size(), &s_master, to_bytes(s_view_balance_out)); +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_generateimage_key(const crypto::secret_key &s_view_balance, + crypto::secret_key &k_generate_image_out) +{ + // k_gi = H_n(s_vb) + const auto transcript = sp::make_fixed_transcript(); + derive_scalar(transcript.data(), transcript.size(), &s_view_balance, to_bytes(k_generate_image_out)); +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_viewincoming_key(const crypto::secret_key &s_view_balance, + crypto::secret_key &k_view_out) +{ + // k_v = H_n(s_vb) + const auto transcript = sp::make_fixed_transcript(); + derive_scalar(transcript.data(), transcript.size(), &s_view_balance, to_bytes(k_view_out)); +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_generateaddress_secret(const crypto::secret_key &s_view_balance, + crypto::secret_key &s_generate_address_out) +{ + // s_ga = H_32(s_vb) + const auto transcript = sp::make_fixed_transcript(); + derive_bytes_32(transcript.data(), transcript.size(), &s_view_balance, to_bytes(s_generate_address_out)); +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_spend_pubkey(const crypto::secret_key &k_generate_image, + const crypto::secret_key &k_prove_spend, + crypto::public_key &spend_pubkey_out) +{ + // k_ps T + rct::key tmp; + rct::scalarmultKey(tmp, rct::pk2rct(crypto::get_T()), rct::sk2rct(k_prove_spend)); + + // K_s = k_gi G + k_ps T + rct::addKeys1(tmp, rct::sk2rct(k_generate_image), tmp); + spend_pubkey_out = rct::rct2pk(tmp); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace carrot diff --git a/src/carrot_core/account_secrets.h b/src/carrot_core/account_secrets.h new file mode 100644 index 00000000000..4fef1c55c3b --- /dev/null +++ b/src/carrot_core/account_secrets.h @@ -0,0 +1,103 @@ +// 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. + +//// +// Core implementation details for making Carrot privkeys, secrets, and pubkeys. +// - Carrot is a specification for FCMP-RingCT compatible addressing +// +// references: +// * https://github.com/jeffro256/carrot/blob/master/carrot.md +/// + +#pragma once + +//local headers +#include "crypto/crypto.h" + +//third party headers + +//standard headers + +//forward declarations + + +namespace carrot +{ + +/** +* brief: make_carrot_provespend_key - prove-spend key, for signing input proofs to spend enotes +* k_ps = H_n(s_m) +* param: s_master - s_m +* outparam: k_prove_spend_out - k_ps +*/ +void make_carrot_provespend_key(const crypto::secret_key &s_master, + crypto::secret_key &k_prove_spend_out); +/** +* brief: make_carrot_viewbalance_secret - view-balance secret, for viewing all balance information +* s_vb = H_n(s_m) +* param: s_master - s_m +* outparam: s_view_balance_out - s_vb +*/ +void make_carrot_viewbalance_secret(const crypto::secret_key &s_master, + crypto::secret_key &s_view_balance_out); +/** +* brief: make_carrot_generateimage_key - generate-image key, for identifying enote spends +* k_gi = H_n(s_vb) +* param: s_view_balance - s_vb +* outparam: k_generate_image_out - k_gi +*/ +void make_carrot_generateimage_key(const crypto::secret_key &s_view_balance, + crypto::secret_key &k_generate_image_out); +/** +* brief: make_carrot_viewincoming_key - view-incoming key, for identifying received external enotes +* k_v = H_n(s_vb) +* param: s_view_balance - s_vb +* outparam: k_view_out - k_v +*/ +void make_carrot_viewincoming_key(const crypto::secret_key &s_view_balance, + crypto::secret_key &k_view_out); +/** +* brief: make_carrot_generateaddress_secret - generate-address secret, for generating addresses +* s_ga = H_32(s_vb) +* param: s_view_balance - s_vb +* outparam: s_generate_address_out - s_ga +*/ +void make_carrot_generateaddress_secret(const crypto::secret_key &s_view_balance, + crypto::secret_key &s_generate_address_out); +/** + * brief: make_carrot_spend_pubkey - base public spendkey for rerandomizable RingCT + * K_s = k_gi G + k_ps T + * param: k_generate_image - k_gi + * param: k_prove_spend - k_ps + * outparam: spend_pubkey_out - K_s +*/ +void make_carrot_spend_pubkey(const crypto::secret_key &k_generate_image, + const crypto::secret_key &k_prove_spend, + crypto::public_key &spend_pubkey_out); + +} //namespace carrot diff --git a/src/carrot_core/address_utils.cpp b/src/carrot_core/address_utils.cpp new file mode 100644 index 00000000000..2466777429b --- /dev/null +++ b/src/carrot_core/address_utils.cpp @@ -0,0 +1,85 @@ +// 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.h" + +//local headers +#include "config.h" +#include "hash_functions.h" +#include "ringct/rctOps.h" +#include "transcript_fixed.h" + +//third party headers + +//standard headers + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "carrot" + +namespace carrot +{ +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_index_extension_generator(const crypto::secret_key &s_generate_address, + const std::uint32_t j_major, + const std::uint32_t j_minor, + crypto::secret_key &address_generator_out) +{ + // s^j_gen = H_32[s_ga](j_major, j_minor) + const auto transcript = sp::make_fixed_transcript(j_major, j_minor); + derive_bytes_32(transcript.data(), transcript.size(), &s_generate_address, &address_generator_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_subaddress_scalar(const crypto::public_key &account_spend_pubkey, + const crypto::secret_key &s_address_generator, + const std::uint32_t j_major, + const std::uint32_t j_minor, + crypto::secret_key &subaddress_scalar_out) +{ + // k^j_subscal = H_n(K_s, j_major, j_minor, s^j_gen) + const auto transcript = sp::make_fixed_transcript( + account_spend_pubkey, j_major, j_minor); + derive_scalar(transcript.data(), transcript.size(), &s_address_generator, subaddress_scalar_out.data); +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_address_spend_pubkey(const crypto::public_key &account_spend_pubkey, + const crypto::secret_key &s_generate_address, + const std::uint32_t j_major, + const std::uint32_t j_minor, + crypto::public_key &address_spend_pubkey_out) +{ + // k^j_subscal = H_n(K_s, j_major, j_minor, s^j_gen) + crypto::secret_key subaddress_scalar; + make_carrot_subaddress_scalar(account_spend_pubkey, s_generate_address, j_major, j_minor, subaddress_scalar); + + // K^j_s = k^j_subscal * K_s + address_spend_pubkey_out = rct::rct2pk(rct::scalarmultKey( + rct::pk2rct(account_spend_pubkey), rct::sk2rct(subaddress_scalar))); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace carrot diff --git a/src/carrot_core/address_utils.h b/src/carrot_core/address_utils.h new file mode 100644 index 00000000000..d369b350cc8 --- /dev/null +++ b/src/carrot_core/address_utils.h @@ -0,0 +1,95 @@ +// 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. + +// Utilities for building carrot addresses. + +#pragma once + +//local headers +#include "crypto/crypto.h" + +//third party headers + +//standard headers + +//forward declarations + + +namespace carrot +{ + +/** +* brief: is_main_address_index - determine whether j=(j_major, j_minor) represents the main address +*/ +static constexpr bool is_main_address_index(const std::uint32_t j_major, const std::uint32_t j_minor) +{ + return !(j_major || j_minor); +} + +/** +* brief: make_carrot_index_extension_generator - s^j_gen +* s^j_gen = H_32[s_ga](j_major, j_minor) +* param: s_generate_address - s_ga +* param: j_major - +* param: j_minor - +* outparam: address_generator_out - s^j_gen +*/ +void make_carrot_index_extension_generator(const crypto::secret_key &s_generate_address, + const std::uint32_t j_major, + const std::uint32_t j_minor, + crypto::secret_key &address_generator_out); +/** +* brief: make_carrot_address_privkey - d^j_a +* k^j_subscal = H_n(K_s, j_major, j_minor, s^j_gen) +* param: account_spend_pubkey - K_s = k_vb X + k_m U +* param: s_address_generator - s^j_gen +* param: j_major - +* param: j_minor - +* outparam: subaddress_scalar_out - k^j_subscal +*/ +void make_carrot_subaddress_scalar(const crypto::public_key &account_spend_pubkey, + const crypto::secret_key &s_address_generator, + const std::uint32_t j_major, + const std::uint32_t j_minor, + crypto::secret_key &subaddress_scalar_out); +/** +* brief: make_carrot_address_spend_pubkey - K^j_s +* K^j_s = k^j_subscal * K_s +* param: account_spend_pubkey - K_s = k_gi G + k_ps U +* param: s_generate_address - s_ga +* param: j_major - +* param: j_minor - +* outparam: address_spend_pubkey_out - K^j_s +*/ +void make_carrot_address_spend_pubkey(const crypto::public_key &account_spend_pubkey, + const crypto::secret_key &s_generate_address, + const std::uint32_t j_major, + const std::uint32_t j_minor, + crypto::public_key &address_spend_pubkey_out); + +} //namespace carrot diff --git a/src/carrot_core/carrot_enote_scan.cpp b/src/carrot_core/carrot_enote_scan.cpp new file mode 100644 index 00000000000..8b45496094b --- /dev/null +++ b/src/carrot_core/carrot_enote_scan.cpp @@ -0,0 +1,334 @@ +// 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. + +// Utilities for scanning carrot enotes + +//paired header +#include "carrot_enote_scan.h" + +//local headers +#include "crypto/generators.h" +#include "enote_utils.h" +#include "ringct/rctOps.h" + +//third party headers + +//standard headers + + +namespace carrot +{ +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool try_scan_carrot_non_coinbase_core(const CarrotEnoteV1 &enote, + const std::optional encrypted_payment_id, + const crypto::hash &s_sender_receiver, + 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, + janus_anchor_t &nominal_janus_anchor_out) +{ + // k^o_g = H_n("..g..", s^ctx_sr, C_a) + make_carrot_onetime_address_extension_g(s_sender_receiver, + enote.amount_commitment, + sender_extension_g_out); + + // k^o_t = H_n("..t..", s^ctx_sr, C_a) + make_carrot_onetime_address_extension_t(s_sender_receiver, + enote.amount_commitment, + sender_extension_t_out); + + // K^j_s = Ko - K^o_ext = Ko - (k^o_g G + k^o_t T) + recover_address_spend_pubkey(enote.onetime_address, + s_sender_receiver, + enote.amount_commitment, + address_spend_pubkey_out); + + // if cannot recompute C_a, then FAIL + if (!try_get_carrot_amount(s_sender_receiver, + enote.amount_enc, + enote.onetime_address, + address_spend_pubkey_out, + enote.amount_commitment, + enote_type_out, + amount_out, + amount_blinding_factor_out)) + return false; + + // pid = pid_enc XOR m_pid, if applicable + if (encrypted_payment_id) + payment_id_out = decrypt_legacy_payment_id(*encrypted_payment_id, s_sender_receiver, enote.onetime_address); + else + payment_id_out = null_payment_id; + + // anchor = anchor_enc XOR m_anchor + nominal_janus_anchor_out = decrypt_carrot_anchor(enote.anchor_enc, + s_sender_receiver, + enote.onetime_address); + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +bool verify_carrot_janus_protection(const input_context_t &input_context, + const crypto::public_key &onetime_address, + const view_incoming_key_device &k_view_dev, + const crypto::public_key &account_spend_pubkey, + const crypto::public_key &nominal_address_spend_pubkey, + const mx25519_pubkey &enote_ephemeral_pubkey, + const janus_anchor_t &nominal_anchor, + payment_id_t &nominal_payment_id_inout) +{ + const bool is_subaddress = nominal_address_spend_pubkey != account_spend_pubkey; + + // make K^j_v' + crypto::public_key nominal_address_view_pubkey; + if (is_subaddress) + { + // K^j_v' = k_v K^j_s' + if (!k_view_dev.view_key_scalar_mult_ed25519(nominal_address_spend_pubkey, nominal_address_view_pubkey)) + return false; + } + else // cryptonote address + { + // K^j_v' = k_v G + if (!k_view_dev.view_key_scalar_mult_ed25519(crypto::get_G(), nominal_address_view_pubkey)) + return false; + } + + // if can recompute D_e with pid', then PASS + if (verify_carrot_external_janus_protection(nominal_anchor, + input_context, + nominal_address_spend_pubkey, + nominal_address_view_pubkey, + is_subaddress, + nominal_payment_id_inout, + enote_ephemeral_pubkey)) + return true; + + // if can recompute D_e with null pid, then PASS + nominal_payment_id_inout = null_payment_id; + if (verify_carrot_external_janus_protection(nominal_anchor, + input_context, + nominal_address_spend_pubkey, + nominal_address_view_pubkey, + is_subaddress, + null_payment_id, + enote_ephemeral_pubkey)) + return true; + + // anchor_sp = H_16(D_e, input_context, Ko, k_v, K_s) + janus_anchor_t expected_special_anchor; + k_view_dev.make_janus_anchor_special(enote_ephemeral_pubkey, + input_context, + onetime_address, + account_spend_pubkey, + expected_special_anchor); + + // attempt special janus check: anchor_sp ?= anchor' + return expected_special_anchor == nominal_anchor; +} +//------------------------------------------------------------------------------------------------------------------- +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 &account_spend_pubkey, + crypto::secret_key &sender_extension_g_out, + crypto::secret_key &sender_extension_t_out, + crypto::public_key &address_spend_pubkey_out) +{ + // input_context + input_context_t input_context; + make_carrot_input_context_coinbase(enote.block_index, input_context); + + // if vt' != vt, then FAIL + if (!test_carrot_view_tag(s_sender_receiver_unctx.data, input_context, enote.onetime_address, enote.view_tag)) + return false; + + // s^ctx_sr = H_32(s_sr, D_e, input_context) + crypto::hash s_sender_receiver; + make_carrot_sender_receiver_secret(s_sender_receiver_unctx.data, + enote.enote_ephemeral_pubkey, + input_context, + s_sender_receiver); + + // C_a = G + a H + const rct::key implied_amount_commitment = rct::zeroCommit(enote.amount); + + // k^o_g = H_n("..g..", s^ctx_sr, C_a) + make_carrot_onetime_address_extension_g(s_sender_receiver, + implied_amount_commitment, + sender_extension_g_out); + + // k^o_t = H_n("..t..", s^ctx_sr, C_a) + make_carrot_onetime_address_extension_t(s_sender_receiver, + implied_amount_commitment, + sender_extension_t_out); + + // K^j_s = Ko - K^o_ext = Ko - (k^o_g G + k^o_t T) + recover_address_spend_pubkey(enote.onetime_address, + s_sender_receiver, + implied_amount_commitment, + address_spend_pubkey_out); + + // if K^j_s != K^s, then FAIL + // - We have no "hard target" in the amount commitment, so if we want deterministic enote + // scanning without a subaddress table, we reject all non-main addresses in coinbase enotes + if (address_spend_pubkey_out != account_spend_pubkey) + return false; + + // anchor = anchor_enc XOR m_anchor + const janus_anchor_t nominal_anchor = decrypt_carrot_anchor(enote.anchor_enc, + s_sender_receiver, + enote.onetime_address); + + // verify Janus attack protection + payment_id_t dummy_payment_id = null_payment_id; + if (!verify_carrot_janus_protection(input_context, + enote.onetime_address, + k_view_dev, + account_spend_pubkey, + address_spend_pubkey_out, + enote.enote_ephemeral_pubkey, + nominal_anchor, + dummy_payment_id)) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool try_scan_carrot_enote_external(const CarrotEnoteV1 &enote, + const std::optional encrypted_payment_id, + const mx25519_pubkey &s_sender_receiver_unctx, + const view_incoming_key_device &k_view_dev, + const crypto::public_key &account_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) +{ + // input_context + input_context_t input_context; + make_carrot_input_context(enote.tx_first_key_image, input_context); + + // test view tag + if (!test_carrot_view_tag(s_sender_receiver_unctx.data, input_context, enote.onetime_address, enote.view_tag)) + return false; + + // s^ctx_sr = H_32(s_sr, D_e, input_context) + crypto::hash s_sender_receiver; + make_carrot_sender_receiver_secret(s_sender_receiver_unctx.data, + enote.enote_ephemeral_pubkey, + input_context, + s_sender_receiver); + + // do core scanning + janus_anchor_t nominal_anchor; + if (!try_scan_carrot_non_coinbase_core(enote, + encrypted_payment_id, + s_sender_receiver, + sender_extension_g_out, + sender_extension_t_out, + address_spend_pubkey_out, + amount_out, + amount_blinding_factor_out, + payment_id_out, + enote_type_out, + nominal_anchor)) + return false; + + // verify Janus attack protection + if (!verify_carrot_janus_protection(input_context, + enote.onetime_address, + k_view_dev, + account_spend_pubkey, + address_spend_pubkey_out, + enote.enote_ephemeral_pubkey, + nominal_anchor, + payment_id_out)) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +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, + 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, + CarrotEnoteType &enote_type_out, + janus_anchor_t &internal_message_out) +{ + // input_context + input_context_t input_context; + make_carrot_input_context(enote.tx_first_key_image, input_context); + + // vt = H_3(s_sr || input_context || Ko) + view_tag_t nominal_view_tag; + s_view_balance_dev.make_internal_view_tag(input_context, enote.onetime_address, nominal_view_tag); + + // test view tag + if (nominal_view_tag != enote.view_tag) + return false; + + // s^ctx_sr = H_32(s_vb, D_e, input_context) + crypto::hash s_sender_receiver; + s_view_balance_dev.make_internal_sender_receiver_secret(enote.enote_ephemeral_pubkey, + input_context, + s_sender_receiver); + + // do core scanning + payment_id_t dummy_payment_id; + if (!try_scan_carrot_non_coinbase_core(enote, + std::nullopt, + s_sender_receiver, + sender_extension_g_out, + sender_extension_t_out, + address_spend_pubkey_out, + amount_out, + amount_blinding_factor_out, + dummy_payment_id, + enote_type_out, + internal_message_out)) + return false; + + // janus protection checks are not needed for internal scans + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace carrot diff --git a/src/carrot_core/carrot_enote_scan.h b/src/carrot_core/carrot_enote_scan.h new file mode 100644 index 00000000000..7bc160d23e7 --- /dev/null +++ b/src/carrot_core/carrot_enote_scan.h @@ -0,0 +1,87 @@ +// 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. + +//! @file Utilities for scanning carrot enotes + +#pragma once + +//local headers +#include "carrot_enote_types.h" +#include "device.h" + +//third party headers + +//standard headers +#include + +//forward declarations + + +namespace carrot +{ +bool verify_carrot_janus_protection(const input_context_t &input_context, + const crypto::public_key &onetime_address, + const view_incoming_key_device &k_view_dev, + const crypto::public_key &account_spend_pubkey, + const crypto::public_key &nominal_address_spend_pubkey, + const mx25519_pubkey &enote_ephemeral_pubkey, + const janus_anchor_t &nominal_anchor, + payment_id_t &nominal_payment_id_inout); + +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 &account_spend_pubkey, + crypto::secret_key &sender_extension_g_out, + crypto::secret_key &sender_extension_t_out, + crypto::public_key &address_spend_pubkey_out); + +bool try_scan_carrot_enote_external(const CarrotEnoteV1 &enote, + const std::optional encrypted_payment_id, + const mx25519_pubkey &s_sender_receiver_unctx, + const view_incoming_key_device &k_view_dev, + const crypto::public_key &account_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_scan_carrot_enote_internal(const CarrotEnoteV1 &enote, + const view_balance_secret_device &s_view_balance_dev, + 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, + CarrotEnoteType &enote_type_out, + janus_anchor_t &internal_message_out); + +} //namespace carrot diff --git a/src/carrot_core/carrot_enote_types.cpp b/src/carrot_core/carrot_enote_types.cpp new file mode 100644 index 00000000000..dfc71dc34fb --- /dev/null +++ b/src/carrot_core/carrot_enote_types.cpp @@ -0,0 +1,73 @@ +// 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. + +// Utilities for scanning carrot enotes + +//paired header +#include "carrot_enote_types.h" + +//local headers + +//third party headers + +//standard headers + +/* + onetime address +// - amount commitment +// - encrypted amount +// - encrypted janus anchor +// - view tag +// - ephemeral pubkey +// - tx first key image*/ + +namespace carrot +{ +//------------------------------------------------------------------------------------------------------------------- +bool operator==(const CarrotEnoteV1 &a, const CarrotEnoteV1 &b) +{ + return a.onetime_address == b.onetime_address && + a.amount_commitment == b.amount_commitment && + a.amount_enc == b.amount_enc && + a.anchor_enc == b.anchor_enc && + a.view_tag == b.view_tag && + a.tx_first_key_image == b.tx_first_key_image && + memcmp(a.enote_ephemeral_pubkey.data, b.enote_ephemeral_pubkey.data, sizeof(mx25519_pubkey)) == 0; +} +//------------------------------------------------------------------------------------------------------------------- +bool operator==(const CarrotCoinbaseEnoteV1 &a, const CarrotCoinbaseEnoteV1 &b) +{ + return a.onetime_address == b.onetime_address && + a.amount == b.amount && + a.anchor_enc == b.anchor_enc && + a.view_tag == b.view_tag && + a.block_index == b.block_index && + memcmp(a.enote_ephemeral_pubkey.data, b.enote_ephemeral_pubkey.data, sizeof(mx25519_pubkey)) == 0; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace carrot \ No newline at end of file diff --git a/src/carrot_core/carrot_enote_types.h b/src/carrot_core/carrot_enote_types.h new file mode 100644 index 00000000000..6cfc99b9e18 --- /dev/null +++ b/src/carrot_core/carrot_enote_types.h @@ -0,0 +1,109 @@ +// 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. + +// Seraphis core types. + +#pragma once + +//local headers +#include "core_types.h" +#include "mx25519.h" +#include "ringct/rctTypes.h" + +//third party headers + +//standard headers + +//forward declarations + + +namespace carrot +{ + +//// +// CarrotEnoteV1 +// - onetime address +// - amount commitment +// - encrypted amount +// - encrypted janus anchor +// - view tag +// - ephemeral pubkey +// - tx first key image +/// +struct CarrotEnoteV1 final +{ + /// K_o + crypto::public_key onetime_address; + /// C_a + rct::key amount_commitment; + /// a_enc + encrypted_amount_t amount_enc; + /// anchor_enc + encrypted_janus_anchor_t anchor_enc; + /// view_tag + view_tag_t view_tag; + /// D_e + mx25519_pubkey enote_ephemeral_pubkey; + /// L_0 + crypto::key_image tx_first_key_image; +}; + +/// equality operators +bool operator==(const CarrotEnoteV1 &a, const CarrotEnoteV1 &b); +static inline bool operator!=(const CarrotEnoteV1 &a, const CarrotEnoteV1 &b) { return !(a == b); } + +//// +// CarrotCoinbaseEnoteV1 +// - onetime address +// - cleartext amount +// - encrypted janus anchor +// - view tag +// - ephemeral pubkey +// - block index +/// +struct CarrotCoinbaseEnoteV1 final +{ + /// K_o + crypto::public_key onetime_address; + /// a + rct::xmr_amount amount; + /// anchor_enc + encrypted_janus_anchor_t anchor_enc; + /// view_tag + view_tag_t view_tag; + /// D_e + mx25519_pubkey enote_ephemeral_pubkey; + /// block_index + std::uint64_t block_index; +}; + +/// equality operators +bool operator==(const CarrotCoinbaseEnoteV1 &a, const CarrotCoinbaseEnoteV1 &b); +static inline bool operator!=(const CarrotCoinbaseEnoteV1 &a, const CarrotCoinbaseEnoteV1 &b) { return !(a == b); } + +} //namespace carrot diff --git a/src/carrot_core/config.h b/src/carrot_core/config.h new file mode 100644 index 00000000000..3f999b11522 --- /dev/null +++ b/src/carrot_core/config.h @@ -0,0 +1,73 @@ +// 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. + +//! @file Constants used in Carrot + +#pragma once + +//local headers + +//third party headers + +//standard headers + +//forward declarations + +namespace carrot +{ + +// Carrot addressing protocol domain separators +static constexpr const unsigned char CARROT_DOMAIN_SEP_AMOUNT_BLINDING_FACTOR[] = "Carrot commitment mask"; +static constexpr const unsigned char CARROT_DOMAIN_SEP_ONETIME_EXTENSION_G[] = "Carrot key extension G"; +static constexpr const unsigned char CARROT_DOMAIN_SEP_ONETIME_EXTENSION_T[] = "Carrot key extension T"; +static constexpr const unsigned char CARROT_DOMAIN_SEP_ENCRYPTION_MASK_ANCHOR[] = "Carrot encryption mask anchor"; +static constexpr const unsigned char CARROT_DOMAIN_SEP_ENCRYPTION_MASK_AMOUNT[] = "Carrot encryption mask a"; +static constexpr const unsigned char CARROT_DOMAIN_SEP_ENCRYPTION_MASK_PAYMENT_ID[] = "Carrot encryption mask pid"; +static constexpr const unsigned char CARROT_DOMAIN_SEP_JANUS_ANCHOR_SPECIAL[] = "Carrot janus anchor special"; +static constexpr const unsigned char CARROT_DOMAIN_SEP_EPHEMERAL_PRIVKEY[] = "Carrot sending key normal"; +static constexpr const unsigned char CARROT_DOMAIN_SEP_VIEW_TAG[] = "Carrot view tag"; +static constexpr const unsigned char CARROT_DOMAIN_SEP_SENDER_RECEIVER_SECRET[] = "Carrot sender-receiver secret"; +static constexpr const unsigned char CARROT_DOMAIN_SEP_INPUT_CONTEXT_COINBASE = 'C'; +static constexpr const unsigned char CARROT_DOMAIN_SEP_INPUT_CONTEXT_RINGCT = 'R'; + +// Carrot account secret domain separators +static constexpr const unsigned char CARROT_DOMAIN_SEP_PROVE_SPEND_KEY[] = "Carrot prove-spend key"; +static constexpr const unsigned char CARROT_DOMAIN_SEP_VIEW_BALANCE_SECRET[] = "Carrot view-balance secret"; +static constexpr const unsigned char CARROT_DOMAIN_SEP_GENERATE_IMAGE_KEY[] = "Carrot generate-image key"; +static constexpr const unsigned char CARROT_DOMAIN_SEP_INCOMING_VIEW_KEY[] = "Carrot incoming view key"; +static constexpr const unsigned char CARROT_DOMAIN_SEP_GENERATE_ADDRESS_SECRET[] = "Carrot generate-address secret"; + +// Carrot address domain separators +static constexpr const unsigned char CARROT_DOMAIN_SEP_ADDRESS_INDEX_GEN[] = "Carrot address index generator"; +static constexpr const unsigned char CARROT_DOMAIN_SEP_SUBADDRESS_SCALAR[] = "Carrot subaddress scalar"; + +// Carrot misc constants +static constexpr const unsigned int CARROT_MIN_TX_OUTPUTS = 2; +static constexpr const unsigned int CARROT_MAX_TX_OUTPUTS = 16; +static constexpr const unsigned int CARROT_MAX_TX_INPUTS = 16; +} //namespace carrot diff --git a/src/carrot_core/core_types.cpp b/src/carrot_core/core_types.cpp new file mode 100644 index 00000000000..137b6a952d3 --- /dev/null +++ b/src/carrot_core/core_types.cpp @@ -0,0 +1,137 @@ +// 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 "core_types.h" + +//local headers +#include "crypto/crypto.h" +extern "C" +{ +#include "crypto/crypto-ops.h" +} + +//third party headers +#include + +//standard headers + +namespace carrot +{ +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +template +static void xor_bytes(const unsigned char(&a)[Sz], const unsigned char(&b)[Sz], unsigned char(&c_out)[Sz]) +{ + for (std::size_t i{0}; i < Sz; ++i) + c_out[i] = a[i] ^ b[i]; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +template +static T xor_bytes(const T &a, const T &b) +{ + T temp; + xor_bytes(a.bytes, b.bytes, temp.bytes); + return temp; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +bool operator==(const janus_anchor_t &a, const janus_anchor_t &b) +{ + return memcmp(&a, &b, sizeof(janus_anchor_t)) == 0; +} +//------------------------------------------------------------------------------------------------------------------- +janus_anchor_t operator^(const janus_anchor_t &a, const janus_anchor_t &b) +{ + return xor_bytes(a, b); +} +//------------------------------------------------------------------------------------------------------------------- +bool operator==(const encrypted_amount_t &a, const encrypted_amount_t &b) +{ + return memcmp(&a, &b, sizeof(encrypted_amount_t)) == 0; +} +//------------------------------------------------------------------------------------------------------------------- +encrypted_amount_t operator^(const encrypted_amount_t &a, const encrypted_amount_t &b) +{ + return xor_bytes(a, b); +} +//------------------------------------------------------------------------------------------------------------------- +bool operator==(const payment_id_t &a, const payment_id_t &b) +{ + return memcmp(&a, &b, sizeof(payment_id_t)) == 0; +} +//------------------------------------------------------------------------------------------------------------------- +payment_id_t operator^(const payment_id_t &a, const payment_id_t &b) +{ + return xor_bytes(a, b); +} +//------------------------------------------------------------------------------------------------------------------- +bool operator==(const input_context_t &a, const input_context_t &b) +{ + return memcmp(&a, &b, sizeof(input_context_t)) == 0; +} +//------------------------------------------------------------------------------------------------------------------- +bool operator==(const view_tag_t &a, const view_tag_t &b) +{ + return memcmp(&a, &b, sizeof(view_tag_t)) == 0; +} +//------------------------------------------------------------------------------------------------------------------- +janus_anchor_t gen_janus_anchor() +{ + return crypto::rand(); +} +//------------------------------------------------------------------------------------------------------------------- +payment_id_t gen_payment_id() +{ + return crypto::rand(); +} +//------------------------------------------------------------------------------------------------------------------- +view_tag_t gen_view_tag() +{ + return crypto::rand(); +} +//------------------------------------------------------------------------------------------------------------------- +input_context_t gen_input_context() +{ + return crypto::rand(); +} +//------------------------------------------------------------------------------------------------------------------- +mx25519_pubkey gen_x25519_pubkey() +{ + unsigned char sc64[64]; + crypto::rand(sizeof(sc64), sc64); + sc_reduce(sc64); + ge_p3 P; + ge_scalarmult_base(&P, sc64); + mx25519_pubkey P_x25519; + ge_p3_to_x25519(P_x25519.data, &P); + return P_x25519; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace carrot diff --git a/src/carrot_core/core_types.h b/src/carrot_core/core_types.h new file mode 100644 index 00000000000..98b096201dc --- /dev/null +++ b/src/carrot_core/core_types.h @@ -0,0 +1,133 @@ +// 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. + +//! @file Supporting types for Carrot (anchor, view tag, etc.). + +#pragma once + +//local headers +#include "mx25519.h" + +//third party headers + +//standard headers +#include +#include + +//forward declarations + +namespace carrot +{ + +constexpr std::size_t JANUS_ANCHOR_BYTES{16}; + +/// either encodes randomness the private key of, or an HMAC of, the ephemeral pubkey +struct janus_anchor_t final +{ + unsigned char bytes[JANUS_ANCHOR_BYTES]; +}; + +/// carrot janus anchor XORd with a user-defined secret +using encrypted_janus_anchor_t = janus_anchor_t; + +/// carrot enote types +enum class CarrotEnoteType : unsigned char +{ + PAYMENT = 0, + CHANGE = 1 +}; + +/// carrot encrypted amount +constexpr std::size_t ENCRYPTED_AMOUNT_BYTES{8}; +struct encrypted_amount_t final +{ + unsigned char bytes[ENCRYPTED_AMOUNT_BYTES]; +}; + +/// legacy payment ID +constexpr std::size_t PAYMENT_ID_BYTES{8}; +struct payment_id_t final +{ + unsigned char bytes[PAYMENT_ID_BYTES]; +}; +static constexpr payment_id_t null_payment_id{{0}}; + +/// legacy encrypted payment ID +using encrypted_payment_id_t = payment_id_t; + +/// carrot view tags +constexpr std::size_t VIEW_TAG_BYTES{3}; +struct view_tag_t final +{ + unsigned char bytes[VIEW_TAG_BYTES]; +}; + +static_assert(sizeof(view_tag_t) < 32, "uint8_t cannot index all view tag bits"); + +/// carrot input context +constexpr std::size_t INPUT_CONTEXT_BYTES{1 + 32}; +struct input_context_t final +{ + unsigned char bytes[INPUT_CONTEXT_BYTES]; +}; + +/// overloaded operators: address tag +bool operator==(const janus_anchor_t &a, const janus_anchor_t &b); +static inline bool operator!=(const janus_anchor_t &a, const janus_anchor_t &b) { return !(a == b); } +janus_anchor_t operator^(const janus_anchor_t &a, const janus_anchor_t &b); + +/// overloaded operators: encrypted amount +bool operator==(const encrypted_amount_t &a, const encrypted_amount_t &b); +static inline bool operator!=(const encrypted_amount_t &a, const encrypted_amount_t &b) { return !(a == b); } +encrypted_amount_t operator^(const encrypted_amount_t &a, const encrypted_amount_t &b); + +/// overloaded operators: payment ID +bool operator==(const payment_id_t &a, const payment_id_t &b); +static inline bool operator!=(const payment_id_t &a, const payment_id_t &b) { return !(a == b); } +payment_id_t operator^(const payment_id_t &a, const payment_id_t &b); + +/// overloaded operators: input context +bool operator==(const input_context_t &a, const input_context_t &b); +static inline bool operator!=(const input_context_t &a, const input_context_t &b) { return !(a == b); } + +/// overloaded operators: view tag +bool operator==(const view_tag_t &a, const view_tag_t &b); +static inline bool operator!=(const view_tag_t &a, const view_tag_t &b) { return !(a == b); } + +/// generate a random janus anchor +janus_anchor_t gen_janus_anchor(); +/// generate a random (non-zero) payment ID +payment_id_t gen_payment_id(); +/// generate a random view tag +view_tag_t gen_view_tag(); +/// generate a random input context +input_context_t gen_input_context(); +/// generate a random X25519 pubkey (unclamped) +mx25519_pubkey gen_x25519_pubkey(); + +} //namespace carrot diff --git a/src/carrot_core/destination.cpp b/src/carrot_core/destination.cpp new file mode 100644 index 00000000000..839d5bf2356 --- /dev/null +++ b/src/carrot_core/destination.cpp @@ -0,0 +1,149 @@ +// 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 "destination.h" + +//local headers +#include "address_utils.h" +#include "misc_log_ex.h" +#include "ringct/rctOps.h" + +//third party headers + +//standard headers + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "carrot" + +namespace carrot +{ +//------------------------------------------------------------------------------------------------------------------- +bool operator==(const CarrotDestinationV1 &a, const CarrotDestinationV1 &b) +{ + return a.address_spend_pubkey == b.address_spend_pubkey && + a.address_view_pubkey == b.address_view_pubkey && + a.is_subaddress == b.is_subaddress && + a.payment_id == b.payment_id; +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_main_address_v1(const crypto::public_key &account_spend_pubkey, + const crypto::public_key &primary_address_view_pubkey, + CarrotDestinationV1 &destination_out) +{ + destination_out = CarrotDestinationV1{ + .address_spend_pubkey = account_spend_pubkey, + .address_view_pubkey = primary_address_view_pubkey, + .is_subaddress = false, + .payment_id = null_payment_id + }; +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_subaddress_v1(const crypto::public_key &account_spend_pubkey, + const crypto::public_key &account_view_pubkey, + const crypto::secret_key &s_generate_address, + const std::uint32_t &j_major, + const std::uint32_t &j_minor, + CarrotDestinationV1 &destination_out) +{ + CHECK_AND_ASSERT_THROW_MES(j_major != 0 || j_minor, + "make carrot subaddress v1: j cannot be 0 for a subaddress, only for main addresses"); + + // s^j_gen = H_32[s_ga](j_major, j_minor) + crypto::secret_key address_index_generator; + make_carrot_index_extension_generator(s_generate_address, j_major, j_minor, address_index_generator); + + // k^j_subscal = H_n(K_s, j_major, j_minor, s^j_gen) + crypto::secret_key subaddress_scalar; + make_carrot_subaddress_scalar(account_spend_pubkey, address_index_generator, j_major, j_minor, subaddress_scalar); + + // K^j_s = k^j_subscal * K_s + const rct::key address_spend_pubkey = + rct::scalarmultKey(rct::pk2rct(account_spend_pubkey), rct::sk2rct(subaddress_scalar)); + + // K^j_v = k^j_subscal * K_v + const rct::key address_view_pubkey = + rct::scalarmultKey(rct::pk2rct(account_view_pubkey), rct::sk2rct(subaddress_scalar)); + + destination_out = CarrotDestinationV1{ + .address_spend_pubkey = rct::rct2pk(address_spend_pubkey), + .address_view_pubkey = rct::rct2pk(address_view_pubkey), + .is_subaddress = true, + .payment_id = null_payment_id + }; +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_integrated_address_v1(const crypto::public_key &account_spend_pubkey, + const crypto::public_key &primary_address_view_pubkey, + const payment_id_t payment_id, + CarrotDestinationV1 &destination_out) +{ + destination_out = CarrotDestinationV1{ + .address_spend_pubkey = account_spend_pubkey, + .address_view_pubkey = primary_address_view_pubkey, + .is_subaddress = false, + .payment_id = payment_id + }; +} +//------------------------------------------------------------------------------------------------------------------- +CarrotDestinationV1 gen_carrot_main_address_v1() +{ + return CarrotDestinationV1{ + .address_spend_pubkey = rct::rct2pk(rct::pkGen()), + .address_view_pubkey = rct::rct2pk(rct::pkGen()), + .is_subaddress = false, + .payment_id = null_payment_id + }; +} +//------------------------------------------------------------------------------------------------------------------- +CarrotDestinationV1 gen_carrot_subaddress_v1() +{ + return CarrotDestinationV1{ + .address_spend_pubkey = rct::rct2pk(rct::pkGen()), + .address_view_pubkey = rct::rct2pk(rct::pkGen()), + .is_subaddress = true, + .payment_id = null_payment_id + }; +} +//------------------------------------------------------------------------------------------------------------------- +CarrotDestinationV1 gen_carrot_integrated_address_v1() +{ + // force generate non-zero payment id + payment_id_t payment_id{gen_payment_id()}; + while (payment_id == null_payment_id) + payment_id = gen_payment_id(); + + return CarrotDestinationV1{ + .address_spend_pubkey = rct::rct2pk(rct::pkGen()), + .address_view_pubkey = rct::rct2pk(rct::pkGen()), + .is_subaddress = false, + .payment_id = payment_id + }; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace carrot diff --git a/src/carrot_core/destination.h b/src/carrot_core/destination.h new file mode 100644 index 00000000000..98a5ea407f9 --- /dev/null +++ b/src/carrot_core/destination.h @@ -0,0 +1,116 @@ +// 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. + +// A 'payment proposal' is a proposal to make an enote sending funds to a Carrot address. +// Carrot: Cryptonote Address For Rerandomizable-RingCT-Output Transactions + +#pragma once + +//local headers +#include "core_types.h" +#include "crypto/crypto.h" + +//third party headers + +//standard headers + +//forward declarations + + +namespace carrot +{ + +//// +// CarrotDestinationV1 +// - for creating an output proposal to send an amount to someone +/// +struct CarrotDestinationV1 final +{ + /// K^j_s + crypto::public_key address_spend_pubkey; + /// K^j_v + crypto::public_key address_view_pubkey; + /// is a subaddress? + bool is_subaddress; + /// legacy payment id pid: null for main addresses and subaddresses + payment_id_t payment_id; +}; + +/// equality operators +bool operator==(const CarrotDestinationV1 &a, const CarrotDestinationV1 &b); +static inline bool operator!=(const CarrotDestinationV1 &a, const CarrotDestinationV1 &b) { return !(a == b); } + +/** +* brief: make_carrot_main_address_v1 - make a destination address +* param: account_spend_pubkey - K_s = k_gi X + k_ps T +* param: primary_address_view_pubkey - K^0_v = k_v G +* outparam: destination_out - the full main address +*/ +void make_carrot_main_address_v1(const crypto::public_key &account_spend_pubkey, + const crypto::public_key &primary_address_view_pubkey, + CarrotDestinationV1 &destination_out); +/** +* brief: make_carrot_subaddress_v1 - make a destination address +* param: account_spend_pubkey - K_s = k_gi X + k_ps T +* param: account_view_pubkey - K_v = k_v K_s +* param: s_generate_address - s_ga +* param: j_major - +* param: j_minor - +* outparam: destination_out - the full subaddress +*/ +void make_carrot_subaddress_v1(const crypto::public_key &account_spend_pubkey, + const crypto::public_key &account_view_pubkey, + const crypto::secret_key &s_generate_address, + const std::uint32_t &j_major, + const std::uint32_t &j_minor, + CarrotDestinationV1 &destination_out); +/** +* brief: make_carrot_integrated_address_v1 - make a destination address +* param: account_spend_pubkey - K_s = k_gi X + k_ps T +* param: primary_address_view_pubkey - K^0_v = k_v G +* param: payment_id - pid +* outparam: destination_out - the full main address +*/ +void make_carrot_integrated_address_v1(const crypto::public_key &account_spend_pubkey, + const crypto::public_key &primary_address_view_pubkey, + const payment_id_t payment_id, + CarrotDestinationV1 &destination_out); +/** +* brief: gen_carrot_main_address_v1 - generate a random main address +*/ +CarrotDestinationV1 gen_carrot_main_address_v1(); +/** +* brief: gen_carrot_main_address_v1 - generate a random subaddress +*/ +CarrotDestinationV1 gen_carrot_subaddress_v1(); +/** +* brief: gen_carrot_main_address_v1 - generate a random integrated address +*/ +CarrotDestinationV1 gen_carrot_integrated_address_v1(); + +} //namespace carrot diff --git a/src/carrot_core/device.h b/src/carrot_core/device.h new file mode 100644 index 00000000000..c095fe7323a --- /dev/null +++ b/src/carrot_core/device.h @@ -0,0 +1,165 @@ +// 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. + +//! @file Abstract interfaces for performing scanning without revealing account keys + +#pragma once + +//local headers +#include "core_types.h" +#include "crypto/crypto.h" +#include "mx25519.h" + +//third party headers + +//standard headers +#include +#include +#include + +//forward declarations + + +/** + * These device interfaces were written primarily to be as lean as possible, for ease of the + * implementor, so long as account keys don't leak. As such, the interfaces do not shield + * sender-receiver secrets, and thus temporary access to this device interface can expose + * transaction content permanently in a provable manner. The device interface currently used in + * Monero (hw::device) also exposes transaction content, which can be saved permanently, but it + * wouldn't necessarily be provable. Thus, in the case of a breach, the original user has some + * plausible deniability with (hw::device), which cannot be said of the interfaces in this file. + * It's not impossible to make carrot scanning happen completely on-device, but it is significantly + * more involved. + */ + +namespace carrot +{ +/** + * brief: base exception type for reporting carrot device errors. + * note: devices should only throw this exception or derived classes + */ +struct device_error: public std::runtime_error +{ + /** + * param: dev_make - e.g. "Trezor", "Ledger" + * param: dev_model - e.g. "Model T", "Nano X" + * param: func_called - verbatim device interface method name, e.g. "view_key_scalar_mult_x25519" + * param: msg - arbitrary error message + * param: code - arbitrary error code + */ + device_error(std::string &&dev_make, + std::string &&dev_model, + std::string &&func_called, + std::string &&msg, + const int code) + : std::runtime_error(make_formatted_message(dev_make, dev_model, func_called, msg, code)), + dev_make(dev_make), dev_model(dev_model), func_called(func_called), msg(msg), code(code) + {} + + static std::string make_formatted_message(const std::string &dev_make, + const std::string &dev_model, + const std::string &func_called, + const std::string &msg, + const int code) + { + char buf[384]; + snprintf(buf, sizeof(buf), + "%s %s device error (%d), at %s(): %s", + dev_make.c_str(), dev_model.c_str(), code, func_called.c_str(), msg.c_str()); + return {buf}; + } + + const std::string dev_make; + const std::string dev_model; + const std::string func_called; + const std::string msg; + const int code; +}; + +struct view_incoming_key_device +{ + /** + * brief: view_key_scalar_mult_ed25519 - do an Ed25519 scalar mult against the incoming view key + * param: P - Ed25519 base point + * outparam: kvP = k_v P + * return: true on success, false on failure (e.g. unable to decompress point) + */ + virtual bool view_key_scalar_mult_ed25519(const crypto::public_key &P, + crypto::public_key &kvP) const = 0; + + /** + * brief: view_key_scalar_mult_x25519 - do an X25519 scalar mult and cofactor clear against the incoming view key + * param: D - X25519 base point + * outparam: kvD = k_v D + * return: true on success, false on failure (e.g. unable to decompress point) + */ + virtual bool view_key_scalar_mult_x25519(const mx25519_pubkey &D, + mx25519_pubkey &kvD) const = 0; + + /** + * brief: make_janus_anchor_special - make a janus anchor for "special" enotes + * param: enote_ephemeral_pubkey - D_e + * param: input_context - input_context + * param: account_spend_pubkey - K_s + * outparam: anchor_special_out - anchor_sp = anchor_sp = H_16(D_e, input_context, Ko, k_v, K_s) + */ + virtual void make_janus_anchor_special(const mx25519_pubkey &enote_ephemeral_pubkey, + const input_context_t &input_context, + const crypto::public_key &onetime_address, + const crypto::public_key &account_spend_pubkey, + janus_anchor_t &anchor_special_out) const = 0; + + virtual ~view_incoming_key_device() = default; +}; + +struct view_balance_secret_device +{ + /** + * brief: make_internal_view_tag - make an internal view tag, given non-secret data + * param: input_context - input_context + * param: onetime_address - Ko + * outparam: view_tag_out - vt = H_3(s_vb || input_context || Ko) + */ + virtual void make_internal_view_tag(const input_context_t &input_context, + const crypto::public_key &onetime_address, + view_tag_t &view_tag_out) const = 0; + + /** + * brief: make_internal_sender_receiver_secret - make internal sender-receiver secret, given non-secret data + * param: enote_ephemeral_pubkey - D_e + * param: input_context - input_context + * outparam: s_sender_receiver_out - s_sr = s^ctx_sr = H_32(s_sr, D_e, input_context) + */ + virtual void make_internal_sender_receiver_secret(const mx25519_pubkey &enote_ephemeral_pubkey, + const input_context_t &input_context, + crypto::hash &s_sender_receiver_out) const = 0; + + virtual ~view_balance_secret_device() = default; +}; + +} //namespace carrot diff --git a/src/carrot_core/device_ram_borrowed.cpp b/src/carrot_core/device_ram_borrowed.cpp new file mode 100644 index 00000000000..f749f4587e8 --- /dev/null +++ b/src/carrot_core/device_ram_borrowed.cpp @@ -0,0 +1,90 @@ +// 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. + +//pair header +#include "device_ram_borrowed.h" + +//local headers +#include "enote_utils.h" +#include "ringct/rctOps.h" + +//third party headers + +//standard headers + + +namespace carrot +{ +//------------------------------------------------------------------------------------------------------------------- +bool view_incoming_key_ram_borrowed_device::view_key_scalar_mult_ed25519(const crypto::public_key &P, + crypto::public_key &kvP) const +{ + kvP = rct::rct2pk(rct::scalarmultKey(rct::pk2rct(P), rct::sk2rct(m_k_view_incoming))); + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool view_incoming_key_ram_borrowed_device::view_key_scalar_mult_x25519(const mx25519_pubkey &D, + mx25519_pubkey &kvD) const +{ + return make_carrot_uncontextualized_shared_key_receiver(m_k_view_incoming, D, kvD); +} +//------------------------------------------------------------------------------------------------------------------- +void view_incoming_key_ram_borrowed_device::make_janus_anchor_special( + const mx25519_pubkey &enote_ephemeral_pubkey, + const input_context_t &input_context, + const crypto::public_key &onetime_address, + const crypto::public_key &account_spend_pubkey, + janus_anchor_t &anchor_special_out) const +{ + return make_carrot_janus_anchor_special(enote_ephemeral_pubkey, + input_context, + onetime_address, + m_k_view_incoming, + account_spend_pubkey, + anchor_special_out); +} +//------------------------------------------------------------------------------------------------------------------- +void view_balance_secret_ram_borrowed_device::make_internal_view_tag(const input_context_t &input_context, + const crypto::public_key &onetime_address, + view_tag_t &view_tag_out) const +{ + make_carrot_view_tag(to_bytes(m_s_view_balance), input_context, onetime_address, view_tag_out); +} +//------------------------------------------------------------------------------------------------------------------- +void view_balance_secret_ram_borrowed_device::make_internal_sender_receiver_secret( + const mx25519_pubkey &enote_ephemeral_pubkey, + const input_context_t &input_context, + crypto::hash &s_sender_receiver_out) const +{ + make_carrot_sender_receiver_secret(to_bytes(m_s_view_balance), + enote_ephemeral_pubkey, + input_context, + s_sender_receiver_out); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace carrot diff --git a/src/carrot_core/device_ram_borrowed.h b/src/carrot_core/device_ram_borrowed.h new file mode 100644 index 00000000000..d9337425e25 --- /dev/null +++ b/src/carrot_core/device_ram_borrowed.h @@ -0,0 +1,86 @@ +// 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. + +//! @file Carrot device implementations for in-memory keys & secrets + +#pragma once + +//local headers +#include "device.h" + +//third party headers + +//standard headers + +//forward declarations + + +namespace carrot +{ + +class view_incoming_key_ram_borrowed_device: public view_incoming_key_device +{ +public: + view_incoming_key_ram_borrowed_device(const crypto::secret_key &k_view_incoming): + m_k_view_incoming(k_view_incoming) {} + + bool view_key_scalar_mult_ed25519(const crypto::public_key &P, + crypto::public_key &kvP) const override; + + bool view_key_scalar_mult_x25519(const mx25519_pubkey &D, + mx25519_pubkey &kvD) const override; + + void make_janus_anchor_special(const mx25519_pubkey &enote_ephemeral_pubkey, + const input_context_t &input_context, + const crypto::public_key &onetime_address, + const crypto::public_key &account_spend_pubkey, + janus_anchor_t &anchor_special_out) const override; + +private: + const crypto::secret_key &m_k_view_incoming; +}; + +class view_balance_secret_ram_borrowed_device: public view_balance_secret_device +{ +public: + view_balance_secret_ram_borrowed_device(const crypto::secret_key &s_view_balance): + m_s_view_balance(s_view_balance) {} + + void make_internal_view_tag(const input_context_t &input_context, + const crypto::public_key &onetime_address, + view_tag_t &view_tag_out) const override; + + void make_internal_sender_receiver_secret(const mx25519_pubkey &enote_ephemeral_pubkey, + const input_context_t &input_context, + crypto::hash &s_sender_receiver_out) const override; + +private: + const crypto::secret_key &m_s_view_balance; +}; + +} //namespace carrot diff --git a/src/carrot_core/enote_utils.cpp b/src/carrot_core/enote_utils.cpp new file mode 100644 index 00000000000..e8b3e3d1383 --- /dev/null +++ b/src/carrot_core/enote_utils.cpp @@ -0,0 +1,499 @@ +// 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 "enote_utils.h" + +//local headers +#include "config.h" +extern "C" +{ +#include "crypto/crypto-ops.h" +} +#include "crypto/generators.h" +#include "crypto/wallet/crypto.h" +#include "hash_functions.h" +#include "int-util.h" +#include "misc_language.h" +#include "ringct/rctOps.h" +#include "transcript_fixed.h" + +//third party headers + +//standard headers +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "carrot" + +namespace carrot +{ +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static const mx25519_impl* get_mx25519_impl() +{ + static std::once_flag of; + static const mx25519_impl *impl; + std::call_once(of, [&](){ impl = mx25519_select_impl(MX25519_TYPE_AUTO); }); + if (impl == nullptr) + throw std::runtime_error("failed to obtain a mx25519 implementation"); + return impl; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static encrypted_amount_t enc_amount(const rct::xmr_amount amount, const encrypted_amount_t &mask) +{ + static_assert(sizeof(rct::xmr_amount) == sizeof(encrypted_amount_t), ""); + + // little_endian(amount) XOR H_8(q, Ko) + encrypted_amount_t amount_LE; + memcpy_swap64le(amount_LE.bytes, &amount, 1); + return amount_LE ^ mask; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static rct::xmr_amount dec_amount(const encrypted_amount_t &encrypted_amount, const encrypted_amount_t &mask) +{ + static_assert(sizeof(rct::xmr_amount) == sizeof(encrypted_amount_t), ""); + + // system_endian(encrypted_amount XOR H_8(q, Ko)) + const encrypted_amount_t decryptd_amount{encrypted_amount ^ mask}; + rct::xmr_amount amount; + memcpy_swap64le(&amount, &decryptd_amount, 1); + return amount; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_enote_ephemeral_privkey(const janus_anchor_t &anchor_norm, + const input_context_t &input_context, + const crypto::public_key &address_spend_pubkey, + const crypto::public_key &address_view_pubkey, + const payment_id_t payment_id, + crypto::secret_key &enote_ephemeral_privkey_out) +{ + // k_e = (H_64(anchor_norm, input_context, K^j_s, K^j_v, pid)) mod l + const auto transcript = sp::make_fixed_transcript( + anchor_norm, input_context, address_spend_pubkey, address_view_pubkey, payment_id); + derive_scalar(transcript.data(), transcript.size(), nullptr, &enote_ephemeral_privkey_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_enote_ephemeral_pubkey_cryptonote(const crypto::secret_key &enote_ephemeral_privkey, + mx25519_pubkey &enote_ephemeral_pubkey_out) +{ + // D_e = d_e G + mx25519_scmul_base(get_mx25519_impl(), + &enote_ephemeral_pubkey_out, + reinterpret_cast(&enote_ephemeral_privkey)); +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_enote_ephemeral_pubkey_subaddress(const crypto::secret_key &enote_ephemeral_privkey, + const crypto::public_key &address_spend_pubkey, + mx25519_pubkey &enote_ephemeral_pubkey_out) +{ + // deserialize K^j_s + ge_p3 address_spend_pubkey_p3; + ge_frombytes_vartime(&address_spend_pubkey_p3, to_bytes(address_spend_pubkey)); + + // K_e = d_e K^j_s + ge_p3 D_e_in_ed25519; + ge_scalarmult_p3(&D_e_in_ed25519, to_bytes(enote_ephemeral_privkey), &address_spend_pubkey_p3); + + // D_e = ConvertPointE(K_e) + ge_p3_to_x25519(enote_ephemeral_pubkey_out.data, &D_e_in_ed25519); +} +//------------------------------------------------------------------------------------------------------------------- +bool make_carrot_uncontextualized_shared_key_receiver(const crypto::secret_key &k_view, + const mx25519_pubkey &enote_ephemeral_pubkey, + mx25519_pubkey &s_sender_receiver_unctx_out) +{ + // s_sr = k_v D_e + mx25519_scmul_key(get_mx25519_impl(), + &s_sender_receiver_unctx_out, + reinterpret_cast(&k_view), + &enote_ephemeral_pubkey); + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool make_carrot_uncontextualized_shared_key_sender(const crypto::secret_key &enote_ephemeral_privkey, + const crypto::public_key &address_view_pubkey, + mx25519_pubkey &s_sender_receiver_unctx_out) +{ + // if K^j_v not in prime order subgroup, then FAIL + ge_p3 address_view_pubkey_p3; + if (!rct::toPointCheckOrder(&address_view_pubkey_p3, to_bytes(address_view_pubkey))) + return false; + + // D^j_v = ConvertPointE(K^j_v) + mx25519_pubkey address_view_pubkey_x25519; + ge_p3_to_x25519(address_view_pubkey_x25519.data, &address_view_pubkey_p3); + + // s_sr = d_e D^j_v + mx25519_scmul_key(get_mx25519_impl(), + &s_sender_receiver_unctx_out, + reinterpret_cast(&enote_ephemeral_privkey), + &address_view_pubkey_x25519); + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_view_tag(const unsigned char s_sender_receiver_unctx[32], + const input_context_t &input_context, + const crypto::public_key &onetime_address, + view_tag_t &view_tag_out) +{ + // vt = H_3(s_sr || input_context || Ko) + const auto transcript = sp::make_fixed_transcript(input_context, onetime_address); + derive_bytes_3(transcript.data(), transcript.size(), s_sender_receiver_unctx, &view_tag_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_input_context_coinbase(const std::uint64_t block_index, input_context_t &input_context_out) +{ + // input_context = "C" || IntToBytes256(block_index) + memset(input_context_out.bytes, 0, sizeof(input_context_t)); + input_context_out.bytes[0] = CARROT_DOMAIN_SEP_INPUT_CONTEXT_COINBASE; + memcpy_swap64le(input_context_out.bytes + 1, &block_index, 1); +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_input_context(const crypto::key_image &first_rct_key_image, input_context_t &input_context_out) +{ + // input_context = "R" || KI_1 + input_context_out.bytes[0] = CARROT_DOMAIN_SEP_INPUT_CONTEXT_RINGCT; + memcpy(input_context_out.bytes + 1, first_rct_key_image.data, sizeof(crypto::key_image)); +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_sender_receiver_secret(const unsigned char s_sender_receiver_unctx[32], + const mx25519_pubkey &enote_ephemeral_pubkey, + const input_context_t &input_context, + crypto::hash &s_sender_receiver_out) +{ + // s^ctx_sr = H_32(s_sr, D_e, input_context) + const auto transcript = sp::make_fixed_transcript( + enote_ephemeral_pubkey, input_context); + derive_bytes_32(transcript.data(), transcript.size(), s_sender_receiver_unctx, &s_sender_receiver_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_onetime_address_extension_g(const crypto::hash &s_sender_receiver, + const rct::key &amount_commitment, + crypto::secret_key &sender_extension_out) +{ + // k^o_g = H_n("..g..", s^ctx_sr, C_a) + const auto transcript = sp::make_fixed_transcript(amount_commitment); + derive_scalar(transcript.data(), transcript.size(), &s_sender_receiver, &sender_extension_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_onetime_address_extension_t(const crypto::hash &s_sender_receiver, + const rct::key &amount_commitment, + crypto::secret_key &sender_extension_out) +{ + // k^o_t = H_n("..t..", s^ctx_sr, C_a) + const auto transcript = sp::make_fixed_transcript(amount_commitment); + derive_scalar(transcript.data(), transcript.size(), &s_sender_receiver, &sender_extension_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_onetime_address_extension_pubkey(const crypto::hash &s_sender_receiver, + const rct::key &amount_commitment, + crypto::public_key &sender_extension_pubkey_out) +{ + // k^o_g = H_n("..g..", s^ctx_sr, C_a) + crypto::secret_key sender_extension_g; + make_carrot_onetime_address_extension_g(s_sender_receiver, amount_commitment, sender_extension_g); + + // k^o_t = H_n("..t..", s^ctx_sr, C_a) + crypto::secret_key sender_extension_t; + make_carrot_onetime_address_extension_t(s_sender_receiver, amount_commitment, sender_extension_t); + + // K^o_ext = k^o_g G + k^o_t T + rct::key sender_extension_pubkey_tmp; + rct::addKeys2(sender_extension_pubkey_tmp, + rct::sk2rct(sender_extension_g), + rct::sk2rct(sender_extension_t), + rct::pk2rct(crypto::get_T())); + + sender_extension_pubkey_out = rct::rct2pk(sender_extension_pubkey_tmp); +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_onetime_address(const crypto::public_key &address_spend_pubkey, + const crypto::hash &s_sender_receiver, + const rct::key &amount_commitment, + crypto::public_key &onetime_address_out) +{ + // K^o_ext = k^o_g G + k^o_t T + crypto::public_key sender_extension_pubkey; + make_carrot_onetime_address_extension_pubkey(s_sender_receiver, amount_commitment, sender_extension_pubkey); + + // Ko = K^j_s + K^o_ext + onetime_address_out = rct::rct2pk(rct::addKeys( + rct::pk2rct(address_spend_pubkey), rct::pk2rct(sender_extension_pubkey))); +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_amount_blinding_factor(const crypto::hash &s_sender_receiver, + const rct::xmr_amount amount, + const crypto::public_key &address_spend_pubkey, + const CarrotEnoteType enote_type, + crypto::secret_key &amount_blinding_factor_out) +{ + // k_a = H_n(s^ctx_sr, a, K^j_s, enote_type) + const auto transcript = sp::make_fixed_transcript( + amount, address_spend_pubkey, static_cast(enote_type)); + derive_scalar(transcript.data(), transcript.size(), &s_sender_receiver, &amount_blinding_factor_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_anchor_encryption_mask(const crypto::hash &s_sender_receiver, + const crypto::public_key &onetime_address, + encrypted_janus_anchor_t &anchor_encryption_mask_out) +{ + // m_anchor = H_16(s^ctx_sr, Ko) + const auto transcript = sp::make_fixed_transcript(onetime_address); + derive_bytes_16(transcript.data(), transcript.size(), &s_sender_receiver, &anchor_encryption_mask_out); +} +//------------------------------------------------------------------------------------------------------------------- +encrypted_janus_anchor_t encrypt_carrot_anchor(const janus_anchor_t &anchor, + const crypto::hash &s_sender_receiver, + const crypto::public_key &onetime_address) +{ + // m_anchor = H_16(s^ctx_sr, Ko) + encrypted_janus_anchor_t mask; + make_carrot_anchor_encryption_mask(s_sender_receiver, onetime_address, mask); + + // anchor_enc = anchor XOR m_anchor + return anchor ^ mask; +} +//------------------------------------------------------------------------------------------------------------------- +janus_anchor_t decrypt_carrot_anchor(const encrypted_janus_anchor_t &encrypted_anchor, + const crypto::hash &s_sender_receiver, + const crypto::public_key &onetime_address) +{ + // m_anchor = H_16(s^ctx_sr, Ko) + encrypted_janus_anchor_t mask; + make_carrot_anchor_encryption_mask(s_sender_receiver, onetime_address, mask); + + // anchor = anchor_enc XOR m_anchor + return encrypted_anchor ^ mask; +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_amount_encryption_mask(const crypto::hash &s_sender_receiver, + const crypto::public_key &onetime_address, + encrypted_amount_t &amount_encryption_mask_out) +{ + // m_a = H_8(s^ctx_sr, Ko) + const auto transcript = sp::make_fixed_transcript(onetime_address); + derive_bytes_8(transcript.data(), transcript.size(), &s_sender_receiver, &amount_encryption_mask_out); +} +//------------------------------------------------------------------------------------------------------------------- +encrypted_amount_t encrypt_carrot_amount(const rct::xmr_amount amount, + const crypto::hash &s_sender_receiver, + const crypto::public_key &onetime_address) +{ + // m_a = H_8(s^ctx_sr, Ko) + encrypted_amount_t mask; + make_carrot_amount_encryption_mask(s_sender_receiver, onetime_address, mask); + + // a_enc = a XOR m_a [paying attention to system endianness] + return enc_amount(amount, mask); +} +//------------------------------------------------------------------------------------------------------------------- +rct::xmr_amount decrypt_carrot_amount(const encrypted_amount_t encrypted_amount, + const crypto::hash &s_sender_receiver, + const crypto::public_key &onetime_address) +{ + // m_a = H_8(s^ctx_sr, Ko) + encrypted_amount_t mask; + make_carrot_amount_encryption_mask(s_sender_receiver, onetime_address, mask); + + // a = a_enc XOR m_a [paying attention to system endianness] + return dec_amount(encrypted_amount, mask); +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_payment_id_encryption_mask(const crypto::hash &s_sender_receiver, + const crypto::public_key &onetime_address, + encrypted_payment_id_t &payment_id_encryption_mask_out) +{ + // m_pid = H_8(s^ctx_sr, Ko) + const auto transcript = sp::make_fixed_transcript(onetime_address); + derive_bytes_8(transcript.data(), transcript.size(), &s_sender_receiver, &payment_id_encryption_mask_out); +} +//------------------------------------------------------------------------------------------------------------------- +encrypted_payment_id_t encrypt_legacy_payment_id(const payment_id_t payment_id, + const crypto::hash &s_sender_receiver, + const crypto::public_key &onetime_address) +{ + // m_pid = H_8(s^ctx_sr, Ko) + encrypted_payment_id_t mask; + make_carrot_payment_id_encryption_mask(s_sender_receiver, onetime_address, mask); + + // pid_enc = pid XOR m_pid + return payment_id ^ mask; +} +//------------------------------------------------------------------------------------------------------------------- +payment_id_t decrypt_legacy_payment_id(const encrypted_payment_id_t encrypted_payment_id, + const crypto::hash &s_sender_receiver, + const crypto::public_key &onetime_address) +{ + // m_pid = H_8(s^ctx_sr, Ko) + encrypted_payment_id_t mask; + make_carrot_payment_id_encryption_mask(s_sender_receiver, onetime_address, mask); + + // pid = pid_enc XOR m_pid + return encrypted_payment_id ^ mask; +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_janus_anchor_special(const mx25519_pubkey &enote_ephemeral_pubkey, + const input_context_t &input_context, + const crypto::public_key &onetime_address, + const crypto::secret_key &k_view, + const crypto::public_key &account_spend_pubkey, + janus_anchor_t &anchor_special_out) +{ + // anchor_sp = H_16(D_e, input_context, Ko, k_v, K_s) + const auto transcript = sp::make_fixed_transcript( + enote_ephemeral_pubkey, input_context, account_spend_pubkey); + derive_bytes_16(transcript.data(), transcript.size(), &k_view, &anchor_special_out); +} +//------------------------------------------------------------------------------------------------------------------- +void recover_address_spend_pubkey(const crypto::public_key &onetime_address, + const crypto::hash &s_sender_receiver, + const rct::key &amount_commitment, + crypto::public_key &address_spend_key_out) +{ + // K^o_ext = k^o_g G + k^o_t T + crypto::public_key sender_extension_pubkey; + make_carrot_onetime_address_extension_pubkey(s_sender_receiver, amount_commitment, sender_extension_pubkey); + + // K^j_s = Ko - K^o_ext + rct::key res_tmp; + rct::subKeys(res_tmp, rct::pk2rct(onetime_address), rct::pk2rct(sender_extension_pubkey)); + address_spend_key_out = rct::rct2pk(res_tmp); +} +//------------------------------------------------------------------------------------------------------------------- +bool test_carrot_view_tag(const unsigned char s_sender_receiver_unctx[32], + const input_context_t input_context, + const crypto::public_key &onetime_address, + const view_tag_t view_tag) +{ + // vt' = H_3(s_sr || input_context || Ko) + view_tag_t nominal_view_tag; + make_carrot_view_tag(s_sender_receiver_unctx, input_context, onetime_address, nominal_view_tag); + + // vt' ?= vt + return nominal_view_tag == view_tag; +} +//------------------------------------------------------------------------------------------------------------------- +bool try_recompute_carrot_amount_commitment(const crypto::hash &s_sender_receiver, + const rct::xmr_amount nominal_amount, + const crypto::public_key &nominal_address_spend_pubkey, + const CarrotEnoteType nominal_enote_type, + const rct::key &amount_commitment, + crypto::secret_key &amount_blinding_factor_out) +{ + // k_a' = H_n(s^ctx_sr, a', K^j_s', enote_type') + make_carrot_amount_blinding_factor(s_sender_receiver, + nominal_amount, + nominal_address_spend_pubkey, + nominal_enote_type, + amount_blinding_factor_out); + + // C_a' = k_a' G + a' H + const rct::key nominal_amount_commitment = rct::commit(nominal_amount, rct::sk2rct(amount_blinding_factor_out)); + + // C_a' ?= C_a + return nominal_amount_commitment == amount_commitment; +} +//------------------------------------------------------------------------------------------------------------------- +bool try_get_carrot_amount(const crypto::hash &s_sender_receiver, + const encrypted_amount_t &encrypted_amount, + const crypto::public_key &onetime_address, + const crypto::public_key &address_spend_pubkey, + const rct::key &amount_commitment, + CarrotEnoteType &enote_type_out, + rct::xmr_amount &amount_out, + crypto::secret_key &amount_blinding_factor_out) +{ + // a' = a_enc XOR m_a + amount_out = decrypt_carrot_amount(encrypted_amount, s_sender_receiver, onetime_address); + + // set enote_type <- "payment" + enote_type_out = CarrotEnoteType::PAYMENT; + + // if C_a ?= k_a' G + a' H, then PASS + if (try_recompute_carrot_amount_commitment(s_sender_receiver, + amount_out, + address_spend_pubkey, + enote_type_out, + amount_commitment, + amount_blinding_factor_out)) + return true; + + // set enote_type <- "change" + enote_type_out = CarrotEnoteType::CHANGE; + + // if C_a ?= k_a' G + a' H, then PASS + if (try_recompute_carrot_amount_commitment(s_sender_receiver, + amount_out, + address_spend_pubkey, + enote_type_out, + amount_commitment, + amount_blinding_factor_out)) + return true; + + // neither attempt at recomputing passed: so FAIL + return false; +} +//------------------------------------------------------------------------------------------------------------------- +bool verify_carrot_external_janus_protection(const janus_anchor_t &nominal_anchor, + const input_context_t &input_context, + const crypto::public_key &nominal_address_spend_pubkey, + const crypto::public_key &nominal_address_view_pubkey, + const bool is_subaddress, + const payment_id_t nominal_payment_id, + const mx25519_pubkey &enote_ephemeral_pubkey) +{ + // d_e' = H_n(anchor_norm, input_context, K^j_s, K^j_v, pid)) + crypto::secret_key nominal_enote_ephemeral_privkey; + make_carrot_enote_ephemeral_privkey(nominal_anchor, + input_context, + nominal_address_spend_pubkey, + nominal_address_view_pubkey, + nominal_payment_id, + nominal_enote_ephemeral_privkey); + + // recompute D_e' for d_e' and address type + mx25519_pubkey nominal_enote_ephemeral_pubkey; + if (is_subaddress) + make_carrot_enote_ephemeral_pubkey_subaddress(nominal_enote_ephemeral_privkey, + nominal_address_spend_pubkey, + nominal_enote_ephemeral_pubkey); + else // cryptonote address + make_carrot_enote_ephemeral_pubkey_cryptonote(nominal_enote_ephemeral_privkey, + nominal_enote_ephemeral_pubkey); + + // D_e' ?= D_e + return 0 == memcmp(&nominal_enote_ephemeral_pubkey, &enote_ephemeral_pubkey, sizeof(mx25519_pubkey)); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace carrot diff --git a/src/carrot_core/enote_utils.h b/src/carrot_core/enote_utils.h new file mode 100644 index 00000000000..fb9747422b8 --- /dev/null +++ b/src/carrot_core/enote_utils.h @@ -0,0 +1,392 @@ +// 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. + +// @file Utilities for making and handling enotes with carrot. + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "core_types.h" +#include "mx25519.h" +#include "ringct/rctTypes.h" + +//third party headers + +//standard headers + +//forward declarations + +namespace carrot +{ + +/** + * brief: make_carrot_enote_ephemeral_privkey - enote ephemeral privkey k_e for Carrot enotes + * d_e = H_n(anchor_norm, input_context, K^j_s, K^j_v, pid)) + * param: anchor_norm - anchor_norm + * param: input_context - input_context + * param: address_spend_pubkey - K^j_s + * param: address_view_pubkey - K^j_v + * param: payment_id - pid + * outparam: enote_ephemeral_privkey_out - k_e + */ +void make_carrot_enote_ephemeral_privkey(const janus_anchor_t &anchor_norm, + const input_context_t &input_context, + const crypto::public_key &address_spend_pubkey, + const crypto::public_key &address_view_pubkey, + const payment_id_t payment_id, + crypto::secret_key &enote_ephemeral_privkey_out); +/** + * brief: make_carrot_enote_ephemeral_pubkey_main - make enote ephemeral pubkey D_e for a main address + * D_e = d_e B + * param: enote_ephemeral_privkey - d_e + * outparam: enote_ephemeral_pubkey_out - D_e + */ +void make_carrot_enote_ephemeral_pubkey_cryptonote(const crypto::secret_key &enote_ephemeral_privkey, + mx25519_pubkey &enote_ephemeral_pubkey_out); +/** + * brief: make_carrot_enote_ephemeral_pubkey_subaddress - make enote ephemeral pubkey D_e for a subaddress + * D_e = d_e ConvertPointE(K^j_s) + * param: enote_ephemeral_privkey - d_e + * param: address_spend_pubkey - K^j_s + * outparam: enote_ephemeral_pubkey_out - D_e + */ +void make_carrot_enote_ephemeral_pubkey_subaddress(const crypto::secret_key &enote_ephemeral_privkey, + const crypto::public_key &address_spend_pubkey, + mx25519_pubkey &enote_ephemeral_pubkey_out); +/** + * brief: make_carrot_uncontextualized_shared_key_receiver - perform the receiver-side ECDH exchange for Carrot enotes + * s_sr = k_v D_e + * param: k_view - k_v + * param: enote_ephemeral_pubkey - D_e + * outparam: s_sender_receiver_unctx_out - s_sr + * return: true if successful, false if a failure occurred in point decompression + */ +bool make_carrot_uncontextualized_shared_key_receiver(const crypto::secret_key &k_view, + const mx25519_pubkey &enote_ephemeral_pubkey, + mx25519_pubkey &s_sender_receiver_unctx_out); +/** + * brief: make_carrot_uncontextualized_shared_key_sender - perform the sender-side ECDH exchange for Carrot enotes + * s_sr = d_e ConvertPointE(K^j_v) + * param: enote_ephemeral_privkey - d_e + * param: address_view_pubkey - K^j_v + * outparam: s_sender_receiver_unctx_out - s_sr + * return: true if successful, false if a failure occurred in point decompression + */ +bool make_carrot_uncontextualized_shared_key_sender(const crypto::secret_key &enote_ephemeral_privkey, + const crypto::public_key &address_view_pubkey, + mx25519_pubkey &s_sender_receiver_unctx_out); +/** +* brief: make_carrot_view_tag - used for optimized identification of enotes +* vt = H_3(s_sr || input_context || Ko) +* param: s_sender_receiver_unctx - s_sr +* param: input_context - input_context +* param: onetime_address - Ko +* outparam: view_tag_out - vt +*/ +void make_carrot_view_tag(const unsigned char s_sender_receiver_unctx[32], + const input_context_t &input_context, + const crypto::public_key &onetime_address, + view_tag_t &view_tag_out); +/** +* brief: make_carrot_input_context_coinbase - input context for a sender-receiver secret (coinbase txs) +* input_context = "C" || IntToBytes256(block_index) +* param: block_index - block index of the coinbase tx +* outparam: input_context_out - "C" || IntToBytes256(block_index) +*/ +void make_carrot_input_context_coinbase(const std::uint64_t block_index, input_context_t &input_context_out); +/** +* brief: make_carrot_input_context - input context for a sender-receiver secret (standard RingCT txs) +* input_context = "R" || KI_1 +* param: first_rct_key_image - KI_1, the first spent RingCT key image in a tx +* outparam: input_context_out - "S" || KI_1 +*/ +void make_carrot_input_context(const crypto::key_image &first_rct_key_image, input_context_t &input_context_out); +/** +* brief: make_carrot_sender_receiver_secret - contextualized sender-receiver secret s^ctx_sr +* s^ctx_sr = H_32(s_sr, D_e, input_context) +* param: s_sender_receiver_unctx - s_sr +* param: enote_ephemeral_pubkey - D_e +* param: input_context - [standard: KI_1] [coinbase: block index] +* outparam: s_sender_receiver_out - s^ctx_sr +* - note: this is 'crypto::hash' instead of 'crypto::secret_key' for better performance in multithreaded environments +*/ +void make_carrot_sender_receiver_secret(const unsigned char s_sender_receiver_unctx[32], + const mx25519_pubkey &enote_ephemeral_pubkey, + const input_context_t &input_context, + crypto::hash &s_sender_receiver_out); +/** +* brief: make_carrot_onetime_address_extension_g - extension for transforming a receiver's spendkey into an +* enote one-time address +* k^o_g = H_n("..g..", s^ctx_sr, C_a) +* param: s_sender_receiver - s^ctx_sr +* param: amount_commitment - C_a +* outparam: sender_extension_out - k^o_g +*/ +void make_carrot_onetime_address_extension_g(const crypto::hash &s_sender_receiver, + const rct::key &amount_commitment, + crypto::secret_key &sender_extension_out); +/** +* brief: make_carrot_onetime_address_extension_t - extension for transforming a receiver's spendkey into an +* enote one-time address +* k^o_t = H_n("..t..", s^ctx_sr, C_a) +* param: s_sender_receiver - s^ctx_sr +* param: amount_commitment - C_a +* outparam: sender_extension_out - k^o_t +*/ +void make_carrot_onetime_address_extension_t(const crypto::hash &s_sender_receiver, + const rct::key &amount_commitment, + crypto::secret_key &sender_extension_out); +/** +* brief: make_carrot_onetime_address_extension_pubkey - create a FCMP++ onetime address extension pubkey +* K^o_ext = k^o_g G + k^o_t T +* param: s_sender_receiver - s^ctx_sr +* param: amount_commitment - C_a +* outparam: sender_extension_pubkey_out - K^o_ext +*/ +void make_carrot_onetime_address_extension_pubkey(const crypto::hash &s_sender_receiver, + const rct::key &amount_commitment, + crypto::public_key &sender_extension_pubkey_out); +/** +* brief: make_carrot_onetime_address - create a FCMP++ onetime address +* Ko = K^j_s + K^o_ext = K^j_s + (k^o_g G + k^o_t T) +* param: address_spend_pubkey - K^j_s +* param: s_sender_receiver - s^ctx_sr +* param: amount_commitment - C_a +* outparam: onetime_address_out - Ko +*/ +void make_carrot_onetime_address(const crypto::public_key &address_spend_pubkey, + const crypto::hash &s_sender_receiver, + const rct::key &amount_commitment, + crypto::public_key &onetime_address_out); +/** +* brief: make_carrot_amount_blinding_factor - create blinding factor for enote's amount commitment C_a +* k_a = H_n(s^ctx_sr, a, K^j_s, enote_type) +* param: s_sender_receiver - s^ctx_sr +* param: amount - a +* param: address_spend_pubkey - K^j_s +* param: enote_type - enote_type +* outparam: amount_blinding_factor_out - k_a +*/ +void make_carrot_amount_blinding_factor(const crypto::hash &s_sender_receiver, + const rct::xmr_amount amount, + const crypto::public_key &address_spend_pubkey, + const CarrotEnoteType enote_type, + crypto::secret_key &amount_blinding_factor_out); +/** +* brief: make_carrot_anchor_encryption_mask - create XOR encryption mask for enote's anchor +* m_anchor = H_16(s^ctx_sr, Ko) +* param: s_sender_receiver - s^ctx_sr +* param: onetime_address - Ko +* outparam: anchor_encryption_mask_out - m_anchor +*/ +void make_carrot_anchor_encryption_mask(const crypto::hash &s_sender_receiver, + const crypto::public_key &onetime_address, + encrypted_janus_anchor_t &anchor_encryption_mask_out); +/** +* brief: encrypt_carrot_anchor - encrypt a Janus anchor for an enote +* anchor_enc = anchor XOR m_anchor +* param: anchor - +* param: s_sender_receiver - s^ctx_sr +* param: onetime_address - Ko +* return: anchor_enc +*/ +encrypted_janus_anchor_t encrypt_carrot_anchor(const janus_anchor_t &anchor, + const crypto::hash &s_sender_receiver, + const crypto::public_key &onetime_address); +/** +* brief: decrypt_carrot_address_tag - decrypt a Janus anchor from an enote +* anchor = anchor_enc XOR m_anchor +* param: encrypted_anchor - anchor_enc +* param: s_sender_receiver - s^ctx_sr +* param: onetime_address - Ko +* return: anchor +*/ +janus_anchor_t decrypt_carrot_anchor(const encrypted_janus_anchor_t &encrypted_anchor, + const crypto::hash &s_sender_receiver, + const crypto::public_key &onetime_address); +/** +* brief: make_carrot_amount_encryption_mask - create XOR encryption mask for enote's amount +* m_a = H_8(s^ctx_sr, Ko) +* param: s_sender_receiver - s^ctx_sr +* param: onetime_address - Ko +* outparam: amount_encryption_mask_out - m_a +*/ +void make_carrot_amount_encryption_mask(const crypto::hash &s_sender_receiver, + const crypto::public_key &onetime_address, + encrypted_amount_t &amount_encryption_mask_out); +/** +* brief: encrypt_carrot_amount - encrypt an amount for an enote +* a_enc = a XOR m_a +* param: amount - a +* param: s_sender_receiver - s^ctx_sr +* param: onetime_address - Ko +* return: a_enc +*/ +encrypted_amount_t encrypt_carrot_amount(const rct::xmr_amount amount, + const crypto::hash &s_sender_receiver, + const crypto::public_key &onetime_address); +/** +* brief: decrypt_carrot_amount - decrypt an amount from an enote +* a = a_enc XOR m_a +* param: encrypted_amount - a_enc +* param: s_sender_receiver - s^ctx_sr +* param: onetime_address - Ko +* return: a +*/ +rct::xmr_amount decrypt_carrot_amount(const encrypted_amount_t encrypted_amount, + const crypto::hash &s_sender_receiver, + const crypto::public_key &onetime_address); +/** +* brief: make_carrot_payment_id_encryption_mask - create XOR encryption mask for enote's payment ID +* m_pid = H_8(s^ctx_sr, Ko) +* param: s_sender_receiver - s^ctx_sr +* param: onetime_address - Ko +* outparam: payment_id_encryption_mask_out - m_pid +*/ +void make_carrot_payment_id_encryption_mask(const crypto::hash &s_sender_receiver, + const crypto::public_key &onetime_address, + encrypted_payment_id_t &payment_id_encryption_mask_out); +/** +* brief: encrypt_legacy_payment_id - encrypt a payment ID from an enote +* pid_enc = pid XOR m_pid +* param: payment_id - pid +* param: s_sender_receiver - s^ctx_sr +* param: onetime_address - Ko +* return: pid_enc +*/ +encrypted_payment_id_t encrypt_legacy_payment_id(const payment_id_t payment_id, + const crypto::hash &s_sender_receiver, + const crypto::public_key &onetime_address); +/** +* brief: decrypt_legacy_payment_id - decrypt a payment ID from an enote +* pid = pid_enc XOR m_pid +* param: encrypted_payment_id - pid_enc +* param: s_sender_receiver - s^ctx_sr +* param: onetime_address - Ko +* return: pid +*/ +payment_id_t decrypt_legacy_payment_id(const encrypted_payment_id_t encrypted_payment_id, + const crypto::hash &s_sender_receiver, + const crypto::public_key &onetime_address); +/** + * brief: make_carrot_janus_anchor_special - make a janus anchor for "special" enotes + * anchor_sp = H_16(D_e, input_context, Ko, k_v, K_s) + * param: enote_ephemeral_pubkey - D_e + * param: input_context - + * param: onetime_address - Ko + * param: k_view - k_v + * param: account_spend_pubkey - K_s + * outparam: anchor_special_out - anchor_sp + */ +void make_carrot_janus_anchor_special(const mx25519_pubkey &enote_ephemeral_pubkey, + const input_context_t &input_context, + const crypto::public_key &onetime_address, + const crypto::secret_key &k_view, + const crypto::public_key &account_spend_pubkey, + janus_anchor_t &anchor_special_out); +/** +* brief: recover_address_spend_pubkey - get the receiver's spend key for which this RingCT onetime address +* can be reconstructed as 'owned' by +* K^j_s = Ko - K^o_ext = Ko - (k^o_g G + k^o_t U) +* param: onetime_address - Ko +* param: s_sender_receiver - s^ctx_sr +* param: amount_commitment - C_a +* outparam: address_spend_key_out: - K^j_s +*/ +void recover_address_spend_pubkey(const crypto::public_key &onetime_address, + const crypto::hash &s_sender_receiver, + const rct::key &amount_commitment, + crypto::public_key &address_spend_key_out); +/** +* brief: test_carrot_view_tag - test carrot view tag +* param: s_sender_receiver_unctx - s_sr +* param: input_context - +* param: onetime_address - Ko +* param: view_tag - vt +* return: true if successfully recomputed the view tag +*/ +bool test_carrot_view_tag(const unsigned char s_sender_receiver_unctx[32], + const input_context_t input_context, + const crypto::public_key &onetime_address, + const view_tag_t view_tag); +/** +* brief: try_recompute_carrot_amount_commitment - test recreating the amount commitment for given enote_type and amount +* param: s_sender_receiver - s^ctx_sr +* param: nominal_amount - a' +* param: nominal_address_spend_pubkey - K^j_s' +* param: nominal_enote_type - enote_type' +* param: amount_commitment - C_a +* outparam: amount_blinding_factor_out - k_a' = H_n(s^ctx_sr, enote_type') +* return: true if successfully recomputed the amount commitment (C_a ?= k_a' G + a' H) +*/ +bool try_recompute_carrot_amount_commitment(const crypto::hash &s_sender_receiver, + const rct::xmr_amount nominal_amount, + const crypto::public_key &nominal_address_spend_pubkey, + const CarrotEnoteType nominal_enote_type, + const rct::key &amount_commitment, + crypto::secret_key &amount_blinding_factor_out); +/** +* brief: try_get_amount - test decrypting the amount and recomputing the amount commitment +* param: s_sender_receiver - s^ctx_sr +* param: encrypted_amount - a_enc +* param: onetime_address - Ko +* param: address_spend_pubkey - K^j_s +* param: amount_commitment - C_a +* outparam: enote_type_out - enote_type' +* outparam: amount_out - a' = a_enc XOR m_a +* outparam: amount_blinding_factor_out - k_a' = H_n(s^ctx_sr, enote_type') +* return: true if successfully recomputed the amount commitment (C_a ?= k_a' G + a' H) +*/ +bool try_get_carrot_amount(const crypto::hash &s_sender_receiver, + const encrypted_amount_t &encrypted_amount, + const crypto::public_key &onetime_address, + const crypto::public_key &address_spend_pubkey, + const rct::key &amount_commitment, + CarrotEnoteType &enote_type_out, + rct::xmr_amount &amount_out, + crypto::secret_key &amount_blinding_factor_out); +/** + * brief: verify_carrot_external_janus_protection - check normal external enote is Janus safe (i.e. can recompute D_e) + * param: nominal_anchor - anchor' + * param: input_context - + * param: nominal_address_spend_pubkey - K^j_s' + * param: nominal_address_view_pubkey - K^j_v' + * param: is_subaddress - + * param: nominal_payment_id - pid' + * param: enote_ephemeral_pubkey - D_e + * return: true if this normal external enote is safe from Janus attacks + */ +bool verify_carrot_external_janus_protection(const janus_anchor_t &nominal_anchor, + const input_context_t &input_context, + const crypto::public_key &nominal_address_spend_pubkey, + const crypto::public_key &nominal_address_view_pubkey, + const bool is_subaddress, + const payment_id_t nominal_payment_id, + const mx25519_pubkey &enote_ephemeral_pubkey); +} //namespace carrot diff --git a/src/carrot_core/hash_functions.cpp b/src/carrot_core/hash_functions.cpp new file mode 100644 index 00000000000..8eca353d1e2 --- /dev/null +++ b/src/carrot_core/hash_functions.cpp @@ -0,0 +1,109 @@ +// 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 "hash_functions.h" + +//local headers +extern "C" +{ +#include "crypto/crypto-ops.h" +} +#include "crypto/blake2b.h" +#include "misc_log_ex.h" + +//third party headers + +//standard headers + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "carrot" + +namespace carrot +{ +//------------------------------------------------------------------------------------------------------------------- +// H_x[k](data) +// - if derivation_key == nullptr, then the hash is NOT keyed +//------------------------------------------------------------------------------------------------------------------- +static void hash_base(const void *derivation_key, //32 bytes + const void *data, + const std::size_t data_length, + void *hash_out, + const std::size_t out_length) +{ + CHECK_AND_ASSERT_THROW_MES(blake2b(hash_out, + out_length, + data, + data_length, + derivation_key, + derivation_key ? 32 : 0) == 0, + "carrot hash base: blake2b failed."); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +void derive_bytes_3(const void *data, const std::size_t data_length, const void *key, void *hash_out) +{ + // H_3(x): 2-byte output + hash_base(key, data, data_length, hash_out, 3); +} +//------------------------------------------------------------------------------------------------------------------- +void derive_bytes_8(const void *data, const std::size_t data_length, const void *key, void *hash_out) +{ + // H_8(x): 8-byte output + hash_base(key, data, data_length, hash_out, 8); +} +//------------------------------------------------------------------------------------------------------------------- +void derive_bytes_16(const void *data, const std::size_t data_length, const void *key, void *hash_out) +{ + // H_16(x): 16-byte output + hash_base(key, data, data_length, hash_out, 16); +} +//------------------------------------------------------------------------------------------------------------------- +void derive_bytes_32(const void *data, const std::size_t data_length, const void *key, void *hash_out) +{ + // H_32(x): 32-byte output + hash_base(key, data, data_length, hash_out, 32); +} +//------------------------------------------------------------------------------------------------------------------- +void derive_bytes_64(const void *data, const std::size_t data_length, const void *key, void *hash_out) +{ + // H_64(x): 64-byte output + hash_base(key, data, data_length, hash_out, 64); +} +//------------------------------------------------------------------------------------------------------------------- +void derive_scalar(const void *data, const std::size_t data_length, const void *key, void *hash_out) +{ + // H_n(x): Ed25519 group scalar output (32 bytes) + // note: hash to 64 bytes then mod l + unsigned char temp[64]; + hash_base(key, data, data_length, temp, 64); + sc_reduce(temp); //mod l + memcpy(hash_out, temp, 32); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace carrot diff --git a/src/carrot_core/hash_functions.h b/src/carrot_core/hash_functions.h new file mode 100644 index 00000000000..cb6baf830b7 --- /dev/null +++ b/src/carrot_core/hash_functions.h @@ -0,0 +1,59 @@ +// 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. + +// Core hash functions for Seraphis (note: this implementation satisfies the Jamtis specification). + +#pragma once + +//local headers + +//third party headers + +//standard headers +#include + +//forward declarations + + +namespace carrot +{ + +/// H_3(x): 3-byte output +void derive_bytes_3(const void *data, const std::size_t data_length, const void *key, void *hash_out); +/// H_8(x): 8-byte output +void derive_bytes_8(const void *data, const std::size_t data_length, const void* key, void *hash_out); +/// H_16(x): 16-byte output +void derive_bytes_16(const void *data, const std::size_t data_length, const void *key, void *hash_out); +/// H_32(x): 32-byte output +void derive_bytes_32(const void *data, const std::size_t data_length, const void *key, void *hash_out); +/// H_64(x): 64-byte output +void derive_bytes_64(const void *data, const std::size_t data_length, const void *key, void *hash_out); +/// H_n(x): unclamped Curve25519/Ed25519 group scalar output (32 bytes) +void derive_scalar(const void *data, const std::size_t data_length, const void *key, void *hash_out); + +} //namespace carrot diff --git a/src/carrot_core/output_set_finalization.cpp b/src/carrot_core/output_set_finalization.cpp new file mode 100644 index 00000000000..d2f6d8f4406 --- /dev/null +++ b/src/carrot_core/output_set_finalization.cpp @@ -0,0 +1,268 @@ +// 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 "output_set_finalization.h" + +//local headers +#include "common/container_helpers.h" +#include "enote_utils.h" +#include "misc_log_ex.h" + +//third party headers + +//standard headers + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "carrot" + +namespace carrot +{ +//------------------------------------------------------------------------------------------------------------------- +std::optional get_additional_output_type(const size_t num_outgoing, + const size_t num_selfsend, + const bool need_change_output, + const bool have_payment_type_selfsend) +{ + const size_t num_outputs = num_outgoing + num_selfsend; + const bool already_completed = num_outputs >= 2 && num_selfsend >= 1 && !need_change_output; + if (num_outputs == 0) + { + ASSERT_MES_AND_THROW("get additional output type: set contains 0 outputs"); + } + else if (already_completed) + { + return std::nullopt; + } + else if (num_outputs == 1) + { + if (num_selfsend == 0) + { + return AdditionalOutputType::CHANGE_SHARED; + } + else if (!need_change_output) + { + return AdditionalOutputType::DUMMY; + } + else // num_selfsend == 1 && need_change_output + { + if (have_payment_type_selfsend) + { + return AdditionalOutputType::CHANGE_SHARED; + } + else + { + return AdditionalOutputType::PAYMENT_SHARED; + } + } + } + else if (num_outputs < CARROT_MAX_TX_OUTPUTS) + { + return AdditionalOutputType::CHANGE_UNIQUE; + } + else // num_outputs >= CARROT_MAX_TX_OUTPUTS + { + ASSERT_MES_AND_THROW("get additional output type: " + "set needs finalization but already contains too many outputs"); + } +} +//------------------------------------------------------------------------------------------------------------------- +tools::optional_variant get_additional_output_proposal( + const size_t num_outgoing, + const size_t num_selfsend, + const rct::xmr_amount needed_change_amount, + const bool have_payment_type_selfsend, + const crypto::public_key &change_address_spend_pubkey) +{ + const std::optional additional_output_type = get_additional_output_type( + num_outgoing, + num_selfsend, + needed_change_amount, + have_payment_type_selfsend + ); + + if (!additional_output_type) + return {}; + + switch (*additional_output_type) + { + case AdditionalOutputType::PAYMENT_SHARED: + return CarrotPaymentProposalSelfSendV1{ + .destination_address_spend_pubkey = change_address_spend_pubkey, + .amount = needed_change_amount, + .enote_type = CarrotEnoteType::PAYMENT, + .enote_ephemeral_pubkey = std::nullopt + }; + case AdditionalOutputType::CHANGE_SHARED: + return CarrotPaymentProposalSelfSendV1{ + .destination_address_spend_pubkey = change_address_spend_pubkey, + .amount = needed_change_amount, + .enote_type = CarrotEnoteType::CHANGE, + .enote_ephemeral_pubkey = std::nullopt + }; + case AdditionalOutputType::CHANGE_UNIQUE: + return CarrotPaymentProposalSelfSendV1{ + .destination_address_spend_pubkey = change_address_spend_pubkey, + .amount = needed_change_amount, + .enote_type = CarrotEnoteType::CHANGE, + .enote_ephemeral_pubkey = std::nullopt + }; + case AdditionalOutputType::DUMMY: + return CarrotPaymentProposalV1{ + .destination = gen_carrot_main_address_v1(), + .amount = 0, + .randomness = gen_janus_anchor() + }; + } + + ASSERT_MES_AND_THROW("get additional output proposal: unrecognized additional output type"); +} +//------------------------------------------------------------------------------------------------------------------- +void get_output_enote_proposals(std::vector &&normal_payment_proposals, + std::vector &&selfsend_payment_proposals, + 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 crypto::key_image &tx_first_key_image, + std::vector &output_enote_proposals_out, + encrypted_payment_id_t &encrypted_payment_id_out) +{ + output_enote_proposals_out.clear(); + encrypted_payment_id_out = null_payment_id; + + // assert payment proposals numbers + const size_t num_proposals = normal_payment_proposals.size() + selfsend_payment_proposals.size(); + CHECK_AND_ASSERT_THROW_MES(num_proposals >= CARROT_MIN_TX_OUTPUTS, + "get output enote proposals: too few payment proposals"); + CHECK_AND_ASSERT_THROW_MES(num_proposals <= CARROT_MAX_TX_OUTPUTS, + "get output enote proposals: too many payment proposals"); + CHECK_AND_ASSERT_THROW_MES(selfsend_payment_proposals.size(), + "get output enote proposals: no selfsend payment proposal"); + + // assert there is a max of 1 integrated address payment proposals + size_t num_integrated = 0; + for (const CarrotPaymentProposalV1 &normal_payment_proposal : normal_payment_proposals) + if (normal_payment_proposal.destination.payment_id != null_payment_id) + ++num_integrated; + CHECK_AND_ASSERT_THROW_MES(num_integrated <= 1, + "get output enote proposals: only one integrated address is allowed per tx output set"); + + // assert anchor_norm != 0 for payments + for (const CarrotPaymentProposalV1 &normal_payment_proposal : normal_payment_proposals) + CHECK_AND_ASSERT_THROW_MES(normal_payment_proposal.randomness != janus_anchor_t{}, + "get output enote proposals: normal payment proposal has unset anchor_norm AKA randomness"); + + // sort normal payment proposals by anchor_norm and assert uniqueness of randomness for each payment + const auto sort_by_randomness = [](const CarrotPaymentProposalV1 &a, const CarrotPaymentProposalV1 &b) -> bool + { + return memcmp(&a.randomness, &b.randomness, JANUS_ANCHOR_BYTES) < 0; + }; + std::sort(normal_payment_proposals.begin(), normal_payment_proposals.end(), sort_by_randomness); + const bool has_unique_randomness = tools::is_sorted_and_unique(normal_payment_proposals, + sort_by_randomness); + CHECK_AND_ASSERT_THROW_MES(has_unique_randomness, + "get output enote proposals: normal payment proposals contain duplicate anchor_norm AKA randomness"); + + // input_context = "R" || KI_1 + input_context_t input_context; + make_carrot_input_context(tx_first_key_image, input_context); + + // construct normal enotes + output_enote_proposals_out.reserve(num_proposals); + for (size_t i = 0; i < normal_payment_proposals.size(); ++i) + { + encrypted_payment_id_t encrypted_payment_id; + get_output_proposal_normal_v1(normal_payment_proposals[i], + tx_first_key_image, + tools::add_element(output_enote_proposals_out), + encrypted_payment_id); + + // set pid to the first payment proposal or only integrated proposal + const bool is_first = i == 0; + const bool is_integrated = normal_payment_proposals[i].destination.payment_id != null_payment_id; + if (is_first || is_integrated) + encrypted_payment_id_out = encrypted_payment_id; + } + + // in the case that the pid target is ambiguous, set it to random bytes + const bool ambiguous_pid_destination = num_integrated == 0 && normal_payment_proposals.size() > 1; + if (ambiguous_pid_destination) + encrypted_payment_id_out = gen_payment_id(); + + // construct selfsend enotes, preferring internal enotes over special enotes when possible + for (const CarrotPaymentProposalSelfSendV1 &selfsend_payment_proposal : selfsend_payment_proposals) + { + const std::optional other_enote_ephemeral_pubkey = + (num_proposals == 2 && output_enote_proposals_out.size()) + ? output_enote_proposals_out.at(0).enote.enote_ephemeral_pubkey + : std::optional{}; + + if (s_view_balance_dev != nullptr) + { + get_output_proposal_internal_v1(selfsend_payment_proposal, + *s_view_balance_dev, + tx_first_key_image, + other_enote_ephemeral_pubkey, + tools::add_element(output_enote_proposals_out)); + } + else if (k_view_dev != nullptr) + { + get_output_proposal_special_v1(selfsend_payment_proposal, + *k_view_dev, + account_spend_pubkey, + tx_first_key_image, + other_enote_ephemeral_pubkey, + tools::add_element(output_enote_proposals_out)); + } + else // neither k_v nor s_vb device passed + { + ASSERT_MES_AND_THROW( + "get output enote proposals: neither a view-balance nor view-incoming device was provided"); + } + } + + // sort enotes by D_e and assert uniqueness properties of D_e + const auto sort_by_ephemeral_pubkey = [](const RCTOutputEnoteProposal &a, const RCTOutputEnoteProposal &b) -> bool + { + return memcmp(&a.enote.enote_ephemeral_pubkey, + &b.enote.enote_ephemeral_pubkey, + sizeof(mx25519_pubkey)) < 0; + }; + std::sort(output_enote_proposals_out.begin(), output_enote_proposals_out.end(), sort_by_ephemeral_pubkey); + const bool has_unique_ephemeral_pubkeys = tools::is_sorted_and_unique(output_enote_proposals_out, + sort_by_ephemeral_pubkey); + CHECK_AND_ASSERT_THROW_MES(!(num_proposals == 2 && has_unique_ephemeral_pubkeys), + "get output enote proposals: a 2-out set needs to share an ephemeral pubkey, but this 2-out set doesn't"); + CHECK_AND_ASSERT_THROW_MES(!(num_proposals != 2 && !has_unique_ephemeral_pubkeys), + "get output enote proposals: this >2-out set contains duplicate enote ephemeral pubkeys"); + + // sort enotes by Ko + std::sort(output_enote_proposals_out.begin(), output_enote_proposals_out.end()); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace carrot diff --git a/src/carrot_core/output_set_finalization.h b/src/carrot_core/output_set_finalization.h new file mode 100644 index 00000000000..d0311987098 --- /dev/null +++ b/src/carrot_core/output_set_finalization.h @@ -0,0 +1,111 @@ +// 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. + +//! @file Utilities for constructing output proposal sets that adhere to Carrot rules + +#pragma once + +//local headers +#include "carrot_enote_types.h" +#include "common/variant.h" +#include "config.h" +#include "payment_proposal.h" +#include "ringct/rctTypes.h" + +//third party headers + +//standard headers +#include + +//forward declarations + + +namespace carrot +{ +enum class AdditionalOutputType +{ + PAYMENT_SHARED, // selfsend proposal with enote_type="payment" with a shared D_e + CHANGE_SHARED, // selfsend proposal with enote_type="change" with a shared D_e + CHANGE_UNIQUE, // selfsend proposal with enote_type="change" with a unique D_e + DUMMY // outgoing proposal to a random address +}; + +/** + * brief: get_additional_output_type - get the type of the additional enote needed to finalize an output set + * param: num_outgoing - number of outgoing transfers + * param: num_selfsend - number of selfsend transfers + * param: need_change_output - whether an additional change output needs to be included for balance + * param: have_payment_type_selfsend - true if the enote set has a selfsend enote with enote_type="payment" + * return: AdditionalOutputType if need an additional enote, else std::nullopt + * throw: std::runtime_error if the output set is in a state where it cannot be finalized + */ +std::optional get_additional_output_type(const size_t num_outgoing, + const size_t num_selfsend, + const bool need_change_output, + const bool have_payment_type_selfsend); +/** + * brief: get_additional_output_proposal - get an additional output proposal to complete an output set + * param: num_outgoing - number of outgoing transfers + * param: num_selfsend - number of selfsend transfers + * param: needed_change_amount - the amount of leftover change needed to be included + * param: have_payment_type_selfsend - true if the enote set has a selfsend enote with enote_type="payment" + * param: change_address_spend_pubkey - K^j_s of our change address + * return: an output proposal if need an additional enote, else none + * throw: std::runtime_error if the output set is in a state where it cannot be finalized + */ +tools::optional_variant get_additional_output_proposal( + const size_t num_outgoing, + const size_t num_selfsend, + const rct::xmr_amount needed_change_amount, + const bool have_payment_type_selfsend, + const crypto::public_key &change_address_spend_pubkey); +/** + * brief: get_output_enote_proposals - convert a *finalized* set of payment proposals into output enote proposals + * param: normal_payment_proposals - + * param: selfsend_payment_proposals - + * param: s_view_balance_dev - pointer to view-balance device (OPTIONAL) + * param: k_view_dev - pointer to view-incoming device (OPTIONAL) + * param: account_spend_pubkey - K_s + * param: tx_first_key_image - KI_1 + * outparam: output_enote_proposals_out - + * outparam: encrypted_payment_id_out - pid_enc + * throw: std::runtime_error if the payment proposals do not represent a valid tx output set, or if no devices + * + * If s_view_balance_dev is not NULL, then the selfsend payments are converted into *internal* enotes. + * Otherwise, if k_view_dev is not NULL, then the selfsend payments are converted into *external* enotes. + */ +void get_output_enote_proposals(std::vector &&normal_payment_proposals, + std::vector &&selfsend_payment_proposals, + 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 crypto::key_image &tx_first_key_image, + std::vector &output_enote_proposals_out, + encrypted_payment_id_t &encrypted_payment_id_out); + +} //namespace carrot diff --git a/src/carrot_core/payment_proposal.cpp b/src/carrot_core/payment_proposal.cpp new file mode 100644 index 00000000000..2ab501c7292 --- /dev/null +++ b/src/carrot_core/payment_proposal.cpp @@ -0,0 +1,489 @@ +// 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 "payment_proposal.h" + +//local headers +#include "int-util.h" +#include "enote_utils.h" +#include "misc_language.h" +#include "misc_log_ex.h" +#include "ringct/rctOps.h" + +//third party headers + +//standard headers + + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "carrot" + +namespace carrot +{ +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static const janus_anchor_t null_anchor{{0}}; +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +template +static auto auto_wiper(T &obj) +{ + static_assert(std::is_trivially_copyable()); + return epee::misc_utils::create_scope_leave_handler([&]{ memwipe(&obj, sizeof(T)); }); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static crypto::secret_key get_enote_ephemeral_privkey(const janus_anchor_t randomness, + const CarrotDestinationV1 &destination, + const input_context_t &input_context) +{ + // d_e = H_n(anchor_norm, input_context, K^j_s, K^j_v, pid)) + crypto::secret_key enote_ephemeral_privkey; + make_carrot_enote_ephemeral_privkey(randomness, + input_context, + destination.address_spend_pubkey, + destination.address_view_pubkey, + destination.payment_id, + enote_ephemeral_privkey); + + return enote_ephemeral_privkey; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static mx25519_pubkey get_enote_ephemeral_pubkey(const janus_anchor_t randomness, + const CarrotDestinationV1 &destination, + const input_context_t &input_context) +{ + // d_e = H_n(anchor_norm, input_context, K^j_s, K^j_v, pid)) + const crypto::secret_key enote_ephemeral_privkey{get_enote_ephemeral_privkey(randomness, + destination, + input_context)}; + + mx25519_pubkey enote_ephemeral_pubkey; + if (destination.is_subaddress) + // D_e = d_e ConvertPointE(K^j_s) + make_carrot_enote_ephemeral_pubkey_subaddress(enote_ephemeral_privkey, + destination.address_spend_pubkey, + enote_ephemeral_pubkey); + else + // D_e = d_e B + make_carrot_enote_ephemeral_pubkey_cryptonote(enote_ephemeral_privkey, + enote_ephemeral_pubkey); + + return enote_ephemeral_pubkey; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void get_normal_proposal_ecdh_parts(const CarrotPaymentProposalV1 &proposal, + const input_context_t &input_context, + mx25519_pubkey &enote_ephemeral_pubkey_out, + mx25519_pubkey &s_sender_receiver_unctx_out) +{ + // 1. d_e = H_n(anchor_norm, input_context, K^j_s, K^j_v, pid)) + const crypto::secret_key enote_ephemeral_privkey = get_enote_ephemeral_privkey(proposal.randomness, + proposal.destination, + input_context); + + // 2. make D_e + enote_ephemeral_pubkey_out = get_enote_ephemeral_pubkey(proposal, input_context); + + // 3. s_sr = d_e ConvertPointE(K^j_v) + make_carrot_uncontextualized_shared_key_sender(enote_ephemeral_privkey, + proposal.destination.address_view_pubkey, + s_sender_receiver_unctx_out); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void get_output_proposal_parts(const crypto::hash &s_sender_receiver, + const crypto::public_key &destination_spend_pubkey, + const payment_id_t payment_id, + const rct::xmr_amount amount, + const CarrotEnoteType enote_type, + const mx25519_pubkey &enote_ephemeral_pubkey, + const input_context_t &input_context, + const bool coinbase_amount_commitment, + crypto::secret_key &amount_blinding_factor_out, + rct::key &amount_commitment_out, + crypto::public_key &onetime_address_out, + encrypted_amount_t &encrypted_amount_out, + encrypted_payment_id_t &encrypted_payment_id_out) +{ + // 1. k_a = H_n(s^ctx_sr, enote_type) if !coinbase, else 1 + if (coinbase_amount_commitment) + amount_blinding_factor_out = rct::rct2sk(rct::I); + else + make_carrot_amount_blinding_factor(s_sender_receiver, + amount, + destination_spend_pubkey, + enote_type, + amount_blinding_factor_out); + + // 2. C_a = k_a G + a H + amount_commitment_out = rct::commit(amount, rct::sk2rct(amount_blinding_factor_out)); + + // 3. Ko = K^j_s + K^o_ext = K^j_s + (k^o_g G + k^o_t T) + make_carrot_onetime_address(destination_spend_pubkey, + s_sender_receiver, + amount_commitment_out, + onetime_address_out); + + // 4. a_enc = a XOR m_a + encrypted_amount_out = encrypt_carrot_amount(amount, + s_sender_receiver, + onetime_address_out); + + // 5. pid_enc = pid XOR m_pid + encrypted_payment_id_out = encrypt_legacy_payment_id(payment_id, s_sender_receiver, onetime_address_out); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void get_external_output_proposal_parts(const mx25519_pubkey &s_sender_receiver_unctx, + const crypto::public_key &destination_spend_pubkey, + const payment_id_t payment_id, + const rct::xmr_amount amount, + const CarrotEnoteType enote_type, + const mx25519_pubkey &enote_ephemeral_pubkey, + const input_context_t &input_context, + const bool coinbase_amount_commitment, + crypto::hash &s_sender_receiver_out, + crypto::secret_key &amount_blinding_factor_out, + rct::key &amount_commitment_out, + crypto::public_key &onetime_address_out, + encrypted_amount_t &encrypted_amount_out, + encrypted_payment_id_t &encrypted_payment_id_out, + view_tag_t &view_tag_out) +{ + // 1. s^ctx_sr = H_32(s_sr, D_e, input_context) + make_carrot_sender_receiver_secret(s_sender_receiver_unctx.data, + enote_ephemeral_pubkey, + input_context, + s_sender_receiver_out); + + // 2. get other parts: k_a, C_a, Ko, a_enc, pid_enc + get_output_proposal_parts(s_sender_receiver_out, + destination_spend_pubkey, + payment_id, + amount, + enote_type, + enote_ephemeral_pubkey, + input_context, + coinbase_amount_commitment, + amount_blinding_factor_out, + amount_commitment_out, + onetime_address_out, + encrypted_amount_out, + encrypted_payment_id_out); + + // 3. vt = H_3(s_sr || input_context || Ko) + make_carrot_view_tag(s_sender_receiver_unctx.data, input_context, onetime_address_out, view_tag_out); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +bool operator==(const CarrotPaymentProposalV1 &a, const CarrotPaymentProposalV1 &b) +{ + return a.destination == b.destination && + a.amount == b.amount && + a.randomness == b.randomness; +} +//------------------------------------------------------------------------------------------------------------------- +bool operator==(const CarrotPaymentProposalSelfSendV1 &a, const CarrotPaymentProposalSelfSendV1 &b) +{ + return a.destination_address_spend_pubkey == b.destination_address_spend_pubkey && + a.amount == b.amount && + a.enote_type == b.enote_type && + a.internal_message == b.internal_message && + 0 == memcmp(&a.enote_ephemeral_pubkey, &b.enote_ephemeral_pubkey, sizeof(mx25519_pubkey)); +} +//------------------------------------------------------------------------------------------------------------------- +bool operator<(const RCTOutputEnoteProposal &a, const RCTOutputEnoteProposal &b) +{ + return memcmp(&a.enote.onetime_address, &b.enote.onetime_address, sizeof(crypto::public_key)) < 0; +} +//------------------------------------------------------------------------------------------------------------------- +mx25519_pubkey get_enote_ephemeral_pubkey(const CarrotPaymentProposalV1 &proposal, + const input_context_t &input_context) +{ + return get_enote_ephemeral_pubkey(proposal.randomness, proposal.destination, input_context); +} +//------------------------------------------------------------------------------------------------------------------- +void get_coinbase_output_proposal_v1(const CarrotPaymentProposalV1 &proposal, + const std::uint64_t block_index, + CarrotCoinbaseEnoteV1 &output_enote_out) +{ + // 1. sanity checks + CHECK_AND_ASSERT_THROW_MES(proposal.randomness != null_anchor, + "get coinbase output proposal v1: invalid randomness for janus anchor (zero)."); + CHECK_AND_ASSERT_THROW_MES(!proposal.destination.is_subaddress, + "get coinbase output proposal v1: subaddresses aren't allowed as destinations of coinbase outputs"); + CHECK_AND_ASSERT_THROW_MES(proposal.destination.payment_id == null_payment_id, + "get coinbase output proposal v1: integrated addresses aren't allowed as destinations of coinbase outputs"); + + // 2. coinbase input context + input_context_t input_context; + make_carrot_input_context_coinbase(block_index, input_context); + + // 3. make D_e and do external ECDH + mx25519_pubkey s_sender_receiver_unctx; auto dhe_wiper = auto_wiper(s_sender_receiver_unctx); + get_normal_proposal_ecdh_parts(proposal, + input_context, + output_enote_out.enote_ephemeral_pubkey, + s_sender_receiver_unctx); + + // 4. build the output enote address pieces + crypto::hash s_sender_receiver; auto q_wiper = auto_wiper(s_sender_receiver); + crypto::secret_key dummy_amount_blinding_factor; + rct::key dummy_amount_commitment; + encrypted_amount_t dummy_encrypted_amount; + encrypted_payment_id_t dummy_encrypted_payment_id; + get_external_output_proposal_parts(s_sender_receiver_unctx, + proposal.destination.address_spend_pubkey, + null_payment_id, + proposal.amount, + CarrotEnoteType::PAYMENT, + output_enote_out.enote_ephemeral_pubkey, + input_context, + true, // coinbase_amount_commitment + s_sender_receiver, + dummy_amount_blinding_factor, + dummy_amount_commitment, + output_enote_out.onetime_address, + dummy_encrypted_amount, + dummy_encrypted_payment_id, + output_enote_out.view_tag); + + // 5. anchor_enc = anchor XOR m_anchor + output_enote_out.anchor_enc = encrypt_carrot_anchor(proposal.randomness, + s_sender_receiver, + output_enote_out.onetime_address); + + // 6. save the amount and block index + output_enote_out.amount = proposal.amount; + output_enote_out.block_index = block_index; +} +//------------------------------------------------------------------------------------------------------------------- +void get_output_proposal_normal_v1(const CarrotPaymentProposalV1 &proposal, + const crypto::key_image &tx_first_key_image, + RCTOutputEnoteProposal &output_enote_out, + encrypted_payment_id_t &encrypted_payment_id_out) +{ + // 1. sanity checks + CHECK_AND_ASSERT_THROW_MES(proposal.randomness != null_anchor, + "jamtis payment proposal: invalid randomness for janus anchor (zero)."); + + // 2. input context: input_context = "R" || KI_1 + input_context_t input_context; + make_carrot_input_context(tx_first_key_image, input_context); + + // 3. make D_e and do external ECDH + mx25519_pubkey s_sender_receiver_unctx; auto dhe_wiper = auto_wiper(s_sender_receiver_unctx); + get_normal_proposal_ecdh_parts(proposal, + input_context, + output_enote_out.enote.enote_ephemeral_pubkey, + s_sender_receiver_unctx); + + // 4. build the output enote address pieces + crypto::hash s_sender_receiver; auto q_wiper = auto_wiper(s_sender_receiver); + get_external_output_proposal_parts(s_sender_receiver_unctx, + proposal.destination.address_spend_pubkey, + proposal.destination.payment_id, + proposal.amount, + CarrotEnoteType::PAYMENT, + output_enote_out.enote.enote_ephemeral_pubkey, + input_context, + false, // coinbase_amount_commitment + s_sender_receiver, + output_enote_out.amount_blinding_factor, + output_enote_out.enote.amount_commitment, + output_enote_out.enote.onetime_address, + output_enote_out.enote.amount_enc, + encrypted_payment_id_out, + output_enote_out.enote.view_tag); + + // 5. anchor_enc = anchor XOR m_anchor + output_enote_out.enote.anchor_enc = encrypt_carrot_anchor(proposal.randomness, + s_sender_receiver, + output_enote_out.enote.onetime_address); + + // 6. save the amount and first key image + output_enote_out.amount = proposal.amount; + output_enote_out.enote.tx_first_key_image = tx_first_key_image; +} +//------------------------------------------------------------------------------------------------------------------- +void get_output_proposal_special_v1(const CarrotPaymentProposalSelfSendV1 &proposal, + const view_incoming_key_device &k_view_dev, + const crypto::public_key &account_spend_pubkey, + const crypto::key_image &tx_first_key_image, + const std::optional &other_enote_ephemeral_pubkey, + RCTOutputEnoteProposal &output_enote_out) +{ + // 1. sanity checks + CHECK_AND_ASSERT_THROW_MES(!proposal.internal_message, + "get output proposal special v1: internal messages are only for internal selfsends, not special selfsends"); + + // 2. input context: input_context = "R" || KI_1 + input_context_t input_context; + make_carrot_input_context(tx_first_key_image, input_context); + + // 3. D_e + const bool mismatched_enote_ephemeral_pubkeys = proposal.enote_ephemeral_pubkey && + other_enote_ephemeral_pubkey && + memcmp(&*proposal.enote_ephemeral_pubkey, &*other_enote_ephemeral_pubkey, sizeof(mx25519_pubkey)); + CHECK_AND_ASSERT_THROW_MES(!mismatched_enote_ephemeral_pubkeys, + "get output proposal special v1: mismatched enote ephemeral pubkeys provided"); + const mx25519_pubkey enote_ephemeral_pubkey = proposal.enote_ephemeral_pubkey.value_or( + other_enote_ephemeral_pubkey.value_or(gen_x25519_pubkey())); + + // 4. s_sr = k_v D_e + mx25519_pubkey s_sender_receiver_unctx; auto ecdh_wiper = auto_wiper(s_sender_receiver_unctx); + CHECK_AND_ASSERT_THROW_MES(k_view_dev.view_key_scalar_mult_x25519(enote_ephemeral_pubkey, + s_sender_receiver_unctx), + "get output proposal special v1: HW device failed to perform ECDH with ephemeral pubkey"); + + // 5. build the output enote address pieces + crypto::hash s_sender_receiver; auto q_wiper = auto_wiper(s_sender_receiver); + encrypted_payment_id_t dummy_encrypted_payment_id; + get_external_output_proposal_parts(s_sender_receiver_unctx, + proposal.destination_address_spend_pubkey, + null_payment_id, + proposal.amount, + proposal.enote_type, + enote_ephemeral_pubkey, + input_context, + false, // coinbase_amount_commitment + s_sender_receiver, + output_enote_out.amount_blinding_factor, + output_enote_out.enote.amount_commitment, + output_enote_out.enote.onetime_address, + output_enote_out.enote.amount_enc, + dummy_encrypted_payment_id, + output_enote_out.enote.view_tag); + + // 6. make special janus anchor: anchor_sp = H_16(D_e, input_context, Ko, k_v, K_s) + janus_anchor_t janus_anchor_special; + k_view_dev.make_janus_anchor_special(enote_ephemeral_pubkey, + input_context, + output_enote_out.enote.onetime_address, + account_spend_pubkey, + janus_anchor_special); + + // 7. encrypt special anchor: anchor_enc = anchor XOR m_anchor + output_enote_out.enote.anchor_enc = encrypt_carrot_anchor(janus_anchor_special, + s_sender_receiver, + output_enote_out.enote.onetime_address); + + // 8. save the enote ephemeral pubkey, first tx key image, and amount + output_enote_out.enote.enote_ephemeral_pubkey = enote_ephemeral_pubkey; + output_enote_out.enote.tx_first_key_image = tx_first_key_image; + output_enote_out.amount = proposal.amount; +} +//------------------------------------------------------------------------------------------------------------------- +void get_output_proposal_internal_v1(const CarrotPaymentProposalSelfSendV1 &proposal, + const view_balance_secret_device &s_view_balance_dev, + const crypto::key_image &tx_first_key_image, + const std::optional &other_enote_ephemeral_pubkey, + RCTOutputEnoteProposal &output_enote_out) +{ + // 1. sanity checks + // @TODO + + // 2. input_context = "R" || KI_1 + input_context_t input_context; + make_carrot_input_context(tx_first_key_image, input_context); + + // 3. D_e + const bool mismatched_enote_ephemeral_pubkeys = proposal.enote_ephemeral_pubkey && + other_enote_ephemeral_pubkey && + memcmp(&*proposal.enote_ephemeral_pubkey, &*other_enote_ephemeral_pubkey, sizeof(mx25519_pubkey)); + CHECK_AND_ASSERT_THROW_MES(!mismatched_enote_ephemeral_pubkeys, + "get output proposal internal v1: mismatched enote ephemeral pubkeys provided"); + const mx25519_pubkey enote_ephemeral_pubkey = proposal.enote_ephemeral_pubkey.value_or( + other_enote_ephemeral_pubkey.value_or(gen_x25519_pubkey())); + + // 4. s^ctx_sr = H_32(s_vb, D_e, input_context) + crypto::hash s_sender_receiver; auto q_wiper = auto_wiper(s_sender_receiver); + s_view_balance_dev.make_internal_sender_receiver_secret(enote_ephemeral_pubkey, + input_context, + s_sender_receiver); + + // 5. build the output enote address pieces + encrypted_payment_id_t dummy_encrypted_payment_id; + get_output_proposal_parts(s_sender_receiver, + proposal.destination_address_spend_pubkey, + null_payment_id, + proposal.amount, + proposal.enote_type, + enote_ephemeral_pubkey, + input_context, + false, // coinbase_amount_commitment + output_enote_out.amount_blinding_factor, + output_enote_out.enote.amount_commitment, + output_enote_out.enote.onetime_address, + output_enote_out.enote.amount_enc, + dummy_encrypted_payment_id); + + // 6. vt = H_3(s_vb || input_context || Ko) + s_view_balance_dev.make_internal_view_tag(input_context, + output_enote_out.enote.onetime_address, + output_enote_out.enote.view_tag); + + // 7. anchor = given message OR 0s, if not available + const janus_anchor_t anchor = proposal.internal_message.value_or(janus_anchor_t{}); + + // 8. encrypt anchor: anchor_enc = anchor XOR m_anchor + output_enote_out.enote.anchor_enc = encrypt_carrot_anchor(anchor, + s_sender_receiver, + output_enote_out.enote.onetime_address); + + // 9. save the enote ephemeral pubkey, first tx key image, and amount + output_enote_out.enote.enote_ephemeral_pubkey = enote_ephemeral_pubkey; + output_enote_out.enote.tx_first_key_image = tx_first_key_image; + output_enote_out.amount = proposal.amount; +} +//------------------------------------------------------------------------------------------------------------------- +CarrotPaymentProposalV1 gen_carrot_payment_proposal_v1(const bool is_subaddress, + const bool has_payment_id, + const rct::xmr_amount amount, + const std::size_t num_random_memo_elements) +{ + CarrotPaymentProposalV1 temp; + + if (is_subaddress) + temp.destination = gen_carrot_subaddress_v1(); + else if (has_payment_id) + temp.destination = gen_carrot_integrated_address_v1(); + else + temp.destination = gen_carrot_main_address_v1(); + + temp.amount = amount; + temp.randomness = gen_janus_anchor(); + + return temp; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace carrot diff --git a/src/carrot_core/payment_proposal.h b/src/carrot_core/payment_proposal.h new file mode 100644 index 00000000000..e0fc3187bc1 --- /dev/null +++ b/src/carrot_core/payment_proposal.h @@ -0,0 +1,171 @@ +// 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. + +// A 'payment proposal' is a proposal to make an enote sending funds to a Carrot address. +// Carrot: Cryptonote Address For Rerandomizable-RingCT-Output Transactions + +#pragma once + +//local headers +#include "carrot_enote_types.h" +#include "destination.h" +#include "device.h" +#include "ringct/rctTypes.h" +#include "common/variant.h" + +//third party headers + +//standard headers +#include + +//forward declarations + + +namespace carrot +{ + +//// +// CarrotPaymentProposalV1 +// - for creating an output proposal to send an amount to someone +/// +struct CarrotPaymentProposalV1 final +{ + /// user address + CarrotDestinationV1 destination; + /// b + rct::xmr_amount amount; + /// anchor_norm: secret 16-byte randomness for Janus anchor + janus_anchor_t randomness; +}; + +//// +// CarrotPaymentProposalSelfSendV1 +// - for creating an output proposal to send an change to yourself +/// +struct CarrotPaymentProposalSelfSendV1 final +{ + /// one of our own address spend pubkeys: K^j_s + crypto::public_key destination_address_spend_pubkey; + /// a + rct::xmr_amount amount; + + /// enote_type + CarrotEnoteType enote_type; + /// enote ephemeral pubkey: D_e + std::optional enote_ephemeral_pubkey; + /// anchor: arbitrary, pre-encrypted message for _internal_ selfsends + std::optional internal_message; +}; + +struct RCTOutputEnoteProposal +{ + CarrotEnoteV1 enote; + + // we need this opening information to make amount range proofs + rct::xmr_amount amount; + crypto::secret_key amount_blinding_factor; +}; + +/// equality operators +bool operator==(const CarrotPaymentProposalV1 &a, const CarrotPaymentProposalV1 &b); +/// equality operators +bool operator==(const CarrotPaymentProposalSelfSendV1 &a, const CarrotPaymentProposalSelfSendV1 &b); + +/// comparison operators +bool operator<(const RCTOutputEnoteProposal &a, const RCTOutputEnoteProposal &b); + +/** +* brief: get_enote_ephemeral_pubkey - get the proposal's enote ephemeral pubkey D_e +* param: proposal - +* param: input_context - +* return: D_e +*/ +mx25519_pubkey get_enote_ephemeral_pubkey(const CarrotPaymentProposalV1 &proposal, + const input_context_t &input_context); +/** +* brief: get_coinbase_output_proposal_v1 - convert the carrot proposal to a coinbase output proposal +* param: proposal - +* param: block_index - index of the coinbase tx's block +* outparam: output_enote_out - +* outparam: partial_memo_out - +*/ +void get_coinbase_output_proposal_v1(const CarrotPaymentProposalV1 &proposal, + const std::uint64_t block_index, + CarrotCoinbaseEnoteV1 &output_enote_out); +/** +* brief: get_output_proposal_normal_v1 - convert the carrot proposal to an output proposal +* param: proposal - +* param: tx_first_key_image - +* outparam: output_enote_out - +* outparam: encrypted_payment_id_out - pid_enc +*/ +void get_output_proposal_normal_v1(const CarrotPaymentProposalV1 &proposal, + const crypto::key_image &tx_first_key_image, + RCTOutputEnoteProposal &output_enote_out, + encrypted_payment_id_t &encrypted_payment_id_out); +/** +* brief: get_output_proposal_v1 - convert the carrot proposal to an output proposal (external selfsend) +* param: proposal - +* param: k_view_dev - +* param: account_spend_pubkey - +* param: tx_first_key_image - +* param: other_enote_ephemeral_pubkey - +* outparam: output_enote_out - +*/ +void get_output_proposal_special_v1(const CarrotPaymentProposalSelfSendV1 &proposal, + const view_incoming_key_device &k_view_dev, + const crypto::public_key &account_spend_pubkey, + const crypto::key_image &tx_first_key_image, + const std::optional &other_enote_ephemeral_pubkey, + RCTOutputEnoteProposal &output_enote_out); +/** +* brief: get_output_proposal_internal_v1 - convert the carrot proposal to an output proposal (internal) +* param: proposal - +* param: s_view_balance_dev - +* param: account_spend_pubkey - +* param: tx_first_key_image - +* param: other_enote_ephemeral_pubkey - +* outparam: output_enote_out - +*/ +void get_output_proposal_internal_v1(const CarrotPaymentProposalSelfSendV1 &proposal, + const view_balance_secret_device &s_view_balance_dev, + const crypto::key_image &tx_first_key_image, + const std::optional &other_enote_ephemeral_pubkey, + RCTOutputEnoteProposal &output_enote_out); +/** +* brief: gen_jamtis_payment_proposal_v1 - generate a random proposal +* param: is_subaddress - whether to generate a proposal to subaddress +* param: has_payment_id - true to generate non-zero payment ID, false for null payment ID +* param: amount - +* return: a random proposal +*/ +CarrotPaymentProposalV1 gen_carrot_payment_proposal_v1(const bool is_subaddress, + const bool has_payment_id, + const rct::xmr_amount amount); + +} //namespace carrot diff --git a/src/carrot_core/transcript_fixed.h b/src/carrot_core/transcript_fixed.h new file mode 100644 index 00000000000..3179cbe236f --- /dev/null +++ b/src/carrot_core/transcript_fixed.h @@ -0,0 +1,185 @@ +// 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. + +// Transcript class for assembling data that needs to be hashed. + +#pragma once + +//local headers +#include "int-util.h" +#include "memwipe.h" + +//third party headers + +//standard headers +#include +#include +#include + +//forward declarations + + +namespace sp +{ +namespace detail +{ +template +constexpr size_t sizeof_sum() +{ + return (sizeof(Ts) + ...); +} + +template <> +constexpr size_t sizeof_sum<>() +{ + return 0; +} +} //namespace detail + +//// +// SpFixedTranscript +// - build a transcript of a fixed bytesize and input types, enforced at compile time +// - written to be the simplest correct transcript of data possible +// - requires domain separators at compile-time as well +// - ensures that no two transcripts with different domain separators will ever be equal +// - does not use dynamic allocation +// - unsigned integers are added to the transcript in little-endian form +// - signed integers are not allowed +// - domain separator is length-prefixed with a single unsigned byte at the beginning +// - passed domain separator can be null terminated or not, null bytes and after will be dropped +/// +template +class SpFixedTranscript final +{ +public: +//constructors + /// normal constructor + SpFixedTranscript(const Ts&... args) + { + // copy domain separator length prefix + m_transcript[0] = static_cast(domain_sep_size()); + + // copy domain separator + memcpy(m_transcript + 1, domain_sep, domain_sep_size()); + + // copy types into buffer + append<1 + domain_sep_size()>(args...); + } + +//overloaded operators + /// disable copy/move + SpFixedTranscript& operator=(const SpFixedTranscript&) = delete; + SpFixedTranscript& operator=(SpFixedTranscript&&) = delete; + +//member functions + constexpr const void* data() const noexcept { return m_transcript; } + + static constexpr std::size_t size() + { + return 1 + domain_sep_size() + detail::sizeof_sum(); + } + +//destructors + ~SpFixedTranscript() + { + // wipe the buffer on leave in case it contains sensitive data + memwipe(m_transcript, sizeof(m_transcript)); + } + +private: +//member functions + template + void append() {} + + template + void append(const U0 &arg0, const Us&... args) + { + // write current argument to buffer + write(arg0); + + // call append for next argument + static constexpr size_t new_offset = offset + sizeof(arg0); + append(args...); + } + + template + void write(const U &val) + { + static_assert(std::has_unique_object_representations_v); + static_assert(std::is_standard_layout_v); + static_assert(!std::is_signed_v || std::is_same_v); + static_assert(alignof(U) == 1); + static_assert(!std::is_pointer_v); + + memcpy(m_transcript + offset, &val, sizeof(val)); + } + + template + void write(std::uint16_t val) + { + val = SWAP16LE(val); + memcpy(m_transcript + offset, &val, sizeof(val)); + } + + template + void write(std::uint32_t val) + { + val = SWAP32LE(val); + memcpy(m_transcript + offset, &val, sizeof(val)); + } + + template + void write(std::uint64_t val) + { + val = SWAP64LE(val); + memcpy(m_transcript + offset, &val, sizeof(val)); + } + + static constexpr std::size_t domain_sep_size() + { + for (std::size_t i = 0; i < N; ++i) + if (domain_sep[i] == '\0') + return i; + + return N; + } + + static_assert(domain_sep_size() <= 255, "domain separator must be less than 256 characters long"); + +//member variables + /// the transcript buffer + unsigned char m_transcript[size()]; +}; + +template +auto make_fixed_transcript(const Ts&... args) +{ + return SpFixedTranscript(args...); +} + +} //namespace sp diff --git a/src/crypto/CMakeLists.txt b/src/crypto/CMakeLists.txt index b0006faba87..0f8edecb535 100644 --- a/src/crypto/CMakeLists.txt +++ b/src/crypto/CMakeLists.txt @@ -76,6 +76,9 @@ target_link_libraries(cncrypto ${sodium_LIBRARIES} PRIVATE ${EXTRA_LIBRARIES}) +target_include_directories(cncrypto + PRIVATE + ${MX25519_INCLUDE}) if (ARM) option(NO_OPTIMIZED_MULTIPLY_ON_ARM diff --git a/src/crypto/crypto-ops.c b/src/crypto/crypto-ops.c index 314fe448a20..df28a190385 100644 --- a/src/crypto/crypto-ops.c +++ b/src/crypto/crypto-ops.c @@ -3829,6 +3829,92 @@ int sc_isnonzero(const unsigned char *s) { s[27] | s[28] | s[29] | s[30] | s[31]) - 1) >> 8) + 1; } +static void edwardsYZ_to_x25519(unsigned char *xbytes, const fe Y, const fe Z) { + // y = Y/Z + // x_mont = (1 + y) / (1 - y) + // = (1 + Y/Z) / (1 - Y/Z) + // = (Z + Y) / (Z - Y) + + fe tmp0; + fe tmp1; + fe_add(tmp0, Z, Y); // Z + Y + fe_sub(tmp1, Z, Y); // Z - Y + fe_invert(tmp1, tmp1); // 1/(Z - Y) + fe_mul(tmp0, tmp0, tmp1); // (Z + Y) / (Z - Y) + fe_tobytes(xbytes, tmp0); // tobytes((Z + Y) / (Z - Y)) +} + +void ge_p3_to_x25519(unsigned char *xbytes, const ge_p3 *h) +{ + edwardsYZ_to_x25519(xbytes, h->Y, h->Z); +} + +int edwards_bytes_to_x25519_vartime(unsigned char *xbytes, const unsigned char *s) +{ + /* From fe_frombytes.c */ + + int64_t h0 = load_4(s); + int64_t h1 = load_3(s + 4) << 6; + int64_t h2 = load_3(s + 7) << 5; + int64_t h3 = load_3(s + 10) << 3; + int64_t h4 = load_3(s + 13) << 2; + int64_t h5 = load_4(s + 16); + int64_t h6 = load_3(s + 20) << 7; + int64_t h7 = load_3(s + 23) << 5; + int64_t h8 = load_3(s + 26) << 4; + int64_t h9 = (load_3(s + 29) & 8388607) << 2; + int64_t carry0; + int64_t carry1; + int64_t carry2; + int64_t carry3; + int64_t carry4; + int64_t carry5; + int64_t carry6; + int64_t carry7; + int64_t carry8; + int64_t carry9; + + /* Validate the number to be canonical */ + if (h9 == 33554428 && h8 == 268435440 && h7 == 536870880 && h6 == 2147483520 && + h5 == 4294967295 && h4 == 67108860 && h3 == 134217720 && h2 == 536870880 && + h1 == 1073741760 && h0 >= 4294967277) { + return -1; + } + + carry9 = (h9 + (int64_t) (1<<24)) >> 25; h0 += carry9 * 19; h9 -= carry9 << 25; + carry1 = (h1 + (int64_t) (1<<24)) >> 25; h2 += carry1; h1 -= carry1 << 25; + carry3 = (h3 + (int64_t) (1<<24)) >> 25; h4 += carry3; h3 -= carry3 << 25; + carry5 = (h5 + (int64_t) (1<<24)) >> 25; h6 += carry5; h5 -= carry5 << 25; + carry7 = (h7 + (int64_t) (1<<24)) >> 25; h8 += carry7; h7 -= carry7 << 25; + + carry0 = (h0 + (int64_t) (1<<25)) >> 26; h1 += carry0; h0 -= carry0 << 26; + carry2 = (h2 + (int64_t) (1<<25)) >> 26; h3 += carry2; h2 -= carry2 << 26; + carry4 = (h4 + (int64_t) (1<<25)) >> 26; h5 += carry4; h4 -= carry4 << 26; + carry6 = (h6 + (int64_t) (1<<25)) >> 26; h7 += carry6; h6 -= carry6 << 26; + carry8 = (h8 + (int64_t) (1<<25)) >> 26; h9 += carry8; h8 -= carry8 << 26; + + fe Y; + Y[0] = h0; + Y[1] = h1; + Y[2] = h2; + Y[3] = h3; + Y[4] = h4; + Y[5] = h5; + Y[6] = h6; + Y[7] = h7; + Y[8] = h8; + Y[9] = h9; + + /* End fe_frombytes.c */ + + fe Z; + fe_1(Z); + + edwardsYZ_to_x25519(xbytes, Y, Z); + + return 0; +} + int ge_p3_is_point_at_infinity_vartime(const ge_p3 *p) { // https://eprint.iacr.org/2008/522 // X == T == 0 and Y/Z == 1 diff --git a/src/crypto/crypto-ops.h b/src/crypto/crypto-ops.h index c103f1f789d..569a446c7b3 100644 --- a/src/crypto/crypto-ops.h +++ b/src/crypto/crypto-ops.h @@ -156,6 +156,12 @@ void sc_muladd(unsigned char *s, const unsigned char *a, const unsigned char *b, int sc_check(const unsigned char *); int sc_isnonzero(const unsigned char *); /* Doesn't normalize */ +/** + * brief: Convert Ed25519 y-coord to X25519 x-coord, AKA "ConvertPointE()" in the Carrot spec + */ +void ge_p3_to_x25519(unsigned char *xbytes, const ge_p3 *h); +int edwards_bytes_to_x25519_vartime(unsigned char *xbytes, const unsigned char *s); + // internal uint64_t load_3(const unsigned char *in); uint64_t load_4(const unsigned char *in); diff --git a/src/crypto/generators.cpp b/src/crypto/generators.cpp index 493d18334d9..44dc7299e37 100644 --- a/src/crypto/generators.cpp +++ b/src/crypto/generators.cpp @@ -39,6 +39,7 @@ extern "C" #include #include #include +#include namespace crypto { @@ -68,14 +69,35 @@ constexpr public_key G = bytes_to({ 0x58, 0x66, 0x66, 0x66, 0x66, 0x //pedersen commitment generator H: toPoint(cn_fast_hash(G)) constexpr public_key H = bytes_to({ 0x8b, 0x65, 0x59, 0x70, 0x15, 0x37, 0x99, 0xaf, 0x2a, 0xea, 0xdc, 0x9f, 0xf1, 0xad, 0xd0, 0xea, 0x6c, 0x72, 0x51, 0xd5, 0x41, 0x54, 0xcf, 0xa9, 0x2c, 0x17, 0x3a, 0x0d, 0xd3, 0x9c, 0x1f, 0x94 }); +//seraphis generator T: keccak_to_pt(keccak("Monero Generator T")) +constexpr public_key T = bytes_to({ 0x96, 0x6f, 0xc6, 0x6b, 0x82, 0xcd, 0x56, 0xcf, 0x85, 0xea, 0xec, 0x80, 0x1c, + 0x42, 0x84, 0x5f, 0x5f, 0x40, 0x88, 0x78, 0xd1, 0x56, 0x1e, 0x00, 0xd3, 0xd7, 0xde, 0xd2, 0x79, 0x4d, 0x09, 0x4f }); static ge_p3 G_p3; static ge_p3 H_p3; +static ge_p3 T_p3; static ge_cached G_cached; static ge_cached H_cached; +static ge_cached T_cached; // misc static std::once_flag init_gens_once_flag; +//------------------------------------------------------------------------------------------------------------------- +// hash-to-point: H_p(x) = 8*point_from_bytes(keccak(x)) +//------------------------------------------------------------------------------------------------------------------- +static void hash_to_point(const hash &x, crypto::ec_point &point_out) +{ + hash h; + ge_p3 temp_p3; + ge_p2 temp_p2; + ge_p1p1 temp_p1p1; + + cn_fast_hash(reinterpret_cast(&x), sizeof(hash), h); + ge_fromfe_frombytes_vartime(&temp_p2, reinterpret_cast(&h)); + ge_mul8(&temp_p1p1, &temp_p2); + ge_p1p1_to_p3(&temp_p3, &temp_p1p1); + ge_p3_tobytes(to_bytes(point_out), &temp_p3); +} //------------------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------------------- static public_key reproduce_generator_G() @@ -120,6 +142,18 @@ static public_key reproduce_generator_H() return reproduced_H; } //------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static public_key reproduce_generator_T() +{ + // U = H_p(keccak("Monero Generator T")) + const std::string_view T_seed{"Monero Generator T"}; + const hash T_temp_hash{cn_fast_hash(T_seed.data(), T_seed.size())}; + public_key reproduced_T; + hash_to_point(T_temp_hash, reproduced_T); + + return reproduced_T; +} +//------------------------------------------------------------------------------------------------------------------- // Make generators, but only once //------------------------------------------------------------------------------------------------------------------- static void init_gens() @@ -130,21 +164,26 @@ static void init_gens() // sanity check the generators static_assert(static_cast(G.data[0]) == 0x58, "compile-time constant sanity check"); static_assert(static_cast(H.data[0]) == 0x8b, "compile-time constant sanity check"); + static_assert(static_cast(T.data[0]) == 0x96, "compile-time constant sanity check"); // build ge_p3 representations of generators const int G_deserialize = ge_frombytes_vartime(&G_p3, to_bytes(G)); const int H_deserialize = ge_frombytes_vartime(&H_p3, to_bytes(H)); + const int T_deserialize = ge_frombytes_vartime(&T_p3, to_bytes(T)); (void)G_deserialize; assert(G_deserialize == 0); (void)H_deserialize; assert(H_deserialize == 0); + (void)T_deserialize; assert(T_deserialize == 0); // get cached versions ge_p3_to_cached(&G_cached, &G_p3); ge_p3_to_cached(&H_cached, &H_p3); + ge_p3_to_cached(&T_cached, &T_p3); // in debug mode, check that generators are reproducible (void)reproduce_generator_G; assert(reproduce_generator_G() == G); (void)reproduce_generator_H; assert(reproduce_generator_H() == H); + (void)reproduce_generator_T; assert(reproduce_generator_T() == T); }); } @@ -159,6 +198,11 @@ public_key get_H() return H; } //------------------------------------------------------------------------------------------------------------------- +public_key get_T() +{ + return T; +} +//------------------------------------------------------------------------------------------------------------------- ge_p3 get_G_p3() { init_gens(); @@ -171,6 +215,12 @@ ge_p3 get_H_p3() return H_p3; } //------------------------------------------------------------------------------------------------------------------- +ge_p3 get_T_p3() +{ + init_gens(); + return T_p3; +} +//------------------------------------------------------------------------------------------------------------------- ge_cached get_G_cached() { init_gens(); @@ -183,4 +233,10 @@ ge_cached get_H_cached() return H_cached; } //------------------------------------------------------------------------------------------------------------------- +ge_cached get_T_cached() +{ + init_gens(); + return T_cached; +} +//------------------------------------------------------------------------------------------------------------------- } //namespace crypto diff --git a/src/crypto/generators.h b/src/crypto/generators.h index c7d5e693e60..5ab40848aea 100644 --- a/src/crypto/generators.h +++ b/src/crypto/generators.h @@ -39,9 +39,12 @@ namespace crypto public_key get_G(); public_key get_H(); +public_key get_T(); ge_p3 get_G_p3(); ge_p3 get_H_p3(); +ge_p3 get_T_p3(); ge_cached get_G_cached(); ge_cached get_H_cached(); +ge_cached get_T_cached(); } //namespace crypto diff --git a/tests/performance_tests/CMakeLists.txt b/tests/performance_tests/CMakeLists.txt index a1158fcecd6..95d8c1774a0 100644 --- a/tests/performance_tests/CMakeLists.txt +++ b/tests/performance_tests/CMakeLists.txt @@ -55,7 +55,8 @@ set(performance_tests_headers multi_tx_test_base.h performance_tests.h performance_utils.h - single_tx_test_base.h) + single_tx_test_base.h + view_scan.h) monero_add_minimal_executable(performance_tests ${performance_tests_sources} @@ -63,6 +64,7 @@ monero_add_minimal_executable(performance_tests target_link_libraries(performance_tests PRIVATE wallet + carrot_core cryptonote_core common cncrypto diff --git a/tests/performance_tests/main.cpp b/tests/performance_tests/main.cpp index fa8a10540ab..e184f687dfc 100644 --- a/tests/performance_tests/main.cpp +++ b/tests/performance_tests/main.cpp @@ -66,6 +66,7 @@ #include "multiexp.h" #include "sig_mlsag.h" #include "sig_clsag.h" +#include "view_scan.h" namespace po = boost::program_options; @@ -197,6 +198,18 @@ int main(int argc, char** argv) TEST_PERFORMANCE4(filter, p, test_check_hash, 0xffffffffffffffff, 0xffffffffffffffff, 0, 1); TEST_PERFORMANCE4(filter, p, test_check_hash, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff); + // test view scan performance with view tags + ParamsShuttleViewScan p_view_scan; + p_view_scan.core_params = p.core_params; + + TEST_PERFORMANCE0(filter, p_view_scan, test_view_scan_cn); + TEST_PERFORMANCE0(filter, p_view_scan, test_view_scan_cn_optimized); + TEST_PERFORMANCE0(filter, p_view_scan, test_view_scan_carrot); + p_view_scan.test_view_tag_check = true; + TEST_PERFORMANCE0(filter, p_view_scan, test_view_scan_cn); + TEST_PERFORMANCE0(filter, p_view_scan, test_view_scan_cn_optimized); + TEST_PERFORMANCE0(filter, p_view_scan, test_view_scan_carrot); + TEST_PERFORMANCE0(filter, p, test_is_out_to_acc); TEST_PERFORMANCE0(filter, p, test_is_out_to_acc_precomp); TEST_PERFORMANCE2(filter, p, test_out_can_be_to_acc, false, true); // no view tag, owned diff --git a/tests/performance_tests/view_scan.h b/tests/performance_tests/view_scan.h new file mode 100644 index 00000000000..1fc043979de --- /dev/null +++ b/tests/performance_tests/view_scan.h @@ -0,0 +1,307 @@ +// 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 + +#include "carrot_core/carrot_enote_scan.h" +#include "carrot_core/device_ram_borrowed.h" +#include "carrot_core/enote_utils.h" +#include "carrot_core/payment_proposal.h" +#include "crypto/crypto.h" +#include "device/device.hpp" +#include "performance_tests.h" +#include "ringct/rctOps.h" +#include "ringct/rctTypes.h" + +//--------------------------------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------------------------------- + +struct ParamsShuttleViewScan final : public ParamsShuttle +{ + bool test_view_tag_check{false}; +}; + +/// cryptonote view key scanning (with optional view tag check) +/// test: +/// - sender-receiver secret: kv*R_t +/// - view tag: H1(kv*R_t) +/// - (optional): return here to mimick a view tag check failure +/// - Ks_nom = Ko - H(kv*R_t)*G +/// - Ks ?= Ks_nom +class test_view_scan_cn +{ +public: + static const size_t loop_count = 1000; + + bool init(const ParamsShuttleViewScan ¶ms) + { + m_test_view_tag_check = params.test_view_tag_check; + + // kv, Ks = ks*G, R_t = r_t*G + m_view_secret_key = rct::rct2sk(rct::skGen()); + m_spendkey = rct::rct2pk(rct::pkGen()); + m_tx_pub_key = rct::rct2pk(rct::pkGen()); + + // kv*R_t (i.e. r_t*Kv) + crypto::key_derivation derivation; + crypto::generate_key_derivation(m_tx_pub_key, m_view_secret_key, derivation); + + // Ko = H(kv*R_t, t)*G + Ks + crypto::derive_public_key(derivation, 0, m_spendkey, m_onetime_address); + + return true; + } + + bool test() + { + // kv*R_t + crypto::key_derivation derivation; + crypto::generate_key_derivation(m_tx_pub_key, m_view_secret_key, derivation); + + // view tag: H1(kv*R_t, t) + crypto::view_tag mock_view_tag; + crypto::derive_view_tag(derivation, 0, mock_view_tag); + + // check: early return after computing a view tag (e.g. if nominal view tag doesn't match enote view tag) + if (m_test_view_tag_check) + return true; + + // Ks_nom = Ko - H(kv*R_t, t)*G + crypto::public_key nominal_spendkey; + crypto::derive_subaddress_public_key(m_onetime_address, derivation, 0, nominal_spendkey); + + // Ks_nom ?= Ks + return nominal_spendkey == m_spendkey; + } + +private: + /// kv + crypto::secret_key m_view_secret_key; + /// Ks = ks*G + crypto::public_key m_spendkey; + + /// R_t = r_t*G + crypto::public_key m_tx_pub_key; + /// Ko = H(kv*R_t, t)*G + Ks + crypto::public_key m_onetime_address; + + bool m_test_view_tag_check; +}; + +//--------------------------------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------------------------------- + +//// +// cryptonote view key scanning using optimized crypto library (with optional view tag check) +// note: this relies on 'default hwdev' to auto-find the current machine's best available crypto implementation +/// +class test_view_scan_cn_optimized +{ +public: + static const size_t loop_count = 1000; + + bool init(const ParamsShuttleViewScan ¶ms) + { + m_test_view_tag_check = params.test_view_tag_check; + + // kv, Ks = ks*G, R_t = r_t*G + m_view_secret_key = rct::rct2sk(rct::skGen()); + m_spendkey = rct::rct2pk(rct::pkGen()); + m_tx_pub_key = rct::rct2pk(rct::pkGen()); + + // kv*R_t (i.e. r_t*Kv) + crypto::key_derivation derivation; + m_hwdev.generate_key_derivation(m_tx_pub_key, m_view_secret_key, derivation); + + // Ko = H(kv*R_t, t)*G + Ks + m_hwdev.derive_public_key(derivation, 0, m_spendkey, m_onetime_address); + + return true; + } + + bool test() + { + // kv*R_t + crypto::key_derivation derivation; + m_hwdev.generate_key_derivation(m_tx_pub_key, m_view_secret_key, derivation); + + // view tag: H1(kv*R_t, t) + crypto::view_tag mock_view_tag; + m_hwdev.derive_view_tag(derivation, 0, mock_view_tag); + + // check: early return after computing a view tag (e.g. if nominal view tag doesn't match enote view tag) + if (m_test_view_tag_check) + return true; + + // Ks_nom = Ko - H(kv*R_t, t)*G + crypto::public_key nominal_spendkey; + m_hwdev.derive_subaddress_public_key(m_onetime_address, derivation, 0, nominal_spendkey); + + // Ks_nom ?= Ks + return nominal_spendkey == m_spendkey; + } + +private: + hw::device &m_hwdev{hw::get_device("default")}; + + /// kv + crypto::secret_key m_view_secret_key; + /// Ks = ks*G + crypto::public_key m_spendkey; + + /// R_t = r_t*G + crypto::public_key m_tx_pub_key; + /// Ko = H(kv*R_t, t)*G + Ks + crypto::public_key m_onetime_address; + + bool m_test_view_tag_check; +}; + +//--------------------------------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------------------------------- + +/// carrot view key scanning +class test_view_scan_carrot +{ +public: + static const size_t loop_count = 1000; + + test_view_scan_carrot(): m_k_view_dev(m_k_view_incoming) {} + + bool init(const ParamsShuttleViewScan ¶ms) + { + m_test_view_tag_check = params.test_view_tag_check; + + m_k_view_incoming = rct::rct2sk(rct::skGen()); + + m_account_spend_pubkey = rct::rct2pk(rct::pkGen()); + const crypto::public_key account_view_pubkey = rct::rct2pk(rct::scalarmultKey(rct::pk2rct(m_account_spend_pubkey), rct::sk2rct(m_k_view_incoming))); + + carrot::CarrotDestinationV1 subaddress; + carrot::make_carrot_subaddress_v1(m_account_spend_pubkey, account_view_pubkey, {}, 88, 88, subaddress); + + const carrot::CarrotPaymentProposalV1 payment_proposal{ + .destination = subaddress, + .amount = crypto::rand(), + .randomness = carrot::gen_janus_anchor() + }; + + carrot::RCTOutputEnoteProposal output_proposal; + carrot::get_output_proposal_normal_v1(payment_proposal, + {}, + output_proposal, + m_encrypted_payment_id); + m_enote = output_proposal.enote; + + mx25519_pubkey s_sender_receiver_unctx; + carrot::make_carrot_uncontextualized_shared_key_receiver(m_k_view_incoming, + m_enote.enote_ephemeral_pubkey, + s_sender_receiver_unctx); + + crypto::secret_key _1, _2, _3; + crypto::public_key recovered_address_spend_pubkey; + rct::xmr_amount recovered_amount; + carrot::payment_id_t recovered_payment_id; + carrot::CarrotEnoteType recovered_enote_type; + if (!carrot::try_scan_carrot_enote_external(m_enote, + m_encrypted_payment_id, + s_sender_receiver_unctx, + m_k_view_dev, + m_account_spend_pubkey, + _1, + _2, + recovered_address_spend_pubkey, + recovered_amount, + _3, + recovered_payment_id, + recovered_enote_type)) + return false; + + if (recovered_address_spend_pubkey != subaddress.address_spend_pubkey) + return false; + + if (recovered_amount != payment_proposal.amount) + return false; + + if (recovered_payment_id != carrot::null_payment_id) + return false; + + if (recovered_enote_type != carrot::CarrotEnoteType::PAYMENT) + return false; + + if (m_test_view_tag_check) + m_enote.view_tag.bytes[0] ^= 1; + + return true; + } + + bool test() + { + mx25519_pubkey s_sender_receiver_unctx; + carrot::make_carrot_uncontextualized_shared_key_receiver(m_k_view_incoming, + m_enote.enote_ephemeral_pubkey, + s_sender_receiver_unctx); + + crypto::secret_key _1, _2, _3; + crypto::public_key recovered_address_spend_pubkey; + rct::xmr_amount recovered_amount; + carrot::payment_id_t recovered_payment_id; + carrot::CarrotEnoteType recovered_enote_type; + const bool scan_success = carrot::try_scan_carrot_enote_external(m_enote, + m_encrypted_payment_id, + s_sender_receiver_unctx, + m_k_view_dev, + m_account_spend_pubkey, + _1, + _2, + recovered_address_spend_pubkey, + recovered_amount, + _3, + recovered_payment_id, + recovered_enote_type); + + return scan_success ^ m_test_view_tag_check; + } + +private: + crypto::public_key m_account_spend_pubkey; + crypto::secret_key m_k_view_incoming; + carrot::view_incoming_key_ram_borrowed_device m_k_view_dev; + + carrot::CarrotEnoteV1 m_enote; + carrot::encrypted_payment_id_t m_encrypted_payment_id; + + bool m_test_view_tag_check; +}; + +//--------------------------------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------------------------------- diff --git a/tests/unit_tests/CMakeLists.txt b/tests/unit_tests/CMakeLists.txt index e329b7506fa..93aba5a237f 100644 --- a/tests/unit_tests/CMakeLists.txt +++ b/tests/unit_tests/CMakeLists.txt @@ -38,6 +38,9 @@ set(unit_tests_sources bulletproofs.cpp bulletproofs_plus.cpp canonical_amounts.cpp + carrot_core.cpp + carrot_legacy.cpp + carrot_transcript_fixed.cpp chacha.cpp checkpoints.cpp command_line.cpp @@ -102,6 +105,7 @@ set(unit_tests_sources is_hdd.cpp aligned.cpp rpc_version_str.cpp + x25519.cpp zmq_rpc.cpp) set(unit_tests_headers @@ -113,6 +117,7 @@ monero_add_minimal_executable(unit_tests target_link_libraries(unit_tests PRIVATE ringct + carrot_core cryptonote_protocol cryptonote_core daemon_messages diff --git a/tests/unit_tests/carrot_core.cpp b/tests/unit_tests/carrot_core.cpp new file mode 100644 index 00000000000..e4fac4189c6 --- /dev/null +++ b/tests/unit_tests/carrot_core.cpp @@ -0,0 +1,1151 @@ +// 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. + +#include "gtest/gtest.h" + +#include "carrot_core/account_secrets.h" +#include "carrot_core/address_utils.h" +#include "carrot_core/carrot_enote_scan.h" +#include "carrot_core/device_ram_borrowed.h" +#include "carrot_core/enote_utils.h" +#include "carrot_core/output_set_finalization.h" +#include "carrot_core/payment_proposal.h" +#include "crypto/crypto.h" +#include "crypto/generators.h" +#include "ringct/rctOps.h" + +using namespace carrot; + +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +namespace +{ +// https://github.com/jedisct1/libsodium/blob/master/src/libsodium/crypto_scalarmult/curve25519/ref10/x25519_ref10.c#L17 +static const mx25519_pubkey x25519_small_order_points[7] = { + /* 0 (order 4) */ + {{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }}, + /* 1 (order 1) */ + {{ 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }}, + /* 325606250916557431795983626356110631294008115727848805560023387167927233504 + (order 8) */ + {{ 0xe0, 0xeb, 0x7a, 0x7c, 0x3b, 0x41, 0xb8, 0xae, 0x16, 0x56, 0xe3, + 0xfa, 0xf1, 0x9f, 0xc4, 0x6a, 0xda, 0x09, 0x8d, 0xeb, 0x9c, 0x32, + 0xb1, 0xfd, 0x86, 0x62, 0x05, 0x16, 0x5f, 0x49, 0xb8, 0x00 }}, + /* 39382357235489614581723060781553021112529911719440698176882885853963445705823 + (order 8) */ + {{ 0x5f, 0x9c, 0x95, 0xbc, 0xa3, 0x50, 0x8c, 0x24, 0xb1, 0xd0, 0xb1, + 0x55, 0x9c, 0x83, 0xef, 0x5b, 0x04, 0x44, 0x5c, 0xc4, 0x58, 0x1c, + 0x8e, 0x86, 0xd8, 0x22, 0x4e, 0xdd, 0xd0, 0x9f, 0x11, 0x57 }}, + /* p-1 (order 2) */ + {{ 0xec, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f }}, + /* p (=0, order 4) */ + {{ 0xed, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f }}, + /* p+1 (=1, order 1) */ + {{ 0xee, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f }} +}; +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +struct mock_carrot_keys +{ + crypto::secret_key s_master; + crypto::secret_key k_prove_spend; + crypto::secret_key s_view_balance; + crypto::secret_key k_generate_image; + crypto::secret_key k_view; + crypto::secret_key s_generate_address; + crypto::public_key account_spend_pubkey; + crypto::public_key account_view_pubkey; + crypto::public_key main_address_view_pubkey; + + view_incoming_key_ram_borrowed_device k_view_dev; + view_balance_secret_ram_borrowed_device s_view_balance_dev; + + mock_carrot_keys(): k_view_dev(k_view), s_view_balance_dev(s_view_balance) + {} + + static mock_carrot_keys generate() + { + mock_carrot_keys k; + crypto::generate_random_bytes_thread_safe(sizeof(crypto::secret_key), to_bytes(k.s_master)); + make_carrot_provespend_key(k.s_master, k.k_prove_spend); + make_carrot_viewbalance_secret(k.s_master, k.s_view_balance); + make_carrot_generateimage_key(k.s_view_balance, k.k_generate_image); + make_carrot_viewincoming_key(k.s_view_balance, k.k_view); + make_carrot_generateaddress_secret(k.s_view_balance, k.s_generate_address); + make_carrot_spend_pubkey(k.k_generate_image, k.k_prove_spend, k.account_spend_pubkey); + k.account_view_pubkey = rct::rct2pk(rct::scalarmultKey(rct::pk2rct(k.account_spend_pubkey), + rct::sk2rct(k.k_view))); + k.main_address_view_pubkey = rct::rct2pk(rct::scalarmultBase(rct::sk2rct(k.k_view))); + return k; + } +}; +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +static bool can_open_fcmp_onetime_address(const crypto::secret_key &k_prove_spend, + const crypto::secret_key &k_generate_image, + const crypto::secret_key &subaddr_scalar, + const crypto::secret_key &sender_extension_g, + const crypto::secret_key &sender_extension_t, + const crypto::public_key &onetime_address) +{ + // K_s = k_gi G + k_ps T + // K^j_s = k^j_subscal * K_s + // Ko = K^j_s + k^o_g G + k^o_t T + // = (k^o_g + k^j_subscal * k_gi) G + (k^o_t + k^j_subscal * k_ps) T + + // combined_g = k^o_g + k^j_subscal * k_gi + rct::key combined_g; + sc_muladd(combined_g.bytes, to_bytes(subaddr_scalar), to_bytes(k_generate_image), to_bytes(sender_extension_g)); + + // combined_t = k^o_t + k^j_subscal * k_ps + rct::key combined_t; + sc_muladd(combined_t.bytes, to_bytes(subaddr_scalar), to_bytes(k_prove_spend), to_bytes(sender_extension_t)); + + // Ko' = combined_g G + combined_t T + rct::key recomputed_onetime_address; + rct::addKeys2(recomputed_onetime_address, combined_g, combined_t, rct::pk2rct(crypto::get_T())); + + // Ko' ?= Ko + return recomputed_onetime_address == onetime_address; +} +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +struct unittest_carrot_scan_result_t +{ + crypto::public_key address_spend_pubkey = rct::rct2pk(rct::I); + crypto::secret_key sender_extension_g = rct::rct2sk(rct::I); + crypto::secret_key sender_extension_t = rct::rct2sk(rct::I); + + rct::xmr_amount amount = 0; + crypto::secret_key amount_blinding_factor = rct::rct2sk(rct::I); + + CarrotEnoteType enote_type = CarrotEnoteType::PAYMENT; + + payment_id_t payment_id = null_payment_id; + + janus_anchor_t internal_message = janus_anchor_t{}; + + size_t output_index = 0; +}; +static void unittest_scan_enote_set(const std::vector &enotes, + const encrypted_payment_id_t encrypted_payment_id, + const mock_carrot_keys keys, + std::vector &res) +{ + res.clear(); + + // external scans + for (size_t output_index = 0; output_index < enotes.size(); ++output_index) + { + const CarrotEnoteV1 &enote = enotes.at(output_index); + + // s_sr = k_v D_e + mx25519_pubkey s_sr; + make_carrot_uncontextualized_shared_key_receiver(keys.k_view, enote.enote_ephemeral_pubkey, s_sr); + + unittest_carrot_scan_result_t scan_result{}; + const bool r = try_scan_carrot_enote_external(enote, + encrypted_payment_id, + s_sr, + keys.k_view_dev, + keys.account_spend_pubkey, + scan_result.sender_extension_g, + scan_result.sender_extension_t, + scan_result.address_spend_pubkey, + scan_result.amount, + scan_result.amount_blinding_factor, + scan_result.payment_id, + scan_result.enote_type); + + scan_result.output_index = output_index; + + if (r) + res.push_back(scan_result); + } + + // internal scans + for (size_t output_index = 0; output_index < enotes.size(); ++output_index) + { + const CarrotEnoteV1 &enote = enotes.at(output_index); + + unittest_carrot_scan_result_t scan_result{}; + const bool r = try_scan_carrot_enote_internal(enote, + keys.s_view_balance_dev, + scan_result.sender_extension_g, + scan_result.sender_extension_t, + scan_result.address_spend_pubkey, + scan_result.amount, + scan_result.amount_blinding_factor, + scan_result.enote_type, + scan_result.internal_message); + + scan_result.output_index = output_index; + + if (r) + res.push_back(scan_result); + } +} +} // namespace +static inline bool operator==(const mx25519_pubkey &a, const mx25519_pubkey &b) +{ + return 0 == memcmp(&a, &b, sizeof(mx25519_pubkey)); +} +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_core, ECDH_cryptonote_completeness) +{ + crypto::secret_key k_view = rct::rct2sk(rct::skGen()); + crypto::public_key view_pubkey = rct::rct2pk(rct::scalarmultBase(rct::sk2rct(k_view))); + crypto::secret_key k_ephem = rct::rct2sk(rct::skGen()); + ASSERT_NE(k_view, k_ephem); + + mx25519_pubkey enote_ephemeral_pubkey; + make_carrot_enote_ephemeral_pubkey_cryptonote(k_ephem, enote_ephemeral_pubkey); + + mx25519_pubkey s_sr_sender; + ASSERT_TRUE(make_carrot_uncontextualized_shared_key_sender(k_ephem, view_pubkey, s_sr_sender)); + + mx25519_pubkey s_sr_receiver; + ASSERT_TRUE(make_carrot_uncontextualized_shared_key_receiver(k_view, enote_ephemeral_pubkey, s_sr_receiver)); + + EXPECT_EQ(s_sr_sender, s_sr_receiver); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_core, ECDH_subaddress_completeness) +{ + crypto::secret_key k_view = rct::rct2sk(rct::skGen()); + crypto::public_key spend_pubkey = rct::rct2pk(rct::pkGen()); + crypto::public_key view_pubkey = rct::rct2pk(rct::scalarmultKey(rct::pk2rct(spend_pubkey), rct::sk2rct(k_view))); + crypto::secret_key k_ephem = rct::rct2sk(rct::skGen()); + ASSERT_NE(k_view, k_ephem); + + mx25519_pubkey enote_ephemeral_pubkey; + make_carrot_enote_ephemeral_pubkey_subaddress(k_ephem, spend_pubkey, enote_ephemeral_pubkey); + + mx25519_pubkey s_sr_sender; + ASSERT_TRUE(make_carrot_uncontextualized_shared_key_sender(k_ephem, view_pubkey, s_sr_sender)); + + mx25519_pubkey s_sr_receiver; + ASSERT_TRUE(make_carrot_uncontextualized_shared_key_receiver(k_view, enote_ephemeral_pubkey, s_sr_receiver)); + + EXPECT_EQ(s_sr_sender, s_sr_receiver); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_core, ECDH_mx25519_convergence) +{ + const mx25519_pubkey P = gen_x25519_pubkey(); + const crypto::secret_key a = rct::rct2sk(rct::skGen()); + + const mx25519_impl *impl = mx25519_select_impl(MX25519_TYPE_AUTO); + ASSERT_NE(nullptr, impl); + + // do Q = a * P using mx25519 + mx25519_pubkey Q_mx25519; + mx25519_scmul_key(impl, &Q_mx25519, reinterpret_cast(&a), &P); + + // do Q = a * P using make_carrot_uncontextualized_shared_key_receiver() + mx25519_pubkey Q_carrot; + ASSERT_TRUE(make_carrot_uncontextualized_shared_key_receiver(a, P, Q_carrot)); + + // check equal + EXPECT_EQ(Q_mx25519, Q_carrot); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_core, main_address_normal_scan_completeness) +{ + const mock_carrot_keys keys = mock_carrot_keys::generate(); + + CarrotDestinationV1 main_address; + make_carrot_main_address_v1(keys.account_spend_pubkey, keys.main_address_view_pubkey, main_address); + + const CarrotPaymentProposalV1 proposal = CarrotPaymentProposalV1{ + .destination = main_address, + .amount = crypto::rand(), + .randomness = gen_janus_anchor() + }; + + const crypto::key_image tx_first_key_image = rct::rct2ki(rct::pkGen()); + + RCTOutputEnoteProposal enote_proposal; + encrypted_payment_id_t encrypted_payment_id; + get_output_proposal_normal_v1(proposal, + tx_first_key_image, + enote_proposal, + encrypted_payment_id); + + ASSERT_EQ(proposal.amount, enote_proposal.amount); + const rct::key recomputed_amount_commitment = rct::commit(enote_proposal.amount, rct::sk2rct(enote_proposal.amount_blinding_factor)); + ASSERT_EQ(enote_proposal.enote.amount_commitment, recomputed_amount_commitment); + + mx25519_pubkey s_sender_receiver_unctx; + make_carrot_uncontextualized_shared_key_receiver(keys.k_view, + enote_proposal.enote.enote_ephemeral_pubkey, + s_sender_receiver_unctx); + + crypto::secret_key recovered_sender_extension_g; + crypto::secret_key recovered_sender_extension_t; + crypto::public_key recovered_address_spend_pubkey; + rct::xmr_amount recovered_amount; + crypto::secret_key recovered_amount_blinding_factor; + encrypted_payment_id_t recovered_payment_id; + CarrotEnoteType recovered_enote_type; + const bool scan_success = try_scan_carrot_enote_external(enote_proposal.enote, + encrypted_payment_id, + s_sender_receiver_unctx, + keys.k_view_dev, + keys.account_spend_pubkey, + recovered_sender_extension_g, + recovered_sender_extension_t, + recovered_address_spend_pubkey, + recovered_amount, + recovered_amount_blinding_factor, + recovered_payment_id, + recovered_enote_type); + + ASSERT_TRUE(scan_success); + + // check recovered data + EXPECT_EQ(proposal.destination.address_spend_pubkey, recovered_address_spend_pubkey); + EXPECT_EQ(proposal.amount, recovered_amount); + EXPECT_EQ(enote_proposal.amount_blinding_factor, recovered_amount_blinding_factor); + EXPECT_EQ(null_payment_id, recovered_payment_id); + EXPECT_EQ(CarrotEnoteType::PAYMENT, recovered_enote_type); + + // check spendability + EXPECT_TRUE(can_open_fcmp_onetime_address(keys.k_prove_spend, + keys.k_generate_image, + rct::rct2sk(rct::I), + recovered_sender_extension_g, + recovered_sender_extension_t, + enote_proposal.enote.onetime_address)); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_core, subaddress_normal_scan_completeness) +{ + const mock_carrot_keys keys = mock_carrot_keys::generate(); + + const uint32_t j_major = crypto::rand(); + const uint32_t j_minor = crypto::rand(); + + CarrotDestinationV1 subaddress; + make_carrot_subaddress_v1(keys.account_spend_pubkey, + keys.account_view_pubkey, + keys.s_generate_address, + j_major, + j_minor, + subaddress); + + const CarrotPaymentProposalV1 proposal = CarrotPaymentProposalV1{ + .destination = subaddress, + .amount = crypto::rand(), + .randomness = gen_janus_anchor() + }; + + const crypto::key_image tx_first_key_image = rct::rct2ki(rct::pkGen()); + + RCTOutputEnoteProposal enote_proposal; + encrypted_payment_id_t encrypted_payment_id; + get_output_proposal_normal_v1(proposal, + tx_first_key_image, + enote_proposal, + encrypted_payment_id); + + ASSERT_EQ(proposal.amount, enote_proposal.amount); + const rct::key recomputed_amount_commitment = rct::commit(enote_proposal.amount, rct::sk2rct(enote_proposal.amount_blinding_factor)); + ASSERT_EQ(enote_proposal.enote.amount_commitment, recomputed_amount_commitment); + + mx25519_pubkey s_sender_receiver_unctx; + make_carrot_uncontextualized_shared_key_receiver(keys.k_view, + enote_proposal.enote.enote_ephemeral_pubkey, + s_sender_receiver_unctx); + + crypto::secret_key recovered_sender_extension_g; + crypto::secret_key recovered_sender_extension_t; + crypto::public_key recovered_address_spend_pubkey; + rct::xmr_amount recovered_amount; + crypto::secret_key recovered_amount_blinding_factor; + encrypted_payment_id_t recovered_payment_id; + CarrotEnoteType recovered_enote_type; + const bool scan_success = try_scan_carrot_enote_external(enote_proposal.enote, + encrypted_payment_id, + s_sender_receiver_unctx, + keys.k_view_dev, + keys.account_spend_pubkey, + recovered_sender_extension_g, + recovered_sender_extension_t, + recovered_address_spend_pubkey, + recovered_amount, + recovered_amount_blinding_factor, + recovered_payment_id, + recovered_enote_type); + + ASSERT_TRUE(scan_success); + + // check recovered data + EXPECT_EQ(proposal.destination.address_spend_pubkey, recovered_address_spend_pubkey); + EXPECT_EQ(proposal.amount, recovered_amount); + EXPECT_EQ(enote_proposal.amount_blinding_factor, recovered_amount_blinding_factor); + EXPECT_EQ(null_payment_id, recovered_payment_id); + EXPECT_EQ(CarrotEnoteType::PAYMENT, recovered_enote_type); + + // check spendability + crypto::secret_key address_generator; + make_carrot_index_extension_generator(keys.s_generate_address, + j_major, + j_minor, + address_generator); + + crypto::secret_key subaddr_scalar; + make_carrot_subaddress_scalar(keys.account_spend_pubkey, + address_generator, + j_major, + j_minor, + subaddr_scalar); + + EXPECT_TRUE(can_open_fcmp_onetime_address(keys.k_prove_spend, + keys.k_generate_image, + subaddr_scalar, + recovered_sender_extension_g, + recovered_sender_extension_t, + enote_proposal.enote.onetime_address)); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_core, integrated_address_normal_scan_completeness) +{ + const mock_carrot_keys keys = mock_carrot_keys::generate(); + + CarrotDestinationV1 integrated_address; + make_carrot_integrated_address_v1(keys.account_spend_pubkey, + keys.main_address_view_pubkey, + gen_payment_id(), + integrated_address); + + const CarrotPaymentProposalV1 proposal = CarrotPaymentProposalV1{ + .destination = integrated_address, + .amount = crypto::rand(), + .randomness = gen_janus_anchor() + }; + + const crypto::key_image tx_first_key_image = rct::rct2ki(rct::pkGen()); + + RCTOutputEnoteProposal enote_proposal; + encrypted_payment_id_t encrypted_payment_id; + get_output_proposal_normal_v1(proposal, + tx_first_key_image, + enote_proposal, + encrypted_payment_id); + + ASSERT_EQ(proposal.amount, enote_proposal.amount); + const rct::key recomputed_amount_commitment = rct::commit(enote_proposal.amount, rct::sk2rct(enote_proposal.amount_blinding_factor)); + ASSERT_EQ(enote_proposal.enote.amount_commitment, recomputed_amount_commitment); + + mx25519_pubkey s_sender_receiver_unctx; + make_carrot_uncontextualized_shared_key_receiver(keys.k_view, + enote_proposal.enote.enote_ephemeral_pubkey, + s_sender_receiver_unctx); + + crypto::secret_key recovered_sender_extension_g; + crypto::secret_key recovered_sender_extension_t; + crypto::public_key recovered_address_spend_pubkey; + rct::xmr_amount recovered_amount; + crypto::secret_key recovered_amount_blinding_factor; + encrypted_payment_id_t recovered_payment_id; + CarrotEnoteType recovered_enote_type; + const bool scan_success = try_scan_carrot_enote_external(enote_proposal.enote, + encrypted_payment_id, + s_sender_receiver_unctx, + keys.k_view_dev, + keys.account_spend_pubkey, + recovered_sender_extension_g, + recovered_sender_extension_t, + recovered_address_spend_pubkey, + recovered_amount, + recovered_amount_blinding_factor, + recovered_payment_id, + recovered_enote_type); + + ASSERT_TRUE(scan_success); + + // check recovered data + EXPECT_EQ(proposal.destination.address_spend_pubkey, recovered_address_spend_pubkey); + EXPECT_EQ(proposal.amount, recovered_amount); + EXPECT_EQ(enote_proposal.amount_blinding_factor, recovered_amount_blinding_factor); + EXPECT_EQ(integrated_address.payment_id, recovered_payment_id); + EXPECT_EQ(CarrotEnoteType::PAYMENT, recovered_enote_type); + + // check spendability + EXPECT_TRUE(can_open_fcmp_onetime_address(keys.k_prove_spend, + keys.k_generate_image, + rct::rct2sk(rct::I), + recovered_sender_extension_g, + recovered_sender_extension_t, + enote_proposal.enote.onetime_address)); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_core, main_address_special_scan_completeness) +{ + const mock_carrot_keys keys = mock_carrot_keys::generate(); + + CarrotDestinationV1 main_address; + make_carrot_main_address_v1(keys.account_spend_pubkey, keys.main_address_view_pubkey, main_address); + + // try once with PAYMENT, once with CHANGE + for (int i = 0; i < 2; ++i) + { + const CarrotEnoteType enote_type = i ? CarrotEnoteType::PAYMENT : CarrotEnoteType::CHANGE; + + const CarrotPaymentProposalSelfSendV1 proposal = CarrotPaymentProposalSelfSendV1{ + .destination_address_spend_pubkey = keys.account_spend_pubkey, + .amount = crypto::rand(), + .enote_type = enote_type, + .enote_ephemeral_pubkey = gen_x25519_pubkey(), + }; + + const crypto::key_image tx_first_key_image = rct::rct2ki(rct::pkGen()); + + RCTOutputEnoteProposal enote_proposal; + get_output_proposal_special_v1(proposal, + keys.k_view_dev, + keys.account_spend_pubkey, + tx_first_key_image, + std::nullopt, + enote_proposal); + + ASSERT_EQ(proposal.amount, enote_proposal.amount); + const rct::key recomputed_amount_commitment = rct::commit(enote_proposal.amount, rct::sk2rct(enote_proposal.amount_blinding_factor)); + ASSERT_EQ(enote_proposal.enote.amount_commitment, recomputed_amount_commitment); + + mx25519_pubkey s_sender_receiver_unctx; + make_carrot_uncontextualized_shared_key_receiver(keys.k_view, + enote_proposal.enote.enote_ephemeral_pubkey, + s_sender_receiver_unctx); + + crypto::secret_key recovered_sender_extension_g; + crypto::secret_key recovered_sender_extension_t; + crypto::public_key recovered_address_spend_pubkey; + rct::xmr_amount recovered_amount; + crypto::secret_key recovered_amount_blinding_factor; + encrypted_payment_id_t recovered_payment_id; + CarrotEnoteType recovered_enote_type; + const bool scan_success = try_scan_carrot_enote_external(enote_proposal.enote, + std::nullopt, + s_sender_receiver_unctx, + keys.k_view_dev, + keys.account_spend_pubkey, + recovered_sender_extension_g, + recovered_sender_extension_t, + recovered_address_spend_pubkey, + recovered_amount, + recovered_amount_blinding_factor, + recovered_payment_id, + recovered_enote_type); + + ASSERT_TRUE(scan_success); + + // check recovered data + EXPECT_EQ(proposal.destination_address_spend_pubkey, recovered_address_spend_pubkey); + EXPECT_EQ(proposal.amount, recovered_amount); + EXPECT_EQ(enote_proposal.amount_blinding_factor, recovered_amount_blinding_factor); + EXPECT_EQ(null_payment_id, recovered_payment_id); + EXPECT_EQ(enote_type, recovered_enote_type); + + // check spendability + EXPECT_TRUE(can_open_fcmp_onetime_address(keys.k_prove_spend, + keys.k_generate_image, + rct::rct2sk(rct::I), + recovered_sender_extension_g, + recovered_sender_extension_t, + enote_proposal.enote.onetime_address)); + } +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_core, subaddress_special_scan_completeness) +{ + const mock_carrot_keys keys = mock_carrot_keys::generate(); + + const uint32_t j_major = crypto::rand(); + const uint32_t j_minor = crypto::rand(); + + CarrotDestinationV1 subaddress; + make_carrot_subaddress_v1(keys.account_spend_pubkey, + keys.account_view_pubkey, + keys.s_generate_address, + j_major, + j_minor, + subaddress); + + // try once with PAYMENT, once with CHANGE + for (int i = 0; i < 2; ++i) + { + const CarrotEnoteType enote_type = i ? CarrotEnoteType::PAYMENT : CarrotEnoteType::CHANGE; + + const CarrotPaymentProposalSelfSendV1 proposal = CarrotPaymentProposalSelfSendV1{ + .destination_address_spend_pubkey = subaddress.address_spend_pubkey, + .amount = crypto::rand(), + .enote_type = enote_type, + .enote_ephemeral_pubkey = gen_x25519_pubkey(), + }; + + const crypto::key_image tx_first_key_image = rct::rct2ki(rct::pkGen()); + + RCTOutputEnoteProposal enote_proposal; + get_output_proposal_special_v1(proposal, + keys.k_view_dev, + keys.account_spend_pubkey, + tx_first_key_image, + std::nullopt, + enote_proposal); + + ASSERT_EQ(proposal.amount, enote_proposal.amount); + const rct::key recomputed_amount_commitment = rct::commit(enote_proposal.amount, rct::sk2rct(enote_proposal.amount_blinding_factor)); + ASSERT_EQ(enote_proposal.enote.amount_commitment, recomputed_amount_commitment); + + mx25519_pubkey s_sender_receiver_unctx; + make_carrot_uncontextualized_shared_key_receiver(keys.k_view, + enote_proposal.enote.enote_ephemeral_pubkey, + s_sender_receiver_unctx); + + crypto::secret_key recovered_sender_extension_g; + crypto::secret_key recovered_sender_extension_t; + crypto::public_key recovered_address_spend_pubkey; + rct::xmr_amount recovered_amount; + crypto::secret_key recovered_amount_blinding_factor; + encrypted_payment_id_t recovered_payment_id; + CarrotEnoteType recovered_enote_type; + const bool scan_success = try_scan_carrot_enote_external(enote_proposal.enote, + std::nullopt, + s_sender_receiver_unctx, + keys.k_view_dev, + keys.account_spend_pubkey, + recovered_sender_extension_g, + recovered_sender_extension_t, + recovered_address_spend_pubkey, + recovered_amount, + recovered_amount_blinding_factor, + recovered_payment_id, + recovered_enote_type); + + ASSERT_TRUE(scan_success); + + // check recovered data + EXPECT_EQ(proposal.destination_address_spend_pubkey, recovered_address_spend_pubkey); + EXPECT_EQ(proposal.amount, recovered_amount); + EXPECT_EQ(enote_proposal.amount_blinding_factor, recovered_amount_blinding_factor); + EXPECT_EQ(null_payment_id, recovered_payment_id); + EXPECT_EQ(enote_type, recovered_enote_type); + + // check spendability + crypto::secret_key address_generator; + make_carrot_index_extension_generator(keys.s_generate_address, + j_major, + j_minor, + address_generator); + + crypto::secret_key subaddr_scalar; + make_carrot_subaddress_scalar(keys.account_spend_pubkey, + address_generator, + j_major, + j_minor, + subaddr_scalar); + + EXPECT_TRUE(can_open_fcmp_onetime_address(keys.k_prove_spend, + keys.k_generate_image, + subaddr_scalar, + recovered_sender_extension_g, + recovered_sender_extension_t, + enote_proposal.enote.onetime_address)); + } +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_core, main_address_internal_scan_completeness) +{ + const mock_carrot_keys keys = mock_carrot_keys::generate(); + + CarrotDestinationV1 main_address; + make_carrot_main_address_v1(keys.account_spend_pubkey, keys.main_address_view_pubkey, main_address); + + // try once with PAYMENT, once with CHANGE + for (int i = 0; i < 2; ++i) + { + const CarrotEnoteType enote_type = i ? CarrotEnoteType::PAYMENT : CarrotEnoteType::CHANGE; + + const CarrotPaymentProposalSelfSendV1 proposal = CarrotPaymentProposalSelfSendV1{ + .destination_address_spend_pubkey = main_address.address_spend_pubkey, + .amount = crypto::rand(), + .enote_type = enote_type, + .enote_ephemeral_pubkey = gen_x25519_pubkey(), + .internal_message = gen_janus_anchor() + }; + + const crypto::key_image tx_first_key_image = rct::rct2ki(rct::pkGen()); + + RCTOutputEnoteProposal enote_proposal; + get_output_proposal_internal_v1(proposal, + keys.s_view_balance_dev, + tx_first_key_image, + std::nullopt, + enote_proposal); + + ASSERT_EQ(proposal.amount, enote_proposal.amount); + const rct::key recomputed_amount_commitment = rct::commit(enote_proposal.amount, rct::sk2rct(enote_proposal.amount_blinding_factor)); + ASSERT_EQ(enote_proposal.enote.amount_commitment, recomputed_amount_commitment); + + crypto::secret_key recovered_sender_extension_g; + crypto::secret_key recovered_sender_extension_t; + crypto::public_key recovered_address_spend_pubkey; + rct::xmr_amount recovered_amount; + crypto::secret_key recovered_amount_blinding_factor; + CarrotEnoteType recovered_enote_type; + janus_anchor_t recovered_internal_message; + const bool scan_success = try_scan_carrot_enote_internal(enote_proposal.enote, + keys.s_view_balance_dev, + recovered_sender_extension_g, + recovered_sender_extension_t, + recovered_address_spend_pubkey, + recovered_amount, + recovered_amount_blinding_factor, + recovered_enote_type, + recovered_internal_message); + + ASSERT_TRUE(scan_success); + + // check recovered data + EXPECT_EQ(proposal.destination_address_spend_pubkey, recovered_address_spend_pubkey); + EXPECT_EQ(proposal.amount, recovered_amount); + EXPECT_EQ(enote_proposal.amount_blinding_factor, recovered_amount_blinding_factor); + EXPECT_EQ(enote_type, recovered_enote_type); + EXPECT_EQ(proposal.internal_message, recovered_internal_message); + + // check spendability + EXPECT_TRUE(can_open_fcmp_onetime_address(keys.k_prove_spend, + keys.k_generate_image, + rct::rct2sk(rct::I), + recovered_sender_extension_g, + recovered_sender_extension_t, + enote_proposal.enote.onetime_address)); + } +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_core, subaddress_internal_scan_completeness) +{ + const mock_carrot_keys keys = mock_carrot_keys::generate(); + + const uint32_t j_major = crypto::rand(); + const uint32_t j_minor = crypto::rand(); + + CarrotDestinationV1 subaddress; + make_carrot_subaddress_v1(keys.account_spend_pubkey, + keys.account_view_pubkey, + keys.s_generate_address, + j_major, + j_minor, + subaddress); + + // try once with PAYMENT, once with CHANGE + for (int i = 0; i < 2; ++i) + { + const CarrotEnoteType enote_type = i ? CarrotEnoteType::PAYMENT : CarrotEnoteType::CHANGE; + + const CarrotPaymentProposalSelfSendV1 proposal = CarrotPaymentProposalSelfSendV1{ + .destination_address_spend_pubkey = subaddress.address_spend_pubkey, + .amount = crypto::rand(), + .enote_type = enote_type, + .enote_ephemeral_pubkey = gen_x25519_pubkey(), + .internal_message = gen_janus_anchor() + }; + + const crypto::key_image tx_first_key_image = rct::rct2ki(rct::pkGen()); + + RCTOutputEnoteProposal enote_proposal; + get_output_proposal_internal_v1(proposal, + keys.s_view_balance_dev, + tx_first_key_image, + std::nullopt, + enote_proposal); + + ASSERT_EQ(proposal.amount, enote_proposal.amount); + const rct::key recomputed_amount_commitment = rct::commit(enote_proposal.amount, rct::sk2rct(enote_proposal.amount_blinding_factor)); + ASSERT_EQ(enote_proposal.enote.amount_commitment, recomputed_amount_commitment); + + crypto::secret_key recovered_sender_extension_g; + crypto::secret_key recovered_sender_extension_t; + crypto::public_key recovered_address_spend_pubkey; + rct::xmr_amount recovered_amount; + crypto::secret_key recovered_amount_blinding_factor; + CarrotEnoteType recovered_enote_type; + janus_anchor_t recovered_internal_message; + const bool scan_success = try_scan_carrot_enote_internal(enote_proposal.enote, + keys.s_view_balance_dev, + recovered_sender_extension_g, + recovered_sender_extension_t, + recovered_address_spend_pubkey, + recovered_amount, + recovered_amount_blinding_factor, + recovered_enote_type, + recovered_internal_message); + + ASSERT_TRUE(scan_success); + + // check recovered data + EXPECT_EQ(proposal.destination_address_spend_pubkey, recovered_address_spend_pubkey); + EXPECT_EQ(proposal.amount, recovered_amount); + EXPECT_EQ(enote_proposal.amount_blinding_factor, recovered_amount_blinding_factor); + EXPECT_EQ(enote_type, recovered_enote_type); + EXPECT_EQ(proposal.internal_message, recovered_internal_message); + + // check spendability + crypto::secret_key address_generator; + make_carrot_index_extension_generator(keys.s_generate_address, + j_major, + j_minor, + address_generator); + + crypto::secret_key subaddr_scalar; + make_carrot_subaddress_scalar(keys.account_spend_pubkey, + address_generator, + j_major, + j_minor, + subaddr_scalar); + + EXPECT_TRUE(can_open_fcmp_onetime_address(keys.k_prove_spend, + keys.k_generate_image, + subaddr_scalar, + recovered_sender_extension_g, + recovered_sender_extension_t, + enote_proposal.enote.onetime_address)); + } +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_core, main_address_coinbase_scan_completeness) +{ + const mock_carrot_keys keys = mock_carrot_keys::generate(); + + CarrotDestinationV1 main_address; + make_carrot_main_address_v1(keys.account_spend_pubkey, keys.main_address_view_pubkey, main_address); + + const CarrotPaymentProposalV1 proposal = CarrotPaymentProposalV1{ + .destination = main_address, + .amount = crypto::rand(), + .randomness = gen_janus_anchor() + }; + + const uint64_t block_index = crypto::rand(); + + CarrotCoinbaseEnoteV1 enote; + get_coinbase_output_proposal_v1(proposal, + block_index, + enote); + + ASSERT_EQ(proposal.amount, enote.amount); + + mx25519_pubkey s_sender_receiver_unctx; + make_carrot_uncontextualized_shared_key_receiver(keys.k_view, + enote.enote_ephemeral_pubkey, + s_sender_receiver_unctx); + + crypto::secret_key recovered_sender_extension_g; + crypto::secret_key recovered_sender_extension_t; + crypto::public_key recovered_address_spend_pubkey; + const bool scan_success = try_scan_carrot_coinbase_enote(enote, + s_sender_receiver_unctx, + keys.k_view_dev, + keys.account_spend_pubkey, + recovered_sender_extension_g, + recovered_sender_extension_t, + recovered_address_spend_pubkey); + + ASSERT_TRUE(scan_success); + + // check recovered data + EXPECT_EQ(proposal.destination.address_spend_pubkey, recovered_address_spend_pubkey); + + // check spendability + EXPECT_TRUE(can_open_fcmp_onetime_address(keys.k_prove_spend, + keys.k_generate_image, + rct::rct2sk(rct::I), + recovered_sender_extension_g, + recovered_sender_extension_t, + enote.onetime_address)); +} +//---------------------------------------------------------------------------------------------------------------------- +static void subtest_2out_transfer_get_output_enote_proposals_completeness(const bool alice_subaddress, + const bool bob_subaddress, + const bool bob_integrated, + const CarrotEnoteType alice_selfsend_type, + const bool alice_internal_selfsends) +{ + // generate alice keys and address + const mock_carrot_keys alice = mock_carrot_keys::generate(); + const uint32_t alice_j_major = crypto::rand(); + const uint32_t alice_j_minor = crypto::rand(); + CarrotDestinationV1 alice_address; + if (alice_subaddress) + { + make_carrot_subaddress_v1(alice.account_spend_pubkey, + alice.account_view_pubkey, + alice.s_generate_address, + alice_j_major, + alice_j_minor, + alice_address); + } + else + { + make_carrot_main_address_v1(alice.account_spend_pubkey, + alice.main_address_view_pubkey, + alice_address); + } + + // generate bob keys and address + const mock_carrot_keys bob = mock_carrot_keys::generate(); + const uint32_t bob_j_major = crypto::rand(); + const uint32_t bob_j_minor = crypto::rand(); + CarrotDestinationV1 bob_address; + if (bob_subaddress) + { + make_carrot_subaddress_v1(bob.account_spend_pubkey, + bob.account_view_pubkey, + bob.s_generate_address, + bob_j_major, + bob_j_minor, + bob_address); + } + else if (bob_integrated) + { + make_carrot_integrated_address_v1(bob.account_spend_pubkey, + bob.main_address_view_pubkey, + gen_payment_id(), + bob_address); + } + else + { + make_carrot_main_address_v1(bob.account_spend_pubkey, + bob.main_address_view_pubkey, + bob_address); + } + + // generate input context + const crypto::key_image tx_first_key_image = rct::rct2ki(rct::pkGen()); + input_context_t input_context; + make_carrot_input_context(tx_first_key_image, input_context); + + // outgoing payment proposal to bob + const CarrotPaymentProposalV1 bob_payment_proposal = CarrotPaymentProposalV1{ + .destination = bob_address, + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + + // selfsend payment proposal to alice + const CarrotPaymentProposalSelfSendV1 alice_payment_proposal = CarrotPaymentProposalSelfSendV1{ + .destination_address_spend_pubkey = alice_address.address_spend_pubkey, + .amount = crypto::rand_idx(1000000), + .enote_type = CarrotEnoteType::CHANGE, + .enote_ephemeral_pubkey = get_enote_ephemeral_pubkey(bob_payment_proposal, input_context), + .internal_message = alice_internal_selfsends ? std::make_optional(gen_janus_anchor()) : std::nullopt + }; + + // turn payment proposals into enotes + std::vector enote_proposals; + encrypted_payment_id_t encrypted_payment_id; + get_output_enote_proposals({bob_payment_proposal}, + {alice_payment_proposal}, + alice_internal_selfsends ? &alice.s_view_balance_dev : nullptr, + &alice.k_view_dev, + alice.account_spend_pubkey, + tx_first_key_image, + enote_proposals, + encrypted_payment_id); + + ASSERT_EQ(2, enote_proposals.size()); // 2-out tx + + // collect enotes + std::vector enotes; + for (const RCTOutputEnoteProposal &enote_proposal : enote_proposals) + enotes.push_back(enote_proposal.enote); + + // check that alice scanned 1 enote + std::vector alice_scan_vec; + unittest_scan_enote_set(enotes, encrypted_payment_id, alice, alice_scan_vec); + ASSERT_EQ(1, alice_scan_vec.size()); + unittest_carrot_scan_result_t alice_scan = alice_scan_vec.front(); + + // check that bob scanned 1 enote + std::vector bob_scan_vec; + unittest_scan_enote_set(enotes, encrypted_payment_id, bob, bob_scan_vec); + ASSERT_EQ(1, bob_scan_vec.size()); + unittest_carrot_scan_result_t bob_scan = bob_scan_vec.front(); + + // set named references to enotes + ASSERT_TRUE((alice_scan.output_index == 0 && bob_scan.output_index == 1) || + (alice_scan.output_index == 1 && bob_scan.output_index == 0)); + const CarrotEnoteV1 &alice_enote = enotes.at(alice_scan.output_index); + const CarrotEnoteV1 &bob_enote = enotes.at(bob_scan.output_index); + + // check Alice's recovered data + EXPECT_EQ(alice_payment_proposal.destination_address_spend_pubkey, alice_scan.address_spend_pubkey); + EXPECT_EQ(alice_payment_proposal.amount, alice_scan.amount); + EXPECT_EQ(alice_enote.amount_commitment, rct::commit(alice_scan.amount, rct::sk2rct(alice_scan.amount_blinding_factor))); + EXPECT_EQ(null_payment_id, alice_scan.payment_id); + EXPECT_EQ(alice_payment_proposal.enote_type, alice_scan.enote_type); + if (alice_internal_selfsends) + { + EXPECT_EQ(alice_payment_proposal.internal_message, alice_scan.internal_message); + } + + // check Bob's recovered data + EXPECT_EQ(bob_payment_proposal.destination.address_spend_pubkey, bob_scan.address_spend_pubkey); + EXPECT_EQ(bob_payment_proposal.amount, bob_scan.amount); + EXPECT_EQ(bob_enote.amount_commitment, rct::commit(bob_scan.amount, rct::sk2rct(bob_scan.amount_blinding_factor))); + EXPECT_EQ(bob_integrated ? bob_address.payment_id : null_payment_id, bob_scan.payment_id); + EXPECT_EQ(CarrotEnoteType::PAYMENT, bob_scan.enote_type); + + // check Alice spendability + crypto::secret_key alice_address_generator; + make_carrot_index_extension_generator(alice.s_generate_address, + alice_j_major, + alice_j_minor, + alice_address_generator); + + crypto::secret_key alice_subaddr_scalar; + make_carrot_subaddress_scalar(alice.account_spend_pubkey, + alice_address_generator, + alice_j_major, + alice_j_minor, + alice_subaddr_scalar); + + EXPECT_TRUE(can_open_fcmp_onetime_address(alice.k_prove_spend, + alice.k_generate_image, + alice_subaddress ? alice_subaddr_scalar : crypto::secret_key{{1}}, + alice_scan.sender_extension_g, + alice_scan.sender_extension_t, + alice_enote.onetime_address)); + + // check Bob spendability + crypto::secret_key bob_address_generator; + make_carrot_index_extension_generator(bob.s_generate_address, + bob_j_major, + bob_j_minor, + bob_address_generator); + + crypto::secret_key bob_subaddr_scalar; + make_carrot_subaddress_scalar(bob.account_spend_pubkey, + bob_address_generator, + bob_j_major, + bob_j_minor, + bob_subaddr_scalar); + + EXPECT_TRUE(can_open_fcmp_onetime_address(bob.k_prove_spend, + bob.k_generate_image, + bob_subaddress ? bob_subaddr_scalar : crypto::secret_key{{1}}, + bob_scan.sender_extension_g, + bob_scan.sender_extension_t, + bob_enote.onetime_address)); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_core, get_enote_output_proposals_internal_ss_main2main_completeness) +{ + subtest_2out_transfer_get_output_enote_proposals_completeness(false, false, false, CarrotEnoteType::PAYMENT, true); + subtest_2out_transfer_get_output_enote_proposals_completeness(false, false, false, CarrotEnoteType::CHANGE, true); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_core, get_enote_output_proposals_internal_ss_main2sub_completeness) +{ + subtest_2out_transfer_get_output_enote_proposals_completeness(false, true, false, CarrotEnoteType::PAYMENT, true); + subtest_2out_transfer_get_output_enote_proposals_completeness(false, true, false, CarrotEnoteType::CHANGE, true); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_core, get_enote_output_proposals_internal_ss_main2integ_completeness) +{ + subtest_2out_transfer_get_output_enote_proposals_completeness(false, false, true, CarrotEnoteType::PAYMENT, true); + subtest_2out_transfer_get_output_enote_proposals_completeness(false, false, true, CarrotEnoteType::CHANGE, true); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_core, get_enote_output_proposals_internal_ss_sub2main_completeness) +{ + subtest_2out_transfer_get_output_enote_proposals_completeness(true, false, false, CarrotEnoteType::PAYMENT, true); + subtest_2out_transfer_get_output_enote_proposals_completeness(true, false, false, CarrotEnoteType::CHANGE, true); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_core, get_enote_output_proposals_internal_ss_sub2sub_completeness) +{ + subtest_2out_transfer_get_output_enote_proposals_completeness(true, true, false, CarrotEnoteType::PAYMENT, true); + subtest_2out_transfer_get_output_enote_proposals_completeness(true, true, false, CarrotEnoteType::CHANGE, true); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_core, get_enote_output_proposals_internal_ss_sub2integ_completeness) +{ + subtest_2out_transfer_get_output_enote_proposals_completeness(true, false, true, CarrotEnoteType::PAYMENT, true); + subtest_2out_transfer_get_output_enote_proposals_completeness(true, false, true, CarrotEnoteType::CHANGE, true); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_core, get_enote_output_proposals_external_ss_main2main_completeness) +{ + subtest_2out_transfer_get_output_enote_proposals_completeness(false, false, false, CarrotEnoteType::PAYMENT, false); + subtest_2out_transfer_get_output_enote_proposals_completeness(false, false, false, CarrotEnoteType::CHANGE, false); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_core, get_enote_output_proposals_external_ss_main2sub_completeness) +{ + subtest_2out_transfer_get_output_enote_proposals_completeness(false, true, false, CarrotEnoteType::PAYMENT, false); + subtest_2out_transfer_get_output_enote_proposals_completeness(false, true, false, CarrotEnoteType::CHANGE, false); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_core, get_enote_output_proposals_external_ss_main2integ_completeness) +{ + subtest_2out_transfer_get_output_enote_proposals_completeness(false, false, true, CarrotEnoteType::PAYMENT, false); + subtest_2out_transfer_get_output_enote_proposals_completeness(false, false, true, CarrotEnoteType::CHANGE, false); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_core, get_enote_output_proposals_external_ss_sub2main_completeness) +{ + subtest_2out_transfer_get_output_enote_proposals_completeness(true, false, false, CarrotEnoteType::PAYMENT, false); + subtest_2out_transfer_get_output_enote_proposals_completeness(true, false, false, CarrotEnoteType::CHANGE, false); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_core, get_enote_output_proposals_external_ss_sub2sub_completeness) +{ + subtest_2out_transfer_get_output_enote_proposals_completeness(true, true, false, CarrotEnoteType::PAYMENT, false); + subtest_2out_transfer_get_output_enote_proposals_completeness(true, true, false, CarrotEnoteType::CHANGE, false); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_core, get_enote_output_proposals_external_ss_sub2integ_completeness) +{ + subtest_2out_transfer_get_output_enote_proposals_completeness(true, false, true, CarrotEnoteType::PAYMENT, false); + subtest_2out_transfer_get_output_enote_proposals_completeness(true, false, true, CarrotEnoteType::CHANGE, false); +} +//---------------------------------------------------------------------------------------------------------------------- diff --git a/tests/unit_tests/carrot_legacy.cpp b/tests/unit_tests/carrot_legacy.cpp new file mode 100644 index 00000000000..d94d49fc9a3 --- /dev/null +++ b/tests/unit_tests/carrot_legacy.cpp @@ -0,0 +1,291 @@ +// 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. + +#include "gtest/gtest.h" + +#include "carrot_core/carrot_enote_scan.h" +#include "carrot_core/device_ram_borrowed.h" +#include "carrot_core/enote_utils.h" +#include "carrot_core/output_set_finalization.h" +#include "carrot_core/payment_proposal.h" +#include "crypto/crypto.h" +#include "crypto/generators.h" +#include "cryptonote_basic/account.h" +#include "cryptonote_basic/subaddress_index.h" +#include "device/device_default.hpp" +#include "ringct/rctOps.h" + +using namespace carrot; + +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +static bool can_open_fcmp_onetime_address_from_legacy_addr(const cryptonote::account_keys &keys, + const uint32_t j_major, + const uint32_t j_minor, + const crypto::secret_key &sender_extension_g, + const crypto::secret_key &sender_extension_t, + const crypto::public_key &onetime_address) +{ + // K_s = k_s G + // m = Hn(k_v || j_major || j_minor) if j else 0 + // K^j_s = K_s + m G + // Ko = K^j_s + k^o_g G + k^o_t T + // = (k^o_g + m + k_s) G + k^o_t T + + // m = Hn(k_v || j_major || j_minor) if j else 0 + crypto::secret_key subaddress_ext{}; + if (j_major || j_minor) + subaddress_ext = keys.get_device().get_subaddress_secret_key(keys.m_view_secret_key, {j_major, j_minor}); + + // combined_g = k^o_g + m + k_s + rct::key combined_g; + sc_add(combined_g.bytes, to_bytes(sender_extension_g), to_bytes(subaddress_ext)); + sc_add(combined_g.bytes, combined_g.bytes, to_bytes(keys.m_spend_secret_key)); + + // Ko' = combined_g G + k^o_t T + rct::key recomputed_onetime_address; + rct::addKeys2(recomputed_onetime_address, combined_g, rct::sk2rct(sender_extension_t), rct::pk2rct(crypto::get_T())); + + // Ko' ?= Ko + return recomputed_onetime_address == onetime_address; +} +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +struct unittest_legacy_scan_result_t +{ + crypto::public_key address_spend_pubkey = rct::rct2pk(rct::I); + crypto::secret_key sender_extension_g = rct::rct2sk(rct::I); + crypto::secret_key sender_extension_t = rct::rct2sk(rct::I); + + rct::xmr_amount amount = 0; + crypto::secret_key amount_blinding_factor = rct::rct2sk(rct::I); + + CarrotEnoteType enote_type = CarrotEnoteType::PAYMENT; + + payment_id_t payment_id = null_payment_id; + + size_t output_index = 0; +}; +static void unittest_legacy_scan_enote_set(const std::vector &enotes, + const encrypted_payment_id_t encrypted_payment_id, + const cryptonote::account_base acb, + std::vector &res) +{ + res.clear(); + + // external scans + for (size_t output_index = 0; output_index < enotes.size(); ++output_index) + { + const CarrotEnoteV1 &enote = enotes.at(output_index); + + // s_sr = k_v D_e + mx25519_pubkey s_sr; + make_carrot_uncontextualized_shared_key_receiver(acb.get_keys().m_view_secret_key, + enote.enote_ephemeral_pubkey, + s_sr); + + unittest_legacy_scan_result_t scan_result{}; + const bool r = try_scan_carrot_enote_external(enote, + encrypted_payment_id, + s_sr, + view_incoming_key_ram_borrowed_device(acb.get_keys().m_view_secret_key), + acb.get_keys().m_account_address.m_spend_public_key, + scan_result.sender_extension_g, + scan_result.sender_extension_t, + scan_result.address_spend_pubkey, + scan_result.amount, + scan_result.amount_blinding_factor, + scan_result.payment_id, + scan_result.enote_type); + + scan_result.output_index = output_index; + + if (r) + res.push_back(scan_result); + } +} +//---------------------------------------------------------------------------------------------------------------------- +static void subtest_legacy_2out_transfer_get_output_enote_proposals_completeness(const bool alice_subaddress, + const bool bob_subaddress, + const bool bob_integrated, + const CarrotEnoteType alice_selfsend_type) +{ + hw::device &hwdev = hw::get_device("default"); + + // generate alice keys and address + cryptonote::account_base alice; + alice.generate(); + const uint32_t alice_j_major = alice_subaddress ? crypto::rand() : 0; + const uint32_t alice_j_minor = alice_subaddress ? crypto::rand() : 0; + CarrotDestinationV1 alice_address{}; + cryptonote::account_public_address subaddr = hwdev.get_subaddress(alice.get_keys(), + {alice_j_major, alice_j_minor}); + alice_address.address_spend_pubkey = subaddr.m_spend_public_key; + alice_address.address_view_pubkey = subaddr.m_view_public_key; + alice_address.is_subaddress = alice_subaddress; + alice_address.payment_id = null_payment_id; + + // generate bob keys and address + cryptonote::account_base bob; + bob.generate(); + const uint32_t bob_j_major = bob_subaddress ? crypto::rand() : 0; + const uint32_t bob_j_minor = bob_subaddress ? crypto::rand() : 0; + CarrotDestinationV1 bob_address{}; + subaddr = hwdev.get_subaddress(bob.get_keys(), {bob_j_major, bob_j_minor}); + bob_address.address_spend_pubkey = subaddr.m_spend_public_key; + bob_address.address_view_pubkey = subaddr.m_view_public_key; + bob_address.is_subaddress = bob_subaddress; + bob_address.payment_id = bob_integrated ? gen_payment_id() : null_payment_id; + + // generate input context + const crypto::key_image tx_first_key_image = rct::rct2ki(rct::pkGen()); + input_context_t input_context; + make_carrot_input_context(tx_first_key_image, input_context); + + // outgoing payment proposal to bob + const CarrotPaymentProposalV1 bob_payment_proposal = CarrotPaymentProposalV1{ + .destination = bob_address, + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + + // selfsend payment proposal to alice + const CarrotPaymentProposalSelfSendV1 alice_payment_proposal = CarrotPaymentProposalSelfSendV1{ + .destination_address_spend_pubkey = alice_address.address_spend_pubkey, + .amount = crypto::rand_idx(1000000), + .enote_type = CarrotEnoteType::CHANGE, + .enote_ephemeral_pubkey = get_enote_ephemeral_pubkey(bob_payment_proposal, input_context) + }; + + // alice mem devices + view_incoming_key_ram_borrowed_device alive_k_v_dev(alice.get_keys().m_view_secret_key); + + // turn payment proposals into enotes + std::vector enote_proposals; + encrypted_payment_id_t encrypted_payment_id; + get_output_enote_proposals({bob_payment_proposal}, + {alice_payment_proposal}, + nullptr, + &alive_k_v_dev, + alice.get_keys().m_account_address.m_spend_public_key, + tx_first_key_image, + enote_proposals, + encrypted_payment_id); + + ASSERT_EQ(2, enote_proposals.size()); // 2-out tx + + // collect enotes + std::vector enotes; + for (const RCTOutputEnoteProposal &enote_proposal : enote_proposals) + enotes.push_back(enote_proposal.enote); + + // check that alice scanned 1 enote + std::vector alice_scan_vec; + unittest_legacy_scan_enote_set(enotes, encrypted_payment_id, alice, alice_scan_vec); + ASSERT_EQ(1, alice_scan_vec.size()); + unittest_legacy_scan_result_t alice_scan = alice_scan_vec.front(); + + // check that bob scanned 1 enote + std::vector bob_scan_vec; + unittest_legacy_scan_enote_set(enotes, encrypted_payment_id, bob, bob_scan_vec); + ASSERT_EQ(1, bob_scan_vec.size()); + unittest_legacy_scan_result_t bob_scan = bob_scan_vec.front(); + + // set named references to enotes + ASSERT_TRUE((alice_scan.output_index == 0 && bob_scan.output_index == 1) || + (alice_scan.output_index == 1 && bob_scan.output_index == 0)); + const CarrotEnoteV1 &alice_enote = enotes.at(alice_scan.output_index); + const CarrotEnoteV1 &bob_enote = enotes.at(bob_scan.output_index); + + // check Alice's recovered data + EXPECT_EQ(alice_payment_proposal.destination_address_spend_pubkey, alice_scan.address_spend_pubkey); + EXPECT_EQ(alice_payment_proposal.amount, alice_scan.amount); + EXPECT_EQ(alice_enote.amount_commitment, rct::commit(alice_scan.amount, rct::sk2rct(alice_scan.amount_blinding_factor))); + EXPECT_EQ(null_payment_id, alice_scan.payment_id); + EXPECT_EQ(alice_payment_proposal.enote_type, alice_scan.enote_type); + + // check Bob's recovered data + EXPECT_EQ(bob_payment_proposal.destination.address_spend_pubkey, bob_scan.address_spend_pubkey); + EXPECT_EQ(bob_payment_proposal.amount, bob_scan.amount); + EXPECT_EQ(bob_enote.amount_commitment, rct::commit(bob_scan.amount, rct::sk2rct(bob_scan.amount_blinding_factor))); + EXPECT_EQ(bob_integrated ? bob_address.payment_id : null_payment_id, bob_scan.payment_id); + EXPECT_EQ(CarrotEnoteType::PAYMENT, bob_scan.enote_type); + + // check Alice spendability + EXPECT_TRUE(can_open_fcmp_onetime_address_from_legacy_addr(alice.get_keys(), + alice_j_major, + alice_j_minor, + alice_scan.sender_extension_g, + alice_scan.sender_extension_t, + alice_enote.onetime_address)); + + // check Bob spendability + EXPECT_TRUE(can_open_fcmp_onetime_address_from_legacy_addr(bob.get_keys(), + bob_j_major, + bob_j_minor, + bob_scan.sender_extension_g, + bob_scan.sender_extension_t, + bob_enote.onetime_address)); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_legacy, legacy_get_enote_output_proposals_main2main_completeness) +{ + subtest_legacy_2out_transfer_get_output_enote_proposals_completeness(false, false, false, CarrotEnoteType::PAYMENT); + subtest_legacy_2out_transfer_get_output_enote_proposals_completeness(false, false, false, CarrotEnoteType::CHANGE); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_legacy, legacy_get_enote_output_proposals_main2sub_completeness) +{ + subtest_legacy_2out_transfer_get_output_enote_proposals_completeness(false, true, false, CarrotEnoteType::PAYMENT); + subtest_legacy_2out_transfer_get_output_enote_proposals_completeness(false, true, false, CarrotEnoteType::CHANGE); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_legacy, legacy_get_enote_output_proposals_main2integ_completeness) +{ + subtest_legacy_2out_transfer_get_output_enote_proposals_completeness(false, false, true, CarrotEnoteType::PAYMENT); + subtest_legacy_2out_transfer_get_output_enote_proposals_completeness(false, false, true, CarrotEnoteType::CHANGE); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_legacy, legacy_get_enote_output_proposals_sub2main_completeness) +{ + subtest_legacy_2out_transfer_get_output_enote_proposals_completeness(true, false, false, CarrotEnoteType::PAYMENT); + subtest_legacy_2out_transfer_get_output_enote_proposals_completeness(true, false, false, CarrotEnoteType::CHANGE); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_legacy, legacy_get_enote_output_proposals_sub2sub_completeness) +{ + subtest_legacy_2out_transfer_get_output_enote_proposals_completeness(true, true, false, CarrotEnoteType::PAYMENT); + subtest_legacy_2out_transfer_get_output_enote_proposals_completeness(true, true, false, CarrotEnoteType::CHANGE); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_legacy, legacy_get_enote_output_proposals_sub2integ_completeness) +{ + subtest_legacy_2out_transfer_get_output_enote_proposals_completeness(true, false, true, CarrotEnoteType::PAYMENT); + subtest_legacy_2out_transfer_get_output_enote_proposals_completeness(true, false, true, CarrotEnoteType::CHANGE); +} +//---------------------------------------------------------------------------------------------------------------------- diff --git a/tests/unit_tests/carrot_transcript_fixed.cpp b/tests/unit_tests/carrot_transcript_fixed.cpp new file mode 100644 index 00000000000..97b4ea99e74 --- /dev/null +++ b/tests/unit_tests/carrot_transcript_fixed.cpp @@ -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. + +#include "gtest/gtest.h" + +#include "carrot_core/config.h" +#include "carrot_core/core_types.h" +#include "carrot_core/transcript_fixed.h" +#include "crypto/crypto.h" + +#include + +TEST(carrot_transcript_fixed, sizeof_sum) +{ + EXPECT_EQ(0, sp::detail::sizeof_sum<>()); + EXPECT_EQ(1, sp::detail::sizeof_sum()); + EXPECT_EQ(12, (sp::detail::sizeof_sum())); +} + +static constexpr const unsigned char DS1[] = "perspicacious"; +static constexpr const unsigned char DS2[] = "recrudescence"; + +TEST(carrot_transcript_fixed, ts_size) +{ + const auto transcript1 = sp::make_fixed_transcript((uint32_t)32); + EXPECT_EQ(1 + 13 + 4, transcript1.size()); + + const auto transcript2 = sp::make_fixed_transcript((uint32_t)32, (uint64_t)64); + EXPECT_EQ(1 + 13 + 4 + 8, transcript2.size()); + + // vt = H_3(s_sr || input_context || Ko) + const auto transcript_vt = sp::make_fixed_transcript( + carrot::input_context_t{}, + crypto::public_key{}); + EXPECT_EQ(1 + 15 + 33 + 32, transcript_vt.size()); +} diff --git a/tests/unit_tests/x25519.cpp b/tests/unit_tests/x25519.cpp new file mode 100644 index 00000000000..30b640eece7 --- /dev/null +++ b/tests/unit_tests/x25519.cpp @@ -0,0 +1,205 @@ +// Copyright (c) 2017-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. + +#include +#include +#include + +#include "common/container_helpers.h" +extern "C" +{ +#include "crypto/crypto-ops.h" +} +#include "crypto/generators.h" +#include "misc_log_ex.h" +#include "mx25519.h" +#include "ringct/rctOps.h" +#include "string_tools.h" + +namespace +{ + static std::vector get_available_mx25519_impls() + { + static constexpr const mx25519_type ALL_IMPL_TYPES[4] = {MX25519_TYPE_PORTABLE, + MX25519_TYPE_ARM64, + MX25519_TYPE_AMD64, + MX25519_TYPE_AMD64X}; + static constexpr const size_t NUM_IMPLS = sizeof(ALL_IMPL_TYPES) / sizeof(ALL_IMPL_TYPES[0]); + + std::vector available_impls; + available_impls.reserve(NUM_IMPLS); + for (int i = 0; i < NUM_IMPLS; ++i) + { + const mx25519_type impl_type = ALL_IMPL_TYPES[i]; + const mx25519_impl * const impl = mx25519_select_impl(impl_type); + if (nullptr == impl) + continue; + available_impls.push_back(impl); + } + + return available_impls; + } + + static std::string get_name_of_mx25519_impl(const mx25519_impl* impl) + { +# define get_name_of_mx25519_impl_CASE(x) case x: return #x; + CHECK_AND_ASSERT_THROW_MES(impl != nullptr, "null impl"); + const mx25519_type impl_type = mx25519_impl_type(impl); + switch (impl_type) + { + get_name_of_mx25519_impl_CASE(MX25519_TYPE_PORTABLE) + get_name_of_mx25519_impl_CASE(MX25519_TYPE_ARM64) + get_name_of_mx25519_impl_CASE(MX25519_TYPE_AMD64) + get_name_of_mx25519_impl_CASE(MX25519_TYPE_AMD64X) + default: + throw std::runtime_error("get name of mx25519 impl: unrecognized impl type"); + } +# undef get_name_of_mx25519_impl_CASE + } + + void dump_mx25519_impls(const std::vector &impls) + { + std::cout << "Testing " << impls.size() << " mx25519 implementations:" << std::endl; + for (const mx25519_impl *impl : impls) + std::cout << " - " << get_name_of_mx25519_impl(impl) << std::endl; + } + + template + static T hex2pod(boost::string_ref s) + { + T v; + if (!epee::string_tools::hex_to_pod(s, v)) + throw std::runtime_error("hex2pod conversion failed"); + return v; + } +} // namespace + +static inline bool operator==(const mx25519_pubkey &a, const mx25519_pubkey &b) +{ + return memcmp(&a, &b, sizeof(mx25519_pubkey)) == 0; +} + +static inline bool operator!=(const mx25519_pubkey &a, const mx25519_pubkey &b) +{ + return !(a == b); +} + +TEST(x25519, scmul_key_convergence) +{ + std::vector available_impls = get_available_mx25519_impls(); + + ASSERT_GT(available_impls.size(), 0); + + dump_mx25519_impls(available_impls); + + std::vector scalars; + for (int i = 0; i <= 254; ++i) + { + for (unsigned char j = 0; j < 8; ++j) + { + // add 2^i + j (sometimes with duplicates, which is okay) + mx25519_privkey &s = tools::add_element(scalars); + memset(s.data, 0, sizeof(mx25519_privkey)); + const int msb_byte_index = i >> 3; + const int msb_bit_index = i & 7; + s.data[msb_byte_index] = 1 << msb_bit_index; + s.data[0] = j; + } + } + // add -1 + scalars.push_back(hex2pod("ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f")); + // add random + const rct::key a = rct::skGen(); + memcpy(tools::add_element(scalars).data, &a, sizeof(mx25519_privkey)); + + std::vector> points; + // add base point + points.push_back({rct::G, mx25519_pubkey{.data={9}}}); + // add RFC 7784 test point + points.push_back({ + hex2pod("8120f299c37ae1ca64a179f638a6c6fafde968f1c33705e28c413c7579d9884f"), + hex2pod("8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a") + }); + // add random point + const rct::key P_random = rct::pkGen(); + mx25519_pubkey P_random_x; + edwards_bytes_to_x25519_vartime(P_random_x.data, P_random.bytes); + points.push_back({P_random, P_random_x}); + + for (const mx25519_privkey &scalar : scalars) + { + for (const auto &point : points) + { + // D1 = ConvertPointE(a * P_base) + ge_p3 P_ed; + ASSERT_EQ(0, ge_frombytes_vartime(&P_ed, point.first.bytes)); + ge_p3 res_p3; + ge_scalarmult_p3(&res_p3, scalar.data, &P_ed); + mx25519_pubkey res; + ge_p3_to_x25519(res.data, &res_p3); + + for (const mx25519_impl *impl : available_impls) + { + // D2 = a * D_base + mx25519_pubkey res_mx; + mx25519_scmul_key(impl, &res_mx, &scalar, &point.second); + + // D1 ?= D2 + EXPECT_EQ(res, res_mx); + } + } + } +} + +TEST(x25519, ConvertPointE_Base) +{ + const crypto::public_key G = crypto::get_G(); + const mx25519_pubkey B_expected = {{9}}; + + mx25519_pubkey B_actual; + edwards_bytes_to_x25519_vartime(B_actual.data, to_bytes(G)); + + EXPECT_EQ(B_expected, B_actual); +} + +TEST(x25519, ConvertPointE_EraseSign) +{ + // generate a random point P and test that ConvertPointE(P) == ConvertPointE(-P) + + const rct::key P = rct::pkGen(); + rct::key negP; + rct::subKeys(negP, rct::I, P); + + mx25519_pubkey P_mont; + edwards_bytes_to_x25519_vartime(P_mont.data, P.bytes); + + mx25519_pubkey negP_mont; + edwards_bytes_to_x25519_vartime(negP_mont.data, negP.bytes); + + EXPECT_EQ(P_mont, negP_mont); +} From 2ad10dcc35577c4b32010a7d13cf4c4997a9b210 Mon Sep 17 00:00:00 2001 From: jeffro256 Date: Wed, 29 Jan 2025 16:57:16 -0600 Subject: [PATCH 02/15] take payment proposals by const reference, not move --- src/carrot_core/output_set_finalization.cpp | 40 ++++++++++----------- src/carrot_core/output_set_finalization.h | 5 +-- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/src/carrot_core/output_set_finalization.cpp b/src/carrot_core/output_set_finalization.cpp index d2f6d8f4406..bf7cad388b5 100644 --- a/src/carrot_core/output_set_finalization.cpp +++ b/src/carrot_core/output_set_finalization.cpp @@ -37,6 +37,7 @@ //third party headers //standard headers +#include #undef MONERO_DEFAULT_LOG_CATEGORY #define MONERO_DEFAULT_LOG_CATEGORY "carrot" @@ -44,6 +45,13 @@ namespace carrot { //------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +template +struct compare_memcmp{ bool operator()(const T &a, const T &b) const { return memcmp(&a, &b, sizeof(T)) < 0; } }; +template +using memcmp_set = std::set>; +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- std::optional get_additional_output_type(const size_t num_outgoing, const size_t num_selfsend, const bool need_change_output, @@ -143,8 +151,8 @@ tools::optional_variant &&normal_payment_proposals, - std::vector &&selfsend_payment_proposals, +void get_output_enote_proposals(const std::vector &normal_payment_proposals, + const std::vector &selfsend_payment_proposals, const view_balance_secret_device *s_view_balance_dev, const view_incoming_key_device *k_view_dev, const crypto::public_key &account_spend_pubkey, @@ -177,14 +185,11 @@ void get_output_enote_proposals(std::vector &&normal_pa CHECK_AND_ASSERT_THROW_MES(normal_payment_proposal.randomness != janus_anchor_t{}, "get output enote proposals: normal payment proposal has unset anchor_norm AKA randomness"); - // sort normal payment proposals by anchor_norm and assert uniqueness of randomness for each payment - const auto sort_by_randomness = [](const CarrotPaymentProposalV1 &a, const CarrotPaymentProposalV1 &b) -> bool - { - return memcmp(&a.randomness, &b.randomness, JANUS_ANCHOR_BYTES) < 0; - }; - std::sort(normal_payment_proposals.begin(), normal_payment_proposals.end(), sort_by_randomness); - const bool has_unique_randomness = tools::is_sorted_and_unique(normal_payment_proposals, - sort_by_randomness); + // assert uniqueness of randomness for each payment + memcmp_set randomnesses; + for (const CarrotPaymentProposalV1 &normal_payment_proposal : normal_payment_proposals) + randomnesses.insert(normal_payment_proposal.randomness); + const bool has_unique_randomness = randomnesses.size() == normal_payment_proposals.size(); CHECK_AND_ASSERT_THROW_MES(has_unique_randomness, "get output enote proposals: normal payment proposals contain duplicate anchor_norm AKA randomness"); @@ -246,16 +251,11 @@ void get_output_enote_proposals(std::vector &&normal_pa } } - // sort enotes by D_e and assert uniqueness properties of D_e - const auto sort_by_ephemeral_pubkey = [](const RCTOutputEnoteProposal &a, const RCTOutputEnoteProposal &b) -> bool - { - return memcmp(&a.enote.enote_ephemeral_pubkey, - &b.enote.enote_ephemeral_pubkey, - sizeof(mx25519_pubkey)) < 0; - }; - std::sort(output_enote_proposals_out.begin(), output_enote_proposals_out.end(), sort_by_ephemeral_pubkey); - const bool has_unique_ephemeral_pubkeys = tools::is_sorted_and_unique(output_enote_proposals_out, - sort_by_ephemeral_pubkey); + // assert uniqueness of D_e + memcmp_set ephemeral_pubkeys; + for (const RCTOutputEnoteProposal &p : output_enote_proposals_out) + ephemeral_pubkeys.insert(p.enote.enote_ephemeral_pubkey); + const bool has_unique_ephemeral_pubkeys = ephemeral_pubkeys.size() == output_enote_proposals_out.size(); CHECK_AND_ASSERT_THROW_MES(!(num_proposals == 2 && has_unique_ephemeral_pubkeys), "get output enote proposals: a 2-out set needs to share an ephemeral pubkey, but this 2-out set doesn't"); CHECK_AND_ASSERT_THROW_MES(!(num_proposals != 2 && !has_unique_ephemeral_pubkeys), diff --git a/src/carrot_core/output_set_finalization.h b/src/carrot_core/output_set_finalization.h index d0311987098..f578757f6c2 100644 --- a/src/carrot_core/output_set_finalization.h +++ b/src/carrot_core/output_set_finalization.h @@ -41,6 +41,7 @@ //standard headers #include +#include //forward declarations @@ -99,8 +100,8 @@ tools::optional_variant &&normal_payment_proposals, - std::vector &&selfsend_payment_proposals, +void get_output_enote_proposals(const std::vector &normal_payment_proposals, + const std::vector &selfsend_payment_proposals, const view_balance_secret_device *s_view_balance_dev, const view_incoming_key_device *k_view_dev, const crypto::public_key &account_spend_pubkey, From 828661657dbfec8ab8774aa7bcefca161c196e0c Mon Sep 17 00:00:00 2001 From: jeffro256 Date: Wed, 29 Jan 2025 16:58:34 -0600 Subject: [PATCH 03/15] payment proposal derivations fails if non-deterministic, instead of filling with randomness --- src/carrot_core/payment_proposal.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/carrot_core/payment_proposal.cpp b/src/carrot_core/payment_proposal.cpp index 2ab501c7292..0b3ac3f47be 100644 --- a/src/carrot_core/payment_proposal.cpp +++ b/src/carrot_core/payment_proposal.cpp @@ -351,13 +351,16 @@ void get_output_proposal_special_v1(const CarrotPaymentProposalSelfSendV1 &propo make_carrot_input_context(tx_first_key_image, input_context); // 3. D_e + const bool missing_enote_ephemeral_pubkeys = !proposal.enote_ephemeral_pubkey && !other_enote_ephemeral_pubkey; const bool mismatched_enote_ephemeral_pubkeys = proposal.enote_ephemeral_pubkey && other_enote_ephemeral_pubkey && memcmp(&*proposal.enote_ephemeral_pubkey, &*other_enote_ephemeral_pubkey, sizeof(mx25519_pubkey)); + CHECK_AND_ASSERT_THROW_MES(!missing_enote_ephemeral_pubkeys, + "get output proposal special v1: no enote ephemeral pubkey provided"); CHECK_AND_ASSERT_THROW_MES(!mismatched_enote_ephemeral_pubkeys, "get output proposal special v1: mismatched enote ephemeral pubkeys provided"); const mx25519_pubkey enote_ephemeral_pubkey = proposal.enote_ephemeral_pubkey.value_or( - other_enote_ephemeral_pubkey.value_or(gen_x25519_pubkey())); + other_enote_ephemeral_pubkey.value_or(mx25519_pubkey{})); // 4. s_sr = k_v D_e mx25519_pubkey s_sender_receiver_unctx; auto ecdh_wiper = auto_wiper(s_sender_receiver_unctx); @@ -417,13 +420,16 @@ void get_output_proposal_internal_v1(const CarrotPaymentProposalSelfSendV1 &prop make_carrot_input_context(tx_first_key_image, input_context); // 3. D_e + const bool missing_enote_ephemeral_pubkeys = !proposal.enote_ephemeral_pubkey && !other_enote_ephemeral_pubkey; const bool mismatched_enote_ephemeral_pubkeys = proposal.enote_ephemeral_pubkey && other_enote_ephemeral_pubkey && memcmp(&*proposal.enote_ephemeral_pubkey, &*other_enote_ephemeral_pubkey, sizeof(mx25519_pubkey)); + CHECK_AND_ASSERT_THROW_MES(!missing_enote_ephemeral_pubkeys, + "get output proposal special v1: no enote ephemeral pubkey provided"); CHECK_AND_ASSERT_THROW_MES(!mismatched_enote_ephemeral_pubkeys, "get output proposal internal v1: mismatched enote ephemeral pubkeys provided"); const mx25519_pubkey enote_ephemeral_pubkey = proposal.enote_ephemeral_pubkey.value_or( - other_enote_ephemeral_pubkey.value_or(gen_x25519_pubkey())); + other_enote_ephemeral_pubkey.value_or(mx25519_pubkey{})); // 4. s^ctx_sr = H_32(s_vb, D_e, input_context) crypto::hash s_sender_receiver; auto q_wiper = auto_wiper(s_sender_receiver); From 5c5421cdf2488ab16b3f62c817c9e667ae4de5ac Mon Sep 17 00:00:00 2001 From: jeffro256 Date: Thu, 30 Jan 2025 01:14:33 -0600 Subject: [PATCH 04/15] more sanity checks for get_output_enote_proposals --- src/carrot_core/output_set_finalization.cpp | 39 +++++++++++++++++++-- src/carrot_core/payment_proposal.cpp | 5 --- src/carrot_core/payment_proposal.h | 3 -- 3 files changed, 36 insertions(+), 11 deletions(-) diff --git a/src/carrot_core/output_set_finalization.cpp b/src/carrot_core/output_set_finalization.cpp index bf7cad388b5..c5c02bffec2 100644 --- a/src/carrot_core/output_set_finalization.cpp +++ b/src/carrot_core/output_set_finalization.cpp @@ -33,6 +33,7 @@ #include "common/container_helpers.h" #include "enote_utils.h" #include "misc_log_ex.h" +#include "ringct/rctOps.h" //third party headers @@ -251,18 +252,50 @@ void get_output_enote_proposals(const std::vector &norm } } - // assert uniqueness of D_e + // assert uniqueness of D_e if >2-out, shared otherwise. also check D_e is not trivial memcmp_set ephemeral_pubkeys; for (const RCTOutputEnoteProposal &p : output_enote_proposals_out) + { + const bool trivial_enote_ephemeral_pubkey = memcmp(p.enote.enote_ephemeral_pubkey.data, + mx25519_pubkey{}.data, + sizeof(mx25519_pubkey)) == 0; + CHECK_AND_ASSERT_THROW_MES(!trivial_enote_ephemeral_pubkey, + "get output enote proposals: this set contains enote ephemeral pubkeys with x=0"); ephemeral_pubkeys.insert(p.enote.enote_ephemeral_pubkey); + } const bool has_unique_ephemeral_pubkeys = ephemeral_pubkeys.size() == output_enote_proposals_out.size(); CHECK_AND_ASSERT_THROW_MES(!(num_proposals == 2 && has_unique_ephemeral_pubkeys), "get output enote proposals: a 2-out set needs to share an ephemeral pubkey, but this 2-out set doesn't"); CHECK_AND_ASSERT_THROW_MES(!(num_proposals != 2 && !has_unique_ephemeral_pubkeys), "get output enote proposals: this >2-out set contains duplicate enote ephemeral pubkeys"); - // sort enotes by Ko - std::sort(output_enote_proposals_out.begin(), output_enote_proposals_out.end()); + // sort enotes by K_o + const auto sort_output_enote_proposal = [](const RCTOutputEnoteProposal &a, const RCTOutputEnoteProposal &b) + -> bool { return a.enote.onetime_address < b.enote.onetime_address; }; + std::sort(output_enote_proposals_out.begin(), output_enote_proposals_out.end(), sort_output_enote_proposal); + + // assert uniqueness of K_o + CHECK_AND_ASSERT_THROW_MES(tools::is_sorted_and_unique(output_enote_proposals_out, sort_output_enote_proposal), + "get output enote proposals: this set contains duplicate onetime addresses"); + + // assert all K_o lie in prime order subgroup + for (const RCTOutputEnoteProposal &output_enote_proposal : output_enote_proposals_out) + { + CHECK_AND_ASSERT_THROW_MES(rct::isInMainSubgroup(rct::pk2rct(output_enote_proposal.enote.onetime_address)), + "get output enote proposals: this set contains an invalid onetime address"); + } + + // assert unique and non-trivial k_a + memcmp_set amount_blinding_factors; + for (const RCTOutputEnoteProposal &output_enote_proposal : output_enote_proposals_out) + { + CHECK_AND_ASSERT_THROW_MES(output_enote_proposal.amount_blinding_factor != crypto::null_skey, + "get output enote proposals: this set contains a trivial amount blinding factor"); + + amount_blinding_factors.insert(output_enote_proposal.amount_blinding_factor); + } + CHECK_AND_ASSERT_THROW_MES(amount_blinding_factors.size() == num_proposals, + "get output enote proposals: this set contains duplicate amount blinding factors"); } //------------------------------------------------------------------------------------------------------------------- } //namespace carrot diff --git a/src/carrot_core/payment_proposal.cpp b/src/carrot_core/payment_proposal.cpp index 0b3ac3f47be..c23619bd980 100644 --- a/src/carrot_core/payment_proposal.cpp +++ b/src/carrot_core/payment_proposal.cpp @@ -221,11 +221,6 @@ bool operator==(const CarrotPaymentProposalSelfSendV1 &a, const CarrotPaymentPro 0 == memcmp(&a.enote_ephemeral_pubkey, &b.enote_ephemeral_pubkey, sizeof(mx25519_pubkey)); } //------------------------------------------------------------------------------------------------------------------- -bool operator<(const RCTOutputEnoteProposal &a, const RCTOutputEnoteProposal &b) -{ - return memcmp(&a.enote.onetime_address, &b.enote.onetime_address, sizeof(crypto::public_key)) < 0; -} -//------------------------------------------------------------------------------------------------------------------- mx25519_pubkey get_enote_ephemeral_pubkey(const CarrotPaymentProposalV1 &proposal, const input_context_t &input_context) { diff --git a/src/carrot_core/payment_proposal.h b/src/carrot_core/payment_proposal.h index e0fc3187bc1..8278fbb8227 100644 --- a/src/carrot_core/payment_proposal.h +++ b/src/carrot_core/payment_proposal.h @@ -96,9 +96,6 @@ bool operator==(const CarrotPaymentProposalV1 &a, const CarrotPaymentProposalV1 /// equality operators bool operator==(const CarrotPaymentProposalSelfSendV1 &a, const CarrotPaymentProposalSelfSendV1 &b); -/// comparison operators -bool operator<(const RCTOutputEnoteProposal &a, const RCTOutputEnoteProposal &b); - /** * brief: get_enote_ephemeral_pubkey - get the proposal's enote ephemeral pubkey D_e * param: proposal - From eaaa43dc29e63d3fb9efafdfcfb6584803f95efe Mon Sep 17 00:00:00 2001 From: jeffro256 Date: Thu, 30 Jan 2025 01:17:17 -0600 Subject: [PATCH 05/15] typo in get_output_proposal_internal_v1 log msg --- src/carrot_core/payment_proposal.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/carrot_core/payment_proposal.cpp b/src/carrot_core/payment_proposal.cpp index c23619bd980..94dc9c4d554 100644 --- a/src/carrot_core/payment_proposal.cpp +++ b/src/carrot_core/payment_proposal.cpp @@ -420,7 +420,7 @@ void get_output_proposal_internal_v1(const CarrotPaymentProposalSelfSendV1 &prop other_enote_ephemeral_pubkey && memcmp(&*proposal.enote_ephemeral_pubkey, &*other_enote_ephemeral_pubkey, sizeof(mx25519_pubkey)); CHECK_AND_ASSERT_THROW_MES(!missing_enote_ephemeral_pubkeys, - "get output proposal special v1: no enote ephemeral pubkey provided"); + "get output proposal internal v1: no enote ephemeral pubkey provided"); CHECK_AND_ASSERT_THROW_MES(!mismatched_enote_ephemeral_pubkeys, "get output proposal internal v1: mismatched enote ephemeral pubkeys provided"); const mx25519_pubkey enote_ephemeral_pubkey = proposal.enote_ephemeral_pubkey.value_or( From b5bd5cf9d8bc870d4bb3b0605a39568fdc04334d Mon Sep 17 00:00:00 2001 From: jeffro256 Date: Thu, 30 Jan 2025 23:33:46 -0600 Subject: [PATCH 06/15] get_output_enote_proposals: fail if dummy pid is needed but not provided instead of random generation --- src/carrot_core/output_set_finalization.cpp | 13 +- src/carrot_core/output_set_finalization.h | 2 + tests/unit_tests/carrot_core.cpp | 12 +- tests/unit_tests/carrot_impl.cpp | 1520 +++++++++++++++++++ tests/unit_tests/carrot_legacy.cpp | 3 +- 5 files changed, 1539 insertions(+), 11 deletions(-) create mode 100644 tests/unit_tests/carrot_impl.cpp diff --git a/src/carrot_core/output_set_finalization.cpp b/src/carrot_core/output_set_finalization.cpp index c5c02bffec2..e584f3be667 100644 --- a/src/carrot_core/output_set_finalization.cpp +++ b/src/carrot_core/output_set_finalization.cpp @@ -154,6 +154,7 @@ tools::optional_variant &normal_payment_proposals, const std::vector &selfsend_payment_proposals, + const std::optional &dummy_encrypted_payment_id, const view_balance_secret_device *s_view_balance_dev, const view_incoming_key_device *k_view_dev, const crypto::public_key &account_spend_pubkey, @@ -162,7 +163,6 @@ void get_output_enote_proposals(const std::vector &norm encrypted_payment_id_t &encrypted_payment_id_out) { output_enote_proposals_out.clear(); - encrypted_payment_id_out = null_payment_id; // assert payment proposals numbers const size_t num_proposals = normal_payment_proposals.size() + selfsend_payment_proposals.size(); @@ -208,17 +208,16 @@ void get_output_enote_proposals(const std::vector &norm tools::add_element(output_enote_proposals_out), encrypted_payment_id); - // set pid to the first payment proposal or only integrated proposal - const bool is_first = i == 0; + // set pid_enc from integrated address proposal pic_enc const bool is_integrated = normal_payment_proposals[i].destination.payment_id != null_payment_id; - if (is_first || is_integrated) + if (is_integrated) encrypted_payment_id_out = encrypted_payment_id; } // in the case that the pid target is ambiguous, set it to random bytes - const bool ambiguous_pid_destination = num_integrated == 0 && normal_payment_proposals.size() > 1; - if (ambiguous_pid_destination) - encrypted_payment_id_out = gen_payment_id(); + const bool missing_encrypted_pid = num_integrated == 0 && !dummy_encrypted_payment_id; + CHECK_AND_ASSERT_THROW_MES(!missing_encrypted_pid, + "get output enote proposals: missing encrypted payment ID: no integrated address nor provided dummy"); // construct selfsend enotes, preferring internal enotes over special enotes when possible for (const CarrotPaymentProposalSelfSendV1 &selfsend_payment_proposal : selfsend_payment_proposals) diff --git a/src/carrot_core/output_set_finalization.h b/src/carrot_core/output_set_finalization.h index f578757f6c2..3797de59619 100644 --- a/src/carrot_core/output_set_finalization.h +++ b/src/carrot_core/output_set_finalization.h @@ -89,6 +89,7 @@ tools::optional_variant &normal_payment_proposals, const std::vector &selfsend_payment_proposals, + const std::optional &dummy_encrypted_payment_id, const view_balance_secret_device *s_view_balance_dev, const view_incoming_key_device *k_view_dev, const crypto::public_key &account_spend_pubkey, diff --git a/tests/unit_tests/carrot_core.cpp b/tests/unit_tests/carrot_core.cpp index e4fac4189c6..65c4df2ac99 100644 --- a/tests/unit_tests/carrot_core.cpp +++ b/tests/unit_tests/carrot_core.cpp @@ -923,7 +923,7 @@ static void subtest_2out_transfer_get_output_enote_proposals_completeness(const alice_j_minor, alice_address); } - else + else // alice main address { make_carrot_main_address_v1(alice.account_spend_pubkey, alice.main_address_view_pubkey, @@ -951,7 +951,7 @@ static void subtest_2out_transfer_get_output_enote_proposals_completeness(const gen_payment_id(), bob_address); } - else + else // bob main address { make_carrot_main_address_v1(bob.account_spend_pubkey, bob.main_address_view_pubkey, @@ -979,11 +979,17 @@ static void subtest_2out_transfer_get_output_enote_proposals_completeness(const .internal_message = alice_internal_selfsends ? std::make_optional(gen_janus_anchor()) : std::nullopt }; - // turn payment proposals into enotes + // calculate dummy encrypted pid + const std::optional dummy_encrypted_pid = bob_integrated + ? std::optional{} + : gen_payment_id(); + + // turn payment proposals into enotes, passing dummy pid_enc if bob isn't integrated std::vector enote_proposals; encrypted_payment_id_t encrypted_payment_id; get_output_enote_proposals({bob_payment_proposal}, {alice_payment_proposal}, + dummy_encrypted_pid, alice_internal_selfsends ? &alice.s_view_balance_dev : nullptr, &alice.k_view_dev, alice.account_spend_pubkey, diff --git a/tests/unit_tests/carrot_impl.cpp b/tests/unit_tests/carrot_impl.cpp new file mode 100644 index 00000000000..4fe5c9e536e --- /dev/null +++ b/tests/unit_tests/carrot_impl.cpp @@ -0,0 +1,1520 @@ +// 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. + +#include "gtest/gtest.h" + +#include + +#include "carrot_core/account_secrets.h" +#include "carrot_core/address_utils.h" +#include "carrot_core/carrot_enote_scan.h" +#include "carrot_core/destination.h" +#include "carrot_core/device_ram_borrowed.h" +#include "carrot_core/enote_utils.h" +#include "carrot_core/output_set_finalization.h" +#include "carrot_core/payment_proposal.h" +#include "carrot_impl/carrot_tx_builder.h" +#include "carrot_impl/carrot_tx_format_utils.h" +#include "common/container_helpers.h" +#include "crypto/generators.h" +#include "cryptonote_basic/account.h" +#include "cryptonote_basic/subaddress_index.h" +#include "ringct/rctOps.h" + +using namespace carrot; + +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +namespace +{ +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +static constexpr std::uint32_t MAX_SUBADDRESS_MAJOR_INDEX = 5; +static constexpr std::uint32_t MAX_SUBADDRESS_MINOR_INDEX = 20; +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +struct mock_carrot_or_legacy_keys +{ + bool is_carrot; + + crypto::secret_key s_master; + crypto::secret_key k_prove_spend; + crypto::secret_key s_view_balance; + crypto::secret_key k_generate_image; + crypto::secret_key k_view; + crypto::secret_key s_generate_address; + crypto::public_key account_spend_pubkey; + crypto::public_key account_view_pubkey; + crypto::public_key main_address_view_pubkey; + + cryptonote::account_base legacy_acb; + + view_incoming_key_ram_borrowed_device k_view_dev; + view_balance_secret_ram_borrowed_device s_view_balance_dev; + + std::unordered_map subaddress_map; + + mock_carrot_or_legacy_keys(): k_view_dev(k_view), s_view_balance_dev(s_view_balance) {} + + const view_balance_secret_device* get_view_balance_device() const + { + return is_carrot ? &s_view_balance_dev : nullptr; + } + + CarrotDestinationV1 cryptonote_address(const payment_id_t payment_id = null_payment_id) const + { + CarrotDestinationV1 addr; + if (is_carrot) + { + make_carrot_integrated_address_v1(account_spend_pubkey, + main_address_view_pubkey, + payment_id, + addr); + } + else + { + make_carrot_integrated_address_v1(legacy_acb.get_keys().m_account_address.m_spend_public_key, + legacy_acb.get_keys().m_account_address.m_view_public_key, + payment_id, + addr); + } + return addr; + } + + CarrotDestinationV1 subaddress(const uint32_t major_index, const uint32_t minor_index) const + { + if (!major_index && !minor_index) + return cryptonote_address(); + + CarrotDestinationV1 addr; + if (is_carrot) + { + make_carrot_subaddress_v1(account_spend_pubkey, + account_view_pubkey, + s_generate_address, + major_index, + minor_index, + addr); + } + else + { + const cryptonote::account_keys &ks = legacy_acb.get_keys(); + const cryptonote::account_public_address cnaddr = + ks.m_device->get_subaddress(ks, {major_index, minor_index}); + addr = CarrotDestinationV1{ + .address_spend_pubkey = cnaddr.m_spend_public_key, + .address_view_pubkey = cnaddr.m_view_public_key, + .is_subaddress = true, + .payment_id = null_payment_id + }; + } + return addr; + } + + // brief: opening_for_subaddress - return (k^g_a, k^t_a) for j s.t. K^j_s = (k^g_a * G + k^t_a * T) + void opening_for_subaddress(const uint32_t major_index, + const uint32_t minor_index, + crypto::secret_key &address_privkey_g_out, + crypto::secret_key &address_privkey_t_out, + crypto::public_key &address_spend_pubkey_out) const + { + const bool is_subaddress = major_index || minor_index; + + if (is_carrot) + { + // s^j_gen = H_32[s_ga](j_major, j_minor) + crypto::secret_key address_index_generator; + make_carrot_index_extension_generator(s_generate_address, major_index, minor_index, address_index_generator); + + crypto::secret_key subaddress_scalar{}; + if (is_subaddress) + { + // k^j_subscal = H_n(K_s, j_major, j_minor, s^j_gen) + make_carrot_subaddress_scalar(account_spend_pubkey, address_index_generator, major_index, minor_index, subaddress_scalar); + } + else + { + subaddress_scalar.data[0] = 1; + } + + // k^g_a = k_gi * k^j_subscal + sc_mul(to_bytes(address_privkey_g_out), to_bytes(k_generate_image), to_bytes(subaddress_scalar)); + + // k^t_a = k_ps * k^j_subscal + sc_mul(to_bytes(address_privkey_t_out), to_bytes(k_prove_spend), to_bytes(subaddress_scalar)); + } + else // legacy keys + { + // m = Hn(k_v || j_major || j_minor) if subaddress else 0 + const cryptonote::account_keys &ks = legacy_acb.get_keys(); + const crypto::secret_key subaddress_extension = is_subaddress + ? ks.get_device().get_subaddress_secret_key(ks.m_view_secret_key, {major_index, minor_index}) + : crypto::null_skey; + + // k^g_a = k_s + m + sc_add(to_bytes(address_privkey_g_out), to_bytes(ks.m_spend_secret_key), to_bytes(subaddress_extension)); + + // k^t_a = 0 + memset(address_privkey_t_out.data, 0, sizeof(address_privkey_t_out)); + } + + // perform sanity check + const CarrotDestinationV1 addr = subaddress(major_index, minor_index); + rct::key recomputed_address_spend_pubkey; + rct::addKeys2(recomputed_address_spend_pubkey, + rct::sk2rct(address_privkey_g_out), + rct::sk2rct(address_privkey_t_out), + rct::pk2rct(crypto::get_T())); + CHECK_AND_ASSERT_THROW_MES(rct::rct2pk(recomputed_address_spend_pubkey) == addr.address_spend_pubkey, + "mock carrot or legacy keys: opening for subaddress: failed sanity check"); + address_spend_pubkey_out = addr.address_spend_pubkey; + } + + bool try_searching_for_opening_for_subaddress(const crypto::public_key &address_spend_pubkey, + const uint32_t max_major_index, + const uint32_t max_minor_index, + uint32_t major_index_out, + uint32_t minor_index_out, + crypto::secret_key &address_privkey_g_out, + crypto::secret_key &address_privkey_t_out) const + { + const auto it = subaddress_map.find(address_spend_pubkey); + if (it == subaddress_map.cend()) + return false; + + crypto::public_key recomputed_address_spend_pubkey; + opening_for_subaddress(it->second.major, + it->second.minor, + address_privkey_g_out, + address_privkey_t_out, + recomputed_address_spend_pubkey); + + return address_spend_pubkey == recomputed_address_spend_pubkey; + } + + void generate_subaddress_map() + { + for (uint32_t major_index = 0; major_index < MAX_SUBADDRESS_MAJOR_INDEX; ++major_index) + { + for (uint32_t minor_index = 0; minor_index < MAX_SUBADDRESS_MINOR_INDEX; ++minor_index) + { + const CarrotDestinationV1 addr = subaddress(major_index, minor_index); + subaddress_map.insert({addr.address_spend_pubkey, {major_index, minor_index}}); + } + } + } + + void generate_carrot() + { + is_carrot = true; + crypto::generate_random_bytes_thread_safe(sizeof(crypto::secret_key), to_bytes(s_master)); + make_carrot_provespend_key(s_master, k_prove_spend); + make_carrot_viewbalance_secret(s_master, s_view_balance); + make_carrot_generateimage_key(s_view_balance, k_generate_image); + make_carrot_viewincoming_key(s_view_balance, k_view); + make_carrot_generateaddress_secret(s_view_balance, s_generate_address); + make_carrot_spend_pubkey(k_generate_image, k_prove_spend, account_spend_pubkey); + account_view_pubkey = rct::rct2pk(rct::scalarmultKey(rct::pk2rct(account_spend_pubkey), + rct::sk2rct(k_view))); + main_address_view_pubkey = rct::rct2pk(rct::scalarmultBase(rct::sk2rct(k_view))); + + generate_subaddress_map(); + } + + void generate_legacy() + { + is_carrot = false; + legacy_acb.generate(); + k_view = legacy_acb.get_keys().m_view_secret_key; + account_spend_pubkey = legacy_acb.get_keys().m_account_address.m_spend_public_key; + main_address_view_pubkey = legacy_acb.get_keys().m_account_address.m_view_public_key; + + generate_subaddress_map(); + } +}; +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +static bool can_open_fcmp_onetime_address(const crypto::secret_key &address_privkey_g, + const crypto::secret_key &address_privkey_t, + const crypto::secret_key &sender_extension_g, + const crypto::secret_key &sender_extension_t, + const crypto::public_key &onetime_address) +{ + rct::key combined_g; + sc_add(combined_g.bytes, to_bytes(address_privkey_g), to_bytes(sender_extension_g)); + + rct::key combined_t; + sc_add(combined_t.bytes, to_bytes(address_privkey_t), to_bytes(sender_extension_t)); + + // Ko' = combined_g G + combined_t T + rct::key recomputed_onetime_address; + rct::addKeys2(recomputed_onetime_address, combined_g, combined_t, rct::pk2rct(crypto::get_T())); + + // Ko' ?= Ko + return recomputed_onetime_address == onetime_address; +} +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +struct unittest_carrot_scan_result_t +{ + crypto::public_key address_spend_pubkey = rct::rct2pk(rct::I); + crypto::secret_key sender_extension_g = rct::rct2sk(rct::I); + crypto::secret_key sender_extension_t = rct::rct2sk(rct::I); + + rct::xmr_amount amount = 0; + crypto::secret_key amount_blinding_factor = rct::rct2sk(rct::I); + + CarrotEnoteType enote_type = CarrotEnoteType::PAYMENT; + + payment_id_t payment_id = null_payment_id; + + janus_anchor_t internal_message = janus_anchor_t{}; + + size_t output_index = 0; +}; +static void unittest_scan_enote_set(const std::vector &enotes, + const encrypted_payment_id_t encrypted_payment_id, + const mock_carrot_or_legacy_keys keys, + std::vector &res) +{ + res.clear(); + + // for each enote... + for (size_t output_index = 0; output_index < enotes.size(); ++output_index) + { + const CarrotEnoteV1 &enote = enotes.at(output_index); + + // s_sr = k_v D_e + mx25519_pubkey s_sr; + make_carrot_uncontextualized_shared_key_receiver(keys.k_view, enote.enote_ephemeral_pubkey, s_sr); + + // external scan + unittest_carrot_scan_result_t scan_result{}; + bool r = try_scan_carrot_enote_external(enote, + encrypted_payment_id, + s_sr, + keys.k_view_dev, + keys.account_spend_pubkey, + scan_result.sender_extension_g, + scan_result.sender_extension_t, + scan_result.address_spend_pubkey, + scan_result.amount, + scan_result.amount_blinding_factor, + scan_result.payment_id, + scan_result.enote_type); + + // internal scan + r = r || try_scan_carrot_enote_internal(enote, + keys.s_view_balance_dev, + scan_result.sender_extension_g, + scan_result.sender_extension_t, + scan_result.address_spend_pubkey, + scan_result.amount, + scan_result.amount_blinding_factor, + scan_result.enote_type, + scan_result.internal_message); + + scan_result.output_index = output_index; + + if (r) + res.push_back(scan_result); + } +} +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +static void unittest_scan_enote_set_multi_account(const std::vector &enotes, + const encrypted_payment_id_t encrypted_payment_id, + const epee::span accounts, + std::vector> &res) +{ + res.clear(); + res.reserve(accounts.size()); + + for (const mock_carrot_or_legacy_keys *account : accounts) + unittest_scan_enote_set(enotes, encrypted_payment_id, *account, tools::add_element(res)); +} +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +static bool compare_scan_result(const unittest_carrot_scan_result_t &scan_res, + const CarrotPaymentProposalV1 &normal_payment_proposal, + const rct::xmr_amount allowed_fee_margin_opt = 0) +{ + if (scan_res.address_spend_pubkey != normal_payment_proposal.destination.address_spend_pubkey) + return false; + + if (scan_res.amount > normal_payment_proposal.amount) + return false; + + if (normal_payment_proposal.amount - scan_res.amount > allowed_fee_margin_opt) + return false; + + if (scan_res.enote_type != CarrotEnoteType::PAYMENT) + return false; + + if (scan_res.payment_id != normal_payment_proposal.destination.payment_id) + return false; + + if (scan_res.internal_message != janus_anchor_t{}) + return false; + + return true; +} +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +static bool compare_scan_result(const unittest_carrot_scan_result_t &scan_res, + const CarrotPaymentProposalSelfSendV1 &selfsend_payment_proposal, + const rct::xmr_amount allowed_fee_margin_opt = 0) +{ + if (scan_res.address_spend_pubkey != selfsend_payment_proposal.destination_address_spend_pubkey) + return false; + + if (scan_res.amount > selfsend_payment_proposal.amount) + return false; + + if (selfsend_payment_proposal.amount - scan_res.amount > allowed_fee_margin_opt) + return false; + + if (scan_res.enote_type != selfsend_payment_proposal.enote_type) + return false; + + if (scan_res.payment_id != null_payment_id) + return false; + + if (scan_res.internal_message != selfsend_payment_proposal.internal_message.value_or(janus_anchor_t{})) + return false; + + return true; +} +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +struct unittest_transaction_proposal +{ + using per_payment_proposal = std::pair; + using per_ss_payment_proposal = std::pair; + using per_account = std::pair>; + using per_input = std::pair; + + std::vector per_account_payments; + std::vector explicit_selfsend_proposals; + size_t self_sender_index{0}; + rct::xmr_amount fee_per_weight; + + void get_flattened_payment_proposals(std::vector &normal_payment_proposals_out, + std::vector &selfsend_payment_proposals_out, + std::set &subtractable_normal_payment_proposals, + std::set &subtractable_selfsend_payment_proposals) const + { + size_t norm_idx = 0; + for (const per_account &pa : per_account_payments) + { + for (const per_payment_proposal &ppp : pa.second) + { + normal_payment_proposals_out.push_back(ppp.first); + if (ppp.second) + subtractable_normal_payment_proposals.insert(norm_idx); + + norm_idx++; + } + } + + for (size_t ss_idx = 0; ss_idx < explicit_selfsend_proposals.size(); ++ss_idx) + { + const per_ss_payment_proposal &pspp = explicit_selfsend_proposals.at(ss_idx); + selfsend_payment_proposals_out.push_back(pspp.first); + if (pspp.second) + subtractable_selfsend_payment_proposals.insert(ss_idx); + } + } +}; +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +select_inputs_func_t make_fake_input_selection_callback(size_t num_ins = 0) +{ + return [num_ins](const boost::multiprecision::int128_t &nominal_output_sum, + const std::map &fee_per_input_count, + std::vector &selected_inputs) + { + const size_t nins = num_ins ? num_ins : 1; + selected_inputs.clear(); + selected_inputs.reserve(nins); + + const rct::xmr_amount fee = fee_per_input_count.at(nins); + rct::xmr_amount in_amount_sum_64 = boost::numeric_cast(nominal_output_sum + fee); + + for (size_t i = 0; i < nins - 1; ++i) + { + const rct::xmr_amount current_in_amount = in_amount_sum_64 ? crypto::rand_idx(in_amount_sum_64) : 0; + const crypto::key_image current_key_image = rct::rct2ki(rct::pkGen()); + selected_inputs.push_back({current_in_amount, current_key_image}); + in_amount_sum_64 -= current_in_amount; + } + + selected_inputs.push_back({in_amount_sum_64, rct::rct2ki(rct::pkGen())}); + }; +} +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +} // namespace +static inline bool operator==(const mx25519_pubkey &a, const mx25519_pubkey &b) +{ + return 0 == memcmp(&a, &b, sizeof(mx25519_pubkey)); +} +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +static void subtest_multi_account_transfer_over_transaction(const unittest_transaction_proposal &tx_proposal) +{ + // get payment proposals + std::vector normal_payment_proposals; + std::vector selfsend_payment_proposals; + std::set subtractable_normal_payment_proposals; + std::set subtractable_selfsend_payment_proposals; + tx_proposal.get_flattened_payment_proposals(normal_payment_proposals, + selfsend_payment_proposals, + subtractable_normal_payment_proposals, + subtractable_selfsend_payment_proposals); + + // get self-sender account + const mock_carrot_or_legacy_keys &ss_keys = + tx_proposal.per_account_payments.at(tx_proposal.self_sender_index).first; + + // make unsigned transaction + std::vector modified_normal_payment_proposals = normal_payment_proposals; + std::vector modified_selfsend_payment_proposals = selfsend_payment_proposals; + cryptonote::transaction tx; + std::vector output_amount_blinding_factors; + make_unsigned_transaction_transfer(modified_normal_payment_proposals, + modified_selfsend_payment_proposals, + tx_proposal.fee_per_weight, + make_fake_input_selection_callback(), + ss_keys.get_view_balance_device(), + &ss_keys.k_view_dev, + ss_keys.account_spend_pubkey, + tx, + output_amount_blinding_factors); + + // calculate acceptable fee margin between proposed amount and actual amount for subtractable outputs + const size_t num_subtractable = subtractable_normal_payment_proposals.size() + + subtractable_selfsend_payment_proposals.size(); + const rct::xmr_amount acceptable_fee_margin = num_subtractable + ? (tx.rct_signatures.txnFee / num_subtractable) + 1 + : 0; + + // load carrot stuff from tx + std::vector parsed_enotes; + std::vector parsed_key_images; + rct::xmr_amount parsed_fee; + std::optional parsed_encrypted_payment_id; + ASSERT_TRUE(try_load_carrot_from_transaction_v1(tx, + parsed_enotes, + parsed_key_images, + parsed_fee, + parsed_encrypted_payment_id)); + + // sanity check that the enotes and pid_enc loaded from the transaction are equal to the enotes + // and pic_enc returned from get_output_enote_proposals() when called with the modified payment + // proposals. we do this so that the modified payment proposals from make_unsigned_transaction() + // can be passed to a hardware device for deterministic verification of the signable tx hash + std::vector rederived_output_enote_proposals; + encrypted_payment_id_t rederived_encrypted_payment_id; + get_output_enote_proposals(modified_normal_payment_proposals, + modified_selfsend_payment_proposals, + ss_keys.get_view_balance_device(), + &ss_keys.k_view_dev, + ss_keys.account_spend_pubkey, + parsed_key_images.at(0), + rederived_output_enote_proposals, + rederived_encrypted_payment_id); + ASSERT_TRUE(parsed_encrypted_payment_id); + EXPECT_EQ(*parsed_encrypted_payment_id, rederived_encrypted_payment_id); + ASSERT_EQ(parsed_enotes.size(), rederived_output_enote_proposals.size()); + for (size_t enote_idx = 0; enote_idx < parsed_enotes.size(); ++enote_idx) + { + EXPECT_EQ(parsed_enotes.at(enote_idx), rederived_output_enote_proposals.at(enote_idx).enote); + } + + // collect accounts + std::vector accounts; + for (const auto &pa : tx_proposal.per_account_payments) + accounts.push_back(&pa.first); + + // do scanning of all accounts on every enotes + std::vector> scan_results; + unittest_scan_enote_set_multi_account(parsed_enotes, + *parsed_encrypted_payment_id, + epee::to_span(accounts), + scan_results); + + // check that the scan results for each *normal* account match the corresponding payment + // proposals for each account. also check that the accounts can each open their corresponding + // onetime outut pubkeys + ASSERT_EQ(scan_results.size(), accounts.size()); + // for each normal account... + for (size_t account_idx = 0; account_idx < accounts.size(); ++account_idx) + { + // skip self-sender account + if (account_idx == tx_proposal.self_sender_index) + continue; + + const std::vector &account_scan_results = scan_results.at(account_idx); + const auto &account_payment_proposals = tx_proposal.per_account_payments.at(account_idx).second; + ASSERT_EQ(account_payment_proposals.size(), account_scan_results.size()); + std::set matched_payment_proposals; + + // for each scan result assigned to this account... + for (const unittest_carrot_scan_result_t &single_scan_res : account_scan_results) + { + // for each normal payment proposal to this account... + for (size_t norm_prop_idx = 0; norm_prop_idx < account_payment_proposals.size(); ++norm_prop_idx) + { + // calculate acceptable loss from fee subtraction + const CarrotPaymentProposalV1 &account_payment_proposal = account_payment_proposals.at(norm_prop_idx).first; + const bool is_subtractable = subtractable_normal_payment_proposals.count(norm_prop_idx); + const rct::xmr_amount acceptable_fee_margin_for_proposal = is_subtractable ? acceptable_fee_margin : 0; + + // if the scan result matches the payment proposal... + if (compare_scan_result(single_scan_res, account_payment_proposal, acceptable_fee_margin_for_proposal)) + { + // try searching for subaddress opening + crypto::secret_key address_privkey_g; + crypto::secret_key address_privkey_t; + uint32_t _1{}, _2{}; + EXPECT_TRUE(accounts.at(account_idx)->try_searching_for_opening_for_subaddress( + single_scan_res.address_spend_pubkey, + MAX_SUBADDRESS_MAJOR_INDEX, + MAX_SUBADDRESS_MINOR_INDEX, + _1, + _2, + address_privkey_g, + address_privkey_t)); + + // try opening Ko + EXPECT_TRUE(can_open_fcmp_onetime_address(address_privkey_g, + address_privkey_t, + single_scan_res.sender_extension_g, + single_scan_res.sender_extension_t, + parsed_enotes.at(single_scan_res.output_index).onetime_address)); + + // if this payment proposal isn't already marked as scanned, mark as scanned + if (!matched_payment_proposals.count(norm_prop_idx)) + { + matched_payment_proposals.insert(norm_prop_idx); + break; + } + } + } + } + // check that the number of matched payment proposals is equal to the original number of them + // doing it this way checks that the same payment proposal isn't marked twice and another left out + EXPECT_EQ(account_payment_proposals.size(), matched_payment_proposals.size()); + } + + // check that the scan results for the selfsend account match the corresponding payment + // proposals. also check that the accounts can each open their corresponding onetime outut pubkeys + const std::vector &account_scan_results = scan_results.at(tx_proposal.self_sender_index); + ASSERT_EQ(selfsend_payment_proposals.size() + 1, account_scan_results.size()); + std::set matched_payment_proposals; + const unittest_carrot_scan_result_t* implicit_change_scan_res = nullptr; + // for each scan result assigned to the self-sender account... + for (const unittest_carrot_scan_result_t &single_scan_res : account_scan_results) + { + bool matched_payment = false; + // for each self-send payment proposal... + for (size_t ss_prop_idx = 0; ss_prop_idx < selfsend_payment_proposals.size(); ++ss_prop_idx) + { + // calculate acceptable loss from fee subtraction + const CarrotPaymentProposalSelfSendV1 &account_payment_proposal = selfsend_payment_proposals.at(ss_prop_idx); + const bool is_subtractable = subtractable_selfsend_payment_proposals.count(ss_prop_idx); + const rct::xmr_amount acceptable_fee_margin_for_proposal = is_subtractable ? acceptable_fee_margin : 0; + + // if the scan result matches the payment proposal... + if (compare_scan_result(single_scan_res, account_payment_proposal, acceptable_fee_margin_for_proposal)) + { + // try searching for subaddress opening + crypto::secret_key address_privkey_g; + crypto::secret_key address_privkey_t; + uint32_t _1{}, _2{}; + EXPECT_TRUE(ss_keys.try_searching_for_opening_for_subaddress( + single_scan_res.address_spend_pubkey, + MAX_SUBADDRESS_MAJOR_INDEX, + MAX_SUBADDRESS_MINOR_INDEX, + _1, + _2, + address_privkey_g, + address_privkey_t)); + + // try opening Ko + EXPECT_TRUE(can_open_fcmp_onetime_address(address_privkey_g, + address_privkey_t, + single_scan_res.sender_extension_g, + single_scan_res.sender_extension_t, + parsed_enotes.at(single_scan_res.output_index).onetime_address)); + + // if this payment proposal isn't already marked as scanned, mark as scanned + if (!matched_payment_proposals.count(ss_prop_idx)) + { + matched_payment = true; + matched_payment_proposals.insert(ss_prop_idx); + break; + } + } + } + + // if this scan result has no matching payment... + if (!matched_payment) + { + EXPECT_EQ(nullptr, implicit_change_scan_res); // only one non-matched scan result is allowed + implicit_change_scan_res = &single_scan_res; // save the implicit change scan result for later + } + } + EXPECT_EQ(selfsend_payment_proposals.size(), matched_payment_proposals.size()); + EXPECT_NE(nullptr, implicit_change_scan_res); + // @TODO: assert properties of `implicit_change_scan_res` +} +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_impl, multi_account_transfer_over_transaction_1) +{ + // two accounts, both carrot + // 1/2 tx + // 1 normal payment to main address + // 0 explicit selfsend payments + + unittest_transaction_proposal tx_proposal; + tx_proposal.per_account_payments.resize(2); + mock_carrot_or_legacy_keys &acc0 = tx_proposal.per_account_payments[0].first; + mock_carrot_or_legacy_keys &acc1 = tx_proposal.per_account_payments[1].first; + acc0.generate_carrot(); + acc1.generate_carrot(); + + // 1 normal payment + CarrotPaymentProposalV1 &normal_payment_proposal = tools::add_element( tx_proposal.per_account_payments[0].second).first; + normal_payment_proposal = CarrotPaymentProposalV1{ + .destination = acc0.cryptonote_address(), + .amount = crypto::rand_idx((rct::xmr_amount) 1ull << 63), + .randomness = gen_janus_anchor() + }; + + // specify self-sender + tx_proposal.self_sender_index = 1; + + // specify fee per weight + tx_proposal.fee_per_weight = 20250510; + + // test + subtest_multi_account_transfer_over_transaction(tx_proposal); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_impl, multi_account_transfer_over_transaction_2) +{ + // four accounts, all carrot + // 1/4 tx + // 1 normal payment to main address, integrated address, and subaddress each + // 0 explicit selfsend payments + + unittest_transaction_proposal tx_proposal; + tx_proposal.per_account_payments.resize(4); + auto &acc0 = tx_proposal.per_account_payments[0]; + auto &acc1 = tx_proposal.per_account_payments[1]; + auto &acc2 = tx_proposal.per_account_payments[2]; + auto &acc3 = tx_proposal.per_account_payments[3]; + acc0.first.generate_carrot(); + acc1.first.generate_carrot(); + acc2.first.generate_carrot(); + acc3.first.generate_carrot(); + + // specify self-sender + tx_proposal.self_sender_index = 2; + + // 1 subaddress payment + tools::add_element(acc0.second).first = CarrotPaymentProposalV1{ + .destination = acc0.first.subaddress(2, 3), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + + // 1 main address payment + tools::add_element(acc1.second).first = CarrotPaymentProposalV1{ + .destination = acc1.first.cryptonote_address(), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + + // 1 integrated address payment + tools::add_element(acc3.second).first = CarrotPaymentProposalV1{ + .destination = acc3.first.cryptonote_address(gen_payment_id()), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + + // specify fee per weight + tx_proposal.fee_per_weight = 314159; + + // test + subtest_multi_account_transfer_over_transaction(tx_proposal); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_impl, multi_account_transfer_over_transaction_3) +{ + // four accounts, all carrot + // 1/6 tx + // 2 normal payment to main address, 1 integrated address, and 2 subaddress, each copied except integrated + // 0 explicit selfsend payments + + unittest_transaction_proposal tx_proposal; + tx_proposal.per_account_payments.resize(4); + auto &acc0 = tx_proposal.per_account_payments[0]; + auto &acc1 = tx_proposal.per_account_payments[1]; + auto &acc2 = tx_proposal.per_account_payments[2]; + auto &acc3 = tx_proposal.per_account_payments[3]; + acc0.first.generate_carrot(); + acc1.first.generate_carrot(); + acc2.first.generate_carrot(); + acc3.first.generate_carrot(); + + // specify self-sender + tx_proposal.self_sender_index = 2; + + // 2 subaddress payment + tools::add_element(acc0.second).first = CarrotPaymentProposalV1{ + .destination = acc0.first.subaddress(2, 3), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + acc0.second.push_back(acc0.second.front()); + acc0.second.back().first.randomness = gen_janus_anchor(); //mangle anchor_norm + + // 2 main address payment + tools::add_element(acc1.second).first = CarrotPaymentProposalV1{ + .destination = acc1.first.cryptonote_address(), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + acc1.second.push_back(acc1.second.front()); + acc1.second.back().first.randomness = gen_janus_anchor(); //mangle anchor_norm + + // 1 integrated address payment + tools::add_element(acc3.second).first = CarrotPaymentProposalV1{ + .destination = acc3.first.cryptonote_address(gen_payment_id()), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + + // specify fee per weight + tx_proposal.fee_per_weight = 314159; + + // test + subtest_multi_account_transfer_over_transaction(tx_proposal); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_impl, multi_account_transfer_over_transaction_4) +{ + // four accounts, all carrot + // 1/8 tx + // 2 normal payment to main address, 1 integrated address, and 2 subaddress, each copied except integrated + // 2 explicit selfsend payments: 1 main address destination, 1 subaddress destination + + unittest_transaction_proposal tx_proposal; + tx_proposal.per_account_payments.resize(4); + auto &acc0 = tx_proposal.per_account_payments[0]; + auto &acc1 = tx_proposal.per_account_payments[1]; + auto &acc2 = tx_proposal.per_account_payments[2]; + auto &acc3 = tx_proposal.per_account_payments[3]; + acc0.first.generate_carrot(); + acc1.first.generate_carrot(); + acc2.first.generate_carrot(); + acc3.first.generate_carrot(); + + // specify self-sender + tx_proposal.self_sender_index = 2; + + // 2 subaddress payment + tools::add_element(acc0.second).first = CarrotPaymentProposalV1{ + .destination = acc0.first.subaddress(2, 3), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + acc0.second.push_back(acc0.second.front()); + acc0.second.back().first.randomness = gen_janus_anchor(); //mangle anchor_norm + + // 2 main address payment + tools::add_element(acc1.second).first = CarrotPaymentProposalV1{ + .destination = acc1.first.cryptonote_address(), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + acc1.second.push_back(acc1.second.front()); + acc1.second.back().first.randomness = gen_janus_anchor(); //mangle anchor_norm + + // 1 integrated address payment + tools::add_element(acc3.second).first = CarrotPaymentProposalV1{ + .destination = acc3.first.cryptonote_address(gen_payment_id()), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + + // 1 main address selfsend + tools::add_element(tx_proposal.explicit_selfsend_proposals).first = CarrotPaymentProposalSelfSendV1{ + .destination_address_spend_pubkey = acc2.first.account_spend_pubkey, + .amount = crypto::rand_idx(1000000), + .enote_type = CarrotEnoteType::PAYMENT, + .internal_message = gen_janus_anchor() + }; + + // 1 subaddress selfsend + tools::add_element(tx_proposal.explicit_selfsend_proposals).first = CarrotPaymentProposalSelfSendV1{ + .destination_address_spend_pubkey = acc2.first.subaddress(4, 19).address_spend_pubkey, + .amount = crypto::rand_idx(1000000), + .enote_type = CarrotEnoteType::CHANGE + }; + + // specify fee per weight + tx_proposal.fee_per_weight = 314159; + + // test + subtest_multi_account_transfer_over_transaction(tx_proposal); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_impl, multi_account_transfer_over_transaction_5) +{ + // two accounts, both legacy + // 1/2 tx + // 1 normal payment to main address + // 0 explicit selfsend payments + + unittest_transaction_proposal tx_proposal; + tx_proposal.per_account_payments.resize(2); + mock_carrot_or_legacy_keys &acc0 = tx_proposal.per_account_payments[0].first; + mock_carrot_or_legacy_keys &acc1 = tx_proposal.per_account_payments[1].first; + acc0.generate_legacy(); + acc1.generate_legacy(); + + // 1 normal payment + CarrotPaymentProposalV1 &normal_payment_proposal = tools::add_element( tx_proposal.per_account_payments[0].second).first; + normal_payment_proposal = CarrotPaymentProposalV1{ + .destination = acc0.cryptonote_address(), + .amount = crypto::rand_idx((rct::xmr_amount) 1ull << 63), + .randomness = gen_janus_anchor() + }; + + // specify self-sender + tx_proposal.self_sender_index = 1; + + // specify fee per weight + tx_proposal.fee_per_weight = 20250510; + + // test + subtest_multi_account_transfer_over_transaction(tx_proposal); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_impl, multi_account_transfer_over_transaction_6) +{ + // four accounts, all legacy + // 1/4 tx + // 1 normal payment to main address, integrated address, and subaddress each + // 0 explicit selfsend payments + + unittest_transaction_proposal tx_proposal; + tx_proposal.per_account_payments.resize(4); + auto &acc0 = tx_proposal.per_account_payments[0]; + auto &acc1 = tx_proposal.per_account_payments[1]; + auto &acc2 = tx_proposal.per_account_payments[2]; + auto &acc3 = tx_proposal.per_account_payments[3]; + acc0.first.generate_legacy(); + acc1.first.generate_legacy(); + acc2.first.generate_legacy(); + acc3.first.generate_legacy(); + + // specify self-sender + tx_proposal.self_sender_index = 2; + + // 1 subaddress payment + tools::add_element(acc0.second).first = CarrotPaymentProposalV1{ + .destination = acc0.first.subaddress(2, 3), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + + // 1 main address payment + tools::add_element(acc1.second).first = CarrotPaymentProposalV1{ + .destination = acc1.first.cryptonote_address(), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + + // 1 integrated address payment + tools::add_element(acc3.second).first = CarrotPaymentProposalV1{ + .destination = acc3.first.cryptonote_address(gen_payment_id()), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + + // specify fee per weight + tx_proposal.fee_per_weight = 314159; + + // test + subtest_multi_account_transfer_over_transaction(tx_proposal); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_impl, multi_account_transfer_over_transaction_7) +{ + // four accounts, all legacy + // 1/6 tx + // 2 normal payment to main address, 1 integrated address, and 2 subaddress, each copied except integrated + // 0 explicit selfsend payments + + unittest_transaction_proposal tx_proposal; + tx_proposal.per_account_payments.resize(4); + auto &acc0 = tx_proposal.per_account_payments[0]; + auto &acc1 = tx_proposal.per_account_payments[1]; + auto &acc2 = tx_proposal.per_account_payments[2]; + auto &acc3 = tx_proposal.per_account_payments[3]; + acc0.first.generate_legacy(); + acc1.first.generate_legacy(); + acc2.first.generate_legacy(); + acc3.first.generate_legacy(); + + // specify self-sender + tx_proposal.self_sender_index = 2; + + // 2 subaddress payment + tools::add_element(acc0.second).first = CarrotPaymentProposalV1{ + .destination = acc0.first.subaddress(2, 3), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + acc0.second.push_back(acc0.second.front()); + acc0.second.back().first.randomness = gen_janus_anchor(); //mangle anchor_norm + + // 2 main address payment + tools::add_element(acc1.second).first = CarrotPaymentProposalV1{ + .destination = acc1.first.cryptonote_address(), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + acc1.second.push_back(acc1.second.front()); + acc1.second.back().first.randomness = gen_janus_anchor(); //mangle anchor_norm + + // 1 integrated address payment + tools::add_element(acc3.second).first = CarrotPaymentProposalV1{ + .destination = acc3.first.cryptonote_address(gen_payment_id()), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + + // specify fee per weight + tx_proposal.fee_per_weight = 314159; + + // test + subtest_multi_account_transfer_over_transaction(tx_proposal); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_impl, multi_account_transfer_over_transaction_8) +{ + // four accounts, all legacy + // 1/8 tx + // 2 normal payment to main address, 1 integrated address, and 2 subaddress, each copied except integrated + // 2 explicit selfsend payments: 1 main address destination, 1 subaddress destination + + unittest_transaction_proposal tx_proposal; + tx_proposal.per_account_payments.resize(4); + auto &acc0 = tx_proposal.per_account_payments[0]; + auto &acc1 = tx_proposal.per_account_payments[1]; + auto &acc2 = tx_proposal.per_account_payments[2]; + auto &acc3 = tx_proposal.per_account_payments[3]; + acc0.first.generate_legacy(); + acc1.first.generate_legacy(); + acc2.first.generate_legacy(); + acc3.first.generate_legacy(); + + // specify self-sender + tx_proposal.self_sender_index = 2; + + // 2 subaddress payment + tools::add_element(acc0.second).first = CarrotPaymentProposalV1{ + .destination = acc0.first.subaddress(2, 3), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + acc0.second.push_back(acc0.second.front()); + acc0.second.back().first.randomness = gen_janus_anchor(); //mangle anchor_norm + + // 2 main address payment + tools::add_element(acc1.second).first = CarrotPaymentProposalV1{ + .destination = acc1.first.cryptonote_address(), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + acc1.second.push_back(acc1.second.front()); + acc1.second.back().first.randomness = gen_janus_anchor(); //mangle anchor_norm + + // 1 integrated address payment + tools::add_element(acc3.second).first = CarrotPaymentProposalV1{ + .destination = acc3.first.cryptonote_address(gen_payment_id()), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + + // 1 main address selfsend + tools::add_element(tx_proposal.explicit_selfsend_proposals).first = CarrotPaymentProposalSelfSendV1{ + .destination_address_spend_pubkey = acc2.first.account_spend_pubkey, + .amount = crypto::rand_idx(1000000), + .enote_type = CarrotEnoteType::PAYMENT, + // no internal messages for legacy self-sends + }; + + // 1 subaddress selfsend + tools::add_element(tx_proposal.explicit_selfsend_proposals).first = CarrotPaymentProposalSelfSendV1{ + .destination_address_spend_pubkey = acc2.first.subaddress(4, 19).address_spend_pubkey, + .amount = crypto::rand_idx(1000000), + .enote_type = CarrotEnoteType::CHANGE + }; + + // specify fee per weight + tx_proposal.fee_per_weight = 314159; + + // test + subtest_multi_account_transfer_over_transaction(tx_proposal); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_impl, multi_account_transfer_over_transaction_9) +{ + // two accounts, both carrot + // 1/2 tx + // 1 normal payment to main address + // 0 explicit selfsend payments + // subtractable + + unittest_transaction_proposal tx_proposal; + tx_proposal.per_account_payments.resize(2); + mock_carrot_or_legacy_keys &acc0 = tx_proposal.per_account_payments[0].first; + mock_carrot_or_legacy_keys &acc1 = tx_proposal.per_account_payments[1].first; + acc0.generate_carrot(); + acc1.generate_carrot(); + + // 1 normal payment (subtractable) + CarrotPaymentProposalV1 &normal_payment_proposal = tools::add_element( tx_proposal.per_account_payments[0].second).first; + normal_payment_proposal = CarrotPaymentProposalV1{ + .destination = acc0.cryptonote_address(), + .amount = crypto::rand_idx((rct::xmr_amount) 1ull << 63), + .randomness = gen_janus_anchor() + }; + tx_proposal.per_account_payments[0].second.back().second = true; + + // specify self-sender + tx_proposal.self_sender_index = 1; + + // specify fee per weight + tx_proposal.fee_per_weight = 20250510; + + // test + subtest_multi_account_transfer_over_transaction(tx_proposal); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_impl, multi_account_transfer_over_transaction_10) +{ + // four accounts, all carrot + // 1/4 tx + // 1 normal payment to main address, integrated address, and subaddress each + // 0 explicit selfsend payments + // subaddress and integrated address are subtractable + + unittest_transaction_proposal tx_proposal; + tx_proposal.per_account_payments.resize(4); + auto &acc0 = tx_proposal.per_account_payments[0]; + auto &acc1 = tx_proposal.per_account_payments[1]; + auto &acc2 = tx_proposal.per_account_payments[2]; + auto &acc3 = tx_proposal.per_account_payments[3]; + acc0.first.generate_carrot(); + acc1.first.generate_carrot(); + acc2.first.generate_carrot(); + acc3.first.generate_carrot(); + + // specify self-sender + tx_proposal.self_sender_index = 2; + + // 1 subaddress payment (subtractable) + tools::add_element(acc0.second) = {CarrotPaymentProposalV1{ + .destination = acc0.first.subaddress(2, 3), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }, true}; + + // 1 main address payment + tools::add_element(acc1.second).first = CarrotPaymentProposalV1{ + .destination = acc1.first.cryptonote_address(), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + + // 1 integrated address payment + tools::add_element(acc3.second) = {CarrotPaymentProposalV1{ + .destination = acc3.first.cryptonote_address(gen_payment_id()), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }, true}; + + // specify fee per weight + tx_proposal.fee_per_weight = 314159; + + // test + subtest_multi_account_transfer_over_transaction(tx_proposal); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_impl, multi_account_transfer_over_transaction_11) +{ + // four accounts, all carrot + // 1/6 tx + // 2 normal payment to main address, 1 integrated address, and 2 subaddress, each copied except integrated + // 0 explicit selfsend payments + // 1 main and 1 subaddress is subtractable + + unittest_transaction_proposal tx_proposal; + tx_proposal.per_account_payments.resize(4); + auto &acc0 = tx_proposal.per_account_payments[0]; + auto &acc1 = tx_proposal.per_account_payments[1]; + auto &acc2 = tx_proposal.per_account_payments[2]; + auto &acc3 = tx_proposal.per_account_payments[3]; + acc0.first.generate_carrot(); + acc1.first.generate_carrot(); + acc2.first.generate_carrot(); + acc3.first.generate_carrot(); + + // specify self-sender + tx_proposal.self_sender_index = 2; + + // 2 subaddress payment + tools::add_element(acc0.second).first = CarrotPaymentProposalV1{ + .destination = acc0.first.subaddress(2, 3), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + acc0.second.push_back(acc0.second.front()); + acc0.second.back().first.randomness = gen_janus_anchor(); //mangle anchor_norm + acc0.second.back().second = true; //set copy as subtractable + + // 2 main address payment + tools::add_element(acc1.second).first = CarrotPaymentProposalV1{ + .destination = acc1.first.cryptonote_address(), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + acc1.second.push_back(acc1.second.front()); + acc1.second.back().first.randomness = gen_janus_anchor(); //mangle anchor_norm + acc1.second.back().second = true; //set copy as subtractable + + // 1 integrated address payment + tools::add_element(acc3.second).first = CarrotPaymentProposalV1{ + .destination = acc3.first.cryptonote_address(gen_payment_id()), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + + // specify fee per weight + tx_proposal.fee_per_weight = 314159; + + // test + subtest_multi_account_transfer_over_transaction(tx_proposal); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_impl, multi_account_transfer_over_transaction_12) +{ + // four accounts, all carrot + // 1/8 tx + // 2 normal payment to main address, 1 integrated address, and 2 subaddress, each copied except integrated + // 2 explicit selfsend payments: 1 main address destination, 1 subaddress destination + // 1 normal main address, 1 integrated, and 1 self-send subaddress is subtractable + + unittest_transaction_proposal tx_proposal; + tx_proposal.per_account_payments.resize(4); + auto &acc0 = tx_proposal.per_account_payments[0]; + auto &acc1 = tx_proposal.per_account_payments[1]; + auto &acc2 = tx_proposal.per_account_payments[2]; + auto &acc3 = tx_proposal.per_account_payments[3]; + acc0.first.generate_carrot(); + acc1.first.generate_carrot(); + acc2.first.generate_carrot(); + acc3.first.generate_carrot(); + + // specify self-sender + tx_proposal.self_sender_index = 2; + + // 2 subaddress payment (1 subtractable) + tools::add_element(acc0.second) = {CarrotPaymentProposalV1{ + .destination = acc0.first.subaddress(2, 3), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }, true}; + acc0.second.push_back(acc0.second.front()); + acc0.second.back().first.randomness = gen_janus_anchor(); //mangle anchor_norm + acc0.second.back().second = false; //set not subtractable, first already is + + // 2 main address payment + tools::add_element(acc1.second).first = CarrotPaymentProposalV1{ + .destination = acc1.first.cryptonote_address(), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + acc1.second.push_back(acc1.second.front()); + acc1.second.back().first.randomness = gen_janus_anchor(); //mangle anchor_norm + + // 1 integrated address payment (subtractable) + tools::add_element(acc3.second) = {CarrotPaymentProposalV1{ + .destination = acc3.first.cryptonote_address(gen_payment_id()), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }, true}; + + // 1 main address selfsend + tools::add_element(tx_proposal.explicit_selfsend_proposals).first = CarrotPaymentProposalSelfSendV1{ + .destination_address_spend_pubkey = acc2.first.account_spend_pubkey, + .amount = crypto::rand_idx(1000000), + .enote_type = CarrotEnoteType::PAYMENT, + .internal_message = gen_janus_anchor() + }; + + // 1 subaddress selfsend (subtractable) + tools::add_element(tx_proposal.explicit_selfsend_proposals) = {CarrotPaymentProposalSelfSendV1{ + .destination_address_spend_pubkey = acc2.first.subaddress(4, 19).address_spend_pubkey, + .amount = crypto::rand_idx(1000000), + .enote_type = CarrotEnoteType::CHANGE + }, true}; + + // specify fee per weight + tx_proposal.fee_per_weight = 314159; + + // test + subtest_multi_account_transfer_over_transaction(tx_proposal); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_impl, multi_account_transfer_over_transaction_13) +{ + // two accounts, both legacy + // 1/2 tx + // 1 normal payment to main address + // 0 explicit selfsend payments + // subtractable + + unittest_transaction_proposal tx_proposal; + tx_proposal.per_account_payments.resize(2); + mock_carrot_or_legacy_keys &acc0 = tx_proposal.per_account_payments[0].first; + mock_carrot_or_legacy_keys &acc1 = tx_proposal.per_account_payments[1].first; + acc0.generate_legacy(); + acc1.generate_legacy(); + + // 1 normal payment (subtractable) + CarrotPaymentProposalV1 &normal_payment_proposal = tools::add_element( tx_proposal.per_account_payments[0].second).first; + normal_payment_proposal = CarrotPaymentProposalV1{ + .destination = acc0.cryptonote_address(), + .amount = crypto::rand_idx((rct::xmr_amount) 1ull << 63), + .randomness = gen_janus_anchor() + }; + tx_proposal.per_account_payments[0].second.back().second = true; + + // specify self-sender + tx_proposal.self_sender_index = 1; + + // specify fee per weight + tx_proposal.fee_per_weight = 20250510; + + // test + subtest_multi_account_transfer_over_transaction(tx_proposal); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_impl, multi_account_transfer_over_transaction_14) +{ + // four accounts, all legacy + // 1/4 tx + // 1 normal payment to main address, integrated address, and subaddress each + // 0 explicit selfsend payments + // 1 integrated and 1 subaddress subtractable + + unittest_transaction_proposal tx_proposal; + tx_proposal.per_account_payments.resize(4); + auto &acc0 = tx_proposal.per_account_payments[0]; + auto &acc1 = tx_proposal.per_account_payments[1]; + auto &acc2 = tx_proposal.per_account_payments[2]; + auto &acc3 = tx_proposal.per_account_payments[3]; + acc0.first.generate_legacy(); + acc1.first.generate_legacy(); + acc2.first.generate_legacy(); + acc3.first.generate_legacy(); + + // specify self-sender + tx_proposal.self_sender_index = 2; + + // 1 subaddress payment (subtractable) + tools::add_element(acc0.second) = {CarrotPaymentProposalV1{ + .destination = acc0.first.subaddress(2, 3), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }, true}; + + // 1 main address payment + tools::add_element(acc1.second).first = CarrotPaymentProposalV1{ + .destination = acc1.first.cryptonote_address(), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + + // 1 integrated address payment (subtractable) + tools::add_element(acc3.second) = {CarrotPaymentProposalV1{ + .destination = acc3.first.cryptonote_address(gen_payment_id()), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }, true}; + + // specify fee per weight + tx_proposal.fee_per_weight = 314159; + + // test + subtest_multi_account_transfer_over_transaction(tx_proposal); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_impl, multi_account_transfer_over_transaction_15) +{ + // four accounts, all legacy + // 1/6 tx + // 2 normal payment to main address, 1 integrated address, and 2 subaddress, each copied except integrated + // 0 explicit selfsend payments + // all subtractable + + unittest_transaction_proposal tx_proposal; + tx_proposal.per_account_payments.resize(4); + auto &acc0 = tx_proposal.per_account_payments[0]; + auto &acc1 = tx_proposal.per_account_payments[1]; + auto &acc2 = tx_proposal.per_account_payments[2]; + auto &acc3 = tx_proposal.per_account_payments[3]; + acc0.first.generate_legacy(); + acc1.first.generate_legacy(); + acc2.first.generate_legacy(); + acc3.first.generate_legacy(); + + // specify self-sender + tx_proposal.self_sender_index = 2; + + // 2 subaddress payment (subtractable) + tools::add_element(acc0.second) = {CarrotPaymentProposalV1{ + .destination = acc0.first.subaddress(2, 3), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }, true}; + acc0.second.push_back(acc0.second.front()); + acc0.second.back().first.randomness = gen_janus_anchor(); //mangle anchor_norm + + // 2 main address payment (subtractable) + tools::add_element(acc1.second) = {CarrotPaymentProposalV1{ + .destination = acc1.first.cryptonote_address(), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }, true}; + acc1.second.push_back(acc1.second.front()); + acc1.second.back().first.randomness = gen_janus_anchor(); //mangle anchor_norm + + // 1 integrated address payment (subtractable) + tools::add_element(acc3.second) = {CarrotPaymentProposalV1{ + .destination = acc3.first.cryptonote_address(gen_payment_id()), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }, true}; + + // specify fee per weight + tx_proposal.fee_per_weight = 314159; + + // test + subtest_multi_account_transfer_over_transaction(tx_proposal); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_impl, multi_account_transfer_over_transaction_16) +{ + // four accounts, all legacy + // 1/8 tx + // 2 normal payment to main address, 1 integrated address, and 2 subaddress, each copied except integrated + // 2 explicit selfsend payments: 1 main address destination, 1 subaddress destination + // all subtractable + + unittest_transaction_proposal tx_proposal; + tx_proposal.per_account_payments.resize(4); + auto &acc0 = tx_proposal.per_account_payments[0]; + auto &acc1 = tx_proposal.per_account_payments[1]; + auto &acc2 = tx_proposal.per_account_payments[2]; + auto &acc3 = tx_proposal.per_account_payments[3]; + acc0.first.generate_legacy(); + acc1.first.generate_legacy(); + acc2.first.generate_legacy(); + acc3.first.generate_legacy(); + + // specify self-sender + tx_proposal.self_sender_index = 2; + + // 2 subaddress payment (subtractable) + tools::add_element(acc0.second) = {CarrotPaymentProposalV1{ + .destination = acc0.first.subaddress(2, 3), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }, true}; + acc0.second.push_back(acc0.second.front()); + acc0.second.back().first.randomness = gen_janus_anchor(); //mangle anchor_norm + + // 2 main address payment (subtractable) + tools::add_element(acc1.second) = {CarrotPaymentProposalV1{ + .destination = acc1.first.cryptonote_address(), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }, true}; + acc1.second.push_back(acc1.second.front()); + acc1.second.back().first.randomness = gen_janus_anchor(); //mangle anchor_norm + + // 1 integrated address payment (subtractable) + tools::add_element(acc3.second) = {CarrotPaymentProposalV1{ + .destination = acc3.first.cryptonote_address(gen_payment_id()), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }, true}; + + // 1 main address selfsend (subtractable) + tools::add_element(tx_proposal.explicit_selfsend_proposals) = {CarrotPaymentProposalSelfSendV1{ + .destination_address_spend_pubkey = acc2.first.account_spend_pubkey, + .amount = crypto::rand_idx(1000000), + .enote_type = CarrotEnoteType::PAYMENT, + // no internal messages for legacy self-sends + }, true}; + + // 1 subaddress selfsend (subtractable) + tools::add_element(tx_proposal.explicit_selfsend_proposals) = {CarrotPaymentProposalSelfSendV1{ + .destination_address_spend_pubkey = acc2.first.subaddress(4, 19).address_spend_pubkey, + .amount = crypto::rand_idx(1000000), + .enote_type = CarrotEnoteType::CHANGE + }, true}; + + // specify fee per weight + tx_proposal.fee_per_weight = 314159; + + // test + subtest_multi_account_transfer_over_transaction(tx_proposal); +} +//---------------------------------------------------------------------------------------------------------------------- diff --git a/tests/unit_tests/carrot_legacy.cpp b/tests/unit_tests/carrot_legacy.cpp index d94d49fc9a3..7e1faf0e86e 100644 --- a/tests/unit_tests/carrot_legacy.cpp +++ b/tests/unit_tests/carrot_legacy.cpp @@ -185,11 +185,12 @@ static void subtest_legacy_2out_transfer_get_output_enote_proposals_completeness // alice mem devices view_incoming_key_ram_borrowed_device alive_k_v_dev(alice.get_keys().m_view_secret_key); - // turn payment proposals into enotes + // turn payment proposals into enotes, always pass dummy pid std::vector enote_proposals; encrypted_payment_id_t encrypted_payment_id; get_output_enote_proposals({bob_payment_proposal}, {alice_payment_proposal}, + gen_payment_id(), nullptr, &alive_k_v_dev, alice.get_keys().m_account_address.m_spend_public_key, From f3cf1b57e7850d0876f110c84328f7a8b065ff5d Mon Sep 17 00:00:00 2001 From: jeffro256 Date: Thu, 30 Jan 2025 23:35:36 -0600 Subject: [PATCH 07/15] rm carrot_impl.cpp --- tests/unit_tests/carrot_impl.cpp | 1520 ------------------------------ 1 file changed, 1520 deletions(-) delete mode 100644 tests/unit_tests/carrot_impl.cpp diff --git a/tests/unit_tests/carrot_impl.cpp b/tests/unit_tests/carrot_impl.cpp deleted file mode 100644 index 4fe5c9e536e..00000000000 --- a/tests/unit_tests/carrot_impl.cpp +++ /dev/null @@ -1,1520 +0,0 @@ -// 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. - -#include "gtest/gtest.h" - -#include - -#include "carrot_core/account_secrets.h" -#include "carrot_core/address_utils.h" -#include "carrot_core/carrot_enote_scan.h" -#include "carrot_core/destination.h" -#include "carrot_core/device_ram_borrowed.h" -#include "carrot_core/enote_utils.h" -#include "carrot_core/output_set_finalization.h" -#include "carrot_core/payment_proposal.h" -#include "carrot_impl/carrot_tx_builder.h" -#include "carrot_impl/carrot_tx_format_utils.h" -#include "common/container_helpers.h" -#include "crypto/generators.h" -#include "cryptonote_basic/account.h" -#include "cryptonote_basic/subaddress_index.h" -#include "ringct/rctOps.h" - -using namespace carrot; - -//---------------------------------------------------------------------------------------------------------------------- -//---------------------------------------------------------------------------------------------------------------------- -namespace -{ -//---------------------------------------------------------------------------------------------------------------------- -//---------------------------------------------------------------------------------------------------------------------- -static constexpr std::uint32_t MAX_SUBADDRESS_MAJOR_INDEX = 5; -static constexpr std::uint32_t MAX_SUBADDRESS_MINOR_INDEX = 20; -//---------------------------------------------------------------------------------------------------------------------- -//---------------------------------------------------------------------------------------------------------------------- -struct mock_carrot_or_legacy_keys -{ - bool is_carrot; - - crypto::secret_key s_master; - crypto::secret_key k_prove_spend; - crypto::secret_key s_view_balance; - crypto::secret_key k_generate_image; - crypto::secret_key k_view; - crypto::secret_key s_generate_address; - crypto::public_key account_spend_pubkey; - crypto::public_key account_view_pubkey; - crypto::public_key main_address_view_pubkey; - - cryptonote::account_base legacy_acb; - - view_incoming_key_ram_borrowed_device k_view_dev; - view_balance_secret_ram_borrowed_device s_view_balance_dev; - - std::unordered_map subaddress_map; - - mock_carrot_or_legacy_keys(): k_view_dev(k_view), s_view_balance_dev(s_view_balance) {} - - const view_balance_secret_device* get_view_balance_device() const - { - return is_carrot ? &s_view_balance_dev : nullptr; - } - - CarrotDestinationV1 cryptonote_address(const payment_id_t payment_id = null_payment_id) const - { - CarrotDestinationV1 addr; - if (is_carrot) - { - make_carrot_integrated_address_v1(account_spend_pubkey, - main_address_view_pubkey, - payment_id, - addr); - } - else - { - make_carrot_integrated_address_v1(legacy_acb.get_keys().m_account_address.m_spend_public_key, - legacy_acb.get_keys().m_account_address.m_view_public_key, - payment_id, - addr); - } - return addr; - } - - CarrotDestinationV1 subaddress(const uint32_t major_index, const uint32_t minor_index) const - { - if (!major_index && !minor_index) - return cryptonote_address(); - - CarrotDestinationV1 addr; - if (is_carrot) - { - make_carrot_subaddress_v1(account_spend_pubkey, - account_view_pubkey, - s_generate_address, - major_index, - minor_index, - addr); - } - else - { - const cryptonote::account_keys &ks = legacy_acb.get_keys(); - const cryptonote::account_public_address cnaddr = - ks.m_device->get_subaddress(ks, {major_index, minor_index}); - addr = CarrotDestinationV1{ - .address_spend_pubkey = cnaddr.m_spend_public_key, - .address_view_pubkey = cnaddr.m_view_public_key, - .is_subaddress = true, - .payment_id = null_payment_id - }; - } - return addr; - } - - // brief: opening_for_subaddress - return (k^g_a, k^t_a) for j s.t. K^j_s = (k^g_a * G + k^t_a * T) - void opening_for_subaddress(const uint32_t major_index, - const uint32_t minor_index, - crypto::secret_key &address_privkey_g_out, - crypto::secret_key &address_privkey_t_out, - crypto::public_key &address_spend_pubkey_out) const - { - const bool is_subaddress = major_index || minor_index; - - if (is_carrot) - { - // s^j_gen = H_32[s_ga](j_major, j_minor) - crypto::secret_key address_index_generator; - make_carrot_index_extension_generator(s_generate_address, major_index, minor_index, address_index_generator); - - crypto::secret_key subaddress_scalar{}; - if (is_subaddress) - { - // k^j_subscal = H_n(K_s, j_major, j_minor, s^j_gen) - make_carrot_subaddress_scalar(account_spend_pubkey, address_index_generator, major_index, minor_index, subaddress_scalar); - } - else - { - subaddress_scalar.data[0] = 1; - } - - // k^g_a = k_gi * k^j_subscal - sc_mul(to_bytes(address_privkey_g_out), to_bytes(k_generate_image), to_bytes(subaddress_scalar)); - - // k^t_a = k_ps * k^j_subscal - sc_mul(to_bytes(address_privkey_t_out), to_bytes(k_prove_spend), to_bytes(subaddress_scalar)); - } - else // legacy keys - { - // m = Hn(k_v || j_major || j_minor) if subaddress else 0 - const cryptonote::account_keys &ks = legacy_acb.get_keys(); - const crypto::secret_key subaddress_extension = is_subaddress - ? ks.get_device().get_subaddress_secret_key(ks.m_view_secret_key, {major_index, minor_index}) - : crypto::null_skey; - - // k^g_a = k_s + m - sc_add(to_bytes(address_privkey_g_out), to_bytes(ks.m_spend_secret_key), to_bytes(subaddress_extension)); - - // k^t_a = 0 - memset(address_privkey_t_out.data, 0, sizeof(address_privkey_t_out)); - } - - // perform sanity check - const CarrotDestinationV1 addr = subaddress(major_index, minor_index); - rct::key recomputed_address_spend_pubkey; - rct::addKeys2(recomputed_address_spend_pubkey, - rct::sk2rct(address_privkey_g_out), - rct::sk2rct(address_privkey_t_out), - rct::pk2rct(crypto::get_T())); - CHECK_AND_ASSERT_THROW_MES(rct::rct2pk(recomputed_address_spend_pubkey) == addr.address_spend_pubkey, - "mock carrot or legacy keys: opening for subaddress: failed sanity check"); - address_spend_pubkey_out = addr.address_spend_pubkey; - } - - bool try_searching_for_opening_for_subaddress(const crypto::public_key &address_spend_pubkey, - const uint32_t max_major_index, - const uint32_t max_minor_index, - uint32_t major_index_out, - uint32_t minor_index_out, - crypto::secret_key &address_privkey_g_out, - crypto::secret_key &address_privkey_t_out) const - { - const auto it = subaddress_map.find(address_spend_pubkey); - if (it == subaddress_map.cend()) - return false; - - crypto::public_key recomputed_address_spend_pubkey; - opening_for_subaddress(it->second.major, - it->second.minor, - address_privkey_g_out, - address_privkey_t_out, - recomputed_address_spend_pubkey); - - return address_spend_pubkey == recomputed_address_spend_pubkey; - } - - void generate_subaddress_map() - { - for (uint32_t major_index = 0; major_index < MAX_SUBADDRESS_MAJOR_INDEX; ++major_index) - { - for (uint32_t minor_index = 0; minor_index < MAX_SUBADDRESS_MINOR_INDEX; ++minor_index) - { - const CarrotDestinationV1 addr = subaddress(major_index, minor_index); - subaddress_map.insert({addr.address_spend_pubkey, {major_index, minor_index}}); - } - } - } - - void generate_carrot() - { - is_carrot = true; - crypto::generate_random_bytes_thread_safe(sizeof(crypto::secret_key), to_bytes(s_master)); - make_carrot_provespend_key(s_master, k_prove_spend); - make_carrot_viewbalance_secret(s_master, s_view_balance); - make_carrot_generateimage_key(s_view_balance, k_generate_image); - make_carrot_viewincoming_key(s_view_balance, k_view); - make_carrot_generateaddress_secret(s_view_balance, s_generate_address); - make_carrot_spend_pubkey(k_generate_image, k_prove_spend, account_spend_pubkey); - account_view_pubkey = rct::rct2pk(rct::scalarmultKey(rct::pk2rct(account_spend_pubkey), - rct::sk2rct(k_view))); - main_address_view_pubkey = rct::rct2pk(rct::scalarmultBase(rct::sk2rct(k_view))); - - generate_subaddress_map(); - } - - void generate_legacy() - { - is_carrot = false; - legacy_acb.generate(); - k_view = legacy_acb.get_keys().m_view_secret_key; - account_spend_pubkey = legacy_acb.get_keys().m_account_address.m_spend_public_key; - main_address_view_pubkey = legacy_acb.get_keys().m_account_address.m_view_public_key; - - generate_subaddress_map(); - } -}; -//---------------------------------------------------------------------------------------------------------------------- -//---------------------------------------------------------------------------------------------------------------------- -static bool can_open_fcmp_onetime_address(const crypto::secret_key &address_privkey_g, - const crypto::secret_key &address_privkey_t, - const crypto::secret_key &sender_extension_g, - const crypto::secret_key &sender_extension_t, - const crypto::public_key &onetime_address) -{ - rct::key combined_g; - sc_add(combined_g.bytes, to_bytes(address_privkey_g), to_bytes(sender_extension_g)); - - rct::key combined_t; - sc_add(combined_t.bytes, to_bytes(address_privkey_t), to_bytes(sender_extension_t)); - - // Ko' = combined_g G + combined_t T - rct::key recomputed_onetime_address; - rct::addKeys2(recomputed_onetime_address, combined_g, combined_t, rct::pk2rct(crypto::get_T())); - - // Ko' ?= Ko - return recomputed_onetime_address == onetime_address; -} -//---------------------------------------------------------------------------------------------------------------------- -//---------------------------------------------------------------------------------------------------------------------- -struct unittest_carrot_scan_result_t -{ - crypto::public_key address_spend_pubkey = rct::rct2pk(rct::I); - crypto::secret_key sender_extension_g = rct::rct2sk(rct::I); - crypto::secret_key sender_extension_t = rct::rct2sk(rct::I); - - rct::xmr_amount amount = 0; - crypto::secret_key amount_blinding_factor = rct::rct2sk(rct::I); - - CarrotEnoteType enote_type = CarrotEnoteType::PAYMENT; - - payment_id_t payment_id = null_payment_id; - - janus_anchor_t internal_message = janus_anchor_t{}; - - size_t output_index = 0; -}; -static void unittest_scan_enote_set(const std::vector &enotes, - const encrypted_payment_id_t encrypted_payment_id, - const mock_carrot_or_legacy_keys keys, - std::vector &res) -{ - res.clear(); - - // for each enote... - for (size_t output_index = 0; output_index < enotes.size(); ++output_index) - { - const CarrotEnoteV1 &enote = enotes.at(output_index); - - // s_sr = k_v D_e - mx25519_pubkey s_sr; - make_carrot_uncontextualized_shared_key_receiver(keys.k_view, enote.enote_ephemeral_pubkey, s_sr); - - // external scan - unittest_carrot_scan_result_t scan_result{}; - bool r = try_scan_carrot_enote_external(enote, - encrypted_payment_id, - s_sr, - keys.k_view_dev, - keys.account_spend_pubkey, - scan_result.sender_extension_g, - scan_result.sender_extension_t, - scan_result.address_spend_pubkey, - scan_result.amount, - scan_result.amount_blinding_factor, - scan_result.payment_id, - scan_result.enote_type); - - // internal scan - r = r || try_scan_carrot_enote_internal(enote, - keys.s_view_balance_dev, - scan_result.sender_extension_g, - scan_result.sender_extension_t, - scan_result.address_spend_pubkey, - scan_result.amount, - scan_result.amount_blinding_factor, - scan_result.enote_type, - scan_result.internal_message); - - scan_result.output_index = output_index; - - if (r) - res.push_back(scan_result); - } -} -//---------------------------------------------------------------------------------------------------------------------- -//---------------------------------------------------------------------------------------------------------------------- -static void unittest_scan_enote_set_multi_account(const std::vector &enotes, - const encrypted_payment_id_t encrypted_payment_id, - const epee::span accounts, - std::vector> &res) -{ - res.clear(); - res.reserve(accounts.size()); - - for (const mock_carrot_or_legacy_keys *account : accounts) - unittest_scan_enote_set(enotes, encrypted_payment_id, *account, tools::add_element(res)); -} -//---------------------------------------------------------------------------------------------------------------------- -//---------------------------------------------------------------------------------------------------------------------- -static bool compare_scan_result(const unittest_carrot_scan_result_t &scan_res, - const CarrotPaymentProposalV1 &normal_payment_proposal, - const rct::xmr_amount allowed_fee_margin_opt = 0) -{ - if (scan_res.address_spend_pubkey != normal_payment_proposal.destination.address_spend_pubkey) - return false; - - if (scan_res.amount > normal_payment_proposal.amount) - return false; - - if (normal_payment_proposal.amount - scan_res.amount > allowed_fee_margin_opt) - return false; - - if (scan_res.enote_type != CarrotEnoteType::PAYMENT) - return false; - - if (scan_res.payment_id != normal_payment_proposal.destination.payment_id) - return false; - - if (scan_res.internal_message != janus_anchor_t{}) - return false; - - return true; -} -//---------------------------------------------------------------------------------------------------------------------- -//---------------------------------------------------------------------------------------------------------------------- -static bool compare_scan_result(const unittest_carrot_scan_result_t &scan_res, - const CarrotPaymentProposalSelfSendV1 &selfsend_payment_proposal, - const rct::xmr_amount allowed_fee_margin_opt = 0) -{ - if (scan_res.address_spend_pubkey != selfsend_payment_proposal.destination_address_spend_pubkey) - return false; - - if (scan_res.amount > selfsend_payment_proposal.amount) - return false; - - if (selfsend_payment_proposal.amount - scan_res.amount > allowed_fee_margin_opt) - return false; - - if (scan_res.enote_type != selfsend_payment_proposal.enote_type) - return false; - - if (scan_res.payment_id != null_payment_id) - return false; - - if (scan_res.internal_message != selfsend_payment_proposal.internal_message.value_or(janus_anchor_t{})) - return false; - - return true; -} -//---------------------------------------------------------------------------------------------------------------------- -//---------------------------------------------------------------------------------------------------------------------- -struct unittest_transaction_proposal -{ - using per_payment_proposal = std::pair; - using per_ss_payment_proposal = std::pair; - using per_account = std::pair>; - using per_input = std::pair; - - std::vector per_account_payments; - std::vector explicit_selfsend_proposals; - size_t self_sender_index{0}; - rct::xmr_amount fee_per_weight; - - void get_flattened_payment_proposals(std::vector &normal_payment_proposals_out, - std::vector &selfsend_payment_proposals_out, - std::set &subtractable_normal_payment_proposals, - std::set &subtractable_selfsend_payment_proposals) const - { - size_t norm_idx = 0; - for (const per_account &pa : per_account_payments) - { - for (const per_payment_proposal &ppp : pa.second) - { - normal_payment_proposals_out.push_back(ppp.first); - if (ppp.second) - subtractable_normal_payment_proposals.insert(norm_idx); - - norm_idx++; - } - } - - for (size_t ss_idx = 0; ss_idx < explicit_selfsend_proposals.size(); ++ss_idx) - { - const per_ss_payment_proposal &pspp = explicit_selfsend_proposals.at(ss_idx); - selfsend_payment_proposals_out.push_back(pspp.first); - if (pspp.second) - subtractable_selfsend_payment_proposals.insert(ss_idx); - } - } -}; -//---------------------------------------------------------------------------------------------------------------------- -//---------------------------------------------------------------------------------------------------------------------- -select_inputs_func_t make_fake_input_selection_callback(size_t num_ins = 0) -{ - return [num_ins](const boost::multiprecision::int128_t &nominal_output_sum, - const std::map &fee_per_input_count, - std::vector &selected_inputs) - { - const size_t nins = num_ins ? num_ins : 1; - selected_inputs.clear(); - selected_inputs.reserve(nins); - - const rct::xmr_amount fee = fee_per_input_count.at(nins); - rct::xmr_amount in_amount_sum_64 = boost::numeric_cast(nominal_output_sum + fee); - - for (size_t i = 0; i < nins - 1; ++i) - { - const rct::xmr_amount current_in_amount = in_amount_sum_64 ? crypto::rand_idx(in_amount_sum_64) : 0; - const crypto::key_image current_key_image = rct::rct2ki(rct::pkGen()); - selected_inputs.push_back({current_in_amount, current_key_image}); - in_amount_sum_64 -= current_in_amount; - } - - selected_inputs.push_back({in_amount_sum_64, rct::rct2ki(rct::pkGen())}); - }; -} -//---------------------------------------------------------------------------------------------------------------------- -//---------------------------------------------------------------------------------------------------------------------- -} // namespace -static inline bool operator==(const mx25519_pubkey &a, const mx25519_pubkey &b) -{ - return 0 == memcmp(&a, &b, sizeof(mx25519_pubkey)); -} -//---------------------------------------------------------------------------------------------------------------------- -//---------------------------------------------------------------------------------------------------------------------- -static void subtest_multi_account_transfer_over_transaction(const unittest_transaction_proposal &tx_proposal) -{ - // get payment proposals - std::vector normal_payment_proposals; - std::vector selfsend_payment_proposals; - std::set subtractable_normal_payment_proposals; - std::set subtractable_selfsend_payment_proposals; - tx_proposal.get_flattened_payment_proposals(normal_payment_proposals, - selfsend_payment_proposals, - subtractable_normal_payment_proposals, - subtractable_selfsend_payment_proposals); - - // get self-sender account - const mock_carrot_or_legacy_keys &ss_keys = - tx_proposal.per_account_payments.at(tx_proposal.self_sender_index).first; - - // make unsigned transaction - std::vector modified_normal_payment_proposals = normal_payment_proposals; - std::vector modified_selfsend_payment_proposals = selfsend_payment_proposals; - cryptonote::transaction tx; - std::vector output_amount_blinding_factors; - make_unsigned_transaction_transfer(modified_normal_payment_proposals, - modified_selfsend_payment_proposals, - tx_proposal.fee_per_weight, - make_fake_input_selection_callback(), - ss_keys.get_view_balance_device(), - &ss_keys.k_view_dev, - ss_keys.account_spend_pubkey, - tx, - output_amount_blinding_factors); - - // calculate acceptable fee margin between proposed amount and actual amount for subtractable outputs - const size_t num_subtractable = subtractable_normal_payment_proposals.size() + - subtractable_selfsend_payment_proposals.size(); - const rct::xmr_amount acceptable_fee_margin = num_subtractable - ? (tx.rct_signatures.txnFee / num_subtractable) + 1 - : 0; - - // load carrot stuff from tx - std::vector parsed_enotes; - std::vector parsed_key_images; - rct::xmr_amount parsed_fee; - std::optional parsed_encrypted_payment_id; - ASSERT_TRUE(try_load_carrot_from_transaction_v1(tx, - parsed_enotes, - parsed_key_images, - parsed_fee, - parsed_encrypted_payment_id)); - - // sanity check that the enotes and pid_enc loaded from the transaction are equal to the enotes - // and pic_enc returned from get_output_enote_proposals() when called with the modified payment - // proposals. we do this so that the modified payment proposals from make_unsigned_transaction() - // can be passed to a hardware device for deterministic verification of the signable tx hash - std::vector rederived_output_enote_proposals; - encrypted_payment_id_t rederived_encrypted_payment_id; - get_output_enote_proposals(modified_normal_payment_proposals, - modified_selfsend_payment_proposals, - ss_keys.get_view_balance_device(), - &ss_keys.k_view_dev, - ss_keys.account_spend_pubkey, - parsed_key_images.at(0), - rederived_output_enote_proposals, - rederived_encrypted_payment_id); - ASSERT_TRUE(parsed_encrypted_payment_id); - EXPECT_EQ(*parsed_encrypted_payment_id, rederived_encrypted_payment_id); - ASSERT_EQ(parsed_enotes.size(), rederived_output_enote_proposals.size()); - for (size_t enote_idx = 0; enote_idx < parsed_enotes.size(); ++enote_idx) - { - EXPECT_EQ(parsed_enotes.at(enote_idx), rederived_output_enote_proposals.at(enote_idx).enote); - } - - // collect accounts - std::vector accounts; - for (const auto &pa : tx_proposal.per_account_payments) - accounts.push_back(&pa.first); - - // do scanning of all accounts on every enotes - std::vector> scan_results; - unittest_scan_enote_set_multi_account(parsed_enotes, - *parsed_encrypted_payment_id, - epee::to_span(accounts), - scan_results); - - // check that the scan results for each *normal* account match the corresponding payment - // proposals for each account. also check that the accounts can each open their corresponding - // onetime outut pubkeys - ASSERT_EQ(scan_results.size(), accounts.size()); - // for each normal account... - for (size_t account_idx = 0; account_idx < accounts.size(); ++account_idx) - { - // skip self-sender account - if (account_idx == tx_proposal.self_sender_index) - continue; - - const std::vector &account_scan_results = scan_results.at(account_idx); - const auto &account_payment_proposals = tx_proposal.per_account_payments.at(account_idx).second; - ASSERT_EQ(account_payment_proposals.size(), account_scan_results.size()); - std::set matched_payment_proposals; - - // for each scan result assigned to this account... - for (const unittest_carrot_scan_result_t &single_scan_res : account_scan_results) - { - // for each normal payment proposal to this account... - for (size_t norm_prop_idx = 0; norm_prop_idx < account_payment_proposals.size(); ++norm_prop_idx) - { - // calculate acceptable loss from fee subtraction - const CarrotPaymentProposalV1 &account_payment_proposal = account_payment_proposals.at(norm_prop_idx).first; - const bool is_subtractable = subtractable_normal_payment_proposals.count(norm_prop_idx); - const rct::xmr_amount acceptable_fee_margin_for_proposal = is_subtractable ? acceptable_fee_margin : 0; - - // if the scan result matches the payment proposal... - if (compare_scan_result(single_scan_res, account_payment_proposal, acceptable_fee_margin_for_proposal)) - { - // try searching for subaddress opening - crypto::secret_key address_privkey_g; - crypto::secret_key address_privkey_t; - uint32_t _1{}, _2{}; - EXPECT_TRUE(accounts.at(account_idx)->try_searching_for_opening_for_subaddress( - single_scan_res.address_spend_pubkey, - MAX_SUBADDRESS_MAJOR_INDEX, - MAX_SUBADDRESS_MINOR_INDEX, - _1, - _2, - address_privkey_g, - address_privkey_t)); - - // try opening Ko - EXPECT_TRUE(can_open_fcmp_onetime_address(address_privkey_g, - address_privkey_t, - single_scan_res.sender_extension_g, - single_scan_res.sender_extension_t, - parsed_enotes.at(single_scan_res.output_index).onetime_address)); - - // if this payment proposal isn't already marked as scanned, mark as scanned - if (!matched_payment_proposals.count(norm_prop_idx)) - { - matched_payment_proposals.insert(norm_prop_idx); - break; - } - } - } - } - // check that the number of matched payment proposals is equal to the original number of them - // doing it this way checks that the same payment proposal isn't marked twice and another left out - EXPECT_EQ(account_payment_proposals.size(), matched_payment_proposals.size()); - } - - // check that the scan results for the selfsend account match the corresponding payment - // proposals. also check that the accounts can each open their corresponding onetime outut pubkeys - const std::vector &account_scan_results = scan_results.at(tx_proposal.self_sender_index); - ASSERT_EQ(selfsend_payment_proposals.size() + 1, account_scan_results.size()); - std::set matched_payment_proposals; - const unittest_carrot_scan_result_t* implicit_change_scan_res = nullptr; - // for each scan result assigned to the self-sender account... - for (const unittest_carrot_scan_result_t &single_scan_res : account_scan_results) - { - bool matched_payment = false; - // for each self-send payment proposal... - for (size_t ss_prop_idx = 0; ss_prop_idx < selfsend_payment_proposals.size(); ++ss_prop_idx) - { - // calculate acceptable loss from fee subtraction - const CarrotPaymentProposalSelfSendV1 &account_payment_proposal = selfsend_payment_proposals.at(ss_prop_idx); - const bool is_subtractable = subtractable_selfsend_payment_proposals.count(ss_prop_idx); - const rct::xmr_amount acceptable_fee_margin_for_proposal = is_subtractable ? acceptable_fee_margin : 0; - - // if the scan result matches the payment proposal... - if (compare_scan_result(single_scan_res, account_payment_proposal, acceptable_fee_margin_for_proposal)) - { - // try searching for subaddress opening - crypto::secret_key address_privkey_g; - crypto::secret_key address_privkey_t; - uint32_t _1{}, _2{}; - EXPECT_TRUE(ss_keys.try_searching_for_opening_for_subaddress( - single_scan_res.address_spend_pubkey, - MAX_SUBADDRESS_MAJOR_INDEX, - MAX_SUBADDRESS_MINOR_INDEX, - _1, - _2, - address_privkey_g, - address_privkey_t)); - - // try opening Ko - EXPECT_TRUE(can_open_fcmp_onetime_address(address_privkey_g, - address_privkey_t, - single_scan_res.sender_extension_g, - single_scan_res.sender_extension_t, - parsed_enotes.at(single_scan_res.output_index).onetime_address)); - - // if this payment proposal isn't already marked as scanned, mark as scanned - if (!matched_payment_proposals.count(ss_prop_idx)) - { - matched_payment = true; - matched_payment_proposals.insert(ss_prop_idx); - break; - } - } - } - - // if this scan result has no matching payment... - if (!matched_payment) - { - EXPECT_EQ(nullptr, implicit_change_scan_res); // only one non-matched scan result is allowed - implicit_change_scan_res = &single_scan_res; // save the implicit change scan result for later - } - } - EXPECT_EQ(selfsend_payment_proposals.size(), matched_payment_proposals.size()); - EXPECT_NE(nullptr, implicit_change_scan_res); - // @TODO: assert properties of `implicit_change_scan_res` -} -//---------------------------------------------------------------------------------------------------------------------- -//---------------------------------------------------------------------------------------------------------------------- -TEST(carrot_impl, multi_account_transfer_over_transaction_1) -{ - // two accounts, both carrot - // 1/2 tx - // 1 normal payment to main address - // 0 explicit selfsend payments - - unittest_transaction_proposal tx_proposal; - tx_proposal.per_account_payments.resize(2); - mock_carrot_or_legacy_keys &acc0 = tx_proposal.per_account_payments[0].first; - mock_carrot_or_legacy_keys &acc1 = tx_proposal.per_account_payments[1].first; - acc0.generate_carrot(); - acc1.generate_carrot(); - - // 1 normal payment - CarrotPaymentProposalV1 &normal_payment_proposal = tools::add_element( tx_proposal.per_account_payments[0].second).first; - normal_payment_proposal = CarrotPaymentProposalV1{ - .destination = acc0.cryptonote_address(), - .amount = crypto::rand_idx((rct::xmr_amount) 1ull << 63), - .randomness = gen_janus_anchor() - }; - - // specify self-sender - tx_proposal.self_sender_index = 1; - - // specify fee per weight - tx_proposal.fee_per_weight = 20250510; - - // test - subtest_multi_account_transfer_over_transaction(tx_proposal); -} -//---------------------------------------------------------------------------------------------------------------------- -TEST(carrot_impl, multi_account_transfer_over_transaction_2) -{ - // four accounts, all carrot - // 1/4 tx - // 1 normal payment to main address, integrated address, and subaddress each - // 0 explicit selfsend payments - - unittest_transaction_proposal tx_proposal; - tx_proposal.per_account_payments.resize(4); - auto &acc0 = tx_proposal.per_account_payments[0]; - auto &acc1 = tx_proposal.per_account_payments[1]; - auto &acc2 = tx_proposal.per_account_payments[2]; - auto &acc3 = tx_proposal.per_account_payments[3]; - acc0.first.generate_carrot(); - acc1.first.generate_carrot(); - acc2.first.generate_carrot(); - acc3.first.generate_carrot(); - - // specify self-sender - tx_proposal.self_sender_index = 2; - - // 1 subaddress payment - tools::add_element(acc0.second).first = CarrotPaymentProposalV1{ - .destination = acc0.first.subaddress(2, 3), - .amount = crypto::rand_idx(1000000), - .randomness = gen_janus_anchor() - }; - - // 1 main address payment - tools::add_element(acc1.second).first = CarrotPaymentProposalV1{ - .destination = acc1.first.cryptonote_address(), - .amount = crypto::rand_idx(1000000), - .randomness = gen_janus_anchor() - }; - - // 1 integrated address payment - tools::add_element(acc3.second).first = CarrotPaymentProposalV1{ - .destination = acc3.first.cryptonote_address(gen_payment_id()), - .amount = crypto::rand_idx(1000000), - .randomness = gen_janus_anchor() - }; - - // specify fee per weight - tx_proposal.fee_per_weight = 314159; - - // test - subtest_multi_account_transfer_over_transaction(tx_proposal); -} -//---------------------------------------------------------------------------------------------------------------------- -TEST(carrot_impl, multi_account_transfer_over_transaction_3) -{ - // four accounts, all carrot - // 1/6 tx - // 2 normal payment to main address, 1 integrated address, and 2 subaddress, each copied except integrated - // 0 explicit selfsend payments - - unittest_transaction_proposal tx_proposal; - tx_proposal.per_account_payments.resize(4); - auto &acc0 = tx_proposal.per_account_payments[0]; - auto &acc1 = tx_proposal.per_account_payments[1]; - auto &acc2 = tx_proposal.per_account_payments[2]; - auto &acc3 = tx_proposal.per_account_payments[3]; - acc0.first.generate_carrot(); - acc1.first.generate_carrot(); - acc2.first.generate_carrot(); - acc3.first.generate_carrot(); - - // specify self-sender - tx_proposal.self_sender_index = 2; - - // 2 subaddress payment - tools::add_element(acc0.second).first = CarrotPaymentProposalV1{ - .destination = acc0.first.subaddress(2, 3), - .amount = crypto::rand_idx(1000000), - .randomness = gen_janus_anchor() - }; - acc0.second.push_back(acc0.second.front()); - acc0.second.back().first.randomness = gen_janus_anchor(); //mangle anchor_norm - - // 2 main address payment - tools::add_element(acc1.second).first = CarrotPaymentProposalV1{ - .destination = acc1.first.cryptonote_address(), - .amount = crypto::rand_idx(1000000), - .randomness = gen_janus_anchor() - }; - acc1.second.push_back(acc1.second.front()); - acc1.second.back().first.randomness = gen_janus_anchor(); //mangle anchor_norm - - // 1 integrated address payment - tools::add_element(acc3.second).first = CarrotPaymentProposalV1{ - .destination = acc3.first.cryptonote_address(gen_payment_id()), - .amount = crypto::rand_idx(1000000), - .randomness = gen_janus_anchor() - }; - - // specify fee per weight - tx_proposal.fee_per_weight = 314159; - - // test - subtest_multi_account_transfer_over_transaction(tx_proposal); -} -//---------------------------------------------------------------------------------------------------------------------- -TEST(carrot_impl, multi_account_transfer_over_transaction_4) -{ - // four accounts, all carrot - // 1/8 tx - // 2 normal payment to main address, 1 integrated address, and 2 subaddress, each copied except integrated - // 2 explicit selfsend payments: 1 main address destination, 1 subaddress destination - - unittest_transaction_proposal tx_proposal; - tx_proposal.per_account_payments.resize(4); - auto &acc0 = tx_proposal.per_account_payments[0]; - auto &acc1 = tx_proposal.per_account_payments[1]; - auto &acc2 = tx_proposal.per_account_payments[2]; - auto &acc3 = tx_proposal.per_account_payments[3]; - acc0.first.generate_carrot(); - acc1.first.generate_carrot(); - acc2.first.generate_carrot(); - acc3.first.generate_carrot(); - - // specify self-sender - tx_proposal.self_sender_index = 2; - - // 2 subaddress payment - tools::add_element(acc0.second).first = CarrotPaymentProposalV1{ - .destination = acc0.first.subaddress(2, 3), - .amount = crypto::rand_idx(1000000), - .randomness = gen_janus_anchor() - }; - acc0.second.push_back(acc0.second.front()); - acc0.second.back().first.randomness = gen_janus_anchor(); //mangle anchor_norm - - // 2 main address payment - tools::add_element(acc1.second).first = CarrotPaymentProposalV1{ - .destination = acc1.first.cryptonote_address(), - .amount = crypto::rand_idx(1000000), - .randomness = gen_janus_anchor() - }; - acc1.second.push_back(acc1.second.front()); - acc1.second.back().first.randomness = gen_janus_anchor(); //mangle anchor_norm - - // 1 integrated address payment - tools::add_element(acc3.second).first = CarrotPaymentProposalV1{ - .destination = acc3.first.cryptonote_address(gen_payment_id()), - .amount = crypto::rand_idx(1000000), - .randomness = gen_janus_anchor() - }; - - // 1 main address selfsend - tools::add_element(tx_proposal.explicit_selfsend_proposals).first = CarrotPaymentProposalSelfSendV1{ - .destination_address_spend_pubkey = acc2.first.account_spend_pubkey, - .amount = crypto::rand_idx(1000000), - .enote_type = CarrotEnoteType::PAYMENT, - .internal_message = gen_janus_anchor() - }; - - // 1 subaddress selfsend - tools::add_element(tx_proposal.explicit_selfsend_proposals).first = CarrotPaymentProposalSelfSendV1{ - .destination_address_spend_pubkey = acc2.first.subaddress(4, 19).address_spend_pubkey, - .amount = crypto::rand_idx(1000000), - .enote_type = CarrotEnoteType::CHANGE - }; - - // specify fee per weight - tx_proposal.fee_per_weight = 314159; - - // test - subtest_multi_account_transfer_over_transaction(tx_proposal); -} -//---------------------------------------------------------------------------------------------------------------------- -TEST(carrot_impl, multi_account_transfer_over_transaction_5) -{ - // two accounts, both legacy - // 1/2 tx - // 1 normal payment to main address - // 0 explicit selfsend payments - - unittest_transaction_proposal tx_proposal; - tx_proposal.per_account_payments.resize(2); - mock_carrot_or_legacy_keys &acc0 = tx_proposal.per_account_payments[0].first; - mock_carrot_or_legacy_keys &acc1 = tx_proposal.per_account_payments[1].first; - acc0.generate_legacy(); - acc1.generate_legacy(); - - // 1 normal payment - CarrotPaymentProposalV1 &normal_payment_proposal = tools::add_element( tx_proposal.per_account_payments[0].second).first; - normal_payment_proposal = CarrotPaymentProposalV1{ - .destination = acc0.cryptonote_address(), - .amount = crypto::rand_idx((rct::xmr_amount) 1ull << 63), - .randomness = gen_janus_anchor() - }; - - // specify self-sender - tx_proposal.self_sender_index = 1; - - // specify fee per weight - tx_proposal.fee_per_weight = 20250510; - - // test - subtest_multi_account_transfer_over_transaction(tx_proposal); -} -//---------------------------------------------------------------------------------------------------------------------- -TEST(carrot_impl, multi_account_transfer_over_transaction_6) -{ - // four accounts, all legacy - // 1/4 tx - // 1 normal payment to main address, integrated address, and subaddress each - // 0 explicit selfsend payments - - unittest_transaction_proposal tx_proposal; - tx_proposal.per_account_payments.resize(4); - auto &acc0 = tx_proposal.per_account_payments[0]; - auto &acc1 = tx_proposal.per_account_payments[1]; - auto &acc2 = tx_proposal.per_account_payments[2]; - auto &acc3 = tx_proposal.per_account_payments[3]; - acc0.first.generate_legacy(); - acc1.first.generate_legacy(); - acc2.first.generate_legacy(); - acc3.first.generate_legacy(); - - // specify self-sender - tx_proposal.self_sender_index = 2; - - // 1 subaddress payment - tools::add_element(acc0.second).first = CarrotPaymentProposalV1{ - .destination = acc0.first.subaddress(2, 3), - .amount = crypto::rand_idx(1000000), - .randomness = gen_janus_anchor() - }; - - // 1 main address payment - tools::add_element(acc1.second).first = CarrotPaymentProposalV1{ - .destination = acc1.first.cryptonote_address(), - .amount = crypto::rand_idx(1000000), - .randomness = gen_janus_anchor() - }; - - // 1 integrated address payment - tools::add_element(acc3.second).first = CarrotPaymentProposalV1{ - .destination = acc3.first.cryptonote_address(gen_payment_id()), - .amount = crypto::rand_idx(1000000), - .randomness = gen_janus_anchor() - }; - - // specify fee per weight - tx_proposal.fee_per_weight = 314159; - - // test - subtest_multi_account_transfer_over_transaction(tx_proposal); -} -//---------------------------------------------------------------------------------------------------------------------- -TEST(carrot_impl, multi_account_transfer_over_transaction_7) -{ - // four accounts, all legacy - // 1/6 tx - // 2 normal payment to main address, 1 integrated address, and 2 subaddress, each copied except integrated - // 0 explicit selfsend payments - - unittest_transaction_proposal tx_proposal; - tx_proposal.per_account_payments.resize(4); - auto &acc0 = tx_proposal.per_account_payments[0]; - auto &acc1 = tx_proposal.per_account_payments[1]; - auto &acc2 = tx_proposal.per_account_payments[2]; - auto &acc3 = tx_proposal.per_account_payments[3]; - acc0.first.generate_legacy(); - acc1.first.generate_legacy(); - acc2.first.generate_legacy(); - acc3.first.generate_legacy(); - - // specify self-sender - tx_proposal.self_sender_index = 2; - - // 2 subaddress payment - tools::add_element(acc0.second).first = CarrotPaymentProposalV1{ - .destination = acc0.first.subaddress(2, 3), - .amount = crypto::rand_idx(1000000), - .randomness = gen_janus_anchor() - }; - acc0.second.push_back(acc0.second.front()); - acc0.second.back().first.randomness = gen_janus_anchor(); //mangle anchor_norm - - // 2 main address payment - tools::add_element(acc1.second).first = CarrotPaymentProposalV1{ - .destination = acc1.first.cryptonote_address(), - .amount = crypto::rand_idx(1000000), - .randomness = gen_janus_anchor() - }; - acc1.second.push_back(acc1.second.front()); - acc1.second.back().first.randomness = gen_janus_anchor(); //mangle anchor_norm - - // 1 integrated address payment - tools::add_element(acc3.second).first = CarrotPaymentProposalV1{ - .destination = acc3.first.cryptonote_address(gen_payment_id()), - .amount = crypto::rand_idx(1000000), - .randomness = gen_janus_anchor() - }; - - // specify fee per weight - tx_proposal.fee_per_weight = 314159; - - // test - subtest_multi_account_transfer_over_transaction(tx_proposal); -} -//---------------------------------------------------------------------------------------------------------------------- -TEST(carrot_impl, multi_account_transfer_over_transaction_8) -{ - // four accounts, all legacy - // 1/8 tx - // 2 normal payment to main address, 1 integrated address, and 2 subaddress, each copied except integrated - // 2 explicit selfsend payments: 1 main address destination, 1 subaddress destination - - unittest_transaction_proposal tx_proposal; - tx_proposal.per_account_payments.resize(4); - auto &acc0 = tx_proposal.per_account_payments[0]; - auto &acc1 = tx_proposal.per_account_payments[1]; - auto &acc2 = tx_proposal.per_account_payments[2]; - auto &acc3 = tx_proposal.per_account_payments[3]; - acc0.first.generate_legacy(); - acc1.first.generate_legacy(); - acc2.first.generate_legacy(); - acc3.first.generate_legacy(); - - // specify self-sender - tx_proposal.self_sender_index = 2; - - // 2 subaddress payment - tools::add_element(acc0.second).first = CarrotPaymentProposalV1{ - .destination = acc0.first.subaddress(2, 3), - .amount = crypto::rand_idx(1000000), - .randomness = gen_janus_anchor() - }; - acc0.second.push_back(acc0.second.front()); - acc0.second.back().first.randomness = gen_janus_anchor(); //mangle anchor_norm - - // 2 main address payment - tools::add_element(acc1.second).first = CarrotPaymentProposalV1{ - .destination = acc1.first.cryptonote_address(), - .amount = crypto::rand_idx(1000000), - .randomness = gen_janus_anchor() - }; - acc1.second.push_back(acc1.second.front()); - acc1.second.back().first.randomness = gen_janus_anchor(); //mangle anchor_norm - - // 1 integrated address payment - tools::add_element(acc3.second).first = CarrotPaymentProposalV1{ - .destination = acc3.first.cryptonote_address(gen_payment_id()), - .amount = crypto::rand_idx(1000000), - .randomness = gen_janus_anchor() - }; - - // 1 main address selfsend - tools::add_element(tx_proposal.explicit_selfsend_proposals).first = CarrotPaymentProposalSelfSendV1{ - .destination_address_spend_pubkey = acc2.first.account_spend_pubkey, - .amount = crypto::rand_idx(1000000), - .enote_type = CarrotEnoteType::PAYMENT, - // no internal messages for legacy self-sends - }; - - // 1 subaddress selfsend - tools::add_element(tx_proposal.explicit_selfsend_proposals).first = CarrotPaymentProposalSelfSendV1{ - .destination_address_spend_pubkey = acc2.first.subaddress(4, 19).address_spend_pubkey, - .amount = crypto::rand_idx(1000000), - .enote_type = CarrotEnoteType::CHANGE - }; - - // specify fee per weight - tx_proposal.fee_per_weight = 314159; - - // test - subtest_multi_account_transfer_over_transaction(tx_proposal); -} -//---------------------------------------------------------------------------------------------------------------------- -TEST(carrot_impl, multi_account_transfer_over_transaction_9) -{ - // two accounts, both carrot - // 1/2 tx - // 1 normal payment to main address - // 0 explicit selfsend payments - // subtractable - - unittest_transaction_proposal tx_proposal; - tx_proposal.per_account_payments.resize(2); - mock_carrot_or_legacy_keys &acc0 = tx_proposal.per_account_payments[0].first; - mock_carrot_or_legacy_keys &acc1 = tx_proposal.per_account_payments[1].first; - acc0.generate_carrot(); - acc1.generate_carrot(); - - // 1 normal payment (subtractable) - CarrotPaymentProposalV1 &normal_payment_proposal = tools::add_element( tx_proposal.per_account_payments[0].second).first; - normal_payment_proposal = CarrotPaymentProposalV1{ - .destination = acc0.cryptonote_address(), - .amount = crypto::rand_idx((rct::xmr_amount) 1ull << 63), - .randomness = gen_janus_anchor() - }; - tx_proposal.per_account_payments[0].second.back().second = true; - - // specify self-sender - tx_proposal.self_sender_index = 1; - - // specify fee per weight - tx_proposal.fee_per_weight = 20250510; - - // test - subtest_multi_account_transfer_over_transaction(tx_proposal); -} -//---------------------------------------------------------------------------------------------------------------------- -TEST(carrot_impl, multi_account_transfer_over_transaction_10) -{ - // four accounts, all carrot - // 1/4 tx - // 1 normal payment to main address, integrated address, and subaddress each - // 0 explicit selfsend payments - // subaddress and integrated address are subtractable - - unittest_transaction_proposal tx_proposal; - tx_proposal.per_account_payments.resize(4); - auto &acc0 = tx_proposal.per_account_payments[0]; - auto &acc1 = tx_proposal.per_account_payments[1]; - auto &acc2 = tx_proposal.per_account_payments[2]; - auto &acc3 = tx_proposal.per_account_payments[3]; - acc0.first.generate_carrot(); - acc1.first.generate_carrot(); - acc2.first.generate_carrot(); - acc3.first.generate_carrot(); - - // specify self-sender - tx_proposal.self_sender_index = 2; - - // 1 subaddress payment (subtractable) - tools::add_element(acc0.second) = {CarrotPaymentProposalV1{ - .destination = acc0.first.subaddress(2, 3), - .amount = crypto::rand_idx(1000000), - .randomness = gen_janus_anchor() - }, true}; - - // 1 main address payment - tools::add_element(acc1.second).first = CarrotPaymentProposalV1{ - .destination = acc1.first.cryptonote_address(), - .amount = crypto::rand_idx(1000000), - .randomness = gen_janus_anchor() - }; - - // 1 integrated address payment - tools::add_element(acc3.second) = {CarrotPaymentProposalV1{ - .destination = acc3.first.cryptonote_address(gen_payment_id()), - .amount = crypto::rand_idx(1000000), - .randomness = gen_janus_anchor() - }, true}; - - // specify fee per weight - tx_proposal.fee_per_weight = 314159; - - // test - subtest_multi_account_transfer_over_transaction(tx_proposal); -} -//---------------------------------------------------------------------------------------------------------------------- -TEST(carrot_impl, multi_account_transfer_over_transaction_11) -{ - // four accounts, all carrot - // 1/6 tx - // 2 normal payment to main address, 1 integrated address, and 2 subaddress, each copied except integrated - // 0 explicit selfsend payments - // 1 main and 1 subaddress is subtractable - - unittest_transaction_proposal tx_proposal; - tx_proposal.per_account_payments.resize(4); - auto &acc0 = tx_proposal.per_account_payments[0]; - auto &acc1 = tx_proposal.per_account_payments[1]; - auto &acc2 = tx_proposal.per_account_payments[2]; - auto &acc3 = tx_proposal.per_account_payments[3]; - acc0.first.generate_carrot(); - acc1.first.generate_carrot(); - acc2.first.generate_carrot(); - acc3.first.generate_carrot(); - - // specify self-sender - tx_proposal.self_sender_index = 2; - - // 2 subaddress payment - tools::add_element(acc0.second).first = CarrotPaymentProposalV1{ - .destination = acc0.first.subaddress(2, 3), - .amount = crypto::rand_idx(1000000), - .randomness = gen_janus_anchor() - }; - acc0.second.push_back(acc0.second.front()); - acc0.second.back().first.randomness = gen_janus_anchor(); //mangle anchor_norm - acc0.second.back().second = true; //set copy as subtractable - - // 2 main address payment - tools::add_element(acc1.second).first = CarrotPaymentProposalV1{ - .destination = acc1.first.cryptonote_address(), - .amount = crypto::rand_idx(1000000), - .randomness = gen_janus_anchor() - }; - acc1.second.push_back(acc1.second.front()); - acc1.second.back().first.randomness = gen_janus_anchor(); //mangle anchor_norm - acc1.second.back().second = true; //set copy as subtractable - - // 1 integrated address payment - tools::add_element(acc3.second).first = CarrotPaymentProposalV1{ - .destination = acc3.first.cryptonote_address(gen_payment_id()), - .amount = crypto::rand_idx(1000000), - .randomness = gen_janus_anchor() - }; - - // specify fee per weight - tx_proposal.fee_per_weight = 314159; - - // test - subtest_multi_account_transfer_over_transaction(tx_proposal); -} -//---------------------------------------------------------------------------------------------------------------------- -TEST(carrot_impl, multi_account_transfer_over_transaction_12) -{ - // four accounts, all carrot - // 1/8 tx - // 2 normal payment to main address, 1 integrated address, and 2 subaddress, each copied except integrated - // 2 explicit selfsend payments: 1 main address destination, 1 subaddress destination - // 1 normal main address, 1 integrated, and 1 self-send subaddress is subtractable - - unittest_transaction_proposal tx_proposal; - tx_proposal.per_account_payments.resize(4); - auto &acc0 = tx_proposal.per_account_payments[0]; - auto &acc1 = tx_proposal.per_account_payments[1]; - auto &acc2 = tx_proposal.per_account_payments[2]; - auto &acc3 = tx_proposal.per_account_payments[3]; - acc0.first.generate_carrot(); - acc1.first.generate_carrot(); - acc2.first.generate_carrot(); - acc3.first.generate_carrot(); - - // specify self-sender - tx_proposal.self_sender_index = 2; - - // 2 subaddress payment (1 subtractable) - tools::add_element(acc0.second) = {CarrotPaymentProposalV1{ - .destination = acc0.first.subaddress(2, 3), - .amount = crypto::rand_idx(1000000), - .randomness = gen_janus_anchor() - }, true}; - acc0.second.push_back(acc0.second.front()); - acc0.second.back().first.randomness = gen_janus_anchor(); //mangle anchor_norm - acc0.second.back().second = false; //set not subtractable, first already is - - // 2 main address payment - tools::add_element(acc1.second).first = CarrotPaymentProposalV1{ - .destination = acc1.first.cryptonote_address(), - .amount = crypto::rand_idx(1000000), - .randomness = gen_janus_anchor() - }; - acc1.second.push_back(acc1.second.front()); - acc1.second.back().first.randomness = gen_janus_anchor(); //mangle anchor_norm - - // 1 integrated address payment (subtractable) - tools::add_element(acc3.second) = {CarrotPaymentProposalV1{ - .destination = acc3.first.cryptonote_address(gen_payment_id()), - .amount = crypto::rand_idx(1000000), - .randomness = gen_janus_anchor() - }, true}; - - // 1 main address selfsend - tools::add_element(tx_proposal.explicit_selfsend_proposals).first = CarrotPaymentProposalSelfSendV1{ - .destination_address_spend_pubkey = acc2.first.account_spend_pubkey, - .amount = crypto::rand_idx(1000000), - .enote_type = CarrotEnoteType::PAYMENT, - .internal_message = gen_janus_anchor() - }; - - // 1 subaddress selfsend (subtractable) - tools::add_element(tx_proposal.explicit_selfsend_proposals) = {CarrotPaymentProposalSelfSendV1{ - .destination_address_spend_pubkey = acc2.first.subaddress(4, 19).address_spend_pubkey, - .amount = crypto::rand_idx(1000000), - .enote_type = CarrotEnoteType::CHANGE - }, true}; - - // specify fee per weight - tx_proposal.fee_per_weight = 314159; - - // test - subtest_multi_account_transfer_over_transaction(tx_proposal); -} -//---------------------------------------------------------------------------------------------------------------------- -TEST(carrot_impl, multi_account_transfer_over_transaction_13) -{ - // two accounts, both legacy - // 1/2 tx - // 1 normal payment to main address - // 0 explicit selfsend payments - // subtractable - - unittest_transaction_proposal tx_proposal; - tx_proposal.per_account_payments.resize(2); - mock_carrot_or_legacy_keys &acc0 = tx_proposal.per_account_payments[0].first; - mock_carrot_or_legacy_keys &acc1 = tx_proposal.per_account_payments[1].first; - acc0.generate_legacy(); - acc1.generate_legacy(); - - // 1 normal payment (subtractable) - CarrotPaymentProposalV1 &normal_payment_proposal = tools::add_element( tx_proposal.per_account_payments[0].second).first; - normal_payment_proposal = CarrotPaymentProposalV1{ - .destination = acc0.cryptonote_address(), - .amount = crypto::rand_idx((rct::xmr_amount) 1ull << 63), - .randomness = gen_janus_anchor() - }; - tx_proposal.per_account_payments[0].second.back().second = true; - - // specify self-sender - tx_proposal.self_sender_index = 1; - - // specify fee per weight - tx_proposal.fee_per_weight = 20250510; - - // test - subtest_multi_account_transfer_over_transaction(tx_proposal); -} -//---------------------------------------------------------------------------------------------------------------------- -TEST(carrot_impl, multi_account_transfer_over_transaction_14) -{ - // four accounts, all legacy - // 1/4 tx - // 1 normal payment to main address, integrated address, and subaddress each - // 0 explicit selfsend payments - // 1 integrated and 1 subaddress subtractable - - unittest_transaction_proposal tx_proposal; - tx_proposal.per_account_payments.resize(4); - auto &acc0 = tx_proposal.per_account_payments[0]; - auto &acc1 = tx_proposal.per_account_payments[1]; - auto &acc2 = tx_proposal.per_account_payments[2]; - auto &acc3 = tx_proposal.per_account_payments[3]; - acc0.first.generate_legacy(); - acc1.first.generate_legacy(); - acc2.first.generate_legacy(); - acc3.first.generate_legacy(); - - // specify self-sender - tx_proposal.self_sender_index = 2; - - // 1 subaddress payment (subtractable) - tools::add_element(acc0.second) = {CarrotPaymentProposalV1{ - .destination = acc0.first.subaddress(2, 3), - .amount = crypto::rand_idx(1000000), - .randomness = gen_janus_anchor() - }, true}; - - // 1 main address payment - tools::add_element(acc1.second).first = CarrotPaymentProposalV1{ - .destination = acc1.first.cryptonote_address(), - .amount = crypto::rand_idx(1000000), - .randomness = gen_janus_anchor() - }; - - // 1 integrated address payment (subtractable) - tools::add_element(acc3.second) = {CarrotPaymentProposalV1{ - .destination = acc3.first.cryptonote_address(gen_payment_id()), - .amount = crypto::rand_idx(1000000), - .randomness = gen_janus_anchor() - }, true}; - - // specify fee per weight - tx_proposal.fee_per_weight = 314159; - - // test - subtest_multi_account_transfer_over_transaction(tx_proposal); -} -//---------------------------------------------------------------------------------------------------------------------- -TEST(carrot_impl, multi_account_transfer_over_transaction_15) -{ - // four accounts, all legacy - // 1/6 tx - // 2 normal payment to main address, 1 integrated address, and 2 subaddress, each copied except integrated - // 0 explicit selfsend payments - // all subtractable - - unittest_transaction_proposal tx_proposal; - tx_proposal.per_account_payments.resize(4); - auto &acc0 = tx_proposal.per_account_payments[0]; - auto &acc1 = tx_proposal.per_account_payments[1]; - auto &acc2 = tx_proposal.per_account_payments[2]; - auto &acc3 = tx_proposal.per_account_payments[3]; - acc0.first.generate_legacy(); - acc1.first.generate_legacy(); - acc2.first.generate_legacy(); - acc3.first.generate_legacy(); - - // specify self-sender - tx_proposal.self_sender_index = 2; - - // 2 subaddress payment (subtractable) - tools::add_element(acc0.second) = {CarrotPaymentProposalV1{ - .destination = acc0.first.subaddress(2, 3), - .amount = crypto::rand_idx(1000000), - .randomness = gen_janus_anchor() - }, true}; - acc0.second.push_back(acc0.second.front()); - acc0.second.back().first.randomness = gen_janus_anchor(); //mangle anchor_norm - - // 2 main address payment (subtractable) - tools::add_element(acc1.second) = {CarrotPaymentProposalV1{ - .destination = acc1.first.cryptonote_address(), - .amount = crypto::rand_idx(1000000), - .randomness = gen_janus_anchor() - }, true}; - acc1.second.push_back(acc1.second.front()); - acc1.second.back().first.randomness = gen_janus_anchor(); //mangle anchor_norm - - // 1 integrated address payment (subtractable) - tools::add_element(acc3.second) = {CarrotPaymentProposalV1{ - .destination = acc3.first.cryptonote_address(gen_payment_id()), - .amount = crypto::rand_idx(1000000), - .randomness = gen_janus_anchor() - }, true}; - - // specify fee per weight - tx_proposal.fee_per_weight = 314159; - - // test - subtest_multi_account_transfer_over_transaction(tx_proposal); -} -//---------------------------------------------------------------------------------------------------------------------- -TEST(carrot_impl, multi_account_transfer_over_transaction_16) -{ - // four accounts, all legacy - // 1/8 tx - // 2 normal payment to main address, 1 integrated address, and 2 subaddress, each copied except integrated - // 2 explicit selfsend payments: 1 main address destination, 1 subaddress destination - // all subtractable - - unittest_transaction_proposal tx_proposal; - tx_proposal.per_account_payments.resize(4); - auto &acc0 = tx_proposal.per_account_payments[0]; - auto &acc1 = tx_proposal.per_account_payments[1]; - auto &acc2 = tx_proposal.per_account_payments[2]; - auto &acc3 = tx_proposal.per_account_payments[3]; - acc0.first.generate_legacy(); - acc1.first.generate_legacy(); - acc2.first.generate_legacy(); - acc3.first.generate_legacy(); - - // specify self-sender - tx_proposal.self_sender_index = 2; - - // 2 subaddress payment (subtractable) - tools::add_element(acc0.second) = {CarrotPaymentProposalV1{ - .destination = acc0.first.subaddress(2, 3), - .amount = crypto::rand_idx(1000000), - .randomness = gen_janus_anchor() - }, true}; - acc0.second.push_back(acc0.second.front()); - acc0.second.back().first.randomness = gen_janus_anchor(); //mangle anchor_norm - - // 2 main address payment (subtractable) - tools::add_element(acc1.second) = {CarrotPaymentProposalV1{ - .destination = acc1.first.cryptonote_address(), - .amount = crypto::rand_idx(1000000), - .randomness = gen_janus_anchor() - }, true}; - acc1.second.push_back(acc1.second.front()); - acc1.second.back().first.randomness = gen_janus_anchor(); //mangle anchor_norm - - // 1 integrated address payment (subtractable) - tools::add_element(acc3.second) = {CarrotPaymentProposalV1{ - .destination = acc3.first.cryptonote_address(gen_payment_id()), - .amount = crypto::rand_idx(1000000), - .randomness = gen_janus_anchor() - }, true}; - - // 1 main address selfsend (subtractable) - tools::add_element(tx_proposal.explicit_selfsend_proposals) = {CarrotPaymentProposalSelfSendV1{ - .destination_address_spend_pubkey = acc2.first.account_spend_pubkey, - .amount = crypto::rand_idx(1000000), - .enote_type = CarrotEnoteType::PAYMENT, - // no internal messages for legacy self-sends - }, true}; - - // 1 subaddress selfsend (subtractable) - tools::add_element(tx_proposal.explicit_selfsend_proposals) = {CarrotPaymentProposalSelfSendV1{ - .destination_address_spend_pubkey = acc2.first.subaddress(4, 19).address_spend_pubkey, - .amount = crypto::rand_idx(1000000), - .enote_type = CarrotEnoteType::CHANGE - }, true}; - - // specify fee per weight - tx_proposal.fee_per_weight = 314159; - - // test - subtest_multi_account_transfer_over_transaction(tx_proposal); -} -//---------------------------------------------------------------------------------------------------------------------- From 1b266b4000f487a080741b79ca5f113f0df929a2 Mon Sep 17 00:00:00 2001 From: jeffro256 Date: Fri, 31 Jan 2025 00:09:42 -0600 Subject: [PATCH 08/15] actually set the dummy pid duurrhhh --- src/carrot_core/output_set_finalization.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/carrot_core/output_set_finalization.cpp b/src/carrot_core/output_set_finalization.cpp index e584f3be667..9d5243da77e 100644 --- a/src/carrot_core/output_set_finalization.cpp +++ b/src/carrot_core/output_set_finalization.cpp @@ -214,10 +214,13 @@ void get_output_enote_proposals(const std::vector &norm encrypted_payment_id_out = encrypted_payment_id; } - // in the case that the pid target is ambiguous, set it to random bytes - const bool missing_encrypted_pid = num_integrated == 0 && !dummy_encrypted_payment_id; - CHECK_AND_ASSERT_THROW_MES(!missing_encrypted_pid, - "get output enote proposals: missing encrypted payment ID: no integrated address nor provided dummy"); + // in the case that there is no required pid_enc, set it to the provided dummy + if (0 == num_integrated) + { + CHECK_AND_ASSERT_THROW_MES(dummy_encrypted_payment_id, + "get output enote proposals: missing encrypted payment ID: no integrated address nor provided dummy"); + encrypted_payment_id_out = *dummy_encrypted_payment_id; + } // construct selfsend enotes, preferring internal enotes over special enotes when possible for (const CarrotPaymentProposalSelfSendV1 &selfsend_payment_proposal : selfsend_payment_proposals) From 25007bd5a5fc2120455403044781c7cbc09866d2 Mon Sep 17 00:00:00 2001 From: jeffro256 Date: Fri, 10 Jan 2025 14:59:27 -0600 Subject: [PATCH 09/15] carrot_impl 27/1/25 [WIP] --- src/CMakeLists.txt | 1 + src/carrot_impl/CMakeLists.txt | 51 + src/carrot_impl/carrot_boost_serialization.h | 61 + src/carrot_impl/carrot_chain_serialization.h | 42 + src/carrot_impl/carrot_tx_builder.cpp | 444 ++++++++ src/carrot_impl/carrot_tx_builder.h | 130 +++ src/carrot_impl/carrot_tx_format_utils.cpp | 356 ++++++ src/carrot_impl/carrot_tx_format_utils.h | 92 ++ src/carrot_impl/tx_builder_inputs.h | 46 + src/cryptonote_basic/cryptonote_basic.h | 34 +- .../cryptonote_boost_serialization.h | 12 +- src/cryptonote_basic/tx_extra.h | 2 + src/serialization/json_object.cpp | 33 +- src/serialization/json_object.h | 4 +- tests/core_tests/tx_validation.cpp | 13 - tests/unit_tests/CMakeLists.txt | 2 + tests/unit_tests/carrot_impl.cpp | 1002 +++++++++++++++++ 17 files changed, 2264 insertions(+), 61 deletions(-) create mode 100644 src/carrot_impl/CMakeLists.txt create mode 100644 src/carrot_impl/carrot_boost_serialization.h create mode 100644 src/carrot_impl/carrot_chain_serialization.h create mode 100644 src/carrot_impl/carrot_tx_builder.cpp create mode 100644 src/carrot_impl/carrot_tx_builder.h create mode 100644 src/carrot_impl/carrot_tx_format_utils.cpp create mode 100644 src/carrot_impl/carrot_tx_format_utils.h create mode 100644 src/carrot_impl/tx_builder_inputs.h create mode 100644 tests/unit_tests/carrot_impl.cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6c53b115d6f..eaec90928f8 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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) diff --git a/src/carrot_impl/CMakeLists.txt b/src/carrot_impl/CMakeLists.txt new file mode 100644 index 00000000000..7891c5420ea --- /dev/null +++ b/src/carrot_impl/CMakeLists.txt @@ -0,0 +1,51 @@ +# 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 + carrot_tx_builder.cpp + carrot_tx_format_utils.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_basic + PRIVATE + ${EXTRA_LIBRARIES}) + +target_include_directories(carrot_impl + PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}" + PRIVATE + ${Boost_INCLUDE_DIRS}) diff --git a/src/carrot_impl/carrot_boost_serialization.h b/src/carrot_impl/carrot_boost_serialization.h new file mode 100644 index 00000000000..8b066b376d5 --- /dev/null +++ b/src/carrot_impl/carrot_boost_serialization.h @@ -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 + +//standard headers + +//forward declarations + +namespace boost +{ +namespace serialization +{ +//--------------------------------------------------- +template +inline void serialize(Archive &a, carrot::view_tag_t &x, const boost::serialization::version_type ver) +{ + a & x.bytes; +} +//--------------------------------------------------- +template +inline void serialize(Archive &a, carrot::encrypted_janus_anchor_t &x, const boost::serialization::version_type ver) +{ + a & x.bytes; +} +//--------------------------------------------------- +} //namespace serialization +} //namespace boot diff --git a/src/carrot_impl/carrot_chain_serialization.h b/src/carrot_impl/carrot_chain_serialization.h new file mode 100644 index 00000000000..8a3139fa6ab --- /dev/null +++ b/src/carrot_impl/carrot_chain_serialization.h @@ -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); diff --git a/src/carrot_impl/carrot_tx_builder.cpp b/src/carrot_impl/carrot_tx_builder.cpp new file mode 100644 index 00000000000..bcb5cfe96ad --- /dev/null +++ b/src/carrot_impl/carrot_tx_builder.cpp @@ -0,0 +1,444 @@ +// 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.h" + +//local headers +extern "C" +{ +#include "crypto/crypto-ops.h" +} +#include "carrot_core/output_set_finalization.h" +#include "carrot_tx_format_utils.h" +#include "ringct/rctOps.h" +#include "tx_builder_inputs.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& normal_payment_proposals_inout, + std::vector &selfsend_payment_proposals_inout, + const crypto::public_key &change_address_spend_pubkey) +{ + 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(p); } + + std::vector& normal_proposals_inout; + std::vector &selfsend_proposals_inout; + }; + + bool have_payment_type_selfsend = false; + for (const CarrotPaymentProposalSelfSendV1 &selfsend_payment_proposal : selfsend_payment_proposals_inout) + { + if (selfsend_payment_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 + }); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +void make_unsigned_transaction(std::vector &&normal_payment_proposals, + std::vector &&selfsend_payment_proposals, + const rct::xmr_amount fee_per_weight, + 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, + cryptonote::transaction &tx_out, + crypto::secret_key &amount_blinding_factor_sum_out) +{ + // 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); + + // calculate number of outputs and the size of tx.extra + const size_t num_outs = normal_payment_proposals.size() + selfsend_payment_proposals.size(); + 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 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 CarrotPaymentProposalSelfSendV1 &selfsend_proposal : selfsend_payment_proposals) + nominal_output_amount_sum += selfsend_proposal.amount; + + // callback to select inputs given nominal output sum and fee per input count + std::vector selected_inputs; + select_inputs(nominal_output_amount_sum, fee_per_input_count, selected_inputs); + + // get fee given the number of selected inputs + // note: this will fail if input selection returned a bad number of inputs + const rct::xmr_amount 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, fee, normal_payment_proposals, selfsend_payment_proposals); + + // sanity check balance + input_amount_sum -= fee; + for (const CarrotPaymentProposalV1 &normal_payment_proposal : normal_payment_proposals) + input_amount_sum -= normal_payment_proposal.amount; + for (const CarrotPaymentProposalSelfSendV1 &selfsend_payment_proposal : selfsend_payment_proposals) + input_amount_sum -= selfsend_payment_proposal.amount; + CHECK_AND_ASSERT_THROW_MES(input_amount_sum == 0, + "make unsigned transaction: post-carved transaction does not balance"); + + // sort inputs by key image and get first key image + std::sort(selected_inputs.begin(), selected_inputs.end(), [](const auto &a, const auto &b){ + return compare_input_key_images(a.key_image, b.key_image); + }); + const crypto::key_image &tx_first_key_image = selected_inputs.at(0).key_image; + + // finalize payment proposals into enotes + std::vector output_enote_proposals; + encrypted_payment_id_t encrypted_payment_id; + get_output_enote_proposals(std::forward>(normal_payment_proposals), + std::forward>(selfsend_payment_proposals), + s_view_balance_dev, + k_view_dev, + account_spend_pubkey, + tx_first_key_image, + output_enote_proposals, + encrypted_payment_id); + + // collect enotes + std::vector enotes; + enotes.reserve(output_enote_proposals.size()); + for (const RCTOutputEnoteProposal &e : output_enote_proposals) + enotes.push_back(e.enote); + + // collect key images + std::vector key_images; + key_images.reserve(selected_inputs.size()); + for (const CarrotSelectedInput &selected_input : selected_inputs) + key_images.push_back(selected_input.key_image); + + // serialize pruned transaction + tx_out = store_carrot_to_transaction_v1(enotes, key_images, fee, encrypted_payment_id); + + // calculate the sum of k_a for each output + unsigned char * const p_ka_sum = to_bytes(amount_blinding_factor_sum_out); + memcpy(p_ka_sum, rct::I.bytes, sizeof(amount_blinding_factor_sum_out)); // = 1 + for (const RCTOutputEnoteProposal &e : output_enote_proposals) + sc_add(p_ka_sum, p_ka_sum, to_bytes(e.amount_blinding_factor)); // += k_a +} +//------------------------------------------------------------------------------------------------------------------- +void make_unsigned_transaction_transfer_subtractable( + std::vector &&normal_payment_proposals, + std::vector &&selfsend_payment_proposals, + const rct::xmr_amount fee_per_weight, + 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 &subtractable_normal_payment_proposals, + const std::set &subtractable_selfsend_payment_proposals, + cryptonote::transaction &tx_out, + crypto::secret_key &amount_blinding_factor_sum_out) +{ + // 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).enote_type == CarrotEnoteType::CHANGE; + + selfsend_payment_proposals.push_back(CarrotPaymentProposalSelfSendV1{ + .destination_address_spend_pubkey = account_spend_pubkey, + .amount = 0, + .enote_type = add_payment_type_selfsend ? CarrotEnoteType::PAYMENT : CarrotEnoteType::CHANGE + }); + + // 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 &normal_payment_proposals, + std::vector &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().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 CarrotPaymentProposalSelfSendV1 &selfsend_payment_proposal : selfsend_payment_proposals) + implicit_change_amount -= selfsend_payment_proposal.amount; + + selfsend_payment_proposals.back().amount = boost::numeric_cast(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]; + 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]; + 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 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_unsigned_transaction(std::forward>(normal_payment_proposals), + std::forward>(selfsend_payment_proposals), + fee_per_weight, + std::forward(select_inputs), + std::move(carve_fees_and_balance), + s_view_balance_dev, + k_view_dev, + account_spend_pubkey, + tx_out, + amount_blinding_factor_sum_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_unsigned_transaction_transfer( + std::vector &&normal_payment_proposals, + std::vector &&selfsend_payment_proposals, + const rct::xmr_amount fee_per_weight, + 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, + cryptonote::transaction &tx_out, + crypto::secret_key &amount_blinding_factor_sum_out) +{ + make_unsigned_transaction_transfer_subtractable( + std::forward>(normal_payment_proposals), + std::forward>(selfsend_payment_proposals), + fee_per_weight, + std::forward(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_out, + amount_blinding_factor_sum_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_unsigned_transaction_sweep( + const tools::variant &payment_proposal, + const rct::xmr_amount fee_per_weight, + std::vector &&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, + cryptonote::transaction &tx_out, + crypto::secret_key &amount_blinding_factor_sum_out) +{ + // initialize payment proposals list from `payment_proposal` + std::vector normal_payment_proposals; + std::vector selfsend_payment_proposals; + struct add_payment_proposal_visitor + { + void operator()(const CarrotPaymentProposalV1 &p) const { normal_payment_proposals.push_back(p); } + void operator()(const CarrotPaymentProposalSelfSendV1 &p) const { selfsend_payment_proposals.push_back(p); } + std::vector &normal_payment_proposals; + std::vector &selfsend_payment_proposals; + }; + payment_proposal.visit(add_payment_proposal_visitor{normal_payment_proposals, selfsend_payment_proposals}); + + 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::vector &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 &normal_payment_proposals, + std::vector &selfsend_payment_proposals + ) + { + // get pointer to sweep destination amount + rct::xmr_amount *amount_ptr = nullptr; + if (is_selfsend_sweep) + { + CHECK_AND_ASSERT_THROW_MES(!selfsend_payment_proposals.empty(), + "make unsigned transaction sweep: bug: missing selfsend proposal"); + amount_ptr = &selfsend_payment_proposals.front().amount; + } + else + { + CHECK_AND_ASSERT_THROW_MES(!normal_payment_proposals.empty(), + "make unsigned transaction sweep: bug: missing normal proposal"); + amount_ptr = &normal_payment_proposals.front().amount; + } + + // set amount + const boost::multiprecision::int128_t sweep_output_amount = input_sum_amount - fee; + *amount_ptr = boost::numeric_cast(sweep_output_amount); + }; //end carve_fees_and_balance + + // make unsigned transaction with sweep carving callback and selected inputs + make_unsigned_transaction(std::forward>(normal_payment_proposals), + std::forward>(selfsend_payment_proposals), + fee_per_weight, + std::move(select_inputs), + std::move(carve_fees_and_balance), + s_view_balance_dev, + k_view_dev, + account_spend_pubkey, + tx_out, + amount_blinding_factor_sum_out); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace carrot diff --git a/src/carrot_impl/carrot_tx_builder.h b/src/carrot_impl/carrot_tx_builder.h new file mode 100644 index 00000000000..06e434f631e --- /dev/null +++ b/src/carrot_impl/carrot_tx_builder.h @@ -0,0 +1,130 @@ +// 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 "cryptonote_basic/cryptonote_basic.h" + +//third party headers +#include + +//standard headers +#include +#include +#include +#include + +//forward declarations + +namespace carrot +{ +struct CarrotSelectedInput +{ + rct::xmr_amount amount; + crypto::key_image key_image; +}; + +using select_inputs_func_t = std::function&, // absolute fee per input count + std::vector& // selected inputs result + )>; + +using carve_fees_and_balance_func_t = std::function&, // normal payment proposals [inout] + std::vector& // selfsend payment proposals [inout] + )>; + +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); + +void make_unsigned_transaction(std::vector &&normal_payment_proposals, + std::vector &&selfsend_payment_proposals, + const rct::xmr_amount fee_per_weight, + 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, + cryptonote::transaction &tx_out, + crypto::secret_key &amount_blinding_factor_sum_out); + +void make_unsigned_transaction_transfer_subtractable( + std::vector &&normal_payment_proposals, + std::vector &&selfsend_payment_proposals, + const rct::xmr_amount fee_per_weight, + 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 &subtractable_normal_payment_proposals, + const std::set &subtractable_selfsend_payment_proposals, + cryptonote::transaction &tx_out, + crypto::secret_key &amount_blinding_factor_sum_out); + +void make_unsigned_transaction_transfer( + std::vector &&normal_payment_proposals, + std::vector &&selfsend_payment_proposals, + const rct::xmr_amount fee_per_weight, + 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, + cryptonote::transaction &tx_out, + crypto::secret_key &amount_blinding_factor_sum_out); + +void make_unsigned_transaction_sweep( + const tools::variant &payment_proposal, + const rct::xmr_amount fee_per_weight, + std::vector &&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, + cryptonote::transaction &tx_out, + crypto::secret_key &amount_blinding_factor_sum_out); +} //namespace carrot diff --git a/src/carrot_impl/carrot_tx_format_utils.cpp b/src/carrot_impl/carrot_tx_format_utils.cpp new file mode 100644 index 00000000000..5c6fd7bbc34 --- /dev/null +++ b/src/carrot_impl/carrot_tx_format_utils.cpp @@ -0,0 +1,356 @@ +// 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 +{ +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static constexpr const std::uint8_t carrot_rct_type = rct::RCTTypeBulletproof2; // @TODO: WRONG version +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +template +static void store_carrot_ephemeral_pubkeys_to_extra(const EnoteContainer &enotes, std::vector &extra_inout) +{ + const size_t nouts = enotes.size(); + const bool use_shared_ephemeral_pubkey = nouts == 2 && !is_coinbase; + bool success = true; + if (use_shared_ephemeral_pubkey) + { + crypto::public_key tx_pubkey; + const mx25519_pubkey &enote_ephemeral_pubkey = enotes.at(0).enote_ephemeral_pubkey; + memcpy(tx_pubkey.data, enote_ephemeral_pubkey.data, sizeof(tx_pubkey)); + success = success && cryptonote::add_tx_pub_key_to_extra(extra_inout, tx_pubkey); + } + else // nouts != 2 or coinbase + { + std::vector tx_pubkeys(nouts); + for (size_t i = 0; i < nouts; ++i) + { + const mx25519_pubkey &enote_ephemeral_pubkey = enotes.at(i).enote_ephemeral_pubkey; + memcpy(tx_pubkeys[i].data, enote_ephemeral_pubkey.data, sizeof(tx_pubkeys[i])); + } + 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"); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +template +static bool try_load_carrot_ephemeral_pubkeys_from_extra(const std::vector &extra_fields, + EnoteContainer &enotes_inout) +{ + const size_t nouts = enotes_inout.size(); + const bool use_shared_ephemeral_pubkey = nouts == 2 && !is_coinbase; + if (use_shared_ephemeral_pubkey) + { + cryptonote::tx_extra_pub_key tx_pubkey; + if (!cryptonote::find_tx_extra_field_by_type(extra_fields, tx_pubkey)) + return false; + + memcpy(enotes_inout.front().enote_ephemeral_pubkey.data, tx_pubkey.pub_key.data, sizeof(mx25519_pubkey)); + memcpy(enotes_inout.back().enote_ephemeral_pubkey.data, tx_pubkey.pub_key.data, sizeof(mx25519_pubkey)); + } + else // nouts != 2 + { + cryptonote::tx_extra_additional_pub_keys tx_pubkeys; + if (!cryptonote::find_tx_extra_field_by_type(extra_fields, tx_pubkeys)) + return false; + else if (tx_pubkeys.data.size() != nouts) + return false; + + for (size_t i = 0; i < nouts; ++i) + memcpy(enotes_inout[i].enote_ephemeral_pubkey.data, tx_pubkeys.data.at(i).data, sizeof(mx25519_pubkey)); + } + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +cryptonote::transaction store_carrot_to_transaction_v1(const std::vector &enotes, + const std::vector &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_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 + crypto::hash8 pid_enc_8; + memcpy(pid_enc_8.data, encrypted_payment_id.bytes, sizeof(pid_enc_8)); + 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 &enotes_out, + std::vector &key_images_out, + rct::xmr_amount &fee_out, + std::optional &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(&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(&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 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_t &pid_enc = encrypted_payment_id_out.emplace(); + memcpy(pid_enc.bytes, pid_enc_8.data, sizeof(pid_enc)); + } + } + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +cryptonote::transaction store_carrot_to_coinbase_transaction_v1( + const std::vector &enotes, + const std::uint64_t block_index) +{ + const size_t nouts = enotes.size(); + + 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 = 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 &enotes_out, + std::uint64_t &block_index_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(&tx.vin.front()); + if (nullptr == h) + return false; + block_index_out = h->height; + + //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(&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 = block_index_out; + } + + //parse tx_extra + std::vector 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 diff --git a/src/carrot_impl/carrot_tx_format_utils.h b/src/carrot_impl/carrot_tx_format_utils.h new file mode 100644 index 00000000000..904e7f179c2 --- /dev/null +++ b/src/carrot_impl/carrot_tx_format_utils.h @@ -0,0 +1,92 @@ +// 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" + +//third party headers + +//standard headers +#include +#include + +//forward declarations + +namespace carrot +{ + +/** + * 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 &enotes, + const std::vector &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 &enotes_out, + std::vector &key_images_out, + rct::xmr_amount &fee_out, + std::optional &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 &enotes, + const std::uint64_t block_index); +/** + * 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 &enotes_out, + std::uint64_t &block_index_out); + +} //namespace carrot diff --git a/src/carrot_impl/tx_builder_inputs.h b/src/carrot_impl/tx_builder_inputs.h new file mode 100644 index 00000000000..4ae0f0c3844 --- /dev/null +++ b/src/carrot_impl/tx_builder_inputs.h @@ -0,0 +1,46 @@ +// 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 "crypto/crypto.h" + +//third party headers + +//standard headers + +//forward declarations + +namespace carrot +{ +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; +} +} //namespace carrot diff --git a/src/cryptonote_basic/cryptonote_basic.h b/src/cryptonote_basic/cryptonote_basic.h index a50ae9c32d8..97b5f61af00 100644 --- a/src/cryptonote_basic/cryptonote_basic.h +++ b/src/cryptonote_basic/cryptonote_basic.h @@ -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 keys; - std::vector 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 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_v; - typedef boost::variant txout_target_v; + typedef boost::variant txout_target_v; //typedef std::pair 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"); diff --git a/src/cryptonote_basic/cryptonote_boost_serialization.h b/src/cryptonote_basic/cryptonote_boost_serialization.h index 8948c650cd4..3aea707e52c 100644 --- a/src/cryptonote_basic/cryptonote_boost_serialization.h +++ b/src/cryptonote_basic/cryptonote_boost_serialization.h @@ -38,6 +38,7 @@ #include #include #include +#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 - 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 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 diff --git a/src/cryptonote_basic/tx_extra.h b/src/cryptonote_basic/tx_extra.h index 74c319ec3b8..266e0c48837 100644 --- a/src/cryptonote_basic/tx_extra.h +++ b/src/cryptonote_basic/tx_extra.h @@ -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 data; BEGIN_SERIALIZE() diff --git a/src/serialization/json_object.cpp b/src/serialization/json_object.cpp index 134bf1c69be..905378065be 100644 --- a/src/serialization/json_object.cpp +++ b/src/serialization/json_object.cpp @@ -458,12 +458,6 @@ void fromJsonValue(const rapidjson::Value& val, cryptonote::txin_to_script& txin void toJsonValue(rapidjson::Writer& 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& 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& dest, const cryptonote::txout_to_script& txout) +void toJsonValue(rapidjson::Writer& 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& 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); } diff --git a/src/serialization/json_object.h b/src/serialization/json_object.h index 3dfff3336c4..4a9dff563ed 100644 --- a/src/serialization/json_object.h +++ b/src/serialization/json_object.h @@ -221,8 +221,8 @@ void fromJsonValue(const rapidjson::Value& val, cryptonote::txin_to_key& txin); void toJsonValue(rapidjson::Writer& dest, const cryptonote::txout_target_v& txout); void fromJsonValue(const rapidjson::Value& val, cryptonote::txout_target_v& txout); -void toJsonValue(rapidjson::Writer& dest, const cryptonote::txout_to_script& txout); -void fromJsonValue(const rapidjson::Value& val, cryptonote::txout_to_script& txout); +void toJsonValue(rapidjson::Writer& dest, const cryptonote::txout_to_carrot_v1& txout); +void fromJsonValue(const rapidjson::Value& val, cryptonote::txout_to_carrot_v1& txout); void toJsonValue(rapidjson::Writer& dest, const cryptonote::txout_to_scripthash& txout); void fromJsonValue(const rapidjson::Value& val, cryptonote::txout_to_scripthash& txout); diff --git a/tests/core_tests/tx_validation.cpp b/tests/core_tests/tx_validation.cpp index 48f2064b05b..10c506deb9d 100644 --- a/tests/core_tests/tx_validation.cpp +++ b/tests/core_tests/tx_validation.cpp @@ -731,19 +731,6 @@ bool gen_tx_output_is_not_txout_to_key::generate(std::vector& 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(); diff --git a/tests/unit_tests/CMakeLists.txt b/tests/unit_tests/CMakeLists.txt index 93aba5a237f..df90e67ea46 100644 --- a/tests/unit_tests/CMakeLists.txt +++ b/tests/unit_tests/CMakeLists.txt @@ -39,6 +39,7 @@ set(unit_tests_sources bulletproofs_plus.cpp canonical_amounts.cpp carrot_core.cpp + carrot_impl.cpp carrot_legacy.cpp carrot_transcript_fixed.cpp chacha.cpp @@ -118,6 +119,7 @@ target_link_libraries(unit_tests PRIVATE ringct carrot_core + carrot_impl cryptonote_protocol cryptonote_core daemon_messages diff --git a/tests/unit_tests/carrot_impl.cpp b/tests/unit_tests/carrot_impl.cpp new file mode 100644 index 00000000000..58110a8d07d --- /dev/null +++ b/tests/unit_tests/carrot_impl.cpp @@ -0,0 +1,1002 @@ +// 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. + +#include "gtest/gtest.h" + +#include + +#include "carrot_core/account_secrets.h" +#include "carrot_core/address_utils.h" +#include "carrot_core/carrot_enote_scan.h" +#include "carrot_core/destination.h" +#include "carrot_core/device_ram_borrowed.h" +#include "carrot_core/enote_utils.h" +#include "carrot_core/output_set_finalization.h" +#include "carrot_core/payment_proposal.h" +#include "carrot_impl/carrot_tx_builder.h" +#include "carrot_impl/carrot_tx_format_utils.h" +#include "common/container_helpers.h" +#include "crypto/generators.h" +#include "cryptonote_basic/account.h" +#include "cryptonote_basic/subaddress_index.h" +#include "ringct/rctOps.h" + +using namespace carrot; + +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +namespace +{ +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +static constexpr std::uint32_t MAX_SUBADDRESS_MAJOR_INDEX = 50; +static constexpr std::uint32_t MAX_SUBADDRESS_MINOR_INDEX = 200; +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +struct mock_carrot_or_legacy_keys +{ + bool is_carrot; + + crypto::secret_key s_master; + crypto::secret_key k_prove_spend; + crypto::secret_key s_view_balance; + crypto::secret_key k_generate_image; + crypto::secret_key k_view; + crypto::secret_key s_generate_address; + crypto::public_key account_spend_pubkey; + crypto::public_key account_view_pubkey; + crypto::public_key main_address_view_pubkey; + + cryptonote::account_base legacy_acb; + + view_incoming_key_ram_borrowed_device k_view_dev; + view_balance_secret_ram_borrowed_device s_view_balance_dev; + + mock_carrot_or_legacy_keys(): k_view_dev(k_view), s_view_balance_dev(s_view_balance) {} + + void generate_carrot() + { + is_carrot = true; + crypto::generate_random_bytes_thread_safe(sizeof(crypto::secret_key), to_bytes(s_master)); + make_carrot_provespend_key(s_master, k_prove_spend); + make_carrot_viewbalance_secret(s_master, s_view_balance); + make_carrot_generateimage_key(s_view_balance, k_generate_image); + make_carrot_viewincoming_key(s_view_balance, k_view); + make_carrot_generateaddress_secret(s_view_balance, s_generate_address); + make_carrot_spend_pubkey(k_generate_image, k_prove_spend, account_spend_pubkey); + account_view_pubkey = rct::rct2pk(rct::scalarmultKey(rct::pk2rct(account_spend_pubkey), + rct::sk2rct(k_view))); + main_address_view_pubkey = rct::rct2pk(rct::scalarmultBase(rct::sk2rct(k_view))); + } + + void generate_legacy() + { + is_carrot = false; + legacy_acb.generate(); + k_view = legacy_acb.get_keys().m_view_secret_key; + account_spend_pubkey = legacy_acb.get_keys().m_account_address.m_spend_public_key; + main_address_view_pubkey = legacy_acb.get_keys().m_account_address.m_view_public_key; + } + + const view_balance_secret_device* get_view_balance_device() const + { + return is_carrot ? &s_view_balance_dev : nullptr; + } + + CarrotDestinationV1 cryptonote_address(const payment_id_t payment_id = null_payment_id) const + { + CarrotDestinationV1 addr; + if (is_carrot) + { + make_carrot_integrated_address_v1(account_spend_pubkey, + main_address_view_pubkey, + payment_id, + addr); + } + else + { + make_carrot_integrated_address_v1(legacy_acb.get_keys().m_account_address.m_spend_public_key, + legacy_acb.get_keys().m_account_address.m_view_public_key, + payment_id, + addr); + } + return addr; + } + + CarrotDestinationV1 subaddress(const uint32_t major_index, const uint32_t minor_index) const + { + if (!major_index && !minor_index) + return cryptonote_address(); + + CarrotDestinationV1 addr; + if (is_carrot) + { + make_carrot_subaddress_v1(account_spend_pubkey, + account_view_pubkey, + s_generate_address, + major_index, + minor_index, + addr); + } + else + { + const cryptonote::account_keys &ks = legacy_acb.get_keys(); + const cryptonote::account_public_address cnaddr = + ks.m_device->get_subaddress(ks, {major_index, minor_index}); + addr = CarrotDestinationV1{ + .address_spend_pubkey = cnaddr.m_spend_public_key, + .address_view_pubkey = cnaddr.m_view_public_key, + .is_subaddress = true, + .payment_id = null_payment_id + }; + } + return addr; + } + + // brief: opening_for_subaddress - return (k^g_a, k^t_a) for j s.t. K^j_s = (k^g_a * G + k^t_a * T) + void opening_for_subaddress(const uint32_t major_index, + const uint32_t minor_index, + crypto::secret_key &address_privkey_g_out, + crypto::secret_key &address_privkey_t_out, + crypto::public_key &address_spend_pubkey_out) const + { + const bool is_subaddress = major_index || minor_index; + + if (is_carrot) + { + // s^j_gen = H_32[s_ga](j_major, j_minor) + crypto::secret_key address_index_generator; + make_carrot_index_extension_generator(s_generate_address, major_index, minor_index, address_index_generator); + + crypto::secret_key subaddress_scalar{}; + if (is_subaddress) + { + // k^j_subscal = H_n(K_s, j_major, j_minor, s^j_gen) + make_carrot_subaddress_scalar(account_spend_pubkey, address_index_generator, major_index, minor_index, subaddress_scalar); + } + else + { + subaddress_scalar.data[0] = 1; + } + + // k^g_a = k_gi * k^j_subscal + sc_mul(to_bytes(address_privkey_g_out), to_bytes(k_generate_image), to_bytes(subaddress_scalar)); + + // k^t_a = k_ps * k^j_subscal + sc_mul(to_bytes(address_privkey_t_out), to_bytes(k_prove_spend), to_bytes(subaddress_scalar)); + } + else // legacy keys + { + // m = Hn(k_v || j_major || j_minor) if subaddress else 0 + const cryptonote::account_keys &ks = legacy_acb.get_keys(); + const crypto::secret_key subaddress_extension = is_subaddress + ? ks.get_device().get_subaddress_secret_key(ks.m_view_secret_key, {major_index, minor_index}) + : crypto::null_skey; + + // k^g_a = k_s + m + sc_add(to_bytes(address_privkey_g_out), to_bytes(ks.m_spend_secret_key), to_bytes(subaddress_extension)); + + // k^t_a = 0 + memset(address_privkey_t_out.data, 0, sizeof(address_privkey_t_out)); + } + + // perform sanity check + const CarrotDestinationV1 addr = subaddress(major_index, minor_index); + rct::key recomputed_address_spend_pubkey; + rct::addKeys2(recomputed_address_spend_pubkey, + rct::sk2rct(address_privkey_g_out), + rct::sk2rct(address_privkey_t_out), + rct::pk2rct(crypto::get_T())); + CHECK_AND_ASSERT_THROW_MES(rct::rct2pk(recomputed_address_spend_pubkey) == addr.address_spend_pubkey, + "mock carrot or legacy keys: opening for subaddress: failed sanity check"); + address_spend_pubkey_out = addr.address_spend_pubkey; + } + + bool try_searching_for_opening_for_subaddress(const crypto::public_key &address_spend_pubkey, + const uint32_t max_major_index, + const uint32_t max_minor_index, + uint32_t major_index_out, + uint32_t minor_index_out, + crypto::secret_key &address_privkey_g_out, + crypto::secret_key &address_privkey_t_out) const + { + // shittier version of a subaddress lookahead table + + for (major_index_out = 0; major_index_out < max_major_index; ++major_index_out) + { + for (minor_index_out = 0; minor_index_out < max_minor_index; ++minor_index_out) + { + crypto::public_key recomputed_address_spend_pubkey; + opening_for_subaddress(major_index_out, + minor_index_out, + address_privkey_g_out, + address_privkey_t_out, + recomputed_address_spend_pubkey); + if (address_spend_pubkey == recomputed_address_spend_pubkey) + return true; + } + } + + return false; + } +}; +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +static bool can_open_fcmp_onetime_address(const crypto::secret_key &address_privkey_g, + const crypto::secret_key &address_privkey_t, + const crypto::secret_key &sender_extension_g, + const crypto::secret_key &sender_extension_t, + const crypto::public_key &onetime_address) +{ + rct::key combined_g; + sc_add(combined_g.bytes, to_bytes(address_privkey_g), to_bytes(sender_extension_g)); + + rct::key combined_t; + sc_add(combined_t.bytes, to_bytes(address_privkey_t), to_bytes(sender_extension_t)); + + // Ko' = combined_g G + combined_t T + rct::key recomputed_onetime_address; + rct::addKeys2(recomputed_onetime_address, combined_g, combined_t, rct::pk2rct(crypto::get_T())); + + // Ko' ?= Ko + return recomputed_onetime_address == onetime_address; +} +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +struct unittest_carrot_scan_result_t +{ + crypto::public_key address_spend_pubkey = rct::rct2pk(rct::I); + crypto::secret_key sender_extension_g = rct::rct2sk(rct::I); + crypto::secret_key sender_extension_t = rct::rct2sk(rct::I); + + rct::xmr_amount amount = 0; + crypto::secret_key amount_blinding_factor = rct::rct2sk(rct::I); + + CarrotEnoteType enote_type = CarrotEnoteType::PAYMENT; + + payment_id_t payment_id = null_payment_id; + + janus_anchor_t internal_message = janus_anchor_t{}; + + size_t output_index = 0; +}; +static void unittest_scan_enote_set(const std::vector &enotes, + const encrypted_payment_id_t encrypted_payment_id, + const mock_carrot_or_legacy_keys keys, + std::vector &res) +{ + res.clear(); + + // for each enote... + for (size_t output_index = 0; output_index < enotes.size(); ++output_index) + { + const CarrotEnoteV1 &enote = enotes.at(output_index); + + // s_sr = k_v D_e + mx25519_pubkey s_sr; + make_carrot_uncontextualized_shared_key_receiver(keys.k_view, enote.enote_ephemeral_pubkey, s_sr); + + // external scan + unittest_carrot_scan_result_t scan_result{}; + bool r = try_scan_carrot_enote_external(enote, + encrypted_payment_id, + s_sr, + keys.k_view_dev, + keys.account_spend_pubkey, + scan_result.sender_extension_g, + scan_result.sender_extension_t, + scan_result.address_spend_pubkey, + scan_result.amount, + scan_result.amount_blinding_factor, + scan_result.payment_id, + scan_result.enote_type); + + // internal scan + r = r || try_scan_carrot_enote_internal(enote, + keys.s_view_balance_dev, + scan_result.sender_extension_g, + scan_result.sender_extension_t, + scan_result.address_spend_pubkey, + scan_result.amount, + scan_result.amount_blinding_factor, + scan_result.enote_type, + scan_result.internal_message); + + scan_result.output_index = output_index; + + if (r) + res.push_back(scan_result); + } +} +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +static void unittest_scan_enote_set_multi_account(const std::vector &enotes, + const encrypted_payment_id_t encrypted_payment_id, + const epee::span accounts, + std::vector> &res) +{ + res.clear(); + res.reserve(accounts.size()); + + for (const mock_carrot_or_legacy_keys *account : accounts) + unittest_scan_enote_set(enotes, encrypted_payment_id, *account, tools::add_element(res)); +} +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +static bool compare_scan_result(const unittest_carrot_scan_result_t &scan_res, + const CarrotPaymentProposalV1 &normal_payment_proposal) +{ + if (scan_res.address_spend_pubkey != normal_payment_proposal.destination.address_spend_pubkey) + return false; + + if (scan_res.amount != normal_payment_proposal.amount) + return false; + + if (scan_res.enote_type != CarrotEnoteType::PAYMENT) + return false; + + if (scan_res.payment_id != normal_payment_proposal.destination.payment_id) + return false; + + if (scan_res.internal_message != janus_anchor_t{}) + return false; + + return true; +} +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +static bool compare_scan_result(const unittest_carrot_scan_result_t &scan_res, + const CarrotPaymentProposalSelfSendV1 &selfsend_payment_proposal) +{ + if (scan_res.address_spend_pubkey != selfsend_payment_proposal.destination_address_spend_pubkey) + return false; + + if (scan_res.amount != selfsend_payment_proposal.amount) + return false; + + if (scan_res.enote_type != selfsend_payment_proposal.enote_type) + return false; + + if (scan_res.payment_id != null_payment_id) + return false; + + if (scan_res.internal_message != selfsend_payment_proposal.internal_message.value_or(janus_anchor_t{})) + return false; + + return true; +} +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +struct unittest_transaction_proposal +{ + using per_account = std::pair>; + using per_input = std::pair; + + std::vector per_account_payments; + std::vector explicit_selfsend_proposals; + size_t self_sender_index{0}; + rct::xmr_amount fee_per_weight; + + void get_flattened_payment_proposals(std::vector &normal_payment_proposals_out, + std::vector &selfsend_payment_proposals_out) const + { + for (const per_account &pa : per_account_payments) + { + normal_payment_proposals_out.insert(normal_payment_proposals_out.end(), + pa.second.cbegin(), + pa.second.cend()); + } + + selfsend_payment_proposals_out = explicit_selfsend_proposals; + } +}; +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +select_inputs_func_t make_fake_input_selection_callback(size_t num_ins = 0) +{ + return [num_ins](const boost::multiprecision::int128_t &nominal_output_sum, + const std::map &fee_per_input_count, + std::vector &selected_inputs) + { + const size_t nins = num_ins ? num_ins : 1; + selected_inputs.clear(); + selected_inputs.reserve(nins); + + const rct::xmr_amount fee = fee_per_input_count.at(nins); + rct::xmr_amount in_amount_sum_64 = boost::numeric_cast(nominal_output_sum + fee); + + for (size_t i = 0; i < nins - 1; ++i) + { + const rct::xmr_amount current_in_amount = in_amount_sum_64 ? crypto::rand_idx(in_amount_sum_64) : 0; + const crypto::key_image current_key_image = rct::rct2ki(rct::pkGen()); + selected_inputs.push_back({current_in_amount, current_key_image}); + in_amount_sum_64 -= current_in_amount; + } + + selected_inputs.push_back({in_amount_sum_64, rct::rct2ki(rct::pkGen())}); + }; +} +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +} // namespace +static inline bool operator==(const mx25519_pubkey &a, const mx25519_pubkey &b) +{ + return 0 == memcmp(&a, &b, sizeof(mx25519_pubkey)); +} +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +static void subtest_multi_account_transfer_over_transaction(const unittest_transaction_proposal &tx_proposal) +{ + // get payment proposals + std::vector normal_payment_proposals; + std::vector selfsend_payment_proposals; + tx_proposal.get_flattened_payment_proposals(normal_payment_proposals, selfsend_payment_proposals); + + // get self-sender account + const mock_carrot_or_legacy_keys &ss_keys = + tx_proposal.per_account_payments.at(tx_proposal.self_sender_index).first; + + // make unsigned transaction + cryptonote::transaction tx; + crypto::secret_key amount_blinding_factor_sum; + make_unsigned_transaction_transfer(std::vector(normal_payment_proposals), + std::vector(selfsend_payment_proposals), + tx_proposal.fee_per_weight, + make_fake_input_selection_callback(), + ss_keys.get_view_balance_device(), + &ss_keys.k_view_dev, + ss_keys.account_spend_pubkey, + tx, + amount_blinding_factor_sum); + + // load carrot stuff from tx + std::vector parsed_enotes; + std::vector parsed_key_images; + rct::xmr_amount parsed_fee; + std::optional parsed_encrypted_payment_id; + ASSERT_TRUE(try_load_carrot_from_transaction_v1(tx, + parsed_enotes, + parsed_key_images, + parsed_fee, + parsed_encrypted_payment_id)); + + // collect accounts + std::vector accounts; + for (const auto &pa : tx_proposal.per_account_payments) + accounts.push_back(&pa.first); + + // do scanning of all accounts on every enotes + std::vector> scan_results; + unittest_scan_enote_set_multi_account(parsed_enotes, + *parsed_encrypted_payment_id, + epee::to_span(accounts), + scan_results); + + // check that the scan results for each account match the corresponding payment proposals for each account + // also check that the accounts can each open their corresponding onetime outut pubkeys + ASSERT_EQ(scan_results.size(), accounts.size()); + for (size_t account_idx = 0; account_idx < accounts.size(); ++account_idx) + { + const std::vector &account_scan_results = scan_results.at(account_idx); + if (account_idx == tx_proposal.self_sender_index) + { + ASSERT_EQ(selfsend_payment_proposals.size() + 1, account_scan_results.size()); + std::set matched_payment_proposals; + const unittest_carrot_scan_result_t* implicit_change_scan_res = nullptr; + for (const unittest_carrot_scan_result_t &single_scan_res : account_scan_results) + { + bool matched_payment = false; + for (size_t ss_prop_idx = 0; ss_prop_idx < selfsend_payment_proposals.size(); ++ss_prop_idx) + { + const CarrotPaymentProposalSelfSendV1 &account_payment_proposal = selfsend_payment_proposals.at(ss_prop_idx); + if (compare_scan_result(single_scan_res, account_payment_proposal)) + { + crypto::secret_key address_privkey_g; + crypto::secret_key address_privkey_t; + uint32_t _1{}, _2{}; + EXPECT_TRUE(accounts.at(account_idx)->try_searching_for_opening_for_subaddress( + single_scan_res.address_spend_pubkey, + MAX_SUBADDRESS_MAJOR_INDEX, + MAX_SUBADDRESS_MINOR_INDEX, + _1, + _2, + address_privkey_g, + address_privkey_t)); + + EXPECT_TRUE(can_open_fcmp_onetime_address(address_privkey_g, + address_privkey_t, + single_scan_res.sender_extension_g, + single_scan_res.sender_extension_t, + parsed_enotes.at(single_scan_res.output_index).onetime_address)); + + if (!matched_payment_proposals.count(ss_prop_idx)) + { + matched_payment = true; + matched_payment_proposals.insert(ss_prop_idx); + break; + } + } + } + if (!matched_payment) + { + EXPECT_EQ(nullptr, implicit_change_scan_res); // only one non-matched scan result is allowed + implicit_change_scan_res = &single_scan_res; + } + } + EXPECT_EQ(selfsend_payment_proposals.size(), matched_payment_proposals.size()); + EXPECT_NE(nullptr, implicit_change_scan_res); + // @TODO: assert properties of `implicit_change_scan_res` + } + else + { + const std::vector &account_payment_proposals = tx_proposal.per_account_payments.at(account_idx).second; + ASSERT_EQ(account_payment_proposals.size(), account_scan_results.size()); + std::set matched_payment_proposals; + for (const unittest_carrot_scan_result_t &single_scan_res : account_scan_results) + { + for (size_t norm_prop_idx = 0; norm_prop_idx < account_payment_proposals.size(); ++norm_prop_idx) + { + const CarrotPaymentProposalV1 &account_payment_proposal = account_payment_proposals.at(norm_prop_idx); + if (compare_scan_result(single_scan_res, account_payment_proposal)) + { + crypto::secret_key address_privkey_g; + crypto::secret_key address_privkey_t; + uint32_t _1{}, _2{}; + EXPECT_TRUE(accounts.at(account_idx)->try_searching_for_opening_for_subaddress( + single_scan_res.address_spend_pubkey, + MAX_SUBADDRESS_MAJOR_INDEX, + MAX_SUBADDRESS_MINOR_INDEX, + _1, + _2, + address_privkey_g, + address_privkey_t)); + + EXPECT_TRUE(can_open_fcmp_onetime_address(address_privkey_g, + address_privkey_t, + single_scan_res.sender_extension_g, + single_scan_res.sender_extension_t, + parsed_enotes.at(single_scan_res.output_index).onetime_address)); + + if (!matched_payment_proposals.count(norm_prop_idx)) + { + matched_payment_proposals.insert(norm_prop_idx); + break; + } + } + } + } + EXPECT_EQ(account_payment_proposals.size(), matched_payment_proposals.size()); + } + } +} +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_impl, multi_account_transfer_over_transaction_1) +{ + // two accounts, both carrot + // 1/2 tx + // 1 normal payment to main address + // 0 explicit selfsend payments + + unittest_transaction_proposal tx_proposal; + tx_proposal.per_account_payments.resize(2); + mock_carrot_or_legacy_keys &acc0 = tx_proposal.per_account_payments[0].first; + mock_carrot_or_legacy_keys &acc1 = tx_proposal.per_account_payments[1].first; + acc0.generate_carrot(); + acc1.generate_carrot(); + + // 1 normal payment + CarrotPaymentProposalV1 &normal_payment_proposal = tools::add_element( tx_proposal.per_account_payments[0].second); + normal_payment_proposal = CarrotPaymentProposalV1{ + .destination = acc0.cryptonote_address(), + .amount = crypto::rand_idx((rct::xmr_amount) 1ull << 63), + .randomness = gen_janus_anchor() + }; + + // specify self-sender + tx_proposal.self_sender_index = 1; + + // specify fee per weight + tx_proposal.fee_per_weight = 20250510; + + // test + subtest_multi_account_transfer_over_transaction(tx_proposal); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_impl, multi_account_transfer_over_transaction_2) +{ + // four accounts, all carrot + // 1/4 tx + // 1 normal payment to main address, integrated address, and subaddress each + // 0 explicit selfsend payments + + unittest_transaction_proposal tx_proposal; + tx_proposal.per_account_payments.resize(4); + auto &acc0 = tx_proposal.per_account_payments[0]; + auto &acc1 = tx_proposal.per_account_payments[1]; + auto &acc2 = tx_proposal.per_account_payments[2]; + auto &acc3 = tx_proposal.per_account_payments[3]; + acc0.first.generate_carrot(); + acc1.first.generate_carrot(); + acc2.first.generate_carrot(); + acc3.first.generate_carrot(); + + // specify self-sender + tx_proposal.self_sender_index = 2; + + // 1 subaddress payment + tools::add_element(acc0.second) = CarrotPaymentProposalV1{ + .destination = acc0.first.subaddress(17, 18), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + + // 1 main address payment + tools::add_element(acc1.second) = CarrotPaymentProposalV1{ + .destination = acc1.first.cryptonote_address(), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + + // 1 integrated address payment + tools::add_element(acc3.second) = CarrotPaymentProposalV1{ + .destination = acc3.first.cryptonote_address(gen_payment_id()), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + + // specify fee per weight + tx_proposal.fee_per_weight = 314159; + + // test + subtest_multi_account_transfer_over_transaction(tx_proposal); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_impl, multi_account_transfer_over_transaction_3) +{ + // four accounts, all carrot + // 1/6 tx + // 2 normal payment to main address, 1 integrated address, and 2 subaddress, each copied except integrated + // 0 explicit selfsend payments + + unittest_transaction_proposal tx_proposal; + tx_proposal.per_account_payments.resize(4); + auto &acc0 = tx_proposal.per_account_payments[0]; + auto &acc1 = tx_proposal.per_account_payments[1]; + auto &acc2 = tx_proposal.per_account_payments[2]; + auto &acc3 = tx_proposal.per_account_payments[3]; + acc0.first.generate_carrot(); + acc1.first.generate_carrot(); + acc2.first.generate_carrot(); + acc3.first.generate_carrot(); + + // specify self-sender + tx_proposal.self_sender_index = 2; + + // 2 subaddress payment + tools::add_element(acc0.second) = CarrotPaymentProposalV1{ + .destination = acc0.first.subaddress(17, 18), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + acc0.second.push_back(acc0.second.front()); + acc0.second.back().randomness = gen_janus_anchor(); //mangle anchor_norm + + // 2 main address payment + tools::add_element(acc1.second) = CarrotPaymentProposalV1{ + .destination = acc1.first.cryptonote_address(), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + acc1.second.push_back(acc1.second.front()); + acc1.second.back().randomness = gen_janus_anchor(); //mangle anchor_norm + + // 1 integrated address payment + tools::add_element(acc3.second) = CarrotPaymentProposalV1{ + .destination = acc3.first.cryptonote_address(gen_payment_id()), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + + // specify fee per weight + tx_proposal.fee_per_weight = 314159; + + // test + subtest_multi_account_transfer_over_transaction(tx_proposal); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_impl, multi_account_transfer_over_transaction_4) +{ + // four accounts, all carrot + // 1/8 tx + // 2 normal payment to main address, 1 integrated address, and 2 subaddress, each copied except integrated + // 2 explicit selfsend payments: 1 main address destination, 1 subaddress destination + + unittest_transaction_proposal tx_proposal; + tx_proposal.per_account_payments.resize(4); + auto &acc0 = tx_proposal.per_account_payments[0]; + auto &acc1 = tx_proposal.per_account_payments[1]; + auto &acc2 = tx_proposal.per_account_payments[2]; + auto &acc3 = tx_proposal.per_account_payments[3]; + acc0.first.generate_carrot(); + acc1.first.generate_carrot(); + acc2.first.generate_carrot(); + acc3.first.generate_carrot(); + + // specify self-sender + tx_proposal.self_sender_index = 2; + + // 2 subaddress payment + tools::add_element(acc0.second) = CarrotPaymentProposalV1{ + .destination = acc0.first.subaddress(17, 18), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + acc0.second.push_back(acc0.second.front()); + acc0.second.back().randomness = gen_janus_anchor(); //mangle anchor_norm + + // 2 main address payment + tools::add_element(acc1.second) = CarrotPaymentProposalV1{ + .destination = acc1.first.cryptonote_address(), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + acc1.second.push_back(acc1.second.front()); + acc1.second.back().randomness = gen_janus_anchor(); //mangle anchor_norm + + // 1 integrated address payment + tools::add_element(acc3.second) = CarrotPaymentProposalV1{ + .destination = acc3.first.cryptonote_address(gen_payment_id()), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + + // 1 main address selfsend + tools::add_element(tx_proposal.explicit_selfsend_proposals) = CarrotPaymentProposalSelfSendV1{ + .destination_address_spend_pubkey = acc2.first.account_spend_pubkey, + .amount = crypto::rand_idx(1000000), + .enote_type = CarrotEnoteType::PAYMENT, + .internal_message = gen_janus_anchor() + }; + + // 1 subaddress selfsend + tools::add_element(tx_proposal.explicit_selfsend_proposals) = CarrotPaymentProposalSelfSendV1{ + .destination_address_spend_pubkey = acc2.first.subaddress(49, 199).address_spend_pubkey, + .amount = crypto::rand_idx(1000000), + .enote_type = CarrotEnoteType::CHANGE + }; + + // specify fee per weight + tx_proposal.fee_per_weight = 314159; + + // test + subtest_multi_account_transfer_over_transaction(tx_proposal); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_impl, multi_account_transfer_over_transaction_5) +{ + // two accounts, both legacy + // 1/2 tx + // 1 normal payment to main address + // 0 explicit selfsend payments + + unittest_transaction_proposal tx_proposal; + tx_proposal.per_account_payments.resize(2); + mock_carrot_or_legacy_keys &acc0 = tx_proposal.per_account_payments[0].first; + mock_carrot_or_legacy_keys &acc1 = tx_proposal.per_account_payments[1].first; + acc0.generate_legacy(); + acc1.generate_legacy(); + + // 1 normal payment + CarrotPaymentProposalV1 &normal_payment_proposal = tools::add_element( tx_proposal.per_account_payments[0].second); + normal_payment_proposal = CarrotPaymentProposalV1{ + .destination = acc0.cryptonote_address(), + .amount = crypto::rand_idx((rct::xmr_amount) 1ull << 63), + .randomness = gen_janus_anchor() + }; + + // specify self-sender + tx_proposal.self_sender_index = 1; + + // specify fee per weight + tx_proposal.fee_per_weight = 20250510; + + // test + subtest_multi_account_transfer_over_transaction(tx_proposal); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_impl, multi_account_transfer_over_transaction_6) +{ + // four accounts, all legacy + // 1/4 tx + // 1 normal payment to main address, integrated address, and subaddress each + // 0 explicit selfsend payments + + unittest_transaction_proposal tx_proposal; + tx_proposal.per_account_payments.resize(4); + auto &acc0 = tx_proposal.per_account_payments[0]; + auto &acc1 = tx_proposal.per_account_payments[1]; + auto &acc2 = tx_proposal.per_account_payments[2]; + auto &acc3 = tx_proposal.per_account_payments[3]; + acc0.first.generate_legacy(); + acc1.first.generate_legacy(); + acc2.first.generate_legacy(); + acc3.first.generate_legacy(); + + // specify self-sender + tx_proposal.self_sender_index = 2; + + // 1 subaddress payment + tools::add_element(acc0.second) = CarrotPaymentProposalV1{ + .destination = acc0.first.subaddress(17, 18), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + + // 1 main address payment + tools::add_element(acc1.second) = CarrotPaymentProposalV1{ + .destination = acc1.first.cryptonote_address(), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + + // 1 integrated address payment + tools::add_element(acc3.second) = CarrotPaymentProposalV1{ + .destination = acc3.first.cryptonote_address(gen_payment_id()), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + + // specify fee per weight + tx_proposal.fee_per_weight = 314159; + + // test + subtest_multi_account_transfer_over_transaction(tx_proposal); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_impl, multi_account_transfer_over_transaction_7) +{ + // four accounts, all legacy + // 1/6 tx + // 2 normal payment to main address, 1 integrated address, and 2 subaddress, each copied except integrated + // 0 explicit selfsend payments + + unittest_transaction_proposal tx_proposal; + tx_proposal.per_account_payments.resize(4); + auto &acc0 = tx_proposal.per_account_payments[0]; + auto &acc1 = tx_proposal.per_account_payments[1]; + auto &acc2 = tx_proposal.per_account_payments[2]; + auto &acc3 = tx_proposal.per_account_payments[3]; + acc0.first.generate_legacy(); + acc1.first.generate_legacy(); + acc2.first.generate_legacy(); + acc3.first.generate_legacy(); + + // specify self-sender + tx_proposal.self_sender_index = 2; + + // 2 subaddress payment + tools::add_element(acc0.second) = CarrotPaymentProposalV1{ + .destination = acc0.first.subaddress(17, 18), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + acc0.second.push_back(acc0.second.front()); + acc0.second.back().randomness = gen_janus_anchor(); //mangle anchor_norm + + // 2 main address payment + tools::add_element(acc1.second) = CarrotPaymentProposalV1{ + .destination = acc1.first.cryptonote_address(), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + acc1.second.push_back(acc1.second.front()); + acc1.second.back().randomness = gen_janus_anchor(); //mangle anchor_norm + + // 1 integrated address payment + tools::add_element(acc3.second) = CarrotPaymentProposalV1{ + .destination = acc3.first.cryptonote_address(gen_payment_id()), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + + // specify fee per weight + tx_proposal.fee_per_weight = 314159; + + // test + subtest_multi_account_transfer_over_transaction(tx_proposal); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_impl, multi_account_transfer_over_transaction_8) +{ + // four accounts, all legacy + // 1/8 tx + // 2 normal payment to main address, 1 integrated address, and 2 subaddress, each copied except integrated + // 2 explicit selfsend payments: 1 main address destination, 1 subaddress destination + + unittest_transaction_proposal tx_proposal; + tx_proposal.per_account_payments.resize(4); + auto &acc0 = tx_proposal.per_account_payments[0]; + auto &acc1 = tx_proposal.per_account_payments[1]; + auto &acc2 = tx_proposal.per_account_payments[2]; + auto &acc3 = tx_proposal.per_account_payments[3]; + acc0.first.generate_legacy(); + acc1.first.generate_legacy(); + acc2.first.generate_legacy(); + acc3.first.generate_legacy(); + + // specify self-sender + tx_proposal.self_sender_index = 2; + + // 2 subaddress payment + tools::add_element(acc0.second) = CarrotPaymentProposalV1{ + .destination = acc0.first.subaddress(17, 18), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + acc0.second.push_back(acc0.second.front()); + acc0.second.back().randomness = gen_janus_anchor(); //mangle anchor_norm + + // 2 main address payment + tools::add_element(acc1.second) = CarrotPaymentProposalV1{ + .destination = acc1.first.cryptonote_address(), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + acc1.second.push_back(acc1.second.front()); + acc1.second.back().randomness = gen_janus_anchor(); //mangle anchor_norm + + // 1 integrated address payment + tools::add_element(acc3.second) = CarrotPaymentProposalV1{ + .destination = acc3.first.cryptonote_address(gen_payment_id()), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + + // 1 main address selfsend + tools::add_element(tx_proposal.explicit_selfsend_proposals) = CarrotPaymentProposalSelfSendV1{ + .destination_address_spend_pubkey = acc2.first.account_spend_pubkey, + .amount = crypto::rand_idx(1000000), + .enote_type = CarrotEnoteType::PAYMENT, + // no internal messages for legacy self-sends + }; + + // 1 subaddress selfsend + tools::add_element(tx_proposal.explicit_selfsend_proposals) = CarrotPaymentProposalSelfSendV1{ + .destination_address_spend_pubkey = acc2.first.subaddress(49, 199).address_spend_pubkey, + .amount = crypto::rand_idx(1000000), + .enote_type = CarrotEnoteType::CHANGE + }; + + // specify fee per weight + tx_proposal.fee_per_weight = 314159; + + // test + subtest_multi_account_transfer_over_transaction(tx_proposal); +} +//---------------------------------------------------------------------------------------------------------------------- From 6ff956eba147d1f789c0eaf43ea7a4b0818e0481 Mon Sep 17 00:00:00 2001 From: jeffro256 Date: Wed, 29 Jan 2025 13:58:24 -0600 Subject: [PATCH 10/15] fix ARMv7 compile --- src/carrot_impl/carrot_tx_format_utils.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/carrot_impl/carrot_tx_format_utils.cpp b/src/carrot_impl/carrot_tx_format_utils.cpp index 5c6fd7bbc34..b3d7db34650 100644 --- a/src/carrot_impl/carrot_tx_format_utils.cpp +++ b/src/carrot_impl/carrot_tx_format_utils.cpp @@ -276,7 +276,7 @@ cryptonote::transaction store_carrot_to_coinbase_transaction_v1( tx.rct_signatures.type = rct::RCTTypeNull; //input - tx.vin.emplace_back(cryptonote::txin_gen{.height = block_index}); + tx.vin.emplace_back(cryptonote::txin_gen{.height = static_cast(block_index)}); //outputs for (const CarrotCoinbaseEnoteV1 &enote : enotes) From 925703736cfebcd819b4ca1d27c0128135a6fc1a Mon Sep 17 00:00:00 2001 From: jeffro256 Date: Wed, 29 Jan 2025 15:02:49 -0600 Subject: [PATCH 11/15] optimize tests and setup for subtractable fees --- src/carrot_impl/carrot_tx_builder.cpp | 28 +- src/carrot_impl/carrot_tx_builder.h | 8 +- tests/unit_tests/carrot_impl.cpp | 433 +++++++++++++++----------- 3 files changed, 273 insertions(+), 196 deletions(-) diff --git a/src/carrot_impl/carrot_tx_builder.cpp b/src/carrot_impl/carrot_tx_builder.cpp index bcb5cfe96ad..cc530c28d48 100644 --- a/src/carrot_impl/carrot_tx_builder.cpp +++ b/src/carrot_impl/carrot_tx_builder.cpp @@ -97,8 +97,10 @@ void make_unsigned_transaction(std::vector &&normal_pay const view_incoming_key_device *k_view_dev, const crypto::public_key &account_spend_pubkey, cryptonote::transaction &tx_out, - crypto::secret_key &amount_blinding_factor_sum_out) + std::vector &output_amount_blinding_factors_out) { + output_amount_blinding_factors_out.clear(); + // add an additional payment proposal to satisfy scanning/consensus rules, if applicable append_additional_payment_proposal_if_necessary(normal_payment_proposals, selfsend_payment_proposals, @@ -167,11 +169,15 @@ void make_unsigned_transaction(std::vector &&normal_pay output_enote_proposals, encrypted_payment_id); - // collect enotes + // collect enotes and blinding factors std::vector enotes; enotes.reserve(output_enote_proposals.size()); + output_amount_blinding_factors_out.reserve(output_enote_proposals.size()); for (const RCTOutputEnoteProposal &e : output_enote_proposals) + { enotes.push_back(e.enote); + output_amount_blinding_factors_out.push_back(e.amount_blinding_factor); + } // collect key images std::vector key_images; @@ -181,12 +187,6 @@ void make_unsigned_transaction(std::vector &&normal_pay // serialize pruned transaction tx_out = store_carrot_to_transaction_v1(enotes, key_images, fee, encrypted_payment_id); - - // calculate the sum of k_a for each output - unsigned char * const p_ka_sum = to_bytes(amount_blinding_factor_sum_out); - memcpy(p_ka_sum, rct::I.bytes, sizeof(amount_blinding_factor_sum_out)); // = 1 - for (const RCTOutputEnoteProposal &e : output_enote_proposals) - sc_add(p_ka_sum, p_ka_sum, to_bytes(e.amount_blinding_factor)); // += k_a } //------------------------------------------------------------------------------------------------------------------- void make_unsigned_transaction_transfer_subtractable( @@ -200,7 +200,7 @@ void make_unsigned_transaction_transfer_subtractable( const std::set &subtractable_normal_payment_proposals, const std::set &subtractable_selfsend_payment_proposals, cryptonote::transaction &tx_out, - crypto::secret_key &amount_blinding_factor_sum_out) + std::vector &output_amount_blinding_factors_out) { // 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 @@ -336,7 +336,7 @@ void make_unsigned_transaction_transfer_subtractable( k_view_dev, account_spend_pubkey, tx_out, - amount_blinding_factor_sum_out); + output_amount_blinding_factors_out); } //------------------------------------------------------------------------------------------------------------------- void make_unsigned_transaction_transfer( @@ -348,7 +348,7 @@ void make_unsigned_transaction_transfer( const view_incoming_key_device *k_view_dev, const crypto::public_key &account_spend_pubkey, cryptonote::transaction &tx_out, - crypto::secret_key &amount_blinding_factor_sum_out) + std::vector &output_amount_blinding_factors_out) { make_unsigned_transaction_transfer_subtractable( std::forward>(normal_payment_proposals), @@ -361,7 +361,7 @@ void make_unsigned_transaction_transfer( /*subtractable_normal_payment_proposals=*/{}, /*subtractable_selfsend_payment_proposals=*/{selfsend_payment_proposals.size()}, tx_out, - amount_blinding_factor_sum_out); + output_amount_blinding_factors_out); } //------------------------------------------------------------------------------------------------------------------- void make_unsigned_transaction_sweep( @@ -372,7 +372,7 @@ void make_unsigned_transaction_sweep( const view_incoming_key_device *k_view_dev, const crypto::public_key &account_spend_pubkey, cryptonote::transaction &tx_out, - crypto::secret_key &amount_blinding_factor_sum_out) + std::vector &output_amount_blinding_factors_out) { // initialize payment proposals list from `payment_proposal` std::vector normal_payment_proposals; @@ -438,7 +438,7 @@ void make_unsigned_transaction_sweep( k_view_dev, account_spend_pubkey, tx_out, - amount_blinding_factor_sum_out); + output_amount_blinding_factors_out); } //------------------------------------------------------------------------------------------------------------------- } //namespace carrot diff --git a/src/carrot_impl/carrot_tx_builder.h b/src/carrot_impl/carrot_tx_builder.h index 06e434f631e..d4e80d2b604 100644 --- a/src/carrot_impl/carrot_tx_builder.h +++ b/src/carrot_impl/carrot_tx_builder.h @@ -92,7 +92,7 @@ void make_unsigned_transaction(std::vector &&normal_pay const view_incoming_key_device *k_view_dev, const crypto::public_key &account_spend_pubkey, cryptonote::transaction &tx_out, - crypto::secret_key &amount_blinding_factor_sum_out); + std::vector &output_amount_blinding_factors_out); void make_unsigned_transaction_transfer_subtractable( std::vector &&normal_payment_proposals, @@ -105,7 +105,7 @@ void make_unsigned_transaction_transfer_subtractable( const std::set &subtractable_normal_payment_proposals, const std::set &subtractable_selfsend_payment_proposals, cryptonote::transaction &tx_out, - crypto::secret_key &amount_blinding_factor_sum_out); + std::vector &output_amount_blinding_factors_out); void make_unsigned_transaction_transfer( std::vector &&normal_payment_proposals, @@ -116,7 +116,7 @@ void make_unsigned_transaction_transfer( const view_incoming_key_device *k_view_dev, const crypto::public_key &account_spend_pubkey, cryptonote::transaction &tx_out, - crypto::secret_key &amount_blinding_factor_sum_out); + std::vector &output_amount_blinding_factors_out); void make_unsigned_transaction_sweep( const tools::variant &payment_proposal, @@ -126,5 +126,5 @@ void make_unsigned_transaction_sweep( const view_incoming_key_device *k_view_dev, const crypto::public_key &account_spend_pubkey, cryptonote::transaction &tx_out, - crypto::secret_key &amount_blinding_factor_sum_out); + std::vector &output_amount_blinding_factors_out); } //namespace carrot diff --git a/tests/unit_tests/carrot_impl.cpp b/tests/unit_tests/carrot_impl.cpp index 58110a8d07d..271b1c0a6e6 100644 --- a/tests/unit_tests/carrot_impl.cpp +++ b/tests/unit_tests/carrot_impl.cpp @@ -54,8 +54,8 @@ namespace { //---------------------------------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------------------------------- -static constexpr std::uint32_t MAX_SUBADDRESS_MAJOR_INDEX = 50; -static constexpr std::uint32_t MAX_SUBADDRESS_MINOR_INDEX = 200; +static constexpr std::uint32_t MAX_SUBADDRESS_MAJOR_INDEX = 5; +static constexpr std::uint32_t MAX_SUBADDRESS_MINOR_INDEX = 20; //---------------------------------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------------------------------- struct mock_carrot_or_legacy_keys @@ -77,31 +77,9 @@ struct mock_carrot_or_legacy_keys view_incoming_key_ram_borrowed_device k_view_dev; view_balance_secret_ram_borrowed_device s_view_balance_dev; - mock_carrot_or_legacy_keys(): k_view_dev(k_view), s_view_balance_dev(s_view_balance) {} + std::unordered_map subaddress_map; - void generate_carrot() - { - is_carrot = true; - crypto::generate_random_bytes_thread_safe(sizeof(crypto::secret_key), to_bytes(s_master)); - make_carrot_provespend_key(s_master, k_prove_spend); - make_carrot_viewbalance_secret(s_master, s_view_balance); - make_carrot_generateimage_key(s_view_balance, k_generate_image); - make_carrot_viewincoming_key(s_view_balance, k_view); - make_carrot_generateaddress_secret(s_view_balance, s_generate_address); - make_carrot_spend_pubkey(k_generate_image, k_prove_spend, account_spend_pubkey); - account_view_pubkey = rct::rct2pk(rct::scalarmultKey(rct::pk2rct(account_spend_pubkey), - rct::sk2rct(k_view))); - main_address_view_pubkey = rct::rct2pk(rct::scalarmultBase(rct::sk2rct(k_view))); - } - - void generate_legacy() - { - is_carrot = false; - legacy_acb.generate(); - k_view = legacy_acb.get_keys().m_view_secret_key; - account_spend_pubkey = legacy_acb.get_keys().m_account_address.m_spend_public_key; - main_address_view_pubkey = legacy_acb.get_keys().m_account_address.m_view_public_key; - } + mock_carrot_or_legacy_keys(): k_view_dev(k_view), s_view_balance_dev(s_view_balance) {} const view_balance_secret_device* get_view_balance_device() const { @@ -225,24 +203,58 @@ struct mock_carrot_or_legacy_keys crypto::secret_key &address_privkey_g_out, crypto::secret_key &address_privkey_t_out) const { - // shittier version of a subaddress lookahead table + const auto it = subaddress_map.find(address_spend_pubkey); + if (it == subaddress_map.cend()) + return false; + + crypto::public_key recomputed_address_spend_pubkey; + opening_for_subaddress(it->second.major, + it->second.minor, + address_privkey_g_out, + address_privkey_t_out, + recomputed_address_spend_pubkey); + + return address_spend_pubkey == recomputed_address_spend_pubkey; + } - for (major_index_out = 0; major_index_out < max_major_index; ++major_index_out) + void generate_subaddress_map() + { + for (uint32_t major_index = 0; major_index < MAX_SUBADDRESS_MAJOR_INDEX; ++major_index) { - for (minor_index_out = 0; minor_index_out < max_minor_index; ++minor_index_out) + for (uint32_t minor_index = 0; minor_index < MAX_SUBADDRESS_MINOR_INDEX; ++minor_index) { - crypto::public_key recomputed_address_spend_pubkey; - opening_for_subaddress(major_index_out, - minor_index_out, - address_privkey_g_out, - address_privkey_t_out, - recomputed_address_spend_pubkey); - if (address_spend_pubkey == recomputed_address_spend_pubkey) - return true; + const CarrotDestinationV1 addr = subaddress(major_index, minor_index); + subaddress_map.insert({addr.address_spend_pubkey, {major_index, minor_index}}); } } + } - return false; + void generate_carrot() + { + is_carrot = true; + crypto::generate_random_bytes_thread_safe(sizeof(crypto::secret_key), to_bytes(s_master)); + make_carrot_provespend_key(s_master, k_prove_spend); + make_carrot_viewbalance_secret(s_master, s_view_balance); + make_carrot_generateimage_key(s_view_balance, k_generate_image); + make_carrot_viewincoming_key(s_view_balance, k_view); + make_carrot_generateaddress_secret(s_view_balance, s_generate_address); + make_carrot_spend_pubkey(k_generate_image, k_prove_spend, account_spend_pubkey); + account_view_pubkey = rct::rct2pk(rct::scalarmultKey(rct::pk2rct(account_spend_pubkey), + rct::sk2rct(k_view))); + main_address_view_pubkey = rct::rct2pk(rct::scalarmultBase(rct::sk2rct(k_view))); + + generate_subaddress_map(); + } + + void generate_legacy() + { + is_carrot = false; + legacy_acb.generate(); + k_view = legacy_acb.get_keys().m_view_secret_key; + account_spend_pubkey = legacy_acb.get_keys().m_account_address.m_spend_public_key; + main_address_view_pubkey = legacy_acb.get_keys().m_account_address.m_view_public_key; + + generate_subaddress_map(); } }; //---------------------------------------------------------------------------------------------------------------------- @@ -349,42 +361,50 @@ static void unittest_scan_enote_set_multi_account(const std::vector normal_payment_proposal.amount) + return false; + + if (normal_payment_proposal.amount - scan_res.amount > allowed_fee_margin_opt) return false; - + if (scan_res.enote_type != CarrotEnoteType::PAYMENT) return false; - + if (scan_res.payment_id != normal_payment_proposal.destination.payment_id) return false; - + if (scan_res.internal_message != janus_anchor_t{}) return false; - + return true; } //---------------------------------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------------------------------- static bool compare_scan_result(const unittest_carrot_scan_result_t &scan_res, - const CarrotPaymentProposalSelfSendV1 &selfsend_payment_proposal) + const CarrotPaymentProposalSelfSendV1 &selfsend_payment_proposal, + const rct::xmr_amount allowed_fee_margin_opt = 0) { if (scan_res.address_spend_pubkey != selfsend_payment_proposal.destination_address_spend_pubkey) return false; - - if (scan_res.amount != selfsend_payment_proposal.amount) + + if (scan_res.amount > selfsend_payment_proposal.amount) return false; - + + if (selfsend_payment_proposal.amount - scan_res.amount > allowed_fee_margin_opt) + return false; + if (scan_res.enote_type != selfsend_payment_proposal.enote_type) return false; - + if (scan_res.payment_id != null_payment_id) return false; - + if (scan_res.internal_message != selfsend_payment_proposal.internal_message.value_or(janus_anchor_t{})) return false; @@ -394,25 +414,41 @@ static bool compare_scan_result(const unittest_carrot_scan_result_t &scan_res, //---------------------------------------------------------------------------------------------------------------------- struct unittest_transaction_proposal { - using per_account = std::pair>; + using per_payment_proposal = std::pair; + using per_ss_payment_proposal = std::pair; + using per_account = std::pair>; using per_input = std::pair; std::vector per_account_payments; - std::vector explicit_selfsend_proposals; + std::vector explicit_selfsend_proposals; size_t self_sender_index{0}; rct::xmr_amount fee_per_weight; void get_flattened_payment_proposals(std::vector &normal_payment_proposals_out, - std::vector &selfsend_payment_proposals_out) const + std::vector &selfsend_payment_proposals_out, + std::set &subtractable_normal_payment_proposals, + std::set &subtractable_selfsend_payment_proposals) const { + size_t norm_idx = 0; for (const per_account &pa : per_account_payments) { - normal_payment_proposals_out.insert(normal_payment_proposals_out.end(), - pa.second.cbegin(), - pa.second.cend()); + for (const per_payment_proposal &ppp : pa.second) + { + normal_payment_proposals_out.push_back(ppp.first); + if (ppp.second) + subtractable_normal_payment_proposals.insert(norm_idx); + + norm_idx++; + } } - selfsend_payment_proposals_out = explicit_selfsend_proposals; + for (size_t ss_idx = 0; ss_idx < explicit_selfsend_proposals.size(); ++ss_idx) + { + const per_ss_payment_proposal &pspp = explicit_selfsend_proposals.at(ss_idx); + selfsend_payment_proposals_out.push_back(pspp.first); + if (pspp.second) + subtractable_selfsend_payment_proposals.insert(ss_idx); + } } }; //---------------------------------------------------------------------------------------------------------------------- @@ -455,7 +491,12 @@ static void subtest_multi_account_transfer_over_transaction(const unittest_trans // get payment proposals std::vector normal_payment_proposals; std::vector selfsend_payment_proposals; - tx_proposal.get_flattened_payment_proposals(normal_payment_proposals, selfsend_payment_proposals); + std::set subtractable_normal_payment_proposals; + std::set subtractable_selfsend_payment_proposals; + tx_proposal.get_flattened_payment_proposals(normal_payment_proposals, + selfsend_payment_proposals, + subtractable_normal_payment_proposals, + subtractable_selfsend_payment_proposals); // get self-sender account const mock_carrot_or_legacy_keys &ss_keys = @@ -463,7 +504,7 @@ static void subtest_multi_account_transfer_over_transaction(const unittest_trans // make unsigned transaction cryptonote::transaction tx; - crypto::secret_key amount_blinding_factor_sum; + std::vector output_amount_blinding_factors; make_unsigned_transaction_transfer(std::vector(normal_payment_proposals), std::vector(selfsend_payment_proposals), tx_proposal.fee_per_weight, @@ -472,7 +513,14 @@ static void subtest_multi_account_transfer_over_transaction(const unittest_trans &ss_keys.k_view_dev, ss_keys.account_spend_pubkey, tx, - amount_blinding_factor_sum); + output_amount_blinding_factors); + + // calculate acceptable fee margin between proposed amount and actual amount for subtractable outputs + const size_t num_subtractable = subtractable_normal_payment_proposals.size() + + subtractable_selfsend_payment_proposals.size(); + const rct::xmr_amount acceptable_fee_margin = num_subtractable + ? (tx.rct_signatures.txnFee / num_subtractable) + 1 + : 0; // load carrot stuff from tx std::vector parsed_enotes; @@ -497,102 +545,131 @@ static void subtest_multi_account_transfer_over_transaction(const unittest_trans epee::to_span(accounts), scan_results); - // check that the scan results for each account match the corresponding payment proposals for each account - // also check that the accounts can each open their corresponding onetime outut pubkeys + // check that the scan results for each *normal* account match the corresponding payment + // proposals for each account. also check that the accounts can each open their corresponding + // onetime outut pubkeys ASSERT_EQ(scan_results.size(), accounts.size()); + // for each normal account... for (size_t account_idx = 0; account_idx < accounts.size(); ++account_idx) { - const std::vector &account_scan_results = scan_results.at(account_idx); + // skip self-sender account if (account_idx == tx_proposal.self_sender_index) + continue; + + const std::vector &account_scan_results = scan_results.at(account_idx); + const auto &account_payment_proposals = tx_proposal.per_account_payments.at(account_idx).second; + ASSERT_EQ(account_payment_proposals.size(), account_scan_results.size()); + std::set matched_payment_proposals; + + // for each scan result assigned to this account... + for (const unittest_carrot_scan_result_t &single_scan_res : account_scan_results) { - ASSERT_EQ(selfsend_payment_proposals.size() + 1, account_scan_results.size()); - std::set matched_payment_proposals; - const unittest_carrot_scan_result_t* implicit_change_scan_res = nullptr; - for (const unittest_carrot_scan_result_t &single_scan_res : account_scan_results) + // for each normal payment proposal to this account... + for (size_t norm_prop_idx = 0; norm_prop_idx < account_payment_proposals.size(); ++norm_prop_idx) { - bool matched_payment = false; - for (size_t ss_prop_idx = 0; ss_prop_idx < selfsend_payment_proposals.size(); ++ss_prop_idx) + // calculate acceptable loss from fee subtraction + const CarrotPaymentProposalV1 &account_payment_proposal = account_payment_proposals.at(norm_prop_idx).first; + const bool is_subtractable = subtractable_normal_payment_proposals.count(norm_prop_idx); + const rct::xmr_amount acceptable_fee_margin_for_proposal = is_subtractable ? acceptable_fee_margin : 0; + + // if the scan result matches the payment proposal... + if (compare_scan_result(single_scan_res, account_payment_proposal, acceptable_fee_margin_for_proposal)) { - const CarrotPaymentProposalSelfSendV1 &account_payment_proposal = selfsend_payment_proposals.at(ss_prop_idx); - if (compare_scan_result(single_scan_res, account_payment_proposal)) + // try searching for subaddress opening + crypto::secret_key address_privkey_g; + crypto::secret_key address_privkey_t; + uint32_t _1{}, _2{}; + EXPECT_TRUE(accounts.at(account_idx)->try_searching_for_opening_for_subaddress( + single_scan_res.address_spend_pubkey, + MAX_SUBADDRESS_MAJOR_INDEX, + MAX_SUBADDRESS_MINOR_INDEX, + _1, + _2, + address_privkey_g, + address_privkey_t)); + + // try opening Ko + EXPECT_TRUE(can_open_fcmp_onetime_address(address_privkey_g, + address_privkey_t, + single_scan_res.sender_extension_g, + single_scan_res.sender_extension_t, + parsed_enotes.at(single_scan_res.output_index).onetime_address)); + + // if this payment proposal isn't already marked as scanned, mark as scanned + if (!matched_payment_proposals.count(norm_prop_idx)) { - crypto::secret_key address_privkey_g; - crypto::secret_key address_privkey_t; - uint32_t _1{}, _2{}; - EXPECT_TRUE(accounts.at(account_idx)->try_searching_for_opening_for_subaddress( - single_scan_res.address_spend_pubkey, - MAX_SUBADDRESS_MAJOR_INDEX, - MAX_SUBADDRESS_MINOR_INDEX, - _1, - _2, - address_privkey_g, - address_privkey_t)); - - EXPECT_TRUE(can_open_fcmp_onetime_address(address_privkey_g, - address_privkey_t, - single_scan_res.sender_extension_g, - single_scan_res.sender_extension_t, - parsed_enotes.at(single_scan_res.output_index).onetime_address)); - - if (!matched_payment_proposals.count(ss_prop_idx)) - { - matched_payment = true; - matched_payment_proposals.insert(ss_prop_idx); - break; - } + matched_payment_proposals.insert(norm_prop_idx); + break; } } - if (!matched_payment) - { - EXPECT_EQ(nullptr, implicit_change_scan_res); // only one non-matched scan result is allowed - implicit_change_scan_res = &single_scan_res; - } } - EXPECT_EQ(selfsend_payment_proposals.size(), matched_payment_proposals.size()); - EXPECT_NE(nullptr, implicit_change_scan_res); - // @TODO: assert properties of `implicit_change_scan_res` } - else + // check that the number of matched payment proposals is equal to the original number of them + // doing it this way checks that the same payment proposal isn't marked twice and another left out + EXPECT_EQ(account_payment_proposals.size(), matched_payment_proposals.size()); + } + + // check that the scan results for the selfsend account match the corresponding payment + // proposals. also check that the accounts can each open their corresponding onetime outut pubkeys + const std::vector &account_scan_results = scan_results.at(tx_proposal.self_sender_index); + ASSERT_EQ(selfsend_payment_proposals.size() + 1, account_scan_results.size()); + std::set matched_payment_proposals; + const unittest_carrot_scan_result_t* implicit_change_scan_res = nullptr; + // for each scan result assigned to the self-sender account... + for (const unittest_carrot_scan_result_t &single_scan_res : account_scan_results) + { + bool matched_payment = false; + // for each self-send payment proposal... + for (size_t ss_prop_idx = 0; ss_prop_idx < selfsend_payment_proposals.size(); ++ss_prop_idx) { - const std::vector &account_payment_proposals = tx_proposal.per_account_payments.at(account_idx).second; - ASSERT_EQ(account_payment_proposals.size(), account_scan_results.size()); - std::set matched_payment_proposals; - for (const unittest_carrot_scan_result_t &single_scan_res : account_scan_results) + // calculate acceptable loss from fee subtraction + const CarrotPaymentProposalSelfSendV1 &account_payment_proposal = selfsend_payment_proposals.at(ss_prop_idx); + const bool is_subtractable = subtractable_selfsend_payment_proposals.count(ss_prop_idx); + const rct::xmr_amount acceptable_fee_margin_for_proposal = is_subtractable ? acceptable_fee_margin : 0; + + // if the scan result matches the payment proposal... + if (compare_scan_result(single_scan_res, account_payment_proposal, acceptable_fee_margin_for_proposal)) { - for (size_t norm_prop_idx = 0; norm_prop_idx < account_payment_proposals.size(); ++norm_prop_idx) + // try searching for subaddress opening + crypto::secret_key address_privkey_g; + crypto::secret_key address_privkey_t; + uint32_t _1{}, _2{}; + EXPECT_TRUE(ss_keys.try_searching_for_opening_for_subaddress( + single_scan_res.address_spend_pubkey, + MAX_SUBADDRESS_MAJOR_INDEX, + MAX_SUBADDRESS_MINOR_INDEX, + _1, + _2, + address_privkey_g, + address_privkey_t)); + + // try opening Ko + EXPECT_TRUE(can_open_fcmp_onetime_address(address_privkey_g, + address_privkey_t, + single_scan_res.sender_extension_g, + single_scan_res.sender_extension_t, + parsed_enotes.at(single_scan_res.output_index).onetime_address)); + + // if this payment proposal isn't already marked as scanned, mark as scanned + if (!matched_payment_proposals.count(ss_prop_idx)) { - const CarrotPaymentProposalV1 &account_payment_proposal = account_payment_proposals.at(norm_prop_idx); - if (compare_scan_result(single_scan_res, account_payment_proposal)) - { - crypto::secret_key address_privkey_g; - crypto::secret_key address_privkey_t; - uint32_t _1{}, _2{}; - EXPECT_TRUE(accounts.at(account_idx)->try_searching_for_opening_for_subaddress( - single_scan_res.address_spend_pubkey, - MAX_SUBADDRESS_MAJOR_INDEX, - MAX_SUBADDRESS_MINOR_INDEX, - _1, - _2, - address_privkey_g, - address_privkey_t)); - - EXPECT_TRUE(can_open_fcmp_onetime_address(address_privkey_g, - address_privkey_t, - single_scan_res.sender_extension_g, - single_scan_res.sender_extension_t, - parsed_enotes.at(single_scan_res.output_index).onetime_address)); - - if (!matched_payment_proposals.count(norm_prop_idx)) - { - matched_payment_proposals.insert(norm_prop_idx); - break; - } - } + matched_payment = true; + matched_payment_proposals.insert(ss_prop_idx); + break; } } - EXPECT_EQ(account_payment_proposals.size(), matched_payment_proposals.size()); + } + + // if this scan result has no matching payment... + if (!matched_payment) + { + EXPECT_EQ(nullptr, implicit_change_scan_res); // only one non-matched scan result is allowed + implicit_change_scan_res = &single_scan_res; // save the implicit change scan result for later } } + EXPECT_EQ(selfsend_payment_proposals.size(), matched_payment_proposals.size()); + EXPECT_NE(nullptr, implicit_change_scan_res); + // @TODO: assert properties of `implicit_change_scan_res` } //---------------------------------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------------------------------- @@ -611,7 +688,7 @@ TEST(carrot_impl, multi_account_transfer_over_transaction_1) acc1.generate_carrot(); // 1 normal payment - CarrotPaymentProposalV1 &normal_payment_proposal = tools::add_element( tx_proposal.per_account_payments[0].second); + CarrotPaymentProposalV1 &normal_payment_proposal = tools::add_element( tx_proposal.per_account_payments[0].second).first; normal_payment_proposal = CarrotPaymentProposalV1{ .destination = acc0.cryptonote_address(), .amount = crypto::rand_idx((rct::xmr_amount) 1ull << 63), @@ -650,21 +727,21 @@ TEST(carrot_impl, multi_account_transfer_over_transaction_2) tx_proposal.self_sender_index = 2; // 1 subaddress payment - tools::add_element(acc0.second) = CarrotPaymentProposalV1{ - .destination = acc0.first.subaddress(17, 18), + tools::add_element(acc0.second).first = CarrotPaymentProposalV1{ + .destination = acc0.first.subaddress(2, 3), .amount = crypto::rand_idx(1000000), .randomness = gen_janus_anchor() }; // 1 main address payment - tools::add_element(acc1.second) = CarrotPaymentProposalV1{ + tools::add_element(acc1.second).first = CarrotPaymentProposalV1{ .destination = acc1.first.cryptonote_address(), .amount = crypto::rand_idx(1000000), .randomness = gen_janus_anchor() }; // 1 integrated address payment - tools::add_element(acc3.second) = CarrotPaymentProposalV1{ + tools::add_element(acc3.second).first = CarrotPaymentProposalV1{ .destination = acc3.first.cryptonote_address(gen_payment_id()), .amount = crypto::rand_idx(1000000), .randomness = gen_janus_anchor() @@ -699,25 +776,25 @@ TEST(carrot_impl, multi_account_transfer_over_transaction_3) tx_proposal.self_sender_index = 2; // 2 subaddress payment - tools::add_element(acc0.second) = CarrotPaymentProposalV1{ - .destination = acc0.first.subaddress(17, 18), + tools::add_element(acc0.second).first = CarrotPaymentProposalV1{ + .destination = acc0.first.subaddress(2, 3), .amount = crypto::rand_idx(1000000), .randomness = gen_janus_anchor() }; acc0.second.push_back(acc0.second.front()); - acc0.second.back().randomness = gen_janus_anchor(); //mangle anchor_norm + acc0.second.back().first.randomness = gen_janus_anchor(); //mangle anchor_norm // 2 main address payment - tools::add_element(acc1.second) = CarrotPaymentProposalV1{ + tools::add_element(acc1.second).first = CarrotPaymentProposalV1{ .destination = acc1.first.cryptonote_address(), .amount = crypto::rand_idx(1000000), .randomness = gen_janus_anchor() }; acc1.second.push_back(acc1.second.front()); - acc1.second.back().randomness = gen_janus_anchor(); //mangle anchor_norm + acc1.second.back().first.randomness = gen_janus_anchor(); //mangle anchor_norm // 1 integrated address payment - tools::add_element(acc3.second) = CarrotPaymentProposalV1{ + tools::add_element(acc3.second).first = CarrotPaymentProposalV1{ .destination = acc3.first.cryptonote_address(gen_payment_id()), .amount = crypto::rand_idx(1000000), .randomness = gen_janus_anchor() @@ -752,32 +829,32 @@ TEST(carrot_impl, multi_account_transfer_over_transaction_4) tx_proposal.self_sender_index = 2; // 2 subaddress payment - tools::add_element(acc0.second) = CarrotPaymentProposalV1{ - .destination = acc0.first.subaddress(17, 18), + tools::add_element(acc0.second).first = CarrotPaymentProposalV1{ + .destination = acc0.first.subaddress(2, 3), .amount = crypto::rand_idx(1000000), .randomness = gen_janus_anchor() }; acc0.second.push_back(acc0.second.front()); - acc0.second.back().randomness = gen_janus_anchor(); //mangle anchor_norm + acc0.second.back().first.randomness = gen_janus_anchor(); //mangle anchor_norm // 2 main address payment - tools::add_element(acc1.second) = CarrotPaymentProposalV1{ + tools::add_element(acc1.second).first = CarrotPaymentProposalV1{ .destination = acc1.first.cryptonote_address(), .amount = crypto::rand_idx(1000000), .randomness = gen_janus_anchor() }; acc1.second.push_back(acc1.second.front()); - acc1.second.back().randomness = gen_janus_anchor(); //mangle anchor_norm + acc1.second.back().first.randomness = gen_janus_anchor(); //mangle anchor_norm // 1 integrated address payment - tools::add_element(acc3.second) = CarrotPaymentProposalV1{ + tools::add_element(acc3.second).first = CarrotPaymentProposalV1{ .destination = acc3.first.cryptonote_address(gen_payment_id()), .amount = crypto::rand_idx(1000000), .randomness = gen_janus_anchor() }; // 1 main address selfsend - tools::add_element(tx_proposal.explicit_selfsend_proposals) = CarrotPaymentProposalSelfSendV1{ + tools::add_element(tx_proposal.explicit_selfsend_proposals).first = CarrotPaymentProposalSelfSendV1{ .destination_address_spend_pubkey = acc2.first.account_spend_pubkey, .amount = crypto::rand_idx(1000000), .enote_type = CarrotEnoteType::PAYMENT, @@ -785,8 +862,8 @@ TEST(carrot_impl, multi_account_transfer_over_transaction_4) }; // 1 subaddress selfsend - tools::add_element(tx_proposal.explicit_selfsend_proposals) = CarrotPaymentProposalSelfSendV1{ - .destination_address_spend_pubkey = acc2.first.subaddress(49, 199).address_spend_pubkey, + tools::add_element(tx_proposal.explicit_selfsend_proposals).first = CarrotPaymentProposalSelfSendV1{ + .destination_address_spend_pubkey = acc2.first.subaddress(4, 19).address_spend_pubkey, .amount = crypto::rand_idx(1000000), .enote_type = CarrotEnoteType::CHANGE }; @@ -813,7 +890,7 @@ TEST(carrot_impl, multi_account_transfer_over_transaction_5) acc1.generate_legacy(); // 1 normal payment - CarrotPaymentProposalV1 &normal_payment_proposal = tools::add_element( tx_proposal.per_account_payments[0].second); + CarrotPaymentProposalV1 &normal_payment_proposal = tools::add_element( tx_proposal.per_account_payments[0].second).first; normal_payment_proposal = CarrotPaymentProposalV1{ .destination = acc0.cryptonote_address(), .amount = crypto::rand_idx((rct::xmr_amount) 1ull << 63), @@ -852,21 +929,21 @@ TEST(carrot_impl, multi_account_transfer_over_transaction_6) tx_proposal.self_sender_index = 2; // 1 subaddress payment - tools::add_element(acc0.second) = CarrotPaymentProposalV1{ - .destination = acc0.first.subaddress(17, 18), + tools::add_element(acc0.second).first = CarrotPaymentProposalV1{ + .destination = acc0.first.subaddress(2, 3), .amount = crypto::rand_idx(1000000), .randomness = gen_janus_anchor() }; // 1 main address payment - tools::add_element(acc1.second) = CarrotPaymentProposalV1{ + tools::add_element(acc1.second).first = CarrotPaymentProposalV1{ .destination = acc1.first.cryptonote_address(), .amount = crypto::rand_idx(1000000), .randomness = gen_janus_anchor() }; // 1 integrated address payment - tools::add_element(acc3.second) = CarrotPaymentProposalV1{ + tools::add_element(acc3.second).first = CarrotPaymentProposalV1{ .destination = acc3.first.cryptonote_address(gen_payment_id()), .amount = crypto::rand_idx(1000000), .randomness = gen_janus_anchor() @@ -901,25 +978,25 @@ TEST(carrot_impl, multi_account_transfer_over_transaction_7) tx_proposal.self_sender_index = 2; // 2 subaddress payment - tools::add_element(acc0.second) = CarrotPaymentProposalV1{ - .destination = acc0.first.subaddress(17, 18), + tools::add_element(acc0.second).first = CarrotPaymentProposalV1{ + .destination = acc0.first.subaddress(2, 3), .amount = crypto::rand_idx(1000000), .randomness = gen_janus_anchor() }; acc0.second.push_back(acc0.second.front()); - acc0.second.back().randomness = gen_janus_anchor(); //mangle anchor_norm + acc0.second.back().first.randomness = gen_janus_anchor(); //mangle anchor_norm // 2 main address payment - tools::add_element(acc1.second) = CarrotPaymentProposalV1{ + tools::add_element(acc1.second).first = CarrotPaymentProposalV1{ .destination = acc1.first.cryptonote_address(), .amount = crypto::rand_idx(1000000), .randomness = gen_janus_anchor() }; acc1.second.push_back(acc1.second.front()); - acc1.second.back().randomness = gen_janus_anchor(); //mangle anchor_norm + acc1.second.back().first.randomness = gen_janus_anchor(); //mangle anchor_norm // 1 integrated address payment - tools::add_element(acc3.second) = CarrotPaymentProposalV1{ + tools::add_element(acc3.second).first = CarrotPaymentProposalV1{ .destination = acc3.first.cryptonote_address(gen_payment_id()), .amount = crypto::rand_idx(1000000), .randomness = gen_janus_anchor() @@ -954,32 +1031,32 @@ TEST(carrot_impl, multi_account_transfer_over_transaction_8) tx_proposal.self_sender_index = 2; // 2 subaddress payment - tools::add_element(acc0.second) = CarrotPaymentProposalV1{ - .destination = acc0.first.subaddress(17, 18), + tools::add_element(acc0.second).first = CarrotPaymentProposalV1{ + .destination = acc0.first.subaddress(2, 3), .amount = crypto::rand_idx(1000000), .randomness = gen_janus_anchor() }; acc0.second.push_back(acc0.second.front()); - acc0.second.back().randomness = gen_janus_anchor(); //mangle anchor_norm + acc0.second.back().first.randomness = gen_janus_anchor(); //mangle anchor_norm // 2 main address payment - tools::add_element(acc1.second) = CarrotPaymentProposalV1{ + tools::add_element(acc1.second).first = CarrotPaymentProposalV1{ .destination = acc1.first.cryptonote_address(), .amount = crypto::rand_idx(1000000), .randomness = gen_janus_anchor() }; acc1.second.push_back(acc1.second.front()); - acc1.second.back().randomness = gen_janus_anchor(); //mangle anchor_norm + acc1.second.back().first.randomness = gen_janus_anchor(); //mangle anchor_norm // 1 integrated address payment - tools::add_element(acc3.second) = CarrotPaymentProposalV1{ + tools::add_element(acc3.second).first = CarrotPaymentProposalV1{ .destination = acc3.first.cryptonote_address(gen_payment_id()), .amount = crypto::rand_idx(1000000), .randomness = gen_janus_anchor() }; // 1 main address selfsend - tools::add_element(tx_proposal.explicit_selfsend_proposals) = CarrotPaymentProposalSelfSendV1{ + tools::add_element(tx_proposal.explicit_selfsend_proposals).first = CarrotPaymentProposalSelfSendV1{ .destination_address_spend_pubkey = acc2.first.account_spend_pubkey, .amount = crypto::rand_idx(1000000), .enote_type = CarrotEnoteType::PAYMENT, @@ -987,8 +1064,8 @@ TEST(carrot_impl, multi_account_transfer_over_transaction_8) }; // 1 subaddress selfsend - tools::add_element(tx_proposal.explicit_selfsend_proposals) = CarrotPaymentProposalSelfSendV1{ - .destination_address_spend_pubkey = acc2.first.subaddress(49, 199).address_spend_pubkey, + tools::add_element(tx_proposal.explicit_selfsend_proposals).first = CarrotPaymentProposalSelfSendV1{ + .destination_address_spend_pubkey = acc2.first.subaddress(4, 19).address_spend_pubkey, .amount = crypto::rand_idx(1000000), .enote_type = CarrotEnoteType::CHANGE }; From afe73fd22488cc3317a6cb8b3b83764f1dd4e0b7 Mon Sep 17 00:00:00 2001 From: jeffro256 Date: Wed, 29 Jan 2025 15:28:26 -0600 Subject: [PATCH 12/15] test subtractable fees --- tests/unit_tests/carrot_impl.cpp | 490 +++++++++++++++++++++++++++++++ 1 file changed, 490 insertions(+) diff --git a/tests/unit_tests/carrot_impl.cpp b/tests/unit_tests/carrot_impl.cpp index 271b1c0a6e6..ad49e571013 100644 --- a/tests/unit_tests/carrot_impl.cpp +++ b/tests/unit_tests/carrot_impl.cpp @@ -1077,3 +1077,493 @@ TEST(carrot_impl, multi_account_transfer_over_transaction_8) subtest_multi_account_transfer_over_transaction(tx_proposal); } //---------------------------------------------------------------------------------------------------------------------- + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_impl, multi_account_transfer_over_transaction_9) +{ + // two accounts, both carrot + // 1/2 tx + // 1 normal payment to main address + // 0 explicit selfsend payments + // subtractable + + unittest_transaction_proposal tx_proposal; + tx_proposal.per_account_payments.resize(2); + mock_carrot_or_legacy_keys &acc0 = tx_proposal.per_account_payments[0].first; + mock_carrot_or_legacy_keys &acc1 = tx_proposal.per_account_payments[1].first; + acc0.generate_carrot(); + acc1.generate_carrot(); + + // 1 normal payment (subtractable) + CarrotPaymentProposalV1 &normal_payment_proposal = tools::add_element( tx_proposal.per_account_payments[0].second).first; + normal_payment_proposal = CarrotPaymentProposalV1{ + .destination = acc0.cryptonote_address(), + .amount = crypto::rand_idx((rct::xmr_amount) 1ull << 63), + .randomness = gen_janus_anchor() + }; + tx_proposal.per_account_payments[0].second.back().second = true; + + // specify self-sender + tx_proposal.self_sender_index = 1; + + // specify fee per weight + tx_proposal.fee_per_weight = 20250510; + + // test + subtest_multi_account_transfer_over_transaction(tx_proposal); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_impl, multi_account_transfer_over_transaction_10) +{ + // four accounts, all carrot + // 1/4 tx + // 1 normal payment to main address, integrated address, and subaddress each + // 0 explicit selfsend payments + // subaddress and integrated address are subtractable + + unittest_transaction_proposal tx_proposal; + tx_proposal.per_account_payments.resize(4); + auto &acc0 = tx_proposal.per_account_payments[0]; + auto &acc1 = tx_proposal.per_account_payments[1]; + auto &acc2 = tx_proposal.per_account_payments[2]; + auto &acc3 = tx_proposal.per_account_payments[3]; + acc0.first.generate_carrot(); + acc1.first.generate_carrot(); + acc2.first.generate_carrot(); + acc3.first.generate_carrot(); + + // specify self-sender + tx_proposal.self_sender_index = 2; + + // 1 subaddress payment (subtractable) + tools::add_element(acc0.second) = {CarrotPaymentProposalV1{ + .destination = acc0.first.subaddress(2, 3), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }, true}; + + // 1 main address payment + tools::add_element(acc1.second).first = CarrotPaymentProposalV1{ + .destination = acc1.first.cryptonote_address(), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + + // 1 integrated address payment + tools::add_element(acc3.second) = {CarrotPaymentProposalV1{ + .destination = acc3.first.cryptonote_address(gen_payment_id()), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }, true}; + + // specify fee per weight + tx_proposal.fee_per_weight = 314159; + + // test + subtest_multi_account_transfer_over_transaction(tx_proposal); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_impl, multi_account_transfer_over_transaction_11) +{ + // four accounts, all carrot + // 1/6 tx + // 2 normal payment to main address, 1 integrated address, and 2 subaddress, each copied except integrated + // 0 explicit selfsend payments + // 1 main and 1 subaddress is subtractable + + unittest_transaction_proposal tx_proposal; + tx_proposal.per_account_payments.resize(4); + auto &acc0 = tx_proposal.per_account_payments[0]; + auto &acc1 = tx_proposal.per_account_payments[1]; + auto &acc2 = tx_proposal.per_account_payments[2]; + auto &acc3 = tx_proposal.per_account_payments[3]; + acc0.first.generate_carrot(); + acc1.first.generate_carrot(); + acc2.first.generate_carrot(); + acc3.first.generate_carrot(); + + // specify self-sender + tx_proposal.self_sender_index = 2; + + // 2 subaddress payment + tools::add_element(acc0.second).first = CarrotPaymentProposalV1{ + .destination = acc0.first.subaddress(2, 3), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + acc0.second.push_back(acc0.second.front()); + acc0.second.back().first.randomness = gen_janus_anchor(); //mangle anchor_norm + acc0.second.back().second = true; //set copy as subtractable + + // 2 main address payment + tools::add_element(acc1.second).first = CarrotPaymentProposalV1{ + .destination = acc1.first.cryptonote_address(), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + acc1.second.push_back(acc1.second.front()); + acc1.second.back().first.randomness = gen_janus_anchor(); //mangle anchor_norm + acc1.second.back().second = true; //set copy as subtractable + + // 1 integrated address payment + tools::add_element(acc3.second).first = CarrotPaymentProposalV1{ + .destination = acc3.first.cryptonote_address(gen_payment_id()), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + + // specify fee per weight + tx_proposal.fee_per_weight = 314159; + + // test + subtest_multi_account_transfer_over_transaction(tx_proposal); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_impl, multi_account_transfer_over_transaction_12) +{ + // four accounts, all carrot + // 1/8 tx + // 2 normal payment to main address, 1 integrated address, and 2 subaddress, each copied except integrated + // 2 explicit selfsend payments: 1 main address destination, 1 subaddress destination + // 1 normal main address, 1 integrated, and 1 self-send subaddress is subtractable + + unittest_transaction_proposal tx_proposal; + tx_proposal.per_account_payments.resize(4); + auto &acc0 = tx_proposal.per_account_payments[0]; + auto &acc1 = tx_proposal.per_account_payments[1]; + auto &acc2 = tx_proposal.per_account_payments[2]; + auto &acc3 = tx_proposal.per_account_payments[3]; + acc0.first.generate_carrot(); + acc1.first.generate_carrot(); + acc2.first.generate_carrot(); + acc3.first.generate_carrot(); + + // specify self-sender + tx_proposal.self_sender_index = 2; + + // 2 subaddress payment (1 subtractable) + tools::add_element(acc0.second) = {CarrotPaymentProposalV1{ + .destination = acc0.first.subaddress(2, 3), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }, true}; + acc0.second.push_back(acc0.second.front()); + acc0.second.back().first.randomness = gen_janus_anchor(); //mangle anchor_norm + acc0.second.back().second = false; //set not subtractable, first already is + + // 2 main address payment + tools::add_element(acc1.second).first = CarrotPaymentProposalV1{ + .destination = acc1.first.cryptonote_address(), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + acc1.second.push_back(acc1.second.front()); + acc1.second.back().first.randomness = gen_janus_anchor(); //mangle anchor_norm + + // 1 integrated address payment (subtractable) + tools::add_element(acc3.second) = {CarrotPaymentProposalV1{ + .destination = acc3.first.cryptonote_address(gen_payment_id()), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }, true}; + + // 1 main address selfsend + tools::add_element(tx_proposal.explicit_selfsend_proposals).first = CarrotPaymentProposalSelfSendV1{ + .destination_address_spend_pubkey = acc2.first.account_spend_pubkey, + .amount = crypto::rand_idx(1000000), + .enote_type = CarrotEnoteType::PAYMENT, + .internal_message = gen_janus_anchor() + }; + + // 1 subaddress selfsend (subtractable) + tools::add_element(tx_proposal.explicit_selfsend_proposals) = {CarrotPaymentProposalSelfSendV1{ + .destination_address_spend_pubkey = acc2.first.subaddress(4, 19).address_spend_pubkey, + .amount = crypto::rand_idx(1000000), + .enote_type = CarrotEnoteType::CHANGE + }, true}; + + // specify fee per weight + tx_proposal.fee_per_weight = 314159; + + // test + subtest_multi_account_transfer_over_transaction(tx_proposal); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_impl, multi_account_transfer_over_transaction_13) +{ + // two accounts, both legacy + // 1/2 tx + // 1 normal payment to main address + // 0 explicit selfsend payments + // subtractable + + unittest_transaction_proposal tx_proposal; + tx_proposal.per_account_payments.resize(2); + mock_carrot_or_legacy_keys &acc0 = tx_proposal.per_account_payments[0].first; + mock_carrot_or_legacy_keys &acc1 = tx_proposal.per_account_payments[1].first; + acc0.generate_legacy(); + acc1.generate_legacy(); + + // 1 normal payment (subtractable) + CarrotPaymentProposalV1 &normal_payment_proposal = tools::add_element( tx_proposal.per_account_payments[0].second).first; + normal_payment_proposal = CarrotPaymentProposalV1{ + .destination = acc0.cryptonote_address(), + .amount = crypto::rand_idx((rct::xmr_amount) 1ull << 63), + .randomness = gen_janus_anchor() + }; + tx_proposal.per_account_payments[0].second.back().second = true; + + // specify self-sender + tx_proposal.self_sender_index = 1; + + // specify fee per weight + tx_proposal.fee_per_weight = 20250510; + + // test + subtest_multi_account_transfer_over_transaction(tx_proposal); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_impl, multi_account_transfer_over_transaction_14) +{ + // four accounts, all legacy + // 1/4 tx + // 1 normal payment to main address, integrated address, and subaddress each + // 0 explicit selfsend payments + // 1 integrated and 1 subaddress subtractable + + unittest_transaction_proposal tx_proposal; + tx_proposal.per_account_payments.resize(4); + auto &acc0 = tx_proposal.per_account_payments[0]; + auto &acc1 = tx_proposal.per_account_payments[1]; + auto &acc2 = tx_proposal.per_account_payments[2]; + auto &acc3 = tx_proposal.per_account_payments[3]; + acc0.first.generate_legacy(); + acc1.first.generate_legacy(); + acc2.first.generate_legacy(); + acc3.first.generate_legacy(); + + // specify self-sender + tx_proposal.self_sender_index = 2; + + // 1 subaddress payment (subtractable) + tools::add_element(acc0.second) = {CarrotPaymentProposalV1{ + .destination = acc0.first.subaddress(2, 3), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }, true}; + + // 1 main address payment + tools::add_element(acc1.second).first = CarrotPaymentProposalV1{ + .destination = acc1.first.cryptonote_address(), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }; + + // 1 integrated address payment (subtractable) + tools::add_element(acc3.second) = {CarrotPaymentProposalV1{ + .destination = acc3.first.cryptonote_address(gen_payment_id()), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }, true}; + + // specify fee per weight + tx_proposal.fee_per_weight = 314159; + + // test + subtest_multi_account_transfer_over_transaction(tx_proposal); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_impl, multi_account_transfer_over_transaction_15) +{ + // four accounts, all legacy + // 1/6 tx + // 2 normal payment to main address, 1 integrated address, and 2 subaddress, each copied except integrated + // 0 explicit selfsend payments + // all subtractable + + unittest_transaction_proposal tx_proposal; + tx_proposal.per_account_payments.resize(4); + auto &acc0 = tx_proposal.per_account_payments[0]; + auto &acc1 = tx_proposal.per_account_payments[1]; + auto &acc2 = tx_proposal.per_account_payments[2]; + auto &acc3 = tx_proposal.per_account_payments[3]; + acc0.first.generate_legacy(); + acc1.first.generate_legacy(); + acc2.first.generate_legacy(); + acc3.first.generate_legacy(); + + // specify self-sender + tx_proposal.self_sender_index = 2; + + // 2 subaddress payment (subtractable) + tools::add_element(acc0.second) = {CarrotPaymentProposalV1{ + .destination = acc0.first.subaddress(2, 3), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }, true}; + acc0.second.push_back(acc0.second.front()); + acc0.second.back().first.randomness = gen_janus_anchor(); //mangle anchor_norm + + // 2 main address payment (subtractable) + tools::add_element(acc1.second) = {CarrotPaymentProposalV1{ + .destination = acc1.first.cryptonote_address(), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }, true}; + acc1.second.push_back(acc1.second.front()); + acc1.second.back().first.randomness = gen_janus_anchor(); //mangle anchor_norm + + // 1 integrated address payment (subtractable) + tools::add_element(acc3.second) = {CarrotPaymentProposalV1{ + .destination = acc3.first.cryptonote_address(gen_payment_id()), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }, true}; + + // specify fee per weight + tx_proposal.fee_per_weight = 314159; + + // test + subtest_multi_account_transfer_over_transaction(tx_proposal); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_impl, multi_account_transfer_over_transaction_16) +{ + // four accounts, all legacy + // 1/8 tx + // 2 normal payment to main address, 1 integrated address, and 2 subaddress, each copied except integrated + // 2 explicit selfsend payments: 1 main address destination, 1 subaddress destination + // all subtractable + + unittest_transaction_proposal tx_proposal; + tx_proposal.per_account_payments.resize(4); + auto &acc0 = tx_proposal.per_account_payments[0]; + auto &acc1 = tx_proposal.per_account_payments[1]; + auto &acc2 = tx_proposal.per_account_payments[2]; + auto &acc3 = tx_proposal.per_account_payments[3]; + acc0.first.generate_legacy(); + acc1.first.generate_legacy(); + acc2.first.generate_legacy(); + acc3.first.generate_legacy(); + + // specify self-sender + tx_proposal.self_sender_index = 2; + + // 2 subaddress payment (subtractable) + tools::add_element(acc0.second) = {CarrotPaymentProposalV1{ + .destination = acc0.first.subaddress(2, 3), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }, true}; + acc0.second.push_back(acc0.second.front()); + acc0.second.back().first.randomness = gen_janus_anchor(); //mangle anchor_norm + + // 2 main address payment (subtractable) + tools::add_element(acc1.second) = {CarrotPaymentProposalV1{ + .destination = acc1.first.cryptonote_address(), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }, true}; + acc1.second.push_back(acc1.second.front()); + acc1.second.back().first.randomness = gen_janus_anchor(); //mangle anchor_norm + + // 1 integrated address payment (subtractable) + tools::add_element(acc3.second) = {CarrotPaymentProposalV1{ + .destination = acc3.first.cryptonote_address(gen_payment_id()), + .amount = crypto::rand_idx(1000000), + .randomness = gen_janus_anchor() + }, true}; + + // 1 main address selfsend (subtractable) + tools::add_element(tx_proposal.explicit_selfsend_proposals) = {CarrotPaymentProposalSelfSendV1{ + .destination_address_spend_pubkey = acc2.first.account_spend_pubkey, + .amount = crypto::rand_idx(1000000), + .enote_type = CarrotEnoteType::PAYMENT, + // no internal messages for legacy self-sends + }, true}; + + // 1 subaddress selfsend (subtractable) + tools::add_element(tx_proposal.explicit_selfsend_proposals) = {CarrotPaymentProposalSelfSendV1{ + .destination_address_spend_pubkey = acc2.first.subaddress(4, 19).address_spend_pubkey, + .amount = crypto::rand_idx(1000000), + .enote_type = CarrotEnoteType::CHANGE + }, true}; + + // specify fee per weight + tx_proposal.fee_per_weight = 314159; + + // test + subtest_multi_account_transfer_over_transaction(tx_proposal); +} +//---------------------------------------------------------------------------------------------------------------------- From 654a0f5c3f82e4637668dd02bdbbd340d09313d2 Mon Sep 17 00:00:00 2001 From: jeffro256 Date: Thu, 30 Jan 2025 23:11:10 -0600 Subject: [PATCH 13/15] make_unsigned_transaction: generate dummmy ephemeral pubkeys when applicable and return modified payment proposals --- src/carrot_impl/carrot_tx_builder.cpp | 67 ++++++++++++++++----------- src/carrot_impl/carrot_tx_builder.h | 12 ++--- tests/unit_tests/carrot_impl.cpp | 28 ++++++++++- 3 files changed, 71 insertions(+), 36 deletions(-) diff --git a/src/carrot_impl/carrot_tx_builder.cpp b/src/carrot_impl/carrot_tx_builder.cpp index cc530c28d48..45a3b48f613 100644 --- a/src/carrot_impl/carrot_tx_builder.cpp +++ b/src/carrot_impl/carrot_tx_builder.cpp @@ -88,8 +88,8 @@ static void append_additional_payment_proposal_if_necessary( } //------------------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------------------- -void make_unsigned_transaction(std::vector &&normal_payment_proposals, - std::vector &&selfsend_payment_proposals, +void make_unsigned_transaction(std::vector &normal_payment_proposals_inout, + std::vector &selfsend_payment_proposals_inout, const rct::xmr_amount fee_per_weight, select_inputs_func_t &&select_inputs, carve_fees_and_balance_func_t &&carve_fees_and_balance, @@ -102,12 +102,23 @@ void make_unsigned_transaction(std::vector &&normal_pay output_amount_blinding_factors_out.clear(); // add an additional payment proposal to satisfy scanning/consensus rules, if applicable - append_additional_payment_proposal_if_necessary(normal_payment_proposals, - selfsend_payment_proposals, + append_additional_payment_proposal_if_necessary(normal_payment_proposals_inout, + selfsend_payment_proposals_inout, account_spend_pubkey); - // calculate number of outputs and the size of tx.extra - const size_t num_outs = normal_payment_proposals.size() + selfsend_payment_proposals.size(); + // generate random X25519 ephemeral pubkeys for selfsend proposals if not explicitly provided in a >2-out tx + const size_t num_outs = normal_payment_proposals_inout.size() + selfsend_payment_proposals_inout.size(); + const bool will_shared_ephemeral_pubkey = num_outs == 2; + if (!will_shared_ephemeral_pubkey) + { + for (CarrotPaymentProposalSelfSendV1 &selfsend_payment_proposal : selfsend_payment_proposals_inout) + { + if (!selfsend_payment_proposal.enote_ephemeral_pubkey) + selfsend_payment_proposal.enote_ephemeral_pubkey = gen_x25519_pubkey(); + } + } + + // 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 @@ -121,9 +132,9 @@ void make_unsigned_transaction(std::vector &&normal_pay // 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) + for (const CarrotPaymentProposalV1 &normal_proposal : normal_payment_proposals_inout) nominal_output_amount_sum += normal_proposal.amount; - for (const CarrotPaymentProposalSelfSendV1 &selfsend_proposal : selfsend_payment_proposals) + for (const CarrotPaymentProposalSelfSendV1 &selfsend_proposal : selfsend_payment_proposals_inout) nominal_output_amount_sum += selfsend_proposal.amount; // callback to select inputs given nominal output sum and fee per input count @@ -140,13 +151,13 @@ void make_unsigned_transaction(std::vector &&normal_pay input_amount_sum += selected_input.amount; // callback to balance the outputs with the fee and input sum - carve_fees_and_balance(input_amount_sum, fee, normal_payment_proposals, selfsend_payment_proposals); + carve_fees_and_balance(input_amount_sum, fee, normal_payment_proposals_inout, selfsend_payment_proposals_inout); // sanity check balance input_amount_sum -= fee; - for (const CarrotPaymentProposalV1 &normal_payment_proposal : normal_payment_proposals) + for (const CarrotPaymentProposalV1 &normal_payment_proposal : normal_payment_proposals_inout) input_amount_sum -= normal_payment_proposal.amount; - for (const CarrotPaymentProposalSelfSendV1 &selfsend_payment_proposal : selfsend_payment_proposals) + for (const CarrotPaymentProposalSelfSendV1 &selfsend_payment_proposal : selfsend_payment_proposals_inout) input_amount_sum -= selfsend_payment_proposal.amount; CHECK_AND_ASSERT_THROW_MES(input_amount_sum == 0, "make unsigned transaction: post-carved transaction does not balance"); @@ -160,8 +171,8 @@ void make_unsigned_transaction(std::vector &&normal_pay // finalize payment proposals into enotes std::vector output_enote_proposals; encrypted_payment_id_t encrypted_payment_id; - get_output_enote_proposals(std::forward>(normal_payment_proposals), - std::forward>(selfsend_payment_proposals), + get_output_enote_proposals(normal_payment_proposals_inout, + selfsend_payment_proposals_inout, s_view_balance_dev, k_view_dev, account_spend_pubkey, @@ -190,8 +201,8 @@ void make_unsigned_transaction(std::vector &&normal_pay } //------------------------------------------------------------------------------------------------------------------- void make_unsigned_transaction_transfer_subtractable( - std::vector &&normal_payment_proposals, - std::vector &&selfsend_payment_proposals, + std::vector &normal_payment_proposals_inout, + std::vector &selfsend_payment_proposals_inout, const rct::xmr_amount fee_per_weight, select_inputs_func_t &&select_inputs, const view_balance_secret_device *s_view_balance_dev, @@ -210,11 +221,11 @@ void make_unsigned_transaction_transfer_subtractable( // 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).enote_type == CarrotEnoteType::CHANGE; + const bool add_payment_type_selfsend = normal_payment_proposals_inout.empty() && + selfsend_payment_proposals_inout.size() == 1 && + selfsend_payment_proposals_inout.at(0).enote_type == CarrotEnoteType::CHANGE; - selfsend_payment_proposals.push_back(CarrotPaymentProposalSelfSendV1{ + selfsend_payment_proposals_inout.push_back(CarrotPaymentProposalSelfSendV1{ .destination_address_spend_pubkey = account_spend_pubkey, .amount = 0, .enote_type = add_payment_type_selfsend ? CarrotEnoteType::PAYMENT : CarrotEnoteType::CHANGE @@ -327,8 +338,8 @@ void make_unsigned_transaction_transfer_subtractable( }; //end carve_fees_and_balance // make unsigned transaction with fee carving callback - make_unsigned_transaction(std::forward>(normal_payment_proposals), - std::forward>(selfsend_payment_proposals), + make_unsigned_transaction(normal_payment_proposals_inout, + selfsend_payment_proposals_inout, fee_per_weight, std::forward(select_inputs), std::move(carve_fees_and_balance), @@ -340,8 +351,8 @@ void make_unsigned_transaction_transfer_subtractable( } //------------------------------------------------------------------------------------------------------------------- void make_unsigned_transaction_transfer( - std::vector &&normal_payment_proposals, - std::vector &&selfsend_payment_proposals, + std::vector &normal_payment_proposals_inout, + std::vector &selfsend_payment_proposals_inout, const rct::xmr_amount fee_per_weight, select_inputs_func_t &&select_inputs, const view_balance_secret_device *s_view_balance_dev, @@ -351,15 +362,15 @@ void make_unsigned_transaction_transfer( std::vector &output_amount_blinding_factors_out) { make_unsigned_transaction_transfer_subtractable( - std::forward>(normal_payment_proposals), - std::forward>(selfsend_payment_proposals), + normal_payment_proposals_inout, + selfsend_payment_proposals_inout, fee_per_weight, std::forward(select_inputs), s_view_balance_dev, k_view_dev, account_spend_pubkey, /*subtractable_normal_payment_proposals=*/{}, - /*subtractable_selfsend_payment_proposals=*/{selfsend_payment_proposals.size()}, + /*subtractable_selfsend_payment_proposals=*/{selfsend_payment_proposals_inout.size()}, tx_out, output_amount_blinding_factors_out); } @@ -429,8 +440,8 @@ void make_unsigned_transaction_sweep( }; //end carve_fees_and_balance // make unsigned transaction with sweep carving callback and selected inputs - make_unsigned_transaction(std::forward>(normal_payment_proposals), - std::forward>(selfsend_payment_proposals), + make_unsigned_transaction(normal_payment_proposals, + selfsend_payment_proposals, fee_per_weight, std::move(select_inputs), std::move(carve_fees_and_balance), diff --git a/src/carrot_impl/carrot_tx_builder.h b/src/carrot_impl/carrot_tx_builder.h index d4e80d2b604..37a742ec866 100644 --- a/src/carrot_impl/carrot_tx_builder.h +++ b/src/carrot_impl/carrot_tx_builder.h @@ -83,8 +83,8 @@ static inline std::size_t get_fcmppp_tx_weight(const std::size_t num_inputs, std::size_t get_fcmppp_coinbase_tx_weight(const std::size_t num_outputs, const std::size_t tx_extra_size); -void make_unsigned_transaction(std::vector &&normal_payment_proposals, - std::vector &&selfsend_payment_proposals, +void make_unsigned_transaction(std::vector &normal_payment_proposals_inout, + std::vector &selfsend_payment_proposals_inout, const rct::xmr_amount fee_per_weight, select_inputs_func_t &&select_inputs, carve_fees_and_balance_func_t &&carve_fees_and_balance, @@ -95,8 +95,8 @@ void make_unsigned_transaction(std::vector &&normal_pay std::vector &output_amount_blinding_factors_out); void make_unsigned_transaction_transfer_subtractable( - std::vector &&normal_payment_proposals, - std::vector &&selfsend_payment_proposals, + std::vector &normal_payment_proposals_inout, + std::vector &selfsend_payment_proposals_inout, const rct::xmr_amount fee_per_weight, select_inputs_func_t &&select_inputs, const view_balance_secret_device *s_view_balance_dev, @@ -108,8 +108,8 @@ void make_unsigned_transaction_transfer_subtractable( std::vector &output_amount_blinding_factors_out); void make_unsigned_transaction_transfer( - std::vector &&normal_payment_proposals, - std::vector &&selfsend_payment_proposals, + std::vector &normal_payment_proposals_inout, + std::vector &selfsend_payment_proposals_inout, const rct::xmr_amount fee_per_weight, select_inputs_func_t &&select_inputs, const view_balance_secret_device *s_view_balance_dev, diff --git a/tests/unit_tests/carrot_impl.cpp b/tests/unit_tests/carrot_impl.cpp index ad49e571013..b1835cc8b2e 100644 --- a/tests/unit_tests/carrot_impl.cpp +++ b/tests/unit_tests/carrot_impl.cpp @@ -503,10 +503,12 @@ static void subtest_multi_account_transfer_over_transaction(const unittest_trans tx_proposal.per_account_payments.at(tx_proposal.self_sender_index).first; // make unsigned transaction + std::vector modified_normal_payment_proposals = normal_payment_proposals; + std::vector modified_selfsend_payment_proposals = selfsend_payment_proposals; cryptonote::transaction tx; std::vector output_amount_blinding_factors; - make_unsigned_transaction_transfer(std::vector(normal_payment_proposals), - std::vector(selfsend_payment_proposals), + make_unsigned_transaction_transfer(modified_normal_payment_proposals, + modified_selfsend_payment_proposals, tx_proposal.fee_per_weight, make_fake_input_selection_callback(), ss_keys.get_view_balance_device(), @@ -533,6 +535,28 @@ static void subtest_multi_account_transfer_over_transaction(const unittest_trans parsed_fee, parsed_encrypted_payment_id)); + // sanity check that the enotes and pid_enc loaded from the transaction are equal to the enotes + // and pic_enc returned from get_output_enote_proposals() when called with the modified payment + // proposals. we do this so that the modified payment proposals from make_unsigned_transaction() + // can be passed to a hardware device for deterministic verification of the signable tx hash + std::vector rederived_output_enote_proposals; + encrypted_payment_id_t rederived_encrypted_payment_id; + get_output_enote_proposals(modified_normal_payment_proposals, + modified_selfsend_payment_proposals, + ss_keys.get_view_balance_device(), + &ss_keys.k_view_dev, + ss_keys.account_spend_pubkey, + parsed_key_images.at(0), + rederived_output_enote_proposals, + rederived_encrypted_payment_id); + ASSERT_TRUE(parsed_encrypted_payment_id); + EXPECT_EQ(*parsed_encrypted_payment_id, rederived_encrypted_payment_id); + ASSERT_EQ(parsed_enotes.size(), rederived_output_enote_proposals.size()); + for (size_t enote_idx = 0; enote_idx < parsed_enotes.size(); ++enote_idx) + { + EXPECT_EQ(parsed_enotes.at(enote_idx), rederived_output_enote_proposals.at(enote_idx).enote); + } + // collect accounts std::vector accounts; for (const auto &pa : tx_proposal.per_account_payments) From 694954a415193df252190dbac2c0ab7bd63bcc92 Mon Sep 17 00:00:00 2001 From: jeffro256 Date: Thu, 30 Jan 2025 23:40:20 -0600 Subject: [PATCH 14/15] refactor for explicit dummy payment id in get_output_enote_proposals --- src/carrot_impl/carrot_tx_builder.cpp | 4 ++++ tests/unit_tests/carrot_impl.cpp | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/carrot_impl/carrot_tx_builder.cpp b/src/carrot_impl/carrot_tx_builder.cpp index 45a3b48f613..6ccc604f71f 100644 --- a/src/carrot_impl/carrot_tx_builder.cpp +++ b/src/carrot_impl/carrot_tx_builder.cpp @@ -118,6 +118,9 @@ void make_unsigned_transaction(std::vector &normal_paym } } + // generate random dummy encrypted payment ID for if none of the normal payment proposals are integrated + const encrypted_payment_id_t 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); @@ -173,6 +176,7 @@ void make_unsigned_transaction(std::vector &normal_paym encrypted_payment_id_t encrypted_payment_id; get_output_enote_proposals(normal_payment_proposals_inout, selfsend_payment_proposals_inout, + dummy_encrypted_payment_id, s_view_balance_dev, k_view_dev, account_spend_pubkey, diff --git a/tests/unit_tests/carrot_impl.cpp b/tests/unit_tests/carrot_impl.cpp index b1835cc8b2e..423c734079b 100644 --- a/tests/unit_tests/carrot_impl.cpp +++ b/tests/unit_tests/carrot_impl.cpp @@ -534,6 +534,7 @@ static void subtest_multi_account_transfer_over_transaction(const unittest_trans parsed_key_images, parsed_fee, parsed_encrypted_payment_id)); + ASSERT_TRUE(parsed_encrypted_payment_id); // sanity check that the enotes and pid_enc loaded from the transaction are equal to the enotes // and pic_enc returned from get_output_enote_proposals() when called with the modified payment @@ -543,13 +544,13 @@ static void subtest_multi_account_transfer_over_transaction(const unittest_trans encrypted_payment_id_t rederived_encrypted_payment_id; get_output_enote_proposals(modified_normal_payment_proposals, modified_selfsend_payment_proposals, + *parsed_encrypted_payment_id, ss_keys.get_view_balance_device(), &ss_keys.k_view_dev, ss_keys.account_spend_pubkey, parsed_key_images.at(0), rederived_output_enote_proposals, rederived_encrypted_payment_id); - ASSERT_TRUE(parsed_encrypted_payment_id); EXPECT_EQ(*parsed_encrypted_payment_id, rederived_encrypted_payment_id); ASSERT_EQ(parsed_enotes.size(), rederived_output_enote_proposals.size()); for (size_t enote_idx = 0; enote_idx < parsed_enotes.size(); ++enote_idx) From 0c132426e00561a5770a451d2094ae49757862cf Mon Sep 17 00:00:00 2001 From: jeffro256 Date: Fri, 31 Jan 2025 00:05:31 -0600 Subject: [PATCH 15/15] refactor carrot_tx_builder files --- src/carrot_impl/CMakeLists.txt | 2 +- src/carrot_impl/carrot_tx_builder_types.h | 65 +++++++++++++++++++ ...uilder.cpp => carrot_tx_builder_utils.cpp} | 2 +- ...tx_builder.h => carrot_tx_builder_utils.h} | 25 +------ tests/unit_tests/carrot_impl.cpp | 2 +- 5 files changed, 70 insertions(+), 26 deletions(-) create mode 100644 src/carrot_impl/carrot_tx_builder_types.h rename src/carrot_impl/{carrot_tx_builder.cpp => carrot_tx_builder_utils.cpp} (99%) rename src/carrot_impl/{carrot_tx_builder.h => carrot_tx_builder_utils.h} (83%) diff --git a/src/carrot_impl/CMakeLists.txt b/src/carrot_impl/CMakeLists.txt index 7891c5420ea..d31d55a8c95 100644 --- a/src/carrot_impl/CMakeLists.txt +++ b/src/carrot_impl/CMakeLists.txt @@ -27,7 +27,7 @@ # THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. set(carrot_impl_sources - carrot_tx_builder.cpp + carrot_tx_builder_utils.cpp carrot_tx_format_utils.cpp ) diff --git a/src/carrot_impl/carrot_tx_builder_types.h b/src/carrot_impl/carrot_tx_builder_types.h new file mode 100644 index 00000000000..fc058e0def5 --- /dev/null +++ b/src/carrot_impl/carrot_tx_builder_types.h @@ -0,0 +1,65 @@ +// 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" + +//third party headers +#include + +//standard headers +#include +#include + +//forward declarations + +namespace carrot +{ +struct CarrotSelectedInput +{ + rct::xmr_amount amount; + crypto::key_image key_image; +}; + +using select_inputs_func_t = std::function&, // absolute fee per input count + std::vector& // selected inputs result + )>; + +using carve_fees_and_balance_func_t = std::function&, // normal payment proposals [inout] + std::vector& // selfsend payment proposals [inout] + )>; +} //namespace carrot diff --git a/src/carrot_impl/carrot_tx_builder.cpp b/src/carrot_impl/carrot_tx_builder_utils.cpp similarity index 99% rename from src/carrot_impl/carrot_tx_builder.cpp rename to src/carrot_impl/carrot_tx_builder_utils.cpp index 6ccc604f71f..5a0b71b800d 100644 --- a/src/carrot_impl/carrot_tx_builder.cpp +++ b/src/carrot_impl/carrot_tx_builder_utils.cpp @@ -27,7 +27,7 @@ // THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. //paired header -#include "carrot_tx_builder.h" +#include "carrot_tx_builder_utils.h" //local headers extern "C" diff --git a/src/carrot_impl/carrot_tx_builder.h b/src/carrot_impl/carrot_tx_builder_utils.h similarity index 83% rename from src/carrot_impl/carrot_tx_builder.h rename to src/carrot_impl/carrot_tx_builder_utils.h index 37a742ec866..fb9cfeefb78 100644 --- a/src/carrot_impl/carrot_tx_builder.h +++ b/src/carrot_impl/carrot_tx_builder_utils.h @@ -29,40 +29,18 @@ #pragma once //local headers -#include "carrot_core/payment_proposal.h" +#include "carrot_tx_builder_types.h" #include "cryptonote_basic/cryptonote_basic.h" //third party headers -#include //standard headers #include -#include -#include -#include //forward declarations namespace carrot { -struct CarrotSelectedInput -{ - rct::xmr_amount amount; - crypto::key_image key_image; -}; - -using select_inputs_func_t = std::function&, // absolute fee per input count - std::vector& // selected inputs result - )>; - -using carve_fees_and_balance_func_t = std::function&, // normal payment proposals [inout] - std::vector& // selfsend payment proposals [inout] - )>; static inline std::size_t get_carrot_default_tx_extra_size(const std::size_t num_outputs) { @@ -127,4 +105,5 @@ void make_unsigned_transaction_sweep( const crypto::public_key &account_spend_pubkey, cryptonote::transaction &tx_out, std::vector &output_amount_blinding_factors_out); + } //namespace carrot diff --git a/tests/unit_tests/carrot_impl.cpp b/tests/unit_tests/carrot_impl.cpp index 423c734079b..2e0c5e4278c 100644 --- a/tests/unit_tests/carrot_impl.cpp +++ b/tests/unit_tests/carrot_impl.cpp @@ -38,7 +38,7 @@ #include "carrot_core/enote_utils.h" #include "carrot_core/output_set_finalization.h" #include "carrot_core/payment_proposal.h" -#include "carrot_impl/carrot_tx_builder.h" +#include "carrot_impl/carrot_tx_builder_utils.h" #include "carrot_impl/carrot_tx_format_utils.h" #include "common/container_helpers.h" #include "crypto/generators.h"