diff --git a/.gitmodules b/.gitmodules index 721cce3b4bb..e33ce8619c6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -14,3 +14,7 @@ path = external/supercop url = https://github.com/monero-project/supercop branch = monero +[submodule "external/mx25519"] + path = external/mx25519 + url = https://github.com/jeffro256/mx25519 + branch = unclamped diff --git a/CMakeLists.txt b/CMakeLists.txt index 7f19b3b6099..94460f39133 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) @@ -380,6 +381,7 @@ if(NOT MANUAL_SUBMODULES) check_submodule(external/trezor-common) check_submodule(external/randomx) check_submodule(external/supercop) + check_submodule(external/mx25519) endif() endif() @@ -461,7 +463,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 45c0e4f14a1..d1663e8bac8 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -70,3 +70,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..eaec90928f8 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -83,6 +83,8 @@ endfunction () 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_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..c15100c5637 --- /dev/null +++ b/src/carrot_core/config.h @@ -0,0 +1,72 @@ +// 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; +} //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..6a6019fdb18 --- /dev/null +++ b/src/carrot_core/output_set_finalization.cpp @@ -0,0 +1,262 @@ +// 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 remaining_change, + 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 && !remaining_change; + 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 (!remaining_change) + { + return AdditionalOutputType::DUMMY; + } + else // num_selfsend == 1 && remaining_change + { + 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 remaining_change, + const bool have_payment_type_selfsend, + const crypto::public_key &change_address_spend_pubkey, + const mx25519_pubkey &other_enote_ephemeral_pubkey) +{ + const std::optional additional_output_type = get_additional_output_type( + num_outgoing, + num_selfsend, + remaining_change, + 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 = remaining_change, + .enote_type = CarrotEnoteType::PAYMENT, + .enote_ephemeral_pubkey = other_enote_ephemeral_pubkey + }; + case AdditionalOutputType::CHANGE_SHARED: + return CarrotPaymentProposalSelfSendV1{ + .destination_address_spend_pubkey = change_address_spend_pubkey, + .amount = remaining_change, + .enote_type = CarrotEnoteType::CHANGE, + .enote_ephemeral_pubkey = other_enote_ephemeral_pubkey + }; + case AdditionalOutputType::CHANGE_UNIQUE: + return CarrotPaymentProposalSelfSendV1{ + .destination_address_spend_pubkey = change_address_spend_pubkey, + .amount = remaining_change, + .enote_type = CarrotEnoteType::CHANGE, + .enote_ephemeral_pubkey = gen_x25519_pubkey() + }; + 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) + { + if (s_view_balance_dev != nullptr) + { + get_output_proposal_internal_v1(selfsend_payment_proposal, + *s_view_balance_dev, + tx_first_key_image, + 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, + 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..fd76b3155ae --- /dev/null +++ b/src/carrot_core/output_set_finalization.h @@ -0,0 +1,113 @@ +// 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: remaining_change - whether there is any leftover change needed to be included + * 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 remaining_change, + 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: remaining_change - 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 + * param: other_enote_ephemeral_pubkey - D^other_e + * 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 remaining_change, + const bool have_payment_type_selfsend, + const crypto::public_key &change_address_spend_pubkey, + const mx25519_pubkey &other_enote_ephemeral_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..6e65efa7013 --- /dev/null +++ b/src/carrot_core/payment_proposal.cpp @@ -0,0 +1,456 @@ +// 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 CarrotPaymentProposalV1 &proposal, + 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(proposal.randomness, + input_context, + proposal.destination.address_spend_pubkey, + proposal.destination.address_view_pubkey, + proposal.destination.payment_id, + enote_ephemeral_privkey); + + return enote_ephemeral_privkey; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +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, 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) +{ + // 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, input_context)}; + + mx25519_pubkey enote_ephemeral_pubkey; + if (proposal.destination.is_subaddress) + // D_e = d_e ConvertPointE(K^j_s) + make_carrot_enote_ephemeral_pubkey_subaddress(enote_ephemeral_privkey, + proposal.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; +} +//------------------------------------------------------------------------------------------------------------------- +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, + 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. s_sr = k_v D_e + mx25519_pubkey s_sender_receiver_unctx; + CHECK_AND_ASSERT_THROW_MES(k_view_dev.view_key_scalar_mult_x25519(proposal.enote_ephemeral_pubkey, + s_sender_receiver_unctx), + "get output proposal special v1: HW device failed to perform ECDH with ephemeral pubkey"); + + // 4. 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, + proposal.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); + + // 5. 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(proposal.enote_ephemeral_pubkey, + input_context, + output_enote_out.enote.onetime_address, + account_spend_pubkey, + janus_anchor_special); + + // 6. 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); + + // 7. save the enote ephemeral pubkey, first tx key image, and amount + output_enote_out.enote.enote_ephemeral_pubkey = proposal.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, + 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. 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(proposal.enote_ephemeral_pubkey, + input_context, + s_sender_receiver); + + // 4. 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, + proposal.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); + + // 5. 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); + + // 6. anchor = given message OR 0s, if not available + const janus_anchor_t anchor = proposal.internal_message.value_or(janus_anchor_t{}); + + // 7. 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); + + // 8. save the enote ephemeral pubkey, first tx key image, and amount + output_enote_out.enote.enote_ephemeral_pubkey = proposal.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..219877000fa --- /dev/null +++ b/src/carrot_core/payment_proposal.h @@ -0,0 +1,167 @@ +// 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" + +//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: xr G + mx25519_pubkey 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 - +* 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, + 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 - +* outparam: output_enote_out - +* outparam: partial_memo_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, + 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/carrot_impl/CMakeLists.txt b/src/carrot_impl/CMakeLists.txt new file mode 100644 index 00000000000..1c22f6958df --- /dev/null +++ b/src/carrot_impl/CMakeLists.txt @@ -0,0 +1,50 @@ +# 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_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_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/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/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/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..df90e67ea46 100644 --- a/tests/unit_tests/CMakeLists.txt +++ b/tests/unit_tests/CMakeLists.txt @@ -38,6 +38,10 @@ set(unit_tests_sources bulletproofs.cpp bulletproofs_plus.cpp canonical_amounts.cpp + carrot_core.cpp + carrot_impl.cpp + carrot_legacy.cpp + carrot_transcript_fixed.cpp chacha.cpp checkpoints.cpp command_line.cpp @@ -102,6 +106,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 +118,8 @@ monero_add_minimal_executable(unit_tests target_link_libraries(unit_tests PRIVATE ringct + carrot_core + carrot_impl 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..ba72dba5ea0 --- /dev/null +++ b/tests/unit_tests/carrot_core.cpp @@ -0,0 +1,1147 @@ +// 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, + 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, + 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, + 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, + 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_impl.cpp b/tests/unit_tests/carrot_impl.cpp new file mode 100644 index 00000000000..b9cfc5b81f4 --- /dev/null +++ b/tests/unit_tests/carrot_impl.cpp @@ -0,0 +1,683 @@ +// 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_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; + } + + 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; + } + + void get_output_enote_proposals_as_self_sender(std::vector &&normal_payment_proposals, + std::vector &&selfsend_payment_proposals, + const crypto::key_image &tx_first_key_image, + std::vector &output_enote_proposals_out, + encrypted_payment_id_t &encrypted_payment_id_out) const + { + const crypto::public_key &account_spend_pubkey = is_carrot + ? this->account_spend_pubkey : legacy_acb.get_keys().m_account_address.m_spend_public_key; + + get_output_enote_proposals(std::forward>(normal_payment_proposals), + std::forward>(selfsend_payment_proposals), + is_carrot ? &s_view_balance_dev : nullptr, + is_carrot ? nullptr : &k_view_dev, + account_spend_pubkey, + tx_first_key_image, + output_enote_proposals_out, + encrypted_payment_id_out); + } + + // 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, minor_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) + const cryptonote::account_keys &ks = legacy_acb.get_keys(); + const crypto::secret_key subaddress_extension = + ks.get_device().get_subaddress_secret_key(ks.m_view_secret_key, {major_index, minor_index}); + + // 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; + std::vector inputs; + + tools::optional_variant + get_additional_output_proposal() const + { + boost::multiprecision::uint128_t input_sum = 0; + for (const per_input &input : inputs) + input_sum += input.second; + + CHECK_AND_ASSERT_THROW_MES(inputs.size(), "we need at least one input"); + CHECK_AND_ASSERT_THROW_MES(per_account_payments.at(self_sender_index).second.empty(), + "self-sender shouldn't contain any normal payment proposals in their own tx"); + + input_context_t input_context; + make_carrot_input_context(inputs.front().first, input_context); + + size_t num_payment_proposals = 0; + boost::multiprecision::uint128_t output_sum = fee; + bool has_payment_selfsend = false; + mx25519_pubkey other_enote_ephemeral_pubkey; + for (const per_account &per_acc : per_account_payments) + { + for (const CarrotPaymentProposalV1 &payment_proposal : per_acc.second) + { + output_sum += payment_proposal.amount; + other_enote_ephemeral_pubkey = get_enote_ephemeral_pubkey(payment_proposal, input_context); + num_payment_proposals++; + } + } + for (const CarrotPaymentProposalSelfSendV1 &selfsend_proposal : explicit_selfsend_proposals) + { + output_sum += selfsend_proposal.amount; + other_enote_ephemeral_pubkey = selfsend_proposal.enote_ephemeral_pubkey; + if (selfsend_proposal.enote_type == CarrotEnoteType::PAYMENT) + has_payment_selfsend = true; + } + + CHECK_AND_ASSERT_THROW_MES(input_sum >= output_sum, "not enough funds"); + + const rct::xmr_amount remaining_change = boost::numeric_cast(input_sum - output_sum); + + return carrot::get_additional_output_proposal(num_payment_proposals, + explicit_selfsend_proposals.size(), + remaining_change, + has_payment_selfsend, + per_account_payments.at(self_sender_index).first.cryptonote_address().address_spend_pubkey, + other_enote_ephemeral_pubkey); + } + + void finalize_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; + + const auto additional_proposal = get_additional_output_proposal(); + struct additional_proposal_visitor + { + void operator()(boost::blank) {} + void operator()(const CarrotPaymentProposalV1 &p) { normal_payment_proposals_out.push_back(p); } + void operator()(const CarrotPaymentProposalSelfSendV1 &p) { selfsend_payment_proposals_out.push_back(p); } + + std::vector &normal_payment_proposals_out; + std::vector &selfsend_payment_proposals_out; + }; + + additional_proposal.visit(additional_proposal_visitor{normal_payment_proposals_out, selfsend_payment_proposals_out}); + } +}; +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +} // 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) +{ + // finalize payment proposals + std::vector normal_payment_proposals; + std::vector selfsend_payment_proposals; + tx_proposal.finalize_payment_proposals(normal_payment_proposals, selfsend_payment_proposals); + + // convert to enotes and pid_enc + std::vector enote_output_proposals; + encrypted_payment_id_t encrypted_payment_id; + tx_proposal.per_account_payments.at(tx_proposal.self_sender_index).first.get_output_enote_proposals_as_self_sender( + std::move(normal_payment_proposals), // move + std::vector(selfsend_payment_proposals), // copy (tested later) + tx_proposal.inputs.at(0).first, + enote_output_proposals, + encrypted_payment_id); + + // collect enotes + std::vector enotes; + for (const RCTOutputEnoteProposal &oep : enote_output_proposals) + enotes.push_back(oep.enote); + + // collect key images + std::vector key_images; + for (const auto &i : tx_proposal.inputs) + key_images.push_back(i.first); + + // stuff carrot info into tx + const cryptonote::transaction tx = store_carrot_to_transaction_v1(enotes, + key_images, + tx_proposal.fee, + encrypted_payment_id); + + // 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)); + + // check loaded carrot stuff == stored carrot stuff + EXPECT_EQ(enotes, parsed_enotes); + EXPECT_EQ(key_images, parsed_key_images); + EXPECT_EQ(tx_proposal.fee, parsed_fee); + ASSERT_TRUE(parsed_encrypted_payment_id); + EXPECT_EQ(encrypted_payment_id, *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); + + // assert properties of finalized selfsend payment proposals as compared to explicit selfsend payment proposals + ASSERT_GE(selfsend_payment_proposals.size(), tx_proposal.explicit_selfsend_proposals.size()); + for (size_t i = 0; i < tx_proposal.explicit_selfsend_proposals.size(); ++i) + { + EXPECT_EQ(tx_proposal.explicit_selfsend_proposals.at(i), selfsend_payment_proposals.at(i)); + } + + // 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(), account_scan_results.size()); + for (const unittest_carrot_scan_result_t &single_scan_res : account_scan_results) + { + bool matched_payment = false; + for (const CarrotPaymentProposalSelfSendV1 &account_payment_proposal : selfsend_payment_proposals) + { + 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)); + + matched_payment = true; + break; + } + } + EXPECT_TRUE(matched_payment); + } + } + 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()); + for (const unittest_carrot_scan_result_t &single_scan_res : account_scan_results) + { + bool matched_payment = false; + for (const CarrotPaymentProposalV1 &account_payment_proposal : account_payment_proposals) + { + 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)); + + matched_payment = true; + break; + } + } + EXPECT_TRUE(matched_payment); + } + } + } +} +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +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 input + tx_proposal.inputs.emplace_back(rct::rct2ki(rct::pkGen()), normal_payment_proposal.amount | (1ull << 63)); + + // specify fee + tx_proposal.fee = 3853481201; + + // 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 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..a12cebb13f9 --- /dev/null +++ b/tests/unit_tests/carrot_transcript_fixed.cpp @@ -0,0 +1,60 @@ +// 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())); +} + +TEST(carrot_transcript_fixed, ts_size) +{ + static constexpr const unsigned char DS1[14] = "perspicacious"; + const auto transcript1 = sp::make_fixed_transcript((uint32_t)32); + EXPECT_EQ(1 + 13 + 4, transcript1.size()); + + static constexpr const unsigned char DS2[14] = "recrudescence"; + 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); +}