diff --git a/.gitmodules b/.gitmodules index 721cce3b4b..5e5d01cbb1 100644 --- a/.gitmodules +++ b/.gitmodules @@ -14,3 +14,6 @@ path = external/supercop url = https://github.com/monero-project/supercop branch = monero +[submodule "external/mx25519"] + path = external/mx25519 + url = https://github.com/tevador/mx25519 diff --git a/CMakeLists.txt b/CMakeLists.txt index d036f74569..ed321a972d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -369,6 +369,7 @@ if(NOT MANUAL_SUBMODULES) check_submodule(external/trezor-common) check_submodule(external/randomx) check_submodule(external/supercop) + check_submodule(external/mx25519) endif() endif() @@ -455,7 +456,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 8deadc7ba6..b577ed6561 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 0000000000..84ca1290fa --- /dev/null +++ b/external/mx25519 @@ -0,0 +1 @@ +Subproject commit 84ca1290fa344351c95692f20f41a174b70e0c7b diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index fed042ce29..6a1089b9e5 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -83,6 +83,7 @@ endfunction () include(Version) monero_add_library(version SOURCES ${CMAKE_BINARY_DIR}/version.cpp DEPENDS genversion) +add_subdirectory(async) add_subdirectory(common) add_subdirectory(crypto) add_subdirectory(ringct) @@ -96,6 +97,12 @@ add_subdirectory(hardforks) add_subdirectory(blockchain_db) add_subdirectory(mnemonics) add_subdirectory(rpc) +add_subdirectory(seraphis_core) +add_subdirectory(seraphis_crypto) +add_subdirectory(seraphis_impl) +add_subdirectory(seraphis_main) +add_subdirectory(seraphis_mocks) +add_subdirectory(seraphis_wallet) if(NOT IOS) add_subdirectory(serialization) endif() diff --git a/src/async/CMakeLists.txt b/src/async/CMakeLists.txt new file mode 100644 index 0000000000..e1cfd512ee --- /dev/null +++ b/src/async/CMakeLists.txt @@ -0,0 +1,51 @@ +# Copyright (c) 2021, 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(async_sources + sleepy_task_queue.cpp + task_types.cpp + threadpool.cpp + waiter_manager.cpp) + +monero_find_all_headers(async_headers, "${CMAKE_CURRENT_SOURCE_DIR}") + +monero_add_library(async + ${async_sources} + ${async_headers}) + +target_link_libraries(async + PUBLIC + common + PRIVATE + ${EXTRA_LIBRARIES}) + +target_include_directories(async + PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}" + PRIVATE + ${Boost_INCLUDE_DIRS}) diff --git a/src/async/misc_utils.h b/src/async/misc_utils.h new file mode 100644 index 0000000000..a8fad493ca --- /dev/null +++ b/src/async/misc_utils.h @@ -0,0 +1,87 @@ +// Copyright (c) 2023, 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. + +/// Miscellaneous async utils. + +#pragma once + +//local headers +#include "common/expect.h" + +//third-party headers + +//standard headers +#include +#include + +//forward declarations + + +namespace async +{ +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +template +bool future_is_ready(const std::future &future) +{ + try + { + if (!future.valid()) + return false; + if (future.wait_for(std::chrono::seconds(0)) != std::future_status::ready) + return false; + } catch (...) { return false; } + return true; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +template +bool future_is_ready(const std::shared_future &future) +{ + try + { + if (!future.valid()) + return false; + if (future.wait_for(std::chrono::seconds(0)) != std::future_status::ready) + return false; + } catch (...) { return false; } + return true; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +template +expect unwrap_future(std::future &future) +{ + if (!future_is_ready(future)) { return std::error_code{}; } + try { return std::move(future.get()); } + catch (std::error_code e) { return e; } + catch (...) { return std::error_code{}; } +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +} //namespace async diff --git a/src/async/parent_reference_tasking_system.h b/src/async/parent_reference_tasking_system.h new file mode 100644 index 0000000000..ea4c39054f --- /dev/null +++ b/src/async/parent_reference_tasking_system.h @@ -0,0 +1,259 @@ +// Copyright (c) 2023, 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. + +/// Sean Parent's reference thread pool +/// ref: https://github.com/stlab/libraries/blob/main/stlab/concurrency/default_executor.hpp + +#pragma once + +//local headers + +//third-party headers + +//standard headers +#include +#include +#include +#include +#include +#include +#include + +//forward declarations + + +namespace parent +{ + +enum class TokenQueueResult : unsigned char +{ + SUCCESS, + SHUTTING_DOWN, + QUEUE_EMPTY, + TRY_LOCK_FAIL +}; + +/// async token queue +template +class TokenQueue final +{ + struct element_t final + { + std::size_t index; + TokenT token; + + template + element_t(const std::size_t index, T&& new_element) : + index{index}, + token{std::forward(new_element)} + {} + + bool operator<(const element_t& other) const + { + return this->index < other.index; + }; + }; + + // must be called under lock with non-empty queue + TokenT pop_not_empty() + { + TokenT temp{std::move(m_queue.front()).token}; + std::pop_heap(std::begin(m_queue), std::end(m_queue)); + m_queue.pop_back(); + return temp; + } + +public: +//constructors + /// resurrect default constructor + TokenQueue() = default; + +//member functions + /// try to add an element to the top + template + TokenQueueResult try_push(T &&new_element_in) + { + { + std::unique_lock lock{m_mutex, std::try_to_lock}; + if (!lock.owns_lock()) + return TokenQueueResult::TRY_LOCK_FAIL; + + m_queue.emplace_back(m_index_counter++, std::forward(new_element_in)); + std::push_heap(std::begin(m_queue), std::end(m_queue)); + } + m_condvar.notify_one(); + return TokenQueueResult::SUCCESS; + } + /// add an element to the top (always succeeds) + template + void force_push(T &&new_element_in) + { + { + std::lock_guard lock{m_mutex}; + m_queue.emplace_back(m_index_counter++, std::forward(new_element_in)); + std::push_heap(std::begin(m_queue), std::end(m_queue)); + } + m_condvar.notify_one(); + } + + /// try to remove an element from the bottom + TokenQueueResult try_pop(TokenT &token_out) + { + // try to lock the queue, then check if there are any elements + std::unique_lock lock{m_mutex, std::try_to_lock}; + if (!lock.owns_lock()) + return TokenQueueResult::TRY_LOCK_FAIL; + if (m_queue.size() == 0) + return TokenQueueResult::QUEUE_EMPTY; + + // pop the bottom element + token_out = pop_not_empty(); + return TokenQueueResult::SUCCESS; + } + /// wait until you can remove an element from the bottom + TokenQueueResult force_pop(TokenT &token_out) + { + // lock the queue then wait until there is an element or the queue is shutting down + std::unique_lock lock{m_mutex}; + while (m_queue.size() == 0 && !m_shutting_down) + m_condvar.wait(lock); + if (m_queue.size() == 0 && m_shutting_down) + return TokenQueueResult::SHUTTING_DOWN; + + // pop the bottom element + token_out = pop_not_empty(); + return TokenQueueResult::SUCCESS; + } + + void shut_down() + { + { + std::unique_lock lock{m_mutex}; + m_shutting_down = true; + } + m_condvar.notify_all(); + } + +private: +//member variables + /// queue status + bool m_shutting_down{false}; + + /// queue context + std::vector m_queue; + std::mutex m_mutex; + std::condition_variable m_condvar; + std::size_t m_index_counter{0}; +}; + + +/// Sean Parent's reference threadpool +class ThreadPool final +{ + using queue_t = TokenQueue>; +public: + ThreadPool(const std::uint16_t num_workers) : + m_queues{static_cast(num_workers > 0 ? num_workers : 1)} + { + m_workers.reserve(m_queues.size()); + for (std::uint16_t worker_index{0}; worker_index < m_queues.size(); ++worker_index) + { + try + { + m_workers.emplace_back( + [this, worker_index]() mutable + { + try { this->run(worker_index); } catch (...) { /* can't do anything */ } + } + ); + } + catch (...) { /* can't do anything */ } + } + } + + ~ThreadPool() + { + this->shut_down(); + } + + void run(const std::uint16_t worker_index) + { + while (true) + { + // cycle all the queues + std::function task; + for (std::uint16_t i{0}; i < m_queues.size(); ++i) + { + if (m_queues[(i + worker_index) % m_queues.size()].try_pop(task) == TokenQueueResult::SUCCESS) + break; + } + + // fallback: force pop + if (!task && + m_queues[worker_index].force_pop(task) == TokenQueueResult::SHUTTING_DOWN) + break; + + // run the task + try { task(); } catch (...) {} + } + } + + template + void submit(F &&function) + { + std::uint32_t start_counter{m_submit_rotation_counter.fetch_add(1, std::memory_order_relaxed)}; + + // cycle all the queues + for (std::uint32_t i{0}; i < m_queues.size() * 40; ++i) + { + if (m_queues[(i + start_counter) % m_queues.size()].try_push(std::forward(function)) == + TokenQueueResult::SUCCESS) + return; + } + + // fallback: force push + m_queues[start_counter % m_queues.size()].force_push(std::forward(function)); + } + + void shut_down() + { + for (queue_t &queue : m_queues) + try { queue.shut_down(); } catch (...) {} + + for (std::thread &worker : m_workers) + try { if (worker.joinable()) worker.join(); } catch (...) {} + } + +private: +//member variables + std::vector m_queues; + std::vector m_workers; + std::atomic m_submit_rotation_counter{0}; +}; + +} //namespace asyc diff --git a/src/async/rw_lock.h b/src/async/rw_lock.h new file mode 100644 index 0000000000..c9653961c7 --- /dev/null +++ b/src/async/rw_lock.h @@ -0,0 +1,373 @@ +// Copyright (c) 2023, 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. + +/// Single-writer/multi-reader value containers. +/// - Accessing a moved-from container will throw. +/// - We do not guarantee that accessing the same container from multiple threads is not UB. +/// - The containers use a shared_ptr internally, so misuse WILL cause reference cycles. +/// +/// Implementation notes: +/// - We use a shared_mutex for the context mutex since after a write_lock is released multiple waiting readers may +/// concurrently acquire a shared lock on the value. + +#pragma once + +//local headers + +//third-party headers +#include +#include + +//standard headers +#include +#include +#include +#include +#include + +//forward declarations + + +namespace async +{ + +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- + +namespace detail +{ + +/// enable if nonconst +template +struct enable_if_nonconst; +template +struct enable_if_nonconst::value>> {}; +template +struct enable_if_nonconst::value>> final { enable_if_nonconst() = delete; }; + +/// test a rw_lock pointer +[[noreturn]] inline void rw_lock_ptr_access_error() { throw std::runtime_error{"rw_lock invalid ptr access."}; } +inline void test_rw_ptr(const void *ptr) { if (ptr == nullptr) rw_lock_ptr_access_error(); } + +/// value context +template +struct rw_context final +{ + value_t value; + boost::shared_mutex value_mutex; + boost::shared_mutex ctx_mutex; + std::condition_variable_any ctx_condvar; + std::atomic num_readers{0}; +}; + +} //namespace detail + +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- + +/// declarations +template +class read_lock; +template +class write_lock; +template +class readable; +template +class writable; + +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- + +/// READ LOCK (can read the locked value concurrently with other read_locks) +template +class read_lock final : public detail::enable_if_nonconst +{ + friend class readable; + using ctx_t = detail::rw_context; + +protected: +//constructors + /// default constructor: disabled + read_lock() = delete; + /// normal constructor: only callable by readable and writable + read_lock(boost::shared_lock lock, std::shared_ptr context) : + m_lock{std::move(lock)}, + m_context{std::move(context)} + {} + /// copies: disabled + read_lock(const read_lock&) = delete; + read_lock& operator=(const read_lock&) = delete; + +public: + /// moves: default + read_lock(read_lock&&) = default; + read_lock& operator=(read_lock&&) = default; + +//destructor + ~read_lock() + { + if (m_context && + m_lock.mutex() != nullptr && + m_lock.owns_lock()) + { + { + boost::shared_lock ctx_lock{m_context->ctx_mutex}; + m_lock.unlock(); + } + + // if there seem to be no existing readers, notify one waiting writer + // NOTE: this is only an optimization + // - there is a race condition where a new reader is being added/gets added concurrently and the notified + // writer ends up failing to get a lock + // - there is also a race conditon where a writer is added after our .unlock() call above, then a reader gets + // stuck on ctx_condvar, then our notify_one() call here causes that reader to needlessly try to get a lock + if (m_context->num_readers.fetch_sub(1, std::memory_order_relaxed) <= 0) + m_context->ctx_condvar.notify_one(); //notify one waiting writer + } + } + +//member functions + /// access the value + const value_t& value() const { detail::test_rw_ptr(m_context.get()); return m_context->value; } + +private: +//member variables + boost::shared_lock m_lock; + std::shared_ptr m_context; +}; + +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- + +/// WRITE LOCK (can mutate the locked value) +template +class write_lock final : public detail::enable_if_nonconst +{ + friend class writable; + using ctx_t = detail::rw_context; + +protected: +//constructors + /// default constructor: disabled + write_lock() = delete; + /// normal constructor: only callable by writable + write_lock(boost::unique_lock lock, std::shared_ptr context) : + m_lock{std::move(lock)}, + m_context{std::move(context)} + {} + /// copies: disabled + write_lock(const write_lock&) = delete; + write_lock& operator=(const write_lock&) = delete; + +public: + /// moves: default + write_lock(write_lock&&) = default; + write_lock& operator=(write_lock&&) = default; + +//destructor + ~write_lock() + { + if (m_context && + m_lock.mutex() != nullptr && + m_lock.owns_lock()) + { + { + boost::unique_lock ctx_lock{m_context->ctx_mutex}; + m_lock.unlock(); + } + m_context->ctx_condvar.notify_all(); //notify all waiting + } + } + +//member functions + /// access the value + value_t& value() { detail::test_rw_ptr(m_context.get()); return m_context->value; } + +private: +//member variables + boost::unique_lock m_lock; + std::shared_ptr m_context; +}; + +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- + +/// READ LOCKABLE (can be copied and spawn read_locks) +template +class readable final : public detail::enable_if_nonconst +{ + friend class writable; + using ctx_t = detail::rw_context; + +protected: +//constructors + /// default constructor: disabled + readable() = delete; + /// normal constructor: only callable by writable + readable(std::shared_ptr context) : + m_context{std::move(context)} + {} + +public: + /// normal constructor: from value + readable(const value_t &raw_value) : + m_context{std::make_shared()} + { + m_context->value = raw_value; + } + readable(value_t &&raw_value) : + m_context{std::make_shared()} + { + m_context->value = std::move(raw_value); + } + + /// moves and copies: default + +//member functions + /// try to get a write lock + /// FAILS IF THERE IS A CONCURRENT WRITE LOCK + boost::optional> try_lock() + { + detail::test_rw_ptr(m_context.get()); + boost::shared_lock lock{m_context->value_mutex, boost::try_to_lock}; + if (!lock.owns_lock()) return boost::none; + else + { + m_context->num_readers.fetch_add(1, std::memory_order_relaxed); + return read_lock{std::move(lock), m_context}; + } + } + + /// get a read lock + /// BLOCKS IF THERE IS A CONCURRENT WRITE LOCK + read_lock lock() + { + // cheap attempt + boost::optional> lock; + if ((lock = this->try_lock())) + return std::move(*lock); + + // blocking attempt + detail::test_rw_ptr(m_context.get()); + boost::shared_lock ctx_lock{m_context->ctx_mutex}; + m_context->ctx_condvar.wait(ctx_lock, + [&]() -> bool + { + return (lock = this->try_lock()) != boost::none; + } + ); + + return std::move(*lock); + } + +private: +//member variables + std::shared_ptr m_context; +}; + +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- + +/// WRITE LOCKABLE (can spawn readables and write_locks) +template +class writable final : public detail::enable_if_nonconst +{ + using ctx_t = detail::rw_context; + +public: +//constructors + /// default constructor: disabled + writable() = delete; + /// normal constructor: from value + writable(const value_t &raw_value) : + m_context{std::make_shared()} + { + m_context->value = raw_value; + } + writable(value_t &&raw_value) : + m_context{std::make_shared()} + { + m_context->value = std::move(raw_value); + } + + /// copies: disabled + writable(const writable&) = delete; + writable& operator=(const writable&) = delete; + /// moves: default + writable(writable&&) = default; + writable& operator=(writable&&) = default; + +//member functions + /// get a readable + readable get_readable() + { + detail::test_rw_ptr(m_context.get()); + return readable{m_context}; + } + + /// try to get a write lock + /// FAILS IF THERE ARE ANY CONCURRENT WRITE OR READ LOCKS + boost::optional> try_lock() + { + detail::test_rw_ptr(m_context.get()); + boost::unique_lock lock{m_context->value_mutex, boost::try_to_lock}; + if (!lock.owns_lock()) return boost::none; + else return write_lock{std::move(lock), m_context}; + } + + /// get a write lock + /// BLOCKS IF THERE ARE ANY CONCURRENT WRITE OR READ LOCKS + write_lock lock() + { + // cheap attempt + boost::optional> lock; + if ((lock = this->try_lock())) + return std::move(*lock); + + // blocking attempt + detail::test_rw_ptr(m_context.get()); + boost::unique_lock ctx_lock{m_context->ctx_mutex}; + m_context->ctx_condvar.wait(ctx_lock, + [&]() -> bool + { + return (lock = this->try_lock()) != boost::none; + } + ); + + return std::move(*lock); + } + +private: +//member variables + std::shared_ptr m_context; +}; + +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- + +} //namespace async diff --git a/src/async/scoped_notification.h b/src/async/scoped_notification.h new file mode 100644 index 0000000000..96912131f8 --- /dev/null +++ b/src/async/scoped_notification.h @@ -0,0 +1,95 @@ +// Copyright (c) 2023, 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. + +/// Scoped notification: calls a function when destroyed. + +#pragma once + +//local headers + +//third-party headers + +//standard headers +#include + +//forward declarations + + +namespace async +{ + +/// scoped notification (notifies on destruction) +/// - only use this if you can GUARANTEE the lifetimes of any references in the notification function are longer +/// than the notification's lifetime +class ScopedNotification final +{ +public: +//constructors + /// normal constructor + ScopedNotification(std::function notification_func) : + m_notification_func{std::move(notification_func)} + {} + + /// disable copies (this is a scoped manager) + ScopedNotification(const ScopedNotification&) = delete; + ScopedNotification& operator=(const ScopedNotification&) = delete; + + /// moved-from notifications need empty notification functions so they are not called in the destructor + ScopedNotification(ScopedNotification &&other) + { + *this = std::move(other); + } + ScopedNotification& operator=(ScopedNotification &&other) + { + this->notify(); + this->m_notification_func = std::move(other).m_notification_func; + other.m_notification_func = nullptr; //nullify the moved-from function + return *this; + } + +//destructor + ~ScopedNotification() + { + this->notify(); + } + +private: +//member functions + void notify() noexcept + { + if (m_notification_func) + { + try { m_notification_func(); } catch (...) {} + } + } + +//member variables + std::function m_notification_func; +}; + +} //namespace asyc diff --git a/src/async/sleepy_task_queue.cpp b/src/async/sleepy_task_queue.cpp new file mode 100644 index 0000000000..0e9dfe5362 --- /dev/null +++ b/src/async/sleepy_task_queue.cpp @@ -0,0 +1,166 @@ +// Copyright (c) 2022, 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 "sleepy_task_queue.h" + +//local headers +#include "task_types.h" + +//third party headers + +//standard headers +#include +#include +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "async" + +namespace async +{ +//------------------------------------------------------------------------------------------------------------------- +long long time_as_tick_count(const WakeTime &waketime) +{ + return wake_time(waketime).time_since_epoch().count(); +} +//------------------------------------------------------------------------------------------------------------------- +void SleepyTaskQueue::force_push(SleepyTask &&task) +{ + std::lock_guard lock{m_mutex};; + m_queue.emplace( + time_as_tick_count(task.wake_time), + std::make_unique(std::move(task), SleepingTaskStatus::UNCLAIMED) + ); +} +//------------------------------------------------------------------------------------------------------------------- +bool SleepyTaskQueue::try_push(SleepyTask &&task) +{ + std::unique_lock lock{m_mutex, std::try_to_lock}; + if (!lock.owns_lock()) + return false; + m_queue.emplace( + time_as_tick_count(task.wake_time), + std::make_unique(std::move(task), SleepingTaskStatus::UNCLAIMED) + ); + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool SleepyTaskQueue::try_swap(const unsigned char max_task_priority, SleepingTask* &task_inout) +{ + // initialize the current task's waketime (set to max if there is no task) + auto current_task_waketime_count = + task_inout + ? wake_time(task_inout->sleepy_task.wake_time).time_since_epoch().count() + : std::chrono::time_point::max().time_since_epoch().count(); + + // lock the queue + std::unique_lock lock{m_mutex, std::try_to_lock}; + if (!lock.owns_lock()) + return false; + + // try to find an unclaimed task that wakes up sooner than our input task + for (auto &candidate_task : m_queue) + { + const SleepingTaskStatus candidate_status{candidate_task.second->status.load(std::memory_order_acquire)}; + + // skip reserved and dead tasks + if (candidate_status == SleepingTaskStatus::RESERVED || + candidate_status == SleepingTaskStatus::DEAD) + continue; + + // skip tasks with too-high priority + if (candidate_task.second->sleepy_task.simple_task.priority < max_task_priority) + continue; + + // give up: the first unclaimed task does not wake up sooner than our input task + if (current_task_waketime_count <= candidate_task.first) + return false; + + // success + // a. release our input task if we have one + if (task_inout) + unclaim_sleeping_task(*task_inout); + + // b. acquire this candidate + task_inout = candidate_task.second.get(); + reserve_sleeping_task(*task_inout); + return true; + } + + return false; +} +//------------------------------------------------------------------------------------------------------------------- +std::list> SleepyTaskQueue::try_perform_maintenance( + const std::chrono::time_point ¤t_time) +{ + // current time + auto now_count = current_time.time_since_epoch().count(); + + // lock the queue + std::unique_lock lock{m_mutex, std::try_to_lock}; + if (!lock.owns_lock()) + return {}; + + // delete dead tasks and extract awake tasks until the lowest sleeping unclaimed task is encountered + std::list> awakened_tasks; + + for (auto queue_it = m_queue.begin(); queue_it != m_queue.end();) + { + const SleepingTaskStatus task_status{queue_it->second->status.load(std::memory_order_acquire)}; + + // skip reserved tasks + if (task_status == SleepingTaskStatus::RESERVED) + { + ++queue_it; + continue; + } + + // delete dead tasks + if (task_status == SleepingTaskStatus::DEAD) + { + queue_it = m_queue.erase(queue_it); + continue; + } + + // extract awake unclaimed tasks + if (queue_it->first <= now_count) + { + awakened_tasks.emplace_back(std::move(queue_it->second)); + queue_it = m_queue.erase(queue_it); + continue; + } + + // exit when we found an asleep unclaimed task + break; + } + + return awakened_tasks; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace async diff --git a/src/async/sleepy_task_queue.h b/src/async/sleepy_task_queue.h new file mode 100644 index 0000000000..b422a1c89d --- /dev/null +++ b/src/async/sleepy_task_queue.h @@ -0,0 +1,94 @@ +// Copyright (c) 2023, 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. + +/// Queue of sleepy tasks. + +#pragma once + +//local headers +#include "task_types.h" + +//third-party headers +#include + +//standard headers +#include +#include +#include +#include +#include + +//forward declarations + + +namespace async +{ + +/// SleepyTaskQueue +/// - PRECONDITION: a user of a sleepy task queue with a pointer/reference to a task in that queue should ONLY change +/// the task's status from RESERVED to UNCLAIMED/DEAD (and not any other direction) +/// - once a RESERVED task's status has been changed, the user should assume they no longer have valid access to the +/// task +/// - only change a task's status from RESERVED -> UNCLAIMED if its contents will be left in a valid state after the +/// change (e.g. the internal task shouldn't be in a moved-from state) +class SleepyTaskQueue final +{ +public: +//overloaded operators + /// disable copy/move since this may be accessed concurrently from multiple threads + SleepyTaskQueue& operator=(SleepyTaskQueue&&) = delete; + +//member functions + /// force push a sleepy task into the queue + void force_push(SleepyTask &&task); + /// try to push a sleepy task into the queue + bool try_push(SleepyTask &&task); + + /// try to swap an existing sleepy task with a task that wakes up sooner + /// - this function does not add/remove elements from the queue; instead, it simply adjusts task statuses then + /// swaps pointers + /// - if 'task_inout == nullptr', then we set it to the unclaimed task with the lowest waketime + /// - the cost of this function may be higher than expected if there are many tasks with higher priority than our + /// allowed max priority + bool try_swap(const unsigned char max_task_priority, SleepingTask* &task_inout); + + /// try to clean up the queue + /// - remove dead tasks + /// - extract awake unclaimed tasks + std::list> try_perform_maintenance( + const std::chrono::time_point ¤t_time); + +private: +//member variables + /// queue context (sorted by waketime) + boost::container::multimap::rep, + std::unique_ptr> m_queue; + std::mutex m_mutex; +}; + +} //namespace asyc diff --git a/src/async/task_types.cpp b/src/async/task_types.cpp new file mode 100644 index 0000000000..a1201fad41 --- /dev/null +++ b/src/async/task_types.cpp @@ -0,0 +1,80 @@ +// Copyright (c) 2022, 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 "task_types.h" + +//local headers + +//third party headers + +//standard headers +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "async" + +namespace async +{ +//------------------------------------------------------------------------------------------------------------------- +std::chrono::time_point wake_time(const WakeTime waketime) +{ + return waketime.start_time + waketime.duration; +} +//------------------------------------------------------------------------------------------------------------------- +bool sleepy_task_is_awake(const SleepyTask &task) +{ + return wake_time(task.wake_time) <= std::chrono::steady_clock::now(); +} +//------------------------------------------------------------------------------------------------------------------- +bool sleeping_task_is_unclaimed(const SleepingTask &task) +{ + return task.status.load(std::memory_order_acquire) == SleepingTaskStatus::UNCLAIMED; +} +//------------------------------------------------------------------------------------------------------------------- +bool sleeping_task_is_dead(const SleepingTask &task) +{ + return task.status.load(std::memory_order_acquire) == SleepingTaskStatus::DEAD; +} +//------------------------------------------------------------------------------------------------------------------- +void unclaim_sleeping_task(SleepingTask &sleeping_task_inout) +{ + sleeping_task_inout.status.store(SleepingTaskStatus::UNCLAIMED, std::memory_order_release); +} +//------------------------------------------------------------------------------------------------------------------- +void reserve_sleeping_task(SleepingTask &sleeping_task_inout) +{ + sleeping_task_inout.status.store(SleepingTaskStatus::RESERVED, std::memory_order_release); +} +//------------------------------------------------------------------------------------------------------------------- +void kill_sleeping_task(SleepingTask &sleeping_task_inout) +{ + sleeping_task_inout.status.store(SleepingTaskStatus::DEAD, std::memory_order_release); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace async diff --git a/src/async/task_types.h b/src/async/task_types.h new file mode 100644 index 0000000000..fef41a75a0 --- /dev/null +++ b/src/async/task_types.h @@ -0,0 +1,266 @@ +// Copyright (c) 2023, 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. + +/// Task types for a threadpool + +#pragma once + +//local headers +#include "common/variant.h" + +//third-party headers +#include + +//standard headers +#include +#include +#include +#include +#include + +//forward declarations + + +namespace async +{ + +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- + +/// waketime +/// - waketime = start time + duration +/// - if 'start time == 0' when a task is received, then the start time will be set to the time at that moment +/// - this allows task-makers to specify either a task's waketime or its sleep duration from the moment it is +/// submitted, e.g. for task continuations that are defined well in advance of when they are submitted +struct WakeTime final +{ + std::chrono::time_point start_time{ + std::chrono::time_point::min() + }; + std::chrono::nanoseconds duration{0}; +}; + +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- + +/// possible statuses of a sleepy task in a sleepy queue +enum class SleepingTaskStatus : unsigned char +{ + /// task is waiting for a worker + UNCLAIMED, + /// task is reserved by a worker + RESERVED, + /// task has been consumed by a worker + DEAD +}; + +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- + +struct SimpleTask; +struct SleepyTask; + +/// task +using TaskVariant = tools::variant; +using task_t = std::function; //tasks auto-return their continuation (or an empty variant) + +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- + +/// pending task +struct SimpleTask final +{ + unsigned char priority; + task_t task; + + SimpleTask() = default; + SimpleTask(const SimpleTask&) = delete; + SimpleTask(SimpleTask&&) = default; +}; + +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- + +/// sleepy task +struct SleepyTask final +{ + SimpleTask simple_task; + WakeTime wake_time; + + SleepyTask() = default; + SleepyTask(const SleepyTask&) = delete; + SleepyTask(SleepyTask&&) = default; +}; + +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- + +/// sleeping task +/// note: we need an extra type for sleeping tasks because SleepyTasks are not copy-constructible, and the atomic status +/// is not move-constructible, which means SleepingTasks are very hard to move around +struct SleepingTask final +{ + SleepyTask sleepy_task; + std::atomic status{SleepingTaskStatus::UNCLAIMED}; + + /// normal constructor (this struct is not movable or copyable, so it needs some help...) + SleepingTask(SleepyTask &&sleepy_task, const SleepingTaskStatus status) : + sleepy_task{std::move(sleepy_task)}, status{status} + {} +}; + +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- + +/// copyable tasks +template < + typename F, + typename std::enable_if< + std::is_same()())>::value + && std::is_constructible, F>::value, + bool + >::type = true +> +std::function as_task_function(F &&func) +{ + return std::move(func); +} + +template < + typename F, + typename std::enable_if< + std::is_same()())>::value + && std::is_constructible, F>::value, + bool + >::type = true +> +std::function as_task_function(F &&func) +{ + return [l_func = std::move(func)]() -> TaskVariant { l_func(); return boost::none; }; +} + +/// move-only tasks +//todo: std::packaged_task is inefficient for this use-case but std::move_only_function is C++23 +template < + typename F, + typename std::enable_if< + std::is_same()())>::value + && !std::is_constructible, F>::value, + bool + >::type = true +> +std::function as_task_function(F &&func) +{ + return + [l_func = std::make_shared>(std::move(func))] + () -> TaskVariant + { + if (!l_func) return boost::none; + try { (*l_func)(); return l_func->get_future().get(); } catch (...) {} + return boost::none; + }; +} + +template < + typename F, + typename std::enable_if< + std::is_same()())>::value + && !std::is_constructible, F>::value, + bool + >::type = true +> +std::function as_task_function(F &&func) +{ + return + [l_func = std::make_shared>(std::move(func))] + () -> TaskVariant + { + if (!l_func) return boost::none; + try { (*l_func)(); } catch (...) {} + return boost::none; + }; +} + +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- + +/// make simple task +template +SimpleTask make_simple_task(const unsigned char priority, F &&func) +{ + static_assert(std::is_same::value, "tasks must return task variants"); + return SimpleTask{ + .priority = priority, + .task = as_task_function(std::forward(func)) + }; +} + +/// make sleepy task +template +SleepyTask make_sleepy_task(const unsigned char priority, const WakeTime &waketime, F &&func) +{ + return { + make_simple_task(priority, std::forward(func)), + waketime + }; +} +template +SleepyTask make_sleepy_task(const unsigned char priority, const std::chrono::nanoseconds &duration, F &&func) +{ + // note: the start time is left undefined/zero until the task gets scheduled + WakeTime waketime{}; + waketime.duration = duration; + + return { + make_simple_task(priority, std::forward(func)), + waketime + }; +} +template +SleepyTask make_sleepy_task(const unsigned char priority, + const std::chrono::time_point &waketime, + F &&func) +{ + return { + make_simple_task(priority, std::forward(func)), + WakeTime{ .start_time = waketime, .duration = std::chrono::nanoseconds{0} } + }; +} + +//todo +std::chrono::time_point wake_time(const WakeTime waketime); + +//todo +bool sleepy_task_is_awake(const SleepyTask &task); +bool sleeping_task_is_unclaimed(const SleepingTask &task); +bool sleeping_task_is_dead(const SleepingTask &task); +void unclaim_sleeping_task(SleepingTask &sleeping_task_inout); +void reserve_sleeping_task(SleepingTask &sleeping_task_inout); +void kill_sleeping_task(SleepingTask &sleeping_task_inout); + +} //namespace asyc diff --git a/src/async/threadpool.cpp b/src/async/threadpool.cpp new file mode 100644 index 0000000000..9685cf61e7 --- /dev/null +++ b/src/async/threadpool.cpp @@ -0,0 +1,803 @@ +// Copyright (c) 2022, 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 "threadpool.h" + +//local headers +#include "task_types.h" + +//third party headers +#include + +//standard headers +#include +#include +#include +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "async" + +namespace async +{ +// start at 1 so each thread's default context id does not match any actual context +static std::atomic s_context_id_counter{1}; +static thread_local std::uint64_t tl_context_id{0}; //context this thread is attached to +static thread_local std::uint16_t tl_worker_id{0}; //this thread's id within its context + +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static std::uint64_t initialize_threadpool_owner() +{ + assert(tl_worker_id == 0); //only threads with id = 0 may own threadpools + + // the first time this function is called, initialize with a unique threadpool id + // - a threadpool owner gets its own unique context id to facilitate owning multiple threadpools with + // overlapping lifetimes + static const std::uint64_t id{ + []() + { + tl_context_id = s_context_id_counter.fetch_add(1, std::memory_order_relaxed); + return tl_context_id; + }() + }; + + return id; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void initialize_threadpool_worker_thread(const std::uint64_t threadpool_id, const std::uint16_t worker_id) +{ + assert(tl_context_id == 0); //only threads without a context may become subthreads of a threadpool + assert(worker_id > 0); //id 0 is reserved for pool owners, who have their own unique context id + tl_context_id = threadpool_id; + tl_worker_id = worker_id; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static std::uint16_t thread_context_id() +{ + return tl_context_id; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static std::uint16_t threadpool_worker_id() +{ + return tl_worker_id; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool test_threadpool_member_invariants(const std::uint64_t threadpool_id, const std::uint64_t owner_id) +{ + // if this thread owns the threadpool, its worker id should be 0 + if (owner_id == thread_context_id()) + return threadpool_worker_id() == 0; + + // if this thread doesn't own the threadpool, it should be a subthread of the pool + return (threadpool_id == thread_context_id()) && (threadpool_worker_id() > 0); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static unsigned char clamp_priority(const unsigned char max_priority_level, const unsigned char priority) +{ + if (priority > max_priority_level) + return max_priority_level; + return priority; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void set_current_time_if_undefined(std::chrono::time_point &time_inout) +{ + // 'undefined' means set to zero + if (time_inout == std::chrono::time_point::min()) + time_inout = std::chrono::steady_clock::now(); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static TaskVariant execute_task(task_t &task) noexcept +{ + try + { + //std::future result{task.get_future()}; + //task(); + //return result.get(); + return task(); + } catch (...) {} + return boost::none; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- + +//------------------------------------------------------------------------------------------------------------------- +// ThreadPool INTERNAL +//------------------------------------------------------------------------------------------------------------------- +void Threadpool::perform_sleepy_queue_maintenance() +{ + // don't do maintenance if there are no unclaimed sleepy tasks (this can allow dead sleepy tasks to linger longer, + // but at the benefit of not performing maintenance when it's not needed) + if (m_num_unclaimed_sleepy_tasks.load(std::memory_order_relaxed) == 0) + return; + + // cycle through the sleepy queues once, cleaning up each queue as we go + const std::chrono::time_point current_time{std::chrono::steady_clock::now()}; + + for (std::uint16_t queue_index{0}; queue_index < m_num_queues; ++queue_index) + { + // perform maintenance on this queue + std::list> awakened_tasks{ + m_sleepy_task_queues[queue_index].try_perform_maintenance(current_time) + }; + + // submit the awakened sleepy tasks + // - note: elements at the bottom of the awakened sleepy tasks are assumed to be higher priority, so we submit + // those first + for (std::unique_ptr &task : awakened_tasks) + { + if (!task) continue; + this->submit_simple_task(std::move(task->sleepy_task).simple_task); + } + } +} +//------------------------------------------------------------------------------------------------------------------- +// ThreadPool INTERNAL +//------------------------------------------------------------------------------------------------------------------- +void Threadpool::submit_simple_task(SimpleTask &&simple_task) +{ + // spin through the simple task queues at our task's priority level + // - start at the task queue one-after the previous start queue as a naive/simple way to spread tasks out evenly + const unsigned char clamped_priority{clamp_priority(m_max_priority_level, simple_task.priority)}; + const std::uint16_t start_counter{m_normal_queue_submission_counter.fetch_add(1, std::memory_order_relaxed)}; + std::uint32_t queue_index{0}; + + for (std::uint32_t i{0}; i < m_num_queues * m_num_submit_cycle_attempts; ++i) + { + // try to push into the specified queue + queue_index = (i + start_counter) % m_num_queues; + + // leave if submitting the task succeeded + if (m_task_queues[clamped_priority][queue_index].try_push(std::move(simple_task).task) == + TokenQueueResult::SUCCESS) + { + m_waiter_manager.notify_one(); + return; + } + } + + // fallback: force insert + queue_index = start_counter % m_num_queues; + m_task_queues[clamped_priority][queue_index].force_push(std::move(simple_task).task); + m_waiter_manager.notify_one(); +} +//------------------------------------------------------------------------------------------------------------------- +// ThreadPool INTERNAL +//------------------------------------------------------------------------------------------------------------------- +void Threadpool::submit_sleepy_task(SleepyTask &&sleepy_task) +{ + // set the start time of sleepy tasks with undefined start time + set_current_time_if_undefined(sleepy_task.wake_time.start_time); + + // if the sleepy task is awake, unwrap its internal simple task + if (sleepy_task_is_awake(sleepy_task)) + { + this->submit(std::move(sleepy_task).simple_task); + return; + } + + // cycle the sleepy queues + const std::uint16_t start_counter{m_sleepy_queue_submission_counter.fetch_add(1, std::memory_order_relaxed)}; + std::uint32_t queue_index{0}; + + for (std::uint32_t i{0}; i < m_num_queues * m_num_submit_cycle_attempts; ++i) + { + // try to push into a queue + queue_index = (i + start_counter) % m_num_queues; + if (!m_sleepy_task_queues[queue_index].try_push(std::move(sleepy_task))) + continue; + + // success + m_num_unclaimed_sleepy_tasks.fetch_add(1, std::memory_order_relaxed); + m_waiter_manager.notify_one(); + return; + } + + // fallback: force insert + queue_index = start_counter % m_num_queues; + m_sleepy_task_queues[queue_index].force_push(std::move(sleepy_task)); + m_num_unclaimed_sleepy_tasks.fetch_add(1, std::memory_order_relaxed); + m_waiter_manager.notify_one(); +} +//------------------------------------------------------------------------------------------------------------------- +// ThreadPool INTERNAL +//------------------------------------------------------------------------------------------------------------------- +boost::optional Threadpool::try_get_simple_task_to_run(const unsigned char max_task_priority, + const std::uint16_t worker_index) +{ + // cycle the simple queues once, from highest to lowest priority (starting at the specified max task priority) + // - note: priority '0' is the highest priority so if the threadpool user adds a priority level, all their highest + // priority tasks will remain highest priority until they manually change them + // - note: we include a 'max task priority' so a worker can choose to only work on low-priority tasks (useful for + // purging the queue when you have multiple contending high-priority self-extending task loops) + task_t new_task; + std::uint16_t queue_index {0}; + + for (unsigned char priority{clamp_priority(m_max_priority_level, max_task_priority)}; + priority <= m_max_priority_level; + ++priority) + { + for (std::uint16_t i{0}; i < m_num_queues; ++i) + { + queue_index = (i + worker_index) % m_num_queues; + + if (m_task_queues[priority][queue_index].try_pop(new_task) == TokenQueueResult::SUCCESS) + return new_task; + } + } + + // failure + return boost::none; +} +//------------------------------------------------------------------------------------------------------------------- +// ThreadPool INTERNAL +//------------------------------------------------------------------------------------------------------------------- +boost::optional Threadpool::try_wait_for_sleepy_task_to_run(const unsigned char max_task_priority, + const std::uint16_t worker_index, + const std::function< + WaiterManager::Result( + const std::uint16_t, + const std::chrono::time_point&, + const WaiterManager::ShutdownPolicy + ) + > &custom_wait_until) +{ + // wait until we have an awake task while listening to the task notification system + SleepingTask* sleeping_task{nullptr}; + boost::optional final_task{}; + bool found_sleepy_task{false}; + std::uint16_t queue_index{0}; + + while (true) + { + // try to grab a sleepy task with the lowest waketime possible + for (std::uint16_t i{0}; i < m_num_queues; ++i) + { + queue_index = (i + worker_index) % m_num_queues; + m_sleepy_task_queues[queue_index].try_swap(max_task_priority, sleeping_task); + } + + // failure: no sleepy task available + if (!sleeping_task) + break; + else if (!found_sleepy_task) + { + // record that there is one fewer unclaimed task in the sleepy queues + m_num_unclaimed_sleepy_tasks.fetch_sub(1, std::memory_order_relaxed); + found_sleepy_task = true; + } + + // wait while listening + // - when shutting down, aggressively awaken sleepy tasks (this tends to burn CPU for tasks that really + // do need to wait, but improves shutdown responsiveness) + const WaiterManager::Result wait_result{ + custom_wait_until(worker_index, + wake_time(sleeping_task->sleepy_task.wake_time), + WaiterManager::ShutdownPolicy::EXIT_EARLY) + }; + + // if we stopped waiting due to a wait condition being satisfied, release our sleepy task + if (wait_result == WaiterManager::Result::CONDITION_TRIGGERED) + { + // release our sleepy task + unclaim_sleeping_task(*sleeping_task); + m_num_unclaimed_sleepy_tasks.fetch_add(1, std::memory_order_relaxed); + + // notify another worker now that our sleepy task is available again + m_waiter_manager.notify_one(); + break; + } + + // if our sleepy task is awake then we can extract its internal task + if (sleepy_task_is_awake(sleeping_task->sleepy_task) || wait_result == WaiterManager::Result::SHUTTING_DOWN) + { + // get the task + final_task = std::move(sleeping_task->sleepy_task).simple_task.task; + + // kill the sleepy task so it can be cleaned up + kill_sleeping_task(*sleeping_task); + + // if we finished waiting due to something other than a timeout, notify another worker + // - if we ended waiting due to a notification, then there is another task in the pool that can be worked + // on, but we are going to work on our awakened sleepy task so we need another worker to grab that new task + // - if we ended waiting due to a shutdown, then we don't want workers to be waiting (unless on a conditional + // wait), so it is fine to aggressively notify in that case + if (wait_result != WaiterManager::Result::TIMEOUT) + m_waiter_manager.notify_one(); + break; + } + + // try to replace our sleepy task with a simple task + if ((final_task = try_get_simple_task_to_run(max_task_priority, worker_index))) + { + // release our sleepy task + unclaim_sleeping_task(*sleeping_task); + m_num_unclaimed_sleepy_tasks.fetch_add(1, std::memory_order_relaxed); + + // notify another worker now that our sleepy task is available again + m_waiter_manager.notify_one(); + break; + } + } + + return final_task; +} +//------------------------------------------------------------------------------------------------------------------- +// ThreadPool INTERNAL +//------------------------------------------------------------------------------------------------------------------- +boost::optional Threadpool::try_get_task_to_run(const unsigned char max_task_priority, + const std::uint16_t worker_index, + const std::function< + WaiterManager::Result( + const std::uint16_t, + const std::chrono::time_point&, + const WaiterManager::ShutdownPolicy + ) + > &custom_wait_until) noexcept +{ + assert(test_threadpool_member_invariants(m_threadpool_id, m_threadpool_owner_id)); + + try + { + // try to find a simple task + if (auto task = try_get_simple_task_to_run(max_task_priority, worker_index)) + return task; + + // try to wait on a sleepy task + if (auto task = try_wait_for_sleepy_task_to_run(max_task_priority, worker_index, custom_wait_until)) + return task; + } catch (...) {} + + // failure + return boost::none; +} +//------------------------------------------------------------------------------------------------------------------- +// ThreadPool INTERNAL +//------------------------------------------------------------------------------------------------------------------- +void Threadpool::run_as_worker_DONT_CALL_ME() +{ + // only call run_as_worker_DONT_CALL_ME() from worker subthreads of the threadpool or from the owner when shutting down + assert(test_threadpool_member_invariants(m_threadpool_id, m_threadpool_owner_id)); + const std::uint16_t worker_id{threadpool_worker_id()}; + assert(worker_id < m_num_queues); + assert(worker_id > 0 || + (thread_context_id() == m_threadpool_owner_id && m_waiter_manager.is_shutting_down())); + + // prepare custom wait-until function + std::function< + WaiterManager::Result( + const std::uint16_t, + const std::chrono::time_point&, + const WaiterManager::ShutdownPolicy + ) + > custom_wait_until{ + [this] + ( + const std::uint16_t worker_id, + const std::chrono::time_point &timepoint, + const WaiterManager::ShutdownPolicy shutdown_policy + ) mutable -> WaiterManager::Result + { + // low priority wait since this will be used for sitting on sleepy tasks + return m_waiter_manager.wait_until(worker_id, timepoint, + shutdown_policy, + WaiterManager::WaitPriority::LOW); + } + }; + + while (true) + { + // try to get the next task, then run it and immediately submit its continuation + // - note: we don't immediately run task continuations because we want to always be pulling tasks from + // the bottom of the task pile + if (auto task = this->try_get_task_to_run(0, worker_id, custom_wait_until)) + { + this->submit(execute_task(*task)); + continue; + } + + // we failed to get a task, so wait until some other worker submits a task and notifies us + // - we only test the shutdown condition immediately after failing to get a task because we want the pool to + // continue draining tasks until it is completely empty (users should directly/manually cancel in-flight tasks + // if that is needed) + // - due to race conditions in the waiter manager, it is possible for workers to shut down even with tasks in + // the queues; typically, the worker that submits a task will be able to pick up that task and finish it, but + // as a fall-back the thread that destroys the threadpool will purge the pool of all tasks + // - we periodically wake up to check the queues in case of race conditions around task submission (submitted + // tasks will always be executed eventually, but may be excessively delayed if we don't wake up here) + // - this is a high priority wait since we are not sitting on any tasks here + if (m_waiter_manager.is_shutting_down()) + break; + m_waiter_manager.wait_for(worker_id, + m_max_wait_duration, + WaiterManager::ShutdownPolicy::EXIT_EARLY, + WaiterManager::WaitPriority::HIGH); + } +} +//------------------------------------------------------------------------------------------------------------------- +// ThreadPool INTERNAL +//------------------------------------------------------------------------------------------------------------------- +void Threadpool::run_as_fanout_worker_DONT_CALL_ME() +{ + // only call run_as_fanout_worker_DONT_CALL_ME() from fanout subthreads of the threadpool + assert(test_threadpool_member_invariants(m_threadpool_id, m_threadpool_owner_id)); + assert(threadpool_worker_id() >= m_num_queues); + + while (true) + { + // try to get a fanout wait condition + // - failure means we are shutting down + FanoutCondition fanout_condition; + if (m_fanout_condition_queue.force_pop(fanout_condition) != TokenQueueResult::SUCCESS) + return; + + // set the fanout worker index + // - we need to set this here since it can't be known in advance + // - don't do any work if we got here after the condition was set + if (!fanout_condition.worker_index || + fanout_condition.worker_index->exchange(threadpool_worker_id(), std::memory_order_acq_rel) != 0) + continue; + + // work while waiting for the fanout condition + this->work_while_waiting(fanout_condition.condition, 0); + } +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- + +//------------------------------------------------------------------------------------------------------------------- +Threadpool::Threadpool(const unsigned char max_priority_level, + const std::uint16_t num_managed_workers, + const unsigned char num_submit_cycle_attempts, + const std::chrono::nanoseconds max_wait_duration) : + m_threadpool_id{s_context_id_counter.fetch_add(1, std::memory_order_relaxed)}, + m_threadpool_owner_id{initialize_threadpool_owner()}, + m_max_priority_level{max_priority_level}, + m_num_queues{static_cast(num_managed_workers + 1)}, //+1 to include the threadpool owner + m_num_submit_cycle_attempts{num_submit_cycle_attempts}, + m_max_wait_duration{max_wait_duration}, + m_waiter_manager{static_cast(2*m_num_queues)} +{ + // create task queues + m_task_queues = std::vector>>{static_cast(m_max_priority_level + 1)}; + + for (auto &priority_queues : m_task_queues) + priority_queues = std::vector>{static_cast(m_num_queues)}; + + // create sleepy task queues + m_sleepy_task_queues = std::vector{m_num_queues}; + + // launch workers + // - note: we reserve worker index 0 for the threadpool owner + m_workers.reserve(m_num_queues - 1); + for (std::uint16_t worker_index{1}; worker_index < m_num_queues; ++worker_index) + { + try + { + m_workers.emplace_back( + [this, worker_index]() mutable + { + initialize_threadpool_worker_thread(this->threadpool_id(), worker_index); + try { this->run_as_worker_DONT_CALL_ME(); } catch (...) { /* can't do anything */ } + } + ); + } + catch (...) { /* can't do anything */ } + } + + // launch fanout workers + // - note: we launch one fanout worker for each main worker (additional worker + threadpool owner) + m_fanout_workers.reserve(m_num_queues); + for (std::uint16_t fanout_worker_index{m_num_queues}; fanout_worker_index < 2*m_num_queues; ++fanout_worker_index) + { + try + { + m_fanout_workers.emplace_back( + [this, fanout_worker_index]() mutable + { + initialize_threadpool_worker_thread(this->threadpool_id(), fanout_worker_index); + try { this->run_as_fanout_worker_DONT_CALL_ME(); } catch (...) { /* can't do anything */ } + } + ); + } + catch (...) { /* can't do anything */ } + } +} +//------------------------------------------------------------------------------------------------------------------- +Threadpool::~Threadpool() +{ + (void)test_threadpool_member_invariants; //suppress unused warning... + assert(test_threadpool_member_invariants(m_threadpool_id, m_threadpool_owner_id)); + assert(thread_context_id() == m_threadpool_owner_id); //only the owner may destroy the object + + // shut down the pool + try { this->shut_down(); } catch (...) {} + + // join all workers + for (std::thread &worker : m_workers) + try { worker.join(); } catch (...) {} + + for (std::thread &fanout_worker : m_fanout_workers) + try { fanout_worker.join(); } catch (...) {} + + // clear out any tasks lingering in the pool + try { this->run_as_worker_DONT_CALL_ME(); } catch (...) {} + + //todo: if there was an exception above then the threadpool may hang or lead to UB, so maybe it would be best to + // just abort when an exception is detected +} +//------------------------------------------------------------------------------------------------------------------- +bool Threadpool::submit(TaskVariant task) noexcept +{ + assert(test_threadpool_member_invariants(m_threadpool_id, m_threadpool_owner_id)); + + // submit the task + try + { + // case: empty task + if (!task) ; //skip ahead to sleepy queue maintenance + // case: simple task + else if (SimpleTask *simpletask = task.try_unwrap()) + this->submit_simple_task(std::move(*simpletask)); + // case: sleepy task + else if (SleepyTask *sleepytask = task.try_unwrap()) + this->submit_sleepy_task(std::move(*sleepytask)); + + // maintain the sleepy queues + this->perform_sleepy_queue_maintenance(); + } catch (...) { return false; } + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +join_signal_t Threadpool::make_join_signal() +{ + return std::make_shared>(); +} +//------------------------------------------------------------------------------------------------------------------- +join_token_t Threadpool::get_join_token(join_signal_t &join_signal_inout) +{ + assert(test_threadpool_member_invariants(m_threadpool_id, m_threadpool_owner_id)); + + return std::make_shared( + [ + this, + l_waiter_index = threadpool_worker_id(), + l_threadpool_id = m_threadpool_id, + l_threadpool_owner_id = m_threadpool_owner_id, + l_join_signal = join_signal_inout + ]() mutable + { + // set the signal early in case the invariants were violated + if (l_join_signal) { try { l_join_signal->store(true, std::memory_order_relaxed); } catch (...) {} } + + // check thread id invariants (to avoid accessing a dangling 'this' pointer) + if (!test_threadpool_member_invariants(l_threadpool_id, l_threadpool_owner_id)) return; + + // notify any waiter + m_waiter_manager.notify_conditional_waiter(l_waiter_index, [](){}); + } + ); +} +//------------------------------------------------------------------------------------------------------------------- +join_condition_t Threadpool::get_join_condition(join_signal_t &&join_signal_in, join_token_t &&join_token_in) +{ + // clear the joiner's copy of the join token + join_token_in = nullptr; + + // create the join condition + return + [l_join_signal = std::move(join_signal_in)]() -> bool + { + return !l_join_signal || l_join_signal->load(std::memory_order_relaxed); + }; +} +//------------------------------------------------------------------------------------------------------------------- +void Threadpool::work_while_waiting(const std::chrono::time_point &deadline, + const unsigned char max_task_priority) +{ + assert(test_threadpool_member_invariants(m_threadpool_id, m_threadpool_owner_id)); + const std::uint16_t worker_id{threadpool_worker_id()}; + + // prepare custom wait-until function + std::function< + WaiterManager::Result( + const std::uint16_t, + const std::chrono::time_point&, + const WaiterManager::ShutdownPolicy + ) + > custom_wait_until{ + [this, &deadline] + ( + const std::uint16_t worker_id, + const std::chrono::time_point &timepoint, + const WaiterManager::ShutdownPolicy shutdown_policy + ) mutable -> WaiterManager::Result + { + const WaiterManager::Result wait_result{ + m_waiter_manager.wait_until(worker_id, + (timepoint < deadline) ? timepoint : deadline, //don't wait longer than the deadline + shutdown_policy, + WaiterManager::WaitPriority::LOW) //low priority wait since we are sitting on a timer + }; + + // treat the deadline as a condition + if (std::chrono::steady_clock::now() >= deadline) + return WaiterManager::Result::CONDITION_TRIGGERED; + return wait_result; + } + }; + + // work until the deadline + while (std::chrono::steady_clock::now() < deadline) + { + // try to get the next task, then run it and immediately submit its continuation + if (auto task = this->try_get_task_to_run(max_task_priority, worker_id, custom_wait_until)) + { + this->submit(execute_task(*task)); + continue; + } + + // we failed to get a task, so wait until the deadline + const WaiterManager::Result wait_result{ + custom_wait_until(worker_id, deadline, WaiterManager::ShutdownPolicy::WAIT) + }; + + // exit immediately if the deadline condition was triggered (don't re-test it) + if (wait_result == WaiterManager::Result::CONDITION_TRIGGERED) + break; + } +} +//------------------------------------------------------------------------------------------------------------------- +void Threadpool::work_while_waiting(const std::chrono::nanoseconds &duration, const unsigned char max_task_priority) +{ + this->work_while_waiting(std::chrono::steady_clock::now() + duration, max_task_priority); +} +//------------------------------------------------------------------------------------------------------------------- +void Threadpool::work_while_waiting(const std::function &wait_condition_func, + const unsigned char max_task_priority) +{ + assert(test_threadpool_member_invariants(m_threadpool_id, m_threadpool_owner_id)); + const std::uint16_t worker_id{threadpool_worker_id()}; + + // prepare custom wait-until function + std::function< + WaiterManager::Result( + const std::uint16_t, + const std::chrono::time_point&, + const WaiterManager::ShutdownPolicy + ) + > custom_wait_until{ + [this, &wait_condition_func] + ( + const std::uint16_t worker_id, + const std::chrono::time_point &timepoint, + const WaiterManager::ShutdownPolicy shutdown_policy + ) mutable -> WaiterManager::Result + { + return m_waiter_manager.conditional_wait_until(worker_id, + wait_condition_func, + timepoint, + shutdown_policy); + } + }; + + // work until the wait condition is satisfied + while (!wait_condition_func()) + { + // try to get the next task, then run it and immediately submit its continuation + if (auto task = this->try_get_task_to_run(max_task_priority, worker_id, custom_wait_until)) + { + this->submit(execute_task(*task)); + continue; + } + + // we failed to get a task, so wait until the condition is satisfied + const WaiterManager::Result wait_result{ + custom_wait_until(worker_id, + std::chrono::steady_clock::now() + m_max_wait_duration, + WaiterManager::ShutdownPolicy::WAIT) + }; + + // exit immediately if the condition was triggered (don't re-test it) + if (wait_result == WaiterManager::Result::CONDITION_TRIGGERED) + break; + } +} +//------------------------------------------------------------------------------------------------------------------- +fanout_token_t Threadpool::launch_temporary_worker() +{ + assert(test_threadpool_member_invariants(m_threadpool_id, m_threadpool_owner_id)); + + // 1. join signal + join_signal_t join_signal{this->make_join_signal()}; + + // 2. worker index channel for targeted notifications + std::shared_ptr> worker_index_channel{std::make_shared>(0)}; + + // 3. fanout condition + // - when condition is satisfied, return one fanout worker to the fanout pool + m_fanout_condition_queue.force_push( + FanoutCondition{ + .worker_index = worker_index_channel, + .condition = + [l_join_signal = join_signal]() -> bool + { + return !l_join_signal || l_join_signal->load(std::memory_order_relaxed); + } + } + ); + + // 4. fanout token + // - when token is destroyed, the fanout condition will be triggered + return std::make_unique( + [ + this, + l_threadpool_id = m_threadpool_id, + l_threadpool_owner_id = m_threadpool_owner_id, + l_join_signal = std::move(join_signal), + l_worker_index_channel = std::move(worker_index_channel) + ]() mutable + { + // leave early if we got here before the worker id became available + if (!l_worker_index_channel) return; + const std::uint64_t worker_id{ + l_worker_index_channel->exchange(static_cast(-1), std::memory_order_acq_rel) + }; + if (worker_id == 0) return; + + // set the signal early in case the invariants were violated + if (l_join_signal) { try { l_join_signal->store(true, std::memory_order_relaxed); } catch (...) {} } + + // check thread id invariants (to avoid accessing a dangling 'this' pointer) + if (!test_threadpool_member_invariants(l_threadpool_id, l_threadpool_owner_id)) return; + + // notify the waiting fanout worker + m_waiter_manager.notify_conditional_waiter(worker_id, [](){}); + } + ); +} +//------------------------------------------------------------------------------------------------------------------- +void Threadpool::shut_down() noexcept +{ + // shut down the fanout queue + m_fanout_condition_queue.shut_down(); + + // shut down the waiter manager, which should notify any waiting workers + m_waiter_manager.shut_down(); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace async diff --git a/src/async/threadpool.h b/src/async/threadpool.h new file mode 100644 index 0000000000..7b4b99bf7d --- /dev/null +++ b/src/async/threadpool.h @@ -0,0 +1,218 @@ +// Copyright (c) 2023, 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. + +/// threadpool + +#pragma once + +//local headers +#include "scoped_notification.h" +#include "sleepy_task_queue.h" +#include "task_types.h" +#include "token_queue.h" +#include "waiter_manager.h" + +//third-party headers +#include +#include + +//standard headers +#include +#include +#include +#include +#include +#include +#include + +//forward declarations + + +namespace async +{ + +/// join signal/token/condition helper types +using join_signal_t = std::shared_ptr>; +using join_token_t = std::shared_ptr; +using join_condition_t = std::function; + +/// fanout token helper type +using fanout_token_t = std::unique_ptr; + +struct FanoutCondition final +{ + std::shared_ptr> worker_index; + std::function condition; +}; + +/// thread pool +class Threadpool final +{ + /// clean up pass on the sleepy queues + void perform_sleepy_queue_maintenance(); + + /// submit task types + void submit_simple_task(SimpleTask &&simple_task); + void submit_sleepy_task(SleepyTask &&sleepy_task); + + /// get a task to run + boost::optional try_get_simple_task_to_run(const unsigned char max_task_priority, + const std::uint16_t worker_index); + boost::optional try_wait_for_sleepy_task_to_run(const unsigned char max_task_priority, + const std::uint16_t worker_index, + const std::function< + WaiterManager::Result( + const std::uint16_t, + const std::chrono::time_point&, + const WaiterManager::ShutdownPolicy + ) + > &custom_wait_until); + boost::optional try_get_task_to_run(const unsigned char max_task_priority, + const std::uint16_t worker_index, + const std::function< + WaiterManager::Result( + const std::uint16_t, + const std::chrono::time_point&, + const WaiterManager::ShutdownPolicy + ) + > &custom_wait_until) noexcept; + +public: + /// run as a pool worker + /// - this function is only invoked when launching pool workers and from within the pool destructor + void run_as_worker_DONT_CALL_ME(); + /// run as a pool fanout worker + /// - this function is only invoked when launching pool fanout workers + void run_as_fanout_worker_DONT_CALL_ME(); + +//constructors + /// default constructor: disabled + Threadpool() = delete; + /// normal constructor: from config + Threadpool(const unsigned char max_priority_level, + const std::uint16_t num_managed_workers, + const unsigned char num_submit_cycle_attempts, + const std::chrono::nanoseconds max_wait_duration); + + /// disable copy/moves so references to this object can't be invalidated until this object's lifetime ends + Threadpool& operator=(Threadpool&&) = delete; + +//destructor + /// destroy the threadpool + /// 1) shuts down the pool + /// 2) joins all worker threads + /// 3) clears out any remaining tasks + /// - note that this ensures any ScopedNotifications attached to tasks will be executed before the pool dies, + /// which ensures references in those notifications + ~Threadpool(); + +//member functions + /// submit a task + /// - note: if submit() returns true, then it is guaranteed the submission succeeded; otherwise it is unspecified + /// what happened to the task (it may have been submitted, or an exception may have caused it to be dropped) + bool submit(TaskVariant task) noexcept; + + /// toolkit for manually joining on a set of tasks + /// - how to use this: + /// 1) make a new join signal in the thread that will be joining on a set of tasks yet to be launched + /// 2) [get_join_token()]: get a new join token using the join signal + /// 3) save a copy of the token in the lambda capture of each task in the set of tasks that you want to join on + /// 4) [get_join_condition()]: consume the joining thread's copy of the join token and the join signal to get the + /// join condition + /// 5) call ThreadPool::work_while_waiting() from the joining thread, using that join condition + /// + /// - PRECONDITION: the thread that joins on a join token must be the same thread that created that token + /// - PRECONDITION: there must be NO living copies of a join token after the corresponding threadpool has died + static join_signal_t make_join_signal(); + join_token_t get_join_token(join_signal_t &join_signal_inout); + static join_condition_t get_join_condition(join_signal_t &&join_signal_in, join_token_t &&join_token_in); + + void work_while_waiting(const std::chrono::time_point &deadline, + const unsigned char max_task_priority = 0); + void work_while_waiting(const std::chrono::nanoseconds &duration, const unsigned char max_task_priority = 0); + void work_while_waiting(const std::function &wait_condition_func, const unsigned char max_task_priority = 0); + + /// launch a temporary worker + fanout_token_t launch_temporary_worker(); + + /// shut down the threadpool + void shut_down() noexcept; + + /// id of this threadpool + std::uint64_t threadpool_id() const { return m_threadpool_id; } + std::uint64_t threadpool_owner_id() const { return m_threadpool_owner_id; } + +private: +//member variables + /// config + const std::uint64_t m_threadpool_id; //unique identifier for this threadpool + const std::uint64_t m_threadpool_owner_id; //unique identifier for the thread that owns this threadpool + const unsigned char m_max_priority_level; //note: priority 0 is the 'highest' priority + const std::uint16_t m_num_queues; //num workers + 1 for the threadpool owner + const unsigned char m_num_submit_cycle_attempts; + const std::chrono::nanoseconds m_max_wait_duration; + + /// worker context + std::vector m_workers; + std::vector m_fanout_workers; //extra workers that can be manually activated + + /// queues + std::vector>> m_task_queues; //outer vector: priorities, inner vector: workers + std::vector m_sleepy_task_queues; //vector: workers + TokenQueue m_fanout_condition_queue; + std::atomic m_normal_queue_submission_counter{0}; + std::atomic m_sleepy_queue_submission_counter{0}; + std::atomic m_num_unclaimed_sleepy_tasks{0}; + + // waiter manager + WaiterManager m_waiter_manager; +}; + + +/// default priorities +enum DefaultPriorityLevels : unsigned char +{ + MAX = 0, + MEDIUM, + LOW, + MIN = LOW +}; + +/// default threadpool +inline Threadpool& get_default_threadpool() +{ + static Threadpool default_threadpool{ + static_cast(DefaultPriorityLevels::MIN), + static_cast(std::max(2u, std::thread::hardware_concurrency()) - 1), + 20, + std::chrono::milliseconds(500) + }; + return default_threadpool; +} + +} //namespace asyc diff --git a/src/async/token_queue.h b/src/async/token_queue.h new file mode 100644 index 0000000000..4e72fefa24 --- /dev/null +++ b/src/async/token_queue.h @@ -0,0 +1,139 @@ +// Copyright (c) 2023, 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. + +/// Simple token queue. + +#pragma once + +//local headers + +//third-party headers + +//standard headers +#include +#include +#include +#include + +//forward declarations + + +namespace async +{ + +enum class TokenQueueResult : unsigned char +{ + SUCCESS, + QUEUE_EMPTY, + TRY_LOCK_FAIL, + SHUTTING_DOWN +}; + +/// async token queue +template +class TokenQueue final +{ +public: +//member functions + /// try to add an element to the top + template + TokenQueueResult try_push(T &&new_element_in) + { + { + std::unique_lock lock{m_mutex, std::try_to_lock}; + if (!lock.owns_lock()) + return TokenQueueResult::TRY_LOCK_FAIL; + + m_queue.emplace_back(std::forward(new_element_in)); + } + m_condvar.notify_one(); + return TokenQueueResult::SUCCESS; + } + /// add an element to the top (always succeeds) + template + void force_push(T &&new_element_in) + { + { + std::lock_guard lock{m_mutex}; + m_queue.emplace_back(std::forward(new_element_in)); + } + m_condvar.notify_one(); + } + + /// try to remove an element from the bottom + TokenQueueResult try_pop(TokenT &token_out) + { + // try to lock the queue, then check if there are any elements + std::unique_lock lock{m_mutex, std::try_to_lock}; + if (!lock.owns_lock()) + return TokenQueueResult::TRY_LOCK_FAIL; + if (m_queue.size() == 0) + return TokenQueueResult::QUEUE_EMPTY; + + // pop the bottom element + token_out = std::move(m_queue.front()); + m_queue.pop_front(); + return TokenQueueResult::SUCCESS; + } + /// remove an element from the bottom (always succeeds) + TokenQueueResult force_pop(TokenT &token_out) + { + // lock and wait until the queue is not empty or the queue is shutting down + std::unique_lock lock{m_mutex}; + m_condvar.wait(lock, [this]() -> bool { return m_queue.size() > 0 || m_is_shutting_down; }); + if (m_queue.size() == 0 && m_is_shutting_down) + return TokenQueueResult::SHUTTING_DOWN; + else if (m_queue.size() == 0) + return TokenQueueResult::QUEUE_EMPTY; + + // pop the bottom element + token_out = std::move(m_queue.front()); + m_queue.pop_front(); + return TokenQueueResult::SUCCESS; + } + + /// shut down the queue + void shut_down() + { + { + std::lock_guard lock{m_mutex}; + m_is_shutting_down = true; + } + m_condvar.notify_all(); + } + +private: +//member variables + /// queue context + std::list m_queue; + std::mutex m_mutex; + std::condition_variable m_condvar; + bool m_is_shutting_down{false}; +}; + +} //namespace asyc diff --git a/src/async/waiter_manager.cpp b/src/async/waiter_manager.cpp new file mode 100644 index 0000000000..796162ef4f --- /dev/null +++ b/src/async/waiter_manager.cpp @@ -0,0 +1,279 @@ +// Copyright (c) 2022, 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 "waiter_manager.h" + +//local headers + +//third party headers + +//standard headers +#include +#include +#include +#include +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "async" + +namespace async +{ +//------------------------------------------------------------------------------------------------------------------- +// WaiterManager INTERNAL +//------------------------------------------------------------------------------------------------------------------- +std::uint16_t WaiterManager::clamp_waiter_index(const std::uint16_t nominal_index) noexcept +{ + assert(m_num_managed_waiters > 0); + if (nominal_index >= m_num_managed_waiters) + return m_num_managed_waiters - 1; + return nominal_index; +} +//------------------------------------------------------------------------------------------------------------------- +// WaiterManager INTERNAL +// - note: the order of result checks is intentional based on their assumed importance to the caller +//------------------------------------------------------------------------------------------------------------------- +WaiterManager::Result WaiterManager::wait_impl(std::mutex &mutex_inout, + std::condition_variable_any &condvar_inout, + std::atomic &counter_inout, + const std::function &condition_checker_func, + const std::function&)> &wait_func, + const WaiterManager::ShutdownPolicy shutdown_policy) noexcept +{ + try + { + std::unique_lock lock{mutex_inout}; + + // pre-wait checks + if (condition_checker_func) + { + try { if (condition_checker_func()) return Result::CONDITION_TRIGGERED; } + catch (...) { return Result::CONDITION_TRIGGERED; } + } + if (shutdown_policy == WaiterManager::ShutdownPolicy::EXIT_EARLY && this->is_shutting_down()) + return Result::SHUTTING_DOWN; + + // wait + // note: using a signed int for counters means underflow due to reordering of the decrement won't yield a value > 0 + counter_inout.fetch_add(1, std::memory_order_relaxed); + const std::cv_status wait_status{wait_func(condvar_inout, lock)}; + counter_inout.fetch_sub(1, std::memory_order_relaxed); + + // post-wait checks + if (condition_checker_func) + { + try { if (condition_checker_func()) return Result::CONDITION_TRIGGERED; } + catch (...) { return Result::CONDITION_TRIGGERED; } + } + if (this->is_shutting_down()) return Result::SHUTTING_DOWN; + if (wait_status == std::cv_status::timeout) return Result::TIMEOUT; + + return Result::DONE_WAITING; + } + catch (...) { return Result::CRITICAL_EXCEPTION; } +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- + +//------------------------------------------------------------------------------------------------------------------- +WaiterManager::WaiterManager(const std::uint16_t num_managed_waiters) : + //we always want at least one waiter slot to avoid UB + m_num_managed_waiters{static_cast(num_managed_waiters > 0 ? num_managed_waiters : 1)} +{ + m_waiter_mutexes = std::vector{m_num_managed_waiters}; + m_conditional_waiters = std::vector{m_num_managed_waiters}; +} +//------------------------------------------------------------------------------------------------------------------- +WaiterManager::Result WaiterManager::wait(const std::uint16_t waiter_index, + const WaiterManager::ShutdownPolicy shutdown_policy, + const WaitPriority wait_priority) noexcept +{ + return this->wait_impl(m_waiter_mutexes[this->clamp_waiter_index(waiter_index)], + (wait_priority == WaitPriority::HIGH) ? m_primary_shared_cond_var : m_secondary_shared_cond_var, + (wait_priority == WaitPriority::HIGH) ? m_num_primary_waiters : m_num_secondary_waiters, + nullptr, + [](std::condition_variable_any &cv_inout, std::unique_lock &lock) -> std::cv_status + { + cv_inout.wait(lock); + return std::cv_status::no_timeout; + }, + shutdown_policy + ); +} +//------------------------------------------------------------------------------------------------------------------- +WaiterManager::Result WaiterManager::wait_for(const std::uint16_t waiter_index, + const std::chrono::nanoseconds &duration, + const WaiterManager::ShutdownPolicy shutdown_policy, + const WaitPriority wait_priority) noexcept +{ + return this->wait_impl(m_waiter_mutexes[this->clamp_waiter_index(waiter_index)], + (wait_priority == WaitPriority::HIGH) ? m_primary_shared_cond_var : m_secondary_shared_cond_var, + (wait_priority == WaitPriority::HIGH) ? m_num_primary_waiters : m_num_secondary_waiters, + nullptr, + [&duration](std::condition_variable_any &cv_inout, std::unique_lock &lock_inout) -> std::cv_status + { + return cv_inout.wait_for(lock_inout, duration); + }, + shutdown_policy + ); +} +//------------------------------------------------------------------------------------------------------------------- +WaiterManager::Result WaiterManager::wait_until(const std::uint16_t waiter_index, + const std::chrono::time_point &timepoint, + const WaiterManager::ShutdownPolicy shutdown_policy, + const WaitPriority wait_priority) noexcept +{ + return this->wait_impl(m_waiter_mutexes[this->clamp_waiter_index(waiter_index)], + (wait_priority == WaitPriority::HIGH) ? m_primary_shared_cond_var : m_secondary_shared_cond_var, + (wait_priority == WaitPriority::HIGH) ? m_num_primary_waiters : m_num_secondary_waiters, + nullptr, + [&timepoint](std::condition_variable_any &cv_inout, std::unique_lock &lock_inout) -> std::cv_status + { + return cv_inout.wait_until(lock_inout, timepoint); + }, + shutdown_policy + ); +} +//------------------------------------------------------------------------------------------------------------------- +WaiterManager::Result WaiterManager::conditional_wait(const std::uint16_t waiter_index, + const std::function &condition_checker_func, + const WaiterManager::ShutdownPolicy shutdown_policy) noexcept +{ + const std::uint16_t clamped_waiter_index{this->clamp_waiter_index(waiter_index)}; + return this->wait_impl(m_waiter_mutexes[clamped_waiter_index], + m_conditional_waiters[clamped_waiter_index].cond_var, + m_conditional_waiters[clamped_waiter_index].num_waiting, + condition_checker_func, + [](std::condition_variable_any &cv_inout, std::unique_lock &lock_inout) -> std::cv_status + { + cv_inout.wait(lock_inout); + return std::cv_status::no_timeout; + }, + shutdown_policy + ); +} +//------------------------------------------------------------------------------------------------------------------- +WaiterManager::Result WaiterManager::conditional_wait_for(const std::uint16_t waiter_index, + const std::function &condition_checker_func, + const std::chrono::nanoseconds &duration, + const WaiterManager::ShutdownPolicy shutdown_policy) noexcept +{ + const std::uint16_t clamped_waiter_index{this->clamp_waiter_index(waiter_index)}; + return this->wait_impl(m_waiter_mutexes[clamped_waiter_index], + m_conditional_waiters[clamped_waiter_index].cond_var, + m_conditional_waiters[clamped_waiter_index].num_waiting, + condition_checker_func, + [&duration](std::condition_variable_any &cv_inout, std::unique_lock &lock_inout) -> std::cv_status + { + return cv_inout.wait_for(lock_inout, duration); + }, + shutdown_policy + ); +} +//------------------------------------------------------------------------------------------------------------------- +WaiterManager::Result WaiterManager::conditional_wait_until(const std::uint16_t waiter_index, + const std::function &condition_checker_func, + const std::chrono::time_point &timepoint, + const WaiterManager::ShutdownPolicy shutdown_policy) noexcept +{ + const std::uint16_t clamped_waiter_index{this->clamp_waiter_index(waiter_index)}; + return this->wait_impl(m_waiter_mutexes[clamped_waiter_index], + m_conditional_waiters[clamped_waiter_index].cond_var, + m_conditional_waiters[clamped_waiter_index].num_waiting, + condition_checker_func, + [&timepoint](std::condition_variable_any &cv_inout, std::unique_lock &lock_inout) + -> std::cv_status + { + return cv_inout.wait_until(lock_inout, timepoint); + }, + shutdown_policy + ); +} +//------------------------------------------------------------------------------------------------------------------- +void WaiterManager::notify_one() noexcept +{ + // try to notify a normal waiter + if (m_num_primary_waiters.load(std::memory_order_relaxed) > 0) + { + m_primary_shared_cond_var.notify_one(); + return; + } + + // try to notify a sleepy waiter + if (m_num_secondary_waiters.load(std::memory_order_relaxed) > 0) + { + m_secondary_shared_cond_var.notify_one(); + return; + } + + // find a conditional waiter to notify + for (ConditionalWaiterContext &conditional_waiter : m_conditional_waiters) + { + if (conditional_waiter.num_waiting.load(std::memory_order_relaxed) > 0) + { + conditional_waiter.cond_var.notify_one(); + break; + } + } +} +//------------------------------------------------------------------------------------------------------------------- +void WaiterManager::notify_all() noexcept +{ + m_primary_shared_cond_var.notify_all(); + m_secondary_shared_cond_var.notify_all(); + for (ConditionalWaiterContext &conditional_waiter : m_conditional_waiters) + conditional_waiter.cond_var.notify_all(); +} +//------------------------------------------------------------------------------------------------------------------- +void WaiterManager::notify_conditional_waiter(const std::uint16_t waiter_index, + std::function condition_setter_func) noexcept +{ + const std::uint16_t clamped_waiter_index{this->clamp_waiter_index(waiter_index)}; + if (condition_setter_func) try { condition_setter_func(); } catch (...) {} + // tap the mutex here to synchronize with conditional waiters + { const std::lock_guard lock{m_waiter_mutexes[clamped_waiter_index]}; }; + // notify all because if there are multiple threads waiting on this index (not recommended, but possible), + // we don't know which one actually cares about this condition function + m_conditional_waiters[clamped_waiter_index].cond_var.notify_all(); +} +//------------------------------------------------------------------------------------------------------------------- +void WaiterManager::shut_down() noexcept +{ + // shut down + m_shutting_down.store(true, std::memory_order_relaxed); + + // tap all the mutexes to synchronize with waiters + for (std::mutex &mutex : m_waiter_mutexes) + try { const std::lock_guard lock{mutex}; } catch (...) {} + + // notify all waiters + this->notify_all(); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace async diff --git a/src/async/waiter_manager.h b/src/async/waiter_manager.h new file mode 100644 index 0000000000..bfd30119a1 --- /dev/null +++ b/src/async/waiter_manager.h @@ -0,0 +1,169 @@ +// Copyright (c) 2023, 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. + +/// Coordinates async wait/notify for a threadpool. + +#pragma once + +//local headers + +//third-party headers + +//standard headers +#include +#include +#include +#include +#include +#include + +//forward declarations + + +namespace async +{ + +/// WaiterManager +/// - performance will decrease significantly if multiple threads try to claim the same waiter index +/// - notify_one() prioritizes: primary waiters > secondary waiters > conditional waiters +/// - this function has several race conditions that can mean no worker gets notified even if there are several actually +/// waiting (these are non-critical race conditions that marginally reduce throughput under low to moderate load) +/// - there is also a race condition where a conditional waiter gets notified but ends up detecting its condition was +/// triggered, implying it will go down some custom upstream control path instead of the normal path that +/// 'notify_one()' is aimed at (e.g. 'go find a task to work on'); (this marginally reduces throughput under moderate +/// to high load) +/// - conditional waiting is designed so a conditional waiter will never wait after its condition is set if a conditional +/// notify is used to set the condition +/// - COST: the condition setting/checking is protected by a unique lock, so any real system WILL waste time fighting +/// over those locks (to maximize throughput, consider using large task graphs to avoid manual joining and other +/// mechanisms that use conditional waits) +/// - 'shutting down' the manager means +/// A) existing waiters will all be woken up +/// B) future waiters using 'ShutdownPolicy::EXIT_EARLY' will simply exit without waiting +class WaiterManager final +{ +public: + enum class ShutdownPolicy : unsigned char + { + WAIT, + EXIT_EARLY + }; + + enum class WaitPriority : unsigned char + { + HIGH, + LOW + }; + + enum class Result : unsigned char + { + CONDITION_TRIGGERED, + SHUTTING_DOWN, + TIMEOUT, + DONE_WAITING, + CRITICAL_EXCEPTION + }; + +private: + struct ConditionalWaiterContext final + { + std::atomic num_waiting; + std::condition_variable_any cond_var; + }; + + std::uint16_t clamp_waiter_index(const std::uint16_t nominal_index) noexcept; + + /// wait + Result wait_impl(std::mutex &mutex_inout, + std::condition_variable_any &condvar_inout, + std::atomic &counter_inout, + const std::function &condition_checker_func, + const std::function&)> &wait_func, + const ShutdownPolicy shutdown_policy) noexcept; + +public: +//constructors + WaiterManager(const std::uint16_t num_managed_waiters); + +//overloaded operators + /// disable copy/moves so references to this object can't be invalidated until this object's lifetime ends + WaiterManager& operator=(WaiterManager&&) = delete; + +//member functions + Result wait(const std::uint16_t waiter_index, + const ShutdownPolicy shutdown_policy, + const WaitPriority wait_priority) noexcept; + Result wait_for(const std::uint16_t waiter_index, + const std::chrono::nanoseconds &duration, + const ShutdownPolicy shutdown_policy, + const WaitPriority wait_priority) noexcept; + Result wait_until(const std::uint16_t waiter_index, + const std::chrono::time_point &timepoint, + const ShutdownPolicy shutdown_policy, + const WaitPriority wait_priority) noexcept; + Result conditional_wait(const std::uint16_t waiter_index, + const std::function &condition_checker_func, + const ShutdownPolicy shutdown_policy) noexcept; + Result conditional_wait_for(const std::uint16_t waiter_index, + const std::function &condition_checker_func, + const std::chrono::nanoseconds &duration, + const ShutdownPolicy shutdown_policy) noexcept; + Result conditional_wait_until(const std::uint16_t waiter_index, + const std::function &condition_checker_func, + const std::chrono::time_point &timepoint, + const ShutdownPolicy shutdown_policy) noexcept; + + void notify_one() noexcept; + void notify_all() noexcept; + void notify_conditional_waiter(const std::uint16_t waiter_index, + std::function condition_setter_func) noexcept; + + void shut_down() noexcept; + + bool is_shutting_down() const noexcept { return m_shutting_down.load(std::memory_order_relaxed); } + +private: +//member variables + /// config + const std::uint16_t m_num_managed_waiters; + + /// manager status + std::atomic m_num_primary_waiters{0}; + std::atomic m_num_secondary_waiters{0}; + std::atomic m_shutting_down{false}; + + /// synchronization + std::vector m_waiter_mutexes; + std::condition_variable_any m_primary_shared_cond_var; + std::condition_variable_any m_secondary_shared_cond_var; + + /// conditional waiters + std::vector m_conditional_waiters; +}; + +} //namespace asyc diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index 7f0b0c18f8..cf38466fe2 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -81,6 +81,9 @@ target_link_libraries(common PRIVATE ${OPENSSL_LIBRARIES} ${EXTRA_LIBRARIES}) +target_include_directories(common + PRIVATE + ${Boost_INCLUDE_DIRS}) #monero_install_headers(common # ${common_headers}) diff --git a/src/common/container_helpers.h b/src/common/container_helpers.h new file mode 100644 index 0000000000..5824c9c374 --- /dev/null +++ b/src/common/container_helpers.h @@ -0,0 +1,170 @@ +// Copyright (c) 2022, 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. + +// Miscellaneous container helpers. + +#pragma once + +//local headers + +//third party headers + +//standard headers +#include +#include +#include + +//forward declarations + + +namespace tools +{ + +/// convert an arbitrary function to functor +template +inline auto as_functor(F f) +{ + return [f = std::move(f)](auto&&... args) { return f(std::forward(args)...); }; +} +/// convert a binary comparison function to a functor +/// note: for most use-cases 'const T&' will work because only non-trivial types need a user-defined comparison operation +template +inline auto compare_func(ComparisonOpT comparison_op_func) +{ + static_assert( + std::is_same< + bool, + decltype( + comparison_op_func( + std::declval>(), + std::declval>() + ) + ) + >::value, + "invalid callable - expected callable in form bool(T, T)" + ); + + return as_functor(std::move(comparison_op_func)); +} +/// test if a container is sorted and unique according to a comparison criteria (defaults to operator<) +/// NOTE: ComparisonOpT must establish 'strict weak ordering' https://en.cppreference.com/w/cpp/named_req/Compare +template > +bool is_sorted_and_unique(const T &container, ComparisonOpT comparison_op = ComparisonOpT{}) +{ + using ValueT = typename T::value_type; + static_assert( + std::is_same< + bool, + decltype( + comparison_op( + std::declval>(), + std::declval>() + ) + ) + >::value, + "invalid callable - expected callable in form bool(ValueT, ValueT)" + ); + + if (!std::is_sorted(container.begin(), container.end(), comparison_op)) + return false; + + if (std::adjacent_find(container.begin(), + container.end(), + [comparison_op = std::move(comparison_op)](const ValueT &a, const ValueT &b) -> bool + { + return !comparison_op(a, b) && !comparison_op(b, a); + }) + != container.end()) + return false; + + return true; +} +/// specialization for raw function pointers +template +bool is_sorted_and_unique(const T &container, + bool (*const comparison_op_func)(const typename T::value_type&, const typename T::value_type&)) +{ + return is_sorted_and_unique(container, compare_func(comparison_op_func)); +} +/// convenience wrapper for checking if the key to a mapped object is correct for that object +/// example: std::unorderd_map> where the map key is supposed to +/// reproduce the pair's rct::key; use the predicate to check that relationship +template +bool keys_match_internal_values(const std::unordered_map &map, PredT check_key_func) +{ + static_assert( + std::is_same< + bool, + decltype( + check_key_func( + std::declval>(), + std::declval>() + ) + ) + >::value, + "invalid callable - expected callable in form bool(KeyT, ValueT)" + ); + + for (const auto &map_element : map) + { + if (!check_key_func(map_element.first, map_element.second)) + return false; + } + + return true; +} +/// convenience wrapper for getting the last element after emplacing back +template +typename ContainerT::value_type& add_element(ContainerT &container) +{ + container.emplace_back(); + return container.back(); +} +/// convenience erasor for unordered maps: std::erase_if(std::unordered_map) is C++20 +template +void for_all_in_map_erase_if(std::unordered_map &map_inout, PredT predicate) +{ + using MapValueT = typename std::unordered_map::value_type; + static_assert( + std::is_same< + bool, + decltype(predicate(std::declval>())) + >::value, + "invalid callable - expected callable in form bool(Value)" + ); + + for (auto map_it = map_inout.begin(); map_it != map_inout.end();) + { + if (predicate(*map_it)) + map_it = map_inout.erase(map_it); + else + ++map_it; + } +} + +} //namespace tools diff --git a/src/common/timings.cc b/src/common/timings.cc index 612ac2cc67..46a759f366 100644 --- a/src/common/timings.cc +++ b/src/common/timings.cc @@ -12,10 +12,11 @@ TimingsDatabase::TimingsDatabase() { } -TimingsDatabase::TimingsDatabase(const std::string &filename): +TimingsDatabase::TimingsDatabase(const std::string &filename, const bool load_previous /*=false*/): filename(filename) { - load(); + if (load_previous) + load(); } TimingsDatabase::~TimingsDatabase() @@ -73,53 +74,100 @@ bool TimingsDatabase::load() { i.deciles.push_back(atoi(fields[idx++].c_str())); } - instances.insert(std::make_pair(name, i)); + instances.emplace_back(name, i); } fclose(f); return true; } -bool TimingsDatabase::save() +bool TimingsDatabase::save(const bool save_current_time /*=true*/) { - if (filename.empty()) + if (filename.empty() || instances.empty()) return true; - FILE *f = fopen(filename.c_str(), "w"); + FILE *f = fopen(filename.c_str(), "a"); // append if (!f) { MERROR("Failed to write to file " << filename << ": " << strerror(errno)); return false; } - for (const auto &i: instances) + + if (save_current_time) { - fprintf(f, "%s", i.first.c_str()); - fprintf(f, "\t%lu", (unsigned long)i.second.t); - fprintf(f, " %zu", i.second.npoints); - fprintf(f, " %f", i.second.min); - fprintf(f, " %f", i.second.max); - fprintf(f, " %f", i.second.mean); - fprintf(f, " %f", i.second.median); - fprintf(f, " %f", i.second.stddev); - fprintf(f, " %f", i.second.npskew); - for (uint64_t v: i.second.deciles) - fprintf(f, " %lu", (unsigned long)v); + // save current time in readable format (UTC) + std::time_t sys_time{std::time(nullptr)}; + std::tm *utc_time = std::gmtime(&sys_time); //GMT is equivalent to UTC + + // format: year-month-day : hour:minute:second + std::string current_time{}; + if (utc_time && sys_time != (std::time_t)(-1)) + { + current_time += std::to_string(utc_time->tm_year + 1900) + '-'; + current_time += (std::to_string(utc_time->tm_mon + 1).size() == 1 ? std::string{'0'} : std::string{}) + + std::to_string(utc_time->tm_mon + 1) + '-'; + current_time += (std::to_string(utc_time->tm_mday).size() == 1 ? std::string{'0'} : std::string{}) + + std::to_string(utc_time->tm_mday) + " : "; + current_time += (std::to_string(utc_time->tm_hour).size() == 1 ? std::string{'0'} : std::string{}) + + std::to_string(utc_time->tm_hour) + ':'; + current_time += (std::to_string(utc_time->tm_min).size() == 1 ? std::string{'0'} : std::string{}) + + std::to_string(utc_time->tm_min) + ':'; + current_time += (std::to_string(utc_time->tm_sec).size() == 1 ? std::string{'0'} : std::string{}) + + std::to_string(utc_time->tm_sec); + } + else + { + current_time += "TIME_ERROR_"; + } + fputc('\n', f); // add an extra line before each 'print time' + fprintf(f, "%s", current_time.c_str()); fputc('\n', f); } + + for (const auto &i: instances) + { + fprintf(f, "%s,", i.first.c_str()); + + if (i.second.npoints > 0) + { + fprintf(f, "%lu,", (unsigned long)i.second.t); + fprintf(f, "%zu,", i.second.npoints); + fprintf(f, "%f,", i.second.min); + fprintf(f, "%f,", i.second.max); + fprintf(f, "%f,", i.second.mean); + fprintf(f, "%f,", i.second.median); + fprintf(f, "%f,", i.second.stddev); + fprintf(f, "%f,", i.second.npskew); + for (uint64_t v: i.second.deciles) + fprintf(f, "%lu,", (unsigned long)v); + + // note: only add a new line if there are points; assume that 'no points' means i.first is a message meant to be + // prepended to the next save operation + fputc('\n', f); + } + } fclose(f); + + // after saving, clear so next save does not append the same stuff over again + instances.clear(); + return true; } std::vector TimingsDatabase::get(const char *name) const { std::vector ret; - auto range = instances.equal_range(name); - for (auto i = range.first; i != range.second; ++i) - ret.push_back(i->second); + for (const auto &i: instances) + { + if (i.first != name) + continue; + + ret.push_back(i.second); + } std::sort(ret.begin(), ret.end(), [](const instance &e0, const instance &e1){ return e0.t < e1.t; }); return ret; } void TimingsDatabase::add(const char *name, const instance &i) { - instances.insert(std::make_pair(name, i)); + instances.emplace_back(name, i); } diff --git a/src/common/timings.h b/src/common/timings.h index fb905611f8..f88861c629 100644 --- a/src/common/timings.h +++ b/src/common/timings.h @@ -1,9 +1,10 @@ #pragma once +#include #include #include +#include #include -#include class TimingsDatabase { @@ -18,17 +19,17 @@ class TimingsDatabase public: TimingsDatabase(); - TimingsDatabase(const std::string &filename); + TimingsDatabase(const std::string &filename, const bool load_previous = false); ~TimingsDatabase(); std::vector get(const char *name) const; void add(const char *name, const instance &data); + bool save(const bool print_current_time = true); private: bool load(); - bool save(); private: std::string filename; - std::multimap instances; + std::list> instances; }; diff --git a/src/common/variant.h b/src/common/variant.h new file mode 100644 index 0000000000..ffb34e40a7 --- /dev/null +++ b/src/common/variant.h @@ -0,0 +1,167 @@ +// Copyright (c) 2022, 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. + +// Variant wrapper class. + +#pragma once + +//local headers + +//third party headers +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +//standard headers +#include +#include +#include + +//forward declarations + + +namespace tools +{ + +[[noreturn]] inline void variant_static_visitor_blank_err() +{ throw std::runtime_error("variant: tried to visit an empty variant."); } +[[noreturn]] inline void variant_unwrap_err() +{ throw std::runtime_error("variant: tried to access value of incorrect type."); } + +//// +// variant: convenience wrapper around boost::variant with a cleaner interface +// - the variant is 'optional' - an empty variant will evaluate to 'false' and an initialized variant will be 'true' +/// +template +struct variant_static_visitor : public boost::static_visitor +{ + /// provide visitation for empty variants + /// - add this to your visitor with: using variant_static_visitor::operator(); + [[noreturn]] ResultT operator()(const boost::blank) { variant_static_visitor_blank_err(); } + [[noreturn]] ResultT operator()(const boost::blank) const { variant_static_visitor_blank_err(); } +}; + +template +class variant final +{ + using VType = boost::variant; + +public: +//constructors + /// default constructor + variant() = default; + variant(boost::none_t) : variant{} {} //act like boost::optional + + /// construct from variant type (use enable_if to avoid issues with copy/move constructor) + template >, + variant + >::value, + bool + >::type = true> + variant(T &&value) : m_value{std::forward(value)} {} + +//overloaded operators + /// boolean operator: true if the variant isn't empty/uninitialized + explicit operator bool() const noexcept { return !this->is_empty(); } + +//member functions + /// check if empty/uninitialized + bool is_empty() const noexcept { return m_value.which() == 0; } + + /// check the variant type + template + bool is_type() const noexcept { return this->index() == this->type_index_of(); } + + /// try to get a handle to the embedded value (return nullptr on failure) + template + T* try_unwrap() noexcept { return boost::strict_get< T>(&m_value); } + template + const T* try_unwrap() const noexcept { return boost::strict_get(&m_value); } + + /// get a handle to the embedded value + template + T& unwrap() + { + T *value_ptr{this->try_unwrap()}; + if (!value_ptr) variant_unwrap_err(); + return *value_ptr; + } + template + const T& unwrap() const + { + const T *value_ptr{this->try_unwrap()}; + if (!value_ptr) variant_unwrap_err(); + return *value_ptr; + } + + /// get the type index of the currently stored type + int index() const noexcept { return m_value.which(); } + + /// get the type index of a requested type (compile error for invalid types) (boost::mp11 is boost 1.66.0) + template + static constexpr int type_index_of() noexcept + { + using types = boost::mpl::vector; + using elem = typename boost::mpl::find::type; + using begin = typename boost::mpl::begin::type; + return boost::mpl::distance::value; + } + + /// check if two variants have the same type + static bool same_type(const variant &v1, const variant &v2) noexcept + { return v1.index() == v2.index(); } + + /// apply a visitor to the variant + template + typename VisitorT::result_type visit(VisitorT &&visitor) + { + return boost::apply_visitor(std::forward(visitor), m_value); + } + template + typename VisitorT::result_type visit(VisitorT &&visitor) const + { + return boost::apply_visitor(std::forward(visitor), m_value); + } + +private: +//member variables + /// variant of all value types + VType m_value; +}; + +} //namespace tools diff --git a/src/crypto/CMakeLists.txt b/src/crypto/CMakeLists.txt index e17584c906..abd4ba783d 100644 --- a/src/crypto/CMakeLists.txt +++ b/src/crypto/CMakeLists.txt @@ -29,10 +29,14 @@ set(crypto_sources aesb.c blake256.c + blake2b.c chacha.c crypto-ops-data.c crypto-ops.c crypto.cpp + eclib_interface.cpp + eclib_test.cpp + generators.cpp groestl.c hash-extra-blake.c hash-extra-groestl.c @@ -48,7 +52,9 @@ set(crypto_sources slow-hash.c rx-slow-hash.c CryptonightR_JIT.c - tree-hash.c) + tree-hash.c + twofish.c + x25519.cpp) if(ARCH_ID STREQUAL "i386" OR ARCH_ID STREQUAL "x86_64" OR ARCH_ID STREQUAL "x86-64" OR ARCH_ID STREQUAL "amd64") list(APPEND crypto_sources CryptonightR_template.S) @@ -69,11 +75,15 @@ monero_add_library(cncrypto target_link_libraries(cncrypto PUBLIC epee + mx25519_static randomx ${Boost_SYSTEM_LIBRARY} ${SODIUM_LIBRARY} PRIVATE ${EXTRA_LIBRARIES}) +target_include_directories(cncrypto + PRIVATE + ${MX25519_INCLUDE}) if (ARM) option(NO_OPTIMIZED_MULTIPLY_ON_ARM diff --git a/src/crypto/blake2b.c b/src/crypto/blake2b.c new file mode 100644 index 0000000000..60ca3132c7 --- /dev/null +++ b/src/crypto/blake2b.c @@ -0,0 +1,564 @@ +/* +Copyright (c) 2018-2019, tevador + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * 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. + * 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. +*/ + +/* Original code from Argon2 reference source code package used under CC0 Licence + * https://github.com/P-H-C/phc-winner-argon2 + * Copyright 2015 + * Daniel Dinu, Dmitry Khovratovich, Jean-Philippe Aumasson, and Samuel Neves +*/ + +#include "blake2b.h" + +#include "memwipe.h" + +#include +#include +#include + + +/// BEGIN: endian.h + +#if defined(_MSC_VER) +#define FORCE_INLINE __inline +#elif defined(__GNUC__) || defined(__clang__) +#define FORCE_INLINE __inline__ +#else +#define FORCE_INLINE +#endif + + /* Argon2 Team - Begin Code */ + /* + Not an exhaustive list, but should cover the majority of modern platforms + Additionally, the code will always be correct---this is only a performance + tweak. + */ +#if (defined(__BYTE_ORDER__) && \ + (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)) || \ + defined(__LITTLE_ENDIAN__) || defined(__ARMEL__) || defined(__MIPSEL__) || \ + defined(__AARCH64EL__) || defined(__amd64__) || defined(__i386__) || \ + defined(_M_IX86) || defined(_M_X64) || defined(_M_AMD64) || \ + defined(_M_ARM) +#define NATIVE_LITTLE_ENDIAN +#endif + /* Argon2 Team - End Code */ + +static FORCE_INLINE uint32_t load32(const void *src) { +#if defined(NATIVE_LITTLE_ENDIAN) + uint32_t w; + memcpy(&w, src, sizeof w); + return w; +#else + const uint8_t *p = (const uint8_t *)src; + uint32_t w = *p++; + w |= (uint32_t)(*p++) << 8; + w |= (uint32_t)(*p++) << 16; + w |= (uint32_t)(*p++) << 24; + return w; +#endif +} + +static FORCE_INLINE uint64_t load64_native(const void *src) { + uint64_t w; + memcpy(&w, src, sizeof w); + return w; +} + +static FORCE_INLINE uint64_t load64(const void *src) { +#if defined(NATIVE_LITTLE_ENDIAN) + return load64_native(src); +#else + const uint8_t *p = (const uint8_t *)src; + uint64_t w = *p++; + w |= (uint64_t)(*p++) << 8; + w |= (uint64_t)(*p++) << 16; + w |= (uint64_t)(*p++) << 24; + w |= (uint64_t)(*p++) << 32; + w |= (uint64_t)(*p++) << 40; + w |= (uint64_t)(*p++) << 48; + w |= (uint64_t)(*p++) << 56; + return w; +#endif +} + +static FORCE_INLINE void store32(void *dst, uint32_t w) { +#if defined(NATIVE_LITTLE_ENDIAN) + memcpy(dst, &w, sizeof w); +#else + uint8_t *p = (uint8_t *)dst; + *p++ = (uint8_t)w; + w >>= 8; + *p++ = (uint8_t)w; + w >>= 8; + *p++ = (uint8_t)w; + w >>= 8; + *p++ = (uint8_t)w; +#endif +} + +static FORCE_INLINE void store64_native(void *dst, uint64_t w) { + memcpy(dst, &w, sizeof w); +} + +static FORCE_INLINE void store64(void *dst, uint64_t w) { +#if defined(NATIVE_LITTLE_ENDIAN) + store64_native(dst, w); +#else + uint8_t *p = (uint8_t *)dst; + *p++ = (uint8_t)w; + w >>= 8; + *p++ = (uint8_t)w; + w >>= 8; + *p++ = (uint8_t)w; + w >>= 8; + *p++ = (uint8_t)w; + w >>= 8; + *p++ = (uint8_t)w; + w >>= 8; + *p++ = (uint8_t)w; + w >>= 8; + *p++ = (uint8_t)w; + w >>= 8; + *p++ = (uint8_t)w; +#endif +} + +/// END: endian.h + +/// BEGIN: blake2-impl.h + +static FORCE_INLINE uint64_t load48(const void *src) { + const uint8_t *p = (const uint8_t *)src; + uint64_t w = *p++; + w |= (uint64_t)(*p++) << 8; + w |= (uint64_t)(*p++) << 16; + w |= (uint64_t)(*p++) << 24; + w |= (uint64_t)(*p++) << 32; + w |= (uint64_t)(*p++) << 40; + return w; +} + +static FORCE_INLINE void store48(void *dst, uint64_t w) { + uint8_t *p = (uint8_t *)dst; + *p++ = (uint8_t)w; + w >>= 8; + *p++ = (uint8_t)w; + w >>= 8; + *p++ = (uint8_t)w; + w >>= 8; + *p++ = (uint8_t)w; + w >>= 8; + *p++ = (uint8_t)w; + w >>= 8; + *p++ = (uint8_t)w; +} + +static FORCE_INLINE uint32_t rotr32(const uint32_t w, const unsigned c) { + return (w >> c) | (w << (32 - c)); +} + +static FORCE_INLINE uint64_t rotr64(const uint64_t w, const unsigned c) { + return (w >> c) | (w << (64 - c)); +} + +/// END: blake2-impl.h + +void clear_internal_memory(void *mem, const size_t length) { + memwipe(mem, length); +} + +/// BEGIN: blake2b.c + +static const uint64_t blake2b_IV[8] = { + UINT64_C(0x6a09e667f3bcc908), UINT64_C(0xbb67ae8584caa73b), + UINT64_C(0x3c6ef372fe94f82b), UINT64_C(0xa54ff53a5f1d36f1), + UINT64_C(0x510e527fade682d1), UINT64_C(0x9b05688c2b3e6c1f), + UINT64_C(0x1f83d9abfb41bd6b), UINT64_C(0x5be0cd19137e2179) }; + +static const unsigned int blake2b_sigma[12][16] = { + {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, + {14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3}, + {11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4}, + {7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8}, + {9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13}, + {2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9}, + {12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11}, + {13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10}, + {6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5}, + {10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0}, + {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, + {14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3}, +}; + +static FORCE_INLINE void blake2b_set_lastnode(blake2b_state *S) { + S->f[1] = (uint64_t)-1; +} + +static FORCE_INLINE void blake2b_set_lastblock(blake2b_state *S) { + if (S->last_node) { + blake2b_set_lastnode(S); + } + S->f[0] = (uint64_t)-1; +} + +static FORCE_INLINE void blake2b_increment_counter(blake2b_state *S, + uint64_t inc) { + S->t[0] += inc; + S->t[1] += (S->t[0] < inc); +} + +static FORCE_INLINE void blake2b_invalidate_state(blake2b_state *S) { + clear_internal_memory(S, sizeof(*S)); /* wipe */ + blake2b_set_lastblock(S); /* invalidate for further use */ +} + +static FORCE_INLINE void blake2b_init0(blake2b_state *S) { + memset(S, 0, sizeof(*S)); + memcpy(S->h, blake2b_IV, sizeof(S->h)); +} + +int blake2b_init_param(blake2b_state *S, const blake2b_param *P) { + const unsigned char *p = (const unsigned char *)P; + unsigned int i; + + if (NULL == P || NULL == S) { + return -1; + } + + blake2b_init0(S); + /* IV XOR Parameter Block */ + for (i = 0; i < 8; ++i) { + S->h[i] ^= load64(&p[i * sizeof(S->h[i])]); + } + S->outlen = P->digest_length; + return 0; +} + +/* Sequential blake2b initialization */ +int blake2b_init(blake2b_state *S, size_t outlen) { + blake2b_param P; + + if (S == NULL) { + return -1; + } + + if ((outlen == 0) || (outlen > BLAKE2B_OUTBYTES)) { + blake2b_invalidate_state(S); + return -1; + } + + /* Setup Parameter Block for unkeyed BLAKE2 */ + P.digest_length = (uint8_t)outlen; + P.key_length = 0; + P.fanout = 1; + P.depth = 1; + P.leaf_length = 0; + P.node_offset = 0; + P.node_depth = 0; + P.inner_length = 0; + memset(P.reserved, 0, sizeof(P.reserved)); + memset(P.salt, 0, sizeof(P.salt)); + memset(P.personal, 0, sizeof(P.personal)); + + return blake2b_init_param(S, &P); +} + +int blake2b_init_key(blake2b_state *S, size_t outlen, const void *key, size_t keylen) { + blake2b_param P; + + if (S == NULL) { + return -1; + } + + if ((outlen == 0) || (outlen > BLAKE2B_OUTBYTES)) { + blake2b_invalidate_state(S); + return -1; + } + + if ((key == 0) || (keylen == 0) || (keylen > BLAKE2B_KEYBYTES)) { + blake2b_invalidate_state(S); + return -1; + } + + /* Setup Parameter Block for keyed BLAKE2 */ + P.digest_length = (uint8_t)outlen; + P.key_length = (uint8_t)keylen; + P.fanout = 1; + P.depth = 1; + P.leaf_length = 0; + P.node_offset = 0; + P.node_depth = 0; + P.inner_length = 0; + memset(P.reserved, 0, sizeof(P.reserved)); + memset(P.salt, 0, sizeof(P.salt)); + memset(P.personal, 0, sizeof(P.personal)); + + if (blake2b_init_param(S, &P) < 0) { + blake2b_invalidate_state(S); + return -1; + } + + { + uint8_t block[BLAKE2B_BLOCKBYTES]; + memset(block, 0, BLAKE2B_BLOCKBYTES); + memcpy(block, key, keylen); + blake2b_update(S, block, BLAKE2B_BLOCKBYTES); + /* Burn the key from stack */ + clear_internal_memory(block, BLAKE2B_BLOCKBYTES); + } + return 0; +} + +static void blake2b_compress(blake2b_state *S, const uint8_t *block) { + uint64_t m[16]; + uint64_t v[16]; + unsigned int i, r; + + for (i = 0; i < 16; ++i) { + m[i] = load64(block + i * sizeof(m[i])); + } + + for (i = 0; i < 8; ++i) { + v[i] = S->h[i]; + } + + v[8] = blake2b_IV[0]; + v[9] = blake2b_IV[1]; + v[10] = blake2b_IV[2]; + v[11] = blake2b_IV[3]; + v[12] = blake2b_IV[4] ^ S->t[0]; + v[13] = blake2b_IV[5] ^ S->t[1]; + v[14] = blake2b_IV[6] ^ S->f[0]; + v[15] = blake2b_IV[7] ^ S->f[1]; + +#define G(r, i, a, b, c, d) \ + do { \ + a = a + b + m[blake2b_sigma[r][2 * i + 0]]; \ + d = rotr64(d ^ a, 32); \ + c = c + d; \ + b = rotr64(b ^ c, 24); \ + a = a + b + m[blake2b_sigma[r][2 * i + 1]]; \ + d = rotr64(d ^ a, 16); \ + c = c + d; \ + b = rotr64(b ^ c, 63); \ + } while ((void)0, 0) + +#define ROUND(r) \ + do { \ + G(r, 0, v[0], v[4], v[8], v[12]); \ + G(r, 1, v[1], v[5], v[9], v[13]); \ + G(r, 2, v[2], v[6], v[10], v[14]); \ + G(r, 3, v[3], v[7], v[11], v[15]); \ + G(r, 4, v[0], v[5], v[10], v[15]); \ + G(r, 5, v[1], v[6], v[11], v[12]); \ + G(r, 6, v[2], v[7], v[8], v[13]); \ + G(r, 7, v[3], v[4], v[9], v[14]); \ + } while ((void)0, 0) + + for (r = 0; r < 12; ++r) { + ROUND(r); + } + + for (i = 0; i < 8; ++i) { + S->h[i] = S->h[i] ^ v[i] ^ v[i + 8]; + } + +#undef G +#undef ROUND +} + +int blake2b_update(blake2b_state *S, const void *in, size_t inlen) { + const uint8_t *pin = (const uint8_t *)in; + + if (inlen == 0) { + return 0; + } + + /* Sanity check */ + if (S == NULL || in == NULL) { + return -1; + } + + /* Is this a reused state? */ + if (S->f[0] != 0) { + return -1; + } + + if (S->buflen + inlen > BLAKE2B_BLOCKBYTES) { + /* Complete current block */ + size_t left = S->buflen; + size_t fill = BLAKE2B_BLOCKBYTES - left; + memcpy(&S->buf[left], pin, fill); + blake2b_increment_counter(S, BLAKE2B_BLOCKBYTES); + blake2b_compress(S, S->buf); + S->buflen = 0; + inlen -= fill; + pin += fill; + /* Avoid buffer copies when possible */ + while (inlen > BLAKE2B_BLOCKBYTES) { + blake2b_increment_counter(S, BLAKE2B_BLOCKBYTES); + blake2b_compress(S, pin); + inlen -= BLAKE2B_BLOCKBYTES; + pin += BLAKE2B_BLOCKBYTES; + } + } + memcpy(&S->buf[S->buflen], pin, inlen); + S->buflen += (unsigned int)inlen; + return 0; +} + +int blake2b_final(blake2b_state *S, void *out, size_t outlen) { + uint8_t buffer[BLAKE2B_OUTBYTES] = { 0 }; + unsigned int i; + + /* Sanity checks */ + if (S == NULL || out == NULL || outlen < S->outlen) { + return -1; + } + + /* Is this a reused state? */ + if (S->f[0] != 0) { + return -1; + } + + blake2b_increment_counter(S, S->buflen); + blake2b_set_lastblock(S); + memset(&S->buf[S->buflen], 0, BLAKE2B_BLOCKBYTES - S->buflen); /* Padding */ + blake2b_compress(S, S->buf); + + for (i = 0; i < 8; ++i) { /* Output full hash to temp buffer */ + store64(buffer + sizeof(S->h[i]) * i, S->h[i]); + } + + memcpy(out, buffer, S->outlen); + clear_internal_memory(buffer, sizeof(buffer)); + clear_internal_memory(S->buf, sizeof(S->buf)); + clear_internal_memory(S->h, sizeof(S->h)); + return 0; +} + +int blake2b(void *out, size_t outlen, const void *in, size_t inlen, + const void *key, size_t keylen) { + blake2b_state S; + int ret = -1; + + /* Verify parameters */ + if (NULL == in && inlen > 0) { + goto fail; + } + + if (NULL == out || outlen == 0 || outlen > BLAKE2B_OUTBYTES) { + goto fail; + } + + if ((NULL == key && keylen > 0) || keylen > BLAKE2B_KEYBYTES) { + goto fail; + } + + if (keylen > 0) { + if (blake2b_init_key(&S, outlen, key, keylen) < 0) { + goto fail; + } + } + else { + if (blake2b_init(&S, outlen) < 0) { + goto fail; + } + } + + if (blake2b_update(&S, in, inlen) < 0) { + goto fail; + } + ret = blake2b_final(&S, out, outlen); + +fail: + clear_internal_memory(&S, sizeof(S)); + return ret; +} + +/* Argon2 Team - Begin Code */ +int blake2b_long(void *pout, size_t outlen, const void *in, size_t inlen) { + uint8_t *out = (uint8_t *)pout; + blake2b_state blake_state; + uint8_t outlen_bytes[sizeof(uint32_t)] = { 0 }; + int ret = -1; + + if (outlen > UINT32_MAX) { + goto fail; + } + + /* Ensure little-endian byte order! */ + store32(outlen_bytes, (uint32_t)outlen); + +#define TRY(statement) \ + do { \ + ret = statement; \ + if (ret < 0) { \ + goto fail; \ + } \ + } while ((void)0, 0) + + if (outlen <= BLAKE2B_OUTBYTES) { + TRY(blake2b_init(&blake_state, outlen)); + TRY(blake2b_update(&blake_state, outlen_bytes, sizeof(outlen_bytes))); + TRY(blake2b_update(&blake_state, in, inlen)); + TRY(blake2b_final(&blake_state, out, outlen)); + } + else { + uint32_t toproduce; + uint8_t out_buffer[BLAKE2B_OUTBYTES]; + uint8_t in_buffer[BLAKE2B_OUTBYTES]; + TRY(blake2b_init(&blake_state, BLAKE2B_OUTBYTES)); + TRY(blake2b_update(&blake_state, outlen_bytes, sizeof(outlen_bytes))); + TRY(blake2b_update(&blake_state, in, inlen)); + TRY(blake2b_final(&blake_state, out_buffer, BLAKE2B_OUTBYTES)); + memcpy(out, out_buffer, BLAKE2B_OUTBYTES / 2); + out += BLAKE2B_OUTBYTES / 2; + toproduce = (uint32_t)outlen - BLAKE2B_OUTBYTES / 2; + + while (toproduce > BLAKE2B_OUTBYTES) { + memcpy(in_buffer, out_buffer, BLAKE2B_OUTBYTES); + TRY(blake2b(out_buffer, BLAKE2B_OUTBYTES, in_buffer, + BLAKE2B_OUTBYTES, NULL, 0)); + memcpy(out, out_buffer, BLAKE2B_OUTBYTES / 2); + out += BLAKE2B_OUTBYTES / 2; + toproduce -= BLAKE2B_OUTBYTES / 2; + } + + memcpy(in_buffer, out_buffer, BLAKE2B_OUTBYTES); + TRY(blake2b(out_buffer, toproduce, in_buffer, BLAKE2B_OUTBYTES, NULL, + 0)); + memcpy(out, out_buffer, toproduce); + } +fail: + clear_internal_memory(&blake_state, sizeof(blake_state)); + return ret; +#undef TRY +} +/* Argon2 Team - End Code */ + +/// END: blake2b.c diff --git a/src/crypto/blake2b.h b/src/crypto/blake2b.h new file mode 100644 index 0000000000..4b6b4c2d4e --- /dev/null +++ b/src/crypto/blake2b.h @@ -0,0 +1,116 @@ +/* +Copyright (c) 2018-2019, tevador + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * 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. + * 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. +*/ + +/* Original code from Argon2 reference source code package used under CC0 Licence + * https://github.com/P-H-C/phc-winner-argon2 + * Copyright 2015 + * Daniel Dinu, Dmitry Khovratovich, Jean-Philippe Aumasson, and Samuel Neves +*/ + +/// NOTE: implementation lifted out of randomx package + +#pragma once + +#include +#include +#include + +#if defined(__cplusplus) +extern "C" { +#endif + + enum blake2b_constant { + BLAKE2B_BLOCKBYTES = 128, + BLAKE2B_OUTBYTES = 64, + BLAKE2B_KEYBYTES = 64, + BLAKE2B_SALTBYTES = 16, + BLAKE2B_PERSONALBYTES = 16 + }; + +#pragma pack(push, 1) + typedef struct __blake2b_param { + uint8_t digest_length; /* 1 */ + uint8_t key_length; /* 2 */ + uint8_t fanout; /* 3 */ + uint8_t depth; /* 4 */ + uint32_t leaf_length; /* 8 */ + uint64_t node_offset; /* 16 */ + uint8_t node_depth; /* 17 */ + uint8_t inner_length; /* 18 */ + uint8_t reserved[14]; /* 32 */ + uint8_t salt[BLAKE2B_SALTBYTES]; /* 48 */ + uint8_t personal[BLAKE2B_PERSONALBYTES]; /* 64 */ + } blake2b_param; +#pragma pack(pop) + + typedef struct __blake2b_state { + uint64_t h[8]; + uint64_t t[2]; + uint64_t f[2]; + uint8_t buf[BLAKE2B_BLOCKBYTES]; + unsigned buflen; + unsigned outlen; + uint8_t last_node; + } blake2b_state; + + /* Ensure param structs have not been wrongly padded */ + /* Poor man's static_assert */ + enum { + blake2_size_check_0 = 1 / !!(CHAR_BIT == 8), + blake2_size_check_2 = + 1 / !!(sizeof(blake2b_param) == sizeof(uint64_t) * CHAR_BIT) + }; + + // crypto namespace (fixes naming collisions with libsodium) +#define blake2b_init crypto_blake2b_init +#define blake2b_init_key crypto_blake2b_init_key +#define blake2b_init_param crypto_blake2b_init_param +#define blake2b_update crypto_blake2b_update +#define blake2b_final crypto_blake2b_final +#define blake2b crypto_blake2b +#define blake2b_long crypto_blake2b_long + + /* Streaming API */ + int blake2b_init(blake2b_state *S, size_t outlen); + int blake2b_init_key(blake2b_state *S, size_t outlen, const void *key, + size_t keylen); + int blake2b_init_param(blake2b_state *S, const blake2b_param *P); + int blake2b_update(blake2b_state *S, const void *in, size_t inlen); + int blake2b_final(blake2b_state *S, void *out, size_t outlen); + + /* Simple API */ + int blake2b(void *out, size_t outlen, const void *in, size_t inlen, + const void *key, size_t keylen); + + /* Argon2 Team - Begin Code */ + int blake2b_long(void *out, size_t outlen, const void *in, size_t inlen); + /* Argon2 Team - End Code */ + +#if defined(__cplusplus) +} +#endif diff --git a/src/crypto/eclib_interface.cpp b/src/crypto/eclib_interface.cpp new file mode 100644 index 0000000000..3508fb22c5 --- /dev/null +++ b/src/crypto/eclib_interface.cpp @@ -0,0 +1,70 @@ +// Copyright (c) 2022, 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 "eclib_test.h" +#include "eclib_utils.h" +#include "eclib_utils.inl" //should be included in ONLY this file + +namespace crypto +{ +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +template +static void eclib_interface() +{ + // eclib types + typename LIBT::key KEY{}; + const typename LIBT::key CONST_KEY{}; + + // eclib functions + LIBT::core_func(CONST_KEY, KEY); + + // eclib::utils functions + LIBT::utils::util_func(CONST_KEY, KEY); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- + +//------------------------------------------------------------------------------------------------------------------- +// instantiate the utils for each eclib type +//------------------------------------------------------------------------------------------------------------------- +template struct eclib_utils; +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- + +//------------------------------------------------------------------------------------------------------------------- +// expect the interface to compile for each eclib type +//------------------------------------------------------------------------------------------------------------------- +void eclib_interfaces_impl() +{ + eclib_interface(); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +} //namespace crypto diff --git a/src/crypto/eclib_test.cpp b/src/crypto/eclib_test.cpp new file mode 100644 index 0000000000..462ffb74f9 --- /dev/null +++ b/src/crypto/eclib_test.cpp @@ -0,0 +1,40 @@ +// Copyright (c) 2022, 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 "eclib_test.h" + +namespace crypto +{ +//------------------------------------------------------------------------------------------------------------------- +void eclib_test::core_func(const eclib_test::key &k, eclib_test::key &key_out) +{ + key_out = k*10; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace crypto diff --git a/src/crypto/eclib_test.h b/src/crypto/eclib_test.h new file mode 100644 index 0000000000..ed04cd60dd --- /dev/null +++ b/src/crypto/eclib_test.h @@ -0,0 +1,48 @@ +// Copyright (c) 2022, 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 "eclib_utils.h" + +namespace crypto +{ + +struct eclib_test final +{ + +using utils = eclib_utils; + +using key = unsigned char; + +static void core_func(const key &k, key &key_out); + +}; //eclib_test + +} //namespace crypto diff --git a/src/crypto/eclib_utils.h b/src/crypto/eclib_utils.h new file mode 100644 index 0000000000..ef8cffbf38 --- /dev/null +++ b/src/crypto/eclib_utils.h @@ -0,0 +1,45 @@ +// Copyright (c) 2022, 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 + +namespace crypto +{ + +template +struct eclib_utils +{ +static void util_func(const typename LIBT::key &k, typename LIBT::key &key_inout); +}; + + +//template +//void eclib_utils::util_func(const typename LIBT::key &k, typename LIBT::key &key_inout); + +} //namespace crypto diff --git a/src/crypto/eclib_utils.inl b/src/crypto/eclib_utils.inl new file mode 100644 index 0000000000..14a30d82dc --- /dev/null +++ b/src/crypto/eclib_utils.inl @@ -0,0 +1,41 @@ +// Copyright (c) 2022, 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 + +namespace crypto +{ +//------------------------------------------------------------------------------------------------------------------- +template +void eclib_utils::util_func(const typename LIBT::key &k, typename LIBT::key &key_inout) +{ + key_inout = k + 20; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace crypto diff --git a/src/crypto/generators.cpp b/src/crypto/generators.cpp new file mode 100644 index 0000000000..df4e182bb9 --- /dev/null +++ b/src/crypto/generators.cpp @@ -0,0 +1,291 @@ +// Copyright (c) 2022, 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 "generators.h" + +#include "crypto.h" +extern "C" +{ +#include "crypto-ops.h" +#include "mx25519.h" +} +#include "cryptonote_config.h" +#include "hash.h" +#include "x25519.h" + +#include +#include +#include +#include +#include + +namespace crypto +{ + +/// constexpr assert for old gcc bug: https://stackoverflow.com/questions/34280729/throw-in-constexpr-function +/// - this function won't compile in a constexpr context if b == false +constexpr void constexpr_assert(const bool b) { b ? 0 : throw std::runtime_error("constexpr assert failed"); }; + +/// constexpr paste bytes into an array-of-bytes type +template +constexpr T bytes_to(const std::initializer_list bytes) +{ + T out{}; // zero-initialize trailing bytes + + auto current = std::begin(out.data); + constexpr_assert(static_cast(bytes.size()) <= std::end(out.data) - current); + + for (const unsigned char byte : bytes) + *current++ = byte; + return out; +} + +// generators +//standard ed25519 generator G: {x, 4/5} (positive x when decompressing y = 4/5) +constexpr public_key G = bytes_to({ 0x58, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, + 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66 }); +//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 X: keccak_to_pt(keccak("seraphis_X")) +constexpr public_key X = bytes_to({ 0xa4, 0xfb, 0x43, 0xca, 0x69, 0x5e, 0x12, 0x99, 0x88, 0x02, 0xa2, 0x0a, 0x15, + 0x8f, 0x12, 0xea, 0x79, 0x47, 0x4f, 0xb9, 0x01, 0x21, 0x16, 0x95, 0x6a, 0x69, 0x76, 0x7c, 0x4d, 0x41, 0x11, 0x0f }); +//seraphis generator U: keccak_to_pt(keccak("seraphis_U")) +constexpr public_key U = bytes_to({ 0x10, 0x94, 0x8b, 0x00, 0xd2, 0xde, 0x50, 0xb5, 0x76, 0x99, 0x8c, 0x11, 0xe8, + 0x3c, 0x59, 0xa7, 0x96, 0x84, 0xd2, 0x5c, 0x9f, 0x8a, 0x0d, 0xc6, 0x86, 0x45, 0x70, 0xd7, 0x97, 0xb9, 0xc1, 0x6e }); +static ge_p3 G_p3; +static ge_p3 H_p3; +static ge_p3 X_p3; +static ge_p3 U_p3; +static ge_cached G_cached; +static ge_cached H_cached; +static ge_cached X_cached; +static ge_cached U_cached; +//X25519 generator: x = 9 +static const x25519_pubkey x25519_G{ mx25519_pubkey{ .data = { 9 } } }; + +// 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() +{ + // G = {x, 4/5 mod q} + fe four, five, inv_five, y; + fe_0(four); + fe_0(five); + four[0] = 4; + five[0] = 5; + fe_invert(inv_five, five); + fe_mul(y, four, inv_five); + + public_key reproduced_G; + fe_tobytes(to_bytes(reproduced_G), y); + + return reproduced_G; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static public_key reproduce_generator_H() +{ + // H = 8*to_point(keccak(G)) + // note: this does not use the point_from_bytes() function found in H_p(), instead directly interpreting the + // input bytes as a compressed point (this can fail, so should not be used generically) + // note2: to_point(keccak(G)) is known to succeed for the canonical value of G (it will fail 7/8ths of the time + // normally) + ge_p3 temp_p3; + ge_p2 temp_p2; + ge_p1p1 temp_p1p1; + + hash H_temp_hash{cn_fast_hash(to_bytes(G), sizeof(ec_point))}; + (void)H_temp_hash; //suppress unused warning + assert(ge_frombytes_vartime(&temp_p3, reinterpret_cast(&H_temp_hash)) == 0); + ge_p3_to_p2(&temp_p2, &temp_p3); + ge_mul8(&temp_p1p1, &temp_p2); + ge_p1p1_to_p3(&temp_p3, &temp_p1p1); + + public_key reproduced_H; + ge_p3_tobytes(to_bytes(reproduced_H), &temp_p3); + + return reproduced_H; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static public_key reproduce_generator_X() +{ + // X = H_p(keccak("seraphis_X")) + const std::string X_salt{config::HASH_KEY_SERAPHIS_X}; + hash X_temp_hash{cn_fast_hash(X_salt.data(), X_salt.size())}; + public_key reproduced_X; + hash_to_point(X_temp_hash, reproduced_X); + + return reproduced_X; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static public_key reproduce_generator_U() +{ + // U = H_p(keccak("seraphis_U")) + const std::string U_salt{config::HASH_KEY_SERAPHIS_U}; + hash U_temp_hash{cn_fast_hash(U_salt.data(), U_salt.size())}; + public_key reproduced_U; + hash_to_point(U_temp_hash, reproduced_U); + + return reproduced_U; +} +//------------------------------------------------------------------------------------------------------------------- +// Make generators, but only once +//------------------------------------------------------------------------------------------------------------------- +static void init_gens() +{ + std::call_once(init_gens_once_flag, + [&](){ + + // 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(X.data[0]) == 0xa4, "compile-time constant sanity check"); + static_assert(static_cast(U.data[0]) == 0x10, "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 X_deserialize = ge_frombytes_vartime(&X_p3, to_bytes(X)); + const int U_deserialize = ge_frombytes_vartime(&U_p3, to_bytes(U)); + + (void)G_deserialize; assert(G_deserialize == 0); + (void)H_deserialize; assert(H_deserialize == 0); + (void)X_deserialize; assert(X_deserialize == 0); + (void)U_deserialize; assert(U_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(&X_cached, &X_p3); + ge_p3_to_cached(&U_cached, &U_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_X; assert(reproduce_generator_X() == X); + (void)reproduce_generator_U; assert(reproduce_generator_U() == U); + + }); +} +//------------------------------------------------------------------------------------------------------------------- +public_key get_G() +{ + return G; +} +//------------------------------------------------------------------------------------------------------------------- +public_key get_H() +{ + return H; +} +//------------------------------------------------------------------------------------------------------------------- +public_key get_X() +{ + return X; +} +//------------------------------------------------------------------------------------------------------------------- +public_key get_U() +{ + return U; +} +//------------------------------------------------------------------------------------------------------------------- +ge_p3 get_G_p3() +{ + init_gens(); + return G_p3; +} +//------------------------------------------------------------------------------------------------------------------- +ge_p3 get_H_p3() +{ + init_gens(); + return H_p3; +} +//------------------------------------------------------------------------------------------------------------------- +ge_p3 get_X_p3() +{ + init_gens(); + return X_p3; +} +//------------------------------------------------------------------------------------------------------------------- +ge_p3 get_U_p3() +{ + init_gens(); + return U_p3; +} +//------------------------------------------------------------------------------------------------------------------- +ge_cached get_G_cached() +{ + init_gens(); + return G_cached; +} +//------------------------------------------------------------------------------------------------------------------- +ge_cached get_H_cached() +{ + init_gens(); + return H_cached; +} +//------------------------------------------------------------------------------------------------------------------- +ge_cached get_X_cached() +{ + init_gens(); + return X_cached; +} +//------------------------------------------------------------------------------------------------------------------- +ge_cached get_U_cached() +{ + init_gens(); + return U_cached; +} +//------------------------------------------------------------------------------------------------------------------- +x25519_pubkey get_x25519_G() +{ + return x25519_G; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace crypto diff --git a/src/crypto/generators.h b/src/crypto/generators.h new file mode 100644 index 0000000000..cb74929eed --- /dev/null +++ b/src/crypto/generators.h @@ -0,0 +1,57 @@ +// Copyright (c) 2022, 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 + +extern "C" +{ +#include "crypto-ops.h" +} +#include "crypto.h" +#include "x25519.h" + +namespace crypto +{ + +public_key get_G(); +public_key get_H(); +public_key get_X(); +public_key get_U(); +ge_p3 get_G_p3(); +ge_p3 get_H_p3(); +ge_p3 get_X_p3(); +ge_p3 get_U_p3(); +ge_cached get_G_cached(); +ge_cached get_H_cached(); +ge_cached get_X_cached(); +ge_cached get_U_cached(); + +x25519_pubkey get_x25519_G(); + +} //namespace crypto diff --git a/src/crypto/twofish.c b/src/crypto/twofish.c new file mode 100644 index 0000000000..af93db9665 --- /dev/null +++ b/src/crypto/twofish.c @@ -0,0 +1,1764 @@ +/* + * Fast, portable, and easy-to-use Twofish implementation, + * Version 0.3. + * Copyright (c) 2002 by Niels Ferguson. + * (See further down for the almost-unrestricted licensing terms.) + * + * -------------------------------------------------------------------------- + * There are two files for this implementation: + * - twofish.h, the header file. + * - twofish.c, the code file. + * + * To incorporate this code into your program you should: + * - Check the licensing terms further down in this comment. + * - Fix the two type definitions in twofish.h to suit your platform. + * - Fix a few definitions in twofish.c in the section marked + * PLATFORM FIXES. There is one important ones that affects + * functionality, and then a few definitions that you can optimise + * for efficiency but those have no effect on the functionality. + * Don't change anything else. + * - Put the code in your project and compile it. + * + * To use this library you should: + * - Call Twofish_initialise() in your program before any other function in + * this library. + * - Use Twofish_prepare_key(...) to convert a key to internal form. + * - Use Twofish_encrypt_block(...) and Twofish_decrypt_block(...) to encrypt and decrypt + * data. + * See the comments in the header file for details on these functions. + * -------------------------------------------------------------------------- + * + * There are many Twofish implementation available for free on the web. + * Most of them are hard to integrate into your own program. + * As we like people to use our cipher, I thought I would make it easier. + * Here is a free and easy-to-integrate Twofish implementation in C. + * The latest version is always available from my personal home page at + * http://niels.ferguson.net/ + * + * Integrating library code into a project is difficult because the library + * header files interfere with the project's header files and code. + * And of course the project's header files interfere with the library code. + * I've tried to resolve these problems here. + * The header file of this implementation is very light-weight. + * It contains two typedefs, a structure, and a few function declarations. + * All names it defines start with "Twofish_". + * The header file is therefore unlikely to cause problems in your project. + * The code file of this implementation doesn't need to include the header + * files of the project. There is thus no danger of the project interfering + * with all the definitions and macros of the Twofish code. + * In most situations, all you need to do is fill in a few platform-specific + * definitions in the header file and code file, + * and you should be able to run the Twofish code in your project. + * I estimate it should take you less than an hour to integrate this code + * into your project, most of it spent reading the comments telling you what + * to do. + * + * For people using C++: it is very easy to wrap this library into a + * TwofishKey class. One of the big advantages is that you can automate the + * wiping of the key material in the destructor. I have not provided a C++ + * class because the interface depends too much on the abstract base class + * you use for block ciphers in your program, which I don't know about. + * + * This implementation is designed for use on PC-class machines. It uses the + * Twofish 'full' keying option which uses large tables. Total table size is + * around 5-6 kB for static tables plus 4.5 kB for each pre-processed key. + * If you need an implementation that uses less memory, + * take a look at Brian Gladman's code on his web site: + * http://fp.gladman.plus.com/cryptography_technology/aes/ + * He has code for all AES candidates. + * His Twofish code has lots of options trading off table size vs. speed. + * You can also take a look at the optimised code by Doug Whiting on the + * Twofish web site + * http://www.counterpane.com/twofish.html + * which has loads of options. + * I believe these existing implementations are harder to re-use because they + * are not clean libraries and they impose requirements on the environment. + * This implementation is very careful to minimise those, + * and should be easier to integrate into any larger program. + * + * The default mode of this implementation is fully portable as it uses no + * behaviour not defined in the C standard. (This is harder than you think.) + * If you have any problems porting the default mode, please let me know + * so that I can fix the problem. (But only if this code is at fault, I + * don't fix compilers.) + * Most of the platform fixes are related to non-portable but faster ways + * of implementing certain functions. + * + * In general I've tried to make the code as fast as possible, at the expense + * of memory and code size. However, C does impose limits, and this + * implementation will be slower than an optimised assembler implementation. + * But beware of assembler implementations: a good Pentium implementation + * uses completely different code than a good Pentium II implementation. + * You basically have to re-write the assembly code for every generation of + * processor. Unless you are severely pressed for speed, stick with C. + * + * The initialisation routine of this implementation contains a self-test. + * If initialisation succeeds without calling the fatal routine, then + * the implementation works. I don't think you can break the implementation + * in such a way that it still passes the tests, unless you are malicious. + * In other words: if the initialisation routine returns, + * you have successfully ported the implementation. + * (Or not implemented the fatal routine properly, but that is your problem.) + * + * I'm indebted to many people who helped me in one way or another to write + * this code. During the design of Twofish and the AES process I had very + * extensive discussions of all implementation issues with various people. + * Doug Whiting in particular provided a wealth of information. The Twofish + * team spent untold hours discussion various cipher features, and their + * implementation. Brian Gladman implemented all AES candidates in C, + * and we had some fruitful discussions on how to implement Twofish in C. + * Jan Nieuwenhuizen tested this code on Linux using GCC. + * + * Now for the license: + * The author hereby grants a perpetual license to everybody to + * use this code for any purpose as long as the copyright message is included + * in the source code of this or any derived work. + * + * Yes, this means that you, your company, your club, and anyone else + * can use this code anywhere you want. You can change it and distribute it + * under the GPL, include it in your commercial product without releasing + * the source code, put it on the web, etc. + * The only thing you cannot do is remove my copyright message, + * or distribute any source code based on this implementation that does not + * include my copyright message. + * + * I appreciate a mention in the documentation or credits, + * but I understand if that is difficult to do. + * I also appreciate it if you tell me where and why you used my code. + * + * Please send any questions or comments to niels@ferguson.net + * + * Have Fun! + * + * Niels + */ + +/* + * DISCLAIMER: As I'm giving away my work for free, I'm of course not going + * to accept any liability of any form. This code, or the Twofish cipher, + * might very well be flawed; you have been warned. + * This software is provided as-is, without any kind of warrenty or + * guarantee. And that is really all you can expect when you download + * code for free from the Internet. + * + * I think it is really sad that disclaimers like this seem to be necessary. + * If people only had a little bit more common sense, and didn't come + * whining like little children every time something happens.... + */ + +/* + * Version history: + * Version 0.0, 2002-08-30 + * First written. + * Version 0.1, 2002-09-03 + * Added disclaimer. Improved self-tests. + * Version 0.2, 2002-09-09 + * Removed last non-portabilities. Default now works completely within + * the C standard. UInt32 can be larger than 32 bits without problems. + * Version 0.3, 2002-09-28 + * Bugfix: use instead of to adhere to ANSI/ISO. + * Rename BIG_ENDIAN macro to CPU_IS_BIG_ENDIAN. The gcc library + * header already defines BIG_ENDIAN, even though it is not + * supposed to. + */ + + +/* + * Minimum set of include files. + * You should not need any application-specific include files for this code. + * In fact, adding you own header files could break one of the many macros or + * functions in this file. Be very careful. + * Standard include files will probably be ok. + */ +#include /* for memset(), memcpy(), and memcmp() */ +#include "twofish.h" + + +/* + * PLATFORM FIXES + * ============== + * + * Fix the type definitions in twofish.h first! + * + * The following definitions have to be fixed for each particular platform + * you work on. If you have a multi-platform program, you no doubt have + * portable definitions that you can substitute here without changing the + * rest of the code. + */ + + +/* + * Function called if something is fatally wrong with the implementation. + * This fatal function is called when a coding error is detected in the + * Twofish implementation, or when somebody passes an obviously erroneous + * parameter to this implementation. There is not much you can do when + * the code contains bugs, so we just stop. + * + * The argument is a string. Ideally the fatal function prints this string + * as an error message. Whatever else this function does, it should never + * return. A typical implementation would stop the program completely after + * printing the error message. + * + * This default implementation is not very useful, + * but does not assume anything about your environment. + * It will at least let you know something is wrong.... + * I didn't want to include any libraries to print and error or so, + * as this makes the code much harder to integrate in a project. + * + * Note that the Twofish_fatal function may not return to the caller. + * Unfortunately this is not something the self-test can test for, + * so you have to make sure of this yourself. + * + * If you want to call an external function, be careful about including + * your own header files here. This code uses a lot of macros, and your + * header file could easily break it. Maybe the best solution is to use + * a separate extern statement for your fatal function. + */ +#define Twofish_fatal( msg ) {for(;;);} + + +/* + * The rest of the settings are not important for the functionality + * of this Twofish implementation. That is, their default settings + * work on all platforms. You can change them to improve the + * speed of the implementation on your platform. Erroneous settings + * will result in erroneous implementations, but the self-test should + * catch those. + */ + + +/* + * Macros to rotate a Twofish_UInt32 value left or right by the + * specified number of bits. This should be a 32-bit rotation, + * and not rotation of, say, 64-bit values. + * + * Every encryption or decryption operation uses 32 of these rotations, + * so it is a good idea to make these macros efficient. + * + * This fully portable definition has one piece of tricky stuff. + * The UInt32 might be larger than 32 bits, so we have to mask + * any higher bits off. The simplest way to do this is to 'and' the + * value first with 0xffffffff and then shift it right. An optimising + * compiler that has a 32-bit type can optimise this 'and' away. + * + * Unfortunately there is no portable way of writing the constant + * 0xffffffff. You don't know which suffix to use (U, or UL?) + * The UINT32_MASK definition uses a bit of trickery. Shift-left + * is only defined if the shift amount is strictly less than the size + * of the UInt32, so we can't use (1<<32). The answer it to take the value + * 2, cast it to a UInt32, shift it left 31 positions, and subtract one. + * Another example of how to make something very simple extremely difficult. + * I hate C. + * + * The rotation macros are straightforward. + * They are only applied to UInt32 values, which are _unsigned_ + * so the >> operator must do a logical shift that brings in zeroes. + * On most platforms you will only need to optimise the ROL32 macro; the + * ROR32 macro is not inefficient on an optimising compiler as all rotation + * amounts in this code are known at compile time. + * + * On many platforms there is a faster solution. + * For example, MS compilers have the __rotl and __rotr functions + * that generate x86 rotation instructions. + */ +#define UINT32_MASK ( (((UInt32)2)<<31) - 1 ) +#define ROL32( x, n ) ( (x)<<(n) | ((x) & UINT32_MASK) >> (32-(n)) ) +#define ROR32( x, n ) ROL32( (x), 32-(n) ) + + +/* + * Select data type for q-table entries. + * + * Larger entry types cost more memory (1.5 kB), and might be faster + * or slower depending on the CPU and compiler details. + * + * This choice only affects the static data size and the key setup speed. + * Functionality, expanded key size, or encryption speed are not affected. + * Define to 1 to get large q-table entries. + */ +#define LARGE_Q_TABLE 0 /* default = 0 */ + + +/* + * Method to select a single byte from a UInt32. + * WARNING: non-portable code if set; might not work on all platforms. + * + * Inside the inner loop of Twofish it is necessary to access the 4 + * individual bytes of a UInt32. This can be done using either shifts + * and masks, or memory accesses. + * + * Set to 0 to use shift and mask operations for the byte selection. + * This is more ALU intensive. It is also fully portable. + * + * Set to 1 to use memory accesses. The UInt32 is stored in memory and + * the individual bytes are read from memory one at a time. + * This solution is more memory-intensive, and not fully portable. + * It might be faster on your platform, or not. If you use this option, + * make sure you set the CPU_IS_BIG_ENDIAN flag appropriately. + * + * This macro does not affect the conversion of the inputs and outputs + * of the cipher. See the CONVERT_USING_CASTS macro for that. + */ +#define SELECT_BYTE_FROM_UINT32_IN_MEMORY 0 /* default = 0 */ + + +/* + * Method used to read the input and write the output. + * WARNING: non-portable code if set; might not work on all platforms. + * + * Twofish operates on 32-bit words. The input to the cipher is + * a byte array, as is the output. The portable method of doing the + * conversion is a bunch of rotate and mask operations, but on many + * platforms it can be done faster using a cast. + * This only works if your CPU allows UInt32 accesses to arbitrary Byte + * addresses. + * + * Set to 0 to use the shift and mask operations. This is fully + * portable. . + * + * Set to 1 to use a cast. The Byte * is cast to a UInt32 *, and a + * UInt32 is read. If necessary (as indicated by the CPU_IS_BIG_ENDIAN + * macro) the byte order in the UInt32 is swapped. The reverse is done + * to write the output of the encryption/decryption. Make sure you set + * the CPU_IS_BIG_ENDIAN flag appropriately. + * This option does not work unless a UInt32 is exactly 32 bits. + * + * This macro only changes the reading/writing of the plaintext/ciphertext. + * See the SELECT_BYTE_FROM_UINT32_IN_MEMORY to affect the way in which + * a UInt32 is split into 4 bytes for the S-box selection. + */ +#define CONVERT_USING_CASTS 0 /* default = 0 */ + + +/* + * Endianness switch. + * Only relevant if SELECT_BYTE_FROM_UINT32_IN_MEMORY or + * CONVERT_USING_CASTS is set. + * + * Set to 1 on a big-endian machine, and to 0 on a little-endian machine. + * Twofish uses the little-endian convention (least significant byte first) + * and big-endian machines (using most significant byte first) + * have to do a few conversions. + * + * CAUTION: This code has never been tested on a big-endian machine, + * because I don't have access to one. Feedback appreciated. + */ +#define CPU_IS_BIG_ENDIAN 0 + + +/* + * Macro to reverse the order of the bytes in a UInt32. + * Used to convert to little-endian on big-endian machines. + * This macro is always tested, but only used in the encryption and + * decryption if CONVERT_USING_CASTS, and CPU_IS_BIG_ENDIAN + * are both set. In other words: this macro is only speed-critical if + * both these flags have been set. + * + * This default definition of SWAP works, but on many platforms there is a + * more efficient implementation. + */ +#define BSWAP(x) ((ROL32((x),8) & 0x00ff00ff) | (ROR32((x),8) & 0xff00ff00)) + + +/* + * END OF PLATFORM FIXES + * ===================== + * + * You should not have to touch the rest of this file. + */ + + +/* + * Convert the external type names to some that are easier to use inside + * this file. I didn't want to use the names Byte and UInt32 in the + * header file, because many programs already define them and using two + * conventions at once can be very difficult. + * Don't change these definitions! Change the originals + * in twofish.h instead. + */ +/* A Byte must be an unsigned integer, 8 bits long. */ +typedef Twofish_Byte Byte; +/* A UInt32 must be an unsigned integer at least 32 bits long. */ +typedef Twofish_UInt32 UInt32; + + +/* + * Define a macro ENDIAN_CONVERT. + * + * We define a macro ENDIAN_CONVERT that performs a BSWAP on big-endian + * machines, and is the identity function on little-endian machines. + * The code then uses this macro without considering the endianness. + */ +#if CPU_IS_BIG_ENDIAN +#define ENDIAN_CONVERT(x) BSWAP(x) +#else +#define ENDIAN_CONVERT(x) (x) +#endif + + +/* + * Compute byte offset within a UInt32 stored in memory. + * + * This is only used when SELECT_BYTE_FROM_UINT32_IN_MEMORY is set. + * + * The input is the byte number 0..3, 0 for least significant. + * Note the use of sizeof() to support UInt32 types that are larger + * than 4 bytes. + */ +#if CPU_IS_BIG_ENDIAN +#define BYTE_OFFSET( n ) (sizeof(UInt32) - 1 - (n) ) +#else +#define BYTE_OFFSET( n ) (n) +#endif + + +/* + * Macro to get Byte no. b from UInt32 value X. + * We use two different definition, depending on the settings. + */ +#if SELECT_BYTE_FROM_UINT32_IN_MEMORY +/* Pick the byte from the memory in which X is stored. */ +#define SELECT_BYTE( X, b ) (((Byte *)(&(X)))[BYTE_OFFSET(b)]) +#else +/* Portable solution: Pick the byte directly from the X value. */ +#define SELECT_BYTE( X, b ) (((X) >> 8*(b)) & 0xff) +#endif + + +/* Some shorthands because we use byte selection in large formulae. */ +#define b0(X) SELECT_BYTE((X),0) +#define b1(X) SELECT_BYTE((X),1) +#define b2(X) SELECT_BYTE((X),2) +#define b3(X) SELECT_BYTE((X),3) + + +/* + * We need macros to load and store UInt32 from/to byte arrays + * using the least-significant-byte-first convention. + * + * GET32( p ) gets a UInt32 in lsb-first form from four bytes pointed to + * by p. + * PUT32( v, p ) writes the UInt32 value v at address p in lsb-first form. + */ +#if CONVERT_USING_CASTS + +/* Get UInt32 from four bytes pointed to by p. */ +#define GET32( p ) ENDIAN_CONVERT( *((UInt32 *)(p)) ) +/* Put UInt32 into four bytes pointed to by p */ +#define PUT32( v, p ) *((UInt32 *)(p)) = ENDIAN_CONVERT(v) + +#else + +/* Get UInt32 from four bytes pointed to by p. */ +#define GET32( p ) \ +( \ +(UInt32)((p)[0]) \ +| (UInt32)((p)[1])<< 8\ +| (UInt32)((p)[2])<<16\ +| (UInt32)((p)[3])<<24\ +) +/* Put UInt32 into four bytes pointed to by p */ +#define PUT32( v, p ) \ +(p)[0] = (Byte)(((v) ) & 0xff);\ +(p)[1] = (Byte)(((v) >> 8) & 0xff);\ +(p)[2] = (Byte)(((v) >> 16) & 0xff);\ +(p)[3] = (Byte)(((v) >> 24) & 0xff) + +#endif + + +/* + * Test the platform-specific macros. + * This function tests the macros defined so far to make sure the + * definitions are appropriate for this platform. + * If you make any mistake in the platform configuration, this should detect + * that and inform you what went wrong. + * Somewhere, someday, this is going to save somebody a lot of time, + * because misbehaving macros are hard to debug. + */ +static void test_platform(void) +{ + /* Buffer with test values. */ + Byte buf[] = {0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0}; + UInt32 C; + UInt32 x,y; + int i; + + /* + * Some sanity checks on the types that can't be done in compile time. + * A smart compiler will just optimise these tests away. + * The pre-processor doesn't understand different types, so we cannot + * do these checks in compile-time. + * + * I hate C. + * + * The first check in each case is to make sure the size is correct. + * The second check is to ensure that it is an unsigned type. + */ + if( ((UInt32) ((UInt32)1 << 31) == 0) || ((UInt32)-1 < 0) ) + { + Twofish_fatal( "Twofish code: Twofish_UInt32 type not suitable" ); + } + if( (sizeof( Byte ) != 1) || ((Byte)-1 < 0) ) + { + Twofish_fatal( "Twofish code: Twofish_Byte type not suitable" ); + } + + /* + * Sanity-check the endianness conversions. + * This is just an aid to find problems. If you do the endianness + * conversion macros wrong you will fail the full cipher test, + * but that does not help you find the error. + * Always make it easy to find the bugs! + * + * Detail: There is no fully portable way of writing UInt32 constants, + * as you don't know whether to use the U or UL suffix. Using only U you + * might only be allowed 16-bit constants. Using UL you might get 64-bit + * constants which cannot be stored in a UInt32 without warnings, and + * which generally behave subtly different from a true UInt32. + * As long as we're just comparing with the constant, + * we can always use the UL suffix and at worst lose some efficiency. + * I use a separate '32-bit constant' macro in most of my other code. + * + * I hate C. + * + * Start with testing GET32. We test it on all positions modulo 4 + * to make sure we can handly any position of inputs. (Some CPUs + * do not allow non-aligned accesses which we would do if you used + * the CONVERT_USING_CASTS option. + */ + if( GET32( buf ) != 0x78563412UL || GET32(buf+1) != 0x9a785634UL + || GET32( buf+2 ) != 0xbc9a7856UL || GET32(buf+3) != 0xdebc9a78UL ) + { + Twofish_fatal( "Twofish code: GET32 not implemented properly" ); + } + + /* + * We can now use GET32 to test PUT32. + * We don't test the shifted versions. If GET32 can do that then + * so should PUT32. + */ + C = GET32( buf ); + PUT32( 3*C, buf ); + if( GET32( buf ) != 0x69029c36UL ) + { + Twofish_fatal( "Twofish code: PUT32 not implemented properly" ); + } + + + /* Test ROL and ROR */ + for( i=1; i<32; i++ ) + { + /* Just a simple test. */ + x = ROR32( C, i ); + y = ROL32( C, i ); + x ^= (C>>i) ^ (C<<(32-i)); + y ^= (C<>(32-i)); + x |= y; + /* + * Now all we check is that x is zero in the least significant + * 32 bits. Using the UL suffix is safe here, as it doesn't matter + * if we get a larger type. + */ + if( (x & 0xffffffffUL) != 0 ) + { + Twofish_fatal( "Twofish ROL or ROR not properly defined." ); + } + } + + /* Test the BSWAP macro */ + if( (BSWAP(C)) != 0x12345678UL ) + { + /* + * The BSWAP macro should always work, even if you are not using it. + * A smart optimising compiler will just remove this entire test. + */ + Twofish_fatal( "BSWAP not properly defined." ); + } + + /* And we can test the b macros which use SELECT_BYTE. */ + if( (b0(C)!=0x12) || (b1(C) != 0x34) || (b2(C) != 0x56) || (b3(C) != 0x78) ) + { + /* + * There are many reasons why this could fail. + * Most likely is that CPU_IS_BIG_ENDIAN has the wrong value. + */ + Twofish_fatal( "Twofish code: SELECT_BYTE not implemented properly" ); + } +} + + +/* + * Finally, we can start on the Twofish-related code. + * You really need the Twofish specifications to understand this code. The + * best source is the Twofish book: + * "The Twofish Encryption Algorithm", by Bruce Schneier, John Kelsey, + * Doug Whiting, David Wagner, Chris Hall, and Niels Ferguson. + * you can also use the AES submission document of Twofish, which is + * available from my list of publications on my personal web site at + * http://niels.ferguson.net/. + * + * The first thing we do is write the testing routines. This is what the + * implementation has to satisfy in the end. We only test the external + * behaviour of the implementation of course. + */ + + +/* + * Perform a single self test on a (plaintext,ciphertext,key) triple. + * Arguments: + * key array of key bytes + * key_len length of key in bytes + * p plaintext + * c ciphertext + */ +static void test_vector( Byte key[], int key_len, Byte p[16], Byte c[16] ) +{ + Byte tmp[16]; /* scratch pad. */ + Twofish_key xkey; /* The expanded key */ + int i; + + + /* Prepare the key */ + Twofish_prepare_key( key, key_len, &xkey ); + + /* + * We run the test twice to ensure that the xkey structure + * is not damaged by the first encryption. + * Those are hideous bugs to find if you get them in an application. + */ + for( i=0; i<2; i++ ) + { + /* Encrypt and test */ + Twofish_encrypt_block( &xkey, p, tmp ); + if( memcmp( c, tmp, 16 ) != 0 ) + { + Twofish_fatal( "Twofish encryption failure" ); + } + + /* Decrypt and test */ + Twofish_decrypt_block( &xkey, c, tmp ); + if( memcmp( p, tmp, 16 ) != 0 ) + { + Twofish_fatal( "Twofish decryption failure" ); + } + } + + /* The test keys are not secret, so we don't need to wipe xkey. */ +} + + +/* + * Check implementation using three (key,plaintext,ciphertext) + * test vectors, one for each major key length. + * + * This is an absolutely minimal self-test. + * This routine does not test odd-sized keys. + */ +static void test_vectors() +{ + /* + * We run three tests, one for each major key length. + * These test vectors come from the Twofish specification. + * One encryption and one decryption using randomish data and key + * will detect almost any error, especially since we generate the + * tables ourselves, so we don't have the problem of a single + * damaged table entry in the source. + */ + + /* 128-bit test is the I=3 case of section B.2 of the Twofish book. */ + static Byte k128[] = { + 0x9F, 0x58, 0x9F, 0x5C, 0xF6, 0x12, 0x2C, 0x32, + 0xB6, 0xBF, 0xEC, 0x2F, 0x2A, 0xE8, 0xC3, 0x5A, + }; + static Byte p128[] = { + 0xD4, 0x91, 0xDB, 0x16, 0xE7, 0xB1, 0xC3, 0x9E, + 0x86, 0xCB, 0x08, 0x6B, 0x78, 0x9F, 0x54, 0x19 + }; + static Byte c128[] = { + 0x01, 0x9F, 0x98, 0x09, 0xDE, 0x17, 0x11, 0x85, + 0x8F, 0xAA, 0xC3, 0xA3, 0xBA, 0x20, 0xFB, 0xC3 + }; + + /* 192-bit test is the I=4 case of section B.2 of the Twofish book. */ + static Byte k192[] = { + 0x88, 0xB2, 0xB2, 0x70, 0x6B, 0x10, 0x5E, 0x36, + 0xB4, 0x46, 0xBB, 0x6D, 0x73, 0x1A, 0x1E, 0x88, + 0xEF, 0xA7, 0x1F, 0x78, 0x89, 0x65, 0xBD, 0x44 + }; + static Byte p192[] = { + 0x39, 0xDA, 0x69, 0xD6, 0xBA, 0x49, 0x97, 0xD5, + 0x85, 0xB6, 0xDC, 0x07, 0x3C, 0xA3, 0x41, 0xB2 + }; + static Byte c192[] = { + 0x18, 0x2B, 0x02, 0xD8, 0x14, 0x97, 0xEA, 0x45, + 0xF9, 0xDA, 0xAC, 0xDC, 0x29, 0x19, 0x3A, 0x65 + }; + + /* 256-bit test is the I=4 case of section B.2 of the Twofish book. */ + static Byte k256[] = { + 0xD4, 0x3B, 0xB7, 0x55, 0x6E, 0xA3, 0x2E, 0x46, + 0xF2, 0xA2, 0x82, 0xB7, 0xD4, 0x5B, 0x4E, 0x0D, + 0x57, 0xFF, 0x73, 0x9D, 0x4D, 0xC9, 0x2C, 0x1B, + 0xD7, 0xFC, 0x01, 0x70, 0x0C, 0xC8, 0x21, 0x6F + }; + static Byte p256[] = { + 0x90, 0xAF, 0xE9, 0x1B, 0xB2, 0x88, 0x54, 0x4F, + 0x2C, 0x32, 0xDC, 0x23, 0x9B, 0x26, 0x35, 0xE6 + }; + static Byte c256[] = { + 0x6C, 0xB4, 0x56, 0x1C, 0x40, 0xBF, 0x0A, 0x97, + 0x05, 0x93, 0x1C, 0xB6, 0xD4, 0x08, 0xE7, 0xFA + }; + + /* Run the actual tests. */ + test_vector( k128, 16, p128, c128 ); + test_vector( k192, 24, p192, c192 ); + test_vector( k256, 32, p256, c256 ); +} + + +/* + * Perform extensive test for a single key size. + * + * Test a single key size against the test vectors from section + * B.2 in the Twofish book. This is a sequence of 49 encryptions + * and decryptions. Each plaintext is equal to the ciphertext of + * the previous encryption. The key is made up from the ciphertext + * two and three encryptions ago. Both plaintext and key start + * at the zero value. + * We should have designed a cleaner recurrence relation for + * these tests, but it is too late for that now. At least we learned + * how to do it better next time. + * For details see appendix B of the book. + * + * Arguments: + * key_len Number of bytes of key + * final_value Final plaintext value after 49 iterations + */ +static void test_sequence( int key_len, Byte final_value[] ) +{ + Byte buf[ (50+3)*16 ]; /* Buffer to hold our computation values. */ + Byte tmp[16]; /* Temp for testing the decryption. */ + Twofish_key xkey; /* The expanded key */ + int i; + Byte * p; + + /* Wipe the buffer */ + memset( buf, 0, sizeof( buf ) ); + + /* + * Because the recurrence relation is done in an inconvenient manner + * we end up looping backwards over the buffer. + */ + + /* Pointer in buffer points to current plaintext. */ + p = &buf[50*16]; + for( i=1; i<50; i++ ) + { + /* + * Prepare a key. + * This automatically checks that key_len is valid. + */ + Twofish_prepare_key( p+16, key_len, &xkey ); + + /* Compute the next 16 bytes in the buffer */ + Twofish_encrypt_block( &xkey, p, p-16 ); + + /* Check that the decryption is correct. */ + Twofish_decrypt_block( &xkey, p-16, tmp ); + if( memcmp( tmp, p, 16 ) != 0 ) + { + Twofish_fatal( "Twofish decryption failure in sequence" ); + } + /* Move on to next 16 bytes in the buffer. */ + p -= 16; + } + + /* And check the final value. */ + if( memcmp( p, final_value, 16 ) != 0 ) + { + Twofish_fatal( "Twofish encryption failure in sequence" ); + } + + /* None of the data was secret, so there is no need to wipe anything. */ +} + + +/* + * Run all three sequence tests from the Twofish test vectors. + * + * This checks the most extensive test vectors currently available + * for Twofish. The data is from the Twofish book, appendix B.2. + */ +static void test_sequences() +{ + static Byte r128[] = { + 0x5D, 0x9D, 0x4E, 0xEF, 0xFA, 0x91, 0x51, 0x57, + 0x55, 0x24, 0xF1, 0x15, 0x81, 0x5A, 0x12, 0xE0 + }; + static Byte r192[] = { + 0xE7, 0x54, 0x49, 0x21, 0x2B, 0xEE, 0xF9, 0xF4, + 0xA3, 0x90, 0xBD, 0x86, 0x0A, 0x64, 0x09, 0x41 + }; + static Byte r256[] = { + 0x37, 0xFE, 0x26, 0xFF, 0x1C, 0xF6, 0x61, 0x75, + 0xF5, 0xDD, 0xF4, 0xC3, 0x3B, 0x97, 0xA2, 0x05 + }; + + /* Run the three sequence test vectors */ + test_sequence( 16, r128 ); + test_sequence( 24, r192 ); + test_sequence( 32, r256 ); +} + + +/* + * Test the odd-sized keys. + * + * Every odd-sized key is equivalent to a one of 128, 192, or 256 bits. + * The equivalent key is found by padding at the end with zero bytes + * until a regular key size is reached. + * + * We just test that the key expansion routine behaves properly. + * If the expanded keys are identical, then the encryptions and decryptions + * will behave the same. + */ +static void test_odd_sized_keys() +{ + Byte buf[32]; + Twofish_key xkey; + Twofish_key xkey_two; + int i; + + /* + * We first create an all-zero key to use as PRNG key. + * Normally we would not have to fill the buffer with zeroes, as we could + * just pass a zero key length to the Twofish_prepare_key function. + * However, this relies on using odd-sized keys, and those are just the + * ones we are testing here. We can't use an untested function to test + * itself. + */ + memset( buf, 0, sizeof( buf ) ); + Twofish_prepare_key( buf, 16, &xkey ); + + /* Fill buffer with pseudo-random data derived from two encryptions */ + Twofish_encrypt_block( &xkey, buf, buf ); + Twofish_encrypt_block( &xkey, buf, buf+16 ); + + /* Create all possible shorter keys that are prefixes of the buffer. */ + for( i=31; i>=0; i-- ) + { + /* Set a byte to zero. This is the new padding byte */ + buf[i] = 0; + + /* Expand the key with only i bytes of length */ + Twofish_prepare_key( buf, i, &xkey ); + + /* Expand the corresponding padded key of regular length */ + Twofish_prepare_key( buf, i<=16 ? 16 : i<= 24 ? 24 : 32, &xkey_two ); + + /* Compare the two */ + if( memcmp( &xkey, &xkey_two, sizeof( xkey ) ) != 0 ) + { + Twofish_fatal( "Odd sized keys do not expand properly" ); + } + } + + /* None of the key values are secret, so we don't need to wipe them. */ +} + + +/* + * Test the Twofish implementation. + * + * This routine runs all the self tests, in order of importance. + * It is called by the Twofish_initialise routine. + * + * In almost all applications the cost of running the self tests during + * initialisation is insignificant, especially + * compared to the time it takes to load the application from disk. + * If you are very pressed for initialisation performance, + * you could remove some of the tests. Make sure you did run them + * once in the software and hardware configuration you are using. + */ +static void self_test() +{ + /* The three test vectors form an absolute minimal test set. */ + test_vectors(); + + /* + * If at all possible you should run these tests too. They take + * more time, but provide a more thorough coverage. + */ + test_sequences(); + + /* Test the odd-sized keys. */ + test_odd_sized_keys(); +} + + +/* + * And now, the actual Twofish implementation. + * + * This implementation generates all the tables during initialisation. + * I don't like large tables in the code, especially since they are easily + * damaged in the source without anyone noticing it. You need code to + * generate them anyway, and this way all the code is close together. + * Generating them in the application leads to a smaller executable + * (the code is smaller than the tables it generates) and a + * larger static memory footprint. + * + * Twofish can be implemented in many ways. I have chosen to + * use large tables with a relatively long key setup time. + * If you encrypt more than a few blocks of data it pays to pre-compute + * as much as possible. This implementation is relatively inefficient for + * applications that need to re-key every block or so. + */ + +/* + * We start with the t-tables, directly from the Twofish definition. + * These are nibble-tables, but merging them and putting them two nibbles + * in one byte is more work than it is worth. + */ +static Byte t_table[2][4][16] = { + { + {0x8,0x1,0x7,0xD,0x6,0xF,0x3,0x2,0x0,0xB,0x5,0x9,0xE,0xC,0xA,0x4}, + {0xE,0xC,0xB,0x8,0x1,0x2,0x3,0x5,0xF,0x4,0xA,0x6,0x7,0x0,0x9,0xD}, + {0xB,0xA,0x5,0xE,0x6,0xD,0x9,0x0,0xC,0x8,0xF,0x3,0x2,0x4,0x7,0x1}, + {0xD,0x7,0xF,0x4,0x1,0x2,0x6,0xE,0x9,0xB,0x3,0x0,0x8,0x5,0xC,0xA} + }, + { + {0x2,0x8,0xB,0xD,0xF,0x7,0x6,0xE,0x3,0x1,0x9,0x4,0x0,0xA,0xC,0x5}, + {0x1,0xE,0x2,0xB,0x4,0xC,0x3,0x7,0x6,0xD,0xA,0x5,0xF,0x9,0x0,0x8}, + {0x4,0xC,0x7,0x5,0x1,0x6,0x9,0xA,0x0,0xE,0xD,0x8,0x2,0xB,0x3,0xF}, + {0xB,0x9,0x5,0x1,0xC,0x3,0xD,0xE,0x6,0x4,0x7,0xF,0x2,0x0,0x8,0xA} + } +}; + + +/* A 1-bit rotation of 4-bit values. Input must be in range 0..15 */ +#define ROR4BY1( x ) (((x)>>1) | (((x)<<3) & 0x8) ) + +/* + * The q-boxes are only used during the key schedule computations. + * These are 8->8 bit lookup tables. Some CPUs prefer to have 8->32 bit + * lookup tables as it is faster to load a 32-bit value than to load an + * 8-bit value and zero the rest of the register. + * The LARGE_Q_TABLE switch allows you to choose 32-bit entries in + * the q-tables. Here we just define the Qtype which is used to store + * the entries of the q-tables. + */ +#if LARGE_Q_TABLE +typedef UInt32 Qtype; +#else +typedef Byte Qtype; +#endif + +/* + * The actual q-box tables. + * There are two q-boxes, each having 256 entries. + */ +static Qtype q_table[2][256]; + + +/* + * Now the function that converts a single t-table into a q-table. + * + * Arguments: + * t[4][16] : four 4->4bit lookup tables that define the q-box + * q[256] : output parameter: the resulting q-box as a lookup table. + */ +static void make_q_table( Byte t[4][16], Qtype q[256] ) +{ + int ae,be,ao,bo; /* Some temporaries. */ + int i; + /* Loop over all input values and compute the q-box result. */ + for( i=0; i<256; i++ ) { + /* + * This is straight from the Twofish specifications. + * + * The ae variable is used for the a_i values from the specs + * with even i, and ao for the odd i's. Similarly for the b's. + */ + ae = i>>4; be = i&0xf; + ao = ae ^ be; bo = ae ^ ROR4BY1(be) ^ ((ae<<3)&8); + ae = t[0][ao]; be = t[1][bo]; + ao = ae ^ be; bo = ae ^ ROR4BY1(be) ^ ((ae<<3)&8); + ae = t[2][ao]; be = t[3][bo]; + + /* Store the result in the q-box table, the cast avoids a warning. */ + q[i] = (Qtype) ((be<<4) | ae); + } +} + + +/* + * Initialise both q-box tables. + */ +static void initialise_q_boxes() { + /* Initialise each of the q-boxes using the t-tables */ + make_q_table( t_table[0], q_table[0] ); + make_q_table( t_table[1], q_table[1] ); +} + + +/* + * Next up is the MDS matrix multiplication. + * The MDS matrix multiplication operates in the field + * GF(2)[x]/p(x) with p(x)=x^8+x^6+x^5+x^3+1. + * If you don't understand this, read a book on finite fields. You cannot + * follow the finite-field computations without some background. + * + * In this field, multiplication by x is easy: shift left one bit + * and if bit 8 is set then xor the result with 0x169. + * + * The MDS coefficients use a multiplication by 1/x, + * or rather a division by x. This is easy too: first make the + * value 'even' (i.e. bit 0 is zero) by xorring with 0x169 if necessary, + * and then shift right one position. + * Even easier: shift right and xor with 0xb4 if the lsbit was set. + * + * The MDS coefficients are 1, EF, and 5B, and we use the fact that + * EF = 1 + 1/x + 1/x^2 + * 5B = 1 + 1/x^2 + * in this field. This makes multiplication by EF and 5B relatively easy. + * + * This property is no accident, the MDS matrix was designed to allow + * this implementation technique to be used. + * + * We have four MDS tables, each mapping 8 bits to 32 bits. + * Each table performs one column of the matrix multiplication. + * As the MDS is always preceded by q-boxes, each of these tables + * also implements the q-box just previous to that column. + */ + +/* The actual MDS tables. */ +static UInt32 MDS_table[4][256]; + +/* A small table to get easy conditional access to the 0xb4 constant. */ +static UInt32 mds_poly_divx_const[] = {0,0xb4}; + +/* Function to initialise the MDS tables. */ +static void initialise_mds_tables() +{ + int i; + UInt32 q,qef,q5b; /* Temporary variables. */ + + /* Loop over all 8-bit input values */ + for( i=0; i<256; i++ ) + { + /* + * To save some work during the key expansion we include the last + * of the q-box layers from the h() function in these MDS tables. + */ + + /* We first do the inputs that are mapped through the q0 table. */ + q = q_table[0][i]; + /* + * Here we divide by x, note the table to get 0xb4 only if the + * lsbit is set. + * This sets qef = (1/x)*q in the finite field + */ + qef = (q >> 1) ^ mds_poly_divx_const[ q & 1 ]; + /* + * Divide by x again, and add q to get (1+1/x^2)*q. + * Note that (1+1/x^2) = 5B in the field, and addition in the field + * is exclusive or on the bits. + */ + q5b = (qef >> 1) ^ mds_poly_divx_const[ qef & 1 ] ^ q; + /* + * Add q5b to qef to set qef = (1+1/x+1/x^2)*q. + * Again, (1+1/x+1/x^2) = EF in the field. + */ + qef ^= q5b; + + /* + * Now that we have q5b = 5B * q and qef = EF * q + * we can fill two of the entries in the MDS matrix table. + * See the Twofish specifications for the order of the constants. + */ + MDS_table[1][i] = q <<24 | q5b<<16 | qef<<8 | qef; + MDS_table[3][i] = q5b<<24 | qef<<16 | q <<8 | q5b; + + /* Now we do it all again for the two columns that have a q1 box. */ + q = q_table[1][i]; + qef = (q >> 1) ^ mds_poly_divx_const[ q & 1 ]; + q5b = (qef >> 1) ^ mds_poly_divx_const[ qef & 1 ] ^ q; + qef ^= q5b; + + /* The other two columns use the coefficient in a different order. */ + MDS_table[0][i] = qef<<24 | qef<<16 | q5b<<8 | q ; + MDS_table[2][i] = qef<<24 | q <<16 | qef<<8 | q5b; + } +} + + +/* + * The h() function is the heart of the Twofish cipher. + * It is a complicated sequence of q-box lookups, key material xors, + * and finally the MDS matrix. + * We use lots of macros to make this reasonably fast. + */ + +/* First a shorthand for the two q-tables */ +#define q0 q_table[0] +#define q1 q_table[1] + +/* + * Each macro computes one column of the h for either 2, 3, or 4 stages. + * As there are 4 columns, we have 12 macros in all. + * + * The key bytes are stored in the Byte array L at offset + * 0,1,2,3, 8,9,10,11, [16,17,18,19, [24,25,26,27]] as this is the + * order we get the bytes from the user. If you look at the Twofish + * specs, you'll see that h() is applied to the even key words or the + * odd key words. The bytes of the even words appear in this spacing, + * and those of the odd key words too. + * + * These macros are the only place where the q-boxes and the MDS table + * are used. + */ +#define H02( y, L ) MDS_table[0][q0[q0[y]^L[ 8]]^L[0]] +#define H12( y, L ) MDS_table[1][q0[q1[y]^L[ 9]]^L[1]] +#define H22( y, L ) MDS_table[2][q1[q0[y]^L[10]]^L[2]] +#define H32( y, L ) MDS_table[3][q1[q1[y]^L[11]]^L[3]] +#define H03( y, L ) H02( q1[y]^L[16], L ) +#define H13( y, L ) H12( q1[y]^L[17], L ) +#define H23( y, L ) H22( q0[y]^L[18], L ) +#define H33( y, L ) H32( q0[y]^L[19], L ) +#define H04( y, L ) H03( q1[y]^L[24], L ) +#define H14( y, L ) H13( q0[y]^L[25], L ) +#define H24( y, L ) H23( q0[y]^L[26], L ) +#define H34( y, L ) H33( q1[y]^L[27], L ) + +/* + * Now we can define the h() function given an array of key bytes. + * This function is only used in the key schedule, and not to pre-compute + * the keyed S-boxes. + * + * In the key schedule, the input is always of the form k*(1+2^8+2^16+2^24) + * so we only provide k as an argument. + * + * Arguments: + * k input to the h() function. + * L pointer to array of key bytes at + * offsets 0,1,2,3, ... 8,9,10,11, [16,17,18,19, [24,25,26,27]] + * kCycles # key cycles, 2, 3, or 4. + */ +static UInt32 h( int k, Byte L[], int kCycles ) +{ + switch( kCycles ) { + /* We code all 3 cases separately for speed reasons. */ + case 2: + return H02(k,L) ^ H12(k,L) ^ H22(k,L) ^ H32(k,L); + case 3: + return H03(k,L) ^ H13(k,L) ^ H23(k,L) ^ H33(k,L); + case 4: + return H04(k,L) ^ H14(k,L) ^ H24(k,L) ^ H34(k,L); + default: + /* This is always a coding error, which is fatal. */ + Twofish_fatal( "Twofish h(): Illegal argument" ); + } +} + + +/* + * Pre-compute the keyed S-boxes. + * Fill the pre-computed S-box array in the expanded key structure. + * Each pre-computed S-box maps 8 bits to 32 bits. + * + * The S argument contains half the number of bytes of the full key, but is + * derived from the full key. (See Twofish specifications for details.) + * S has the weird byte input order used by the Hxx macros. + * + * This function takes most of the time of a key expansion. + * + * Arguments: + * S pointer to array of 8*kCycles Bytes containing the S vector. + * kCycles number of key words, must be in the set {2,3,4} + * xkey pointer to Twofish_key structure that will contain the S-boxes. + */ +static void fill_keyed_sboxes( Byte S[], int kCycles, Twofish_key * xkey ) +{ + int i; + switch( kCycles ) { + /* We code all 3 cases separately for speed reasons. */ + case 2: + for( i=0; i<256; i++ ) + { + xkey->s[0][i]= H02( i, S ); + xkey->s[1][i]= H12( i, S ); + xkey->s[2][i]= H22( i, S ); + xkey->s[3][i]= H32( i, S ); + } + break; + case 3: + for( i=0; i<256; i++ ) + { + xkey->s[0][i]= H03( i, S ); + xkey->s[1][i]= H13( i, S ); + xkey->s[2][i]= H23( i, S ); + xkey->s[3][i]= H33( i, S ); + } + break; + case 4: + for( i=0; i<256; i++ ) + { + xkey->s[0][i]= H04( i, S ); + xkey->s[1][i]= H14( i, S ); + xkey->s[2][i]= H24( i, S ); + xkey->s[3][i]= H34( i, S ); + } + break; + default: + /* This is always a coding error, which is fatal. */ + Twofish_fatal( "Twofish fill_keyed_sboxes(): Illegal argument" ); + } +} + + +/* A flag to keep track of whether we have been initialised or not. */ +static int Twofish_initialised = 0; + +/* + * Initialise the Twofish implementation. + * This function must be called before any other function in the + * Twofish implementation is called. + * This routine also does some sanity checks, to make sure that + * all the macros behave, and it tests the whole cipher. + */ +void Twofish_initialise(void) +{ + /* First test the various platform-specific definitions. */ + test_platform(); + + /* We can now generate our tables, in the right order of course. */ + initialise_q_boxes(); + initialise_mds_tables(); + + /* We're finished with the initialisation itself. */ + Twofish_initialised = 1; + + /* + * And run some tests on the whole cipher. + * Yes, you need to do this every time you start your program. + * It is called assurance; you have to be certain that your program + * still works properly. + */ + self_test(); +} + + +/* + * The Twofish key schedule uses an Reed-Solomon code matrix multiply. + * Just like the MDS matrix, the RS-matrix is designed to be easy + * to implement. Details are below in the code. + * + * These constants make it easy to compute in the finite field used + * for the RS code. + * + * We use Bytes for the RS computation, but these are automatically + * widened to unsigned integers in the expressions. Having unsigned + * ints in these tables therefore provides the fastest access. + */ +static unsigned int rs_poly_const[] = {0, 0x14d}; +static unsigned int rs_poly_div_const[] = {0, 0xa6 }; + +void Twofish_setup(Twofish_context *context, Twofish_Byte key[32], Twofish_Byte iv[16], Twofish_options options) { + Twofish_initialise(); + context->options = options; + Twofish_prepare_key(key, 32, &(context->key)); + memcpy(context->iv, iv, 16); +} + + +/* + * Prepare a key for use in encryption and decryption. + * Like most block ciphers, Twofish allows the key schedule + * to be pre-computed given only the key. + * Twofish has a fairly 'heavy' key schedule that takes a lot of time + * to compute. The main work is pre-computing the S-boxes used in the + * encryption and decryption. We feel that this makes the cipher much + * harder to attack. The attacker doesn't even know what the S-boxes + * contain without including the entire key schedule in the analysis. + * + * Unlike most Twofish implementations, this one allows any key size from + * 0 to 32 bytes. Odd key sizes are defined for Twofish (see the + * specifications); the key is simply padded with zeroes to the next real + * key size of 16, 24, or 32 bytes. + * Each odd-sized key is thus equivalent to a single normal-sized key. + * + * Arguments: + * key array of key bytes + * key_len number of bytes in the key, must be in the range 0,...,32. + * xkey Pointer to an Twofish_key structure that will be filled + * with the internal form of the cipher key. + */ +void Twofish_prepare_key( const Byte key[], int key_len, Twofish_key * xkey ) +{ + /* We use a single array to store all key material in, + * to simplify the wiping of the key material at the end. + * The first 32 bytes contain the actual (padded) cipher key. + * The next 32 bytes contain the S-vector in its weird format, + * and we have 4 bytes of overrun necessary for the RS-reduction. + */ + Byte K[32+32+4]; + + int kCycles; /* # key cycles, 2,3, or 4. */ + + int i; + UInt32 A, B; /* Used to compute the round keys. */ + + Byte * kptr; /* Three pointers for the RS computation. */ + Byte * sptr; + Byte * t; + + Byte b,bx,bxx; /* Some more temporaries for the RS computation. */ + + /* Check that the Twofish implementation was initialised. */ + if( Twofish_initialised == 0 ) + { + /* + * You didn't call Twofish_initialise before calling this routine. + * This is a programming error, and therefore we call the fatal + * routine. + * + * I could of course call the initialisation routine here, + * but there are a few reasons why I don't. First of all, the + * self-tests have to be done at startup. It is no good to inform + * the user that the cipher implementation fails when he wants to + * write his data to disk in encrypted form. You have to warn him + * before he spends time typing his data. Second, the initialisation + * and self test are much slower than a single key expansion. + * Calling the initialisation here makes the performance of the + * cipher unpredictable. This can lead to really weird problems + * if you use the cipher for a real-time task. Suddenly it fails + * once in a while the first time you try to use it. Things like + * that are almost impossible to debug. + */ + Twofish_fatal( "Twofish implementation was not initialised." ); + + /* + * There is always a danger that the Twofish_fatal routine returns, + * in spite of the specifications that it should not. + * (A good programming rule: don't trust the rest of the code.) + * This would be disasterous. If the q-tables and MDS-tables have + * not been initialised, they are probably still filled with zeroes. + * Suppose the MDS-tables are all zero. The key expansion would then + * generate all-zero round keys, and all-zero s-boxes. The danger + * is that nobody would notice as the encryption function still + * mangles the input, and the decryption still 'decrypts' it, + * but now in a completely key-independent manner. + * To stop such security disasters, we use blunt force. + * If your program hangs here: fix the fatal routine! + */ + //for(;;); /* Infinite loop, which beats being insecure. */ + return; + } + + /* Check for valid key length. */ + if( key_len < 0 || key_len > 32 ) + { + /* + * This can only happen if a programmer didn't read the limitations + * on the key size. + */ + Twofish_fatal( "Twofish_prepare_key: illegal key length" ); + /* + * A return statement just in case the fatal macro returns. + * The rest of the code assumes that key_len is in range, and would + * buffer-overflow if it wasn't. + * + * Why do we still use a programming language that has problems like + * buffer overflows, when these problems were solved in 1960 with + * the development of Algol? Have we not leared anything? + */ + return; + } + + /* Pad the key with zeroes to the next suitable key length. */ + memcpy( K, key, key_len ); + memset( K+key_len, 0, sizeof(K)-key_len ); + + /* + * Compute kCycles: the number of key cycles used in the cipher. + * 2 for 128-bit keys, 3 for 192-bit keys, and 4 for 256-bit keys. + */ + kCycles = (key_len + 7) >> 3; + /* Handle the special case of very short keys: minimum 2 cycles. */ + if( kCycles < 2 ) + { + kCycles = 2; + } + + /* + * From now on we just pretend to have 8*kCycles bytes of + * key material in K. This handles all the key size cases. + */ + + /* + * We first compute the 40 expanded key words, + * formulas straight from the Twofish specifications. + */ + for( i=0; i<40; i+=2 ) + { + /* + * Due to the byte spacing expected by the h() function + * we can pick the bytes directly from the key K. + * As we use bytes, we never have the little/big endian + * problem. + * + * Note that we apply the rotation function only to simple + * variables, as the rotation macro might evaluate its argument + * more than once. + */ + A = h( i , K , kCycles ); + B = h( i+1, K+4, kCycles ); + B = ROL32( B, 8 ); + + /* Compute and store the round keys. */ + A += B; + B += A; + xkey->K[i] = A; + xkey->K[i+1] = ROL32( B, 9 ); + } + + /* Wipe variables that contained key material. */ + A=B=0; + + /* + * And now the dreaded RS multiplication that few seem to understand. + * The RS matrix is not random, and is specially designed to compute the + * RS matrix multiplication in a simple way. + * + * We work in the field GF(2)[x]/x^8+x^6+x^3+x^2+1. Note that this is a + * different field than used for the MDS matrix. + * (At least, it is a different representation because all GF(2^8) + * representations are equivalent in some form.) + * + * We take 8 consecutive bytes of the key and interpret them as + * a polynomial k_0 + k_1 y + k_2 y^2 + ... + k_7 y^7 where + * the k_i bytes are the key bytes and are elements of the finite field. + * We multiply this polynomial by y^4 and reduce it modulo + * y^4 + (x + 1/x)y^3 + (x)y^2 + (x + 1/x)y + 1. + * using straightforward polynomial modulo reduction. + * The coefficients of the result are the result of the RS + * matrix multiplication. When we wrote the Twofish specification, + * the original RS definition used the polynomials, + * but that requires much more mathematical knowledge. + * We were already using matrix multiplication in a finite field for + * the MDS matrix, so I re-wrote the RS operation as a matrix + * multiplication to reduce the difficulty of understanding it. + * Some implementors have not picked up on this simpler method of + * computing the RS operation, even though it is mentioned in the + * specifications. + * + * It is possible to perform these computations faster by using 32-bit + * word operations, but that is not portable and this is not a speed- + * critical area. + * + * We explained the 1/x computation when we did the MDS matrix. + * + * The S vector is stored in K[32..64]. + * The S vector has to be reversed, so we loop cross-wise. + * + * Note the weird byte spacing of the S-vector, to match the even + * or odd key words arrays. See the discussion at the Hxx macros for + * details. + */ + kptr = K + 8*kCycles; /* Start at end of key */ + sptr = K + 32; /* Start at start of S */ + + /* Loop over all key material */ + while( kptr > K ) + { + kptr -= 8; + /* + * Initialise the polynimial in sptr[0..12] + * The first four coefficients are 0 as we have to multiply by y^4. + * The next 8 coefficients are from the key material. + */ + memset( sptr, 0, 4 ); + memcpy( sptr+4, kptr, 8 ); + + /* + * The 12 bytes starting at sptr are now the coefficients of + * the polynomial we need to reduce. + */ + + /* Loop over the polynomial coefficients from high to low */ + t = sptr+11; + /* Keep looping until polynomial is degree 3; */ + while( t > sptr+3 ) + { + /* Pick up the highest coefficient of the poly. */ + b = *t; + + /* + * Compute x and (x+1/x) times this coefficient. + * See the MDS matrix implementation for a discussion of + * multiplication by x and 1/x. We just use different + * constants here as we are in a + * different finite field representation. + * + * These two statements set + * bx = (x) * b + * bxx= (x + 1/x) * b + */ + bx = (Byte)((b<<1) ^ rs_poly_const[ b>>7 ]); + bxx= (Byte)((b>>1) ^ rs_poly_div_const[ b&1 ] ^ bx); + + /* + * Subtract suitable multiple of + * y^4 + (x + 1/x)y^3 + (x)y^2 + (x + 1/x)y + 1 + * from the polynomial, except that we don't bother + * updating t[0] as it will become zero anyway. + */ + t[-1] ^= bxx; + t[-2] ^= bx; + t[-3] ^= bxx; + t[-4] ^= b; + + /* Go to the next coefficient. */ + t--; + } + + /* Go to next S-vector word, obeying the weird spacing rules. */ + sptr += 8; + } + + /* Wipe variables that contained key material. */ + b = bx = bxx = 0; + + /* And finally, we can compute the key-dependent S-boxes. */ + fill_keyed_sboxes( &K[32], kCycles, xkey ); + + /* Wipe array that contained key material. */ + memset( K, 0, sizeof( K ) ); +} + + +/* + * We can now start on the actual encryption and decryption code. + * As these are often speed-critical we will use a lot of macros. + */ + +/* + * The g() function is the heart of the round function. + * We have two versions of the g() function, one without an input + * rotation and one with. + * The pre-computed S-boxes make this pretty simple. + */ +#define g0(X,xkey) \ +(xkey->s[0][b0(X)]^xkey->s[1][b1(X)]^xkey->s[2][b2(X)]^xkey->s[3][b3(X)]) + +#define g1(X,xkey) \ +(xkey->s[0][b3(X)]^xkey->s[1][b0(X)]^xkey->s[2][b1(X)]^xkey->s[3][b2(X)]) + +/* + * A single round of Twofish. The A,B,C,D are the four state variables, + * T0 and T1 are temporaries, xkey is the expanded key, and r the + * round number. + * + * Note that this macro does not implement the swap at the end of the round. + */ +#define ENCRYPT_RND( A,B,C,D, T0, T1, xkey, r ) \ +T0 = g0(A,xkey); T1 = g1(B,xkey);\ +C ^= T0+T1+xkey->K[8+2*(r)]; C = ROR32(C,1);\ +D = ROL32(D,1); D ^= T0+2*T1+xkey->K[8+2*(r)+1] + +/* + * Encrypt a single cycle, consisting of two rounds. + * This avoids the swapping of the two halves. + * Parameter r is now the cycle number. + */ +#define ENCRYPT_CYCLE( A, B, C, D, T0, T1, xkey, r ) \ +ENCRYPT_RND( A,B,C,D,T0,T1,xkey,2*(r) );\ +ENCRYPT_RND( C,D,A,B,T0,T1,xkey,2*(r)+1 ) + +/* Full 16-round encryption */ +#define ENCRYPT( A,B,C,D,T0,T1,xkey ) \ +ENCRYPT_CYCLE( A,B,C,D,T0,T1,xkey, 0 );\ +ENCRYPT_CYCLE( A,B,C,D,T0,T1,xkey, 1 );\ +ENCRYPT_CYCLE( A,B,C,D,T0,T1,xkey, 2 );\ +ENCRYPT_CYCLE( A,B,C,D,T0,T1,xkey, 3 );\ +ENCRYPT_CYCLE( A,B,C,D,T0,T1,xkey, 4 );\ +ENCRYPT_CYCLE( A,B,C,D,T0,T1,xkey, 5 );\ +ENCRYPT_CYCLE( A,B,C,D,T0,T1,xkey, 6 );\ +ENCRYPT_CYCLE( A,B,C,D,T0,T1,xkey, 7 ) + +/* + * A single round of Twofish for decryption. It differs from + * ENCRYTP_RND only because of the 1-bit rotations. + */ +#define DECRYPT_RND( A,B,C,D, T0, T1, xkey, r ) \ +T0 = g0(A,xkey); T1 = g1(B,xkey);\ +C = ROL32(C,1); C ^= T0+T1+xkey->K[8+2*(r)];\ +D ^= T0+2*T1+xkey->K[8+2*(r)+1]; D = ROR32(D,1) + +/* + * Decrypt a single cycle, consisting of two rounds. + * This avoids the swapping of the two halves. + * Parameter r is now the cycle number. + */ +#define DECRYPT_CYCLE( A, B, C, D, T0, T1, xkey, r ) \ +DECRYPT_RND( A,B,C,D,T0,T1,xkey,2*(r)+1 );\ +DECRYPT_RND( C,D,A,B,T0,T1,xkey,2*(r) ) + +/* Full 16-round decryption. */ +#define DECRYPT( A,B,C,D,T0,T1, xkey ) \ +DECRYPT_CYCLE( A,B,C,D,T0,T1,xkey, 7 );\ +DECRYPT_CYCLE( A,B,C,D,T0,T1,xkey, 6 );\ +DECRYPT_CYCLE( A,B,C,D,T0,T1,xkey, 5 );\ +DECRYPT_CYCLE( A,B,C,D,T0,T1,xkey, 4 );\ +DECRYPT_CYCLE( A,B,C,D,T0,T1,xkey, 3 );\ +DECRYPT_CYCLE( A,B,C,D,T0,T1,xkey, 2 );\ +DECRYPT_CYCLE( A,B,C,D,T0,T1,xkey, 1 );\ +DECRYPT_CYCLE( A,B,C,D,T0,T1,xkey, 0 ) + +/* + * A macro to read the state from the plaintext and do the initial key xors. + * The koff argument allows us to use the same macro + * for the decryption which uses different key words at the start. + */ +#define GET_INPUT( src, A,B,C,D, xkey, koff ) \ +A = GET32(src )^xkey->K[ koff]; B = GET32(src+ 4)^xkey->K[1+koff]; \ +C = GET32(src+ 8)^xkey->K[2+koff]; D = GET32(src+12)^xkey->K[3+koff] + +/* + * Similar macro to put the ciphertext in the output buffer. + * We xor the keys into the state variables before we use the PUT32 + * macro as the macro might use its argument multiple times. + */ +#define PUT_OUTPUT( A,B,C,D, dst, xkey, koff ) \ +A ^= xkey->K[ koff]; B ^= xkey->K[1+koff]; \ +C ^= xkey->K[2+koff]; D ^= xkey->K[3+koff]; \ +PUT32( A, dst ); PUT32( B, dst+ 4 ); \ +PUT32( C, dst+8 ); PUT32( D, dst+12 ) + + +/* + * Twofish block encryption + * + * Arguments: + * xkey expanded key array + * p 16 bytes of plaintext + * c 16 bytes in which to store the ciphertext + */ +void Twofish_encrypt_block( const Twofish_key * xkey, Byte p[16], Byte c[16]) { + UInt32 A,B,C,D,T0,T1; /* Working variables */ + + /* Get the four plaintext words xorred with the key */ + GET_INPUT( p, A,B,C,D, xkey, 0 ); + + /* Do 8 cycles (= 16 rounds) */ + ENCRYPT( A,B,C,D,T0,T1,xkey ); + + /* Store them with the final swap and the output whitening. */ + PUT_OUTPUT( C,D,A,B, c, xkey, 4 ); +} + + +/* + * Twofish block decryption. + * + * Arguments: + * xkey expanded key array + * p 16 bytes of plaintext + * c 16 bytes in which to store the ciphertext + */ +void Twofish_decrypt_block( const Twofish_key * xkey, Byte c[16], Byte p[16]) { + UInt32 A,B,C,D,T0,T1; /* Working variables */ + + /* Get the four plaintext words xorred with the key */ + GET_INPUT( c, A,B,C,D, xkey, 4 ); + + /* Do 8 cycles (= 16 rounds) */ + DECRYPT( A,B,C,D,T0,T1,xkey ); + + /* Store them with the final swap and the output whitening. */ + PUT_OUTPUT( C,D,A,B, p, xkey, 0 ); +} + +/* + * Using the macros it is easy to make special routines for + * CBC mode, CTR mode etc. The only thing you might want to + * add is a XOR_PUT_OUTPUT which xors the outputs into the + * destinationa instead of overwriting the data. This requires + * a XOR_PUT32 macro as well, but that should all be trivial. + * + * I thought about including routines for the separate cipher + * modes here, but it is unclear which modes should be included, + * and each encryption or decryption routine takes up a lot of code space. + * Also, I don't have any test vectors for any cipher modes + * with Twofish. + */ + +#define TWOFISH_BLOCKSIZE 16 + +Twofish_UInt64 Twofish_get_block_count(Twofish_context *context, Twofish_UInt64 input_lenght) { + if(context->options & Twofish_option_PaddingPKCS7) { + return 1 + (input_lenght / TWOFISH_BLOCKSIZE); + } + return (input_lenght / TWOFISH_BLOCKSIZE); +} + +Twofish_UInt64 Twofish_get_output_length(Twofish_context *context, Twofish_UInt64 input_lenght) { + return TWOFISH_BLOCKSIZE * Twofish_get_block_count(context, input_lenght); +} + +#define TWOFISH_MIN(x,y) (x) < (y) ? (x) : (y) + +void Twofish_encrypt(Twofish_context *context, Twofish_Byte *input, Twofish_UInt64 input_length, Twofish_Byte *output, Twofish_UInt64 output_length) { + Twofish_UInt64 blockCount = Twofish_get_block_count(context, input_length); + if(output_length < Twofish_get_output_length(context, input_length)) { + return; + } + + for(uint32_t blockIndex = 0; blockIndex < blockCount; blockIndex++) { + uint8_t inputBlock[TWOFISH_BLOCKSIZE]; + uint8_t copy_length = TWOFISH_MIN(input_length - blockIndex * TWOFISH_BLOCKSIZE, TWOFISH_BLOCKSIZE); + uint8_t paddingCount = (TWOFISH_BLOCKSIZE - copy_length); + if(copy_length > 0) { + memcpy(inputBlock, (input+(blockIndex*TWOFISH_BLOCKSIZE)), TWOFISH_BLOCKSIZE); + } + for(int index=0; index < paddingCount; index++) { + inputBlock[TWOFISH_BLOCKSIZE - 1 - index] = paddingCount; + } + for(uint8_t index=0; index < TWOFISH_BLOCKSIZE; index++) { + inputBlock[index] ^= context->iv[index]; + } + uint8_t outputBlock[TWOFISH_BLOCKSIZE]; + Twofish_encrypt_block(&(context->key), inputBlock, outputBlock); + /* update iv with block */ + memcpy(context->iv, outputBlock, TWOFISH_BLOCKSIZE); + memcpy((output + (blockIndex * TWOFISH_BLOCKSIZE)), outputBlock, TWOFISH_BLOCKSIZE); + /* whipe input plain text */ + //memset(inputBlock, 0, sizeof(inputBlock)); + } +} + +void Twofish_decrypt(Twofish_context *context, Twofish_Byte *input, Twofish_UInt64 input_length, Twofish_Byte *output, Twofish_UInt64 *output_length) { + if(NULL == output) { + return; + } + if(*output_length < input_length) { + return; + } + Twofish_UInt64 blockCount = input_length / TWOFISH_BLOCKSIZE; + for(uint32_t blockIndex = 0; blockIndex < blockCount; blockIndex++) { + uint8_t inputBlock[TWOFISH_BLOCKSIZE]; + memcpy(inputBlock, (input+(blockIndex*TWOFISH_BLOCKSIZE)), TWOFISH_BLOCKSIZE); + uint8_t outputBlock[TWOFISH_BLOCKSIZE]; + Twofish_decrypt_block(&(context->key), inputBlock, outputBlock); + for(uint8_t index=0; index < TWOFISH_BLOCKSIZE; index++) { + outputBlock[index] ^= context->iv[index]; + } + /* update iv with block */ + memcpy(context->iv, inputBlock, TWOFISH_BLOCKSIZE); + memcpy((output + (blockIndex * TWOFISH_BLOCKSIZE)), outputBlock, TWOFISH_BLOCKSIZE); + } + *output_length = (input_length - (Twofish_Byte)output[input_length-1]); +} + diff --git a/src/crypto/twofish.h b/src/crypto/twofish.h new file mode 100644 index 0000000000..ada106a07f --- /dev/null +++ b/src/crypto/twofish.h @@ -0,0 +1,207 @@ +/* + * Fast, portable, and easy-to-use Twofish implementation, + * Version 0.3. + * Copyright (c) 2002 by Niels Ferguson. + * + * See the twofish.c file for the details of the how and why of this code. + * + * The author hereby grants a perpetual license to everybody to + * use this code for any purpose as long as the copyright message is included + * in the source code of this or any derived work. + */ + +#pragma once + +#include + +/* + * PLATFORM FIXES + * ============== + * + * The following definitions have to be fixed for each particular platform + * you work on. If you have a multi-platform program, you no doubt have + * portable definitions that you can substitute here without changing + * the rest of the code. + * + * The defaults provided here should work on most PC compilers. + */ + + +/* + * A Twofish_Byte must be an unsigned 8-bit integer. + * It must also be the elementary data size of your C platform, + * i.e. sizeof( Twofish_Byte ) == 1. + */ +typedef uint8_t Twofish_Byte; + +/* + * A Twofish_UInt32 must be an unsigned integer of at least 32 bits. + * + * This type is used only internally in the implementation, so ideally it + * would not appear in the header file, but it is used inside the + * Twofish_key structure which means it has to be included here. + */ +typedef uint32_t Twofish_UInt32; +typedef uint64_t Twofish_UInt64; + + +/* + * END OF PLATFORM FIXES + * ===================== + * + * You should not have to touch the rest of this file, but the code + * in twofish.c has a few things you need to fix too. + */ + + +/* + * Structure that contains a prepared Twofish key. + * A cipher key is used in two stages. In the first stage it is converted + * form the original form to an internal representation. + * This internal form is then used to encrypt and decrypt data. + * This structure contains the internal form. It is rather large: 4256 bytes + * on a platform with 32-bit unsigned values. + * + * Treat this as an opague structure, and don't try to manipulate the + * elements in it. I wish I could hide the inside of the structure, + * but C doesn't allow that. + */ +typedef struct { + Twofish_UInt32 s[4][256]; /* pre-computed S-boxes */ + Twofish_UInt32 K[40]; /* Round key words */ +} Twofish_key; + +typedef enum { + Twofish_option_CBC, + Twofish_option_PaddingPKCS7, + Twofish_options_default = Twofish_option_CBC | Twofish_option_PaddingPKCS7 +} Twofish_options; + +typedef struct { + Twofish_Byte iv[16]; + Twofish_key key; + Twofish_options options; +} Twofish_context; + + +/* + * Initialise and test the Twofish implementation. + * + * This function MUST be called before any other function in the + * Twofish implementation is called. + * It only needs to be called once. + * + * Apart from initialising the implementation it performs a self test. + * If the Twofish_fatal function is not called, the code passed the test. + * (See the twofish.c file for details on the Twofish_fatal function.) + */ +extern void Twofish_initialise(void); + + +/* + * Convert a cipher key to the internal form used for + * encryption and decryption. + * + * The cipher key is an array of bytes; the Twofish_Byte type is + * defined above to a type suitable on your platform. + * + * Any key must be converted to an internal form in the Twofisk_key structure + * before it can be used. + * The encryption and decryption functions only work with the internal form. + * The conversion to internal form need only be done once for each key value. + * + * Be sure to wipe all key storage, including the Twofish_key structure, + * once you are done with the key data. + * A simple memset( TwofishKey, 0, sizeof( TwofishKey ) ) will do just fine. + * + * Unlike most implementations, this one allows any key size from 0 bytes + * to 32 bytes. According to the Twofish specifications, + * irregular key sizes are handled by padding the key with zeroes at the end + * until the key size is 16, 24, or 32 bytes, whichever + * comes first. Note that each key of irregular size is equivalent to exactly + * one key of 16, 24, or 32 bytes. + * + * WARNING: Short keys have low entropy, and result in low security. + * Anything less than 8 bytes is utterly insecure. For good security + * use at least 16 bytes. I prefer to use 32-byte keys to prevent + * any collision attacks on the key. + * + * The key length argument key_len must be in the proper range. + * If key_len is not in the range 0,...,32 this routine attempts to generate + * a fatal error (depending on the code environment), + * and at best (or worst) returns without having done anything. + * + * Arguments: + * key Array of key bytes + * key_len Number of key bytes, must be in the range 0,1,...,32. + * xkey Pointer to an Twofish_key structure that will be filled + * with the internal form of the cipher key. + */ +extern void Twofish_prepare_key( const Twofish_Byte key[], int key_len, Twofish_key * xkey ); + + +/* + * Encrypt a single block of data. + * + * This function encrypts a single block of 16 bytes of data. + * If you want to encrypt a larger or variable-length message, + * you will have to use a cipher mode, such as CBC or CTR. + * These are outside the scope of this implementation. + * + * The xkey structure is not modified by this routine, and can be + * used for further encryption and decryption operations. + * + * Arguments: + * xkey pointer to Twofish_key, internal form of the key + * produces by Twofish_prepare_key() + * p Plaintext to be encrypted + * c Place to store the ciphertext + */ +extern void Twofish_encrypt_block(const Twofish_key * xkey, Twofish_Byte p[16], Twofish_Byte c[16]); + +/* + * Encrypt arbitratry dta + * + * This functions uses CBC and PKS#7 padding to encrypt the intput data + * + * Arguments: + * context pointer to the context for the opration + * input pointer to the plaintext input data array + * input_lenght length of the plaintext array + * output pointer to the output buffer. This needs to be preallocated by the caller! + * output_length available space for the output buffer. Use Twofish_get_output_lenght to determine the size an allocate the buffer! + */ +extern void Twofish_encrypt(Twofish_context *context, Twofish_Byte *input, Twofish_UInt64 input_length, Twofish_Byte *output, Twofish_UInt64 output_length); +/* + * Determine the output buffer size for a given input lenght + * + * This function simply calculated the size of the output if a given input is used. + */ +extern Twofish_UInt64 Twofish_get_output_length(Twofish_context *context, Twofish_UInt64 input_lenght); +/* + * Decrypt a single block of data. + * + * This function decrypts a single block of 16 bytes of data. + * If you want to decrypt a larger or variable-length message, + * you will have to use a cipher mode, such as CBC or CTR. + * These are outside the scope of this implementation. + * + * The xkey structure is not modified by this routine, and can be + * used for further encryption and decryption operations. + * + * Arguments: + * xkey pointer to Twofish_key, internal form of the key + * produces by Twofish_prepare_key() + * c Ciphertext to be decrypted + * p Place to store the plaintext + */ +extern void Twofish_decrypt_block(const Twofish_key * xkey, Twofish_Byte c[16], Twofish_Byte p[16]); + + +/* output length denotes the space available in output an needs ot be at least as big as input_length + * after exiting, output_lenght contains the actual ammount of data written to output. Use this to copy the plaintext + */ +extern void Twofish_decrypt(Twofish_context *context, Twofish_Byte *input, Twofish_UInt64 input_length, Twofish_Byte *output, Twofish_UInt64 *output_length); + +extern void Twofish_setup(Twofish_context *context, Twofish_Byte key[32], Twofish_Byte iv[16], Twofish_options options); + diff --git a/src/crypto/x25519.cpp b/src/crypto/x25519.cpp new file mode 100644 index 0000000000..9fa59e0dd7 --- /dev/null +++ b/src/crypto/x25519.cpp @@ -0,0 +1,119 @@ +// Copyright (c) 2022, 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 "x25519.h" + +//local headers +#include "crypto/crypto.h" +extern "C" +{ +#include "mx25519.h" +} + +//third party headers + +//standard headers +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "crypto" + +namespace crypto +{ + +/// File-scope data +static const x25519_scalar X25519_EIGHT{ mx25519_privkey{ .data = { 8 } } }; + +//------------------------------------------------------------------------------------------------------------------- +x25519_scalar x25519_eight() +{ + return X25519_EIGHT; +} +//------------------------------------------------------------------------------------------------------------------- +x25519_secret_key x25519_secret_key_gen() +{ + x25519_secret_key privkey; + do + { + crypto::rand(32, privkey.data); + privkey.data[0] &= 255 - 7; + privkey.data[31] &= 127; + } while (privkey == x25519_secret_key{}); + + return privkey; +} +//------------------------------------------------------------------------------------------------------------------- +x25519_pubkey x25519_pubkey_gen() +{ + const x25519_secret_key privkey{x25519_secret_key_gen()}; + x25519_pubkey pubkey; + x25519_scmul_base(privkey, pubkey); + + return pubkey; +} +//------------------------------------------------------------------------------------------------------------------- +bool x25519_scalar_is_canonical(const x25519_scalar &test_scalar) +{ + //todo: is this constant time? + return (test_scalar.data[0] & 7) == 0 && + (test_scalar.data[31] & 128) == 0; +} +//------------------------------------------------------------------------------------------------------------------- +void x25519_scmul_base(const x25519_scalar &scalar, x25519_pubkey &result_out) +{ + static const mx25519_impl *impl{mx25519_select_impl(mx25519_type::MX25519_TYPE_AUTO)}; + mx25519_scmul_base(impl, &result_out, &scalar); +} +//------------------------------------------------------------------------------------------------------------------- +void x25519_scmul_key(const x25519_scalar &scalar, const x25519_pubkey &pubkey, x25519_pubkey &result_out) +{ + static const mx25519_impl *impl{mx25519_select_impl(mx25519_type::MX25519_TYPE_AUTO)}; + mx25519_scmul_key(impl, &result_out, &scalar, &pubkey); +} +//------------------------------------------------------------------------------------------------------------------- +void x25519_invmul_key(std::vector privkeys_to_invert, + const x25519_pubkey &initial_pubkey, + x25519_pubkey &result_out) +{ + // 1. (1/({privkey1 * privkey2 * ...})) + // note: mx25519_invkey() will error if the resulting X25519 scalar is >= 2^255, so we 'search' for a valid solution + x25519_secret_key inverted_xkey; + result_out = initial_pubkey; + + while (mx25519_invkey(&inverted_xkey, privkeys_to_invert.data(), privkeys_to_invert.size()) != 0) + { + privkeys_to_invert.emplace_back(X25519_EIGHT); //add 8 to keys to invert + x25519_scmul_key(X25519_EIGHT, result_out, result_out); //xK = 8 * xK + } + + // 2. (1/([8*8*...*8] * {privkey1 * privkey2 * ...})) * [8*8*...*8] * xK + x25519_scmul_key(inverted_xkey, result_out, result_out); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace crypto diff --git a/src/crypto/x25519.h b/src/crypto/x25519.h new file mode 100644 index 0000000000..ef8db93a21 --- /dev/null +++ b/src/crypto/x25519.h @@ -0,0 +1,123 @@ +// Copyright (c) 2022, 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. + +// Interface for an x25519 implementation (mx25519). + +#pragma once + +//local headers +#include "crypto.h" +#include "generic-ops.h" +#include "memwipe.h" +#include "mlocker.h" + +//third party headers + +//standard headers +#include + +//forward declarations + + +namespace crypto +{ + +extern "C" +{ +#include "mx25519.h" +} + +struct x25519_pubkey : public mx25519_pubkey +{ + x25519_pubkey() = default; + x25519_pubkey(const mx25519_pubkey &other) { *this = other; } + x25519_pubkey& operator=(const mx25519_pubkey &other) { memcpy(data, other.data, 32); return *this; } +}; +struct x25519_scalar : public mx25519_privkey +{ + x25519_scalar() = default; + x25519_scalar(const mx25519_privkey &other) { *this = other; } + x25519_scalar& operator=(const mx25519_privkey &other) { memcpy(data, other.data, 32); return *this; } +}; +struct x25519_secret_key : public epee::mlocked> +{ + x25519_secret_key() = default; + x25519_secret_key(const x25519_scalar &other) { *this = other; } + x25519_secret_key& operator=(const x25519_scalar &other) { memcpy(data, other.data, 32); return *this; } +}; + +/** +* brief: x25519_eight - scalar 8 +* return: scalar 8 +*/ +x25519_scalar x25519_eight(); +/** +* brief: x25519_secret_key_gen - generate a random x25519 privkey +* return: random canonical x25519 privkey +*/ +x25519_secret_key x25519_secret_key_gen(); +/** +* brief: x25519_pubkey_gen - generate a random x25519 pubkey +* return: random x25519 pubkey +*/ +x25519_pubkey x25519_pubkey_gen(); +/** +* brief: x25519_scalar_is_canonical - check that an X25519 scalar is canonical +* - expect: 2^255 > scalar >= 8 (i.e. last bit and first three bits not set) +* result: true if input scalar is canonical +*/ +bool x25519_scalar_is_canonical(const x25519_scalar &test_scalar); +/** +* brief: x25519_scmul_base - compute scalar * xG +* param: scalar - scalar to multiply +* result: scalar * xG +*/ +void x25519_scmul_base(const x25519_scalar &scalar, x25519_pubkey &result_out); +/** +* brief: x25519_scmul_key - compute scalar * pubkey +* param: scalar - scalar to multiply +* param: pubkey - public key to multiple against +* result: scalar * pubkey +*/ +void x25519_scmul_key(const x25519_scalar &scalar, const x25519_pubkey &pubkey, x25519_pubkey &result_out); +/** +* brief: x25519_invmul_key - compute (1/({privkey1 * privkey2 * ...})) * initial_pubkey +* param: privkeys_to_invert - {privkey1, privkey2, ...} +* param: initial_pubkey - base key for inversion +* result: (1/({privkey1 * privkey2 * ...})) * initial_pubkey +*/ +void x25519_invmul_key(std::vector privkeys_to_invert, + const x25519_pubkey &initial_pubkey, + x25519_pubkey &result_out); + +} //namespace crypto + +/// upgrade x25519 keys +CRYPTO_MAKE_HASHABLE(x25519_pubkey) +CRYPTO_MAKE_HASHABLE_CONSTANT_TIME(x25519_scalar) +CRYPTO_MAKE_HASHABLE_CONSTANT_TIME(x25519_secret_key) diff --git a/src/multisig/multisig_kex_msg_serialization.h b/src/cryptonote_basic/account_generators.h similarity index 59% rename from src/multisig/multisig_kex_msg_serialization.h rename to src/cryptonote_basic/account_generators.h index d1e07ea8ae..11a465280f 100644 --- a/src/multisig/multisig_kex_msg_serialization.h +++ b/src/cryptonote_basic/account_generators.h @@ -29,50 +29,48 @@ #pragma once #include "crypto/crypto.h" -#include "serialization/containers.h" -#include "serialization/crypto.h" -#include "serialization/serialization.h" +#include "crypto/generators.h" -#include -#include +namespace cryptonote +{ + +enum class account_generator_era : unsigned char +{ + unknown = 0, + cryptonote = 1, //and ringct + seraphis = 2 +}; -namespace multisig +struct account_generators { - /// round 1 kex message - struct multisig_kex_msg_serializable_round1 - { - // privkey stored in msg - crypto::secret_key msg_privkey; - // pubkey used to sign this msg - crypto::public_key signing_pubkey; - // message signature - crypto::signature signature; + crypto::public_key m_primary; //e.g. for spend key + crypto::public_key m_secondary; //e.g. for view key +}; - BEGIN_SERIALIZE() - FIELD(msg_privkey) - FIELD(signing_pubkey) - FIELD(signature) - END_SERIALIZE() - }; +inline crypto::public_key get_primary_generator(const account_generator_era era) +{ + if (era == account_generator_era::cryptonote) + return crypto::get_G(); + else if (era == account_generator_era::seraphis) + return crypto::get_U(); + else + return crypto::null_pkey; //error +} - /// general kex message (if round > 1) - struct multisig_kex_msg_serializable_general - { - // key exchange round this msg was produced for - std::uint32_t kex_round; - // pubkeys stored in msg - std::vector msg_pubkeys; - // pubkey used to sign this msg - crypto::public_key signing_pubkey; - // message signature - crypto::signature signature; +inline crypto::public_key get_secondary_generator(const account_generator_era era) +{ + if (era == account_generator_era::cryptonote) + return crypto::get_G(); + else if (era == account_generator_era::seraphis) + return crypto::get_X(); + else + return crypto::null_pkey; //error +} + +inline account_generators get_account_generators(const account_generator_era era) +{ + return account_generators{get_primary_generator(era), get_secondary_generator(era)}; +} - BEGIN_SERIALIZE() - VARINT_FIELD(kex_round) - FIELD(msg_pubkeys) - FIELD(signing_pubkey) - FIELD(signature) - END_SERIALIZE() - }; -} //namespace multisig +} //namespace cryptonote diff --git a/src/cryptonote_basic/subaddress_index.h b/src/cryptonote_basic/subaddress_index.h index 788040ae4f..f1ee6d3c79 100644 --- a/src/cryptonote_basic/subaddress_index.h +++ b/src/cryptonote_basic/subaddress_index.h @@ -29,6 +29,7 @@ #pragma once #include "serialization/keyvalue_serialization.h" +#include "serialization/serialization.h" #include #include #include diff --git a/src/cryptonote_config.h b/src/cryptonote_config.h index 45da75b66a..ddc352730f 100644 --- a/src/cryptonote_config.h +++ b/src/cryptonote_config.h @@ -231,6 +231,20 @@ namespace config std::string const GENESIS_TX = "013c01ff0001ffffffffffff03029b2e4c0281c0b02e7c53291a94d1d0cbff8883f8024f5142ee494ffbbd08807121017767aafcde9be00dcfd098715ebcf7f410daebc582fda69d24a28e9d0bc890d1"; uint32_t const GENESIS_NONCE = 10000; + // misc config + const constexpr uint16_t SP_MAX_COINBASE_OUTPUTS_V1 = 60000; //todo: what to set this to? + const constexpr uint16_t SP_MAX_INPUTS_V1 = 128 - BULLETPROOF_MAX_OUTPUTS; + const constexpr uint16_t SP_MAX_OUTPUTS_V1 = BULLETPROOF_MAX_OUTPUTS; + const constexpr uint16_t SP_GROOTLE_N_V1 = 2; + const constexpr uint16_t SP_GROOTLE_M_V1 = 7; //v1: 2^7 = 128 + // note: SP_REF_SET_* version number should line up with intended grootle n^m decomposition + const constexpr std::uint64_t LEGACY_RING_SIZE_V1 = 16; + const constexpr std::uint64_t SP_REF_SET_BIN_RADIUS_V1 = 127; + const constexpr std::uint64_t SP_REF_SET_NUM_BIN_MEMBERS_V1 = 8; + const constexpr std::uint64_t DISCRETIZED_FEE_LEVEL_NUMERATOR_X100 = 150; //fee level factor = 1.5 + const constexpr std::uint64_t DISCRETIZED_FEE_SIG_FIGS = 1; + const constexpr std::uint64_t BULLETPROOF_PLUS2_MAX_COMMITMENTS = 128; + // Hash domain separators const char HASH_KEY_BULLETPROOF_EXPONENT[] = "bulletproof"; const char HASH_KEY_BULLETPROOF_PLUS_EXPONENT[] = "bulletproof_plus"; @@ -254,6 +268,67 @@ namespace config const constexpr char HASH_KEY_MULTISIG_TX_PRIVKEYS_SEED[] = "multisig_tx_privkeys_seed"; const constexpr char HASH_KEY_MULTISIG_TX_PRIVKEYS[] = "multisig_tx_privkeys"; const constexpr char HASH_KEY_TXHASH_AND_MIXRING[] = "txhash_and_mixring"; + const constexpr char HASH_KEY_MULTISIG_BINONCE_MERGE_FACTOR[] = "multisig_binonce_merge_factor"; + const constexpr char HASH_KEY_SERAPHIS_X[] = "seraphis_X"; + const constexpr char HASH_KEY_SERAPHIS_U[] = "seraphis_U"; + + const constexpr char TRANSCRIPT_PREFIX[] = "monero"; + + const constexpr char HASH_KEY_LEGACY_ENOTE_IDENTIFIER[] = "legacy_enote_identifier"; + const constexpr char HASH_KEY_LEGACY_RING_SIGNATURES_MESSAGE_V1[] = "legacy_ring_signatures_message_v1"; + + const constexpr char HASH_KEY_SERAPHIS_GENERATOR_FACTORY[] = "sp_generator_factory"; + const constexpr char HASH_KEY_SERAPHIS_SQUASHED_ENOTE[] = "sp_squashed_enote"; + const constexpr char HASH_KEY_SERAPHIS_TX_CONTEXTUAL_VALIDATION_ID_V1[] = "sp_tx_contextual_validation_id_v1"; + const constexpr char HASH_KEY_SERAPHIS_TX_CONTEXTUAL_VALIDATION_ID_V2[] = "sp_tx_contextual_validation_id_v2"; + const constexpr char HASH_KEY_SERAPHIS_TX_PROPOSAL_MESSAGE_V1[] = "sp_tx_proposal_message_v1"; + const constexpr char HASH_KEY_SERAPHIS_MEMBERSHIP_PROOF_MESSAGE_V1[] = "sp_membership_proof_message_v1"; + const constexpr char HASH_KEY_SERAPHIS_INPUT_IMAGES_PREFIX_V1[] = "sp_input_images_prefix_v1"; + const constexpr char HASH_KEY_SERAPHIS_TX_PROOFS_PREFIX_V1[] = "sp_tx_proofs_prefix_v1"; + const constexpr char HASH_KEY_SERAPHIS_TX_ARTIFACTS_MERKLE_ROOT_V1[] = "sp_tx_artifacts_merkle_root_v1"; + const constexpr char HASH_KEY_SERAPHIS_TRANSACTION_TYPE_COINBASE_V1[] = "sp_txtype_coinbase_v1"; + const constexpr char HASH_KEY_SERAPHIS_TRANSACTION_TYPE_SQUASHED_V1[] = "sp_txtype_squashed_v1"; + + const constexpr char HASH_KEY_SERAPHIS_ADDRESS_OWNERSHIP_PROOF_OFFSET_V1[] = "sp_address_ownership_proof_offset_v1"; + const constexpr char HASH_KEY_SERAPHIS_ENOTE_KEY_IMAGE_PROOF_MESSAGE_V1[] = "sp_enote_key_image_proof_message_v1"; + const constexpr char HASH_KEY_SERAPHIS_ENOTE_UNSPENT_PROOF_MESSAGE_V1[] = "sp_enote_unspent_proof_message_v1"; + + const constexpr char HASH_KEY_BULLETPROOF_PLUS2_TRANSCRIPT[] = "bpp2_transcript"; + const constexpr char HASH_KEY_BULLETPROOF_PLUS2_TRANSCRIPT_UPDATE[] = "bpp2_tupdate"; + const constexpr char HASH_KEY_MATRIX_PROOF_AGGREGATION_COEFF[] = "matrix_proof_aggregation_coeff"; + const constexpr char HASH_KEY_MATRIX_PROOF_CHALLENGE_MSG[] = "matrix_proof_challenge_msg"; + const constexpr char HASH_KEY_MATRIX_PROOF_CHALLENGE[] = "matrix_proof_challenge"; + const constexpr char HASH_KEY_GROOTLE_CHALLENGE[] = "grootle_challenge"; + const constexpr char HASH_KEY_SP_COMPOSITION_PROOF_CHALLENGE_MESSAGE[] = "sp_composition_proof_challenge_message"; + const constexpr char HASH_KEY_SP_COMPOSITION_PROOF_CHALLENGE[] = "sp_composition_proof_challenge"; + const constexpr char HASH_KEY_BINNED_REF_SET_GENERATOR_SEED[] = "binned_refset_generator_seed"; + const constexpr char HASH_KEY_BINNED_REF_SET_MEMBER[] = "binned_refset_member"; + + const constexpr char HASH_KEY_JAMTIS_UNLOCKAMOUNTS_KEY[] = "jamtis_unlock_amounts_key"; + const constexpr char HASH_KEY_JAMTIS_GENERATEADDRESS_SECRET[] = "jamtis_generate_address_secret"; + const constexpr char HASH_KEY_JAMTIS_CIPHERTAG_SECRET[] = "jamtis_cipher_tag_secret"; + const constexpr char HASH_KEY_JAMTIS_FINDRECEIVED_KEY[] = "jamtis_find_received_key"; + const constexpr char HASH_KEY_JAMTIS_INDEX_EXTENSION_GENERATOR[] = "jamtis_index_extension_generator"; + const constexpr char HASH_KEY_JAMTIS_ADDRESS_PRIVKEY[] = "jamtis_address_privkey"; + const constexpr char HASH_KEY_JAMTIS_SPENDKEY_EXTENSION_G[] = "jamtis_spendkey_extension_g"; + const constexpr char HASH_KEY_JAMTIS_SPENDKEY_EXTENSION_X[] = "jamtis_spendkey_extension_x"; + const constexpr char HASH_KEY_JAMTIS_SPENDKEY_EXTENSION_U[] = "jamtis_spendkey_extension_u"; + const constexpr char HASH_KEY_JAMTIS_ADDRESS_TAG_HINT[] = "jamtis_address_tag_hint"; + const constexpr char HASH_KEY_JAMTIS_ENCRYPTED_ADDRESS_TAG[] = "jamtis_encrypted_address_tag"; + const constexpr char HASH_KEY_JAMTIS_VIEW_TAG[] = "jamtis_view_tag"; + const constexpr char HASH_KEY_JAMTIS_SENDER_RECEIVER_SECRET_PLAIN[] = "jamtis_sr_secret_plain"; + const constexpr char HASH_KEY_JAMTIS_SENDER_RECEIVER_SECRET_SELFSEND_DUMMY[] = "jamtis_selfsend_dummy"; + const constexpr char HASH_KEY_JAMTIS_SENDER_RECEIVER_SECRET_SELFSEND_CHANGE[] = "jamtis_selfsend_change"; + const constexpr char HASH_KEY_JAMTIS_SENDER_RECEIVER_SECRET_SELFSEND_SELF_SPEND[] = "jamtis_selfsend_self_spend"; + const constexpr char HASH_KEY_JAMTIS_SENDER_ONETIME_ADDRESS_EXTENSION_G[] = "jamtis_sender_extension_g"; + const constexpr char HASH_KEY_JAMTIS_SENDER_ONETIME_ADDRESS_EXTENSION_X[] = "jamtis_sender_extension_x"; + const constexpr char HASH_KEY_JAMTIS_SENDER_ONETIME_ADDRESS_EXTENSION_U[] = "jamtis_sender_extension_u"; + const constexpr char HASH_KEY_JAMTIS_AMOUNT_BAKED_KEY_PLAIN[] = "jamtis_amount_baked_key_plain"; + const constexpr char HASH_KEY_JAMTIS_AMOUNT_BAKED_KEY_SELFSEND[] = "jamtis_amount_baked_key_selfsend"; + const constexpr char HASH_KEY_JAMTIS_AMOUNT_BLINDING_FACTOR[] = "jamtis_amount_commitment_blinding_factor"; + const constexpr char HASH_KEY_JAMTIS_ENCODED_AMOUNT_MASK[] = "jamtis_encoded_amount_mask"; + const constexpr char HASH_KEY_JAMTIS_INPUT_CONTEXT_COINBASE[] = "jamtis_input_context_coinbase"; + const constexpr char HASH_KEY_JAMTIS_INPUT_CONTEXT_STANDARD[] = "jamtis_input_context_standard"; // Multisig const uint32_t MULTISIG_MAX_SIGNERS{16}; diff --git a/src/multisig/CMakeLists.txt b/src/multisig/CMakeLists.txt index 62ae00e805..70e82f127a 100644 --- a/src/multisig/CMakeLists.txt +++ b/src/multisig/CMakeLists.txt @@ -29,9 +29,20 @@ set(multisig_sources multisig.cpp multisig_account.cpp + multisig_account_era_conversion_msg.cpp multisig_account_kex_impl.cpp + multisig_clsag.cpp multisig_clsag_context.cpp multisig_kex_msg.cpp + multisig_mocks.cpp + multisig_nonce_cache.cpp + multisig_partial_cn_key_image_msg.cpp + multisig_partial_sig_makers.cpp + multisig_signer_set_filter.cpp + multisig_signing_errors.cpp + multisig_signing_helper_types.cpp + multisig_signing_helper_utils.cpp + multisig_sp_composition_proof.cpp multisig_tx_builder_ringct.cpp) set(multisig_headers) @@ -53,5 +64,12 @@ target_link_libraries(multisig cryptonote_core common cncrypto + seraphis_crypto PRIVATE ${EXTRA_LIBRARIES}) + +target_include_directories(multisig + PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}" + PRIVATE + ${Boost_INCLUDE_DIRS}) diff --git a/src/multisig/multisig.cpp b/src/multisig/multisig.cpp index 70baa7f19c..3d4fa794f7 100644 --- a/src/multisig/multisig.cpp +++ b/src/multisig/multisig.cpp @@ -32,7 +32,10 @@ #include "cryptonote_config.h" #include "include_base_utils.h" #include "multisig.h" +#include "multisig_partial_cn_key_image_msg.h" +#include "multisig_signer_set_filter.h" #include "ringct/rctOps.h" +#include "ringct/rctTypes.h" #include #include @@ -44,6 +47,194 @@ namespace multisig { + //---------------------------------------------------------------------------------------------------------------------- + // note: keyshares stored in multisig_partial_cn_key_image_msg's are guaranteed to be canonical (prime order subgroup) + //---------------------------------------------------------------------------------------------------------------------- + static bool try_process_partial_ki_msg(const std::vector &multisig_signers, + const crypto::public_key &expected_onetime_address, + const crypto::public_key &expected_msg_signer, + const multisig_partial_cn_key_image_msg &partial_ki_msg, + std::unordered_set &collected_multisig_keyshares_inout, + std::unordered_set &collected_partial_key_images_inout) + { + // ignore messages from signers outside the designated signer list + if (std::find(multisig_signers.begin(), multisig_signers.end(), expected_msg_signer) == multisig_signers.end()) + return false; + + // ignore message with unexpected signer (probably an upstream mapping bug) + if (!(expected_msg_signer == partial_ki_msg.get_signing_pubkey())) + return false; + + // ignore messages with unexpected onetime address (probably an upstream mapping bug) + if (!(expected_onetime_address == partial_ki_msg.get_onetime_address())) + return false; + + // save the multisig keyshares + for (const crypto::public_key &multisig_keyshare : partial_ki_msg.get_multisig_keyshares()) + collected_multisig_keyshares_inout.insert(multisig_keyshare); + + // save the partial key images + for (const crypto::public_key &partial_ki : partial_ki_msg.get_partial_key_images()) + collected_partial_key_images_inout.insert(partial_ki); + + return true; + } + //---------------------------------------------------------------------------------------------------------------------- + //---------------------------------------------------------------------------------------------------------------------- + static bool try_collect_partial_ki_keyshares(const std::vector &multisig_signers, + const crypto::public_key &expected_onetime_address, + // [ signer : msg ] + const std::unordered_map &partial_ki_msgs, + // [ signer : signer group ] + const std::unordered_map &signers_as_filters, + const signer_set_filter filter, + std::unordered_set &collected_multisig_keyshares_out, + std::unordered_set &collected_partial_key_images_out) + { + collected_multisig_keyshares_out.clear(); + collected_partial_key_images_out.clear(); + + // collect multisig and ki keyshares for this signer subgroup + for (const auto &partial_ki_msg : partial_ki_msgs) + { + // ignore messages with unknown associated signers (continuing here is probably due to a bug) + if (signers_as_filters.find(partial_ki_msg.first) == signers_as_filters.end()) + continue; + // ignore messages from signers not in the specified subgroup + if (!(signers_as_filters.at(partial_ki_msg.first) & filter)) + continue; + + if (!try_process_partial_ki_msg(multisig_signers, + expected_onetime_address, + partial_ki_msg.first, + partial_ki_msg.second, + collected_multisig_keyshares_out, + collected_partial_key_images_out)) + return false; + } + + return true; + } + //---------------------------------------------------------------------------------------------------------------------- + //---------------------------------------------------------------------------------------------------------------------- + static bool try_combine_partial_ki_shares(const crypto::public_key &multisig_base_spend_key, + const std::unordered_set &collected_multisig_keyshares, + const std::unordered_set &collected_partial_key_images, + crypto::public_key &recovered_key_image_core_out) + { + // partial ki shares cannot be combined safely if the multisig base spend key can't be reproduced from the associated + // multisig base spend key keyshares + // - the entire purpose of partial KI messages (which contain dual-base vector proofs) is to prove that the constructed + // key image core has a proper discrete-log relation with the multisig group's base spend key k^s G + // - note: this will fail if the multisig base spend key has a small order subgroup offset, because multisig + // keyshares collected from partial ki messages are 'small order sanitized'; preventing non-canonical multisig + // base spend keys is the responsibility of the account setup process + rct::key nominal_base_spendkey{rct::identity()}; + + for (const crypto::public_key &multisig_keyshare : collected_multisig_keyshares) + rct::addKeys(nominal_base_spendkey, nominal_base_spendkey, rct::pk2rct(multisig_keyshare)); + + if (!(nominal_base_spendkey == rct::pk2rct(multisig_base_spend_key))) + return false; + + // compute the constructed key image core: k^s * Hp(Ko) + rct::key key_image_core{rct::identity()}; + + for (const crypto::public_key &partial_key_image : collected_partial_key_images) + rct::addKeys(key_image_core, key_image_core, rct::pk2rct(partial_key_image)); + + recovered_key_image_core_out = rct::rct2pk(key_image_core); + return true; + } + //---------------------------------------------------------------------------------------------------------------------- + //---------------------------------------------------------------------------------------------------------------------- + static bool try_get_key_image_core(const std::uint32_t multisig_threshold, + const std::vector &multisig_signers, + const crypto::public_key &multisig_base_spend_key, + const crypto::public_key &expected_onetime_address, + // [ signer : msg ] + const std::unordered_map &partial_ki_msgs, + // [ Ko : missing signers ] + std::unordered_map &onetime_addresses_with_insufficient_partial_kis_inout, + // [ Ko : possibly invalid signers ] + std::unordered_map &onetime_addresses_with_invalid_partial_kis_inout, + // [ Ko : KI core ] + std::unordered_map &recovered_key_image_cores_inout) + { + CHECK_AND_ASSERT_THROW_MES(multisig_threshold <= multisig_signers.size(), + "multisig recover cn key image bases: threshold is greater than the number of signers."); + + // 1. identify available signers + signer_set_filter available_signers_filter{}; + std::unordered_map signers_as_filters; + + for (const auto &signer_with_msg : partial_ki_msgs) + { + try { multisig_signer_to_filter(signer_with_msg.first, multisig_signers, signers_as_filters[signer_with_msg.first]); } + catch (...) + { + // skip unknown signers + signers_as_filters.erase(signer_with_msg.first); + continue; + } + + available_signers_filter |= signers_as_filters[signer_with_msg.first]; + } + + // 2. early return if there are insufficient valid signers + if (signers_as_filters.size() < multisig_threshold) + { + onetime_addresses_with_insufficient_partial_kis_inout[expected_onetime_address] = available_signers_filter; + return false; + } + + // 3. get permutations of available signers so we can make a separate ki combination attempt for each possible + // subgroup (this way malicious signers can't pollute honest subgroups) + std::vector filter_permutations; + aggregate_multisig_signer_set_filter_to_permutations(multisig_threshold, + multisig_signers.size(), + available_signers_filter, + filter_permutations); + + // 4. for each permutation of available signers, try to assemble ki shares into a KI core for the specified Ko + std::unordered_set collected_multisig_keyshares_temp; + std::unordered_set collected_partial_key_images_temp; + crypto::public_key recovered_key_image_core_temp; + + for (const signer_set_filter filter : filter_permutations) + { + // a. try to collect collect multisig and ki keyshares for this combination attempt + if (!try_collect_partial_ki_keyshares(multisig_signers, + expected_onetime_address, + partial_ki_msgs, + signers_as_filters, + filter, + collected_multisig_keyshares_temp, + collected_partial_key_images_temp)) + { + onetime_addresses_with_invalid_partial_kis_inout[expected_onetime_address] |= filter; + continue; + } + + // b. try to get the key image core using this subgroup + if (!try_combine_partial_ki_shares(multisig_base_spend_key, + collected_multisig_keyshares_temp, + collected_partial_key_images_temp, + recovered_key_image_core_temp)) + { + // if the assembly attempt fails, record the signer subgroup that caused the failure (add to existing failures) + onetime_addresses_with_invalid_partial_kis_inout[expected_onetime_address] |= filter; + continue; + } + + // c. assembly succeeded + recovered_key_image_cores_inout[expected_onetime_address] = recovered_key_image_core_temp; + return true; + } + + return false; //all attempts failed + } + //---------------------------------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------------------------------- crypto::secret_key get_multisig_blinded_secret_key(const crypto::secret_key &key) { @@ -97,7 +288,15 @@ namespace multisig // - the 'multisig priv keys' here are those held by the local account // - later, we add in the components held by other participants cryptonote::keypair in_ephemeral; - if (!cryptonote::generate_key_image_helper(keys, subaddresses, out_key, tx_public_key, additional_tx_public_keys, real_output_index, in_ephemeral, ki, keys.get_device())) + if (!cryptonote::generate_key_image_helper(keys, + subaddresses, + out_key, + tx_public_key, + additional_tx_public_keys, + real_output_index, + in_ephemeral, + ki, + keys.get_device())) return false; std::unordered_set used; @@ -137,4 +336,34 @@ namespace multisig return true; } //---------------------------------------------------------------------------------------------------------------------- + void multisig_recover_cn_keyimage_cores(const std::uint32_t multisig_threshold, + const std::vector &multisig_signers, + const crypto::public_key &multisig_base_spend_key, + // [ Ko : [ signer : msg ] ] + const std::unordered_map> &partial_ki_msgs, + // [ Ko : missing signers ] + std::unordered_map &onetime_addresses_with_insufficient_partial_kis_out, + // [ Ko : possibly invalid signers ] + std::unordered_map &onetime_addresses_with_invalid_partial_kis_out, + // [ Ko : KI core ] + std::unordered_map &recovered_key_image_cores_out) + { + onetime_addresses_with_insufficient_partial_kis_out.clear(); + onetime_addresses_with_invalid_partial_kis_out.clear(); + recovered_key_image_cores_out.clear(); + + for (const auto &partial_ki_set : partial_ki_msgs) + { + try_get_key_image_core(multisig_threshold, + multisig_signers, + multisig_base_spend_key, + partial_ki_set.first, + partial_ki_set.second, + onetime_addresses_with_insufficient_partial_kis_out, + onetime_addresses_with_invalid_partial_kis_out, + recovered_key_image_cores_out); + } + } + //---------------------------------------------------------------------------------------------------------------------- } //namespace multisig diff --git a/src/multisig/multisig.h b/src/multisig/multisig.h index 2ec2f4e66d..d457959dca 100644 --- a/src/multisig/multisig.h +++ b/src/multisig/multisig.h @@ -30,6 +30,8 @@ #include "crypto/crypto.h" #include "cryptonote_basic/cryptonote_format_utils.h" +#include "multisig_partial_cn_key_image_msg.h" +#include "multisig_signer_set_filter.h" #include "ringct/rctTypes.h" #include @@ -66,4 +68,49 @@ namespace multisig std::size_t real_output_index, const std::vector &pkis, crypto::key_image &ki); + /** + * @brief multisig_recover_cn_keyimage_cores - recover cryptonote-style key image cores k^s * Hp(Ko) for onetime + * addresses Ko owned by a multisig group with aggregate spend privkey k^s + * - Processes multisig partial key image messages to collect key image cores for as many onetime addresses as possible + * with the given messages. The algorithm only requires messages from 'at least' M signers to complete a key image base, + * which means the algorithm works fine if there are more than M messages. + * - The algorithm will attempt to combine keyshares using every available group of messages of size M associated with a + * given onetime address, so malicious signers can't block honest subgroups of size M. + * - Records onetime addresses that have messages but don't have enough messages to complete their key image cores. + * - Records onetime addresses that have messages that record invalid key shares (e.g. because a keyshare that wasn't + * produced by the canonical multisig account setup process was used to make a message). + * - For each set of messages associated with a onetime address, the algorithm tries to compute the multisig group's base + * spend key k^s G by summing together unique 'multisig keyshares' from the messages. If the computed key equals k^s G, + * then the corresponding assembled key image base correctly equals k^s Hp(Ko). + * - NOTE: this algorithm only produces k^s Hp(Ko). It is up to the caller to add in any 'view key'-related material to + * make completed key images. + * + * @param multisig_threshold - the threshold 'M' in the user's M-of-N multisig group + * @param multisig_signers - message-signing pubkeys of all members of the user's multisig group + * @param multisig_base_spend_key - base spend key of the user's multisig group: K^s = k^s G + * @param partial_ki_msgs - map of partial key image messages with format [ Ko : [ signer : msg ] ] + * @outparam onetime_addresses_with_insufficient_partial_kis_out - onetime addresses that don't have enough messages to + * assemble their key image cores, mapped to filters representing the signers who did NOT provide partial ki messages + * for those onetime addresses + * @outparam onetime_addresses_with_invalid_partial_kis_out - onetime addresses with messages that contain invalid key + * shares, mapped to filters representing the signers who MAY have caused partial ki combination to fail; note that + * we include ALL signers who were members of failing subgroups, and don't subtract signers from succeeding subgroups; + * subtracting succeeding signers could allow two malicious signers to collaborate to 'blame' an honest signer for + * partial ki combination failures (i.e. by each of them contributing invalid keyshares that cancel when their messages + * are combined) + * @outparam recovered_key_image_cores_out - successfully assembled key image cores k^s Hp(Ko) for onetime addresses Ko with + * format [ Ko : KI core ] + */ + void multisig_recover_cn_keyimage_cores(const std::uint32_t multisig_threshold, + const std::vector &multisig_signers, + const crypto::public_key &multisig_base_spend_key, + // [ Ko : [ signer : msg ] ] + const std::unordered_map> &partial_ki_msgs, + // [ Ko : missing signers ] + std::unordered_map &onetime_addresses_with_insufficient_partial_kis_out, + // [ Ko : possibly invalid signers ] + std::unordered_map &onetime_addresses_with_invalid_partial_kis_out, + // [ Ko : KI core ] + std::unordered_map &recovered_key_image_cores_out); } //namespace multisig diff --git a/src/multisig/multisig_account.cpp b/src/multisig/multisig_account.cpp index 4f9711b15f..5be970664c 100644 --- a/src/multisig/multisig_account.cpp +++ b/src/multisig/multisig_account.cpp @@ -29,13 +29,21 @@ #include "multisig_account.h" #include "crypto/crypto.h" +extern "C" +{ +#include "crypto/crypto-ops.h" +} +#include "cryptonote_basic/account_generators.h" #include "cryptonote_config.h" #include "include_base_utils.h" #include "multisig.h" +#include "multisig_account_era_conversion_msg.h" #include "multisig_kex_msg.h" +#include "multisig_signer_set_filter.h" #include "ringct/rctOps.h" #include "ringct/rctTypes.h" +#include #include #include #include @@ -49,60 +57,142 @@ namespace multisig //---------------------------------------------------------------------------------------------------------------------- // multisig_account: EXTERNAL //---------------------------------------------------------------------------------------------------------------------- - multisig_account::multisig_account(const crypto::secret_key &base_privkey, + multisig_account::multisig_account(const cryptonote::account_generator_era era, + const crypto::secret_key &base_privkey, const crypto::secret_key &base_common_privkey) : + m_account_era{era}, m_base_privkey{base_privkey}, m_base_common_privkey{base_common_privkey}, m_multisig_pubkey{rct::rct2pk(rct::identity())}, m_common_pubkey{rct::rct2pk(rct::identity())}, - m_kex_rounds_complete{0}, - m_next_round_kex_message{multisig_kex_msg{1, base_privkey, std::vector{}, base_common_privkey}.get_msg()} + m_kex_rounds_complete{0} { + // initialize base pubkey CHECK_AND_ASSERT_THROW_MES(crypto::secret_key_to_public_key(m_base_privkey, m_base_pubkey), "Failed to derive public key"); + + // prepare initial kex message + rct::key initial_pubkey{ + rct::scalarmultKey(rct::pk2rct(get_primary_generator(m_account_era)), rct::sk2rct(m_base_privkey)) + }; + m_next_round_kex_message = + multisig_kex_msg{ + get_kex_msg_version(era), + 1, + base_privkey, + {rct::rct2pk(initial_pubkey)}, + base_common_privkey + }.get_msg(); } //---------------------------------------------------------------------------------------------------------------------- // multisig_account: EXTERNAL //---------------------------------------------------------------------------------------------------------------------- - multisig_account::multisig_account(const std::uint32_t threshold, + multisig_account::multisig_account(const cryptonote::account_generator_era era, + const std::uint32_t threshold, std::vector signers, const crypto::secret_key &base_privkey, const crypto::secret_key &base_common_privkey, std::vector multisig_privkeys, const crypto::secret_key &common_privkey, const crypto::public_key &multisig_pubkey, - const crypto::public_key &common_pubkey, + multisig_keyshare_origins_map_t keyshare_origins_map, const std::uint32_t kex_rounds_complete, multisig_keyset_map_memsafe_t kex_origins_map, std::string next_round_kex_message) : + m_account_era{era}, m_base_privkey{base_privkey}, m_base_common_privkey{base_common_privkey}, m_multisig_privkeys{std::move(multisig_privkeys)}, m_common_privkey{common_privkey}, m_multisig_pubkey{multisig_pubkey}, - m_common_pubkey{common_pubkey}, + m_keyshare_to_origins_map{std::move(keyshare_origins_map)}, m_kex_rounds_complete{kex_rounds_complete}, m_kex_keys_to_origins_map{std::move(kex_origins_map)}, m_next_round_kex_message{std::move(next_round_kex_message)} { - CHECK_AND_ASSERT_THROW_MES(kex_rounds_complete > 0, "multisig account: can't reconstruct account if its kex wasn't initialized"); + CHECK_AND_ASSERT_THROW_MES(account_is_active(), "multisig account: cannot reconstruct an uninitialized account."); + + // 1) initialize base pubkey and common pubkey CHECK_AND_ASSERT_THROW_MES(crypto::secret_key_to_public_key(m_base_privkey, m_base_pubkey), "Failed to derive public key"); + + m_common_pubkey = rct::rct2pk(rct::scalarmultKey( + rct::pk2rct(cryptonote::get_secondary_generator(m_account_era)), + rct::sk2rct(m_common_privkey) + )); + + // 2) initialize keyshare pubkeys and keyshare map + m_multisig_keyshare_pubkeys.reserve(m_multisig_privkeys.size()); + const rct::key primary_generator{rct::pk2rct(get_primary_generator(m_account_era))}; + for (const crypto::secret_key &multisig_privkey : m_multisig_privkeys) + { + m_multisig_keyshare_pubkeys.emplace_back( + rct::rct2pk(rct::scalarmultKey(primary_generator, rct::sk2rct(multisig_privkey))) + ); + m_keyshare_to_origins_map[m_multisig_keyshare_pubkeys.back()]; //this will add any missing keyshares + } + + // 3) set config set_multisig_config(threshold, std::move(signers)); - // kex rounds should not exceed post-kex verification round + // - kex rounds should not exceed post-kex verification round const std::uint32_t kex_rounds_required{multisig_kex_rounds_required(m_signers.size(), m_threshold)}; CHECK_AND_ASSERT_THROW_MES(m_kex_rounds_complete <= kex_rounds_required + 1, "multisig account: tried to reconstruct account, but kex rounds complete counter is invalid."); - // once an account is done with kex, the 'next kex msg' is always the post-kex verification message + // 4) add all other signers available for aggregation-style signing + signer_set_filter temp_filter; + for (const auto &keyshare_and_origins : m_keyshare_to_origins_map) + { + multisig_signers_to_filter(keyshare_and_origins.second, m_signers, temp_filter); + m_available_signers_for_aggregation |= temp_filter; + } + + // 5) once an account is done with kex, the 'next kex msg' is always the post-kex verification message // i.e. the multisig account pubkey signed by the signer's privkey AND the common pubkey if (main_kex_rounds_done()) { - m_next_round_kex_message = multisig_kex_msg{kex_rounds_required + 1, + m_next_round_kex_message = multisig_kex_msg{get_kex_msg_version(m_account_era), + kex_rounds_required + 1, m_base_privkey, std::vector{m_multisig_pubkey, m_common_pubkey}}.get_msg(); } + + // 6) final checks + CHECK_AND_ASSERT_THROW_MES(m_kex_rounds_complete > 0, + "multisig account: can't reconstruct account if its kex wasn't initialized"); + + if (multisig_is_ready()) + { + CHECK_AND_ASSERT_THROW_MES(!(m_multisig_pubkey == crypto::null_pkey), + "multisig account: tried to reconstruct a finalized account, but the multisig pubkey is null"); + CHECK_AND_ASSERT_THROW_MES(!(m_multisig_pubkey == rct::rct2pk(rct::identity())), + "multisig account: tried to reconstruct a finalized account, but the multisig pubkey is identity"); + CHECK_AND_ASSERT_THROW_MES(rct::isInMainSubgroup(rct::pk2rct(m_multisig_pubkey)), + "multisig account: tried to reconstruct account, but multisig pubkey is not in the main subgroup."); + } + } + //---------------------------------------------------------------------------------------------------------------------- + // multisig_account: EXTERNAL + //---------------------------------------------------------------------------------------------------------------------- + std::vector multisig_account::get_signers_available_for_aggregation_signing() const + { + // get all signers who the local signer can perform aggregation-style signing with + std::vector available_signers; + get_filtered_multisig_signers(m_available_signers_for_aggregation, + get_num_flags_set(m_available_signers_for_aggregation), + m_signers, + available_signers); + + return available_signers; + } + //---------------------------------------------------------------------------------------------------------------------- + // multisig_account: EXTERNAL + //---------------------------------------------------------------------------------------------------------------------- + multisig_account_era_conversion_msg multisig_account::get_account_era_conversion_msg( + const cryptonote::account_generator_era new_era) const + { + return multisig_account_era_conversion_msg{m_base_privkey, m_account_era, new_era, m_multisig_privkeys}; } //---------------------------------------------------------------------------------------------------------------------- // multisig_account: EXTERNAL @@ -137,14 +227,22 @@ namespace multisig void multisig_account::set_multisig_config(const std::size_t threshold, std::vector signers) { // validate - CHECK_AND_ASSERT_THROW_MES(threshold > 0 && threshold <= signers.size(), "multisig account: tried to set invalid threshold."); + CHECK_AND_ASSERT_THROW_MES(threshold > 0 && threshold <= signers.size(), + "multisig account: tried to set invalid threshold."); CHECK_AND_ASSERT_THROW_MES(signers.size() >= 2 && signers.size() <= config::MULTISIG_MAX_SIGNERS, - "multisig account: tried to set invalid number of signers."); + "multisig account: tried to set invalid number of signers (" << signers.size() << ")."); + + // sort signers + std::sort(signers.begin(), signers.end()); + + // signers should all be unique + CHECK_AND_ASSERT_THROW_MES(std::adjacent_find(signers.begin(), signers.end()) == signers.end(), + "multisig account: tried to set signers, but found a duplicate signer unexpectedly."); - for (auto signer_it = signers.begin(); signer_it != signers.end(); ++signer_it) + for (const crypto::public_key &signer : signers) { // signer pubkeys must be in main subgroup, and not identity - CHECK_AND_ASSERT_THROW_MES(rct::isInMainSubgroup(rct::pk2rct(*signer_it)) && !(*signer_it == rct::rct2pk(rct::identity())), + CHECK_AND_ASSERT_THROW_MES(rct::isInMainSubgroup(rct::pk2rct(signer)) && !(signer == rct::rct2pk(rct::identity())), "multisig account: tried to set signers, but a signer pubkey is invalid."); } @@ -152,16 +250,24 @@ namespace multisig CHECK_AND_ASSERT_THROW_MES(std::find(signers.begin(), signers.end(), m_base_pubkey) != signers.end(), "multisig account: tried to set signers, but did not find the account's base pubkey in signer list."); - // sort signers - std::sort(signers.begin(), signers.end()); - - // signers should all be unique - CHECK_AND_ASSERT_THROW_MES(std::adjacent_find(signers.begin(), signers.end()) == signers.end(), - "multisig account: tried to set signers, but there are duplicate signers unexpectedly."); - // set m_threshold = threshold; m_signers = std::move(signers); + + // set signers available by default for aggregation-style signing + if (m_threshold == m_signers.size()) + { + // N-of-N: all signers + m_available_signers_for_aggregation = + static_cast(-1) >> (8*sizeof(signer_set_filter) - m_signers.size()); + } + else + { + // M-of-N: local signer + signer_set_filter temp_filter; + multisig_signer_to_filter(m_base_pubkey, m_signers, temp_filter); + m_available_signers_for_aggregation |= temp_filter; + } } //---------------------------------------------------------------------------------------------------------------------- // multisig_account: EXTERNAL @@ -171,6 +277,8 @@ namespace multisig const std::vector &expanded_msgs_rnd1) { CHECK_AND_ASSERT_THROW_MES(!account_is_active(), "multisig account: tried to initialize kex, but already initialized"); + CHECK_AND_ASSERT_THROW_MES(check_kex_msg_versions(expanded_msgs_rnd1, get_kex_msg_version(m_account_era)), + "multisig account: tried to initialize kex with messages that have incompatible versions"); // only mutate account if update succeeds multisig_account temp_account{*this}; @@ -186,12 +294,104 @@ namespace multisig { CHECK_AND_ASSERT_THROW_MES(account_is_active(), "multisig account: tried to update kex, but kex isn't initialized yet."); CHECK_AND_ASSERT_THROW_MES(!multisig_is_ready(), "multisig account: tried to update kex, but kex is already complete."); + CHECK_AND_ASSERT_THROW_MES(check_kex_msg_versions(expanded_msgs, get_kex_msg_version(m_account_era)), + "multisig account: tried to update kex with messages that have incompatible versions"); multisig_account temp_account{*this}; temp_account.kex_update_impl(expanded_msgs, force_update_use_with_caution); *this = std::move(temp_account); } //---------------------------------------------------------------------------------------------------------------------- + // multisig_account: EXTERNAL + //---------------------------------------------------------------------------------------------------------------------- + void multisig_account::add_signer_recommendations(const multisig_account_era_conversion_msg &conversion_msg) + { + CHECK_AND_ASSERT_THROW_MES(multisig_is_ready(), + "multisig account: tried to add signer recommendations, but account isn't ready."); + CHECK_AND_ASSERT_THROW_MES(std::find(m_signers.begin(), m_signers.end(), conversion_msg.get_signing_pubkey()) != + m_signers.end(), "multisig account: tried to add signer recommendations, but signer is unknown."); + CHECK_AND_ASSERT_THROW_MES(!(conversion_msg.get_signing_pubkey() == m_base_pubkey), + "multisig account: tried to add signer recommendations, conversion msg is from self."); + CHECK_AND_ASSERT_THROW_MES(conversion_msg.get_old_era() == m_account_era || conversion_msg.get_new_era() == + m_account_era, "multisig account: tried to add signer recommendations, but input msg doesn't match the account era."); + + // add signer to 'available signers' + signer_set_filter new_signer_flag; + multisig_signer_to_filter(conversion_msg.get_signing_pubkey(), m_signers, new_signer_flag); + m_available_signers_for_aggregation |= new_signer_flag; + + // abuse conversion msg api to get keyshares we care about + // note: prior assert ensures we will get keyshares in our current account era + const std::vector &recommended_keys{ + m_account_era == conversion_msg.get_old_era() + ? conversion_msg.get_old_keyshares() + : conversion_msg.get_new_keyshares() //m_era == conversion_msg.get_new_era() + }; + + // for each local keyshare that the other signer also recommends, add that signer as an 'origin' + for (const crypto::public_key &keyshare : recommended_keys) + { + // skip keyshares that the local account doesn't have + if (m_keyshare_to_origins_map.find(keyshare) == m_keyshare_to_origins_map.end()) + continue; + + m_keyshare_to_origins_map[keyshare].insert(conversion_msg.get_signing_pubkey()); + } + } + //---------------------------------------------------------------------------------------------------------------------- + // multisig_account: EXTERNAL + //---------------------------------------------------------------------------------------------------------------------- + bool multisig_account::try_get_aggregate_signing_key(const signer_set_filter filter, crypto::secret_key &aggregate_key_out) const + { + CHECK_AND_ASSERT_THROW_MES(multisig_is_ready(), "multisig account: tried to get signing key, but account isn't ready."); + CHECK_AND_ASSERT_THROW_MES(m_multisig_privkeys.size() == m_multisig_keyshare_pubkeys.size(), + "multisig account: tried to get signing key, but there is a mismatch between multisig privkeys and pubkeys."); + + // check that local signer is able to make an aggregate key with all signers in input filter + if ((filter & m_available_signers_for_aggregation) != filter) + return false; + + // check if local signer is in input filter + if (!signer_is_in_filter(m_base_pubkey, m_signers, filter)) + return false; + + // filter the signer list to get group of signers + std::vector filtered_signers; + get_filtered_multisig_signers(filter, m_threshold, m_signers, filtered_signers); + CHECK_AND_ASSERT_THROW_MES(std::is_sorted(filtered_signers.begin(), filtered_signers.end()), + "multisig account: filtered signers are unsorted (bug)."); + + // find local signer's location in filtered set + auto self_location = std::find(filtered_signers.begin(), filtered_signers.end(), m_base_pubkey); + CHECK_AND_ASSERT_THROW_MES(self_location != filtered_signers.end(), + "multisig_account: local signer unexpectedly not in filtered signers despite filter match (bug)."); + + // accumulate keyshares that other signers whose ids are lower in the filtered list won't be contributing + aggregate_key_out = rct::rct2sk(rct::zero()); + + for (std::size_t key_index{0}; key_index < m_multisig_privkeys.size(); ++key_index) + { + const auto &origins = + m_keyshare_to_origins_map.find(m_multisig_keyshare_pubkeys[key_index]) != m_keyshare_to_origins_map.end() + ? m_keyshare_to_origins_map.at(m_multisig_keyshare_pubkeys[key_index]) + : std::unordered_set{}; + + if (std::find_if(origins.begin(), origins.end(), + [&](const crypto::public_key &origin) -> bool + { + return std::find(filtered_signers.begin(), self_location, origin) != self_location; + } + ) == origins.end()) + { + sc_add((unsigned char*)(&aggregate_key_out), + (const unsigned char*)(&aggregate_key_out), + (const unsigned char*)(&m_multisig_privkeys[key_index])); + } + } + + return true; + } + //---------------------------------------------------------------------------------------------------------------------- // EXTERNAL //---------------------------------------------------------------------------------------------------------------------- std::uint32_t multisig_kex_rounds_required(const std::uint32_t num_signers, const std::uint32_t threshold) @@ -208,4 +408,116 @@ namespace multisig return multisig_kex_rounds_required(num_signers, threshold) + 1; } //---------------------------------------------------------------------------------------------------------------------- + // EXTERNAL + //---------------------------------------------------------------------------------------------------------------------- + void get_multisig_account_with_new_generator_era(const multisig_account &original_account, + const cryptonote::account_generator_era new_era, + const std::vector &conversion_msgs, + multisig_account &new_account_out) + { + // validate original account + CHECK_AND_ASSERT_THROW_MES(original_account.multisig_is_ready(), "Failed to make a multisig account with new " + "generator era. Account has not completed the setup ceremony (key exchange)."); + CHECK_AND_ASSERT_THROW_MES(new_era != original_account.get_era(), "Failed to make a multisig account with new " + "generator era. Account is already era (" << static_cast(new_era) << ")."); + + // add local keyshares to old and new keyshare sets (abuse conversion msg API for convenience) + // - and save them to a new keyshare map (and preserve existing recommendations) + std::unordered_set old_keyshares; + std::unordered_set new_keyshares; + multisig_keyshare_origins_map_t keyshare_origins_map; + const multisig_account_era_conversion_msg local_conversion_msg{original_account.get_account_era_conversion_msg(new_era)}; + + const std::vector &local_old_keyshares{local_conversion_msg.get_old_keyshares()}; + for (const crypto::public_key &local_old_keyshare : local_old_keyshares) + old_keyshares.insert(local_old_keyshare); + + const std::vector &local_new_keyshares{local_conversion_msg.get_new_keyshares()}; + const multisig_keyshare_origins_map_t &original_keyshare_origins_map{original_account.get_keyshares_to_origins_map()}; + for (std::size_t keyshare_index{0}; keyshare_index < local_new_keyshares.size(); ++keyshare_index) + { + new_keyshares.insert(local_new_keyshares[keyshare_index]); + + // copy over old recommendations + // NOTE: assumes the conversion message preserves ordering between old/new keyshares + keyshare_origins_map[local_new_keyshares[keyshare_index]].insert( + original_keyshare_origins_map.at(local_old_keyshares[keyshare_index]).begin(), + original_keyshare_origins_map.at(local_old_keyshares[keyshare_index]).end() + ); + } + + // validate input messages and collect their keyshares + const std::vector &signers{original_account.get_signers()}; + std::unordered_set msg_signers; + + for (const multisig_account_era_conversion_msg &msg : conversion_msgs) + { + // skip the local signer so it doesn't get added as an origin to the keyshare_origins_map + if (msg.get_signing_pubkey() == original_account.get_base_pubkey()) + continue; + + CHECK_AND_ASSERT_THROW_MES(msg.get_old_era() == original_account.get_era(), "Failed to make a multisig account with " + "new generator era. Conversion message's old era (" << static_cast(msg.get_old_era()) << ") doesn't match " + "account to convert (" << static_cast(original_account.get_era()) << ")."); + CHECK_AND_ASSERT_THROW_MES(msg.get_new_era() == new_era, "Failed to make a multisig account with " + "new generator era. Conversion message's new era (" << static_cast(msg.get_new_era()) << ") doesn't match " + "expected new era (" <(new_era) << ")."); + CHECK_AND_ASSERT_THROW_MES(std::find(signers.begin(), signers.end(), msg.get_signing_pubkey()) != signers.end(), + "Failed to make a multisig account with new generator era. Conversion message from unknown signer."); + msg_signers.insert(msg.get_signing_pubkey()); + + // collect old keyshares to verify that the old multisig pubkey can be reproduced + const std::vector &msg_old_keyshares = msg.get_old_keyshares(); + for (const crypto::public_key &msg_old_keyshare : msg_old_keyshares) + old_keyshares.insert(msg_old_keyshare); + + // collect new keyshares to construct the new multisig pubkey + // - and save the msg signing key as an origin if the keyshare will be shared with the new account + const std::vector &msg_new_keyshares = msg.get_new_keyshares(); + for (const crypto::public_key &msg_new_keyshare : msg_new_keyshares) + { + new_keyshares.insert(msg_new_keyshare); + + if (keyshare_origins_map.find(msg_new_keyshare) != keyshare_origins_map.end()) + keyshare_origins_map[msg_new_keyshare].insert(msg.get_signing_pubkey()); + } + } + + // there should be at least threshold signers involved in converting an account + msg_signers.insert(original_account.get_base_pubkey()); + CHECK_AND_ASSERT_THROW_MES(msg_signers.size() >= original_account.get_threshold(), "Failed to make a multisig account with " + "new generator era. Need conversion messages from more members of the multisig group (have: " << msg_signers.size() << + ", need: " << original_account.get_threshold() <<")."); + + // reproduce old multisig pubkey + rct::key old_pubkey_recomputed{rct::identity()}; + for (const crypto::public_key &old_keyshare : old_keyshares) + rct::addKeys(old_pubkey_recomputed, old_pubkey_recomputed, rct::pk2rct(old_keyshare)); + + CHECK_AND_ASSERT_THROW_MES(rct::rct2pk(old_pubkey_recomputed) == original_account.get_multisig_pubkey(), + "Failed to make a multisig account with new generator era. Could not reproduce the account's original pubkey from " + "conversion msgs."); + + // construct new multisig pubkey (new keyshares are 1:1 with old keyshares according to conversion msg invariants, + // so if the old pubkey was reproduced then the new pubkey will have the expected cross-generator DL equivalence) + rct::key new_multisig_pubkey{rct::identity()}; + for (const crypto::public_key &new_keyshare : new_keyshares) + rct::addKeys(new_multisig_pubkey, new_multisig_pubkey, rct::pk2rct(new_keyshare)); + + // return new account with new era but same privkeys as old account + // note: this is safe even if new_account_out and original_account are the same object + new_account_out = multisig_account{new_era, + original_account.get_threshold(), + original_account.get_signers(), + original_account.get_base_privkey(), + original_account.get_base_common_privkey(), + original_account.get_multisig_privkeys(), + original_account.get_common_privkey(), + rct::rct2pk(new_multisig_pubkey), + std::move(keyshare_origins_map), + original_account.get_kex_rounds_complete(), + multisig_keyset_map_memsafe_t{}, //note: no kex-origins map, only accounts that completed kex can be converted + ""}; + } + //---------------------------------------------------------------------------------------------------------------------- } //namespace multisig diff --git a/src/multisig/multisig_account.h b/src/multisig/multisig_account.h index 0d832f2434..af645ebe3b 100644 --- a/src/multisig/multisig_account.h +++ b/src/multisig/multisig_account.h @@ -29,7 +29,11 @@ #pragma once #include "crypto/crypto.h" +#include "cryptonote_basic/account_generators.h" +#include "multisig_account_era_conversion_msg.h" #include "multisig_kex_msg.h" +#include "multisig_partial_cn_key_image_msg.h" +#include "multisig_signer_set_filter.h" #include #include @@ -77,6 +81,7 @@ namespace multisig */ using multisig_keyset_map_memsafe_t = std::unordered_map>; + using multisig_keyshare_origins_map_t = std::unordered_map>; class multisig_account final { @@ -90,20 +95,23 @@ namespace multisig * * - prepares a kex msg for the first round of multisig key construction. * - the local account's kex msgs are signed with the base_privkey - * - the first kex msg transmits the local base_common_privkey to other participants, for creating the group's common_privkey + * - the first kex msg transmits the local base_common_privkey to other participants, for creating the group's + * common_privkey */ - multisig_account(const crypto::secret_key &base_privkey, + multisig_account(const cryptonote::account_generator_era era, + const crypto::secret_key &base_privkey, const crypto::secret_key &base_common_privkey); // reconstruct from full account details (not recommended) - multisig_account(const std::uint32_t threshold, + multisig_account(const cryptonote::account_generator_era era, + const std::uint32_t threshold, std::vector signers, const crypto::secret_key &base_privkey, const crypto::secret_key &base_common_privkey, std::vector multisig_privkeys, const crypto::secret_key &common_privkey, const crypto::public_key &multisig_pubkey, - const crypto::public_key &common_pubkey, + multisig_keyshare_origins_map_t keyshare_origins_map, const std::uint32_t kex_rounds_complete, multisig_keyset_map_memsafe_t kex_origins_map, std::string next_round_kex_message); @@ -116,10 +124,14 @@ namespace multisig //overloaded operators: none //getters + // get account era + cryptonote::account_generator_era get_era() const { return m_account_era; } // get threshold std::uint32_t get_threshold() const { return m_threshold; } // get signers const std::vector& get_signers() const { return m_signers; } + // get signers who are available for aggregation-style signing + std::vector get_signers_available_for_aggregation_signing() const; // get base privkey const crypto::secret_key& get_base_privkey() const { return m_base_privkey; } // get base pubkey @@ -134,12 +146,16 @@ namespace multisig const crypto::public_key& get_multisig_pubkey() const { return m_multisig_pubkey; } // get common pubkey const crypto::public_key& get_common_pubkey() const { return m_common_pubkey; } + // get keyshare to origins map + const multisig_keyshare_origins_map_t& get_keyshares_to_origins_map() const { return m_keyshare_to_origins_map; } // get kex rounds complete std::uint32_t get_kex_rounds_complete() const { return m_kex_rounds_complete; } // get kex keys to origins map const multisig_keyset_map_memsafe_t& get_kex_keys_to_origins_map() const { return m_kex_keys_to_origins_map; } // get the kex msg for the next round const std::string& get_next_kex_round_msg() const { return m_next_round_kex_message; } + // get account era conversion message for converting this account to 'new_era' + multisig_account_era_conversion_msg get_account_era_conversion_msg(const cryptonote::account_generator_era new_era) const; //account status functions // account has been intialized, and the account holder can use the 'common' key @@ -151,7 +167,7 @@ namespace multisig //account helpers private: - // set the threshold (M) and signers (N) + // set the threshold (M) and signers (N), and initialize the 'available signers for aggregation signing' filter void set_multisig_config(const std::size_t threshold, std::vector signers); //account mutators: key exchange to set up account @@ -177,35 +193,100 @@ namespace multisig * - If force updating with maliciously-crafted messages, the resulting account will be invalid (either unable * to complete signatures, or a 'hostage' to the malicious signer [i.e. can't sign without his participation]). */ - void kex_update(const std::vector &expanded_msgs, - const bool force_update_use_with_caution = false); + void kex_update(const std::vector &expanded_msgs, const bool force_update_use_with_caution = false); + /** + * brief: get_multisig_kex_round_booster - Create a multisig kex msg for the kex round that follows the kex round this + * account is currently working on, in order to 'boost' another participant's kex setup. + * - A booster message is for the round after the in-progress round because get_next_kex_round_msg() provides access + * to the in-progress round's message. + * - Useful for 'jumpstarting' the following kex round when you don't have messages from all other signers to complete + * the current round. + * - Sanitizes input messages and produces a new kex msg for round 'num_completed_rounds + 2'. + * + * - For example, in 2-of-3 escrowed purchasing, the [vendor, arbitrator] pair can boost the second round + * of key exchange by calling this function with the 'round 1' messages of each other. + * Then the [buyer] can use the resulting boost messages, in combination with [vender, arbitrator] round 1 messages, + * to complete the address in one step. In other words, call initialize_kex() on the round 1 messages, + * then call kex_update() on the round 2 booster messages to finish the multisig key. + * + * - Note: The 'threshold' and 'num_signers' are inputs here in case kex has not been initialized yet. + * param: threshold - threshold for multisig (M in M-of-N) + * param: num_signers - number of participants in multisig (N) + * param: expanded_msgs - set of multisig kex messages to process + * return: multisig kex message for next round + */ + multisig_kex_msg get_multisig_kex_round_booster(const std::uint32_t threshold, + const std::uint32_t num_signers, + const std::vector &expanded_msgs) const; + /** + * brief: add_signer_recommendations - Update keyshare-to-origins map with a specific signer's recommendations. + * - Used to recover the keyshare-to-origins map if it is lost. + * - Note: It is not a security problem if the recommended keys vector is unvalidated. A malicious signer COULD + * provide an invalid keyshare recommendation list, which would likely prevent the local signer from + * successfully completing signatures with that signer, BUT malicious signers have + * other ways to prevent the local account from co-signing a message with them. + * It is worth noting that: + * 1) The malicious signer recommending invalid keyshares CANNOT prevent the local account from co-signing + * messages with M-1 honest other signers. + * 2) Not validating keyshare lists may make it difficult to properly track down which signer caused a given + * signature attempt to fail. However, effective validation would require messages from all signers + * in order to do something like evaluate_multisig_kex_round_msgs(). Unfortunately, requiring > M signers + * to recover aggregation-style signing would violate the invariant that a multisig account should only + * require M honest signers to work once account setup is complete. + * param: conversion_msg - a conversion message from a non-local signer ('origin') with recommended keyshares + * (we abuse the conversion msg api instead of implementing an entirely new msg format and plumbing for this + * method that primarly exists to help legacy accounts) + */ + void add_signer_recommendations(const multisig_account_era_conversion_msg &conversion_msg); private: // implementation of kex_update() (non-transactional) void kex_update_impl(const std::vector &expanded_msgs, const bool incomplete_signer_set); /** - * brief: initialize_kex_update - Helper for kex_update_impl() + * brief: get_kex_exclude_pubkeys - collect the local signer's shared keys to ignore in incoming messages + * param: generators - generators this account uses + * return: keys held by the local account corresponding to the 'in-progress round' + * - If 'in-progress round' is the final round, these are the local account's shares of the final aggregate key. + */ + std::vector get_kex_exclude_pubkeys(const cryptonote::account_generators &generators) const; + /** + * brief: initialize_kex_update - initialize the multisig account for the first kex round * - Collect the local signer's shared keys to ignore in incoming messages, build the aggregate ancillary key * if appropriate. + * param: generators - generators this account uses * param: expanded_msgs - set of multisig kex messages to process * param: kex_rounds_required - number of rounds required for kex (not including post-kex verification round) - * outparam: exclude_pubkeys_out - keys held by the local account corresponding to round 'current_round' - * - If 'current_round' is the final round, these are the local account's shares of the final aggregate key. */ - void initialize_kex_update(const std::vector &expanded_msgs, - const std::uint32_t kex_rounds_required, - std::vector &exclude_pubkeys_out); + void initialize_kex_update(const cryptonote::account_generators &generators, + const std::vector &expanded_msgs, + const std::uint32_t kex_rounds_required); /** * brief: finalize_kex_update - Helper for kex_update_impl() - * param: kex_rounds_required - number of rounds required for kex (not including post-kex verification round) + * param: generators - generators this account uses + * param: kex_rounds_required - number of rounds required for kex * param: result_keys_to_origins_map - map between keys for the next round and the other participants they correspond to * inoutparam: temp_account_inout - account to perform last update steps on */ - void finalize_kex_update(const std::uint32_t kex_rounds_required, + void finalize_kex_update(const cryptonote::account_generators &generators, + const std::uint32_t kex_rounds_required, multisig_keyset_map_memsafe_t result_keys_to_origins_map); + //account use functions + public: + /** + * brief: try_get_aggregate_signing_key - Get an aggregate privkey corresponding to a filtered list of signers. + * - For each privkey share that the local signer has, it only contributes that privkey if it's signer id + * is ordered lowest in the filtered list. + * param: filter - filter for selecting signers out of the signer list for creating a signature + * outparam: aggregate_key_out - local signer's privkey contribution to a multisig signing event + */ + bool try_get_aggregate_signing_key(const signer_set_filter filter, crypto::secret_key &aggregate_key_out) const; + //member variables private: + /// which era this account is calibrated for + cryptonote::account_generator_era m_account_era; + /// misc. account details // [M] minimum number of co-signers to sign a message with the aggregate pubkey std::uint32_t m_threshold{0}; @@ -214,16 +295,17 @@ namespace multisig /// local participant's personal keys // base keypair of the participant - // - used for signing messages, as the initial base key for key exchange, and to make DH derivations for key exchange + // - used for signing messages, to make the initial base key for key exchange, and to make DH derivations for key exchange crypto::secret_key m_base_privkey; + // - used for signing messages (base_privkey * G) crypto::public_key m_base_pubkey; // common base privkey, used to produce the aggregate common privkey crypto::secret_key m_base_common_privkey; /// core multisig account keys // the account's private key shares of the multisig address - // TODO: also record which other signers have these privkeys, to enable aggregation signing (instead of round-robin) std::vector m_multisig_privkeys; + std::vector m_multisig_keyshare_pubkeys; // a privkey owned by all multisig participants (e.g. a cryptonote view key) crypto::secret_key m_common_privkey; // the multisig public key (e.g. a cryptonote spend key) @@ -231,6 +313,11 @@ namespace multisig // the common public key (e.g. a view spend key) crypto::public_key m_common_pubkey; + /// records which other signers have each of the local signer's multisig privkeys + multisig_keyshare_origins_map_t m_keyshare_to_origins_map; + /// helper filter that records which other signers are present in m_keyshare_to_origins_map + signer_set_filter m_available_signers_for_aggregation{0}; + /// kex variables // number of key exchange rounds that have been completed (all messages for the round collected and processed) std::uint32_t m_kex_rounds_complete{0}; @@ -262,4 +349,22 @@ namespace multisig * return: number of setup rounds required */ std::uint32_t multisig_setup_rounds_required(const std::uint32_t num_signers, const std::uint32_t threshold); + + /** + * brief: get_multisig_account_with_new_generator_era - get a multisig account built around an account generator era + * different from an existing account (i.e. migrate the old account to a different account generator era)) + * - Requires at least M - 1 other signers to contribute conversion messages. + * - Conversion messages are needed to compute the new account's multisig group key (and prove that the new key + * has the correct discrete-log equivalence with the old multisig group key), and to provide signer keyshare + * recommendations to the new account so the new account can perform aggregation-style signing (more signers can + * be added to the account later on with the .add_signer_recommendations() method). + * param: original_account - original account to migrate + * param: new_era - era of the new account + * param: conversion_msgs - account conversion messages from other signers (msgs from local signer are ignored) + * outparam: new_account_out - migrated account using the new era + */ + void get_multisig_account_with_new_generator_era(const multisig_account &original_account, + const cryptonote::account_generator_era new_era, + const std::vector &conversion_msgs, + multisig_account &new_account_out); } //namespace multisig diff --git a/src/multisig/multisig_account_era_conversion_msg.cpp b/src/multisig/multisig_account_era_conversion_msg.cpp new file mode 100644 index 0000000000..af604c8693 --- /dev/null +++ b/src/multisig/multisig_account_era_conversion_msg.cpp @@ -0,0 +1,291 @@ +// Copyright (c) 2021, 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 "multisig_account_era_conversion_msg.h" +#include "multisig_msg_serialization.h" + +#include "common/base58.h" +#include "crypto/crypto.h" +extern "C" +{ +#include "crypto/crypto-ops.h" +} +#include "cryptonote_basic/account_generators.h" +#include "include_base_utils.h" +#include "ringct/rctOps.h" +#include "ringct/rctTypes.h" +#include "seraphis_crypto/matrix_proof.h" +#include "serialization/binary_archive.h" +#include "serialization/serialization.h" + +#include + +#include +#include +#include +#include + + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "multisig" + +const boost::string_ref MULTISIG_CONVERSION_MSG_MAGIC_V1{"MultisigConversionV1"}; + +namespace multisig +{ +//---------------------------------------------------------------------------------------------------------------------- +// INTERNAL +//---------------------------------------------------------------------------------------------------------------------- +static std::vector pubkeys_mul8(std::vector keys) +{ + for (crypto::public_key &key : keys) + key = rct::rct2pk(rct::scalarmult8(rct::pk2rct(key))); + + return keys; +} +//---------------------------------------------------------------------------------------------------------------------- +// INTERNAL +//---------------------------------------------------------------------------------------------------------------------- +static void set_msg_magic(std::string &msg_out) +{ + msg_out.clear(); + msg_out.append(MULTISIG_CONVERSION_MSG_MAGIC_V1.data(), MULTISIG_CONVERSION_MSG_MAGIC_V1.size()); +} +//---------------------------------------------------------------------------------------------------------------------- +// INTERNAL +//---------------------------------------------------------------------------------------------------------------------- +static bool try_get_message_no_magic(const std::string &original_msg, + const boost::string_ref magic, + std::string &msg_no_magic_out) +{ + // abort if magic doesn't match the message + if (original_msg.substr(0, magic.size()) != magic) + return false; + + // decode message + CHECK_AND_ASSERT_THROW_MES(tools::base58::decode(original_msg.substr(magic.size()), msg_no_magic_out), + "Multisig conversion msg decoding error."); + + return true; +} +//---------------------------------------------------------------------------------------------------------------------- +// INTERNAL +//---------------------------------------------------------------------------------------------------------------------- +static void get_matrix_proof_msg(const boost::string_ref magic, + const crypto::public_key &signing_pubkey, + const cryptonote::account_generator_era old_era, + const cryptonote::account_generator_era new_era, + rct::key &proof_msg_out) +{ + // proof_msg = versioning-domain-sep || signing_pubkey || old_era || new_era + std::string data; + data.reserve(magic.size() + sizeof(crypto::public_key) + 2); + + // magic + data.append(magic.data(), magic.size()); + + // signing pubkey + data.append(reinterpret_cast(&signing_pubkey), sizeof(crypto::public_key)); + + // new era and old era + data += static_cast(old_era); + data += static_cast(new_era); + + rct::cn_fast_hash(proof_msg_out, data.data(), data.size()); +} +//---------------------------------------------------------------------------------------------------------------------- +// INTERNAL +//---------------------------------------------------------------------------------------------------------------------- +static crypto::hash get_signature_msg(const sp::MatrixProof &matrix_proof) +{ + // signature_msg = matrix_proof_challenge || matrix_proof_response + std::string data; + data.reserve(2*sizeof(crypto::public_key)); + data.append(reinterpret_cast(&matrix_proof.c), sizeof(crypto::public_key)); + data.append(reinterpret_cast(&matrix_proof.r), sizeof(crypto::public_key)); + + return crypto::cn_fast_hash(data.data(), data.size()); +} +//---------------------------------------------------------------------------------------------------------------------- +// multisig_account_era_conversion_msg: EXTERNAL +//---------------------------------------------------------------------------------------------------------------------- +multisig_account_era_conversion_msg::multisig_account_era_conversion_msg(const crypto::secret_key &signing_privkey, + const cryptonote::account_generator_era old_account_era, + const cryptonote::account_generator_era new_account_era, + const std::vector &keyshare_privkeys) : + m_old_era{old_account_era}, + m_new_era{new_account_era} +{ + CHECK_AND_ASSERT_THROW_MES(sc_check(to_bytes(signing_privkey)) == 0 && + signing_privkey != crypto::null_skey, "Invalid msg signing key."); + const rct::key G_1{rct::pk2rct(cryptonote::get_primary_generator(m_old_era))}; + const rct::key G_2{rct::pk2rct(cryptonote::get_primary_generator(m_new_era))}; + CHECK_AND_ASSERT_THROW_MES(!(G_1 == rct::Z), "Unknown conversion msg old era."); + CHECK_AND_ASSERT_THROW_MES(!(G_2 == rct::Z), "Unknown conversion msg new era."); + CHECK_AND_ASSERT_THROW_MES(keyshare_privkeys.size() > 0, "Can't make conversion message with no keys to convert."); + + // save signing pubkey + CHECK_AND_ASSERT_THROW_MES(crypto::secret_key_to_public_key(signing_privkey, m_signing_pubkey), + "Failed to derive public key"); + + // make matrix proof + rct::key proof_msg; + get_matrix_proof_msg(MULTISIG_CONVERSION_MSG_MAGIC_V1, m_signing_pubkey, m_old_era, m_new_era, proof_msg); + sp::MatrixProof proof; + sp::make_matrix_proof(proof_msg, {rct::rct2pk(G_1), rct::rct2pk(G_2)}, keyshare_privkeys, proof); + + // sets message and signing pub key + this->construct_msg(signing_privkey, proof); + + // set keyshares + CHECK_AND_ASSERT_THROW_MES(proof.M.size() == 2, "multisig conversion msg: invalid matrix proof keys size."); + + m_old_keyshares = pubkeys_mul8(std::move(proof.M[0])); + m_new_keyshares = pubkeys_mul8(std::move(proof.M[1])); +} +//---------------------------------------------------------------------------------------------------------------------- +// multisig_account_era_conversion_msg: EXTERNAL +//---------------------------------------------------------------------------------------------------------------------- +multisig_account_era_conversion_msg::multisig_account_era_conversion_msg(std::string msg) : m_msg{std::move(msg)} +{ + this->parse_and_validate_msg(); +} +//---------------------------------------------------------------------------------------------------------------------- +// multisig_account_era_conversion_msg: INTERNAL +//---------------------------------------------------------------------------------------------------------------------- +void multisig_account_era_conversion_msg::construct_msg(const crypto::secret_key &signing_privkey, + const sp::MatrixProof &matrix_proof) +{ + //// + // msg_to_sign = matrix_proof_challenge || matrix_proof_response + // + // msg = versioning-domain-sep || + // b58(signing_pubkey || old_era || new_era || {old_keyshares} || {new_keyshares} || matrix_proof_challenge || + // matrix_proof_response || crypto_sig[signing_privkey](matrix_proof_challenge || matrix_proof_response)) + /// + + // sign the message + crypto::signature msg_signature; + crypto::generate_signature(get_signature_msg(matrix_proof), m_signing_pubkey, signing_privkey, msg_signature); + + // mangle the matrix proof into a crypto::signature + const crypto::signature mangled_matrix_proof{rct::rct2sk(matrix_proof.c), rct::rct2sk(matrix_proof.r)}; + + // prepare the message + CHECK_AND_ASSERT_THROW_MES(matrix_proof.M.size() == 2, + "serializing multisig conversion msg: invalid matrix proof keys size."); + + std::stringstream serialized_msg_ss; + binary_archive b_archive(serialized_msg_ss); + + multisig_conversion_msg_serializable msg_serializable; + msg_serializable.old_era = m_old_era; + msg_serializable.new_era = m_new_era; + msg_serializable.old_keyshares = matrix_proof.M[0]; + msg_serializable.new_keyshares = matrix_proof.M[1]; + msg_serializable.signing_pubkey = m_signing_pubkey; + msg_serializable.matrix_proof_partial = mangled_matrix_proof; + msg_serializable.signature = msg_signature; + + CHECK_AND_ASSERT_THROW_MES(::serialization::serialize(b_archive, msg_serializable), + "Failed to serialize multisig conversion msg."); + + // make the message + set_msg_magic(m_msg); + m_msg.append(tools::base58::encode(serialized_msg_ss.str())); +} +//---------------------------------------------------------------------------------------------------------------------- +// multisig_account_era_conversion_msg: INTERNAL +//---------------------------------------------------------------------------------------------------------------------- +void multisig_account_era_conversion_msg::parse_and_validate_msg() +{ + // early return on empty messages + if (m_msg == "") + return; + + // deserialize the message + std::string msg_no_magic; + CHECK_AND_ASSERT_THROW_MES(try_get_message_no_magic(m_msg, MULTISIG_CONVERSION_MSG_MAGIC_V1, msg_no_magic), + "Could not remove magic from conversion message."); + + binary_archive archived_msg{epee::strspan(msg_no_magic)}; + + // extract data from the message + sp::MatrixProof matrix_proof; + crypto::signature msg_signature; + + multisig_conversion_msg_serializable deserialized_msg; + if (::serialization::serialize(archived_msg, deserialized_msg)) + { + m_old_era = deserialized_msg.old_era; + m_new_era = deserialized_msg.new_era; + matrix_proof.M = + { + std::move(deserialized_msg.old_keyshares), + std::move(deserialized_msg.new_keyshares) + }; + m_signing_pubkey = deserialized_msg.signing_pubkey; + memcpy(matrix_proof.c.bytes, to_bytes(deserialized_msg.matrix_proof_partial.c), sizeof(crypto::ec_scalar)); + memcpy(matrix_proof.r.bytes, to_bytes(deserialized_msg.matrix_proof_partial.r), sizeof(crypto::ec_scalar)); + msg_signature = deserialized_msg.signature; + } + else CHECK_AND_ASSERT_THROW_MES(false, "Deserializing conversion msg failed."); + + // checks + const rct::key G_1{rct::pk2rct(cryptonote::get_primary_generator(m_old_era))}; + const rct::key G_2{rct::pk2rct(cryptonote::get_primary_generator(m_new_era))}; + CHECK_AND_ASSERT_THROW_MES(!(G_1 == rct::Z), "Unknown conversion msg old era."); + CHECK_AND_ASSERT_THROW_MES(!(G_2 == rct::Z), "Unknown conversion msg new era."); + CHECK_AND_ASSERT_THROW_MES(matrix_proof.M.size() == 2, "Conversion message is malformed."); + CHECK_AND_ASSERT_THROW_MES(matrix_proof.M[0].size() > 0, "Conversion message has no conversion keys."); + CHECK_AND_ASSERT_THROW_MES(matrix_proof.M[0].size() == matrix_proof.M[1].size(), + "Conversion message key vectors don't line up."); + CHECK_AND_ASSERT_THROW_MES(m_signing_pubkey != crypto::null_pkey && m_signing_pubkey != rct::rct2pk(rct::identity()), + "Message signing key was invalid."); + CHECK_AND_ASSERT_THROW_MES(rct::isInMainSubgroup(rct::pk2rct(m_signing_pubkey)), + "Message signing key was not in prime subgroup."); + + // validate matrix proof + get_matrix_proof_msg(MULTISIG_CONVERSION_MSG_MAGIC_V1, m_signing_pubkey, m_old_era, m_new_era, matrix_proof.m); + CHECK_AND_ASSERT_THROW_MES(sp::verify_matrix_proof(matrix_proof, + { + rct::rct2pk(G_1), rct::rct2pk(G_2) + }), + "Conversion message matrix proof invalid."); + + // validate signature + CHECK_AND_ASSERT_THROW_MES(crypto::check_signature(get_signature_msg(matrix_proof), m_signing_pubkey, msg_signature), + "Multisig conversion msg signature invalid."); + + // save keyshares (note: saving these after checking the signature ensures if the signature is invalid then the + // message's internal state won't be usable even if the invalid-signature exception is caught) + m_old_keyshares = pubkeys_mul8(std::move(matrix_proof.M[0])); + m_new_keyshares = pubkeys_mul8(std::move(matrix_proof.M[1])); +} +//---------------------------------------------------------------------------------------------------------------------- +} //namespace multisig diff --git a/src/multisig/multisig_account_era_conversion_msg.h b/src/multisig/multisig_account_era_conversion_msg.h new file mode 100644 index 0000000000..4c247358f6 --- /dev/null +++ b/src/multisig/multisig_account_era_conversion_msg.h @@ -0,0 +1,120 @@ +// Copyright (c) 2021, 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 "crypto/crypto.h" +#include "cryptonote_basic/account_generators.h" + +#include +#include +#include + +namespace sp { struct MatrixProof; } + + +namespace multisig +{ +//// +// multisig account era conversion msg +// - This message contains a proof that one set of keys correspond 1:1 with another set across two generators, which +// are defined by account_generator_eras. +// e.x. {a G, b G, c G} -> {a U, b U, c U} +// - In an M-of-N multisig, if M players send each other account conversion messages, that set of messages can be used +// to trustlessly convert an old account to one with a new account_generator_era. +// See the multisig::get_multisig_account_with_new_generator_era() method for more information. +// - INVARIANT: keyshares stored here are canonical prime-order subgroup points. +// +// matrix_proof_msg = versioning-domain-sep || signing_pubkey || old_era || new_era +// +// msg = versioning-domain-sep || +// b58(signing_pubkey || old_era || new_era || {old_keyshares} || {new_keyshares} || matrix_proof_challenge || +// matrix_proof_response || crypto_sig[signing_privkey](matrix_proof_challenge || matrix_proof_response)) +/// +class multisig_account_era_conversion_msg final +{ +//constructors +public: + // default constructor + multisig_account_era_conversion_msg() = default; + + // construct from info + multisig_account_era_conversion_msg(const crypto::secret_key &signing_privkey, + const cryptonote::account_generator_era old_account_era, + const cryptonote::account_generator_era new_account_era, + const std::vector &keyshare_privkeys); + + // construct from string + multisig_account_era_conversion_msg(std::string msg); + + // copy constructor: default + +//destructor: default + ~multisig_account_era_conversion_msg() = default; + +//overloaded operators: none + +//member functions + // get msg string + const std::string& get_msg() const { return m_msg; } + // get generator era of old account + cryptonote::account_generator_era get_old_era() const { return m_old_era; } + // get generator era of new account + cryptonote::account_generator_era get_new_era() const { return m_new_era; } + // get the msg signer's old keyshares + const std::vector& get_old_keyshares() const { return m_old_keyshares; } + // get the msg signer's new keyshares + const std::vector& get_new_keyshares() const { return m_new_keyshares; } + // get msg signing pubkey + const crypto::public_key& get_signing_pubkey() const { return m_signing_pubkey; } + +private: + // set: msg string based on msg contents, with signing pubkey defined from input privkey + void construct_msg(const crypto::secret_key &signing_privkey, const sp::MatrixProof &matrix_proof); + // parse msg string into parts, validate contents and signature + void parse_and_validate_msg(); + +//member variables +private: + // message as string + std::string m_msg; + + // generator era of old account + cryptonote::account_generator_era m_old_era; + // generator era of new account (being converted to) + cryptonote::account_generator_era m_new_era; + // the msg signer's old keyshares + std::vector m_old_keyshares; + // the msg signer's new keyshares (1:1 with old keyshares) + std::vector m_new_keyshares; + + // pubkey used to sign this msg + crypto::public_key m_signing_pubkey; +}; + +} //namespace multisig diff --git a/src/multisig/multisig_account_kex_impl.cpp b/src/multisig/multisig_account_kex_impl.cpp index ef0acf3074..59de812a47 100644 --- a/src/multisig/multisig_account_kex_impl.cpp +++ b/src/multisig/multisig_account_kex_impl.cpp @@ -29,18 +29,21 @@ #include "multisig_account.h" #include "crypto/crypto.h" +extern "C" +{ +#include "crypto/crypto-ops.h" +} +#include "cryptonote_basic/account_generators.h" #include "cryptonote_config.h" #include "include_base_utils.h" #include "multisig.h" #include "multisig_kex_msg.h" +#include "multisig_signer_set_filter.h" #include "ringct/rctOps.h" - -#include +#include "seraphis_crypto/math_utils.h" #include -#include #include -#include #include #include #include @@ -87,17 +90,19 @@ namespace multisig * * Result: * - privkey = H(derivation) - * - pubkey = privkey * G + * - pubkey = privkey * derivation_generator + * param: derivation_generator - generator to derive with * param: derivation - a curve point * outparam: derived_pubkey_out - public key of the resulting privkey * return: multisig private key */ //---------------------------------------------------------------------------------------------------------------------- - static crypto::secret_key calculate_multisig_keypair_from_derivation(const crypto::public_key_memsafe &derivation, + static crypto::secret_key calculate_multisig_keypair_from_derivation(const rct::key &derivation_generator, + const crypto::public_key_memsafe &derivation, crypto::public_key &derived_pubkey_out) { crypto::secret_key blinded_skey = get_multisig_blinded_secret_key(rct::rct2sk(rct::pk2rct(derivation))); - CHECK_AND_ASSERT_THROW_MES(crypto::secret_key_to_public_key(blinded_skey, derived_pubkey_out), "Failed to derive public key"); + derived_pubkey_out = rct::rct2pk(rct::scalarmultKey(derivation_generator, rct::sk2rct(blinded_skey))); return blinded_skey; } @@ -186,7 +191,8 @@ namespace multisig * return: final multisig public spend key for the account */ //---------------------------------------------------------------------------------------------------------------------- - static crypto::public_key generate_multisig_aggregate_key(std::vector final_keys, + static crypto::public_key generate_multisig_aggregate_key(const rct::key &aggregation_generator, + std::vector final_keys, std::vector &privkeys_inout) { // collect all public keys that will go into the spend key (these don't need to be memsafe) @@ -199,9 +205,9 @@ namespace multisig for (std::size_t multisig_keys_index{0}; multisig_keys_index < privkeys_inout.size(); ++multisig_keys_index) { - crypto::public_key pubkey; - CHECK_AND_ASSERT_THROW_MES(crypto::secret_key_to_public_key(privkeys_inout[multisig_keys_index], pubkey), - "Failed to derive public key"); + crypto::public_key pubkey{ + rct::rct2pk(rct::scalarmultKey(aggregation_generator, rct::sk2rct(privkeys_inout[multisig_keys_index]))) + }; own_keys_mapping[pubkey] = multisig_keys_index; @@ -244,6 +250,10 @@ namespace multisig rct::addKeys(aggregate_key, aggregate_key, converted_pubkey); } + // sanity check (this should never fail because multisig kex messages internally prevent non-prime message keys) + CHECK_AND_ASSERT_THROW_MES(rct::isInMainSubgroup(aggregate_key), + "Multisig pubkey is not in the main subgroup."); + return rct::rct2pk(aggregate_key); } //---------------------------------------------------------------------------------------------------------------------- @@ -338,25 +348,15 @@ namespace multisig // - origins = all the signing pubkeys that recommended a given msg pubkey for (const auto &expanded_msg : expanded_msgs) { - // in round 1, only the signing pubkey is treated as a msg pubkey - if (round == 1) + // copy all pubkeys from message into list + for (const auto &pubkey : expanded_msg.get_msg_pubkeys()) { - // note: ignores duplicates - sanitized_pubkeys_out[expanded_msg.get_signing_pubkey()].insert(expanded_msg.get_signing_pubkey()); - } - // in other rounds, only the msg pubkeys are treated as msg pubkeys - else - { - // copy all pubkeys from message into list - for (const auto &pubkey : expanded_msg.get_msg_pubkeys()) - { - // ignore pubkeys in 'ignore' set - if (std::find(exclude_pubkeys.begin(), exclude_pubkeys.end(), pubkey) != exclude_pubkeys.end()) - continue; + // ignore pubkeys in 'ignore' set + if (std::find(exclude_pubkeys.begin(), exclude_pubkeys.end(), pubkey) != exclude_pubkeys.end()) + continue; - // note: ignores duplicates - sanitized_pubkeys_out[pubkey].insert(expanded_msg.get_signing_pubkey()); - } + // note: ignores duplicates + sanitized_pubkeys_out[pubkey].insert(expanded_msg.get_signing_pubkey()); } } @@ -476,28 +476,6 @@ namespace multisig num_signers_required << ")."); // 3. each origin should recommend a precise number of pubkeys - - // TODO: move to a 'math' library, with unit tests - auto n_choose_k_f = - [](const std::uint32_t n, const std::uint32_t k) -> std::uint32_t - { - static_assert(std::numeric_limits::digits <= std::numeric_limits::digits, - "n_choose_k requires no rounding issues when converting between int32 <-> double."); - - if (n < k) - return 0; - - double fp_result = boost::math::binomial_coefficient(n, k); - - if (fp_result < 0) - return 0; - - if (fp_result > std::numeric_limits::max()) // note: std::round() returns std::int32_t - return 0; - - return static_cast(std::round(fp_result)); - }; - // other signers: (N - 2) choose (msg_round_num - 1) // - Each signer recommends keys they share with other signers. // - In each round, every group of size 'round num' will have a key. From a single signer's perspective, @@ -507,10 +485,10 @@ namespace multisig // - Other signers will recommend (N - 2) choose (msg_round_num - 1) pubkeys (after removing keys shared with local). // Note: Keys shared with local are filtered out to facilitate kex round boosting, where one or more signers may // have boosted the local signer (implying they didn't have access to the local signer's previous round msg). - const std::uint32_t expected_recommendations_others = n_choose_k_f(signers.size() - 2, round - 1); + const std::uint32_t expected_recommendations_others = sp::math::n_choose_k(signers.size() - 2, round - 1); // local: (N - 1) choose (msg_round_num - 1) - const std::uint32_t expected_recommendations_self = n_choose_k_f(signers.size() - 1, round - 1); + const std::uint32_t expected_recommendations_self = sp::math::n_choose_k(signers.size() - 1, round - 1); // note: expected_recommendations_others would be 0 in the last round of 1-of-N, but we don't call this function for // that case @@ -567,7 +545,7 @@ namespace multisig // note: do NOT remove the local signer from the pubkey origins map, since the post-kex round can be force-updated with // just the local signer's post-kex message (if the local signer were removed, then the post-kex message's pubkeys - // would be completely lost) + // would be completely deleted) // evaluate pubkeys collected @@ -608,7 +586,7 @@ namespace multisig * INTERNAL * * brief: multisig_kex_process_round_msgs - Process kex messages for the active kex round. - * - A wrapper around evaluate_multisig_kex_round_msgs() -> multisig_kex_make_next_msg(). + * - A wrapper around evaluate_multisig_kex_round_msgs() -> multisig_kex_make_round_keys(). * - In other words, evaluate the input messages and try to make a message for the next round. * - Note: Must be called on the final round's msgs to evaluate the final key components * recommended by other participants. @@ -623,7 +601,7 @@ namespace multisig * outparam: keys_to_origins_map_out - map between round keys and identity keys * - If in the final round, these are key shares recommended by other signers for the final aggregate key. * - Otherwise, these are the local account's DH derivations for the next round. - * - See multisig_kex_make_next_msg() for an explanation. + * - See multisig_kex_make_round_keys() for an explanation. * return: multisig kex message for next round, or empty message if 'current_round' is the final round */ //---------------------------------------------------------------------------------------------------------------------- @@ -684,66 +662,78 @@ namespace multisig //---------------------------------------------------------------------------------------------------------------------- // multisig_account: INTERNAL //---------------------------------------------------------------------------------------------------------------------- - void multisig_account::initialize_kex_update(const std::vector &expanded_msgs, - const std::uint32_t kex_rounds_required, - std::vector &exclude_pubkeys_out) + std::vector multisig_account::get_kex_exclude_pubkeys( + const cryptonote::account_generators &generators) const { + // exclude all keys the local account recommends + std::vector exclude_pubkeys; + if (m_kex_rounds_complete == 0) { - // the first round of kex msgs will contain each participant's base pubkeys and ancillary privkeys - - // collect participants' base common privkey shares - // note: duplicate privkeys are acceptable, and duplicates due to duplicate signers - // will be blocked by duplicate-signer errors after this function is called - std::vector participant_base_common_privkeys; - participant_base_common_privkeys.reserve(expanded_msgs.size() + 1); + // in the first round, only the local pubkey is recommended by the local signer + const rct::key initial_pubkey{rct::scalarmultKey(rct::pk2rct(generators.m_primary), rct::sk2rct(m_base_privkey))}; + exclude_pubkeys.emplace_back(rct::rct2pk(initial_pubkey)); + } + else + { + // in other rounds, kex msgs will contain participants' shared keys - // add local ancillary base privkey - participant_base_common_privkeys.emplace_back(m_base_common_privkey); + // ignore shared keys the account helped create for this round + for (const auto &shared_key_with_origins : m_kex_keys_to_origins_map) + exclude_pubkeys.emplace_back(shared_key_with_origins.first); + } - // add other signers' base common privkeys - for (const auto &expanded_msg : expanded_msgs) - { - if (expanded_msg.get_signing_pubkey() != m_base_pubkey) - { - participant_base_common_privkeys.emplace_back(expanded_msg.get_msg_privkey()); - } - } + return exclude_pubkeys; + } + //---------------------------------------------------------------------------------------------------------------------- + // multisig_account: INTERNAL + //---------------------------------------------------------------------------------------------------------------------- + void multisig_account::initialize_kex_update(const cryptonote::account_generators &generators, + const std::vector &expanded_msgs, + const std::uint32_t kex_rounds_required) + { + // initialization is only needed during the first round + if (m_kex_rounds_complete > 0) + return; - // make common privkey - make_multisig_common_privkey(std::move(participant_base_common_privkeys), m_common_privkey); + // the first round of kex msgs will contain each participant's base pubkeys and ancillary privkeys, so we prepare + // them here - // set common pubkey - CHECK_AND_ASSERT_THROW_MES(crypto::secret_key_to_public_key(m_common_privkey, m_common_pubkey), - "Failed to derive public key"); + // collect participants' base common privkey shares + // note: duplicate privkeys are acceptable, and duplicates due to duplicate signers + // will be blocked by duplicate-signer errors after this function is called + std::vector participant_base_common_privkeys; + participant_base_common_privkeys.reserve(expanded_msgs.size() + 1); - // if N-of-N, then the base privkey will be used directly to make the account's share of the final key - if (kex_rounds_required == 1) - { - m_multisig_privkeys.clear(); - m_multisig_privkeys.emplace_back(m_base_privkey); - } + // add local ancillary base privkey + participant_base_common_privkeys.emplace_back(m_base_common_privkey); - // exclude all keys the local account recommends - // - in the first round, only the local pubkey is recommended by the local signer - exclude_pubkeys_out.emplace_back(m_base_pubkey); - } - else + // add other signers' base common privkeys + for (const multisig_kex_msg &expanded_msg : expanded_msgs) { - // in other rounds, kex msgs will contain participants' shared keys + if (expanded_msg.get_signing_pubkey() != m_base_pubkey) + participant_base_common_privkeys.emplace_back(expanded_msg.get_msg_privkey()); + } - // ignore shared keys the account helped create for this round - for (const auto &shared_key_with_origins : m_kex_keys_to_origins_map) - { - exclude_pubkeys_out.emplace_back(shared_key_with_origins.first); - } + // make common privkey + make_multisig_common_privkey(std::move(participant_base_common_privkeys), m_common_privkey); + + // set common pubkey + m_common_pubkey = rct::rct2pk(rct::scalarmultKey(rct::pk2rct(generators.m_secondary), rct::sk2rct(m_common_privkey))); + + // if N-of-N, then the base privkey will be used directly to make the account's share of the final key + if (kex_rounds_required == 1) + { + m_multisig_privkeys.clear(); + m_multisig_privkeys.emplace_back(m_base_privkey); } } //---------------------------------------------------------------------------------------------------------------------- // multisig_account: INTERNAL //---------------------------------------------------------------------------------------------------------------------- - void multisig_account::finalize_kex_update(const std::uint32_t kex_rounds_required, - multisig_keyset_map_memsafe_t result_keys_to_origins_map) + void multisig_account::finalize_kex_update(const cryptonote::account_generators &generators, + const std::uint32_t kex_rounds_required, + multisig_keyset_map_memsafe_t result_keys_to_origins_map) { std::vector next_msg_keys; @@ -771,15 +761,46 @@ namespace multisig result_keys.reserve(result_keys_to_origins_map.size()); for (const auto &result_key_and_origins : result_keys_to_origins_map) - { result_keys.emplace_back(result_key_and_origins.first); + + // save pre-aggregation privkeys as pubkeys for migrating the origins map below + std::vector preagg_keyshares; + preagg_keyshares.reserve(m_multisig_privkeys.size()); + + for (const crypto::secret_key &multisig_privkey : m_multisig_privkeys) + { + preagg_keyshares.emplace_back( + rct::rct2pk(rct::scalarmultKey(rct::pk2rct(generators.m_primary), rct::sk2rct(multisig_privkey))) + ); } // compute final aggregate key, update local multisig privkeys with aggregation coefficients applied - m_multisig_pubkey = generate_multisig_aggregate_key(std::move(result_keys), m_multisig_privkeys); + m_multisig_pubkey = + generate_multisig_aggregate_key(rct::pk2rct(generators.m_primary), std::move(result_keys), m_multisig_privkeys); + + // prepare for aggregation-style signing + m_multisig_keyshare_pubkeys.resize(m_multisig_privkeys.size()); + signer_set_filter temp_filter; + + for (std::size_t keyshare_index{0}; keyshare_index < m_multisig_privkeys.size(); ++keyshare_index) + { + // 1) convert keyshares to pubkeys for convenience when assembling aggregate keys + m_multisig_keyshare_pubkeys[keyshare_index] = rct::rct2pk(rct::scalarmultKey( + rct::pk2rct(generators.m_primary), + rct::sk2rct(m_multisig_privkeys[keyshare_index])) + ); + + // 2) record [post-aggregation pubkeys : origins] map for aggregation-style signing + m_keyshare_to_origins_map[m_multisig_keyshare_pubkeys[keyshare_index]] = + std::move(m_kex_keys_to_origins_map[preagg_keyshares[keyshare_index]]); + + // 3) update 'available signers' for aggregation-style signing + multisig_signers_to_filter(m_keyshare_to_origins_map[m_multisig_keyshare_pubkeys[keyshare_index]], + m_signers, + temp_filter); + m_available_signers_for_aggregation |= temp_filter; + } - // no longer need the account's pubkeys saved for this round (they were only used to build exclude_pubkeys) - // TODO: record [pre-aggregation pubkeys : origins] map for aggregation-style signing m_kex_keys_to_origins_map.clear(); // save keys that should be recommended to other signers @@ -797,7 +818,7 @@ namespace multisig // derivations are shared secrets between each group of N - M + 1 signers of which the local account is a member // - convert them to private keys: multisig_key = H(derivation) - // - note: shared key = multisig_key[i]*G is recorded in the kex msg for sending to other participants + // - note: shared key = multisig_key[i]*primary_generator is recorded in the kex msg for sending to other participants // instead of the original 'derivation' value (which MUST be kept secret!) m_multisig_privkeys.clear(); m_multisig_privkeys.reserve(result_keys_to_origins_map.size()); @@ -808,10 +829,13 @@ namespace multisig for (const auto &derivation_and_origins : result_keys_to_origins_map) { // multisig_privkey = H(derivation) - // derived pubkey = multisig_key * G + // derived pubkey = multisig_key * generators.m_primary crypto::public_key_memsafe derived_pubkey; m_multisig_privkeys.push_back( - calculate_multisig_keypair_from_derivation(derivation_and_origins.first, derived_pubkey)); + calculate_multisig_keypair_from_derivation(rct::pk2rct(generators.m_primary), + derivation_and_origins.first, + derived_pubkey) + ); // save the account's kex key mappings for this round [derived pubkey : other signers who will have the same key] m_kex_keys_to_origins_map[derived_pubkey] = std::move(derivation_and_origins.second); @@ -842,9 +866,11 @@ namespace multisig // make next round's message (or reproduce the post-kex verification round if kex is complete) m_next_round_kex_message = multisig_kex_msg{ - (m_kex_rounds_complete > kex_rounds_required ? kex_rounds_required : m_kex_rounds_complete) + 1, - m_base_privkey, - std::move(next_msg_keys)}.get_msg(); + get_kex_msg_version(m_account_era), + (m_kex_rounds_complete > kex_rounds_required ? kex_rounds_required : m_kex_rounds_complete) + 1, + m_base_privkey, + std::move(next_msg_keys) + }.get_msg(); } //---------------------------------------------------------------------------------------------------------------------- // multisig_account: INTERNAL @@ -862,12 +888,15 @@ namespace multisig CHECK_AND_ASSERT_THROW_MES(m_kex_rounds_complete < kex_rounds_required + 1, "Multisig kex has already completed all required rounds (including post-kex verification)."); + // account generators + cryptonote::account_generators generators{get_account_generators(m_account_era)}; + // initialize account update - std::vector exclude_pubkeys; - initialize_kex_update(expanded_msgs, kex_rounds_required, exclude_pubkeys); + this->initialize_kex_update(generators, expanded_msgs, kex_rounds_required); // process messages into a [pubkey : {origins}] map multisig_keyset_map_memsafe_t result_keys_to_origins_map; + multisig_kex_process_round_msgs( m_base_privkey, m_base_pubkey, @@ -875,12 +904,85 @@ namespace multisig m_threshold, m_signers, expanded_msgs, - exclude_pubkeys, + this->get_kex_exclude_pubkeys(generators), incomplete_signer_set, result_keys_to_origins_map); // finish account update - finalize_kex_update(kex_rounds_required, std::move(result_keys_to_origins_map)); + this->finalize_kex_update(generators, kex_rounds_required, std::move(result_keys_to_origins_map)); + } + //----------------------------------------------------------------- + // multisig_account: EXTERNAL + //----------------------------------------------------------------- + multisig_kex_msg multisig_account::get_multisig_kex_round_booster(const std::uint32_t threshold, + const std::uint32_t num_signers, + const std::vector &expanded_msgs) const + { + // the messages passed in should be required for the next kex round of this account (the round it is currently + // working on) + const std::uint32_t expected_msgs_round{m_kex_rounds_complete + 1}; + const std::uint32_t kex_rounds_required{multisig_kex_rounds_required(num_signers, threshold)}; + + CHECK_AND_ASSERT_THROW_MES(num_signers > 1, "Must be at least one other multisig signer."); + CHECK_AND_ASSERT_THROW_MES(num_signers <= config::MULTISIG_MAX_SIGNERS, "Too many multisig signers specified."); + CHECK_AND_ASSERT_THROW_MES(expected_msgs_round < kex_rounds_required, + "Multisig kex booster: this account has already completed all intermediate kex rounds so it can't make a kex " + "booster (there is no round available to boost)."); + CHECK_AND_ASSERT_THROW_MES(expanded_msgs.size() > 0, "At least one input kex message expected."); + + // account generators + cryptonote::account_generators generators{get_account_generators(m_account_era)}; + + // sanitize pubkeys from input msgs + multisig_keyset_map_memsafe_t pubkey_origins_map; + const std::uint32_t msgs_round{ + multisig_kex_msgs_sanitize_pubkeys(expanded_msgs, this->get_kex_exclude_pubkeys(generators), pubkey_origins_map) + }; + CHECK_AND_ASSERT_THROW_MES(msgs_round == expected_msgs_round, "Kex messages were not for expected round."); + + // remove the local signer from sanitized messages + remove_key_from_mapped_sets(m_base_pubkey, pubkey_origins_map); + + // make DH derivations for booster message + multisig_keyset_map_memsafe_t derivation_to_origins_map; + multisig_kex_make_round_keys(m_base_privkey, std::move(pubkey_origins_map), derivation_to_origins_map); + + // collect keys for booster message + std::vector next_msg_keys; + next_msg_keys.reserve(derivation_to_origins_map.size()); + + if (msgs_round + 1 == kex_rounds_required) + { + // final kex round: send DH derivation pubkeys in the message + for (const auto &derivation_and_origins : derivation_to_origins_map) + { + // multisig_privkey = H(derivation) + // derived pubkey = multisig_key * generators.m_primary + crypto::public_key_memsafe derived_pubkey; + calculate_multisig_keypair_from_derivation(rct::pk2rct(generators.m_primary), + derivation_and_origins.first, + derived_pubkey); + + // save keys that should be recommended to other signers + // - The keys multisig_key*generators.m_primary are sent to other participants in the message, so they can be used + // to produce the final multisig key via generate_multisig_spend_public_key(). + next_msg_keys.push_back(derived_pubkey); + } + } + else //(msgs_round + 1 < kex_rounds_required) + { + // intermediate kex round: send DH derivations directly in the message + for (const auto &derivation_and_origins : derivation_to_origins_map) + next_msg_keys.push_back(derivation_and_origins.first); + } + + // produce a kex message for the round after the round this account is currently working on + return multisig_kex_msg{ + get_kex_msg_version(m_account_era), + msgs_round + 1, + m_base_privkey, + std::move(next_msg_keys) + }.get_msg(); } //---------------------------------------------------------------------------------------------------------------------- } //namespace multisig diff --git a/src/multisig/multisig_clsag.cpp b/src/multisig/multisig_clsag.cpp new file mode 100644 index 0000000000..e790a860ce --- /dev/null +++ b/src/multisig/multisig_clsag.cpp @@ -0,0 +1,431 @@ +// Copyright (c) 2022, 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 "multisig_clsag.h" + +//local headers +#include "common/container_helpers.h" +#include "crypto/crypto.h" +extern "C" +{ +#include "crypto/crypto-ops.h" +} +#include "misc_language.h" +#include "misc_log_ex.h" +#include "multisig_clsag_context.h" +#include "multisig_nonce_cache.h" +#include "ringct/rctOps.h" +#include "ringct/rctSigs.h" +#include "ringct/rctTypes.h" + +//third party headers + +//standard headers +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "multisig" + +namespace multisig +{ +//------------------------------------------------------------------------------------------------------------------- +// CLSAG proof response +// r = alpha - c * w +// r = alpha - c * (mu_K*k + mu_C*z) +//------------------------------------------------------------------------------------------------------------------- +static void compute_response(const rct::key &challenge, + const rct::key &alpha, + const crypto::secret_key &k, + const crypto::secret_key &z, + const rct::key &mu_K, + const rct::key &mu_C, + rct::key &r_out) +{ + // r = alpha - c * (mu_K*k + mu_C*z) + sc_mul(r_out.bytes, mu_K.bytes, to_bytes(k)); //mu_K*k + sc_muladd(r_out.bytes, mu_C.bytes, to_bytes(z), r_out.bytes); //+ mu_C*z + sc_mul(r_out.bytes, challenge.bytes, r_out.bytes); //c * (...) + sc_sub(r_out.bytes, alpha.bytes, r_out.bytes); //alpha - c * (...) +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void signer_nonces_mul8(const MultisigPubNonces &signer_pub_nonce_pair, MultisigPubNonces &nonce_pair_mul8_out) +{ + nonce_pair_mul8_out.signature_nonce_1_pub = rct::scalarmult8(signer_pub_nonce_pair.signature_nonce_1_pub); + nonce_pair_mul8_out.signature_nonce_2_pub = rct::scalarmult8(signer_pub_nonce_pair.signature_nonce_2_pub); + + CHECK_AND_ASSERT_THROW_MES(!(nonce_pair_mul8_out.signature_nonce_1_pub == rct::identity()), + "clsag multisig: bad signer nonce (alpha_1 identity)!"); + CHECK_AND_ASSERT_THROW_MES(!(nonce_pair_mul8_out.signature_nonce_2_pub == rct::identity()), + "clsag multisig: bad signer nonce (alpha_2 identity)!"); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static rct::keyV sum_together_multisig_pub_nonces(const std::vector &pub_nonces) +{ + rct::keyV summed_nonces; + summed_nonces.resize(2, rct::identity()); + + for (const MultisigPubNonces &pub_nonce : pub_nonces) + { + rct::addKeys(summed_nonces[0], summed_nonces[0], pub_nonce.signature_nonce_1_pub); + rct::addKeys(summed_nonces[1], summed_nonces[1], pub_nonce.signature_nonce_2_pub); + } + + return summed_nonces; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +const rct::key& main_proof_key_ref(const CLSAGMultisigProposal &proposal) +{ + CHECK_AND_ASSERT_THROW_MES(proposal.l < proposal.ring_members.size(), + "CLSAGMultisigProposal (get main proof key): l is out of range."); + + return proposal.ring_members[proposal.l].dest; +} +//------------------------------------------------------------------------------------------------------------------- +const rct::key& auxilliary_proof_key_ref(const CLSAGMultisigProposal &proposal) +{ + CHECK_AND_ASSERT_THROW_MES(proposal.l < proposal.ring_members.size(), + "CLSAGMultisigProposal (get auxilliary proof key): l is out of range."); + + return proposal.ring_members[proposal.l].mask; +} +//------------------------------------------------------------------------------------------------------------------- +void make_clsag_multisig_proposal(const rct::key &message, + rct::ctkeyV ring_members, + const rct::key &masked_C, + const crypto::key_image &KI, + const crypto::key_image &D, + const std::uint32_t l, + CLSAGMultisigProposal &proposal_out) +{ + // checks + const std::size_t num_ring_members{ring_members.size()}; + CHECK_AND_ASSERT_THROW_MES(l < num_ring_members, "make CLSAG multisig proposal: l is out of range."); + + // assemble proposal + proposal_out.message = message; + proposal_out.ring_members = std::move(ring_members); + proposal_out.masked_C = masked_C; + proposal_out.KI = KI; + proposal_out.D = D; + proposal_out.decoy_responses = rct::skvGen(num_ring_members); + proposal_out.l = l; +} +//------------------------------------------------------------------------------------------------------------------- +void make_clsag_multisig_partial_sig(const CLSAGMultisigProposal &proposal, + const crypto::secret_key &k_e, + const crypto::secret_key &z_e, + const std::vector &signer_pub_nonces_G, + const std::vector &signer_pub_nonces_Hp, + const crypto::secret_key &local_nonce_1_priv, + const crypto::secret_key &local_nonce_2_priv, + CLSAGMultisigPartial &partial_sig_out) +{ + // check multisig proposal + CHECK_AND_ASSERT_THROW_MES(!(main_proof_key_ref(proposal) == rct::identity()), + "make CLSAG multisig partial sig: bad proof key (main key identity)!"); + CHECK_AND_ASSERT_THROW_MES(!(rct::ki2rct(proposal.KI) == rct::identity()), + "make CLSAG multisig partial sig: bad proof key (KI identity)!"); + + for (const rct::key &decoy_response : proposal.decoy_responses) + { + CHECK_AND_ASSERT_THROW_MES(sc_isnonzero(decoy_response.bytes), + "make CLSAG multisig partial sig: bad private key (proposal decoy response zero)!"); + CHECK_AND_ASSERT_THROW_MES(sc_check(decoy_response.bytes) == 0, + "make CLSAG multisig partial sig: bad private key (proposal decoy response)!"); + } + + const std::size_t num_ring_members{proposal.ring_members.size()}; + CHECK_AND_ASSERT_THROW_MES(proposal.decoy_responses.size() == num_ring_members, + "make CLSAG multisig partial sig: inconsistent number of decoy responses!"); + CHECK_AND_ASSERT_THROW_MES(proposal.l < num_ring_members, "make CLSAG multisig partial sig: l is out of range."); + + // check other inputs + CHECK_AND_ASSERT_THROW_MES(sc_isnonzero(to_bytes(k_e)), + "make CLSAG multisig partial sig: bad private key (proposal nonce k_e zero)!"); + CHECK_AND_ASSERT_THROW_MES(sc_check(to_bytes(k_e)) == 0, + "make CLSAG multisig partial sig: bad private key (proposal nonce k_e)!"); + CHECK_AND_ASSERT_THROW_MES(sc_isnonzero(to_bytes(z_e)), + "make CLSAG multisig partial sig: bad private key (proposal nonce z_e zero)!"); + CHECK_AND_ASSERT_THROW_MES(sc_check(to_bytes(z_e)) == 0, + "make CLSAG multisig partial sig: bad private key (proposal nonce z_e)!"); + + const std::size_t num_signers{signer_pub_nonces_G.size()}; + CHECK_AND_ASSERT_THROW_MES(signer_pub_nonces_Hp.size() == num_signers, + "make CLSAG multisig partial sig: inconsistent signer pub nonce set sizes!"); + + CHECK_AND_ASSERT_THROW_MES(sc_isnonzero(to_bytes(local_nonce_1_priv)), + "make CLSAG multisig partial sig: bad private key (local_nonce_1_priv zero)!"); + CHECK_AND_ASSERT_THROW_MES(sc_check(to_bytes(local_nonce_1_priv)) == 0, + "make CLSAG multisig partial sig: bad private key (local_nonce_1_priv)!"); + CHECK_AND_ASSERT_THROW_MES(sc_isnonzero(to_bytes(local_nonce_2_priv)), + "make CLSAG multisig partial sig: bad private key (local_nonce_2_priv zero)!"); + CHECK_AND_ASSERT_THROW_MES(sc_check(to_bytes(local_nonce_2_priv)) == 0, + "make CLSAG multisig partial sig: bad private key (local_nonce_2_priv)!"); + + // prepare participant nonces + std::vector signer_pub_nonces_G_mul8; + std::vector signer_pub_nonces_Hp_mul8; + signer_pub_nonces_G_mul8.reserve(num_signers); + signer_pub_nonces_Hp_mul8.reserve(num_signers); + + for (const MultisigPubNonces &signer_pub_nonce_pair : signer_pub_nonces_G) + signer_nonces_mul8(signer_pub_nonce_pair, tools::add_element(signer_pub_nonces_G_mul8)); + for (const MultisigPubNonces &signer_pub_nonce_pair : signer_pub_nonces_Hp) + signer_nonces_mul8(signer_pub_nonce_pair, tools::add_element(signer_pub_nonces_Hp_mul8)); + + // check that the local signer's signature opening is in the input set of opening nonces (for both G and Hp versions) + MultisigPubNonces local_pub_nonces_G; + rct::scalarmultBase(local_pub_nonces_G.signature_nonce_1_pub, rct::sk2rct(local_nonce_1_priv)); + rct::scalarmultBase(local_pub_nonces_G.signature_nonce_2_pub, rct::sk2rct(local_nonce_2_priv)); + + crypto::key_image key_image_base; + crypto::generate_key_image(rct::rct2pk(main_proof_key_ref(proposal)), rct::rct2sk(rct::I), key_image_base); //Hp(K[l]) + + MultisigPubNonces local_nonce_pubs_Hp; + rct::scalarmultKey(local_nonce_pubs_Hp.signature_nonce_1_pub, + rct::ki2rct(key_image_base), + rct::sk2rct(local_nonce_1_priv)); + rct::scalarmultKey(local_nonce_pubs_Hp.signature_nonce_2_pub, + rct::ki2rct(key_image_base), + rct::sk2rct(local_nonce_2_priv)); + + CHECK_AND_ASSERT_THROW_MES( + std::find(signer_pub_nonces_G_mul8.begin(), signer_pub_nonces_G_mul8.end(), local_pub_nonces_G) != + signer_pub_nonces_G_mul8.end(), + "make CLSAG multisig partial sig: local signer's opening nonces not in input set (G)!"); + CHECK_AND_ASSERT_THROW_MES( + std::find(signer_pub_nonces_Hp_mul8.begin(), signer_pub_nonces_Hp_mul8.end(), local_nonce_pubs_Hp) != + signer_pub_nonces_Hp_mul8.end(), + "make CLSAG multisig partial sig: local signer's opening nonces not in input set (Hp)!"); + + // sum participant nonces to satisfy CLSAG_context_t, which pre-combines participant nonces before applying the + // multisig nonce merge factor + const rct::keyV signer_nonce_pub_sum_G{ + sum_together_multisig_pub_nonces(signer_pub_nonces_G_mul8) + }; + const rct::keyV signer_nonce_pub_sum_Hp{ + sum_together_multisig_pub_nonces(signer_pub_nonces_Hp_mul8) + }; + + // split the ring members + rct::keyV nominal_proof_Ks; + rct::keyV nominal_pedersen_Cs; + nominal_proof_Ks.reserve(num_ring_members); + nominal_pedersen_Cs.reserve(num_ring_members); + + for (const rct::ctkey &ring_member : proposal.ring_members) + { + nominal_proof_Ks.emplace_back(ring_member.dest); + nominal_pedersen_Cs.emplace_back(ring_member.mask); + } + + + /// prepare CLSAG context + signing::CLSAG_context_t multisig_CLSAG_context; + + multisig_CLSAG_context.init(nominal_proof_Ks, + nominal_pedersen_Cs, + proposal.masked_C, + proposal.message, + rct::ki2rct(proposal.KI), + rct::ki2rct(proposal.D), + proposal.l, + proposal.decoy_responses, + 2); + + + /// get the local signer's combined musig2-style private nonce and the CLSAG challenges (both the nominal challenge + // at index 0, and the challenge that is responded to by the signer at index l) + rct::key combined_local_nonce_privkey; + rct::key clsag_challenge_c_0; + rct::key signer_challenge; + + CHECK_AND_ASSERT_THROW_MES(multisig_CLSAG_context.combine_alpha_and_compute_challenge(signer_nonce_pub_sum_G, + signer_nonce_pub_sum_Hp, + {rct::sk2rct(local_nonce_1_priv), rct::sk2rct(local_nonce_2_priv)}, + combined_local_nonce_privkey, + clsag_challenge_c_0, + signer_challenge), + "make CLSAG multisig partial sig: failed to get combined local nonce privkey and CLSAG challenges."); + + + /// compute the local signer's partial response + + // prepare the CLSAG merge factors that separate the main proof key and ancillary proof key components + rct::key mu_K; + rct::key mu_C; + + CHECK_AND_ASSERT_THROW_MES(multisig_CLSAG_context.get_mu(mu_K, mu_C), + "make CLSAG multisig partial sig: failed to get CLSAG merge factors."); + + // compute the local signer's partial response + rct::key partial_response; + + compute_response(signer_challenge, + combined_local_nonce_privkey, + k_e, + z_e, + mu_K, + mu_C, + partial_response); + + + /// set the partial signature components + partial_sig_out.message = proposal.message; + partial_sig_out.main_proof_key_K = main_proof_key_ref(proposal); + partial_sig_out.l = proposal.l; + partial_sig_out.responses = proposal.decoy_responses; + partial_sig_out.responses[proposal.l] = partial_response; //inject partial response + partial_sig_out.c_0 = clsag_challenge_c_0, + partial_sig_out.KI = proposal.KI; + partial_sig_out.D = proposal.D; +} +//------------------------------------------------------------------------------------------------------------------- +bool try_make_clsag_multisig_partial_sig(const CLSAGMultisigProposal &proposal, + const crypto::secret_key &k_e, + const crypto::secret_key &z_e, + const std::vector &signer_pub_nonces_G, + const std::vector &signer_pub_nonces_Hp, + const signer_set_filter filter, + MultisigNonceCache &nonce_record_inout, + CLSAGMultisigPartial &partial_sig_out) +{ + // get the nonce privkeys to sign with + crypto::secret_key nonce_privkey_1; + crypto::secret_key nonce_privkey_2; + if (!nonce_record_inout.try_get_recorded_nonce_privkeys(proposal.message, + main_proof_key_ref(proposal), + filter, + nonce_privkey_1, + nonce_privkey_2)) + return false; + + // make the partial signature + CLSAGMultisigPartial partial_sig_temp; + make_clsag_multisig_partial_sig(proposal, + k_e, + z_e, + signer_pub_nonces_G, + signer_pub_nonces_Hp, + nonce_privkey_1, + nonce_privkey_2, + partial_sig_temp); + + // clear the used nonces + CHECK_AND_ASSERT_THROW_MES(nonce_record_inout.try_remove_record(proposal.message, main_proof_key_ref(proposal), filter), + "try make clsag multisig partial sig: failed to clear nonces from nonce record (aborting partial signature)!"); + + // set the output partial sig AFTER used nonces are cleared, in case of exception + partial_sig_out = std::move(partial_sig_temp); + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +void finalize_clsag_multisig_proof(const std::vector &partial_sigs, + const rct::ctkeyV &ring_members, + const rct::key &masked_commitment, + rct::clsag &proof_out) +{ + /// input checks + CHECK_AND_ASSERT_THROW_MES(partial_sigs.size() > 0, + "finalize clsag multisig proof: no partial signatures to make proof out of!"); + + // common parts between partial signatures should match + const std::size_t num_ring_members{partial_sigs[0].responses.size()}; + const std::size_t real_signing_index{partial_sigs[0].l}; + + CHECK_AND_ASSERT_THROW_MES(real_signing_index < num_ring_members, + "finalize clsag multisig proof: input partial sigs invalid l!"); + + for (const CLSAGMultisigPartial &partial_sig : partial_sigs) + { + CHECK_AND_ASSERT_THROW_MES(partial_sig.message == partial_sigs[0].message, + "finalize clsag multisig proof: input partial sigs don't match!"); + CHECK_AND_ASSERT_THROW_MES(partial_sig.main_proof_key_K == partial_sigs[0].main_proof_key_K, + "finalize clsag multisig proof: input partial sigs don't match!"); + CHECK_AND_ASSERT_THROW_MES(partial_sig.l == real_signing_index, + "finalize clsag multisig proof: input partial sigs don't match!");; + + CHECK_AND_ASSERT_THROW_MES(partial_sig.responses.size() == num_ring_members, + "finalize clsag multisig proof: input partial sigs don't match!");; + + for (std::size_t ring_index{0}; ring_index < num_ring_members; ++ring_index) + { + // the response at the real signing index is for a partial signature, which is unique per signer, so it isn't + // checked here + if (ring_index == real_signing_index) + continue; + + CHECK_AND_ASSERT_THROW_MES(partial_sig.responses[ring_index] == partial_sigs[0].responses[ring_index], + "finalize clsag multisig proof: input partial sigs don't match!"); + } + + CHECK_AND_ASSERT_THROW_MES(partial_sig.c_0 == partial_sigs[0].c_0, + "finalize clsag multisig proof: input partial sigs don't match!"); + CHECK_AND_ASSERT_THROW_MES(partial_sig.KI == partial_sigs[0].KI, + "finalize clsag multisig proof: input partial sigs don't match!"); + CHECK_AND_ASSERT_THROW_MES(partial_sig.D == partial_sigs[0].D, + "finalize clsag multisig proof: input partial sigs don't match!"); + } + + // ring members should line up with the partial sigs + CHECK_AND_ASSERT_THROW_MES(ring_members.size() == num_ring_members, + "finalize clsag multisig proof: ring members are inconsistent with the partial sigs!"); + CHECK_AND_ASSERT_THROW_MES(ring_members[real_signing_index].dest == partial_sigs[0].main_proof_key_K, + "finalize clsag multisig proof: ring members are inconsistent with the partial sigs!"); + + + /// assemble the final proof + proof_out.s = partial_sigs[0].responses; + proof_out.c1 = partial_sigs[0].c_0; //note: c_0 is correct notation according to the paper, c1 is a typo + proof_out.I = rct::ki2rct(partial_sigs[0].KI); + proof_out.D = rct::scalarmultKey(rct::ki2rct(partial_sigs[0].D), rct::INV_EIGHT); + + proof_out.s[real_signing_index] = rct::zero(); + for (const CLSAGMultisigPartial &partial_sig : partial_sigs) + { + // sum of responses from each multisig signer + sc_add(proof_out.s[real_signing_index].bytes, + proof_out.s[real_signing_index].bytes, + partial_sig.responses[real_signing_index].bytes); + } + + + /// verify that proof assembly succeeded + CHECK_AND_ASSERT_THROW_MES(rct::verRctCLSAGSimple(partial_sigs[0].message, + proof_out, + ring_members, + masked_commitment), + "Multisig CLSAG failed to verify on assembly!"); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace multisig diff --git a/src/multisig/multisig_clsag.h b/src/multisig/multisig_clsag.h new file mode 100644 index 0000000000..61d696f26a --- /dev/null +++ b/src/multisig/multisig_clsag.h @@ -0,0 +1,194 @@ +// Copyright (c) 2022, 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. + +//// +// Multisig utilities for CLSAG proofs. +// +// multisig notation: alpha_{n,e} +// - n: for MuSig2-style bi-nonce signing, alpha_{1,e} is nonce 'D', alpha_{2,e} is nonce 'E' (in their notation) +// - e: multisig signer index in the signer group +// +// Multisig references: +// - MuSig2 (Nick): https://eprint.iacr.org/2020/1261 +// - FROST (Komlo): https://eprint.iacr.org/2020/852 +// - Multisig/threshold security (Crites): https://eprint.iacr.org/2021/1375 +// - MRL-0009 (Brandon Goodell and Sarang Noether): https://web.getmonero.org/resources/research-lab/pubs/MRL-0009.pdf +/// + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "multisig_nonce_cache.h" +#include "ringct/rctTypes.h" + +//third party headers + +//standard headers +#include + +//forward declarations + + +namespace multisig +{ + +//// +// Multisig signature proposal for CLSAG proofs +// +// WARNING: must only use a proposal to make ONE signature, after that the shared decoy responses stored here +// should be deleted immediately +/// +struct CLSAGMultisigProposal final +{ + // message to be signed + rct::key message; + // ring of proof keys {main keys, auxilliary keys (Pedersen commitments)} + rct::ctkeyV ring_members; + // masked Pedersen commitment at index l (commitment to zero: ring_members[l].mask - masked_C = z G) + rct::key masked_C; + // main key image KI + // note: KI = k * Hp(ring_members[l].dest) + crypto::key_image KI; + // ancillary key image D (note: D is stored as '1/8 * D' in the rct::clsag struct, but is stored unmultiplied here) + // note: D = z * Hp(ring_members[l].dest) + crypto::key_image D; + // decoy responses for each {proof key, ancillary proof key} pair (the decoy at index l will be replaced by + // the real multisig aggregate response in the final proof) + rct::keyV decoy_responses; + + // signing key pair's index in the ring + std::uint32_t l; +}; + +/// range-checked access to the signing main proof pubkey +const rct::key& main_proof_key_ref(const CLSAGMultisigProposal &proposal); +/// range-checked access to the signing auxilliary proof pubkey +const rct::key& auxilliary_proof_key_ref(const CLSAGMultisigProposal &proposal); + +//// +// Multisig partially signed CLSAG (from one multisig participant) +// - stores multisig partial response for proof position at index l +// note: does not store ring members because those are not included in the final rct::clsag; ring members are hashed +// into c_0, so checking that c_0 is consistent between partial sigs is sufficient to ensure partial sigs +// are combinable +/// +struct CLSAGMultisigPartial final +{ + // message + rct::key message; + // main proof key K + rct::key main_proof_key_K; + // signing key pair's index in the ring + std::uint32_t l; + + // responses for each {proof key, ancillary proof key} pair + // - the response at index l is this multisig partial signature's partial response + rct::keyV responses; + // challenge + rct::key c_0; + // key image KI + crypto::key_image KI; + // ancillary key image D + crypto::key_image D; +}; + +/** +* brief: make_clsag_multisig_proposal - propose to make a multisig CLSAG proof +* param: message - message to insert in the proof's Fiat-Shamir transform hash +* param: ring_members - ring of proof keys {main key, auxiliary key (Pedersen commitments)} +* param: masked_C - masked auxilliary proof key at index l +* commitment to zero: ring_members[l].mask - masked_C = z G +* param: KI - main key image +* param: D - auxilliary key image +* param: l - index of the signing keys in the key ring +* outparam: proposal_out - CLSAG multisig proposal +*/ +void make_clsag_multisig_proposal(const rct::key &message, + rct::ctkeyV ring_members, + const rct::key &masked_C, + const crypto::key_image &KI, + const crypto::key_image &D, + const std::uint32_t l, + CLSAGMultisigProposal &proposal_out); +/** +* brief: make_clsag_multisig_partial_sig - make local multisig signer's partial signature for a CLSAG proof +* - caller must validate the CLSAG multisig proposal +* - are the key images well-made? +* - are the main key, ancillary key, and masked key legitimate? +* - is the message correct? +* - are all the decoy ring members valid? +* param: proposal - proof proposal to use when constructing the partial signature +* param: k_e - secret key of multisig signer e for main proof key at position l +* param: z_e - secret key of multisig signer e for commitment to zero at position l (for the auxilliary component) +* param: signer_pub_nonces_G - signature nonce pubkeys (1/8) * {alpha_{1,e}*G, alpha_{2,e}*G} from all signers +* (including local signer) +* param: signer_pub_nonces_Hp - signature nonce pubkeys (1/8) * {alpha_{1,e}*Hp(K[l]), alpha_{2,e}*Hp(K[l])} from all +* signers (including local signer) +* param: local_nonce_1_priv - alpha_{1,e} for local signer +* param: local_nonce_2_priv - alpha_{2,e} for local signer +* outparam: partial_sig_out - partially signed CLSAG +*/ +void make_clsag_multisig_partial_sig(const CLSAGMultisigProposal &proposal, + const crypto::secret_key &k_e, + const crypto::secret_key &z_e, + const std::vector &signer_pub_nonces_G, + const std::vector &signer_pub_nonces_Hp, + const crypto::secret_key &local_nonce_1_priv, + const crypto::secret_key &local_nonce_2_priv, + CLSAGMultisigPartial &partial_sig_out); +/** +* brief: try_make_clsag_multisig_partial_sig - make a partial signature using a nonce record (nonce safety guarantee) +* - caller must validate the CLSAG multisig proposal +* param: ...(see make_clsag_multisig_partial_sig()) +* param: filter - filter representing the multisig signer group that is supposedly working on this signature +* inoutparam: nonce_record_inout - a record of nonces for making partial signatures; used nonces will be cleared +* outparam: partial_sig_out - the partial signature +* return: true if creating the partial signature succeeded +*/ +bool try_make_clsag_multisig_partial_sig(const CLSAGMultisigProposal &proposal, + const crypto::secret_key &k_e, + const crypto::secret_key &z_e, + const std::vector &signer_pub_nonces_G, + const std::vector &signer_pub_nonces_Hp, + const signer_set_filter filter, + MultisigNonceCache &nonce_record_inout, + CLSAGMultisigPartial &partial_sig_out); +/** +* brief: finalize_clsag_multisig_proof - create a CLSAG proof from multisig partial signatures +* param: partial_sigs - partial signatures from the multisig subgroup that collaborated on this proof +* param: ring_members - ring member keys used by the proof (for validating the assembled proof) +* param: masked_commitment - masked commitment used by the proof (for validating the assembled proof) +* outparam: proof_out - CLSAG +*/ +void finalize_clsag_multisig_proof(const std::vector &partial_sigs, + const rct::ctkeyV &ring_members, + const rct::key &masked_commitment, + rct::clsag &proof_out); + +} //namespace multisig diff --git a/src/multisig/multisig_kex_msg.cpp b/src/multisig/multisig_kex_msg.cpp index 49350948a4..c37ed1eb5d 100644 --- a/src/multisig/multisig_kex_msg.cpp +++ b/src/multisig/multisig_kex_msg.cpp @@ -27,7 +27,7 @@ // THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "multisig_kex_msg.h" -#include "multisig_kex_msg_serialization.h" +#include "multisig_msg_serialization.h" #include "common/base58.h" #include "crypto/crypto.h" @@ -41,9 +41,10 @@ extern "C" #include "serialization/binary_archive.h" #include "serialization/serialization.h" -#include +#include #include +#include #include #include @@ -51,45 +52,129 @@ extern "C" #undef MONERO_DEFAULT_LOG_CATEGORY #define MONERO_DEFAULT_LOG_CATEGORY "multisig" +// pre-rework multisig (deprecated) const boost::string_ref MULTISIG_KEX_V1_MAGIC{"MultisigV1"}; const boost::string_ref MULTISIG_KEX_MSG_V1_MAGIC{"MultisigxV1"}; +// cryptonote/ringct multisig kex const boost::string_ref MULTISIG_KEX_MSG_V2_MAGIC_1{"MultisigxV2R1"}; //round 1 const boost::string_ref MULTISIG_KEX_MSG_V2_MAGIC_N{"MultisigxV2Rn"}; //round n > 1 +// seraphis multisig kex +const boost::string_ref MULTISIG_KEX_MSG_V3_MAGIC_1{"MultisigxV3R1"}; //round 1 +const boost::string_ref MULTISIG_KEX_MSG_V3_MAGIC_N{"MultisigxV3Rn"}; //round n > 1 namespace multisig { + //---------------------------------------------------------------------------------------------------------------------- + // INTERNAL + //---------------------------------------------------------------------------------------------------------------------- + static void set_msg_magic(const std::uint32_t version, const std::uint32_t kex_round, std::string &msg_out) + { + msg_out.clear(); + + if (kex_round == 1) + { + if (version == 2) + msg_out.append(MULTISIG_KEX_MSG_V2_MAGIC_1.data(), MULTISIG_KEX_MSG_V2_MAGIC_1.size()); + else if (version == 3) + msg_out.append(MULTISIG_KEX_MSG_V3_MAGIC_1.data(), MULTISIG_KEX_MSG_V3_MAGIC_1.size()); + } + else + { + if (version == 2) + msg_out.append(MULTISIG_KEX_MSG_V2_MAGIC_N.data(), MULTISIG_KEX_MSG_V2_MAGIC_N.size()); + else if (version == 3) + msg_out.append(MULTISIG_KEX_MSG_V3_MAGIC_N.data(), MULTISIG_KEX_MSG_V3_MAGIC_N.size()); + } + } + //---------------------------------------------------------------------------------------------------------------------- + // INTERNAL + //---------------------------------------------------------------------------------------------------------------------- + static std::uint32_t get_message_version(const std::string &original_msg) + { + CHECK_AND_ASSERT_THROW_MES(original_msg.substr(0, MULTISIG_KEX_V1_MAGIC.size()) != MULTISIG_KEX_V1_MAGIC, + "V1 multisig kex messages are deprecated (unsafe)."); + CHECK_AND_ASSERT_THROW_MES(original_msg.substr(0, MULTISIG_KEX_MSG_V1_MAGIC.size()) != MULTISIG_KEX_MSG_V1_MAGIC, + "V1 multisig kex messages are deprecated (unsafe)."); + + if (original_msg.substr(0, MULTISIG_KEX_MSG_V2_MAGIC_1.size()) == MULTISIG_KEX_MSG_V2_MAGIC_1 || + original_msg.substr(0, MULTISIG_KEX_MSG_V2_MAGIC_N.size()) == MULTISIG_KEX_MSG_V2_MAGIC_N) + return 2; + else if (original_msg.substr(0, MULTISIG_KEX_MSG_V3_MAGIC_1.size()) == MULTISIG_KEX_MSG_V3_MAGIC_1 || + original_msg.substr(0, MULTISIG_KEX_MSG_V3_MAGIC_N.size()) == MULTISIG_KEX_MSG_V3_MAGIC_N) + return 3; + + return 0; + } + //---------------------------------------------------------------------------------------------------------------------- + // INTERNAL + //---------------------------------------------------------------------------------------------------------------------- + static bool try_get_message_no_magic(const std::string &original_msg, + const boost::string_ref &magic, + std::string &msg_no_magic_out) + { + // abort if magic doesn't match the message + if (original_msg.substr(0, magic.size()) != magic) + return false; + + // decode message + CHECK_AND_ASSERT_THROW_MES(tools::base58::decode(original_msg.substr(magic.size()), msg_no_magic_out), + "Multisig kex msg decoding error."); + + return true; + } + //---------------------------------------------------------------------------------------------------------------------- + // INTERNAL + //---------------------------------------------------------------------------------------------------------------------- + static bool try_get_message_no_magic_round1(const std::string &original_msg, std::string &msg_no_magic_out) + { + return try_get_message_no_magic(original_msg, MULTISIG_KEX_MSG_V2_MAGIC_1, msg_no_magic_out) || + try_get_message_no_magic(original_msg, MULTISIG_KEX_MSG_V3_MAGIC_1, msg_no_magic_out); + } + //---------------------------------------------------------------------------------------------------------------------- + // INTERNAL + //---------------------------------------------------------------------------------------------------------------------- + static bool try_get_message_no_magic_roundN(const std::string &original_msg, std::string &msg_no_magic_out) + { + return try_get_message_no_magic(original_msg, MULTISIG_KEX_MSG_V2_MAGIC_N, msg_no_magic_out) || + try_get_message_no_magic(original_msg, MULTISIG_KEX_MSG_V3_MAGIC_N, msg_no_magic_out); + } //---------------------------------------------------------------------------------------------------------------------- // multisig_kex_msg: EXTERNAL //---------------------------------------------------------------------------------------------------------------------- - multisig_kex_msg::multisig_kex_msg(const std::uint32_t round, + multisig_kex_msg::multisig_kex_msg(const std::uint32_t version, + const std::uint32_t round, const crypto::secret_key &signing_privkey, std::vector msg_pubkeys, const crypto::secret_key &msg_privkey) : + m_version{version}, m_kex_round{round} { + CHECK_AND_ASSERT_THROW_MES(version >= 2 && version <= 3, "Invalid kex message version."); CHECK_AND_ASSERT_THROW_MES(round > 0, "Kex round must be > 0."); CHECK_AND_ASSERT_THROW_MES(sc_check((const unsigned char*)&signing_privkey) == 0 && signing_privkey != crypto::null_skey, "Invalid msg signing key."); + // round 1 special case if (round == 1) { CHECK_AND_ASSERT_THROW_MES(sc_check((const unsigned char*)&msg_privkey) == 0 && msg_privkey != crypto::null_skey, "Invalid msg privkey."); + CHECK_AND_ASSERT_THROW_MES(msg_pubkeys.size() == 1, "In round 1, expect one msg pubkey."); m_msg_privkey = msg_privkey; } - else - { - for (const auto &pubkey : msg_pubkeys) - { - CHECK_AND_ASSERT_THROW_MES(pubkey != crypto::null_pkey && pubkey != rct::rct2pk(rct::identity()), - "Pubkey for message was invalid."); - CHECK_AND_ASSERT_THROW_MES((rct::scalarmultKey(rct::pk2rct(pubkey), rct::curveOrder()) == rct::identity()), - "Pubkey for message was not in prime subgroup."); - } - m_msg_pubkeys = std::move(msg_pubkeys); + // check and save pubkeys + for (const auto &pubkey : msg_pubkeys) + { + CHECK_AND_ASSERT_THROW_MES(pubkey != crypto::null_pkey && pubkey != rct::rct2pk(rct::identity()), + "Pubkey for message was invalid."); + CHECK_AND_ASSERT_THROW_MES((rct::scalarmultKey(rct::pk2rct(pubkey), rct::curveOrder()) == rct::identity()), + "Pubkey for message was not in prime subgroup."); } + + m_msg_pubkeys = std::move(msg_pubkeys); + CHECK_AND_ASSERT_THROW_MES(crypto::secret_key_to_public_key(signing_privkey, m_signing_pubkey), "Failed to derive public key"); @@ -109,38 +194,36 @@ namespace multisig crypto::hash multisig_kex_msg::get_msg_to_sign() const { //// - // msg_content = kex_round | signing_pubkey | expand(msg_pubkeys) | OPTIONAL msg_privkey - // sign_msg = versioning-domain-sep | msg_content + // msg_content = kex_round || signing_pubkey || expand(msg_pubkeys) || OPTIONAL msg_privkey + // sign_msg = versioning-domain-sep || msg_content /// std::string data; - CHECK_AND_ASSERT_THROW_MES(MULTISIG_KEX_MSG_V2_MAGIC_1.size() == MULTISIG_KEX_MSG_V2_MAGIC_N.size(), - "Multisig kex msg magic inconsistency."); data.reserve(MULTISIG_KEX_MSG_V2_MAGIC_1.size() + 4 + 32*(1 + (m_kex_round == 1 ? 1 : 0) + m_msg_pubkeys.size())); // versioning domain-sep - if (m_kex_round == 1) - data.append(MULTISIG_KEX_MSG_V2_MAGIC_1.data(), MULTISIG_KEX_MSG_V2_MAGIC_1.size()); - else - data.append(MULTISIG_KEX_MSG_V2_MAGIC_N.data(), MULTISIG_KEX_MSG_V2_MAGIC_N.size()); + set_msg_magic(m_version, m_kex_round, data); // kex_round as little-endian bytes for (std::size_t i{0}; i < 4; ++i) - { data += static_cast(m_kex_round >> i*8); - } // signing pubkey data.append((const char *)&m_signing_pubkey, sizeof(crypto::public_key)); // add msg privkey if kex_round == 1 if (m_kex_round == 1) + { + data.append((const char *)&m_msg_privkey, sizeof(crypto::secret_key)); data.append((const char *)&m_msg_privkey, sizeof(crypto::secret_key)); + } + + // msg pubkeys + // - legacy case: don't include msg pubkeys in msg signed for round 1 of v2 + if (m_version == 2 && m_kex_round == 1) + {} else { - // only add pubkeys if not round 1 - - // msg pubkeys for (const auto &key : m_msg_pubkeys) data.append((const char *)&key, sizeof(crypto::public_key)); } @@ -157,28 +240,36 @@ namespace multisig void multisig_kex_msg::construct_msg(const crypto::secret_key &signing_privkey) { //// - // msg_content = kex_round | signing_pubkey | expand(msg_pubkeys) | OPTIONAL msg_privkey - // sign_msg = versioning-domain-sep | msg_content - // msg = versioning-domain-sep | serialize(msg_content | crypto_sig[signing_privkey](sign_msg)) + // msg_content = kex_round || signing_pubkey || expand(msg_pubkeys) || OPTIONAL msg_privkey + // sign_msg = versioning-domain-sep || msg_content + // msg = versioning-domain-sep || serialize(msg_content || crypto_sig[signing_privkey](sign_msg)) /// // sign the message crypto::signature msg_signature; - crypto::hash msg_to_sign{get_msg_to_sign()}; - crypto::generate_signature(msg_to_sign, m_signing_pubkey, signing_privkey, msg_signature); - - // assemble the message - m_msg.clear(); + crypto::generate_signature(get_msg_to_sign(), m_signing_pubkey, signing_privkey, msg_signature); + // prepare the message std::stringstream serialized_msg_ss; binary_archive b_archive(serialized_msg_ss); - if (m_kex_round == 1) + if (m_kex_round == 1 && m_version == 2) + { + multisig_kex_msg_serializable_round1_legacy msg_serializable; + msg_serializable.msg_privkey = m_msg_privkey; + msg_serializable.signing_pubkey = m_signing_pubkey; + msg_serializable.signature = msg_signature; + + CHECK_AND_ASSERT_THROW_MES(::serialization::serialize(b_archive, msg_serializable), + "Failed to serialize multisig kex msg"); + } + else if (m_kex_round == 1 && m_version > 2) { - m_msg.append(MULTISIG_KEX_MSG_V2_MAGIC_1.data(), MULTISIG_KEX_MSG_V2_MAGIC_1.size()); + CHECK_AND_ASSERT_THROW_MES(m_msg_pubkeys.size() == 1, "Unexpected number of msg pubkeys in first kex round."); multisig_kex_msg_serializable_round1 msg_serializable; msg_serializable.msg_privkey = m_msg_privkey; + msg_serializable.msg_pubkey = m_msg_pubkeys[0]; msg_serializable.signing_pubkey = m_signing_pubkey; msg_serializable.signature = msg_signature; @@ -187,8 +278,6 @@ namespace multisig } else { - m_msg.append(MULTISIG_KEX_MSG_V2_MAGIC_N.data(), MULTISIG_KEX_MSG_V2_MAGIC_N.size()); - multisig_kex_msg_serializable_general msg_serializable; msg_serializable.kex_round = m_kex_round; msg_serializable.msg_pubkeys = m_msg_pubkeys; @@ -199,6 +288,8 @@ namespace multisig "Failed to serialize multisig kex msg"); } + // make the message + set_msg_magic(m_version, m_kex_round, m_msg); m_msg.append(tools::base58::encode(serialized_msg_ss.str())); } //---------------------------------------------------------------------------------------------------------------------- @@ -206,47 +297,74 @@ namespace multisig //---------------------------------------------------------------------------------------------------------------------- void multisig_kex_msg::parse_and_validate_msg() { - // check message type - CHECK_AND_ASSERT_THROW_MES(m_msg.size() > 0, "Kex message unexpectedly empty."); - CHECK_AND_ASSERT_THROW_MES(m_msg.substr(0, MULTISIG_KEX_V1_MAGIC.size()) != MULTISIG_KEX_V1_MAGIC, - "V1 multisig kex messages are deprecated (unsafe)."); - CHECK_AND_ASSERT_THROW_MES(m_msg.substr(0, MULTISIG_KEX_MSG_V1_MAGIC.size()) != MULTISIG_KEX_MSG_V1_MAGIC, - "V1 multisig kex messages are deprecated (unsafe)."); + // early return on empty messages + if (m_msg == "") + return; + + // set message version + m_version = get_message_version(m_msg); + CHECK_AND_ASSERT_THROW_MES(m_version != 0, "Kex message is invalid."); // deserialize the message std::string msg_no_magic; - CHECK_AND_ASSERT_THROW_MES(MULTISIG_KEX_MSG_V2_MAGIC_1.size() == MULTISIG_KEX_MSG_V2_MAGIC_N.size(), - "Multisig kex msg magic inconsistency."); - CHECK_AND_ASSERT_THROW_MES(tools::base58::decode(m_msg.substr(MULTISIG_KEX_MSG_V2_MAGIC_1.size()), msg_no_magic), - "Multisig kex msg decoding error."); - binary_archive b_archive{epee::strspan(msg_no_magic)}; + + bool round1{false}; + bool roundN{false}; + if (try_get_message_no_magic_round1(m_msg, msg_no_magic)) + round1 = true; + else if (try_get_message_no_magic_roundN(m_msg, msg_no_magic)) + roundN = true; + CHECK_AND_ASSERT_THROW_MES(round1 || roundN, "Could not remove magic from kex message."); + + binary_archive archived_msg{epee::strspan(msg_no_magic)}; + + // extract data from the message crypto::signature msg_signature; - if (m_msg.substr(0, MULTISIG_KEX_MSG_V2_MAGIC_1.size()) == MULTISIG_KEX_MSG_V2_MAGIC_1) + if (round1) { - // try round 1 message - multisig_kex_msg_serializable_round1 kex_msg_rnd1; - - if (::serialization::serialize(b_archive, kex_msg_rnd1)) + // round 1 message + if (m_version == 2) { - // in round 1 the message stores a private ancillary key component for the multisig account - // that will be shared by all participants (e.g. a shared private view key) - m_kex_round = 1; - m_msg_privkey = kex_msg_rnd1.msg_privkey; - m_signing_pubkey = kex_msg_rnd1.signing_pubkey; - msg_signature = kex_msg_rnd1.signature; + // round 1: legacy + multisig_kex_msg_serializable_round1_legacy kex_msg_rnd1; + + if (::serialization::serialize(archived_msg, kex_msg_rnd1)) + { + // in round 1 the message stores a private ancillary key component for the multisig account + // that will be shared by all participants (e.g. a shared private view key) + m_kex_round = 1; + m_msg_privkey = kex_msg_rnd1.msg_privkey; + m_msg_pubkeys = {kex_msg_rnd1.signing_pubkey}; + m_signing_pubkey = kex_msg_rnd1.signing_pubkey; + msg_signature = kex_msg_rnd1.signature; + } + else CHECK_AND_ASSERT_THROW_MES(false, "Deserializing kex msg failed."); } else { - CHECK_AND_ASSERT_THROW_MES(false, "Deserializing kex msg failed."); + // round 1: current + multisig_kex_msg_serializable_round1 kex_msg_rnd1; + + if (::serialization::serialize(archived_msg, kex_msg_rnd1)) + { + // in round 1 the message stores a private ancillary key component for the multisig account + // that will be shared by all participants (e.g. a shared private view key) + m_kex_round = 1; + m_msg_privkey = kex_msg_rnd1.msg_privkey; + m_msg_pubkeys = {kex_msg_rnd1.msg_pubkey}; + m_signing_pubkey = kex_msg_rnd1.signing_pubkey; + msg_signature = kex_msg_rnd1.signature; + } + else CHECK_AND_ASSERT_THROW_MES(false, "Deserializing kex msg failed."); } } - else if (m_msg.substr(0, MULTISIG_KEX_MSG_V2_MAGIC_N.size()) == MULTISIG_KEX_MSG_V2_MAGIC_N) + else if (roundN) { - // try general message + // round >1 message multisig_kex_msg_serializable_general kex_msg_general; - if (::serialization::serialize(b_archive, kex_msg_general)) + if (::serialization::serialize(archived_msg, kex_msg_general)) { m_kex_round = kex_msg_general.kex_round; m_msg_privkey = crypto::null_skey; @@ -256,15 +374,7 @@ namespace multisig CHECK_AND_ASSERT_THROW_MES(m_kex_round > 1, "Invalid kex message round (must be > 1 for the general msg type)."); } - else - { - CHECK_AND_ASSERT_THROW_MES(false, "Deserializing kex msg failed."); - } - } - else - { - // unknown message type - CHECK_AND_ASSERT_THROW_MES(false, "Only v2 multisig kex messages are supported."); + else CHECK_AND_ASSERT_THROW_MES(false, "Deserializing kex msg failed."); } // checks @@ -287,4 +397,29 @@ namespace multisig "Multisig kex msg signature invalid."); } //---------------------------------------------------------------------------------------------------------------------- + // EXTERNAL + //---------------------------------------------------------------------------------------------------------------------- + std::uint32_t get_kex_msg_version(const cryptonote::account_generator_era era) + { + if (era == cryptonote::account_generator_era::cryptonote) + return 2; + else if (era == cryptonote::account_generator_era::seraphis) + return 3; + else + return 0; //error + } + //---------------------------------------------------------------------------------------------------------------------- + // EXTERNAL + //---------------------------------------------------------------------------------------------------------------------- + bool check_kex_msg_versions(const std::vector &messages, const std::uint32_t expected_version) + { + for (const multisig_kex_msg &msg : messages) + { + if (msg.get_version() != expected_version) + return false; + } + + return true; + } + //---------------------------------------------------------------------------------------------------------------------- } //namespace multisig diff --git a/src/multisig/multisig_kex_msg.h b/src/multisig/multisig_kex_msg.h index 8cb8faa7ce..d60a90701b 100644 --- a/src/multisig/multisig_kex_msg.h +++ b/src/multisig/multisig_kex_msg.h @@ -29,8 +29,10 @@ #pragma once #include "crypto/crypto.h" +#include "cryptonote_basic/account_generators.h" #include +#include #include @@ -40,24 +42,24 @@ namespace multisig // multisig key exchange message // - can parse and validate an input message // - can construct and sign a new message + // - INVARIANT: message pubkeys are in the prime subgroup // - // msg_content = kex_round | signing_pubkey | expand(msg_pubkeys) | OPTIONAL msg_privkey - // msg_to_sign = versioning-domain-sep | msg_content - // msg = versioning-domain-sep | b58(msg_content | crypto_sig[signing_privkey](msg_to_sign)) + // msg_content = kex_round || signing_pubkey || expand(msg_pubkeys) || OPTIONAL msg_privkey + // msg_to_sign = versioning-domain-sep || msg_content + // msg = versioning-domain-sep || b58(msg_content || crypto_sig[signing_privkey](msg_to_sign)) // // note: round 1 messages will contain a private key (e.g. for the aggregate multisig private view key) /// class multisig_kex_msg final { - //member types: none - //constructors public: // default constructor multisig_kex_msg() = default; // construct from info - multisig_kex_msg(const std::uint32_t round, + multisig_kex_msg(const std::uint32_t version, + const std::uint32_t round, const crypto::secret_key &signing_privkey, std::vector msg_pubkeys, const crypto::secret_key &msg_privkey = crypto::null_skey); @@ -83,11 +85,14 @@ namespace multisig const crypto::secret_key& get_msg_privkey() const { return m_msg_privkey; } // get msg signing pubkey const crypto::public_key& get_signing_pubkey() const { return m_signing_pubkey; } + // get msg version + std::uint32_t get_version() const { return m_version; } private: - // msg_to_sign = versioning-domain-sep | kex_round | signing_pubkey | expand(msg_pubkeys) | OPTIONAL msg_privkey + // msg_to_sign = versioning-domain-sep || kex_round || signing_pubkey || expand(msg_pubkeys) || OPTIONAL msg_privkey + // - signed by the signing pubkey crypto::hash get_msg_to_sign() const; - // set: msg string based on msg contents, signing pubkey based on input privkey + // set: msg string based on msg contents, signing pubkey defined from input privkey void construct_msg(const crypto::secret_key &signing_privkey); // parse msg string into parts, validate contents and signature void parse_and_validate_msg(); @@ -96,6 +101,8 @@ namespace multisig private: // message as string std::string m_msg; + // kex message version + std::uint32_t m_version; // key exchange round this msg was produced for std::uint32_t m_kex_round; @@ -103,7 +110,15 @@ namespace multisig std::vector m_msg_pubkeys; // privkey stored in msg (if kex round 1) crypto::secret_key m_msg_privkey; + // pubkey used to sign this msg crypto::public_key m_signing_pubkey; }; + + //todo + std::uint32_t get_kex_msg_version(const cryptonote::account_generator_era era); + + //todo + bool check_kex_msg_versions(const std::vector &messages, const std::uint32_t expected_version); + } //namespace multisig diff --git a/src/multisig/multisig_mocks.cpp b/src/multisig/multisig_mocks.cpp new file mode 100644 index 0000000000..f7c01ef704 --- /dev/null +++ b/src/multisig/multisig_mocks.cpp @@ -0,0 +1,185 @@ +// Copyright (c) 2022, 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 "multisig_mocks.h" + +//local headers +#include "crypto/crypto.h" +#include "cryptonote_basic/account_generators.h" +#include "misc_log_ex.h" +#include "multisig.h" +#include "multisig_account.h" +#include "multisig_kex_msg.h" +#include "multisig_partial_cn_key_image_msg.h" +#include "multisig_signer_set_filter.h" +#include "ringct/rctOps.h" +#include "ringct/rctTypes.h" + +//third party headers + +//standard headers +#include +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "multisig" + +namespace multisig +{ +namespace mocks +{ +//------------------------------------------------------------------------------------------------------------------- +void make_multisig_mock_accounts(const cryptonote::account_generator_era account_era, + const std::uint32_t threshold, + const std::uint32_t num_signers, + std::vector &accounts_out) +{ + std::vector signers; + std::vector current_round_msgs; + std::vector next_round_msgs; + accounts_out.clear(); + accounts_out.reserve(num_signers); + signers.reserve(num_signers); + next_round_msgs.reserve(accounts_out.size()); + + // create multisig accounts for each signer + for (std::size_t account_index{0}; account_index < num_signers; ++account_index) + { + // create account [[ROUND 0]] + accounts_out.emplace_back(account_era, rct::rct2sk(rct::skGen()), rct::rct2sk(rct::skGen())); + + // collect signer + signers.emplace_back(accounts_out.back().get_base_pubkey()); + + // collect account's first kex msg + next_round_msgs.emplace_back(accounts_out.back().get_next_kex_round_msg()); + } + + // perform key exchange rounds until the accounts are ready + while (accounts_out.size() && !accounts_out[0].multisig_is_ready()) + { + current_round_msgs = std::move(next_round_msgs); + next_round_msgs.clear(); + next_round_msgs.reserve(accounts_out.size()); + + for (multisig_account &account : accounts_out) + { + // initialize or update account + if (!account.account_is_active()) + account.initialize_kex(threshold, signers, current_round_msgs); //[[ROUND 1]] + else + account.kex_update(current_round_msgs); //[[ROUND 2+]] + + next_round_msgs.emplace_back(account.get_next_kex_round_msg()); + } + } +} +//------------------------------------------------------------------------------------------------------------------- +void mock_convert_multisig_accounts(const cryptonote::account_generator_era new_era, + std::vector &accounts_inout) +{ + if (accounts_inout.size() == 0 || new_era == accounts_inout[0].get_era()) + return; + + // collect messages + std::vector conversion_msgs; + conversion_msgs.reserve(accounts_inout.size()); + + for (const multisig_account &account : accounts_inout) + conversion_msgs.emplace_back(account.get_account_era_conversion_msg(new_era)); + + // convert accounts to 'new_era' + for (multisig_account &account : accounts_inout) + get_multisig_account_with_new_generator_era(account, new_era, conversion_msgs, account); +} +//------------------------------------------------------------------------------------------------------------------- +void mock_multisig_cn_key_image_recovery(const std::vector &accounts, + //[ base key for key image : shared offset privkey material in base key ] + const std::unordered_map &saved_key_components, + std::unordered_map &recovered_key_images_out) +{ + // 1. prepare partial key image messages for the key image base keys from all multisig group members + std::unordered_map> partial_ki_msgs; + + for (const multisig_account &account : accounts) + { + CHECK_AND_ASSERT_THROW_MES(account.get_era() == cryptonote::account_generator_era::cryptonote, + "mock multisig cn key image recovery: account has unexpected account era."); + + for (const auto &saved_keys : saved_key_components) + { + partial_ki_msgs[saved_keys.first][account.get_base_pubkey()] = + multisig_partial_cn_key_image_msg{ + account.get_base_privkey(), + saved_keys.first, + account.get_multisig_privkeys() + }; + } + } + + // 2. process the messages + std::unordered_map onetime_addresses_with_insufficient_partial_kis; + std::unordered_map onetime_addresses_with_invalid_partial_kis; + std::unordered_map recovered_key_image_cores; + + multisig_recover_cn_keyimage_cores(accounts[0].get_threshold(), + accounts[0].get_signers(), + accounts[0].get_multisig_pubkey(), + partial_ki_msgs, + onetime_addresses_with_insufficient_partial_kis, + onetime_addresses_with_invalid_partial_kis, + recovered_key_image_cores); + + CHECK_AND_ASSERT_THROW_MES(onetime_addresses_with_insufficient_partial_kis.size() == 0, + "mock multisig cn key image recovery: failed to make partial kis for some onetime addresses."); + CHECK_AND_ASSERT_THROW_MES(onetime_addresses_with_invalid_partial_kis.size() == 0, + "mock multisig cn key image recovery: failed to make partial kis for some onetime addresses."); + + // 3. add the shared offset component to each key image core + for (const auto &recovered_key_image_core : recovered_key_image_cores) + { + CHECK_AND_ASSERT_THROW_MES(saved_key_components.find(recovered_key_image_core.first) != + saved_key_components.end(), + "mock multisig cn key image recovery: did not produce an expected key image core."); + + // KI_shared_piece = shared_offset * Hp(base key) + crypto::key_image KI_shared_piece; + crypto::generate_key_image(recovered_key_image_core.first, + saved_key_components.at(recovered_key_image_core.first), + KI_shared_piece); + + // KI = shared_offset * Hp(base key) + k_multisig * Hp(base key) + recovered_key_images_out[recovered_key_image_core.first] = + rct::rct2ki(rct::addKeys(rct::ki2rct(KI_shared_piece), rct::pk2rct(recovered_key_image_core.second))); + } +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace mocks +} //namespace multisig diff --git a/src/multisig/multisig_mocks.h b/src/multisig/multisig_mocks.h new file mode 100644 index 0000000000..4e934cb2ac --- /dev/null +++ b/src/multisig/multisig_mocks.h @@ -0,0 +1,83 @@ +// Copyright (c) 2022, 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. + +// Mockups for multisig unit tests. + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "cryptonote_basic/account_generators.h" +#include "multisig_account.h" +#include "ringct/rctTypes.h" + +//third party headers + +//standard headers +#include +#include + +//forward declarations + + +namespace multisig +{ +namespace mocks +{ + +/** +* brief: make_multisig_mock_accounts - make accounts for a mock multisig group +* param: account_era - account era +* param: threshold - M +* param: num_signers - N +* outparam: accounts_out - mock accounts +*/ +void make_multisig_mock_accounts(const cryptonote::account_generator_era account_era, + const std::uint32_t threshold, + const std::uint32_t num_signers, + std::vector &accounts_out); +/** +* brief: mock_convert_multisig_accounts - convert multisig accounts to a new account era +* param: new_era - account era to convert to +* inoutparam: accounts_inout - accounts to convert +*/ +void mock_convert_multisig_accounts(const cryptonote::account_generator_era new_era, + std::vector &accounts_inout); +/** +* brief: mock_multisig_cn_key_image_recovery - perform multisig cryptonote key image recovery for a set of keys +* param: accounts - multisig group accounts +* param: saved_key_components - [ base key for key image : shared offset privkey material in base key ] +* outparam: recovered_key_images_out - the recovered key images mapped to their corresponding original keys +*/ +void mock_multisig_cn_key_image_recovery(const std::vector &accounts, + // [ base key for key image : shared offset privkey material in base key ] + const std::unordered_map &saved_key_components, + std::unordered_map &recovered_key_images_out); + +} //namespace mocks +} //namespace multisig diff --git a/src/multisig/multisig_msg_serialization.h b/src/multisig/multisig_msg_serialization.h new file mode 100644 index 0000000000..7662b6f265 --- /dev/null +++ b/src/multisig/multisig_msg_serialization.h @@ -0,0 +1,157 @@ +// Copyright (c) 2021-2022, 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 "crypto/crypto.h" +#include "cryptonote_basic/account_generators.h" +#include "serialization/containers.h" +#include "serialization/crypto.h" +#include "serialization/serialization.h" + +#include +#include + + +namespace multisig +{ + //// + // round 1 kex message + // - legacy: use signing_pubkey as a msg_pubkey directly + /// + struct multisig_kex_msg_serializable_round1_legacy final + { + // privkey stored in msg + crypto::secret_key msg_privkey; + // pubkey used to sign this msg + crypto::public_key signing_pubkey; + // message signature + crypto::signature signature; + + BEGIN_SERIALIZE() + FIELD(msg_privkey) + FIELD(signing_pubkey) + FIELD(signature) + END_SERIALIZE() + }; + + /// round 1 kex message + struct multisig_kex_msg_serializable_round1 final + { + // privkey stored in msg + crypto::secret_key msg_privkey; + // pubkey stored in msg + crypto::public_key msg_pubkey; + // pubkey used to sign this msg + crypto::public_key signing_pubkey; + // message signature + crypto::signature signature; + + BEGIN_SERIALIZE() + FIELD(msg_privkey) + FIELD(msg_pubkey) + FIELD(signing_pubkey) + FIELD(signature) + END_SERIALIZE() + }; + + /// general kex message (if round > 1) + struct multisig_kex_msg_serializable_general final + { + // key exchange round this msg was produced for + std::uint32_t kex_round; + // pubkeys stored in msg + std::vector msg_pubkeys; + // pubkey used to sign this msg + crypto::public_key signing_pubkey; + // message signature + crypto::signature signature; + + BEGIN_SERIALIZE() + VARINT_FIELD(kex_round) + FIELD(msg_pubkeys) + FIELD(signing_pubkey) + FIELD(signature) + END_SERIALIZE() + }; + + /// multisig partial cryptonote key image message + struct multisig_partial_cn_ki_msg_serializable final + { + // onetime address + crypto::public_key onetime_address; + // multisig keyshares + std::vector multisig_keyshares; + // partial keyimage shares + std::vector partial_key_images; + // pubkey used to sign this msg + crypto::public_key signing_pubkey; + // matrix proof (challenge/response shoved into crypto::signature structure) + crypto::signature matrix_proof_partial; + // message signature + crypto::signature signature; + + BEGIN_SERIALIZE() + FIELD(onetime_address) + FIELD(multisig_keyshares) + FIELD(partial_key_images) + FIELD(signing_pubkey) + FIELD(matrix_proof_partial) + FIELD(signature) + END_SERIALIZE() + }; + + /// multisig account era conversion message + struct multisig_conversion_msg_serializable final + { + // old era + cryptonote::account_generator_era old_era; + // new era + cryptonote::account_generator_era new_era; + // old keyshares + std::vector old_keyshares; + // new keyshares + std::vector new_keyshares; + // pubkey used to sign this msg + crypto::public_key signing_pubkey; + // matrix proof (challenge/response shoved into crypto::signature structure) + crypto::signature matrix_proof_partial; + // message signature + crypto::signature signature; + + BEGIN_SERIALIZE() + VARINT_FIELD(old_era) + VARINT_FIELD(new_era) + FIELD(old_keyshares) + FIELD(new_keyshares) + FIELD(signing_pubkey) + FIELD(matrix_proof_partial) + FIELD(signature) + END_SERIALIZE() + }; +} //namespace multisig diff --git a/src/multisig/multisig_nonce_cache.cpp b/src/multisig/multisig_nonce_cache.cpp new file mode 100644 index 0000000000..a27464f128 --- /dev/null +++ b/src/multisig/multisig_nonce_cache.cpp @@ -0,0 +1,211 @@ +// Copyright (c) 2022, 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 "multisig_nonce_cache.h" + +//local headers +#include "common/container_helpers.h" +#include "crypto/crypto.h" +#include "misc_log_ex.h" +#include "multisig_signer_set_filter.h" +#include "ringct/rctOps.h" +#include "ringct/rctTypes.h" +#include "seraphis_crypto/sp_crypto_utils.h" +#include "seraphis_crypto/sp_transcript.h" + +//third party headers +#include + +//standard headers +#include +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "multisig" + +namespace multisig +{ +//------------------------------------------------------------------------------------------------------------------- +bool operator<(const MultisigPubNonces &a, const MultisigPubNonces &b) +{ + // sort by nonce pubkey 1 then nonce pubkey 2 if pubkey 1 is equal + const int nonce_1_comparison{ + memcmp(a.signature_nonce_1_pub.bytes, &b.signature_nonce_1_pub.bytes, sizeof(rct::key)) + }; + + if (nonce_1_comparison < 0) + return true; + + if (nonce_1_comparison == 0 && + memcmp(a.signature_nonce_2_pub.bytes, &b.signature_nonce_2_pub.bytes, sizeof(rct::key)) < 0) + return true; + + return false; +} +//------------------------------------------------------------------------------------------------------------------- +bool operator==(const MultisigPubNonces &a, const MultisigPubNonces &b) +{ + return !(a < b) && !(b < a); +} +//------------------------------------------------------------------------------------------------------------------- +void append_to_transcript(const MultisigPubNonces &container, sp::SpTranscriptBuilder &transcript_inout) +{ + transcript_inout.append("nonce1", container.signature_nonce_1_pub); + transcript_inout.append("nonce2", container.signature_nonce_2_pub); +} +//------------------------------------------------------------------------------------------------------------------- +MultisigNonceCache::MultisigNonceCache(const std::vector< + std::tuple + > &raw_nonce_data) +{ + for (const auto &signature_attempt : raw_nonce_data) + { + // note: ignore failures + this->try_add_nonces_impl( + std::get<0>(signature_attempt), + std::get<1>(signature_attempt), + std::get<2>(signature_attempt), + std::get<3>(signature_attempt) + ); + } +} +//------------------------------------------------------------------------------------------------------------------- +bool MultisigNonceCache::has_record(const rct::key &message, + const rct::key &proof_key, + const signer_set_filter &filter) const +{ + return m_cache.find(message) != m_cache.end() && + m_cache.at(message).find(proof_key) != m_cache.at(message).end() && + m_cache.at(message).at(proof_key).find(filter) != m_cache.at(message).at(proof_key).end(); +} +//------------------------------------------------------------------------------------------------------------------- +bool MultisigNonceCache::try_add_nonces(const rct::key &message, + const rct::key &proof_key, + const signer_set_filter &filter) +{ + if (!this->try_add_nonces_impl(message, + proof_key, + filter, + MultisigNonces{rct::rct2sk(rct::skGen()), rct::rct2sk(rct::skGen())})) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool MultisigNonceCache::try_get_nonce_pubkeys_for_base(const rct::key &message, + const rct::key &proof_key, + const signer_set_filter &filter, + const rct::key &pubkey_base, + MultisigPubNonces &nonce_pubkeys_out) const +{ + CHECK_AND_ASSERT_THROW_MES(sp::key_domain_is_prime_subgroup(pubkey_base) && !(pubkey_base == rct::identity()), + "multisig nonce record get nonce pubkeys: pubkey base is invalid."); + + if (!this->has_record(message, proof_key, filter)) + return false; + + const MultisigNonces &nonces{m_cache.at(message).at(proof_key).at(filter)}; + + // pubkeys (store with (1/8)) + nonce_pubkeys_out.signature_nonce_1_pub = + rct::scalarmultKey(rct::scalarmultKey(pubkey_base, rct::sk2rct(nonces.signature_nonce_1_priv)), rct::INV_EIGHT); + nonce_pubkeys_out.signature_nonce_2_pub = + rct::scalarmultKey(rct::scalarmultKey(pubkey_base, rct::sk2rct(nonces.signature_nonce_2_priv)), rct::INV_EIGHT); + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool MultisigNonceCache::try_get_recorded_nonce_privkeys(const rct::key &message, + const rct::key &proof_key, + const signer_set_filter &filter, + crypto::secret_key &nonce_privkey_1_out, + crypto::secret_key &nonce_privkey_2_out) const +{ + if (!this->has_record(message, proof_key, filter)) + return false; + + // privkeys + nonce_privkey_1_out = m_cache.at(message).at(proof_key).at(filter).signature_nonce_1_priv; + nonce_privkey_2_out = m_cache.at(message).at(proof_key).at(filter).signature_nonce_2_priv; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool MultisigNonceCache::try_remove_record(const rct::key &message, + const rct::key &proof_key, + const signer_set_filter &filter) +{ + if (!this->has_record(message, proof_key, filter)) + return false; + + // cleanup + m_cache[message][proof_key].erase(filter); + if (m_cache[message][proof_key].empty()) + m_cache[message].erase(proof_key); + if (m_cache[message].empty()) + m_cache.erase(message); + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +std::vector> MultisigNonceCache::export_data() const +{ + // flatten the record and return it + std::vector> raw_data; + + for (const auto &message_map : m_cache) + { + for (const auto &key_map : message_map.second) + { + for (const auto &filter_map : key_map.second) + raw_data.emplace_back(message_map.first, key_map.first, filter_map.first, filter_map.second); + } + } + + return raw_data; +} +//------------------------------------------------------------------------------------------------------------------- +bool MultisigNonceCache::try_add_nonces_impl(const rct::key &message, + const rct::key &proof_key, + const signer_set_filter &filter, + const MultisigNonces &nonces) +{ + if (this->has_record(message, proof_key, filter)) + return false; + + if (!sp::key_domain_is_prime_subgroup(proof_key)) + return false; + + // add record + m_cache[message][proof_key][filter] = nonces; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace multisig diff --git a/src/multisig/multisig_nonce_cache.h b/src/multisig/multisig_nonce_cache.h new file mode 100644 index 0000000000..56f9dd0ef6 --- /dev/null +++ b/src/multisig/multisig_nonce_cache.h @@ -0,0 +1,164 @@ +// Copyright (c) 2022, 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. + +// Caches Musig2-style nonces for multisig signing. + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "multisig_signer_set_filter.h" +#include "ringct/rctTypes.h" +#include "seraphis_crypto/sp_transcript.h" + +//third party headers +#include + +//standard headers +#include +#include +#include + +//forward declarations +namespace sp { class SpTranscriptBuilder; } + +namespace multisig +{ + +//// +// Multisig public nonces +// - store multisig participant's MuSig2-style signature opening nonces for an arbitrary base point J +// - IMPORTANT: these are stored *(1/8) so another person can efficiently mul8 and be confident the result is canonical +// +// WARNINGS: +// - must only use nonces to make ONE 'partial signature', after that the opening nonce privkeys should be deleted +// immediately +// - the nonce privkeys are for local storage, only the pubkeys should be transmitted to other multisig participants +// - the user is expected to maintain consistency between the J used to define nonce pubkeys and the J used when signing +/// +struct MultisigPubNonces final +{ + // signature nonce pubkey: (1/8) * alpha_{1,e}*J + rct::key signature_nonce_1_pub; + // signature nonce pubkey: (1/8) * alpha_{2,e}*J + rct::key signature_nonce_2_pub; +}; +inline const boost::string_ref container_name(const MultisigPubNonces&) { return "MultisigPubNonces"; } +void append_to_transcript(const MultisigPubNonces &container, sp::SpTranscriptBuilder &transcript_inout); + +/// overload operator< for sorting: compare nonce_1 then nonce_2 (does not need to be constant time) +bool operator<(const MultisigPubNonces &a, const MultisigPubNonces &b); +bool operator==(const MultisigPubNonces &a, const MultisigPubNonces &b); +/// get size in bytes +inline std::size_t multisig_pub_nonces_size_bytes() { return 2*sizeof(rct::key); } + +//// +// Multisig nonce cache +// - store a multisig signer's signature nonces +// - nonces may be stored for multiple signing attempts on different messages, keys, and for different signer subgroups +// of which the signer is a member +// +// WARNING: a nonce removed from the cache may still exist in persistent storage (a file somewhere); users should +// ALWAYS refresh that storage after making a signature and before exposing that signature outside the local +// context, to avoid a situation where the signature is exported then the local context crashes/closes without +// updating the nonces in storage; those nonces could be used to make another signature, thereby leaking the +// local signer's private multisig key material +/// +struct MultisigNonces final +{ + // signature nonce privkey: alpha_{1,e} + crypto::secret_key signature_nonce_1_priv; + // signature nonce privkey: alpha_{2,e} + crypto::secret_key signature_nonce_2_priv; +}; + +class MultisigNonceCache final +{ +public: +//constructors + /// default constructor + MultisigNonceCache() = default; + /// import raw nonce data (e.g. from a file) + MultisigNonceCache(const std::vector< + std::tuple + > &raw_nonce_data); + /// copy constructor: disabled (don't want copies of the nonces floating around) + MultisigNonceCache(const MultisigNonceCache&) = delete; + /// move constructor: defaulted + MultisigNonceCache(MultisigNonceCache&&) = default; +//overloaded operators + /// copy assignment: disabled (don't want copies of the nonces floating around) + MultisigNonceCache& operator=(const MultisigNonceCache&) = delete; + /// move assignment: defaulted + MultisigNonceCache& operator=(MultisigNonceCache&&) = default; + +//member functions + /// true if there is a nonce record for a given signing scenario + bool has_record(const rct::key &message, const rct::key &proof_key, const signer_set_filter &filter) const; + /// true if successfully added nonces for a given signing scenario + /// note: nonces are generated internally and only exposed by try_get_recorded_nonce_privkeys() + bool try_add_nonces(const rct::key &message, const rct::key &proof_key, const signer_set_filter &filter); + /// true if created nonce pubkeys for a given signing scenario on the specified base key J + bool try_get_nonce_pubkeys_for_base(const rct::key &message, + const rct::key &proof_key, + const signer_set_filter &filter, + const rct::key &pubkey_base, + MultisigPubNonces &nonce_pubkeys_out) const; + /// true if found nonce privkeys for a given signing scenario + bool try_get_recorded_nonce_privkeys(const rct::key &message, + const rct::key &proof_key, + const signer_set_filter &filter, + crypto::secret_key &nonce_privkey_1_out, + crypto::secret_key &nonce_privkey_2_out) const; + /// true if successfully removed a record for a given signing scenario + bool try_remove_record(const rct::key &message, const rct::key &proof_key, const signer_set_filter &filter); + /// export the nonce data (e.g. to record in a file) + std::vector> export_data() const; + +private: + /// true if successfully added nonces for a given signing scenario + bool try_add_nonces_impl(const rct::key &message, + const rct::key &proof_key, + const signer_set_filter &filter, + const MultisigNonces &nonces); + +//member variables + /// [ message : [ proof key : [ filter : nonces ] ] ] + std::unordered_map< + rct::key, //message to sign + std::unordered_map< + rct::key, //proof key to sign with using multisig + std::unordered_map< + signer_set_filter, //filter representing the signer group that should make this signature + MultisigNonces //the local signer's private nonce material for this signing attempt + > + > + > m_cache; +}; + +} //namespace multisig diff --git a/src/multisig/multisig_partial_cn_key_image_msg.cpp b/src/multisig/multisig_partial_cn_key_image_msg.cpp new file mode 100644 index 0000000000..bee2798d42 --- /dev/null +++ b/src/multisig/multisig_partial_cn_key_image_msg.cpp @@ -0,0 +1,293 @@ +// Copyright (c) 2021, 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 "multisig_partial_cn_key_image_msg.h" +#include "multisig_msg_serialization.h" + +#include "common/base58.h" +#include "crypto/crypto.h" +extern "C" +{ +#include "crypto/crypto-ops.h" +} +#include "include_base_utils.h" +#include "ringct/rctOps.h" +#include "seraphis_crypto/matrix_proof.h" +#include "seraphis_crypto/sp_hash_functions.h" +#include "seraphis_crypto/sp_transcript.h" +#include "serialization/binary_archive.h" +#include "serialization/serialization.h" + +#include + +#include +#include +#include +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "multisig" + +const boost::string_ref MULTISIG_PARTIAL_CN_KI_MSG_MAGIC_V1{"MultisigPartialCNKIV1"}; + +namespace multisig +{ +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +static std::vector pubkeys_mul8(std::vector keys) +{ + for (crypto::public_key &key : keys) + key = rct::rct2pk(rct::scalarmult8(rct::pk2rct(key))); + + return keys; +} +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +static void set_msg_magic(const boost::string_ref magic, std::string &msg_out) +{ + msg_out.clear(); + msg_out.append(magic.data(), magic.size()); +} +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +static bool try_get_message_no_magic(const std::string &original_msg, + const boost::string_ref magic, + std::string &msg_no_magic_out) +{ + // abort if magic doesn't match the message + if (original_msg.substr(0, magic.size()) != magic) + return false; + + // decode message + CHECK_AND_ASSERT_THROW_MES(tools::base58::decode(original_msg.substr(magic.size()), msg_no_magic_out), + "multisig partial cn key image msg (recover): message decoding error."); + + return true; +} +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +static rct::key get_matrix_proof_msg(const boost::string_ref magic, + const crypto::public_key &signing_pubkey, + const crypto::public_key &onetime_address) +{ + // proof_msg = H_32(signing_pubkey, Ko) + sp::SpFSTranscript transcript{magic, 2*sizeof(rct::key)}; + transcript.append("signing_pubkey", signing_pubkey); + transcript.append("Ko", onetime_address); + + // message + rct::key message; + sp::sp_hash_to_32(transcript.data(), transcript.size(), message.bytes); + + return message; +} +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +static crypto::hash get_signature_msg(const boost::string_ref magic, + const crypto::public_key &onetime_address, + const sp::MatrixProof &matrix_proof) +{ + // signature_msg = H_32(Ko, matrix proof) + sp::SpFSTranscript transcript{magic, 2*sizeof(rct::key)}; + transcript.append("Ko", onetime_address); + transcript.append("matrix_proof", matrix_proof); + + // message + crypto::hash message; + sp::sp_hash_to_32(transcript.data(), transcript.size(), message.data); + + return message; +} +//---------------------------------------------------------------------------------------------------------------------- +// multisig_partial_cn_key_image_msg: EXTERNAL +//---------------------------------------------------------------------------------------------------------------------- +multisig_partial_cn_key_image_msg::multisig_partial_cn_key_image_msg(const crypto::secret_key &signing_privkey, + const crypto::public_key &onetime_address, + const std::vector &keyshare_privkeys) : + m_onetime_address{onetime_address} +{ + CHECK_AND_ASSERT_THROW_MES(sc_check(to_bytes(signing_privkey)) == 0 && + sc_isnonzero(to_bytes(signing_privkey)), + "multisig partial cn key image msg (build): invalid msg signing key."); + CHECK_AND_ASSERT_THROW_MES(!(rct::pk2rct(onetime_address) == rct::Z), + "multisig partial cn key image msg (build): empty onetime address."); + CHECK_AND_ASSERT_THROW_MES(keyshare_privkeys.size() > 0, + "multisig partial cn key image msg (build): can't make message with no keys to convert."); + + // save signing pubkey + CHECK_AND_ASSERT_THROW_MES(crypto::secret_key_to_public_key(signing_privkey, m_signing_pubkey), + "multisig partial cn key image msg (build): failed to derive signing pubkey"); + + // prepare key image base key: Hp(Ko) + crypto::key_image key_image_base; + crypto::generate_key_image(m_onetime_address, rct::rct2sk(rct::I), key_image_base); + + // make matrix proof + sp::MatrixProof proof; + sp::make_matrix_proof( + get_matrix_proof_msg(MULTISIG_PARTIAL_CN_KI_MSG_MAGIC_V1, m_signing_pubkey, m_onetime_address), + { + crypto::get_G(), + rct::rct2pk(rct::ki2rct(key_image_base)) + }, + keyshare_privkeys, + proof + ); + + // set message and signing pub key + this->construct_msg(signing_privkey, proof); + + // cache the keyshares (mul8 means they are guaranteed to be canonical points) + CHECK_AND_ASSERT_THROW_MES(proof.M.size() == 2, "multisig partial cn ki msg: invalid matrix proof keys size."); + + m_multisig_keyshares = pubkeys_mul8(std::move(proof.M[0])); + m_partial_key_images = pubkeys_mul8(std::move(proof.M[1])); +} +//---------------------------------------------------------------------------------------------------------------------- +// multisig_partial_cn_key_image_msg: EXTERNAL +//---------------------------------------------------------------------------------------------------------------------- +multisig_partial_cn_key_image_msg::multisig_partial_cn_key_image_msg(std::string msg) : m_msg{std::move(msg)} +{ + this->parse_and_validate_msg(); +} +//---------------------------------------------------------------------------------------------------------------------- +// multisig_partial_cn_key_image_msg: INTERNAL +//---------------------------------------------------------------------------------------------------------------------- +void multisig_partial_cn_key_image_msg::construct_msg(const crypto::secret_key &signing_privkey, + const sp::MatrixProof &matrix_proof) +{ + // sign the message + crypto::signature msg_signature; + crypto::generate_signature(get_signature_msg(MULTISIG_PARTIAL_CN_KI_MSG_MAGIC_V1, m_onetime_address, matrix_proof), + m_signing_pubkey, + signing_privkey, + msg_signature); + + // mangle the matrix proof into a crypto::signature + const crypto::signature mangled_matrix_proof{rct::rct2sk(matrix_proof.c), rct::rct2sk(matrix_proof.r)}; + + // prepare the message + CHECK_AND_ASSERT_THROW_MES(matrix_proof.M.size() == 2, + "serializing multisig partial cn ki msg: invalid matrix proof keys size."); + + std::stringstream serialized_msg_ss; + binary_archive b_archive(serialized_msg_ss); + + multisig_partial_cn_ki_msg_serializable msg_serializable; + msg_serializable.onetime_address = m_onetime_address; + msg_serializable.multisig_keyshares = matrix_proof.M[0]; + msg_serializable.partial_key_images = matrix_proof.M[1]; + msg_serializable.signing_pubkey = m_signing_pubkey; + msg_serializable.matrix_proof_partial = mangled_matrix_proof; + msg_serializable.signature = msg_signature; + + CHECK_AND_ASSERT_THROW_MES(::serialization::serialize(b_archive, msg_serializable), + "multisig partial cn key image msg (build): failed to serialize message."); + + // make the message + set_msg_magic(MULTISIG_PARTIAL_CN_KI_MSG_MAGIC_V1, m_msg); + m_msg.append(tools::base58::encode(serialized_msg_ss.str())); +} +//---------------------------------------------------------------------------------------------------------------------- +// multisig_partial_cn_key_image_msg: INTERNAL +//---------------------------------------------------------------------------------------------------------------------- +void multisig_partial_cn_key_image_msg::parse_and_validate_msg() +{ + // early return on empty messages + if (m_msg == "") + return; + + // deserialize the message + std::string msg_no_magic; + CHECK_AND_ASSERT_THROW_MES(try_get_message_no_magic(m_msg, MULTISIG_PARTIAL_CN_KI_MSG_MAGIC_V1, msg_no_magic), + "multisig partial cn key image msg (recover): could not remove magic from message."); + + binary_archive archived_msg{epee::strspan(msg_no_magic)}; + + // extract data from the message + sp::MatrixProof matrix_proof; + crypto::signature msg_signature; + + multisig_partial_cn_ki_msg_serializable deserialized_msg; + CHECK_AND_ASSERT_THROW_MES(::serialization::serialize(archived_msg, deserialized_msg), + "multisig partial cn key image msg (recover): deserializing message failed."); + + m_onetime_address = deserialized_msg.onetime_address; + matrix_proof.M = + { + std::move(deserialized_msg.multisig_keyshares), + std::move(deserialized_msg.partial_key_images) + }; + m_signing_pubkey = deserialized_msg.signing_pubkey; + memcpy(matrix_proof.c.bytes, to_bytes(deserialized_msg.matrix_proof_partial.c), sizeof(crypto::ec_scalar)); + memcpy(matrix_proof.r.bytes, to_bytes(deserialized_msg.matrix_proof_partial.r), sizeof(crypto::ec_scalar)); + msg_signature = deserialized_msg.signature; + + // checks + CHECK_AND_ASSERT_THROW_MES(!(rct::pk2rct(m_onetime_address) == rct::Z), + "multisig partial cn key image msg (recover): message onetime address is null."); + CHECK_AND_ASSERT_THROW_MES(matrix_proof.M.size() == 2, + "multisig partial cn key image msg (recover): message is malformed."); + CHECK_AND_ASSERT_THROW_MES(matrix_proof.M[0].size() > 0, + "multisig partial cn key image msg (recover): message has no conversion keys."); + CHECK_AND_ASSERT_THROW_MES(matrix_proof.M[0].size() == matrix_proof.M[1].size(), + "multisig partial cn key image msg (recover): message key vectors don't line up."); + CHECK_AND_ASSERT_THROW_MES(m_signing_pubkey != crypto::null_pkey && + m_signing_pubkey != rct::rct2pk(rct::identity()), + "multisig partial cn key image msg (recover): message signing key is invalid."); + CHECK_AND_ASSERT_THROW_MES(rct::isInMainSubgroup(rct::pk2rct(m_signing_pubkey)), + "multisig partial cn key image msg (recover): message signing key is not in prime subgroup."); + + // prepare key image base key + crypto::key_image key_image_base; + crypto::generate_key_image(m_onetime_address, rct::rct2sk(rct::I), key_image_base); + + // validate matrix proof + matrix_proof.m = get_matrix_proof_msg(MULTISIG_PARTIAL_CN_KI_MSG_MAGIC_V1, m_signing_pubkey, m_onetime_address); + CHECK_AND_ASSERT_THROW_MES(sp::verify_matrix_proof(matrix_proof, + { + crypto::get_G(), + rct::rct2pk(rct::ki2rct(key_image_base)) + }), + "multisig partial cn key image msg (recover): message matrix proof invalid."); + + // validate signature + CHECK_AND_ASSERT_THROW_MES(crypto::check_signature( + get_signature_msg(MULTISIG_PARTIAL_CN_KI_MSG_MAGIC_V1, m_onetime_address, matrix_proof), + m_signing_pubkey, + msg_signature + ), + "multisig partial cn key image msg (recover): msg signature invalid."); + + // cache the keyshares (note: caching these after checking the signature ensures if the signature is invalid then the + // message's internal state won't be usable even if the invalid-signature exception is caught) + m_multisig_keyshares = pubkeys_mul8(std::move(matrix_proof.M[0])); + m_partial_key_images = pubkeys_mul8(std::move(matrix_proof.M[1])); +} +//---------------------------------------------------------------------------------------------------------------------- +} //namespace multisig diff --git a/src/multisig/multisig_partial_cn_key_image_msg.h b/src/multisig/multisig_partial_cn_key_image_msg.h new file mode 100644 index 0000000000..af86a28a56 --- /dev/null +++ b/src/multisig/multisig_partial_cn_key_image_msg.h @@ -0,0 +1,102 @@ +// Copyright (c) 2021, 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 "crypto/crypto.h" + +#include +#include +#include + +namespace sp { struct MatrixProof; } + +namespace multisig +{ + +//// +// multisig partial cryptonote key image message +// - This message contains a proof that a set of public keys on generator G have 1:1 discrete log relations with a +// set of partial key images on base key Hp(Ko) for hash-to-point algorithm Hp() and some onetime address Ko. +// - A multisig group member (for an M-of-N multisig) can recover the key image KI for a cryptonote onetime address +// Ko owned by the group by collecting these messages from M group members (where the private signing keys are +// shares of the group key held by each group member). Once at least M messages are collected, sum together unique +// partial KI keys from those message (plus the onetime address's view component times Hp(Ko)) to get the actual +// key image KI. Verify the key image by summing the unique multisig public keyshares from the messages and expecting +// the result to equal the group's base spend key. +// - INVARIANT: keyshares stored here are canonical prime-order subgroup points (this is guaranteed by obtaining the +// keyshares from a MatrixProof). +/// +class multisig_partial_cn_key_image_msg final +{ +//constructors +public: + // default constructor + multisig_partial_cn_key_image_msg() = default; + // construct from info (create message) + multisig_partial_cn_key_image_msg(const crypto::secret_key &signing_privkey, + const crypto::public_key &onetime_address, + const std::vector &keyshare_privkeys); + // construct from string (deserialize and validate message) + multisig_partial_cn_key_image_msg(std::string msg); + +//member functions + // get msg string + const std::string& get_msg() const { return m_msg; } + // get onetime address this message is built for + const crypto::public_key& get_onetime_address() const { return m_onetime_address; } + // get the multisig group key keyshares (these are guaranteed to be canonical points) + const std::vector& get_multisig_keyshares() const { return m_multisig_keyshares; } + // get the partial key image keys (these are guaranteed to be canonical points) + const std::vector& get_partial_key_images() const { return m_partial_key_images; } + // get msg signing pubkey (guaranteed to be a canonical point) + const crypto::public_key& get_signing_pubkey() const { return m_signing_pubkey; } + +private: + // set: msg string based on msg contents, with signing pubkey defined from signing privkey + void construct_msg(const crypto::secret_key &signing_privkey, const sp::MatrixProof &matrix_proof); + // parse msg string into parts, validate contents and signature + void parse_and_validate_msg(); + +//member variables +private: + // message as string + std::string m_msg; + + // onetime address this message is built for + crypto::public_key m_onetime_address; + // the msg signer's multisig key keyshares + std::vector m_multisig_keyshares; + // the msg signer's partial key images for the designated onetime address + std::vector m_partial_key_images; + + // pubkey used to sign this msg + crypto::public_key m_signing_pubkey; +}; + +} //namespace multisig diff --git a/src/multisig/multisig_partial_sig_makers.cpp b/src/multisig/multisig_partial_sig_makers.cpp new file mode 100644 index 0000000000..9ab5356dea --- /dev/null +++ b/src/multisig/multisig_partial_sig_makers.cpp @@ -0,0 +1,256 @@ +// Copyright (c) 2022, 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 "multisig_partial_sig_makers.h" + +//local headers +#include "crypto/crypto.h" +extern "C" +{ +#include "crypto/crypto-ops.h" +} +#include "misc_log_ex.h" +#include "multisig_clsag.h" +#include "multisig_nonce_cache.h" +#include "multisig_signing_helper_types.h" +#include "multisig_signer_set_filter.h" +#include "multisig_sp_composition_proof.h" +#include "ringct/rctOps.h" +#include "ringct/rctTypes.h" +#include "seraphis_crypto/sp_crypto_utils.h" + +//third party headers + +//standard headers +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "multisig" + +namespace multisig +{ +//------------------------------------------------------------------------------------------------------------------- +// for local signer's partial proof key K_e = (k_offset + k_e)*G +// and secondary proof key C_z = z*G +//------------------------------------------------------------------------------------------------------------------- +static CLSAGMultisigPartial attempt_make_clsag_multisig_partial_sig(const rct::key &one_div_threshold, + const crypto::secret_key &k_e, + const crypto::secret_key &k_offset, + const crypto::secret_key &z, + const CLSAGMultisigProposal &proof_proposal, + const std::vector &signer_pub_nonces_G, + const std::vector &signer_pub_nonces_Hp, + const multisig::signer_set_filter filter, + MultisigNonceCache &nonce_record_inout) +{ + // prepare the main signing privkey: (1/threshold)*k_offset + k_e + // note: k_offset is assumed to be a value known by all signers, so each signer adds (1/threshold)*k_offset to ensure + // the sum of partial signatures works out + crypto::secret_key k_e_signing; + sc_mul(to_bytes(k_e_signing), one_div_threshold.bytes, to_bytes(k_offset)); //(1/threshold)*k_offset + sc_add(to_bytes(k_e_signing), to_bytes(k_e_signing), to_bytes(k_e)); //+ k_e + + // prepare the auxilliary signing key: (1/threshold)*z + crypto::secret_key z_e_signing; + sc_mul(to_bytes(z_e_signing), one_div_threshold.bytes, to_bytes(z)); + + // local signer's partial sig for this proof key + CLSAGMultisigPartial partial_sig; + + if (!try_make_clsag_multisig_partial_sig(proof_proposal, + k_e_signing, + z_e_signing, + signer_pub_nonces_G, + signer_pub_nonces_Hp, + filter, + nonce_record_inout, + partial_sig)) + throw; + + return partial_sig; +} +//------------------------------------------------------------------------------------------------------------------- +// for local signer's partial proof key K_e = x*G + y*X + z_multiplier*( (1/threshold) * z_offset + z_e )*U +//------------------------------------------------------------------------------------------------------------------- +static SpCompositionProofMultisigPartial attempt_make_sp_composition_multisig_partial_sig( + const rct::key &one_div_threshold, + const crypto::secret_key &x, + const crypto::secret_key &y, + const crypto::secret_key &z_offset, + const crypto::secret_key &z_multiplier, + const crypto::secret_key &z_e, + const SpCompositionProofMultisigProposal &proof_proposal, + const std::vector &signer_pub_nonces, + const multisig::signer_set_filter filter, + MultisigNonceCache &nonce_record_inout) +{ + // prepare the signing privkey: z_multiplier*((1/threshold)*z_offset + z_e) + // note: z_offset is assumed to be a value known by all signers, so each signer adds (1/threshold)*z_offset to ensure + // the sum of partial signatures works out + crypto::secret_key z_e_signing; + sc_mul(to_bytes(z_e_signing), one_div_threshold.bytes, to_bytes(z_offset)); //(1/threshold)*z_offset + sc_add(to_bytes(z_e_signing), to_bytes(z_e_signing), to_bytes(z_e)); //... + z_e + sc_mul(to_bytes(z_e_signing), to_bytes(z_multiplier), to_bytes(z_e_signing)); //z_multiplier*(...) + + // local signer's partial sig for this proof key + SpCompositionProofMultisigPartial partial_sig; + + if (!try_make_sp_composition_multisig_partial_sig(proof_proposal, + x, + y, + z_e_signing, + signer_pub_nonces, + filter, + nonce_record_inout, + partial_sig)) + throw; + + return partial_sig; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +MultisigPartialSigMakerCLSAG::MultisigPartialSigMakerCLSAG(const std::uint32_t threshold, + const std::vector &proof_proposals, + const std::vector &proof_privkeys_k_offset, + const std::vector &proof_privkeys_z) : + m_inv_threshold{threshold ? sp::invert(rct::d2h(threshold)) : rct::zero()}, //avoid throwing in call to invert() + m_proof_proposals{proof_proposals}, + m_proof_privkeys_k_offset{proof_privkeys_k_offset}, + m_proof_privkeys_z{proof_privkeys_z} +{ + const std::size_t num_proposals{m_proof_proposals.size()}; + + CHECK_AND_ASSERT_THROW_MES(threshold > 0, + "MultisigPartialSigMakerCLSAG: multisig threshold is zero."); + CHECK_AND_ASSERT_THROW_MES(m_proof_privkeys_k_offset.size() == num_proposals, + "MultisigPartialSigMakerCLSAG: proof k offset privkeys don't line up with proof proposals."); + CHECK_AND_ASSERT_THROW_MES(m_proof_privkeys_z.size() == num_proposals, + "MultisigPartialSigMakerCLSAG: proof z privkeys don't line up with proof proposals."); + + // cache the proof keys mapped to indices in the referenced signature context data + for (std::size_t signature_proposal_index{0}; signature_proposal_index < num_proposals; ++signature_proposal_index) + m_cached_proof_keys[main_proof_key_ref(m_proof_proposals[signature_proposal_index])] = signature_proposal_index; +} +//------------------------------------------------------------------------------------------------------------------- +void MultisigPartialSigMakerCLSAG::attempt_make_partial_sig(const rct::key &proof_message, + const rct::key &proof_key, + const multisig::signer_set_filter signer_group_filter, + const std::vector> &signer_group_pub_nonces, + const crypto::secret_key &local_multisig_signing_key, + MultisigNonceCache &nonce_record_inout, + MultisigPartialSigVariant &partial_sig_out) const +{ + CHECK_AND_ASSERT_THROW_MES(m_cached_proof_keys.find(proof_key) != m_cached_proof_keys.end(), + "MultisigPartialSigMakerCLSAG (attempt make partial sig): requested signature proposal's proof key is unknown."); + CHECK_AND_ASSERT_THROW_MES(signer_group_pub_nonces.size() == 2, + "MultisigPartialSigMakerCLSAG (attempt make partial sig): signer group's pub nonces don't line up with signature " + "requirements (must be two sets for base keys G and Hp(proof key))."); + + const std::size_t signature_proposal_index{m_cached_proof_keys.at(proof_key)}; + + CHECK_AND_ASSERT_THROW_MES(m_proof_proposals.at(signature_proposal_index).message == proof_message, + "MultisigPartialSigMakerCLSAG (attempt make partial sig): proof message doesn't match with the requested " + "proof proposal."); + + partial_sig_out = attempt_make_clsag_multisig_partial_sig(m_inv_threshold, + local_multisig_signing_key, + m_proof_privkeys_k_offset.at(signature_proposal_index), + m_proof_privkeys_z.at(signature_proposal_index), + m_proof_proposals.at(signature_proposal_index), + signer_group_pub_nonces.at(0), //G + signer_group_pub_nonces.at(1), //Hp(proof key) + signer_group_filter, + nonce_record_inout); +} +//------------------------------------------------------------------------------------------------------------------- +MultisigPartialSigMakerSpCompositionProof::MultisigPartialSigMakerSpCompositionProof(const std::uint32_t threshold, + const std::vector &proof_proposals, + const std::vector &proof_privkeys_x, + const std::vector &proof_privkeys_y, + const std::vector &proof_privkeys_z_offset, + const std::vector &proof_privkeys_z_multiplier) : + m_inv_threshold{threshold ? sp::invert(rct::d2h(threshold)) : rct::zero()}, //avoid throwing in call to invert() + m_proof_proposals{proof_proposals}, + m_proof_privkeys_x{proof_privkeys_x}, + m_proof_privkeys_y{proof_privkeys_y}, + m_proof_privkeys_z_offset{proof_privkeys_z_offset}, + m_proof_privkeys_z_multiplier{proof_privkeys_z_multiplier} +{ + const std::size_t num_proposals{m_proof_proposals.size()}; + + CHECK_AND_ASSERT_THROW_MES(threshold > 0, + "MultisigPartialSigMakerSpCompositionProof: multisig threshold is zero."); + CHECK_AND_ASSERT_THROW_MES(m_proof_privkeys_x.size() == num_proposals, + "MultisigPartialSigMakerSpCompositionProof: proof x privkeys don't line up with proof proposals."); + CHECK_AND_ASSERT_THROW_MES(m_proof_privkeys_y.size() == num_proposals, + "MultisigPartialSigMakerSpCompositionProof: proof y privkeys don't line up with proof proposals."); + CHECK_AND_ASSERT_THROW_MES(m_proof_privkeys_z_offset.size() == num_proposals, + "MultisigPartialSigMakerSpCompositionProof: proof z_offset privkeys don't line up with proof proposals."); + CHECK_AND_ASSERT_THROW_MES(m_proof_privkeys_z_multiplier.size() == num_proposals, + "MultisigPartialSigMakerSpCompositionProof: proof z_multiplier privkeys don't line up with proof proposals."); + + // cache the proof keys mapped to indices in the referenced signature context data + for (std::size_t signature_proposal_index{0}; signature_proposal_index < num_proposals; ++signature_proposal_index) + m_cached_proof_keys[m_proof_proposals[signature_proposal_index].K] = signature_proposal_index; +} +//------------------------------------------------------------------------------------------------------------------- +void MultisigPartialSigMakerSpCompositionProof::attempt_make_partial_sig(const rct::key &proof_message, + const rct::key &proof_key, + const multisig::signer_set_filter signer_group_filter, + const std::vector> &signer_group_pub_nonces, + const crypto::secret_key &local_multisig_signing_key, + MultisigNonceCache &nonce_record_inout, + MultisigPartialSigVariant &partial_sig_out) const +{ + CHECK_AND_ASSERT_THROW_MES(m_cached_proof_keys.find(proof_key) != m_cached_proof_keys.end(), + "MultisigPartialSigMakerSpCompositionProof (attempt make partial sig): requested signature proposal's proof key " + "is unknown."); + CHECK_AND_ASSERT_THROW_MES(signer_group_pub_nonces.size() == 1, + "MultisigPartialSigMakerSpCompositionProof (attempt make partial sig): signer group's pub nonces don't line up with " + "signature requirements (must be one set for base key U)."); + + const std::size_t signature_proposal_index{m_cached_proof_keys.at(proof_key)}; + + CHECK_AND_ASSERT_THROW_MES(m_proof_proposals.at(signature_proposal_index).message == proof_message, + "MultisigPartialSigMakerCLSAG (attempt make partial sig): proof message doesn't match with the requested " + "proof proposal."); + + partial_sig_out = attempt_make_sp_composition_multisig_partial_sig(m_inv_threshold, + m_proof_privkeys_x.at(signature_proposal_index), + m_proof_privkeys_y.at(signature_proposal_index), + m_proof_privkeys_z_offset.at(signature_proposal_index), + m_proof_privkeys_z_multiplier.at(signature_proposal_index), + local_multisig_signing_key, + m_proof_proposals.at(signature_proposal_index), + signer_group_pub_nonces.at(0), + signer_group_filter, + nonce_record_inout); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace multisig diff --git a/src/multisig/multisig_partial_sig_makers.h b/src/multisig/multisig_partial_sig_makers.h new file mode 100644 index 0000000000..c140651c5b --- /dev/null +++ b/src/multisig/multisig_partial_sig_makers.h @@ -0,0 +1,174 @@ +// Copyright (c) 2022, 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. + +// Tool for making multisig partial signatures in a type-agnostic way for a range of signature schemes. + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "multisig_clsag.h" +#include "multisig_signer_set_filter.h" +#include "multisig_signing_helper_types.h" +#include "multisig_sp_composition_proof.h" +#include "ringct/rctTypes.h" + +//third party headers + +//standard headers + +//forward declarations +namespace multisig { class MultisigNonceCache; } + +namespace multisig +{ + +//// +// MultisigPartialSigMaker +// - interface for producing multisig partial signatures, agnostic to the signature scheme (it must be Schnorr-like +// and use musig2-style multisig via MultisigNonceCache) +// - must support wrapping multiple multisig signature proposals, which are accessed via the primary proof key +/// +class MultisigPartialSigMaker +{ +public: +//destructor + virtual ~MultisigPartialSigMaker() = default; + +//overloaded operators + /// disable copy/move (this is a virtual base class) + MultisigPartialSigMaker& operator=(MultisigPartialSigMaker&&) = delete; + +//member functions + /** + * brief: attempt_make_partial_sig - attempt to make a partial multisig signature (i.e. partially sign using the local + * multisig signer's private key) + * - throws on failure + * param: proof_message - proof message to make a signature for + * param: proof_key - proof key of one of the multisig proposals stored in this signature maker + * param: signer_group_filter - filter representing the subgroup of multisig signers who are expected to participate + * in making this partial signature (i.e. their public nonces will be used) + * param: signer_group_pub_nonces - the public nonces the signers who are participating in this signature attempt; + * the main vector lines up with the nonce base keys used in the proof (e.g. G and Hp(proof key) for CLSAG, and + * U for sp composition proofs); the internal vector lines up with the signers participating in this signature + * attempt + * param: local_multisig_signing_key - the local multisig signer's multisig signing key for the multisig subgroup + * represented by 'signer_group_filter' + * inoutparam: nonce_record_inout - the nonce record from which the local signer's nonce private keys for this + * signing attempt will be extracted + * outparam: partial_sig_out - partial signature created by the local signer for the specified signature proposal and + * signing group + */ + virtual void attempt_make_partial_sig(const rct::key &proof_message, + const rct::key &proof_key, + const signer_set_filter signer_group_filter, + const std::vector> &signer_group_pub_nonces, + const crypto::secret_key &local_multisig_signing_key, + MultisigNonceCache &nonce_record_inout, + MultisigPartialSigVariant &partial_sig_out) const = 0; +}; + +//// +// MultisigPartialSigMakerCLSAG: make CLSAG multisig partial signatures +/// +class MultisigPartialSigMakerCLSAG final : public MultisigPartialSigMaker +{ +public: +//constructors + /// normal constructor: data to wrap + MultisigPartialSigMakerCLSAG(const std::uint32_t threshold, + const std::vector &proof_proposals, + const std::vector &proof_privkeys_k_offset, + const std::vector &proof_privkeys_z); + +//overloaded operators + /// disable copy/move (this is a scoped manager [reference wrapper]) + MultisigPartialSigMakerCLSAG& operator=(MultisigPartialSigMakerCLSAG&&) = delete; + +//member functions + void attempt_make_partial_sig(const rct::key &proof_message, + const rct::key &proof_key, + const signer_set_filter signer_group_filter, + const std::vector> &signer_group_pub_nonces, + const crypto::secret_key &local_multisig_signing_key, + MultisigNonceCache &nonce_record_inout, + MultisigPartialSigVariant &partial_sig_out) const override; + +//member variables +private: + const rct::key m_inv_threshold; // 1/threshold + const std::vector &m_proof_proposals; + const std::vector &m_proof_privkeys_k_offset; + const std::vector &m_proof_privkeys_z; + + // cached proof keys mapped to indices in the set of proof proposals + std::unordered_map m_cached_proof_keys; +}; + +//// +// MultisigPartialSigMakerSpCompositionProof: make seraphis composition proof multisig partial signatures +/// +class MultisigPartialSigMakerSpCompositionProof final : public MultisigPartialSigMaker +{ +public: +//constructors + /// normal constructor: data to wrap + MultisigPartialSigMakerSpCompositionProof(const std::uint32_t threshold, + const std::vector &proof_proposals, + const std::vector &proof_privkeys_x, + const std::vector &proof_privkeys_y, + const std::vector &proof_privkeys_z_offset, + const std::vector &proof_privkeys_z_multiplier); + +//overloaded operators + /// disable copy/move (this is a scoped manager [reference wrapper]) + MultisigPartialSigMakerSpCompositionProof& operator=(MultisigPartialSigMakerSpCompositionProof&&) = delete; + +//member functions + void attempt_make_partial_sig(const rct::key &proof_message, + const rct::key &proof_key, + const signer_set_filter signer_group_filter, + const std::vector> &signer_group_pub_nonces, + const crypto::secret_key &local_multisig_signing_key, + MultisigNonceCache &nonce_record_inout, + MultisigPartialSigVariant &partial_sig_out) const override; + +//member variables +private: + const rct::key m_inv_threshold; // 1/threshold + const std::vector &m_proof_proposals; + const std::vector &m_proof_privkeys_x; + const std::vector &m_proof_privkeys_y; + const std::vector &m_proof_privkeys_z_offset; + const std::vector &m_proof_privkeys_z_multiplier; + + // cached proof keys mapped to indices in the set of proof proposals + std::unordered_map m_cached_proof_keys; +}; + +} //namespace multisig diff --git a/src/multisig/multisig_signer_set_filter.cpp b/src/multisig/multisig_signer_set_filter.cpp new file mode 100644 index 0000000000..815a3db0f3 --- /dev/null +++ b/src/multisig/multisig_signer_set_filter.cpp @@ -0,0 +1,292 @@ +// Copyright (c) 2021, 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 "multisig_signer_set_filter.h" + +//local headers +#include "crypto/crypto.h" +#include "misc_log_ex.h" +#include "seraphis_crypto/math_utils.h" + +//third party headers + +//standard headers +#include +#include +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "multisig" + +namespace multisig +{ +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +static bool check_multisig_config_for_filter(const std::uint32_t threshold, const std::uint32_t num_signers) +{ + if (num_signers > 8*sizeof(signer_set_filter)) + return false; + if (threshold > num_signers) + return false; + + return true; +} +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +static signer_set_filter right_shift_filter(const signer_set_filter filter, const std::uint32_t num_bits) +{ + // note: it is UB to bitshift 32-bit and 64-bit uints by more than 'bitlength(T) - 1' + return num_bits < 8*sizeof(signer_set_filter) + ? filter >> num_bits + : 0; +} +//---------------------------------------------------------------------------------------------------------------------- +// get filter with least significant 'num_bits' flags set +//---------------------------------------------------------------------------------------------------------------------- +static signer_set_filter get_squashed_full_filter(const std::uint32_t num_bits) +{ + return right_shift_filter(static_cast(-1), 8*sizeof(signer_set_filter) - num_bits); +} +//---------------------------------------------------------------------------------------------------------------------- +// map a filter mask onto the set bits of an aggregate filter (ignore all unset bits in the aggregate filter) +// - ex: mask=[1010], agg=[00110110] -> ret=[00100100] +//---------------------------------------------------------------------------------------------------------------------- +static signer_set_filter apply_mask_to_filter(signer_set_filter filter_mask, signer_set_filter aggregate_filter) +{ + signer_set_filter temp_filter{0}; + std::uint32_t agg_filter_position{0}; + + // find the first set bit in the aggregate filter + while (aggregate_filter && !(aggregate_filter & 1)) + { + aggregate_filter >>= 1; + ++agg_filter_position; + } + + while (filter_mask && aggregate_filter) + { + // set the return filter's flag at the aggregate filter position if the reference filter's top flag is set + temp_filter |= ((filter_mask & 1) << agg_filter_position); + + // find the next set bit in the aggregate filter + do + { + aggregate_filter >>= 1; + ++agg_filter_position; + } while (aggregate_filter && !(aggregate_filter & 1)); + + // remove the reference filter's last flag + filter_mask >>= 1; + } + + return temp_filter; +} +//---------------------------------------------------------------------------------------------------------------------- +// - assumes input signer is a member of the list +//---------------------------------------------------------------------------------------------------------------------- +static std::size_t signer_index_in_list(const crypto::public_key &signer, + const std::vector &signer_list) +{ + std::size_t signer_index{0}; + for (const crypto::public_key &other_signer : signer_list) + { + if (signer == other_signer) + break; + ++signer_index; + } + + return signer_index; +} +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +std::uint32_t get_num_flags_set(signer_set_filter filter) +{ + // note: will compile to 'popcnt' on supporting architectures (std::popcount needs C++20) + std::uint32_t set_flags_count{0}; + for (; filter != 0; filter &= filter - 1) + ++set_flags_count; + + return set_flags_count; +} +//---------------------------------------------------------------------------------------------------------------------- +bool validate_multisig_signer_set_filter(const std::uint32_t threshold, + const std::uint32_t num_signers, + const signer_set_filter filter) +{ + // the filter should only have flags set for possible signers + if (!check_multisig_config_for_filter(threshold, num_signers)) + return false; + if (right_shift_filter(filter, num_signers) != 0) + return false; + + // the filter should only have 'threshold' number of flags set + if (get_num_flags_set(filter) != threshold) + return false; + + return true; +} +//---------------------------------------------------------------------------------------------------------------------- +bool validate_multisig_signer_set_filters(const std::uint32_t threshold, + const std::uint32_t num_signers, + const std::vector &filters) +{ + for (const signer_set_filter filter : filters) + { + if (!validate_multisig_signer_set_filter(threshold, num_signers, filter)) + return false; + } + + return true; +} +//---------------------------------------------------------------------------------------------------------------------- +bool validate_aggregate_multisig_signer_set_filter(const std::uint32_t threshold, + const std::uint32_t num_signers, + const signer_set_filter aggregate_filter) +{ + const std::uint32_t num_signers_requested{get_num_flags_set(aggregate_filter)}; + + return (num_signers_requested >= threshold) && + validate_multisig_signer_set_filter(num_signers_requested, num_signers, aggregate_filter); +} +//---------------------------------------------------------------------------------------------------------------------- +void aggregate_multisig_signer_set_filter_to_permutations(const std::uint32_t threshold, + const std::uint32_t num_signers, + const signer_set_filter aggregate_filter, + std::vector &filter_permutations_out) +{ + CHECK_AND_ASSERT_THROW_MES(check_multisig_config_for_filter(threshold, num_signers), + "Invalid multisig config when getting filter permutations."); + + const std::uint32_t num_flags_set{get_num_flags_set(aggregate_filter)}; + + CHECK_AND_ASSERT_THROW_MES(num_flags_set <= num_signers && + num_flags_set >= threshold, + "Invalid aggregate multisig signer set filter when getting filter permutations."); + + const std::uint32_t expected_num_permutations(sp::math::n_choose_k(num_flags_set, threshold)); + filter_permutations_out.clear(); + filter_permutations_out.reserve(expected_num_permutations); + + // start getting permutations with the filter where the first 'threshold' signers in the aggregate filter are set + signer_set_filter filter_mask{get_squashed_full_filter(threshold)}; + + // apply all masks where 'threshold' flags are set + do + { + // if found a useful bit pattern, map it onto the aggregate filter and save that permutation + if (get_num_flags_set(filter_mask) == threshold) + { + filter_permutations_out.emplace_back(apply_mask_to_filter(filter_mask, aggregate_filter)); + + CHECK_AND_ASSERT_THROW_MES(validate_multisig_signer_set_filter(threshold, + num_signers, + filter_permutations_out.back()), + "Invalid multisig set filter extracted from aggregate filter."); + } + //note: post-increment the reference filter so the filter 'just used' is tested + //note2: do-while pattern lets us use == to exit the loop, which supports the case where we need to exit after the + // mask equals the max value of a filter (i.e. when all the flags are set) + } while (filter_mask++ < get_squashed_full_filter(num_flags_set)); + + // sanity check + CHECK_AND_ASSERT_THROW_MES(filter_permutations_out.size() == expected_num_permutations, + "Invalid number of permutations when disaggregating a signer set filter. (bug)"); +} +//---------------------------------------------------------------------------------------------------------------------- +void multisig_signers_to_filter(const std::vector &allowed_signers, + const std::vector &signer_list, + signer_set_filter &aggregate_filter_out) +{ + CHECK_AND_ASSERT_THROW_MES(check_multisig_config_for_filter(0, signer_list.size()), + "Invalid multisig config when making multisig signer filters."); + CHECK_AND_ASSERT_THROW_MES(allowed_signers.size() <= signer_list.size(), + "Invalid number of allowed signers when making multisig signer filters."); + + for (const crypto::public_key &allowed_signer : allowed_signers) + { + CHECK_AND_ASSERT_THROW_MES(std::find(signer_list.begin(), signer_list.end(), allowed_signer) != signer_list.end(), + "Unknown allowed signer when making multisig signer filters."); + } + + // make aggregate filter from all allowed signers + aggregate_filter_out = 0; + + for (const crypto::public_key &allowed_signer : allowed_signers) + aggregate_filter_out |= signer_set_filter{1} << signer_index_in_list(allowed_signer, signer_list); +} +//---------------------------------------------------------------------------------------------------------------------- +void multisig_signers_to_filter(const std::unordered_set &allowed_signers, + const std::vector &signer_list, + signer_set_filter &aggregate_filter_out) +{ + // convert: unordered_set -> vector + std::vector allowed_signers_temp; + allowed_signers_temp.reserve(allowed_signers.size()); + for (const crypto::public_key &allowed_signer : allowed_signers) + allowed_signers_temp.emplace_back(allowed_signer); + + multisig_signers_to_filter(allowed_signers_temp, signer_list, aggregate_filter_out); +} +//---------------------------------------------------------------------------------------------------------------------- +void multisig_signer_to_filter(const crypto::public_key &allowed_signer, + const std::vector &signer_list, + signer_set_filter &aggregate_filter_out) +{ + multisig_signers_to_filter(std::vector{allowed_signer}, signer_list, aggregate_filter_out); +} +//---------------------------------------------------------------------------------------------------------------------- +void get_filtered_multisig_signers(const signer_set_filter filter, + const std::uint32_t threshold, + const std::vector &signer_list, + std::vector &filtered_signers_out) +{ + CHECK_AND_ASSERT_THROW_MES(validate_multisig_signer_set_filter(threshold, signer_list.size(), filter), + "Invalid signer set filter when filtering a list of multisig signers."); + + filtered_signers_out.clear(); + filtered_signers_out.reserve(threshold); + + // filter the signer list + for (std::size_t signer_index{0}; signer_index < signer_list.size(); ++signer_index) + { + if ((filter >> signer_index) & 1) + filtered_signers_out.emplace_back(signer_list[signer_index]); + } +} +//---------------------------------------------------------------------------------------------------------------------- +bool signer_is_in_filter(const crypto::public_key &signer, + const std::vector &signer_list, + const signer_set_filter test_filter) +{ + signer_set_filter temp_filter; + multisig_signer_to_filter(signer, signer_list, temp_filter); + return temp_filter & test_filter; +} +//---------------------------------------------------------------------------------------------------------------------- +} //namespace multisig diff --git a/src/multisig/multisig_signer_set_filter.h b/src/multisig/multisig_signer_set_filter.h new file mode 100644 index 0000000000..947a682629 --- /dev/null +++ b/src/multisig/multisig_signer_set_filter.h @@ -0,0 +1,139 @@ +// Copyright (c) 2021, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "cryptonote_config.h" + +//third party headers + +//standard headers +#include +#include +#include + +//forward declarations + + +namespace multisig +{ + +/** +* multisig signer set filter +* - a set of multisig signers, represented as bit flags that correspond 1:1 with a list of sorted signer ids +*/ +using signer_set_filter = std::uint64_t; +static_assert(8*sizeof(signer_set_filter) >= config::MULTISIG_MAX_SIGNERS, ""); + +/** +* brief: get_num_flags_set - count how many flags are set in a filter +* param: filter - a set of signer flags +* return: number of flags set in the filter +*/ +std::uint32_t get_num_flags_set(signer_set_filter filter); +/** +* brief: validate_multisig_signer_set_filter - check that a signer set is valid +* - check: only possible signers are flagged +* - check: only 'threshold' number of signers are flagged +* param: threshold - threshold of multisig (M) +* param: num_signers - number of participants in multisig (N) +* param: filter - a filter representation of multisig signers to validate +* return: true/false on validation result +*/ +bool validate_multisig_signer_set_filter(const std::uint32_t threshold, + const std::uint32_t num_signers, + const signer_set_filter filter); +bool validate_multisig_signer_set_filters(const std::uint32_t threshold, + const std::uint32_t num_signers, + const std::vector &filters); +/** +* brief: validate_aggregate_multisig_signer_set_filter - check that an aggregate signer set is valid +* - check: only possible signers are flagged +* - check: at least 'threshold' number of signers are flagged (more than threshold are allowed) +* param: threshold - threshold of multisig (M) +* param: num_signers - number of participants in multisig (N) +* param: aggregate_filter - an aggregate set of multisig signers to validate +* return: true/false on validation result +*/ +bool validate_aggregate_multisig_signer_set_filter(const std::uint32_t threshold, + const std::uint32_t num_signers, + const signer_set_filter aggregate_filter); +/** +* brief: aggregate_multisig_signer_set_filter_to_permutations - extract filters from an aggregate filter +* - An aggregate filter is bitwise-or between all contained filters. +* - Every permutation of 'threshold' number of signers from the aggregate set is a separate signer set that can +* collaborate on a multisig signature. Dis-aggregating the aggregate filter provides filters corresponding +* to each of those sets. +* param: threshold - number of signers a filter can represent +* param: num_signers - total number of signers the filter acts on +* param: aggregate_filter - signer set filter that can represent multiple filters each representing threshold signers +* outparam: filter_permutations_out - all the filters that can be extracted from the aggregate filter +*/ +void aggregate_multisig_signer_set_filter_to_permutations(const std::uint32_t threshold, + const std::uint32_t num_signers, + const signer_set_filter aggregate_filter, + std::vector &filter_permutations_out); +/** +* brief: multisig_signers_to_filter - represent a set of multisig signers as an aggregate filter +* param: allowed_signers - the signers from the signer list that should be represented in the filter +* param: signer_list - list of signer ids (should be sorted) +* outparam: aggregate_filter_out - an aggregate filter that maps the allowed signer list to the signer list +*/ +void multisig_signers_to_filter(const std::vector &allowed_signers, + const std::vector &signer_list, + signer_set_filter &aggregate_filter_out); +void multisig_signers_to_filter(const std::unordered_set &allowed_signers, + const std::vector &signer_list, + signer_set_filter &aggregate_filter_out); +void multisig_signer_to_filter(const crypto::public_key &allowed_signer, + const std::vector &signer_list, + signer_set_filter &aggregate_filter_out); +/** +* brief: get_filtered_multisig_signers - filter a signer list using a signer_set_filter +* param: filter - signer set filter +* param: threshold - number of signers the filter is expected to represent +* param: signer_list - list of signer ids (should be sorted) +* outparam: filtered_signers_out - a filtered set of multisig signer ids +*/ +void get_filtered_multisig_signers(const signer_set_filter filter, + const std::uint32_t threshold, + const std::vector &signer_list, + std::vector &filtered_signers_out); +/** +* brief: signer_is_in_filter - check if a signer is in a filter +* param: signer - signer to check +* param: signer_list - list of signer ids to look for the signer in (should be sorted) +* param: test_filter - filter to apply to the signer list +*/ +bool signer_is_in_filter(const crypto::public_key &signer, + const std::vector &signer_list, + const signer_set_filter test_filter); + +} //namespace multisig diff --git a/src/multisig/multisig_signing_errors.cpp b/src/multisig/multisig_signing_errors.cpp new file mode 100644 index 0000000000..e07f315baa --- /dev/null +++ b/src/multisig/multisig_signing_errors.cpp @@ -0,0 +1,72 @@ +// Copyright (c) 2022, 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 "multisig_signing_errors.h" + +//local headers +#include "common/variant.h" + +//third party headers + +//standard headers +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "multisig" + +namespace multisig +{ +//------------------------------------------------------------------------------------------------------------------- +const std::string& error_message_ref(const MultisigSigningErrorVariant &variant) +{ + struct visitor final : public tools::variant_static_visitor + { + using variant_static_visitor::operator(); //for blank overload + const std::string& operator()(const MultisigSigningErrorBadInitSet &error) const + { return error.error_message; } + const std::string& operator()(const MultisigSigningErrorBadInitSetCollection &error) const + { return error.error_message; } + const std::string& operator()(const MultisigSigningErrorAvailableSigners &error) const + { return error.error_message; } + const std::string& operator()(const MultisigSigningErrorBadPartialSig &error) const + { return error.error_message; } + const std::string& operator()(const MultisigSigningErrorMakePartialSigSet &error) const + { return error.error_message; } + const std::string& operator()(const MultisigSigningErrorBadPartialSigSet &error) const + { return error.error_message; } + const std::string& operator()(const MultisigSigningErrorBadSigAssembly &error) const + { return error.error_message; } + const std::string& operator()(const MultisigSigningErrorBadSigSet &error) const + { return error.error_message; } + }; + + return variant.visit(visitor{}); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace multisig diff --git a/src/multisig/multisig_signing_errors.h b/src/multisig/multisig_signing_errors.h new file mode 100644 index 0000000000..878826b0f0 --- /dev/null +++ b/src/multisig/multisig_signing_errors.h @@ -0,0 +1,232 @@ +// Copyright (c) 2022, 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. + +// Error objects for reporting problems that occur during multisig signing ceremonies. +// NOTE: The error messages are declared last in each error type so they can be ignored +// when using designated initialization. + +#pragma once + +//local headers +#include "common/variant.h" +#include "crypto/crypto.h" +#include "multisig/multisig_signer_set_filter.h" +#include "ringct/rctTypes.h" + +//third party headers + +//standard headers +#include +#include + +//forward declarations + + +namespace multisig +{ + +struct dummy_multisig_exception final : public std::exception +{}; + +struct MultisigSigningErrorBadInitSet final +{ + enum class ErrorCode + { + SEMANTICS_EXCEPTION, + UNEXPECTED_FILTER, + UNEXPECTED_SIGNER, + UNEXPECTED_PROOF_MESSAGE, + UNEXPECTED_MAIN_PROOF_KEY + }; + + /// error code + ErrorCode error_code; + + /// all multisig signers allowed to participate in signature attempts + multisig::signer_set_filter aggregate_signer_set_filter; + /// id of signer who made this proof initializer set + crypto::public_key signer_id; + /// message to be signed by the multisig proofs + rct::key proof_message; + /// main proof key to be signed by the multisig proofs + rct::key proof_key; + + /// optional error message (e.g. for exceptions) + std::string error_message; +}; + +struct MultisigSigningErrorBadInitSetCollection final +{ + enum class ErrorCode + { + EMPTY_COLLECTION_EXPECTED, + PROOF_CONTEXT_MISMATCH, + INVALID_MAPPING, + GET_NONCES_FAIL, + INVALID_NONCES_SET_SIZE + }; + + /// error code + ErrorCode error_code; + + /// id of signer who supposedly made this collection of proof initializer sets + crypto::public_key signer_id; + + /// optional error message (e.g. for exceptions) + std::string error_message; +}; + +struct MultisigSigningErrorAvailableSigners final +{ + enum class ErrorCode + { + INCOMPLETE_AVAILABLE_SIGNERS + }; + + /// error code + ErrorCode error_code; + + /// signers that are allowed to participate in a given multisig signing ceremony but are missing + multisig::signer_set_filter missing_signers; + /// signers that are not allowed to participate in a given multisig signing ceremony but are present anyway + multisig::signer_set_filter unexpected_available_signers; + + /// optional error message (e.g. for exceptions) + std::string error_message; +}; + +struct MultisigSigningErrorBadPartialSig final +{ + enum class ErrorCode + { + UNEXPECTED_MAIN_PROOF_KEY, + UNEXPECTED_PROOF_MESSAGE, + UNEXPECTED_VARIANT_TYPE + }; + + /// error code + ErrorCode error_code; + + /// main proof key of the partial sig + rct::key proof_key; + /// proof message of the partial sig + rct::key proof_message; + + /// optional error message (e.g. for exceptions) + std::string error_message; +}; + +struct MultisigSigningErrorMakePartialSigSet final +{ + enum class ErrorCode + { + GET_KEY_FAIL, + MAKE_SET_EXCEPTION, + MAKE_SIGNATURE_EXCEPTION, + INVALID_NONCES_SET_QUANTITY + }; + + /// error code + ErrorCode error_code; + + /// set of multisig signers the partial signature set corresponds to + multisig::signer_set_filter signature_set_filter; + + /// optional error message (e.g. for exceptions) + std::string error_message; +}; + +struct MultisigSigningErrorBadPartialSigSet final +{ + enum class ErrorCode + { + SEMANTICS_EXCEPTION, + INVALID_MAPPING + }; + + /// error code + ErrorCode error_code; + + /// set of multisig signers the partial signature set corresponds to + multisig::signer_set_filter signature_set_filter; + /// signer that produced this partial sig set + crypto::public_key signer_id; + + /// optional error message (e.g. for exceptions) + std::string error_message; +}; + +struct MultisigSigningErrorBadSigAssembly final +{ + enum class ErrorCode + { + PROOF_KEYS_MISMATCH, + SIG_ASSEMBLY_FAIL + }; + + /// error code + ErrorCode error_code; + + /// set of multisig signers the partial signature set corresponds to + multisig::signer_set_filter signer_set_filter; + + /// optional error message (e.g. for exceptions) + std::string error_message; +}; + +struct MultisigSigningErrorBadSigSet final +{ + enum class ErrorCode + { + INVALID_SIG_SET + }; + + /// error code + ErrorCode error_code; + + /// optional error message (e.g. for exceptions) + std::string error_message; +}; + +//// +// MultisigSigningErrorVariant +/// +using MultisigSigningErrorVariant = + tools::variant< + MultisigSigningErrorBadInitSet, + MultisigSigningErrorBadInitSetCollection, + MultisigSigningErrorAvailableSigners, + MultisigSigningErrorBadPartialSig, + MultisigSigningErrorMakePartialSigSet, + MultisigSigningErrorBadPartialSigSet, + MultisigSigningErrorBadSigAssembly, + MultisigSigningErrorBadSigSet + >; +const std::string& error_message_ref(const MultisigSigningErrorVariant &variant); + +} //namespace multisig diff --git a/src/multisig/multisig_signing_helper_types.cpp b/src/multisig/multisig_signing_helper_types.cpp new file mode 100644 index 0000000000..f3a47ef8e4 --- /dev/null +++ b/src/multisig/multisig_signing_helper_types.cpp @@ -0,0 +1,88 @@ +// Copyright (c) 2022, 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 "multisig_signing_helper_types.h" + +//local headers +#include "common/variant.h" +#include "multisig_clsag.h" +#include "multisig_sp_composition_proof.h" +#include "ringct/rctTypes.h" + +//third party headers + +//standard headers +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "multisig" + +namespace multisig +{ +//------------------------------------------------------------------------------------------------------------------- +const rct::key& proof_key_ref(const MultisigPartialSigVariant &variant) +{ + struct visitor final : public tools::variant_static_visitor + { + using variant_static_visitor::operator(); //for blank overload + const rct::key& operator()(const CLSAGMultisigPartial &partial_sig) const + { return partial_sig.main_proof_key_K; } + const rct::key& operator()(const SpCompositionProofMultisigPartial &partial_sig) const + { return partial_sig.K; } + }; + + return variant.visit(visitor{}); +} +//------------------------------------------------------------------------------------------------------------------- +const rct::key& message_ref(const MultisigPartialSigVariant &variant) +{ + struct visitor final : public tools::variant_static_visitor + { + using variant_static_visitor::operator(); //for blank overload + const rct::key& operator()(const CLSAGMultisigPartial &partial_sig) const + { return partial_sig.message; } + const rct::key& operator()(const SpCompositionProofMultisigPartial &partial_sig) const + { return partial_sig.message; } + }; + + return variant.visit(visitor{}); +} +//------------------------------------------------------------------------------------------------------------------- +bool try_get_nonces(const MultisigProofInitSetV1 &init_set, + const std::size_t filter_index, + std::vector &nonces_out) +{ + if (filter_index >= init_set.inits.size()) + return false; + + nonces_out = init_set.inits[filter_index]; + return true; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace multisig diff --git a/src/multisig/multisig_signing_helper_types.h b/src/multisig/multisig_signing_helper_types.h new file mode 100644 index 0000000000..1f63d693fd --- /dev/null +++ b/src/multisig/multisig_signing_helper_types.h @@ -0,0 +1,118 @@ +// Copyright (c) 2022, 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. + +// Multisig signing helper types. + +#pragma once + +//local headers +#include "common/variant.h" +#include "crypto/crypto.h" +#include "multisig_clsag.h" +#include "multisig_nonce_cache.h" +#include "multisig_signer_set_filter.h" +#include "multisig_sp_composition_proof.h" +#include "ringct/rctTypes.h" + +//third party headers + +//standard headers +#include +#include + +//forward declarations + + +namespace multisig +{ + +//// +// MultisigProofInitSetV1 +// - this signer initializes a proof to be signed by a multisig group +// - the init set initializes a proof attempt for every signer subgroup this signer is a member of in the specified +// aggregate signer set filter +/// +struct MultisigProofInitSetV1 final +{ + /// all multisig signers who should participate in attempting to make these multisig proofs (get this from e.g. a + /// multisig proof proposal) + signer_set_filter aggregate_signer_set_filter; + /// id of signer who made this proof initializer set + crypto::public_key signer_id; + /// message to be signed by the multisig proofs + rct::key proof_message; + /// main proof key to be signed by the multisig proofs (any additional/auxilliary proof keys aren't recorded here, + /// since they are assumed to be implicitly tied to the main proof key) + rct::key proof_key; + + /// proof initializers + // - for each signer set in permutations of the aggregate signer set that includes the specified signer id, record a + // vector of pub nonces where each element aligns to a set of nonce base keys across which the multisig signature will + // be made (for example: CLSAG signs across both G and Hp(Ko), where Ko = ko*G is the proof key recorded here) + // - note that permutations of signers depend on the threshold and list of multisig signers, which are not recorded here + // - WARNING: ordering is dependent on the signer set filter permutation generator + // { { {pub nonces: filter 0 and proof base key 0}, {pub nonces: filter 0 and proof base key 1 } }, ... } + std::vector< //filter permutations + std::vector< //proof base keys + MultisigPubNonces //nonces + > + > inits; +}; + +//// +// MultisigPartialSigVariant +// - variant of multisig partial signatures +// +// proof_key_ref(): get the main proof key used in the partial signature (there may be additional auxilliary proof keys) +// message_ref(): get the message signed by the partial signature +/// +using MultisigPartialSigVariant = tools::variant; +const rct::key& proof_key_ref(const MultisigPartialSigVariant &variant); +const rct::key& message_ref(const MultisigPartialSigVariant &variant); + +//// +// MultisigPartialSigSetV1 +// - set of multisig partial signatures for different proof keys; combine partial signatures to complete a proof +/// +struct MultisigPartialSigSetV1 final +{ + /// multisig signer subgroup these partial signatures were created for + multisig::signer_set_filter signer_set_filter; + /// id of signer who made these partial signatures + crypto::public_key signer_id; + + /// [ proof key : partial signatures ] partial signatures mapped to their internally cached proof keys + std::unordered_map partial_signatures; +}; + +/// get set of nonces from an init set for a given filter (returns false if the location doesn't exist) +bool try_get_nonces(const MultisigProofInitSetV1 &init_set, + const std::size_t filter_index, + std::vector &nonces_out); + +} //namespace multisig diff --git a/src/multisig/multisig_signing_helper_utils.cpp b/src/multisig/multisig_signing_helper_utils.cpp new file mode 100644 index 0000000000..e1a0c8d83b --- /dev/null +++ b/src/multisig/multisig_signing_helper_utils.cpp @@ -0,0 +1,1026 @@ +// Copyright (c) 2022, 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 "multisig_signing_helper_utils.h" + +//local headers +#include "common/container_helpers.h" +#include "crypto/crypto.h" +#include "misc_language.h" +#include "misc_log_ex.h" +#include "multisig/multisig_signer_set_filter.h" +#include "multisig_nonce_cache.h" +#include "multisig_partial_sig_makers.h" +#include "multisig_signing_errors.h" +#include "multisig_signing_helper_types.h" +#include "ringct/rctOps.h" +#include "ringct/rctTypes.h" +#include "seraphis_crypto/math_utils.h" +#include "seraphis_crypto/sp_crypto_utils.h" + +//third party headers +#include + +//standard headers +#include +#include +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "multisig" + +namespace multisig +{ +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void prepare_multisig_init_set_collections_v1(const std::uint32_t threshold, + const std::vector &multisig_signers, + const signer_set_filter aggregate_signer_set_filter, + const crypto::public_key &local_signer_id, + const std::unordered_map &expected_proof_contexts, //[ proof key : proof message ] + const std::size_t num_expected_nonce_sets_per_proofkey, + //[ proof key : init set ] + std::unordered_map local_init_set_collection, + //[ signer id : [ proof key : init set ] ] + std::unordered_map> + other_init_set_collections, + std::list &multisig_errors_inout, + //[ signer id : [ proof key : init set ] ] + std::unordered_map> + &all_init_set_collections_out) +{ + /// validate and filter inits + + // 1. local init set must always be valid + auto validation_error_self_init = validate_v1_multisig_init_set_collection_v1(local_init_set_collection, + threshold, + multisig_signers, + aggregate_signer_set_filter, + local_signer_id, + expected_proof_contexts, + num_expected_nonce_sets_per_proofkey); + CHECK_AND_ASSERT_THROW_MES(!validation_error_self_init, + "validate and prepare multisig init set collections: the local signer's collection is invalid."); + + // 2. weed out invalid other init set collections + tools::for_all_in_map_erase_if(other_init_set_collections, + [&](const auto &other_signer_init_set_collection) -> bool + { + auto validation_error = validate_v1_multisig_init_set_collection_v1( + other_signer_init_set_collection.second, + threshold, + multisig_signers, + aggregate_signer_set_filter, + other_signer_init_set_collection.first, //check that the mapped id is correct + expected_proof_contexts, + num_expected_nonce_sets_per_proofkey); + + if (validation_error) + multisig_errors_inout.emplace_back(validation_error); + + return !validation_error.is_empty(); + } + ); + + // 3. collect all init sets + all_init_set_collections_out = std::move(other_init_set_collections); + all_init_set_collections_out[local_signer_id] = std::move(local_init_set_collection); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void prepare_filters_for_multisig_partial_signing(const std::uint32_t threshold, + const std::vector &multisig_signers, + const crypto::public_key &local_signer_id, + const signer_set_filter multisig_proposal_aggregate_signer_set_filter, + //[ signer id : [ proof key : init set ] ] + const std::unordered_map> + &all_init_set_collections, + signer_set_filter &local_signer_filter_out, + signer_set_filter &available_signers_filter_out, + //[ signer id : signer as filter ] + std::unordered_map &available_signers_as_filters_out, + std::vector &filter_permutations_out) +{ + // 1. save local signer as filter + multisig_signer_to_filter(local_signer_id, multisig_signers, local_signer_filter_out); + + // 2. collect available signers + std::vector available_signers; + available_signers.reserve(all_init_set_collections.size()); + + for (const auto &input_init_set_collection : all_init_set_collections) + available_signers.emplace_back(input_init_set_collection.first); + + // 3. available signers as a filter + multisig_signers_to_filter(available_signers, multisig_signers, available_signers_filter_out); + + // 4. available signers as individual filters (note: available_signers contains no duplicates because it's built + // from a map) + available_signers_as_filters_out.clear(); + available_signers_as_filters_out.reserve(available_signers.size()); + + for (const crypto::public_key &available_signer : available_signers) + { + multisig_signer_to_filter(available_signer, + multisig_signers, + available_signers_as_filters_out[available_signer]); + } + + // 5. filter permutations (every subgroup of signers that is eligible to make a signature attempt) + aggregate_multisig_signer_set_filter_to_permutations(threshold, + multisig_signers.size(), + multisig_proposal_aggregate_signer_set_filter, + filter_permutations_out); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static MultisigSigningErrorVariant try_make_v1_multisig_partial_signatures_v1( + const std::uint32_t threshold, + const signer_set_filter filter, + const std::unordered_map &proof_contexts, //[ proof key : proof message ] + const std::size_t num_expected_proof_basekeys, + const std::unordered_map &available_signers_as_filters, + //[ signer id : [ proof key : init set ] ] + const std::unordered_map> + &all_init_set_collections, + const std::unordered_map &signer_nonce_trackers, + const MultisigPartialSigMaker &partial_sig_maker, + const crypto::secret_key &local_signer_privkey, + MultisigNonceCache &nonce_record_inout, + std::unordered_map &partial_signatures_out) +{ + /// make partial signatures for one group of signers of size threshold that is presumed to include the local signer + + // 1. checks + CHECK_AND_ASSERT_THROW_MES(all_init_set_collections.size() >= threshold, + "make multisig partial signatures: there are fewer init sets than the signing threshold of the multisig group."); + CHECK_AND_ASSERT_THROW_MES(available_signers_as_filters.size() == all_init_set_collections.size(), + "make multisig partial signatures: available signers as filters don't line up with init sets (bug)."); + CHECK_AND_ASSERT_THROW_MES(signer_nonce_trackers.size() == all_init_set_collections.size(), + "make multisig partial signatures: signer nonce trackers don't line up with init sets (bug)."); + + // 2. try to make the partial signatures (if unable to make a partial signature on all requested proof contexts, + // then an error will be returned) + std::vector signer_pub_nonces_set_temp; + std::vector> split_signer_pub_nonce_sets_temp; + + partial_signatures_out.clear(); + + for (const auto &proof_context : proof_contexts) + { + // a. collect nonces from all signers in this signing group + split_signer_pub_nonce_sets_temp.clear(); + split_signer_pub_nonce_sets_temp.resize(num_expected_proof_basekeys); + + for (const auto &init_set_collection : all_init_set_collections) + { + // i. ignore unknown signers + if (available_signers_as_filters.find(init_set_collection.first) == available_signers_as_filters.end()) + continue; + if (signer_nonce_trackers.find(init_set_collection.first) == signer_nonce_trackers.end()) + continue; + + // ii. ignore signers not in the requested signing group + if ((available_signers_as_filters.at(init_set_collection.first) & filter) == 0) + continue; + + // iii. ignore unknown proof keys + if (init_set_collection.second.find(proof_context.first) == init_set_collection.second.end()) + continue; + + // iv. get public nonces from this init set collection, indexed by: + // - this signer's init set + // - select the proof we are working on (via this proof's proof key) + // - select the nonces that line up with the signer's nonce tracker (i.e. the nonces associated with this + // filter for this signer) + if (!try_get_nonces(init_set_collection.second.at(proof_context.first), + signer_nonce_trackers.at(init_set_collection.first), + signer_pub_nonces_set_temp)) + { + return MultisigSigningErrorBadInitSetCollection{ + .error_code = MultisigSigningErrorBadInitSetCollection::ErrorCode::GET_NONCES_FAIL, + .signer_id = init_set_collection.first + }; + } + + // v. expect nonce sets to be consistently sized + if (signer_pub_nonces_set_temp.size() != num_expected_proof_basekeys) + { + return MultisigSigningErrorBadInitSetCollection{ + .error_code = MultisigSigningErrorBadInitSetCollection::ErrorCode::INVALID_NONCES_SET_SIZE, + .signer_id = init_set_collection.first + }; + } + + // vi. save nonce sets; the set members are split between rows in the split_signer_pub_nonce_sets_temp matrix + for (std::size_t nonce_set_index{0}; nonce_set_index < num_expected_proof_basekeys; ++nonce_set_index) + { + split_signer_pub_nonce_sets_temp[nonce_set_index].emplace_back( + signer_pub_nonces_set_temp[nonce_set_index] + ); + } + } + + // b. sanity check + for (const std::vector &signer_pub_nonce_set : split_signer_pub_nonce_sets_temp) + { + if (signer_pub_nonce_set.size() != threshold) + { + return MultisigSigningErrorMakePartialSigSet{ + .error_code = MultisigSigningErrorMakePartialSigSet::ErrorCode::INVALID_NONCES_SET_QUANTITY, + .signature_set_filter = filter + }; + } + } + + // c. attempt making a partial signature for this: proof message, proof key, signer group (filter) + try + { + partial_sig_maker.attempt_make_partial_sig(proof_context.second, + proof_context.first, + filter, + split_signer_pub_nonce_sets_temp, + local_signer_privkey, + nonce_record_inout, + partial_signatures_out[proof_context.first]); + } + catch (const std::exception &exception) + { + return MultisigSigningErrorMakePartialSigSet{ + .error_code = MultisigSigningErrorMakePartialSigSet::ErrorCode::MAKE_SIGNATURE_EXCEPTION, + .signature_set_filter = filter, + .error_message = exception.what() + }; + } + catch (...) + { + return MultisigSigningErrorMakePartialSigSet{ + .error_code = MultisigSigningErrorMakePartialSigSet::ErrorCode::MAKE_SIGNATURE_EXCEPTION, + .signature_set_filter = filter, + .error_message = "unknown exception" + }; + } + } + + return boost::none; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void make_v1_multisig_partial_sig_sets_v1(const multisig_account &signer_account, + const std::unordered_map &proof_contexts, //[ proof key : proof message ] + const std::size_t num_expected_proof_basekeys, + const std::vector &filter_permutations, + const signer_set_filter local_signer_filter, + const signer_set_filter available_signers_filter, + //[ signer id : signer as filter ] + const std::unordered_map &available_signers_as_filters, + //[ signer id : [ proof key : init set ] ] + const std::unordered_map> + &all_init_set_collections, + const MultisigPartialSigMaker &partial_sig_maker, + std::list &multisig_errors_inout, + MultisigNonceCache &nonce_record_inout, + std::vector &partial_sig_sets_out) +{ + /// make partial signatures for every available group of signers of size threshold that includes the local signer + CHECK_AND_ASSERT_THROW_MES(signer_account.multisig_is_ready(), + "make multisig partial sigs: signer account is not complete, so it can't make partial signatures."); + + const std::size_t num_available_signers{available_signers_as_filters.size()}; + + // signer nonce trackers are pointers into the nonce vectors in each signer's init set + // - a signer's nonce vectors line up 1:1 with the filters in 'filter_permutations' of which the signer is a member + // - we want to track through each signers' vectors as we go through the full set of 'filter_permutations' + std::unordered_map signer_nonce_trackers; + signer_nonce_trackers.reserve(available_signers_as_filters.size()); + + for (const auto &available_signer_filter : available_signers_as_filters) + signer_nonce_trackers[available_signer_filter.first] = 0; + + // make partial signatures for each filter permutation + const std::uint32_t expected_num_partial_sig_sets{ + sp::math::n_choose_k(num_available_signers - 1, signer_account.get_threshold() - 1) + }; + partial_sig_sets_out.clear(); + partial_sig_sets_out.reserve(expected_num_partial_sig_sets); + + std::uint32_t num_aborted_partial_sig_sets{0}; + crypto::secret_key k_e_temp; + + for (const signer_set_filter filter : filter_permutations) + { + // for filters that contain only available signers (and include the local signer), make a partial signature set + // - throw on failure so the partial sig set can be rolled back + if ((filter & available_signers_filter) == filter && + (filter & local_signer_filter)) + { + // if this throws, then the signer's nonces for this filter/proposal/init_set combo that were used before + // the throw will be completely lost (i.e. in the 'nonce_record_inout'); however, if it does throw then + // this signing attempt was futile to begin with (it's all or nothing) + partial_sig_sets_out.emplace_back(); + try + { + // 1. get local signer's signing key for this group + if (!signer_account.try_get_aggregate_signing_key(filter, k_e_temp)) + { + multisig_errors_inout.emplace_back( + MultisigSigningErrorMakePartialSigSet{ + .error_code = MultisigSigningErrorMakePartialSigSet::ErrorCode::GET_KEY_FAIL, + .signature_set_filter = filter + } + ); + throw dummy_multisig_exception{}; + } + + // 2. try to make the partial sig set + if (auto make_sigs_error = try_make_v1_multisig_partial_signatures_v1(signer_account.get_threshold(), + filter, + proof_contexts, + num_expected_proof_basekeys, + available_signers_as_filters, + all_init_set_collections, + signer_nonce_trackers, + partial_sig_maker, + k_e_temp, + nonce_record_inout, + partial_sig_sets_out.back().partial_signatures)) + { + multisig_errors_inout.emplace_back(make_sigs_error); + throw dummy_multisig_exception{}; + } + + // 3. copy miscellanea + partial_sig_sets_out.back().signer_set_filter = filter; + partial_sig_sets_out.back().signer_id = signer_account.get_base_pubkey(); + + // 4. sanity check + check_v1_multisig_partial_sig_set_semantics_v1(partial_sig_sets_out.back(), signer_account.get_signers()); + } + catch (const dummy_multisig_exception&) + { + // no error message for dummy exceptions (error message recorded elsewhere) + + partial_sig_sets_out.pop_back(); + ++num_aborted_partial_sig_sets; + } + catch (const std::exception &exception) + { + multisig_errors_inout.emplace_back( + MultisigSigningErrorMakePartialSigSet{ + .error_code = MultisigSigningErrorMakePartialSigSet::ErrorCode::MAKE_SET_EXCEPTION, + .signature_set_filter = filter, + .error_message = exception.what() + } + ); + + partial_sig_sets_out.pop_back(); + ++num_aborted_partial_sig_sets; + } + catch (...) + { + multisig_errors_inout.emplace_back( + MultisigSigningErrorMakePartialSigSet{ + .error_code = MultisigSigningErrorMakePartialSigSet::ErrorCode::MAKE_SET_EXCEPTION, + .signature_set_filter = filter, + .error_message = "unknown exception" + } + ); + + partial_sig_sets_out.pop_back(); + ++num_aborted_partial_sig_sets; + } + } + + // increment nonce trackers for all signers in this filter + for (const auto &available_signer_filter : available_signers_as_filters) + { + if (available_signer_filter.second & filter) + ++signer_nonce_trackers[available_signer_filter.first]; + } + } + + // sanity check + CHECK_AND_ASSERT_THROW_MES(expected_num_partial_sig_sets - num_aborted_partial_sig_sets == + partial_sig_sets_out.size(), + "make multisig partial sig sets: did not produce expected number of partial sig sets (bug)."); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +void check_v1_multisig_init_set_semantics_v1(const MultisigProofInitSetV1 &init_set, + const std::uint32_t threshold, + const std::vector &multisig_signers, + const std::size_t num_expected_nonce_sets_per_proofkey) +{ + // 1. signer set filter must be valid (at least 'threshold' signers allowed, format is valid) + CHECK_AND_ASSERT_THROW_MES(validate_aggregate_multisig_signer_set_filter(threshold, + multisig_signers.size(), + init_set.aggregate_signer_set_filter), + "multisig init set semantics: invalid aggregate signer set filter."); + + // the init's signer must be in allowed signers list, and contained in the aggregate filter + CHECK_AND_ASSERT_THROW_MES(std::find(multisig_signers.begin(), multisig_signers.end(), init_set.signer_id) != + multisig_signers.end(), "multisig init set semantics: initializer from unknown signer."); + CHECK_AND_ASSERT_THROW_MES(signer_is_in_filter(init_set.signer_id, + multisig_signers, + init_set.aggregate_signer_set_filter), + "multisig init set semantics: signer is not eligible."); + + // 2. for each proof key to sign, there should be one nonce set (signing attempt) per signer subgroup that contains the + // signer + // - there are 'num signers requested' choose 'threshold' total signer subgroups who can participate in signing this + // proof + // - remove our init's signer, then choose 'threshold - 1' signers from the remaining 'num signers requested - 1' to + // get the number of permutations that include our init's signer + const std::uint32_t num_sets_with_signer_expected( + sp::math::n_choose_k(get_num_flags_set(init_set.aggregate_signer_set_filter) - 1, threshold - 1) + ); + + CHECK_AND_ASSERT_THROW_MES(init_set.inits.size() == num_sets_with_signer_expected, + "multisig init set semantics: don't have expected number of nonce sets (one per signer set that has signer)."); + + for (const std::vector &nonce_pubkey_set : init_set.inits) + { + CHECK_AND_ASSERT_THROW_MES(nonce_pubkey_set.size() == num_expected_nonce_sets_per_proofkey, + "multisig init set semantics: don't have expected number of nonce pubkey pairs (each proof key should have " + "(" << num_expected_nonce_sets_per_proofkey << ") nonce pubkey pairs)."); + } +} +//------------------------------------------------------------------------------------------------------------------- +MultisigSigningErrorVariant validate_v1_multisig_init_set_v1(const MultisigProofInitSetV1 &init_set, + const std::uint32_t threshold, + const std::vector &multisig_signers, + const signer_set_filter expected_aggregate_signer_set_filter, + const crypto::public_key &expected_signer_id, + const rct::key &expected_proof_message, + const rct::key &expected_main_proof_key, + const std::size_t num_expected_nonce_sets_per_proofkey) +{ + // 1. aggregate filter should match the expected aggregate filter + if (init_set.aggregate_signer_set_filter != expected_aggregate_signer_set_filter) + { + return MultisigSigningErrorBadInitSet{ + .error_code = MultisigSigningErrorBadInitSet::ErrorCode::UNEXPECTED_FILTER, + .aggregate_signer_set_filter = init_set.aggregate_signer_set_filter, + .signer_id = init_set.signer_id, + .proof_message = init_set.proof_message, + .proof_key = init_set.proof_key + }; + } + + // 2. signer should be expected + if (!(init_set.signer_id == expected_signer_id)) + { + return MultisigSigningErrorBadInitSet{ + .error_code = MultisigSigningErrorBadInitSet::ErrorCode::UNEXPECTED_SIGNER, + .aggregate_signer_set_filter = init_set.aggregate_signer_set_filter, + .signer_id = init_set.signer_id, + .proof_message = init_set.proof_message, + .proof_key = init_set.proof_key + }; + } + + // 3. proof message should be expected + if (!(init_set.proof_message == expected_proof_message)) + { + return MultisigSigningErrorBadInitSet{ + .error_code = MultisigSigningErrorBadInitSet::ErrorCode::UNEXPECTED_PROOF_MESSAGE, + .aggregate_signer_set_filter = init_set.aggregate_signer_set_filter, + .signer_id = init_set.signer_id, + .proof_message = init_set.proof_message, + .proof_key = init_set.proof_key + }; + } + + // 4. proof key should be expected + // NOTE: the relationship between the main proof key and any auxilliary/secondary keys must be implemented by the + // caller + if (!(init_set.proof_key == expected_main_proof_key)) + { + return MultisigSigningErrorBadInitSet{ + .error_code = MultisigSigningErrorBadInitSet::ErrorCode::UNEXPECTED_MAIN_PROOF_KEY, + .aggregate_signer_set_filter = init_set.aggregate_signer_set_filter, + .signer_id = init_set.signer_id, + .proof_message = init_set.proof_message, + .proof_key = init_set.proof_key + }; + } + + // 5. init set semantics must be valid + try + { + check_v1_multisig_init_set_semantics_v1(init_set, + threshold, + multisig_signers, + num_expected_nonce_sets_per_proofkey); + } + catch (const dummy_multisig_exception&) + { + // no error message for dummy exceptions (error message recorded elsewhere) + } + catch (const std::exception &exception) + { + return MultisigSigningErrorBadInitSet{ + .error_code = MultisigSigningErrorBadInitSet::ErrorCode::SEMANTICS_EXCEPTION, + .aggregate_signer_set_filter = init_set.aggregate_signer_set_filter, + .signer_id = init_set.signer_id, + .proof_message = init_set.proof_message, + .proof_key = init_set.proof_key, + .error_message = exception.what() + }; + } + catch (...) + { + return MultisigSigningErrorBadInitSet{ + .error_code = MultisigSigningErrorBadInitSet::ErrorCode::SEMANTICS_EXCEPTION, + .aggregate_signer_set_filter = init_set.aggregate_signer_set_filter, + .signer_id = init_set.signer_id, + .proof_message = init_set.proof_message, + .proof_key = init_set.proof_key, + .error_message = "unknown exception" + }; + } + + return boost::none; +} +//------------------------------------------------------------------------------------------------------------------- +MultisigSigningErrorVariant validate_v1_multisig_init_set_collection_v1( + const std::unordered_map &init_set_collection, //[ proof key : init set ] + const std::uint32_t threshold, + const std::vector &multisig_signers, + const signer_set_filter expected_aggregate_signer_set_filter, + const crypto::public_key &expected_signer_id, + const std::unordered_map &expected_proof_contexts, //[ proof key : proof message ] + const std::size_t num_expected_nonce_sets_per_proofkey) +{ + // 1. expect the init set collection was built for at least one proof context + if (expected_proof_contexts.size() == 0) + { + return MultisigSigningErrorBadInitSetCollection{ + .error_code = MultisigSigningErrorBadInitSetCollection::ErrorCode::EMPTY_COLLECTION_EXPECTED, + .signer_id = expected_signer_id + }; + } + + // 2. expect the same number of proof messages as init sets in the collection + if (init_set_collection.size() != expected_proof_contexts.size()) + { + return MultisigSigningErrorBadInitSetCollection{ + .error_code = MultisigSigningErrorBadInitSetCollection::ErrorCode::PROOF_CONTEXT_MISMATCH, + .signer_id = expected_signer_id + }; + } + + // 3. check that the init set collection maps to its internal proof keys correctly + if (!tools::keys_match_internal_values(init_set_collection, + [](const rct::key &key, const MultisigProofInitSetV1 &init_set) -> bool + { + return key == init_set.proof_key; + } + )) + { + return MultisigSigningErrorBadInitSetCollection{ + .error_code = MultisigSigningErrorBadInitSetCollection::ErrorCode::INVALID_MAPPING, + .signer_id = expected_signer_id + }; + } + + // 4. validate each init set in the input collection + for (const auto &init_set : init_set_collection) + { + // a. check that the init set has one of the expected messages + // note: using maps ensures the expected proof contexts line up 1:1 with init sets without duplicates + if (expected_proof_contexts.find(init_set.first) == expected_proof_contexts.end()) + { + return MultisigSigningErrorBadInitSetCollection{ + .error_code = MultisigSigningErrorBadInitSetCollection::ErrorCode::PROOF_CONTEXT_MISMATCH, + .signer_id = expected_signer_id + }; + } + + // b. validate the init set + if (auto validation_error = validate_v1_multisig_init_set_v1(init_set.second, + threshold, + multisig_signers, + expected_aggregate_signer_set_filter, + expected_signer_id, + expected_proof_contexts.at(init_set.first), + init_set.first, + num_expected_nonce_sets_per_proofkey)) + return validation_error; + } + + return boost::none; +} +//------------------------------------------------------------------------------------------------------------------- +void make_v1_multisig_init_set_v1(const std::uint32_t threshold, + const std::vector &multisig_signers, + const signer_set_filter aggregate_signer_set_filter, + const crypto::public_key &local_signer_id, + const rct::key &proof_message, + const rct::key &main_proof_key, + const rct::keyV &proof_key_base_points, + MultisigNonceCache &nonce_record_inout, + MultisigProofInitSetV1 &init_set_out) +{ + // 1. enforce canonical proof keys (NOTE: this is only a sanity check) + CHECK_AND_ASSERT_THROW_MES(sp::key_domain_is_prime_subgroup(main_proof_key), + "make multisig proof initializer: found proof key with non-canonical representation!"); + + for (const rct::key &proof_key_base_point : proof_key_base_points) + { + CHECK_AND_ASSERT_THROW_MES(sp::key_domain_is_prime_subgroup(proof_key_base_point), + "make multisig proof initializer: found proof key base point with non-canonical representation!"); + } + + // 2. prepare init nonce map + const std::uint32_t num_sets_with_signer_expected{ + sp::math::n_choose_k(get_num_flags_set(aggregate_signer_set_filter) - 1, threshold - 1) + }; + + init_set_out.inits.clear(); + init_set_out.inits.reserve(num_sets_with_signer_expected); + + // 3. add nonces for every possible signer set that includes the signer + CHECK_AND_ASSERT_THROW_MES(signer_is_in_filter(local_signer_id, multisig_signers, aggregate_signer_set_filter), + "make multisig proof initializer: local signer is not in signer list requested!"); + + std::vector filter_permutations; + aggregate_multisig_signer_set_filter_to_permutations(threshold, + multisig_signers.size(), + aggregate_signer_set_filter, + filter_permutations); + + for (const signer_set_filter filter : filter_permutations) + { + // a. ignore filters that don't include the signer + if (!signer_is_in_filter(local_signer_id, multisig_signers, filter)) + continue; + + // b. add new nonces to the nonce record for this combination + // - ignore failures to add nonces (re-using existing nonces is allowed) + // NOTE: the relationship between the main proof key and any auxilliary/secondary keys must be enforced + // by the caller (an init set can be used with any auxilliary keys, which may defy the caller's + // expectations) + nonce_record_inout.try_add_nonces(proof_message, main_proof_key, filter); + + // c. add nonces to the inits at this filter permutation for each requested proof base point + init_set_out.inits.emplace_back(); + init_set_out.inits.back().reserve(proof_key_base_points.size()); + + for (const rct::key &proof_base : proof_key_base_points) + { + CHECK_AND_ASSERT_THROW_MES(nonce_record_inout.try_get_nonce_pubkeys_for_base(proof_message, + main_proof_key, + filter, + proof_base, + tools::add_element(init_set_out.inits.back())), + "make multisig proof initializer: could not get nonce pubkeys from nonce record (bug)."); + } + } + + // 5. set cached context + init_set_out.aggregate_signer_set_filter = aggregate_signer_set_filter; + init_set_out.signer_id = local_signer_id; + init_set_out.proof_message = proof_message; + init_set_out.proof_key = main_proof_key; + + // 6. sanity check that the initializer is well-formed + check_v1_multisig_init_set_semantics_v1(init_set_out, threshold, multisig_signers, proof_key_base_points.size()); +} +//------------------------------------------------------------------------------------------------------------------- +void make_v1_multisig_init_set_collection_v1(const std::uint32_t threshold, + const std::vector &multisig_signers, + const signer_set_filter aggregate_signer_set_filter, + const crypto::public_key &local_signer_id, + const std::unordered_map &proof_contexts, //[ proof key : proof message ] + const std::unordered_map &proof_key_base_points, //[ proof key : {proof key base points} ] + MultisigNonceCache &nonce_record_inout, + std::unordered_map &init_set_collection_out) //[ proof key : init set ] +{ + // make an init set for every proof context provided + init_set_collection_out.clear(); + init_set_collection_out.reserve(proof_contexts.size()); + + for (const auto &proof_context : proof_contexts) + { + CHECK_AND_ASSERT_THROW_MES(proof_key_base_points.find(proof_context.first) != proof_key_base_points.end(), + "make multisig init set collection (v1): proof key base points map is missing a requested proof key."); + + make_v1_multisig_init_set_v1(threshold, + multisig_signers, + aggregate_signer_set_filter, + local_signer_id, + proof_context.second, + proof_context.first, + proof_key_base_points.at(proof_context.first), + nonce_record_inout, + init_set_collection_out[proof_context.first]); + } +} +//------------------------------------------------------------------------------------------------------------------- +void check_v1_multisig_partial_sig_set_semantics_v1(const MultisigPartialSigSetV1 &partial_sig_set, + const std::vector &multisig_signers) +{ + // 1. signer is in filter + CHECK_AND_ASSERT_THROW_MES(signer_is_in_filter(partial_sig_set.signer_id, + multisig_signers, + partial_sig_set.signer_set_filter), + "multisig partial sig set semantics: the signer is not a member of the signer group (or the filter is invalid)."); + + // 2. the partial signatures map to their proof keys properly + CHECK_AND_ASSERT_THROW_MES(tools::keys_match_internal_values(partial_sig_set.partial_signatures, + [](const rct::key &key, const MultisigPartialSigVariant &partial_sig) -> bool + { + return key == proof_key_ref(partial_sig); + } + ), + "multisig partial sig set semantics: a partial signature's mapped proof key does not match its stored key."); + + // 3. all partial sigs must have the same underlying type + CHECK_AND_ASSERT_THROW_MES(std::adjacent_find(partial_sig_set.partial_signatures.begin(), + partial_sig_set.partial_signatures.end(), + [](const auto &v1, const auto &v2) -> bool + { + // find an adjacent pair that DONT have the same type + return !MultisigPartialSigVariant::same_type(v1.second, v2.second); + }) == partial_sig_set.partial_signatures.end(), + "multisig partial sig set semantics: partial signatures are not all the same type."); +} +//------------------------------------------------------------------------------------------------------------------- +bool try_make_v1_multisig_partial_sig_sets_v1(const multisig_account &signer_account, + const cryptonote::account_generator_era expected_multisig_account_era, + const signer_set_filter aggregate_signer_set_filter, + const std::unordered_map &expected_proof_contexts, //[ proof key : proof message ] + const std::size_t num_expected_proof_basekeys, + const MultisigPartialSigMaker &partial_sig_maker, + //[ proof key : init set ] + std::unordered_map local_init_set_collection, + //[ signer id : [ proof key : init set ] ] + std::unordered_map> + other_init_set_collections, + std::list &multisig_errors_inout, + MultisigNonceCache &nonce_record_inout, + std::vector &partial_sig_sets_out) +{ + CHECK_AND_ASSERT_THROW_MES(signer_account.multisig_is_ready(), + "multisig input partial sigs: signer account is not complete, so it can't make partial signatures."); + CHECK_AND_ASSERT_THROW_MES(signer_account.get_era() == expected_multisig_account_era, + "multisig input partial sigs: signer account does not have the expected account era."); + + partial_sig_sets_out.clear(); + + // if there are no proof contexts to sign, then we succeed 'automatically' + if (expected_proof_contexts.size() == 0) + return true; + + + /// prepare pieces to use below + + // 1. misc. from account + const std::uint32_t threshold{signer_account.get_threshold()}; + const std::vector &multisig_signers{signer_account.get_signers()}; + const crypto::public_key &local_signer_id{signer_account.get_base_pubkey()}; + + // 2. validate and assemble all inits + std::unordered_map> + all_init_set_collections; //[ signer id : [ proof key : init set ] ] + + prepare_multisig_init_set_collections_v1(threshold, + multisig_signers, + aggregate_signer_set_filter, + local_signer_id, + expected_proof_contexts, + num_expected_proof_basekeys, + std::move(local_init_set_collection), + std::move(other_init_set_collections), + multisig_errors_inout, + all_init_set_collections); + + // 3. prepare filters for signing + signer_set_filter local_signer_filter; + signer_set_filter available_signers_filter; + std::unordered_map available_signers_as_filters; + std::vector filter_permutations; + + prepare_filters_for_multisig_partial_signing(threshold, + multisig_signers, + local_signer_id, + aggregate_signer_set_filter, + all_init_set_collections, + local_signer_filter, + available_signers_filter, + available_signers_as_filters, + filter_permutations); + + // 4. check how the available signers line up against the signers allowed to participate in this multisig ceremony + // note: signers not permitted by the ceremony should not make it this far, but we record them just in case; the + // partial signature maker will ignore them + if (available_signers_filter != aggregate_signer_set_filter) + { + multisig_errors_inout.emplace_back( + MultisigSigningErrorAvailableSigners{ + .error_code = MultisigSigningErrorAvailableSigners::ErrorCode::INCOMPLETE_AVAILABLE_SIGNERS, + .missing_signers = + static_cast( + (~available_signers_filter) & aggregate_signer_set_filter + ), + .unexpected_available_signers = + static_cast( + (~aggregate_signer_set_filter) & available_signers_filter + ) + } + ); + } + + + /// give up if not enough signers provided material to initialize a signature + if (available_signers_as_filters.size() < threshold) + return false; + + + /// make partial signature sets + make_v1_multisig_partial_sig_sets_v1(signer_account, + expected_proof_contexts, + num_expected_proof_basekeys, + filter_permutations, + local_signer_filter, + available_signers_filter, + available_signers_as_filters, + all_init_set_collections, + partial_sig_maker, + multisig_errors_inout, + nonce_record_inout, + partial_sig_sets_out); + + if (partial_sig_sets_out.size() == 0) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +void filter_multisig_partial_signatures_for_combining_v1(const std::vector &multisig_signers, + const std::unordered_map &allowed_proof_contexts, //[ proof key : proof message ] + const int expected_partial_sig_variant_index, + const std::unordered_map> &partial_sigs_per_signer, + std::list &multisig_errors_inout, + std::unordered_map>> &collected_sigs_per_key_per_filter_out) +{ + collected_sigs_per_key_per_filter_out.clear(); + + // filter the partial signatures passed in into the 'collected sigs' output map + std::unordered_map> collected_signers_per_filter; + + if (partial_sigs_per_signer.size() > 0) + { + // estimate total number of filters: num filters for the first signer * num signers available + // note: we optimize performance for non-adversarial multisig interactions, which should be the norm + collected_signers_per_filter.reserve( + partial_sigs_per_signer.begin()->second.size() * partial_sigs_per_signer.size() + ); + } + + for (const auto &partial_sigs_for_signer : partial_sigs_per_signer) + { + for (const MultisigPartialSigSetV1 &partial_sig_set : partial_sigs_for_signer.second) + { + // a. skip sig sets that are invalid + try { check_v1_multisig_partial_sig_set_semantics_v1(partial_sig_set, multisig_signers); } + catch (const std::exception &exception) + { + multisig_errors_inout.emplace_back( + MultisigSigningErrorBadPartialSigSet{ + .error_code = MultisigSigningErrorBadPartialSigSet::ErrorCode::SEMANTICS_EXCEPTION, + .signature_set_filter = partial_sig_set.signer_set_filter, + .signer_id = partial_sig_set.signer_id, + .error_message = exception.what() + } + ); + + continue; + } + catch (...) + { + multisig_errors_inout.emplace_back( + MultisigSigningErrorBadPartialSigSet{ + .error_code = MultisigSigningErrorBadPartialSigSet::ErrorCode::SEMANTICS_EXCEPTION, + .signature_set_filter = partial_sig_set.signer_set_filter, + .signer_id = partial_sig_set.signer_id, + .error_message = "unknown exception" + } + ); + + continue; + } + + // b. skip sig sets that don't map to their signer ids properly + if (!(partial_sig_set.signer_id == partial_sigs_for_signer.first)) + { + multisig_errors_inout.emplace_back( + MultisigSigningErrorBadPartialSigSet{ + .error_code = MultisigSigningErrorBadPartialSigSet::ErrorCode::INVALID_MAPPING, + .signature_set_filter = partial_sig_set.signer_set_filter, + .signer_id = partial_sig_set.signer_id + } + ); + + continue; + } + + // c. skip sig sets that look like duplicates (same signer group and signer) + // - do this after checking sig set validity to avoid inserting invalid filters into the collected signers + // map, which could allow a malicious signer to block signer groups they aren't a member of + if (collected_signers_per_filter[partial_sig_set.signer_set_filter].find(partial_sig_set.signer_id) != + collected_signers_per_filter[partial_sig_set.signer_set_filter].end()) + continue; + + // d. record the partial sigs + collected_sigs_per_key_per_filter_out[partial_sig_set.signer_set_filter] + .reserve(partial_sig_set.partial_signatures.size()); + + for (const auto &partial_sig : partial_sig_set.partial_signatures) + { + // i. skip partial sigs with unknown proof keys + if (allowed_proof_contexts.find(partial_sig.first) == allowed_proof_contexts.end()) + { + multisig_errors_inout.emplace_back( + MultisigSigningErrorBadPartialSig{ + .error_code = + MultisigSigningErrorBadPartialSig::ErrorCode::UNEXPECTED_MAIN_PROOF_KEY, + .proof_key = proof_key_ref(partial_sig.second), + .proof_message = message_ref(partial_sig.second) + } + ); + + continue; + } + + // ii. skip sig sets with unexpected proof messages + if (!(allowed_proof_contexts.at(partial_sig.first) == message_ref(partial_sig.second))) + { + multisig_errors_inout.emplace_back( + MultisigSigningErrorBadPartialSig{ + .error_code = + MultisigSigningErrorBadPartialSig::ErrorCode::UNEXPECTED_PROOF_MESSAGE, + .proof_key = proof_key_ref(partial_sig.second), + .proof_message = message_ref(partial_sig.second) + } + ); + + continue; + } + + // iii. skip partial sigs with unexpected internal variant type + if (partial_sig.second.index() != expected_partial_sig_variant_index) + { + multisig_errors_inout.emplace_back( + MultisigSigningErrorBadPartialSig{ + .error_code = + MultisigSigningErrorBadPartialSig::ErrorCode::UNEXPECTED_VARIANT_TYPE, + .proof_key = proof_key_ref(partial_sig.second), + .proof_message = message_ref(partial_sig.second) + } + ); + + continue; + } + + // iv. add this signer's partial signature for this proof key for this signer group + collected_sigs_per_key_per_filter_out[partial_sig_set.signer_set_filter][partial_sig.first] + .emplace_back(partial_sig.second); + } + + // e. record that this signer/filter combo has been used + collected_signers_per_filter[partial_sig_set.signer_set_filter].insert(partial_sig_set.signer_id); + } + } +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace multisig diff --git a/src/multisig/multisig_signing_helper_utils.h b/src/multisig/multisig_signing_helper_utils.h new file mode 100644 index 0000000000..11cc1fbebb --- /dev/null +++ b/src/multisig/multisig_signing_helper_utils.h @@ -0,0 +1,330 @@ +// Copyright (c) 2022, 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 to assist with multisig signing ceremonies. +// IMPORTANT: These utilities should enforce strong guarantees about signer ID consistency. It is imperative that +// a malicious signer not be allowed to pretend they are a different signer or part of signer subgroup +// they aren't actually a member of. + +#pragma once + +//local headers +#include "common/container_helpers.h" +#include "crypto/crypto.h" +#include "multisig_account.h" +#include "multisig_signer_set_filter.h" +#include "multisig_signing_errors.h" +#include "multisig_signing_helper_types.h" +#include "ringct/rctTypes.h" + +//third party headers + +//standard headers +#include +#include +#include +#include + +//forward declarations +namespace multisig +{ + class MultisigNonceCache; + class MultisigPartialSigMaker; +} + +namespace multisig +{ + +/** +* brief: check_v1_multisig_init_set_semantics_v1 - check semantics of a multisig initializer set +* - throws if a check fails +* param: init_set - +* param: threshold - +* param: multisig_signers - +* param: num_expected_nonce_sets_per_proofkey - +*/ +void check_v1_multisig_init_set_semantics_v1(const MultisigProofInitSetV1 &init_set, + const std::uint32_t threshold, + const std::vector &multisig_signers, + const std::size_t num_expected_nonce_sets_per_proofkey); +/** +* brief: validate_v1_multisig_init_set_v1 - validate a multisig init set (non-throwing) +* param: init_set - +* param: threshold - +* param: multisig_signers - +* param: expected_aggregate_signer_set_filter - +* param: expected_signer_id - +* param: expected_proof_message - +* param: expected_main_proof_key - +* param: num_expected_nonce_sets_per_proofkey - +* return: empty variant if the init set is valid, otherwise a multisig error +*/ +MultisigSigningErrorVariant validate_v1_multisig_init_set_v1(const MultisigProofInitSetV1 &init_set, + const std::uint32_t threshold, + const std::vector &multisig_signers, + const signer_set_filter expected_aggregate_signer_set_filter, + const crypto::public_key &expected_signer_id, + const rct::key &expected_proof_message, + const rct::key &expected_main_proof_key, + const std::size_t num_expected_nonce_sets_per_proofkey); +MultisigSigningErrorVariant validate_v1_multisig_init_set_collection_v1( + const std::unordered_map &init_set_collection, //[ proof key : init set ] + const std::uint32_t threshold, + const std::vector &multisig_signers, + const signer_set_filter expected_aggregate_signer_set_filter, + const crypto::public_key &expected_signer_id, + const std::unordered_map &expected_proof_contexts, //[ proof key : proof message ] + const std::size_t num_expected_nonce_sets_per_proofkey); +/** +* brief: make_v1_multisig_init_set_v1 - make a multisig initialization set for specified proof info +* param: threshold - +* param: multisig_signers - +* param: aggregate_signer_set_filter - +* param: local_signer_id - +* param: proof_message - +* param: main_proof_key - +* param: proof_key_base_points - +* inoutparam: nonce_record_inout - +* outparam: init_set_out - +*/ +void make_v1_multisig_init_set_v1(const std::uint32_t threshold, + const std::vector &multisig_signers, + const signer_set_filter aggregate_signer_set_filter, + const crypto::public_key &local_signer_id, + const rct::key &proof_message, + const rct::key &main_proof_key, + const rct::keyV &proof_key_base_points, + MultisigNonceCache &nonce_record_inout, + MultisigProofInitSetV1 &init_set_out); +void make_v1_multisig_init_set_collection_v1(const std::uint32_t threshold, + const std::vector &multisig_signers, + const signer_set_filter aggregate_signer_set_filter, + const crypto::public_key &local_signer_id, + const std::unordered_map &proof_contexts, //[ proof key : proof message ] + const std::unordered_map &proof_key_base_points, //[ proof key : {proof key base points} ] + MultisigNonceCache &nonce_record_inout, + std::unordered_map &init_set_collection_out); //[ proof key : init set ] +/** +* brief: check_v1_multisig_partial_sig_set_semantics_v1 - check semantics of a multisig partial signature set +* - throws if a check fails +* param: partial_sig_set - +* param: multisig_signers - +*/ +void check_v1_multisig_partial_sig_set_semantics_v1(const MultisigPartialSigSetV1 &partial_sig_set, + const std::vector &multisig_signers); +/** +* brief: try_make_v1_multisig_partial_sig_sets_v1 - try to make multisig partial signature sets with an injected partial +* sig maker +* - weak preconditions: ignores invalid initializers from non-local signers +* - will throw if local signer is not in the aggregate signer filter (or has an invalid initializer) +* - will only return true if at least one partial sig set can be made containing a partial sig for each of the +* requested proof contexts +* param: signer_account - +* param: expected_multisig_account_era - +* param: aggregate_signer_set_filter - +* param: expected_proof_contexts - +* param: num_expected_proof_basekeys - +* param: partial_sig_maker - +* param: local_init_set_collection - +* param: other_init_set_collections - +* inoutparam: multisig_errors_inout - +* inoutparam: nonce_record_inout - +* outparam: partial_sig_sets_out - +*/ +bool try_make_v1_multisig_partial_sig_sets_v1(const multisig_account &signer_account, + const cryptonote::account_generator_era expected_multisig_account_era, + const signer_set_filter aggregate_signer_set_filter, + const std::unordered_map &expected_proof_contexts, //[ proof key : proof message ] + const std::size_t num_expected_proof_basekeys, + const MultisigPartialSigMaker &partial_sig_maker, + //[ proof key : init set ] + std::unordered_map local_init_set_collection, + //[ signer id : [ proof key : init set ] ] + std::unordered_map> + other_init_set_collections, + std::list &multisig_errors_inout, + MultisigNonceCache &nonce_record_inout, + std::vector &partial_sig_sets_out); +/** +* brief: filter_multisig_partial_signatures_for_combining_v1 - filter multisig partial signature sets into a convenient +* map for combining them into complete signatures +* - weak preconditions: ignores signature sets that don't conform to expectations +* param: multisig_signers - +* param: allowed_proof_contexts - +* param: expected_partial_sig_variant_index - +* param: partial_sigs_per_signer - +* inoutparam: multisig_errors_inout - +* outparam: collected_sigs_per_key_per_filter_out - +*/ +void filter_multisig_partial_signatures_for_combining_v1(const std::vector &multisig_signers, + const std::unordered_map &allowed_proof_contexts, //[ proof key : proof message ] + const int expected_partial_sig_variant_index, + const std::unordered_map> &partial_sigs_per_signer, + std::list &multisig_errors_inout, + std::unordered_map>> &collected_sigs_per_key_per_filter_out); +/** +* brief: collect_partial_sigs_v1 - unwrap multisig partial signatures +* type: PartialSigT - +* param: type_erased_partial_sigs - +* outparam: partial_sigs_out - +*/ +template +void collect_partial_sigs_v1(const std::vector &type_erased_partial_sigs, + std::vector &partial_sigs_out) +{ + partial_sigs_out.clear(); + partial_sigs_out.reserve(type_erased_partial_sigs.size()); + + for (const MultisigPartialSigVariant &type_erased_partial_sig : type_erased_partial_sigs) + { + // skip partial signatures of undesired types + if (!type_erased_partial_sig.is_type()) + continue; + + partial_sigs_out.emplace_back(type_erased_partial_sig.unwrap()); + } +} +/** +* brief: try_assemble_multisig_partial_sigs - try to combine multisig partial signatures into full signatures of +* type ResultSigT using an injected function for merging partial signatures +* - takes as input a set of {proof key, {partial signatures}} pairs, and only succeeds if each of those pairs can be +* resolved to a complete signature +* type: PartialSigT - +* type: ResultSigT - +* param: collected_sigs_per_key - +* param: try_assemble_partial_sigs_func - +* outparam: result_sigs_out - +* return: true if a complete signature was assembled for every {proof key, {partial signatures}} pair passed in +*/ +template +bool try_assemble_multisig_partial_sigs( + //[ proof key : partial signatures ] + const std::unordered_map> &collected_sigs_per_key, + const std::function&, ResultSigT&)> + &try_assemble_partial_sigs_func, + std::vector &result_sigs_out) +{ + result_sigs_out.clear(); + result_sigs_out.reserve(collected_sigs_per_key.size()); + std::vector partial_sigs_temp; + + for (const auto &proof_key_and_partial_sigs : collected_sigs_per_key) + { + // a. convert type-erased partial sigs to the type we want + collect_partial_sigs_v1(proof_key_and_partial_sigs.second, partial_sigs_temp); + + // b. try to make the contextual signature + if (!try_assemble_partial_sigs_func(proof_key_and_partial_sigs.first, + partial_sigs_temp, + tools::add_element(result_sigs_out))) + return false; + } + + return true; +} +/** +* brief: try_assemble_multisig_partial_sigs_signer_group_attempts - try to combine multisig partial signatures into full +* signatures of type ResultSigT using an injected function for merging partial signatures; makes attempts for +* multiple signer groups +* - note: it is the responsibility of the caller to validate the 'collected_sigs_per_key_per_filter' map; failing to +* validate it could allow a malicious signer to pollute the signature attempts of signer subgroups they aren't +* a member of, or lead to unexpected failures where the signatures output from here are invalid according to +* a broader context (e.g. undesired proof keys or proof messages, etc.) +* type: PartialSigT - +* type: ResultSigT - +* param: num_expected_completed_sigs - +* param: collected_sigs_per_key_per_filter - +* param: try_assemble_partial_sigs_func - +* inoutparam: multisig_errors_inout - +* outparam: result_sigs_out - +* return: true if the requested number of signatures were assembled (e.g. one per proof key represented in the +* collected_sigs_per_key_per_filter input) +*/ +template +bool try_assemble_multisig_partial_sigs_signer_group_attempts(const std::size_t num_expected_completed_sigs, + const std::unordered_map>> &collected_sigs_per_key_per_filter, + const std::function&, ResultSigT&)> + &try_assemble_partial_sigs_func, + std::list &multisig_errors_inout, + std::vector &result_sigs_out) +{ + // try to assemble a collection of signatures from partial signatures provided by different signer groups + // - all-or-nothing: a signer group must produce the expected number of completed signatures for their signatures + // to be used + std::vector partial_sigs_temp; + + for (const auto &signer_group_partial_sigs : collected_sigs_per_key_per_filter) + { + // a. skip this signer group if it doesn't have enough proof keys + if (signer_group_partial_sigs.second.size() != num_expected_completed_sigs) + { + multisig_errors_inout.emplace_back( + MultisigSigningErrorBadSigAssembly{ + .error_code = MultisigSigningErrorBadSigAssembly::ErrorCode::PROOF_KEYS_MISMATCH, + .signer_set_filter = signer_group_partial_sigs.first + } + ); + continue; + } + + // b. try to assemble the set of signatures that this signer group is working on + if (try_assemble_multisig_partial_sigs(signer_group_partial_sigs.second, + try_assemble_partial_sigs_func, + result_sigs_out) + && + result_sigs_out.size() == num_expected_completed_sigs) + break; + else + { + multisig_errors_inout.emplace_back( + MultisigSigningErrorBadSigAssembly{ + .error_code = MultisigSigningErrorBadSigAssembly::ErrorCode::SIG_ASSEMBLY_FAIL, + .signer_set_filter = signer_group_partial_sigs.first + } + ); + } + } + + if (result_sigs_out.size() != num_expected_completed_sigs) + { + multisig_errors_inout.emplace_back( + MultisigSigningErrorBadSigSet{ + .error_code = MultisigSigningErrorBadSigSet::ErrorCode::INVALID_SIG_SET + } + ); + return false; + } + + return true; +} + +} //namespace multisig diff --git a/src/multisig/multisig_sp_composition_proof.cpp b/src/multisig/multisig_sp_composition_proof.cpp new file mode 100644 index 0000000000..5faf5eec39 --- /dev/null +++ b/src/multisig/multisig_sp_composition_proof.cpp @@ -0,0 +1,346 @@ +// Copyright (c) 2022, 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 "multisig_sp_composition_proof.h" + +//local headers +#include "common/container_helpers.h" +#include "crypto/crypto.h" +extern "C" +{ +#include "crypto/crypto-ops.h" +} +#include "crypto/generators.h" +#include "cryptonote_config.h" +#include "misc_language.h" +#include "misc_log_ex.h" +#include "multisig_nonce_cache.h" +#include "ringct/rctOps.h" +#include "ringct/rctTypes.h" +#include "seraphis_crypto/sp_composition_proof.h" +#include "seraphis_crypto/sp_crypto_utils.h" +#include "seraphis_crypto/sp_hash_functions.h" +#include "seraphis_crypto/sp_transcript.h" + +//third party headers + +//standard headers +#include +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "multisig" + +namespace multisig +{ +//------------------------------------------------------------------------------------------------------------------- +// MuSig2-style bi-nonce signing merge factor +// rho_e = H_n(m, alpha_1_1*U, alpha_2_1*U, ..., alpha_1_N*U, alpha_2_N*U) +//------------------------------------------------------------------------------------------------------------------- +static rct::key multisig_binonce_merge_factor(const rct::key &message, const std::vector &nonces) +{ + // build hash + sp::SpKDFTranscript transcript{ + config::HASH_KEY_MULTISIG_BINONCE_MERGE_FACTOR, + sizeof(rct::key) + nonces.size() * multisig_pub_nonces_size_bytes() + }; + transcript.append("message", message); + transcript.append("nonces", nonces); + + rct::key merge_factor; + sp::sp_hash_to_scalar(transcript.data(), transcript.size(), merge_factor.bytes); + CHECK_AND_ASSERT_THROW_MES(sc_isnonzero(merge_factor.bytes), + "multisig sp composition proof: binonce merge factor must be nonzero!"); + + return merge_factor; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void signer_nonces_mul8(const MultisigPubNonces &signer_pub_nonce_pair, MultisigPubNonces &nonce_pair_mul8_out) +{ + nonce_pair_mul8_out.signature_nonce_1_pub = rct::scalarmult8(signer_pub_nonce_pair.signature_nonce_1_pub); + nonce_pair_mul8_out.signature_nonce_2_pub = rct::scalarmult8(signer_pub_nonce_pair.signature_nonce_2_pub); + + CHECK_AND_ASSERT_THROW_MES(!(nonce_pair_mul8_out.signature_nonce_1_pub == rct::identity()), + "multisig sp composition proof: bad signer nonce (alpha_1 identity)!"); + CHECK_AND_ASSERT_THROW_MES(!(nonce_pair_mul8_out.signature_nonce_2_pub == rct::identity()), + "multisig sp composition proof: bad signer nonce (alpha_2 identity)!"); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +void make_sp_composition_multisig_proposal(const rct::key &message, + const rct::key &K, + const crypto::key_image &KI, + SpCompositionProofMultisigProposal &proposal_out) +{ + /// assemble proposal + proposal_out.message = message; + proposal_out.K = K; + proposal_out.KI = KI; + + rct::key dummy; + sp::generate_proof_nonce(K, proposal_out.signature_nonce_K_t1, dummy); + sp::generate_proof_nonce(rct::G, proposal_out.signature_nonce_K_t2, dummy); +} +//------------------------------------------------------------------------------------------------------------------- +void make_sp_composition_multisig_partial_sig(const SpCompositionProofMultisigProposal &proposal, + const crypto::secret_key &x, + const crypto::secret_key &y, + const crypto::secret_key &z_e, + const std::vector &signer_pub_nonces, + const crypto::secret_key &local_nonce_1_priv, + const crypto::secret_key &local_nonce_2_priv, + SpCompositionProofMultisigPartial &partial_sig_out) +{ + /// input checks and initialization + const std::size_t num_signers{signer_pub_nonces.size()}; + + CHECK_AND_ASSERT_THROW_MES(!(proposal.K == rct::identity()), + "make sp composition multisig partial sig: bad proof key (K identity)!"); + CHECK_AND_ASSERT_THROW_MES(!(rct::ki2rct(proposal.KI) == rct::identity()), + "make sp composition multisig partial sig: bad proof key (KI identity)!"); + CHECK_AND_ASSERT_THROW_MES(sc_isnonzero(to_bytes(proposal.signature_nonce_K_t1)), + "make sp composition multisig partial sig: bad private key (proposal nonce K_t1 zero)!"); + CHECK_AND_ASSERT_THROW_MES(sc_check(to_bytes(proposal.signature_nonce_K_t1)) == 0, + "make sp composition multisig partial sig: bad private key (proposal nonce K_t1)!"); + CHECK_AND_ASSERT_THROW_MES(sc_isnonzero(to_bytes(proposal.signature_nonce_K_t2)), + "make sp composition multisig partial sig: bad private key (proposal nonce K_t2 zero)!"); + CHECK_AND_ASSERT_THROW_MES(sc_check(to_bytes(proposal.signature_nonce_K_t2)) == 0, + "make sp composition multisig partial sig: bad private key (proposal nonce K_t2)!"); + + CHECK_AND_ASSERT_THROW_MES(sc_isnonzero(to_bytes(x)), + "make sp composition multisig partial sig: bad private key (x zero)!"); + CHECK_AND_ASSERT_THROW_MES(sc_check(to_bytes(x)) == 0, + "make sp composition multisig partial sig: bad private key (x)!"); + CHECK_AND_ASSERT_THROW_MES(sc_isnonzero(to_bytes(y)), + "make sp composition multisig partial sig: bad private key (y zero)!"); + CHECK_AND_ASSERT_THROW_MES(sc_check(to_bytes(y)) == 0, + "make sp composition multisig partial sig: bad private key (y)!"); + CHECK_AND_ASSERT_THROW_MES(sc_isnonzero(to_bytes(z_e)), + "make sp composition multisig partial sig: bad private key (z_e zero)!"); + CHECK_AND_ASSERT_THROW_MES(sc_check(to_bytes(z_e)) == 0, + "make sp composition multisig partial sig: bad private key (z)!"); + + CHECK_AND_ASSERT_THROW_MES(sc_check(to_bytes(local_nonce_1_priv)) == 0, + "make sp composition multisig partial sig: bad private key (local_nonce_1_priv)!"); + CHECK_AND_ASSERT_THROW_MES(sc_isnonzero(to_bytes(local_nonce_1_priv)), + "make sp composition multisig partial sig: bad private key (local_nonce_1_priv zero)!"); + CHECK_AND_ASSERT_THROW_MES(sc_check(to_bytes(local_nonce_2_priv)) == 0, + "make sp composition multisig partial sig: bad private key (local_nonce_2_priv)!"); + CHECK_AND_ASSERT_THROW_MES(sc_isnonzero(to_bytes(local_nonce_2_priv)), + "make sp composition multisig partial sig: bad private key (local_nonce_2_priv zero)!"); + + // prepare participant nonces + std::vector signer_pub_nonces_mul8; + signer_pub_nonces_mul8.reserve(num_signers); + + for (const MultisigPubNonces &signer_pub_nonce_pair : signer_pub_nonces) + signer_nonces_mul8(signer_pub_nonce_pair, tools::add_element(signer_pub_nonces_mul8)); + + // sort participant nonces so binonce merge factor is deterministic + std::sort(signer_pub_nonces_mul8.begin(), signer_pub_nonces_mul8.end()); + + // check that the local signer's signature opening is in the input set of opening nonces + MultisigPubNonces local_nonce_pubs; + const rct::key U_gen{rct::pk2rct(crypto::get_U())}; + rct::scalarmultKey(local_nonce_pubs.signature_nonce_1_pub, U_gen, rct::sk2rct(local_nonce_1_priv)); + rct::scalarmultKey(local_nonce_pubs.signature_nonce_2_pub, U_gen, rct::sk2rct(local_nonce_2_priv)); + + CHECK_AND_ASSERT_THROW_MES( + std::find(signer_pub_nonces_mul8.begin(), signer_pub_nonces_mul8.end(), local_nonce_pubs) != + signer_pub_nonces_mul8.end(), + "make sp composition multisig partial sig: local signer's opening nonces not in input set!"); + + + /// prepare partial signature + + // set partial sig pieces + partial_sig_out.message = proposal.message; + partial_sig_out.K = proposal.K; + partial_sig_out.KI = proposal.KI; + + // make K_t1 = (1/8) * (1/y) * K + sp::composition_proof_detail::compute_K_t1_for_proof(y, proposal.K, partial_sig_out.K_t1); + + + /// challenge message and binonce merge factor + // rho = H_n(m, {alpha_ki_1_e * U}, {alpha_ki_2_e * U}) (binonce merge factor) + const rct::key m{ + sp::composition_proof_detail::compute_challenge_message(partial_sig_out.message, + partial_sig_out.K, + partial_sig_out.KI, + partial_sig_out.K_t1) + }; + + const rct::key binonce_merge_factor{multisig_binonce_merge_factor(m, signer_pub_nonces_mul8)}; + + + /// signature openers + + // alpha_t1 * K + rct::key alpha_t1_pub; + rct::scalarmultKey(alpha_t1_pub, partial_sig_out.K, rct::sk2rct(proposal.signature_nonce_K_t1)); + + // alpha_t2 * G + rct::key alpha_t2_pub; + rct::scalarmultKey(alpha_t2_pub, rct::G, rct::sk2rct(proposal.signature_nonce_K_t2)); + + // alpha_ki * U + // - MuSig2-style merged nonces from all multisig participants + + // alpha_ki_1 * U = sum_e(alpha_ki_1_e * U) + // alpha_ki_2 * U = rho * sum_e(alpha_ki_2_e * U) + rct::key alpha_ki_1_pub{rct::identity()}; + rct::key alpha_ki_2_pub{rct::identity()}; + + for (const MultisigPubNonces &nonce_pair : signer_pub_nonces_mul8) + { + rct::addKeys(alpha_ki_1_pub, alpha_ki_1_pub, nonce_pair.signature_nonce_1_pub); + rct::addKeys(alpha_ki_2_pub, alpha_ki_2_pub, nonce_pair.signature_nonce_2_pub); + } + + rct::scalarmultKey(alpha_ki_2_pub, alpha_ki_2_pub, binonce_merge_factor); //rho * sum_e(alpha_ki_2_e * U) + + // alpha_ki * U = alpha_ki_1 * U + alpha_ki_2 * U + const rct::key alpha_ki_pub{rct::addKeys(alpha_ki_1_pub, alpha_ki_2_pub)}; + + + /// compute proof challenge + partial_sig_out.c = sp::composition_proof_detail::compute_challenge(m, alpha_t1_pub, alpha_t2_pub, alpha_ki_pub); + + + /// responses + crypto::secret_key merged_nonce_KI_priv; //alpha_1_local + rho * alpha_2_local + sc_muladd(to_bytes(merged_nonce_KI_priv), + to_bytes(local_nonce_2_priv), + binonce_merge_factor.bytes, + to_bytes(local_nonce_1_priv)); + + sp::composition_proof_detail::compute_responses(partial_sig_out.c, + rct::sk2rct(proposal.signature_nonce_K_t1), + rct::sk2rct(proposal.signature_nonce_K_t2), + rct::sk2rct(merged_nonce_KI_priv), //for partial signature + x, + y, + z_e, //for partial signature + partial_sig_out.r_t1, + partial_sig_out.r_t2, + partial_sig_out.r_ki_partial //partial response + ); +} +//------------------------------------------------------------------------------------------------------------------- +bool try_make_sp_composition_multisig_partial_sig(const SpCompositionProofMultisigProposal &proposal, + const crypto::secret_key &x, + const crypto::secret_key &y, + const crypto::secret_key &z_e, + const std::vector &signer_pub_nonces, + const signer_set_filter filter, + MultisigNonceCache &nonce_record_inout, + SpCompositionProofMultisigPartial &partial_sig_out) +{ + // get the nonce privkeys to sign with + crypto::secret_key nonce_privkey_1; + crypto::secret_key nonce_privkey_2; + if (!nonce_record_inout.try_get_recorded_nonce_privkeys(proposal.message, + proposal.K, + filter, + nonce_privkey_1, + nonce_privkey_2)) + return false; + + // make the partial signature + SpCompositionProofMultisigPartial partial_sig_temp; + make_sp_composition_multisig_partial_sig(proposal, + x, + y, + z_e, + signer_pub_nonces, + nonce_privkey_1, + nonce_privkey_2, + partial_sig_temp); + + // clear the used nonces + CHECK_AND_ASSERT_THROW_MES(nonce_record_inout.try_remove_record(proposal.message, proposal.K, filter), + "try make sp composition proof multisig partial sig: failed to clear nonces from nonce record (aborting partial " + "signature)!"); + + // set the output partial sig AFTER used nonces are cleared, in case of exception + partial_sig_out = std::move(partial_sig_temp); + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +void finalize_sp_composition_multisig_proof(const std::vector &partial_sigs, + sp::SpCompositionProof &proof_out) +{ + /// input checks + CHECK_AND_ASSERT_THROW_MES(partial_sigs.size() > 0, + "finalize sp composition multisig proof: no partial signatures to make a proof out of!"); + + // common parts between partial signatures should match + for (const SpCompositionProofMultisigPartial &partial_sig : partial_sigs) + { + CHECK_AND_ASSERT_THROW_MES(partial_sigs[0].message == partial_sig.message, + "finalize sp composition multisig proof: input partial sigs don't match!"); + CHECK_AND_ASSERT_THROW_MES(partial_sigs[0].K == partial_sig.K, + "finalize sp composition multisig proof: input partial sigs don't match!"); + CHECK_AND_ASSERT_THROW_MES(partial_sigs[0].KI == partial_sig.KI, + "finalize sp composition multisig proof: input partial sigs don't match!"); + CHECK_AND_ASSERT_THROW_MES(partial_sigs[0].c == partial_sig.c, + "finalize sp composition multisig proof: input partial sigs don't match!"); + CHECK_AND_ASSERT_THROW_MES(partial_sigs[0].r_t1 == partial_sig.r_t1, + "finalize sp composition multisig proof: input partial sigs don't match!"); + CHECK_AND_ASSERT_THROW_MES(partial_sigs[0].r_t2 == partial_sig.r_t2, + "finalize sp composition multisig proof: input partial sigs don't match!"); + CHECK_AND_ASSERT_THROW_MES(partial_sigs[0].K_t1 == partial_sig.K_t1, + "finalize sp composition multisig proof: input partial sigs don't match!"); + } + + + /// assemble the final proof + proof_out.c = partial_sigs[0].c; + proof_out.r_t1 = partial_sigs[0].r_t1; + proof_out.r_t2 = partial_sigs[0].r_t2; + + proof_out.r_ki = rct::zero(); //sum of responses from each multisig participant + for (const SpCompositionProofMultisigPartial &partial_sig : partial_sigs) + sc_add(proof_out.r_ki.bytes, proof_out.r_ki.bytes, partial_sig.r_ki_partial.bytes); + + proof_out.K_t1 = partial_sigs[0].K_t1; + + + /// verify that proof assembly succeeded + CHECK_AND_ASSERT_THROW_MES(sp::verify_sp_composition_proof(proof_out, + partial_sigs[0].message, + partial_sigs[0].K, + partial_sigs[0].KI), + "finalize sp composition multisig proof: proof failed to verify on assembly!"); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace multisig diff --git a/src/multisig/multisig_sp_composition_proof.h b/src/multisig/multisig_sp_composition_proof.h new file mode 100644 index 0000000000..0cecaec153 --- /dev/null +++ b/src/multisig/multisig_sp_composition_proof.h @@ -0,0 +1,171 @@ +// Copyright (c) 2022, 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. + +//// +// Multisig utilities for the seraphis composition proof. +// +// multisig notation: alpha_{ki,n,e} +// - ki: indicates that multisig signing is on the key image part of the proof +// - n: for MuSig2-style bi-nonce signing, alpha_{ki,1,e} is nonce 'D', alpha_{ki,2,e} is nonce 'E' (in their notation) +// - e: multisig signer index in the signer group +// +// Multisig references: +// - MuSig2 (Nick): https://eprint.iacr.org/2020/1261 +// - FROST (Komlo): https://eprint.iacr.org/2020/852 +// - Multisig/threshold security (Crites): https://eprint.iacr.org/2021/1375 +/// + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "multisig_nonce_cache.h" +#include "ringct/rctTypes.h" +#include "seraphis_crypto/sp_composition_proof.h" + +//third party headers + +//standard headers +#include + +//forward declarations + + +namespace multisig +{ + +//// +// Multisig signature proposal for seraphis composition proofs +// +// WARNING: must only use a proposal to make ONE signature, after that the shared signature nonces stored here +// should be deleted immediately +/// +struct SpCompositionProofMultisigProposal final +{ + // message + rct::key message; + // main proof key K + rct::key K; + // key image KI + crypto::key_image KI; + + // signature nonce (shared component): alpha_t1 + crypto::secret_key signature_nonce_K_t1; + // signature nonce (shared component): alpha_t2 + crypto::secret_key signature_nonce_K_t2; +}; + +//// +// Multisig partially signed composition proof (from one multisig signer) +// - only proof component KI is subject to multisig signing (proof privkey z is split between signers) +// - r_ki is the partial response from this multisig signer +/// +struct SpCompositionProofMultisigPartial final +{ + // message + rct::key message; + // main proof key K + rct::key K; + // key image KI + crypto::key_image KI; + + // challenge + rct::key c; + // responses r_t1, r_t2 + rct::key r_t1; + rct::key r_t2; + // intermediate proof key K_t1 + rct::key K_t1; + + // partial response for r_ki (from one multisig signer) + rct::key r_ki_partial; +}; + +/** +* brief: make_sp_composition_multisig_proposal - propose to make a multisig seraphis composition proof +* param: message - message to insert in the proof's Fiat-Shamir transform hash +* param: K - main proof key +* param: KI - key image +* outparam: proposal_out - proposal +*/ +void make_sp_composition_multisig_proposal(const rct::key &message, + const rct::key &K, + const crypto::key_image &KI, + SpCompositionProofMultisigProposal &proposal_out); +/** +* brief: make_sp_composition_multisig_partial_sig - make local multisig signer's partial signature for a seraphis +* composition proof +* - caller must validate the multisig proposal +* - is the key image well-made and canonical? +* - is the main key legitimate? +* - is the message correct? +* param: proposal - proof proposal to use when constructing the partial signature +* param: x - secret key +* param: y - secret key +* param: z_e - secret key of multisig signer e +* param: signer_pub_nonces - signature nonce pubkeys (1/8) * {alpha_{ki,1,e}*U, alpha_{ki,2,e}*U} from all signers +* (including local signer) +* param: local_nonce_1_priv - alpha_{ki,1,e} for local signer +* param: local_nonce_2_priv - alpha_{ki,2,e} for local signer +* outparam: partial_sig_out - partially signed seraphis composition proof +*/ +void make_sp_composition_multisig_partial_sig(const SpCompositionProofMultisigProposal &proposal, + const crypto::secret_key &x, + const crypto::secret_key &y, + const crypto::secret_key &z_e, + const std::vector &signer_pub_nonces, + const crypto::secret_key &local_nonce_1_priv, + const crypto::secret_key &local_nonce_2_priv, + SpCompositionProofMultisigPartial &partial_sig_out); +/** +* brief: try_make_sp_composition_multisig_partial_sig - make a partial signature using a nonce record (nonce safety +* guarantee) +* - caller must validate the multisig proposal +* param: ...(see make_sp_composition_multisig_partial_sig()) +* param: filter - filter representing the multisig signer group that is supposedly working on this signature +* inoutparam: nonce_record_inout - a record of nonces for making partial signatures; used nonces will be cleared here +* outparam: partial_sig_out - the partial signature +* return: true if creating the partial signature succeeded +*/ +bool try_make_sp_composition_multisig_partial_sig(const SpCompositionProofMultisigProposal &proposal, + const crypto::secret_key &x, + const crypto::secret_key &y, + const crypto::secret_key &z_e, + const std::vector &signer_pub_nonces, + const signer_set_filter filter, + MultisigNonceCache &nonce_record_inout, + SpCompositionProofMultisigPartial &partial_sig_out); +/** +* brief: finalize_sp_composition_multisig_proof - create a seraphis composition proof from multisig partial signatures +* param: partial_sigs - partial signatures from enough multisig signers to complete a full proof +* outparam: proof_out - seraphis composition proof +*/ +void finalize_sp_composition_multisig_proof(const std::vector &partial_sigs, + sp::SpCompositionProof &proof_out); + +} //namespace multisig diff --git a/src/ringct/bulletproofs.cc b/src/ringct/bulletproofs.cc index 2cfaf18c02..39677481d9 100644 --- a/src/ringct/bulletproofs.cc +++ b/src/ringct/bulletproofs.cc @@ -69,7 +69,7 @@ static rct::keyV vector_dup(const rct::key &x, size_t n); static rct::key inner_product(const rct::keyV &a, const rct::keyV &b); static constexpr size_t maxN = 64; -static constexpr size_t maxM = BULLETPROOF_MAX_OUTPUTS; +static constexpr size_t maxM = BULLETPROOF_PLUS_MAX_OUTPUTS*8; static ge_p3 Hi_p3[maxN*maxM], Gi_p3[maxN*maxM]; static std::shared_ptr straus_HiGi_cache; static std::shared_ptr pippenger_HiGi_cache; diff --git a/src/seraphis_core/CMakeLists.txt b/src/seraphis_core/CMakeLists.txt new file mode 100644 index 0000000000..3cf169109e --- /dev/null +++ b/src/seraphis_core/CMakeLists.txt @@ -0,0 +1,71 @@ +# Copyright (c) 2021, 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(seraphis_core_sources + binned_reference_set.cpp + binned_reference_set_utils.cpp + discretized_fee.cpp + jamtis_address_tag_utils.cpp + jamtis_address_utils.cpp + jamtis_core_utils.cpp + jamtis_destination.cpp + jamtis_enote_utils.cpp + jamtis_payment_proposal.cpp + jamtis_support_types.cpp + legacy_core_utils.cpp + legacy_decoy_selector_flat.cpp + legacy_enote_types.cpp + legacy_enote_utils.cpp + sp_core_enote_utils.cpp + sp_core_types.cpp + sp_ref_set_index_mapper_flat.cpp + tx_extra.cpp) + +monero_find_all_headers(seraphis_core_headers, "${CMAKE_CURRENT_SOURCE_DIR}") + +monero_add_library(seraphis_core + ${seraphis_core_sources} + ${seraphis_core_headers}) + +target_link_libraries(seraphis_core + PUBLIC + cncrypto + common + cryptonote_basic + device + epee + ringct + seraphis_crypto + PRIVATE + ${EXTRA_LIBRARIES}) + +target_include_directories(seraphis_core + PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}" + PRIVATE + ${Boost_INCLUDE_DIRS}) diff --git a/src/seraphis_core/binned_reference_set.cpp b/src/seraphis_core/binned_reference_set.cpp new file mode 100644 index 0000000000..38a904229d --- /dev/null +++ b/src/seraphis_core/binned_reference_set.cpp @@ -0,0 +1,104 @@ +// Copyright (c) 2022, 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 "binned_reference_set.h" + +//local headers +#include "seraphis_crypto/sp_transcript.h" + +//third party headers + +//standard headers + + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis" + +namespace sp +{ +//------------------------------------------------------------------------------------------------------------------- +void append_to_transcript(const SpBinnedReferenceSetConfigV1 &container, SpTranscriptBuilder &transcript_inout) +{ + transcript_inout.append("bin_radius", container.bin_radius); + transcript_inout.append("num_bin_members", container.num_bin_members); +} +//------------------------------------------------------------------------------------------------------------------- +std::size_t sp_binned_ref_set_config_v1_size_bytes() +{ + return sizeof(SpBinnedReferenceSetConfigV1::bin_radius) + sizeof(SpBinnedReferenceSetConfigV1::num_bin_members); +} +//------------------------------------------------------------------------------------------------------------------- +void append_to_transcript(const SpBinnedReferenceSetV1 &container, SpTranscriptBuilder &transcript_inout) +{ + transcript_inout.append("bin_config", container.bin_config); + transcript_inout.append("bin_generator_seed", container.bin_generator_seed); + transcript_inout.append("bin_rotation_factor", container.bin_rotation_factor); + transcript_inout.append("bin_loci", container.bin_loci); +} +//------------------------------------------------------------------------------------------------------------------- +std::size_t sp_binned_ref_set_v1_size_bytes(const std::size_t num_bins) +{ + return sp_binned_ref_set_config_v1_size_bytes() + + sizeof(SpBinnedReferenceSetV1::bin_generator_seed) + + sizeof(ref_set_bin_dimension_v1_t) + + num_bins * 8; +} +//------------------------------------------------------------------------------------------------------------------- +std::size_t sp_binned_ref_set_v1_size_bytes_compact(const std::size_t num_bins) +{ + return sizeof(ref_set_bin_dimension_v1_t) + num_bins * 8; +} +//------------------------------------------------------------------------------------------------------------------- +std::size_t sp_binned_ref_set_v1_size_bytes(const SpBinnedReferenceSetV1 &reference_set) +{ + return sp_binned_ref_set_v1_size_bytes(reference_set.bin_loci.size()); +} +//------------------------------------------------------------------------------------------------------------------- +std::size_t sp_binned_ref_set_v1_size_bytes_compact(const SpBinnedReferenceSetV1 &reference_set) +{ + return sp_binned_ref_set_v1_size_bytes_compact(reference_set.bin_loci.size()); +} +//------------------------------------------------------------------------------------------------------------------- +bool operator==(const SpBinnedReferenceSetConfigV1 &a, const SpBinnedReferenceSetConfigV1 &b) +{ + return a.bin_radius == b.bin_radius && + a.num_bin_members == b.num_bin_members; +} +//------------------------------------------------------------------------------------------------------------------- +bool operator!=(const SpBinnedReferenceSetConfigV1 &a, const SpBinnedReferenceSetConfigV1 &b) +{ + return !(a == b); +} +//------------------------------------------------------------------------------------------------------------------- +std::uint64_t reference_set_size(const SpBinnedReferenceSetV1 &reference_set) +{ + return reference_set.bin_config.num_bin_members * reference_set.bin_loci.size(); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace sp diff --git a/src/seraphis_core/binned_reference_set.h b/src/seraphis_core/binned_reference_set.h new file mode 100644 index 0000000000..6640439f28 --- /dev/null +++ b/src/seraphis_core/binned_reference_set.h @@ -0,0 +1,106 @@ +// Copyright (c) 2022, 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 reference set using deterministic bins. + +#pragma once + +//local headers +#include "ringct/rctTypes.h" + +//third party headers +#include + +//standard headers +#include +#include + +//forward declarations +namespace sp { class SpTranscriptBuilder; } + +namespace sp +{ + +/// WARNING: changing this is not backward compatible! (struct sizes will change) +using ref_set_bin_dimension_v1_t = std::uint16_t; + +//// +// SpBinnedReferenceSetConfigV1 +/// +struct SpBinnedReferenceSetConfigV1 final +{ + /// bin radius (defines the range of elements that a bin covers in the parent set) + ref_set_bin_dimension_v1_t bin_radius; + /// number of elements referenced by a bin + ref_set_bin_dimension_v1_t num_bin_members; +}; +inline const boost::string_ref container_name(const SpBinnedReferenceSetConfigV1&) { return "SpBinnedReferenceSetConfigV1"; } +void append_to_transcript(const SpBinnedReferenceSetConfigV1 &container, SpTranscriptBuilder &transcript_inout); + +/// get size in bytes +std::size_t sp_binned_ref_set_config_v1_size_bytes(); + +//// +// SpBinnedReferenceSetV1 +// - reference set: a set of elements that are selected from a larger set; all elements except one are decoys (random) +// - binned: the reference set is split into subsets of elements that are located in 'bins' +// - bin: a specific range of elements in a larger set +// - bin locus: the center of the bin range, as an index into that larger set; the range is +// [bin_locus - bin_radius, bin_locus + bin-radius] +// - rotation factor: reference set elements are deterministically selected from bins; the rotation factor rotates all +// bin members within their bins so that one bin member in one of the bins lines up with a pre-selected non-decoy element +/// +struct SpBinnedReferenceSetV1 final +{ + /// bin configuration details (shared by all bins) + SpBinnedReferenceSetConfigV1 bin_config; + /// bin generator seed (shared by all bins) + rct::key bin_generator_seed; + /// rotation factor (shared by all bins) + ref_set_bin_dimension_v1_t bin_rotation_factor; + /// bin loci + std::vector bin_loci; +}; +inline const boost::string_ref container_name(const SpBinnedReferenceSetV1&) { return "SpBinnedReferenceSetV1"; } +void append_to_transcript(const SpBinnedReferenceSetV1 &container, SpTranscriptBuilder &transcript_inout); + +/// get size in bytes +/// - compact version does not include: config, seed +std::size_t sp_binned_ref_set_v1_size_bytes(const std::size_t num_bins); +std::size_t sp_binned_ref_set_v1_size_bytes_compact(const std::size_t num_bins); +std::size_t sp_binned_ref_set_v1_size_bytes(const SpBinnedReferenceSetV1 &reference_set); +std::size_t sp_binned_ref_set_v1_size_bytes_compact(const SpBinnedReferenceSetV1 &reference_set); + +/// equivalence operators for equality checks +bool operator==(const SpBinnedReferenceSetConfigV1 &a, const SpBinnedReferenceSetConfigV1 &b); +bool operator!=(const SpBinnedReferenceSetConfigV1 &a, const SpBinnedReferenceSetConfigV1 &b); + +/// compute the reference set size of a binned reference set +std::uint64_t reference_set_size(const SpBinnedReferenceSetV1 &reference_set); + +} //namespace sp diff --git a/src/seraphis_core/binned_reference_set_utils.cpp b/src/seraphis_core/binned_reference_set_utils.cpp new file mode 100644 index 0000000000..cd1173cb2b --- /dev/null +++ b/src/seraphis_core/binned_reference_set_utils.cpp @@ -0,0 +1,467 @@ +// Copyright (c) 2022, 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 "binned_reference_set_utils.h" + +//local headers +#include "binned_reference_set.h" +#include "crypto/crypto.h" +#include "cryptonote_config.h" +#include "int-util.h" +#include "misc_log_ex.h" +#include "ringct/rctTypes.h" +#include "seraphis_crypto/math_utils.h" +#include "seraphis_crypto/sp_hash_functions.h" +#include "seraphis_crypto/sp_transcript.h" +#include "sp_ref_set_index_mapper.h" + +//third party headers + +//standard headers +#include +#include +#include +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis" + +namespace sp +{ +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void rotate_elements(const std::uint64_t range_limit, + const std::uint64_t rotation_factor, + std::vector &elements_inout) +{ + // rotate a group of elements by a rotation factor + for (std::uint64_t &element : elements_inout) + element = math::mod_add(element, rotation_factor, range_limit); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void denormalize_elements(const std::uint64_t normalization_factor, std::vector &elements_inout) +{ + // de-normalize elements + for (std::uint64_t &element : elements_inout) + element += normalization_factor; +} +//------------------------------------------------------------------------------------------------------------------- +// deterministically generate unique members of a bin (return indices within the bin) +//------------------------------------------------------------------------------------------------------------------- +static void make_normalized_bin_members(const SpBinnedReferenceSetConfigV1 &bin_config, + const rct::key &bin_generator_seed, + const std::uint64_t bin_locus, + const std::uint64_t bin_index_in_set, + std::vector &members_of_bin_out) +{ + // checks and initialization + const std::uint64_t bin_width{compute_bin_width(bin_config.bin_radius)}; + + CHECK_AND_ASSERT_THROW_MES(bin_config.num_bin_members > 0, + "making normalized bin members: zero bin members were requested (at least one expected)."); + CHECK_AND_ASSERT_THROW_MES(bin_config.num_bin_members <= bin_width, + "making normalized bin members: too many bin members were requested (cannot exceed bin width)."); + + // early return case + if (bin_width == 1) + { + members_of_bin_out.clear(); + members_of_bin_out.resize(bin_config.num_bin_members, 0); + return; + } + + // we will discard randomly generated bin members that don't land in a multiple of the bin width + // - set clip allowed max to be a large multiple of the bin width (minus 1 since we are zero-basis), + // to avoid bias in the bin members + // example 1: + // max = 15 (e.g. 4 bits instead of uint64_t) + // bin width = 4 + // 15 = 15 - ((15 mod 4) + 1 mod 4) + // 15 = 15 - ((3) + 1 mod 4) + // 15 = 15 - 0 + // perfect partitioning: [0..3][4..7][8..11][12..15] + // example 2: + // max = 15 (e.g. 4 bits) + // bin width = 6 + // 11 = 15 - ((15 mod 6) + 1 mod 6) + // 11 = 15 - ((3) + 1 mod 6) + // 11 = 15 - 4 + // perfect partitioning: [0..5][6..11] + const std::uint64_t clip_allowed_max{ + std::numeric_limits::max() - + math::mod(math::mod(std::numeric_limits::max(), bin_width) + 1, bin_width) + }; + + // generate each bin member (as a unique index within the bin) + // - make 64-byte blobs via hashing, then use each 8-byte block to try to generate a bin member + // - getting 8 blocks at a time reduces calls to the hash function + unsigned char member_generator[64]; + std::size_t member_generator_offset_blocks{0}; + std::uint64_t generator_clip{}; + std::uint64_t member_candidate{}; + std::size_t num_generator_refreshes{0}; + members_of_bin_out.clear(); + members_of_bin_out.reserve(bin_config.num_bin_members); + + for (std::size_t bin_member_index{0}; bin_member_index < bin_config.num_bin_members; ++bin_member_index) + { + // look for a unique bin member to add + do + { + // update the generator (find a generator that is within the allowed max) + do + { + if (member_generator_offset_blocks*8 >= sizeof(member_generator)) + member_generator_offset_blocks = 0; + + if (member_generator_offset_blocks == 0) + { + // make a bin member generator + // g = H_64(bin_generator_seed, bin_locus, bin_index_in_set, num_generator_refreshes) + SpKDFTranscript transcript{ + config::HASH_KEY_BINNED_REF_SET_MEMBER, + sizeof(bin_generator_seed) + sizeof(bin_locus) + sizeof(bin_index_in_set) + 4 + }; + transcript.append("seed", bin_generator_seed); + transcript.append("length", bin_locus); + transcript.append("bin_index", bin_index_in_set); + transcript.append("num_generator_refreshes", num_generator_refreshes); + sp_hash_to_64(transcript.data(), transcript.size(), member_generator); + ++num_generator_refreshes; + } + + memcpy(&generator_clip, member_generator + 8*member_generator_offset_blocks, 8); + generator_clip = SWAP64LE(generator_clip); + ++member_generator_offset_blocks; + } while (generator_clip > clip_allowed_max); + + // compute the candidate bin member: generator mod bin_width + member_candidate = math::mod(generator_clip, bin_width); + } while (std::find(members_of_bin_out.begin(), members_of_bin_out.end(), member_candidate) != + members_of_bin_out.end()); + + members_of_bin_out.emplace_back(member_candidate); + } +} +//------------------------------------------------------------------------------------------------------------------- +// make bin loci for a reference set (one of which will be the locus for the bin with the real reference) +//------------------------------------------------------------------------------------------------------------------- +static void generate_bin_loci(const SpRefSetIndexMapper &index_mapper, + const SpBinnedReferenceSetConfigV1 &bin_config, + const std::uint64_t reference_set_size, + const std::uint64_t real_reference_index, + std::vector &bin_loci_out, + std::uint64_t &bin_index_with_real_out) +{ + /// checks and initialization + const std::uint64_t distribution_min_index{index_mapper.distribution_min_index()}; + const std::uint64_t distribution_max_index{index_mapper.distribution_max_index()}; + + CHECK_AND_ASSERT_THROW_MES(real_reference_index >= distribution_min_index && + real_reference_index <= distribution_max_index, + "generating bin loci: real element reference is not within the element distribution."); + CHECK_AND_ASSERT_THROW_MES(reference_set_size >= 1, + "generating bin loci: reference set size too small (needs to be >= 1)."); + CHECK_AND_ASSERT_THROW_MES(distribution_min_index <= distribution_max_index, + "generating bin loci: invalid distribution range."); + CHECK_AND_ASSERT_THROW_MES(distribution_max_index - distribution_min_index >= + compute_bin_width(bin_config.bin_radius) - 1, //note: range may span uint64_t + "generating bin loci: bin width is too large for the distribution range."); + CHECK_AND_ASSERT_THROW_MES(validate_bin_config_v1(reference_set_size, bin_config), + "generating bin loci: invalid config."); + + const std::uint64_t num_bins{reference_set_size/bin_config.num_bin_members}; + const std::uint64_t distribution_width{distribution_max_index - distribution_min_index + 1}; + + + /// pick a locus for the real reference's bin + + // 1) define range where the locus may reside (clamp bounds to element distribution range) + const std::uint64_t real_locus_min{ + math::saturating_sub(real_reference_index, bin_config.bin_radius, distribution_min_index) + }; + const std::uint64_t real_locus_max{ + math::saturating_add(real_reference_index, bin_config.bin_radius, distribution_max_index) + }; + + // 2) generate the bin locus within the element distribution + const std::uint64_t real_locus{crypto::rand_range(real_locus_min, real_locus_max)}; + + // 3) translate the real locus to uniform space (uniform distribution across [0, 2^64 - 1]) + const std::uint64_t real_locus_flattened{index_mapper.element_index_to_uniform_index(real_locus)}; + + + /// randomly generate a set of bin loci in uniform space + std::vector bin_loci; + bin_loci.resize(num_bins); + + for (std::uint64_t &bin_locus : bin_loci) + bin_locus = crypto::rand_range(0, std::numeric_limits::max()); + + + /// rotate the randomly generated bins so a random bin lines up with the real bin locus (in uniform space) + + // 1) randomly select one of the bins + const std::uint64_t designated_real_bin{crypto::rand_range(0, num_bins - 1)}; + + // 2) compute rotation factor + const std::uint64_t bin_loci_rotation_factor{math::mod_sub(real_locus_flattened, bin_loci[designated_real_bin], 0)}; + + // 3) rotate all the bin loci + rotate_elements(0, bin_loci_rotation_factor, bin_loci); + + + /// get bin loci into the element distribution space + + // 1) map the bin loci into the distribution space + for (std::uint64_t &bin_locus : bin_loci) + bin_locus = index_mapper.uniform_index_to_element_index(bin_locus); + + // 2) find the bin locus closest to the real locus (the index mapper might have precision loss) + // WARNING: all possible values in the element distribution space should map to values in uniform space, + // otherwise decoy bin loci could be 'ruled out' + std::uint64_t locus_closest_to_real{0}; + std::uint64_t locus_gap{distribution_width - 1}; //all gaps will be <= the range of locus values + std::uint64_t smallest_gap; + + for (std::size_t bin_loci_index{0}; bin_loci_index < bin_loci.size(); ++bin_loci_index) + { + // test for gaps above and below the locus + smallest_gap = std::min( + math::mod_sub(real_locus, bin_loci[bin_loci_index], distribution_width), //gap below + math::mod_sub(bin_loci[bin_loci_index], real_locus, distribution_width) //gap above + ); + + if (smallest_gap < locus_gap) + { + locus_gap = smallest_gap; + locus_closest_to_real = bin_loci_index; + } + } + + // 3) reset the bin locus closest to the real locus + bin_loci[locus_closest_to_real] = real_locus; + + + /// prepare outputs + + // 1) sort bin loci + std::sort(bin_loci.begin(), bin_loci.end()); + + // 2) shift bin loci so their entire widths are within the element distribution + for (std::uint64_t &bin_locus : bin_loci) + { + bin_locus = math::clamp(bin_locus, + distribution_min_index + bin_config.bin_radius, + distribution_max_index - bin_config.bin_radius); + } + + const std::uint64_t real_locus_shifted{ + math::clamp(real_locus, + distribution_min_index + bin_config.bin_radius, + distribution_max_index - bin_config.bin_radius) + }; + + // 3) select the real reference's locus (if multiple loci equal the real locus, pick one randomly) + std::uint64_t last_locus_equal_to_real{0}; + std::uint64_t num_loci_equal_to_real{0}; + + for (std::size_t bin_loci_index{0}; bin_loci_index < bin_loci.size(); ++bin_loci_index) + { + if (bin_loci[bin_loci_index] == real_locus_shifted) + { + last_locus_equal_to_real = bin_loci_index; + ++num_loci_equal_to_real; + } + } + + bin_index_with_real_out = + crypto::rand_range(last_locus_equal_to_real - num_loci_equal_to_real + 1, last_locus_equal_to_real); + + // 4) set bin loci output + bin_loci_out = std::move(bin_loci); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +std::uint64_t compute_bin_width(const std::uint64_t bin_radius) +{ + return 2*bin_radius + 1; +} +//------------------------------------------------------------------------------------------------------------------- +bool validate_bin_config_v1(const std::uint64_t reference_set_size, const SpBinnedReferenceSetConfigV1 &bin_config) +{ + // bin width outside bin dimension + if (bin_config.bin_radius > (std::numeric_limits::max() - 1)/2) + return false; + // too many bin members + if (bin_config.num_bin_members > std::numeric_limits::max()) + return false; + // can't fit bin members uniquely in bin (note: bin can't contain more than std::uint64_t::max members) + if (bin_config.num_bin_members > compute_bin_width(bin_config.bin_radius)) + return false; + // no bin members + if (bin_config.num_bin_members < 1) + return false; + // reference set can't be perfectly divided into bins + if (bin_config.num_bin_members * (reference_set_size / bin_config.num_bin_members) != reference_set_size) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +void make_binned_reference_set_v1(const SpRefSetIndexMapper &index_mapper, + const SpBinnedReferenceSetConfigV1 &bin_config, + const rct::key &generator_seed, + const std::uint64_t reference_set_size, + const std::uint64_t real_reference_index, + SpBinnedReferenceSetV1 &binned_reference_set_out) +{ + // make binned reference set + + /// generate bin loci + std::vector bin_loci; + std::uint64_t bin_index_with_real; + generate_bin_loci(index_mapper, bin_config, reference_set_size, real_reference_index, bin_loci, bin_index_with_real); + + + /// checks and initialization + const std::uint64_t bin_width{compute_bin_width(bin_config.bin_radius)}; + + CHECK_AND_ASSERT_THROW_MES(validate_bin_config_v1(bin_loci.size() * bin_config.num_bin_members, bin_config), + "binned reference set: invalid bin config."); + CHECK_AND_ASSERT_THROW_MES(std::is_sorted(bin_loci.begin(), bin_loci.end()), + "binned reference set: bin loci aren't sorted."); + + for (const std::uint64_t bin_locus : bin_loci) + { + CHECK_AND_ASSERT_THROW_MES(bin_locus >= bin_config.bin_radius, + "binned reference set: the bottom of a proposed bin hangs below 0."); + CHECK_AND_ASSERT_THROW_MES(bin_locus <= std::numeric_limits::max() - bin_config.bin_radius, + "binned reference set: the top of a proposed bin extends above uint64::max()."); + } + + CHECK_AND_ASSERT_THROW_MES(bin_index_with_real < bin_loci.size(), + "binned reference set: real element's bin isn't in the bins proposed."); + CHECK_AND_ASSERT_THROW_MES(real_reference_index >= bin_loci[bin_index_with_real] - bin_config.bin_radius, + "binned reference set: real element is below its proposed bin."); + CHECK_AND_ASSERT_THROW_MES(real_reference_index <= bin_loci[bin_index_with_real] + bin_config.bin_radius, + "binned reference set: real element is above its proposed bin."); + + + /// set real reference's bin rotation factor + + // 1) generate the real bin's bin members' element set indices (normalized and not rotated) + std::vector members_of_real_bin; + make_normalized_bin_members(bin_config, + generator_seed, + bin_loci[bin_index_with_real], + bin_index_with_real, + members_of_real_bin); + CHECK_AND_ASSERT_THROW_MES(members_of_real_bin.size() == bin_config.num_bin_members, + "binned reference set: getting normalized bin members failed (bug)."); + + // 2) select a random bin member to land on the real reference + const std::uint64_t designated_real_bin_member{crypto::rand_range(0, bin_config.num_bin_members - 1)}; + + // 3) normalize the real reference within its bin (subtract the bottom of the bin) + const std::uint64_t normalized_real_reference{ + real_reference_index - (bin_loci[bin_index_with_real] - bin_config.bin_radius) + }; + + // 4) compute rotation factor + binned_reference_set_out.bin_rotation_factor = static_cast( + math::mod_sub(normalized_real_reference, members_of_real_bin[designated_real_bin_member], bin_width) + ); + + + /// set remaining pieces of the output reference set + binned_reference_set_out.bin_config = bin_config; + binned_reference_set_out.bin_generator_seed = generator_seed; + binned_reference_set_out.bin_loci = std::move(bin_loci); +} +//------------------------------------------------------------------------------------------------------------------- +bool try_get_reference_indices_from_binned_reference_set_v1(const SpBinnedReferenceSetV1 &binned_reference_set, + std::vector &reference_indices_out) +{ + // initialization + const std::uint64_t bin_width{compute_bin_width(binned_reference_set.bin_config.bin_radius)}; + const std::uint64_t reference_set_size{ + binned_reference_set.bin_loci.size() * binned_reference_set.bin_config.num_bin_members + }; + + // sanity check the bin config + if (!validate_bin_config_v1(reference_set_size, binned_reference_set.bin_config)) + return false; + + // rotation factor must be within the bins (normalized) + if (binned_reference_set.bin_rotation_factor >= bin_width) + return false; + + // validate bins + for (const std::uint64_t bin_locus : binned_reference_set.bin_loci) + { + // bins must all fit in the range [0, 2^64 - 1] + if (bin_locus < binned_reference_set.bin_config.bin_radius) + return false; + if (bin_locus > std::numeric_limits::max() - binned_reference_set.bin_config.bin_radius) + return false; + } + + // add all the bin members + reference_indices_out.clear(); + reference_indices_out.reserve(reference_set_size); + + std::vector bin_members_temp; + + for (std::size_t bin_index{0}; bin_index < binned_reference_set.bin_loci.size(); ++bin_index) + { + // 1) make normalized bin members + make_normalized_bin_members(binned_reference_set.bin_config, + binned_reference_set.bin_generator_seed, + binned_reference_set.bin_loci[bin_index], + bin_index, + bin_members_temp); + + // 2) rotate the bin members by the rotation factor + rotate_elements(bin_width, binned_reference_set.bin_rotation_factor, bin_members_temp); + + // 3) de-normalize the bin members + denormalize_elements( + binned_reference_set.bin_loci[bin_index] - binned_reference_set.bin_config.bin_radius, + bin_members_temp); + + // 4) save the bin members + reference_indices_out.insert(reference_indices_out.end(), bin_members_temp.begin(), bin_members_temp.end()); + } + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace sp diff --git a/src/seraphis_core/binned_reference_set_utils.h b/src/seraphis_core/binned_reference_set_utils.h new file mode 100644 index 0000000000..5dd6594258 --- /dev/null +++ b/src/seraphis_core/binned_reference_set_utils.h @@ -0,0 +1,90 @@ +// Copyright (c) 2022, 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 interacting with binned reference sets. + +#pragma once + +//local headers +#include "binned_reference_set.h" +#include "ringct/rctTypes.h" +#include "sp_ref_set_index_mapper.h" + +//third party headers + +//standard headers +#include +#include + +//forward declarations + + +namespace sp +{ + +/** +* brief: compute_bin_width - compute the width of a bin (number of ledger indices that the bin spans) +* - width = 2*radius + 1 +* param: bin_radius - +* return: 2*radius + 1 +*/ +std::uint64_t compute_bin_width(const std::uint64_t bin_radius); +/** +* brief: validate_bin_config_v1 - check that a reference set bin configuration is valid +* param: reference_set_size - +* param: bin_config - +* return: true if the config is valid +*/ +bool validate_bin_config_v1(const std::uint64_t reference_set_size, const SpBinnedReferenceSetConfigV1 &bin_config); +/** +* brief: make_binned_reference_set_v1 - make a binned reference set (a reference set represented using bins, where one +* reference element is pre-defined but should be hidden within the generated reference set) +* param: index_mapper - +* param: bin_config - +* param: generator_seed - +* param: reference_set_size - +* param: real_reference_index - +* outparam: binned_reference_set_out - +*/ +void make_binned_reference_set_v1(const SpRefSetIndexMapper &index_mapper, + const SpBinnedReferenceSetConfigV1 &bin_config, + const rct::key &generator_seed, + const std::uint64_t reference_set_size, + const std::uint64_t real_reference_index, + SpBinnedReferenceSetV1 &binned_reference_set_out); +/** +* brief: try_get_reference_indices_from_binned_reference_set_v1 - try to converted a binned reference set into a list of +* indices (i.e. the underlying reference set) +* param: binned_reference_set - +* outparam: reference_indices_out - +* return: true if extracting the reference indices succeeded +*/ +bool try_get_reference_indices_from_binned_reference_set_v1(const SpBinnedReferenceSetV1 &binned_reference_set, + std::vector &reference_indices_out); + +} //namespace sp diff --git a/src/seraphis_core/discretized_fee.cpp b/src/seraphis_core/discretized_fee.cpp new file mode 100644 index 0000000000..34325cedf6 --- /dev/null +++ b/src/seraphis_core/discretized_fee.cpp @@ -0,0 +1,264 @@ +// Copyright (c) 2022, 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 "discretized_fee.h" + +//local headers +#include "cryptonote_config.h" +#include "misc_log_ex.h" +#include "seraphis_crypto/sp_transcript.h" + +//third party headers + +//standard headers +#include +#include +#include +#include +#include +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis" + +namespace sp +{ + +/// discretized fee context: set of pairs +struct DiscretizedFeeContext final +{ + std::vector fee_encodings; + std::vector value_encodings; + std::unordered_map mapped_values; +}; + +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static long double round_down_to_sig_figs(long double value, const std::uint64_t num_sig_figs) +{ + // 1. put value into scientific notation (with each desired significant digit left above the decimal point) + std::size_t decimal_scale{0}; + + while (value >= std::pow(10.0, num_sig_figs)) + { + value /= 10.0; + ++decimal_scale; + } + + // 2. remove digits that have been moved below the decimal + value = std::round(value); + + // 3. put value back into normal notation + while (decimal_scale) + { + value *= 10.0; + --decimal_scale; + } + + return value; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static DiscretizedFeeContext generate_discretized_fee_context(const long double fee_level_factor, + const std::uint64_t fee_sig_figs) +{ + CHECK_AND_ASSERT_THROW_MES(fee_level_factor > 0.01 && fee_sig_figs > 0, + "generate seraphis discretized fees: invalid config."); + + DiscretizedFeeContext fee_context; + fee_context.fee_encodings.reserve( + (std::log(std::numeric_limits::max()) / std::log(fee_level_factor)) + 10 + ); + fee_context.value_encodings.reserve(fee_context.fee_encodings.capacity()); + + // 1. special encoding: 0 + fee_context.fee_encodings.emplace_back(0); + fee_context.value_encodings.emplace_back(0); + fee_context.mapped_values[fee_context.fee_encodings.back()] = fee_context.value_encodings.back(); + + // 2. collect powers of the fee level factor (e.g. powers of 1.5, powers of 2, etc.) + static_assert(sizeof(discretized_fee_encoding_t) <= sizeof(std::size_t), ""); + const std::size_t recorded_levels_offset(fee_context.fee_encodings.size()); + const std::size_t max_level_allowed{ + std::numeric_limits::max() - recorded_levels_offset - 2 + }; + std::size_t current_level{0}; + std::uint64_t prev_fee_value{static_cast(-1)}; + std::uint64_t fee_value; + + do + { + CHECK_AND_ASSERT_THROW_MES(current_level <= max_level_allowed, + "generate seraphis discretized fees: invalid config (too many fee levels)."); + + // a. value = factor ^ level -> crop digits below specified number of significant digits + fee_value = static_cast( + round_down_to_sig_figs(std::pow(fee_level_factor, current_level), fee_sig_figs) + ); + + // b. skip if we already have this value (i.e. because we got the same fee value due to rounding) + if (fee_value == prev_fee_value) + continue; + + // c. save fee level and value + fee_context.fee_encodings.emplace_back( + static_cast(current_level + recorded_levels_offset) + ); + fee_context.value_encodings.emplace_back(fee_value); + fee_context.mapped_values[fee_context.fee_encodings.back()] = fee_context.value_encodings.back(); + prev_fee_value = fee_value; + + // d. increase the fee level and check the termination condition + // note: increment within the condition expression in case 'continue' was called + } while (round_down_to_sig_figs(std::pow(fee_level_factor, ++current_level), fee_sig_figs) < + static_cast(std::numeric_limits::max())); + + // 3. special encoding: uint64::max + fee_context.fee_encodings.emplace_back( + static_cast(current_level + recorded_levels_offset) + ); + fee_context.value_encodings.emplace_back(std::numeric_limits::max()); + fee_context.mapped_values[fee_context.fee_encodings.back()] = fee_context.value_encodings.back(); + + // 4. special encoding: invalid + // - all remaining levels are invalid (there should be at least one) + CHECK_AND_ASSERT_THROW_MES(fee_context.mapped_values.find(std::numeric_limits::max()) == + fee_context.mapped_values.end(), + "generate seraphis discretized fees: invalid discretized maps, there is no 'invalid fee' encoding."); + + return fee_context; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static std::size_t num_encodings(const DiscretizedFeeContext &fee_context) +{ + CHECK_AND_ASSERT_THROW_MES(fee_context.fee_encodings.size() == fee_context.value_encodings.size(), + "seraphis discretized fee context num encodings: invalid context."); + CHECK_AND_ASSERT_THROW_MES(fee_context.fee_encodings.size() == fee_context.mapped_values.size(), + "seraphis discretized fee context num encodings: invalid context."); + + return fee_context.fee_encodings.size(); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static DiscretizedFee discretize_fee(const DiscretizedFeeContext &fee_context, const rct::xmr_amount raw_fee_value) +{ + // find the closest discretized fee that is >= the specified fee value + + // 1. start with the highest fee level (should be invalid) + discretized_fee_encoding_t fee_encoding = std::numeric_limits::max(); + + // 2. start with the max discretized fee value, so we can reduce it as we get closer to the final solution + std::uint64_t closest_discretized_fee_value{static_cast(-1)}; + + // 3. search the fees for the closest encoded fee value >= our raw fee value + for (std::size_t encoding_index{0}; encoding_index < num_encodings(fee_context); ++encoding_index) + { + // a. check if this encoding value is below our raw fee value + if (fee_context.value_encodings[encoding_index] < raw_fee_value) + continue; + + // b. check if this encoding value is closer to our raw fee value than the previous saved encoding + if (fee_context.value_encodings[encoding_index] <= closest_discretized_fee_value) + { + fee_encoding = fee_context.fee_encodings[encoding_index]; + closest_discretized_fee_value = fee_context.value_encodings[encoding_index]; + } + } + + return DiscretizedFee{fee_encoding}; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool try_get_fee_value(const DiscretizedFeeContext &fee_context, + const DiscretizedFee discretized_fee, + std::uint64_t &fee_value_out) +{ + // try to find the discretized fee in the map and return its fee value + const auto found_fee = fee_context.mapped_values.find(discretized_fee.fee_encoding); + if (found_fee == fee_context.mapped_values.end()) + return false; + + fee_value_out = found_fee->second; + return true; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static const DiscretizedFeeContext& default_fee_context() +{ + static const DiscretizedFeeContext fee_context{ + generate_discretized_fee_context( + config::DISCRETIZED_FEE_LEVEL_NUMERATOR_X100 / 100.0, + config::DISCRETIZED_FEE_SIG_FIGS + ) + }; + return fee_context; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +void append_to_transcript(const DiscretizedFee container, SpTranscriptBuilder &transcript_inout) +{ + transcript_inout.append("fee_encoding", container.fee_encoding); +} +//------------------------------------------------------------------------------------------------------------------- +bool operator==(const DiscretizedFee a, const DiscretizedFee b) +{ + return a.fee_encoding == b.fee_encoding; +} +//------------------------------------------------------------------------------------------------------------------- +bool operator==(const DiscretizedFee fee, const discretized_fee_encoding_t fee_level) +{ + return fee.fee_encoding == fee_level; +} +//------------------------------------------------------------------------------------------------------------------- +bool operator==(const discretized_fee_encoding_t fee_level, const DiscretizedFee fee) +{ + return fee_level == fee.fee_encoding; +} +//------------------------------------------------------------------------------------------------------------------- +bool operator==(const DiscretizedFee fee, const rct::xmr_amount raw_fee_value) +{ + rct::xmr_amount fee_value; + CHECK_AND_ASSERT_THROW_MES(try_get_fee_value(fee, fee_value), + "seraphis discretized fee equality check with a raw fee failed: the discretized fee is an invalid encoding."); + + return fee_value == raw_fee_value; +} +//------------------------------------------------------------------------------------------------------------------- +DiscretizedFee discretize_fee(const rct::xmr_amount raw_fee_value) +{ + return discretize_fee(default_fee_context(), raw_fee_value); +} +//------------------------------------------------------------------------------------------------------------------- +bool try_get_fee_value(const DiscretizedFee discretized_fee, std::uint64_t &fee_value_out) +{ + return try_get_fee_value(default_fee_context(), discretized_fee, fee_value_out); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace sp diff --git a/src/seraphis_core/discretized_fee.h b/src/seraphis_core/discretized_fee.h new file mode 100644 index 0000000000..b499e08e0f --- /dev/null +++ b/src/seraphis_core/discretized_fee.h @@ -0,0 +1,87 @@ +// Copyright (c) 2022, 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 discretized fee (i.e. a fee value represented by a discrete identifier). + +#pragma once + +//local headers +#include "ringct/rctTypes.h" + +//third party headers +#include + +//standard headers +#include + +//forward declarations +namespace sp { class SpTranscriptBuilder; } + +namespace sp +{ + +using discretized_fee_encoding_t = unsigned char; + +//// +// DiscretizedFee +// - a discretized fee represents a fee value selected from a limited set of valid fee values +// - a raw fee value is 'discretized' when it is converted into one of those valid fee values (by rounding +// up to the nearest fee level) +// note: a default-initialized discretized fee encodes the fee value '0' +/// +struct DiscretizedFee final +{ + discretized_fee_encoding_t fee_encoding; +}; +inline const boost::string_ref container_name(const DiscretizedFee) { return "DiscretizedFee"; } +void append_to_transcript(const DiscretizedFee container, SpTranscriptBuilder &transcript_inout); + +/// get size in bytes +inline std::size_t discretized_fee_size_bytes() { return sizeof(discretized_fee_encoding_t); } + +/// equality operators +bool operator==(const DiscretizedFee a, const DiscretizedFee b); +bool operator==(const DiscretizedFee fee, const discretized_fee_encoding_t fee_level); +bool operator==(const discretized_fee_encoding_t fee_level, const DiscretizedFee fee); +bool operator==(const DiscretizedFee fee, const rct::xmr_amount raw_fee_value); + +/** +* brief: discretize_fee - convert a raw fee value to a discretized fee (the resulting encoded fee may be >= the raw fee) +* param: raw_fee_value - +* return: discretized fee +*/ +DiscretizedFee discretize_fee(const rct::xmr_amount raw_fee_value); +/** +* brief: try_get_fee_value - try to extract a raw fee value from a discretized fee (fails if the encoding is invalid) +* param: discretized_fee - +* outparam: fee_value_out - +* return: true if an extraction succeeded +*/ +bool try_get_fee_value(const DiscretizedFee discretized_fee, std::uint64_t &fee_value_out); + +} //namespace sp diff --git a/src/seraphis_core/jamtis_address_tag_utils.cpp b/src/seraphis_core/jamtis_address_tag_utils.cpp new file mode 100644 index 0000000000..cd8d08a594 --- /dev/null +++ b/src/seraphis_core/jamtis_address_tag_utils.cpp @@ -0,0 +1,238 @@ +// Copyright (c) 2022, 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 "jamtis_address_tag_utils.h" + +//local headers +#include "crypto/crypto.h" +#include "cryptonote_config.h" +extern "C" +{ +#include "crypto/blake2b.h" +#include "crypto/twofish.h" +} +#include "jamtis_support_types.h" +#include "memwipe.h" +#include "misc_language.h" +#include "misc_log_ex.h" +#include "ringct/rctTypes.h" +#include "seraphis_crypto/sp_crypto_utils.h" +#include "seraphis_crypto/sp_hash_functions.h" +#include "seraphis_crypto/sp_transcript.h" +#include "span.h" + +//third party headers + +//standard headers + + +namespace sp +{ +namespace jamtis +{ +/// secret for encrypting address tags +using encrypted_address_tag_secret_t = encrypted_address_tag_t; +static_assert(sizeof(encrypted_address_tag_secret_t) == sizeof(address_tag_t), ""); + +/// block size +constexpr std::size_t TWOFISH_BLOCK_SIZE{16}; + +//------------------------------------------------------------------------------------------------------------------- +// encryption_secret = truncate_to_addr_tag_size(H_32(q, Ko)) +//------------------------------------------------------------------------------------------------------------------- +static encrypted_address_tag_secret_t get_encrypted_address_tag_secret(const rct::key &sender_receiver_secret, + const rct::key &onetime_address) +{ + static_assert(sizeof(encrypted_address_tag_secret_t) <= 32, ""); + + // temp_encryption_secret = H_32(q, Ko) + SpKDFTranscript transcript{config::HASH_KEY_JAMTIS_ENCRYPTED_ADDRESS_TAG, 2*sizeof(rct::key)}; + transcript.append("q", sender_receiver_secret); + transcript.append("Ko", onetime_address); + + rct::key temp_encryption_secret; + sp_hash_to_32(transcript.data(), transcript.size(), temp_encryption_secret.bytes); + + // truncate to desired size of the secret + encrypted_address_tag_secret_t encryption_secret; + memcpy(encryption_secret.bytes, temp_encryption_secret.bytes, sizeof(encrypted_address_tag_secret_t)); + + return encryption_secret; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static address_tag_hint_t get_address_tag_hint(const crypto::secret_key &cipher_key, + const address_index_t &encrypted_address_index) +{ + static_assert(sizeof(address_tag_hint_t) == 2, ""); + static_assert(sizeof(config::TRANSCRIPT_PREFIX) != 0, ""); + static_assert(sizeof(config::HASH_KEY_JAMTIS_ADDRESS_TAG_HINT) != 0, ""); + + // assemble hash contents: prefix || 'domain-sep' || k || cipher[k](j) + // note: use a raw C-style struct here instead of SpKDFTranscript for maximal performance (the string produced is + // equivalent to what you'd get from SpKDFTranscript) + // note2: '-1' removes the null terminator + struct hash_context_t { + unsigned char prefix[sizeof(config::TRANSCRIPT_PREFIX) - 1]; + unsigned char domain_separator[sizeof(config::HASH_KEY_JAMTIS_ADDRESS_TAG_HINT) - 1]; + rct::key cipher_key; //not crypto::secret_key, which has significant construction cost + address_index_t enc_j; + } hash_context; + static_assert(!epee::has_padding(), ""); + + memcpy(hash_context.prefix, config::TRANSCRIPT_PREFIX, sizeof(config::TRANSCRIPT_PREFIX) - 1); + memcpy(hash_context.domain_separator, + config::HASH_KEY_JAMTIS_ADDRESS_TAG_HINT, + sizeof(config::HASH_KEY_JAMTIS_ADDRESS_TAG_HINT) - 1); + hash_context.cipher_key = rct::sk2rct(cipher_key); + hash_context.enc_j = encrypted_address_index; + + // address_tag_hint = H_2(k, cipher[k](j)) + address_tag_hint_t address_tag_hint; + sp_hash_to_2(&hash_context, sizeof(hash_context), address_tag_hint.bytes); + + // clean up cipher key bytes + memwipe(hash_context.cipher_key.bytes, 32); + + return address_tag_hint; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +jamtis_address_tag_cipher_context::jamtis_address_tag_cipher_context(const crypto::secret_key &cipher_key) +{ + // cache the cipher key + m_cipher_key = cipher_key; + + // prepare the Twofish key + Twofish_initialise(); + Twofish_prepare_key(to_bytes(cipher_key), sizeof(rct::key), &(m_twofish_key)); +} +//------------------------------------------------------------------------------------------------------------------- +jamtis_address_tag_cipher_context::~jamtis_address_tag_cipher_context() +{ + memwipe(&m_twofish_key, sizeof(Twofish_key)); +} +//------------------------------------------------------------------------------------------------------------------- +address_tag_t jamtis_address_tag_cipher_context::cipher(const address_index_t &j) const +{ + // address tag = cipher[k](j) || H_2(k, cipher[k](j)) + + // expect address index to fit in one Twofish block (16 bytes) + static_assert(sizeof(address_index_t) == TWOFISH_BLOCK_SIZE, ""); + + // prepare ciphered index + address_index_t encrypted_j{j}; + + // encrypt the address index + Twofish_encrypt_block(&m_twofish_key, encrypted_j.bytes, encrypted_j.bytes); + + // make the address tag hint and complete the address tag + return make_address_tag(encrypted_j, get_address_tag_hint(m_cipher_key, encrypted_j)); +} +//------------------------------------------------------------------------------------------------------------------- +bool jamtis_address_tag_cipher_context::try_decipher(const address_tag_t &addr_tag, address_index_t &j_out) const +{ + static_assert(sizeof(address_index_t) == TWOFISH_BLOCK_SIZE, ""); + static_assert(sizeof(address_index_t) + sizeof(address_tag_hint_t) == sizeof(address_tag_t), ""); + + // extract the encrypted index + memcpy(j_out.bytes, addr_tag.bytes, sizeof(address_index_t)); + + // recover the address tag hint + const address_tag_hint_t address_tag_hint{get_address_tag_hint(m_cipher_key, j_out)}; + + // check the address tag hint + if (memcmp(addr_tag.bytes + sizeof(address_index_t), address_tag_hint.bytes, sizeof(address_tag_hint_t)) != 0) + return false; + + // decrypt the address index + Twofish_decrypt_block(&m_twofish_key, j_out.bytes, j_out.bytes); + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +address_tag_t cipher_address_index(const jamtis_address_tag_cipher_context &cipher_context, const address_index_t &j) +{ + return cipher_context.cipher(j); +} +//------------------------------------------------------------------------------------------------------------------- +address_tag_t cipher_address_index(const crypto::secret_key &cipher_key, const address_index_t &j) +{ + // prepare to cipher the index + const jamtis_address_tag_cipher_context cipher_context{cipher_key}; + + // cipher it + return cipher_address_index(cipher_context, j); +} +//------------------------------------------------------------------------------------------------------------------- +bool try_decipher_address_index(const jamtis_address_tag_cipher_context &cipher_context, + const address_tag_t &addr_tag, + address_index_t &j_out) +{ + return cipher_context.try_decipher(addr_tag, j_out); +} +//------------------------------------------------------------------------------------------------------------------- +bool try_decipher_address_index(const crypto::secret_key &cipher_key, + const address_tag_t &addr_tag, + address_index_t &j_out) +{ + // prepare to decipher the tag + const jamtis_address_tag_cipher_context cipher_context{cipher_key}; + + // decipher it + return try_decipher_address_index(cipher_context, addr_tag, j_out); +} +//------------------------------------------------------------------------------------------------------------------- +encrypted_address_tag_t encrypt_address_tag(const rct::key &sender_receiver_secret, + const rct::key &onetime_address, + const address_tag_t &addr_tag) +{ + static_assert(sizeof(address_tag_t) == sizeof(encrypted_address_tag_secret_t), ""); + + // addr_tag_enc = addr_tag XOR encryption_secret + return addr_tag ^ get_encrypted_address_tag_secret(sender_receiver_secret, onetime_address); +} +//------------------------------------------------------------------------------------------------------------------- +address_tag_t decrypt_address_tag(const rct::key &sender_receiver_secret, + const rct::key &onetime_address, + const encrypted_address_tag_t &addr_tag_enc) +{ + static_assert(sizeof(encrypted_address_tag_t) == sizeof(encrypted_address_tag_secret_t), ""); + + // addr_tag = addr_tag_enc XOR encryption_secret + return addr_tag_enc ^ get_encrypted_address_tag_secret(sender_receiver_secret, onetime_address); +} +//------------------------------------------------------------------------------------------------------------------- +void gen_address_tag(address_tag_t &addr_tag_inout) +{ + crypto::rand(sizeof(address_tag_t), reinterpret_cast(&addr_tag_inout)); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace jamtis +} //namespace sp diff --git a/src/seraphis_core/jamtis_address_tag_utils.h b/src/seraphis_core/jamtis_address_tag_utils.h new file mode 100644 index 0000000000..52231cece1 --- /dev/null +++ b/src/seraphis_core/jamtis_address_tag_utils.h @@ -0,0 +1,104 @@ +// Copyright (c) 2022, 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. + +// Address tag handling for Jamtis addresses. + +#pragma once + +//local headers +extern "C" +{ +#include "crypto/twofish.h" +} +#include "jamtis_support_types.h" +#include "ringct/rctTypes.h" + +//third party headers + +//standard headers + +//forward declarations + + +namespace sp +{ +namespace jamtis +{ + +/// cipher context for making address tags +struct jamtis_address_tag_cipher_context final +{ +public: +//constructors + /// normal constructor + jamtis_address_tag_cipher_context(const crypto::secret_key &cipher_key); + +//destructor + ~jamtis_address_tag_cipher_context(); + +//overloaded operators + /// disable copy/move (this is a scoped manager) + jamtis_address_tag_cipher_context& operator=(jamtis_address_tag_cipher_context&&) = delete; + +//member functions + address_tag_t cipher(const address_index_t &j) const; + bool try_decipher(const address_tag_t &addr_tag, address_index_t &j_out) const; + +//member variables +private: + crypto::secret_key m_cipher_key; + Twofish_key m_twofish_key; +}; + +/// addr_tag = cipher[k](j) || H_2(k, cipher[k](j)) +address_tag_t cipher_address_index(const jamtis_address_tag_cipher_context &cipher_context, const address_index_t &j); +address_tag_t cipher_address_index(const crypto::secret_key &cipher_key, const address_index_t &j); + +/// try to get j from an address tag +bool try_decipher_address_index(const jamtis_address_tag_cipher_context &cipher_context, + const address_tag_t &addr_tag, + address_index_t &j_out); +bool try_decipher_address_index(const crypto::secret_key &cipher_key, + const address_tag_t &addr_tag, + address_index_t &j_out); + +/// addr_tag_enc = addr_tag XOR H_32(q, Ko) +encrypted_address_tag_t encrypt_address_tag(const rct::key &sender_receiver_secret, + const rct::key &onetime_address, + const address_tag_t &addr_tag); + +/// addr_tag = addr_tag_enc XOR H_32(q, Ko) +address_tag_t decrypt_address_tag(const rct::key &sender_receiver_secret, + const rct::key &onetime_address, + const encrypted_address_tag_t &addr_tag_enc); + +/// generate a random tag +void gen_address_tag(address_tag_t &addr_tag_inout); + +} //namespace jamtis +} //namespace sp diff --git a/src/seraphis_core/jamtis_address_utils.cpp b/src/seraphis_core/jamtis_address_utils.cpp new file mode 100644 index 0000000000..a36d1ab28c --- /dev/null +++ b/src/seraphis_core/jamtis_address_utils.cpp @@ -0,0 +1,202 @@ +// Copyright (c) 2022, 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 "jamtis_address_utils.h" + +//local headers +#include "crypto/crypto.h" +#include "crypto/x25519.h" +#include "cryptonote_config.h" +#include "jamtis_core_utils.h" +#include "jamtis_support_types.h" +#include "ringct/rctOps.h" +#include "seraphis_crypto/sp_crypto_utils.h" +#include "seraphis_crypto/sp_hash_functions.h" +#include "seraphis_crypto/sp_transcript.h" +#include "sp_core_enote_utils.h" + +//third party headers +#include + +//standard headers + + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis" + +namespace sp +{ +namespace jamtis +{ +//------------------------------------------------------------------------------------------------------------------- +void make_jamtis_index_extension_generator(const crypto::secret_key &s_generate_address, + const address_index_t &j, + crypto::secret_key &generator_out) +{ + // s^j_gen = H_32[s_ga](j) + SpKDFTranscript transcript{config::HASH_KEY_JAMTIS_INDEX_EXTENSION_GENERATOR, ADDRESS_INDEX_BYTES}; + transcript.append("j", j.bytes); + + sp_derive_secret(to_bytes(s_generate_address), transcript.data(), transcript.size(), to_bytes(generator_out)); +} +//------------------------------------------------------------------------------------------------------------------- +void make_jamtis_spendkey_extension(const boost::string_ref domain_separator, + const rct::key &spend_pubkey, + const address_index_t &j, + const crypto::secret_key &generator, + crypto::secret_key &extension_out) +{ + // k^j_? = H_n(K_s, j, s^j_gen) + SpKDFTranscript transcript{domain_separator, 2*32 + ADDRESS_INDEX_BYTES}; + transcript.append("K_s", spend_pubkey); + transcript.append("j", j.bytes); + transcript.append("generator", generator); + + sp_hash_to_scalar(transcript.data(), transcript.size(), to_bytes(extension_out)); +} +//------------------------------------------------------------------------------------------------------------------- +void make_jamtis_spendkey_extension(const boost::string_ref domain_separator, + const rct::key &spend_pubkey, + const crypto::secret_key &s_generate_address, + const address_index_t &j, + crypto::secret_key &extension_out) +{ + // s^j_gen + crypto::secret_key generator; + make_jamtis_index_extension_generator(s_generate_address, j, generator); + + // k^j_? + make_jamtis_spendkey_extension(domain_separator, spend_pubkey, j, generator, extension_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_jamtis_spendkey_extension_g(const rct::key &spend_pubkey, + const crypto::secret_key &s_generate_address, + const address_index_t &j, + crypto::secret_key &extension_out) +{ + // k^j_g = H_n("..g..", K_s, j, H_32[s_ga](j)) + make_jamtis_spendkey_extension(config::HASH_KEY_JAMTIS_SPENDKEY_EXTENSION_G, + spend_pubkey, + s_generate_address, + j, + extension_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_jamtis_spendkey_extension_x(const rct::key &spend_pubkey, + const crypto::secret_key &s_generate_address, + const address_index_t &j, + crypto::secret_key &extension_out) +{ + // k^j_x = H_n("..x..", K_s, j, H_32[s_ga](j)) + make_jamtis_spendkey_extension(config::HASH_KEY_JAMTIS_SPENDKEY_EXTENSION_X, + spend_pubkey, + s_generate_address, + j, + extension_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_jamtis_spendkey_extension_u(const rct::key &spend_pubkey, + const crypto::secret_key &s_generate_address, + const address_index_t &j, + crypto::secret_key &extension_out) +{ + // k^j_u = H_n("..u..", K_s, j, H_32[s_ga](j)) + make_jamtis_spendkey_extension(config::HASH_KEY_JAMTIS_SPENDKEY_EXTENSION_U, + spend_pubkey, + s_generate_address, + j, + extension_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_jamtis_address_privkey(const rct::key &spend_pubkey, + const crypto::secret_key &s_generate_address, + const address_index_t &j, + crypto::x25519_secret_key &address_privkey_out) +{ + // s^j_gen + crypto::secret_key generator; + make_jamtis_index_extension_generator(s_generate_address, j, generator); + + // xk^j_a = H_n_x25519(K_s, j, H_32[s_ga](j)) + SpKDFTranscript transcript{config::HASH_KEY_JAMTIS_ADDRESS_PRIVKEY, ADDRESS_INDEX_BYTES}; + transcript.append("K_s", spend_pubkey); + transcript.append("j", j.bytes); + transcript.append("generator", generator); + + sp_hash_to_x25519_scalar(transcript.data(), transcript.size(), address_privkey_out.data); +} +//------------------------------------------------------------------------------------------------------------------- +void make_jamtis_address_spend_key(const rct::key &spend_pubkey, + const crypto::secret_key &s_generate_address, + const address_index_t &j, + rct::key &address_spendkey_out) +{ + // K_1 = k^j_g G + k^j_x X + k^j_u U + K_s + crypto::secret_key address_extension_key_u; + crypto::secret_key address_extension_key_x; + crypto::secret_key address_extension_key_g; + make_jamtis_spendkey_extension_u(spend_pubkey, s_generate_address, j, address_extension_key_u); //k^j_u + make_jamtis_spendkey_extension_x(spend_pubkey, s_generate_address, j, address_extension_key_x); //k^j_x + make_jamtis_spendkey_extension_g(spend_pubkey, s_generate_address, j, address_extension_key_g); //k^j_g + + address_spendkey_out = spend_pubkey; //K_s + extend_seraphis_spendkey_u(address_extension_key_u, address_spendkey_out); //k^j_u U + K_s + extend_seraphis_spendkey_x(address_extension_key_x, address_spendkey_out); //k^j_x X + k^j_u U + K_s + mask_key(address_extension_key_g, address_spendkey_out, address_spendkey_out); //k^j_g G + k^j_x X + k^j_u U + K_s +} +//------------------------------------------------------------------------------------------------------------------- +void make_seraphis_key_image_jamtis_style(const rct::key &spend_pubkey, + const crypto::secret_key &k_view_balance, + const crypto::secret_key &spendkey_extension_x, + const crypto::secret_key &spendkey_extension_u, + const crypto::secret_key &sender_extension_x, + const crypto::secret_key &sender_extension_u, + crypto::key_image &key_image_out) +{ + // KI = ((k^o_u + k^j_u + k_m)/(k^o_x + k^j_x + k_vb)) U + + // k_m U = K_s - k_vb X + rct::key zU{spend_pubkey}; //K_s = k_vb X + k_m U + reduce_seraphis_spendkey_x(k_view_balance, zU); //k_m U + + // z U = (k_u + k_m) U = k^o_u U + k^j_u U + k_m U + extend_seraphis_spendkey_u(spendkey_extension_u, zU); //k^j_u U + k_m U + extend_seraphis_spendkey_u(sender_extension_u, zU); //k^o_u U + k^j_u U + k_m U + + // y = k^o_x + k^j_x + k_vb + crypto::secret_key y; + sc_add(to_bytes(y), to_bytes(sender_extension_x), to_bytes(spendkey_extension_x)); //k^o_x + k^j_x + sc_add(to_bytes(y), to_bytes(y), to_bytes(k_view_balance)); //+ k_vb + + // KI = (1/y)*(k_u + k_m)*U + make_seraphis_key_image(y, rct::rct2pk(zU), key_image_out); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace jamtis +} //namespace sp diff --git a/src/seraphis_core/jamtis_address_utils.h b/src/seraphis_core/jamtis_address_utils.h new file mode 100644 index 0000000000..2b5c1e4bf1 --- /dev/null +++ b/src/seraphis_core/jamtis_address_utils.h @@ -0,0 +1,160 @@ +// Copyright (c) 2022, 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 Jamtis addresses. + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "crypto/x25519.h" +#include "jamtis_support_types.h" +#include "ringct/rctTypes.h" + +//third party headers + +//standard headers +#include + +//forward declarations + + +namespace sp +{ +namespace jamtis +{ + +/** +* brief: make_jamtis_index_extension_generator - s^j_gen +* - s^j_gen = H_32[s_ga](j) +* param: s_generate_address - s_ga +* param: j - address index +* outparam: generator_out - s^j_gen +*/ +void make_jamtis_index_extension_generator(const crypto::secret_key &s_generate_address, + const address_index_t &j, + crypto::secret_key &generator_out); +/** +* brief: make_jamtis_spendkey_extension - k^j_? +* - k^j_? = H_n("domain separator", K_s, j, s^j_gen) +* param: spend_pubkey - K_s = k_vb X + k_m U +* param: j - address index +* param: generator - s^j_gen +* outparam: extension_out - k^j_g +*/ +void make_jamtis_spendkey_extension(const boost::string_ref domain_separator, + const rct::key &spend_pubkey, + const address_index_t &j, + const crypto::secret_key &generator, + crypto::secret_key &extension_out); +void make_jamtis_spendkey_extension(const boost::string_ref domain_separator, + const rct::key &spend_pubkey, + const crypto::secret_key &s_generate_address, + const address_index_t &j, + crypto::secret_key &extension_out); +/** +* brief: make_jamtis_spendkey_extension_g - k^j_g +* - k^j_g = H_n("..g..", K_s, j, s^j_gen) +* param: spend_pubkey - K_s = k_vb X + k_m U +* param: s_generate_address - s_ga +* param: j - address index +* outparam: extension_out - k^j_g +*/ +void make_jamtis_spendkey_extension_g(const rct::key &spend_pubkey, + const crypto::secret_key &s_generate_address, + const address_index_t &j, + crypto::secret_key &extension_out); +/** +* brief: make_jamtis_spendkey_extension_x - k^j_x +* - k^j_x = H_n("..x..", K_s, j, s^j_gen) +* param: spend_pubkey - K_s = k_vb X + k_m U +* param: s_generate_address - s_ga +* param: j - address index +* outparam: extension_out - k^j_x +*/ +void make_jamtis_spendkey_extension_x(const rct::key &spend_pubkey, + const crypto::secret_key &s_generate_address, + const address_index_t &j, + crypto::secret_key &extension_out); +/** +* brief: make_jamtis_spendkey_extension_u - k^j_u +* - k^j_u = H_n("..u..", K_s, j, s^j_gen) +* param: spend_pubkey - K_s = k_vb X + k_m U +* param: s_generate_address - s_ga +* param: j - address index +* outparam: extension_out - k^j_u +*/ +void make_jamtis_spendkey_extension_u(const rct::key &spend_pubkey, + const crypto::secret_key &s_generate_address, + const address_index_t &j, + crypto::secret_key &extension_out); +/** +* brief: make_jamtis_address_privkey - xk^j_a +* - xk^j_a = H_n_x25519(K_s, j, s^j_gen) +* param: spend_pubkey - K_s = k_vb X + k_m U +* param: s_generate_address - s_ga +* param: j - address index +* outparam: address_privkey_out - xk^j_a +*/ +void make_jamtis_address_privkey(const rct::key &spend_pubkey, + const crypto::secret_key &s_generate_address, + const address_index_t &j, + crypto::x25519_secret_key &address_privkey_out); +/** +* brief: make_jamtis_address_spend_key - K_1 +* - K_1 = k^j_g G + k^j_x X + k^j_u U + K_s +* param: spend_pubkey - K_s = k_vb X + k_m U +* param: s_generate_address - s_ga +* param: j - address index +* outparam: address_spendkey_out - K_1 +*/ +void make_jamtis_address_spend_key(const rct::key &spend_pubkey, + const crypto::secret_key &s_generate_address, + const address_index_t &j, + rct::key &address_spendkey_out); +/** +* brief: make_seraphis_key_image_jamtis_style - KI +* - KI = ((k^o_u + k^j_u + k_m)/(k^o_x + k^j_x + k_vb)) U +* param: spend_pubkey - K_s = k_vb X + k_m U +* param: k_view_balance - k_vb +* param: spendkey_extension_x - k^j_x +* param: spendkey_extension_u - k^j_u +* param: sender_extension_x - k^o_x +* param: sender_extension_u - k^o_u +* outparam: key_image_out - KI +*/ +void make_seraphis_key_image_jamtis_style(const rct::key &spend_pubkey, + const crypto::secret_key &k_view_balance, + const crypto::secret_key &spendkey_extension_x, + const crypto::secret_key &spendkey_extension_u, + const crypto::secret_key &sender_extension_x, + const crypto::secret_key &sender_extension_u, + crypto::key_image &key_image_out); + +} //namespace jamtis +} //namespace sp diff --git a/src/seraphis_core/jamtis_core_utils.cpp b/src/seraphis_core/jamtis_core_utils.cpp new file mode 100644 index 0000000000..d1984aa7dd --- /dev/null +++ b/src/seraphis_core/jamtis_core_utils.cpp @@ -0,0 +1,103 @@ +// Copyright (c) 2022, 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 "jamtis_core_utils.h" + +//local headers +#include "crypto/crypto.h" +#include "crypto/x25519.h" +#include "cryptonote_config.h" +#include "ringct/rctOps.h" +#include "seraphis_crypto/sp_crypto_utils.h" +#include "seraphis_crypto/sp_hash_functions.h" +#include "seraphis_crypto/sp_transcript.h" +#include "sp_core_enote_utils.h" + +//third party headers + +//standard headers + + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis" + +namespace sp +{ +namespace jamtis +{ +//------------------------------------------------------------------------------------------------------------------- +void make_jamtis_unlockamounts_key(const crypto::secret_key &k_view_balance, + crypto::x25519_secret_key &xk_unlock_amounts_out) +{ + // xk_ua = H_n_x25519[k_vb]() + SpKDFTranscript transcript{config::HASH_KEY_JAMTIS_UNLOCKAMOUNTS_KEY, 0}; + sp_derive_x25519_key(to_bytes(k_view_balance), transcript.data(), transcript.size(), xk_unlock_amounts_out.data); +} +//------------------------------------------------------------------------------------------------------------------- +void make_jamtis_unlockamounts_pubkey(const crypto::x25519_secret_key &xk_unlock_amounts, + crypto::x25519_pubkey &unlockamounts_pubkey_out) +{ + // xK_ua = xk_ua * xG + x25519_scmul_base(xk_unlock_amounts, unlockamounts_pubkey_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_jamtis_findreceived_key(const crypto::secret_key &k_view_balance, + crypto::x25519_secret_key &xk_find_received_out) +{ + // xk_fr = H_n_x25519[k_vb]() + SpKDFTranscript transcript{config::HASH_KEY_JAMTIS_FINDRECEIVED_KEY, 0}; + sp_derive_x25519_key(to_bytes(k_view_balance), transcript.data(), transcript.size(), xk_find_received_out.data); +} +//------------------------------------------------------------------------------------------------------------------- +void make_jamtis_findreceived_pubkey(const crypto::x25519_secret_key &xk_find_received, + const crypto::x25519_pubkey &unlockamounts_pubkey, + crypto::x25519_pubkey &findreceived_pubkey_out) +{ + // xK_fr = xk_fr * xK_ua + x25519_scmul_key(xk_find_received, unlockamounts_pubkey, findreceived_pubkey_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_jamtis_generateaddress_secret(const crypto::secret_key &k_view_balance, + crypto::secret_key &s_generate_address_out) +{ + // s_ga = H_32[k_vb]() + SpKDFTranscript transcript{config::HASH_KEY_JAMTIS_GENERATEADDRESS_SECRET, 0}; + sp_derive_secret(to_bytes(k_view_balance), transcript.data(), transcript.size(), to_bytes(s_generate_address_out)); +} +//------------------------------------------------------------------------------------------------------------------- +void make_jamtis_ciphertag_secret(const crypto::secret_key &s_generate_address, + crypto::secret_key &s_cipher_tag_out) +{ + // s_ct = H_32[s_ga]() + SpKDFTranscript transcript{config::HASH_KEY_JAMTIS_CIPHERTAG_SECRET, 0}; + sp_derive_secret(to_bytes(s_generate_address), transcript.data(), transcript.size(), to_bytes(s_cipher_tag_out)); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace jamtis +} //namespace sp diff --git a/src/seraphis_core/jamtis_core_utils.h b/src/seraphis_core/jamtis_core_utils.h new file mode 100644 index 0000000000..f86afa37b2 --- /dev/null +++ b/src/seraphis_core/jamtis_core_utils.h @@ -0,0 +1,108 @@ +// Copyright (c) 2022, 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 Jamtis privkeys, secrets, and pubkeys. +// - Jamtis is a specification for Seraphis-compatible addresses +// +// reference: https://gist.github.com/tevador/50160d160d24cfc6c52ae02eb3d17024 +/// + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "crypto/x25519.h" +#include "ringct/rctTypes.h" + +//third party headers + +//standard headers + +//forward declarations + + +namespace sp +{ +namespace jamtis +{ + +/** +* brief: make_jamtis_unlockamounts_key - unlock-amounts key, for recovering amounts and reconstructing amount commitments +* xk_ua = H_n_x25519[k_vb]() +* param: k_view_balance - k_vb +* outparam: xk_unlock_amounts_out - xk_ua +*/ +void make_jamtis_unlockamounts_key(const crypto::secret_key &k_view_balance, + crypto::x25519_secret_key &xk_unlock_amounts_out); +/** +* brief: make_jamtis_unlockamounts_pubkey - xK_ua +* - xK_ua = xk_ua * xG +* param: xk_unlock_amounts - xk_ua +* outparam: unlockamounts_pubkey_out - xK_ua +*/ +void make_jamtis_unlockamounts_pubkey(const crypto::x25519_secret_key &xk_unlock_amounts, + crypto::x25519_pubkey &unlockamounts_pubkey_out); +/** +* brief: make_jamtis_findreceived_key - find-received key, for finding enotes received by the wallet +* - use to compute view tags and nominal spend keys +* xk_fr = H_n_x25519[k_vb]() +* param: k_view_balance - k_vb +* outparam: xk_find_received_out - xk_fr +*/ +void make_jamtis_findreceived_key(const crypto::secret_key &k_view_balance, + crypto::x25519_secret_key &xk_find_received_out); +/** +* brief: make_jamtis_findreceived_pubkey - xK_fr +* - xK_fr = xk_fr * xK_ua +* param: xk_find_received - xk_fr +* param: unlockamounts_pubkey - xK_ua +* outparam: findreceived_pubkey_out - xK_fr +*/ +void make_jamtis_findreceived_pubkey(const crypto::x25519_secret_key &xk_find_received, + const crypto::x25519_pubkey &unlockamounts_pubkey, + crypto::x25519_pubkey &findreceived_pubkey_out); +/** +* brief: make_jamtis_generateaddress_secret - generate-address secret, for generating addresses +* s_ga = H_32[k_vb]() +* param: k_view_balance - k_vb +* outparam: s_generate_address_out - s_ga +*/ +void make_jamtis_generateaddress_secret(const crypto::secret_key &k_view_balance, + crypto::secret_key &s_generate_address_out); +/** +* brief: make_jamtis_ciphertag_secret - cipher-tag secret, for ciphering address indices to/from address tags +* s_ct = H_32[s_ga]() +* param: s_generate_address - s_ga +* outparam: s_cipher_tag_out - s_ct +*/ +void make_jamtis_ciphertag_secret(const crypto::secret_key &s_generate_address, + crypto::secret_key &s_cipher_tag_out); + +} //namespace jamtis +} //namespace sp diff --git a/src/seraphis_core/jamtis_destination.cpp b/src/seraphis_core/jamtis_destination.cpp new file mode 100644 index 0000000000..2e839dc270 --- /dev/null +++ b/src/seraphis_core/jamtis_destination.cpp @@ -0,0 +1,136 @@ +// Copyright (c) 2022, 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 "jamtis_destination.h" + +//local headers +#include "crypto/crypto.h" +#include "crypto/x25519.h" +#include "jamtis_address_tag_utils.h" +#include "jamtis_address_utils.h" +#include "jamtis_core_utils.h" +#include "jamtis_support_types.h" +#include "ringct/rctOps.h" +#include "ringct/rctTypes.h" + +//third party headers + +//standard headers + + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis" + +namespace sp +{ +namespace jamtis +{ +//------------------------------------------------------------------------------------------------------------------- +bool operator==(const JamtisDestinationV1 &a, const JamtisDestinationV1 &b) +{ + return (a.addr_K1 == b.addr_K1) && + (a.addr_K2 == b.addr_K2) && + (a.addr_K3 == b.addr_K3) && + (a.addr_tag == b.addr_tag); +} +//------------------------------------------------------------------------------------------------------------------- +void make_jamtis_destination_v1(const rct::key &spend_pubkey, + const crypto::x25519_pubkey &unlockamounts_pubkey, + const crypto::x25519_pubkey &findreceived_pubkey, + const crypto::secret_key &s_generate_address, + const address_index_t &j, + JamtisDestinationV1 &destination_out) +{ + // K_1 = k^j_g G + k^j_x X + k^j_u U + K_s + make_jamtis_address_spend_key(spend_pubkey, s_generate_address, j, destination_out.addr_K1); + + // xK_2 = xk^j_a xK_fr + crypto::x25519_secret_key address_privkey; + make_jamtis_address_privkey(spend_pubkey, s_generate_address, j, address_privkey); //xk^j_a + + x25519_scmul_key(address_privkey, findreceived_pubkey, destination_out.addr_K2); + + // xK_3 = xk^j_a xK_ua + x25519_scmul_key(address_privkey, unlockamounts_pubkey, destination_out.addr_K3); + + // addr_tag = cipher[k](j) || H_2(k, cipher[k](j)) + crypto::secret_key ciphertag_secret; + make_jamtis_ciphertag_secret(s_generate_address, ciphertag_secret); + + destination_out.addr_tag = cipher_address_index(ciphertag_secret, j); +} +//------------------------------------------------------------------------------------------------------------------- +bool try_get_jamtis_index_from_destination_v1(const JamtisDestinationV1 &destination, + const rct::key &spend_pubkey, + const crypto::x25519_pubkey &unlockamounts_pubkey, + const crypto::x25519_pubkey &findreceived_pubkey, + const crypto::secret_key &s_generate_address, + address_index_t &j_out) +{ + // ciphertag secret + crypto::secret_key ciphertag_secret; + make_jamtis_ciphertag_secret(s_generate_address, ciphertag_secret); + + // get the nominal address index from the destination's address tag + address_index_t nominal_address_index; + if (!try_decipher_address_index(ciphertag_secret, destination.addr_tag, nominal_address_index)) + return false; + + // recreate the destination + JamtisDestinationV1 test_destination; + + make_jamtis_destination_v1(spend_pubkey, + unlockamounts_pubkey, + findreceived_pubkey, + s_generate_address, + nominal_address_index, + test_destination); + + // check the destinations are the same + // note: partial equality will return false + if (!(test_destination == destination)) + return false; + + j_out = nominal_address_index; + return true; +} +//------------------------------------------------------------------------------------------------------------------- +JamtisDestinationV1 gen_jamtis_destination_v1() +{ + JamtisDestinationV1 temp; + temp.addr_K1 = rct::pkGen(); + temp.addr_K2 = crypto::x25519_pubkey_gen(); + temp.addr_K3 = crypto::x25519_pubkey_gen(); + crypto::rand(sizeof(address_tag_t), temp.addr_tag.bytes); + + return temp; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace jamtis +} //namespace sp diff --git a/src/seraphis_core/jamtis_destination.h b/src/seraphis_core/jamtis_destination.h new file mode 100644 index 0000000000..a1961e34af --- /dev/null +++ b/src/seraphis_core/jamtis_destination.h @@ -0,0 +1,109 @@ +// Copyright (c) 2022, 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 Jamtis 'destination', i.e. an address that can receive funds. + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "crypto/x25519.h" +#include "jamtis_support_types.h" +#include "ringct/rctTypes.h" + +//third party headers + +//standard headers + +//forward declarations + + +namespace sp +{ +namespace jamtis +{ + +//// +// JamtisDestinationV1 +// - a user address, aka a 'destination for funds' +/// +struct JamtisDestinationV1 final +{ + /// K_1 = k^j_g G + k^j_x X + k^j_u U + K_s (address spend key) + rct::key addr_K1; + /// xK_2 = xk^j_a xK_fr (address view key) + crypto::x25519_pubkey addr_K2; + /// xK_3 = xk^j_a xK_ua (DH base key) + crypto::x25519_pubkey addr_K3; + /// addr_tag + address_tag_t addr_tag; +}; + +/// equivalence test (false on partial equality) +bool operator==(const JamtisDestinationV1 &a, const JamtisDestinationV1 &b); + +/** +* brief: make_jamtis_destination_v1 - make a destination address +* param: spend_pubkey - K_s = k_vb X + k_m U +* param: unlockamounts_pubkey - xK_ua = xk_ua xG +* param: findreceived_pubkey - xK_fr = xk_fr xk_ua xG +* param: s_generate_address - s_ga +* param: j - address_index +* outparam: destination_out - the full address, with address tag +*/ +void make_jamtis_destination_v1(const rct::key &spend_pubkey, + const crypto::x25519_pubkey &unlockamounts_pubkey, + const crypto::x25519_pubkey &findreceived_pubkey, + const crypto::secret_key &s_generate_address, + const address_index_t &j, + JamtisDestinationV1 &destination_out); +/** +* brief: try_get_jamtis_index_from_destination_v1 - check if a destination can be recreated, then return its address index +* - note: partial-recreation of a destination will return FALSE +* param: destination - destination address to recreate +* param: spend_pubkey - K_s +* param: unlockamounts_pubkey - xK_ua = xk_ua xG +* param: findreceived_pubkey - xK_fr = xk_fr xk_ua xG +* param: s_generate_address - s_ga +* outparam: j_out - address index (if successful) +* return: true if the destination can be recreated +*/ +bool try_get_jamtis_index_from_destination_v1(const JamtisDestinationV1 &destination, + const rct::key &spend_pubkey, + const crypto::x25519_pubkey &unlockamounts_pubkey, + const crypto::x25519_pubkey &findreceived_pubkey, + const crypto::secret_key &s_generate_address, + address_index_t &j_out); +/** +* brief: gen_jamtis_destination_v1 - generate a random destination +* return: a random destination +*/ +JamtisDestinationV1 gen_jamtis_destination_v1(); + +} //namespace jamtis +} //namespace sp diff --git a/src/seraphis_core/jamtis_enote_utils.cpp b/src/seraphis_core/jamtis_enote_utils.cpp new file mode 100644 index 0000000000..bdfa35f953 --- /dev/null +++ b/src/seraphis_core/jamtis_enote_utils.cpp @@ -0,0 +1,469 @@ +// Copyright (c) 2022, 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 "jamtis_enote_utils.h" + +//local headers +#include "crypto/crypto.h" +extern "C" +{ +#include "crypto/crypto-ops.h" +} +#include "crypto/x25519.h" +#include "cryptonote_config.h" +#include "int-util.h" +#include "jamtis_support_types.h" +#include "misc_language.h" +#include "misc_log_ex.h" +#include "ringct/rctOps.h" +#include "ringct/rctTypes.h" +#include "seraphis_crypto/sp_crypto_utils.h" +#include "seraphis_crypto/sp_hash_functions.h" +#include "seraphis_crypto/sp_transcript.h" +#include "sp_core_enote_utils.h" + +//third party headers +#include + +//standard headers + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis" + +namespace sp +{ +namespace jamtis +{ +//------------------------------------------------------------------------------------------------------------------- +// derivation = privkey * DH_key (with X25519) +// note: X25519 DH derivations are implicitly mul 8 +//------------------------------------------------------------------------------------------------------------------- +static auto make_derivation_with_wiper(const crypto::x25519_secret_key &privkey, + const crypto::x25519_pubkey &DH_key, + crypto::x25519_pubkey &derivation_out) +{ + auto a_wiper = epee::misc_utils::create_scope_leave_handler( + [&derivation_out]() + { + memwipe(&derivation_out, sizeof(derivation_out)); + } + ); + + x25519_scmul_key(privkey, DH_key, derivation_out); + + return a_wiper; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static const boost::string_ref selfsend_sender_receiver_secret_domain_separator(const JamtisSelfSendType self_send_type) +{ + CHECK_AND_ASSERT_THROW_MES(self_send_type <= JamtisSelfSendType::MAX, + "jamtis self-send sender-receiver secret: unknown self-send type."); + + // dummy self-send + if (self_send_type == JamtisSelfSendType::DUMMY) + return config::HASH_KEY_JAMTIS_SENDER_RECEIVER_SECRET_SELFSEND_DUMMY; + + // change self-send + if (self_send_type == JamtisSelfSendType::CHANGE) + return config::HASH_KEY_JAMTIS_SENDER_RECEIVER_SECRET_SELFSEND_CHANGE; + + // self-spend self-send + if (self_send_type == JamtisSelfSendType::SELF_SPEND) + return config::HASH_KEY_JAMTIS_SENDER_RECEIVER_SECRET_SELFSEND_SELF_SPEND; + + CHECK_AND_ASSERT_THROW_MES(false, "jamtis self-send sender-receiver secret domain separator error."); + return ""; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static encoded_amount_t enc_amount(const rct::xmr_amount amount, const encoded_amount_t &mask) +{ + static_assert(sizeof(rct::xmr_amount) == sizeof(encoded_amount_t), ""); + + // little_endian(amount) XOR mask + encoded_amount_t amount_LE; + memcpy_swap64le(amount_LE.bytes, &amount, 1); + return amount_LE ^ mask; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static rct::xmr_amount dec_amount(const encoded_amount_t &encoded_amount, const encoded_amount_t &mask) +{ + static_assert(sizeof(rct::xmr_amount) == sizeof(encoded_amount_t), ""); + + // system_endian(encoded_amount XOR H_8(q, xr xG)) + const encoded_amount_t decoded_amount{encoded_amount ^ mask}; + rct::xmr_amount amount; + memcpy_swap64le(&amount, &decoded_amount, 1); + return amount; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static encoded_amount_t jamtis_encoded_amount_mask(const rct::key &sender_receiver_secret, const rct::key &baked_key) +{ + static_assert(sizeof(encoded_amount_t) == 8, ""); + + // H_8(q, baked_key) + SpKDFTranscript transcript{config::HASH_KEY_JAMTIS_ENCODED_AMOUNT_MASK, 2*sizeof(rct::key)}; + transcript.append("q", sender_receiver_secret); + transcript.append("baked_key", baked_key); + + encoded_amount_t mask; + sp_hash_to_8(transcript.data(), transcript.size(), mask.bytes); + + return mask; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void make_jamtis_amount_baked_key_plain(const crypto::x25519_pubkey &reverse_sender_receiver_secret, + rct::key &baked_key_out) +{ + // [plain] baked_key = H_32(xR) + SpKDFTranscript transcript{config::HASH_KEY_JAMTIS_AMOUNT_BAKED_KEY_PLAIN, sizeof(rct::key)}; + transcript.append("xR", reverse_sender_receiver_secret); + + sp_hash_to_32(transcript.data(), transcript.size(), baked_key_out.bytes); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +void make_jamtis_enote_ephemeral_pubkey(const crypto::x25519_secret_key &enote_ephemeral_privkey, + const crypto::x25519_pubkey &DH_base, + crypto::x25519_pubkey &enote_ephemeral_pubkey_out) +{ + // xK_e = xr xK_3 + x25519_scmul_key(enote_ephemeral_privkey, DH_base, enote_ephemeral_pubkey_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_jamtis_view_tag(const crypto::x25519_pubkey &sender_receiver_DH_derivation, + const rct::key &onetime_address, + view_tag_t &view_tag_out) +{ + static_assert(sizeof(view_tag_t) == 1, ""); + + // view_tag = H_1(xK_d, Ko) + SpKDFTranscript transcript{config::HASH_KEY_JAMTIS_VIEW_TAG, 2*sizeof(rct::key)}; + transcript.append("xK_d", sender_receiver_DH_derivation); + transcript.append("Ko", onetime_address); + + sp_hash_to_1(transcript.data(), transcript.size(), &view_tag_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_jamtis_view_tag(const crypto::x25519_secret_key &privkey, + const crypto::x25519_pubkey &DH_key, + const rct::key &onetime_address, + view_tag_t &view_tag_out) +{ + // xK_d = privkey * DH_key + crypto::x25519_pubkey derivation; + auto a_wiper = make_derivation_with_wiper(privkey, DH_key, derivation); + + // view_tag = H_1(xK_d, Ko) + make_jamtis_view_tag(derivation, onetime_address, view_tag_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_jamtis_input_context_coinbase(const std::uint64_t block_height, rct::key &input_context_out) +{ + // block height as varint + SpFSTranscript transcript{config::HASH_KEY_JAMTIS_INPUT_CONTEXT_COINBASE, 4}; + transcript.append("height", block_height); + + // input_context (coinbase) = H_32(block height) + sp_hash_to_32(transcript.data(), transcript.size(), input_context_out.bytes); +} +//------------------------------------------------------------------------------------------------------------------- +void make_jamtis_input_context_standard(const std::vector &legacy_input_key_images, + const std::vector &sp_input_key_images, + rct::key &input_context_out) +{ + CHECK_AND_ASSERT_THROW_MES(std::is_sorted(legacy_input_key_images.begin(), legacy_input_key_images.end()), + "jamtis input context (standard): legacy key images are not sorted."); + CHECK_AND_ASSERT_THROW_MES(std::is_sorted(sp_input_key_images.begin(), sp_input_key_images.end()), + "jamtis input context (standard): seraphis key images are not sorted."); + + // {legacy KI} || {seraphis KI} + SpFSTranscript transcript{ + config::HASH_KEY_JAMTIS_INPUT_CONTEXT_STANDARD, + (legacy_input_key_images.size() + sp_input_key_images.size())*sizeof(crypto::key_image) + }; + transcript.append("legacy_input_KI", legacy_input_key_images); + transcript.append("sp_input_KI", sp_input_key_images); + + // input_context (standard) = H_32({legacy KI}, {seraphis KI}) + sp_hash_to_32(transcript.data(), transcript.size(), input_context_out.bytes); +} +//------------------------------------------------------------------------------------------------------------------- +void make_jamtis_sender_receiver_secret_plain(const crypto::x25519_pubkey &sender_receiver_DH_derivation, + const crypto::x25519_pubkey &enote_ephemeral_pubkey, + const rct::key &input_context, + rct::key &sender_receiver_secret_out) +{ + // q = H_32(xK_d, xK_e, input_context) + SpKDFTranscript transcript{config::HASH_KEY_JAMTIS_SENDER_RECEIVER_SECRET_PLAIN, 3*sizeof(rct::key)}; + transcript.append("xK_d", sender_receiver_DH_derivation); + transcript.append("xK_e", enote_ephemeral_pubkey); + transcript.append("input_context", input_context); + + sp_hash_to_32(transcript.data(), transcript.size(), sender_receiver_secret_out.bytes); +} +//------------------------------------------------------------------------------------------------------------------- +void make_jamtis_sender_receiver_secret_plain(const crypto::x25519_secret_key &privkey, + const crypto::x25519_pubkey &DH_key, + const crypto::x25519_pubkey &enote_ephemeral_pubkey, + const rct::key &input_context, + rct::key &sender_receiver_secret_out) +{ + // xK_d = privkey * DH_key + crypto::x25519_pubkey derivation; + auto a_wiper = make_derivation_with_wiper(privkey, DH_key, derivation); + + // q = H_32(xK_d, xK_e, input_context) + make_jamtis_sender_receiver_secret_plain(derivation, + enote_ephemeral_pubkey, + input_context, + sender_receiver_secret_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_jamtis_sender_receiver_secret_selfsend(const crypto::secret_key &k_view_balance, + const crypto::x25519_pubkey &enote_ephemeral_pubkey, + const rct::key &input_context, + const JamtisSelfSendType self_send_type, + rct::key &sender_receiver_secret_out) +{ + // q = H_32[k_vb](xK_e, input_context) + SpKDFTranscript transcript{selfsend_sender_receiver_secret_domain_separator(self_send_type), 2*sizeof(rct::key)}; + transcript.append("xK_e", enote_ephemeral_pubkey); + transcript.append("input_context", input_context); + + sp_derive_secret(to_bytes(k_view_balance), transcript.data(), transcript.size(), sender_receiver_secret_out.bytes); +} +//------------------------------------------------------------------------------------------------------------------- +void make_jamtis_onetime_address_extension_g(const rct::key &recipient_address_spend_key, + const rct::key &sender_receiver_secret, + const rct::key &amount_commitment, + crypto::secret_key &sender_extension_out) +{ + // k_{g, sender} = H_n("..g..", K_1, q, C) + SpKDFTranscript transcript{config::HASH_KEY_JAMTIS_SENDER_ONETIME_ADDRESS_EXTENSION_G, 3*sizeof(rct::key)}; + transcript.append("K_1", recipient_address_spend_key); + transcript.append("q", sender_receiver_secret); + transcript.append("C", amount_commitment); + + sp_hash_to_scalar(transcript.data(), transcript.size(), to_bytes(sender_extension_out)); +} +//------------------------------------------------------------------------------------------------------------------- +void make_jamtis_onetime_address_extension_x(const rct::key &recipient_address_spend_key, + const rct::key &sender_receiver_secret, + const rct::key &amount_commitment, + crypto::secret_key &sender_extension_out) +{ + // k_{x, sender} = H_n("..x..", K_1, q, C) + SpKDFTranscript transcript{config::HASH_KEY_JAMTIS_SENDER_ONETIME_ADDRESS_EXTENSION_X, 3*sizeof(rct::key)}; + transcript.append("K_1", recipient_address_spend_key); + transcript.append("q", sender_receiver_secret); + transcript.append("C", amount_commitment); + + sp_hash_to_scalar(transcript.data(), transcript.size(), to_bytes(sender_extension_out)); +} +//------------------------------------------------------------------------------------------------------------------- +void make_jamtis_onetime_address_extension_u(const rct::key &recipient_address_spend_key, + const rct::key &sender_receiver_secret, + const rct::key &amount_commitment, + crypto::secret_key &sender_extension_out) +{ + // k_{u, sender} = H_n("..u..", K_1, q, C) + SpKDFTranscript transcript{config::HASH_KEY_JAMTIS_SENDER_ONETIME_ADDRESS_EXTENSION_U, 3*sizeof(rct::key)}; + transcript.append("K_1", recipient_address_spend_key); + transcript.append("q", sender_receiver_secret); + transcript.append("C", amount_commitment); + + sp_hash_to_scalar(transcript.data(), transcript.size(), to_bytes(sender_extension_out)); +} +//------------------------------------------------------------------------------------------------------------------- +void make_jamtis_onetime_address(const rct::key &recipient_address_spend_key, + const rct::key &sender_receiver_secret, + const rct::key &amount_commitment, + rct::key &onetime_address_out) +{ + // Ko = k^o_g G + k^o_x X + k^o_u U + K_1 + crypto::secret_key extension_g; + crypto::secret_key extension_x; + crypto::secret_key extension_u; + make_jamtis_onetime_address_extension_g(recipient_address_spend_key, + sender_receiver_secret, + amount_commitment, + extension_g); //k^o_g + make_jamtis_onetime_address_extension_x(recipient_address_spend_key, + sender_receiver_secret, + amount_commitment, + extension_x); //k^o_x + make_jamtis_onetime_address_extension_u(recipient_address_spend_key, + sender_receiver_secret, + amount_commitment, + extension_u); //k^o_u + + onetime_address_out = recipient_address_spend_key; //K_1 + extend_seraphis_spendkey_u(extension_u, onetime_address_out); //k^o_u U + K_1 + extend_seraphis_spendkey_x(extension_x, onetime_address_out); //k^o_x X + k^o_u U + K_1 + mask_key(extension_g, + onetime_address_out, + onetime_address_out); //k^o_g G + k^o_x X + k^o_u U + K_1 +} +//------------------------------------------------------------------------------------------------------------------- +void make_jamtis_amount_baked_key_plain_sender(const crypto::x25519_secret_key &enote_ephemeral_privkey, + rct::key &baked_key_out) +{ + // xR = xr xG + crypto::x25519_pubkey reverse_sender_receiver_secret; + crypto::x25519_scmul_base(enote_ephemeral_privkey, reverse_sender_receiver_secret); + + // H_32(xR) + make_jamtis_amount_baked_key_plain(reverse_sender_receiver_secret, baked_key_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_jamtis_amount_baked_key_plain_recipient(const crypto::x25519_secret_key &address_privkey, + const crypto::x25519_secret_key &xk_unlock_amounts, + const crypto::x25519_pubkey &enote_ephemeral_pubkey, + rct::key &baked_key_out) +{ + // xR = (1/(xk^j_a * xk_ua)) * xK_e = xr xG + crypto::x25519_pubkey reverse_sender_receiver_secret; + crypto::x25519_invmul_key({address_privkey, xk_unlock_amounts}, + enote_ephemeral_pubkey, + reverse_sender_receiver_secret); + + // H_32(xR) + make_jamtis_amount_baked_key_plain(reverse_sender_receiver_secret, baked_key_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_jamtis_amount_baked_key_selfsend(const crypto::secret_key &k_view_balance, + const rct::key &sender_receiver_secret, + rct::key &baked_key_out) +{ + // [selfsend] baked_key = H_32[k_vb](q) + SpKDFTranscript transcript{config::HASH_KEY_JAMTIS_AMOUNT_BAKED_KEY_SELFSEND, sizeof(rct::key)}; + transcript.append("q", sender_receiver_secret); + + sp_derive_secret(to_bytes(k_view_balance), transcript.data(), transcript.size(), baked_key_out.bytes); +} +//------------------------------------------------------------------------------------------------------------------- +void make_jamtis_amount_blinding_factor(const rct::key &sender_receiver_secret, + const rct::key &baked_key, + crypto::secret_key &mask_out) +{ + // x = H_n(q, baked_key) + SpKDFTranscript transcript{config::HASH_KEY_JAMTIS_AMOUNT_BLINDING_FACTOR, 2*sizeof(rct::key)}; + transcript.append("q", sender_receiver_secret); + transcript.append("baked_key", baked_key); + + sp_hash_to_scalar(transcript.data(), transcript.size(), to_bytes(mask_out)); +} +//------------------------------------------------------------------------------------------------------------------- +encoded_amount_t encode_jamtis_amount(const rct::xmr_amount amount, + const rct::key &sender_receiver_secret, + const rct::key &baked_key) +{ + // a_enc = little_endian(a) XOR H_8(q, baked_key) + return enc_amount(amount, jamtis_encoded_amount_mask(sender_receiver_secret, baked_key)); +} +//------------------------------------------------------------------------------------------------------------------- +rct::xmr_amount decode_jamtis_amount(const encoded_amount_t &encoded_amount, + const rct::key &sender_receiver_secret, + const rct::key &baked_key) +{ + // a = system_endian( a_enc XOR H_8(q, baked_key) ) + return dec_amount(encoded_amount, jamtis_encoded_amount_mask(sender_receiver_secret, baked_key)); +} +//------------------------------------------------------------------------------------------------------------------- +bool test_jamtis_onetime_address(const rct::key &recipient_address_spend_key, + const rct::key &sender_receiver_secret, + const rct::key &amount_commitment, + const rct::key &expected_onetime_address) +{ + // compute a nominal onetime address: K'o + rct::key nominal_onetime_address; + make_jamtis_onetime_address(recipient_address_spend_key, + sender_receiver_secret, + amount_commitment, + nominal_onetime_address); + + // check if the nominal onetime address matches the real onetime address: K'o ?= Ko + return nominal_onetime_address == expected_onetime_address; +} +//------------------------------------------------------------------------------------------------------------------- +bool try_get_jamtis_sender_receiver_secret_plain(const crypto::x25519_pubkey &sender_receiver_DH_derivation, + const crypto::x25519_pubkey &enote_ephemeral_pubkey, + const rct::key &input_context, + const rct::key &onetime_address, + const view_tag_t view_tag, + rct::key &sender_receiver_secret_out) +{ + // recompute view tag and check that it matches; short-circuit on failure + view_tag_t recomputed_view_tag; + make_jamtis_view_tag(sender_receiver_DH_derivation, onetime_address, recomputed_view_tag); + + if (recomputed_view_tag != view_tag) + return false; + + // q (normal derivation path) + make_jamtis_sender_receiver_secret_plain(sender_receiver_DH_derivation, + enote_ephemeral_pubkey, + input_context, + sender_receiver_secret_out); + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool try_get_jamtis_amount(const rct::key &sender_receiver_secret, + const rct::key &baked_key, + const rct::key &amount_commitment, + const encoded_amount_t &encoded_amount, + rct::xmr_amount &amount_out, + crypto::secret_key &amount_blinding_factor_out) +{ + // 1. a' = dec(enc_a) + const rct::xmr_amount nominal_amount{decode_jamtis_amount(encoded_amount, sender_receiver_secret, baked_key)}; + + // 2. C' = x' G + a' H + make_jamtis_amount_blinding_factor(sender_receiver_secret, baked_key, amount_blinding_factor_out); //x' + const rct::key nominal_amount_commitment{rct::commit(nominal_amount, rct::sk2rct(amount_blinding_factor_out))}; + + // 3. check that recomputed commitment matches original commitment + // note: this defends against the Janus attack, and against malformed amount commitments + if (!(nominal_amount_commitment == amount_commitment)) + return false; + + // 4. save the amount + amount_out = nominal_amount; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace jamtis +} //namespace sp diff --git a/src/seraphis_core/jamtis_enote_utils.h b/src/seraphis_core/jamtis_enote_utils.h new file mode 100644 index 0000000000..adcf6612f1 --- /dev/null +++ b/src/seraphis_core/jamtis_enote_utils.h @@ -0,0 +1,306 @@ +// Copyright (c) 2022, 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 making and handling enotes with jamtis. + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "crypto/x25519.h" +#include "jamtis_support_types.h" +#include "ringct/rctTypes.h" + +//third party headers + +//standard headers + +//forward declarations + + +namespace sp +{ +namespace jamtis +{ + +/** +* brief: make_jamtis_enote_ephemeral_pubkey - enote ephemeral pubkey xK_e +* xK_e = xr xK_3 +* param: enote_ephemeral_privkey - xr +* param: DH_base - xK_3 +* outparam: enote_ephemeral_pubkey_out - xK_e +*/ +void make_jamtis_enote_ephemeral_pubkey(const crypto::x25519_secret_key &enote_ephemeral_privkey, + const crypto::x25519_pubkey &DH_base, + crypto::x25519_pubkey &enote_ephemeral_pubkey_out); +/** +* brief: make_jamtis_view_tag - view tag for optimized identification of owned enotes +* view_tag = H_1(xK_d, Ko) +* param: sender_receiver_DH_derivation - xK_d +* param: onetime_address - Ko +* outparam: view_tag_out - view_tag +*/ +void make_jamtis_view_tag(const crypto::x25519_pubkey &sender_receiver_DH_derivation, + const rct::key &onetime_address, + view_tag_t &view_tag_out); +/** +* brief: make_jamtis_view_tag - view tag for optimized identification of owned enotes +* view_tag = H_1(privkey * DH_key, Ko) +* param: privkey - [sender: xr] [recipient: xk_fr] +* param: DH_key - [sender: xK_2] [sender-selfsend-2out: xk_fr * xK_3_other] [recipient: xK_e = xr xK_3] +* param: onetime_address - Ko +* outparam: view_tag_out - view_tag +*/ +void make_jamtis_view_tag(const crypto::x25519_secret_key &privkey, + const crypto::x25519_pubkey &DH_key, + const rct::key &onetime_address, + view_tag_t &view_tag_out); +/** +* brief: make_jamtis_input_context_coinbase - input context for a sender-receiver secret (coinbase txs) +* input_context = H_32(block_height) +* param: block_height - block height of the coinbase tx +* outparam: input_context_out - H_32(block height) +*/ +void make_jamtis_input_context_coinbase(const std::uint64_t block_height, rct::key &input_context_out); +/** +* brief: make_jamtis_input_context_standard - input context for a sender-receiver secret (standard txs) +* input_context = H_32({legacy KI}, {seraphis KI}) +* param: legacy_input_key_images - {KI} from the legacy inputs of a tx (sorted) +* param: sp_input_key_images - {KI} from the seraphis inputs of a tx (sorted) +* outparam: input_context_out - H_32({legacy KI}, {seraphis KI}}) +*/ +void make_jamtis_input_context_standard(const std::vector &legacy_input_key_images, + const std::vector &sp_input_key_images, + rct::key &input_context_out); +/** +* brief: make_jamtis_sender_receiver_secret_plain - sender-receiver secret q for a normal enote +* q = H_32(xK_d, xK_e, input_context) +* param: sender_receiver_DH_derivation - xK_d = xr xK_2 = k_fr xK_e +* param: enote_ephemeral_pubkey - xK_e +* param: input_context - [normal: H_32({legacy KI}, {seraphis KI})] [coinbase: H_32(block height)] +* outparam: sender_receiver_secret_out - q +* - note: this is 'rct::key' instead of 'crypto::secret_key' for better performance in multithreaded environments +*/ +void make_jamtis_sender_receiver_secret_plain(const crypto::x25519_pubkey &sender_receiver_DH_derivation, + const crypto::x25519_pubkey &enote_ephemeral_pubkey, + const rct::key &input_context, + rct::key &sender_receiver_secret_out); +/** +* brief: make_jamtis_sender_receiver_secret_plain - sender-receiver secret q for a normal enote +* q = H_32(xr * xk_fr * xG, input_context) => H_32(privkey * DH_key, input_context) +* param: privkey - [sender: xr] [recipient: xk_fr] +* param: DH_key - [sender: xK_2] [recipient: xK_e = xr xK_3] +* param: enote_ephemeral_pubkey - xK_e +* param: input_context - [normal: H_32({legacy KI}, {seraphis KI})] [coinbase: H_32(block height)] +* outparam: sender_receiver_secret_out - q +* - note: this is 'rct::key' instead of 'crypto::secret_key' for better performance in multithreaded environments +*/ +void make_jamtis_sender_receiver_secret_plain(const crypto::x25519_secret_key &privkey, + const crypto::x25519_pubkey &DH_key, + const crypto::x25519_pubkey &enote_ephemeral_pubkey, + const rct::key &input_context, + rct::key &sender_receiver_secret_out); +/** +* brief: make_jamtis_sender_receiver_secret_selfsend - sender-receiver secret q for a self-send enote of a specific type +* q = H_32[k_vb](xK_e, input_context) +* param: k_view_balance - k_vb +* param: enote_ephemeral_pubkey - xK_e +* param: input_context - [normal: H_32({legacy KI}, {seraphis KI})] [coinbase: H_32(block height)] +* param: self_send_type - type of the self-send enote, used to select the domain separator +* outparam: sender_receiver_secret_out - q +* - note: this is 'rct::key' instead of 'crypto::secret_key' for better performance in multithreaded environments +*/ +void make_jamtis_sender_receiver_secret_selfsend(const crypto::secret_key &k_view_balance, + const crypto::x25519_pubkey &enote_ephemeral_pubkey, + const rct::key &input_context, + const JamtisSelfSendType self_send_type, + rct::key &sender_receiver_secret_out); +/** +* brief: make_jamtis_onetime_address_extension_g - extension for transforming a recipient spendkey into an +* enote one-time address +* k_{g, sender} = k^o_g = H_n("..g..", K_1, q, C) +* param: recipient_address_spend_key - K_1 +* param: sender_receiver_secret - q +* param: amount_commitment - C +* outparam: sender_extension_out - k_{g, sender} +*/ +void make_jamtis_onetime_address_extension_g(const rct::key &recipient_address_spend_key, + const rct::key &sender_receiver_secret, + const rct::key &amount_commitment, + crypto::secret_key &sender_extension_out); +/** +* brief: make_jamtis_onetime_address_extension_x - extension for transforming a recipient spendkey into an +* enote one-time address +* k_{x, sender} = k^o_x = H_n("..x..", K_1, q, C) +* param: recipient_address_spend_key - K_1 +* param: sender_receiver_secret - q +* param: amount_commitment - C +* outparam: sender_extension_out - k_{x, sender} +*/ +void make_jamtis_onetime_address_extension_x(const rct::key &recipient_address_spend_key, + const rct::key &sender_receiver_secret, + const rct::key &amount_commitment, + crypto::secret_key &sender_extension_out); +/** +* brief: make_jamtis_onetime_address_extension_u - extension for transforming a recipient spendkey into an +* enote one-time address +* k_{u, sender} = k^o_u = H_n("..u..", K_1, q, C) +* param: recipient_address_spend_key - K_1 +* param: sender_receiver_secret - q +* param: amount_commitment - C +* outparam: sender_extension_out - k_{u, sender} +*/ +void make_jamtis_onetime_address_extension_u(const rct::key &recipient_address_spend_key, + const rct::key &sender_receiver_secret, + const rct::key &amount_commitment, + crypto::secret_key &sender_extension_out); +/** +* brief: make_jamtis_onetime_address - create a onetime address +* Ko = k^o_g G + k^o_x X + k^o_u U + K_1 +* param: recipient_address_spend_key - K_1 +* param: sender_receiver_secret - q +* param: amount_commitment - C +* outparam: onetime_address_out - Ko +*/ +void make_jamtis_onetime_address(const rct::key &recipient_address_spend_key, + const rct::key &sender_receiver_secret, + const rct::key &amount_commitment, + rct::key &onetime_address_out); +/** +* brief: make_jamtis_amount_baked_key_plain_sender - key baked into amount encodings of plain enotes, to provide +* fine-tuned control over read rights to the amount +* [normal: sender] baked_key = H_32(xr xG) +* param: enote_ephemeral_privkey - xr +* outparam: baked_key_out - baked_key +*/ +void make_jamtis_amount_baked_key_plain_sender(const crypto::x25519_secret_key &enote_ephemeral_privkey, + rct::key &baked_key_out); +/** +* brief: make_jamtis_amount_baked_key_plain_recipient - key baked into amount encodings of plain enotes, to provide +* fine-tuned control over read rights to the amount +* [normal: recipient] baked_key = H_32( (1/(xk^j_a * xk_ua)) * xK_e ) +* param: address_privkey - xk^j_a +* param: xk_unlock_amounts - xk_ua +* param: enote_ephemeral_pubkey - xK_e +* outparam: baked_key_out - baked_key +*/ +void make_jamtis_amount_baked_key_plain_recipient(const crypto::x25519_secret_key &address_privkey, + const crypto::x25519_secret_key &xk_unlock_amounts, + const crypto::x25519_pubkey &enote_ephemeral_pubkey, + rct::key &baked_key_out); +/** +* brief: make_jamtis_amount_baked_key_selfsend - key baked into amount encodings of selfsend enotes, to provide +* fine-tuned control over read rights to the amount +* [selfsend] baked_key = H_32[k_vb](q) +* param: k_view_balance - k_vb +* param: sender_receiver_secret - q +* outparam: baked_key_out - baked_key +*/ +void make_jamtis_amount_baked_key_selfsend(const crypto::secret_key &k_view_balance, + const rct::key &sender_receiver_secret, + rct::key &baked_key_out); +/** +* brief: make_jamtis_amount_blinding_factor - x for a normal enote's amount commitment C = x G + a H +* x = H_n(q, baked_key) +* param: sender_receiver_secret - q +* param: baked_key - baked_key +* outparam: mask_out - x +*/ +void make_jamtis_amount_blinding_factor(const rct::key &sender_receiver_secret, + const rct::key &baked_key, + crypto::secret_key &mask_out); +/** +* brief: encode_jamtis_amount - encode an amount for a normal enote +* a_enc = a XOR H_8(q, baked_key) +* param: amount - a +* param: sender_receiver_secret - q +* param: baked_key - baked_key +* return: a_enc +*/ +encoded_amount_t encode_jamtis_amount(const rct::xmr_amount amount, + const rct::key &sender_receiver_secret, + const rct::key &baked_key); +/** +* brief: decode_jamtis_amount - decode an amount from a normal enote +* a = a_enc XOR H_8(q, baked_key) +* param: encoded_amount - a_enc +* param: sender_receiver_secret - q +* param: baked_key - baked_key +* return: a +*/ +rct::xmr_amount decode_jamtis_amount(const encoded_amount_t &encoded_amount, + const rct::key &sender_receiver_secret, + const rct::key &baked_key); +/** +* brief: test_jamtis_onetime_address - see if a onetime address can be reconstructed +* param: recipient_address_spend_key - recipient's address spendkey K_1 +* param: sender_receiver_secret - q +* param: amount_commitment - amount commtiment C +* param: expected_onetime_address - onetime address to test Ko +* return: true if the expected onetime address can be reconstructed +*/ +bool test_jamtis_onetime_address(const rct::key &recipient_address_spend_key, + const rct::key &sender_receiver_secret, + const rct::key &amount_commitment, + const rct::key &expected_onetime_address); +/** +* brief: try_get_jamtis_sender_receiver_secret_plain - test view tag; if it passes, get the nominal sender-receiver secret +* (for a normal enote) +* param: sender_receiver_DH_derivation - privkey * DH_key +* param: enote_ephemeral_pubkey - xK_e +* param: input_context - input_context +* param: onetime_address - Ko +* param: view_tag - view_tag +* outparam: sender_receiver_secret_out - q +* return: true if successfully recomputed the view tag +*/ +bool try_get_jamtis_sender_receiver_secret_plain(const crypto::x25519_pubkey &sender_receiver_DH_derivation, + const crypto::x25519_pubkey &enote_ephemeral_pubkey, + const rct::key &input_context, + const rct::key &onetime_address, + const view_tag_t view_tag, + rct::key &sender_receiver_secret_out); +/** +* brief: try_get_jamtis_amount - test recreating the amount commitment; if it is recreate-able, return the amount +* param: sender_receiver_secret - q +* param: baked_key - baked_key +* param: amount_commitment - C = x G + a H +* param: encoded_amount - enc_a +* outparam: amount_out - a' = dec(enc_a) +* outparam: amount_blinding_factor_out - x' +* return: true if successfully recomputed the amount commitment (C' = x' G + a' H ?= C) +*/ +bool try_get_jamtis_amount(const rct::key &sender_receiver_secret, + const rct::key &baked_key, + const rct::key &amount_commitment, + const encoded_amount_t &encoded_amount, + rct::xmr_amount &amount_out, + crypto::secret_key &amount_blinding_factor_out); + +} //namespace jamtis +} //namespace sp diff --git a/src/seraphis_core/jamtis_payment_proposal.cpp b/src/seraphis_core/jamtis_payment_proposal.cpp new file mode 100644 index 0000000000..74b7d862f8 --- /dev/null +++ b/src/seraphis_core/jamtis_payment_proposal.cpp @@ -0,0 +1,327 @@ +// Copyright (c) 2022, 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 "jamtis_payment_proposal.h" + +//local headers +#include "crypto/crypto.h" +#include "crypto/x25519.h" +#include "jamtis_address_tag_utils.h" +#include "jamtis_address_utils.h" +#include "jamtis_core_utils.h" +#include "jamtis_enote_utils.h" +#include "jamtis_support_types.h" +#include "memwipe.h" +#include "misc_language.h" +#include "misc_log_ex.h" +#include "ringct/rctOps.h" +#include "ringct/rctTypes.h" +#include "sp_core_types.h" +#include "tx_extra.h" + +//third party headers + +//standard headers + + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis" + +namespace sp +{ +namespace jamtis +{ +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +template +static auto auto_wiper(T &obj) +{ + return epee::misc_utils::create_scope_leave_handler([&]{ memwipe(&obj, sizeof(T)); }); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void get_output_proposal_amount_parts_v1(const rct::key &q, + const rct::key &amount_baked_key, + const rct::xmr_amount output_amount, + crypto::secret_key &amount_blinding_factor_out, + encoded_amount_t &encoded_amount_out) +{ + // 1. amount blinding factor: y = H_n(q, baked_key) + make_jamtis_amount_blinding_factor(q, amount_baked_key, amount_blinding_factor_out); + + // 2. encrypted amount: enc_amount = a ^ H_8(q, baked_key) + encoded_amount_out = encode_jamtis_amount(output_amount, q, amount_baked_key); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void get_output_proposal_address_parts_v1(const rct::key &q, + const crypto::x25519_pubkey &xK_d, + const JamtisDestinationV1 &output_destination, + const rct::key &amount_commitment, + rct::key &onetime_address_out, + encrypted_address_tag_t &addr_tag_enc_out, + view_tag_t &view_tag_out) +{ + // 1. onetime address: Ko = k^o_g G + k^o_x X + k^o_u U + K_1 + make_jamtis_onetime_address(output_destination.addr_K1, q, amount_commitment, onetime_address_out); + + // 2. encrypt address tag: addr_tag_enc = addr_tag ^ H(q, Ko) + addr_tag_enc_out = encrypt_address_tag(q, onetime_address_out, output_destination.addr_tag); + + // 3. view tag: view_tag = H_1(xK_d, Ko) + make_jamtis_view_tag(xK_d, onetime_address_out, view_tag_out); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +void get_enote_ephemeral_pubkey(const JamtisPaymentProposalV1 &proposal, + crypto::x25519_pubkey &enote_ephemeral_pubkey_out) +{ + // sanity checks + CHECK_AND_ASSERT_THROW_MES(sc_isnonzero(proposal.enote_ephemeral_privkey.data), + "jamtis payment proposal: invalid enote ephemeral privkey (zero)."); + CHECK_AND_ASSERT_THROW_MES(x25519_scalar_is_canonical(proposal.enote_ephemeral_privkey), + "jamtis payment proposal: invalid enote ephemeral privkey (not canonical)."); + + // enote ephemeral pubkey: xK_e = xr xK_3 + make_jamtis_enote_ephemeral_pubkey(proposal.enote_ephemeral_privkey, + proposal.destination.addr_K3, + enote_ephemeral_pubkey_out); +} +//------------------------------------------------------------------------------------------------------------------- +void get_enote_ephemeral_pubkey(const JamtisPaymentProposalSelfSendV1 &proposal, + crypto::x25519_pubkey &enote_ephemeral_pubkey_out) +{ + // sanity checks + CHECK_AND_ASSERT_THROW_MES(sc_isnonzero(proposal.enote_ephemeral_privkey.data), + "jamtis payment proposal self-send: invalid enote ephemeral privkey (zero)."); + CHECK_AND_ASSERT_THROW_MES(x25519_scalar_is_canonical(proposal.enote_ephemeral_privkey), + "jamtis payment proposal self-send: invalid enote ephemeral privkey (not canonical)."); + + // enote ephemeral pubkey: xK_e = xr xK_3 + make_jamtis_enote_ephemeral_pubkey(proposal.enote_ephemeral_privkey, + proposal.destination.addr_K3, + enote_ephemeral_pubkey_out); +} +//------------------------------------------------------------------------------------------------------------------- +void get_coinbase_output_proposal_v1(const JamtisPaymentProposalV1 &proposal, + const std::uint64_t block_height, + SpCoinbaseEnoteCore &output_enote_core_out, + crypto::x25519_pubkey &enote_ephemeral_pubkey_out, + encrypted_address_tag_t &addr_tag_enc_out, + view_tag_t &view_tag_out, + TxExtra &partial_memo_out) +{ + // 1. sanity checks + CHECK_AND_ASSERT_THROW_MES(sc_isnonzero(proposal.enote_ephemeral_privkey.data), + "jamtis payment proposal: invalid enote ephemeral privkey (zero)."); + CHECK_AND_ASSERT_THROW_MES(x25519_scalar_is_canonical(proposal.enote_ephemeral_privkey), + "jamtis payment proposal: invalid enote ephemeral privkey (not canonical)."); + + // 2. coinbase input context + rct::key input_context; + make_jamtis_input_context_coinbase(block_height, input_context); + + // 3. enote ephemeral pubkey: xK_e = xr xK_3 + get_enote_ephemeral_pubkey(proposal, enote_ephemeral_pubkey_out); + + // 4. derived key: xK_d = xr * xK_2 + crypto::x25519_pubkey xK_d; auto xKd_wiper = auto_wiper(xK_d); + crypto::x25519_scmul_key(proposal.enote_ephemeral_privkey, proposal.destination.addr_K2, xK_d); + + // 5. sender-receiver shared secret (plain): q = H_32(xK_d, xK_e, input_context) + rct::key q; auto q_wiper = auto_wiper(q); + make_jamtis_sender_receiver_secret_plain(xK_d, enote_ephemeral_pubkey_out, input_context, q); + + // 6. build the output enote address pieces + get_output_proposal_address_parts_v1(q, + xK_d, + proposal.destination, + rct::commit(proposal.amount, rct::I), + output_enote_core_out.onetime_address, + addr_tag_enc_out, + view_tag_out); + + // 7. save the amount and parial memo + output_enote_core_out.amount = proposal.amount; + partial_memo_out = proposal.partial_memo; +} +//------------------------------------------------------------------------------------------------------------------- +void get_output_proposal_v1(const JamtisPaymentProposalV1 &proposal, + const rct::key &input_context, + SpOutputProposalCore &output_proposal_core_out, + crypto::x25519_pubkey &enote_ephemeral_pubkey_out, + encoded_amount_t &encoded_amount_out, + encrypted_address_tag_t &addr_tag_enc_out, + view_tag_t &view_tag_out, + TxExtra &partial_memo_out) +{ + // 1. sanity checks + CHECK_AND_ASSERT_THROW_MES(sc_isnonzero(proposal.enote_ephemeral_privkey.data), + "jamtis payment proposal: invalid enote ephemeral privkey (zero)."); + CHECK_AND_ASSERT_THROW_MES(x25519_scalar_is_canonical(proposal.enote_ephemeral_privkey), + "jamtis payment proposal: invalid enote ephemeral privkey (not canonical)."); + + // 2. enote ephemeral pubkey: xK_e = xr xK_3 + get_enote_ephemeral_pubkey(proposal, enote_ephemeral_pubkey_out); + + // 3. derived key: xK_d = xr * xK_2 + crypto::x25519_pubkey xK_d; auto xKd_wiper = auto_wiper(xK_d); + crypto::x25519_scmul_key(proposal.enote_ephemeral_privkey, proposal.destination.addr_K2, xK_d); + + // 4. sender-receiver shared secret (plain): q = H_32(xK_d, xK_e, input_context) + rct::key q; auto q_wiper = auto_wiper(q); + make_jamtis_sender_receiver_secret_plain(xK_d, enote_ephemeral_pubkey_out, input_context, q); + + // 5. amount baked key (plain): H_32(xr xG) + rct::key amount_baked_key; auto bk_wiper = auto_wiper(amount_baked_key); + make_jamtis_amount_baked_key_plain_sender(proposal.enote_ephemeral_privkey, amount_baked_key); + + // 6. build the output enote amount pieces + get_output_proposal_amount_parts_v1(q, + amount_baked_key, + proposal.amount, + output_proposal_core_out.amount_blinding_factor, + encoded_amount_out); + + // 7. build the output enote address pieces + get_output_proposal_address_parts_v1(q, + xK_d, + proposal.destination, + rct::commit(proposal.amount, rct::sk2rct(output_proposal_core_out.amount_blinding_factor)), + output_proposal_core_out.onetime_address, + addr_tag_enc_out, + view_tag_out); + + // 8. save the amount and partial memo + output_proposal_core_out.amount = proposal.amount; + partial_memo_out = proposal.partial_memo; +} +//------------------------------------------------------------------------------------------------------------------- +void get_output_proposal_v1(const JamtisPaymentProposalSelfSendV1 &proposal, + const crypto::secret_key &k_view_balance, + const rct::key &input_context, + SpOutputProposalCore &output_proposal_core_out, + crypto::x25519_pubkey &enote_ephemeral_pubkey_out, + encoded_amount_t &encoded_amount_out, + encrypted_address_tag_t &addr_tag_enc_out, + view_tag_t &view_tag_out, + TxExtra &partial_memo_out) +{ + // 1. sanity checks + CHECK_AND_ASSERT_THROW_MES(sc_isnonzero(proposal.enote_ephemeral_privkey.data), + "jamtis payment proposal self-send: invalid enote ephemeral privkey (zero)."); + CHECK_AND_ASSERT_THROW_MES(x25519_scalar_is_canonical(proposal.enote_ephemeral_privkey), + "jamtis payment proposal self-send: invalid enote ephemeral privkey (not canonical)."); + CHECK_AND_ASSERT_THROW_MES(sc_isnonzero(to_bytes(k_view_balance)), + "jamtis payment proposal self-send: invalid view-balance privkey (zero)."); + CHECK_AND_ASSERT_THROW_MES(sc_check(to_bytes(k_view_balance)) == 0, + "jamtis payment proposal self-send: invalid view-balance privkey (not canonical)."); + CHECK_AND_ASSERT_THROW_MES(proposal.type <= JamtisSelfSendType::MAX, + "jamtis payment proposal self-send: unknown self-send type."); + + // 2. enote ephemeral pubkey: xK_e = xr xK_3 + get_enote_ephemeral_pubkey(proposal, enote_ephemeral_pubkey_out); + + // 3. derived key: xK_d = xr * xK_2 + crypto::x25519_pubkey xK_d; auto xKd_wiper = auto_wiper(xK_d); + crypto::x25519_scmul_key(proposal.enote_ephemeral_privkey, proposal.destination.addr_K2, xK_d); + + // 4. sender-receiver shared secret (selfsend): q = H_32[k_vb](xK_e, input_context) //note: xK_e not xK_d + rct::key q; auto q_wiper = auto_wiper(q); + make_jamtis_sender_receiver_secret_selfsend(k_view_balance, + enote_ephemeral_pubkey_out, + input_context, + proposal.type, + q); + + // 5. amount baked key (selfsend): H_32[k_vb](q) + rct::key amount_baked_key; auto bk_wiper = auto_wiper(amount_baked_key); + make_jamtis_amount_baked_key_selfsend(k_view_balance, q, amount_baked_key); + + // 6. build the output enote amount pieces + get_output_proposal_amount_parts_v1(q, + amount_baked_key, + proposal.amount, + output_proposal_core_out.amount_blinding_factor, + encoded_amount_out); + + // 7. build the output enote address pieces + get_output_proposal_address_parts_v1(q, + xK_d, + proposal.destination, + rct::commit(proposal.amount, rct::sk2rct(output_proposal_core_out.amount_blinding_factor)), + output_proposal_core_out.onetime_address, + addr_tag_enc_out, + view_tag_out); + + // 8. save the amount and partial memo + output_proposal_core_out.amount = proposal.amount; + partial_memo_out = proposal.partial_memo; +} +//------------------------------------------------------------------------------------------------------------------- +JamtisPaymentProposalV1 gen_jamtis_payment_proposal_v1(const rct::xmr_amount amount, + const std::size_t num_random_memo_elements) +{ + JamtisPaymentProposalV1 temp; + + temp.destination = gen_jamtis_destination_v1(); + temp.amount = amount; + temp.enote_ephemeral_privkey = crypto::x25519_secret_key_gen(); + + std::vector memo_elements; + memo_elements.resize(num_random_memo_elements); + for (ExtraFieldElement &element: memo_elements) + element = gen_extra_field_element(); + make_tx_extra(std::move(memo_elements), temp.partial_memo); + + return temp; +} +//------------------------------------------------------------------------------------------------------------------- +JamtisPaymentProposalSelfSendV1 gen_jamtis_selfsend_payment_proposal_v1(const rct::xmr_amount amount, + const JamtisSelfSendType type, + const std::size_t num_random_memo_elements) +{ + JamtisPaymentProposalSelfSendV1 temp; + + temp.destination = gen_jamtis_destination_v1(); + temp.amount = amount; + temp.type = type; + temp.enote_ephemeral_privkey = crypto::x25519_secret_key_gen(); + + std::vector memo_elements; + memo_elements.resize(num_random_memo_elements); + for (ExtraFieldElement &element: memo_elements) + element = gen_extra_field_element(); + make_tx_extra(std::move(memo_elements), temp.partial_memo); + + return temp; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace jamtis +} //namespace sp diff --git a/src/seraphis_core/jamtis_payment_proposal.h b/src/seraphis_core/jamtis_payment_proposal.h new file mode 100644 index 0000000000..6d737a1224 --- /dev/null +++ b/src/seraphis_core/jamtis_payment_proposal.h @@ -0,0 +1,184 @@ +// Copyright (c) 2022, 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 Jamtis address. +// NOTE: Coinbase output proposals cannot be made from selfsend payment proposals because selfsend balance recovery +// depends on looking in txs with known key images, but coinbase txs don't have key images. + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "crypto/x25519.h" +#include "jamtis_destination.h" +#include "jamtis_support_types.h" +#include "ringct/rctTypes.h" +#include "sp_core_types.h" +#include "tx_extra.h" + +//third party headers + +//standard headers + +//forward declarations + + +namespace sp +{ +namespace jamtis +{ + +//// +// JamtisPaymentProposalV1 +// - for creating an output proposal to send an amount to someone +/// +struct JamtisPaymentProposalV1 final +{ + /// user address + JamtisDestinationV1 destination; + /// b + rct::xmr_amount amount; + + /// enote ephemeral privkey: xr + crypto::x25519_secret_key enote_ephemeral_privkey; + + /// memo elements to add to the tx memo + TxExtra partial_memo; +}; + +//// +// JamtisPaymentProposalSelfSendV1 +// - for creating an output proposal to send an amount to the tx author +/// +struct JamtisPaymentProposalSelfSendV1 final +{ + /// user address + JamtisDestinationV1 destination; + /// b + rct::xmr_amount amount; + + /// self-send type + JamtisSelfSendType type; + /// enote ephemeral privkey: xr + crypto::x25519_secret_key enote_ephemeral_privkey; + + /// memo elements to add to the tx memo + TxExtra partial_memo; +}; + +/** +* brief: get_enote_ephemeral_pubkey - get the proposal's enote ephemeral pubkey xK_e +* param: proposal - +* outparam: enote_ephemeral_pubkey_out - +*/ +void get_enote_ephemeral_pubkey(const JamtisPaymentProposalV1 &proposal, + crypto::x25519_pubkey &enote_ephemeral_pubkey_out); +/** +* brief: get_enote_ephemeral_pubkey - get the proposal's enote ephemeral pubkey xK_e +* outparam: enote_ephemeral_pubkey_out - +*/ +void get_enote_ephemeral_pubkey(const JamtisPaymentProposalSelfSendV1 &proposal, + crypto::x25519_pubkey &enote_ephemeral_pubkey_out); +/** +* brief: get_coinbase_output_proposal_v1 - convert the jamtis proposal to a coinbase output proposal +* param: proposal - +* param: block_height - height of the coinbase tx's block +* outparam: output_enote_core_out - +* outparam: enote_ephemeral_pubkey_out - +* outparam: addr_tag_enc_out - +* outparam: view_tag_out - +* outparam: partial_memo_out - +*/ +void get_coinbase_output_proposal_v1(const JamtisPaymentProposalV1 &proposal, + const std::uint64_t block_height, + SpCoinbaseEnoteCore &output_enote_core_out, + crypto::x25519_pubkey &enote_ephemeral_pubkey_out, + encrypted_address_tag_t &addr_tag_enc_out, + view_tag_t &view_tag_out, + TxExtra &partial_memo_out); +/** +* brief: get_output_proposal_v1 - convert the jamtis proposal to an output proposal +* param: proposal - +* param: input_context - +* outparam: output_proposal_core_out - +* outparam: enote_ephemeral_pubkey_out - +* outparam: encoded_amount_out - +* outparam: addr_tag_enc_out - +* outparam: view_tag_out - +* outparam: partial_memo_out - +*/ +void get_output_proposal_v1(const JamtisPaymentProposalV1 &proposal, + const rct::key &input_context, + SpOutputProposalCore &output_proposal_core_out, + crypto::x25519_pubkey &enote_ephemeral_pubkey_out, + encoded_amount_t &encoded_amount_out, + encrypted_address_tag_t &addr_tag_enc_out, + view_tag_t &view_tag_out, + TxExtra &partial_memo_out); +/** +* brief: get_output_proposal_v1 - convert the jamtis selfsend proposal to an output proposal +* param: proposal - +* param: k_view_balance - +* param: input_context - +* outparam: output_proposal_core_out - +* outparam: enote_ephemeral_pubkey_out - +* outparam: encoded_amount_out - +* outparam: addr_tag_enc_out - +* outparam: view_tag_out - +* outparam: partial_memo_out - +*/ +void get_output_proposal_v1(const JamtisPaymentProposalSelfSendV1 &proposal, + const crypto::secret_key &k_view_balance, + const rct::key &input_context, + SpOutputProposalCore &output_proposal_core_out, + crypto::x25519_pubkey &enote_ephemeral_pubkey_out, + encoded_amount_t &encoded_amount_out, + encrypted_address_tag_t &addr_tag_enc_out, + view_tag_t &view_tag_out, + TxExtra &partial_memo_out); +/** +* brief: gen_jamtis_payment_proposal_v1 - generate a random proposal +* param: amount - +* param: num_random_memo_elements - +* return: a random proposal +*/ +JamtisPaymentProposalV1 gen_jamtis_payment_proposal_v1(const rct::xmr_amount amount, + const std::size_t num_random_memo_elements); +/** +* brief: gen_jamtis_selfsend_payment_proposal_v1 - generate a random selfsend proposal (with specified parameters) +* param: amount - +* param: type - +* param: num_random_memo_elements +* return: a random proposal +*/ +JamtisPaymentProposalSelfSendV1 gen_jamtis_selfsend_payment_proposal_v1(const rct::xmr_amount amount, + const JamtisSelfSendType type, + const std::size_t num_random_memo_elements); + +} //namespace jamtis +} //namespace sp diff --git a/src/seraphis_core/jamtis_support_types.cpp b/src/seraphis_core/jamtis_support_types.cpp new file mode 100644 index 0000000000..d134854631 --- /dev/null +++ b/src/seraphis_core/jamtis_support_types.cpp @@ -0,0 +1,174 @@ +// Copyright (c) 2022, 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 "jamtis_support_types.h" + +//local headers +#include "crypto/crypto.h" +#include "int-util.h" +#include "misc_log_ex.h" + +//third party headers + +//standard headers +#include +#include + +namespace sp +{ +namespace jamtis +{ +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +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; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +address_index_t::address_index_t() +{ + std::memset(this->bytes, 0, ADDRESS_INDEX_BYTES); +} +//------------------------------------------------------------------------------------------------------------------- +address_tag_hint_t::address_tag_hint_t() +{ + std::memset(this->bytes, 0, ADDRESS_TAG_HINT_BYTES); +} +//------------------------------------------------------------------------------------------------------------------- +bool operator==(const address_index_t &a, const address_index_t &b) +{ + return memcmp(a.bytes, b.bytes, sizeof(address_index_t)) == 0; +} +//------------------------------------------------------------------------------------------------------------------- +bool operator==(const address_tag_hint_t &a, const address_tag_hint_t &b) +{ + return memcmp(a.bytes, b.bytes, sizeof(address_tag_hint_t)) == 0; +} +//------------------------------------------------------------------------------------------------------------------- +bool operator==(const address_tag_t &a, const address_tag_t &b) +{ + return memcmp(a.bytes, b.bytes, sizeof(address_tag_t)) == 0; +} +//------------------------------------------------------------------------------------------------------------------- +address_tag_t operator^(const address_tag_t &a, const address_tag_t &b) +{ + return xor_bytes(a, b); +} +//------------------------------------------------------------------------------------------------------------------- +bool operator==(const encoded_amount_t &a, const encoded_amount_t &b) +{ + return memcmp(a.bytes, b.bytes, sizeof(encoded_amount_t)) == 0; +} +//------------------------------------------------------------------------------------------------------------------- +encoded_amount_t operator^(const encoded_amount_t &a, const encoded_amount_t &b) +{ + return xor_bytes(a, b); +} +//------------------------------------------------------------------------------------------------------------------- +address_index_t max_address_index() +{ + address_index_t temp; + std::memset(temp.bytes, static_cast(-1), ADDRESS_INDEX_BYTES); + return temp; +} +//------------------------------------------------------------------------------------------------------------------- +address_index_t make_address_index(std::uint64_t half1, std::uint64_t half2) +{ + static_assert(sizeof(half1) + sizeof(half2) == sizeof(address_index_t), ""); + + // copy each half of the index over (as little endian bytes) + half1 = SWAP64LE(half1); + half2 = SWAP64LE(half2); + + address_index_t temp; + std::memset(temp.bytes, 0, ADDRESS_INDEX_BYTES); + memcpy(temp.bytes, &half1, sizeof(half1)); + memcpy(temp.bytes + sizeof(half1), &half2, sizeof(half2)); + + return temp; +} +//------------------------------------------------------------------------------------------------------------------- +address_tag_t make_address_tag(const address_index_t &enc_j, const address_tag_hint_t &addr_tag_hint) +{ + // addr_tag = enc(j) || hint + address_tag_t temp; + memcpy(temp.bytes, &enc_j, ADDRESS_INDEX_BYTES); + memcpy(temp.bytes + ADDRESS_INDEX_BYTES, &addr_tag_hint, ADDRESS_TAG_HINT_BYTES); + return temp; +} +//------------------------------------------------------------------------------------------------------------------- +address_index_t gen_address_index() +{ + address_index_t temp; + crypto::rand(ADDRESS_INDEX_BYTES, temp.bytes); + return temp; +} +//------------------------------------------------------------------------------------------------------------------- +bool try_get_jamtis_enote_type(const JamtisSelfSendType self_send_type, JamtisEnoteType &enote_type_out) +{ + switch (self_send_type) + { + case (JamtisSelfSendType::DUMMY) : enote_type_out = JamtisEnoteType::DUMMY; return true; + case (JamtisSelfSendType::CHANGE) : enote_type_out = JamtisEnoteType::CHANGE; return true; + case (JamtisSelfSendType::SELF_SPEND) : enote_type_out = JamtisEnoteType::SELF_SPEND; return true; + default : return false; + }; +} +//------------------------------------------------------------------------------------------------------------------- +bool try_get_jamtis_self_send_type(const JamtisEnoteType enote_type, JamtisSelfSendType &self_send_type_out) +{ + switch (enote_type) + { + case (JamtisEnoteType::DUMMY) : self_send_type_out = JamtisSelfSendType::DUMMY; return true; + case (JamtisEnoteType::CHANGE) : self_send_type_out = JamtisSelfSendType::CHANGE; return true; + case (JamtisEnoteType::SELF_SPEND) : self_send_type_out = JamtisSelfSendType::SELF_SPEND; return true; + default : return false; + }; +} +//------------------------------------------------------------------------------------------------------------------- +bool is_jamtis_selfsend_type(const JamtisEnoteType enote_type) +{ + JamtisSelfSendType dummy; + return try_get_jamtis_self_send_type(enote_type, dummy); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace jamtis +} //namespace sp diff --git a/src/seraphis_core/jamtis_support_types.h b/src/seraphis_core/jamtis_support_types.h new file mode 100644 index 0000000000..05337398e0 --- /dev/null +++ b/src/seraphis_core/jamtis_support_types.h @@ -0,0 +1,172 @@ +// Copyright (c) 2022, 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. + +// Supporting types for Jamtis (address index, address tag hint, address tag, etc.). + +#pragma once + +//local headers + +//third party headers + +//standard headers +#include +#include +#include + +//forward declarations + + +namespace sp +{ +namespace jamtis +{ + +/// index (little-endian): j +constexpr std::size_t ADDRESS_INDEX_BYTES{16}; +struct address_index_t final +{ + unsigned char bytes[ADDRESS_INDEX_BYTES]; + + /// default constructor: default initialize to 0 + address_index_t(); +}; + +/// hint for address tags: addr_tag_hint +constexpr std::size_t ADDRESS_TAG_HINT_BYTES{2}; +struct address_tag_hint_t final +{ + unsigned char bytes[ADDRESS_TAG_HINT_BYTES]; + + /// default constructor: default initialize to 0 + address_tag_hint_t(); +}; + +/// index ciphered with a cipher key: addr_tag = enc[cipher_key](j) || addr_tag_hint +struct address_tag_t final +{ + unsigned char bytes[ADDRESS_INDEX_BYTES + ADDRESS_TAG_HINT_BYTES]; +}; + +/// address tag XORd with a user-defined secret: addr_tag_enc = addr_tag XOR addr_tag_enc_secret +using encrypted_address_tag_t = address_tag_t; + +/// sizes must be consistent +static_assert( + sizeof(address_index_t) == ADDRESS_INDEX_BYTES && + sizeof(address_tag_hint_t) == ADDRESS_TAG_HINT_BYTES && + sizeof(address_tag_t) == ADDRESS_INDEX_BYTES + ADDRESS_TAG_HINT_BYTES && + sizeof(address_tag_t) == sizeof(encrypted_address_tag_t), + "" +); + +/// jamtis enote types +enum class JamtisEnoteType : unsigned char +{ + PLAIN = 0, + DUMMY = 1, + CHANGE = 2, + SELF_SPEND = 3 +}; + +/// jamtis self-send types, used to define enote-construction procedure for self-sends +enum class JamtisSelfSendType : unsigned char +{ + DUMMY = 0, + CHANGE = 1, + SELF_SPEND = 2, + MAX = SELF_SPEND +}; + +/// jamtis encoded amount +constexpr std::size_t ENCODED_AMOUNT_BYTES{8}; +struct encoded_amount_t final +{ + unsigned char bytes[ENCODED_AMOUNT_BYTES]; +}; + +/// jamtis view tags +using view_tag_t = unsigned char; + +/// overloaded operators: address index +bool operator==(const address_index_t &a, const address_index_t &b); +inline bool operator!=(const address_index_t &a, const address_index_t &b) { return !(a == b); } +/// overloaded operators: address tag hint +bool operator==(const address_tag_hint_t &a, const address_tag_hint_t &b); +inline bool operator!=(const address_tag_hint_t &a, const address_tag_hint_t &b) { return !(a == b); } +/// overloaded operators: address tag +bool operator==(const address_tag_t &a, const address_tag_t &b); +inline bool operator!=(const address_tag_t &a, const address_tag_t &b) { return !(a == b); } +address_tag_t operator^(const address_tag_t &a, const address_tag_t &b); + +/// overloaded operators: encoded amount +bool operator==(const encoded_amount_t &a, const encoded_amount_t &b); +inline bool operator!=(const encoded_amount_t &a, const encoded_amount_t &b) { return !(a == b); } +encoded_amount_t operator^(const encoded_amount_t &a, const encoded_amount_t &b); + +/// max address index +address_index_t max_address_index(); +/// make an address index +address_index_t make_address_index(std::uint64_t half1, std::uint64_t half2); +inline address_index_t make_address_index(std::uint64_t half1) { return make_address_index(half1, 0); } +/// make an address tag +address_tag_t make_address_tag(const address_index_t &enc_j, const address_tag_hint_t &addr_tag_hint); +/// generate a random address index +address_index_t gen_address_index(); + +/// convert between jamtis enote types and self-send types +bool try_get_jamtis_enote_type(const JamtisSelfSendType self_send_type, JamtisEnoteType &enote_type_out); +bool try_get_jamtis_self_send_type(const JamtisEnoteType enote_type, JamtisSelfSendType &self_send_type_out); +bool is_jamtis_selfsend_type(const JamtisEnoteType enote_type); + +} //namespace jamtis +} //namespace sp + +/// make jamtis address index hashable +namespace sp +{ +namespace jamtis +{ +static_assert(sizeof(std::size_t) <= sizeof(address_index_t), ""); +inline std::size_t hash_value(const address_index_t &_v) +{ + return reinterpret_cast(_v); +} +} //namespace jamtis +} //namespace sp +namespace std +{ +template<> +struct hash +{ + std::size_t operator()(const sp::jamtis::address_index_t &_v) const + { + return reinterpret_cast(_v); + } +}; +} //namespace std diff --git a/src/seraphis_core/legacy_core_utils.cpp b/src/seraphis_core/legacy_core_utils.cpp new file mode 100644 index 0000000000..d6e0fb1feb --- /dev/null +++ b/src/seraphis_core/legacy_core_utils.cpp @@ -0,0 +1,401 @@ +// Copyright (c) 2022, 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 "legacy_core_utils.h" + +//local headers +#include "crypto/crypto.h" +extern "C" +{ +#include "crypto/crypto-ops.h" +} +#include "cryptonote_basic/cryptonote_format_utils.h" +#include "cryptonote_basic/subaddress_index.h" +#include "device/device.hpp" +#include "int-util.h" +#include "jamtis_support_types.h" +#include "ringct/rctOps.h" +#include "ringct/rctTypes.h" +#include "seraphis_crypto/sp_crypto_utils.h" +#include "tx_extra.h" + +//third party headers + +//standard headers + + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis" + +namespace sp +{ +//------------------------------------------------------------------------------------------------------------------- +void make_legacy_subaddress_spendkey(const rct::key &legacy_base_spend_pubkey, + const crypto::secret_key &legacy_view_privkey, + const cryptonote::subaddress_index &subaddress_index, + hw::device &hwdev, + rct::key &subaddress_spendkey_out) +{ + // Hn(k^v, i) = Hn("SubAddr" || k^v || index_major || index_minor) + const crypto::secret_key subaddress_modifier{ + hwdev.get_subaddress_secret_key(legacy_view_privkey, subaddress_index) + }; + + // Hn(k^v, i) G + rct::key subaddress_extension; + hwdev.scalarmultBase(subaddress_extension, rct::sk2rct(subaddress_modifier)); + + // K^{s,i} = Hn(k^v, i) G + k^s G + rct::addKeys(subaddress_spendkey_out, subaddress_extension, legacy_base_spend_pubkey); +} +//------------------------------------------------------------------------------------------------------------------- +void make_legacy_sender_receiver_secret(const rct::key &base_key, + const std::uint64_t tx_output_index, + const crypto::secret_key &DH_privkey, + hw::device &hwdev, + crypto::secret_key &legacy_sender_receiver_secret_out) +{ + // r K^v + crypto::key_derivation derivation; + hwdev.generate_key_derivation(rct::rct2pk(base_key), DH_privkey, derivation); + + // Hn(r K^v, t) + hwdev.derivation_to_scalar(derivation, tx_output_index, legacy_sender_receiver_secret_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_legacy_enote_view_extension(const std::uint64_t tx_output_index, + const crypto::key_derivation &sender_receiver_DH_derivation, + const crypto::secret_key &legacy_view_privkey, + const boost::optional &subaddress_index, + hw::device &hwdev, + crypto::secret_key &enote_view_extension_out) +{ + // Hn(r K^v, t) + hwdev.derivation_to_scalar(sender_receiver_DH_derivation, tx_output_index, enote_view_extension_out); + + // subaddress index modifier + if (subaddress_index) + { + // Hn(k^v, i) = Hn(k^v || index_major || index_minor) + const crypto::secret_key subaddress_modifier{ + hwdev.get_subaddress_secret_key(legacy_view_privkey, *subaddress_index) + }; + + // Hn(r K^v, t) + Hn(k^v, i) + hwdev.sc_secret_add(enote_view_extension_out, enote_view_extension_out, subaddress_modifier); + } +} +//------------------------------------------------------------------------------------------------------------------- +void make_legacy_onetime_address(const rct::key &destination_spendkey, + const rct::key &destination_viewkey, + const std::uint64_t tx_output_index, + const crypto::secret_key &enote_ephemeral_privkey, + hw::device &hwdev, + rct::key &onetime_address_out) +{ + // r K^v + crypto::key_derivation derivation; + hwdev.generate_key_derivation(rct::rct2pk(destination_viewkey), enote_ephemeral_privkey, derivation); + + // K^o = Hn(r K^v, t) G + K^s + crypto::public_key onetime_address_temp; + hwdev.derive_public_key(derivation, tx_output_index, rct::rct2pk(destination_spendkey), onetime_address_temp); + + onetime_address_out = rct::pk2rct(onetime_address_temp); +} +//------------------------------------------------------------------------------------------------------------------- +void make_legacy_key_image(const crypto::secret_key &enote_view_extension, + const crypto::secret_key &legacy_spend_privkey, + const rct::key &onetime_address, + hw::device &hwdev, + crypto::key_image &key_image_out) +{ + // KI = (view_key_stuff + k^s) * Hp(Ko) + crypto::secret_key onetime_address_privkey; + hwdev.sc_secret_add(onetime_address_privkey, enote_view_extension, legacy_spend_privkey); + + hwdev.generate_key_image(rct::rct2pk(onetime_address), onetime_address_privkey, key_image_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_legacy_auxilliary_key_image_v1(const crypto::secret_key &commitment_mask, + const rct::key &onetime_address, + hw::device &hwdev, + crypto::key_image &auxilliary_key_image_out) +{ + // mask Hp(Ko) + hwdev.generate_key_image(rct::rct2pk(onetime_address), commitment_mask, auxilliary_key_image_out); + + // z Hp(Ko) = - mask Hp(Ko) + // note: do this after making the key image instead of computing it directly because there is no way to + // compute the scalar 'z = - mask' with hwdev + auxilliary_key_image_out = rct::rct2ki(rct::scalarmultKey(rct::ki2rct(auxilliary_key_image_out), minus_one())); +} +//------------------------------------------------------------------------------------------------------------------- +void make_legacy_amount_blinding_factor_v2(const crypto::secret_key &sender_receiver_secret, + hw::device &hwdev, + crypto::secret_key &amount_blinding_factor_out) +{ + // Hn("commitment_mask", Hn(r K^v, t)) + amount_blinding_factor_out = rct::rct2sk(hwdev.genCommitmentMask(rct::sk2rct(sender_receiver_secret))); +} +//------------------------------------------------------------------------------------------------------------------- +void make_legacy_amount_blinding_factor_v2(const rct::key &destination_viewkey, + const std::uint64_t tx_output_index, + const crypto::secret_key &enote_ephemeral_privkey, + hw::device &hwdev, + crypto::secret_key &amount_blinding_factor_out) +{ + // Hn(r K^v, t) + crypto::secret_key sender_receiver_secret; + make_legacy_sender_receiver_secret(destination_viewkey, + tx_output_index, + enote_ephemeral_privkey, + hwdev, + sender_receiver_secret); + + // amount mask: Hn("commitment_mask", Hn(r K^v, t)) + make_legacy_amount_blinding_factor_v2(sender_receiver_secret, hwdev, amount_blinding_factor_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_legacy_encoded_amount_v1(const rct::key &destination_viewkey, + const std::uint64_t tx_output_index, + const crypto::secret_key &enote_ephemeral_privkey, + const crypto::secret_key &amount_mask, + const rct::xmr_amount amount, + hw::device &hwdev, + rct::key &encoded_amount_blinding_factor_out, + rct::key &encoded_amount_out) +{ + // Hn(r K^v, t) + crypto::secret_key sender_receiver_secret; + make_legacy_sender_receiver_secret(destination_viewkey, + tx_output_index, + enote_ephemeral_privkey, + hwdev, + sender_receiver_secret); + + // encoded amount blinding factor: enc(x) = x + Hn(Hn(r K^v, t)) + // encoded amount: enc(a) = to_key(little_endian(a)) + Hn(Hn(Hn(r K^v, t))) + rct::ecdhTuple encoded_amount_info{ + .mask = rct::sk2rct(amount_mask), + .amount = rct::d2h(amount) + }; + hwdev.ecdhEncode(encoded_amount_info, rct::sk2rct(sender_receiver_secret), false); + + encoded_amount_blinding_factor_out = encoded_amount_info.mask; + encoded_amount_out = encoded_amount_info.amount; +} +//------------------------------------------------------------------------------------------------------------------- +void make_legacy_encoded_amount_v2(const rct::key &destination_viewkey, + const std::uint64_t tx_output_index, + const crypto::secret_key &enote_ephemeral_privkey, + const rct::xmr_amount amount, + hw::device &hwdev, + jamtis::encoded_amount_t &encoded_amount_out) +{ + // Hn(r K^v, t) + crypto::secret_key sender_receiver_secret; + make_legacy_sender_receiver_secret(destination_viewkey, + tx_output_index, + enote_ephemeral_privkey, + hwdev, + sender_receiver_secret); + + // encoded amount: enc(a) = a XOR_8 H32("amount", Hn(r K^v, t)) + rct::ecdhTuple encoded_amount_info{ + .mask = rct::zero(), + .amount = rct::d2h(amount) + }; + hwdev.ecdhEncode(encoded_amount_info, rct::sk2rct(sender_receiver_secret), true); + + static_assert(sizeof(encoded_amount_info.amount) >= sizeof(encoded_amount_out), ""); + memcpy(encoded_amount_out.bytes, encoded_amount_info.amount.bytes, sizeof(encoded_amount_out)); +} +//------------------------------------------------------------------------------------------------------------------- +bool try_get_legacy_amount_v1(const rct::key &expected_amount_commitment, + const crypto::secret_key &sender_receiver_secret, + const rct::key &encoded_amount_blinding_factor, + const rct::key &encoded_amount, + hw::device &hwdev, + crypto::secret_key &amount_blinding_factor_out, + rct::xmr_amount &amount_out) +{ + // 1. get amount and blinding factor + // x = enc(x) - Hn(Hn(r K^v, t)) + // a = system_endian(trunc_8(enc(a) - Hn(Hn(Hn(r K^v, t))))) + rct::ecdhTuple decoded_amount_info{ + .mask = encoded_amount_blinding_factor, + .amount = encoded_amount + }; + hwdev.ecdhDecode(decoded_amount_info, rct::sk2rct(sender_receiver_secret), false); + + amount_blinding_factor_out = rct::rct2sk(decoded_amount_info.mask); + amount_out = h2d(decoded_amount_info.amount); //todo: is this endian-aware? + + // 2. try to reproduce the amount commitment (sanity check) + if (!(rct::commit(amount_out, rct::sk2rct(amount_blinding_factor_out)) == expected_amount_commitment)) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool try_get_legacy_amount_v1(const rct::key &expected_amount_commitment, + const rct::key &destination_viewkey, + const std::uint64_t tx_output_index, + const crypto::secret_key &enote_ephemeral_privkey, + const rct::key &encoded_amount_blinding_factor, + const rct::key &encoded_amount, + hw::device &hwdev, + crypto::secret_key &amount_blinding_factor_out, + rct::xmr_amount &amount_out) +{ + // Hn(r K^v, t) + crypto::secret_key sender_receiver_secret; + make_legacy_sender_receiver_secret(destination_viewkey, + tx_output_index, + enote_ephemeral_privkey, + hwdev, + sender_receiver_secret); + + // complete the decoding + return try_get_legacy_amount_v1(expected_amount_commitment, + sender_receiver_secret, + encoded_amount_blinding_factor, + encoded_amount, + hwdev, + amount_blinding_factor_out, + amount_out); +} +//------------------------------------------------------------------------------------------------------------------- +bool try_get_legacy_amount_v2(const rct::key &expected_amount_commitment, + const crypto::secret_key &sender_receiver_secret, + const jamtis::encoded_amount_t &encoded_amount, + hw::device &hwdev, + crypto::secret_key &amount_blinding_factor_out, + rct::xmr_amount &amount_out) +{ + // 1. a = enc(a) XOR_8 H32("amount", Hn(r K^v, t)) + rct::ecdhTuple decoded_amount_info; + static_assert(sizeof(decoded_amount_info.amount) >= sizeof(encoded_amount), ""); + memcpy(decoded_amount_info.amount.bytes, encoded_amount.bytes, sizeof(encoded_amount)); + hwdev.ecdhDecode(decoded_amount_info, rct::sk2rct(sender_receiver_secret), true); + + amount_out = h2d(decoded_amount_info.amount); //todo: is this endian-aware? + + // 2. x = Hn("commitment_mask", Hn(r K^v, t)) + make_legacy_amount_blinding_factor_v2(sender_receiver_secret, hwdev, amount_blinding_factor_out); + + // 3. try to reproduce the amount commitment (sanity check) + if (!(rct::commit(amount_out, rct::sk2rct(amount_blinding_factor_out)) == expected_amount_commitment)) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool try_get_legacy_amount_v2(const rct::key &expected_amount_commitment, + const rct::key &destination_viewkey, + const std::uint64_t tx_output_index, + const crypto::secret_key &enote_ephemeral_privkey, + const jamtis::encoded_amount_t &encoded_amount, + hw::device &hwdev, + crypto::secret_key &amount_blinding_factor_out, + rct::xmr_amount &amount_out) +{ + // Hn(r K^v, t) + crypto::secret_key sender_receiver_secret; + make_legacy_sender_receiver_secret(destination_viewkey, + tx_output_index, + enote_ephemeral_privkey, + hwdev, + sender_receiver_secret); + + // complete the decoding + return try_get_legacy_amount_v2(expected_amount_commitment, + sender_receiver_secret, + encoded_amount, + hwdev, + amount_blinding_factor_out, + amount_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_legacy_view_tag(const rct::key &destination_viewkey, + const std::uint64_t tx_output_index, + const crypto::secret_key &enote_ephemeral_privkey, + hw::device &hwdev, + crypto::view_tag &view_tag_out) +{ + // r K^v + crypto::key_derivation derivation; + hwdev.generate_key_derivation(rct::rct2pk(destination_viewkey), + enote_ephemeral_privkey, + derivation); + + // view_tag = H_1("view_tag", r K^v, t) + hwdev.derive_view_tag(derivation, tx_output_index, view_tag_out); +} +//------------------------------------------------------------------------------------------------------------------- +bool try_append_legacy_enote_ephemeral_pubkeys_to_tx_extra(const std::vector &enote_ephemeral_pubkeys, + TxExtra &tx_extra_inout) +{ + std::vector enote_ephemeral_pubkeys_typed; + enote_ephemeral_pubkeys_typed.reserve(enote_ephemeral_pubkeys.size()); + + for (const rct::key &enote_ephemeral_pubkey : enote_ephemeral_pubkeys) + enote_ephemeral_pubkeys_typed.emplace_back(rct::rct2pk(enote_ephemeral_pubkey)); + + return cryptonote::add_additional_tx_pub_keys_to_extra(tx_extra_inout, enote_ephemeral_pubkeys_typed); +} +//------------------------------------------------------------------------------------------------------------------- +void extract_legacy_enote_ephemeral_pubkeys_from_tx_extra(const TxExtra &tx_extra, + crypto::public_key &legacy_main_enote_ephemeral_pubkey_out, + std::vector &legacy_additional_enote_ephemeral_pubkeys) +{ + // 1. parse field + std::vector tx_extra_fields; + parse_tx_extra(tx_extra, tx_extra_fields); + + // 2. try to get solitary enote ephemeral pubkey: r G + // note: we must ALWAYS get this even if there are 'additional pub keys' because change outputs always use the + // main enote ephemeral pubkey for key derivations + cryptonote::tx_extra_pub_key pub_key_field; + + if (cryptonote::find_tx_extra_field_by_type(tx_extra_fields, pub_key_field)) + legacy_main_enote_ephemeral_pubkey_out = pub_key_field.pub_key; + else + legacy_main_enote_ephemeral_pubkey_out = rct::rct2pk(rct::I); + + // 3. try to get 'additional' enote ephemeral pubkeys (one per output): r_t K^v_t + cryptonote::tx_extra_additional_pub_keys additional_pub_keys_field; + legacy_additional_enote_ephemeral_pubkeys.clear(); + + if (cryptonote::find_tx_extra_field_by_type(tx_extra_fields, additional_pub_keys_field)) + legacy_additional_enote_ephemeral_pubkeys = additional_pub_keys_field.data; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace sp diff --git a/src/seraphis_core/legacy_core_utils.h b/src/seraphis_core/legacy_core_utils.h new file mode 100644 index 0000000000..ea9ead650f --- /dev/null +++ b/src/seraphis_core/legacy_core_utils.h @@ -0,0 +1,285 @@ +// Copyright (c) 2022, 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. + +// Miscellaneous legacy utilities. +// Note: these are the bare minimum for unit testing and legacy enote recovery, so are not fully-featured. + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "cryptonote_basic/subaddress_index.h" +#include "device/device.hpp" +#include "jamtis_support_types.h" +#include "ringct/rctTypes.h" +#include "tx_extra.h" + +//third party headers + +//standard headers + +//forward declarations + + +namespace sp +{ + +/** +* brief: make_legacy_subaddress_spendkey - make a legacy subaddress's spendkey +* - K^{s,i} = (Hn(k^v, i) + k^s) G +* - note: Hn(k^v, i) = Hn("SubAddr || k^v || index_major || index_minor) +* param: legacy_base_spend_pubkey - k^s G +* param: legacy_view_privkey - k^v +* param: subaddress_index - i +* inoutparam: hwdev - +* outparam: subaddress_spendkey_out - K^{s,i} = (Hn(k^v, i) + k^s) G +*/ +void make_legacy_subaddress_spendkey(const rct::key &legacy_base_spend_pubkey, + const crypto::secret_key &legacy_view_privkey, + const cryptonote::subaddress_index &subaddress_index, + hw::device &hwdev, + rct::key &subaddress_spendkey_out); +/** +* brief: make_legacy_sender_receiver_secret - make a legacy sender-receiver secret +* - Hn([sender: r_t K^v] [recipient: k^v R_t], t) +* param: base_key - [sender: K^v] [recipient: R_t] +* param: tx_output_index - t +* param: DH_privkey - [sender: r_t] [recipient: k^v] +* inoutparam: hwdev - +* outparam: legacy_sender_receiver_secret_out - Hn([sender: r_t K^v] [recipient: k^v R_t], t) +*/ +void make_legacy_sender_receiver_secret(const rct::key &base_key, + const std::uint64_t tx_output_index, + const crypto::secret_key &DH_privkey, + hw::device &hwdev, + crypto::secret_key &legacy_sender_receiver_secret_out); +/** +* brief: make_legacy_enote_view_extension - make a legacy enote's view extension +* - component of onetime address privkey involving view key +* - Hn(k^v R_t, t) + (IF subaddress enote owner THEN Hn(k^v, i) ELSE 0) +* param: tx_output_index - t +* param: sender_receiver_DH_derivation - k^v R_t +* param: legacy_view_privkey - k^v +* param: subaddress_index - optional(i) +* inoutparam: hwdev - +* outparam: enote_view_extension_out - Hn(k^v R_t, t) + (IF (i) THEN Hn(k^v, i) ELSE 0) +*/ +void make_legacy_enote_view_extension(const std::uint64_t tx_output_index, + const crypto::key_derivation &sender_receiver_DH_derivation, + const crypto::secret_key &legacy_view_privkey, + const boost::optional &subaddress_index, + hw::device &hwdev, + crypto::secret_key &enote_view_extension_out); +/** +* brief: make_legacy_onetime_address - make a legacy onetime address for the enote at index 't' in a tx's output set +* - Ko_t = Hn(r_t K^v, t) G + K^s +* param: destination_spendkey - [normal address: k^s G] [subaddress: (Hn(k^v, i) + k^s) G] +* param: destination_viewkey - [normal address: k^v G] [subaddress: k^v K^{s,i}] +* param: tx_output_index - t +* param: enote_ephemeral_privkey - r_t (note: r_t may be the same for all values of 't' if it is shared) +* inoutparam: hwdev - +* outparam: onetime_address_out - Ko_t +*/ +void make_legacy_onetime_address(const rct::key &destination_spendkey, + const rct::key &destination_viewkey, + const std::uint64_t tx_output_index, + const crypto::secret_key &enote_ephemeral_privkey, + hw::device &hwdev, + rct::key &onetime_address_out); +/** +* brief: make_legacy_key_image - make a legacy cryptonote-style key image +* - KI = (k^{o,v} + k^s) * Hp(Ko) +* - note: we pass Ko by value instead of computing it (Ko = (k^{o,v} + k^s) G) for performance reasons (even though +* skipping that step is less robust) +* param: enote_view_extension - k^{o,v} +* param: legacy_spend_privkey - k^s +* param: onetime_address - Ko +* inoutparam: hwdev - +* outparam: key_image_out - KI = (k^{o,v} + k^s) * Hp(Ko) +*/ +void make_legacy_key_image(const crypto::secret_key &enote_view_extension, + const crypto::secret_key &legacy_spend_privkey, + const rct::key &onetime_address, + hw::device &hwdev, + crypto::key_image &key_image_out); +/** +* brief: make_legacy_auxilliary_key_image_v1 - make a legacy cryptonote-style auxilliary key image (e.g. for use in a +* CLSAG proof) +* - KI_aux = z * Hp(Ko) +* - note: in CLSAG proofs, the commitment to zero is computed as 'C - C_offset = z G', where 'C_offset = -z G + C' +* param: commitment_mask - (-z) +* param: onetime_address - Ko +* inoutparam: hwdev - +* outparam: auxilliary_key_image_out - z * Hp(Ko) +*/ +void make_legacy_auxilliary_key_image_v1(const crypto::secret_key &commitment_mask, + const rct::key &onetime_address, + hw::device &hwdev, + crypto::key_image &auxilliary_key_image_out); +/** +* brief: make_legacy_amount_blinding_factor_v2 - make a legacy amount blinding factor (v2 is deterministic, v1 is not) +* - x = Hn("commitment_mask", Hn(r K^v, t)) +* param: sender_receiver_secret - Hn(r K^v, t) +* inoutparam: hwdev - +* outparam: amount_blinding_factor_out - x = Hn("commitment_mask", Hn(r K^v, t)) +*/ +void make_legacy_amount_blinding_factor_v2(const crypto::secret_key &sender_receiver_secret, + hw::device &hwdev, + crypto::secret_key &amount_blinding_factor_out); +void make_legacy_amount_blinding_factor_v2(const rct::key &destination_viewkey, + const std::uint64_t tx_output_index, + const crypto::secret_key &enote_ephemeral_privkey, + hw::device &hwdev, + crypto::secret_key &amount_blinding_factor_out); +/** +* brief: make_legacy_encoded_amount_v1 - make a legacy encoded amount with encoded amount mask (v1: 32 byte encodings) +* - enc(x) = x + Hn(Hn(r_t K^v, t)) +* - enc(a) = to_key(little_endian(a)) + Hn(Hn(Hn(r_t K^v, t))) +* param: destination_viewkey - K^v +* param: tx_output_index - t +* param: enote_ephemeral_privkey - r_t +* param: amount_mask - x +* param: amount - a +* inoutparam: hwdev - +* outparam: encoded_amount_blinding_factor_out - enc(x) +* outparam: encoded_amount_out - enc(a) +*/ +void make_legacy_encoded_amount_v1(const rct::key &destination_viewkey, + const std::uint64_t tx_output_index, + const crypto::secret_key &enote_ephemeral_privkey, + const crypto::secret_key &amount_mask, + const rct::xmr_amount amount, + hw::device &hwdev, + rct::key &encoded_amount_blinding_factor_out, + rct::key &encoded_amount_out); +/** +* brief: make_legacy_encoded_amount_v2 - make a legacy encoded amount (v2: 8-byte encoding) (note: mask is deterministic) +* - enc(a) = a XOR_8 H32("amount", Hn(r_t K^v, t)) +* param: destination_viewkey - K^v +* param: tx_output_index - t +* param: enote_ephemeral_privkey - r_t +* param: amount - a +* inoutparam: hwdev - +* outparam: encoded_amount_out - enc(a) +*/ +void make_legacy_encoded_amount_v2(const rct::key &destination_viewkey, + const std::uint64_t tx_output_index, + const crypto::secret_key &enote_ephemeral_privkey, + const rct::xmr_amount amount, + hw::device &hwdev, + jamtis::encoded_amount_t &encoded_amount_out); +/** +* brief: try_get_legacy_amount_v1 - try to decode a legacy encoded amount (v1: 32-byte encoding) +* - fails if amount commitment can't be reproduced +* - x = enc(x) - Hn(Hn(r K^v, t)) +* - a = system_endian(trunc_8(enc(a) - Hn(Hn(Hn(r K^v, t))))) +* param: expected_amount_commitment - C' = x G + a H +* param: sender_receiver_secret - Hn(r_t K^v, t) +* param: encoded_amount_blinding_factor - enc(x) +* param: encoded_amount - enc(a) +* inoutparam: hwdev - +* outparam: amount_blinding_factor_out - x +* outparam: amount_out - a +* return: true if amount was successfully recovered +*/ +bool try_get_legacy_amount_v1(const rct::key &expected_amount_commitment, + const crypto::secret_key &sender_receiver_secret, + const rct::key &encoded_amount_blinding_factor, + const rct::key &encoded_amount, + hw::device &hwdev, + crypto::secret_key &amount_blinding_factor_out, + rct::xmr_amount &amount_out); +bool try_get_legacy_amount_v1(const rct::key &expected_amount_commitment, + const rct::key &destination_viewkey, + const std::uint64_t tx_output_index, + const crypto::secret_key &enote_ephemeral_privkey, + const rct::key &encoded_amount_blinding_factor, + const rct::key &encoded_amount, + hw::device &hwdev, + crypto::secret_key &amount_blinding_factor_out, + rct::xmr_amount &amount_out); +/** +* brief: try_get_legacy_amount_v2 - try to decode a legacy encoded amount (v2: 8-byte encoding) (mask is deterministic) +* - fails if amount commitment can't be reproduced +* - x = Hn("commitment_mask", Hn(r K^v, t)) +* - a = enc(a) XOR_8 H32("amount", Hn(r K^v, t)) +* param: expected_amount_commitment - C' = x G + a H +* param: sender_receiver_secret - Hn(r_t K^v, t) +* param: encoded_amount - enc(a) +* inoutparam: hwdev - +* outparam: amount_blinding_factor_out - x +* outparam: amount_out - a +* return: true if amount was successfully recovered +*/ +bool try_get_legacy_amount_v2(const rct::key &expected_amount_commitment, + const crypto::secret_key &sender_receiver_secret, + const jamtis::encoded_amount_t &encoded_amount, + hw::device &hwdev, + crypto::secret_key &amount_blinding_factor_out, + rct::xmr_amount &amount_out); +bool try_get_legacy_amount_v2(const rct::key &expected_amount_commitment, + const rct::key &destination_viewkey, + const std::uint64_t tx_output_index, + const crypto::secret_key &enote_ephemeral_privkey, + const jamtis::encoded_amount_t &encoded_amount, + hw::device &hwdev, + crypto::secret_key &amount_blinding_factor_out, + rct::xmr_amount &amount_out); +/** +* brief: make_legacy_view_tag - make a legacy view tag +* - view_tag = H1("view_tag", r_t K^v, t) +* param: destination_viewkey - K^v +* param: tx_output_index - t +* param: enote_ephemeral_privkey - r_t +* inoutparam: hwdev - +* outparam: view_tag_out - H1("view_tag", r_t K^v, t) +*/ +void make_legacy_view_tag(const rct::key &destination_viewkey, + const std::uint64_t tx_output_index, + const crypto::secret_key &enote_ephemeral_privkey, + hw::device &hwdev, + crypto::view_tag &view_tag_out); +/** +* brief: try_append_legacy_enote_ephemeral_pubkeys_to_tx_extra - try to add legacy enote ephemeral pubkeys to a tx extra +* param: enote_ephemeral_pubkeys - {R_t} +* outparam: tx_extra_inout - the tx extra to append to +*/ +bool try_append_legacy_enote_ephemeral_pubkeys_to_tx_extra(const std::vector &enote_ephemeral_pubkeys, + TxExtra &tx_extra_inout); +/** +* brief: extract_legacy_enote_ephemeral_pubkeys_from_tx_extra - find legacy enote ephemeral pubkeys in a tx extra field +* param: tx_extra - memo field (byte vector) +* outparam: legacy_main_enote_ephemeral_pubkey_out - r G (this should always be present, because yucky legacy tx gen code) +* outparam: legacy_additional_enote_ephemeral_pubkeys_out - [empty if no subaddress destinations] +* [otherwise r_0 K^v_0, ..., r_n K^v_n] +*/ +void extract_legacy_enote_ephemeral_pubkeys_from_tx_extra(const TxExtra &tx_extra, + crypto::public_key &legacy_main_enote_ephemeral_pubkey_out, + std::vector &legacy_additional_enote_ephemeral_pubkeys_out); + +} //namespace sp diff --git a/src/seraphis_core/legacy_decoy_selector.h b/src/seraphis_core/legacy_decoy_selector.h new file mode 100644 index 0000000000..0ec6f28fe7 --- /dev/null +++ b/src/seraphis_core/legacy_decoy_selector.h @@ -0,0 +1,69 @@ +// Copyright (c) 2022, 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. + +// Interface for obtaining legacy ring member sets. + +#pragma once + +//local headers + +//third party headers + +//standard headers +#include +#include + +//forward declarations + + +namespace sp +{ + +//// +// LegacyDecoySelector +// - interface for requesting a ring member set for legacy ring signatures (represented as legacy on-chain enote indices) +/// +class LegacyDecoySelector +{ +public: +//destructor + virtual ~LegacyDecoySelector() = default; + +//overloaded operators + /// disable copy/move (this is a pure virtual base class) + LegacyDecoySelector& operator=(LegacyDecoySelector&&) = delete; + +//member functions + /// request a set of ring members as on-chain enote indices + virtual void get_ring_members(const std::uint64_t real_ring_member_index, + const std::uint64_t num_ring_members, + std::vector &ring_members_out, + std::uint64_t &real_ring_member_index_in_ref_set_out) const = 0; +}; + +} //namespace sp diff --git a/src/seraphis_core/legacy_decoy_selector_flat.cpp b/src/seraphis_core/legacy_decoy_selector_flat.cpp new file mode 100644 index 0000000000..0c5b1ec83b --- /dev/null +++ b/src/seraphis_core/legacy_decoy_selector_flat.cpp @@ -0,0 +1,100 @@ +// Copyright (c) 2022, 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 "legacy_decoy_selector_flat.h" + +//local headers +#include "crypto/crypto.h" +#include "misc_log_ex.h" + +//third party headers + +//standard headers +#include +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis" + +namespace sp +{ +//------------------------------------------------------------------------------------------------------------------- +LegacyDecoySelectorFlat::LegacyDecoySelectorFlat(const std::uint64_t min_index, const std::uint64_t max_index) : + m_min_index{min_index}, + m_max_index{max_index} +{ + // checks + CHECK_AND_ASSERT_THROW_MES(m_max_index >= m_min_index, "legacy decoy selector (flat): invalid element range."); +} +//------------------------------------------------------------------------------------------------------------------- +void LegacyDecoySelectorFlat::get_ring_members(const std::uint64_t real_ring_member_index, + const std::uint64_t num_ring_members, + std::vector &ring_members_out, + std::uint64_t &real_ring_member_index_in_ref_set_out) const +{ + CHECK_AND_ASSERT_THROW_MES(real_ring_member_index >= m_min_index, + "legacy decoy selector (flat): real ring member index below available index range."); + CHECK_AND_ASSERT_THROW_MES(real_ring_member_index <= m_max_index, + "legacy decoy selector (flat): real ring member index above available index range."); + CHECK_AND_ASSERT_THROW_MES(num_ring_members <= m_max_index - m_min_index + 1, + "legacy decoy selector (flat): insufficient available legacy enotes to have unique ring members."); + + // fill in ring members + ring_members_out.clear(); + ring_members_out.reserve(num_ring_members); + ring_members_out.emplace_back(real_ring_member_index); + + while (ring_members_out.size() < num_ring_members) + { + // select a new ring member from indices in the specified range that aren't used yet (only unique ring members + // are allowed) + std::uint64_t new_ring_member; + do { new_ring_member = crypto::rand_range(m_min_index, m_max_index); } + while (std::find(ring_members_out.begin(), ring_members_out.end(), new_ring_member) != ring_members_out.end()); + + ring_members_out.emplace_back(new_ring_member); + } + + // sort reference set + std::sort(ring_members_out.begin(), ring_members_out.end()); + + // find location in reference set where the real reference sits + // note: the reference set does not contain duplicates, so we don't have to handle the case of multiple real references + real_ring_member_index_in_ref_set_out = 0; + + for (const std::uint64_t reference : ring_members_out) + { + if (reference == real_ring_member_index) + return; + + ++real_ring_member_index_in_ref_set_out; + } +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace sp diff --git a/src/seraphis_core/legacy_decoy_selector_flat.h b/src/seraphis_core/legacy_decoy_selector_flat.h new file mode 100644 index 0000000000..0447d653ec --- /dev/null +++ b/src/seraphis_core/legacy_decoy_selector_flat.h @@ -0,0 +1,75 @@ +// Copyright (c) 2022, 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. + +// Implementation of legacy decoy selector: select unique decoys uniformly from the set of available legacy enote indices. + +#pragma once + +//local headers +#include "legacy_decoy_selector.h" + +//third party headers + +//standard headers +#include +#include + +//forward declarations + + +namespace sp +{ + +//// +// LegacyDecoySelectorFlat +// - get a set of unique legacy ring members, selected from a flat distribution across the range of available enotes +/// +class LegacyDecoySelectorFlat final : public LegacyDecoySelector +{ +public: +//constructors + /// default constructor: disabled + /// normal constructor + LegacyDecoySelectorFlat(const std::uint64_t min_index, const std::uint64_t max_index); + +//destructor: default + +//member functions + /// request a set of ring members from range [min_index, max_index] + void get_ring_members(const std::uint64_t real_ring_member_index, + const std::uint64_t num_ring_members, + std::vector &ring_members_out, + std::uint64_t &real_ring_member_index_in_ref_set_out) const override; + +//member variables +private: + std::uint64_t m_min_index; + std::uint64_t m_max_index; +}; + +} //namespace sp diff --git a/src/seraphis_core/legacy_enote_types.cpp b/src/seraphis_core/legacy_enote_types.cpp new file mode 100644 index 0000000000..97b9ea674f --- /dev/null +++ b/src/seraphis_core/legacy_enote_types.cpp @@ -0,0 +1,125 @@ +// Copyright (c) 2022, 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 "legacy_enote_types.h" + +//local headers +#include "common/variant.h" +#include "crypto/crypto.h" +#include "ringct/rctOps.h" +#include "ringct/rctTypes.h" + +//third party headers + +//standard headers + + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis" + +namespace sp +{ +//------------------------------------------------------------------------------------------------------------------- +const rct::key& onetime_address_ref(const LegacyEnoteVariant &variant) +{ + struct visitor final : public tools::variant_static_visitor + { + using variant_static_visitor::operator(); //for blank overload + const rct::key& operator()(const LegacyEnoteV1 &enote) const { return enote.onetime_address; } + const rct::key& operator()(const LegacyEnoteV2 &enote) const { return enote.onetime_address; } + const rct::key& operator()(const LegacyEnoteV3 &enote) const { return enote.onetime_address; } + const rct::key& operator()(const LegacyEnoteV4 &enote) const { return enote.onetime_address; } + const rct::key& operator()(const LegacyEnoteV5 &enote) const { return enote.onetime_address; } + }; + + return variant.visit(visitor{}); +} +//------------------------------------------------------------------------------------------------------------------- +rct::key amount_commitment_ref(const LegacyEnoteVariant &variant) +{ + struct visitor final : public tools::variant_static_visitor + { + using variant_static_visitor::operator(); //for blank overload + rct::key operator()(const LegacyEnoteV1 &enote) const { return rct::zeroCommit(enote.amount); } + rct::key operator()(const LegacyEnoteV2 &enote) const { return enote.amount_commitment; } + rct::key operator()(const LegacyEnoteV3 &enote) const { return enote.amount_commitment; } + rct::key operator()(const LegacyEnoteV4 &enote) const { return rct::zeroCommit(enote.amount); } + rct::key operator()(const LegacyEnoteV5 &enote) const { return enote.amount_commitment; } + }; + + return variant.visit(visitor{}); +} +//------------------------------------------------------------------------------------------------------------------- +LegacyEnoteV1 gen_legacy_enote_v1() +{ + LegacyEnoteV1 temp; + temp.onetime_address = rct::pkGen(); + temp.amount = crypto::rand_idx(0); + return temp; +} +//------------------------------------------------------------------------------------------------------------------- +LegacyEnoteV2 gen_legacy_enote_v2() +{ + LegacyEnoteV2 temp; + temp.onetime_address = rct::pkGen(); + temp.amount_commitment = rct::pkGen(); + temp.encoded_amount_blinding_factor = rct::skGen(); + temp.encoded_amount = rct::skGen(); + return temp; +} +//------------------------------------------------------------------------------------------------------------------- +LegacyEnoteV3 gen_legacy_enote_v3() +{ + LegacyEnoteV3 temp; + temp.onetime_address = rct::pkGen(); + temp.amount_commitment = rct::pkGen(); + crypto::rand(sizeof(temp.encoded_amount), temp.encoded_amount.bytes); + return temp; +} +//------------------------------------------------------------------------------------------------------------------- +LegacyEnoteV4 gen_legacy_enote_v4() +{ + LegacyEnoteV4 temp; + temp.onetime_address = rct::pkGen(); + temp.amount = crypto::rand_idx(0); + temp.view_tag.data = static_cast(crypto::rand_idx(0)); + return temp; +} +//------------------------------------------------------------------------------------------------------------------- +LegacyEnoteV5 gen_legacy_enote_v5() +{ + LegacyEnoteV5 temp; + temp.onetime_address = rct::pkGen(); + temp.amount_commitment = rct::pkGen(); + crypto::rand(sizeof(temp.encoded_amount), temp.encoded_amount.bytes); + temp.view_tag.data = static_cast(crypto::rand_idx(0)); + return temp; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace sp diff --git a/src/seraphis_core/legacy_enote_types.h b/src/seraphis_core/legacy_enote_types.h new file mode 100644 index 0000000000..afffdfc5e1 --- /dev/null +++ b/src/seraphis_core/legacy_enote_types.h @@ -0,0 +1,180 @@ +// Copyright (c) 2022, 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 "common/variant.h" +#include "crypto/crypto.h" +#include "jamtis_support_types.h" +#include "ringct/rctTypes.h" + +//third party headers + +//standard headers + +//forward declarations + + +namespace sp +{ + +//// +// LegacyEnoteV1 +// - onetime address +// - cleartext amount +/// +struct LegacyEnoteV1 final +{ + /// Ko + rct::key onetime_address; + /// a + rct::xmr_amount amount; +}; + +/// get size in bytes +inline std::size_t legacy_enote_v1_size_bytes() { return 32 + 8; } + +//// +// LegacyEnoteV2 +// - onetime address +// - amount commitment +// - encoded amount commitment mask +// - encoded amount (version 1: 32 bytes) +/// +struct LegacyEnoteV2 final +{ + /// Ko + rct::key onetime_address; + /// C + rct::key amount_commitment; + /// enc(x) + rct::key encoded_amount_blinding_factor; + /// enc(a) + rct::key encoded_amount; +}; + +/// get size in bytes +inline std::size_t legacy_enote_v2_size_bytes() { return 4*32; } + +//// +// LegacyEnoteV3 +// - onetime address +// - amount commitment +// - encoded amount (version 2: 8 bytes) +/// +struct LegacyEnoteV3 final +{ + /// Ko + rct::key onetime_address; + /// C + rct::key amount_commitment; + /// enc(a) + jamtis::encoded_amount_t encoded_amount; +}; + +/// get size in bytes +inline std::size_t legacy_enote_v3_size_bytes() { return 2*32 + 8; } + +//// +// LegacyEnoteV4 +// - onetime address +// - cleartext amount +// - view tag +/// +struct LegacyEnoteV4 final +{ + /// Ko + rct::key onetime_address; + /// a + rct::xmr_amount amount; + /// view_tag + crypto::view_tag view_tag; +}; + +/// get size in bytes +inline std::size_t legacy_enote_v4_size_bytes() { return 2*32 + 8 + sizeof(crypto::view_tag); } + +//// +// LegacyEnoteV5 +// - onetime address +// - amount commitment +// - encoded amount (version 2: 8 bytes) +// - view tag +/// +struct LegacyEnoteV5 final +{ + /// Ko + rct::key onetime_address; + /// C + rct::key amount_commitment; + /// enc(a) + jamtis::encoded_amount_t encoded_amount; + /// view_tag + crypto::view_tag view_tag; +}; + +/// get size in bytes +inline std::size_t legacy_enote_v5_size_bytes() { return 2*32 + 8 + sizeof(crypto::view_tag); } + +//// +// LegacyEnoteVariant +// - variant of all legacy enote types +// +// onetime_address_ref(): get the enote's onetime address +// amount_commitment_ref(): get the enote's amount commitment (this is a copy because V1 enotes need to +// compute the commitment) +/// +using LegacyEnoteVariant = tools::variant; +const rct::key& onetime_address_ref(const LegacyEnoteVariant &variant); +rct::key amount_commitment_ref(const LegacyEnoteVariant &variant); + +/** +* brief: gen_legacy_enote_v1() - generate a legacy v1 enote (all random) +*/ +LegacyEnoteV1 gen_legacy_enote_v1(); +/** +* brief: gen_legacy_enote_v2() - generate a legacy v2 enote (all random) +*/ +LegacyEnoteV2 gen_legacy_enote_v2(); +/** +* brief: gen_legacy_enote_v3() - generate a legacy v3 enote (all random) +*/ +LegacyEnoteV3 gen_legacy_enote_v3(); +/** +* brief: gen_legacy_enote_v4() - generate a legacy v4 enote (all random) +*/ +LegacyEnoteV4 gen_legacy_enote_v4(); +/** +* brief: gen_legacy_enote_v5() - generate a legacy v5 enote (all random) +*/ +LegacyEnoteV5 gen_legacy_enote_v5(); + +} //namespace sp diff --git a/src/seraphis_core/legacy_enote_utils.cpp b/src/seraphis_core/legacy_enote_utils.cpp new file mode 100644 index 0000000000..34c3686b21 --- /dev/null +++ b/src/seraphis_core/legacy_enote_utils.cpp @@ -0,0 +1,227 @@ +// Copyright (c) 2022, 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 "legacy_enote_utils.h" + +//local headers +#include "cryptonote_config.h" +#include "legacy_core_utils.h" +#include "ringct/rctOps.h" +#include "ringct/rctTypes.h" +#include "seraphis_crypto/sp_hash_functions.h" +#include "seraphis_crypto/sp_transcript.h" + +//third party headers + +//standard headers + + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis" + +namespace sp +{ +//------------------------------------------------------------------------------------------------------------------- +void get_legacy_enote_identifier(const rct::key &onetime_address, const rct::xmr_amount amount, rct::key &identifier_out) +{ + // identifier = H_32(Ko, a) + SpKDFTranscript transcript{config::HASH_KEY_LEGACY_ENOTE_IDENTIFIER, sizeof(onetime_address) + sizeof(amount)}; + transcript.append("Ko", onetime_address); + transcript.append("a", amount); + + sp_hash_to_32(transcript.data(), transcript.size(), identifier_out.bytes); +} +//------------------------------------------------------------------------------------------------------------------- +void make_legacy_enote_v1(const rct::key &destination_spendkey, + const rct::key &destination_viewkey, + const rct::xmr_amount amount, + const std::uint64_t output_index, + const crypto::secret_key &enote_ephemeral_privkey, + LegacyEnoteV1 &enote_out) +{ + // onetime address: K^o = Hn(r K^v, t) G + K^s + make_legacy_onetime_address(destination_spendkey, + destination_viewkey, + output_index, + enote_ephemeral_privkey, + hw::get_device("default"), + enote_out.onetime_address); + + // amount: a + enote_out.amount = amount; +} +//------------------------------------------------------------------------------------------------------------------- +void make_legacy_enote_v2(const rct::key &destination_spendkey, + const rct::key &destination_viewkey, + const rct::xmr_amount amount, + const std::uint64_t output_index, + const crypto::secret_key &enote_ephemeral_privkey, + LegacyEnoteV2 &enote_out) +{ + // onetime address: K^o = Hn(r K^v, t) G + K^s + make_legacy_onetime_address(destination_spendkey, + destination_viewkey, + output_index, + enote_ephemeral_privkey, + hw::get_device("default"), + enote_out.onetime_address); + + // amount commitment: x G + a H + const crypto::secret_key amount_mask{rct::rct2sk(rct::skGen())}; + enote_out.amount_commitment = rct::commit(amount, rct::sk2rct(amount_mask)); + + // encoded amount blinding factor: enc(x) = x + Hn(Hn(r K^v, t)) + // encoded amount: enc(a) = to_key(a) + Hn(Hn(Hn(r K^v, t))) + make_legacy_encoded_amount_v1(destination_viewkey, + output_index, + enote_ephemeral_privkey, + amount_mask, + amount, + hw::get_device("default"), + enote_out.encoded_amount_blinding_factor, + enote_out.encoded_amount); +} +//------------------------------------------------------------------------------------------------------------------- +void make_legacy_enote_v3(const rct::key &destination_spendkey, + const rct::key &destination_viewkey, + const rct::xmr_amount amount, + const std::uint64_t output_index, + const crypto::secret_key &enote_ephemeral_privkey, + LegacyEnoteV3 &enote_out) +{ + // onetime address: K^o = Hn(r K^v, t) G + K^s + make_legacy_onetime_address(destination_spendkey, + destination_viewkey, + output_index, + enote_ephemeral_privkey, + hw::get_device("default"), + enote_out.onetime_address); + + // amount commitment: Hn("commitment_mask", Hn(r K^v, t)) G + a H + crypto::secret_key amount_mask; + make_legacy_amount_blinding_factor_v2(destination_viewkey, + output_index, + enote_ephemeral_privkey, + hw::get_device("default"), + amount_mask); + + enote_out.amount_commitment = rct::commit(amount, rct::sk2rct(amount_mask)); + + // encoded amount: enc(a) = a XOR_8 H32("amount", Hn(r K^v, t)) + make_legacy_encoded_amount_v2(destination_viewkey, + output_index, + enote_ephemeral_privkey, + amount, + hw::get_device("default"), + enote_out.encoded_amount); +} +//------------------------------------------------------------------------------------------------------------------- +void make_legacy_enote_v4(const rct::key &destination_spendkey, + const rct::key &destination_viewkey, + const rct::xmr_amount amount, + const std::uint64_t output_index, + const crypto::secret_key &enote_ephemeral_privkey, + LegacyEnoteV4 &enote_out) +{ + // onetime address: K^o = Hn(r K^v, t) G + K^s + make_legacy_onetime_address(destination_spendkey, + destination_viewkey, + output_index, + enote_ephemeral_privkey, + hw::get_device("default"), + enote_out.onetime_address); + + // amount: a + enote_out.amount = amount; + + // view tag: + make_legacy_view_tag(destination_viewkey, + output_index, + enote_ephemeral_privkey, + hw::get_device("default"), + enote_out.view_tag); +} +//------------------------------------------------------------------------------------------------------------------- +void make_legacy_enote_v5(const rct::key &destination_spendkey, + const rct::key &destination_viewkey, + const rct::xmr_amount amount, + const std::uint64_t output_index, + const crypto::secret_key &enote_ephemeral_privkey, + LegacyEnoteV5 &enote_out) +{ + // onetime address: K^o = Hn(r K^v, t) G + K^s + make_legacy_onetime_address(destination_spendkey, + destination_viewkey, + output_index, + enote_ephemeral_privkey, + hw::get_device("default"), + enote_out.onetime_address); + + // amount commitment: Hn("commitment_mask", Hn(r K^v, t)) G + a H + crypto::secret_key amount_mask; + make_legacy_amount_blinding_factor_v2(destination_viewkey, + output_index, + enote_ephemeral_privkey, + hw::get_device("default"), + amount_mask); + + enote_out.amount_commitment = rct::commit(amount, rct::sk2rct(amount_mask)); + + // encoded amount: enc(a) = a XOR_8 H32("amount", Hn(r K^v, t)) + make_legacy_encoded_amount_v2(destination_viewkey, + output_index, + enote_ephemeral_privkey, + amount, + hw::get_device("default"), + enote_out.encoded_amount); + + // view tag: + make_legacy_view_tag(destination_viewkey, + output_index, + enote_ephemeral_privkey, + hw::get_device("default"), + enote_out.view_tag); +} +//------------------------------------------------------------------------------------------------------------------- +void make_legacy_ephemeral_pubkey_shared(const crypto::secret_key &enote_ephemeral_privkey, + rct::key &enote_ephemeral_pubkey_out) +{ + // enote ephemeral pubkey (basic): r G + rct::scalarmultBase(enote_ephemeral_pubkey_out, rct::sk2rct(enote_ephemeral_privkey)); +} +//------------------------------------------------------------------------------------------------------------------- +void make_legacy_ephemeral_pubkey_single(const rct::key &destination_spendkey, + const crypto::secret_key &enote_ephemeral_privkey, + rct::key &enote_ephemeral_pubkey_out) +{ + // enote ephemeral pubkey (for single enote): r K^s + rct::scalarmultKey(enote_ephemeral_pubkey_out, destination_spendkey, rct::sk2rct(enote_ephemeral_privkey)); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace sp diff --git a/src/seraphis_core/legacy_enote_utils.h b/src/seraphis_core/legacy_enote_utils.h new file mode 100644 index 0000000000..29a63c8b28 --- /dev/null +++ b/src/seraphis_core/legacy_enote_utils.h @@ -0,0 +1,138 @@ +// Copyright (c) 2022, 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 making legacy (cryptonote) enotes. +// These are not fully-featured. +// - does not support encrypted payment ids +// - does not support nuanced output creation rules (w.r.t. change outputs and subaddresses in txs with normal addresses) +// - only works for hw::device "default" +// Note: The legacy hash functions Hn(), Hx(), Hp() are built on the keccak hash function. + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "legacy_enote_types.h" +#include "ringct/rctTypes.h" + +//third party headers + +//standard headers + +//forward declarations + + +namespace sp +{ + +/** +* brief: get_legacy_enote_identifier - identifier for legacy enotes (for handling enotes with duplicate onetime addresses) +* identifier = H_32(Ko, a) +* note: legacy enotes with identical Ko and a are assumed to be interchangeable +* param: onetime_address - Ko +* param: amount - a +* outparam: identifier_out - H_32(Ko, a) +*/ +void get_legacy_enote_identifier(const rct::key &onetime_address, const rct::xmr_amount amount, rct::key &identifier_out); +/** +* brief: make_legacy_enote_v1 - make a v1 legacy enote sending to an address or subaddress +* param: destination_spendkey - [address: K^s = k^s G] [subaddress: K^{s,i} = (Hn(k^v, i) + k^s) G] +* param: destination_viewkey - [address: K^v = k^v G] [subaddress: K^{v,i} = k^v*(Hn(k^v, i) + k^s) G] +* param: amount - a +* param: output_index - t +* param: enote_ephemeral_privkey - [address: r] [subaddres: r_t] +* outparam: enote_out - [K^o, a] +*/ +void make_legacy_enote_v1(const rct::key &destination_spendkey, + const rct::key &destination_viewkey, + const rct::xmr_amount amount, + const std::uint64_t output_index, + const crypto::secret_key &enote_ephemeral_privkey, + LegacyEnoteV1 &enote_out); +/** +* brief: make_legacy_enote_v2 - make a v2 legacy enote sending to an address or subaddress +... +* outparam: enote_out - [K^o, C, enc(x), enc(a)] +*/ +void make_legacy_enote_v2(const rct::key &destination_spendkey, + const rct::key &destination_viewkey, + const rct::xmr_amount amount, + const std::uint64_t output_index, + const crypto::secret_key &enote_ephemeral_privkey, + LegacyEnoteV2 &enote_out); +/** +* brief: make_legacy_enote_v3 - make a v3 legacy enote sending to an address or subaddress +... +* outparam: enote_out - [K^o, C, enc(a)] +*/ +void make_legacy_enote_v3(const rct::key &destination_spendkey, + const rct::key &destination_viewkey, + const rct::xmr_amount amount, + const std::uint64_t output_index, + const crypto::secret_key &enote_ephemeral_privkey, + LegacyEnoteV3 &enote_out); +/** +* brief: make_legacy_enote_v4 - make a v4 legacy enote sending to an address or subaddress +... +* outparam: enote_out - [K^o, a, view_tag] +*/ +void make_legacy_enote_v4(const rct::key &destination_spendkey, + const rct::key &destination_viewkey, + const rct::xmr_amount amount, + const std::uint64_t output_index, + const crypto::secret_key &enote_ephemeral_privkey, + LegacyEnoteV4 &enote_out); +/** +* brief: make_legacy_enote_v5 - make a v5 legacy enote sending to an address or subaddress +... +* outparam: enote_out - [K^o, C, enc(a), view_tag] +*/ +void make_legacy_enote_v5(const rct::key &destination_spendkey, + const rct::key &destination_viewkey, + const rct::xmr_amount amount, + const std::uint64_t output_index, + const crypto::secret_key &enote_ephemeral_privkey, + LegacyEnoteV5 &enote_out); +/** +* brief: make_legacy_ephemeral_pubkey_shared - make an ephemeral pubkey for an enote (shared by all enotes in a tx) +* param: enote_ephemeral_privkey - r +* outparam: enote_ephemeral_pubkey_out - r G +*/ +void make_legacy_ephemeral_pubkey_shared(const crypto::secret_key &enote_ephemeral_privkey, + rct::key &enote_ephemeral_pubkey_out); +/** +* brief: make_legacy_ephemeral_pubkey_subaddress - make an ephemeral pubkey for a single enote in a tx +* param: destination_spendkey - [address: K^s = k^s G] [subaddress: K^{s,i} = (Hn(k^v, i) + k^s) G] +* param: enote_ephemeral_privkey - r_t +* outparam: enote_ephemeral_pubkey_out - r_t K^s +*/ +void make_legacy_ephemeral_pubkey_single(const rct::key &destination_spendkey, + const crypto::secret_key &enote_ephemeral_privkey, + rct::key &enote_ephemeral_pubkey_out); + +} //namespace sp diff --git a/src/seraphis_core/sp_core_enote_utils.cpp b/src/seraphis_core/sp_core_enote_utils.cpp new file mode 100644 index 0000000000..d56f812d08 --- /dev/null +++ b/src/seraphis_core/sp_core_enote_utils.cpp @@ -0,0 +1,253 @@ +// Copyright (c) 2022, 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 "sp_core_enote_utils.h" + +//local headers +extern "C" +{ +#include "crypto/crypto-ops.h" +} +#include "crypto/generators.h" +#include "cryptonote_config.h" +#include "misc_log_ex.h" +#include "ringct/rctOps.h" +#include "ringct/rctTypes.h" +#include "seraphis_crypto/sp_crypto_utils.h" +#include "seraphis_crypto/sp_hash_functions.h" +#include "seraphis_crypto/sp_transcript.h" +#include "sp_core_types.h" + +//third party headers + +//standard headers +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis" + +namespace sp +{ +//------------------------------------------------------------------------------------------------------------------- +void make_seraphis_key_image(const crypto::secret_key &y, const crypto::public_key &zU, crypto::key_image &key_image_out) +{ + CHECK_AND_ASSERT_THROW_MES(sc_isnonzero(to_bytes(y)), "y must be nonzero for making a key image!"); + CHECK_AND_ASSERT_THROW_MES(!(rct::pk2rct(zU) == rct::identity()), + "zU must not be identity element for making a key image!"); + + // KI = (z/y)*U + rct::key temp{sp::invert(rct::sk2rct(y))}; // 1/y + rct::scalarmultKey(temp, rct::pk2rct(zU), temp); // (z/y)*U + + key_image_out = rct::rct2ki(temp); +} +//------------------------------------------------------------------------------------------------------------------- +void make_seraphis_key_image(const crypto::secret_key &y, const crypto::secret_key &z, crypto::key_image &key_image_out) +{ + CHECK_AND_ASSERT_THROW_MES(sc_isnonzero(to_bytes(y)), "y must be nonzero for making a key image!"); + CHECK_AND_ASSERT_THROW_MES(sc_isnonzero(to_bytes(z)), "z must be nonzero for making a key image!"); + + // KI = (z/y)*U + const rct::key zU{rct::scalarmultKey(rct::pk2rct(crypto::get_U()), rct::sk2rct(z))}; // z U + make_seraphis_key_image(y, rct::rct2pk(zU), key_image_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_seraphis_core_spendkey(const crypto::secret_key &sp_spend_privkey, rct::key &core_spend_pubkey_out) +{ + // k_b U + rct::scalarmultKey(core_spend_pubkey_out, rct::pk2rct(crypto::get_U()), rct::sk2rct(sp_spend_privkey)); +} +//------------------------------------------------------------------------------------------------------------------- +void extend_seraphis_spendkey_x(const crypto::secret_key &k_extender_x, rct::key &spendkey_inout) +{ + // K = k_extender_x X + K_original + rct::key extender_key; + + rct::scalarmultKey(extender_key, rct::pk2rct(crypto::get_X()), rct::sk2rct(k_extender_x)); + rct::addKeys(spendkey_inout, extender_key, spendkey_inout); +} +//------------------------------------------------------------------------------------------------------------------- +void extend_seraphis_spendkey_u(const crypto::secret_key &k_extender_u, rct::key &spendkey_inout) +{ + // K = k_extender_u U + K_original + rct::key extender_key; + + rct::scalarmultKey(extender_key, rct::pk2rct(crypto::get_U()), rct::sk2rct(k_extender_u)); + rct::addKeys(spendkey_inout, extender_key, spendkey_inout); +} +//------------------------------------------------------------------------------------------------------------------- +void reduce_seraphis_spendkey_g(const crypto::secret_key &k_reducer_g, rct::key &spendkey_inout) +{ + static const rct::key MINUS_ONE{minus_one()}; + + // K = K_original - k_reducer_g G + crypto::secret_key mask_to_remove; + + sc_mul(to_bytes(mask_to_remove), MINUS_ONE.bytes, to_bytes(k_reducer_g)); // -k_reducer_g + mask_key(mask_to_remove, spendkey_inout, spendkey_inout); // (-k_reducer_g) G + K_original +} +//------------------------------------------------------------------------------------------------------------------- +void reduce_seraphis_spendkey_x(const crypto::secret_key &k_reducer_x, rct::key &spendkey_inout) +{ + static const rct::key MINUS_ONE{minus_one()}; + + // K = K_original - k_reducer_x X + crypto::secret_key extension; + + sc_mul(to_bytes(extension), MINUS_ONE.bytes, to_bytes(k_reducer_x)); // -k_reducer_x + extend_seraphis_spendkey_x(extension, spendkey_inout); // (-k_reducer_x) X + K_original +} +//------------------------------------------------------------------------------------------------------------------- +void reduce_seraphis_spendkey_u(const crypto::secret_key &k_reducer_u, rct::key &spendkey_inout) +{ + static const rct::key MINUS_ONE{minus_one()}; + + // K = K_original - k_reducer_u U + crypto::secret_key extension; + + sc_mul(to_bytes(extension), MINUS_ONE.bytes, to_bytes(k_reducer_u)); // -k_reducer_u + extend_seraphis_spendkey_u(extension, spendkey_inout); // (-k_reducer_u) U + K_original +} +//------------------------------------------------------------------------------------------------------------------- +void make_seraphis_spendkey(const crypto::secret_key &k_a, const crypto::secret_key &k_b, rct::key &spendkey_out) +{ + // K = k_a X + k_b U + make_seraphis_core_spendkey(k_b, spendkey_out); //k_b U + extend_seraphis_spendkey_x(k_a, spendkey_out); //k_a X + k_b U +} +//------------------------------------------------------------------------------------------------------------------- +void make_seraphis_squash_prefix(const rct::key &onetime_address, + const rct::key &amount_commitment, + rct::key &squash_prefix_out) +{ + // H_n(Ko,C) + SpKDFTranscript transcript{config::HASH_KEY_SERAPHIS_SQUASHED_ENOTE, 2*sizeof(rct::key)}; + transcript.append("Ko", onetime_address); + transcript.append("C", amount_commitment); + + // hash to the result + sp_hash_to_scalar(transcript.data(), transcript.size(), squash_prefix_out.bytes); +} +//------------------------------------------------------------------------------------------------------------------- +void make_seraphis_squashed_address_key(const rct::key &onetime_address, + const rct::key &amount_commitment, + rct::key &squashed_address_out) +{ + // Ko^t = H_n(Ko,C) Ko + rct::key squash_prefix; + make_seraphis_squash_prefix(onetime_address, amount_commitment, squash_prefix); + + rct::scalarmultKey(squashed_address_out, onetime_address, squash_prefix); +} +//------------------------------------------------------------------------------------------------------------------- +void make_seraphis_squashed_enote_Q(const rct::key &onetime_address, + const rct::key &amount_commitment, + rct::key &Q_out) +{ + // Ko^t + make_seraphis_squashed_address_key(onetime_address, amount_commitment, Q_out); + + // Q = Ko^t + C^t + rct::addKeys(Q_out, Q_out, amount_commitment); +} +//------------------------------------------------------------------------------------------------------------------- +void make_seraphis_enote_core(const rct::key &onetime_address, + const rct::xmr_amount amount, + const crypto::secret_key &amount_blinding_factor, + SpEnoteCore &enote_core_out) +{ + // Ko + enote_core_out.onetime_address = onetime_address; + + // C = x G + a H + enote_core_out.amount_commitment = rct::commit(amount, rct::sk2rct(amount_blinding_factor)); +} +//------------------------------------------------------------------------------------------------------------------- +void make_seraphis_enote_core(const crypto::secret_key &extension_privkey_g, + const crypto::secret_key &extension_privkey_x, + const crypto::secret_key &extension_privkey_u, + const rct::key &core_spend_pubkey, + const crypto::secret_key &sp_view_privkey, + const rct::xmr_amount amount, + const crypto::secret_key &amount_blinding_factor, + SpEnoteCore &enote_core_out) +{ + // K_s = k_a X + k_b U + enote_core_out.onetime_address = core_spend_pubkey; + extend_seraphis_spendkey_x(sp_view_privkey, enote_core_out.onetime_address); + + // Ko = k_extension_g G + k_extension_x X + k_extension_u U + K_s + extend_seraphis_spendkey_u(extension_privkey_u, enote_core_out.onetime_address); + extend_seraphis_spendkey_x(extension_privkey_x, enote_core_out.onetime_address); + mask_key(extension_privkey_g, enote_core_out.onetime_address, enote_core_out.onetime_address); + + // finish making the enote + make_seraphis_enote_core(enote_core_out.onetime_address, amount, amount_blinding_factor, enote_core_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_seraphis_enote_core(const crypto::secret_key &enote_view_extension_g, + const crypto::secret_key &enote_view_extension_x, + const crypto::secret_key &enote_view_extension_u, + const crypto::secret_key &sp_spend_privkey, + const crypto::secret_key &sp_view_privkey, + const rct::xmr_amount amount, + const crypto::secret_key &amount_blinding_factor, + SpEnoteCore &enote_core_out) +{ + // k_b U + rct::key core_spend_pubkey; + make_seraphis_core_spendkey(sp_spend_privkey, core_spend_pubkey); + + // finish making the enote + make_seraphis_enote_core(enote_view_extension_g, //k_g + enote_view_extension_x, //k_x + enote_view_extension_u, //k_u + core_spend_pubkey, + sp_view_privkey, + amount, + amount_blinding_factor, + enote_core_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_seraphis_enote_image_masked_keys(const rct::key &onetime_address, + const rct::key &amount_commitment, + const crypto::secret_key &address_mask, + const crypto::secret_key &commitment_mask, + rct::key &masked_address_out, + rct::key &masked_commitment_out) +{ + // K" = t_k G + H_n(Ko,C) Ko + make_seraphis_squashed_address_key(onetime_address, amount_commitment, masked_address_out); //H_n(Ko,C) Ko + sp::mask_key(address_mask, masked_address_out, masked_address_out); //t_k G + H_n(Ko,C) Ko + + // C" = t_c G + C + sp::mask_key(commitment_mask, amount_commitment, masked_commitment_out); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace sp diff --git a/src/seraphis_core/sp_core_enote_utils.h b/src/seraphis_core/sp_core_enote_utils.h new file mode 100644 index 0000000000..5235aa25c6 --- /dev/null +++ b/src/seraphis_core/sp_core_enote_utils.h @@ -0,0 +1,216 @@ +// Copyright (c) 2022, 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 enote and enote image component builders. + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "ringct/rctTypes.h" + +//third party headers + +//standard headers + +//forward declarations +namespace sp +{ + struct SpCoinbaseEnoteCore; + struct SpEnoteCore; +} + +namespace sp +{ + +/** +* brief: make_seraphis_key_image - create a seraphis key image from 'y' and spend key base 'zU' +* KI = (1/y) * z U +* param: y - y = (k_x + k_a +* param: zU - z U = (k_u + k_b) U +* outparam: key_image_out - KI +*/ +void make_seraphis_key_image(const crypto::secret_key &y, const crypto::public_key &zU, crypto::key_image &key_image_out); +/** +* brief: make_seraphis_key_image - create a seraphis key image from private keys 'y' and 'z' +* KI = (z/y)*U +* = ((k_u + k_b) / (k_x + k_a))*U +* param: y - y = (k_x + k_a +* param: z - z = (k_u + k_b) +* outparam: key_image_out - KI +*/ +void make_seraphis_key_image(const crypto::secret_key &y, const crypto::secret_key &z, crypto::key_image &key_image_out); +/** +* brief: make_seraphis_core_spendkey - create the core part of a seraphis spendkey +* spendbase = k_b U +* param: sp_spend_privkey - k_b +* outparam: core_spend_pubkey_out - k_b U +*/ +void make_seraphis_core_spendkey(const crypto::secret_key &sp_spend_privkey, rct::key &core_spend_pubkey_out); +/** +* brief: extend_seraphis_spendkey_x - extend a seraphis spendkey (or onetime address) on generator X +* K = k_extender_x X + K_original +* param: k_extender_x - extends the existing pubkey +* inoutparam: spendkey_inout - [in: K_original] [out: k_extender_x X + K_original] +*/ +void extend_seraphis_spendkey_x(const crypto::secret_key &k_extender_x, rct::key &spendkey_inout); +/** +* brief: extend_seraphis_spendkey_u - extend a seraphis spendkey (or onetime address) on generator U +* K = k_extender_u U + K_original +* param: k_extender_u - extends the existing pubkey +* inoutparam: spendkey_inout - [in: K_original] [out: k_extender_u U + K_original] +*/ +void extend_seraphis_spendkey_u(const crypto::secret_key &k_extender_u, rct::key &spendkey_inout); +/** +* brief: reduce_seraphis_spendkey_g - remove private key material from a seraphis spendkey (or onetime address) on +* generator G +* K = K_original - k_reducer_g G +* param: k_reducer_g - material to remove from the existing pubkey +* inoutparam: spendkey_inout - [in: K_original] [out: K_original - k_reducer_g G] +*/ +void reduce_seraphis_spendkey_g(const crypto::secret_key &k_reducer_g, rct::key &spendkey_inout); +/** +* brief: reduce_seraphis_spendkey_x - remove private key material from a seraphis spendkey (or onetime address) on +* generator X +* K = K_original - k_reducer_x X +* param: k_reducer_x - material to remove from the existing pubkey +* inoutparam: spendkey_inout - [in: K_original] [out: K_original - k_reducer_x X] +*/ +void reduce_seraphis_spendkey_x(const crypto::secret_key &k_reducer_x, rct::key &spendkey_inout); +/** +* brief: reduce_seraphis_spendkey_u - remove private key material from a seraphis spendkey (or onetime address) on +* generator U +* K = K_original - k_reducer_u U +* param: k_reducer_u - material to remove from the existing pubkey +* inoutparam: spendkey_inout - [in: K_original] [out: K_original - k_reducer_u U] +*/ +void reduce_seraphis_spendkey_u(const crypto::secret_key &k_reducer_u, rct::key &spendkey_inout); +/** +* brief: make_seraphis_spendkey - create a seraphis spendkey +* K_s = k_a X + k_b U +* param: view_privkey - k_a +* param: sp_spend_privkey - k_b +* outparam: spendkey_out - k_a X + k_b U +*/ +void make_seraphis_spendkey(const crypto::secret_key &k_a, const crypto::secret_key &k_b, rct::key &spendkey_out); +/** +* brief: make_seraphis_squash_prefix - make the prefix for squashing an enote in the squashed enote model +* H_n(Ko,C) +* param: onetime_address - Ko +* param: amount_commitment - C +* outparam: squash_prefix_out - H_n(Ko,C) +*/ +void make_seraphis_squash_prefix(const rct::key &onetime_address, + const rct::key &amount_commitment, + rct::key &squash_prefix_out); +/** +* brief: make_seraphis_squashed_address_key - make a 'squashed' address in the squashed enote model +* Ko^t = H_n(Ko,C) Ko +* param: onetime_address - Ko +* param: amount_commitment - C +* outparam: squashed_address_out - H_n(Ko,C) Ko +*/ +void make_seraphis_squashed_address_key(const rct::key &onetime_address, + const rct::key &amount_commitment, + rct::key &squashed_address_out); +/** +* brief: make_seraphis_squashed_enote_Q - make a 'squashed' enote in the squashed enote model +* Q = Ko^t + C^t = H_n(Ko,C) Ko + C +* param: onetime_address - Ko +* param: amount_commitment - C +* outparam: Q_out - Q +*/ +void make_seraphis_squashed_enote_Q(const rct::key &onetime_address, + const rct::key &amount_commitment, + rct::key &Q_out); +/** +* brief: make_seraphis_enote_core - make a seraphis enote from a pre-made onetime address +* param: onetime_address - +* param: amount - +* param: amount_blinding_factor - +* outparam: enote_core_out - +*/ +void make_seraphis_enote_core(const rct::key &onetime_address, + const rct::xmr_amount amount, + const crypto::secret_key &amount_blinding_factor, + SpEnoteCore &enote_core_out); +/** +* brief: make_seraphis_enote_core - make a seraphis enote by extending an existing address +* param: extension_privkey_g - +* param: extension_privkey_x - +* param: extension_privkey_u - +* param: core_spend_pubkey - +* param: sp_view_privkey - +* param: amount - +* param: amount_blinding_factor - +* outparam: enote_core_out - +*/ +void make_seraphis_enote_core(const crypto::secret_key &extension_privkey_g, + const crypto::secret_key &extension_privkey_x, + const crypto::secret_key &extension_privkey_u, + const rct::key &core_spend_pubkey, + const crypto::secret_key &sp_view_privkey, + const rct::xmr_amount amount, + const crypto::secret_key &amount_blinding_factor, + SpEnoteCore &enote_core_out); +/** +* brief: make_seraphis_enote_core - make a seraphis enote by building the address from scratch +* param: enote_view_extension_g - k_g +* param: enote_view_extension_x - k_x +* param: enote_view_extension_u - k_u +* param: sp_spend_privkey - k_b +* param: sp_view_privkey - k_a +* param: amount - a +* param: amount_blinding_factor - x +* outparam: enote_core_out - +*/ +void make_seraphis_enote_core(const crypto::secret_key &enote_view_extension_g, + const crypto::secret_key &enote_view_extension_x, + const crypto::secret_key &enote_view_extension_u, + const crypto::secret_key &sp_spend_privkey, + const crypto::secret_key &sp_view_privkey, + const rct::xmr_amount amount, + const crypto::secret_key &amount_blinding_factor, + SpEnoteCore &enote_core_out); +/** +* brief: make_seraphis_enote_image_masked_keys - make the masked keys for a seraphis enote image +* param: onetime_address - +* param: amount_commitment - +* param: address_mask - +* param: commitment_mask - +* outparam: masked_address_out - +* outparam: masked_commitment_out - +*/ +void make_seraphis_enote_image_masked_keys(const rct::key &onetime_address, + const rct::key &amount_commitment, + const crypto::secret_key &address_mask, + const crypto::secret_key &commitment_mask, + rct::key &masked_address_out, + rct::key &masked_commitment_out); + +} //namespace sp diff --git a/src/seraphis_core/sp_core_types.cpp b/src/seraphis_core/sp_core_types.cpp new file mode 100644 index 0000000000..6bcaf1b5d8 --- /dev/null +++ b/src/seraphis_core/sp_core_types.cpp @@ -0,0 +1,255 @@ +// Copyright (c) 2022, 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 "sp_core_types.h" + +//local headers +#include "crypto/crypto.h" +extern "C" +{ +#include "crypto/crypto-ops.h" +} +#include "ringct/rctOps.h" +#include "ringct/rctTypes.h" +#include "seraphis_crypto/sp_crypto_utils.h" +#include "seraphis_crypto/sp_transcript.h" +#include "sp_core_enote_utils.h" + +//third party headers + +//standard headers +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis" + +namespace sp +{ +//------------------------------------------------------------------------------------------------------------------- +void append_to_transcript(const SpCoinbaseEnoteCore &container, SpTranscriptBuilder &transcript_inout) +{ + transcript_inout.append("Ko", container.onetime_address); + transcript_inout.append("a", container.amount); +} +//------------------------------------------------------------------------------------------------------------------- +void append_to_transcript(const SpEnoteCore &container, SpTranscriptBuilder &transcript_inout) +{ + transcript_inout.append("Ko", container.onetime_address); + transcript_inout.append("C", container.amount_commitment); +} +//------------------------------------------------------------------------------------------------------------------- +const rct::key& onetime_address_ref(const SpEnoteCoreVariant &variant) +{ + struct visitor final : public tools::variant_static_visitor + { + using variant_static_visitor::operator(); //for blank overload + const rct::key& operator()(const SpCoinbaseEnoteCore &enote) const { return enote.onetime_address; } + const rct::key& operator()(const SpEnoteCore &enote) const { return enote.onetime_address; } + }; + + return variant.visit(visitor{}); +} +//------------------------------------------------------------------------------------------------------------------- +rct::key amount_commitment_ref(const SpEnoteCoreVariant &variant) +{ + struct visitor final : public tools::variant_static_visitor + { + using variant_static_visitor::operator(); //for blank overload + rct::key operator()(const SpCoinbaseEnoteCore &enote) const { return rct::zeroCommit(enote.amount); } + rct::key operator()(const SpEnoteCore &enote) const { return enote.amount_commitment; } + }; + + return variant.visit(visitor{}); +} +//------------------------------------------------------------------------------------------------------------------- +void append_to_transcript(const SpEnoteImageCore &container, SpTranscriptBuilder &transcript_inout) +{ + transcript_inout.append("K_masked", container.masked_address); + transcript_inout.append("C_masked", container.masked_commitment); + transcript_inout.append("KI", container.key_image); +} +//------------------------------------------------------------------------------------------------------------------- +bool operator==(const SpCoinbaseEnoteCore &a, const SpCoinbaseEnoteCore &b) +{ + return a.onetime_address == b.onetime_address && + a.amount == b.amount; +} +//------------------------------------------------------------------------------------------------------------------- +bool operator==(const SpEnoteCore &a, const SpEnoteCore &b) +{ + return a.onetime_address == b.onetime_address && + a.amount_commitment == b.amount_commitment; +} +//------------------------------------------------------------------------------------------------------------------- +bool operator==(const SpEnoteCoreVariant &variant1, const SpEnoteCoreVariant &variant2) +{ + // check they have the same type + if (!SpEnoteCoreVariant::same_type(variant1, variant2)) + return false; + + // use a visitor to test equality with variant2 + struct visitor final : public tools::variant_static_visitor + { + visitor(const SpEnoteCoreVariant &other_ref) : other{other_ref} {} + const SpEnoteCoreVariant &other; + + using variant_static_visitor::operator(); //for blank overload + bool operator()(const SpCoinbaseEnoteCore &enote) const { return enote == other.unwrap(); } + bool operator()(const SpEnoteCore &enote) const { return enote == other.unwrap(); } + }; + + return variant1.visit(visitor{variant2}); +} +//------------------------------------------------------------------------------------------------------------------- +bool compare_Ko(const SpCoinbaseEnoteCore &a, const SpCoinbaseEnoteCore &b) +{ + return memcmp(a.onetime_address.bytes, b.onetime_address.bytes, sizeof(rct::key)) < 0; +} +//------------------------------------------------------------------------------------------------------------------- +bool compare_Ko(const SpEnoteCore &a, const SpEnoteCore &b) +{ + return memcmp(a.onetime_address.bytes, b.onetime_address.bytes, sizeof(rct::key)) < 0; +} +//------------------------------------------------------------------------------------------------------------------- +bool compare_KI(const SpEnoteImageCore &a, const SpEnoteImageCore &b) +{ + return a.key_image < b.key_image; +} +//------------------------------------------------------------------------------------------------------------------- +bool compare_KI(const SpInputProposalCore &a, const SpInputProposalCore &b) +{ + return a.key_image < b.key_image; +} +//------------------------------------------------------------------------------------------------------------------- +bool compare_Ko(const SpOutputProposalCore &a, const SpOutputProposalCore &b) +{ + return memcmp(&a.onetime_address, &b.onetime_address, sizeof(rct::key)) < 0; +} +//------------------------------------------------------------------------------------------------------------------- +bool onetime_address_is_canonical(const SpCoinbaseEnoteCore &enote_core) +{ + return key_domain_is_prime_subgroup(enote_core.onetime_address); +} +//------------------------------------------------------------------------------------------------------------------- +bool onetime_address_is_canonical(const SpEnoteCore &enote_core) +{ + return key_domain_is_prime_subgroup(enote_core.onetime_address); +} +//------------------------------------------------------------------------------------------------------------------- +bool onetime_address_is_canonical(const SpOutputProposalCore &output_proposal) +{ + return key_domain_is_prime_subgroup(output_proposal.onetime_address); +} +//------------------------------------------------------------------------------------------------------------------- +void get_squash_prefix(const SpInputProposalCore &proposal, rct::key &squash_prefix_out) +{ + // H_n(Ko,C) + make_seraphis_squash_prefix(onetime_address_ref(proposal.enote_core), + amount_commitment_ref(proposal.enote_core), + squash_prefix_out); +} +//------------------------------------------------------------------------------------------------------------------- +void get_enote_image_core(const SpInputProposalCore &proposal, SpEnoteImageCore &image_out) +{ + // K" = t_k G + H_n(Ko,C) Ko + // C" = t_c G + C + make_seraphis_enote_image_masked_keys(onetime_address_ref(proposal.enote_core), + amount_commitment_ref(proposal.enote_core), + proposal.address_mask, + proposal.commitment_mask, + image_out.masked_address, + image_out.masked_commitment); + + // KI = ((k_u + k_m) / k_x) U + image_out.key_image = proposal.key_image; +} +//------------------------------------------------------------------------------------------------------------------- +void get_enote_core(const SpOutputProposalCore &proposal, SpEnoteCore &enote_out) +{ + make_seraphis_enote_core(proposal.onetime_address, proposal.amount, proposal.amount_blinding_factor, enote_out); +} +//------------------------------------------------------------------------------------------------------------------- +SpCoinbaseEnoteCore gen_sp_coinbase_enote_core() +{ + SpCoinbaseEnoteCore temp; + temp.onetime_address = rct::pkGen(); + crypto::rand(8, reinterpret_cast(&temp.amount)); + return temp; +} +//------------------------------------------------------------------------------------------------------------------- +SpEnoteCore gen_sp_enote_core() +{ + SpEnoteCore temp; + temp.onetime_address = rct::pkGen(); + temp.amount_commitment = rct::pkGen(); + return temp; +} +//------------------------------------------------------------------------------------------------------------------- +SpInputProposalCore gen_sp_input_proposal_core(const crypto::secret_key &sp_spend_privkey, + const crypto::secret_key &sp_view_privkey, + const rct::xmr_amount amount) +{ + SpInputProposalCore temp; + + temp.enote_view_extension_g = rct::rct2sk(rct::skGen()); + temp.enote_view_extension_x = rct::rct2sk(rct::skGen()); + temp.enote_view_extension_u = rct::rct2sk(rct::skGen()); + crypto::secret_key sp_spend_privkey_extended; + sc_add(to_bytes(sp_spend_privkey_extended), to_bytes(temp.enote_view_extension_u), to_bytes(sp_spend_privkey)); + make_seraphis_key_image(add_secrets(temp.enote_view_extension_x, sp_view_privkey), + sp_spend_privkey_extended, + temp.key_image); + temp.amount_blinding_factor = rct::rct2sk(rct::skGen()); + temp.amount = amount; + SpEnoteCore enote_core_temp; + make_seraphis_enote_core(temp.enote_view_extension_g, + temp.enote_view_extension_x, + temp.enote_view_extension_u, + sp_spend_privkey, + sp_view_privkey, + temp.amount, + temp.amount_blinding_factor, + enote_core_temp); + temp.enote_core = enote_core_temp; + temp.address_mask = rct::rct2sk(rct::skGen());; + temp.commitment_mask = rct::rct2sk(rct::skGen());; + + return temp; +} +//------------------------------------------------------------------------------------------------------------------- +SpOutputProposalCore gen_sp_output_proposal_core(const rct::xmr_amount amount) +{ + SpOutputProposalCore temp; + temp.onetime_address = rct::pkGen(); + temp.amount_blinding_factor = rct::rct2sk(rct::skGen()); + temp.amount = amount; + return temp; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace sp diff --git a/src/seraphis_core/sp_core_types.h b/src/seraphis_core/sp_core_types.h new file mode 100644 index 0000000000..33e10bc995 --- /dev/null +++ b/src/seraphis_core/sp_core_types.h @@ -0,0 +1,216 @@ +// Copyright (c) 2022, 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 "common/variant.h" +#include "crypto/crypto.h" +#include "ringct/rctTypes.h" + +//third party headers +#include + +//standard headers +#include + +//forward declarations +namespace sp { class SpTranscriptBuilder; } + +namespace sp +{ + +//// +// SpCoinbaseEnoteCore +/// +struct SpCoinbaseEnoteCore final +{ + //// Ko = k_g G + (k_x + k_a) X + (k_u + k_b) U + rct::key onetime_address; + /// a + /// note: C = 1 G + a H (implied) + rct::xmr_amount amount; +}; +inline const boost::string_ref container_name(const SpCoinbaseEnoteCore&) { return "SpCoinbaseEnoteCore"; } +void append_to_transcript(const SpCoinbaseEnoteCore &container, SpTranscriptBuilder &transcript_inout); + +/// get size in bytes +inline std::size_t sp_coinbase_enote_core_size_bytes() { return 32 + 8; } + +//// +// SpEnoteCore +/// +struct SpEnoteCore final +{ + /// Ko = k_g G + (k_x + k_a) X + (k_u + k_b) U + rct::key onetime_address; + /// C = x G + a H + rct::key amount_commitment; +}; +inline const boost::string_ref container_name(const SpEnoteCore&) { return "SpEnoteCore"; } +void append_to_transcript(const SpEnoteCore &container, SpTranscriptBuilder &transcript_inout); + +/// get size in bytes +inline std::size_t sp_enote_core_size_bytes() { return 32*2; } + +//// +// SpEnoteCoreVariant +// - variant of all seraphis core enote types +// +// onetime_address_ref(): get the enote's onetime address +// amount_commitment_ref(): get the enote's amount commitment (this is a copy because coinbase enotes need to +// compute the commitment) +/// +using SpEnoteCoreVariant = tools::variant; +const rct::key& onetime_address_ref(const SpEnoteCoreVariant &variant); +rct::key amount_commitment_ref(const SpEnoteCoreVariant &variant); + +//// +// SpEnoteImageCore +/// +struct SpEnoteImageCore final +{ + /// K" = t_k G + H_n(Ko,C)*Ko (in the squashed enote model) + rct::key masked_address; + /// C" = (t_c + x) G + a H + rct::key masked_commitment; + /// KI = ((k_u + k_b) / (k_x + k_a)) U + crypto::key_image key_image; +}; +inline const boost::string_ref container_name(const SpEnoteImageCore&) { return "SpEnoteImageCore"; } +void append_to_transcript(const SpEnoteImageCore &container, SpTranscriptBuilder &transcript_inout); + +/// get size in bytes +inline std::size_t sp_enote_image_core_size_bytes() { return 32*3; } + +//// +// SpInputProposalCore +// - for spending an enote +/// +struct SpInputProposalCore final +{ + /// core of the original enote + SpEnoteCoreVariant enote_core; + /// the enote's key image + crypto::key_image key_image; + + /// k_g = k_{g, sender} + k_{g, address} + crypto::secret_key enote_view_extension_g; + /// k_x = k_{x, sender} + k_{x, address} (does not include k_a) + crypto::secret_key enote_view_extension_x; + /// k_u = k_{u, sender} + k_{u, address} (does not include k_b) + crypto::secret_key enote_view_extension_u; + /// x + crypto::secret_key amount_blinding_factor; + /// a + rct::xmr_amount amount; + + /// t_k + crypto::secret_key address_mask; + /// t_c + crypto::secret_key commitment_mask; +}; + +//// +// SpOutputProposalCore +// - for creating an enote to send an amount to someone +/// +struct SpOutputProposalCore final +{ + /// Ko + rct::key onetime_address; + /// y + crypto::secret_key amount_blinding_factor; + /// b + rct::xmr_amount amount; +}; + +/// equality operators for equivalence testing +bool operator==(const SpCoinbaseEnoteCore &a, const SpCoinbaseEnoteCore &b); +bool operator==(const SpEnoteCore &a, const SpEnoteCore &b); +bool operator==(const SpEnoteCoreVariant &variant1, const SpEnoteCoreVariant &variant2); +/// comparison methods for sorting: a.Ko < b.Ko +bool compare_Ko(const SpCoinbaseEnoteCore &a, const SpCoinbaseEnoteCore &b); +bool compare_Ko(const SpEnoteCore &a, const SpEnoteCore &b); +bool compare_Ko(const SpOutputProposalCore &a, const SpOutputProposalCore &b); +/// comparison methods for sorting: a.KI < b.KI +bool compare_KI(const SpEnoteImageCore &a, const SpEnoteImageCore &b); +bool compare_KI(const SpInputProposalCore &a, const SpInputProposalCore &b); +/// check if the type has a canonical onetime address +bool onetime_address_is_canonical(const SpCoinbaseEnoteCore &enote_core); +bool onetime_address_is_canonical(const SpEnoteCore &enote_core); +bool onetime_address_is_canonical(const SpOutputProposalCore &output_proposal); + +/** +* brief: get_squash_prefix - get the input proposal's enote's squash prefix +* param: proposal - +* outparam: squash_prefix_out - H_n(Ko,C) +*/ +void get_squash_prefix(const SpInputProposalCore &proposal, rct::key &squash_prefix_out); +/** +* brief: get_enote_image_core - get input proposal's enote image in the squashed enote model +* param: proposal - +* outparam: image_out - +*/ +void get_enote_image_core(const SpInputProposalCore &proposal, SpEnoteImageCore &image_out); +/** +* brief: get_enote_core - get the output proposal's represented enote +* param: proposal - +* outparam: enote_out - +*/ +void get_enote_core(const SpOutputProposalCore &proposal, SpEnoteCore &enote_out); +/** +* brief: gen() - generate a seraphis coinbase enote (all random) +* return: generated proposal +*/ +SpCoinbaseEnoteCore gen_sp_coinbase_enote_core(); +/** +* brief: gen() - generate a seraphis enote (all random) +* return: generated proposal +*/ +SpEnoteCore gen_sp_enote_core(); +/** +* brief: gen_sp_input_proposal_core - generate a random input proposal +* param: sp_spend_privkey - +* param: sp_view_privkey - +* param: amount - +* return: generated proposal +*/ +SpInputProposalCore gen_sp_input_proposal_core(const crypto::secret_key &sp_spend_privkey, + const crypto::secret_key &sp_view_privkey, + const rct::xmr_amount amount); +/** +* brief: gen - generate a random proposal +* param: amount - +* return: generated proposal +*/ +SpOutputProposalCore gen_sp_output_proposal_core(const rct::xmr_amount amount); + +} //namespace sp diff --git a/src/seraphis_core/sp_ref_set_index_mapper.h b/src/seraphis_core/sp_ref_set_index_mapper.h new file mode 100644 index 0000000000..c4ede4c68e --- /dev/null +++ b/src/seraphis_core/sp_ref_set_index_mapper.h @@ -0,0 +1,78 @@ +// Copyright (c) 2022, 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. + +// Interface for mapping reference set indices to and from a custom distribution. + +#pragma once + +//local headers + +//third party headers + +//standard headers +#include +#include + +//forward declarations + + +namespace sp +{ + +//// +// SpRefSetIndexMapper +// - interface for mapping elements of a custom distribution (e.g. uniform over [a, b]) to and from a uniform +// space (the range [0, 2^64 - 1]) +// - the element space is modeled as a range of indices ([min, max]), so the mapping function is a filter between +// element space and uniform space +// - mapping: [min, max] <-(func)-> [0, 2^64 - 1] +// note: intuitively, you can select a random element in element space from the custom distribution by selecting a +// element uniformly at random from the uniform space then mapping it into element space +/// +class SpRefSetIndexMapper +{ +public: +//destructor + virtual ~SpRefSetIndexMapper() = default; + +//overloaded operators + /// disable copy/move (this is a pure virtual base class) + SpRefSetIndexMapper& operator=(SpRefSetIndexMapper&&) = delete; + +//getters + virtual std::uint64_t distribution_min_index() const = 0; + virtual std::uint64_t distribution_max_index() const = 0; + +//member functions + /// [min, max] --(func)-> [0, 2^64 - 1] + virtual std::uint64_t element_index_to_uniform_index(const std::uint64_t element_index) const = 0; + /// [min, max] <-(func)-- [0, 2^64 - 1] + virtual std::uint64_t uniform_index_to_element_index(const std::uint64_t uniform_index) const = 0; +}; + +} //namespace sp diff --git a/src/seraphis_core/sp_ref_set_index_mapper_flat.cpp b/src/seraphis_core/sp_ref_set_index_mapper_flat.cpp new file mode 100644 index 0000000000..38c8bf39b7 --- /dev/null +++ b/src/seraphis_core/sp_ref_set_index_mapper_flat.cpp @@ -0,0 +1,117 @@ +// Copyright (c) 2022, 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 "sp_ref_set_index_mapper_flat.h" + +//local headers +#include "misc_log_ex.h" + +//third party headers +#include "boost/multiprecision/cpp_int.hpp" + +//standard headers +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis" + +namespace sp +{ +//------------------------------------------------------------------------------------------------------------------- +// project element 'a' from range [a_min, a_max] into range [b_min, b_max] +//------------------------------------------------------------------------------------------------------------------- +static std::uint64_t project_between_ranges(const std::uint64_t a, + const std::uint64_t a_min, + const std::uint64_t a_max, + const std::uint64_t b_min, + const std::uint64_t b_max) +{ + // sanity checks + CHECK_AND_ASSERT_THROW_MES( + a >= a_min && + a <= a_max && + a_min <= a_max && + b_min <= b_max, + "ref set index mapper (flat) projecting between ranges: invalid inputs."); + + // (a - a_min)/(a_max - a_min + 1) = (b - b_min)/(b_max - b_min + 1) + // b = (a - a_min)*(b_max - b_min + 1)/(a_max - a_min + 1) + b_min + using boost::multiprecision::uint128_t; + + // numerator: (a - a_min)*(b_max - b_min + 1) + uint128_t result{a - a_min}; + result *= (uint128_t{b_max} - b_min + 1); + + // denominator: (a_max - a_min + 1) + result /= (uint128_t{a_max} - a_min + 1); + + // + b_min + return static_cast(result) + b_min; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +SpRefSetIndexMapperFlat::SpRefSetIndexMapperFlat(const std::uint64_t distribution_min_index, + const std::uint64_t distribution_max_index) : + m_distribution_min_index{distribution_min_index}, + m_distribution_max_index{distribution_max_index} +{ + // checks + CHECK_AND_ASSERT_THROW_MES(m_distribution_max_index >= m_distribution_min_index, + "ref set index mapper (flat): invalid element range."); +} +//------------------------------------------------------------------------------------------------------------------- +std::uint64_t SpRefSetIndexMapperFlat::element_index_to_uniform_index(const std::uint64_t element_index) const +{ + // [min, max] --(projection)-> [0, 2^64 - 1] + CHECK_AND_ASSERT_THROW_MES(element_index >= m_distribution_min_index, + "ref set index manager (flat): element index below distribution range."); + CHECK_AND_ASSERT_THROW_MES(element_index <= m_distribution_max_index, + "ref set index manager (flat): element index above distribution range."); + + // (element_index - min)/(max - min + 1) = (uniform_index - 0)/([2^64 - 1] - 0 + 1) + return project_between_ranges(element_index, + m_distribution_min_index, + m_distribution_max_index, + 0, + std::numeric_limits::max()); +} +//------------------------------------------------------------------------------------------------------------------- +std::uint64_t SpRefSetIndexMapperFlat::uniform_index_to_element_index(const std::uint64_t uniform_index) const +{ + // [min, max] <-(projection)-- [0, 2^64 - 1] + + // (uniform_index - 0)/([2^64 - 1] - 0 + 1) = (element_index - min)/(max - min + 1) + return project_between_ranges(uniform_index, + 0, + std::numeric_limits::max(), + m_distribution_min_index, + m_distribution_max_index); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace sp diff --git a/src/seraphis_core/sp_ref_set_index_mapper_flat.h b/src/seraphis_core/sp_ref_set_index_mapper_flat.h new file mode 100644 index 0000000000..c6fce83da7 --- /dev/null +++ b/src/seraphis_core/sp_ref_set_index_mapper_flat.h @@ -0,0 +1,80 @@ +// Copyright (c) 2022, 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. + +// Implementation of the reference set index mapper for a flat mapping function. + +#pragma once + +//local headers +#include "ringct/rctTypes.h" +#include "sp_ref_set_index_mapper.h" + +//third party headers + +//standard headers +#include +#include + +//forward declarations + + +namespace sp +{ + +//// +// SpRefSetIndexMapperFlat +// - linear mapping function (i.e. project the element range onto the uniform space) +/// +class SpRefSetIndexMapperFlat final : public SpRefSetIndexMapper +{ +public: +//constructors + /// default constructor: disabled + + /// normal constructor + SpRefSetIndexMapperFlat(const std::uint64_t distribution_min_index, const std::uint64_t distribution_max_index); + +//destructor: default + +//getters + std::uint64_t distribution_min_index() const override { return m_distribution_min_index; } + std::uint64_t distribution_max_index() const override { return m_distribution_max_index; } + +//member functions + /// [min, max] --(projection)-> [0, 2^64 - 1] + std::uint64_t element_index_to_uniform_index(const std::uint64_t element_index) const override; + /// [min, max] <-(projection)-- [0, 2^64 - 1] + std::uint64_t uniform_index_to_element_index(const std::uint64_t uniform_index) const override; + +//member variables +private: + std::uint64_t m_distribution_min_index; + std::uint64_t m_distribution_max_index; +}; + +} //namespace sp diff --git a/src/seraphis_core/tx_extra.cpp b/src/seraphis_core/tx_extra.cpp new file mode 100644 index 0000000000..d755278c50 --- /dev/null +++ b/src/seraphis_core/tx_extra.cpp @@ -0,0 +1,233 @@ +// Copyright (c) 2022, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//paired header +#include "tx_extra.h" + +//local headers +#include "common/container_helpers.h" +#include "common/varint.h" +#include "crypto/crypto.h" +#include "misc_log_ex.h" +#include "span.h" + +//third party headers + +//standard headers +#include +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis" + +namespace sp +{ +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +template +static void append_varint(const T value, std::vector &bytes_inout) +{ + unsigned char v_variable[(sizeof(std::size_t) * 8 + 6) / 7]; + unsigned char *v_variable_end = v_variable; + + // 1. write varint into a temp buffer + tools::write_varint(v_variable_end, value); + assert(v_variable_end <= v_variable + sizeof(v_variable)); + + // 2. copy into our bytes buffer + const std::size_t v_length = v_variable_end - v_variable; + bytes_inout.resize(bytes_inout.size() + v_length); + memcpy(bytes_inout.data() + bytes_inout.size() - v_length, v_variable, v_length); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void append_bytes(const unsigned char *data, + const std::size_t length, + std::vector &bytes_inout) +{ + // copy data into our bytes buffer + bytes_inout.resize(bytes_inout.size() + length); + memcpy(bytes_inout.data() + bytes_inout.size() - length, data, length); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +template +static bool try_parse_bytes_varint(const epee::span &bytes, std::size_t &position_inout, T &val_out) +{ + // 1. sanity check range + if (position_inout >= bytes.size()) + return false; + + // 2. try to read a variant into the value + const int parse_result{tools::read_varint(bytes.data() + position_inout, bytes.end(), val_out)}; + + // 3. check if parsing succeeded + if (parse_result <= 0) + return false; + + // 4. return success + position_inout += parse_result; + return true; +} +//------------------------------------------------------------------------------------------------------------------- +// convert an element to bytes and append to the input bytes: varint(type) || varint(length) || value +//------------------------------------------------------------------------------------------------------------------- +static void grow_extra_field_bytes(const ExtraFieldElement &element, std::vector &bytes_inout) +{ + // varint(type) || varint(length) || bytes + bytes_inout.reserve(bytes_inout.size() + 18 + element.value.size()); + + // 1. append type + append_varint(element.type, bytes_inout); + + // 2. append length + append_varint(element.value.size(), bytes_inout); + + // 3. append value + append_bytes(element.value.data(), element.value.size(), bytes_inout); +} +//------------------------------------------------------------------------------------------------------------------- +// get an extra field element from the specified position in the tx extra field +// - returns false if could not get an element +//------------------------------------------------------------------------------------------------------------------- +static bool try_get_extra_field_element(const epee::span &tx_extra, + std::size_t &element_position_inout, + ExtraFieldElement &element_out) +{ + // 1. parse the type + if (!try_parse_bytes_varint(tx_extra, element_position_inout, element_out.type)) + return false; + + // 2. parse the length + std::uint64_t length{0}; + if (!try_parse_bytes_varint(tx_extra, element_position_inout, length)) + return false; + + // 3. check if the value can be extracted (fail if it extends past the field end) + if (element_position_inout + length > tx_extra.size()) + return false; + + // 4. parse the value + append_bytes(tx_extra.data() + element_position_inout, length, element_out.value); + element_position_inout += length; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +bool operator<(const ExtraFieldElement &a, const ExtraFieldElement &b) +{ + // 1. check type + if (a.type < b.type) + return true; + if (a.type > b.type) + return false; + + // 2. check length (type is equal) + if (a.value.size() < b.value.size()) + return true; + if (a.value.size() > b.value.size()) + return false; + + // 3. check value (type, length are equal) + return a.value < b.value; +} +//------------------------------------------------------------------------------------------------------------------- +std::size_t length(const ExtraFieldElement &element) +{ + return element.value.size(); +} +//------------------------------------------------------------------------------------------------------------------- +void make_tx_extra(std::vector elements, TxExtra &tx_extra_out) +{ + tx_extra_out.clear(); + tx_extra_out.reserve(elements.size() * (18 + 32)); //assume 32 byte values + + // 1. tx_extra must be sorted + std::sort(elements.begin(), elements.end()); + + // 2. build the tx extra + for (const ExtraFieldElement &element : elements) + grow_extra_field_bytes(element, tx_extra_out); +} +//------------------------------------------------------------------------------------------------------------------- +bool try_get_extra_field_elements(const TxExtra &tx_extra, std::vector &elements_out) +{ + elements_out.clear(); + elements_out.reserve(tx_extra.size() / 25); //approximate + + // 1. extract elements from the tx extra field + std::size_t element_position{0}; + const epee::span tx_extra_span{epee::to_span(tx_extra)}; + + while (element_position < tx_extra.size()) + { + if (!try_get_extra_field_element(tx_extra_span, element_position, tools::add_element(elements_out))) + { + elements_out.pop_back(); + return false; + } + } + + // 2. if we didn't consume all bytes, then the field is malformed + if (element_position != tx_extra.size()) + return false; + + // 3. if the elements extracted from a tx extra are not sorted, then the field is malformed + if (!std::is_sorted(elements_out.begin(), elements_out.end())) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +void accumulate_extra_field_elements(const std::vector &elements_to_add, + std::vector &elements_inout) +{ + elements_inout.reserve(elements_inout.size() + elements_to_add.size()); + elements_inout.insert(elements_inout.end(), elements_to_add.begin(), elements_to_add.end()); +} +//------------------------------------------------------------------------------------------------------------------- +void accumulate_extra_field_elements(const TxExtra &partial_memo, + std::vector &elements_inout) +{ + std::vector temp_memo_elements; + CHECK_AND_ASSERT_THROW_MES(try_get_extra_field_elements(partial_memo, temp_memo_elements), + "accumulate extra field elements: malformed partial memo."); + accumulate_extra_field_elements(temp_memo_elements, elements_inout); +} +//------------------------------------------------------------------------------------------------------------------- +ExtraFieldElement gen_extra_field_element() +{ + ExtraFieldElement temp; + temp.type = crypto::rand_idx(0); + temp.value.resize(crypto::rand_idx(static_cast(101))); //limit length to 100 bytes for performance + crypto::rand(temp.value.size(), temp.value.data()); + return temp; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace sp diff --git a/src/seraphis_core/tx_extra.h b/src/seraphis_core/tx_extra.h new file mode 100644 index 0000000000..c99eefa6c7 --- /dev/null +++ b/src/seraphis_core/tx_extra.h @@ -0,0 +1,94 @@ +// Copyright (c) 2022, 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. + +// Implementation of the cryptonote tx_extra field, with an enforced 'sorted TLV' format. + +#pragma once + +//local headers + +//third party headers + +//standard headers +#include +#include + +//forward declarations + + +namespace sp +{ + +using TxExtra = std::vector; + +//// +// ExtraFieldElement: Type-Length-Value (TLV) format +/// +struct ExtraFieldElement final +{ + /// type + std::uint64_t type; + /// value length + //m_value.size() + /// value + std::vector value; +}; + +/// less-than operator for sorting: sort order = type, length, value bytewise comparison +bool operator<(const ExtraFieldElement &a, const ExtraFieldElement &b); +/// get length of an extra field element +std::size_t length(const ExtraFieldElement &element); +/** +* brief: make_tx_extra - make a tx extra +* param: elements - +* outparam: tx_extra_out - +*/ +void make_tx_extra(std::vector elements, TxExtra &tx_extra_out); +/** +* brief: try_get_extra_field_elements - try to deserialize a tx extra into extra field elements +* param: tx_extra - +* outparam: elements_out - +* return: true if deserializing succeeds +*/ +bool try_get_extra_field_elements(const TxExtra &tx_extra, std::vector &elements_out); +/** +* brief: accumulate_extra_field_elements - append extra field elements to an existing set of elements +* param: elements_to_add - +* inoutparam: elements_inout - +*/ +void accumulate_extra_field_elements(const std::vector &elements_to_add, + std::vector &elements_inout); +void accumulate_extra_field_elements(const TxExtra &partial_memo, + std::vector &elements_inout); +/** +* brief: gen_extra_field_element - generate a random extra field element +* return: a random field element +*/ +ExtraFieldElement gen_extra_field_element(); + +} //namespace sp diff --git a/src/seraphis_crypto/CMakeLists.txt b/src/seraphis_crypto/CMakeLists.txt new file mode 100644 index 0000000000..c921641149 --- /dev/null +++ b/src/seraphis_crypto/CMakeLists.txt @@ -0,0 +1,60 @@ +# Copyright (c) 2021, 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(seraphis_crypto_sources + bulletproofs_plus2.cpp + grootle.cpp + math_utils.cpp + matrix_proof.cpp + sp_composition_proof.cpp + sp_crypto_utils.cpp + sp_generator_factory.cpp + sp_hash_functions.cpp + sp_legacy_proof_helpers.cpp + sp_multiexp.cpp) + +monero_find_all_headers(seraphis_crypto_headers, "${CMAKE_CURRENT_SOURCE_DIR}") + +monero_add_library(seraphis_crypto + ${seraphis_crypto_sources} + ${seraphis_crypto_headers}) + +target_link_libraries(seraphis_crypto + PUBLIC + cncrypto + common + epee + ringct + PRIVATE + ${EXTRA_LIBRARIES}) + +target_include_directories(seraphis_crypto + PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}" + PRIVATE + ${Boost_INCLUDE_DIRS}) diff --git a/src/seraphis_crypto/bulletproofs_plus2.cpp b/src/seraphis_crypto/bulletproofs_plus2.cpp new file mode 100644 index 0000000000..040199beba --- /dev/null +++ b/src/seraphis_crypto/bulletproofs_plus2.cpp @@ -0,0 +1,1083 @@ +// Copyright (c) 2017-2022, 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. + +// Implements the Bulletproofs+ prover and verifier algorithms +// +// Preprint: https://eprint.iacr.org/2020/735, version 17 Jun 2020 +// +// NOTE ON NOTATION: +// In the signature constructions used in Monero, commitments to zero are treated as +// public keys against the curve group generator G. This means that amount +// commitments must use another generator H for values in order to show balance. +// The result is that the roles of g and h in the preprint are effectively swapped +// in this code, taking on the roles of H and G, respectively. Read carefully! + +#include "bulletproofs_plus2.h" + +extern "C" +{ +#include "crypto/crypto-ops.h" +} +#include "cryptonote_config.h" +#include "misc_log_ex.h" +#include "ringct/rctOps.h" +#include "ringct/multiexp.h" +#include "span.h" +#include "sp_crypto_utils.h" +#include "sp_generator_factory.h" +#include "sp_hash_functions.h" +#include "sp_multiexp.h" +#include "sp_transcript.h" + +#include +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "bulletproof_plus2" + +#define STRAUS_SIZE_LIMIT 232 +#define PIPPENGER_SIZE_LIMIT 0 + +namespace sp +{ + // Vector functions + static rct::key vector_exponent(const rct::keyV &a, const rct::keyV &b); + static rct::keyV vector_of_scalar_powers(const rct::key &x, size_t n); + + // Proof bounds + static constexpr size_t maxN = 64; // maximum number of bits in range + static constexpr size_t maxM = config::BULLETPROOF_PLUS2_MAX_COMMITMENTS; // maximum number of commitments to aggregate into a single proof + + // Cached public generators + static std::shared_ptr straus_HiGi_cache; + static std::shared_ptr pippenger_HiGi_cache; + + // Useful scalar constants + static const constexpr rct::key ZERO = { {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 } }; // 0 + static const constexpr rct::key ONE = { {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 } }; // 1 + static const constexpr rct::key TWO = { {0x02, 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 } }; // 2 + static const constexpr rct::key MINUS_ONE = { { 0xec, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10 } }; // -1 + static const constexpr rct::key MINUS_INV_EIGHT = { { 0x74, 0xa4, 0x19, 0x7a, 0xf0, 0x7d, 0x0b, 0xf7, 0x05, 0xc2, 0xda, 0x25, 0x2b, 0x5c, 0x0b, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a } }; // -(8**(-1)) + static rct::key TWO_SIXTY_FOUR_MINUS_ONE; // 2**64 - 1 + + // Initial transcript hash + static rct::key initial_transcript; + + // Initialization flag + static std::once_flag init_once_flag; + + // Use the generator caches to compute a multiscalar multiplication + static inline rct::key multiexp(const std::vector &data, size_t HiGi_size) + { + if (HiGi_size > 0) + { + static_assert(232 <= STRAUS_SIZE_LIMIT, "Straus in precalc mode can only be calculated till STRAUS_SIZE_LIMIT"); + return ((HiGi_size <= 232) && (data.size() == HiGi_size)) + ? straus(data, straus_HiGi_cache, 0) + : pippenger(data, pippenger_HiGi_cache, HiGi_size, rct::get_pippenger_c(data.size())); + } + else + { + return (data.size() <= 95) + ? straus(data, NULL, 0) + : pippenger(data, NULL, 0, rct::get_pippenger_c(data.size())); + } + } + + // Confirm that a scalar is properly reduced + static inline bool is_reduced(const rct::key &scalar) + { + return sc_check(scalar.bytes) == 0; + } + + // Construct public generators + static void init_exponents() + { + // Only needs to be done once + std::call_once(init_once_flag, [&](){ + + std::vector data; + data.reserve(maxN*maxM*2); + + for (size_t i = 0; i < maxN*maxM; ++i) + { + data.push_back({rct::zero(), generator_factory::get_generator_at_index_p3(i * 2)}); + data.push_back({rct::zero(), generator_factory::get_generator_at_index_p3(i * 2 + 1)}); + } + + straus_HiGi_cache = straus_init_cache(data, STRAUS_SIZE_LIMIT); + pippenger_HiGi_cache = pippenger_init_cache(data, 0, PIPPENGER_SIZE_LIMIT); + + // Compute 2**64 - 1 for later use in simplifying verification + TWO_SIXTY_FOUR_MINUS_ONE = TWO; + for (size_t i = 0; i < 6; i++) + { + sc_mul(TWO_SIXTY_FOUR_MINUS_ONE.bytes, TWO_SIXTY_FOUR_MINUS_ONE.bytes, TWO_SIXTY_FOUR_MINUS_ONE.bytes); + } + sc_sub(TWO_SIXTY_FOUR_MINUS_ONE.bytes, TWO_SIXTY_FOUR_MINUS_ONE.bytes, ONE.bytes); + + // Generate the initial Fiat-Shamir transcript hash, which is constant across all proofs + // h_initial = H_n() + SpFSTranscript initial_transcript_temp{config::HASH_KEY_BULLETPROOF_PLUS2_TRANSCRIPT, 0}; + sp_hash_to_scalar(initial_transcript_temp.data(), initial_transcript_temp.size(), initial_transcript.bytes); + + }); + } + + // Given two scalar arrays, construct a vector pre-commitment: + // + // a = (a_0, ..., a_{n-1}) + // b = (b_0, ..., b_{n-1}) + // + // Outputs a_0*Gi_0 + ... + a_{n-1}*Gi_{n-1} + + // b_0*Hi_0 + ... + b_{n-1}*Hi_{n-1} + static rct::key vector_exponent(const rct::keyV &a, const rct::keyV &b) + { + CHECK_AND_ASSERT_THROW_MES(a.size() == b.size(), "Incompatible sizes of a and b"); + CHECK_AND_ASSERT_THROW_MES(a.size() <= maxN*maxM, "Incompatible sizes of a and maxN"); + + std::vector multiexp_data; + multiexp_data.reserve(a.size()*2); + for (size_t i = 0; i < a.size(); ++i) + { + multiexp_data.emplace_back(a[i], generator_factory::get_generator_at_index_p3(i * 2)); + multiexp_data.emplace_back(b[i], generator_factory::get_generator_at_index_p3(i * 2 + 1)); + } + return multiexp(multiexp_data, 2 * a.size()); + } + + // Helper function used to compute the L and R terms used in the inner-product round function + static rct::key compute_LR(size_t size, + const rct::key &y, + const std::vector &G, + size_t G0, + const std::vector &H, + size_t H0, + const rct::keyV &a, + size_t a0, + const rct::keyV &b, + size_t b0, + const rct::key &c, + const rct::key &d) + { + CHECK_AND_ASSERT_THROW_MES(size + G0 <= G.size(), "Incompatible size for G"); + CHECK_AND_ASSERT_THROW_MES(size + H0 <= H.size(), "Incompatible size for H"); + CHECK_AND_ASSERT_THROW_MES(size + a0 <= a.size(), "Incompatible size for a"); + CHECK_AND_ASSERT_THROW_MES(size + b0 <= b.size(), "Incompatible size for b"); + CHECK_AND_ASSERT_THROW_MES(size <= maxN*maxM, "size is too large"); + + std::vector multiexp_data; + multiexp_data.resize(size*2 + 2); + rct::key temp; + for (size_t i = 0; i < size; ++i) + { + sc_mul(temp.bytes, a[a0+i].bytes, y.bytes); + sc_mul(multiexp_data[i*2].scalar.bytes, temp.bytes, rct::INV_EIGHT.bytes); + multiexp_data[i*2].point = G[G0+i]; + + sc_mul(multiexp_data[i*2+1].scalar.bytes, b[b0+i].bytes, rct::INV_EIGHT.bytes); + multiexp_data[i*2+1].point = H[H0+i]; + } + + sc_mul(multiexp_data[2*size].scalar.bytes, c.bytes, rct::INV_EIGHT.bytes); + ge_p3 H_p3; + ge_frombytes_vartime(&H_p3, rct::H.bytes); + multiexp_data[2*size].point = H_p3; + + sc_mul(multiexp_data[2*size+1].scalar.bytes, d.bytes, rct::INV_EIGHT.bytes); + ge_p3 G_p3; + ge_frombytes_vartime(&G_p3, rct::G.bytes); + multiexp_data[2*size+1].point = G_p3; + + return multiexp(multiexp_data, 0); + } + + // Given a scalar, construct a vector of its powers: + // + // Output (1,x,x**2,...,x**{n-1}) + static rct::keyV vector_of_scalar_powers(const rct::key &x, size_t n) + { + CHECK_AND_ASSERT_THROW_MES(n != 0, "Need n > 0"); + + rct::keyV res(n); + res[0] = rct::identity(); + if (n == 1) + return res; + res[1] = x; + for (size_t i = 2; i < n; ++i) + { + sc_mul(res[i].bytes, res[i-1].bytes, x.bytes); + } + return res; + } + + // Given a scalar, construct the sum of its powers from 2 to n (where n is a power of 2): + // + // Output x**2 + x**4 + x**6 + ... + x**n + static rct::key sum_of_even_powers(const rct::key &x, size_t n) + { + CHECK_AND_ASSERT_THROW_MES((n & (n - 1)) == 0, "Need n to be a power of 2"); + CHECK_AND_ASSERT_THROW_MES(n != 0, "Need n > 0"); + + rct::key x1 = copy(x); + sc_mul(x1.bytes, x1.bytes, x1.bytes); + + rct::key res = copy(x1); + while (n > 2) + { + sc_muladd(res.bytes, x1.bytes, res.bytes, res.bytes); + sc_mul(x1.bytes, x1.bytes, x1.bytes); + n /= 2; + } + + return res; + } + + // Given a scalar, return the sum of its powers from 1 to n + // + // Output x**1 + x**2 + x**3 + ... + x**n + static rct::key sum_of_scalar_powers(const rct::key &x, size_t n) + { + CHECK_AND_ASSERT_THROW_MES(n != 0, "Need n > 0"); + + rct::key res = ONE; + if (n == 1) + return x; + + n += 1; + rct::key x1 = copy(x); + + const bool is_power_of_2 = (n & (n - 1)) == 0; + if (is_power_of_2) + { + sc_add(res.bytes, res.bytes, x1.bytes); + while (n > 2) + { + sc_mul(x1.bytes, x1.bytes, x1.bytes); + sc_muladd(res.bytes, x1.bytes, res.bytes, res.bytes); + n /= 2; + } + } + else + { + rct::key prev = x1; + for (size_t i = 1; i < n; ++i) + { + if (i > 1) + sc_mul(prev.bytes, prev.bytes, x1.bytes); + sc_add(res.bytes, res.bytes, prev.bytes); + } + } + sc_sub(res.bytes, res.bytes, ONE.bytes); + + return res; + } + + // Given two scalar arrays, construct the weighted inner product against another scalar + // + // Output a_0*b_0*y**1 + a_1*b_1*y**2 + ... + a_{n-1}*b_{n-1}*y**n + static rct::key weighted_inner_product(const epee::span &a, + const epee::span &b, + const rct::key &y) + { + CHECK_AND_ASSERT_THROW_MES(a.size() == b.size(), "Incompatible sizes of a and b"); + rct::key res = rct::zero(); + rct::key y_power = ONE; + rct::key temp; + for (size_t i = 0; i < a.size(); ++i) + { + sc_mul(temp.bytes, a[i].bytes, b[i].bytes); + sc_mul(y_power.bytes, y_power.bytes, y.bytes); + sc_muladd(res.bytes, temp.bytes, y_power.bytes, res.bytes); + } + return res; + } + + static rct::key weighted_inner_product(const rct::keyV &a, const epee::span &b, const rct::key &y) + { + CHECK_AND_ASSERT_THROW_MES(a.size() == b.size(), "Incompatible sizes of a and b"); + return weighted_inner_product(epee::to_span(a), b, y); + } + + // Fold inner-product point vectors + static void hadamard_fold(std::vector &v, const rct::key &a, const rct::key &b) + { + CHECK_AND_ASSERT_THROW_MES((v.size() & 1) == 0, "Vector size should be even"); + const size_t sz = v.size() / 2; + for (size_t n = 0; n < sz; ++n) + { + ge_dsmp c[2]; + ge_dsm_precomp(c[0], &v[n]); + ge_dsm_precomp(c[1], &v[sz + n]); + ge_double_scalarmult_precomp_vartime2_p3(&v[n], a.bytes, c[0], b.bytes, c[1]); + } + v.resize(sz); + } + + // Add vectors componentwise + static rct::keyV vector_add(const rct::keyV &a, const rct::keyV &b) + { + CHECK_AND_ASSERT_THROW_MES(a.size() == b.size(), "Incompatible sizes of a and b"); + rct::keyV res(a.size()); + for (size_t i = 0; i < a.size(); ++i) + { + sc_add(res[i].bytes, a[i].bytes, b[i].bytes); + } + return res; + } + + // Add a scalar to all elements of a vector + static rct::keyV vector_add(const rct::keyV &a, const rct::key &b) + { + rct::keyV res(a.size()); + for (size_t i = 0; i < a.size(); ++i) + { + sc_add(res[i].bytes, a[i].bytes, b.bytes); + } + return res; + } + + // Subtract a scalar from all elements of a vector + static rct::keyV vector_subtract(const rct::keyV &a, const rct::key &b) + { + rct::keyV res(a.size()); + for (size_t i = 0; i < a.size(); ++i) + { + sc_sub(res[i].bytes, a[i].bytes, b.bytes); + } + return res; + } + + // Multiply a scalar by all elements of a vector + static rct::keyV vector_scalar(const epee::span &a, const rct::key &x) + { + rct::keyV res(a.size()); + for (size_t i = 0; i < a.size(); ++i) + { + sc_mul(res[i].bytes, a[i].bytes, x.bytes); + } + return res; + } + + // Invert a batch of scalars, all of which _must_ be nonzero + static rct::keyV invert(rct::keyV x) + { + rct::keyV scratch; + scratch.reserve(x.size()); + + rct::key acc = rct::identity(); + for (size_t n = 0; n < x.size(); ++n) + { + CHECK_AND_ASSERT_THROW_MES(!(x[n] == ZERO), "Cannot invert zero!"); + scratch.push_back(acc); + if (n == 0) + acc = x[0]; + else + sc_mul(acc.bytes, acc.bytes, x[n].bytes); + } + + acc = invert(acc); + + rct::key tmp; + for (int i = x.size(); i-- > 0; ) + { + sc_mul(tmp.bytes, acc.bytes, x[i].bytes); + sc_mul(x[i].bytes, acc.bytes, scratch[i].bytes); + acc = tmp; + } + + return x; + } + + // Compute the slice of a vector + static epee::span slice(const rct::keyV &a, size_t start, size_t stop) + { + CHECK_AND_ASSERT_THROW_MES(start < a.size(), "Invalid start index"); + CHECK_AND_ASSERT_THROW_MES(stop <= a.size(), "Invalid stop index"); + CHECK_AND_ASSERT_THROW_MES(start < stop, "Invalid start/stop indices"); + return epee::span(&a[start], stop - start); + } + + // Update the transcript + static rct::key transcript_update(rct::key &transcript) + { + SpFSTranscript transcript_update{config::HASH_KEY_BULLETPROOF_PLUS2_TRANSCRIPT_UPDATE, sizeof(rct::key)}; + transcript_update.append("prev", transcript); + sp_hash_to_scalar(transcript_update.data(), transcript_update.size(), transcript.bytes); + return transcript; + } + + static rct::key transcript_update(rct::key &transcript, const rct::key &update_0) + { + SpFSTranscript transcript_update{config::HASH_KEY_BULLETPROOF_PLUS2_TRANSCRIPT_UPDATE, 2*sizeof(rct::key)}; + transcript_update.append("prev", transcript); + transcript_update.append("0", update_0); + sp_hash_to_scalar(transcript_update.data(), transcript_update.size(), transcript.bytes); + return transcript; + } + + static rct::key transcript_update(rct::key &transcript, const rct::key &update_0, const rct::key &update_1) + { + SpFSTranscript transcript_update{config::HASH_KEY_BULLETPROOF_PLUS2_TRANSCRIPT_UPDATE, 3*sizeof(rct::key)}; + transcript_update.append("prev", transcript); + transcript_update.append("0", update_0); + transcript_update.append("1", update_1); + sp_hash_to_scalar(transcript_update.data(), transcript_update.size(), transcript.bytes); + return transcript; + } + + static rct::key transcript_update(rct::key &transcript, const rct::keyV &update_vec) + { + SpFSTranscript transcript_update{ + config::HASH_KEY_BULLETPROOF_PLUS2_TRANSCRIPT_UPDATE, + update_vec.size()*sizeof(rct::key) + }; + transcript_update.append("prev", transcript); + transcript_update.append("v", update_vec); + sp_hash_to_scalar(transcript_update.data(), transcript_update.size(), transcript.bytes); + return transcript; + } + + // Given a value v [0..2**N) and a mask gamma, construct a range proof + BulletproofPlus2 bulletproof_plus2_PROVE(const rct::key &sv, const rct::key &gamma) + { + return bulletproof_plus2_PROVE(rct::keyV(1, sv), rct::keyV(1, gamma)); + } + + BulletproofPlus2 bulletproof_plus2_PROVE(uint64_t v, const rct::key &gamma) + { + return bulletproof_plus2_PROVE(std::vector(1, v), rct::keyV(1, gamma)); + } + + // Given a set of values v [0..2**N) and masks gamma, construct a range proof + BulletproofPlus2 bulletproof_plus2_PROVE(const rct::keyV &sv, const rct::keyV &gamma) + { + // Sanity check on inputs + CHECK_AND_ASSERT_THROW_MES(sv.size() == gamma.size(), "Incompatible sizes of sv and gamma"); + CHECK_AND_ASSERT_THROW_MES(!sv.empty(), "sv is empty"); + for (const rct::key &sve: sv) + CHECK_AND_ASSERT_THROW_MES(is_reduced(sve), "Invalid sv input"); + for (const rct::key &g: gamma) + CHECK_AND_ASSERT_THROW_MES(is_reduced(g), "Invalid gamma input"); + + init_exponents(); + + // Useful proof bounds + // + // N: number of bits in each range (here, 64) + // logN: base-2 logarithm + // M: first power of 2 greater than or equal to the number of range proofs to aggregate + // logM: base-2 logarithm + constexpr size_t logN = 6; // log2(64) + constexpr size_t N = 1< 0; ) + { + if (j < sv.size() && (sv[j][i/8] & (((uint64_t)1)<<(i%8)))) + { + aL[j*N+i] = rct::identity(); + aL8[j*N+i] = rct::INV_EIGHT; + aR[j*N+i] = aR8[j*N+i] = rct::zero(); + } + else + { + aL[j*N+i] = aL8[j*N+i] = rct::zero(); + aR[j*N+i] = MINUS_ONE; + aR8[j*N+i] = MINUS_INV_EIGHT; + } + } + } + +try_again: + // This is a Fiat-Shamir transcript + rct::key transcript = copy(initial_transcript); + transcript_update(transcript, V); + + // A + rct::key alpha = rct::skGen(); + rct::key pre_A = vector_exponent(aL8, aR8); + rct::key A; + sc_mul(temp.bytes, alpha.bytes, rct::INV_EIGHT.bytes); + rct::addKeys(A, pre_A, rct::scalarmultBase(temp)); + + // Challenges + rct::key y = transcript_update(transcript, A); + if (y == rct::zero()) + { + MINFO("y is 0, trying again"); + goto try_again; + } + rct::key z = transcript_update(transcript); + if (z == rct::zero()) + { + MINFO("z is 0, trying again"); + goto try_again; + } + rct::key z_squared; + sc_mul(z_squared.bytes, z.bytes, z.bytes); + + // Windowed vector + // d[j*N+i] = z**(2*(j+1)) * 2**i + // + // We compute this iteratively in order to reduce scalar operations. + rct::keyV d(MN, rct::zero()); + d[0] = z_squared; + for (size_t i = 1; i < N; i++) + { + sc_mul(d[i].bytes, d[i-1].bytes, TWO.bytes); + } + + for (size_t j = 1; j < M; j++) + { + for (size_t i = 0; i < N; i++) + { + sc_mul(d[j*N+i].bytes, d[(j-1)*N+i].bytes, z_squared.bytes); + } + } + + rct::keyV y_powers = vector_of_scalar_powers(y, MN+2); + + // Prepare inner product terms + rct::keyV aL1 = vector_subtract(aL, z); + + rct::keyV aR1 = vector_add(aR, z); + rct::keyV d_y(MN); + for (size_t i = 0; i < MN; i++) + { + sc_mul(d_y[i].bytes, d[i].bytes, y_powers[MN-i].bytes); + } + aR1 = vector_add(aR1, d_y); + + rct::key alpha1 = alpha; + temp = ONE; + for (size_t j = 0; j < sv.size(); j++) + { + sc_mul(temp.bytes, temp.bytes, z_squared.bytes); + sc_mul(temp2.bytes, y_powers[MN+1].bytes, temp.bytes); + sc_muladd(alpha1.bytes, temp2.bytes, gamma[j].bytes, alpha1.bytes); + } + + // These are used in the inner product rounds + size_t nprime = MN; + std::vector Gprime(MN); + std::vector Hprime(MN); + rct::keyV aprime(MN); + rct::keyV bprime(MN); + + const rct::key yinv = invert(y); + rct::keyV yinvpow(MN); + yinvpow[0] = ONE; + for (size_t i = 0; i < MN; ++i) + { + Gprime[i] = generator_factory::get_generator_at_index_p3(i * 2); + Hprime[i] = generator_factory::get_generator_at_index_p3(i * 2 + 1); + if (i > 0) + { + sc_mul(yinvpow[i].bytes, yinvpow[i-1].bytes, yinv.bytes); + } + aprime[i] = aL1[i]; + bprime[i] = aR1[i]; + } + rct::keyV L(logMN); + rct::keyV R(logMN); + int round = 0; + + // Inner-product rounds + while (nprime > 1) + { + nprime /= 2; + + rct::key cL = weighted_inner_product(slice(aprime, 0, nprime), slice(bprime, nprime, bprime.size()), y); + rct::key cR = weighted_inner_product(vector_scalar(slice(aprime, nprime, aprime.size()), y_powers[nprime]), slice(bprime, 0, nprime), y); + + rct::key dL = rct::skGen(); + rct::key dR = rct::skGen(); + + L[round] = compute_LR(nprime, yinvpow[nprime], Gprime, nprime, Hprime, 0, aprime, 0, bprime, nprime, cL, dL); + R[round] = compute_LR(nprime, y_powers[nprime], Gprime, 0, Hprime, nprime, aprime, nprime, bprime, 0, cR, dR); + + const rct::key challenge = transcript_update(transcript, L[round], R[round]); + if (challenge == rct::zero()) + { + MINFO("challenge is 0, trying again"); + goto try_again; + } + + const rct::key challenge_inv = invert(challenge); + + sc_mul(temp.bytes, yinvpow[nprime].bytes, challenge.bytes); + hadamard_fold(Gprime, challenge_inv, temp); + hadamard_fold(Hprime, challenge, challenge_inv); + + sc_mul(temp.bytes, challenge_inv.bytes, y_powers[nprime].bytes); + aprime = vector_add(vector_scalar(slice(aprime, 0, nprime), challenge), vector_scalar(slice(aprime, nprime, aprime.size()), temp)); + bprime = vector_add(vector_scalar(slice(bprime, 0, nprime), challenge_inv), vector_scalar(slice(bprime, nprime, bprime.size()), challenge)); + + rct::key challenge_squared; + sc_mul(challenge_squared.bytes, challenge.bytes, challenge.bytes); + rct::key challenge_squared_inv; + sc_mul(challenge_squared_inv.bytes, challenge_inv.bytes, challenge_inv.bytes); + sc_muladd(alpha1.bytes, dL.bytes, challenge_squared.bytes, alpha1.bytes); + sc_muladd(alpha1.bytes, dR.bytes, challenge_squared_inv.bytes, alpha1.bytes); + + ++round; + } + + // Final round computations + rct::key r = rct::skGen(); + rct::key s = rct::skGen(); + rct::key d_ = rct::skGen(); + rct::key eta = rct::skGen(); + + std::vector A1_data; + A1_data.reserve(4); + A1_data.resize(4); + + sc_mul(A1_data[0].scalar.bytes, r.bytes, rct::INV_EIGHT.bytes); + A1_data[0].point = Gprime[0]; + + sc_mul(A1_data[1].scalar.bytes, s.bytes, rct::INV_EIGHT.bytes); + A1_data[1].point = Hprime[0]; + + sc_mul(A1_data[2].scalar.bytes, d_.bytes, rct::INV_EIGHT.bytes); + ge_p3 G_p3; + ge_frombytes_vartime(&G_p3, rct::G.bytes); + A1_data[2].point = G_p3; + + sc_mul(temp.bytes, r.bytes, y.bytes); + sc_mul(temp.bytes, temp.bytes, bprime[0].bytes); + sc_mul(temp2.bytes, s.bytes, y.bytes); + sc_mul(temp2.bytes, temp2.bytes, aprime[0].bytes); + sc_add(temp.bytes, temp.bytes, temp2.bytes); + sc_mul(A1_data[3].scalar.bytes, temp.bytes, rct::INV_EIGHT.bytes); + ge_p3 H_p3; + ge_frombytes_vartime(&H_p3, rct::H.bytes); + A1_data[3].point = H_p3; + + rct::key A1 = multiexp(A1_data, 0); + + sc_mul(temp.bytes, r.bytes, y.bytes); + sc_mul(temp.bytes, temp.bytes, s.bytes); + sc_mul(temp.bytes, temp.bytes, rct::INV_EIGHT.bytes); + sc_mul(temp2.bytes, eta.bytes, rct::INV_EIGHT.bytes); + rct::key B; + rct::addKeys2(B, temp2, temp, rct::H); + + rct::key e = transcript_update(transcript, A1, B); + if (e == rct::zero()) + { + MINFO("e is 0, trying again"); + goto try_again; + } + rct::key e_squared; + sc_mul(e_squared.bytes, e.bytes, e.bytes); + + rct::key r1; + sc_muladd(r1.bytes, aprime[0].bytes, e.bytes, r.bytes); + + rct::key s1; + sc_muladd(s1.bytes, bprime[0].bytes, e.bytes, s.bytes); + + rct::key d1; + sc_muladd(d1.bytes, d_.bytes, e.bytes, eta.bytes); + sc_muladd(d1.bytes, alpha1.bytes, e_squared.bytes, d1.bytes); + + return BulletproofPlus2{std::move(V), A, A1, B, r1, s1, d1, std::move(L), std::move(R)}; + } + + BulletproofPlus2 bulletproof_plus2_PROVE(const std::vector &v, const rct::keyV &gamma) + { + CHECK_AND_ASSERT_THROW_MES(v.size() == gamma.size(), "Incompatible sizes of v and gamma"); + + // vG + gammaH + rct::keyV sv(v.size()); + for (size_t i = 0; i < v.size(); ++i) + { + sv[i] = rct::d2h(v[i]); + } + return bulletproof_plus2_PROVE(sv, gamma); + } + + struct bp_plus2_proof_data_t + { + rct::key y, z, e; + std::vector challenges; + size_t logM, inv_offset; + }; + + // Given a batch of range proofs, determine if they are all valid + bool try_get_bulletproof_plus2_verification_data(const std::vector &proofs, + std::list &prep_data_out) + { + init_exponents(); + + const size_t logN = 6; + const size_t N = 1 << logN; + + // Set up + size_t max_length = 0; // size of each of the longest proof's inner-product vectors + size_t inv_offset = 0; + + std::vector proof_data; + proof_data.reserve(proofs.size()); + + // We'll perform only a single batch inversion across all proofs in the batch, + // since batch inversion requires only one scalar inversion operation. + std::vector to_invert; + to_invert.reserve(14 * proofs.size()); // maximal size, given the aggregation limit + + for (const BulletproofPlus2 *p: proofs) + { + const BulletproofPlus2 &proof = *p; + + // Sanity checks + CHECK_AND_ASSERT_MES(is_reduced(proof.r1), false, "Input scalar not in range"); + CHECK_AND_ASSERT_MES(is_reduced(proof.s1), false, "Input scalar not in range"); + CHECK_AND_ASSERT_MES(is_reduced(proof.d1), false, "Input scalar not in range"); + + CHECK_AND_ASSERT_MES(proof.V.size() >= 1, false, "V does not have at least one element"); + CHECK_AND_ASSERT_MES(proof.L.size() == proof.R.size(), false, "Mismatched L and R sizes"); + CHECK_AND_ASSERT_MES(proof.L.size() > 0, false, "Empty proof"); + + max_length = std::max(max_length, proof.L.size()); + + proof_data.push_back({}); + bp_plus2_proof_data_t &pd = proof_data.back(); + + // Reconstruct the challenges + rct::key transcript = copy(initial_transcript); + transcript_update(transcript, proof.V); + pd.y = transcript_update(transcript, proof.A); + CHECK_AND_ASSERT_MES(!(pd.y == rct::zero()), false, "y == 0"); + pd.z = transcript_update(transcript); + CHECK_AND_ASSERT_MES(!(pd.z == rct::zero()), false, "z == 0"); + + // Determine the number of inner-product rounds based on proof size + size_t M; + for (pd.logM = 0; (M = 1< 0, false, "Zero rounds"); + + // The inner-product challenges are computed per round + pd.challenges.resize(rounds); + for (size_t j = 0; j < rounds; ++j) + { + pd.challenges[j] = transcript_update(transcript, proof.L[j], proof.R[j]); + CHECK_AND_ASSERT_MES(!(pd.challenges[j] == rct::zero()), false, "challenges[j] == 0"); + } + + // Final challenge + pd.e = transcript_update(transcript,proof.A1,proof.B); + CHECK_AND_ASSERT_MES(!(pd.e == rct::zero()), false, "e == 0"); + + // Batch scalar inversions + pd.inv_offset = inv_offset; + for (size_t j = 0; j < rounds; ++j) + to_invert.push_back(pd.challenges[j]); + to_invert.push_back(pd.y); + inv_offset += rounds + 1; + } + CHECK_AND_ASSERT_MES(max_length < 32, false, "At least one proof is too large"); + + rct::key temp; + rct::key temp2; + + // Final batch proof data + prep_data_out.clear(); + + const std::vector inverses = invert(std::move(to_invert)); + to_invert.clear(); + + // Weights and aggregates + // + // The idea is to take the single multiscalar multiplication used in the verification + // of each proof in the batch and weight it using a random weighting factor, resulting + // in just one multiscalar multiplication check to zero for the entire batch. + // We can further simplify the verifier complexity by including common group elements + // only once in this single multiscalar multiplication. + // Common group elements' weighted scalar sums are tracked across proofs for this reason. + // + // To build a multiscalar multiplication for each proof, we use the method described in + // Section 6.1 of the preprint. Note that the result given there does not account for + // the construction of the inner-product inputs that are produced in the range proof + // verifier algorithm; we have done so here. + int proof_data_index = 0; + rct::keyV challenges_cache; + std::vector proof8_V, proof8_L, proof8_R; + + + // Process each proof and add to the weighted batch + for (const BulletproofPlus2 *p: proofs) + { + const BulletproofPlus2 &proof = *p; + const bp_plus2_proof_data_t &pd = proof_data[proof_data_index++]; + + CHECK_AND_ASSERT_MES(proof.L.size() == 6+pd.logM, false, "Proof is not the expected size"); + const size_t M = 1 << pd.logM; + const size_t MN = M*N; + + // Random weighting factor must be nonzero, which is exceptionally unlikely! + rct::key weight = ZERO; + while (weight == ZERO) + { + weight = rct::skGen(); + } + + // note: set the multiexp builder's weight to 1 and manually weight proof elements for efficiency + prep_data_out.emplace_back(rct::identity(), 2 * MN, proof.V.size() + 2 * (pd.logM + logN) + 3); + SpMultiexpBuilder &data_builder = prep_data_out.back(); + + // Rescale previously offset proof elements + // + // This ensures that all such group elements are in the prime-order subgroup. + proof8_V.resize(proof.V.size()); for (size_t i = 0; i < proof.V.size(); ++i) rct::scalarmult8(proof8_V[i], proof.V[i]); + proof8_L.resize(proof.L.size()); for (size_t i = 0; i < proof.L.size(); ++i) rct::scalarmult8(proof8_L[i], proof.L[i]); + proof8_R.resize(proof.R.size()); for (size_t i = 0; i < proof.R.size(); ++i) rct::scalarmult8(proof8_R[i], proof.R[i]); + ge_p3 proof8_A1; + ge_p3 proof8_B; + ge_p3 proof8_A; + rct::scalarmult8(proof8_A1, proof.A1); + rct::scalarmult8(proof8_B, proof.B); + rct::scalarmult8(proof8_A, proof.A); + + // Compute necessary powers of the y-challenge + rct::key y_MN = copy(pd.y); + rct::key y_MN_1; + size_t temp_MN = MN; + while (temp_MN > 1) + { + sc_mul(y_MN.bytes, y_MN.bytes, y_MN.bytes); + temp_MN /= 2; + } + sc_mul(y_MN_1.bytes, y_MN.bytes, pd.y.bytes); + + // V_j: -e**2 * z**(2*j+1) * y**(MN+1) * weight + rct::key e_squared; + sc_mul(e_squared.bytes, pd.e.bytes, pd.e.bytes); + + rct::key z_squared; + sc_mul(z_squared.bytes, pd.z.bytes, pd.z.bytes); + + sc_sub(temp.bytes, ZERO.bytes, e_squared.bytes); + sc_mul(temp.bytes, temp.bytes, y_MN_1.bytes); + sc_mul(temp.bytes, temp.bytes, weight.bytes); + for (size_t j = 0; j < proof8_V.size(); j++) + { + sc_mul(temp.bytes, temp.bytes, z_squared.bytes); + data_builder.add_element(temp, proof8_V[j]); + } + + // B: -weight + sc_mul(temp.bytes, MINUS_ONE.bytes, weight.bytes); + data_builder.add_element(temp, proof8_B); + + // A1: -weight*e + sc_mul(temp.bytes, temp.bytes, pd.e.bytes); + data_builder.add_element(temp, proof8_A1); + + // A: -weight*e*e + rct::key minus_weight_e_squared; + sc_mul(minus_weight_e_squared.bytes, temp.bytes, pd.e.bytes); + data_builder.add_element(minus_weight_e_squared, proof8_A); + + // G: weight*d1 + sc_mul(temp.bytes, weight.bytes, proof.d1.bytes); + data_builder.add_G_element(temp); + + // Windowed vector + // d[j*N+i] = z**(2*(j+1)) * 2**i + rct::keyV d(MN, rct::zero()); + d[0] = z_squared; + for (size_t i = 1; i < N; i++) + { + sc_add(d[i].bytes, d[i-1].bytes, d[i-1].bytes); + } + + for (size_t j = 1; j < M; j++) + { + for (size_t i = 0; i < N; i++) + { + sc_mul(d[j*N+i].bytes, d[(j-1)*N+i].bytes, z_squared.bytes); + } + } + + // More efficient computation of sum(d) + rct::key sum_d; + sc_mul(sum_d.bytes, TWO_SIXTY_FOUR_MINUS_ONE.bytes, sum_of_even_powers(pd.z, 2*M).bytes); + + // H: weight*( r1*y*s1 + e**2*( y**(MN+1)*z*sum(d) + (z**2-z)*sum(y) ) ) + rct::key sum_y = sum_of_scalar_powers(pd.y, MN); + sc_sub(temp.bytes, z_squared.bytes, pd.z.bytes); + sc_mul(temp.bytes, temp.bytes, sum_y.bytes); + + sc_mul(temp2.bytes, y_MN_1.bytes, pd.z.bytes); + sc_mul(temp2.bytes, temp2.bytes, sum_d.bytes); + sc_add(temp.bytes, temp.bytes, temp2.bytes); + sc_mul(temp.bytes, temp.bytes, e_squared.bytes); + sc_mul(temp2.bytes, proof.r1.bytes, pd.y.bytes); + sc_mul(temp2.bytes, temp2.bytes, proof.s1.bytes); + sc_add(temp.bytes, temp.bytes, temp2.bytes); + sc_mul(temp.bytes, temp.bytes, weight.bytes); + data_builder.add_H_element(temp); + + // Compute the number of rounds for the inner-product argument + const size_t rounds = pd.logM+logN; + CHECK_AND_ASSERT_MES(rounds > 0, false, "Zero rounds"); + + const rct::key *challenges_inv = &inverses[pd.inv_offset]; + const rct::key yinv = inverses[pd.inv_offset + rounds]; + + // Compute challenge products + challenges_cache.resize(1< 0; --s) + { + sc_mul(challenges_cache[s].bytes, challenges_cache[s/2].bytes, pd.challenges[j].bytes); + sc_mul(challenges_cache[s-1].bytes, challenges_cache[s/2].bytes, challenges_inv[j].bytes); + } + } + + // Gi and Hi + rct::key e_r1_w_y; + sc_mul(e_r1_w_y.bytes, pd.e.bytes, proof.r1.bytes); + sc_mul(e_r1_w_y.bytes, e_r1_w_y.bytes, weight.bytes); + rct::key e_s1_w; + sc_mul(e_s1_w.bytes, pd.e.bytes, proof.s1.bytes); + sc_mul(e_s1_w.bytes, e_s1_w.bytes, weight.bytes); + rct::key e_squared_z_w; + sc_mul(e_squared_z_w.bytes, e_squared.bytes, pd.z.bytes); + sc_mul(e_squared_z_w.bytes, e_squared_z_w.bytes, weight.bytes); + rct::key minus_e_squared_z_w; + sc_sub(minus_e_squared_z_w.bytes, ZERO.bytes, e_squared_z_w.bytes); + rct::key minus_e_squared_w_y; + sc_sub(minus_e_squared_w_y.bytes, ZERO.bytes, e_squared.bytes); + sc_mul(minus_e_squared_w_y.bytes, minus_e_squared_w_y.bytes, weight.bytes); + sc_mul(minus_e_squared_w_y.bytes, minus_e_squared_w_y.bytes, y_MN.bytes); + for (size_t i = 0; i < MN; ++i) + { + rct::key g_scalar = copy(e_r1_w_y); + rct::key h_scalar; + + // Use the binary decomposition of the index + sc_muladd(g_scalar.bytes, g_scalar.bytes, challenges_cache[i].bytes, e_squared_z_w.bytes); + sc_muladd(h_scalar.bytes, e_s1_w.bytes, challenges_cache[(~i) & (MN-1)].bytes, minus_e_squared_z_w.bytes); + + // Complete the scalar derivation + data_builder.add_element_at_generator_index(g_scalar, i * 2); + sc_muladd(h_scalar.bytes, minus_e_squared_w_y.bytes, d[i].bytes, h_scalar.bytes); + data_builder.add_element_at_generator_index(h_scalar, i * 2 + 1); + + // Update iterated values + sc_mul(e_r1_w_y.bytes, e_r1_w_y.bytes, yinv.bytes); + sc_mul(minus_e_squared_w_y.bytes, minus_e_squared_w_y.bytes, yinv.bytes); + } + + // L_j: -weight*e*e*challenges[j]**2 + // R_j: -weight*e*e*challenges[j]**(-2) + for (size_t j = 0; j < rounds; ++j) + { + sc_mul(temp.bytes, pd.challenges[j].bytes, pd.challenges[j].bytes); + sc_mul(temp.bytes, temp.bytes, minus_weight_e_squared.bytes); + data_builder.add_element(temp, proof8_L[j]); + + sc_mul(temp.bytes, challenges_inv[j].bytes, challenges_inv[j].bytes); + sc_mul(temp.bytes, temp.bytes, minus_weight_e_squared.bytes); + data_builder.add_element(temp, proof8_R[j]); + } + } + + return true; + } + + bool bulletproof_plus2_VERIFY(const std::vector &proofs) + { + // build multiexp + std::list prep_data; + if (!try_get_bulletproof_plus2_verification_data(proofs, prep_data)) + return false;; + + // verify all elements sum to zero + if (!SpMultiexp{prep_data}.evaluates_to_point_at_infinity()) + { + MERROR("Verification failure"); + return false; + } + + return true; + } + + bool bulletproof_plus2_VERIFY(const std::vector &proofs) + { + std::vector proof_pointers; + proof_pointers.reserve(proofs.size()); + for (const BulletproofPlus2 &proof: proofs) + proof_pointers.push_back(&proof); + return bulletproof_plus2_VERIFY(proof_pointers); + } + + bool bulletproof_plus2_VERIFY(const BulletproofPlus2 &proof) + { + return bulletproof_plus2_VERIFY({&proof}); + } +} //namespace sp diff --git a/src/seraphis_crypto/bulletproofs_plus2.h b/src/seraphis_crypto/bulletproofs_plus2.h new file mode 100644 index 0000000000..58ee5a9d19 --- /dev/null +++ b/src/seraphis_crypto/bulletproofs_plus2.h @@ -0,0 +1,67 @@ +// Copyright (c) 2017-2022, 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. + +// Adapted from ringct/bulletproofs_plus.h/.cpp to use the seraphis generator factory and transcript utilities. + +#pragma once + +//local headers +#include "ringct/rctTypes.h" +#include "sp_multiexp.h" + +//third party headers + +//standard headers +#include +#include + +//forward declarations + + +namespace sp +{ + +struct BulletproofPlus2 +{ + rct::keyV V; + rct::key A, A1, B; + rct::key r1, s1, d1; + rct::keyV L, R; +}; + +BulletproofPlus2 bulletproof_plus2_PROVE(const rct::key &v, const rct::key &gamma); +BulletproofPlus2 bulletproof_plus2_PROVE(uint64_t v, const rct::key &gamma); +BulletproofPlus2 bulletproof_plus2_PROVE(const rct::keyV &v, const rct::keyV &gamma); +BulletproofPlus2 bulletproof_plus2_PROVE(const std::vector &v, const rct::keyV &gamma); +bool try_get_bulletproof_plus2_verification_data(const std::vector &proofs, + std::list &prep_data_out); +bool bulletproof_plus2_VERIFY(const BulletproofPlus2 &proof); +bool bulletproof_plus2_VERIFY(const std::vector &proofs); +bool bulletproof_plus2_VERIFY(const std::vector &proofs); + +} //namespace sp diff --git a/src/seraphis_crypto/grootle.cpp b/src/seraphis_crypto/grootle.cpp new file mode 100644 index 0000000000..a8da51ff62 --- /dev/null +++ b/src/seraphis_crypto/grootle.cpp @@ -0,0 +1,668 @@ +// Copyright (c) 2022, 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 "grootle.h" + +//local headers +#include "common/varint.h" +#include "crypto/crypto.h" +extern "C" +{ +#include "crypto/crypto-ops.h" +} +#include "crypto/generators.h" +#include "cryptonote_config.h" +#include "misc_log_ex.h" +#include "ringct/multiexp.h" +#include "ringct/rctOps.h" +#include "ringct/rctTypes.h" +#include "sp_crypto_utils.h" +#include "sp_multiexp.h" +#include "sp_generator_factory.h" +#include "sp_hash_functions.h" +#include "sp_transcript.h" + +//third party headers + +//standard headers +#include +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "grootle" + +namespace sp +{ + +// Useful scalar and group constants +static const rct::key ZERO = rct::zero(); +static const rct::key ONE = rct::identity(); +static const rct::key IDENTITY = rct::identity(); +static const rct::key TWO = { {0x02, 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} }; +static const rct::key MINUS_ONE = { {0xec, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, + 0xde, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10} }; + +//------------------------------------------------------------------------------------------------------------------- +// commit to 2 matrices of equal size +// C = x G + {M_A}->Hi_A + {M_B}->Hi_B +// - mapping strategy: concatenate each 'row', e.g. {{1,2}, {3,4}} -> {1,2,3,4}; there are 'm' rows each of size 'n' +// - the generator vectors 'Hi_A' and 'Hi_B' are selected alternating from the generator factory +//------------------------------------------------------------------------------------------------------------------- +static void grootle_matrix_commitment(const rct::key &x, //blinding factor + const rct::keyM &M_priv_A, //matrix A + const rct::keyM &M_priv_B, //matrix B + std::vector &data_out) +{ + const std::size_t m{M_priv_A.size()}; + CHECK_AND_ASSERT_THROW_MES(m > 0, "grootle proof matrix commitment: bad matrix size!"); + CHECK_AND_ASSERT_THROW_MES(m == M_priv_B.size(), "grootle proof matrix commitment: matrix size mismatch (m)!"); + const std::size_t n{M_priv_A[0].size()}; + CHECK_AND_ASSERT_THROW_MES(m*n <= GROOTLE_MAX_MN, "grootle proof matrix commitment: bad matrix commitment parameters!"); + + data_out.resize(1 + 2*m*n); + std::size_t offset; + + // mask: x G + offset = 0; + data_out[offset + 0] = {x, crypto::get_G_p3()}; + + // map M_A onto Hi_A + offset += 1; + for (std::size_t j = 0; j < m; ++j) + { + CHECK_AND_ASSERT_THROW_MES(n == M_priv_A[j].size(), "grootle proof matrix commitment: matrix size mismatch (n)!"); + + for (std::size_t i = 0; i < n; ++i) + data_out[offset + j*n + i] = {M_priv_A[j][i], generator_factory::get_generator_at_index_p3(2*(j*n + i))}; + } + + // map M_B onto Hi_B + offset += m*n; + for (std::size_t j = 0; j < m; ++j) + { + CHECK_AND_ASSERT_THROW_MES(n == M_priv_B[j].size(), "grootle proof matrix commitment: matrix size mismatch (n)!"); + + for (std::size_t i = 0; i < n; ++i) + data_out[offset + j*n + i] = {M_priv_B[j][i], generator_factory::get_generator_at_index_p3(2*(j*n + i) + 1)}; + } +} +//------------------------------------------------------------------------------------------------------------------- +// Fiat-Shamir challenge +// c = H_n(message, n, m, {S}, C_offset, A, B, {X}) +// +// note: c == xi +//------------------------------------------------------------------------------------------------------------------- +static rct::key compute_challenge(const rct::key &message, + const std::size_t n, + const std::size_t m, + const rct::keyV &S, + const rct::key &C_offset, + const rct::key &A, + const rct::key &B, + const rct::keyV &X) +{ + // hash data + SpFSTranscript transcript{config::HASH_KEY_GROOTLE_CHALLENGE, 2*4 + (S.size() + X.size() + 4)*sizeof(rct::key)}; + transcript.append("message", message); + transcript.append("n", n); + transcript.append("m", m); + transcript.append("S", S); + transcript.append("C_offset", C_offset); + transcript.append("A", A); + transcript.append("B", B); + transcript.append("X", X); + + // challenge + rct::key challenge; + sp_hash_to_scalar(transcript.data(), transcript.size(), challenge.bytes); + CHECK_AND_ASSERT_THROW_MES(!(challenge == ZERO), "grootle proof challenge: transcript challenge must be nonzero!"); + + return challenge; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void build_verification_multiexps_for_proof(const GrootleProof &proof, + const rct::key &message, + const rct::keyV &S, + const rct::key &proof_offset, + const std::size_t n, + const std::size_t m, + const rct::key &weight1, + const rct::key &weight2, + SpMultiexpBuilder &builder1_inout, + SpMultiexpBuilder &builder2_inout) +{ + CHECK_AND_ASSERT_THROW_MES(!(weight1 == rct::zero()), "grootle proof: invalid weigh1!"); + CHECK_AND_ASSERT_THROW_MES(!(weight2 == rct::zero()), "grootle proof: invalid weigh2!"); + + // builer 1: A + xi*B == dual_matrix_commit(zA, f, f*(xi - f)) + // per-index storage: + // 0 G (zA*G) + // 1 .. 2*m*n alternate(Hi_A[i], Hi_B[i]) {f, f*(xi - f)} + // ... other proof data: A, B + + // builer 2: sum_k( t_k*(S[k] - C_offset) ) - sum_j( xi^j*X[j] ) - z G == 0 + // per-index storage (builder 2): + // 0 G (z*G) + // 1 S[0] + 1 (f-coefficients) + // ... + // (N-1) + 1 S[N-1] + 1 + // ... other proof data: C_offset, {X} + const std::size_t N = std::pow(n, m); + rct::key temp; + + // Transcript challenge + const rct::key xi{compute_challenge(message, n, m, S, proof_offset, proof.A, proof.B, proof.X)}; + + // Challenge powers (negated) + const rct::keyV minus_xi_pow{powers_of_scalar(xi, m, true)}; + + // Recover proof elements + ge_p3 A_p3; + ge_p3 B_p3; + std::vector X_p3; + X_p3.resize(m); + + scalarmult8(A_p3, proof.A); + scalarmult8(B_p3, proof.B); + for (std::size_t j = 0; j < m; ++j) + { + scalarmult8(X_p3[j], proof.X[j]); + } + + // Reconstruct the f-matrix + rct::keyM f = rct::keyMInit(n, m); + for (std::size_t j = 0; j < m; ++j) + { + // f[j][0] = xi - sum(f[j][i]) [from i = [1, n)] + f[j][0] = xi; + + for (std::size_t i = 1; i < n; ++i) + { + // note: indexing between f-matrix and proof.f is off by 1 because + // 'f[j][0] = xi - sum(f_{j,i})' is only implied by the proof, not recorded in it + f[j][i] = proof.f[j][i - 1]; + sc_sub(f[j][0].bytes, f[j][0].bytes, f[j][i].bytes); + } + CHECK_AND_ASSERT_THROW_MES(!(f[j][0] == ZERO), + "grootle proof verifying: proof matrix element should not be zero!"); + } + + // Signing index matrix commitments sub-proof + // weight1 * [ A + xi*B == dual_matrix_commit(zA, f, f*(xi - f)) ] + // weight1 * [ == zA * G + ... f[j][i] * Hi_A[j][i] ... + ... f[j][i] * (xi - f[j][i]) * Hi_B[j][i] ... ] + // G: weight1 * zA + sc_mul(temp.bytes, weight1.bytes, proof.zA.bytes); + builder1_inout.add_G_element(temp); + + // weight1 * [ ... f[j][i] * Hi_A[j][i] ... + ... f[j][i] * (xi - f[j][i]) * Hi_B[j][i] ... ] + rct::key w1_f_temp; + + for (std::size_t j = 0; j < m; ++j) + { + for (std::size_t i = 0; i < n; ++i) + { + // weight1 * f[j][i] + sc_mul(w1_f_temp.bytes, weight1.bytes, f[j][i].bytes); + + // Hi_A: weight1 * f[j][i] + builder1_inout.add_element_at_generator_index(w1_f_temp, 2*(j*n + i)); + + // Hi_B: weight1 * f[j][i]*(xi - f[j][i]) + sc_sub(temp.bytes, xi.bytes, f[j][i].bytes); //xi - f[j][i] + sc_mul(temp.bytes, w1_f_temp.bytes, temp.bytes); //weight1 * f[j][i]*(xi - f[j][i]) + builder1_inout.add_element_at_generator_index(temp, 2*(j*n + i) + 1); + } + } + + // A, B + // equality test: + // weight1 * [ dual_matrix_commit(zA, f, f*(xi - f)) - (A + xi*B) == 0 ] + // A: weight1 * -A + // B: weight1 * -xi * B + rct::key w1_MINUS_ONE; + sc_mul(w1_MINUS_ONE.bytes, weight1.bytes, MINUS_ONE.bytes); + builder1_inout.add_element(w1_MINUS_ONE, A_p3); //weight1 * -A + + sc_mul(temp.bytes, w1_MINUS_ONE.bytes, xi.bytes); + builder1_inout.add_element(temp, B_p3); //weight1 * -xi * B + + // One-of-many sub-proof + // t_k = mul_all_j(f[j][decomp_k[j]]) + // weight2 * [ sum_k( t_k*(S[k] - C_offset) ) - sum_j( xi^j*X[j] ) - z G == 0 ] + // + // {S} + // weight2 * [ sum_k( t_k*S[k] ) - sum_k( t_k )*C_offset - [ sum(...) + z G ] == 0 ] + // S[k]: weight2 * t_k + std::vector decomp_k; + decomp_k.resize(m); + rct::key w2_sum_t = ZERO; + rct::key w2_t_k; + for (std::size_t k = 0; k < N; ++k) + { + w2_t_k = weight2; + decompose(k, n, m, decomp_k); + + for (std::size_t j = 0; j < m; ++j) + { + sc_mul(w2_t_k.bytes, w2_t_k.bytes, f[j][decomp_k[j]].bytes); //weight2 * mul_all_j(f[j][decomp_k[j]]) + } + + sc_add(w2_sum_t.bytes, w2_sum_t.bytes, w2_t_k.bytes); //weight2 * sum_k( t_k ) + builder2_inout.add_element(w2_t_k, S[k]); //weight2 * t_k*S[k] + } + + // C_offset + // weight2 * [ ... - sum_k( t_k )*C_offset ... ] + // + // proof_offset: weight2 * -sum_t + sc_mul(temp.bytes, MINUS_ONE.bytes, w2_sum_t.bytes); //weight2 * -sum_t + builder2_inout.add_element(temp, proof_offset); + + // {X} + // weight2 * [ ... - sum_j( xi^j*X[j] ) - z G == 0 ] + for (std::size_t j = 0; j < m; ++j) + { + // weight2 * -xi^j + sc_mul(temp.bytes, weight2.bytes, minus_xi_pow[j].bytes); + + // X[j]: weight2 * -xi^j + builder2_inout.add_element(temp, X_p3[j]); + } + + // G + // weight2 * [ ... - z G == 0 ] + // G: weight2 * -z + sc_mul(temp.bytes, weight2.bytes, MINUS_ONE.bytes); + sc_mul(temp.bytes, temp.bytes, proof.z.bytes); + builder2_inout.add_G_element(temp); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +std::size_t grootle_size_bytes(const std::size_t n, const std::size_t m) +{ + return 32 * (m + m*(n-1) + 4); // X + f + {A, B, zA, z} +} +//------------------------------------------------------------------------------------------------------------------- +std::size_t grootle_size_bytes(const GrootleProof &proof) +{ + const std::size_t n{proof.f.size() ? proof.f[0].size() : 0}; + const std::size_t m{proof.X.size()}; + + return grootle_size_bytes(n, m); +} +//------------------------------------------------------------------------------------------------------------------- +void append_to_transcript(const GrootleProof &container, SpTranscriptBuilder &transcript_inout) +{ + transcript_inout.append("A", container.A); + transcript_inout.append("B", container.B); + transcript_inout.append("f", container.f); + transcript_inout.append("X", container.X); + transcript_inout.append("zA", container.zA); + transcript_inout.append("z", container.z); +} +//------------------------------------------------------------------------------------------------------------------- +void make_grootle_proof(const rct::key &message, // message to insert in Fiat-Shamir transform hash + const rct::keyV &S, // referenced commitments + const std::size_t l, // secret index into {S} + const rct::key &C_offset, // offset for commitment to zero at index l + const crypto::secret_key &privkey, // privkey of commitment to zero 'S[l] - C_offset' + const std::size_t n, // decomposition of the reference set size: n^m + const std::size_t m, + GrootleProof &proof_out) +{ + /// input checks and initialization + CHECK_AND_ASSERT_THROW_MES(n > 1, "grootle proof proving: must have n > 1!"); + CHECK_AND_ASSERT_THROW_MES(m > 1, "grootle proof proving: must have m > 1!"); + CHECK_AND_ASSERT_THROW_MES(m*n <= GROOTLE_MAX_MN, "grootle proof proving: size parameters are too large!"); + + // ref set size + const std::size_t N = std::pow(n, m); + + CHECK_AND_ASSERT_THROW_MES(S.size() == N, "grootle proof proving: commitment column is wrong size!"); + + // commitment to zero signing key position + CHECK_AND_ASSERT_THROW_MES(l < N, "grootle proof proving: signing index out of bounds!"); + + // verify: commitment to zero C_zero = S[l] - C_offset = privkey*G + rct::key C_zero_reproduced; + rct::subKeys(C_zero_reproduced, S[l], C_offset); + CHECK_AND_ASSERT_THROW_MES(rct::scalarmultBase(rct::sk2rct(privkey)) == C_zero_reproduced, + "grootle proof proving: bad signing private key!"); + + + /// Grootle proof + GrootleProof proof; + + + /// Decomposition sub-proof commitments: A, B + std::vector data; + + // Matrix masks + rct::key rA{rct::skGen()}; + rct::key rB{rct::skGen()}; + + // A: commit to zero-sum values: {a, -a^2} + rct::keyM a = rct::keyMInit(n, m); + rct::keyM a_sq = a; + for (std::size_t j = 0; j < m; ++j) + { + a[j][0] = ZERO; + for (std::size_t i = 1; i < n; ++i) + { + // a + a[j][i] = rct::skGen(); + sc_sub(a[j][0].bytes, a[j][0].bytes, a[j][i].bytes); //a[j][0] = - sum(a[1,..,n]) + + // -a^2 + sc_mul(a_sq[j][i].bytes, a[j][i].bytes, a[j][i].bytes); + sc_mul(a_sq[j][i].bytes, MINUS_ONE.bytes, a_sq[j][i].bytes); + } + + // -(a[j][0])^2 + sc_mul(a_sq[j][0].bytes, a[j][0].bytes, a[j][0].bytes); + sc_mul(a_sq[j][0].bytes, MINUS_ONE.bytes, a_sq[j][0].bytes); + } + grootle_matrix_commitment(rA, a, a_sq, data); //A = dual_matrix_commit(r_A, a, -a^2) + CHECK_AND_ASSERT_THROW_MES(data.size() == 1 + 2*m*n, + "grootle proof proving: matrix commitment returned unexpected size (A data)!"); + proof.A = rct::straus(data); + CHECK_AND_ASSERT_THROW_MES(!(proof.A == IDENTITY), + "grootle proof proving: linear combination unexpectedly returned zero (A)!"); + + // B: commit to decomposition bits: {sigma, a*(1-2*sigma)} + std::vector decomp_l; + decomp_l.resize(m); + decompose(l, n, m, decomp_l); + + rct::keyM sigma = rct::keyMInit(n, m); + rct::keyM a_sigma = sigma; + for (std::size_t j = 0; j < m; ++j) + { + for (std::size_t i = 0; i < n; ++i) + { + // sigma + sigma[j][i] = kronecker_delta(decomp_l[j], i); + + // a*(1-2*sigma) + sc_mulsub(a_sigma[j][i].bytes, TWO.bytes, sigma[j][i].bytes, ONE.bytes); //1-2*sigma + sc_mul(a_sigma[j][i].bytes, a_sigma[j][i].bytes, a[j][i].bytes); //a*(1-2*sigma) + } + } + grootle_matrix_commitment(rB, sigma, a_sigma, data); //B = dual_matrix_commit(r_B, sigma, a*(1-2*sigma)) + CHECK_AND_ASSERT_THROW_MES(data.size() == 1 + 2*m*n, + "grootle proof proving: matrix commitment returned unexpected size (B data)!"); + proof.B = rct::straus(data); + CHECK_AND_ASSERT_THROW_MES(!(proof.B == IDENTITY), + "grootle proof proving: linear combination unexpectedly returned zero (B)!"); + + // done: store (1/8)*commitment + proof.A = rct::scalarmultKey(proof.A, rct::INV_EIGHT); + proof.B = rct::scalarmultKey(proof.B, rct::INV_EIGHT); + + + /// one-of-many sub-proof: polynomial coefficients 'p' + rct::keyM p = rct::keyMInit(m + 1, N); + CHECK_AND_ASSERT_THROW_MES(p.size() == N, "grootle proof proving: bad matrix size (p)!"); + CHECK_AND_ASSERT_THROW_MES(p[0].size() == m + 1, "grootle proof proving: bad matrix size (p[])!"); + std::vector decomp_k; + rct::keyV pre_convolve_temp; + decomp_k.resize(m); + pre_convolve_temp.resize(2); + for (std::size_t k = 0; k < N; ++k) + { + decompose(k, n, m, decomp_k); + + for (std::size_t j = 0; j < m+1; ++j) + { + p[k][j] = ZERO; + } + p[k][0] = a[0][decomp_k[0]]; + p[k][1] = kronecker_delta(decomp_l[0], decomp_k[0]); + + for (std::size_t j = 1; j < m; ++j) + { + pre_convolve_temp[0] = a[j][decomp_k[j]]; + pre_convolve_temp[1] = kronecker_delta(decomp_l[j], decomp_k[j]); + + p[k] = convolve(p[k], pre_convolve_temp, m); + } + } + + + /// one-of-many sub-proof initial values: {rho}, {X} + + // {rho}: proof entropy + rct::keyV rho; + rho.reserve(m); + for (std::size_t j = 0; j < m; ++j) + { + rho.push_back(rct::skGen()); + } + + // {X}: 'encodings' of [p] (i.e. of the real signing index 'l' in the referenced tuple set) + proof.X = rct::keyV(m); + rct::key C_zero_nominal_temp; + for (std::size_t j = 0; j < m; ++j) + { + std::vector data_X; + data_X.reserve(N); + + for (std::size_t k = 0; k < N; ++k) + { + // X[j] += p[k][j] * (S[k] - C_offset) + rct::subKeys(C_zero_nominal_temp, S[k], C_offset); // S[k] - C_offset + data_X.push_back({p[k][j], C_zero_nominal_temp}); + } + + // X[j] += rho[j]*G + // note: addKeys1(X, rho, P) -> X = rho*G + P + rct::addKeys1(proof.X[j], rho[j], rct::straus(data_X)); + CHECK_AND_ASSERT_THROW_MES(!(proof.X[j] == IDENTITY), + "grootle proof proving: proof coefficient element should not be zero!"); + } + + // done: store (1/8)*X + for (std::size_t j = 0; j < m; ++j) + { + rct::scalarmultKey(proof.X[j], proof.X[j], rct::INV_EIGHT); + } + CHECK_AND_ASSERT_THROW_MES(proof.X.size() == m, "grootle proof proving: proof coefficient vector is unexpected size!"); + + + /// one-of-many sub-proof challenges + + // xi: challenge + const rct::key xi{compute_challenge(message, n, m, S, C_offset, proof.A, proof.B, proof.X)}; + + // xi^j: challenge powers + const rct::keyV xi_pow{powers_of_scalar(xi, m + 1)}; + + + /// grootle proof final components/responses + + // f-matrix: encapsulate index 'l' + proof.f = rct::keyMInit(n - 1, m); + for (std::size_t j = 0; j < m; ++j) + { + for (std::size_t i = 1; i < n; ++i) + { + sc_muladd(proof.f[j][i - 1].bytes, sigma[j][i].bytes, xi.bytes, a[j][i].bytes); + CHECK_AND_ASSERT_THROW_MES(!(proof.f[j][i - 1] == ZERO), + "grootle proof proving: proof matrix element should not be zero!"); + } + } + + // z-terms: responses + // zA = xi*rB + rA + sc_muladd(proof.zA.bytes, xi.bytes, rB.bytes, rA.bytes); + CHECK_AND_ASSERT_THROW_MES(!(proof.zA == ZERO), "grootle proof proving: proof scalar element should not be zero (zA)!"); + + // z = privkey*xi^m - rho[0]*xi^0 - ... - rho[m - 1]*xi^(m - 1) + proof.z = ZERO; + sc_mul(proof.z.bytes, to_bytes(privkey), xi_pow[m].bytes); //z = privkey*xi^m + + for (std::size_t j = 0; j < m; ++j) + { + sc_mulsub(proof.z.bytes, rho[j].bytes, xi_pow[j].bytes, proof.z.bytes); //z -= rho[j]*xi^j + } + CHECK_AND_ASSERT_THROW_MES(!(proof.z == ZERO), "grootle proof proving: proof scalar element should not be zero (z)!"); + + + /// cleanup: clear secret prover data + memwipe(&rA, sizeof(rct::key)); + memwipe(&rB, sizeof(rct::key)); + for (std::size_t j = 0; j < m; ++j) + { + memwipe(a[j].data(), a[j].size()*sizeof(rct::key)); + } + memwipe(rho.data(), rho.size()*sizeof(rct::key)); + + + /// save result + proof_out = std::move(proof); +} +//------------------------------------------------------------------------------------------------------------------- +void get_grootle_verification_data(const std::vector &proofs, + const rct::keyV &messages, + const std::vector &M, + const rct::keyV &proof_offsets, + const std::size_t n, + const std::size_t m, + std::list &verification_data_out) +{ + /// Global checks + const std::size_t num_proofs = proofs.size(); + + CHECK_AND_ASSERT_THROW_MES(num_proofs > 0, "grootle proof verifying: must have at least one proof to verify!"); + + CHECK_AND_ASSERT_THROW_MES(n > 1, "grootle proof verifying: must have n > 1!"); + CHECK_AND_ASSERT_THROW_MES(m > 1, "grootle proof verifying: must have m > 1!"); + CHECK_AND_ASSERT_THROW_MES(m*n <= GROOTLE_MAX_MN, "grootle proof verifying: size parameters are too large!"); + + // reference set size + const std::size_t N = std::pow(n, m); + + CHECK_AND_ASSERT_THROW_MES(M.size() == num_proofs, + "grootle proof verifying: public key vectors don't line up with proofs!"); + for (const rct::keyV &proof_S : M) + { + CHECK_AND_ASSERT_THROW_MES(proof_S.size() == N, + "grootle proof verifying: public key vector for a proof is wrong size!"); + } + + // inputs line up with proofs + CHECK_AND_ASSERT_THROW_MES(messages.size() == num_proofs, "grootle proof verifying: incorrect number of messages!"); + CHECK_AND_ASSERT_THROW_MES(proof_offsets.size() == num_proofs, + "grootle proof verifying: commitment offsets don't line up with input proofs!"); + + + /// Per-proof checks + for (const GrootleProof *p: proofs) + { + CHECK_AND_ASSERT_THROW_MES(p, "grootle proof verifying: proof unexpectedly doesn't exist!"); + const GrootleProof &proof = *p; + + CHECK_AND_ASSERT_THROW_MES(proof.X.size() == m, "grootle proof verifying: bad proof vector size (X)!"); + CHECK_AND_ASSERT_THROW_MES(proof.f.size() == m, "grootle proof verifying: bad proof matrix size (f)!"); + for (std::size_t j = 0; j < m; ++j) + { + CHECK_AND_ASSERT_THROW_MES(proof.f[j].size() == n - 1, + "grootle proof verifying: bad proof matrix size (f internal)!"); + for (std::size_t i = 0; i < n - 1; ++i) + { + CHECK_AND_ASSERT_THROW_MES(!(proof.f[j][i] == ZERO), + "grootle proof verifying: proof matrix element should not be zero (f internal)!"); + CHECK_AND_ASSERT_THROW_MES(sc_check(proof.f[j][i].bytes) == 0, + "grootle proof verifying: bad scalar element in proof (f internal)!"); + } + } + CHECK_AND_ASSERT_THROW_MES(!(proof.zA == ZERO), + "grootle proof verifying: proof scalar element should not be zero (zA)!"); + CHECK_AND_ASSERT_THROW_MES(sc_check(proof.zA.bytes) == 0, + "grootle proof verifying: bad scalar element in proof (zA)!"); + CHECK_AND_ASSERT_THROW_MES(!(proof.z == ZERO), + "grootle proof verifying: proof scalar element should not be zero (z)!"); + CHECK_AND_ASSERT_THROW_MES(sc_check(proof.z.bytes) == 0, + "grootle proof verifying: bad scalar element in proof (z)!"); + } + + + /// per-proof data assembly + std::list builders; + + for (std::size_t proof_i{0}; proof_i < proofs.size(); ++proof_i) + { + // prepare two builders for this proof (for the index-encoding proof and the membership proof) + // note: manually specify the weights for efficiency + builders.emplace_back(rct::identity(), 2*m*n, 2); + SpMultiexpBuilder &builder1 = builders.back(); + builders.emplace_back(rct::identity(), 0, N + m + 1); + SpMultiexpBuilder &builder2 = builders.back(); + + build_verification_multiexps_for_proof(*(proofs[proof_i]), + messages[proof_i], + M[proof_i], + proof_offsets[proof_i], + n, + m, + rct::skGen(), + rct::skGen(), + builder1, + builder2); + } + + + /// return multiexp data for caller to deal with + verification_data_out = builders; +} +//------------------------------------------------------------------------------------------------------------------- +bool verify_grootle_proofs(const std::vector &proofs, + const rct::keyV &messages, + const std::vector &M, + const rct::keyV &proof_offsets, + const std::size_t n, + const std::size_t m) +{ + // build multiexp + std::list verification_data; + get_grootle_verification_data(proofs, messages, M, proof_offsets, n, m, verification_data); + + // verify multiexp + if (!SpMultiexp{verification_data}.evaluates_to_point_at_infinity()) + { + MERROR("Grootle proof: verification failed!"); + return false; + } + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace sp diff --git a/src/seraphis_crypto/grootle.h b/src/seraphis_crypto/grootle.h new file mode 100644 index 0000000000..1bfacbf78f --- /dev/null +++ b/src/seraphis_crypto/grootle.h @@ -0,0 +1,136 @@ +// Copyright (c) 2022, 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. + +//// +// Grootle proof: Groth/Bootle one-of-many proof of a commitment to zero +// - given a set of EC points S +// - given an EC point (the offset) O +// - prove DL knowledge with respect to G of the commitment to zero {S_l - O} for an index l +// in the set that is unknown to verifiers +// - allows proof batching (around (2*n*m)/(n^m + 2*n*m) amortization speedup possible) +// - limitations: assumes each proof uses a different reference set (proofs with the same ref set could be MUCH +// faster), can only batch proofs with the same decomposition (n^m) +// +// note: to prove DL of a point in S with respect to G directly, set the offset equal to the identity element I +// +// References: +// - One-out-of-Many Proofs: Or How to Leak a Secret and Spend a Coin (Groth): https://eprint.iacr.org/2014/764 +// - Short Accountable Ring Signatures Based on DDH (Bootle): https://eprint.iacr.org/2015/643 +// - Triptych (Sarang Noether): https://eprint.iacr.org/2020/018 +// - Lelantus-Spark (Aram Jivanyan, Aaron Feickert [Sarang Noether]): https://eprint.iacr.org/2021/1173 +// - MatRiCT (Esgin et. al; section 1.3 for A/B optimization): https://eprint.iacr.org/2019/1287.pdf +/// + +#pragma once + +//local headers +#include "ringct/rctTypes.h" + +//third party headers +#include + +//standard headers +#include +#include + +//forward declarations +namespace sp +{ + class SpTranscriptBuilder; + class SpMultiexpBuilder; +} + +namespace sp +{ + +/// Maximum matrix entries +constexpr std::size_t GROOTLE_MAX_MN{128}; //2^64, 3^42, etc. + +//// +// Grootle proof: Groth/Bootle proof using the A/B optimization from MatRiCT +/// +struct GrootleProof +{ + rct::key A; + rct::key B; + rct::keyM f; + rct::keyV X; + rct::key zA; + rct::key z; +}; +inline const boost::string_ref container_name(const GrootleProof&) { return "GrootleProof"; } +void append_to_transcript(const GrootleProof &container, SpTranscriptBuilder &transcript_inout); + +/// get size in bytes +std::size_t grootle_size_bytes(const std::size_t n, const std::size_t m); +std::size_t grootle_size_bytes(const GrootleProof &proof); + +/** +* brief: make_grootle_proof - create a grootle proof +* param: message - message to insert in Fiat-Shamir transform hash +* param: S - referenced commitments +* param: l - secret index into {S} +* param: C_offset - offset for commitment to zero at index l +* param: privkey - privkey of commitment to zero 'S[l] - C_offset' (proof signing key) +* param: n - decomposition of the reference set size: n^m +* param: m - ... +* outparam: proof_out - Grootle proof +*/ +void make_grootle_proof(const rct::key &message, + const rct::keyV &S, + const std::size_t l, + const rct::key &C_offset, + const crypto::secret_key &privkey, + const std::size_t n, + const std::size_t m, + GrootleProof &proof_out); +/** +* brief: verify_grootle_proofs - verify a batch of grootle proofs +* param: proofs - batch of proofs to verify +* param: message - (per-proof) message to insert in Fiat-Shamir transform hash +* param: S - (per-proof) referenced commitments +* param: proof_offsets - (per-proof) offset for commitment to zero at unknown indices in each proof +* param: n - decomposition of the reference set size: n^m +* param: m - ... +* return: true/false on verification result +*/ +void get_grootle_verification_data(const std::vector &proofs, + const rct::keyV &messages, + const std::vector &S, + const rct::keyV &proof_offsets, + const std::size_t n, + const std::size_t m, + std::list &verification_data_out); +bool verify_grootle_proofs(const std::vector &proofs, + const rct::keyV &messages, + const std::vector &M, + const rct::keyV &proof_offsets, + const std::size_t n, + const std::size_t m); + +} //namespace sp diff --git a/src/seraphis_crypto/math_utils.cpp b/src/seraphis_crypto/math_utils.cpp new file mode 100644 index 0000000000..6d0c1e998f --- /dev/null +++ b/src/seraphis_crypto/math_utils.cpp @@ -0,0 +1,148 @@ +// Copyright (c) 2022, 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 "math_utils.h" + +//local headers + +//third party headers +#include +#include "boost/multiprecision/cpp_int.hpp" + +//standard headers +#include +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis" + +namespace sp +{ +namespace math +{ +//------------------------------------------------------------------------------------------------------------------- +std::uint32_t n_choose_k(const std::uint32_t n, const std::uint32_t k) +{ + static_assert(std::numeric_limits::digits <= std::numeric_limits::digits, + "n_choose_k requires no rounding issues when converting between int32 <-> double."); + + if (n < k) + return 0; + + const double fp_result{boost::math::binomial_coefficient(n, k)}; + + if (fp_result < 0) + return 0; + + if (fp_result > std::numeric_limits::max()) // note: std::round() returns std::int32_t + return 0; + + return static_cast(std::round(fp_result)); +} +//------------------------------------------------------------------------------------------------------------------- +std::uint64_t clamp(const std::uint64_t a, const std::uint64_t min, const std::uint64_t max) +{ + // clamp 'a' to range [min, max] + if (a < min) + return min; + else if (a > max) + return max; + else + return a; +} +//------------------------------------------------------------------------------------------------------------------- +std::uint64_t saturating_add(const std::uint64_t a, const std::uint64_t b, const std::uint64_t max) +{ + if (a > max || + b > max - a) + return max; + return a + b; +} +//------------------------------------------------------------------------------------------------------------------- +std::uint64_t saturating_sub(const std::uint64_t a, const std::uint64_t b, const std::uint64_t min) +{ + if (a < min || + b > a - min) + return min; + return a - b; +} +//------------------------------------------------------------------------------------------------------------------- +std::uint64_t saturating_mul(const std::uint64_t a, const std::uint64_t b, const std::uint64_t max) +{ + boost::multiprecision::uint128_t a_big{a}; + boost::multiprecision::uint128_t b_big{b}; + boost::multiprecision::uint128_t r_big{a * b}; + + if (r_big > max) + return max; + + return static_cast(r_big); +} +//------------------------------------------------------------------------------------------------------------------- +std::uint64_t mod(const std::uint64_t a, const std::uint64_t n) +{ + // a mod n + // - special case: n = 0 means n = std::uint64_t::max + 1 + return n > 0 ? a % n : a; +} +//------------------------------------------------------------------------------------------------------------------- +std::uint64_t mod_negate(const std::uint64_t a, const std::uint64_t n) +{ + // -a mod n = n - (a mod n) + return n - mod(a, n); +} +//------------------------------------------------------------------------------------------------------------------- +std::uint64_t mod_add(std::uint64_t a, std::uint64_t b, const std::uint64_t n) +{ + // a + b mod n + a = mod(a, n); + b = mod(b, n); + + // if adding doesn't overflow the modulus, then add directly, otherwise overflow the modulus + return (n - a > b) ? a + b : b - (n - a); +} +//------------------------------------------------------------------------------------------------------------------- +std::uint64_t mod_sub(const std::uint64_t a, const std::uint64_t b, const std::uint64_t n) +{ + // a - b mod n + return mod_add(a, mod_negate(b, n), n); +} +//------------------------------------------------------------------------------------------------------------------- +std::uint64_t mod_mul(std::uint64_t a, std::uint64_t b, const std::uint64_t n) +{ + // a * b mod n + boost::multiprecision::uint128_t a_big{mod(a, n)}; + boost::multiprecision::uint128_t b_big{mod(b, n)}; + boost::multiprecision::uint128_t r_big{a * b}; + + return static_cast(n > 0 ? r_big % n : r_big); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace math +} //namespace sp diff --git a/src/seraphis_crypto/math_utils.h b/src/seraphis_crypto/math_utils.h new file mode 100644 index 0000000000..524d8d60fb --- /dev/null +++ b/src/seraphis_crypto/math_utils.h @@ -0,0 +1,134 @@ +// Copyright (c) 2022, 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. + +// Miscellaneous math utils. + +#pragma once + +//local headers + +//third party headers + +//standard headers +#include + +//forward declarations + + +namespace sp +{ +namespace math +{ + +/** +* brief: uint_pow - compute n^m +* param: n - base +* param: m - uint_pow +* return: n^m +* +* note: use this instead of std::pow() for better control over error states +*/ +constexpr std::uint64_t uint_pow(std::uint64_t n, unsigned char m) noexcept +{ + // 1. special case: 0^m = 0 + if (n == 0) return 0; + + // 2. special case: n^0 = 1 + if (m == 0) return 1; + + // 3. normal case: n^m + // - use square and multiply + std::uint64_t result{1}; + std::uint64_t temp{}; + + while (m != 0) + { + // multiply + if (m & 1) result *= n; + + // test end condition + if (m == 1) break; + + // square with overflow check + temp = n*n; + if (temp < n) return -1; + n = temp; + + // next level + m >>= 1; + } + + return result; +} +/** +* brief: n_choose_k - n choose k math function +* param: n - +* param: k - +* return: n choose k +*/ +std::uint32_t n_choose_k(const std::uint32_t n, const std::uint32_t k); +/** +* clamp 'a' to range [min, max] +*/ +std::uint64_t clamp(const std::uint64_t a, const std::uint64_t min, const std::uint64_t max); +/** +* a + b, saturate to 'max' +*/ +std::uint64_t saturating_add(const std::uint64_t a, const std::uint64_t b, const std::uint64_t max); +/** +* a - b, saturate to 'min' +*/ +std::uint64_t saturating_sub(const std::uint64_t a, const std::uint64_t b, const std::uint64_t min); +/** +* a * b, saturate to 'max' +*/ +std::uint64_t saturating_mul(const std::uint64_t a, const std::uint64_t b, const std::uint64_t max); +/** +* a mod n +* special case: n = 0 means n = std::uint64_t::max + 1 +*/ +std::uint64_t mod(const std::uint64_t a, const std::uint64_t n); +/** +* -a mod n +*/ +std::uint64_t mod_negate(const std::uint64_t a, const std::uint64_t n); +/** +* a + b mod n +*/ +std::uint64_t mod_add(std::uint64_t a, std::uint64_t b, const std::uint64_t n); +/** +* a - b mod n +*/ +std::uint64_t mod_sub(const std::uint64_t a, const std::uint64_t b, const std::uint64_t n); +/** +* a * b mod n +*/ +std::uint64_t mod_mul(std::uint64_t a, std::uint64_t b, const std::uint64_t n); + +} //namespace math +} //namespace sp diff --git a/src/seraphis_crypto/matrix_proof.cpp b/src/seraphis_crypto/matrix_proof.cpp new file mode 100644 index 0000000000..f291334854 --- /dev/null +++ b/src/seraphis_crypto/matrix_proof.cpp @@ -0,0 +1,316 @@ +// Copyright (c) 2021, 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 "matrix_proof.h" + +//local headers +#include "common/container_helpers.h" +#include "crypto/crypto.h" +extern "C" +{ +#include "crypto/crypto-ops.h" +} +#include "cryptonote_config.h" +#include "misc_language.h" +#include "misc_log_ex.h" +#include "ringct/rctOps.h" +#include "ringct/rctTypes.h" +#include "sp_crypto_utils.h" +#include "sp_hash_functions.h" +#include "sp_transcript.h" + +//third party headers + +//standard headers +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis" + +namespace sp +{ +//------------------------------------------------------------------------------------------------------------------- +// compute: A_inout += k * P +//------------------------------------------------------------------------------------------------------------------- +static void mul_add(const rct::key &k, const crypto::public_key &P, ge_p3 &A_inout) +{ + ge_p3 temp_p3; + ge_cached temp_cache; + ge_p1p1 temp_p1p1; + + CHECK_AND_ASSERT_THROW_MES(ge_frombytes_vartime(&temp_p3, to_bytes(P)) == 0, "ge_frombytes_vartime failed!"); + ge_scalarmult_p3(&temp_p3, k.bytes, &temp_p3); //k * P + ge_p3_to_cached(&temp_cache, &temp_p3); + ge_add(&temp_p1p1, &A_inout, &temp_cache); //+ k * P + ge_p1p1_to_p3(&A_inout, &temp_p1p1); +} +//------------------------------------------------------------------------------------------------------------------- +// aggregation coefficient 'mu' for concise structure +// +// mu = H_n(message, {B}, {{V}}) +//------------------------------------------------------------------------------------------------------------------- +static rct::key compute_base_aggregation_coefficient(const rct::key &message, + const std::vector &B, + const std::vector> &M) +{ + // collect aggregation coefficient hash data + SpFSTranscript transcript{ + config::HASH_KEY_MATRIX_PROOF_AGGREGATION_COEFF, + (1 + B.size() + (M.size() ? M[0].size() * M.size() : 0))*sizeof(crypto::public_key) + }; + transcript.append("message", message); + transcript.append("B", B); + transcript.append("M", M); + + // mu + rct::key aggregation_coefficient; + sp_hash_to_scalar(transcript.data(), transcript.size(), aggregation_coefficient.bytes); + CHECK_AND_ASSERT_THROW_MES(sc_isnonzero(aggregation_coefficient.bytes), + "matrix proof aggregation coefficient: aggregation coefficient must be nonzero!"); + + return aggregation_coefficient; +} +//------------------------------------------------------------------------------------------------------------------- +// challenge message +// challenge_message = H_32(message) +// +// note: in practice, this extends the aggregation coefficient (i.e. message = mu) +// challenge_message = H_32(mu) = H_32(H_n(message, {B}, {{V}})) +//------------------------------------------------------------------------------------------------------------------- +static rct::key compute_challenge_message(const rct::key &message) +{ + // collect challenge message hash data + SpFSTranscript transcript{config::HASH_KEY_MATRIX_PROOF_CHALLENGE_MSG, sizeof(rct::key)}; + transcript.append("message", message); + + // m + rct::key challenge_message; + sp_hash_to_32(transcript.data(), transcript.size(), challenge_message.bytes); + CHECK_AND_ASSERT_THROW_MES(sc_isnonzero(challenge_message.bytes), + "matrix proof challenge message: challenge_message must be nonzero!"); + + return challenge_message; +} +//------------------------------------------------------------------------------------------------------------------- +// Fiat-Shamir challenge +// c = H_n(challenge_message, [V_1 proof key], [V_2 proof key], ...) +//------------------------------------------------------------------------------------------------------------------- +static rct::key compute_challenge(const rct::key &message, const rct::keyV &V_proofkeys) +{ + // collect challenge hash data + SpFSTranscript transcript{config::HASH_KEY_MATRIX_PROOF_CHALLENGE, (1 + V_proofkeys.size())*sizeof(rct::key)}; + transcript.append("message", message); + transcript.append("V_proofkeys", V_proofkeys); + + // c + rct::key challenge; + sp_hash_to_scalar(transcript.data(), transcript.size(), challenge.bytes); + CHECK_AND_ASSERT_THROW_MES(sc_isnonzero(challenge.bytes), + "matrix proof challenge: challenge must be nonzero!"); + + return challenge; +} +//------------------------------------------------------------------------------------------------------------------- +// proof response +// r = alpha - c * sum_i(mu^i * k_i) +//------------------------------------------------------------------------------------------------------------------- +static void compute_response(const std::vector &k, + const rct::keyV &mu_pows, + const crypto::secret_key &alpha, + const rct::key &challenge, + rct::key &r_out) +{ + CHECK_AND_ASSERT_THROW_MES(k.size() == mu_pows.size(), "matrix proof response: not enough keys!"); + + // compute response + // r = alpha - c * sum_i(mu^i * k_i) + crypto::secret_key r_temp; + crypto::secret_key r_sum_temp{rct::rct2sk(rct::zero())}; + + for (std::size_t i{0}; i < k.size(); ++i) + { + sc_mul(to_bytes(r_temp), mu_pows[i].bytes, to_bytes(k[i])); //mu^i * k_i + sc_add(to_bytes(r_sum_temp), to_bytes(r_sum_temp), to_bytes(r_temp)); //sum_i(...) + } + sc_mulsub(r_out.bytes, challenge.bytes, to_bytes(r_sum_temp), to_bytes(alpha)); //alpha - c * sum_i(...) +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +void append_to_transcript(const MatrixProof &container, SpTranscriptBuilder &transcript_inout) +{ + transcript_inout.append("m", container.m); + transcript_inout.append("c", container.c); + transcript_inout.append("r", container.r); + transcript_inout.append("M", container.M); +} +//------------------------------------------------------------------------------------------------------------------- +void make_matrix_proof(const rct::key &message, + const std::vector &B, + const std::vector &privkeys, + MatrixProof &proof_out) +{ + /// input checks and initialization + const std::size_t num_basekeys{B.size()}; + const std::size_t num_keys{privkeys.size()}; + CHECK_AND_ASSERT_THROW_MES(num_basekeys > 0, "matrix proof: not enough base keys to make a proof!"); + CHECK_AND_ASSERT_THROW_MES(num_keys > 0, "matrix proof: not enough keys to make a proof!"); + + // 1. proof message + proof_out.m = message; + + // 2. prepare (1/8)*{k} + std::vector k_i_inv8_temp; + + for (const crypto::secret_key &k_i : privkeys) + { + CHECK_AND_ASSERT_THROW_MES(sc_isnonzero(to_bytes(k_i)), "matrix proof: bad private key (k_i zero)!"); + CHECK_AND_ASSERT_THROW_MES(sc_check(to_bytes(k_i)) == 0, "matrix proof: bad private key (k_i)!"); + + // k_i * (1/8) + sc_mul(to_bytes(tools::add_element(k_i_inv8_temp)), to_bytes(k_i), rct::INV_EIGHT.bytes); + } + + // 3. prepare (1/8)*{{V}} + proof_out.M.clear(); + proof_out.M.reserve(num_basekeys); + std::vector> V_mul8; + V_mul8.reserve(num_basekeys); + + for (const crypto::public_key &basekey : B) + { + proof_out.M.emplace_back(); + proof_out.M.back().reserve(num_keys); + V_mul8.emplace_back(); + V_mul8.back().reserve(num_keys); + + for (const crypto::secret_key &k_i_inv8 : k_i_inv8_temp) + { + proof_out.M.back().emplace_back(rct::rct2pk(rct::scalarmultKey(rct::pk2rct(basekey), rct::sk2rct(k_i_inv8)))); + V_mul8.back().emplace_back(rct::rct2pk(rct::scalarmult8(rct::pk2rct(proof_out.M.back().back())))); + } + } + + + /// signature openers: alpha * {B} + const crypto::secret_key alpha{rct::rct2sk(rct::skGen())}; + rct::keyV alpha_pubs; + alpha_pubs.reserve(num_basekeys); + + for (const crypto::public_key &basekey : B) + alpha_pubs.emplace_back(rct::scalarmultKey(rct::pk2rct(basekey), rct::sk2rct(alpha))); + + + /// challenge message and aggregation coefficient + const rct::key mu{compute_base_aggregation_coefficient(proof_out.m, B, V_mul8)}; + const rct::keyV mu_pows{sp::powers_of_scalar(mu, num_keys)}; + + const rct::key m{compute_challenge_message(mu)}; + + + /// compute proof challenge + proof_out.c = compute_challenge(m, alpha_pubs); + + + /// response + compute_response(privkeys, mu_pows, alpha, proof_out.c, proof_out.r); +} +//------------------------------------------------------------------------------------------------------------------- +bool verify_matrix_proof(const MatrixProof &proof, const std::vector &B) +{ + /// input checks and initialization + const std::size_t num_basekeys{B.size()}; + CHECK_AND_ASSERT_THROW_MES(num_basekeys > 0, "matrix proof (verify): there are no base keys!"); + CHECK_AND_ASSERT_THROW_MES(num_basekeys == proof.M.size(), "matrix proof (verify): proof has invalid pubkey sets!"); + + const std::size_t num_keys{proof.M[0].size()}; + CHECK_AND_ASSERT_THROW_MES(num_keys > 0, "matrix proof (verify): proof has no pubkeys!"); + + for (const std::vector &V : proof.M) + { + CHECK_AND_ASSERT_THROW_MES(V.size() == num_keys, "matrix proof (verify): inconsistent pubkey set sizes!"); + } + + CHECK_AND_ASSERT_THROW_MES(sc_isnonzero(proof.r.bytes), "matrix proof (verify): bad response (r zero)!"); + CHECK_AND_ASSERT_THROW_MES(sc_check(proof.r.bytes) == 0, "matrix proof (verify): bad response (r)!"); + + // recover the proof keys + std::vector> M_recovered; + M_recovered.reserve(num_basekeys); + + for (std::size_t j{0}; j < num_basekeys; ++j) + { + M_recovered.emplace_back(); + M_recovered.reserve(num_keys); + + for (std::size_t i{0}; i < num_keys; ++i) + M_recovered.back().emplace_back(rct::rct2pk(rct::scalarmult8(rct::pk2rct(proof.M[j][i])))); + } + + + /// challenge message and aggregation coefficient + const rct::key mu{compute_base_aggregation_coefficient(proof.m, B, M_recovered)}; + const rct::keyV mu_pows{sp::powers_of_scalar(mu, num_keys)}; + + const rct::key m{compute_challenge_message(mu)}; + + + /// challenge pieces + rct::keyV V_proofkeys; + V_proofkeys.reserve(num_basekeys); + + rct::key coeff_temp; + ge_p3 V_j_part_p3; + + for (std::size_t j{0}; j < num_basekeys; ++j) + { + // V_j part: [r B_j + c * sum_i(mu^i * V_j[i])] + V_j_part_p3 = ge_p3_identity; + + for (std::size_t i{0}; i < num_keys; ++i) + { + // c * mu^i + sc_mul(coeff_temp.bytes, proof.c.bytes, mu_pows[i].bytes); + + // V_j_part: + c * mu^i * V_j[i] + mul_add(coeff_temp, M_recovered[j][i], V_j_part_p3); + } + + // r B_j + V_j_part + mul_add(proof.r, B[j], V_j_part_p3); + + // convert to pubkey + ge_p3_tobytes(tools::add_element(V_proofkeys).bytes, &V_j_part_p3); + } + + + /// compute nominal challenge and validate proof + return compute_challenge(m, V_proofkeys) == proof.c; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace sp diff --git a/src/seraphis_crypto/matrix_proof.h b/src/seraphis_crypto/matrix_proof.h new file mode 100644 index 0000000000..d3d157f3eb --- /dev/null +++ b/src/seraphis_crypto/matrix_proof.h @@ -0,0 +1,126 @@ +// Copyright (c) 2021, 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. + +//// +// Schnorr-like matrix proof between a vector of base keys and vector of private keys. +// - base keys: {B} = {B1, B2, ..., Bn} +// - private keys: {k} = {k1, k2, ..., kn} +// - public keys per base key: V1 = {k1 B1, k2 B1, ..., kn B1} +// - all public keys: M = {{V}} = {V1, V2, ... Vn} +// - 1. demonstrates knowledge of all {k} +// - 2. demonstrates that members of each public key set in {V1, V2, ... Vn} have a 1:1 discrete-log equivalence with the +// members of the other public key sets across the base keys {B} +// - 3. guarantees that {{V}} contain canonical prime-order subgroup group elements (pubkeys are stored multiplied by +// (1/8) then multiplied by 8 before verification) +// NOTE: does not allow signing with different private keys on different base keys (e.g. the pair {k1 G1, k2 G2}), this +// proof is designed mainly for showing discrete log equivalence across multiple bases (with the bonus of being +// efficient when you have multiple such proofs to construct in parallel) +// NOTE2: at one base key this proof degenerates into a simple Schnorr signature, which can be useful for making signatures +// across arbitrary base keys +// +// proof outline +// 0. preliminaries +// H_32(...) = blake2b(...) -> 32 bytes hash to 32 bytes (domain separated) +// H_n(...) = H_64(...) mod l hash to ed25519 scalar (domain separated) +// {B}: assumed to be ed25519 keys +// 1. proof nonce and challenge +// given: m, {B}, {k} +// {{V}} = {k} * {B} +// mu = H_n(m, {B}, {{V}}) aggregation coefficient +// cm = H(mu) challenge message +// a = rand() prover nonce +// c = H_n(cm, [a*B1], [a*B2], ..., [a*Bn]) +// 2. aggregate response +// r = a - c * sum_i(mu^i * k_i) +// 3. proof: {m, c, r, {{V}}} +// +// verification +// 1. mu, cm = ... +// 2. c' = H_n(cm, [r*B1 + c*sum_i(mu^i*V_1[i])], [r*B2 + c*sum_i(mu^i*V_2[i])], ...) +// 3. if (c' == c) then the proof is valid +// +// note: proofs are 'concise' using the powers-of-aggregation coefficient approach from Triptych +// +// References: +// - Triptych (Sarang Noether): https://eprint.iacr.org/2020/018 +// - Zero to Monero 2 (koe, Kurt Alonso, Sarang Noether): https://web.getmonero.org/library/Zero-to-Monero-2-0-0.pdf +// - informational reference: Sections 3.1 and 3.2 +/// + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "ringct/rctTypes.h" + +//third party headers +#include + +//standard headers +#include + +//forward declarations +namespace sp { class SpTranscriptBuilder; } + +namespace sp +{ + +struct MatrixProof +{ + // message + rct::key m; + // challenge + rct::key c; + // response + rct::key r; + // pubkeys matrix (stored multiplied by (1/8)); each inner vector uses a different base key + std::vector> M; +}; +inline const boost::string_ref container_name(const MatrixProof&) { return "MatrixProof"; } +void append_to_transcript(const MatrixProof &container, SpTranscriptBuilder &transcript_inout); + +/** +* brief: make_matrix_proof - create a matrix proof +* param: message - message to insert in Fiat-Shamir transform hash +* param: B - base keys B1, B2, ... +* param: privkeys - secret keys k1, k2, ... +* outparam: proof_out - the proof +*/ +void make_matrix_proof(const rct::key &message, + const std::vector &B, + const std::vector &privkeys, + MatrixProof &proof_out); +/** +* brief: verify_matrix_proof - verify a matrix proof +* param: proof - proof to verify +* param: B - base keys B1, B2, ... +* return: true/false on verification result +*/ +bool verify_matrix_proof(const MatrixProof &proof, const std::vector &B); + +} //namespace sp diff --git a/src/seraphis_crypto/sp_composition_proof.cpp b/src/seraphis_crypto/sp_composition_proof.cpp new file mode 100644 index 0000000000..110dd09367 --- /dev/null +++ b/src/seraphis_crypto/sp_composition_proof.cpp @@ -0,0 +1,321 @@ +// Copyright (c) 2022, 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 "sp_composition_proof.h" + +//local headers +#include "crypto/crypto.h" +extern "C" +{ +#include "crypto/crypto-ops.h" +} +#include "crypto/generators.h" +#include "cryptonote_config.h" +#include "misc_language.h" +#include "misc_log_ex.h" +#include "ringct/rctOps.h" +#include "ringct/rctTypes.h" +#include "sp_crypto_utils.h" +#include "sp_hash_functions.h" +#include "sp_transcript.h" + +//third party headers +#include + +//standard headers +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis" + +namespace sp +{ +namespace composition_proof_detail +{ +//------------------------------------------------------------------------------------------------------------------- +// Fiat-Shamir challenge message +// +// challenge_message = H_32(X, U, m, K, KI, K_t1) +//------------------------------------------------------------------------------------------------------------------- +rct::key compute_challenge_message(const rct::key &message, + const rct::key &K, + const crypto::key_image &KI, + const rct::key &K_t1) +{ + // collect challenge message hash data + SpFSTranscript transcript{config::HASH_KEY_SP_COMPOSITION_PROOF_CHALLENGE_MESSAGE, 6*sizeof(rct::key)}; + transcript.append("X", crypto::get_X()); + transcript.append("U", crypto::get_U()); + transcript.append("message", message); + transcript.append("K", K); + transcript.append("KI", KI); + transcript.append("K_t1", K_t1); + + // challenge_message + rct::key challenge_message; + sp_hash_to_32(transcript.data(), transcript.size(), challenge_message.bytes); + CHECK_AND_ASSERT_THROW_MES(sc_isnonzero(challenge_message.bytes), "Transcript challenge_message must be nonzero!"); + + return challenge_message; +} +//------------------------------------------------------------------------------------------------------------------- +// Fiat-Shamir challenge: extend the challenge message +// c = H_n(challenge_message, [K_t1 proof key], [K_t2 proof key], [KI proof key]) +//------------------------------------------------------------------------------------------------------------------- +rct::key compute_challenge(const rct::key &challenge_message, + const rct::key &K_t1_proofkey, + const rct::key &K_t2_proofkey, + const rct::key &KI_proofkey) +{ + // collect challenge hash data + SpFSTranscript transcript{config::HASH_KEY_SP_COMPOSITION_PROOF_CHALLENGE, 4*sizeof(rct::key)}; + transcript.append("challenge_message", challenge_message); + transcript.append("K_t1_proofkey", K_t1_proofkey); + transcript.append("K_t2_proofkey", K_t2_proofkey); + transcript.append("KI_proofkey", KI_proofkey); + + rct::key challenge; + sp_hash_to_scalar(transcript.data(), transcript.size(), challenge.bytes); + CHECK_AND_ASSERT_THROW_MES(sc_isnonzero(challenge.bytes), "Transcript challenge must be nonzero!"); + + return challenge; +} +//------------------------------------------------------------------------------------------------------------------- +// Proof responses +// r_t1 = alpha_t1 - c * (1 / y) +// r_t2 = alpha_t2 - c * (x / y) +// r_ki = alpha_ki - c * (z / y) +//------------------------------------------------------------------------------------------------------------------- +void compute_responses(const rct::key &challenge, + const rct::key &alpha_t1, + const rct::key &alpha_t2, + const rct::key &alpha_ki, + const crypto::secret_key &x, + const crypto::secret_key &y, + const crypto::secret_key &z, + rct::key &r_t1_out, + rct::key &r_t2_out, + rct::key &r_ki_out) +{ + // r_t1 = alpha_t1 - c * (1 / y) + r_t1_out = invert(rct::sk2rct(y)); // 1 / y + sc_mulsub(r_t1_out.bytes, challenge.bytes, r_t1_out.bytes, alpha_t1.bytes); // alpha_t1 - c * (1 / y) + + // r_t2 = alpha_t2 - c * (x / y) + r_t2_out = invert(rct::sk2rct(y)); // 1 / y + sc_mul(r_t2_out.bytes, to_bytes(x), r_t2_out.bytes); // x / y + sc_mulsub(r_t2_out.bytes, challenge.bytes, r_t2_out.bytes, alpha_t2.bytes); // alpha_t2 - c * (x / y) + + // r_ki = alpha_ki - c * (z / y) + r_ki_out = invert(rct::sk2rct(y)); // 1 / y + sc_mul(r_ki_out.bytes, to_bytes(z), r_ki_out.bytes); // z / y + sc_mulsub(r_ki_out.bytes, challenge.bytes, r_ki_out.bytes, alpha_ki.bytes); // alpha_ki - c * (z / y) +} +//------------------------------------------------------------------------------------------------------------------- +// Element 'K_t1' for a proof +// - multiplied by (1/8) for storage (and for use in byte-aware contexts) +// K_t1 = (1/y) * K +// return: (1/8)*K_t1 +//------------------------------------------------------------------------------------------------------------------- +void compute_K_t1_for_proof(const crypto::secret_key &y, const rct::key &K, rct::key &K_t1_out) +{ + rct::key inv_y{invert(rct::sk2rct(y))}; + sc_mul(inv_y.bytes, inv_y.bytes, rct::INV_EIGHT.bytes); + rct::scalarmultKey(K_t1_out, K, inv_y); + + memwipe(inv_y.bytes, 32); //try to clean up the lingering bytes +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace composition_proof_detail + + +//------------------------------------------------------------------------------------------------------------------- +void append_to_transcript(const SpCompositionProof &container, SpTranscriptBuilder &transcript_inout) +{ + transcript_inout.append("c", container.c); + transcript_inout.append("r_t1", container.r_t1); + transcript_inout.append("r_t2", container.r_t2); + transcript_inout.append("r_ki", container.r_ki); + transcript_inout.append("K_t1", container.K_t1); +} +//------------------------------------------------------------------------------------------------------------------- +void make_sp_composition_proof(const rct::key &message, + const rct::key &K, + const crypto::secret_key &x, + const crypto::secret_key &y, + const crypto::secret_key &z, + SpCompositionProof &proof_out) +{ + /// input checks and initialization + CHECK_AND_ASSERT_THROW_MES(!(K == rct::identity()), "make sp composition proof: bad proof key (K identity)!"); + + CHECK_AND_ASSERT_THROW_MES(sc_isnonzero(to_bytes(x)), "make sp composition proof: bad private key (x zero)!"); + CHECK_AND_ASSERT_THROW_MES(sc_check(to_bytes(x)) == 0, "make sp composition proof: bad private key (x)!"); + CHECK_AND_ASSERT_THROW_MES(sc_isnonzero(to_bytes(y)), "make sp composition proof: bad private key (y zero)!"); + CHECK_AND_ASSERT_THROW_MES(sc_check(to_bytes(y)) == 0, "make sp composition proof: bad private key (y)!"); + CHECK_AND_ASSERT_THROW_MES(sc_isnonzero(to_bytes(z)), "make sp composition proof: bad private key (z zero)!"); + CHECK_AND_ASSERT_THROW_MES(sc_check(to_bytes(z)) == 0, "make sp composition proof: bad private key (z)!"); + + // verify the input key matches the input private keys: K = x G + y X + z U + rct::key reconstructed_K{ + rct::addKeys( + rct::scalarmultKey(rct::pk2rct(crypto::get_X()), rct::sk2rct(y)), + rct::scalarmultKey(rct::pk2rct(crypto::get_U()), rct::sk2rct(z)) + ) + }; + mask_key(x, reconstructed_K, reconstructed_K); + + CHECK_AND_ASSERT_THROW_MES(reconstructed_K == K, + "make sp composition proof: bad proof key (K doesn't match privkeys)!"); + + + /// make K_t1 and KI + + // K_t1 = (1/8) * (1/y) * K + composition_proof_detail::compute_K_t1_for_proof(y, K, proof_out.K_t1); + + // KI = (z / y) * U + const crypto::key_image KI{ + rct::rct2ki(rct::scalarmultKey( + rct::scalarmultKey(rct::pk2rct(crypto::get_U()), rct::sk2rct(z)), //z U + invert(rct::sk2rct(y)) //1/y + )) + }; + + + /// signature openers + + // alpha_t1 * K + crypto::secret_key alpha_t1; + rct::key alpha_t1_pub; + generate_proof_nonce(K, alpha_t1, alpha_t1_pub); + + // alpha_t2 * G + crypto::secret_key alpha_t2; + rct::key alpha_t2_pub; + generate_proof_nonce(rct::G, alpha_t2, alpha_t2_pub); + + // alpha_ki * U + crypto::secret_key alpha_ki; + rct::key alpha_ki_pub; + generate_proof_nonce(rct::pk2rct(crypto::get_U()), alpha_ki, alpha_ki_pub); + + + /// compute proof challenge + const rct::key m{composition_proof_detail::compute_challenge_message(message, K, KI, proof_out.K_t1)}; + proof_out.c = composition_proof_detail::compute_challenge(m, alpha_t1_pub, alpha_t2_pub, alpha_ki_pub); + + + /// responses + composition_proof_detail::compute_responses(proof_out.c, + rct::sk2rct(alpha_t1), + rct::sk2rct(alpha_t2), + rct::sk2rct(alpha_ki), + x, + y, + z, + proof_out.r_t1, + proof_out.r_t2, + proof_out.r_ki); +} +//------------------------------------------------------------------------------------------------------------------- +bool verify_sp_composition_proof(const SpCompositionProof &proof, + const rct::key &message, + const rct::key &K, + const crypto::key_image &KI) +{ + /// input checks and initialization + CHECK_AND_ASSERT_THROW_MES(sc_check(proof.r_t1.bytes) == 0, "verify sp composition proof: bad response (r_t1)!"); + CHECK_AND_ASSERT_THROW_MES(sc_check(proof.r_t2.bytes) == 0, "verify sp composition proof: bad response (r_t2)!"); + CHECK_AND_ASSERT_THROW_MES(sc_check(proof.r_ki.bytes) == 0, "verify sp composition proof: bad response (r_ki)!"); + + CHECK_AND_ASSERT_THROW_MES(!(rct::ki2rct(KI) == rct::identity()), "verify sp composition proof: invalid key image!"); + + + /// challenge message + const rct::key m{composition_proof_detail::compute_challenge_message(message, K, KI, proof.K_t1)}; + + + /// challenge pieces + static const ge_p3 U_p3{crypto::get_U_p3()}; + static const ge_p3 X_p3{crypto::get_X_p3()}; + + rct::key part_t1, part_t2, part_ki; + ge_p3 K_p3, K_t1_p3, K_t2_p3, KI_p3; + + ge_cached temp_cache; + ge_p1p1 temp_p1p1; + ge_p2 temp_p2; + ge_dsmp temp_dsmp; + + // get K + CHECK_AND_ASSERT_THROW_MES(ge_frombytes_vartime(&K_p3, K.bytes) == 0, "ge_frombytes_vartime failed!"); + + // get K_t1 + rct::scalarmult8(K_t1_p3, proof.K_t1); + CHECK_AND_ASSERT_THROW_MES(!(ge_p3_is_point_at_infinity_vartime(&K_t1_p3)), + "verify sp composition proof: invalid proof element K_t1!"); + + // get KI + CHECK_AND_ASSERT_THROW_MES(ge_frombytes_vartime(&KI_p3, rct::ki2rct(KI).bytes) == 0, "ge_frombytes_vartime failed!"); + + // K_t2 = K_t1 - X - KI + ge_p3_to_cached(&temp_cache, &X_p3); + ge_sub(&temp_p1p1, &K_t1_p3, &temp_cache); //K_t1 - X + ge_p1p1_to_p3(&K_t2_p3, &temp_p1p1); + ge_p3_to_cached(&temp_cache, &KI_p3); + ge_sub(&temp_p1p1, &K_t2_p3, &temp_cache); //(K_t1 - X) - KI + ge_p1p1_to_p3(&K_t2_p3, &temp_p1p1); + CHECK_AND_ASSERT_THROW_MES(!(ge_p3_is_point_at_infinity_vartime(&K_t2_p3)), + "verify sp composition proof: invalid proof element K_t2!"); + + // K_t1 part: [r_t1 * K + c * K_t1] + ge_dsm_precomp(temp_dsmp, &K_t1_p3); + ge_double_scalarmult_precomp_vartime(&temp_p2, proof.r_t1.bytes, &K_p3, proof.c.bytes, temp_dsmp); + ge_tobytes(part_t1.bytes, &temp_p2); + + // K_t2 part: [r_t2 * G + c * K_t2] + ge_double_scalarmult_base_vartime(&temp_p2, proof.c.bytes, &K_t2_p3, proof.r_t2.bytes); + ge_tobytes(part_t2.bytes, &temp_p2); + + // KI part: [r_ki * U + c * KI ] + ge_dsm_precomp(temp_dsmp, &KI_p3); + ge_double_scalarmult_precomp_vartime(&temp_p2, proof.r_ki.bytes, &U_p3, proof.c.bytes, temp_dsmp); + ge_tobytes(part_ki.bytes, &temp_p2); + + + /// compute nominal challenge + const rct::key challenge_nom{composition_proof_detail::compute_challenge(m, part_t1, part_t2, part_ki)}; + + + /// validate proof + return challenge_nom == proof.c; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace sp diff --git a/src/seraphis_crypto/sp_composition_proof.h b/src/seraphis_crypto/sp_composition_proof.h new file mode 100644 index 0000000000..ed236c0b68 --- /dev/null +++ b/src/seraphis_crypto/sp_composition_proof.h @@ -0,0 +1,178 @@ +// Copyright (c) 2022, 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. + +//// +// Schnorr-like composition proof for a key of the form K = x*G + y*X + z*U +// - demonstrates knowledge of secrets x, y, z +// - x, y, z > 0 +// - shows that key image KI = (z/y)*U +// +// proof outline +// 0. preliminaries +// hash to 32 bytes (domain separated): H_32(...) = blake2b(...) -> 32 bytes +// hash to ed25519 scalar (domain separated): H_n(...) = H_64(...) mod l +// ed25519 generators: G, X, U +// 1. pubkeys +// K = x*G + y*X + z*U +// K_t1 = (x/y)*G + X + (z/y)*U = (1/y)*K +// K_t2 = (x/y)*G = K_t1 - X - KI +// KI = (z/y)*U +// 2. proof nonces and challenge +// cm = H_32(X, U, m, K, KI, K_t1) challenge message +// a_t1, a_t2, a_ki = rand() prover nonces +// c = H_n(cm, [a_t1 K], [a_t2 G], [a_ki U]) challenge +// 3. responses +// r_t1 = a_t1 - c*(1/y) +// r_t2 = a_t2 - c*(x/y) +// r_ki = a_ki - c*(z/y) +// 4. proof: {m, c, r_t1, r_t2, r_ki, K, K_t1, KI} +// +// verification +// 1. K_t2 = K_t1 - X - KI, cm = ... +// 2. c' = H_n(cm, [r_t1*K + c*K_t1], [r_t2*G + c*K_t2], [r_ki*U + c*KI]) +// 3. if (c' == c) then the proof is valid +// +// proof explanation +// 1. prove transform: K_t1 = (1/y)*K (invert X component to create key image inside K_t1) +// 2. prove DL on G: (x/y)*G = K_t2 = K_t1 - X - KI (peel X and KI out of K_t1, show only G component remains; removing +// X here proves that step 1 correctly inverted the X component) +// 3. prove DL on U: KI = (z/y) U (key image has DL on only U) +// +// note: G_0 = G, G_1 = X, G_2 = U (for Seraphis paper notation) +// note: in practice, K is a masked address from a Seraphis enote image, and KI is the corresponding 'linking tag' +// note: assume key image KI is in the prime subgroup (canonical bytes) and non-identity +// - WARNING: the caller must validate KI (and check non-identity); either... +// - 1) l*KI == identity +// - 2) store (1/8)*KI with proof material (e.g. in a transaction); pass 8*[(1/8)*KI] as input to composition proof +// validation +// +// References: +// - Seraphis (UkoeHB): https://github.com/UkoeHB/Seraphis (temporary reference) +/// + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "ringct/rctTypes.h" + +//third party headers +#include + +//standard headers +#include + +//forward declarations +namespace sp { class SpTranscriptBuilder; } + +namespace sp +{ + +//// +// Seraphis composition proof +/// +struct SpCompositionProof final +{ + // challenge + rct::key c; + // responses + rct::key r_t1; + rct::key r_t2; + rct::key r_ki; + // intermediate proof key (stored as (1/8)*K_t1) + rct::key K_t1; + + // message m: not stored with proof + // main proof key K: not stored with proof + // key image KI: not stored with proof +}; +inline const boost::string_ref container_name(const SpCompositionProof&) { return "SpCompositionProof"; } +void append_to_transcript(const SpCompositionProof &container, SpTranscriptBuilder &transcript_inout); + +/// get size in bytes +inline std::size_t sp_composition_size_bytes() { return 32*5; } + +/** +* brief: make_sp_composition_proof - create a seraphis composition proof +* param: message - message to insert in Fiat-Shamir transform hash +* param: K - main proof key = x G + y X + z U +* param: x - secret key +* param: y - secret key +* param: z - secret key +* outparam: proof_out - seraphis composition proof +*/ +void make_sp_composition_proof(const rct::key &message, + const rct::key &K, + const crypto::secret_key &x, + const crypto::secret_key &y, + const crypto::secret_key &z, + SpCompositionProof &proof_out); +/** +* brief: verify_sp_composition_proof - verify a seraphis composition proof +* - PRECONDITION: KI is not identity and contains no torsion elements (the caller must perform those tests) +* param: proof - proof to verify +* param: message - message to insert in Fiat-Shamir transform hash +* param: K - main proof key = x G + y X + z U +* param: KI - proof key image = (z/y) U +* return: true/false on verification result +*/ +bool verify_sp_composition_proof(const SpCompositionProof &proof, + const rct::key &message, + const rct::key &K, + const crypto::key_image &KI); + + +//// +// detail namespace for internal proof computations +// - these are needed for e.g. multisig +/// +namespace composition_proof_detail +{ + +rct::key compute_challenge_message(const rct::key &message, + const rct::key &K, + const crypto::key_image &KI, + const rct::key &K_t1); +rct::key compute_challenge(const rct::key &challenge_message, + const rct::key &K_t1_proofkey, + const rct::key &K_t2_proofkey, + const rct::key &KI_proofkey); +void compute_responses(const rct::key &challenge, + const rct::key &alpha_t1, + const rct::key &alpha_t2, + const rct::key &alpha_ki, + const crypto::secret_key &x, + const crypto::secret_key &y, + const crypto::secret_key &z, + rct::key &r_t1_out, + rct::key &r_t2_out, + rct::key &r_ki_out); +void compute_K_t1_for_proof(const crypto::secret_key &y, const rct::key &K, rct::key &K_t1_out); + +} //namespace composition_proof_detail +} //namespace sp diff --git a/src/seraphis_crypto/sp_crypto_utils.cpp b/src/seraphis_crypto/sp_crypto_utils.cpp new file mode 100644 index 0000000000..288a6b03fc --- /dev/null +++ b/src/seraphis_crypto/sp_crypto_utils.cpp @@ -0,0 +1,297 @@ +// Copyright (c) 2022, 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 "sp_crypto_utils.h" + +//local headers +#include "crypto/crypto.h" +extern "C" +{ +#include "crypto/crypto-ops.h" +} +#include "crypto/x25519.h" +#include "misc_log_ex.h" +#include "ringct/rctOps.h" +#include "ringct/rctTypes.h" + +//third party headers +#include "boost/multiprecision/cpp_int.hpp" + +//standard headers +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis" + +namespace sp +{ + +/// File-scope data + +// Useful scalar and group constants +static const rct::key ZERO = rct::zero(); +static const rct::key ONE = rct::identity(); +/// scalar: -1 mod q +static const rct::key MINUS_ONE = { {0xec, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, + 0xde, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10} }; + +//------------------------------------------------------------------------------------------------------------------- +// Helper function for scalar inversion +// return: x*(y^2^n) +//------------------------------------------------------------------------------------------------------------------- +static rct::key sm(rct::key y, int n, const rct::key &x) +{ + while (n--) + sc_mul(y.bytes, y.bytes, y.bytes); + sc_mul(y.bytes, y.bytes, x.bytes); + return y; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +rct::key minus_one() +{ + return MINUS_ONE; +} +//------------------------------------------------------------------------------------------------------------------- +rct::key invert(const rct::key &x) +{ + CHECK_AND_ASSERT_THROW_MES(!(x == ZERO), "Cannot invert zero!"); + + rct::key _1, _10, _100, _11, _101, _111, _1001, _1011, _1111; + + _1 = x; + sc_mul(_10.bytes, _1.bytes, _1.bytes); + sc_mul(_100.bytes, _10.bytes, _10.bytes); + sc_mul(_11.bytes, _10.bytes, _1.bytes); + sc_mul(_101.bytes, _10.bytes, _11.bytes); + sc_mul(_111.bytes, _10.bytes, _101.bytes); + sc_mul(_1001.bytes, _10.bytes, _111.bytes); + sc_mul(_1011.bytes, _10.bytes, _1001.bytes); + sc_mul(_1111.bytes, _100.bytes, _1011.bytes); + + rct::key inv; + sc_mul(inv.bytes, _1111.bytes, _1.bytes); + + inv = sm(inv, 123 + 3, _101); + inv = sm(inv, 2 + 2, _11); + inv = sm(inv, 1 + 4, _1111); + inv = sm(inv, 1 + 4, _1111); + inv = sm(inv, 4, _1001); + inv = sm(inv, 2, _11); + inv = sm(inv, 1 + 4, _1111); + inv = sm(inv, 1 + 3, _101); + inv = sm(inv, 3 + 3, _101); + inv = sm(inv, 3, _111); + inv = sm(inv, 1 + 4, _1111); + inv = sm(inv, 2 + 3, _111); + inv = sm(inv, 2 + 2, _11); + inv = sm(inv, 1 + 4, _1011); + inv = sm(inv, 2 + 4, _1011); + inv = sm(inv, 6 + 4, _1001); + inv = sm(inv, 2 + 2, _11); + inv = sm(inv, 3 + 2, _11); + inv = sm(inv, 3 + 2, _11); + inv = sm(inv, 1 + 4, _1001); + inv = sm(inv, 1 + 3, _111); + inv = sm(inv, 2 + 4, _1111); + inv = sm(inv, 1 + 4, _1011); + inv = sm(inv, 3, _101); + inv = sm(inv, 2 + 4, _1111); + inv = sm(inv, 3, _101); + inv = sm(inv, 1 + 2, _11); + + // Confirm inversion + rct::key temp; + sc_mul(temp.bytes, x.bytes, inv.bytes); + CHECK_AND_ASSERT_THROW_MES(temp == ONE, "Scalar inversion failed!"); + + return inv; +} +//------------------------------------------------------------------------------------------------------------------- +void decompose(const std::size_t val, const std::size_t base, const std::size_t size, std::vector &r_out) +{ + CHECK_AND_ASSERT_THROW_MES(base > 1, "Bad decomposition parameters!"); + CHECK_AND_ASSERT_THROW_MES(size > 0, "Bad decomposition parameters!"); + CHECK_AND_ASSERT_THROW_MES(r_out.size() == size, "Bad decomposition result vector size!"); + + std::size_t temp = val; + + for (std::size_t i = 0; i < size; ++i) + { + std::size_t slot = std::pow(base, size - i - 1); + r_out[size - i - 1] = temp/slot; + temp -= slot*r_out[size - i - 1]; + } +} +//------------------------------------------------------------------------------------------------------------------- +rct::key kronecker_delta(const std::size_t x, const std::size_t y) +{ + if (x == y) + return ONE; + else + return ZERO; +} +//------------------------------------------------------------------------------------------------------------------- +rct::keyV convolve(const rct::keyV &x, const rct::keyV &y, const std::size_t m) +{ + CHECK_AND_ASSERT_THROW_MES(x.size() >= m, "Bad convolution parameters!"); + CHECK_AND_ASSERT_THROW_MES(y.size() == 2, "Bad convolution parameters!"); + + rct::key temp; + rct::keyV result; + result.resize(m + 1, ZERO); + + for (std::size_t i = 0; i < m; ++i) + { + for (std::size_t j = 0; j < 2; ++j) + { + sc_mul(temp.bytes, x[i].bytes, y[j].bytes); + sc_add(result[i + j].bytes, result[i + j].bytes, temp.bytes); + } + } + + return result; +} +//------------------------------------------------------------------------------------------------------------------- +rct::keyV powers_of_scalar(const rct::key &scalar, const std::size_t num_pows, const bool negate_all) +{ + if (num_pows == 0) + return rct::keyV{}; + + rct::keyV pows; + pows.resize(num_pows); + + if (negate_all) + pows[0] = MINUS_ONE; + else + pows[0] = ONE; + + for (std::size_t i = 1; i < num_pows; ++i) + { + sc_mul(pows[i].bytes, pows[i - 1].bytes, scalar.bytes); + } + + return pows; +} +//------------------------------------------------------------------------------------------------------------------- +void generate_proof_nonce(const rct::key &base, crypto::secret_key &nonce_out, rct::key &nonce_pub_out) +{ + // make proof nonce as crypto::secret_key + CHECK_AND_ASSERT_THROW_MES(!(base == rct::identity()), "Bad base for generating proof nonce!"); + + nonce_out = rct::rct2sk(ZERO); + + while (nonce_out == rct::rct2sk(ZERO) || nonce_pub_out == rct::identity()) + { + nonce_out = rct::rct2sk(rct::skGen()); + rct::scalarmultKey(nonce_pub_out, base, rct::sk2rct(nonce_out)); + } +} +//------------------------------------------------------------------------------------------------------------------- +void generate_proof_nonce(const rct::key &base, rct::key &nonce_out, rct::key &nonce_pub_out) +{ + // make proof nonce as rct::key + crypto::secret_key temp; + generate_proof_nonce(base, temp, nonce_pub_out); + nonce_out = rct::sk2rct(temp); +} +//------------------------------------------------------------------------------------------------------------------- +void subtract_secret_key_vectors(const std::vector &keys_A, + const std::vector &keys_B, + crypto::secret_key &result_out) +{ + result_out = rct::rct2sk(rct::zero()); + + // add keys_A + for (const crypto::secret_key &key_A : keys_A) + sc_add(to_bytes(result_out), to_bytes(result_out), to_bytes(key_A)); + + // subtract keys_B + for (const crypto::secret_key &key_B : keys_B) + sc_sub(to_bytes(result_out), to_bytes(result_out), to_bytes(key_B)); +} +//------------------------------------------------------------------------------------------------------------------- +void mask_key(const crypto::secret_key &mask, const rct::key &key, rct::key &masked_key_out) +{ + // K" = mask G + K + rct::addKeys1(masked_key_out, rct::sk2rct(mask), key); +} +//------------------------------------------------------------------------------------------------------------------- +crypto::secret_key add_secrets(const crypto::secret_key &a, const crypto::secret_key &b) +{ + crypto::secret_key temp; + sc_add(to_bytes(temp), to_bytes(a), to_bytes(b)); + return temp; +} +//------------------------------------------------------------------------------------------------------------------- +bool key_domain_is_prime_subgroup(const rct::key &check_key) +{ + // l*K ?= identity + ge_p3 check_key_p3; + CHECK_AND_ASSERT_THROW_MES(ge_frombytes_vartime(&check_key_p3, check_key.bytes) == 0, "ge_frombytes_vartime failed"); + ge_scalarmult_p3(&check_key_p3, rct::curveOrder().bytes, &check_key_p3); + + return (ge_p3_is_point_at_infinity_vartime(&check_key_p3) != 0); +} +//------------------------------------------------------------------------------------------------------------------- +bool keys_are_unique(const std::vector &keys) +{ + for (auto key_it = keys.begin(); key_it != keys.end(); ++key_it) + { + if (std::find(keys.begin(), key_it, *key_it) != key_it) + return false; + } + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool balance_check_equality(const rct::keyV &commitment_set1, const rct::keyV &commitment_set2) +{ + // balance check method chosen from perf test: tests/performance_tests/balance_check.h + return rct::equalKeys(rct::addKeys(commitment_set1), rct::addKeys(commitment_set2)); +} +//------------------------------------------------------------------------------------------------------------------- +bool balance_check_in_out_amnts(const std::vector &input_amounts, + const std::vector &output_amounts, + const rct::xmr_amount transaction_fee) +{ + boost::multiprecision::uint128_t input_sum{0}; + boost::multiprecision::uint128_t output_sum{0}; + + for (const auto amnt : input_amounts) + input_sum += amnt; + + for (const auto amnt : output_amounts) + output_sum += amnt; + output_sum += transaction_fee; + + return input_sum == output_sum; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace sp diff --git a/src/seraphis_crypto/sp_crypto_utils.h b/src/seraphis_crypto/sp_crypto_utils.h new file mode 100644 index 0000000000..30322a5bd7 --- /dev/null +++ b/src/seraphis_crypto/sp_crypto_utils.h @@ -0,0 +1,177 @@ +// Copyright (c) 2022, 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. + +// Miscellaneous crypto utils for seraphis (also includes some more basic math utils). + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "crypto/x25519.h" +#include "ringct/rctTypes.h" + +//third party headers + +//standard headers +#include +#include + +//forward declarations + + +namespace sp +{ + +/// sortable key (e.g. for hash maps) +struct sortable_key +{ + unsigned char bytes[32]; + + sortable_key() = default; + sortable_key(const rct::key &rct_key) { memcpy(bytes, rct_key.bytes, 32); } + bool operator<(const sortable_key &other) const { return memcmp(bytes, other.bytes, 32) < 0; } +}; +inline const rct::key& sortable2rct(const sortable_key &k) { return reinterpret_cast(k); } + +/** +* brief: minus_one - -1 mod q +* return: -1 mod q +*/ +rct::key minus_one(); +/** +* brief: invert - invert a nonzero scalar +* param: x - scalar to invert +* return: (1/x) mod l +*/ +rct::key invert(const rct::key &x); +/** +* brief: decompose - decompose an integer with a fixed base and size +* val -> [_, _, ... ,_] +* - num slots = 'size' +* - numeric base = 'base' +* - PRECONDITION: 'size' must be large enough to accomodate all digits of the decomposed value; if it is too small then +* the last digit will not be mod the base +* e.g. if base = 2 then convert val to binary, if base = 10 then put its decimal digits into the return vector +* param: val - value to decompose +* param: base - numeric base for decomposing the value +* param: size - number of digits to record the value in +* outparam: r_out - decomposed val (little endian) +*/ +void decompose(const std::size_t val, const std::size_t base, const std::size_t size, std::vector &r_out); +/** +* brief: kronecker_delta - Kronecker delta +* param: x - first integer +* param: y - second integer +* return: 1 if x == y, else 0 +*/ +rct::key kronecker_delta(const std::size_t x, const std::size_t y); +/** +* brief: convolve - compute a convolution with a degree-one polynomial +* param: x - x_1, x_2, ..., x_m +* param: y - a, b +* param: m - number of elements to look at from x (only access up to x[m-1] in case x.size() > m) +* return: [a*x_1], [b*x_1 + a*x_2], ..., [b*x_{m - 2} + a*x_{m - 1}], [b*x_m] +*/ +rct::keyV convolve(const rct::keyV &x, const rct::keyV &y, const std::size_t m); +/** +* brief: powers_of_scalar - powers of a scalar +* param: scalar - scalar to take powers of +* param: num_pows - number of powers to take (0-indexed) +* param: negate_all - bool flag for negating all returned values +* return: (negate ? -1 : 1)*([scalar^0], [scalar^1], ..., [scalar^{num_pows - 1}]) +*/ +rct::keyV powers_of_scalar(const rct::key &scalar, const std::size_t num_pows, const bool negate_all = false); +/** +* brief: generate_proof_nonce - generate a random scalar and corresponding pubkey for use in a Schnorr-like signature +* opening +* param: base - base EC pubkey for the nonce term +* outparam: nonce_out - private key 'nonce' +* outparam: nonce_pub_out - public key 'nonce * base' +*/ +void generate_proof_nonce(const rct::key &base, crypto::secret_key &nonce_out, rct::key &nonce_pub_out); +void generate_proof_nonce(const rct::key &base, rct::key &nonce_out, rct::key &nonce_pub_out); +/** +* brief: subtract_secret_key_vectors - subtract one vector of secret keys from another +* sum(A) - sum(B) +* param: keys_A - first vector (addors) +* param: keys_B - second vector (subtractors) +* outparam: result_out - 'sum(A) - sum(B)' +*/ +void subtract_secret_key_vectors(const std::vector &keys_A, + const std::vector &keys_B, + crypto::secret_key &result_out); +/** +* brief: mask_key - commit to an EC key +* K" = mask G + K +* param: mask - commitment mask/blinding factor +* param: key - EC key to commit to +* outparam: masked_key_out - K", the masked key +*/ +void mask_key(const crypto::secret_key &mask, const rct::key &key, rct::key &masked_key_out); +/** +* brief: add_secrets - v = a + b +* K" = mask G + K +* param: mask - commitment mask/blinding factor +* param: key - EC key to commit to +* outparam: masked_key_out - K", the masked key +*/ +crypto::secret_key add_secrets(const crypto::secret_key &a, const crypto::secret_key &b); +/** +* brief: key_domain_is_prime_subgroup - check that input key is in the prime order EC subgroup +* l*K ?= identity +* param: check_key - key to check +* result: true if input key is in prime order EC subgroup +*/ +bool key_domain_is_prime_subgroup(const rct::key &check_key); +/** +* brief: keys_are_unique - check if keys in a vector are unique +* param: keys - +* return: true if keys are unique +*/ +bool keys_are_unique(const std::vector &keys); +/** +* brief: balance_check_equality - balance check between two commitment sets using an equality test +* - i.e. sum(inputs) ?= sum(outputs) +* param: commitment_set1 - +* param: commitment_set2 - +* return: true/false on balance check result +*/ +bool balance_check_equality(const rct::keyV &commitment_set1, const rct::keyV &commitment_set2); +/** +* brief: balance_check_in_out_amnts - balance check between two sets of amounts +* - i.e. sum(inputs) ?= sum(outputs) + transaction_fee +* param: input_amounts - +* param: output_amounts - +* param: transaction_fee - +* return: true/false on balance check result +*/ +bool balance_check_in_out_amnts(const std::vector &input_amounts, + const std::vector &output_amounts, + const rct::xmr_amount transaction_fee); + +} //namespace sp diff --git a/src/seraphis_crypto/sp_generator_factory.cpp b/src/seraphis_crypto/sp_generator_factory.cpp new file mode 100644 index 0000000000..07125f7ce5 --- /dev/null +++ b/src/seraphis_crypto/sp_generator_factory.cpp @@ -0,0 +1,141 @@ +// Copyright (c) 2022, 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 "sp_generator_factory.h" + +//local headers +#include "crypto/crypto.h" +extern "C" +{ +#include "crypto/crypto-ops.h" +} +#include "crypto/hash.h" +#include "cryptonote_config.h" +#include "misc_log_ex.h" +#include "ringct/rctOps.h" +#include "sp_hash_functions.h" +#include "sp_transcript.h" + +//third party headers + +//standard headers +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis" + +namespace sp +{ +namespace generator_factory +{ + +struct SpFactoryGenerator final +{ + crypto::public_key generator; + ge_p3 generator_p3; + ge_cached generator_cached; +}; + +// number of generators to generate (enough for to make a BPP2 proof with the max number of aggregated range proofs) +static constexpr std::size_t MAX_GENERATOR_COUNT{config::BULLETPROOF_PLUS2_MAX_COMMITMENTS*128}; + +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static std::vector prepare_generators() +{ + std::vector generators; + + // make generators + generators.resize(MAX_GENERATOR_COUNT); + + rct::key intermediate_hash; + + for (std::size_t generator_index{0}; generator_index < MAX_GENERATOR_COUNT; ++generator_index) + { + SpKDFTranscript transcript{config::HASH_KEY_SERAPHIS_GENERATOR_FACTORY, 4}; + transcript.append("generator_index", generator_index); + + // G[generator_index] = keccak_to_pt(H_32("sp_generator_factory", generator_index)) + sp_hash_to_32(transcript.data(), transcript.size(), intermediate_hash.bytes); + rct::hash_to_p3(generators[generator_index].generator_p3, intermediate_hash); + + // convert to other representations + ge_p3_tobytes(to_bytes(generators[generator_index].generator), &generators[generator_index].generator_p3); + ge_p3_to_cached(&generators[generator_index].generator_cached, &generators[generator_index].generator_p3); + } + +/* +// demo: print first generator public key to console +for (const unsigned char byte : generators[0].generator.data) +{ + printf("0x"); + if (byte < 16) + printf("0"); + printf("%x ", byte); +} +printf("\n"); +*/ + + return generators; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static const SpFactoryGenerator& factory_generator_at_index(const std::size_t desired_index) +{ + static const std::vector s_factory_gens{prepare_generators()}; + + CHECK_AND_ASSERT_THROW_MES(desired_index < MAX_GENERATOR_COUNT, + "sp generator factory sanity check: requested generator index exceeds available generators."); + + return s_factory_gens[desired_index]; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +std::size_t max_generator_count() +{ + return MAX_GENERATOR_COUNT; +} +//------------------------------------------------------------------------------------------------------------------- +crypto::public_key get_generator_at_index(const std::size_t generator_index) +{ + return factory_generator_at_index(generator_index).generator; +} +//------------------------------------------------------------------------------------------------------------------- +ge_p3 get_generator_at_index_p3(const std::size_t generator_index) +{ + return factory_generator_at_index(generator_index).generator_p3; +} +//------------------------------------------------------------------------------------------------------------------- +ge_cached get_generator_at_index_cached(const std::size_t generator_index) +{ + return factory_generator_at_index(generator_index).generator_cached; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace generator_factory +} //namespace sp diff --git a/src/seraphis_crypto/sp_generator_factory.h b/src/seraphis_crypto/sp_generator_factory.h new file mode 100644 index 0000000000..2e02162b13 --- /dev/null +++ b/src/seraphis_crypto/sp_generator_factory.h @@ -0,0 +1,58 @@ +// Copyright (c) 2022, 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 generator factory. + +#pragma once + +//local headers +extern "C" +{ +#include "crypto/crypto-ops.h" +} + +//third party headers + +//standard headers +#include + +//forward declarations +namespace crypto { struct public_key; } + +namespace sp +{ +namespace generator_factory +{ + +std::size_t max_generator_count(); +crypto::public_key get_generator_at_index(const std::size_t generator_index); +ge_p3 get_generator_at_index_p3(const std::size_t generator_index); +ge_cached get_generator_at_index_cached(const std::size_t generator_index); + +} //namespace generator_factory +} //namespace sp diff --git a/src/seraphis_crypto/sp_hash_functions.cpp b/src/seraphis_crypto/sp_hash_functions.cpp new file mode 100644 index 0000000000..987f6341d8 --- /dev/null +++ b/src/seraphis_crypto/sp_hash_functions.cpp @@ -0,0 +1,150 @@ +// Copyright (c) 2022, 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 "sp_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 "seraphis" + +namespace sp +{ +//------------------------------------------------------------------------------------------------------------------- +// 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, + "seraphis hash base: blake2b failed."); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +void sp_hash_to_1(const void *data, const std::size_t data_length, void *hash_out) +{ + // H_1(x): 1-byte output + hash_base(nullptr, data, data_length, hash_out, 1); +} +//------------------------------------------------------------------------------------------------------------------- +void sp_hash_to_2(const void *data, const std::size_t data_length, void *hash_out) +{ + // H_2(x): 2-byte output + hash_base(nullptr, data, data_length, hash_out, 2); +} +//------------------------------------------------------------------------------------------------------------------- +void sp_hash_to_8(const void *data, const std::size_t data_length, void *hash_out) +{ + // H_8(x): 8-byte output + hash_base(nullptr, data, data_length, hash_out, 8); +} +//------------------------------------------------------------------------------------------------------------------- +void sp_hash_to_16(const void *data, const std::size_t data_length, void *hash_out) +{ + // H_16(x): 16-byte output + hash_base(nullptr, data, data_length, hash_out, 16); +} +//------------------------------------------------------------------------------------------------------------------- +void sp_hash_to_32(const void *data, const std::size_t data_length, void *hash_out) +{ + // H_32(x): 32-byte output + hash_base(nullptr, data, data_length, hash_out, 32); +} +//------------------------------------------------------------------------------------------------------------------- +void sp_hash_to_64(const void *data, const std::size_t data_length, void *hash_out) +{ + // H_64(x): 64-byte output + hash_base(nullptr, data, data_length, hash_out, 64); +} +//------------------------------------------------------------------------------------------------------------------- +void sp_hash_to_scalar(const void *data, const std::size_t data_length, 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(nullptr, data, data_length, temp, 64); + sc_reduce(temp); //mod l + memcpy(hash_out, temp, 32); +} +//------------------------------------------------------------------------------------------------------------------- +void sp_hash_to_x25519_scalar(const void *data, const std::size_t data_length, void *hash_out) +{ + // H_n_x25519(x): canonical X25519 group scalar output (32 bytes) + // - bits 0, 1, 2, 255 set to zero + hash_base(nullptr, data, data_length, hash_out, 32); + reinterpret_cast(hash_out)[0] &= 255 - 7; + reinterpret_cast(hash_out)[31] &= 127; +} +//------------------------------------------------------------------------------------------------------------------- +void sp_derive_key(const void *derivation_key, const void *data, const std::size_t data_length, void *hash_out) +{ + // H_n[k](x): Ed25519 group scalar output (32 bytes) + // note: hash to 64 bytes then mod l + unsigned char temp[64]; + hash_base(derivation_key, data, data_length, temp, 64); + sc_reduce(temp); //mod l + memcpy(hash_out, temp, 32); +} +//------------------------------------------------------------------------------------------------------------------- +void sp_derive_secret(const void *derivation_key, const void *data, const std::size_t data_length, void *hash_out) +{ + // H_32[k](x): 32-byte output + hash_base(derivation_key, data, data_length, hash_out, 32); +} +//------------------------------------------------------------------------------------------------------------------- +void sp_derive_x25519_key(const void *derivation_key, const void *data, const std::size_t data_length, void *hash_out) +{ + // H_n_x25519[k](x): canonical X25519 group scalar output (32 bytes) + // - bits 0, 1, 2, 255 set to zero + hash_base(derivation_key, data, data_length, hash_out, 32); + reinterpret_cast(hash_out)[0] &= 255 - 7; + reinterpret_cast(hash_out)[31] &= 127; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace sp diff --git a/src/seraphis_crypto/sp_hash_functions.h b/src/seraphis_crypto/sp_hash_functions.h new file mode 100644 index 0000000000..90f99906ca --- /dev/null +++ b/src/seraphis_crypto/sp_hash_functions.h @@ -0,0 +1,69 @@ +// Copyright (c) 2022, 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 sp +{ + +/// H_1(x): 1-byte output +void sp_hash_to_1(const void *data, const std::size_t data_length, void *hash_out); +/// H_2(x): 2-byte output +void sp_hash_to_2(const void *data, const std::size_t data_length, void *hash_out); +/// H_8(x): 8-byte output +void sp_hash_to_8(const void *data, const std::size_t data_length, void *hash_out); +/// H_16(x): 16-byte output +void sp_hash_to_16(const void *data, const std::size_t data_length, void *hash_out); +/// H_32(x): 32-byte output +void sp_hash_to_32(const void *data, const std::size_t data_length, void *hash_out); +/// H_64(x): 64-byte output +void sp_hash_to_64(const void *data, const std::size_t data_length, void *hash_out); +/// H_n(x): Ed25519 group scalar output (32 bytes) +void sp_hash_to_scalar(const void *data, const std::size_t data_length, void *hash_out); +/// H_n_x25519(x): canonical X25519 group scalar output (32 bytes) +void sp_hash_to_x25519_scalar(const void *data, const std::size_t data_length, void *hash_out); +/// H_n[k](x): 32-byte key; Ed25519 group scalar output (32 bytes) +void sp_derive_key(const void *derivation_key, const void *data, const std::size_t data_length, void *hash_out); +/// H_32[k](x): 32-byte key; 32-byte output +void sp_derive_secret(const void *derivation_key, const void *data, const std::size_t data_length, void *hash_out); +/// H_n_x25519[k](x): 32-byte key; canonical X25519 group scalar output (32 bytes) +void sp_derive_x25519_key(const void *derivation_key, const void *data, const std::size_t data_length, void *hash_out); + +} //namespace sp diff --git a/src/seraphis_crypto/sp_legacy_proof_helpers.cpp b/src/seraphis_crypto/sp_legacy_proof_helpers.cpp new file mode 100644 index 0000000000..f89ee7f257 --- /dev/null +++ b/src/seraphis_crypto/sp_legacy_proof_helpers.cpp @@ -0,0 +1,160 @@ +// Copyright (c) 2022, 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 "sp_legacy_proof_helpers.h" + +//local headers +#include "bulletproofs_plus2.h" +#include "misc_log_ex.h" +#include "ringct/rctOps.h" +#include "ringct/rctTypes.h" +#include "sp_transcript.h" + +//third party headers + +//standard headers +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis" + +namespace sp +{ +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static std::size_t round_up_to_power_of_2(const std::size_t num) +{ + // error case: can't round up + if (num > ~(static_cast(-1) >> 1)) + return -1; + + // next power of 2 >= num + std::size_t result{1}; + while (result < num) + result <<= 1; + + return result; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static std::size_t highest_bit_position(std::size_t num) +{ + // floor(log2(num)) + std::size_t bit_position{static_cast(-1)}; + while (num > 0) + { + ++bit_position; + num >>= 1; + } + + return bit_position; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +void append_clsag_to_transcript(const rct::clsag &clsag_proof, SpTranscriptBuilder &transcript_inout) +{ + transcript_inout.append("s", clsag_proof.s); + transcript_inout.append("c1", clsag_proof.c1); + transcript_inout.append("I", clsag_proof.I); + transcript_inout.append("D", clsag_proof.D); +} +//------------------------------------------------------------------------------------------------------------------- +std::size_t clsag_size_bytes(const std::size_t ring_size) +{ + return 32 * (ring_size + 2); //does not include 'I', which is treated as a cached value here +} +//------------------------------------------------------------------------------------------------------------------- +void make_bpp2_rangeproofs(const std::vector &amounts, + const std::vector &amount_commitment_blinding_factors, + BulletproofPlus2 &range_proofs_out) +{ + /// range proofs + // - for output amount commitments + CHECK_AND_ASSERT_THROW_MES(amounts.size() == amount_commitment_blinding_factors.size(), + "make bp+2 rangeproofs: mismatching amounts and blinding factors."); + + // make the range proofs + range_proofs_out = bulletproof_plus2_PROVE(amounts, amount_commitment_blinding_factors); +} +//------------------------------------------------------------------------------------------------------------------- +void append_bpp2_to_transcript(const BulletproofPlus2 &bpp2_proof, SpTranscriptBuilder &transcript_inout) +{ + transcript_inout.append("V", bpp2_proof.V); + transcript_inout.append("A", bpp2_proof.A); + transcript_inout.append("A1", bpp2_proof.A1); + transcript_inout.append("B", bpp2_proof.B); + transcript_inout.append("r1", bpp2_proof.r1); + transcript_inout.append("s1", bpp2_proof.s1); + transcript_inout.append("d1", bpp2_proof.d1); + transcript_inout.append("L", bpp2_proof.L); + transcript_inout.append("R", bpp2_proof.R); +} +//------------------------------------------------------------------------------------------------------------------- +std::size_t bpp_size_bytes(const std::size_t num_range_proofs, const bool include_commitments) +{ + // BP+ size: 32 * (2*ceil(log2(64 * num range proofs)) + 6) + std::size_t proof_size{32 * (2 * highest_bit_position(round_up_to_power_of_2(64 * num_range_proofs)) + 6)}; + + // size of commitments that are range proofed (if requested) + if (include_commitments) + proof_size += 32 * num_range_proofs; + + return proof_size; +} +//------------------------------------------------------------------------------------------------------------------- +std::size_t bpp_weight(const std::size_t num_range_proofs, const bool include_commitments) +{ + // BP+ size: 32 * (2*ceil(log2(64 * num range proofs)) + 6) + // BP+ size (2 range proofs): 32 * 20 + // weight = size(proof) + 0.8 * (32*20*(num range proofs + num dummy range proofs)/2) - size(proof)) + // explanation: 'claw back' 80% of the size of this BP+ if it were split into proofs of pairs of range proofs + // note: the weight can optionally include the commitments that are range proofed + + // BP+ size of an aggregate proof with two range proofs + const std::size_t size_two_agg_proof{32 * 20}; + + // number of BP+ proofs if this BP+ were split into proofs of pairs of range proofs + // num = (range proofs + dummy range proofs) / 2 + const std::size_t num_two_agg_groups{round_up_to_power_of_2(num_range_proofs) / 2}; + + // the proof size + const std::size_t proof_size{bpp_size_bytes(num_range_proofs, false)}; //don't include commitments here + + // size of commitments that are range proofed (if requested) + const std::size_t commitments_size{ + include_commitments + ? 32 * num_range_proofs + : 0 + }; + + // return the weight + return (2 * proof_size + 8 * size_two_agg_proof * num_two_agg_groups) / 10 + commitments_size; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace sp diff --git a/src/seraphis_crypto/sp_legacy_proof_helpers.h b/src/seraphis_crypto/sp_legacy_proof_helpers.h new file mode 100644 index 0000000000..a711a4668a --- /dev/null +++ b/src/seraphis_crypto/sp_legacy_proof_helpers.h @@ -0,0 +1,107 @@ +// Copyright (c) 2022, 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. + +// Miscellaneous utility functions. + +#pragma once + +//local headers +#include "bulletproofs_plus2.h" +#include "crypto/crypto.h" +#include "ringct/rctTypes.h" + +//third party headers + +//standard headers +#include + +//forward declarations +namespace sp { class SpTranscriptBuilder; } + +namespace sp +{ + +/** +* brief: append_clsag_to_transcript - append CLSAG proof to a transcript +* transcript += {s} || c1 || I || D +* param: clsag_proof - +* inoutparam: transcript_inout - contents appended to a transcript +*/ +void append_clsag_to_transcript(const rct::clsag &clsag_proof, SpTranscriptBuilder &transcript_inout); +/** +* brief: clsag_size_bytes - get the size of a CLSAG proof in bytes +* - CLSAG size: 32 * (ring size + 2) +* note: the main key image 'I' is not included (it is assumed to be a cached value) +* param: ring_size - +* return: the CLSAG proof's size in bytes +*/ +std::size_t clsag_size_bytes(const std::size_t ring_size); +/** +* brief: make_bpp2_rangeproofs - make a BP+ v2 proof that aggregates several range proofs +* param: amounts - +* param: amount_commitment_blinding_factors - +* outparam: range_proofs_out - aggregate set of amount commitments with range proofs +*/ +void make_bpp2_rangeproofs(const std::vector &amounts, + const std::vector &amount_commitment_blinding_factors, + BulletproofPlus2 &range_proofs_out); +/** +* brief: append_bpp2_to_transcript - append BP+ v2 proof to a transcript +* transcript += {V} || A || A1 || B || r1 || s1 || d1 || {L} || {R} +* param: bpp_proof - +* inoutparam: transcript_inout - contents appended to a transcript +*/ +void append_bpp2_to_transcript(const BulletproofPlus2 &bpp_proof, SpTranscriptBuilder &transcript_inout); +/** +* brief: bpp_size_bytes - get the size of a BP+ proof in bytes +* - BP+ size: 32 * (2*ceil(log2(64 * num range proofs)) + 6) +* param: num_range_proofs - +* param: include_commitments - +* return: the BP+ proof's size in bytes +*/ +std::size_t bpp_size_bytes(const std::size_t num_range_proofs, const bool include_commitments); +/** +* brief: bpp_weight - get the 'weight' of a BP+ proof +* - Verifying a BP+ is linear in the number of aggregated range proofs, but the proof size is logarithmic, +* so the cost of verifying a BP+ isn't proportional to the proof size. To get that proportionality, we 'claw back' +* some of the 'aggregated' proof's size. +* - An aggregate BP+ has 'step-wise' verification costs. It contains 'dummy range proofs' so that the number of +* actual aggregated proofs equals the next power of 2 >= the number of range proofs desired. +* - To 'price in' the additional verification costs from batching range proofs, we add a 'clawback' to the proof size, +* which gives us the proof 'weight'. The clawback is the additional proof size if all the range proofs and dummy +* range proofs were split into 2-aggregate BP+ proofs (with a 20% discount as 'reward' for using an aggregate proof). +* +* weight = size(proof) + clawback +* clawback = 0.8 * [(num range proofs + num dummy range proofs)*size(BP+ proof with 2 range proofs) - size(proof)] +* param: num_range_proofs - +* param: include_commitments - +* return: the BP+ proof's weight +*/ +std::size_t bpp_weight(const std::size_t num_range_proofs, const bool include_commitments); + +} //namespace sp diff --git a/src/seraphis_crypto/sp_multiexp.cpp b/src/seraphis_crypto/sp_multiexp.cpp new file mode 100644 index 0000000000..880bee69cc --- /dev/null +++ b/src/seraphis_crypto/sp_multiexp.cpp @@ -0,0 +1,278 @@ +// Copyright (c) 2022, 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 "sp_multiexp.h" + +//local headers +extern "C" +{ +#include "crypto/crypto-ops.h" +} +#include "crypto/generators.h" +#include "misc_log_ex.h" +#include "ringct/multiexp.h" +#include "ringct/rctOps.h" +#include "ringct/rctTypes.h" +#include "sp_generator_factory.h" + +//third party headers + +//standard headers +#include +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis" + +namespace sp +{ +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void weight_scalar(const boost::optional &weight, rct::key &scalar_inout) +{ + // s *= weight + if (weight) + sc_mul(scalar_inout.bytes, weight->bytes, scalar_inout.bytes); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void update_scalar(const rct::key &new_scalar, rct::key &scalar_inout) +{ + // s += s_new + if (scalar_inout == rct::zero()) + scalar_inout = new_scalar; + else + sc_add(scalar_inout.bytes, scalar_inout.bytes, new_scalar.bytes); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void update_scalar(const rct::key &new_scalar, boost::optional &scalar_inout) +{ + if (!scalar_inout) + scalar_inout = rct::zero(); + + // s += s_new + update_scalar(new_scalar, *scalar_inout); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void update_scalar(const boost::optional &new_scalar, rct::key &scalar_inout) +{ + if (!new_scalar) + return; + + // s += s_new + update_scalar(*new_scalar, scalar_inout); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void prepare_multiexp_cached_generators(const std::size_t num_predef_gen_elements, + rct::pippenger_cached_data &cached_base_points_inout, + std::vector &elements_collected_inout) +{ + // make sure generators requested are available + CHECK_AND_ASSERT_THROW_MES(num_predef_gen_elements <= generator_factory::max_generator_count(), + "prepare sp multiexp cached generators: too many elements were requested."); + + // default initialize caches + cached_base_points_inout.clear(); + cached_base_points_inout.resize(4 + num_predef_gen_elements); + elements_collected_inout.clear(); + elements_collected_inout.resize(4 + num_predef_gen_elements, {rct::zero(), ge_p3_identity}); + + // set generators + cached_base_points_inout[0] = crypto::get_G_cached(); + cached_base_points_inout[1] = crypto::get_H_cached(); + cached_base_points_inout[2] = crypto::get_X_cached(); + cached_base_points_inout[3] = crypto::get_U_cached(); + + elements_collected_inout[0].point = crypto::get_G_p3(); + elements_collected_inout[1].point = crypto::get_H_p3(); + elements_collected_inout[2].point = crypto::get_X_p3(); + elements_collected_inout[3].point = crypto::get_U_p3(); + + for (std::size_t gen_index{0}; gen_index < num_predef_gen_elements; ++gen_index) + { + cached_base_points_inout[4 + gen_index] = generator_factory::get_generator_at_index_cached(gen_index); + elements_collected_inout[4 + gen_index].point = generator_factory::get_generator_at_index_p3(gen_index); + } +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +SpMultiexpBuilder::SpMultiexpBuilder(const rct::key &weight, + const std::size_t estimated_num_predefined_generator_elements, + const std::size_t estimated_num_user_defined_elements) +{ + CHECK_AND_ASSERT_THROW_MES(!(weight == rct::zero()), "multiexp builder: element weight is zero."); + CHECK_AND_ASSERT_THROW_MES(sc_check(weight.bytes) == 0, "multiexp builder: element weight is not canonical."); + + // only initialize weight if not identity + if (!(weight == rct::identity())) + m_weight = weight; + + m_predef_scalars.resize(estimated_num_predefined_generator_elements, rct::zero()); + m_user_def_elements.reserve(estimated_num_user_defined_elements); +} +//------------------------------------------------------------------------------------------------------------------- +void SpMultiexpBuilder::add_G_element(rct::key scalar) +{ + weight_scalar(m_weight, scalar); + update_scalar(scalar, m_G_scalar); +} +//------------------------------------------------------------------------------------------------------------------- +void SpMultiexpBuilder::add_H_element(rct::key scalar) +{ + weight_scalar(m_weight, scalar); + update_scalar(scalar, m_H_scalar); +} +//------------------------------------------------------------------------------------------------------------------- +void SpMultiexpBuilder::add_X_element(rct::key scalar) +{ + weight_scalar(m_weight, scalar); + update_scalar(scalar, m_X_scalar); +} +//------------------------------------------------------------------------------------------------------------------- +void SpMultiexpBuilder::add_U_element(rct::key scalar) +{ + weight_scalar(m_weight, scalar); + update_scalar(scalar, m_U_scalar); +} +//------------------------------------------------------------------------------------------------------------------- +void SpMultiexpBuilder::add_element_at_generator_index(rct::key scalar, const std::size_t predef_generator_index) +{ + if (m_predef_scalars.size() < predef_generator_index + 1) + m_predef_scalars.resize(predef_generator_index + 1, rct::zero()); + + weight_scalar(m_weight, scalar); + update_scalar(scalar, m_predef_scalars[predef_generator_index]); +} +//------------------------------------------------------------------------------------------------------------------- +void SpMultiexpBuilder::add_element(rct::key scalar, const ge_p3 &base_point) +{ + // early return on cheap zero scalar check + if (scalar == rct::zero()) + return; + + weight_scalar(m_weight, scalar); + m_user_def_elements.emplace_back(scalar, base_point); +} +//------------------------------------------------------------------------------------------------------------------- +void SpMultiexpBuilder::add_element(const rct::key &scalar, const rct::key &base_point) +{ + // early return on cheap identity check + if (base_point == rct::identity()) + return; + + ge_p3 base_point_p3; + CHECK_AND_ASSERT_THROW_MES(ge_frombytes_vartime(&base_point_p3, base_point.bytes) == 0, + "ge_frombytes_vartime failed!"); + this->add_element(scalar, base_point_p3); +} +//------------------------------------------------------------------------------------------------------------------- +void SpMultiexpBuilder::add_element(const rct::key &scalar, const crypto::public_key &base_point) +{ + this->add_element(scalar, rct::pk2rct(base_point)); +} +//------------------------------------------------------------------------------------------------------------------- +SpMultiexp::SpMultiexp(const std::list &multiexp_builders) +{ + // figure out how many elements there are + std::size_t num_predef_gen_elements{0}; + std::size_t num_user_def_elements{0}; + + for (const SpMultiexpBuilder &multiexp_builder : multiexp_builders) + { + if (num_predef_gen_elements < multiexp_builder.m_predef_scalars.size()) + num_predef_gen_elements = multiexp_builder.m_predef_scalars.size(); + + num_user_def_elements += multiexp_builder.m_user_def_elements.size(); + } + + // 1. prepare generators + std::shared_ptr cached_base_points{std::make_shared()}; + cached_base_points->reserve(4 + num_predef_gen_elements + num_user_def_elements); + + std::vector elements_collected; + elements_collected.reserve(4 + num_predef_gen_elements + num_user_def_elements); + + prepare_multiexp_cached_generators(num_predef_gen_elements, *cached_base_points, elements_collected); + + CHECK_AND_ASSERT_THROW_MES(cached_base_points->size() == 4 + num_predef_gen_elements, + "sp multiexp sanity check: cached base points wrong size after prepared."); + CHECK_AND_ASSERT_THROW_MES(elements_collected.size() == 4 + num_predef_gen_elements, + "sp multiexp sanity check: elements collected wrong size after prepared."); + + // 2. collect scalars and expand cached points with user-defined elements + for (const SpMultiexpBuilder &multiexp_builder : multiexp_builders) + { + // main generators + update_scalar(multiexp_builder.m_G_scalar, elements_collected[0].scalar); + update_scalar(multiexp_builder.m_H_scalar, elements_collected[1].scalar); + update_scalar(multiexp_builder.m_X_scalar, elements_collected[2].scalar); + update_scalar(multiexp_builder.m_U_scalar, elements_collected[3].scalar); + + // pre-defined generators + for (std::size_t predef_generator_index{0}; + predef_generator_index < multiexp_builder.m_predef_scalars.size(); + ++predef_generator_index) + { + sc_add(elements_collected[4 + predef_generator_index].scalar.bytes, + elements_collected[4 + predef_generator_index].scalar.bytes, + multiexp_builder.m_predef_scalars[predef_generator_index].bytes); + } + + // user-defined elements + for (const rct::MultiexpData &element : multiexp_builder.m_user_def_elements) + { + cached_base_points->emplace_back(); + ge_p3_to_cached(&(cached_base_points->back()), &element.point); + elements_collected.emplace_back(element.scalar, element.point); + } + } + + // 3. evaluate the multiexponentiation + m_result = pippenger_p3(elements_collected, cached_base_points, cached_base_points->size()); +} +//------------------------------------------------------------------------------------------------------------------- +bool SpMultiexp::evaluates_to_point_at_infinity() const +{ + return ge_p3_is_point_at_infinity_vartime(&m_result) != 0; +} +//------------------------------------------------------------------------------------------------------------------- +void SpMultiexp::get_result(rct::key &result_out) const +{ + ge_p3_tobytes(result_out.bytes, &m_result); +} +//------------------------------------------------------------------------------------------------------------------- +void SpMultiexp::get_result_p3(ge_p3 &result_out) const +{ + result_out = m_result; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace sp diff --git a/src/seraphis_crypto/sp_multiexp.h b/src/seraphis_crypto/sp_multiexp.h new file mode 100644 index 0000000000..3297b7d0f3 --- /dev/null +++ b/src/seraphis_crypto/sp_multiexp.h @@ -0,0 +1,128 @@ +// Copyright (c) 2022, 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 performing multiexponentiations. + +#pragma once + +//local headers +extern "C" +{ +#include "crypto/crypto-ops.h" +} +#include "ringct/multiexp.h" +#include "ringct/rctOps.h" +#include "ringct/rctTypes.h" + +//third party headers +#include + +//standard headers +#include +#include + +//forward declarations + + +namespace sp +{ + +//// +// SpMultiexpBuilder +// - collect data points for a multiexponentiation +// - all data points added to the builder are weighted by some factor w +// - for efficiency, pre-defined generators are provided +// - multiexp stored: w * (a G + b_0 G_0 + ... + b_n G_n + c_0 P_0 + ... + c_m P_m) +// - G, H, X, U: ed25519 generators +// - G_0..G_n: generators defined in 'generator_factory' namespace +// - P_0..P_m: user-defined base points +/// +class SpMultiexpBuilder final +{ + friend class SpMultiexp; + +public: +//constructors + /// normal constructor + /// - define a non-zero weight to apply to all elements + /// - use identity if this builder won't be merged with other builders + SpMultiexpBuilder(const rct::key &weight, + const std::size_t estimated_num_predefined_generator_elements, + const std::size_t estimated_num_user_defined_elements); + +//member functions + void add_G_element(rct::key scalar); + void add_H_element(rct::key scalar); + void add_X_element(rct::key scalar); + void add_U_element(rct::key scalar); + void add_element_at_generator_index(rct::key scalar, const std::size_t predef_generator_index); + void add_element(rct::key scalar, const ge_p3 &base_point); + void add_element(const rct::key &scalar, const rct::key &base_point); + void add_element(const rct::key &scalar, const crypto::public_key &base_point); + +//member variables +protected: + /// ed25519 generator scalar + boost::optional m_G_scalar; + /// Pedersen commitment generator scalar + boost::optional m_H_scalar; + /// seraphis spend key extension generator scalar + boost::optional m_X_scalar; + /// seraphis spend key generator scalar + boost::optional m_U_scalar; + /// pre-defined generators scalars + std::vector m_predef_scalars; + /// user-defined [scalar, base point] pairs + std::vector m_user_def_elements; + +private: + /// element weight + boost::optional m_weight; +}; + +//// +// SpMultiexp +// - use a set of multiexp builders to perform a multiexponentiation, then store the result +/// +class SpMultiexp final +{ +public: +//constructors + SpMultiexp(const std::list &multiexp_builders); + +//member functions + bool evaluates_to_point_at_infinity() const; + void get_result(rct::key &result) const; + void get_result_p3(ge_p3 &result) const; + +//member variables +private: + ge_p3 m_result; +}; + +} //namespace sp diff --git a/src/seraphis_crypto/sp_transcript.h b/src/seraphis_crypto/sp_transcript.h new file mode 100644 index 0000000000..58b86edc20 --- /dev/null +++ b/src/seraphis_crypto/sp_transcript.h @@ -0,0 +1,415 @@ +// Copyright (c) 2022, 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 "crypto/crypto.h" +#include "crypto/x25519.h" +#include "cryptonote_config.h" +#include "ringct/rctTypes.h" +#include "wipeable_string.h" + +//third party headers +#include + +//standard headers +#include +#include +#include +#include + +//forward declarations + + +namespace sp +{ + +//// +// SpTranscriptBuilder +// - build a transcript +// - the user must provide a label when trying to append something to the transcript; labels are prepended to objects in +// the transcript +// - data types +// - unsigned int: uint_flag || varint(uint_variable) +// - signed int: int_flag || uchar{int_variable < 0 ? 1 : 0} || varint(abs(int_variable)) +// - byte buffer (assumed little-endian): buffer_flag || buffer_length || buffer +// - all labels are treated as byte buffers +// - named container: container_flag || container_name || data_member1 || ... || container_terminator_flag +// - list-type container (same-type elements only): list_flag || list_length || element1 || element2 || ... +// - the transcript can be used like a string via the .data() and .size() member functions +// - simple mode: exclude all labels, flags, and lengths +/// +class SpTranscriptBuilder final +{ +public: +//public member types + /// transcript builder mode + enum class Mode + { + FULL, + SIMPLE + }; + +//constructors + /// normal constructor + SpTranscriptBuilder(const std::size_t estimated_data_size, const Mode mode) : + m_mode{mode} + { + m_transcript.reserve(2 * estimated_data_size + 20); + } + +//overloaded operators + /// disable copy/move (this is a scoped manager [of the 'transcript' concept]) + SpTranscriptBuilder& operator=(SpTranscriptBuilder&&) = delete; + +//member functions + /// append a value to the transcript + template + void append(const boost::string_ref label, const T &value) + { + this->append_impl(label, value); + } + + /// access the transcript data + const void* data() const { return m_transcript.data(); } + std::size_t size() const { return m_transcript.size(); } + +private: +//member variables + /// in simple mode, exclude labels, flags, and lengths + Mode m_mode; + /// the transcript buffer (wipeable in case it contains sensitive data) + epee::wipeable_string m_transcript; + +//private member types + /// flags for separating items added to the transcript + enum SpTranscriptBuilderFlag : unsigned char + { + UNSIGNED_INTEGER = 0, + SIGNED_INTEGER = 1, + BYTE_BUFFER = 2, + NAMED_CONTAINER = 3, + NAMED_CONTAINER_TERMINATOR = 4, + LIST_TYPE_CONTAINER = 5 + }; + +//transcript builders + void append_character(const unsigned char character) + { + m_transcript += static_cast(character); + } + void append_uint(const std::uint64_t unsigned_integer) + { + unsigned char v_variable[(sizeof(std::uint64_t) * 8 + 6) / 7]; + unsigned char *v_variable_end = v_variable; + + // append uint to string as a varint + tools::write_varint(v_variable_end, unsigned_integer); + assert(v_variable_end <= v_variable + sizeof(v_variable)); + m_transcript.append(reinterpret_cast(v_variable), v_variable_end - v_variable); + } + void append_flag(const SpTranscriptBuilderFlag flag) + { + if (m_mode == Mode::SIMPLE) + return; + + static_assert(sizeof(SpTranscriptBuilderFlag) <= sizeof(std::uint64_t), ""); + this->append_character(static_cast(flag)); + } + void append_length(const std::size_t length) + { + if (m_mode == Mode::SIMPLE) + return; + + static_assert(sizeof(std::size_t) <= sizeof(std::uint64_t), ""); + this->append_uint(static_cast(length)); + } + void append_buffer(const void *data, const std::size_t length) + { + this->append_flag(SpTranscriptBuilderFlag::BYTE_BUFFER); + this->append_length(length); + m_transcript.append(reinterpret_cast(data), length); + } + void append_label(const boost::string_ref label) + { + if (m_mode == Mode::SIMPLE || + label.size() == 0) + return; + + this->append_buffer(label.data(), label.size()); + } + void append_labeled_buffer(const boost::string_ref label, const void *data, const std::size_t length) + { + this->append_label(label); + this->append_buffer(data, length); + } + void begin_named_container(const boost::string_ref container_name) + { + this->append_flag(SpTranscriptBuilderFlag::NAMED_CONTAINER); + this->append_label(container_name); + } + void end_named_container() + { + this->append_flag(SpTranscriptBuilderFlag::NAMED_CONTAINER_TERMINATOR); + } + void begin_list_type_container(const std::size_t list_length) + { + this->append_flag(SpTranscriptBuilderFlag::LIST_TYPE_CONTAINER); + this->append_length(list_length); + } + +//append overloads + void append_impl(const boost::string_ref label, const rct::key &key_buffer) + { + this->append_labeled_buffer(label, key_buffer.bytes, sizeof(key_buffer)); + } + void append_impl(const boost::string_ref label, const rct::ctkey &ctkey) + { + this->append_label(label); + this->append_impl("ctmask", ctkey.mask); + this->append_impl("ctdest", ctkey.dest); + } + void append_impl(const boost::string_ref label, const crypto::secret_key &point_buffer) + { + this->append_labeled_buffer(label, point_buffer.data, sizeof(point_buffer)); + } + void append_impl(const boost::string_ref label, const crypto::public_key &scalar_buffer) + { + this->append_labeled_buffer(label, scalar_buffer.data, sizeof(scalar_buffer)); + } + void append_impl(const boost::string_ref label, const crypto::key_derivation &derivation_buffer) + { + this->append_labeled_buffer(label, derivation_buffer.data, sizeof(derivation_buffer)); + } + void append_impl(const boost::string_ref label, const crypto::key_image &key_image_buffer) + { + this->append_labeled_buffer(label, key_image_buffer.data, sizeof(key_image_buffer)); + } + void append_impl(const boost::string_ref label, const crypto::x25519_scalar &x25519_scalar_buffer) + { + this->append_labeled_buffer(label, x25519_scalar_buffer.data, sizeof(x25519_scalar_buffer)); + } + void append_impl(const boost::string_ref label, const crypto::x25519_pubkey &x25519_pubkey_buffer) + { + this->append_labeled_buffer(label, x25519_pubkey_buffer.data, sizeof(x25519_pubkey_buffer)); + } + void append_impl(const boost::string_ref label, const std::string &string_buffer) + { + this->append_labeled_buffer(label, string_buffer.data(), string_buffer.size()); + } + void append_impl(const boost::string_ref label, const epee::wipeable_string &string_buffer) + { + this->append_labeled_buffer(label, string_buffer.data(), string_buffer.size()); + } + void append_impl(const boost::string_ref label, const boost::string_ref string_buffer) + { + this->append_labeled_buffer(label, string_buffer.data(), string_buffer.size()); + } + template + void append_impl(const boost::string_ref label, const unsigned char(&uchar_buffer)[Sz]) + { + this->append_labeled_buffer(label, uchar_buffer, Sz); + } + template + void append_impl(const boost::string_ref label, const char(&char_buffer)[Sz]) + { + this->append_labeled_buffer(label, char_buffer, Sz); + } + void append_impl(const boost::string_ref label, const std::vector &vector_buffer) + { + this->append_labeled_buffer(label, vector_buffer.data(), vector_buffer.size()); + } + void append_impl(const boost::string_ref label, const std::vector &vector_buffer) + { + this->append_labeled_buffer(label, vector_buffer.data(), vector_buffer.size()); + } + void append_impl(const boost::string_ref label, const char single_character) + { + this->append_label(label); + this->append_character(static_cast(single_character)); + } + void append_impl(const boost::string_ref label, const unsigned char single_character) + { + this->append_label(label); + this->append_character(single_character); + } + template::value, bool> = true> + void append_impl(const boost::string_ref label, const T unsigned_integer) + { + static_assert(sizeof(T) <= sizeof(std::uint64_t), "SpTranscriptBuilder: unsupported unsigned integer type."); + this->append_label(label); + this->append_flag(SpTranscriptBuilderFlag::UNSIGNED_INTEGER); + this->append_uint(unsigned_integer); + } + template::value, bool> = true, + std::enable_if_t::value, bool> = true> + void append_impl(const boost::string_ref label, const T signed_integer) + { + static_assert(sizeof(T) <= sizeof(std::uint64_t), "SpTranscriptBuilder: unsupported signed integer type."); + this->append_label(label); + this->append_flag(SpTranscriptBuilderFlag::SIGNED_INTEGER); + if (signed_integer >= 0) + { + // positive integer: byte{0} || varint(uint(int_variable)) + this->append_uint(0); + this->append_uint(static_cast(signed_integer)); + } + else + { + // negative integer: byte{1} || varint(uint(abs(int_variable))) + this->append_uint(1); + this->append_uint(static_cast(-signed_integer)); + } + } + template::value, bool> = true> + void append_impl(const boost::string_ref label, const T &named_container) + { + // named containers must satisfy two concepts: + // const boost::string_ref container_name(const T &container); + // void append_to_transcript(const T &container, SpTranscriptBuilder &transcript_inout); + //todo: container_name() and append_to_transcript() must be defined in the sp namespace, but that is not generic + this->append_label(label); + this->begin_named_container(container_name(named_container)); + append_to_transcript(named_container, *this); //non-member function assumed to be implemented elsewhere + this->end_named_container(); + } + template + void append_impl(const boost::string_ref label, const std::vector &list_container) + { + this->append_label(label); + this->begin_list_type_container(list_container.size()); + for (const T &element : list_container) + this->append_impl("", element); + } + template + void append_impl(const boost::string_ref label, const std::list &list_container) + { + this->append_label(label); + this->begin_list_type_container(list_container.size()); + for (const T &element : list_container) + this->append_impl("", element); + } +}; + +//// +// SpFSTranscript +// - build a Fiat-Shamir transcript +// - main format: prefix || domain_separator || object1_label || object1 || object2_label || object2 || ... +// note: prefix defaults to "monero" +/// +class SpFSTranscript final +{ +public: +//constructors + /// normal constructor: start building a transcript with the domain separator + SpFSTranscript(const boost::string_ref domain_separator, + const std::size_t estimated_data_size, + const boost::string_ref prefix = config::TRANSCRIPT_PREFIX) : + m_transcript_builder{15 + domain_separator.size() + estimated_data_size + prefix.size(), + SpTranscriptBuilder::Mode::FULL} + { + // transcript = prefix || domain_separator + m_transcript_builder.append("FSp", prefix); + m_transcript_builder.append("ds", domain_separator); + } + +//overloaded operators + /// disable copy/move (this is a scoped manager [of the 'transcript' concept]) + SpFSTranscript& operator=(SpFSTranscript&&) = delete; + +//member functions + /// build the transcript + template + void append(const boost::string_ref label, const T &value) + { + m_transcript_builder.append(label, value); + } + + /// access the transcript data + const void* data() const { return m_transcript_builder.data(); } + std::size_t size() const { return m_transcript_builder.size(); } + +//member variables +private: + /// underlying transcript builder + SpTranscriptBuilder m_transcript_builder; +}; + +//// +// SpKDFTranscript +// - build a data string for a key-derivation function +// - mainly intended for short transcripts (~128 bytes or less) with fixed-length and statically ordered components +// - main format: prefix || domain_separator || object1 || object2 || ... +// - simple transcript mode: no labels, flags, or lengths +// note: prefix defaults to "monero" +/// +class SpKDFTranscript final +{ +public: +//constructors + /// normal constructor: start building a transcript with the domain separator + SpKDFTranscript(const boost::string_ref domain_separator, + const std::size_t estimated_data_size, + const boost::string_ref prefix = config::TRANSCRIPT_PREFIX) : + m_transcript_builder{10 + domain_separator.size() + estimated_data_size + prefix.size(), + SpTranscriptBuilder::Mode::SIMPLE} + { + // transcript = prefix || domain_separator + m_transcript_builder.append("", prefix); + m_transcript_builder.append("", domain_separator); + } + +//overloaded operators + /// disable copy/move (this is a scoped manager [of the 'transcript' concept]) + SpKDFTranscript& operator=(SpKDFTranscript&&) = delete; + +//member functions + /// build the transcript + template + void append(const boost::string_ref, const T &value) + { + m_transcript_builder.append("", value); + } + + /// access the transcript data + const void* data() const { return m_transcript_builder.data(); } + std::size_t size() const { return m_transcript_builder.size(); } + +//member variables +private: + /// underlying transcript builder + SpTranscriptBuilder m_transcript_builder; +}; + +} //namespace sp diff --git a/src/seraphis_impl/CMakeLists.txt b/src/seraphis_impl/CMakeLists.txt new file mode 100644 index 0000000000..4ee232b540 --- /dev/null +++ b/src/seraphis_impl/CMakeLists.txt @@ -0,0 +1,66 @@ +# Copyright (c) 2021, 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(seraphis_impl_sources + checkpoint_cache.cpp + enote_store.cpp + enote_store_payment_validator.cpp + enote_store_utils.cpp + legacy_ki_import_tool.cpp + scan_ledger_chunk_async.cpp + scan_process_basic.cpp + serialization_demo_utils.cpp + tx_builder_utils.cpp + tx_fee_calculator_squashed_v1.cpp + tx_input_selection_output_context_v1.cpp) + +monero_find_all_headers(seraphis_impl_headers, "${CMAKE_CURRENT_SOURCE_DIR}") + +monero_add_library(seraphis_impl + ${seraphis_impl_sources} + ${seraphis_impl_headers}) + +target_link_libraries(seraphis_impl + PUBLIC + cncrypto + common + cryptonote_basic + device + epee + ringct + seraphis_core + seraphis_crypto + seraphis_main + PRIVATE + ${EXTRA_LIBRARIES}) + +target_include_directories(seraphis_impl + PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}" + PRIVATE + ${Boost_INCLUDE_DIRS}) diff --git a/src/seraphis_impl/checkpoint_cache.cpp b/src/seraphis_impl/checkpoint_cache.cpp new file mode 100644 index 0000000000..5651eb0424 --- /dev/null +++ b/src/seraphis_impl/checkpoint_cache.cpp @@ -0,0 +1,299 @@ +// Copyright (c) 2022, 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 "checkpoint_cache.h" + +//local headers +#include "misc_log_ex.h" +#include "ringct/rctOps.h" +#include "ringct/rctTypes.h" +#include "seraphis_crypto/math_utils.h" + +//third party headers + +//standard headers +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis_impl" + +namespace sp +{ +//------------------------------------------------------------------------------------------------------------------- +CheckpointCache::CheckpointCache(const CheckpointCacheConfig &config, const std::uint64_t min_checkpoint_index) : + m_config{config}, + m_min_checkpoint_index{min_checkpoint_index} +{ + CHECK_AND_ASSERT_THROW_MES(m_config.max_separation < math::uint_pow(2, 32), + "checkpoint cache (constructor): max_separation must be < 2^32."); //heuristic to avoid overflow issues + CHECK_AND_ASSERT_THROW_MES(m_config.num_unprunable >= 1, + "checkpoint cache (constructor): num unprunable must be >= 1."); + CHECK_AND_ASSERT_THROW_MES(m_config.density_factor >= 1, + "checkpoint cache (constructor): density_factor must be >= 1."); +} +//------------------------------------------------------------------------------------------------------------------- +std::uint64_t CheckpointCache::top_block_index() const +{ + if (this->num_checkpoints() == 0) + return m_min_checkpoint_index - 1; + + return m_checkpoints.crbegin()->first; +} +//------------------------------------------------------------------------------------------------------------------- +std::uint64_t CheckpointCache::bottom_block_index() const +{ + if (this->num_checkpoints() == 0) + return m_min_checkpoint_index - 1; + + return m_checkpoints.cbegin()->first; +} +//------------------------------------------------------------------------------------------------------------------- +std::uint64_t CheckpointCache::get_next_block_index(const std::uint64_t test_index) const +{ + // 1. special case: test index == -1 + if (test_index == static_cast(-1) && + m_checkpoints.size() > 0) + return m_checkpoints.cbegin()->first; + + // 2. get closest checkpoint > test index + auto test_checkpoint = m_checkpoints.upper_bound(test_index); + + // 3. edge condition: no checkpoints above test index + if (test_checkpoint == m_checkpoints.cend()) + return -1; + + // 4. return next block index + return test_checkpoint->first; +} +//------------------------------------------------------------------------------------------------------------------- +std::uint64_t CheckpointCache::get_nearest_block_index(const std::uint64_t test_index) const +{ + // get the block index of the closest checkpoint <= the test index + + // 1. early return if: + // - no checkpoints + // - test index is -1 + if (this->num_checkpoints() == 0 || + test_index == static_cast(-1)) + return m_min_checkpoint_index - 1; + + // 2. get closest checkpoint > test index + auto test_checkpoint = m_checkpoints.upper_bound(test_index); + + // 3. edge condition: if test index >= highest checkpoint, return the highest checkpoint + if (test_checkpoint == m_checkpoints.cend()) + return m_checkpoints.crbegin()->first; + + // 4. edge condition: if test index < lowest checkpoint, return failure + if (test_checkpoint == m_checkpoints.cbegin()) + return m_min_checkpoint_index - 1; + + // 5. normal case: there is a checkpoint <= the test index + return (--test_checkpoint)->first; +} +//------------------------------------------------------------------------------------------------------------------- +bool CheckpointCache::try_get_block_id(const std::uint64_t block_index, rct::key &block_id_out) const +{ + // 1. check if the index is known + const auto checkpoint = m_checkpoints.find(block_index); + if (checkpoint == m_checkpoints.end()) + return false; + + // 2. return the block id + block_id_out = checkpoint->second; + return true; +} +//------------------------------------------------------------------------------------------------------------------- +void CheckpointCache::insert_new_block_ids(const std::uint64_t first_block_index, + const std::vector &new_block_ids) +{ + // 1. get number of new block ids to ignore + // - we ignore all block ids below our min index + const std::uint64_t num_new_to_ignore{ + first_block_index < m_min_checkpoint_index + ? m_min_checkpoint_index - first_block_index + : 0 + }; + + // 2. remove checkpoints in range [start of blocks to insert, end) + // - we always crop checkpoints even if the new block ids are all below our min index + m_checkpoints.erase( + m_checkpoints.lower_bound(first_block_index + num_new_to_ignore), + m_checkpoints.end() + ); + + // 3. insert new ids + for (std::uint64_t i{num_new_to_ignore}; i < new_block_ids.size(); ++i) + m_checkpoints[first_block_index + i] = new_block_ids[i]; + + // 4. prune excess checkpoints + this->prune_checkpoints(); +} +//------------------------------------------------------------------------------------------------------------------- +// CHECKPOINT CACHE INTERNAL +//------------------------------------------------------------------------------------------------------------------- +std::deque::const_iterator CheckpointCache::get_window_prune_candidate( + const std::deque &window) const +{ + // return the middle element + CHECK_AND_ASSERT_THROW_MES(window.size() > 0, + "checkpoint cache (get window prune candidate): window size is zero."); + return std::next(window.begin(), window.size() / 2); +} +//------------------------------------------------------------------------------------------------------------------- +// CHECKPOINT CACHE INTERNAL +//------------------------------------------------------------------------------------------------------------------- +std::uint64_t CheckpointCache::expected_checkpoint_separation(const std::uint64_t distance_from_highest_prunable) const +{ + // expected separation = distance/density_factor + if ((m_config.density_factor == 0) || + (distance_from_highest_prunable < m_config.density_factor)) + return 1; + return distance_from_highest_prunable/m_config.density_factor; +}; +//------------------------------------------------------------------------------------------------------------------- +// CHECKPOINT CACHE INTERNAL +//------------------------------------------------------------------------------------------------------------------- +bool CheckpointCache::window_is_prunable(const std::deque &window, + const std::uint64_t max_candidate_index) const +{ + // 1. sanity checks + CHECK_AND_ASSERT_THROW_MES(window.front() >= window.back(), + "checkpoint cache (should prune window): window range is invalid."); + + // 2. get the window's prune candidate in the window + const auto prune_candidate_it{this->get_window_prune_candidate(window)}; + CHECK_AND_ASSERT_THROW_MES(prune_candidate_it != window.end(), + "checkpoint cache (should prune window): could not get prune candidate."); + + // 3. window is not prunable if its candidate's index is above the max candidate index + const std::uint64_t prune_candidate_index{*prune_candidate_it}; + if (prune_candidate_index > max_candidate_index) + return false; + + CHECK_AND_ASSERT_THROW_MES(prune_candidate_index <= window.front() && + prune_candidate_index >= window.back(), + "checkpoint cache (should prune window): prune candidate outside window range."); + + // 4. don't prune if our prune candidate is in the 'don't prune' range + if (prune_candidate_index + m_config.num_unprunable > max_candidate_index) + return false; + + // 5. don't prune if our density is <= 1/max_separation + // - subtract 1 to account for the number of deltas in the window range + const std::uint64_t window_range{window.front() - window.back()}; + if (window_range >= (window.size() - 1) * m_config.max_separation) + return false; + + // 6. prune candidate's distance from the highest prunable element + // note: this should never overflow thanks to the 'is unprunable' check + const std::uint64_t distance_from_highest_prunable{ + (max_candidate_index - m_config.num_unprunable) - prune_candidate_index + }; + + // 7. expected separation at this distance from the top + const std::uint64_t expected_separation{this->expected_checkpoint_separation(distance_from_highest_prunable)}; + + // 8. test the expected separation + // - subtract 1 to account for the number of deltas in the window range + if (window_range >= (window.size() - 1) * expected_separation) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +// CHECKPOINT CACHE INTERNAL +//------------------------------------------------------------------------------------------------------------------- +void CheckpointCache::prune_checkpoints() +{ + // 1. sanity checks + if (this->num_checkpoints() == 0 || + this->num_checkpoints() <= m_config.num_unprunable) + return; + + // 2. highest checkpoint index + const std::uint64_t highest_checkpoint_index{this->top_block_index()}; + + // 3. initialize window with simulated elements above our highest checkpoint + // - window is sorted from highest to lowest + std::deque window; + + for (std::uint64_t window_index{0}; window_index < m_window_size; ++window_index) + window.push_front(highest_checkpoint_index + window_index + 1); + + // 4. slide the window from our highest checkpoint to our lowest checkpoint, pruning elements as we go + for (auto checkpoint_it = m_checkpoints.rbegin(); checkpoint_it != m_checkpoints.rend();) + { + // a. insert this checkpoint to our window (it is the lowest element in our window) + window.push_back(checkpoint_it->first); + + // b. early-increment the iterator so it is ready for whatever happens next + ++checkpoint_it; + + // c. skip to next checkpoint if our window is too small + if (window.size() < m_window_size) + continue; + + // d. trim the highest indices in our window + while (window.size() > m_window_size) + window.pop_front(); + + // e. skip to next checkpoint if this window is not prunable + if (!this->window_is_prunable(window, highest_checkpoint_index)) + continue; + + // f. get window element to prune + const auto window_prune_element{this->get_window_prune_candidate(window)}; + CHECK_AND_ASSERT_THROW_MES(window_prune_element != window.end(), + "checkpoint cache (pruning checkpoints): could not get prune candidate."); + + // g. remove the window element from our checkpoints (if it exists) + // - reverse iterators store the next element's iterator, so we need to do a little dance to avoid iterator + // invalidation + if (checkpoint_it != m_checkpoints.rend() && + checkpoint_it.base()->first == *window_prune_element) //this test works because elements are unique in a map + { + ++checkpoint_it; //increment off the element to be pruned + m_checkpoints.erase(*window_prune_element); + --checkpoint_it; //decrement back onto the element we need + } + else if (checkpoint_it == m_checkpoints.rend()) + { + m_checkpoints.erase(*window_prune_element); + checkpoint_it = std::reverse_iterator(m_checkpoints.begin()); + } + else + m_checkpoints.erase(*window_prune_element); + + // i. remove the pruned element from our window + window.erase(window_prune_element); + } +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace sp diff --git a/src/seraphis_impl/checkpoint_cache.h b/src/seraphis_impl/checkpoint_cache.h new file mode 100644 index 0000000000..7442bd9b09 --- /dev/null +++ b/src/seraphis_impl/checkpoint_cache.h @@ -0,0 +1,126 @@ +// Copyright (c) 2022, 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. + +// Checkpoint cache for storing a sequence of block ids whose density exponentially decays into the past. +// Note: A checkpoint cache is useful when you need to track block ids in order to identify and handle reorgs, +// because typically reorgs only affect very recent blocks. + +#pragma once + +//local headers +#include "ringct/rctTypes.h" + +//third party headers + +//standard headers +#include +#include + +//forward declarations + + +namespace sp +{ + +/// Configuration details for a checkpoint cache. +struct CheckpointCacheConfig final +{ + /// number of checkpoints that shouldn't be pruned + /// - affects the upper end of the stored checkpoints + std::uint64_t num_unprunable; + /// maximum separation between checkpoints + /// - affects the lower end of the stored checkpoints + std::uint64_t max_separation; + /// density factor for calibrating the decay rate of checkpoint density + /// - higher factor means more checkpoints are retained + std::uint64_t density_factor; +}; + +//// +// CheckpointCache +// - stores a sequence of checkpoints in the range of block ids [refresh index, highest known block index] +// - the pruning strategy is as follows: +// - range bottom: [refresh index, ..., (top index - num unprunable)] +// - exponentially falling density from the top of the range to the bottom of the range, +// with minimum density = 1/max_separation; pruning is achieved by sliding a window down +// the range and removing the middle window element if the index range covered by the window is too small; simulated +// elements are used for the top part of the range where the window would otherwise be hanging over 'empty space' +// - range top (not pruned): ((top index - num unprunable), top index] +// - the bottom-most and top-most blocks that have been inserted will not be pruned (they can be removed/replaced by +// subsequent inserts) +/// +class CheckpointCache +{ +public: +//constructors + CheckpointCache(const CheckpointCacheConfig &config, const std::uint64_t min_checkpoint_index); + +//member functions + /// get minimum allowed index + std::uint64_t min_checkpoint_index() const { return m_min_checkpoint_index; } + /// get the number of stored checkpoints + std::uint64_t num_checkpoints() const { return m_checkpoints.size(); } + /// get the highest stored index or 'min index - 1' if cache is empty + std::uint64_t top_block_index() const; + /// get the lowest stored index or 'min index - 1' if cache is empty + std::uint64_t bottom_block_index() const; + /// get the block index of the nearest checkpoint > the test index, or -1 on failure + /// note: it is allowed to test index -1 + std::uint64_t get_next_block_index(const std::uint64_t test_index) const; + /// get the block index of the nearest checkpoint <= the test index, or 'min index - 1' on failure + /// note: it is allowed to test index -1 + std::uint64_t get_nearest_block_index(const std::uint64_t test_index) const; + /// try to get the block id with the given index (fails if index is unknown) + bool try_get_block_id(const std::uint64_t block_index, rct::key &block_id_out) const; + + /// insert block ids starting at the specified index (all old blocks >= first_block_index will be removed) + void insert_new_block_ids(const std::uint64_t first_block_index, const std::vector &new_block_ids); + +private: + /// get the window's prune candidate + std::deque::const_iterator get_window_prune_candidate(const std::deque &window) const; + /// get the expected checkpoint separation at a given distance from the highest prunable block + std::uint64_t expected_checkpoint_separation(const std::uint64_t distance_from_highest_prunable) const; + /// test if a window is prunable + bool window_is_prunable(const std::deque &window, const std::uint64_t max_candidate_index) const; + /// remove prunable checkpoints + void prune_checkpoints(); + +//member variables + /// minimum checkpoint index + const std::uint64_t m_min_checkpoint_index; + + /// config + const CheckpointCacheConfig m_config; + static const std::uint64_t m_window_size{3}; + + /// stored checkpoints + std::map m_checkpoints; +}; + +} //namespace sp diff --git a/src/seraphis_impl/enote_store.cpp b/src/seraphis_impl/enote_store.cpp new file mode 100644 index 0000000000..7797b450b1 --- /dev/null +++ b/src/seraphis_impl/enote_store.cpp @@ -0,0 +1,1327 @@ +// Copyright (c) 2022, 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_store.h" + +//local headers +#include "common/container_helpers.h" +#include "misc_log_ex.h" +#include "seraphis_core/jamtis_support_types.h" +#include "seraphis_core/legacy_enote_types.h" +#include "seraphis_core/legacy_enote_utils.h" +#include "seraphis_impl/enote_store_event_types.h" +#include "seraphis_impl/enote_store_utils.h" +#include "seraphis_main/contextual_enote_record_types.h" +#include "seraphis_main/contextual_enote_record_utils.h" +#include "seraphis_main/enote_record_utils_legacy.h" + +//third party headers + +//standard headers +#include +#include +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis_impl" + +namespace sp +{ +//------------------------------------------------------------------------------------------------------------------- +SpEnoteStore::SpEnoteStore(const std::uint64_t refresh_index, + const std::uint64_t first_sp_enabled_block_in_chain, + const std::uint64_t default_spendable_age, + const CheckpointCacheConfig &checkpoint_cache_config) : + m_legacy_block_id_cache { checkpoint_cache_config, refresh_index }, + m_sp_block_id_cache { checkpoint_cache_config, std::max(refresh_index, first_sp_enabled_block_in_chain) }, + m_legacy_partialscan_index { refresh_index - 1 }, + m_legacy_fullscan_index { refresh_index - 1 }, + m_sp_scanned_index { refresh_index - 1 }, + m_default_spendable_age { default_spendable_age } +{} +//------------------------------------------------------------------------------------------------------------------- +std::uint64_t SpEnoteStore::legacy_refresh_index() const +{ + return m_legacy_block_id_cache.min_checkpoint_index(); +} +//------------------------------------------------------------------------------------------------------------------- +std::uint64_t SpEnoteStore::sp_refresh_index() const +{ + return m_sp_block_id_cache.min_checkpoint_index(); +} +//------------------------------------------------------------------------------------------------------------------- +std::uint64_t SpEnoteStore::top_block_index() const +{ + // 1. no blocks + if (m_legacy_block_id_cache.num_checkpoints() == 0 && + m_sp_block_id_cache.num_checkpoints() == 0) + return this->legacy_refresh_index() - 1; + + // 2. only have legacy blocks + if (m_sp_block_id_cache.num_checkpoints() == 0) + return m_legacy_block_id_cache.top_block_index(); + + // 3. only have seraphis blocks + if (m_legacy_block_id_cache.num_checkpoints() == 0) + return m_sp_block_id_cache.top_block_index(); + + // 4. have legacy and seraphis blocks + return std::max( + m_legacy_block_id_cache.top_block_index(), + m_sp_block_id_cache.top_block_index() + ); +} +//------------------------------------------------------------------------------------------------------------------- +std::uint64_t SpEnoteStore::next_legacy_partialscanned_block_index(const std::uint64_t block_index) const +{ + // 1. get the cached legacy block index > the requested index + const std::uint64_t next_index{m_legacy_block_id_cache.get_next_block_index(block_index)}; + + // 2. assume a block is 'unknown' if its index is above the last legacy partial-scanned block index + if (next_index + 1 > m_legacy_partialscan_index + 1) + return -1; + + return next_index; +} +//------------------------------------------------------------------------------------------------------------------- +std::uint64_t SpEnoteStore::next_legacy_fullscanned_block_index(const std::uint64_t block_index) const +{ + // 1. get the cached legacy block index > the requested index + const std::uint64_t next_index{m_legacy_block_id_cache.get_next_block_index(block_index)}; + + // 2. assume a block is 'unknown' if its index is above the last legacy full-scanned block index + if (block_index + 1 > m_legacy_fullscan_index + 1) + return -1; + + return next_index; +} +//------------------------------------------------------------------------------------------------------------------- +std::uint64_t SpEnoteStore::next_sp_scanned_block_index(const std::uint64_t block_index) const +{ + // 1. get the cached seraphis block index > the requested index + const std::uint64_t next_index{m_sp_block_id_cache.get_next_block_index(block_index)}; + + // 2. assume a block is 'unknown' if its index is above the last seraphis block index + if (block_index + 1 > m_sp_scanned_index + 1) + return -1; + + return next_index; +} +//------------------------------------------------------------------------------------------------------------------- +std::uint64_t SpEnoteStore::nearest_legacy_partialscanned_block_index(const std::uint64_t block_index) const +{ + // get the cached legacy block index <= the requested index + return m_legacy_block_id_cache.get_nearest_block_index( + std::min(block_index + 1, m_legacy_partialscan_index + 1) - 1 + ); +} +//------------------------------------------------------------------------------------------------------------------- +std::uint64_t SpEnoteStore::nearest_legacy_fullscanned_block_index(const std::uint64_t block_index) const +{ + // get the cached legacy block index <= the requested index + return m_legacy_block_id_cache.get_nearest_block_index( + std::min(block_index + 1, m_legacy_fullscan_index + 1) - 1 + ); +} +//------------------------------------------------------------------------------------------------------------------- +std::uint64_t SpEnoteStore::nearest_sp_scanned_block_index(const std::uint64_t block_index) const +{ + // get the cached seraphis block index <= the requested index + return m_sp_block_id_cache.get_nearest_block_index( + std::min(block_index + 1, m_sp_scanned_index + 1) - 1 + ); +} +//------------------------------------------------------------------------------------------------------------------- +bool SpEnoteStore::try_get_block_id_for_legacy_partialscan(const std::uint64_t block_index, rct::key &block_id_out) const +{ + // 1. get the nearest cached legacy block index + // - we use this indirection to validate edge conditions + const std::uint64_t nearest_cached_index{this->nearest_legacy_partialscanned_block_index(block_index)}; + + // 2. check error states + if (nearest_cached_index == this->legacy_refresh_index() - 1 || + nearest_cached_index != block_index) + return false; + + // 3. get the block id + CHECK_AND_ASSERT_THROW_MES(m_legacy_block_id_cache.try_get_block_id(block_index, block_id_out), + "sp enote store (try get block id legacy partialscan): failed to get cached block id for index that is known."); + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool SpEnoteStore::try_get_block_id_for_legacy_fullscan(const std::uint64_t block_index, rct::key &block_id_out) const +{ + // 1. get the nearest cached legacy block index + // - we use this indirection to validate edge conditions + const std::uint64_t nearest_cached_index{this->nearest_legacy_fullscanned_block_index(block_index)}; + + // 2. check error states + if (nearest_cached_index == this->legacy_refresh_index() - 1 || + nearest_cached_index != block_index) + return false; + + // 3. get the block id + CHECK_AND_ASSERT_THROW_MES(m_legacy_block_id_cache.try_get_block_id(block_index, block_id_out), + "sp enote store (try get block id legacy fullscan): failed to get cached block id for index that is known."); + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool SpEnoteStore::try_get_block_id_for_sp(const std::uint64_t block_index, rct::key &block_id_out) const +{ + // 1. get the nearest cached sp block index + // - we use this indirection to validate edge conditions + const std::uint64_t nearest_cached_index{this->nearest_sp_scanned_block_index(block_index)}; + + // 2. check error states + if (nearest_cached_index == this->sp_refresh_index() - 1 || + nearest_cached_index != block_index) + return false; + + // 3. get the block id + CHECK_AND_ASSERT_THROW_MES(m_sp_block_id_cache.try_get_block_id(block_index, block_id_out), + "sp enote store (try get block id sp scan): failed to get cached block id for index that is known."); + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool SpEnoteStore::try_get_block_id(const std::uint64_t block_index, rct::key &block_id_out) const +{ + // try to get the block id from each of the scan types + return this->try_get_block_id_for_legacy_partialscan(block_index, block_id_out) || + this->try_get_block_id_for_legacy_fullscan(block_index, block_id_out) || + this->try_get_block_id_for_sp(block_index, block_id_out); +} +//------------------------------------------------------------------------------------------------------------------- +bool SpEnoteStore::has_enote_with_key_image(const crypto::key_image &key_image) const +{ + // note: test sp records first since over time that will be the hottest path + return m_sp_contextual_enote_records.find(key_image) != m_sp_contextual_enote_records.end() || + m_legacy_key_images.find(key_image) != m_legacy_key_images.end(); +} +//------------------------------------------------------------------------------------------------------------------- +bool SpEnoteStore::try_get_legacy_enote_record(const crypto::key_image &key_image, + LegacyContextualEnoteRecordV1 &contextual_record_out) const +{ + // 1. drill into the legacy maps searching for at least one matching legacy enote record + // a. legacy key images map + if (m_legacy_key_images.find(key_image) == m_legacy_key_images.end()) + return false; + + // b. tracked legacy duplicates + const rct::key &onetime_address{m_legacy_key_images.at(key_image)}; + + if (m_tracked_legacy_onetime_address_duplicates.find(onetime_address) == + m_tracked_legacy_onetime_address_duplicates.end()) + return false; + + // c. identifiers associated with this key image's onetime address + const std::unordered_set &identifiers_of_duplicates{ + m_tracked_legacy_onetime_address_duplicates.at(onetime_address) + }; + + if (identifiers_of_duplicates.size() == 0) + return false; + + // 2. search for the highest-amount enote among the enotes that have our key image + rct::key best_identifier{rct::zero()}; + rct::xmr_amount best_amount{0}; + rct::xmr_amount temp_record_amount; + + for (const rct::key &identifier : identifiers_of_duplicates) + { + // a. check intermediate records + if (m_legacy_intermediate_contextual_enote_records.find(identifier) != + m_legacy_intermediate_contextual_enote_records.end()) + { + temp_record_amount = m_legacy_intermediate_contextual_enote_records.at(identifier).record.amount; + } + // b. check full records + else if (m_legacy_contextual_enote_records.find(identifier) != + m_legacy_contextual_enote_records.end()) + { + temp_record_amount = m_legacy_contextual_enote_records.at(identifier).record.amount; + } + else + continue; + + // c. save the highest-amount record + if (best_amount < temp_record_amount) + { + best_identifier = identifier; + best_amount = temp_record_amount; + } + } + + // 3. if the highest-amount enote is not among the full enote records, then we failed + if (m_legacy_contextual_enote_records.find(best_identifier) == m_legacy_contextual_enote_records.end()) + return false; + + // 4. save the highest-amount record + contextual_record_out = m_legacy_contextual_enote_records.at(best_identifier); + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool SpEnoteStore::try_get_sp_enote_record(const crypto::key_image &key_image, + SpContextualEnoteRecordV1 &contextual_record_out) const +{ + // 1. check if the key image has a record + if (m_sp_contextual_enote_records.find(key_image) == m_sp_contextual_enote_records.end()) + return false; + + // 2. save the record + contextual_record_out = m_sp_contextual_enote_records.at(key_image); + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool SpEnoteStore::try_import_legacy_key_image(const crypto::key_image &legacy_key_image, + const rct::key &onetime_address, + std::list &events_inout) +{ + // 1. fail if there are no enote records for this onetime address + const auto duplicates_ref = m_tracked_legacy_onetime_address_duplicates.find(onetime_address); + if (duplicates_ref == m_tracked_legacy_onetime_address_duplicates.end()) + return false; + + // 2. get the spent context if this key image appeared in a seraphis tx + SpEnoteSpentContextV1 spent_context{}; + + if (m_legacy_key_images_in_sp_selfsends.find(legacy_key_image) != m_legacy_key_images_in_sp_selfsends.end()) + spent_context = m_legacy_key_images_in_sp_selfsends.at(legacy_key_image); + + // 3. there may be full legacy enote records with this key image, use them to update the spent context + for (const rct::key &legacy_enote_identifier : duplicates_ref->second) + { + // a. skip identifiers not in the full legacy records map + const auto record_ref = m_legacy_contextual_enote_records.find(legacy_enote_identifier); + if (record_ref == m_legacy_contextual_enote_records.end()) + continue; + + // b. update the spent context + if (try_update_enote_spent_context_v1(record_ref->second.spent_context, spent_context)) + events_inout.emplace_back(UpdatedLegacySpentContext{legacy_enote_identifier}); + } + + // 4. promote intermediate enote records with this onetime address to full enote records + for (const rct::key &legacy_enote_identifier : duplicates_ref->second) + { + // a. skip identifiers not in the intermediate records map + const auto record_ref = m_legacy_intermediate_contextual_enote_records.find(legacy_enote_identifier); + if (record_ref == m_legacy_intermediate_contextual_enote_records.end()) + continue; + + // b. if this identifier has an intermediate record, it should not have a full record + CHECK_AND_ASSERT_THROW_MES(m_legacy_contextual_enote_records.find(legacy_enote_identifier) == + m_legacy_contextual_enote_records.end(), + "sp enote store (import legacy key image): intermediate and full legacy maps inconsistent (bug)."); + + // c. set the full record + get_legacy_enote_record(record_ref->second.record, + legacy_key_image, + m_legacy_contextual_enote_records[legacy_enote_identifier].record); + events_inout.emplace_back(NewLegacyRecord{legacy_enote_identifier}); + + // d. set the full record's contexts + update_contextual_enote_record_contexts_v1( + record_ref->second.origin_context, + spent_context, + m_legacy_contextual_enote_records[legacy_enote_identifier].origin_context, + m_legacy_contextual_enote_records[legacy_enote_identifier].spent_context + ); + + // e. remove the intermediate record + m_legacy_intermediate_contextual_enote_records.erase(legacy_enote_identifier); + events_inout.emplace_back(RemovedLegacyIntermediateRecord{legacy_enote_identifier}); + + // f. save to the legacy key image set + m_legacy_key_images[legacy_key_image] = onetime_address; + } + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +void SpEnoteStore::update_legacy_fullscan_index_for_import_cycle(const std::uint64_t saved_index) +{ + // clamp the imported index to the top known block index in case blocks were popped in the middle of an import + // cycle and the enote store was refreshed before this function call, thereby reducing the top known block index + this->set_last_legacy_fullscan_index(std::min(saved_index + 1, m_legacy_block_id_cache.top_block_index() + 1) - 1); +} +//------------------------------------------------------------------------------------------------------------------- +void SpEnoteStore::set_last_legacy_partialscan_index(const std::uint64_t new_index) +{ + // 1. set this scan index (+1 because if no scanning has been done then we are below the refresh index) + CHECK_AND_ASSERT_THROW_MES(new_index + 1 >= this->legacy_refresh_index(), + "sp enote store (set legacy partialscan index): new index is below refresh index."); + CHECK_AND_ASSERT_THROW_MES(new_index + 1 <= m_legacy_block_id_cache.top_block_index() + 1, + "sp enote store (set legacy partialscan index): new index is above known block range."); + + m_legacy_partialscan_index = new_index; + + // 2. update legacy full scan index + // - if the partialscan index is below the fullscan index, assume this means there was a reorg + m_legacy_fullscan_index = std::min(m_legacy_fullscan_index + 1, m_legacy_partialscan_index + 1) - 1; +} +//------------------------------------------------------------------------------------------------------------------- +void SpEnoteStore::set_last_legacy_fullscan_index(const std::uint64_t new_index) +{ + // 1. set this scan index (+1 because if no scanning has been done then we are below the refresh index) + CHECK_AND_ASSERT_THROW_MES(new_index + 1 >= this->legacy_refresh_index(), + "sp enote store (set legacy fullscan index): new index is below refresh index."); + CHECK_AND_ASSERT_THROW_MES(new_index + 1 <= m_legacy_block_id_cache.top_block_index() + 1, + "sp enote store (set legacy fullscan index): new index is above known block range."); + + m_legacy_fullscan_index = new_index; + + // 2. update legacy partial scan index + // - fullscan qualifies as partialscan + // note: this update intentionally won't fix inaccuracy in the m_legacy_partialscan_index caused by a reorg, because + // in manual workflows the legacy partialscan index is often higher than the legacy fullscan index; that is + // find because the partialscan index only matters when doing a manual view-only workflow, and any reorg- + // induced inaccuracy in that height will be fixed by re-running that workflow + m_legacy_partialscan_index = std::max(m_legacy_partialscan_index + 1, m_legacy_fullscan_index + 1) - 1; +} +//------------------------------------------------------------------------------------------------------------------- +void SpEnoteStore::set_last_sp_scanned_index(const std::uint64_t new_index) +{ + // set this scan index (+1 because if no scanning has been done then we are below the refresh index) + CHECK_AND_ASSERT_THROW_MES(new_index + 1 >= this->sp_refresh_index(), + "sp enote store (set seraphis scan index): new index is below refresh index."); + CHECK_AND_ASSERT_THROW_MES(new_index + 1 <= m_sp_block_id_cache.top_block_index() + 1, + "sp enote store (set seraphis scan index): new index is above known block range."); + + m_sp_scanned_index = new_index; +} +//------------------------------------------------------------------------------------------------------------------- +void SpEnoteStore::update_with_intermediate_legacy_records_from_nonledger( + const SpEnoteOriginStatus nonledger_origin_status, + const std::unordered_map &found_enote_records, + const std::unordered_map &found_spent_key_images, + std::list &events_inout) +{ + // 1. clean up enote store maps in preparation for adding fresh enotes and key images + this->clean_maps_for_legacy_nonledger_update(nonledger_origin_status, found_spent_key_images, events_inout); + + // 2. add found enotes + for (const auto &found_enote_record : found_enote_records) + this->add_record(found_enote_record.second, events_inout); + + // 3. update contexts of stored enotes with found spent key images + this->update_legacy_with_fresh_found_spent_key_images(found_spent_key_images, events_inout); +} +//------------------------------------------------------------------------------------------------------------------- +void SpEnoteStore::update_with_intermediate_legacy_records_from_ledger(const rct::key &alignment_block_id, + const std::uint64_t first_new_block, + const std::vector &new_block_ids, + const std::unordered_map &found_enote_records, + const std::unordered_map &found_spent_key_images, + std::list &events_inout) +{ + // 1. update block tracking info + this->update_with_new_blocks_from_ledger_legacy_partialscan(alignment_block_id, + first_new_block, + new_block_ids, + events_inout); + + // 2. clean up enote store maps in preparation for adding fresh enotes and key images + this->clean_maps_for_legacy_ledger_update(first_new_block, found_spent_key_images, events_inout); + + // 3. add found enotes + for (const auto &found_enote_record : found_enote_records) + this->add_record(found_enote_record.second, events_inout); + + // 4. update contexts of stored enotes with found spent key images + this->update_legacy_with_fresh_found_spent_key_images(found_spent_key_images, events_inout); +} +//------------------------------------------------------------------------------------------------------------------- +void SpEnoteStore::update_with_intermediate_legacy_found_spent_key_images( + const std::unordered_map &found_spent_key_images, + std::list &events_inout) +{ + // 1. clean up enote store maps that conflict with the found spent key images (which take precedence) + this->clean_maps_for_found_spent_legacy_key_images(found_spent_key_images, events_inout); + + // 2. update contexts of stored enotes with found spent key images + this->update_legacy_with_fresh_found_spent_key_images(found_spent_key_images, events_inout); +} +//------------------------------------------------------------------------------------------------------------------- +void SpEnoteStore::update_with_legacy_records_from_nonledger(const SpEnoteOriginStatus nonledger_origin_status, + const std::unordered_map &found_enote_records, + const std::unordered_map &found_spent_key_images, + std::list &events_inout) +{ + // 1. clean up enote store maps in preparation for adding fresh enotes and key images + this->clean_maps_for_legacy_nonledger_update(nonledger_origin_status, found_spent_key_images, events_inout); + + // 2. add found enotes + for (const auto &found_enote_record : found_enote_records) + this->add_record(found_enote_record.second, events_inout); + + // 3. update contexts of stored enotes with found spent key images + this->update_legacy_with_fresh_found_spent_key_images(found_spent_key_images, events_inout); +} +//------------------------------------------------------------------------------------------------------------------- +void SpEnoteStore::update_with_legacy_records_from_ledger(const rct::key &alignment_block_id, + const std::uint64_t first_new_block, + const std::vector &new_block_ids, + const std::unordered_map &found_enote_records, + const std::unordered_map &found_spent_key_images, + std::list &events_inout) +{ + // 1. update block tracking info + this->update_with_new_blocks_from_ledger_legacy_fullscan(alignment_block_id, + first_new_block, + new_block_ids, + events_inout); + + // 2. clean up enote store maps in preparation for adding fresh enotes and key images + this->clean_maps_for_legacy_ledger_update(first_new_block, found_spent_key_images, events_inout); + + // 3. add found enotes + for (const auto &found_enote_record : found_enote_records) + this->add_record(found_enote_record.second, events_inout); + + // 4. update contexts of stored enotes with found spent key images + this->update_legacy_with_fresh_found_spent_key_images(found_spent_key_images, events_inout); +} +//------------------------------------------------------------------------------------------------------------------- +void SpEnoteStore::update_with_sp_records_from_nonledger(const SpEnoteOriginStatus nonledger_origin_status, + const std::unordered_map &found_enote_records, + const std::unordered_map &found_spent_key_images, + const std::unordered_map &legacy_key_images_in_sp_selfsends, + std::list &events_inout) +{ + // 1. remove records that will be replaced + this->clean_maps_for_sp_nonledger_update(nonledger_origin_status, events_inout); + + // 2. add found enotes + for (const auto &found_enote_record : found_enote_records) + this->add_record(found_enote_record.second, events_inout); + + // 3. update spent contexts of stored enotes with found spent key images + this->update_sp_with_fresh_found_spent_key_images(found_spent_key_images, events_inout); + + // 4. handle legacy key images attached to self-spends + this->handle_legacy_key_images_from_sp_selfsends(legacy_key_images_in_sp_selfsends, events_inout); +} +//------------------------------------------------------------------------------------------------------------------- +void SpEnoteStore::update_with_sp_records_from_ledger(const rct::key &alignment_block_id, + const std::uint64_t first_new_block, + const std::vector &new_block_ids, + const std::unordered_map &found_enote_records, + const std::unordered_map &found_spent_key_images, + const std::unordered_map &legacy_key_images_in_sp_selfsends, + std::list &events_inout) +{ + // 1. update block tracking info + this->update_with_new_blocks_from_ledger_sp(alignment_block_id, first_new_block, new_block_ids, events_inout); + + // 2. remove records that will be replaced + this->clean_maps_for_sp_ledger_update(first_new_block, events_inout); + + // 3. add found enotes + for (const auto &found_enote_record : found_enote_records) + this->add_record(found_enote_record.second, events_inout); + + // 4. update contexts of stored enotes with found spent key images + this->update_sp_with_fresh_found_spent_key_images(found_spent_key_images, events_inout); + + // 5. handle legacy key images attached to self-spends + this->handle_legacy_key_images_from_sp_selfsends(legacy_key_images_in_sp_selfsends, events_inout); +} +//------------------------------------------------------------------------------------------------------------------- +// ENOTE STORE INTERNAL +//------------------------------------------------------------------------------------------------------------------- +void SpEnoteStore::update_with_new_blocks_from_ledger_legacy_partialscan(const rct::key &alignment_block_id, + const std::uint64_t first_new_block, + const std::vector &new_block_ids, + std::list &events_inout) +{ + // 1. set new block ids in range [first_new_block, end of chain] + LegacyIntermediateBlocksDiff diff{}; + update_checkpoint_cache_with_new_block_ids(alignment_block_id, + first_new_block, + new_block_ids, + m_legacy_block_id_cache, + diff.old_top_index, + diff.range_start_index, + diff.num_blocks_added); + events_inout.emplace_back(diff); + + // 2. update scanning index for this scan mode (assumed to be LEGACY_INTERMEDIATE_SCAN) + this->set_last_legacy_partialscan_index(first_new_block + new_block_ids.size() - 1); +} +//------------------------------------------------------------------------------------------------------------------- +// ENOTE STORE INTERNAL +//------------------------------------------------------------------------------------------------------------------- +void SpEnoteStore::update_with_new_blocks_from_ledger_legacy_fullscan(const rct::key &alignment_block_id, + const std::uint64_t first_new_block, + const std::vector &new_block_ids, + std::list &events_inout) +{ + // 1. set new block ids in range [first_new_block, end of chain] + LegacyBlocksDiff diff{}; + update_checkpoint_cache_with_new_block_ids(alignment_block_id, + first_new_block, + new_block_ids, + m_legacy_block_id_cache, + diff.old_top_index, + diff.range_start_index, + diff.num_blocks_added); + events_inout.emplace_back(diff); + + // 2. update scanning index for this scan mode (assumed to be LEGACY_FULL) + // note: we must set the partialscan index here in case a reorg dropped blocks; we don't do it inside the + // set_last_legacy_fullscan_index() function because that function needs to be used in manual view-scanning + // workflows where the legacy fullscan index will often lag behind the partialscan index + this->set_last_legacy_partialscan_index(first_new_block + new_block_ids.size() - 1); + this->set_last_legacy_fullscan_index(first_new_block + new_block_ids.size() - 1); +} +//------------------------------------------------------------------------------------------------------------------- +// ENOTE STORE INTERNAL +//------------------------------------------------------------------------------------------------------------------- +void SpEnoteStore::update_with_new_blocks_from_ledger_sp(const rct::key &alignment_block_id, + const std::uint64_t first_new_block, + const std::vector &new_block_ids, + std::list &events_inout) +{ + // 1. set new block ids in range [first_new_block, end of chain] + SpBlocksDiff diff{}; + update_checkpoint_cache_with_new_block_ids(alignment_block_id, + first_new_block, + new_block_ids, + m_sp_block_id_cache, + diff.old_top_index, + diff.range_start_index, + diff.num_blocks_added); + events_inout.emplace_back(diff); + + // 2. update scanning index for this scan mode (assumed to be SERAPHIS) + this->set_last_sp_scanned_index(first_new_block + new_block_ids.size() - 1); +} +//------------------------------------------------------------------------------------------------------------------- +// ENOTE STORE INTERNAL +//------------------------------------------------------------------------------------------------------------------- +void SpEnoteStore::clean_maps_for_found_spent_legacy_key_images( + const std::unordered_map &found_spent_key_images, + std::list &events_inout) +{ + // 1. if a found legacy key image is in the 'legacy key images from sp txs' map, remove it from that map + // - a fresh spent context for legacy key images implies seraphis txs were reorged and replaced with legacy txs + // spending the same legacy enotes; we want to guarantee that the fresh spent contexts are applied to our + // stored enotes, and doing this step achieves that + // - save the key images removed so we can clear the corresponding spent contexts in the enote records + std::unordered_map spent_contexts_removed_from_sp_selfsends; //[ KI : tx id ] + for (const auto &found_spent_key_image : found_spent_key_images) + { + // a. ignore key images not in the sp selfsend tracker + const auto tracked_ki_ref = m_legacy_key_images_in_sp_selfsends.find(found_spent_key_image.first); + if (tracked_ki_ref == m_legacy_key_images_in_sp_selfsends.end()) + continue; + + // b. record [ KI : tx id ] of spent key images found in the sp selfsend tracker + spent_contexts_removed_from_sp_selfsends[found_spent_key_image.first] = tracked_ki_ref->second.transaction_id; + + // c. remove the entry from the sp selfsend tracker + m_legacy_key_images_in_sp_selfsends.erase(found_spent_key_image.first); + } + + // 2. clear spent contexts referencing legacy key images removed from the seraphis legacy key image tracker + for (const auto &removed_element : spent_contexts_removed_from_sp_selfsends) + { + // a. get the identifiers associated with this element's key image + const auto onetime_address_ref = m_legacy_key_images.find(removed_element.first); + if (onetime_address_ref == m_legacy_key_images.end()) + continue; + const auto duplicates_ref = m_tracked_legacy_onetime_address_duplicates.find(onetime_address_ref->second); + if (duplicates_ref == m_tracked_legacy_onetime_address_duplicates.end()) + continue; + + // b. clean up each of the records + for (const rct::key &legacy_identifier : duplicates_ref->second) + { + // i. ignore records that don't match the removed elements + auto record_ref = m_legacy_contextual_enote_records.find(legacy_identifier); + if (record_ref == m_legacy_contextual_enote_records.end()) + continue; + if (!(record_ref->second.spent_context.transaction_id == removed_element.second)) + continue; + + // ii. clear spent contexts of records whose key images were removed from the seraphis selfsends tracker + record_ref->second.spent_context = SpEnoteSpentContextV1{}; + events_inout.emplace_back(ClearedLegacySpentContext{legacy_identifier}); + } + } +} +//------------------------------------------------------------------------------------------------------------------- +// ENOTE STORE INTERNAL +//------------------------------------------------------------------------------------------------------------------- +void SpEnoteStore::clean_maps_for_removed_legacy_enotes( + const std::unordered_map &found_spent_key_images, + const std::unordered_map> &mapped_identifiers_of_removed_enotes, + const std::unordered_map &mapped_key_images_of_removed_enotes, + const SpEnoteSpentStatus clearable_spent_status, + const std::uint64_t first_uncleared_block_index, + std::list &events_inout) +{ + // 1. clean maps that conflict with the found spent key images + this->clean_maps_for_found_spent_legacy_key_images(found_spent_key_images, events_inout); + + // 2. clear spent contexts referencing removed blocks or the unconfirmed cache if the corresponding legacy key image + // is not in the seraphis legacy key image tracker + for (auto &mapped_contextual_enote_record : m_legacy_contextual_enote_records) //todo: this is O(n) + { + // a. ignore legacy key images found in seraphis txs that still exist after cleaning maps for found spent key + // images + if (m_legacy_key_images_in_sp_selfsends.find(mapped_contextual_enote_record.second.record.key_image) != + m_legacy_key_images_in_sp_selfsends.end()) + continue; + + // b. ignore spent contexts that aren't clearable according to the caller + if (mapped_contextual_enote_record.second.spent_context.spent_status != clearable_spent_status) + continue; + if (mapped_contextual_enote_record.second.spent_context.block_index + 1 <= first_uncleared_block_index + 1) + continue; + + // c. clear spent contexts that point to txs that the enote store considers nonexistent + mapped_contextual_enote_record.second.spent_context = SpEnoteSpentContextV1{}; + events_inout.emplace_back(ClearedLegacySpentContext{mapped_contextual_enote_record.first}); + } + + // 3. clean up legacy trackers + // a. onetime address duplicate tracker: remove identifiers of removed txs + for (const auto &mapped_identifiers : mapped_identifiers_of_removed_enotes) + { + // a. ignore unknown onetime addresses + const auto duplicates_ref = m_tracked_legacy_onetime_address_duplicates.find(mapped_identifiers.first); + if (duplicates_ref == m_tracked_legacy_onetime_address_duplicates.end()) + continue; + + // b. remove identifiers of removed enotes + for (const rct::key &identifier_of_removed_enote : mapped_identifiers.second) + duplicates_ref->second.erase(identifier_of_removed_enote); + + // c. clean up empty entries in the duplicate tracker + if (duplicates_ref->second.size() == 0) + m_tracked_legacy_onetime_address_duplicates.erase(mapped_identifiers.first); + } + + // b. legacy key image tracker: remove any key images of removed txs if the corresponding onetime addresses don't + // have any identifiers registered in the duplicate tracker + for (const auto &mapped_key_image : mapped_key_images_of_removed_enotes) + { + if (m_tracked_legacy_onetime_address_duplicates.find(mapped_key_image.first) != + m_tracked_legacy_onetime_address_duplicates.end()) + continue; + + m_legacy_key_images.erase(mapped_key_image.second); + } +} +//------------------------------------------------------------------------------------------------------------------- +// ENOTE STORE INTERNAL +//------------------------------------------------------------------------------------------------------------------- +void SpEnoteStore::clean_maps_for_legacy_nonledger_update(const SpEnoteOriginStatus nonledger_origin_status, + const std::unordered_map &found_spent_key_images, + std::list &events_inout) +{ + CHECK_AND_ASSERT_THROW_MES(nonledger_origin_status == SpEnoteOriginStatus::OFFCHAIN || + nonledger_origin_status == SpEnoteOriginStatus::UNCONFIRMED, + "sp enote store (clean maps for sp nonledger update): invalid origin status."); + + // 1. remove records that will be replaced + // [ Ko : [ identifier ] ] + std::unordered_map> mapped_identifiers_of_removed_enotes; + + auto legacy_contextual_record_is_removable_func = + [nonledger_origin_status, &mapped_identifiers_of_removed_enotes] + (const auto &mapped_contextual_enote_record) -> bool + { + // a. ignore enotes of unspecified origin + if (!has_origin_status(mapped_contextual_enote_record.second, nonledger_origin_status)) + return false; + + // b. save identifiers of records to be removed + mapped_identifiers_of_removed_enotes[ + onetime_address_ref(mapped_contextual_enote_record.second.record.enote) + ].insert(mapped_contextual_enote_record.first); + + // c. remove the record + return true; + }; + + // a. legacy intermediate records + tools::for_all_in_map_erase_if(m_legacy_intermediate_contextual_enote_records, //todo: this is O(n) + [&legacy_contextual_record_is_removable_func, &events_inout] + (const auto &mapped_contextual_enote_record) -> bool + { + // a. check if the record is removable + if (!legacy_contextual_record_is_removable_func(mapped_contextual_enote_record)) + return false; + + // b. record the identifier of the record being removed + events_inout.emplace_back(RemovedLegacyIntermediateRecord{mapped_contextual_enote_record.first}); + + // c. remove the record + return true; + } + ); + + // b. legacy full records + std::unordered_map mapped_key_images_of_removed_enotes; //[ Ko : KI ] + + tools::for_all_in_map_erase_if(m_legacy_contextual_enote_records, //todo: this is O(n) + [ + &legacy_contextual_record_is_removable_func, + &mapped_key_images_of_removed_enotes, + &events_inout + ] + (const auto &mapped_contextual_enote_record) -> bool + { + // a. check if the record is removable + if (!legacy_contextual_record_is_removable_func(mapped_contextual_enote_record)) + return false; + + // b. save key images of full records that are to be removed + mapped_key_images_of_removed_enotes[ + onetime_address_ref(mapped_contextual_enote_record.second.record.enote) + ] = key_image_ref(mapped_contextual_enote_record.second); + + // c. record the identifier of the record being removed + events_inout.emplace_back(RemovedLegacyRecord{mapped_contextual_enote_record.first}); + + // d. remove the record + return true; + } + ); + + // 2. clean maps for removed enotes + this->clean_maps_for_removed_legacy_enotes(found_spent_key_images, + mapped_identifiers_of_removed_enotes, + mapped_key_images_of_removed_enotes, + nonledger_origin_status == SpEnoteOriginStatus::OFFCHAIN + ? SpEnoteSpentStatus::SPENT_OFFCHAIN + : SpEnoteSpentStatus::SPENT_UNCONFIRMED, + -1, + events_inout); +} +//------------------------------------------------------------------------------------------------------------------- +// ENOTE STORE INTERNAL +//------------------------------------------------------------------------------------------------------------------- +void SpEnoteStore::clean_maps_for_legacy_ledger_update(const std::uint64_t first_new_block, + const std::unordered_map &found_spent_key_images, + std::list &events_inout) +{ + // 1. remove records that will be replaced + // [ Ko : [ legacy identifiers ] ] + std::unordered_map> mapped_identifiers_of_removed_enotes; + + auto legacy_contextual_record_is_removable_func = + [first_new_block, &mapped_identifiers_of_removed_enotes](const auto &mapped_contextual_enote_record) -> bool + { + // a. ignore off-chain records + if (!has_origin_status(mapped_contextual_enote_record.second, SpEnoteOriginStatus::ONCHAIN)) + return false; + + // b. ignore onchain enotes outside of range [first_new_block, end of chain] + if (mapped_contextual_enote_record.second.origin_context.block_index < first_new_block) + return false; + + // c. record the identifier of the enote being removed + mapped_identifiers_of_removed_enotes[ + onetime_address_ref(mapped_contextual_enote_record.second.record.enote) + ].insert(mapped_contextual_enote_record.first); + + // d. remove the record + return true; + }; + + // a. legacy intermediate records + tools::for_all_in_map_erase_if(m_legacy_intermediate_contextual_enote_records, //todo: this is O(n) + [&legacy_contextual_record_is_removable_func, &events_inout] + (const auto &mapped_contextual_enote_record) -> bool + { + // a. check if the record is removable + if (!legacy_contextual_record_is_removable_func(mapped_contextual_enote_record)) + return false; + + // b. record the identifier of the record being removed + events_inout.emplace_back(RemovedLegacyIntermediateRecord{mapped_contextual_enote_record.first}); + + // c. remove the record + return true; + } + ); + + // b. legacy full records + std::unordered_map mapped_key_images_of_removed_enotes; //[ Ko : KI ] + + tools::for_all_in_map_erase_if(m_legacy_contextual_enote_records, //todo: this is O(n) + [ + &legacy_contextual_record_is_removable_func, + &mapped_key_images_of_removed_enotes, + &events_inout + ] + (const auto &mapped_contextual_enote_record) -> bool + { + // a. check if the record is removable + if (!legacy_contextual_record_is_removable_func(mapped_contextual_enote_record)) + return false; + + // b. save key images of full records that are to be removed + mapped_key_images_of_removed_enotes[ + onetime_address_ref(mapped_contextual_enote_record.second.record.enote) + ] = mapped_contextual_enote_record.second.record.key_image; + + // c. record the identifier of the record being removed + events_inout.emplace_back(RemovedLegacyRecord{mapped_contextual_enote_record.first}); + + // d. remove the record + return true; + } + ); + + // 2. clean maps for removed enotes + this->clean_maps_for_removed_legacy_enotes(found_spent_key_images, + mapped_identifiers_of_removed_enotes, + mapped_key_images_of_removed_enotes, + SpEnoteSpentStatus::SPENT_ONCHAIN, + first_new_block - 1, + events_inout); +} +//------------------------------------------------------------------------------------------------------------------- +// ENOTE STORE INTERNAL +//------------------------------------------------------------------------------------------------------------------- +void SpEnoteStore::clean_maps_for_removed_sp_enotes(const std::unordered_set &tx_ids_of_removed_selfsend_enotes, + std::list &events_inout) +{ + // clear spent contexts referencing the txs of removed selfsend enotes + // - key images only appear at the same time as selfsends, so clearing spent contexts made from the txs of lost + // enotes is a reliable way to manage spent contexts + + // 1. seraphis enotes + for (auto &mapped_contextual_enote_record : m_sp_contextual_enote_records) //todo: this is O(n) + { + if (tx_ids_of_removed_selfsend_enotes.find(mapped_contextual_enote_record.second.spent_context.transaction_id) == + tx_ids_of_removed_selfsend_enotes.end()) + continue; + + mapped_contextual_enote_record.second.spent_context = SpEnoteSpentContextV1{}; + events_inout.emplace_back(ClearedSpSpentContext{mapped_contextual_enote_record.first}); + } + + // 2. legacy enotes + for (auto &mapped_contextual_enote_record : m_legacy_contextual_enote_records) //todo: this is O(n) + { + if (tx_ids_of_removed_selfsend_enotes.find(mapped_contextual_enote_record.second.spent_context.transaction_id) == + tx_ids_of_removed_selfsend_enotes.end()) + continue; + + mapped_contextual_enote_record.second.spent_context = SpEnoteSpentContextV1{}; + events_inout.emplace_back(ClearedLegacySpentContext{mapped_contextual_enote_record.first}); + } + + // 3. remove legacy key images found in removed txs + tools::for_all_in_map_erase_if(m_legacy_key_images_in_sp_selfsends, //todo: this is O(n) + [&tx_ids_of_removed_selfsend_enotes](const auto &mapped_legacy_key_image) -> bool + { + return tx_ids_of_removed_selfsend_enotes.find(mapped_legacy_key_image.second.transaction_id) != + tx_ids_of_removed_selfsend_enotes.end(); + } + ); +} +//------------------------------------------------------------------------------------------------------------------- +// ENOTE STORE INTERNAL +//------------------------------------------------------------------------------------------------------------------- +void SpEnoteStore::clean_maps_for_sp_nonledger_update(const SpEnoteOriginStatus nonledger_origin_status, + std::list &events_inout) +{ + CHECK_AND_ASSERT_THROW_MES(nonledger_origin_status == SpEnoteOriginStatus::OFFCHAIN || + nonledger_origin_status == SpEnoteOriginStatus::UNCONFIRMED, + "sp enote store (clean maps for sp nonledger update): invalid origin status."); + + // 1. remove records + std::unordered_set tx_ids_of_removed_selfsend_enotes; + + tools::for_all_in_map_erase_if(m_sp_contextual_enote_records, //todo: this is O(n) + [ + nonledger_origin_status, + &tx_ids_of_removed_selfsend_enotes, + &events_inout + ] + (const auto &mapped_contextual_enote_record) -> bool + { + // a. ignore enotes that don't have our specified origin status + if (!has_origin_status(mapped_contextual_enote_record.second, nonledger_origin_status)) + return false; + + // b. save the tx id of the record to be removed if it's a selfsend + if (jamtis::is_jamtis_selfsend_type(mapped_contextual_enote_record.second.record.type)) + { + tx_ids_of_removed_selfsend_enotes.insert( + mapped_contextual_enote_record.second.origin_context.transaction_id + ); + } + + // c. record the onetime address of the record being removed + events_inout.emplace_back(RemovedSpRecord{mapped_contextual_enote_record.first}); + + // d. remove the record + return true; + } + ); + + // 2. clean maps for removed enotes + this->clean_maps_for_removed_sp_enotes(tx_ids_of_removed_selfsend_enotes, events_inout); +} +//------------------------------------------------------------------------------------------------------------------- +// ENOTE STORE INTERNAL +//------------------------------------------------------------------------------------------------------------------- +void SpEnoteStore::clean_maps_for_sp_ledger_update(const std::uint64_t first_new_block, + std::list &events_inout) +{ + // 1. remove records + std::unordered_set tx_ids_of_removed_selfsend_enotes; + + tools::for_all_in_map_erase_if(m_sp_contextual_enote_records, //todo: this is O(n) + [ + first_new_block, + &tx_ids_of_removed_selfsend_enotes, + &events_inout + ] + (const auto &mapped_contextual_enote_record) -> bool + { + // a. ignore off-chain records + if (!has_origin_status(mapped_contextual_enote_record.second, SpEnoteOriginStatus::ONCHAIN)) + return false; + + // b. ignore onchain enotes outside of range [first_new_block, end of chain] + if (mapped_contextual_enote_record.second.origin_context.block_index < first_new_block) + return false; + + // c. save tx id of the record to be removed if it's a selfsend + if (jamtis::is_jamtis_selfsend_type(mapped_contextual_enote_record.second.record.type)) + { + tx_ids_of_removed_selfsend_enotes.insert( + mapped_contextual_enote_record.second.origin_context.transaction_id + ); + } + + // d. record the onetime address of the record being removed + events_inout.emplace_back(RemovedSpRecord{mapped_contextual_enote_record.first}); + + // e. remove the record + return true; + } + ); + + // 2. clean maps for removed enotes + this->clean_maps_for_removed_sp_enotes(tx_ids_of_removed_selfsend_enotes, events_inout); +} +//------------------------------------------------------------------------------------------------------------------- +// ENOTE STORE INTERNAL +//------------------------------------------------------------------------------------------------------------------- +void SpEnoteStore::add_record(const LegacyContextualIntermediateEnoteRecordV1 &new_record, + std::list &events_inout) +{ + // 1. if key image is known, promote to a full enote record + if (m_tracked_legacy_onetime_address_duplicates.find(onetime_address_ref(new_record.record.enote)) != + m_tracked_legacy_onetime_address_duplicates.end()) + { + const auto &identifiers_of_known_enotes = + m_tracked_legacy_onetime_address_duplicates.at(onetime_address_ref(new_record.record.enote)); + + CHECK_AND_ASSERT_THROW_MES(identifiers_of_known_enotes.size() > 0, + "sp enote store (add intermediate record): record's onetime address is known, but there are no " + "identifiers (bug)."); + + for (const rct::key &identifier : identifiers_of_known_enotes) + { + // key image is known if there is a full record associated with this intermediate record's onetime address + if (m_legacy_contextual_enote_records.find(identifier) == m_legacy_contextual_enote_records.end()) + continue; + + CHECK_AND_ASSERT_THROW_MES(identifier == *(identifiers_of_known_enotes.begin()), + "sp enote store (add intermediate record): key image is known but there are intermediate " + "records with this onetime address (a given onetime address should have only intermediate or only " + "full legacy records)."); + + LegacyContextualEnoteRecordV1 temp_full_record{}; + + get_legacy_enote_record(new_record.record, + m_legacy_contextual_enote_records.at(identifier).record.key_image, + temp_full_record.record); + temp_full_record.origin_context = new_record.origin_context; + + this->add_record(temp_full_record, events_inout); + return; + } + } + + // 2. else add the intermediate record or update an existing record's origin context + rct::key new_record_identifier; + get_legacy_enote_identifier(onetime_address_ref(new_record.record.enote), + new_record.record.amount, + new_record_identifier); + + if (m_legacy_intermediate_contextual_enote_records.find(new_record_identifier) == + m_legacy_intermediate_contextual_enote_records.end()) + { + // add new intermediate record + m_legacy_intermediate_contextual_enote_records[new_record_identifier] = new_record; + events_inout.emplace_back(NewLegacyIntermediateRecord{new_record_identifier}); + } + else + { + // update intermediate record's origin context + if (try_update_enote_origin_context_v1(new_record.origin_context, + m_legacy_intermediate_contextual_enote_records[new_record_identifier].origin_context)) + events_inout.emplace_back(UpdatedLegacyIntermediateOriginContext{new_record_identifier}); + } + + // 3. save to the legacy duplicate tracker + m_tracked_legacy_onetime_address_duplicates[onetime_address_ref(new_record.record.enote)] + .insert(new_record_identifier); +} +//------------------------------------------------------------------------------------------------------------------- +// ENOTE STORE INTERNAL +//------------------------------------------------------------------------------------------------------------------- +void SpEnoteStore::add_record(const LegacyContextualEnoteRecordV1 &new_record, + std::list &events_inout) +{ + rct::key new_record_identifier; + get_legacy_enote_identifier(onetime_address_ref(new_record.record.enote), + new_record.record.amount, + new_record_identifier); + + // 1. add the record or update an existing record's contexts + if (m_legacy_contextual_enote_records.find(new_record_identifier) == m_legacy_contextual_enote_records.end()) + { + m_legacy_contextual_enote_records[new_record_identifier] = new_record; + events_inout.emplace_back(NewLegacyRecord{new_record_identifier}); + } + else + { + update_contextual_enote_record_contexts_v1(new_record.origin_context, + new_record.spent_context, + m_legacy_contextual_enote_records[new_record_identifier].origin_context, + m_legacy_contextual_enote_records[new_record_identifier].spent_context + ); + events_inout.emplace_back(UpdatedLegacyOriginContext{new_record_identifier}); + events_inout.emplace_back(UpdatedLegacySpentContext{new_record_identifier}); + } + + // 2. if this enote is located in the legacy key image tracker for seraphis txs, update with the tracker's spent + // context + if (m_legacy_key_images_in_sp_selfsends.find(new_record.record.key_image) != + m_legacy_key_images_in_sp_selfsends.end()) + { + // update the record's spent context + try_update_enote_spent_context_v1(m_legacy_key_images_in_sp_selfsends.at(new_record.record.key_image), + m_legacy_contextual_enote_records[new_record_identifier].spent_context); + //don't add event record: assume it would be redundant + + // note: do not change the tracker's spent context here, the tracker is a helper cache for the scanning process + // and should only be mutated by the relevant code + } + + // 3. if this enote is located in the intermediate enote record map, update the full record with the intermediate + // record's origin context + if (m_legacy_intermediate_contextual_enote_records.find(new_record_identifier) != + m_legacy_intermediate_contextual_enote_records.end()) + { + // update the record's origin context + try_update_enote_origin_context_v1( + m_legacy_intermediate_contextual_enote_records.at(new_record_identifier).origin_context, + m_legacy_contextual_enote_records[new_record_identifier].origin_context + ); + //don't add event record: assume it would be redundant + } + + // 4. there may be other full legacy enote records with this record's key image, use them to update the spent context + for (const rct::key &legacy_enote_identifier : + m_tracked_legacy_onetime_address_duplicates[onetime_address_ref(new_record.record.enote)]) + { + // a. skip identifiers not in the full legacy records map + if (m_legacy_contextual_enote_records.find(legacy_enote_identifier) == m_legacy_contextual_enote_records.end()) + continue; + + // b. update the spent context + try_update_enote_spent_context_v1( + m_legacy_contextual_enote_records.at(legacy_enote_identifier).spent_context, + m_legacy_contextual_enote_records[new_record_identifier].spent_context); + //don't add event record: assume it would be redundant + } + + // 5. remove the intermediate record with this identifier (must do this before importing the key image, since + // the key image importer assumes the intermediate and full legacy maps don't have any overlap) + if (m_legacy_intermediate_contextual_enote_records.erase(new_record_identifier) > 0) + events_inout.emplace_back(RemovedLegacyIntermediateRecord{new_record_identifier}); + + // 6. save to the legacy duplicate tracker + m_tracked_legacy_onetime_address_duplicates[onetime_address_ref(new_record.record.enote)] + .insert(new_record_identifier); + + // 7. save to the legacy key image set + m_legacy_key_images[new_record.record.key_image] = onetime_address_ref(new_record.record.enote); + + // 8. import this key image to force-promote all intermediate records with different identifiers but the same + // onetime address to full records + this->try_import_legacy_key_image(new_record.record.key_image, + onetime_address_ref(new_record.record.enote), + events_inout); +} +//------------------------------------------------------------------------------------------------------------------- +// ENOTE STORE INTERNAL +//------------------------------------------------------------------------------------------------------------------- +void SpEnoteStore::add_record(const SpContextualEnoteRecordV1 &new_record, std::list &events_inout) +{ + const crypto::key_image &record_key_image{key_image_ref(new_record)}; + + // add the record or update an existing record's contexts + if (m_sp_contextual_enote_records.find(record_key_image) == m_sp_contextual_enote_records.end()) + { + m_sp_contextual_enote_records[record_key_image] = new_record; + events_inout.emplace_back(NewSpRecord{record_key_image}); + } + else + { + update_contextual_enote_record_contexts_v1(new_record, m_sp_contextual_enote_records[record_key_image]); + events_inout.emplace_back(UpdatedSpOriginContext{record_key_image}); + events_inout.emplace_back(UpdatedSpSpentContext{record_key_image}); + } +} +//------------------------------------------------------------------------------------------------------------------- +// ENOTE STORE INTERNAL +//------------------------------------------------------------------------------------------------------------------- +void SpEnoteStore::update_legacy_with_fresh_found_spent_key_images( + const std::unordered_map &found_spent_key_images, + std::list &events_inout) +{ + for (const auto &found_spent_key_image : found_spent_key_images) + { + // a. ignore key images with unknown legacy enotes + const auto legacy_ki_ref = m_legacy_key_images.find(found_spent_key_image.first); + if (legacy_ki_ref == m_legacy_key_images.end()) + continue; + + // b. check that legacy key image map and tracked onetime address maps are consistent + CHECK_AND_ASSERT_THROW_MES(m_tracked_legacy_onetime_address_duplicates.find(legacy_ki_ref->second) != + m_tracked_legacy_onetime_address_duplicates.end(), + "sp enote store (update with legacy enote records): duplicate tracker is missing a onetime address " + "(bug)."); + + // c. update contexts of any enotes associated with this key image + const auto &identifiers_of_enotes_to_update = + m_tracked_legacy_onetime_address_duplicates.at(legacy_ki_ref->second); + + for (const rct::key &identifier_of_enote_to_update : identifiers_of_enotes_to_update) + { + auto record_ref = m_legacy_contextual_enote_records.find(identifier_of_enote_to_update); + CHECK_AND_ASSERT_THROW_MES(record_ref != m_legacy_contextual_enote_records.end(), + "sp enote store (update with legacy enote records): full record map is missing identifier (bug)."); + CHECK_AND_ASSERT_THROW_MES(record_ref->second.record.key_image == found_spent_key_image.first, + "sp enote store (update with legacy enote records): full record map is inconsistent (bug)."); + + update_contextual_enote_record_contexts_v1( + record_ref->second.origin_context, + found_spent_key_image.second, + record_ref->second.origin_context, + record_ref->second.spent_context); + events_inout.emplace_back(UpdatedLegacyOriginContext{identifier_of_enote_to_update}); + events_inout.emplace_back(UpdatedLegacySpentContext{identifier_of_enote_to_update}); + } + } +} +//------------------------------------------------------------------------------------------------------------------- +// ENOTE STORE INTERNAL +//------------------------------------------------------------------------------------------------------------------- +void SpEnoteStore::update_sp_with_fresh_found_spent_key_images( + const std::unordered_map &found_spent_key_images, + std::list &events_inout) +{ + for (const auto &found_spent_key_image : found_spent_key_images) + { + // a. ignore enotes with unknown key images + auto record_ref = m_sp_contextual_enote_records.find(found_spent_key_image.first); + if (record_ref == m_sp_contextual_enote_records.end()) + continue; + + // b. update this enote's contexts + update_contextual_enote_record_contexts_v1( + record_ref->second.origin_context, + found_spent_key_image.second, + record_ref->second.origin_context, + record_ref->second.spent_context); + events_inout.emplace_back(UpdatedSpOriginContext{found_spent_key_image.first}); + events_inout.emplace_back(UpdatedSpSpentContext{found_spent_key_image.first}); + } +} +//------------------------------------------------------------------------------------------------------------------- +// ENOTE STORE INTERNAL +//------------------------------------------------------------------------------------------------------------------- +void SpEnoteStore::handle_legacy_key_images_from_sp_selfsends( + const std::unordered_map &legacy_key_images_in_sp_selfsends, + std::list &events_inout) +{ + // handle each key image + for (const auto &legacy_key_image_with_spent_context : legacy_key_images_in_sp_selfsends) + { + // 1. save the key image's spent context in the tracker (or update an existing context) + // note: these are always saved to help with reorg handling + try_update_enote_spent_context_v1(legacy_key_image_with_spent_context.second, + m_legacy_key_images_in_sp_selfsends[legacy_key_image_with_spent_context.first]); + //don't add event record: the m_legacy_key_images_in_sp_selfsends is an internal cache + + // 2. get the identifiers associated with this element's key image + const auto onetime_address_ref = m_legacy_key_images.find(legacy_key_image_with_spent_context.first); + if (onetime_address_ref == m_legacy_key_images.end()) + continue; + const auto duplicates_ref = m_tracked_legacy_onetime_address_duplicates.find(onetime_address_ref->second); + if (duplicates_ref == m_tracked_legacy_onetime_address_duplicates.end()) + continue; + + // 3. try to update the spent contexts of legacy enotes that have this key image + for (const rct::key &legacy_identifier : duplicates_ref->second) + { + // a. ignore identifiers that aren't in the full legacy map + auto record_ref = m_legacy_contextual_enote_records.find(legacy_identifier); + if (record_ref == m_legacy_contextual_enote_records.end()) + continue; + + // b. update the spent context of this legacy enote + if (try_update_enote_spent_context_v1(legacy_key_image_with_spent_context.second, + record_ref->second.spent_context)) + events_inout.emplace_back(UpdatedLegacySpentContext{record_ref->first}); + } + } +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace sp diff --git a/src/seraphis_impl/enote_store.h b/src/seraphis_impl/enote_store.h new file mode 100644 index 0000000000..512633cdb9 --- /dev/null +++ b/src/seraphis_impl/enote_store.h @@ -0,0 +1,299 @@ +// Copyright (c) 2022, 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. + +// Enote store that supports full-featured balance recovery by managing enote-related caches. + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "ringct/rctTypes.h" +#include "seraphis_impl/checkpoint_cache.h" +#include "seraphis_impl/enote_store_event_types.h" +#include "seraphis_main/contextual_enote_record_types.h" + +//third party headers + +//standard headers +#include +#include + +//forward declarations + + +namespace sp +{ + +//// +// SpEnoteStore +// - tracks legacy and seraphis enotes +/// +class SpEnoteStore final +{ +public: +//constructors + /// normal constructor + SpEnoteStore(const std::uint64_t refresh_index, + const std::uint64_t first_sp_enabled_block_in_chain, + const std::uint64_t default_spendable_age, + const CheckpointCacheConfig &checkpoint_cache_config = + CheckpointCacheConfig{ + .num_unprunable = 50, + .max_separation = 100000, + .density_factor = 20 + } + ); + +//member functions + /// config: get index of the first block the enote store cares about + std::uint64_t legacy_refresh_index() const; + std::uint64_t sp_refresh_index() const; + /// config: get default spendable age + std::uint64_t default_spendable_age() const { return m_default_spendable_age; } + + /// get index of the highest recorded block (legacy refresh index - 1 if no recorded blocks) + std::uint64_t top_block_index() const; + /// get index of the highest block that was legacy partialscanned (view-scan only) + std::uint64_t top_legacy_partialscanned_block_index() const { return m_legacy_partialscan_index; } + /// get index of the highest block that was legacy fullscanned (view-scan + comprehensive key image checks) + std::uint64_t top_legacy_fullscanned_block_index() const { return m_legacy_fullscan_index; } + /// get index of the highest block that was seraphis view-balance scanned + std::uint64_t top_sp_scanned_block_index() const { return m_sp_scanned_index; } + + /// get the next cached block index > the requested index (-1 on failure) + std::uint64_t next_legacy_partialscanned_block_index(const std::uint64_t block_index) const; + std::uint64_t next_legacy_fullscanned_block_index (const std::uint64_t block_index) const; + std::uint64_t next_sp_scanned_block_index (const std::uint64_t block_index) const; + /// get the nearest cached block index <= the requested index (refresh index - 1 on failure) + std::uint64_t nearest_legacy_partialscanned_block_index(const std::uint64_t block_index) const; + std::uint64_t nearest_legacy_fullscanned_block_index (const std::uint64_t block_index) const; + std::uint64_t nearest_sp_scanned_block_index (const std::uint64_t block_index) const; + /// try to get the cached block id for a given index and specified scan mode + /// note: during scanning, different scan modes are assumed to 'not see' block ids obtained by a different scan mode; + /// this is necessary to reliably recover from reorgs involving multiple scan modes + bool try_get_block_id_for_legacy_partialscan(const std::uint64_t block_index, rct::key &block_id_out) const; + bool try_get_block_id_for_legacy_fullscan (const std::uint64_t block_index, rct::key &block_id_out) const; + bool try_get_block_id_for_sp (const std::uint64_t block_index, rct::key &block_id_out) const; + /// try to get the cached block id for a given index (checks legacy block ids then seraphis block ids) + bool try_get_block_id(const std::uint64_t block_index, rct::key &block_id_out) const; + /// check if any stored enote has a given key image + bool has_enote_with_key_image(const crypto::key_image &key_image) const; + /// get the legacy [ legacy identifier : legacy intermediate record ] map + /// - note: useful for collecting onetime addresses and viewkey extensions for key image recovery + const std::unordered_map& legacy_intermediate_records() const + { return m_legacy_intermediate_contextual_enote_records; } + /// get the legacy [ legacy identifier : legacy record ] map + const std::unordered_map& legacy_records() const + { return m_legacy_contextual_enote_records; } + /// get the legacy [ Ko : [ legacy identifier ] ] map + const std::unordered_map>& legacy_onetime_address_identifier_map() const + { return m_tracked_legacy_onetime_address_duplicates; } + /// get the legacy [ KI : Ko ] map + const std::unordered_map& legacy_key_images() const + { return m_legacy_key_images; } + /// get the seraphis [ KI : sp record ] map + const std::unordered_map& sp_records() const + { return m_sp_contextual_enote_records; } + /// try to get the legacy enote with a specified key image + /// - will only return the highest-amount legacy enote among duplicates, and will return false if the + /// highest-amount legacy enote is currently in the intermediate records map + bool try_get_legacy_enote_record(const crypto::key_image &key_image, + LegacyContextualEnoteRecordV1 &contextual_record_out) const; + /// try to get the seraphis enote with a specified key image + bool try_get_sp_enote_record(const crypto::key_image &key_image, + SpContextualEnoteRecordV1 &contextual_record_out) const; + + /// try to import a legacy key image + /// - PRECONDITION1: the legacy key image was computed from/for the input onetime address + /// - returns false if the onetime address is unknown (e.g. due to a reorg that removed the corresponding record) + bool try_import_legacy_key_image(const crypto::key_image &legacy_key_image, + const rct::key &onetime_address, + std::list &events_inout); + /// update the legacy fullscan index as part of a legacy key image import cycle + void update_legacy_fullscan_index_for_import_cycle(const std::uint64_t saved_index); + + /// setters for scan indices + /// WARNING: misuse of these will mess up the enote store's state (to recover: set index below problem then rescan) + /// note: to repair the enote store in case of an exception or other error during an update, save all of the last + /// scanned indices from before the update, reset the enote store with them (after the failure), and then + /// re-scan to repair + void set_last_legacy_partialscan_index(const std::uint64_t new_index); + void set_last_legacy_fullscan_index (const std::uint64_t new_index); + void set_last_sp_scanned_index (const std::uint64_t new_index); + + /// update the store with legacy enote records and associated context + void update_with_intermediate_legacy_records_from_nonledger(const SpEnoteOriginStatus nonledger_origin_status, + const std::unordered_map &found_enote_records, + const std::unordered_map &found_spent_key_images, + std::list &events_inout); + void update_with_intermediate_legacy_records_from_ledger(const rct::key &alignment_block_id, + const std::uint64_t first_new_block, + const std::vector &new_block_ids, + const std::unordered_map &found_enote_records, + const std::unordered_map &found_spent_key_images, + std::list &events_inout); + void update_with_intermediate_legacy_found_spent_key_images( + const std::unordered_map &found_spent_key_images, + std::list &events_inout); + void update_with_legacy_records_from_nonledger(const SpEnoteOriginStatus nonledger_origin_status, + const std::unordered_map &found_enote_records, + const std::unordered_map &found_spent_key_images, + std::list &events_inout); + void update_with_legacy_records_from_ledger(const rct::key &alignment_block_id, + const std::uint64_t first_new_block, + const std::vector &new_block_ids, + const std::unordered_map &found_enote_records, + const std::unordered_map &found_spent_key_images, + std::list &events_inout); + + /// update the store with seraphis enote records and associated context + void update_with_sp_records_from_nonledger(const SpEnoteOriginStatus nonledger_origin_status, + const std::unordered_map &found_enote_records, + const std::unordered_map &found_spent_key_images, + const std::unordered_map &legacy_key_images_in_sp_selfsends, + std::list &events_inout); + void update_with_sp_records_from_ledger(const rct::key &alignment_block_id, + const std::uint64_t first_new_block, + const std::vector &new_block_ids, + const std::unordered_map &found_enote_records, + const std::unordered_map &found_spent_key_images, + const std::unordered_map &legacy_key_images_in_sp_selfsends, + std::list &events_inout); + +private: + /// update the store with a set of new block ids from the ledger + void update_with_new_blocks_from_ledger_legacy_partialscan(const rct::key &alignment_block_id, + const std::uint64_t first_new_block, + const std::vector &new_block_ids, + std::list &events_inout); + void update_with_new_blocks_from_ledger_legacy_fullscan(const rct::key &alignment_block_id, + const std::uint64_t first_new_block, + const std::vector &new_block_ids, + std::list &events_inout); + void update_with_new_blocks_from_ledger_sp(const rct::key &alignment_block_id, + const std::uint64_t first_new_block, + const std::vector &new_block_ids, + std::list &events_inout); + + /// clean maps based on new legacy found spent key images + void clean_maps_for_found_spent_legacy_key_images( + const std::unordered_map &found_spent_key_images, + std::list &events_inout); + /// clean maps based on details of removed legacy enotes + void clean_maps_for_removed_legacy_enotes( + const std::unordered_map &found_spent_key_images, + const std::unordered_map> &mapped_identifiers_of_removed_enotes, + const std::unordered_map &mapped_key_images_of_removed_enotes, + const SpEnoteSpentStatus clearable_spent_status, + const std::uint64_t first_uncleared_block_index, + std::list &events_inout); + /// clean up legacy state to prepare for adding fresh legacy enotes and key images + void clean_maps_for_legacy_nonledger_update(const SpEnoteOriginStatus nonledger_origin_status, + const std::unordered_map &found_spent_key_images, + std::list &events_inout); + void clean_maps_for_legacy_ledger_update(const std::uint64_t first_new_block, + const std::unordered_map &found_spent_key_images, + std::list &events_inout); + + /// clean maps based on tx ids of removed seraphis enotes + void clean_maps_for_removed_sp_enotes(const std::unordered_set &tx_ids_of_removed_enotes, + std::list &events_inout); + /// clean up seraphis state to prepare for adding fresh seraphis enotes and key images and legacy key images + void clean_maps_for_sp_nonledger_update(const SpEnoteOriginStatus nonledger_origin_status, + std::list &events_inout); + void clean_maps_for_sp_ledger_update(const std::uint64_t first_new_block, + std::list &events_inout); + + /// add a record + void add_record(const LegacyContextualIntermediateEnoteRecordV1 &new_record, + std::list &events_inout); + void add_record(const LegacyContextualEnoteRecordV1 &new_record, + std::list &events_inout); + void add_record(const SpContextualEnoteRecordV1 &new_record, + std::list &events_inout); + + /// update legacy state with fresh legacy key images that were found to be spent + void update_legacy_with_fresh_found_spent_key_images( + const std::unordered_map &found_spent_key_images, + std::list &events_inout); + /// update seraphis state with fresh seraphis key images that were found to be spent + void update_sp_with_fresh_found_spent_key_images( + const std::unordered_map &found_spent_key_images, + std::list &events_inout); + + /// cache legacy key images obtained from seraphis selfsends + /// - these are the key images of legacy enotes spent by the user in seraphis txs; they are cached because + /// the enote store may not have the corresponding legacy enotes' records loaded in yet (or only the intermediate + /// records are known) + void handle_legacy_key_images_from_sp_selfsends( + const std::unordered_map &legacy_key_images_in_sp_selfsends, + std::list &events_inout); + +//member variables + /// legacy intermediate enotes: [ legacy identifier : legacy intermediate record ] + std::unordered_map + m_legacy_intermediate_contextual_enote_records; + /// legacy enotes: [ legacy identifier : legacy record ] + std::unordered_map m_legacy_contextual_enote_records; + /// seraphis enotes: [ seraphis KI : seraphis record ] + std::unordered_map m_sp_contextual_enote_records; + + /// saved legacy key images from txs with seraphis selfsends (i.e. from txs we created) + /// [ legacy KI : spent context ] + std::unordered_map m_legacy_key_images_in_sp_selfsends; + /// legacy duplicate tracker for dealing with enotes that have duplicated key images + /// note: the user can receive multiple legacy enotes with the same identifier, but those are treated as equivalent, + /// which should only cause problems for users if the associated tx memos are different (very unlikely scenario) + /// [ Ko : [ legacy identifier ] ] + std::unordered_map> m_tracked_legacy_onetime_address_duplicates; + /// legacy onetime addresses attached to known legacy enotes + /// note: might not include all entries in 'm_legacy_key_images_in_sp_selfsends' if some corresponding enotes are + // unknown + /// [ legacy KI : legacy Ko ] + std::unordered_map m_legacy_key_images; + + /// cached block ids in range: [refresh index, end of known legacy-supporting chain] + CheckpointCache m_legacy_block_id_cache; + /// cached block ids in range: + /// [max(refresh index, first seraphis-enabled block), end of known seraphis-supporting chain] + CheckpointCache m_sp_block_id_cache; + + /// highest block that was legacy partialscanned (view-scan only) + std::uint64_t m_legacy_partialscan_index{static_cast(-1)}; + /// highest block that was legacy fullscanned (view-scan + comprehensive key image checks) + std::uint64_t m_legacy_fullscan_index{static_cast(-1)}; + /// highest block that was seraphis view-balance scanned + std::uint64_t m_sp_scanned_index{static_cast(-1)}; + + /// configuration value: default spendable age; an enote is considered 'spendable' in the next block if it is + /// on-chain and the next block's index is >= 'enote origin index + max(1, default_spendable_age)'; legacy + /// enotes also have an unlock_time attribute on top of the default spendable age + std::uint64_t m_default_spendable_age{0}; +}; + +} //namespace sp diff --git a/src/seraphis_impl/enote_store_event_types.h b/src/seraphis_impl/enote_store_event_types.h new file mode 100644 index 0000000000..f1df71752c --- /dev/null +++ b/src/seraphis_impl/enote_store_event_types.h @@ -0,0 +1,219 @@ +// Copyright (c) 2022, 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. + +// Events that can happen when updating an enote store. + +#pragma once + +//local headers +#include "common/variant.h" +#include "crypto/crypto.h" +#include "ringct/rctTypes.h" +#include "seraphis_main/contextual_enote_record_types.h" + +//third party headers + +//standard headers + +//forward declarations + + +namespace sp +{ + +/// blocks added from a legacy intermediate scan update +struct LegacyIntermediateBlocksDiff final +{ + /// old index of top legacy intermediate scanned block + std::uint64_t old_top_index; + + /// range of new blocks added + std::uint64_t range_start_index; + std::uint64_t num_blocks_added; +}; + +/// blocks added from a legacy full scan update +struct LegacyBlocksDiff final +{ + /// old index of top legacy full scanned block + std::uint64_t old_top_index; + + /// range of new blocks added + std::uint64_t range_start_index; + std::uint64_t num_blocks_added; +}; + +/// blocks added from a seraphis intermediate scan update +struct SpIntermediateBlocksDiff final +{ + /// old index of top seraphis intermediate scanned block + std::uint64_t old_top_index; + + /// range of new blocks added + std::uint64_t range_start_index; + std::uint64_t num_blocks_added; +}; + +/// blocks added from a seraphis scan update +struct SpBlocksDiff final +{ + /// old index of top seraphis scanned block + std::uint64_t old_top_index; + + /// range of new blocks added + std::uint64_t range_start_index; + std::uint64_t num_blocks_added; +}; + +/// a legacy record's spent context was cleared +struct ClearedLegacySpentContext final +{ + rct::key identifier; +}; + +/// a seraphis record's spent context was cleared +struct ClearedSpSpentContext final +{ + crypto::key_image key_image; +}; + +/// a legacy record's spent context was updated +struct UpdatedLegacySpentContext final +{ + rct::key identifier; +}; + +/// a seraphis record's spent context was updated +struct UpdatedSpSpentContext final +{ + crypto::key_image key_image; +}; + +/// a legacy intermediate record's origin context was updated +struct UpdatedLegacyIntermediateOriginContext final +{ + rct::key identifier; +}; + +/// a legacy record's origin context was updated +struct UpdatedLegacyOriginContext final +{ + rct::key identifier; +}; + +/// a seraphis intermediate record's origin context was updated +struct UpdatedSpIntermediateOriginContext final +{ + rct::key onetime_address; +}; + +/// a seraphis record's origin context was updated +struct UpdatedSpOriginContext final +{ + crypto::key_image key_image; +}; + +/// a legacy intermediate record was removed +struct RemovedLegacyIntermediateRecord final +{ + rct::key identifier; +}; + +/// a legacy record was removed +struct RemovedLegacyRecord final +{ + rct::key identifier; +}; + +/// a seraphis intermediate record was removed +struct RemovedSpIntermediateRecord final +{ + rct::key onetime_address; +}; + +/// a seraphis record was removed +struct RemovedSpRecord final +{ + crypto::key_image key_image; +}; + +/// a legacy intermediate record was added +struct NewLegacyIntermediateRecord final +{ + rct::key identifier; +}; + +/// a legacy record was added +struct NewLegacyRecord final +{ + rct::key identifier; +}; + +/// a seraphis intermediate record was added +struct NewSpIntermediateRecord final +{ + rct::key onetime_address; +}; + +/// a seraphis record was added +struct NewSpRecord final +{ + crypto::key_image key_image; +}; + +/// an event in a seraphis payment validator enote store +using PaymentValidatorStoreEvent = + tools::variant< + SpIntermediateBlocksDiff, + UpdatedSpIntermediateOriginContext, + RemovedSpIntermediateRecord, + NewSpIntermediateRecord + >; + +/// an event in a generic enote store +using EnoteStoreEvent = + tools::variant< + LegacyIntermediateBlocksDiff, + LegacyBlocksDiff, + SpBlocksDiff, + ClearedLegacySpentContext, + ClearedSpSpentContext, + UpdatedLegacySpentContext, + UpdatedSpSpentContext, + UpdatedLegacyOriginContext, + UpdatedLegacyIntermediateOriginContext, + UpdatedSpOriginContext, + RemovedLegacyIntermediateRecord, + RemovedLegacyRecord, + RemovedSpRecord, + NewLegacyIntermediateRecord, + NewLegacyRecord, + NewSpRecord + >; + +} //namespace sp diff --git a/src/seraphis_impl/enote_store_payment_validator.cpp b/src/seraphis_impl/enote_store_payment_validator.cpp new file mode 100644 index 0000000000..20c8207638 --- /dev/null +++ b/src/seraphis_impl/enote_store_payment_validator.cpp @@ -0,0 +1,183 @@ +// Copyright (c) 2022, 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_store_payment_validator.h" + +//local headers +#include "common/container_helpers.h" +#include "misc_log_ex.h" +#include "seraphis_impl/enote_store.h" +#include "seraphis_impl/enote_store_event_types.h" +#include "seraphis_impl/enote_store_utils.h" +#include "seraphis_main/contextual_enote_record_types.h" +#include "seraphis_main/contextual_enote_record_utils.h" + +//third party headers + +//standard headers +#include +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis_impl" + +namespace sp +{ +//------------------------------------------------------------------------------------------------------------------- +SpEnoteStorePaymentValidator::SpEnoteStorePaymentValidator(const std::uint64_t refresh_index, + const std::uint64_t default_spendable_age, + const CheckpointCacheConfig &checkpoint_cache_config) : + m_sp_block_id_cache { checkpoint_cache_config, refresh_index }, + m_default_spendable_age { default_spendable_age } +{} +//------------------------------------------------------------------------------------------------------------------- +std::uint64_t SpEnoteStorePaymentValidator::next_sp_scanned_block_index(const std::uint64_t block_index) const +{ + // get the cached seraphis block index > the requested index + return m_sp_block_id_cache.get_next_block_index(block_index); +} +//------------------------------------------------------------------------------------------------------------------- +std::uint64_t SpEnoteStorePaymentValidator::nearest_sp_scanned_block_index(const std::uint64_t block_index) const +{ + // get the cached seraphis block index <= the requested index + return m_sp_block_id_cache.get_nearest_block_index(block_index); +} +//------------------------------------------------------------------------------------------------------------------- +bool SpEnoteStorePaymentValidator::try_get_block_id_for_sp(const std::uint64_t block_index, rct::key &block_id_out) const +{ + // 1. get the nearest cached legacy block index + // - we use this indirection to validate edge conditions + const std::uint64_t nearest_cached_index{this->nearest_sp_scanned_block_index(block_index)}; + + // 2. check error states + if (nearest_cached_index == this->refresh_index() - 1 || + nearest_cached_index != block_index) + return false; + + // 3. get the block id + CHECK_AND_ASSERT_THROW_MES(m_sp_block_id_cache.try_get_block_id(block_index, block_id_out), + "sp payment validator (try get block id sp scan): failed to get cached block id for index that is known."); + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +void SpEnoteStorePaymentValidator::update_with_sp_records_from_nonledger( + const SpEnoteOriginStatus nonledger_origin_status, + const std::unordered_map &found_enote_records, + std::list &events_inout) +{ + CHECK_AND_ASSERT_THROW_MES(nonledger_origin_status == SpEnoteOriginStatus::OFFCHAIN || + nonledger_origin_status == SpEnoteOriginStatus::UNCONFIRMED, + "sp payment validator (clean maps for sp nonledger update): invalid origin status."); + + // 1. remove records that will be replaced + tools::for_all_in_map_erase_if(m_sp_contextual_enote_records, + [nonledger_origin_status, &events_inout](const auto &mapped_contextual_enote_record) -> bool + { + // a. ignore enotes that don't have our specified origin + if (mapped_contextual_enote_record.second.origin_context.origin_status != nonledger_origin_status) + return false; + + // b. save the onetime address of the record being removed + events_inout.emplace_back(RemovedSpIntermediateRecord{mapped_contextual_enote_record.first}); + + // c. remove the record + return true; + } + ); + + // 2. add found enotes + for (const auto &found_enote_record : found_enote_records) + this->add_record(found_enote_record.second, events_inout); +} +//------------------------------------------------------------------------------------------------------------------- +void SpEnoteStorePaymentValidator::update_with_sp_records_from_ledger(const rct::key &alignment_block_id, + const std::uint64_t first_new_block, + const std::vector &new_block_ids, + const std::unordered_map &found_enote_records, + std::list &events_inout) +{ + // 1. set new block ids in range [first_new_block, end of chain] + SpIntermediateBlocksDiff diff{}; + update_checkpoint_cache_with_new_block_ids(alignment_block_id, + first_new_block, + new_block_ids, + m_sp_block_id_cache, + diff.old_top_index, + diff.range_start_index, + diff.num_blocks_added); + events_inout.emplace_back(diff); + + // 2. remove records that will be replaced + tools::for_all_in_map_erase_if(m_sp_contextual_enote_records, + [first_new_block, &events_inout](const auto &mapped_contextual_enote_record) -> bool + { + // a. ignore enotes that aren't onchain + if (!has_origin_status(mapped_contextual_enote_record.second, SpEnoteOriginStatus::ONCHAIN)) + return false; + + // b. ignore enotes not in range [first_new_block, end of chain] + if (mapped_contextual_enote_record.second.origin_context.block_index < first_new_block) + return false; + + // c. save the onetime address of the record being removed + events_inout.emplace_back(RemovedSpIntermediateRecord{mapped_contextual_enote_record.first}); + + // d. remove the record + return true; + } + ); + + // 3. add found enotes + for (const auto &found_enote_record : found_enote_records) + this->add_record(found_enote_record.second, events_inout); +} +//------------------------------------------------------------------------------------------------------------------- +// PAYMENT VALIDATOR INTERNAL +//------------------------------------------------------------------------------------------------------------------- +void SpEnoteStorePaymentValidator::add_record(const SpContextualIntermediateEnoteRecordV1 &new_record, + std::list &events_inout) +{ + const rct::key &record_onetime_address{onetime_address_ref(new_record)}; + + // add the record or update an existing record's origin context + if (m_sp_contextual_enote_records.find(record_onetime_address) == m_sp_contextual_enote_records.end()) + { + m_sp_contextual_enote_records[record_onetime_address] = new_record; + events_inout.emplace_back(NewSpIntermediateRecord{record_onetime_address}); + } + else + { + if (try_update_enote_origin_context_v1(new_record.origin_context, + m_sp_contextual_enote_records[record_onetime_address].origin_context)) + events_inout.emplace_back(UpdatedSpIntermediateOriginContext{record_onetime_address}); + } +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace sp diff --git a/src/seraphis_impl/enote_store_payment_validator.h b/src/seraphis_impl/enote_store_payment_validator.h new file mode 100644 index 0000000000..a7e0facdd6 --- /dev/null +++ b/src/seraphis_impl/enote_store_payment_validator.h @@ -0,0 +1,116 @@ +// Copyright (c) 2022, 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. + +// Enote store for a seraphis 'payment validator' that can read the amounts and destinations of incoming normal enotes. + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "seraphis_impl/checkpoint_cache.h" +#include "seraphis_impl/enote_store_event_types.h" +#include "seraphis_main/contextual_enote_record_types.h" + +//third party headers +#include "boost/multiprecision/cpp_int.hpp" + +//standard headers +#include +#include + +//forward declarations + + +namespace sp +{ + +//// +// SpEnoteStorePaymentValidator +// - tracks amounts and destinations of normal seraphis owned enotes (selfsends are not tracked) +/// +class SpEnoteStorePaymentValidator final +{ +public: +//constructors + /// normal constructor + SpEnoteStorePaymentValidator(const std::uint64_t refresh_index, + const std::uint64_t default_spendable_age, + const CheckpointCacheConfig &checkpoint_cache_config = + CheckpointCacheConfig{ + .num_unprunable = 50, + .max_separation = 100000, + .density_factor = 20 + } + ); + +//member functions + /// get index of the first block the enote store cares about + std::uint64_t refresh_index() const { return m_sp_block_id_cache.min_checkpoint_index(); } + /// get index of the highest cached block (refresh index - 1 if no cached blocks) + std::uint64_t top_block_index() const { return m_sp_block_id_cache.top_block_index(); } + /// get the default spendable age (config value) + std::uint64_t default_spendable_age() const { return m_default_spendable_age; } + /// get the next cached block index > the requested index (-1 on failure) + std::uint64_t next_sp_scanned_block_index(const std::uint64_t block_index) const; + /// get the nearest cached block index <= the requested index (refresh index - 1 on failure) + std::uint64_t nearest_sp_scanned_block_index(const std::uint64_t block_index) const; + /// try to get the cached block id for a given index + bool try_get_block_id_for_sp(const std::uint64_t block_index, rct::key &block_id_out) const; + + /// get the seraphis intermediate records: [ Ko : sp intermediate records ] + const std::unordered_map& sp_intermediate_records() const + { return m_sp_contextual_enote_records; } + + /// update the store with enote records, with associated context + void update_with_sp_records_from_nonledger(const SpEnoteOriginStatus nonledger_origin_status, + const std::unordered_map &found_enote_records, + std::list &events_inout); + void update_with_sp_records_from_ledger(const rct::key &alignment_block_id, + const std::uint64_t first_new_block, + const std::vector &new_block_ids, + const std::unordered_map &found_enote_records, + std::list &events_inout); + +private: + /// add a record + void add_record(const SpContextualIntermediateEnoteRecordV1 &new_record, + std::list &events_inout); + +//member variables + /// seraphis enotes + std::unordered_map m_sp_contextual_enote_records; + + /// cached block ids in range [refresh index, end of known chain] + CheckpointCache m_sp_block_id_cache; + + /// configuration value: default spendable age; an enote is considered 'spendable' in the next block if it is + /// on-chain and the next block's index is >= 'enote origin index + max(1, default_spendable_age)' + const std::uint64_t m_default_spendable_age; +}; + +} //namespace sp diff --git a/src/seraphis_impl/enote_store_utils.cpp b/src/seraphis_impl/enote_store_utils.cpp new file mode 100644 index 0000000000..079fb31d0d --- /dev/null +++ b/src/seraphis_impl/enote_store_utils.cpp @@ -0,0 +1,509 @@ +// Copyright (c) 2022, 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_store_utils.h" + +//local headers +#include "misc_log_ex.h" +#include "seraphis_impl/checkpoint_cache.h" +#include "seraphis_impl/enote_store.h" +#include "seraphis_impl/enote_store_payment_validator.h" +#include "seraphis_main/contextual_enote_record_types.h" +#include "seraphis_main/contextual_enote_record_utils.h" +#include "seraphis_main/enote_record_utils_legacy.h" +#include "seraphis_main/scan_machine_types.h" + +//third party headers +#include "boost/multiprecision/cpp_int.hpp" + +//standard headers +#include +#include +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis_impl" + +namespace sp +{ +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static boost::multiprecision::uint128_t get_balance_intermediate_legacy( + // [ legacy identifier : legacy intermediate record ] + const std::unordered_map &legacy_intermediate_records, + // [ Ko : legacy identifier ] + const std::unordered_map> &legacy_onetime_address_identifier_map, + const std::uint64_t top_block_index, + const std::uint64_t default_spendable_age, + const std::unordered_set &origin_statuses, + const std::unordered_set &exclusions) +{ + boost::multiprecision::uint128_t balance{0}; + + // 1. ignore if excluded + if (exclusions.find(BalanceExclusions::LEGACY_INTERMEDIATE) != exclusions.end()) + return 0; + + // 2. accumulate balance + // note: it is unknown if enotes in intermediate records are spent + for (const auto &mapped_contextual_record : legacy_intermediate_records) + { + const LegacyContextualIntermediateEnoteRecordV1 ¤t_contextual_record{mapped_contextual_record.second}; + + // a. ignore this enote if its origin status is not requested + if (origin_statuses.find(current_contextual_record.origin_context.origin_status) == origin_statuses.end()) + continue; + + // b. ignore locked onchain enotes if they should be excluded + if (exclusions.find(BalanceExclusions::ORIGIN_LEDGER_LOCKED) != exclusions.end() && + current_contextual_record.origin_context.origin_status == SpEnoteOriginStatus::ONCHAIN && + onchain_legacy_enote_is_locked( + current_contextual_record.origin_context.block_index, + current_contextual_record.record.unlock_time, + top_block_index, + default_spendable_age, + static_cast(std::time(nullptr)) + )) + continue; + + // c. ignore enotes that share onetime addresses with other enotes but don't have the highest amount among them + CHECK_AND_ASSERT_THROW_MES(legacy_onetime_address_identifier_map + .find(onetime_address_ref(current_contextual_record.record.enote)) != + legacy_onetime_address_identifier_map.end(), + "get balance intermediate legacy: tracked legacy duplicates is missing a onetime address (bug)."); + + if (!legacy_enote_has_highest_amount_in_set(mapped_contextual_record.first, + current_contextual_record.record.amount, + origin_statuses, + legacy_onetime_address_identifier_map.at( + onetime_address_ref(current_contextual_record.record.enote) + ), + [&legacy_intermediate_records](const rct::key &identifier) -> const SpEnoteOriginStatus& + { + CHECK_AND_ASSERT_THROW_MES(legacy_intermediate_records.find(identifier) != + legacy_intermediate_records.end(), + "get balance intermediate legacy: tracked legacy duplicates has an entry that " + "doesn't line up 1:1 with the legacy intermediate map even though it should (bug)."); + + return legacy_intermediate_records + .at(identifier) + .origin_context + .origin_status; + }, + [&legacy_intermediate_records](const rct::key &identifier) -> rct::xmr_amount + { + CHECK_AND_ASSERT_THROW_MES(legacy_intermediate_records.find(identifier) != + legacy_intermediate_records.end(), + "get balance intermediate legacy: tracked legacy duplicates has an entry that " + "doesn't line up 1:1 with the legacy intermediate map even though it should (bug)."); + + return legacy_intermediate_records.at(identifier).record.amount; + })) + continue; + + // d. update balance + balance += current_contextual_record.record.amount; + } + + return balance; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static boost::multiprecision::uint128_t get_balance_full_legacy( + // [ legacy identifier : legacy record ] + const std::unordered_map &legacy_records, + // [ Ko : legacy identifier ] + const std::unordered_map> &legacy_onetime_address_identifier_map, + const std::uint64_t top_block_index, + const std::uint64_t default_spendable_age, + const std::unordered_set &origin_statuses, + const std::unordered_set &spent_statuses, + const std::unordered_set &exclusions) +{ + boost::multiprecision::uint128_t balance{0}; + + // 1. ignore if excluded + if (exclusions.find(BalanceExclusions::LEGACY_FULL) != exclusions.end()) + return 0; + + // 2. accumulate balance + for (const auto &mapped_contextual_record : legacy_records) + { + const LegacyContextualEnoteRecordV1 ¤t_contextual_record{mapped_contextual_record.second}; + + // a. ignore this enote if its origin status is not requested + if (origin_statuses.find(current_contextual_record.origin_context.origin_status) == origin_statuses.end()) + continue; + + // b. ignore this enote if its spent status is requested + if (spent_statuses.find(current_contextual_record.spent_context.spent_status) != spent_statuses.end()) + continue; + + // c. ignore locked onchain enotes if they should be excluded + if (exclusions.find(BalanceExclusions::ORIGIN_LEDGER_LOCKED) != exclusions.end() && + current_contextual_record.origin_context.origin_status == SpEnoteOriginStatus::ONCHAIN && + onchain_legacy_enote_is_locked( + current_contextual_record.origin_context.block_index, + current_contextual_record.record.unlock_time, + top_block_index, + default_spendable_age, + static_cast(std::time(nullptr))) + ) + continue; + + // d. ignore enotes that share onetime addresses with other enotes but don't have the highest amount among them + CHECK_AND_ASSERT_THROW_MES(legacy_onetime_address_identifier_map + .find(onetime_address_ref(current_contextual_record.record.enote)) != + legacy_onetime_address_identifier_map.end(), + "get balance full legacy: tracked legacy duplicates is missing a onetime address (bug)."); + + if (!legacy_enote_has_highest_amount_in_set(mapped_contextual_record.first, + current_contextual_record.record.amount, + origin_statuses, + legacy_onetime_address_identifier_map.at( + onetime_address_ref(current_contextual_record.record.enote) + ), + [&legacy_records](const rct::key &identifier) -> const SpEnoteOriginStatus& + { + CHECK_AND_ASSERT_THROW_MES(legacy_records.find(identifier) != legacy_records.end(), + "get balance full legacy: tracked legacy duplicates has an entry that doesn't line up " + "1:1 with the legacy map even though it should (bug)."); + + return legacy_records + .at(identifier) + .origin_context + .origin_status; + }, + [&legacy_records](const rct::key &identifier) -> rct::xmr_amount + { + CHECK_AND_ASSERT_THROW_MES(legacy_records.find(identifier) != legacy_records.end(), + "get balance full legacy: tracked legacy duplicates has an entry that doesn't line up " + "1:1 with the legacy map even though it should (bug)."); + + return legacy_records.at(identifier).record.amount; + })) + continue; + + // e. update balance + balance += current_contextual_record.record.amount; + } + + return balance; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static boost::multiprecision::uint128_t get_balance_intermediate_seraphis( + const std::unordered_map &sp_intermediate_records, + const std::uint64_t top_block_index, + const std::uint64_t default_spendable_age, + const std::unordered_set &origin_statuses, + const std::unordered_set &exclusions) +{ + boost::multiprecision::uint128_t received_sum{0}; + + // 1. ignore if excluded + if (exclusions.find(BalanceExclusions::SERAPHIS_INTERMEDIATE) != exclusions.end()) + return 0; + + // 2. accumulate received sum + // note: it is unknown if enotes in intermediate records are spent + for (const auto &mapped_contextual_record : sp_intermediate_records) + { + const SpContextualIntermediateEnoteRecordV1 ¤t_contextual_record{mapped_contextual_record.second}; + + // a. ignore this enote if its origin status is not requested + if (origin_statuses.find(current_contextual_record.origin_context.origin_status) == origin_statuses.end()) + continue; + + // b. ignore locked onchain enotes if they should be excluded + if (exclusions.find(BalanceExclusions::ORIGIN_LEDGER_LOCKED) != exclusions.end() && + current_contextual_record.origin_context.origin_status == SpEnoteOriginStatus::ONCHAIN && + onchain_sp_enote_is_locked( + current_contextual_record.origin_context.block_index, + top_block_index, + default_spendable_age + )) + continue; + + // c. update received sum + received_sum += current_contextual_record.record.amount; + } + + return received_sum; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static boost::multiprecision::uint128_t get_balance_full_seraphis( + const std::unordered_map &sp_records, + const std::uint64_t top_block_index, + const std::uint64_t default_spendable_age, + const std::unordered_set &origin_statuses, + const std::unordered_set &spent_statuses, + const std::unordered_set &exclusions) +{ + boost::multiprecision::uint128_t balance{0}; + + // 1. ignore if excluded + if (exclusions.find(BalanceExclusions::SERAPHIS_FULL) != exclusions.end()) + return 0; + + // 2. accumulate balance + for (const auto &mapped_contextual_record : sp_records) + { + const SpContextualEnoteRecordV1 ¤t_contextual_record{mapped_contextual_record.second}; + + // a. ignore this enote if its origin status is not requested + if (origin_statuses.find(current_contextual_record.origin_context.origin_status) == origin_statuses.end()) + continue; + + // b. ignore this enote if its spent status is requested + if (spent_statuses.find(current_contextual_record.spent_context.spent_status) != spent_statuses.end()) + continue; + + // c. ignore locked onchain enotes if they should be excluded + if (exclusions.find(BalanceExclusions::ORIGIN_LEDGER_LOCKED) != exclusions.end() && + current_contextual_record.origin_context.origin_status == SpEnoteOriginStatus::ONCHAIN && + onchain_sp_enote_is_locked( + current_contextual_record.origin_context.block_index, + top_block_index, + default_spendable_age + )) + continue; + + // d. update balance + balance += current_contextual_record.record.amount; + } + + return balance; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +void update_checkpoint_cache_with_new_block_ids(const rct::key &alignment_block_id, + const std::uint64_t first_new_block_index, + const std::vector &new_block_ids, + CheckpointCache &cache_inout, + std::uint64_t &old_top_index_out, + std::uint64_t &range_start_index_out, + std::uint64_t &num_blocks_added_out) +{ + // 1. check inputs + const std::uint64_t first_allowed_index{cache_inout.min_checkpoint_index()}; + + CHECK_AND_ASSERT_THROW_MES(first_new_block_index >= first_allowed_index, + "update checkpoint cache with new block ids: first new block is below the refresh index."); + CHECK_AND_ASSERT_THROW_MES(first_new_block_index - first_allowed_index <= + cache_inout.top_block_index() - cache_inout.min_checkpoint_index() + 1, + "update checkpoint cache with new block ids: new blocks don't line up with existing blocks."); + if (first_new_block_index > first_allowed_index) + { + rct::key cached_alignment_block_id; + CHECK_AND_ASSERT_THROW_MES(cache_inout.try_get_block_id(first_new_block_index - 1, cached_alignment_block_id) && + alignment_block_id == cached_alignment_block_id, + "update checkpoint cache with new block ids: alignment block id doesn't align with cached block ids."); + } + + // 2. save the diff + old_top_index_out = cache_inout.top_block_index(); + range_start_index_out = first_new_block_index; + num_blocks_added_out = new_block_ids.size(); + + // 3. insert the new block ids + cache_inout.insert_new_block_ids(first_new_block_index, new_block_ids); +} +//------------------------------------------------------------------------------------------------------------------- +scanning::ContiguityMarker get_next_legacy_partialscanned_block(const SpEnoteStore &enote_store, + const std::uint64_t block_index) +{ + // 1. next block known by enote store > test index + const std::uint64_t next_index{enote_store.next_legacy_partialscanned_block_index(block_index)}; + + // 2. try to get the block index for the next block + rct::key temp_block_id; + if (!enote_store.try_get_block_id_for_legacy_partialscan(next_index, temp_block_id)) + return scanning::ContiguityMarker{static_cast(-1), boost::none}; + + // 3. { next block index, next block id } + return scanning::ContiguityMarker{next_index, temp_block_id}; +} +//------------------------------------------------------------------------------------------------------------------- +scanning::ContiguityMarker get_next_legacy_fullscanned_block(const SpEnoteStore &enote_store, + const std::uint64_t block_index) +{ + // 1. next block known by enote store > test index + const std::uint64_t next_index{enote_store.next_legacy_fullscanned_block_index(block_index)}; + + // 2. try to get the block index for the next block + rct::key temp_block_id; + if (!enote_store.try_get_block_id_for_legacy_fullscan(next_index, temp_block_id)) + return scanning::ContiguityMarker{static_cast(-1), boost::none}; + + // 3. { next block index, next block id } + return scanning::ContiguityMarker{next_index, temp_block_id}; +} +//------------------------------------------------------------------------------------------------------------------- +scanning::ContiguityMarker get_next_sp_scanned_block(const SpEnoteStorePaymentValidator &enote_store, + const std::uint64_t block_index) +{ + // 1. next block known by enote store > test index + const std::uint64_t next_index{enote_store.next_sp_scanned_block_index(block_index)}; + + // 2. try to get the block index for the next block + rct::key temp_block_id; + if (!enote_store.try_get_block_id_for_sp(next_index, temp_block_id)) + return scanning::ContiguityMarker{static_cast(-1), boost::none}; + + // 3. { next block index, next block id } + return scanning::ContiguityMarker{next_index, temp_block_id}; +} +//------------------------------------------------------------------------------------------------------------------- +scanning::ContiguityMarker get_next_sp_scanned_block(const SpEnoteStore &enote_store, const std::uint64_t block_index) +{ + // 1. next block known by enote store > test index + const std::uint64_t next_index{enote_store.next_sp_scanned_block_index(block_index)}; + + // 2. try to get the block index for the next block + rct::key temp_block_id; + if (!enote_store.try_get_block_id_for_sp(next_index, temp_block_id)) + return scanning::ContiguityMarker{static_cast(-1), boost::none}; + + // 3. { next block index, next block id } + return scanning::ContiguityMarker{next_index, temp_block_id}; +} +//------------------------------------------------------------------------------------------------------------------- +scanning::ContiguityMarker get_nearest_legacy_partialscanned_block(const SpEnoteStore &enote_store, + const std::uint64_t block_index) +{ + // 1. nearest block known by enote store <= test index + const std::uint64_t nearest_index{enote_store.nearest_legacy_partialscanned_block_index(block_index)}; + + // 2. try to get the block index for the nearest block + rct::key temp_block_id; + if (!enote_store.try_get_block_id_for_legacy_partialscan(nearest_index, temp_block_id)) + return scanning::ContiguityMarker{enote_store.legacy_refresh_index() - 1, boost::none}; + + // 3. { nearest block index, nearest block id } + return scanning::ContiguityMarker{nearest_index, temp_block_id}; +} +//------------------------------------------------------------------------------------------------------------------- +scanning::ContiguityMarker get_nearest_legacy_fullscanned_block(const SpEnoteStore &enote_store, + const std::uint64_t block_index) +{ + // 1. nearest block known by enote store <= test index + const std::uint64_t nearest_index{enote_store.nearest_legacy_fullscanned_block_index(block_index)}; + + // 2. try to get the block index for the nearest block + rct::key temp_block_id; + if (!enote_store.try_get_block_id_for_legacy_fullscan(nearest_index, temp_block_id)) + return scanning::ContiguityMarker{enote_store.legacy_refresh_index() - 1, boost::none}; + + // 3. { nearest block index, nearest block id } + return scanning::ContiguityMarker{nearest_index, temp_block_id}; +} +//------------------------------------------------------------------------------------------------------------------- +scanning::ContiguityMarker get_nearest_sp_scanned_block(const SpEnoteStorePaymentValidator &enote_store, + const std::uint64_t block_index) +{ + // 1. nearest block known by enote store <= test index + const std::uint64_t nearest_index{enote_store.nearest_sp_scanned_block_index(block_index)}; + + // 2. try to get the block index for the nearest block + rct::key temp_block_id; + if (!enote_store.try_get_block_id_for_sp(nearest_index, temp_block_id)) + return scanning::ContiguityMarker{enote_store.refresh_index() - 1, boost::none}; + + // 3. { nearest block index, nearest block id } + return scanning::ContiguityMarker{nearest_index, temp_block_id}; +} +//------------------------------------------------------------------------------------------------------------------- +scanning::ContiguityMarker get_nearest_sp_scanned_block(const SpEnoteStore &enote_store, const std::uint64_t block_index) +{ + // 1. nearest block known by enote store <= test index + const std::uint64_t nearest_index{enote_store.nearest_sp_scanned_block_index(block_index)}; + + // 2. try to get the block index for the nearest block + rct::key temp_block_id; + if (!enote_store.try_get_block_id_for_sp(nearest_index, temp_block_id)) + return scanning::ContiguityMarker{enote_store.sp_refresh_index() - 1, boost::none}; + + // 3. { nearest block index, nearest block id } + return scanning::ContiguityMarker{nearest_index, temp_block_id}; +} +//------------------------------------------------------------------------------------------------------------------- +boost::multiprecision::uint128_t get_balance(const SpEnoteStore &enote_store, + const std::unordered_set &origin_statuses, + const std::unordered_set &spent_statuses, + const std::unordered_set &exclusions) +{ + boost::multiprecision::uint128_t balance{0}; + + // 1. intermediate legacy enotes (it is unknown if these enotes are spent) + balance += get_balance_intermediate_legacy(enote_store.legacy_intermediate_records(), + enote_store.legacy_onetime_address_identifier_map(), + enote_store.top_block_index(), + enote_store.default_spendable_age(), + origin_statuses, + exclusions); + + // 2. full legacy enotes + balance += get_balance_full_legacy(enote_store.legacy_records(), + enote_store.legacy_onetime_address_identifier_map(), + enote_store.top_block_index(), + enote_store.default_spendable_age(), + origin_statuses, + spent_statuses, + exclusions); + + // 3. seraphis enotes + balance += get_balance_full_seraphis(enote_store.sp_records(), + enote_store.top_block_index(), + enote_store.default_spendable_age(), + origin_statuses, + spent_statuses, + exclusions); + + return balance; +} +//------------------------------------------------------------------------------------------------------------------- +boost::multiprecision::uint128_t get_received_sum(const SpEnoteStorePaymentValidator &payment_validator, + const std::unordered_set &origin_statuses, + const std::unordered_set &exclusions) +{ + boost::multiprecision::uint128_t received_sum{0}; + + // 1. intermediate seraphis enotes (received normal enotes only; it is unknown if they are spent) + received_sum += get_balance_intermediate_seraphis(payment_validator.sp_intermediate_records(), + payment_validator.top_block_index(), + payment_validator.default_spendable_age(), + origin_statuses, + exclusions); + + return received_sum; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace sp diff --git a/src/seraphis_impl/enote_store_utils.h b/src/seraphis_impl/enote_store_utils.h new file mode 100644 index 0000000000..dac59c80db --- /dev/null +++ b/src/seraphis_impl/enote_store_utils.h @@ -0,0 +1,140 @@ +// Copyright (c) 2022, 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 interacting with enote stores. + +#pragma once + +//local headers +#include "ringct/rctTypes.h" +#include "seraphis_main/contextual_enote_record_types.h" + +//third party headers +#include "boost/multiprecision/cpp_int.hpp" + +//standard headers +#include +#include + +//forward declarations +namespace sp +{ + class CheckpointCache; + class SpEnoteStore; + class SpEnoteStorePaymentValidator; +namespace scanning +{ + struct ContiguityMarker; +} +} + +namespace sp +{ + +//// +// BalanceExclusions +// - Enotes that match with a balance exclusion will not be included in a balance calculation. +/// +enum class BalanceExclusions +{ + LEGACY_FULL, + LEGACY_INTERMEDIATE, + SERAPHIS_INTERMEDIATE, + SERAPHIS_FULL, + ORIGIN_LEDGER_LOCKED +}; + +/** +* brief: update_checkpoint_cache_with_new_block_ids - insert new block ids into a checkpoint cache +* param: alignment_block_id - +* param: first_new_block_index - +* param: new_block_ids - +* inoutparam: cache_inout - +* outparam: old_top_index_out - +* outparam: range_start_index_out - +* outparam: num_blocks_added_out - +*/ +void update_checkpoint_cache_with_new_block_ids(const rct::key &alignment_block_id, + const std::uint64_t first_new_block_index, + const std::vector &new_block_ids, + CheckpointCache &cache_inout, + std::uint64_t &old_top_index_out, + std::uint64_t &range_start_index_out, + std::uint64_t &num_blocks_added_out); +/** +* brief: get_next_*_block - get the enote store's next cached block > the test index +* - marker = {-1, boost::none} on failure +* param: enote_store - +* param: block_index - +* return: marker representing the enote store's next block > the test index +*/ +scanning::ContiguityMarker get_next_legacy_partialscanned_block(const SpEnoteStore &enote_store, + const std::uint64_t block_index); +scanning::ContiguityMarker get_next_legacy_fullscanned_block(const SpEnoteStore &enote_store, + const std::uint64_t block_index); +scanning::ContiguityMarker get_next_sp_scanned_block(const SpEnoteStorePaymentValidator &enote_store, + const std::uint64_t block_index); +scanning::ContiguityMarker get_next_sp_scanned_block(const SpEnoteStore &enote_store, const std::uint64_t block_index); +/** +* brief: get_nearest_*_block - get the enote store's nearest cached block <= the test index +* - marker = {refresh index - 1, boost::none} on failure +* param: enote_store - +* param: block_index - +* return: marker representing the enote store's nearest block <= the test index +*/ +scanning::ContiguityMarker get_nearest_legacy_partialscanned_block(const SpEnoteStore &enote_store, + const std::uint64_t block_index); +scanning::ContiguityMarker get_nearest_legacy_fullscanned_block(const SpEnoteStore &enote_store, + const std::uint64_t block_index); +scanning::ContiguityMarker get_nearest_sp_scanned_block(const SpEnoteStorePaymentValidator &enote_store, + const std::uint64_t block_index); +scanning::ContiguityMarker get_nearest_sp_scanned_block(const SpEnoteStore &enote_store, const std::uint64_t block_index); +/** +* brief: get_balance - get current balance of an enote store using specified origin/spent statuses and exclusions +* param: enote_store - +* param: origin_statuses - +* param: spent_statuses - +* param: exclusions - +* return: the total balance +*/ +boost::multiprecision::uint128_t get_balance(const SpEnoteStore &enote_store, + const std::unordered_set &origin_statuses, + const std::unordered_set &spent_statuses = {}, + const std::unordered_set &exclusions = {}); +/** +* brief: get_balance - get current total amount received using specified origin statuses and exclusions +* param: payment_validator - +* param: origin_statuses - +* param: exclusions - +* return: the total amount received +*/ +boost::multiprecision::uint128_t get_received_sum(const SpEnoteStorePaymentValidator &payment_validator, + const std::unordered_set &origin_statuses, + const std::unordered_set &exclusions = {}); + +} //namespace sp diff --git a/src/seraphis_impl/legacy_ki_import_tool.cpp b/src/seraphis_impl/legacy_ki_import_tool.cpp new file mode 100644 index 0000000000..7ccb988a61 --- /dev/null +++ b/src/seraphis_impl/legacy_ki_import_tool.cpp @@ -0,0 +1,155 @@ +// Copyright (c) 2022, 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 "legacy_ki_import_tool.h" + +//local headers +#include "crypto/crypto.h" +#include "misc_log_ex.h" +#include "ringct/rctTypes.h" +#include "seraphis_impl/enote_store.h" +#include "seraphis_impl/enote_store_event_types.h" +#include "seraphis_main/contextual_enote_record_types.h" + +//third party headers + +//standard headers +#include +#include +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis_impl" + +namespace sp +{ +//------------------------------------------------------------------------------------------------------------------- +void make_legacy_ki_import_checkpoint(const SpEnoteStore &enote_store, LegacyKIImportCheckpoint &checkpoint_out) +{ + // 1. get the enote store's last legacy partialscanned block + const std::uint64_t partialscan_index_pre_import_cycle{ + enote_store.top_legacy_partialscanned_block_index() + }; + + // 2. get the enote store's last legacy fullscanned block + const std::uint64_t fullscan_index_pre_import_cycle{ + enote_store.top_legacy_fullscanned_block_index() + }; + CHECK_AND_ASSERT_THROW_MES(fullscan_index_pre_import_cycle + 1 <= partialscan_index_pre_import_cycle + 1, + "make legacy ki import checkpoint: fullscanned block is higher than partialscanned block."); + + // 3. get the lowest block that the enote store needs to fullscan + const std::uint64_t legacy_refresh_index{enote_store.legacy_refresh_index()}; + const std::uint64_t first_new_index_for_fullscan{ + std::max(fullscan_index_pre_import_cycle + 1, legacy_refresh_index) + }; + + // 4. save block id checkpoints within range of partialscanned blocks we are trying to update + // - range: any block <= the first block to fullscan TO our last partialscanned-only block + checkpoint_out.block_id_checkpoints.clear(); + + for (std::uint64_t block_index{enote_store.nearest_legacy_partialscanned_block_index(first_new_index_for_fullscan)}; + block_index != enote_store.legacy_refresh_index() - 1 && //can happen if we never did ANY legacy scanning + block_index + 1 <= partialscan_index_pre_import_cycle + 1 && //shouldn't ever fail; better safe than sorry + block_index != static_cast(-1); //end condition + block_index = enote_store.next_legacy_partialscanned_block_index(block_index)) + { + CHECK_AND_ASSERT_THROW_MES(enote_store.try_get_block_id_for_legacy_partialscan(block_index, + checkpoint_out.block_id_checkpoints[block_index]), + "make legacy ki import checkpoint: failed to get block id for a legacy partialscan checkpoint."); + } + + // 5. export legacy intermediate records that need key images + checkpoint_out.legacy_intermediate_records = enote_store.legacy_intermediate_records(); +} +//------------------------------------------------------------------------------------------------------------------- +void import_legacy_key_images(const std::unordered_map &legacy_key_images, //[ Ko : KI ] + SpEnoteStore &enote_store_inout, + std::list &update_events_out) +{ + // import key images (ignore failures) + update_events_out.clear(); + for (const auto import_pair : legacy_key_images) + enote_store_inout.try_import_legacy_key_image(import_pair.second, import_pair.first, update_events_out); +} +//------------------------------------------------------------------------------------------------------------------- +void import_legacy_key_images(const std::unordered_map &legacy_key_images, + SpEnoteStore &enote_store_inout, + std::list &update_events_out) +{ + // import key images (ignore failures) + update_events_out.clear(); + for (const auto import_pair : legacy_key_images) + { + enote_store_inout.try_import_legacy_key_image(import_pair.second, + rct::pk2rct(import_pair.first), + update_events_out); + } +} +//------------------------------------------------------------------------------------------------------------------- +void finish_legacy_ki_import_cycle(const LegacyKIImportCheckpoint &checkpoint, SpEnoteStore &enote_store_inout) +{ + // 1. find the highest aligned checkpoint from when intermediate records were exported + // - we want to make sure any reorg that replaced blocks below the partial scan index recorded at the beginning of + // the cycle won't be ignored by the next partial scan + std::uint64_t highest_aligned_index_post_import_cycle{enote_store_inout.top_legacy_fullscanned_block_index()}; + rct::key temp_block_id; + + for (const auto &checkpoint : checkpoint.block_id_checkpoints) + { + if (!enote_store_inout.try_get_block_id_for_legacy_partialscan(checkpoint.first, temp_block_id)) + continue; + if (!(temp_block_id == checkpoint.second)) + break; + + highest_aligned_index_post_import_cycle = + std::max(checkpoint.first + 1, highest_aligned_index_post_import_cycle + 1) - 1; + } + + // 2. clamp the alignment index below the current enote store's lowest intermediate record + // - we do this in case not all records collected at the beginning of this import cycle were imported as expected + for (const auto &intermediate_record : enote_store_inout.legacy_intermediate_records()) + { + // a. ignore enotes that aren't on-chain + if (!has_origin_status(intermediate_record.second, SpEnoteOriginStatus::ONCHAIN)) + continue; + + // b. clamp the alignment index to one block below the intermediate record's origin + highest_aligned_index_post_import_cycle = + std::min( + highest_aligned_index_post_import_cycle + 1, + intermediate_record.second.origin_context.block_index + ) - 1; + } + + // 3. update the legacy fullscan index + enote_store_inout.update_legacy_fullscan_index_for_import_cycle(highest_aligned_index_post_import_cycle); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace sp diff --git a/src/seraphis_impl/legacy_ki_import_tool.h b/src/seraphis_impl/legacy_ki_import_tool.h new file mode 100644 index 0000000000..272bc302fd --- /dev/null +++ b/src/seraphis_impl/legacy_ki_import_tool.h @@ -0,0 +1,105 @@ +// Copyright (c) 2022, 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. + +// Tool for supporting a legacy key image import cycle. +// PROCESS: +// 1. update your enote store with a legacy intermediate view scan in SCAN MODE +// 2. TOOL: make an import cycle checkpoint with an atomic read-lock on your enote store +// 3. obtain key images for the intermediate records stored in the checkpoint +// - no invariants will be broken if only some of the key images are obtained, however that may cause the enote +// store to have an intermediate legacy balance that is higher than expected after the cycle +// 4. TOOL: import the key images to your enote store +// 5. update your enote store with a legacy intermediate view scan in KEY IMAGES ONLY MODE +// - this is needed to see if any of the imported key images exist on-chain +// 6. TOOL: finish the import cycle with an atomic write-lock on your enote store +// - do this AFTER the key-images-only scan, otherwise subsequent import cycles will waste time re-doing the blocks +// from this import cycle +// WARNING: this process will be less efficient if you do step 2, wait a while, do step 1 again, then finish 3-6; the +// reason is alignment tracking relies on block id checkpoints, and step 1 will 'thin out' older block id checkpoints +// in the enote store, making it possible for bad alignment checks when finalizing an import cycle; the end effect +// will be the next import cycle will redo some blocks from the previous cycle + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "ringct/rctTypes.h" +#include "seraphis_impl/enote_store.h" +#include "seraphis_impl/enote_store_event_types.h" +#include "seraphis_main/contextual_enote_record_types.h" + +//third party headers + +//standard headers + +//forward declarations +#include +#include +#include + +namespace sp +{ + +//// +// LegacyKIImportCheckpoint +// - A snapshot of an enote store for use in a legacy key image import cycle. +/// +struct LegacyKIImportCheckpoint final +{ + /// [ block index : block id ] in the range of blocks subject to this import cycle + std::map block_id_checkpoints; + /// [ legacy identifier : legacy intermediate records ] for legacy enotes subject to this import cycle + std::unordered_map legacy_intermediate_records; +}; + +/** +* brief: make_legacy_ki_import_checkpoint - make a legacy key image import cycle checkpoint +* param: enote_store - +* outparam: checkpoint_out - +*/ +void make_legacy_ki_import_checkpoint(const SpEnoteStore &enote_store, LegacyKIImportCheckpoint &checkpoint_out); +/** +* brief: import_legacy_key_images - import legacy key images to an enote store +* param: legacy_key_images - [ Ko : KI ] +* inoutparam: enote_store_inout - +*/ +void import_legacy_key_images(const std::unordered_map &legacy_key_images, + SpEnoteStore &enote_store_inout, + std::list &update_events_out); +void import_legacy_key_images(const std::unordered_map &legacy_key_images, + SpEnoteStore &enote_store_inout, + std::list &update_events_out); +/** +* brief: finish_legacy_ki_import_cycle - finish a legacy key image import cycle by updating the enote store's +* cached fullscan index +* param: checkpoint - +* inoutparam: enote_store_inout - +*/ +void finish_legacy_ki_import_cycle(const LegacyKIImportCheckpoint &checkpoint, SpEnoteStore &enote_store_inout); + +} //namespace sp diff --git a/src/seraphis_impl/scan_context_simple.h b/src/seraphis_impl/scan_context_simple.h new file mode 100644 index 0000000000..c796813481 --- /dev/null +++ b/src/seraphis_impl/scan_context_simple.h @@ -0,0 +1,150 @@ +// Copyright (c) 2022, 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. + +// NOT FOR PRODUCTION + +// Simple implementations of enote scanning contexts. + +#pragma once + +//local headers +#include "seraphis_main/enote_finding_context.h" +#include "seraphis_main/scan_context.h" +#include "seraphis_main/scan_core_types.h" +#include "seraphis_main/scan_ledger_chunk.h" + +//third party headers + +//standard headers +#include + +//forward declarations + + +namespace sp +{ +namespace scanning +{ + +//// +// ScanContextNonLedgerDummy +// - dummy nonledger scanning context +/// +class ScanContextNonLedgerDummy final : public ScanContextNonLedger +{ +public: + void get_nonledger_chunk(ChunkData &chunk_out) override { chunk_out = ChunkData{}; } + bool is_aborted() const override { return false; } +}; + +//// +// ScanContextNonLedgerSimple +// - simple implementation: synchronously obtain chunks from an enote finding context +/// +class ScanContextNonLedgerSimple final : public ScanContextNonLedger +{ +public: +//constructor + ScanContextNonLedgerSimple(const EnoteFindingContextNonLedger &enote_finding_context) : + m_enote_finding_context{enote_finding_context} + {} + +//overloaded operators + /// disable copy/move (this is a scoped manager [reference wrapper]) + ScanContextNonLedgerSimple& operator=(ScanContextNonLedgerSimple&&) = delete; + +//member functions + /// get a scanning chunk for the nonledger txs in the injected context + void get_nonledger_chunk(ChunkData &chunk_out) override + { + m_enote_finding_context.get_nonledger_chunk(chunk_out); + } + /// test if scanning has been aborted + bool is_aborted() const override { return false; } + +//member variables +private: + /// enote finding context: finds chunks of enotes that are potentially owned + const EnoteFindingContextNonLedger &m_enote_finding_context; +}; + +//// +// ScanContextLedgerSimple +// - simple implementation: synchronously obtain chunks from an enote finding context +/// +class ScanContextLedgerSimple final : public ScanContextLedger +{ +public: +//constructor + ScanContextLedgerSimple(const EnoteFindingContextLedger &enote_finding_context) : + m_enote_finding_context{enote_finding_context} + {} + +//overloaded operators + /// disable copy/move (this is a scoped manager [reference wrapper]) + ScanContextLedgerSimple& operator=(ScanContextLedgerSimple&&) = delete; + +//member functions + /// start scanning from a specified block index + void begin_scanning_from_index(const std::uint64_t initial_start_index, + const std::uint64_t max_chunk_size_hint) override + { + m_next_start_index = initial_start_index; + m_max_chunk_size = max_chunk_size_hint; + } + /// get the next available onchain chunk (or empty chunk representing top of current chain) + /// - start past the end of the last chunk acquired since starting to scan + std::unique_ptr get_onchain_chunk() override + { + // 1. try to get a chunk + std::unique_ptr chunk{ + m_enote_finding_context.get_onchain_chunk(m_next_start_index, m_max_chunk_size) + }; + if (!chunk) + return nullptr; + + // 2. save the next chunk's expected start index + m_next_start_index = chunk->get_context().start_index + chunk->get_context().block_ids.size(); + return chunk; + } + /// stop the current scanning process (should be no-throw no-fail) + void terminate_scanning() override { /* no-op */ } + /// test if scanning has been aborted + bool is_aborted() const override { return false; } + +//member variables +private: + /// enote finding context: finds chunks of enotes that are potentially owned + const EnoteFindingContextLedger &m_enote_finding_context; + + std::uint64_t m_next_start_index{static_cast(-1)}; + std::uint64_t m_max_chunk_size{0}; +}; + +} //namespace scanning +} //namespace sp diff --git a/src/seraphis_impl/scan_ledger_chunk_async.cpp b/src/seraphis_impl/scan_ledger_chunk_async.cpp new file mode 100644 index 0000000000..a7b4ca7f1c --- /dev/null +++ b/src/seraphis_impl/scan_ledger_chunk_async.cpp @@ -0,0 +1,113 @@ +// Copyright (c) 2022, 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 "scan_ledger_chunk_async.h" + +//local headers +#include "async/misc_utils.h" +#include "async/threadpool.h" +#include "misc_log_ex.h" +#include "ringct/rctTypes.h" +#include "seraphis_main/scan_core_types.h" +#include "seraphis_main/scan_misc_utils.h" + +//third party headers + +//standard headers +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis_impl" + +namespace sp +{ +namespace scanning +{ +//------------------------------------------------------------------------------------------------------------------- +AsyncLedgerChunk::AsyncLedgerChunk(async::Threadpool &threadpool, + PendingChunkContext &&pending_context, + std::vector &&pending_data, + std::vector subconsumer_ids) : + m_threadpool{threadpool}, + m_pending_context{std::move(pending_context)}, + m_pending_data{std::move(pending_data)}, + m_subconsumer_ids{std::move(subconsumer_ids)} +{ + CHECK_AND_ASSERT_THROW_MES(m_pending_data.size() == m_subconsumer_ids.size(), + "async ledger chunk: pending data and subconsumer ids size mismatch."); +} +//------------------------------------------------------------------------------------------------------------------- +const ChunkContext& AsyncLedgerChunk::get_context() const +{ + this->wait_for_context(); + return m_pending_context.chunk_context.get(); +} +//------------------------------------------------------------------------------------------------------------------- +const ChunkData* AsyncLedgerChunk::try_get_data(const rct::key &subconsumer_id) const +{ + auto id_it = std::find(m_subconsumer_ids.begin(), m_subconsumer_ids.end(), subconsumer_id); + if (id_it == m_subconsumer_ids.end()) return nullptr; + const std::size_t pending_data_index{static_cast(std::distance(m_subconsumer_ids.begin(), id_it))}; + + this->wait_for_data(pending_data_index); + return &(m_pending_data[pending_data_index].chunk_data.get()); +} +//------------------------------------------------------------------------------------------------------------------- +const std::vector& AsyncLedgerChunk::subconsumer_ids() const +{ + return m_subconsumer_ids; +} +//------------------------------------------------------------------------------------------------------------------- +// ASYNC LEDGER CHUNK INTERNAL +//------------------------------------------------------------------------------------------------------------------- +void AsyncLedgerChunk::wait_for_context() const +{ + if (async::future_is_ready(m_pending_context.chunk_context)) + return; + + m_threadpool.work_while_waiting(m_pending_context.context_join_condition, async::DefaultPriorityLevels::MAX); + assert(async::future_is_ready(m_pending_context.chunk_context)); //should be ready at this point +} +//------------------------------------------------------------------------------------------------------------------- +// ASYNC LEDGER CHUNK INTERNAL +//------------------------------------------------------------------------------------------------------------------- +void AsyncLedgerChunk::wait_for_data(const std::size_t pending_data_index) const +{ + if (pending_data_index >= m_pending_data.size()) + return; + if (async::future_is_ready(m_pending_data[pending_data_index].chunk_data)) + return; + + m_threadpool.work_while_waiting(m_pending_data[pending_data_index].data_join_condition, + async::DefaultPriorityLevels::MAX); + assert(async::future_is_ready(m_pending_data[pending_data_index].chunk_data)); //should be ready at this point +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace scanning +} //namespace sp diff --git a/src/seraphis_impl/scan_ledger_chunk_async.h b/src/seraphis_impl/scan_ledger_chunk_async.h new file mode 100644 index 0000000000..b98e4f95d2 --- /dev/null +++ b/src/seraphis_impl/scan_ledger_chunk_async.h @@ -0,0 +1,110 @@ +// Copyright (c) 2022, 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. + +// Async ledger chunk. +// WARNING: It is potentially UB to pass an async ledger chunk to any thread not associated with the referenced +// threadpool. + +#pragma once + +//local headers +#include "async/threadpool.h" +#include "ringct/rctTypes.h" +#include "seraphis_main/scan_core_types.h" +#include "seraphis_main/scan_ledger_chunk.h" + +//third party headers + +//standard headers +#include +#include + +//forward declarations + + +namespace sp +{ +namespace scanning +{ + +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- + +struct PendingChunkContext final +{ + std::promise stop_signal; //for canceling the pending context request + std::shared_future chunk_context; //start index, element ids, prefix id + async::join_condition_t context_join_condition; //for waiting on the chunk context +}; + +struct PendingChunkData final +{ + std::promise stop_signal; //for canceling the pending data request + std::shared_future chunk_data; //basic enote records and contextual key image sets + async::join_condition_t data_join_condition; //for waiting on the chunk data +}; + +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- + +class AsyncLedgerChunk final : public LedgerChunk +{ +public: +//constructors + /// normal constructor + AsyncLedgerChunk(async::Threadpool &threadpool, + PendingChunkContext &&pending_context, + std::vector &&pending_data, + std::vector subconsumer_ids); + +//member functions + /// access the chunk context + const ChunkContext& get_context() const override; + /// access the chunk data for a specified subconsumer + const ChunkData* try_get_data(const rct::key &subconsumer_id) const override; + /// get the cached subconsumer ids associated with this chunk + const std::vector& subconsumer_ids() const override; + +private: + /// wait until the pending context is ready + void wait_for_context() const; + /// wait until the specified pending data is ready + void wait_for_data(const std::size_t pending_data_index) const; + +//member variables + async::Threadpool &m_threadpool; + mutable PendingChunkContext m_pending_context; + mutable std::vector m_pending_data; + const std::vector m_subconsumer_ids; +}; + +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- + +} //namespace scanning +} //namespace sp diff --git a/src/seraphis_impl/scan_ledger_chunk_simple.h b/src/seraphis_impl/scan_ledger_chunk_simple.h new file mode 100644 index 0000000000..472ceae329 --- /dev/null +++ b/src/seraphis_impl/scan_ledger_chunk_simple.h @@ -0,0 +1,109 @@ +// Copyright (c) 2022, 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. + +// Simple ledger chunk types. + +#pragma once + +//local headers +#include "misc_log_ex.h" +#include "seraphis_main/scan_core_types.h" +#include "seraphis_main/scan_ledger_chunk.h" +#include "seraphis_main/scan_misc_utils.h" + +//third party headers + +//standard headers +#include + +//forward declarations + + +namespace sp +{ +namespace scanning +{ + +//// +// LedgerChunkEmpty +// - empty chunks only +/// +class LedgerChunkEmpty final : public LedgerChunk +{ +public: + LedgerChunkEmpty(ChunkContext context) : + m_context{std::move(context)}, + m_data{}, + m_subconsumer_ids{rct::zero()} //we need at least one subconsumer to satisfy ledger chunk semantics checks + { + CHECK_AND_ASSERT_THROW_MES(chunk_context_is_empty(context), "empty ledger chunk: chunk is not empty."); + } + + const ChunkContext& get_context() const override { return m_context; } + const ChunkData* try_get_data(const rct::key&) const override { return &m_data; } + const std::vector& subconsumer_ids() const override { return m_subconsumer_ids; } + +private: + ChunkContext m_context; + ChunkData m_data; + std::vector m_subconsumer_ids; +}; + +//// +// LedgerChunkStandard +// - store data directly +/// +class LedgerChunkStandard final : public LedgerChunk +{ +public: + LedgerChunkStandard(ChunkContext context, std::vector data, std::vector subconsumer_ids) : + m_context{std::move(context)}, + m_data{std::move(data)}, + m_subconsumer_ids{std::move(subconsumer_ids)} + { + CHECK_AND_ASSERT_THROW_MES(m_data.size() == m_subconsumer_ids.size(), + "standard ledger chunk: mismatch between data and subconsumer ids."); + } + + const ChunkContext& get_context() const override { return m_context; } + const ChunkData* try_get_data(const rct::key &subconsumer_id) const override + { + auto id_it = std::find(m_subconsumer_ids.begin(), m_subconsumer_ids.end(), subconsumer_id); + if (id_it == m_subconsumer_ids.end()) return nullptr; + return &(m_data[std::distance(m_subconsumer_ids.begin(), id_it)]); + } + const std::vector& subconsumer_ids() const override { return m_subconsumer_ids; } + +private: + ChunkContext m_context; + std::vector m_data; + std::vector m_subconsumer_ids; +}; + +} //namespace scanning +} //namespace sp diff --git a/src/seraphis_impl/scan_process_basic.cpp b/src/seraphis_impl/scan_process_basic.cpp new file mode 100644 index 0000000000..6e7c37c1dc --- /dev/null +++ b/src/seraphis_impl/scan_process_basic.cpp @@ -0,0 +1,133 @@ +// Copyright (c) 2022, 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 "scan_process_basic.h" + +//local headers +#include "seraphis_main/contextual_enote_record_types.h" +#include "seraphis_main/scan_chunk_consumer.h" +#include "seraphis_main/scan_context.h" +#include "seraphis_main/scan_core_types.h" +#include "seraphis_main/scan_machine.h" +#include "seraphis_main/scan_machine_types.h" +#include "seraphis_main/scan_misc_utils.h" + +//third party headers + +//standard headers + + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis_impl" + +namespace sp +{ +//------------------------------------------------------------------------------------------------------------------- +bool refresh_enote_store_nonledger(const SpEnoteOriginStatus expected_origin_status, + const SpEnoteSpentStatus expected_spent_status, + scanning::ScanContextNonLedger &scan_context_inout, + scanning::ChunkConsumer &chunk_consumer_inout) +{ + try + { + // 1. get the scan chunk + scanning::ChunkData nonledger_chunk; + scan_context_inout.get_nonledger_chunk(nonledger_chunk); + + scanning::check_chunk_data_semantics(nonledger_chunk, expected_origin_status, expected_spent_status, 0, -1); + + // 2. check if the scan context was aborted + // - don't consume chunk if aborted and chunk is empty (it may not represent the real state of the nonledger + // cache) + // - consume chunk if aborted and chunk is non-empty (it's possible for a scan context to be aborted after + // acquiring a chunk) + if (scanning::chunk_data_is_empty(nonledger_chunk) && + scan_context_inout.is_aborted()) + return false; + + // 3. consume the chunk + chunk_consumer_inout.consume_nonledger_chunk(expected_origin_status, nonledger_chunk); + } + catch (...) + { + LOG_ERROR("refresh enote store nonledger failed."); + return false; + } + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool refresh_enote_store_ledger(const scanning::ScanMachineConfig &scan_machine_config, + scanning::ScanContextLedger &ledger_scan_context_inout, + scanning::ChunkConsumer &chunk_consumer_inout) +{ + // 1. prepare metadata + scanning::ScanMachineState state{scanning::initialize_scan_machine_state(scan_machine_config)}; + + // 2. advance the state machine until it terminates or encounters a failure + while (scanning::try_advance_state_machine(ledger_scan_context_inout, chunk_consumer_inout, state) && + !scanning::is_terminal_state(state)) + {} + + // 3. check the result + if (!scanning::is_success_state(state)) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool refresh_enote_store(const scanning::ScanMachineConfig &scan_machine_config, + scanning::ScanContextNonLedger &nonledger_scan_context_inout, + scanning::ScanContextLedger &ledger_scan_context_inout, + scanning::ChunkConsumer &chunk_consumer_inout) +{ + // 1. perform a full scan + if (!refresh_enote_store_ledger(scan_machine_config, ledger_scan_context_inout, chunk_consumer_inout)) + return false; + + // 2. perform an unconfirmed scan + if (!refresh_enote_store_nonledger(SpEnoteOriginStatus::UNCONFIRMED, + SpEnoteSpentStatus::SPENT_UNCONFIRMED, + nonledger_scan_context_inout, + chunk_consumer_inout)) + return false; + + // 3. perform a follow-up full scan + // rationale: + // - blocks may have been added between the initial on-chain pass and the unconfirmed pass, and those blocks may + // contain txs not seen by the unconfirmed pass (i.e. sneaky txs) + // - we want scan results to be chronologically contiguous (it is better for the unconfirmed scan results to be stale + // than the on-chain scan results) + if (!refresh_enote_store_ledger(scan_machine_config, ledger_scan_context_inout, chunk_consumer_inout)) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace sp diff --git a/src/seraphis_impl/scan_process_basic.h b/src/seraphis_impl/scan_process_basic.h new file mode 100644 index 0000000000..f2efd5d97e --- /dev/null +++ b/src/seraphis_impl/scan_process_basic.h @@ -0,0 +1,90 @@ +// Copyright (c) 2022, 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. + +// Basic API for the seraphis balance recovery framework. + +#pragma once + +//local headers +#include "seraphis_main/contextual_enote_record_types.h" + +//third party headers + +//standard headers + +//forward declarations +namespace sp +{ +namespace scanning +{ + struct ScanMachineConfig; + class ScanContextNonLedger; + class ScanContextLedger; + class ChunkConsumer; +} +} + +namespace sp +{ + +/** +* brief: refresh_enote_store_nonledger - perform a non-ledger balance recovery process (e.g. scan the tx pool) +* param: expected_origin_status - +* param: expected_spent_status - +* inoutparam: scan_context_inout - +* inoutparam: chunk_consumer_inout - +* return: false if the refresh was not completely successful +*/ +bool refresh_enote_store_nonledger(const SpEnoteOriginStatus expected_origin_status, + const SpEnoteSpentStatus expected_spent_status, + scanning::ScanContextNonLedger &scan_context_inout, + scanning::ChunkConsumer &chunk_consumer_inout); +/** +* brief: refresh_enote_store_ledger - perform an on-chain balance recovery process (i.e. scan the ledger) +* param: scan_machine_config - +* inoutparam: ledger_scan_context_inout - +* inoutparam: chunk_consumer_inout - +* return: false if the refresh was not completely successful +*/ +bool refresh_enote_store_ledger(const scanning::ScanMachineConfig &scan_machine_config, + scanning::ScanContextLedger &ledger_scan_context_inout, + scanning::ChunkConsumer &chunk_consumer_inout); +/** +* brief: refresh_enote_store - perform a complete on-chain + unconfirmed cache balance recovery process +* param: scan_machine_config - +* inoutparam: nonledger_scan_context_inout - +* inoutparam: ledger_scan_context_inout - +* inoutparam: chunk_consumer_inout - +* return: false if the refresh was not completely successful +*/ +bool refresh_enote_store(const scanning::ScanMachineConfig &scan_machine_config, + scanning::ScanContextNonLedger &nonledger_scan_context_inout, + scanning::ScanContextLedger &ledger_scan_context_inout, + scanning::ChunkConsumer &chunk_consumer_inout); + +} //namespace sp diff --git a/src/seraphis_impl/serialization_demo_types.h b/src/seraphis_impl/serialization_demo_types.h new file mode 100644 index 0000000000..4c9ea7e8e4 --- /dev/null +++ b/src/seraphis_impl/serialization_demo_types.h @@ -0,0 +1,413 @@ +// Copyright (c) 2022, 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. + +// Serializable types for seraphis transaction components and transactions (a demonstration). + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "crypto/x25519.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/binned_reference_set.h" +#include "seraphis_core/discretized_fee.h" +#include "seraphis_core/jamtis_support_types.h" +#include "serialization/containers.h" +#include "serialization/crypto.h" +#include "serialization/serialization.h" +#include "seraphis_main/txtype_coinbase_v1.h" +#include "seraphis_main/txtype_squashed_v1.h" + +//third party headers + +//standard headers +#include + +//forward declarations + + +namespace sp +{ +namespace serialization +{ + +/// serializable jamtis::encrypted_address_tag_t +struct ser_encrypted_address_tag_t final +{ + unsigned char bytes[sizeof(jamtis::encrypted_address_tag_t)]; +}; + +/// serializable jamtis::encoded_amount_t +struct ser_encoded_amount_t final +{ + unsigned char bytes[sizeof(jamtis::encoded_amount_t)]; +}; + +/// serializable SpCoinbaseEnoteCore +struct ser_SpCoinbaseEnoteCore final +{ + /// Ko + rct::key onetime_address; + /// a + rct::xmr_amount amount; + + BEGIN_SERIALIZE() + FIELD(onetime_address) + VARINT_FIELD(amount) + END_SERIALIZE() +}; + +/// serializable SpEnoteCore +struct ser_SpEnoteCore final +{ + /// Ko + rct::key onetime_address; + /// C + rct::key amount_commitment; + + BEGIN_SERIALIZE() + FIELD(onetime_address) + FIELD(amount_commitment) + END_SERIALIZE() +}; + +/// serializable SpEnoteImageCore +struct ser_SpEnoteImageCore final +{ + /// K" + rct::key masked_address; + /// C" + rct::key masked_commitment; + /// KI + crypto::key_image key_image; + + BEGIN_SERIALIZE() + FIELD(masked_address) + FIELD(masked_commitment) + FIELD(key_image) + END_SERIALIZE() +}; + +/// partially serializable BulletproofPlus2 +struct ser_BulletproofPlus2_PARTIAL final +{ + //rct::keyV V; (not serializable here) + rct::key A, A1, B; + rct::key r1, s1, d1; + rct::keyV L, R; + + BEGIN_SERIALIZE() + FIELD(A) + FIELD(A1) + FIELD(B) + FIELD(r1) + FIELD(s1) + FIELD(d1) + FIELD(L) + FIELD(R) + END_SERIALIZE() +}; + +/// partially serializable rct::clsag +struct ser_clsag_PARTIAL final +{ + rct::keyV s; // scalars + rct::key c1; + + //rct::key I; // signing key image (not serializable here) + rct::key D; // commitment key image + + BEGIN_SERIALIZE() + FIELD(s) + FIELD(c1) + FIELD(D) + END_SERIALIZE() +}; + +/// serializable SpCompositionProof +struct ser_SpCompositionProof final +{ + // challenge + rct::key c; + // responses + rct::key r_t1; + rct::key r_t2; + rct::key r_ki; + // intermediate proof key + rct::key K_t1; + + BEGIN_SERIALIZE() + FIELD(c) + FIELD(r_t1) + FIELD(r_t2) + FIELD(r_ki) + FIELD(K_t1) + END_SERIALIZE() +}; + +/// serializable GrootleProof +struct ser_GrootleProof final +{ + rct::key A; + rct::key B; + rct::keyM f; + rct::keyV X; + rct::key zA; + rct::key z; + + BEGIN_SERIALIZE() + FIELD(A) + FIELD(B) + FIELD(f) + FIELD(X) + FIELD(zA) + FIELD(z) + END_SERIALIZE() +}; + +/// partially serializable SpBinnedReferenceSetV1 +struct ser_SpBinnedReferenceSetV1_PARTIAL final +{ + /// bin configuration details (shared by all bins) + //SpBinnedReferenceSetConfigV1 bin_config; (not serializable here) + /// bin generator seed (shared by all bins) + //rct::key bin_generator_seed; (not serializable here) + /// rotation factor (shared by all bins) + std::uint16_t bin_rotation_factor; + /// bin loci (serializable as index offsets) + std::vector bin_loci_COMPACT; + + BEGIN_SERIALIZE() + VARINT_FIELD(bin_rotation_factor) + static_assert(sizeof(bin_rotation_factor) == sizeof(ref_set_bin_dimension_v1_t), ""); + FIELD(bin_loci_COMPACT) + END_SERIALIZE() +}; + +/// serializable LegacyEnoteImageV2 +struct ser_LegacyEnoteImageV2 final +{ + /// masked commitment (aka 'pseudo-output commitment') + rct::key masked_commitment; + /// legacy key image + crypto::key_image key_image; + + BEGIN_SERIALIZE() + FIELD(masked_commitment) + FIELD(key_image) + END_SERIALIZE() +}; + +/// serializable SpEnoteImageV1 +struct ser_SpEnoteImageV1 final +{ + /// enote image core + ser_SpEnoteImageCore core; + + BEGIN_SERIALIZE() + FIELD(core) + END_SERIALIZE() +}; + +/// serializable SpCoinbaseEnoteV1 +struct ser_SpCoinbaseEnoteV1 final +{ + /// enote core (one-time address, amount commitment) + ser_SpCoinbaseEnoteCore core; + + /// addr_tag_enc + ser_encrypted_address_tag_t addr_tag_enc; + /// view_tag + unsigned char view_tag; + + BEGIN_SERIALIZE() + FIELD(core) + FIELD(addr_tag_enc) static_assert(sizeof(addr_tag_enc) == sizeof(jamtis::encrypted_address_tag_t), ""); + VARINT_FIELD(view_tag) static_assert(sizeof(view_tag) == sizeof(jamtis::view_tag_t), ""); + END_SERIALIZE() +}; + +/// serializable SpEnoteV1 +struct ser_SpEnoteV1 final +{ + /// enote core (one-time address, amount commitment) + ser_SpEnoteCore core; + + /// enc(a) + ser_encoded_amount_t encoded_amount; + /// addr_tag_enc + ser_encrypted_address_tag_t addr_tag_enc; + /// view_tag + unsigned char view_tag; + + BEGIN_SERIALIZE() + FIELD(core) + FIELD(encoded_amount) static_assert(sizeof(encoded_amount) == sizeof(jamtis::encoded_amount_t), ""); + FIELD(addr_tag_enc) static_assert(sizeof(addr_tag_enc) == sizeof(jamtis::encrypted_address_tag_t), ""); + VARINT_FIELD(view_tag) static_assert(sizeof(view_tag) == sizeof(jamtis::view_tag_t), ""); + END_SERIALIZE() +}; + +/// partially serializable SpBalanceProofV1 +struct ser_SpBalanceProofV1_PARTIAL final +{ + /// an aggregate set of BP+ proofs (partial serialization) + ser_BulletproofPlus2_PARTIAL bpp2_proof_PARTIAL; + /// the remainder blinding factor + rct::key remainder_blinding_factor; + + BEGIN_SERIALIZE() + FIELD(bpp2_proof_PARTIAL) + FIELD(remainder_blinding_factor) + END_SERIALIZE() +}; + +/// partially serializable LegacyRingSignatureV4 +struct ser_LegacyRingSignatureV4_PARTIAL final +{ + /// a clsag proof + ser_clsag_PARTIAL clsag_proof_PARTIAL; + /// on-chain indices of the proof's ring members (serializable as index offsets) + std::vector reference_set_COMPACT; + + BEGIN_SERIALIZE() + FIELD(clsag_proof_PARTIAL) + FIELD(reference_set_COMPACT) + END_SERIALIZE() +}; + +/// serializable SpImageProofV1 +struct ser_SpImageProofV1 final +{ + /// a seraphis composition proof + ser_SpCompositionProof composition_proof; + + BEGIN_SERIALIZE() + FIELD(composition_proof) + END_SERIALIZE() +}; + +/// partially serializable SpMembershipProofV1 (does not include config info) +struct ser_SpMembershipProofV1_PARTIAL final +{ + /// a grootle proof + ser_GrootleProof grootle_proof; + /// binned representation of ledger indices of enotes referenced by the proof + ser_SpBinnedReferenceSetV1_PARTIAL binned_reference_set_PARTIAL; + /// ref set size = n^m + //std::size_t ref_set_decomp_n; (not serializable here) + //std::size_t ref_set_decomp_m; (not serializable here) + + BEGIN_SERIALIZE() + FIELD(grootle_proof) + FIELD(binned_reference_set_PARTIAL) + END_SERIALIZE() +}; + +/// serializable SpTxSupplementV1 +struct ser_SpTxSupplementV1 final +{ + /// xKe: enote ephemeral pubkeys for outputs + std::vector output_enote_ephemeral_pubkeys; + /// tx memo + std::vector tx_extra; + + BEGIN_SERIALIZE() + FIELD(output_enote_ephemeral_pubkeys) + FIELD(tx_extra) + END_SERIALIZE() +}; + +/// serializable SpTxCoinbaseV1 +struct ser_SpTxCoinbaseV1 final +{ + /// semantic rules version + SpTxCoinbaseV1::SemanticRulesVersion tx_semantic_rules_version; + + /// height of the block whose block reward this coinbase tx disperses + std::uint64_t block_height; + /// block reward dispersed by this coinbase tx + rct::xmr_amount block_reward; + /// tx outputs (new enotes) + std::vector outputs; + /// supplemental data for tx + ser_SpTxSupplementV1 tx_supplement; + + BEGIN_SERIALIZE() + VARINT_FIELD(tx_semantic_rules_version) + VARINT_FIELD(block_height) + VARINT_FIELD(block_reward) + FIELD(outputs) + FIELD(tx_supplement) + END_SERIALIZE() +}; + +/// serializable SpTxSquashedV1 +struct ser_SpTxSquashedV1 final +{ + /// semantic rules version + SpTxSquashedV1::SemanticRulesVersion tx_semantic_rules_version; + + /// legacy tx input images (spent legacy enotes) + std::vector legacy_input_images; + /// seraphis tx input images (spent seraphis enotes) + std::vector sp_input_images; + /// tx outputs (new enotes) + std::vector outputs; + /// balance proof (balance proof and range proofs) + ser_SpBalanceProofV1_PARTIAL balance_proof; + /// ring signature proofs: membership and ownership/key-image-legitimacy for each legacy input + std::vector legacy_ring_signatures; + /// composition proofs: ownership/key-image-legitimacy for each seraphis input + std::vector sp_image_proofs; + /// Grootle proofs on squashed enotes: membership for each seraphis input + std::vector sp_membership_proofs; + /// supplemental data for tx + ser_SpTxSupplementV1 tx_supplement; + /// the transaction fee (discretized representation) + unsigned char tx_fee; + + BEGIN_SERIALIZE() + VARINT_FIELD(tx_semantic_rules_version) + FIELD(legacy_input_images) + FIELD(sp_input_images) + FIELD(outputs) + FIELD(balance_proof) + FIELD(legacy_ring_signatures) + FIELD(sp_image_proofs) + FIELD(sp_membership_proofs) + FIELD(tx_supplement) + VARINT_FIELD(tx_fee) static_assert(sizeof(tx_fee) == sizeof(DiscretizedFee), ""); + END_SERIALIZE() +}; + +} //namespace serialization +} //namespace sp + +BLOB_SERIALIZER(sp::serialization::ser_encrypted_address_tag_t); +BLOB_SERIALIZER(sp::serialization::ser_encoded_amount_t); diff --git a/src/seraphis_impl/serialization_demo_utils.cpp b/src/seraphis_impl/serialization_demo_utils.cpp new file mode 100644 index 0000000000..48dda846a4 --- /dev/null +++ b/src/seraphis_impl/serialization_demo_utils.cpp @@ -0,0 +1,684 @@ +// Copyright (c) 2022, 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 "serialization_demo_utils.h" + +//local headers +#include "common/container_helpers.h" +#include "crypto/crypto.h" +#include "ringct/rctOps.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/binned_reference_set.h" +#include "seraphis_core/discretized_fee.h" +#include "seraphis_crypto/bulletproofs_plus2.h" +#include "seraphis_crypto/grootle.h" +#include "seraphis_crypto/sp_composition_proof.h" +#include "seraphis_impl/serialization_demo_types.h" +#include "seraphis_main/tx_builders_inputs.h" +#include "seraphis_main/tx_component_types.h" +#include "seraphis_main/tx_component_types_legacy.h" +#include "seraphis_main/txtype_coinbase_v1.h" +#include "seraphis_main/txtype_squashed_v1.h" + +//third party headers + +//standard headers +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis_impl" + +namespace sp +{ +namespace serialization +{ +//------------------------------------------------------------------------------------------------------------------- +// array2 copies array1 by invoking copy_func() on each element +//------------------------------------------------------------------------------------------------------------------- +template +static void copy_array(const CopyFuncT ©_func, const std::vector &array1, std::vector &array2_out) +{ + array2_out.clear(); + array2_out.reserve(array1.size()); + for (const Type1 &obj : array1) + copy_func(obj, tools::add_element(array2_out)); +} +//------------------------------------------------------------------------------------------------------------------- +// array2 consumes array1 by invoking relay_func() on each element +//------------------------------------------------------------------------------------------------------------------- +template +static void relay_array(const RelayFuncT &relay_func, std::vector &array1_in, std::vector &array2_out) +{ + array2_out.clear(); + array2_out.reserve(array1_in.size()); + for (Type1 &obj_in : array1_in) + relay_func(obj_in, tools::add_element(array2_out)); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void collect_sp_balance_proof_commitments_v1(const std::vector &seraphis_input_images, + const std::vector &output_enotes, + std::vector &commitments_out) +{ + commitments_out.clear(); + commitments_out.reserve(seraphis_input_images.size() + output_enotes.size()); + + for (const SpEnoteImageV1 &input_image : seraphis_input_images) + commitments_out.emplace_back(rct::scalarmultKey(masked_commitment_ref(input_image), rct::INV_EIGHT)); + + for (const SpEnoteV1 &output_enote : output_enotes) + commitments_out.emplace_back(rct::scalarmultKey(output_enote.core.amount_commitment, rct::INV_EIGHT)); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void indices_to_offsets(std::vector &indices_inout) +{ + if (indices_inout.size() == 0) + return; + + for (std::size_t i{indices_inout.size() - 1}; i != 0; --i) + indices_inout[i] -= indices_inout[i - 1]; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void indices_from_offsets(std::vector &indices_inout) +{ + for (std::size_t i{1}; i < indices_inout.size(); ++i) + indices_inout[i] += indices_inout[i - 1]; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void recover_legacy_ring_signatures_v4( + std::vector &serializable_legacy_ring_signatures_in, + const std::vector &legacy_enote_images, + std::vector &legacy_ring_signatures_out) +{ + CHECK_AND_ASSERT_THROW_MES(legacy_enote_images.size() == serializable_legacy_ring_signatures_in.size(), + "recovering legacy ring signature v4s: legacy input images don't line up with legacy ring signatures."); + + legacy_ring_signatures_out.clear(); + legacy_ring_signatures_out.reserve(serializable_legacy_ring_signatures_in.size()); + + for (std::size_t legacy_input_index{0}; legacy_input_index < legacy_enote_images.size(); ++legacy_input_index) + { + recover_legacy_ring_signature_v4(serializable_legacy_ring_signatures_in[legacy_input_index], + legacy_enote_images[legacy_input_index].key_image, + tools::add_element(legacy_ring_signatures_out)); + } +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void recover_sp_membership_proofs_v1( + std::vector &serializable_membership_proofs_in, + const std::vector &enote_images, + const SpBinnedReferenceSetConfigV1 &sp_refset_bin_config, + const std::size_t sp_ref_set_decomp_n, + const std::size_t sp_ref_set_decomp_m, + std::vector &membership_proofs_out) +{ + CHECK_AND_ASSERT_THROW_MES(enote_images.size() == serializable_membership_proofs_in.size(), + "recovering seraphis membership proof v1s: seraphis input images don't line up with seraphis membership proofs."); + + membership_proofs_out.clear(); + membership_proofs_out.reserve(serializable_membership_proofs_in.size()); + rct::key generator_seed_temp; + + for (std::size_t sp_input_index{0}; sp_input_index < enote_images.size(); ++sp_input_index) + { + make_binned_ref_set_generator_seed_v1(masked_address_ref(enote_images[sp_input_index]), + masked_commitment_ref(enote_images[sp_input_index]), + generator_seed_temp); + + recover_sp_membership_proof_v1(serializable_membership_proofs_in[sp_input_index], + sp_refset_bin_config, + generator_seed_temp, + sp_ref_set_decomp_n, + sp_ref_set_decomp_m, + tools::add_element(membership_proofs_out)); + } +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void make_serializable_legacy_ring_signatures_v4(const std::vector &legacy_ring_signatures, + std::vector &serializable_legacy_ring_signatures_out) +{ + serializable_legacy_ring_signatures_out.clear(); + serializable_legacy_ring_signatures_out.reserve(legacy_ring_signatures.size()); + + for (const LegacyRingSignatureV4 &legacy_ring_signature : legacy_ring_signatures) + { + make_serializable_legacy_ring_signature_v4(legacy_ring_signature, + tools::add_element(serializable_legacy_ring_signatures_out)); + } +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void make_serializable_sp_membership_proofs_v1(const std::vector &membership_proofs, + std::vector &serializable_membership_proofs_out) +{ + serializable_membership_proofs_out.clear(); + serializable_membership_proofs_out.reserve(membership_proofs.size()); + + for (const SpMembershipProofV1 &membership_proof : membership_proofs) + { + make_serializable_sp_membership_proof_v1(membership_proof, + tools::add_element(serializable_membership_proofs_out)); + } +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +void make_serializable_bpp2(const BulletproofPlus2 &bpp2, ser_BulletproofPlus2_PARTIAL &serializable_bpp2_out) +{ + serializable_bpp2_out.A = bpp2.A; + serializable_bpp2_out.A1 = bpp2.A1; + serializable_bpp2_out.B = bpp2.B; + serializable_bpp2_out.r1 = bpp2.r1; + serializable_bpp2_out.s1 = bpp2.s1; + serializable_bpp2_out.d1 = bpp2.d1; + serializable_bpp2_out.L = bpp2.L; + serializable_bpp2_out.R = bpp2.R; +} +//------------------------------------------------------------------------------------------------------------------- +void make_serializable_clsag(const rct::clsag &clsag, ser_clsag_PARTIAL &serializable_clsag_out) +{ + serializable_clsag_out.s = clsag.s; + serializable_clsag_out.c1 = clsag.c1; + serializable_clsag_out.D = clsag.D; +} +//------------------------------------------------------------------------------------------------------------------- +void make_serializable_grootle_proof(const GrootleProof &grootle, ser_GrootleProof &serializable_grootle_out) +{ + serializable_grootle_out.A = grootle.A; + serializable_grootle_out.B = grootle.B; + serializable_grootle_out.f = grootle.f; + serializable_grootle_out.X = grootle.X; + serializable_grootle_out.zA = grootle.zA; + serializable_grootle_out.z = grootle.z; +} +//------------------------------------------------------------------------------------------------------------------- +void make_serializable_sp_composition_proof(const SpCompositionProof &proof, + ser_SpCompositionProof &serializable_proof_out) +{ + serializable_proof_out.c = proof.c; + serializable_proof_out.r_t1 = proof.r_t1; + serializable_proof_out.r_t2 = proof.r_t2; + serializable_proof_out.r_ki = proof.r_ki; + serializable_proof_out.K_t1 = proof.K_t1; +} +//------------------------------------------------------------------------------------------------------------------- +void make_serializable_sp_coinbase_enote_core(const SpCoinbaseEnoteCore &enote, + ser_SpCoinbaseEnoteCore &serializable_enote_out) +{ + serializable_enote_out.onetime_address = enote.onetime_address; + serializable_enote_out.amount = enote.amount; +} +//------------------------------------------------------------------------------------------------------------------- +void make_serializable_sp_enote_core(const SpEnoteCore &enote, ser_SpEnoteCore &serializable_enote_out) +{ + serializable_enote_out.onetime_address = enote.onetime_address; + serializable_enote_out.amount_commitment = enote.amount_commitment; +} +//------------------------------------------------------------------------------------------------------------------- +void make_serializable_sp_enote_image_core(const SpEnoteImageCore &image, ser_SpEnoteImageCore &serializable_image_out) +{ + serializable_image_out.masked_address = image.masked_address; + serializable_image_out.masked_commitment = image.masked_commitment; + serializable_image_out.key_image = image.key_image; +} +//------------------------------------------------------------------------------------------------------------------- +void make_serializable_sp_binned_reference_set_v1(const SpBinnedReferenceSetV1 &refset, + ser_SpBinnedReferenceSetV1_PARTIAL &serializable_refset_out) +{ + serializable_refset_out.bin_rotation_factor = refset.bin_rotation_factor; + serializable_refset_out.bin_loci_COMPACT = refset.bin_loci; + indices_to_offsets(serializable_refset_out.bin_loci_COMPACT); +} +//------------------------------------------------------------------------------------------------------------------- +void make_serializable_legacy_enote_image_v2(const LegacyEnoteImageV2 &image, + ser_LegacyEnoteImageV2 &serializable_image_out) +{ + serializable_image_out.masked_commitment = image.masked_commitment; + serializable_image_out.key_image = image.key_image; +} +//------------------------------------------------------------------------------------------------------------------- +void make_serializable_sp_coinbase_enote_v1(const SpCoinbaseEnoteV1 &enote, ser_SpCoinbaseEnoteV1 &serializable_enote_out) +{ + make_serializable_sp_coinbase_enote_core(enote.core, serializable_enote_out.core); + memcpy(serializable_enote_out.addr_tag_enc.bytes, + enote.addr_tag_enc.bytes, + sizeof(enote.addr_tag_enc)); + serializable_enote_out.view_tag = enote.view_tag; +} +//------------------------------------------------------------------------------------------------------------------- +void make_serializable_sp_enote_v1(const SpEnoteV1 &enote, ser_SpEnoteV1 &serializable_enote_out) +{ + make_serializable_sp_enote_core(enote.core, serializable_enote_out.core); + memcpy(serializable_enote_out.encoded_amount.bytes, + enote.encoded_amount.bytes, + sizeof(enote.encoded_amount)); + memcpy(serializable_enote_out.addr_tag_enc.bytes, + enote.addr_tag_enc.bytes, + sizeof(enote.addr_tag_enc)); + serializable_enote_out.view_tag = enote.view_tag; +} +//------------------------------------------------------------------------------------------------------------------- +void make_serializable_sp_enote_image_v1(const SpEnoteImageV1 &image, ser_SpEnoteImageV1 &serializable_image_out) +{ + make_serializable_sp_enote_image_core(image.core, serializable_image_out.core); +} +//------------------------------------------------------------------------------------------------------------------- +void make_serializable_sp_balance_proof_v1(const SpBalanceProofV1 &proof, + ser_SpBalanceProofV1_PARTIAL &serializable_proof_out) +{ + make_serializable_bpp2(proof.bpp2_proof, serializable_proof_out.bpp2_proof_PARTIAL); + serializable_proof_out.remainder_blinding_factor = proof.remainder_blinding_factor; +} +//------------------------------------------------------------------------------------------------------------------- +void make_serializable_legacy_ring_signature_v4(const LegacyRingSignatureV4 &signature, + ser_LegacyRingSignatureV4_PARTIAL &serializable_signature_out) +{ + make_serializable_clsag(signature.clsag_proof, serializable_signature_out.clsag_proof_PARTIAL); + serializable_signature_out.reference_set_COMPACT = signature.reference_set; + indices_to_offsets(serializable_signature_out.reference_set_COMPACT); +} +//------------------------------------------------------------------------------------------------------------------- +void make_serializable_sp_membership_proof_v1(const SpMembershipProofV1 &proof, + ser_SpMembershipProofV1_PARTIAL &serializable_proof_out) +{ + make_serializable_grootle_proof(proof.grootle_proof, serializable_proof_out.grootle_proof); + make_serializable_sp_binned_reference_set_v1(proof.binned_reference_set, + serializable_proof_out.binned_reference_set_PARTIAL); +} +//------------------------------------------------------------------------------------------------------------------- +void make_serializable_sp_image_proof_v1(const SpImageProofV1 &image_proof, + ser_SpImageProofV1 &serializable_image_proof_out) +{ + make_serializable_sp_composition_proof(image_proof.composition_proof, + serializable_image_proof_out.composition_proof); +} +//------------------------------------------------------------------------------------------------------------------- +void make_serializable_sp_tx_supplement_v1(const SpTxSupplementV1 &supplement, + ser_SpTxSupplementV1 &serializable_supplement_out) +{ + serializable_supplement_out.output_enote_ephemeral_pubkeys = supplement.output_enote_ephemeral_pubkeys; + serializable_supplement_out.tx_extra = supplement.tx_extra; +} +//------------------------------------------------------------------------------------------------------------------- +void make_serializable_discretized_fee(const DiscretizedFee discretized_fee, + unsigned char &serializable_discretized_fee_out) +{ + serializable_discretized_fee_out = discretized_fee.fee_encoding; +} +//------------------------------------------------------------------------------------------------------------------- +void make_serializable_sp_tx_coinbase_v1(const SpTxCoinbaseV1 &tx, ser_SpTxCoinbaseV1 &serializable_tx_out) +{ + // semantic rules version + serializable_tx_out.tx_semantic_rules_version = tx.tx_semantic_rules_version; + + // block height + serializable_tx_out.block_height = tx.block_height; + + // block reward + serializable_tx_out.block_reward = tx.block_reward; + + // tx outputs (new enotes) + copy_array(&make_serializable_sp_coinbase_enote_v1, tx.outputs, serializable_tx_out.outputs); + + // supplemental data for tx + make_serializable_sp_tx_supplement_v1(tx.tx_supplement, serializable_tx_out.tx_supplement); +} +//------------------------------------------------------------------------------------------------------------------- +void make_serializable_sp_tx_squashed_v1(const SpTxSquashedV1 &tx, ser_SpTxSquashedV1 &serializable_tx_out) +{ + // semantic rules version + serializable_tx_out.tx_semantic_rules_version = tx.tx_semantic_rules_version; + + // legacy tx input images (spent legacy enotes) + copy_array(&make_serializable_legacy_enote_image_v2, tx.legacy_input_images, + serializable_tx_out.legacy_input_images); + + // seraphis tx input images (spent seraphis enotes) + copy_array(&make_serializable_sp_enote_image_v1, tx.sp_input_images, serializable_tx_out.sp_input_images); + + // tx outputs (new enotes) + copy_array(&make_serializable_sp_enote_v1, tx.outputs, serializable_tx_out.outputs); + + // balance proof (balance proof and range proofs) + make_serializable_sp_balance_proof_v1(tx.balance_proof, serializable_tx_out.balance_proof); + + // ring signature proofs: membership and ownership/key-image-legitimacy for each legacy input + make_serializable_legacy_ring_signatures_v4(tx.legacy_ring_signatures, + serializable_tx_out.legacy_ring_signatures); + + // composition proofs: ownership/key-image-legitimacy for each seraphis input + copy_array(&make_serializable_sp_image_proof_v1, tx.sp_image_proofs, serializable_tx_out.sp_image_proofs); + + // Grootle proofs on squashed enotes: membership for each seraphis input + make_serializable_sp_membership_proofs_v1(tx.sp_membership_proofs, serializable_tx_out.sp_membership_proofs); + + // supplemental data for tx + make_serializable_sp_tx_supplement_v1(tx.tx_supplement, serializable_tx_out.tx_supplement); + + // the transaction fee (discretized representation) + make_serializable_discretized_fee(tx.tx_fee, serializable_tx_out.tx_fee); +} +//------------------------------------------------------------------------------------------------------------------- + +//------------------------------------------------------------------------------------------------------------------- +void recover_bpp2(ser_BulletproofPlus2_PARTIAL &serializable_bpp2_in, + std::vector balance_proof_commitments_mulinv8, + BulletproofPlus2 &bpp2_out) +{ + bpp2_out.V = std::move(balance_proof_commitments_mulinv8); + bpp2_out.A = serializable_bpp2_in.A; + bpp2_out.A1 = serializable_bpp2_in.A1; + bpp2_out.B = serializable_bpp2_in.B; + bpp2_out.r1 = serializable_bpp2_in.r1; + bpp2_out.s1 = serializable_bpp2_in.s1; + bpp2_out.d1 = serializable_bpp2_in.d1; + bpp2_out.L = std::move(serializable_bpp2_in.L); + bpp2_out.R = std::move(serializable_bpp2_in.R); +} +//------------------------------------------------------------------------------------------------------------------- +void recover_clsag(ser_clsag_PARTIAL &serializable_clsag_in, const crypto::key_image &key_image, rct::clsag &clsag_out) +{ + clsag_out.s = std::move(serializable_clsag_in.s); + clsag_out.c1 = serializable_clsag_in.c1; + clsag_out.I = rct::ki2rct(key_image); + clsag_out.D = serializable_clsag_in.D; +} +//------------------------------------------------------------------------------------------------------------------- +void recover_grootle_proof(ser_GrootleProof &serializable_grootle_in, GrootleProof &grootle_out) +{ + grootle_out.A = serializable_grootle_in.A; + grootle_out.B = serializable_grootle_in.B; + grootle_out.f = std::move(serializable_grootle_in.f); + grootle_out.X = std::move(serializable_grootle_in.X); + grootle_out.zA = serializable_grootle_in.zA; + grootle_out.z = serializable_grootle_in.z; +} +//------------------------------------------------------------------------------------------------------------------- +void recover_sp_composition_proof(const ser_SpCompositionProof &serializable_proof, SpCompositionProof &proof_out) +{ + proof_out.c = serializable_proof.c; + proof_out.r_t1 = serializable_proof.r_t1; + proof_out.r_t2 = serializable_proof.r_t2; + proof_out.r_ki = serializable_proof.r_ki; + proof_out.K_t1 = serializable_proof.K_t1; +} +//------------------------------------------------------------------------------------------------------------------- +void recover_sp_coinbase_enote_core(const ser_SpCoinbaseEnoteCore &serializable_enote, SpCoinbaseEnoteCore &enote_out) +{ + enote_out.onetime_address = serializable_enote.onetime_address; + enote_out.amount = serializable_enote.amount; +} +//------------------------------------------------------------------------------------------------------------------- +void recover_sp_enote_core(const ser_SpEnoteCore &serializable_enote, SpEnoteCore &enote_out) +{ + enote_out.onetime_address = serializable_enote.onetime_address; + enote_out.amount_commitment = serializable_enote.amount_commitment; +} +//------------------------------------------------------------------------------------------------------------------- +void recover_sp_enote_image_core(const ser_SpEnoteImageCore &serializable_image, SpEnoteImageCore &image_out) +{ + image_out.masked_address = serializable_image.masked_address; + image_out.masked_commitment = serializable_image.masked_commitment; + image_out.key_image = serializable_image.key_image; +} +//------------------------------------------------------------------------------------------------------------------- +void recover_sp_binned_reference_set_v1(ser_SpBinnedReferenceSetV1_PARTIAL &serializable_refset_in, + const SpBinnedReferenceSetConfigV1 &bin_config, + const rct::key &generator_seed, + SpBinnedReferenceSetV1 &refset_out) +{ + // bin configuration details + refset_out.bin_config = bin_config; + + // bin generator seed + refset_out.bin_generator_seed = generator_seed; + + // rotation factor + refset_out.bin_rotation_factor = serializable_refset_in.bin_rotation_factor; + + // bin loci + refset_out.bin_loci = std::move(serializable_refset_in.bin_loci_COMPACT); + indices_from_offsets(refset_out.bin_loci); +} +//------------------------------------------------------------------------------------------------------------------- +void recover_legacy_enote_image_v2(const ser_LegacyEnoteImageV2 &serializable_image, LegacyEnoteImageV2 &image_out) +{ + image_out.masked_commitment = serializable_image.masked_commitment; + image_out.key_image = serializable_image.key_image; +} +//------------------------------------------------------------------------------------------------------------------- +void recover_sp_coinbase_enote_v1(const ser_SpCoinbaseEnoteV1 &serializable_enote, SpCoinbaseEnoteV1 &enote_out) +{ + recover_sp_coinbase_enote_core(serializable_enote.core, enote_out.core); + memcpy(enote_out.addr_tag_enc.bytes, + serializable_enote.addr_tag_enc.bytes, + sizeof(serializable_enote.addr_tag_enc)); + enote_out.view_tag = serializable_enote.view_tag; +} +//------------------------------------------------------------------------------------------------------------------- +void recover_sp_enote_v1(const ser_SpEnoteV1 &serializable_enote, SpEnoteV1 &enote_out) +{ + recover_sp_enote_core(serializable_enote.core, enote_out.core); + memcpy(enote_out.encoded_amount.bytes, + serializable_enote.encoded_amount.bytes, + sizeof(serializable_enote.encoded_amount)); + memcpy(enote_out.addr_tag_enc.bytes, + serializable_enote.addr_tag_enc.bytes, + sizeof(serializable_enote.addr_tag_enc)); + enote_out.view_tag = serializable_enote.view_tag; +} +//------------------------------------------------------------------------------------------------------------------- +void recover_sp_enote_image_v1(const ser_SpEnoteImageV1 &serializable_image, SpEnoteImageV1 &image_out) +{ + recover_sp_enote_image_core(serializable_image.core, image_out.core); +} +//------------------------------------------------------------------------------------------------------------------- +void recover_sp_balance_proof_v1(ser_SpBalanceProofV1_PARTIAL &serializable_proof_in, + std::vector commitments_inv8, + SpBalanceProofV1 &proof_out) +{ + // bpp2 + recover_bpp2(serializable_proof_in.bpp2_proof_PARTIAL, std::move(commitments_inv8), proof_out.bpp2_proof); + + // remainder blinding factor + proof_out.remainder_blinding_factor = serializable_proof_in.remainder_blinding_factor; +} +//------------------------------------------------------------------------------------------------------------------- +void recover_legacy_ring_signature_v4(ser_LegacyRingSignatureV4_PARTIAL &serializable_signature_in, + const crypto::key_image &key_image, + LegacyRingSignatureV4 &signature_out) +{ + // clsag + recover_clsag(serializable_signature_in.clsag_proof_PARTIAL, key_image, signature_out.clsag_proof); + + // reference set + signature_out.reference_set = std::move(serializable_signature_in.reference_set_COMPACT); + indices_from_offsets(signature_out.reference_set); +} +//------------------------------------------------------------------------------------------------------------------- +void recover_sp_membership_proof_v1(ser_SpMembershipProofV1_PARTIAL &serializable_proof_in, + const SpBinnedReferenceSetConfigV1 &bin_config, + const rct::key &generator_seed, + const std::size_t ref_set_decomp_n, + const std::size_t ref_set_decomp_m, + SpMembershipProofV1 &proof_out) +{ + // grootle proof + recover_grootle_proof(serializable_proof_in.grootle_proof, proof_out.grootle_proof); + + // binned reference set + recover_sp_binned_reference_set_v1(serializable_proof_in.binned_reference_set_PARTIAL, + bin_config, + generator_seed, + proof_out.binned_reference_set); + + // ref set size decomposition + proof_out.ref_set_decomp_n = ref_set_decomp_n; + proof_out.ref_set_decomp_m = ref_set_decomp_m; +} +//------------------------------------------------------------------------------------------------------------------- +void recover_sp_image_proof_v1(const ser_SpImageProofV1 &serializable_image_proof, SpImageProofV1 &image_proof_out) +{ + recover_sp_composition_proof(serializable_image_proof.composition_proof, image_proof_out.composition_proof); +} +//------------------------------------------------------------------------------------------------------------------- +void recover_sp_tx_supplement_v1(ser_SpTxSupplementV1 &serializable_supplement_in, SpTxSupplementV1 &supplement_out) +{ + supplement_out.output_enote_ephemeral_pubkeys = + std::move(serializable_supplement_in.output_enote_ephemeral_pubkeys); + supplement_out.tx_extra = std::move(serializable_supplement_in.tx_extra); +} +//------------------------------------------------------------------------------------------------------------------- +void recover_discretized_fee(const unsigned char serializable_discretized_fee, DiscretizedFee &discretized_fee_out) +{ + discretized_fee_out.fee_encoding = serializable_discretized_fee; +} +//------------------------------------------------------------------------------------------------------------------- +void recover_sp_tx_coinbase_v1(ser_SpTxCoinbaseV1 &serializable_tx_in, SpTxCoinbaseV1 &tx_out) +{ + // semantic rules version + tx_out.tx_semantic_rules_version = serializable_tx_in.tx_semantic_rules_version; + + // block height + tx_out.block_height = serializable_tx_in.block_height; + + // block reward + tx_out.block_reward = serializable_tx_in.block_reward; + + // tx outputs (new enotes) + relay_array(&recover_sp_coinbase_enote_v1, serializable_tx_in.outputs, tx_out.outputs); + + // supplemental data for tx + recover_sp_tx_supplement_v1(serializable_tx_in.tx_supplement, tx_out.tx_supplement); +} +//------------------------------------------------------------------------------------------------------------------- +void recover_sp_tx_squashed_v1(ser_SpTxSquashedV1 &serializable_tx_in, + const SpBinnedReferenceSetConfigV1 &sp_refset_bin_config, + const std::size_t sp_ref_set_decomp_n, + const std::size_t sp_ref_set_decomp_m, + SpTxSquashedV1 &tx_out) +{ + // semantic rules version + tx_out.tx_semantic_rules_version = serializable_tx_in.tx_semantic_rules_version; + + // legacy tx input images (spent legacy enotes) + relay_array(&recover_legacy_enote_image_v2, serializable_tx_in.legacy_input_images, tx_out.legacy_input_images); + + // seraphis tx input images (spent seraphis enotes) + relay_array(&recover_sp_enote_image_v1, serializable_tx_in.sp_input_images, tx_out.sp_input_images); + + // tx outputs (new enotes) + relay_array(&recover_sp_enote_v1, serializable_tx_in.outputs, tx_out.outputs); + + // balance proof (balance proof and range proofs) + std::vector balance_proof_commitments_mulinv8; + collect_sp_balance_proof_commitments_v1(tx_out.sp_input_images, + tx_out.outputs, + balance_proof_commitments_mulinv8); + recover_sp_balance_proof_v1(serializable_tx_in.balance_proof, + std::move(balance_proof_commitments_mulinv8), + tx_out.balance_proof); + + // ring signature proofs: membership and ownership/key-image-legitimacy for each legacy input + recover_legacy_ring_signatures_v4(serializable_tx_in.legacy_ring_signatures, + tx_out.legacy_input_images, + tx_out.legacy_ring_signatures); + + // composition proofs: ownership/key-image-legitimacy for each seraphis input + relay_array(&recover_sp_image_proof_v1, serializable_tx_in.sp_image_proofs, tx_out.sp_image_proofs); + + // Grootle proofs on squashed enotes: membership for each seraphis input + recover_sp_membership_proofs_v1(serializable_tx_in.sp_membership_proofs, + tx_out.sp_input_images, + sp_refset_bin_config, + sp_ref_set_decomp_n, + sp_ref_set_decomp_m, + tx_out.sp_membership_proofs); + + // supplemental data for tx + recover_sp_tx_supplement_v1(serializable_tx_in.tx_supplement, tx_out.tx_supplement); + + // the transaction fee (discretized representation) + recover_discretized_fee(serializable_tx_in.tx_fee, tx_out.tx_fee); +} +//------------------------------------------------------------------------------------------------------------------- +void recover_sp_tx_squashed_v1(ser_SpTxSquashedV1 &serializable_tx_in, SpTxSquashedV1 &tx_out) +{ + // get config for seraphis reference sets (assume the minimum values are needed; use raw API for other variations) + const SemanticConfigSpRefSetV1 seraphis_ref_set_config{ + semantic_config_sp_ref_sets_v1(serializable_tx_in.tx_semantic_rules_version) + }; + + // finish recovering + recover_sp_tx_squashed_v1(serializable_tx_in, + SpBinnedReferenceSetConfigV1{ + .bin_radius = static_cast(seraphis_ref_set_config.bin_radius_min), + .num_bin_members = static_cast(seraphis_ref_set_config.num_bin_members_min) + }, + seraphis_ref_set_config.decomp_n_min, + seraphis_ref_set_config.decomp_m_min, + tx_out); +} +//------------------------------------------------------------------------------------------------------------------- +bool try_recover_sp_tx_squashed_v1(ser_SpTxSquashedV1 &serializable_tx_in, + const SpBinnedReferenceSetConfigV1 &sp_refset_bin_config, + const std::size_t sp_ref_set_decomp_n, + const std::size_t sp_ref_set_decomp_m, + SpTxSquashedV1 &tx_out) +{ + try + { + recover_sp_tx_squashed_v1(serializable_tx_in, + sp_refset_bin_config, + sp_ref_set_decomp_n, + sp_ref_set_decomp_m, + tx_out); + } + catch (...) { return false; } + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool try_recover_sp_tx_squashed_v1(ser_SpTxSquashedV1 &serializable_tx_in, SpTxSquashedV1 &tx_out) +{ + try + { + recover_sp_tx_squashed_v1(serializable_tx_in, tx_out); + } + catch (...) { return false; } + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace serialization +} //namespace sp diff --git a/src/seraphis_impl/serialization_demo_utils.h b/src/seraphis_impl/serialization_demo_utils.h new file mode 100644 index 0000000000..fe3e44f0e1 --- /dev/null +++ b/src/seraphis_impl/serialization_demo_utils.h @@ -0,0 +1,191 @@ +// Copyright (c) 2022, 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. + +// Serialization utilities for serializable seraphis types (a demonstration). +// WARNING: All of the deserialization functions are **destructive**, meaning the ser_ objects passed in will +// often be left in an invalid state after a function call. Note that the serialization functions +// are copy-only. + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/binned_reference_set.h" +#include "seraphis_core/discretized_fee.h" +#include "seraphis_core/sp_core_types.h" +#include "seraphis_impl/serialization_demo_types.h" +#include "seraphis_main/tx_component_types.h" +#include "seraphis_main/tx_component_types_legacy.h" +#include "seraphis_main/txtype_squashed_v1.h" +#include "serialization/binary_archive.h" +#include "serialization/serialization.h" +#include "span.h" + +//third party headers + +//standard headers +#include +#include + +//forward declarations +namespace sp +{ + struct BulletproofPlus2; + struct GrootleProof; + struct SpCompositionProof; +} + +namespace sp +{ +namespace serialization +{ + +/** +* brief: try_append_serializable - try to serialize an object and append it to an input string +* type: SerializableT - type of the object to be serialized (the object must support serialization/deserialization) +* param: serializable - +* inoutparam: serialized_inout - +* return: true if serialization succeeded +*/ +template +bool try_append_serializable(SerializableT &serializable, std::string &serialized_inout) +{ + // serialize + std::stringstream serializable_ss; + binary_archive b_archive(serializable_ss); + if (!::serialization::serialize(b_archive, serializable)) + return false; + + // save to string + serialized_inout.append(serializable_ss.str()); + + return true; +} +/** +* brief: try_get_serializable - try to deserialize a string into an object +* type: SerializableT - type of the object to be deserialized into (the object must support serialization/deserialization) +* param: serialized - +* outparam: serializable_out - +* return: true if deserialization succeeded +*/ +template +bool try_get_serializable(epee::span serialized, SerializableT &serializable_out) +{ + // recover serializable + binary_archive archived{serialized}; + return ::serialization::serialize(archived, serializable_out); +} +/** +* brief: make_serializable_* - convert a normal object into one that is serializable +* param: object - normal object +* outparam: serializable_object_out - object to map the normal object into; this should be serializable/deserializable +*/ +void make_serializable_bpp2(const BulletproofPlus2 &bpp2, ser_BulletproofPlus2_PARTIAL &serializable_bpp2_out); +void make_serializable_clsag(const rct::clsag &clsag, ser_clsag_PARTIAL &serializable_clsag_out); +void make_serializable_grootle_proof(const GrootleProof &grootle, ser_GrootleProof &serializable_grootle_out); +void make_serializable_sp_composition_proof(const SpCompositionProof &proof, + ser_SpCompositionProof &serializable_proof_out); +void make_serializable_sp_coinbase_enote_core(const SpCoinbaseEnoteCore &enote, + ser_SpCoinbaseEnoteCore &serializable_enote_out); +void make_serializable_sp_enote_core(const SpEnoteCore &enote, ser_SpEnoteCore &serializable_enote_out); +void make_serializable_sp_enote_image_core(const SpEnoteImageCore &image, ser_SpEnoteImageCore &serializable_image_out); +void make_serializable_sp_binned_reference_set_v1(const SpBinnedReferenceSetV1 &refset, + ser_SpBinnedReferenceSetV1_PARTIAL &serializable_refset_out); +void make_serializable_legacy_enote_image_v2(const LegacyEnoteImageV2 &image, + ser_LegacyEnoteImageV2 &serializable_image_out); +void make_serializable_sp_enote_v1(const SpEnoteV1 &enote, ser_SpEnoteV1 &serializable_enote_out); +void make_serializable_sp_enote_image_v1(const SpEnoteImageV1 &image, ser_SpEnoteImageV1 &serializable_image_out); +void make_serializable_sp_balance_proof_v1(const SpBalanceProofV1 &proof, + ser_SpBalanceProofV1_PARTIAL &serializable_proof_out); +void make_serializable_legacy_ring_signature_v4(const LegacyRingSignatureV4 &signature, + ser_LegacyRingSignatureV4_PARTIAL &serializable_signature_out); +void make_serializable_sp_membership_proof_v1(const SpMembershipProofV1 &proof, + ser_SpMembershipProofV1_PARTIAL &serializable_proof_out); +void make_serializable_sp_image_proof_v1(const SpImageProofV1 &image_proof, + ser_SpImageProofV1 &serializable_image_proof_out); +void make_serializable_sp_tx_supplement_v1(const SpTxSupplementV1 &supplement, + ser_SpTxSupplementV1 &serializable_supplement_out); +void make_serializable_discretized_fee(const DiscretizedFee discretized_fee, + unsigned char &serializable_discretized_fee_out); +void make_serializable_sp_tx_coinbase_v1(const SpTxCoinbaseV1 &tx, ser_SpTxCoinbaseV1 &serializable_tx_out); +void make_serializable_sp_tx_squashed_v1(const SpTxSquashedV1 &tx, ser_SpTxSquashedV1 &serializable_tx_out); +/** +* brief: recover_* - convert a serializable object back into its normal object parent +* param: serializable_object_in - serializable object to be consumed (destructive: may be left in an unusable state) +* param: ...params... - additional data not recorded in the serializable object to paste into the normal object +* outparam: object_out - object to map the serializable object and extra params into +*/ +void recover_bpp2(ser_BulletproofPlus2_PARTIAL &serializable_bpp2_in, + std::vector balance_proof_commitments_mulinv8, + BulletproofPlus2 &bpp2_out); +void recover_clsag(ser_clsag_PARTIAL &serializable_clsag_in, const crypto::key_image &key_image, rct::clsag &clsag_out); +void recover_grootle_proof(ser_GrootleProof &serializable_grootle_in, GrootleProof &grootle_out); +void recover_sp_composition_proof(const ser_SpCompositionProof &serializable_proof, SpCompositionProof &proof_out); +void recover_sp_coinbase_enote_core(const ser_SpCoinbaseEnoteCore &serializable_enote, SpCoinbaseEnoteCore &enote_out); +void recover_sp_enote_core(const ser_SpEnoteCore &serializable_enote, SpEnoteCore &enote_out); +void recover_sp_enote_image_core(const ser_SpEnoteImageCore &serializable_image, SpEnoteImageCore &image_out); +void recover_sp_binned_reference_set_v1(ser_SpBinnedReferenceSetV1_PARTIAL &serializable_refset_in, + const SpBinnedReferenceSetConfigV1 &bin_config, + const rct::key &generator_seed, + SpBinnedReferenceSetV1 &refset_out); +void recover_legacy_enote_image_v2(const ser_LegacyEnoteImageV2 &serializable_image, LegacyEnoteImageV2 &image_out); +void recover_sp_coinbase_enote_v1(const ser_SpCoinbaseEnoteV1 &serializable_enote, SpCoinbaseEnoteV1 &enote_out); +void recover_sp_enote_v1(const ser_SpEnoteV1 &serializable_enote, SpEnoteV1 &enote_out); +void recover_sp_enote_image_v1(const ser_SpEnoteImageV1 &serializable_image, SpEnoteImageV1 &image_out); +void recover_sp_balance_proof_v1(ser_SpBalanceProofV1_PARTIAL &serializable_proof_in, + std::vector commitments_inv8, + SpBalanceProofV1 &proof_out); +void recover_legacy_ring_signature_v4(ser_LegacyRingSignatureV4_PARTIAL &serializable_signature_in, + const crypto::key_image &key_image, + LegacyRingSignatureV4 &signature_out); +void recover_sp_membership_proof_v1(ser_SpMembershipProofV1_PARTIAL &serializable_proof_in, + const SpBinnedReferenceSetConfigV1 &bin_config, + const rct::key &generator_seed, + const std::size_t ref_set_decomp_n, + const std::size_t ref_set_decomp_m, + SpMembershipProofV1 &proof_out); +void recover_sp_image_proof_v1(const ser_SpImageProofV1 &serializable_image_proof, SpImageProofV1 &image_proof_out); +void recover_sp_tx_supplement_v1(ser_SpTxSupplementV1 &serializable_supplement_in, SpTxSupplementV1 &supplement_out); +void recover_discretized_fee(const unsigned char serializable_discretized_fee, DiscretizedFee &discretized_fee_out); +void recover_sp_tx_coinbase_v1(ser_SpTxCoinbaseV1 &serializable_tx_in, SpTxCoinbaseV1 &tx_out); +void recover_sp_tx_squashed_v1(ser_SpTxSquashedV1 &serializable_tx_in, + const SpBinnedReferenceSetConfigV1 &sp_refset_bin_config, + const std::size_t sp_ref_set_decomp_n, + const std::size_t sp_ref_set_decomp_m, + SpTxSquashedV1 &tx_out); +void recover_sp_tx_squashed_v1(ser_SpTxSquashedV1 &serializable_tx_in, SpTxSquashedV1 &tx_out); +bool try_recover_sp_tx_squashed_v1(ser_SpTxSquashedV1 &serializable_tx_in, + const SpBinnedReferenceSetConfigV1 &sp_refset_bin_config, + const std::size_t sp_ref_set_decomp_n, + const std::size_t sp_ref_set_decomp_m, + SpTxSquashedV1 &tx_out); +bool try_recover_sp_tx_squashed_v1(ser_SpTxSquashedV1 &serializable_tx_in, SpTxSquashedV1 &tx_out); + +} //namespace serialization +} //namespace sp diff --git a/src/seraphis_impl/tx_builder_utils.cpp b/src/seraphis_impl/tx_builder_utils.cpp new file mode 100644 index 0000000000..70ee1c038e --- /dev/null +++ b/src/seraphis_impl/tx_builder_utils.cpp @@ -0,0 +1,134 @@ +// Copyright (c) 2022, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//paired header +#include "tx_builder_utils.h" + +//local headers +#include "crypto/crypto.h" +#include "misc_log_ex.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/discretized_fee.h" +#include "seraphis_core/jamtis_destination.h" +#include "seraphis_core/jamtis_payment_proposal.h" +#include "seraphis_core/sp_core_enote_utils.h" +#include "seraphis_crypto/sp_legacy_proof_helpers.h" +#include "seraphis_impl/tx_input_selection_output_context_v1.h" +#include "seraphis_main/contextual_enote_record_utils.h" +#include "seraphis_main/tx_builder_types.h" +#include "seraphis_main/tx_builders_inputs.h" +#include "seraphis_main/tx_builders_legacy_inputs.h" +#include "seraphis_main/tx_builders_outputs.h" +#include "seraphis_main/tx_component_types.h" +#include "seraphis_main/tx_component_types_legacy.h" + +//third party headers + +//standard headers +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis_impl" + +namespace sp +{ +//------------------------------------------------------------------------------------------------------------------- +bool try_prepare_inputs_and_outputs_for_transfer_v1(const jamtis::JamtisDestinationV1 &change_address, + const jamtis::JamtisDestinationV1 &dummy_address, + const InputSelectorV1 &local_user_input_selector, + const FeeCalculator &tx_fee_calculator, + const rct::xmr_amount fee_per_tx_weight, + const std::size_t max_inputs, + std::vector normal_payment_proposals, + std::vector selfsend_payment_proposals, + const crypto::secret_key &k_view_balance, + std::vector &legacy_contextual_inputs_out, + std::vector &sp_contextual_inputs_out, + std::vector &final_normal_payment_proposals_out, + std::vector &final_selfsend_payment_proposals_out, + DiscretizedFee &discretized_transaction_fee_out) +{ + legacy_contextual_inputs_out.clear(); + sp_contextual_inputs_out.clear(); + final_normal_payment_proposals_out.clear(); + final_selfsend_payment_proposals_out.clear(); + + // 1. try to select inputs for the tx + const OutputSetContextForInputSelectionV1 output_set_context{ + normal_payment_proposals, + selfsend_payment_proposals + }; + + rct::xmr_amount reported_final_fee; + input_set_tracker_t selected_input_set; + + if (!try_get_input_set_v1(output_set_context, + max_inputs, + local_user_input_selector, + fee_per_tx_weight, + tx_fee_calculator, + reported_final_fee, + selected_input_set)) + return false; + + // 2. separate into legacy and seraphis inputs + split_selected_input_set(selected_input_set, legacy_contextual_inputs_out, sp_contextual_inputs_out); + + // 3. get total input amount + const boost::multiprecision::uint128_t total_input_amount{ + total_amount(legacy_contextual_inputs_out) + + total_amount(sp_contextual_inputs_out) + }; + + // 4. finalize output set + finalize_v1_output_proposal_set_v1(total_input_amount, + reported_final_fee, + change_address, + dummy_address, + k_view_balance, + normal_payment_proposals, + selfsend_payment_proposals); + + CHECK_AND_ASSERT_THROW_MES(tx_fee_calculator.compute_fee(fee_per_tx_weight, + legacy_contextual_inputs_out.size(), sp_contextual_inputs_out.size(), + normal_payment_proposals.size() + selfsend_payment_proposals.size()) == + reported_final_fee, + "prepare inputs and outputs for transfer (v1): final fee is not consistent with input selector fee (bug)."); + + final_normal_payment_proposals_out = std::move(normal_payment_proposals); + final_selfsend_payment_proposals_out = std::move(selfsend_payment_proposals); + + // 5. set transaction fee + discretized_transaction_fee_out = discretize_fee(reported_final_fee); + CHECK_AND_ASSERT_THROW_MES(discretized_transaction_fee_out == reported_final_fee, + "prepare inputs and outputs for transfer (v1): the input selector fee was not properly discretized (bug)."); + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace sp diff --git a/src/seraphis_impl/tx_builder_utils.h b/src/seraphis_impl/tx_builder_utils.h new file mode 100644 index 0000000000..58dfeae3d3 --- /dev/null +++ b/src/seraphis_impl/tx_builder_utils.h @@ -0,0 +1,89 @@ +// Copyright (c) 2022, 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. + +// Utiltities to support seraphis transaction building. + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/discretized_fee.h" +#include "seraphis_core/jamtis_destination.h" +#include "seraphis_core/jamtis_payment_proposal.h" +#include "seraphis_main/tx_builder_types.h" +#include "seraphis_main/tx_builder_types_legacy.h" +#include "seraphis_main/tx_component_types.h" +#include "seraphis_main/tx_component_types_legacy.h" +#include "seraphis_main/tx_input_selection.h" +#include "seraphis_main/tx_fee_calculator.h" + +//third party headers + +//standard headers +#include + +//forward declarations + + +namespace sp +{ + +/** +* brief: try_prepare_inputs_and_outputs_for_transfer_v1 - try to select inputs then finalize outputs for a tx +* param: change_address - +* param: dummy_address - +* param: local_user_input_selector - +* param: tx_fee_calculator - +* param: fee_per_tx_weight - +* param: max_inputs - +* param: normal_payment_proposals - +* param: selfsend_payment_proposals - +* param: k_view_balance - +* outparam: legacy_contextual_inputs_out - +* outparam: sp_contextual_inputs_out - +* outparam: final_normal_payment_proposals_out - +* outparam: final_selfsend_payment_proposals_out - +* outparam: discretized_transaction_fee_out - +*/ +bool try_prepare_inputs_and_outputs_for_transfer_v1(const jamtis::JamtisDestinationV1 &change_address, + const jamtis::JamtisDestinationV1 &dummy_address, + const InputSelectorV1 &local_user_input_selector, + const FeeCalculator &tx_fee_calculator, + const rct::xmr_amount fee_per_tx_weight, + const std::size_t max_inputs, + std::vector normal_payment_proposals, + std::vector selfsend_payment_proposals, + const crypto::secret_key &k_view_balance, + std::vector &legacy_contextual_inputs_out, + std::vector &sp_contextual_inputs_out, + std::vector &final_normal_payment_proposals_out, + std::vector &final_selfsend_payment_proposals_out, + DiscretizedFee &discretized_transaction_fee_out); + +} //namespace sp diff --git a/src/seraphis_impl/tx_fee_calculator_squashed_v1.cpp b/src/seraphis_impl/tx_fee_calculator_squashed_v1.cpp new file mode 100644 index 0000000000..15a198f8c0 --- /dev/null +++ b/src/seraphis_impl/tx_fee_calculator_squashed_v1.cpp @@ -0,0 +1,93 @@ +// Copyright (c) 2022, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//paired header +#include "tx_fee_calculator_squashed_v1.h" + +//local headers +#include "misc_log_ex.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/discretized_fee.h" + +//third party headers + +//standard headers + + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis_impl" + +namespace sp +{ +//------------------------------------------------------------------------------------------------------------------- +FeeCalculatorSpTxSquashedV1::FeeCalculatorSpTxSquashedV1(const std::size_t legacy_ring_size, + const std::size_t ref_set_decomp_n, + const std::size_t ref_set_decomp_m, + const std::size_t num_bin_members, + const std::size_t tx_extra_size) : + m_legacy_ring_size{legacy_ring_size}, + m_ref_set_decomp_n{ref_set_decomp_n}, + m_ref_set_decomp_m{ref_set_decomp_m}, + m_num_bin_members{num_bin_members}, + m_tx_extra_size{tx_extra_size} +{} +//------------------------------------------------------------------------------------------------------------------- +rct::xmr_amount FeeCalculatorSpTxSquashedV1::compute_fee(const std::size_t fee_per_weight, const std::size_t weight) +{ + rct::xmr_amount fee_value; + CHECK_AND_ASSERT_THROW_MES(try_get_fee_value(discretize_fee(fee_per_weight * weight), fee_value), + "tx fee getter (SpTxSquashedV1): extracting discretized fee failed (bug)."); + + return fee_value; +} +//------------------------------------------------------------------------------------------------------------------- +rct::xmr_amount FeeCalculatorSpTxSquashedV1::compute_fee(const std::size_t fee_per_weight, const SpTxSquashedV1 &tx) +{ + return FeeCalculatorSpTxSquashedV1::compute_fee(fee_per_weight, sp_tx_squashed_v1_weight(tx)); +} +//------------------------------------------------------------------------------------------------------------------- +rct::xmr_amount FeeCalculatorSpTxSquashedV1::compute_fee(const std::size_t fee_per_weight, + const std::size_t num_legacy_inputs, + const std::size_t num_sp_inputs, + const std::size_t num_outputs) const +{ + const std::size_t weight{ + sp_tx_squashed_v1_weight(num_legacy_inputs, + num_sp_inputs, + num_outputs, + m_legacy_ring_size, + m_ref_set_decomp_n, + m_ref_set_decomp_m, + m_num_bin_members, + m_tx_extra_size) + }; + + return this->compute_fee(fee_per_weight, weight); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace sp diff --git a/src/seraphis_impl/tx_fee_calculator_squashed_v1.h b/src/seraphis_impl/tx_fee_calculator_squashed_v1.h new file mode 100644 index 0000000000..ec8d22c394 --- /dev/null +++ b/src/seraphis_impl/tx_fee_calculator_squashed_v1.h @@ -0,0 +1,77 @@ +// Copyright (c) 2022, 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. + +// Calculate the fee for an SpTxSquashedV1 tx. + +#pragma once + +//local headers +#include "ringct/rctTypes.h" +#include "seraphis_core/tx_extra.h" +#include "seraphis_main/tx_fee_calculator.h" +#include "seraphis_main/txtype_squashed_v1.h" + +//third party headers + +//standard headers + +//forward declarations + + +namespace sp +{ + +class FeeCalculatorSpTxSquashedV1 final : public FeeCalculator +{ +public: +//constructors + FeeCalculatorSpTxSquashedV1(const std::size_t legacy_ring_size, + const std::size_t ref_set_decomp_n, + const std::size_t ref_set_decomp_m, + const std::size_t num_bin_members, + const std::size_t tx_extra_size); + +//member functions + static rct::xmr_amount compute_fee(const std::size_t fee_per_weight, const std::size_t weight); + static rct::xmr_amount compute_fee(const std::size_t fee_per_weight, const SpTxSquashedV1 &tx); + rct::xmr_amount compute_fee(const std::size_t fee_per_weight, + const std::size_t num_legacy_inputs, + const std::size_t num_sp_inputs, + const std::size_t num_outputs) const override; + +private: +//member variables + /// misc. info for calculating tx weight + std::size_t m_legacy_ring_size; + std::size_t m_ref_set_decomp_n; + std::size_t m_ref_set_decomp_m; + std::size_t m_num_bin_members; + std::size_t m_tx_extra_size; +}; + +} //namespace sp diff --git a/src/seraphis_impl/tx_input_selection_output_context_v1.cpp b/src/seraphis_impl/tx_input_selection_output_context_v1.cpp new file mode 100644 index 0000000000..71ce8449ea --- /dev/null +++ b/src/seraphis_impl/tx_input_selection_output_context_v1.cpp @@ -0,0 +1,139 @@ +// Copyright (c) 2022, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//paired header +#include "tx_input_selection_output_context_v1.h" + +//local headers +#include "crypto/crypto.h" +#include "crypto/x25519.h" +#include "ringct/rctOps.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/jamtis_payment_proposal.h" +#include "seraphis_core/jamtis_support_types.h" +#include "seraphis_main/tx_builders_outputs.h" + +//third party headers +#include "boost/multiprecision/cpp_int.hpp" + +//standard headers +#include +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis_impl" + +namespace sp +{ +//------------------------------------------------------------------------------------------------------------------- +// check that all enote ephemeral pubkeys in an output proposal set are unique +//------------------------------------------------------------------------------------------------------------------- +static bool ephemeral_pubkeys_are_unique(const std::vector &normal_payment_proposals, + const std::vector &selfsend_payment_proposals) +{ + std::unordered_set enote_ephemeral_pubkeys; + enote_ephemeral_pubkeys.reserve(normal_payment_proposals.size() + selfsend_payment_proposals.size()); + crypto::x25519_pubkey temp_enote_ephemeral_pubkey; + + for (const jamtis::JamtisPaymentProposalV1 &normal_proposal : normal_payment_proposals) + { + jamtis::get_enote_ephemeral_pubkey(normal_proposal, temp_enote_ephemeral_pubkey); + enote_ephemeral_pubkeys.insert(temp_enote_ephemeral_pubkey); + } + + for (const jamtis::JamtisPaymentProposalSelfSendV1 &selfsend_proposal : selfsend_payment_proposals) + { + jamtis::get_enote_ephemeral_pubkey(selfsend_proposal, temp_enote_ephemeral_pubkey); + enote_ephemeral_pubkeys.insert(temp_enote_ephemeral_pubkey); + } + + return enote_ephemeral_pubkeys.size() == normal_payment_proposals.size() + selfsend_payment_proposals.size(); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool need_additional_output(const std::size_t num_outputs, + const bool output_ephemeral_pubkeys_are_unique, + const std::vector &self_send_output_types, + const rct::xmr_amount change_amount) +{ + // see if we need an additional output + return static_cast(try_get_additional_output_type_for_output_set_v1(num_outputs, + self_send_output_types, + output_ephemeral_pubkeys_are_unique, + change_amount)); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +OutputSetContextForInputSelectionV1::OutputSetContextForInputSelectionV1( + const std::vector &normal_payment_proposals, + const std::vector &selfsend_payment_proposals) : + m_num_outputs{normal_payment_proposals.size() + selfsend_payment_proposals.size()}, + m_output_ephemeral_pubkeys_are_unique{ + ephemeral_pubkeys_are_unique(normal_payment_proposals, selfsend_payment_proposals) + } +{ + // 1. collect self-send output types + m_self_send_output_types.reserve(selfsend_payment_proposals.size()); + + for (const jamtis::JamtisPaymentProposalSelfSendV1 &selfsend_proposal : selfsend_payment_proposals) + m_self_send_output_types.emplace_back(selfsend_proposal.type); + + // 2. collect total amount + m_total_output_amount = 0; + + for (const jamtis::JamtisPaymentProposalV1 &normal_proposal : normal_payment_proposals) + m_total_output_amount += normal_proposal.amount; + + for (const jamtis::JamtisPaymentProposalSelfSendV1 &selfsend_proposal : selfsend_payment_proposals) + m_total_output_amount += selfsend_proposal.amount; +} +//------------------------------------------------------------------------------------------------------------------- +boost::multiprecision::uint128_t OutputSetContextForInputSelectionV1::total_amount() const +{ + return m_total_output_amount; +} +//------------------------------------------------------------------------------------------------------------------- +std::size_t OutputSetContextForInputSelectionV1::num_outputs_nochange() const +{ + const bool need_additional_output_no_change{ + need_additional_output(m_num_outputs, m_output_ephemeral_pubkeys_are_unique, m_self_send_output_types, 0) + }; + + return m_num_outputs + (need_additional_output_no_change ? 1 : 0); +} +//------------------------------------------------------------------------------------------------------------------- +std::size_t OutputSetContextForInputSelectionV1::num_outputs_withchange() const +{ + const bool need_additional_output_with_change{ + need_additional_output(m_num_outputs, m_output_ephemeral_pubkeys_are_unique, m_self_send_output_types, 1) + }; + + return m_num_outputs + (need_additional_output_with_change ? 1 : 0); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace sp diff --git a/src/seraphis_impl/tx_input_selection_output_context_v1.h b/src/seraphis_impl/tx_input_selection_output_context_v1.h new file mode 100644 index 0000000000..41f4772077 --- /dev/null +++ b/src/seraphis_impl/tx_input_selection_output_context_v1.h @@ -0,0 +1,77 @@ +// Copyright (c) 2022, 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. + +// Output set context for use during input selection. + +#pragma once + +//local headers +#include "seraphis_core/jamtis_payment_proposal.h" +#include "seraphis_core/jamtis_support_types.h" +#include "seraphis_main/tx_input_selection_output_context.h" + +//third party headers +#include "boost/multiprecision/cpp_int.hpp" + +//standard headers +#include + +//forward declarations + + +namespace sp +{ + +class OutputSetContextForInputSelectionV1 final : public OutputSetContextForInputSelection +{ +public: +//constructors + OutputSetContextForInputSelectionV1(const std::vector &normal_payment_proposals, + const std::vector &selfsend_payment_proposals); + +//overloaded operators + /// disable copy/move (this is a scoped manager [concept: context binding]) + OutputSetContextForInputSelectionV1& operator=(OutputSetContextForInputSelectionV1&&) = delete; + +//member functions + /// get total output amount + boost::multiprecision::uint128_t total_amount() const override; + /// get number of outputs assuming no change + std::size_t num_outputs_nochange() const override; + /// get number of outputs assuming non-zero change + std::size_t num_outputs_withchange() const override; + +//member variables +private: + std::size_t m_num_outputs; + bool m_output_ephemeral_pubkeys_are_unique; + std::vector m_self_send_output_types; + boost::multiprecision::uint128_t m_total_output_amount; +}; + +} //namespace sp diff --git a/src/seraphis_main/CMakeLists.txt b/src/seraphis_main/CMakeLists.txt new file mode 100644 index 0000000000..3df3d6ffae --- /dev/null +++ b/src/seraphis_main/CMakeLists.txt @@ -0,0 +1,78 @@ +# Copyright (c) 2021, 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(seraphis_main_sources + contextual_enote_record_types.cpp + contextual_enote_record_utils.cpp + enote_record_utils.cpp + enote_record_utils_legacy.cpp + scan_balance_recovery_utils.cpp + scan_machine.cpp + scan_misc_utils.cpp + sp_knowledge_proof_utils.cpp + tx_builder_types.cpp + tx_builder_types_legacy.cpp + tx_builder_types_multisig.cpp + tx_builders_inputs.cpp + tx_builders_legacy_inputs.cpp + tx_builders_mixed.cpp + tx_builders_multisig.cpp + tx_builders_outputs.cpp + tx_component_types.cpp + tx_component_types_legacy.cpp + tx_input_selection.cpp + tx_validators.cpp + txtype_base.cpp + txtype_coinbase_v1.cpp + txtype_squashed_v1.cpp) + +monero_find_all_headers(seraphis_main_headers, "${CMAKE_CURRENT_SOURCE_DIR}") + +monero_add_library(seraphis_main + ${seraphis_main_sources} + ${seraphis_main_headers}) + +target_link_libraries(seraphis_main + PUBLIC + cncrypto + common + cryptonote_basic + device + epee + multisig + ringct + seraphis_core + seraphis_crypto + PRIVATE + ${EXTRA_LIBRARIES}) + +target_include_directories(seraphis_main + PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}" + PRIVATE + ${Boost_INCLUDE_DIRS}) diff --git a/src/seraphis_main/contextual_enote_record_types.cpp b/src/seraphis_main/contextual_enote_record_types.cpp new file mode 100644 index 0000000000..c155dfb7c7 --- /dev/null +++ b/src/seraphis_main/contextual_enote_record_types.cpp @@ -0,0 +1,266 @@ +// Copyright (c) 2022, 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 "contextual_enote_record_types.h" + +//local headers +#include "common/variant.h" +#include "crypto/crypto.h" +#include "ringct/rctTypes.h" + +//third party headers + +//standard headers +#include + + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis" + +namespace sp +{ +//------------------------------------------------------------------------------------------------------------------- +const rct::key& onetime_address_ref(const LegacyContextualIntermediateEnoteRecordV1 &record) +{ + return onetime_address_ref(record.record.enote); +} +//------------------------------------------------------------------------------------------------------------------- +rct::xmr_amount amount_ref(const LegacyContextualIntermediateEnoteRecordV1 &record) +{ + return record.record.amount; +} +//------------------------------------------------------------------------------------------------------------------- +const crypto::key_image& key_image_ref(const LegacyContextualEnoteRecordV1 &record) +{ + return record.record.key_image; +} +//------------------------------------------------------------------------------------------------------------------- +rct::xmr_amount amount_ref(const LegacyContextualEnoteRecordV1 &record) +{ + return record.record.amount; +} +//------------------------------------------------------------------------------------------------------------------- +const rct::key& onetime_address_ref(const SpContextualIntermediateEnoteRecordV1 &record) +{ + return onetime_address_ref(record.record.enote); +} +//------------------------------------------------------------------------------------------------------------------- +rct::xmr_amount amount_ref(const SpContextualIntermediateEnoteRecordV1 &record) +{ + return record.record.amount; +} +//------------------------------------------------------------------------------------------------------------------- +const crypto::key_image& key_image_ref(const SpContextualEnoteRecordV1 &record) +{ + return record.record.key_image; +} +//------------------------------------------------------------------------------------------------------------------- +rct::xmr_amount amount_ref(const SpContextualEnoteRecordV1 &record) +{ + return record.record.amount; +} +//------------------------------------------------------------------------------------------------------------------- +const SpEnoteOriginContextV1& origin_context_ref(const ContextualBasicRecordVariant &variant) +{ + struct visitor final : public tools::variant_static_visitor + { + using variant_static_visitor::operator(); //for blank overload + const SpEnoteOriginContextV1& operator()(const LegacyContextualBasicEnoteRecordV1 &record) const + { return record.origin_context; } + const SpEnoteOriginContextV1& operator()(const SpContextualBasicEnoteRecordV1 &record) const + { return record.origin_context; } + }; + + return variant.visit(visitor{}); +} +//------------------------------------------------------------------------------------------------------------------- +rct::xmr_amount amount_ref(const ContextualRecordVariant &variant) +{ + struct visitor final : public tools::variant_static_visitor + { + using variant_static_visitor::operator(); //for blank overload + rct::xmr_amount operator()(const LegacyContextualEnoteRecordV1 &record) const { return amount_ref(record); } + rct::xmr_amount operator()(const SpContextualEnoteRecordV1 &record) const { return amount_ref(record); } + }; + + return variant.visit(visitor{}); +} +//------------------------------------------------------------------------------------------------------------------- +const SpEnoteOriginContextV1& origin_context_ref(const ContextualRecordVariant &variant) +{ + struct visitor final : public tools::variant_static_visitor + { + using variant_static_visitor::operator(); //for blank overload + const SpEnoteOriginContextV1& operator()(const LegacyContextualEnoteRecordV1 &record) const + { return record.origin_context; } + const SpEnoteOriginContextV1& operator()(const SpContextualEnoteRecordV1 &record) const + { return record.origin_context; } + }; + + return variant.visit(visitor{}); +} +//------------------------------------------------------------------------------------------------------------------- +const SpEnoteSpentContextV1& spent_context_ref(const ContextualRecordVariant &variant) +{ + struct visitor final : public tools::variant_static_visitor + { + using variant_static_visitor::operator(); //for blank overload + const SpEnoteSpentContextV1& operator()(const LegacyContextualEnoteRecordV1 &record) const + { return record.spent_context; } + const SpEnoteSpentContextV1& operator()(const SpContextualEnoteRecordV1 &record) const + { return record.spent_context; } + }; + + return variant.visit(visitor{}); +} +//------------------------------------------------------------------------------------------------------------------- +bool is_older_than(const SpEnoteOriginContextV1 &context, const SpEnoteOriginContextV1 &other_context) +{ + // 1. origin status (higher statuses are assumed to be 'older') + if (context.origin_status > other_context.origin_status) + return true; + if (context.origin_status < other_context.origin_status) + return false; + + // 2. block index + if (context.block_index < other_context.block_index) + return true; + if (context.block_index > other_context.block_index) + return false; + + // note: don't assess the tx output index + + // 3. enote ledger index + if (context.enote_ledger_index < other_context.enote_ledger_index) + return true; + if (context.enote_ledger_index > other_context.enote_ledger_index) + return false; + + // 4. block timestamp + if (context.block_timestamp < other_context.block_timestamp) + return true; + if (context.block_timestamp > other_context.block_timestamp) + return false; + + return false; +} +//------------------------------------------------------------------------------------------------------------------- +bool is_older_than(const SpEnoteSpentContextV1 &context, const SpEnoteSpentContextV1 &other_context) +{ + // 1. spent status (higher statuses are assumed to be 'older') + if (context.spent_status > other_context.spent_status) + return true; + if (context.spent_status < other_context.spent_status) + return false; + + // 2. block index + if (context.block_index < other_context.block_index) + return true; + if (context.block_index > other_context.block_index) + return false; + + // 3. block timestamp + if (context.block_timestamp < other_context.block_timestamp) + return true; + if (context.block_timestamp > other_context.block_timestamp) + return false; + + return false; +} +//------------------------------------------------------------------------------------------------------------------- +bool have_same_destination(const LegacyContextualBasicEnoteRecordV1 &a, const LegacyContextualBasicEnoteRecordV1 &b) +{ + return onetime_address_ref(a.record.enote) == onetime_address_ref(b.record.enote); +} +//------------------------------------------------------------------------------------------------------------------- +bool have_same_destination(const LegacyContextualIntermediateEnoteRecordV1 &a, + const LegacyContextualIntermediateEnoteRecordV1 &b) +{ + return onetime_address_ref(a.record.enote) == onetime_address_ref(b.record.enote); +} +//------------------------------------------------------------------------------------------------------------------- +bool have_same_destination(const LegacyContextualEnoteRecordV1 &a, const LegacyContextualEnoteRecordV1 &b) +{ + return onetime_address_ref(a.record.enote) == onetime_address_ref(b.record.enote); +} +//------------------------------------------------------------------------------------------------------------------- +bool have_same_destination(const SpContextualBasicEnoteRecordV1 &a, const SpContextualBasicEnoteRecordV1 &b) +{ + return onetime_address_ref(a.record.enote) == onetime_address_ref(b.record.enote); +} +//------------------------------------------------------------------------------------------------------------------- +bool have_same_destination(const SpContextualIntermediateEnoteRecordV1 &a, const SpContextualIntermediateEnoteRecordV1 &b) +{ + return onetime_address_ref(a) == onetime_address_ref(b); +} +//------------------------------------------------------------------------------------------------------------------- +bool have_same_destination(const SpContextualEnoteRecordV1 &a, const SpContextualEnoteRecordV1 &b) +{ + return onetime_address_ref(a.record.enote) == onetime_address_ref(b.record.enote); +} +//------------------------------------------------------------------------------------------------------------------- +bool has_origin_status(const LegacyContextualIntermediateEnoteRecordV1 &record, const SpEnoteOriginStatus test_status) +{ + return record.origin_context.origin_status == test_status; +} +//------------------------------------------------------------------------------------------------------------------- +bool has_origin_status(const LegacyContextualEnoteRecordV1 &record, const SpEnoteOriginStatus test_status) +{ + return record.origin_context.origin_status == test_status; +} +//------------------------------------------------------------------------------------------------------------------- +bool has_origin_status(const SpContextualIntermediateEnoteRecordV1 &record, const SpEnoteOriginStatus test_status) +{ + return record.origin_context.origin_status == test_status; +} +//------------------------------------------------------------------------------------------------------------------- +bool has_origin_status(const SpContextualEnoteRecordV1 &record, const SpEnoteOriginStatus test_status) +{ + return record.origin_context.origin_status == test_status; +} +//------------------------------------------------------------------------------------------------------------------- +bool has_spent_status(const LegacyContextualEnoteRecordV1 &record, const SpEnoteSpentStatus test_status) +{ + return record.spent_context.spent_status == test_status; +} +//------------------------------------------------------------------------------------------------------------------- +bool has_spent_status(const SpContextualEnoteRecordV1 &record, const SpEnoteSpentStatus test_status) +{ + return record.spent_context.spent_status == test_status; +} +//------------------------------------------------------------------------------------------------------------------- +bool has_key_image(const SpContextualKeyImageSetV1 &key_image_set, const crypto::key_image &test_key_image) +{ + return std::find(key_image_set.legacy_key_images.begin(), key_image_set.legacy_key_images.end(), test_key_image) != + key_image_set.legacy_key_images.end() || + std::find(key_image_set.sp_key_images.begin(), key_image_set.sp_key_images.end(), test_key_image) != + key_image_set.sp_key_images.end(); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace sp diff --git a/src/seraphis_main/contextual_enote_record_types.h b/src/seraphis_main/contextual_enote_record_types.h new file mode 100644 index 0000000000..47a12a7393 --- /dev/null +++ b/src/seraphis_main/contextual_enote_record_types.h @@ -0,0 +1,304 @@ +// Copyright (c) 2022, 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. + +// Records of Seraphis enotes with context about their origin and their spent status. + +#pragma once + +//local headers +#include "common/variant.h" +#include "crypto/crypto.h" +#include "enote_record_types.h" +#include "ringct/rctOps.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/jamtis_support_types.h" +#include "seraphis_core/sp_core_types.h" +#include "seraphis_core/tx_extra.h" +#include "tx_component_types.h" + +//third party headers + +//standard headers +#include + +//forward declarations + + +namespace sp +{ + +//////////////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////// Contexts /////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////// + +//// +// SpEnoteOriginStatus +// - flag indicating where an enote is located +/// +enum class SpEnoteOriginStatus : unsigned char +{ + // is only located outside the mining network and blockchain (e.g. is sitting on the user's machine) + OFFCHAIN, + // is submitted to the mining network but not yet added to the blockchain (e.g. is in some node's tx pool) + UNCONFIRMED, + // is in a block in the blockchain + ONCHAIN +}; + +//// +// SpEnoteSpentStatus +// - flag indicating where an enote was spent +/// +enum class SpEnoteSpentStatus : unsigned char +{ + // has not been spent anywhere + UNSPENT, + // is spent in an off-chain tx + SPENT_OFFCHAIN, + // is spent in a tx submitted to the mining network but not yet added to the blockchain + SPENT_UNCONFIRMED, + // is spent in a tx in a block in the blockchain + SPENT_ONCHAIN +}; + +//// +// SpEnoteOriginContextV1 +// - info related to the transaction where an enote was found +// - note that an enote may originate off-chain in a partial tx where the tx id is unknown +/// +struct SpEnoteOriginContextV1 final +{ + /// block index of tx (-1 if index is unknown) + std::uint64_t block_index{static_cast(-1)}; + /// timestamp of tx's block (-1 if timestamp is unknown) + std::uint64_t block_timestamp{static_cast(-1)}; + /// tx id of the tx (0 if tx is unknown) + rct::key transaction_id{rct::zero()}; + /// index of the enote in the tx's output set (-1 if index is unknown) + std::uint64_t enote_tx_index{static_cast(-1)}; + /// ledger index of the enote (-1 if index is unknown) + std::uint64_t enote_ledger_index{static_cast(-1)}; + /// origin status (off-chain by default) + SpEnoteOriginStatus origin_status{SpEnoteOriginStatus::OFFCHAIN}; + + /// associated memo field (none by default) + TxExtra memo{}; +}; + +//// +// SpEnoteSpentContextV1 +// - info related to where an enote was spent +// - note that an enote may be spent off-chain in a partial tx where the tx id is unknown +/// +struct SpEnoteSpentContextV1 final +{ + /// block index of tx where it was spent (-1 if unspent or index is unknown) + std::uint64_t block_index{static_cast(-1)}; + /// timestamp of tx's block (-1 if timestamp is unknown) + std::uint64_t block_timestamp{static_cast(-1)}; + /// tx id of the tx where it was spent (0 if unspent or tx is unknown) + rct::key transaction_id{rct::zero()}; + /// spent status (unspent by default) + SpEnoteSpentStatus spent_status{SpEnoteSpentStatus::UNSPENT}; +}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////// Legacy //////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////// + +//// +// LegacyContextualBasicEnoteRecordV1 +// - a legacy basic enote record, with additional info related to where it was found +/// +struct LegacyContextualBasicEnoteRecordV1 final +{ + /// basic info about the enote + LegacyBasicEnoteRecord record; + /// info about where the enote was found + SpEnoteOriginContextV1 origin_context; +}; + +//// +// LegacyContextualIntermediateEnoteRecordV1 +// - a legacy intermediate enote record, with additional info related to where it was found +// - the key image is unknown, so spent status is also unknown +/// +struct LegacyContextualIntermediateEnoteRecordV1 final +{ + /// intermediate info about the enote + LegacyIntermediateEnoteRecord record; + /// info about where the enote was found + SpEnoteOriginContextV1 origin_context; +}; + +/// get the record's onetime address +const rct::key& onetime_address_ref(const LegacyContextualIntermediateEnoteRecordV1 &record); +/// get the record's amount +rct::xmr_amount amount_ref(const LegacyContextualIntermediateEnoteRecordV1 &record); + +//// +// LegacyContextualEnoteRecordV1 +// - a legacy full enote record with all related contextual information, including spent status +/// +struct LegacyContextualEnoteRecordV1 final +{ + /// info about the enote + LegacyEnoteRecord record; + /// info about where the enote was found + SpEnoteOriginContextV1 origin_context; + /// info about where the enote was spent + SpEnoteSpentContextV1 spent_context; +}; + +/// get the record's key image +const crypto::key_image& key_image_ref(const LegacyContextualEnoteRecordV1 &record); +/// get the record's amount +rct::xmr_amount amount_ref(const LegacyContextualEnoteRecordV1 &record); + +//////////////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////// Seraphis /////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////// + +//// +// SpContextualBasicEnoteRecordV1 +// - a seraphis basic enote record, with additional info related to where it was found +/// +struct SpContextualBasicEnoteRecordV1 final +{ + /// basic info about the enote + SpBasicEnoteRecordV1 record; + /// info about where the enote was found + SpEnoteOriginContextV1 origin_context; +}; + +//// +// SpContextualIntermediateEnoteRecordV1 +// - a seraphis intermediate enote record, with additional info related to where it was found +// - the key image is unknown, so spent status is also unknown +/// +struct SpContextualIntermediateEnoteRecordV1 final +{ + /// intermediate info about the enote + SpIntermediateEnoteRecordV1 record; + /// info about where the enote was found + SpEnoteOriginContextV1 origin_context; +}; + +/// get the record's onetime address +const rct::key& onetime_address_ref(const SpContextualIntermediateEnoteRecordV1 &record); +/// get the enote's amount +rct::xmr_amount amount_ref(const SpContextualIntermediateEnoteRecordV1 &record); + +//// +// SpContextualEnoteRecordV1 +// - a seraphis full enote record with all related contextual information, including spent status +/// +struct SpContextualEnoteRecordV1 final +{ + /// info about the enote + SpEnoteRecordV1 record; + /// info about where the enote was found + SpEnoteOriginContextV1 origin_context; + /// info about where the enote was spent + SpEnoteSpentContextV1 spent_context; +}; + +/// get the record's key image +const crypto::key_image& key_image_ref(const SpContextualEnoteRecordV1 &record); +/// get the record's amount +rct::xmr_amount amount_ref(const SpContextualEnoteRecordV1 &record); + +//////////////////////////////////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////// Joint ///////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////// + +//// +// ContextualBasicRecordVariant +// - variant of all contextual basic enote record types +// +// origin_context_ref(): get the record's origin context +/// +using ContextualBasicRecordVariant = tools::variant; +const SpEnoteOriginContextV1& origin_context_ref(const ContextualBasicRecordVariant &variant); + +//// +// ContextualRecordVariant +// - variant of all contextual full enote record types +// +// amount_ref(): get the record's amount +// origin_context_ref(): get the record's origin context +// spent_context_ref(): get the record's spent context +/// +using ContextualRecordVariant = tools::variant; +rct::xmr_amount amount_ref(const ContextualRecordVariant &variant); +const SpEnoteOriginContextV1& origin_context_ref(const ContextualRecordVariant &variant); +const SpEnoteSpentContextV1& spent_context_ref(const ContextualRecordVariant &variant); + +//// +// SpContextualKeyImageSetV1 +// - info about the tx where a set of key images was found +/// +struct SpContextualKeyImageSetV1 final +{ + /// a set of legacy key images found in a single tx + std::vector legacy_key_images; + /// a set of seraphis key images found in a single tx + std::vector sp_key_images; + /// info about where the corresponding inputs were spent + SpEnoteSpentContextV1 spent_context; +}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////// Free Functions //////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/// check if a context is older than another (returns false if apparently the same age, or younger) +bool is_older_than(const SpEnoteOriginContextV1 &context, const SpEnoteOriginContextV1 &other_context); +bool is_older_than(const SpEnoteSpentContextV1 &context, const SpEnoteSpentContextV1 &other_context); +/// check if records have onetime address equivalence +bool have_same_destination(const LegacyContextualBasicEnoteRecordV1 &a, + const LegacyContextualBasicEnoteRecordV1 &b); +bool have_same_destination(const LegacyContextualIntermediateEnoteRecordV1 &a, + const LegacyContextualIntermediateEnoteRecordV1 &b); +bool have_same_destination(const LegacyContextualEnoteRecordV1 &a, const LegacyContextualEnoteRecordV1 &b); +bool have_same_destination(const SpContextualBasicEnoteRecordV1 &a, const SpContextualBasicEnoteRecordV1 &b); +bool have_same_destination(const SpContextualIntermediateEnoteRecordV1 &a, + const SpContextualIntermediateEnoteRecordV1 &b); +bool have_same_destination(const SpContextualEnoteRecordV1 &a, const SpContextualEnoteRecordV1 &b); +/// check origin status +bool has_origin_status(const LegacyContextualIntermediateEnoteRecordV1 &record, const SpEnoteOriginStatus test_status); +bool has_origin_status(const LegacyContextualEnoteRecordV1 &record, const SpEnoteOriginStatus test_status); +bool has_origin_status(const SpContextualIntermediateEnoteRecordV1 &record, const SpEnoteOriginStatus test_status); +bool has_origin_status(const SpContextualEnoteRecordV1 &record, const SpEnoteOriginStatus test_status); +/// check spent status +bool has_spent_status(const LegacyContextualEnoteRecordV1 &record, const SpEnoteSpentStatus test_status); +bool has_spent_status(const SpContextualEnoteRecordV1 &record, const SpEnoteSpentStatus test_status); +/// check if a key image is present in a key image set +bool has_key_image(const SpContextualKeyImageSetV1 &key_image_set, const crypto::key_image &test_key_image); + +} //namespace sp diff --git a/src/seraphis_main/contextual_enote_record_utils.cpp b/src/seraphis_main/contextual_enote_record_utils.cpp new file mode 100644 index 0000000000..b2be59dc0b --- /dev/null +++ b/src/seraphis_main/contextual_enote_record_utils.cpp @@ -0,0 +1,336 @@ +// Copyright (c) 2022, 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 "contextual_enote_record_utils.h" + +//local headers +#include "contextual_enote_record_types.h" +#include "crypto/crypto.h" +#include "cryptonote_config.h" +#include "ringct/rctTypes.h" +#include "tx_input_selection.h" + +//third party headers +#include "boost/multiprecision/cpp_int.hpp" + +//standard headers +#include +#include +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis" + +namespace sp +{ +//------------------------------------------------------------------------------------------------------------------- +bool onchain_legacy_enote_is_locked(const std::uint64_t enote_origin_block_index, + const std::uint64_t enote_unlock_time, + const std::uint64_t top_block_index, + const std::uint64_t default_spendable_age, + const std::uint64_t current_time) +{ + // 1. check default spendable age + // - test: is the next minable block lower than the first block where the enote is spendable? + // - an enote is not spendable in the block where it originates, so the default spendable age is always at least 1 + if (top_block_index + 1 < enote_origin_block_index + std::max(std::uint64_t{1}, default_spendable_age)) + return true; + + // 2. check unlock time: height encoding + // - test: is the next minable block's height lower than the block height where the enote is unlocked? + // note: block height == block index (there is a lot of confusion around this since it 'seems' like height == chain + // size, but that doesn't take into account that the genesis block is at height 0) + if (enote_unlock_time < CRYPTONOTE_MAX_BLOCK_NUMBER && + top_block_index + 1 < enote_unlock_time) + return true; + + // 3. check unlock time: UNIX encoding + // - test: is the current time lower than the UNIX time when the enote is unlocked? + if (enote_unlock_time >= CRYPTONOTE_MAX_BLOCK_NUMBER && + current_time < enote_unlock_time) + return true; + + return false; +} +//------------------------------------------------------------------------------------------------------------------- +bool onchain_sp_enote_is_locked(const std::uint64_t enote_origin_block_index, + const std::uint64_t top_block_index, + const std::uint64_t default_spendable_age) +{ + // check default spendable age + // - test: is the next minable block lower than the first block where the enote is spendable? + // - an enote is not spendable in the block where it originates, so the default spendable age is always at least 1 + return top_block_index + 1 < enote_origin_block_index + std::max(std::uint64_t{1}, default_spendable_age); +} +//------------------------------------------------------------------------------------------------------------------- +bool legacy_enote_has_highest_amount_in_set(const rct::key &specified_enote_identifier, + const rct::xmr_amount specified_enote_amount, + const std::unordered_set &allowed_origin_statuses, + const std::unordered_set &enote_identifier_set, + const std::function &get_record_origin_status_for_identifier_func, + const std::function &get_record_amount_for_identifier_func) +{ + // 1. collect enote amounts from the set of enote identifiers + std::set collected_amounts; + bool found_specified_enote{false}; + + for (const rct::key &identifier : enote_identifier_set) + { + // a. ignore enotes with unwanted origin statuses + if (allowed_origin_statuses.find(get_record_origin_status_for_identifier_func(identifier)) == + allowed_origin_statuses.end()) + continue; + + // b. record this amount + const rct::xmr_amount amount{get_record_amount_for_identifier_func(identifier)}; + collected_amounts.insert(amount); + + // c. expect that we got the same amount for our specified enote + if (identifier == specified_enote_identifier) + { + CHECK_AND_ASSERT_THROW_MES(amount == specified_enote_amount, + "legacy enote highest amount search: mismatch between specified amount and found amount."); + found_specified_enote = true; + } + } + + // 2. expect that we found our specified identifier + // - do this instead of calling .find() on the identifier set in case the origin status check skips our identifier + CHECK_AND_ASSERT_THROW_MES(found_specified_enote, + "legacy enote highest amount search: the specified enote's identifier was not found."); + + // 3. success if the specified amount is the highest in the set + // - note: it is fine if identifiers in the set have the same amount + return *(collected_amounts.rbegin()) == specified_enote_amount; +} +//------------------------------------------------------------------------------------------------------------------- +void split_selected_input_set(const input_set_tracker_t &input_set, + std::vector &legacy_contextual_records_out, + std::vector &sp_contextual_records_out) +{ + legacy_contextual_records_out.clear(); + sp_contextual_records_out.clear(); + + // 1. obtain legacy records + if (input_set.find(InputSelectionType::LEGACY) != input_set.end()) + { + legacy_contextual_records_out.reserve(input_set.at(InputSelectionType::LEGACY).size()); + + for (const auto &mapped_contextual_enote_record : input_set.at(InputSelectionType::LEGACY)) + { + CHECK_AND_ASSERT_THROW_MES(mapped_contextual_enote_record.second.is_type(), + "splitting an input set: record is supposed to be legacy but is not."); + + legacy_contextual_records_out.emplace_back( + mapped_contextual_enote_record.second.unwrap() + ); + } + } + + // 2. obtain seraphis records + if (input_set.find(InputSelectionType::SERAPHIS) != input_set.end()) + { + sp_contextual_records_out.reserve(input_set.at(InputSelectionType::SERAPHIS).size()); + + for (const auto &mapped_contextual_enote_record : input_set.at(InputSelectionType::SERAPHIS)) + { + CHECK_AND_ASSERT_THROW_MES(mapped_contextual_enote_record.second.is_type(), + "splitting an input set: record is supposed to be seraphis but is not."); + + sp_contextual_records_out.emplace_back( + mapped_contextual_enote_record.second.unwrap() + ); + } + } +} +//------------------------------------------------------------------------------------------------------------------- +boost::multiprecision::uint128_t total_amount(const std::vector &contextual_records) +{ + boost::multiprecision::uint128_t total_amount{0}; + + for (const LegacyContextualEnoteRecordV1 &contextual_record : contextual_records) + total_amount += amount_ref(contextual_record); + + return total_amount; +} +//------------------------------------------------------------------------------------------------------------------- +boost::multiprecision::uint128_t total_amount(const std::vector &contextual_records) +{ + boost::multiprecision::uint128_t total_amount{0}; + + for (const SpContextualEnoteRecordV1 &contextual_record : contextual_records) + total_amount += amount_ref(contextual_record); + + return total_amount; +} +//------------------------------------------------------------------------------------------------------------------- +bool try_get_membership_proof_real_reference_mappings(const std::vector &contextual_records, + std::unordered_map &enote_ledger_mappings_out) +{ + enote_ledger_mappings_out.clear(); + enote_ledger_mappings_out.reserve(contextual_records.size()); + + for (const LegacyContextualEnoteRecordV1 &contextual_record : contextual_records) + { + // 1. only onchain enotes have ledger indices + if (!has_origin_status(contextual_record, SpEnoteOriginStatus::ONCHAIN)) + return false; + + // 2. save the [ KI : enote ledger index ] entry + enote_ledger_mappings_out[key_image_ref(contextual_record)] = + contextual_record.origin_context.enote_ledger_index; + } + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool try_get_membership_proof_real_reference_mappings(const std::vector &contextual_records, + std::unordered_map &enote_ledger_mappings_out) +{ + enote_ledger_mappings_out.clear(); + enote_ledger_mappings_out.reserve(contextual_records.size()); + + for (const SpContextualEnoteRecordV1 &contextual_record : contextual_records) + { + // 1. only onchain enotes have ledger indices + if (!has_origin_status(contextual_record, SpEnoteOriginStatus::ONCHAIN)) + return false; + + // 2. save the [ KI : enote ledger index ] entry + enote_ledger_mappings_out[key_image_ref(contextual_record)] = + contextual_record.origin_context.enote_ledger_index; + } + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool try_update_enote_origin_context_v1(const SpEnoteOriginContextV1 &fresh_origin_context, + SpEnoteOriginContextV1 ¤t_origin_context_inout) +{ + // 1. fail if the current context is older than the fresh one + if (is_older_than(current_origin_context_inout, fresh_origin_context)) + return false; + + // 2. overwrite with the fresh context (do this even if the fresh one seems to have the same age) + current_origin_context_inout = fresh_origin_context; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool try_update_enote_spent_context_v1(const SpEnoteSpentContextV1 &fresh_spent_context, + SpEnoteSpentContextV1 ¤t_spent_context_inout) +{ + // 1. fail if the current context is older than the fresh one + if (is_older_than(current_spent_context_inout, fresh_spent_context)) + return false; + + // 2. overwrite with the fresh context (do this even if the fresh one seems to have the same age) + current_spent_context_inout = fresh_spent_context; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool try_update_contextual_enote_record_spent_context_v1(const SpContextualKeyImageSetV1 &contextual_key_image_set, + SpContextualEnoteRecordV1 &contextual_enote_record_inout) +{ + // 1. fail if our record doesn't have a key image in the set + if (!has_key_image(contextual_key_image_set, key_image_ref(contextual_enote_record_inout))) + return false; + + // 2. try to update the record's spent context + if (!try_update_enote_spent_context_v1(contextual_key_image_set.spent_context, + contextual_enote_record_inout.spent_context)) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +SpEnoteOriginStatus origin_status_from_spent_status_v1(const SpEnoteSpentStatus spent_status) +{ + switch (spent_status) + { + case (SpEnoteSpentStatus::UNSPENT) : + case (SpEnoteSpentStatus::SPENT_OFFCHAIN) : + return SpEnoteOriginStatus::OFFCHAIN; + + case (SpEnoteSpentStatus::SPENT_UNCONFIRMED) : + return SpEnoteOriginStatus::UNCONFIRMED; + + case (SpEnoteSpentStatus::SPENT_ONCHAIN) : + return SpEnoteOriginStatus::ONCHAIN; + + default : + return SpEnoteOriginStatus::OFFCHAIN; + } +} +//------------------------------------------------------------------------------------------------------------------- +bool try_bump_enote_record_origin_status_v1(const SpEnoteSpentStatus spent_status, + SpEnoteOriginStatus &origin_status_inout) +{ + // 1. get the implied origin status + const SpEnoteOriginStatus implied_origin_status{origin_status_from_spent_status_v1(spent_status)}; + + // 2. check if our existing origin status is older than the new implied one + if (origin_status_inout > implied_origin_status) + return false; + + // 3. bump our origin status + origin_status_inout = implied_origin_status; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +void update_contextual_enote_record_contexts_v1(const SpEnoteOriginContextV1 &new_origin_context, + const SpEnoteSpentContextV1 &new_spent_context, + SpEnoteOriginContextV1 &origin_context_inout, + SpEnoteSpentContextV1 &spent_context_inout) +{ + // 1. update the origin context + try_update_enote_origin_context_v1(new_origin_context, origin_context_inout); + + // 2. update the spent context + try_update_enote_spent_context_v1(new_spent_context, spent_context_inout); + + // 3. bump the origin status based on the new spent status + try_bump_enote_record_origin_status_v1(spent_context_inout.spent_status, origin_context_inout.origin_status); +} +//------------------------------------------------------------------------------------------------------------------- +void update_contextual_enote_record_contexts_v1(const SpContextualEnoteRecordV1 &fresh_record, + SpContextualEnoteRecordV1 &existing_record_inout) +{ + CHECK_AND_ASSERT_THROW_MES(fresh_record.record.key_image == existing_record_inout.record.key_image, + "updating a contextual enote record: the fresh record doesn't represent the same enote."); + + update_contextual_enote_record_contexts_v1(fresh_record.origin_context, + fresh_record.spent_context, + existing_record_inout.origin_context, + existing_record_inout.spent_context); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace sp diff --git a/src/seraphis_main/contextual_enote_record_utils.h b/src/seraphis_main/contextual_enote_record_utils.h new file mode 100644 index 0000000000..a64a6f1d75 --- /dev/null +++ b/src/seraphis_main/contextual_enote_record_utils.h @@ -0,0 +1,173 @@ +// Copyright (c) 2022, 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 interacting with contextual enote records. + +#pragma once + +//local headers +#include "contextual_enote_record_types.h" +#include "ringct/rctTypes.h" +#include "tx_input_selection.h" + +//third party headers +#include "boost/multiprecision/cpp_int.hpp" + +//standard headers +#include +#include +#include + +//forward declarations + + +namespace sp +{ + +/** +* brief: onchain_legacy_enote_is_locked - check if an on-chain legacy enote is locked (can't be spent) +* param: enote_origin_block_index - +* param: enote_unlock_time - +* param: top_block_index - +* param: default_spendable_age - +* param: current_time - +* return: true if the enote is locked +*/ +bool onchain_legacy_enote_is_locked(const std::uint64_t enote_origin_block_index, + const std::uint64_t enote_unlock_time, + const std::uint64_t top_block_index, + const std::uint64_t default_spendable_age, + const std::uint64_t current_time); +/** +* brief: onchain_sp_enote_is_locked - check if an on-chain seraphis enote is locked (can't be spent) +* param: enote_origin_block_index - +* param: top_block_index - +* param: default_spendable_age - +* return: true if the enote is locked +*/ +bool onchain_sp_enote_is_locked(const std::uint64_t enote_origin_block_index, + const std::uint64_t top_block_index, + const std::uint64_t default_spendable_age); +/** +* brief: legacy_enote_has_highest_amount_in_set - check if a specified legacy enote has the highest amount in +* a set of legacy enotes (e.g. a set of legacy enotes with the same onetime address) +* - note: it is fine if identifiers in the set have the same amount +* param: specified_enote_identifier - +* param: specified_enote_amount - +* param: allowed_origin_statuses - +* param: enote_identifier_set - +* param: get_record_origin_status_for_identifier_func - return an origin status associated with a specified identifier +* param: get_record_amount_for_identifier_func - return an amount associated with a specified identifier +* return: true if the specified legacy enote has the highest amount in the requested set +*/ +bool legacy_enote_has_highest_amount_in_set(const rct::key &specified_enote_identifier, + const rct::xmr_amount specified_enote_amount, + const std::unordered_set &allowed_origin_statuses, + const std::unordered_set &enote_identifier_set, + const std::function &get_record_origin_status_for_identifier_func, + const std::function &get_record_amount_for_identifier_func); +/** +* brief: split_selected_input_set - split an input set tracker into legacy and seraphis contextual records +* param: input_set - +* outparam: legacy_contextual_records_out - +* outparam: sp_contextual_records_out - +*/ +void split_selected_input_set(const input_set_tracker_t &input_set, + std::vector &legacy_contextual_records_out, + std::vector &sp_contextual_records_out); +/** +* brief: total_amount - get the total amount in a set of contextual records +* param: contextual_records - +* return: the sum of amounts in the records +*/ +boost::multiprecision::uint128_t total_amount(const std::vector &contextual_records); +boost::multiprecision::uint128_t total_amount(const std::vector &contextual_records); +/** +* brief: try_get_membership_proof_real_reference_mappings - map a set of records' key images to the on-chain enote indices +* of those records' enotes (useful for when making membership proofs) +* param: contextual_records - +* outparam: enote_ledger_mappings_out - +*/ +bool try_get_membership_proof_real_reference_mappings(const std::vector &contextual_records, + std::unordered_map &enote_ledger_mappings_out); +bool try_get_membership_proof_real_reference_mappings(const std::vector &contextual_records, + std::unordered_map &enote_ledger_mappings_out); +/** +* brief: try_update_enote_origin_context_v1 - try to update an origin context with another origin context +* - only update our origin context if the fresh context is 'older than' our origin context +* param: fresh_origin_context - +* inoutparam: current_origin_context_inout - +*/ +bool try_update_enote_origin_context_v1(const SpEnoteOriginContextV1 &fresh_origin_context, + SpEnoteOriginContextV1 ¤t_origin_context_inout); +/** +* brief: try_update_enote_spent_context_v1 - try to update a spent context with another spent context +* - only update our spent context if the fresh context is 'older than' our spent context +* param: fresh_spent_context - +* inoutparam: current_spent_context_inout - +*/ +bool try_update_enote_spent_context_v1(const SpEnoteSpentContextV1 &fresh_spent_context, + SpEnoteSpentContextV1 ¤t_spent_context_inout); +/** +* brief: try_update_contextual_enote_record_spent_context_v1 - try to update the spent context of a contextual record +* with the spent context of a contextual key image set if the record's key image exists in that set +* param: contextual_key_image_set - +* inoutparam: contextual_enote_record_inout - +*/ +bool try_update_contextual_enote_record_spent_context_v1(const SpContextualKeyImageSetV1 &contextual_key_image_set, + SpContextualEnoteRecordV1 &contextual_enote_record_inout); +/** +* brief: origin_status_from_spent_status_v1 - infer an origin status from a spent status +* - i.e. if an enote is spent on-chain, then it must originate on-chain +* param: spent_status - +* return: inferred origin status +*/ +SpEnoteOriginStatus origin_status_from_spent_status_v1(const SpEnoteSpentStatus spent_status); +/** +* brief: try_bump_enote_record_origin_status_v1 - 'bump up' an origin status if lower than the origin status inferred from +* an associated spent status +* param: spent_status - +* inoutparam: origin_status_inout - +*/ +bool try_bump_enote_record_origin_status_v1(const SpEnoteSpentStatus spent_status, + SpEnoteOriginStatus &origin_status_inout); +/** +* brief: update_contextual_enote_record_contexts_v1 - update a pair of origin/spent contexts with new contexts +* param: new_origin_context - +* param: new_spent_context - +* inoutparam: origin_context_inout - +* inoutparam: spent_context_inout - +*/ +void update_contextual_enote_record_contexts_v1(const SpEnoteOriginContextV1 &new_origin_context, + const SpEnoteSpentContextV1 &new_spent_context, + SpEnoteOriginContextV1 &origin_context_inout, + SpEnoteSpentContextV1 &spent_context_inout); +void update_contextual_enote_record_contexts_v1(const SpContextualEnoteRecordV1 &fresh_record, + SpContextualEnoteRecordV1 &existing_record_inout); + +} //namespace sp diff --git a/src/seraphis_main/enote_finding_context.h b/src/seraphis_main/enote_finding_context.h new file mode 100644 index 0000000000..b1c10dfc89 --- /dev/null +++ b/src/seraphis_main/enote_finding_context.h @@ -0,0 +1,87 @@ +// Copyright (c) 2022, 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. + +// Dependency injectors for the find-received step of enote scanning. Intended to be stateless. + +#pragma once + +//local headers +#include "seraphis_main/scan_core_types.h" +#include "seraphis_main/scan_ledger_chunk.h" + +//third party headers + +//standard headers +#include + +//forward declarations + + +namespace sp +{ + +//// +// EnoteFindingContextNonLedger +// - wraps a nonledger context of some kind, produces chunks of potentially owned enotes (from find-received scanning) +/// +class EnoteFindingContextNonLedger +{ +public: +//destructor + virtual ~EnoteFindingContextNonLedger() = default; + +//overloaded operators + /// disable copy/move (this is a virtual base class) + EnoteFindingContextNonLedger& operator=(EnoteFindingContextNonLedger&&) = delete; + +//member functions + /// get a fresh nonledger chunk (this is expected to contain all enotes in the nonledger context) + virtual void get_nonledger_chunk(scanning::ChunkData &chunk_out) const = 0; +}; + +//// +// EnoteFindingContextLedger +// - wraps a ledger context of some kind, produces chunks of potentially owned enotes (from find-received scanning) +/// +class EnoteFindingContextLedger +{ +public: +//destructor + virtual ~EnoteFindingContextLedger() = default; + +//overloaded operators + /// disable copy/move (this is a virtual base class) + EnoteFindingContextLedger& operator=(EnoteFindingContextLedger&&) = delete; + +//member functions + /// get an onchain chunk (or empty chunk representing top of current chain) + virtual std::unique_ptr get_onchain_chunk(const std::uint64_t chunk_start_index, + const std::uint64_t chunk_max_size) const = 0; +}; + +} //namespace sp diff --git a/src/seraphis_main/enote_record_types.h b/src/seraphis_main/enote_record_types.h new file mode 100644 index 0000000000..a23b077003 --- /dev/null +++ b/src/seraphis_main/enote_record_types.h @@ -0,0 +1,196 @@ +// Copyright (c) 2022, 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. + +// Records of seraphis enotes owned by some wallet. + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "crypto/x25519.h" +#include "cryptonote_basic/subaddress_index.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/jamtis_support_types.h" +#include "seraphis_core/legacy_enote_types.h" +#include "tx_component_types.h" + +//third party headers +#include + +//standard headers +#include + +//forward declarations + + +namespace sp +{ + +//////////////////////////////////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////// Legacy //////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////// + +//// +// LegacyBasicEnoteRecord +// - a cryptonote/ringct enote that has been identified as owned with view-key scanning +/// +struct LegacyBasicEnoteRecord final +{ + /// original enote + LegacyEnoteVariant enote; + /// the enote's ephemeral pubkey + rct::key enote_ephemeral_pubkey; + /// i: legacy address index (if true, then it's owned by a subaddress) + boost::optional address_index; + /// t: the enote's index in its transaction + std::uint64_t tx_output_index; + /// u: the enote's unlock time + std::uint64_t unlock_time; +}; + +//// +// LegacyIntermediateEnoteRecord +// - a cryptonote/ringct enote that has been view-key scanned +/// +struct LegacyIntermediateEnoteRecord final +{ + /// original enote + LegacyEnoteVariant enote; + /// the enote's ephemeral pubkey + rct::key enote_ephemeral_pubkey; + /// enote view privkey = [address: Hn(r K^v, t)] [subaddress (i): Hn(r K^{v,i}, t) + Hn(k^v, i)] + crypto::secret_key enote_view_extension; + /// a: amount + rct::xmr_amount amount; + /// x: amount blinding factor + crypto::secret_key amount_blinding_factor; + /// i: legacy address index (if true, then it's owned by a subaddress) + boost::optional address_index; + /// t: the enote's index in its transaction + std::uint64_t tx_output_index; + /// u: the enote's unlock time + std::uint64_t unlock_time; +}; + +//// +// LegacyEnoteRecord +// - a cryptonote/ringct enote that has been view-key scanned + key image computed +/// +struct LegacyEnoteRecord final +{ + /// original enote + LegacyEnoteVariant enote; + /// the enote's ephemeral pubkey + rct::key enote_ephemeral_pubkey; + /// enote view privkey = [address: Hn(r K^v, t)] [subaddress (i): Hn(r K^{v,i}, t) + Hn(k^v, i)] + crypto::secret_key enote_view_extension; + /// a: amount + rct::xmr_amount amount; + /// x: amount blinding factor + crypto::secret_key amount_blinding_factor; + /// KI: key image + crypto::key_image key_image; + /// i: legacy address index (if true, then it's owned by a subaddress) + boost::optional address_index; + /// t: the enote's index in its transaction + std::uint64_t tx_output_index; + /// u: the enote's unlock time + std::uint64_t unlock_time; +}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////// Seraphis /////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////// + +//// +// SpBasicEnoteRecordV1 (jamtis non-selfsend enote type only) +// - a seraphis enote that has passed the view-tag check using a jamtis find-received key +/// +struct SpBasicEnoteRecordV1 final +{ + /// original enote + SpEnoteVariant enote; + /// the enote's ephemeral pubkey + crypto::x25519_pubkey enote_ephemeral_pubkey; + /// context of the tx input(s) associated with this enote + rct::key input_context; + /// t'_addr: nominal address tag + jamtis::address_tag_t nominal_address_tag; +}; + +//// +// SpIntermediateEnoteRecordV1 (jamtis non-selfsend enote type only) +// - a seraphis enote with info extracted using a jamtis find-received key, generate-address secret, and unlock-amounts key +/// +struct SpIntermediateEnoteRecordV1 final +{ + /// original enote + SpEnoteVariant enote; + /// the enote's ephemeral pubkey + crypto::x25519_pubkey enote_ephemeral_pubkey; + /// context of the tx input(s) associated with this enote + rct::key input_context; + /// a: amount + rct::xmr_amount amount; + /// x: amount blinding factor + crypto::secret_key amount_blinding_factor; + /// j: jamtis address index + jamtis::address_index_t address_index; +}; + +//// +// SpEnoteRecordV1 (all jamtis enote types) +// - a seraphis enote that has been fully view-scanned with a jamtis view-balance key +/// +struct SpEnoteRecordV1 final +{ + /// original enote + SpEnoteVariant enote; + /// the enote's ephemeral pubkey + crypto::x25519_pubkey enote_ephemeral_pubkey; + /// context of the tx input(s) associated with this enote + rct::key input_context; + /// k_{g, sender} + k_{g, address}: enote view extension for G component + crypto::secret_key enote_view_extension_g; + /// k_{x, sender} + k_{x, address}: enote view extension for X component (excludes k_vb) + crypto::secret_key enote_view_extension_x; + /// k_{u, sender} + k_{u, address}: enote view extension for U component (excludes k_m) + crypto::secret_key enote_view_extension_u; + /// a: amount + rct::xmr_amount amount; + /// x: amount blinding factor + crypto::secret_key amount_blinding_factor; + /// KI: key image + crypto::key_image key_image; + /// j: jamtis address index + jamtis::address_index_t address_index; + /// jamtis enote type + jamtis::JamtisEnoteType type; +}; + +} //namespace sp diff --git a/src/seraphis_main/enote_record_utils.cpp b/src/seraphis_main/enote_record_utils.cpp new file mode 100644 index 0000000000..5fdd6ca72b --- /dev/null +++ b/src/seraphis_main/enote_record_utils.cpp @@ -0,0 +1,952 @@ +// Copyright (c) 2022, 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_record_utils.h" + +//local headers +#include "crypto/crypto.h" +#include "crypto/x25519.h" +extern "C" +{ +#include "crypto/crypto-ops.h" +} +#include "enote_record_types.h" +#include "ringct/rctOps.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/jamtis_address_tag_utils.h" +#include "seraphis_core/jamtis_address_utils.h" +#include "seraphis_core/jamtis_core_utils.h" +#include "seraphis_core/jamtis_enote_utils.h" +#include "seraphis_core/jamtis_support_types.h" +#include "seraphis_core/sp_core_enote_utils.h" +#include "seraphis_crypto/sp_crypto_utils.h" +#include "tx_component_types.h" + +//third party headers + +//standard headers + + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis" + +namespace sp +{ +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void make_enote_view_extension_g_helper(const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &s_generate_address, + const jamtis::address_index_t &j, + const rct::key &recipient_address_spendkey, //K_1 + const rct::key &sender_receiver_secret, + const rct::key &amount_commitment, + crypto::secret_key &enote_view_extension_g_out) +{ + // enote view privkey extension on g: k_g = k^o_g + k^j_g + crypto::secret_key spendkey_extension_g; //k^j_g + crypto::secret_key sender_extension_g; //k^o_g + jamtis::make_jamtis_spendkey_extension_g(jamtis_spend_pubkey, s_generate_address, j, spendkey_extension_g); + jamtis::make_jamtis_onetime_address_extension_g(recipient_address_spendkey, + sender_receiver_secret, + amount_commitment, + sender_extension_g); + + // k_g = k^o_g + k^j_g + sc_add(to_bytes(enote_view_extension_g_out), to_bytes(sender_extension_g), to_bytes(spendkey_extension_g)); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void make_enote_view_extension_x_helper(const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &s_generate_address, + const jamtis::address_index_t &j, + const rct::key &recipient_address_spendkey, //K_1 + const rct::key &sender_receiver_secret, + const rct::key &amount_commitment, + crypto::secret_key &enote_view_extension_x_out) +{ + // enote view privkey extension on x: k_x = k^o_x + k^j_x + crypto::secret_key spendkey_extension_x; //k^j_x + crypto::secret_key sender_extension_x; //k^o_x + jamtis::make_jamtis_spendkey_extension_x(jamtis_spend_pubkey, s_generate_address, j, spendkey_extension_x); + jamtis::make_jamtis_onetime_address_extension_x(recipient_address_spendkey, + sender_receiver_secret, + amount_commitment, + sender_extension_x); + + // k_x = k^o_x + k^j_x + sc_add(to_bytes(enote_view_extension_x_out), to_bytes(sender_extension_x), to_bytes(spendkey_extension_x)); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void make_enote_view_extension_u_helper(const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &s_generate_address, + const jamtis::address_index_t &j, + const rct::key &recipient_address_spendkey, //K_1 + const rct::key &sender_receiver_secret, + const rct::key &amount_commitment, + crypto::secret_key &enote_view_extension_u_out) +{ + // enote view privkey extension on u: k_u = k^o_u + k^j_u + crypto::secret_key spendkey_extension_u; //k^j_u + crypto::secret_key sender_extension_u; //k^o_u + jamtis::make_jamtis_spendkey_extension_u(jamtis_spend_pubkey, s_generate_address, j, spendkey_extension_u); + jamtis::make_jamtis_onetime_address_extension_u(recipient_address_spendkey, + sender_receiver_secret, + amount_commitment, + sender_extension_u); + + // k_u = k^o_u + k^j_u + sc_add(to_bytes(enote_view_extension_u_out), to_bytes(sender_extension_u), to_bytes(spendkey_extension_u)); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void make_enote_view_extensions_helper(const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &s_generate_address, + const jamtis::address_index_t &j, + const rct::key &recipient_address_spendkey, //K_1 + const rct::key &sender_receiver_secret, + const rct::key &amount_commitment, + crypto::secret_key &enote_view_extension_g_out, + crypto::secret_key &enote_view_extension_x_out, + crypto::secret_key &enote_view_extension_u_out) +{ + // 1. construct the enote view privkey for the G component: k_g = k^o_g + k^j_g + make_enote_view_extension_g_helper(jamtis_spend_pubkey, + s_generate_address, + j, + recipient_address_spendkey, + sender_receiver_secret, + amount_commitment, + enote_view_extension_g_out); + + // 2. construct the enote view privkey for the X component: k_x = k^o_x + k^j_x + make_enote_view_extension_x_helper(jamtis_spend_pubkey, + s_generate_address, + j, + recipient_address_spendkey, + sender_receiver_secret, + amount_commitment, + enote_view_extension_x_out); + + // 3. construct the enote view privkey for the U component: k_u = k^o_u + k^j_u + make_enote_view_extension_u_helper(jamtis_spend_pubkey, + s_generate_address, + j, + recipient_address_spendkey, + sender_receiver_secret, + amount_commitment, + enote_view_extension_u_out); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void make_seraphis_key_image_helper(const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + const crypto::secret_key &enote_view_extension_x, + const crypto::secret_key &enote_view_extension_u, + crypto::key_image &key_image_out) +{ + // make key image: (k_u + k_m)/(k_x + k_vb) U + rct::key spend_pubkey_U_component{jamtis_spend_pubkey}; //k_vb X + k_m U + reduce_seraphis_spendkey_x(k_view_balance, spend_pubkey_U_component); //k_m U + extend_seraphis_spendkey_u(enote_view_extension_u, spend_pubkey_U_component); //(k_u + k_m) U + make_seraphis_key_image(add_secrets(enote_view_extension_x, k_view_balance), + rct::rct2pk(spend_pubkey_U_component), + key_image_out); //(k_u + k_m)/(k_x + k_vb) U +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool try_get_amount_commitment_information_plaintext(const rct::xmr_amount &enote_amount, + rct::xmr_amount &amount_out, + crypto::secret_key &amount_blinding_factor_out) +{ + amount_out = enote_amount; + amount_blinding_factor_out = rct::rct2sk(rct::I); + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool try_get_amount_commitment_information(const SpEnoteVariant &enote, + const rct::key &sender_receiver_secret, + const rct::key &amount_baked_key, + rct::xmr_amount &amount_out, + crypto::secret_key &amount_blinding_factor_out) +{ + if (const SpCoinbaseEnoteV1 *enote_ptr = enote.try_unwrap()) + { + return try_get_amount_commitment_information_plaintext(enote_ptr->core.amount, + amount_out, + amount_blinding_factor_out); + } + else if (const SpEnoteV1 *enote_ptr = enote.try_unwrap()) + { + return jamtis::try_get_jamtis_amount(sender_receiver_secret, + amount_baked_key, + enote_ptr->core.amount_commitment, + enote_ptr->encoded_amount, + amount_out, + amount_blinding_factor_out); + } + else + CHECK_AND_ASSERT_THROW_MES(false, "try get amount commitment information: unknown enote type."); + + return false; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool try_get_basic_record_info_v1_helper(const SpEnoteVariant &enote, + const crypto::x25519_pubkey &enote_ephemeral_pubkey, + const rct::key &input_context, + const crypto::x25519_pubkey &sender_receiver_DH_derivation, + rct::key &nominal_sender_receiver_secret_out, + jamtis::address_tag_t &nominal_address_tag_out) +{ + // 1. q' (jamtis plain enote type) + if (!jamtis::try_get_jamtis_sender_receiver_secret_plain(sender_receiver_DH_derivation, + enote_ephemeral_pubkey, + input_context, + onetime_address_ref(enote), + view_tag_ref(enote), + nominal_sender_receiver_secret_out)) + return false; + + // 2. t'_addr + nominal_address_tag_out = jamtis::decrypt_address_tag(nominal_sender_receiver_secret_out, + onetime_address_ref(enote), + addr_tag_enc_ref(enote)); + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool try_get_basic_record_info_v1_helper(const SpEnoteVariant &enote, + const crypto::x25519_pubkey &enote_ephemeral_pubkey, + const rct::key &input_context, + const crypto::x25519_secret_key &xk_find_received, + rct::key &nominal_sender_receiver_secret_out, + jamtis::address_tag_t &nominal_address_tag_out) +{ + // xK_d = xk_fr * xK_e + crypto::x25519_pubkey sender_receiver_DH_derivation; + crypto::x25519_scmul_key(xk_find_received, enote_ephemeral_pubkey, sender_receiver_DH_derivation); + + return try_get_basic_record_info_v1_helper(enote, + enote_ephemeral_pubkey, + input_context, + sender_receiver_DH_derivation, + nominal_sender_receiver_secret_out, + nominal_address_tag_out); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool try_handle_basic_record_info_v1_helper(const SpEnoteVariant &enote, + const crypto::x25519_pubkey &enote_ephemeral_pubkey, + const rct::key &input_context, + const jamtis::address_tag_t &nominal_address_tag, + const crypto::x25519_secret_key &xk_find_received, + const jamtis::jamtis_address_tag_cipher_context &cipher_context, + jamtis::address_index_t &nominal_address_index_out, + rct::key &nominal_sender_receiver_secret_out) +{ + // use basic record info to try and get the nominal address index and recover the nominal sender-receiver secret + + // 1. j' + if (!jamtis::try_decipher_address_index(cipher_context, nominal_address_tag, nominal_address_index_out)) + return false; + + // 2. xK_d = xk_fr * xK_e + crypto::x25519_pubkey sender_receiver_DH_derivation; + crypto::x25519_scmul_key(xk_find_received, enote_ephemeral_pubkey, sender_receiver_DH_derivation); + + // 3. q' (jamtis plain enote type) + if (!jamtis::try_get_jamtis_sender_receiver_secret_plain(sender_receiver_DH_derivation, + enote_ephemeral_pubkey, + input_context, + onetime_address_ref(enote), + view_tag_ref(enote), + nominal_sender_receiver_secret_out)) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool try_get_intermediate_record_info_v1_helper(const SpEnoteVariant &enote, + const crypto::x25519_pubkey &enote_ephemeral_pubkey, + const jamtis::address_index_t &nominal_address_index, + const rct::key &nominal_sender_receiver_secret, + const rct::key &jamtis_spend_pubkey, + const crypto::x25519_secret_key &xk_unlock_amounts, + const crypto::secret_key &s_generate_address, + rct::key &recipient_address_spendkey_out, + rct::xmr_amount &amount_out, + crypto::secret_key &amount_blinding_factor_out) +{ + // get intermediate info (validate address index, amount, amount blinding factor) for a plain jamtis enote + + // 1. spend key of address that might own this enote + jamtis::make_jamtis_address_spend_key(jamtis_spend_pubkey, + s_generate_address, + nominal_address_index, + recipient_address_spendkey_out); + + // 2. check if the spend key owns this enote + if (!jamtis::test_jamtis_onetime_address(recipient_address_spendkey_out, + nominal_sender_receiver_secret, + amount_commitment_ref(enote), + onetime_address_ref(enote))) + return false; + + // 3. make the amount commitment baked key + crypto::x25519_secret_key address_privkey; + jamtis::make_jamtis_address_privkey(jamtis_spend_pubkey, s_generate_address, nominal_address_index, address_privkey); + + rct::key amount_baked_key; + jamtis::make_jamtis_amount_baked_key_plain_recipient(address_privkey, + xk_unlock_amounts, + enote_ephemeral_pubkey, + amount_baked_key); + + // 4. try to recover the amount and amount blinding factor + if (!try_get_amount_commitment_information(enote, + nominal_sender_receiver_secret, + amount_baked_key, + amount_out, + amount_blinding_factor_out)) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void get_final_record_info_v1_helper(const rct::key &sender_receiver_secret, + const rct::key &amount_commitment, + const jamtis::address_index_t &j, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + const crypto::secret_key &s_generate_address, + const rct::key &recipient_address_spendkey, + crypto::secret_key &enote_view_extension_g_out, + crypto::secret_key &enote_view_extension_x_out, + crypto::secret_key &enote_view_extension_u_out, + crypto::key_image &key_image_out) +{ + // get final info (enote view privkey, key image) + + // 1. construct the enote view extensions + make_enote_view_extensions_helper(jamtis_spend_pubkey, + s_generate_address, + j, + recipient_address_spendkey, + sender_receiver_secret, + amount_commitment, + enote_view_extension_g_out, + enote_view_extension_x_out, + enote_view_extension_u_out); + + // 2. make the key image: (k_u + k_m)/(k_x + k_vb) U + make_seraphis_key_image_helper(jamtis_spend_pubkey, + k_view_balance, + enote_view_extension_x_out, + enote_view_extension_u_out, + key_image_out); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool try_get_intermediate_enote_record_v1_finalize(const SpEnoteVariant &enote, + const crypto::x25519_pubkey &enote_ephemeral_pubkey, + const rct::key &input_context, + const jamtis::address_index_t &nominal_address_index, + const rct::key &nominal_sender_receiver_secret, + const rct::key &jamtis_spend_pubkey, + const crypto::x25519_secret_key &xk_unlock_amounts, + const crypto::secret_key &s_generate_address, + SpIntermediateEnoteRecordV1 &record_out) +{ + // finalize an intermediate enote record + + // 1. get intermediate info: address spendkey, amount and amount blinding factor + rct::key recipient_address_spendkey_temp; + if (!try_get_intermediate_record_info_v1_helper(enote, + enote_ephemeral_pubkey, + nominal_address_index, + nominal_sender_receiver_secret, + jamtis_spend_pubkey, + xk_unlock_amounts, + s_generate_address, + recipient_address_spendkey_temp, + record_out.amount, + record_out.amount_blinding_factor)) + return false; + + // 2. record the enote and sender-receiver secret + record_out.enote = enote; + record_out.enote_ephemeral_pubkey = enote_ephemeral_pubkey; + record_out.input_context = input_context; + record_out.address_index = nominal_address_index; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool try_get_enote_record_v1_plain_finalize(const SpEnoteVariant &enote, + const crypto::x25519_pubkey &enote_ephemeral_pubkey, + const rct::key &input_context, + const jamtis::address_index_t &nominal_address_index, + const rct::key &nominal_sender_receiver_secret, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + const crypto::x25519_secret_key &xk_unlock_amounts, + const crypto::secret_key &s_generate_address, + SpEnoteRecordV1 &record_out) +{ + // finalize an enote record + + // 1. get intermediate info: address spendkey, amount and amount blinding factor + rct::key recipient_address_spendkey_temp; + if (!try_get_intermediate_record_info_v1_helper(enote, + enote_ephemeral_pubkey, + nominal_address_index, + nominal_sender_receiver_secret, + jamtis_spend_pubkey, + xk_unlock_amounts, + s_generate_address, + recipient_address_spendkey_temp, + record_out.amount, + record_out.amount_blinding_factor)) + return false; + + // 2. get final info: enote view extensions, key image + get_final_record_info_v1_helper(nominal_sender_receiver_secret, + amount_commitment_ref(enote), + nominal_address_index, + jamtis_spend_pubkey, + k_view_balance, + s_generate_address, + recipient_address_spendkey_temp, + record_out.enote_view_extension_g, + record_out.enote_view_extension_x, + record_out.enote_view_extension_u, + record_out.key_image); + + // 3. record the remaining information + record_out.enote = enote; + record_out.enote_ephemeral_pubkey = enote_ephemeral_pubkey; + record_out.input_context = input_context; + record_out.address_index = nominal_address_index; + record_out.type = jamtis::JamtisEnoteType::PLAIN; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool try_get_enote_record_v1_selfsend_for_type(const SpEnoteVariant &enote, + const crypto::x25519_pubkey &enote_ephemeral_pubkey, + const rct::key &input_context, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + const crypto::x25519_secret_key &xk_find_received, + const crypto::secret_key &s_generate_address, + const jamtis::jamtis_address_tag_cipher_context &cipher_context, + const jamtis::JamtisSelfSendType test_type, + SpEnoteRecordV1 &record_out) +{ + // get an enote record for a specified jamtis selfsend enote type + + // 1. sender-receiver secret for specified self-send type + rct::key q; + jamtis::make_jamtis_sender_receiver_secret_selfsend(k_view_balance, + enote_ephemeral_pubkey, + input_context, + test_type, + q); + + // 2. decrypt encrypted address tag + const jamtis::address_tag_t decrypted_addr_tag{ + decrypt_address_tag(q, onetime_address_ref(enote), addr_tag_enc_ref(enote)) + }; + + // 3. try to get the address index + if (!jamtis::try_decipher_address_index(cipher_context, decrypted_addr_tag, record_out.address_index)) + return false; + + // 4. verify the view tag + // note: we verify the view tag to ensure our enote is 100% well-formed, even though we use the address index + // decipher as the main test for identifying self-sends + jamtis::view_tag_t test_view_tag; + jamtis::make_jamtis_view_tag(xk_find_received, enote_ephemeral_pubkey, onetime_address_ref(enote), test_view_tag); + if (test_view_tag != view_tag_ref(enote)) + return false; + + // 5. spend key of address that might own this enote + rct::key recipient_address_spendkey; + jamtis::make_jamtis_address_spend_key(jamtis_spend_pubkey, + s_generate_address, + record_out.address_index, + recipient_address_spendkey); + + // 6. save a copy of the amount commitment + const rct::key amount_commitment{amount_commitment_ref(enote)}; + + // 7. check if the spend key owns this enote + if (!jamtis::test_jamtis_onetime_address(recipient_address_spendkey, + q, + amount_commitment, + onetime_address_ref(enote))) + return false; + + // 8. compute the amount baked key (selfsend version) + rct::key amount_baked_key; + jamtis::make_jamtis_amount_baked_key_selfsend(k_view_balance, q, amount_baked_key); + + // 9. try to recover the amount and blinding factor + if (!try_get_amount_commitment_information(enote, + q, + amount_baked_key, + record_out.amount, + record_out.amount_blinding_factor)) + return false; + + // 10. construct enote view extensions + make_enote_view_extensions_helper(jamtis_spend_pubkey, + s_generate_address, + record_out.address_index, + recipient_address_spendkey, + q, + amount_commitment, + record_out.enote_view_extension_g, + record_out.enote_view_extension_x, + record_out.enote_view_extension_u); + + // 11. make key image: (k_u + k_m)/(k_x + k_vb) U + make_seraphis_key_image_helper(jamtis_spend_pubkey, + k_view_balance, + record_out.enote_view_extension_x, + record_out.enote_view_extension_u, + record_out.key_image); + + // 12. record the remaining information + record_out.enote = enote; + record_out.enote_ephemeral_pubkey = enote_ephemeral_pubkey; + record_out.input_context = input_context; + CHECK_AND_ASSERT_THROW_MES(jamtis::try_get_jamtis_enote_type(test_type, record_out.type), + "getting self-send enote record (v1): could not convert self-send type to enote type (bug)."); + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +bool try_get_basic_enote_record_v1(const SpEnoteVariant &enote, + const crypto::x25519_pubkey &enote_ephemeral_pubkey, + const rct::key &input_context, + const crypto::x25519_pubkey &sender_receiver_DH_derivation, + SpBasicEnoteRecordV1 &basic_record_out) +{ + // get a basic record + + // 1. try to decrypt the address tag + rct::key dummy_q; + if (!try_get_basic_record_info_v1_helper(enote, + enote_ephemeral_pubkey, + input_context, + sender_receiver_DH_derivation, + dummy_q, + basic_record_out.nominal_address_tag)) + return false; + + // 2. copy remaining information + basic_record_out.enote = enote; + basic_record_out.enote_ephemeral_pubkey = enote_ephemeral_pubkey; + basic_record_out.input_context = input_context; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool try_get_basic_enote_record_v1(const SpEnoteVariant &enote, + const crypto::x25519_pubkey &enote_ephemeral_pubkey, + const rct::key &input_context, + const crypto::x25519_secret_key &xk_find_received, + SpBasicEnoteRecordV1 &basic_record_out) +{ + // compute DH derivation then get basic record + + // sender-receiver DH derivation + crypto::x25519_pubkey sender_receiver_DH_derivation; + crypto::x25519_scmul_key(xk_find_received, enote_ephemeral_pubkey, sender_receiver_DH_derivation); + + return try_get_basic_enote_record_v1(enote, + enote_ephemeral_pubkey, + input_context, + sender_receiver_DH_derivation, + basic_record_out); +} +//------------------------------------------------------------------------------------------------------------------- +bool try_get_intermediate_enote_record_v1(const SpEnoteVariant &enote, + const crypto::x25519_pubkey &enote_ephemeral_pubkey, + const rct::key &input_context, + const rct::key &jamtis_spend_pubkey, + const crypto::x25519_secret_key &xk_unlock_amounts, + const crypto::x25519_secret_key &xk_find_received, + const crypto::secret_key &s_generate_address, + const jamtis::jamtis_address_tag_cipher_context &cipher_context, + SpIntermediateEnoteRecordV1 &record_out) +{ + // try to process basic info then get an intermediate record + + // 1. q' and addr_tag' + rct::key nominal_sender_receiver_secret; + jamtis::address_tag_t nominal_address_tag; + if (!try_get_basic_record_info_v1_helper(enote, + enote_ephemeral_pubkey, + input_context, + xk_find_received, + nominal_sender_receiver_secret, + nominal_address_tag)) + return false; + + // 2. j' + jamtis::address_index_t nominal_address_index; + if (!jamtis::try_decipher_address_index(cipher_context, nominal_address_tag, nominal_address_index)) + return false; + + // 3. try to finalize the intermediate enote record + if (!try_get_intermediate_enote_record_v1_finalize(enote, + enote_ephemeral_pubkey, + input_context, + nominal_address_index, + nominal_sender_receiver_secret, + jamtis_spend_pubkey, + xk_unlock_amounts, + s_generate_address, + record_out)) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool try_get_intermediate_enote_record_v1(const SpEnoteVariant &enote, + const crypto::x25519_pubkey &enote_ephemeral_pubkey, + const rct::key &input_context, + const rct::key &jamtis_spend_pubkey, + const crypto::x25519_secret_key &xk_unlock_amounts, + const crypto::x25519_secret_key &xk_find_received, + const crypto::secret_key &s_generate_address, + SpIntermediateEnoteRecordV1 &record_out) +{ + // get cipher context then get an intermediate record + crypto::secret_key s_cipher_tag; + jamtis::make_jamtis_ciphertag_secret(s_generate_address, s_cipher_tag); + + const jamtis::jamtis_address_tag_cipher_context cipher_context{s_cipher_tag}; + + return try_get_intermediate_enote_record_v1(enote, + enote_ephemeral_pubkey, + input_context, + jamtis_spend_pubkey, + xk_unlock_amounts, + xk_find_received, + s_generate_address, + cipher_context, + record_out); +} +//------------------------------------------------------------------------------------------------------------------- +bool try_get_intermediate_enote_record_v1(const SpBasicEnoteRecordV1 &basic_record, + const rct::key &jamtis_spend_pubkey, + const crypto::x25519_secret_key &xk_unlock_amounts, + const crypto::x25519_secret_key &xk_find_received, + const crypto::secret_key &s_generate_address, + const jamtis::jamtis_address_tag_cipher_context &cipher_context, + SpIntermediateEnoteRecordV1 &record_out) +{ + // process basic record then get an intermediate record + + // 1. process the basic record + jamtis::address_index_t nominal_address_index; + rct::key nominal_sender_receiver_secret; + if (!try_handle_basic_record_info_v1_helper(basic_record.enote, + basic_record.enote_ephemeral_pubkey, + basic_record.input_context, + basic_record.nominal_address_tag, + xk_find_received, + cipher_context, + nominal_address_index, + nominal_sender_receiver_secret)) + return false; + + // 2. finalize the intermediate record + if (!try_get_intermediate_enote_record_v1_finalize(basic_record.enote, + basic_record.enote_ephemeral_pubkey, + basic_record.input_context, + nominal_address_index, + nominal_sender_receiver_secret, + jamtis_spend_pubkey, + xk_unlock_amounts, + s_generate_address, + record_out)) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool try_get_intermediate_enote_record_v1(const SpBasicEnoteRecordV1 &basic_record, + const rct::key &jamtis_spend_pubkey, + const crypto::x25519_secret_key &xk_unlock_amounts, + const crypto::x25519_secret_key &xk_find_received, + const crypto::secret_key &s_generate_address, + SpIntermediateEnoteRecordV1 &record_out) +{ + // make cipher context then get an intermediate record + crypto::secret_key s_cipher_tag; + jamtis::make_jamtis_ciphertag_secret(s_generate_address, s_cipher_tag); + + const jamtis::jamtis_address_tag_cipher_context cipher_context{s_cipher_tag}; + + return try_get_intermediate_enote_record_v1(basic_record, + jamtis_spend_pubkey, + xk_unlock_amounts, + xk_find_received, + s_generate_address, + cipher_context, + record_out); +} +//------------------------------------------------------------------------------------------------------------------- +bool try_get_enote_record_v1_plain(const SpEnoteVariant &enote, + const crypto::x25519_pubkey &enote_ephemeral_pubkey, + const rct::key &input_context, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + SpEnoteRecordV1 &record_out) +{ + // try to process basic info then get intermediate record + crypto::x25519_secret_key xk_unlock_amounts; + crypto::x25519_secret_key xk_find_received; + crypto::secret_key s_generate_address; + crypto::secret_key s_cipher_tag; + jamtis::make_jamtis_unlockamounts_key(k_view_balance, xk_unlock_amounts); + jamtis::make_jamtis_findreceived_key(k_view_balance, xk_find_received); + jamtis::make_jamtis_generateaddress_secret(k_view_balance, s_generate_address); + jamtis::make_jamtis_ciphertag_secret(s_generate_address, s_cipher_tag); + + const jamtis::jamtis_address_tag_cipher_context cipher_context{s_cipher_tag}; + + // 1. q' and addr_tag' + rct::key nominal_sender_receiver_secret; + jamtis::address_tag_t nominal_address_tag; + if (!try_get_basic_record_info_v1_helper(enote, + enote_ephemeral_pubkey, + input_context, + xk_find_received, + nominal_sender_receiver_secret, + nominal_address_tag)) + return false; + + // 2. j' + jamtis::address_index_t nominal_address_index; + if (!jamtis::try_decipher_address_index(cipher_context, nominal_address_tag, nominal_address_index)) + return false; + + // 3. finalize the enote record + if (!try_get_enote_record_v1_plain_finalize(enote, + enote_ephemeral_pubkey, + input_context, + nominal_address_index, + nominal_sender_receiver_secret, + jamtis_spend_pubkey, + k_view_balance, + xk_unlock_amounts, + s_generate_address, + record_out)) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool try_get_enote_record_v1_plain(const SpBasicEnoteRecordV1 &basic_record, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + const crypto::x25519_secret_key &xk_unlock_amounts, + const crypto::x25519_secret_key &xk_find_received, + const crypto::secret_key &s_generate_address, + const jamtis::jamtis_address_tag_cipher_context &cipher_context, + SpEnoteRecordV1 &record_out) +{ + // process basic record then get an enote record + + // 1. process the basic record + jamtis::address_index_t nominal_address_index; + rct::key nominal_sender_receiver_secret; + if (!try_handle_basic_record_info_v1_helper(basic_record.enote, + basic_record.enote_ephemeral_pubkey, + basic_record.input_context, + basic_record.nominal_address_tag, + xk_find_received, + cipher_context, + nominal_address_index, + nominal_sender_receiver_secret)) + return false; + + // 2. finalize the enote record + if (!try_get_enote_record_v1_plain_finalize(basic_record.enote, + basic_record.enote_ephemeral_pubkey, + basic_record.input_context, + nominal_address_index, + nominal_sender_receiver_secret, + jamtis_spend_pubkey, + k_view_balance, + xk_unlock_amounts, + s_generate_address, + record_out)) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool try_get_enote_record_v1_plain(const SpBasicEnoteRecordV1 &basic_record, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + SpEnoteRecordV1 &record_out) +{ + // make secrets then get enote record + crypto::x25519_secret_key xk_unlock_amounts; + crypto::x25519_secret_key xk_find_received; + crypto::secret_key s_generate_address; + crypto::secret_key s_cipher_tag; + jamtis::make_jamtis_unlockamounts_key(k_view_balance, xk_unlock_amounts); + jamtis::make_jamtis_findreceived_key(k_view_balance, xk_find_received); + jamtis::make_jamtis_generateaddress_secret(k_view_balance, s_generate_address); + jamtis::make_jamtis_ciphertag_secret(s_generate_address, s_cipher_tag); + + const jamtis::jamtis_address_tag_cipher_context cipher_context{s_cipher_tag}; + + return try_get_enote_record_v1_plain(basic_record, + jamtis_spend_pubkey, + k_view_balance, + xk_unlock_amounts, + xk_find_received, + s_generate_address, + cipher_context, + record_out); +} +//------------------------------------------------------------------------------------------------------------------- +bool try_get_enote_record_v1_plain(const SpIntermediateEnoteRecordV1 &intermediate_record, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + SpEnoteRecordV1 &record_out) +{ + return try_get_enote_record_v1_plain(intermediate_record.enote, + intermediate_record.enote_ephemeral_pubkey, + intermediate_record.input_context, + jamtis_spend_pubkey, + k_view_balance, + record_out); +} +//------------------------------------------------------------------------------------------------------------------- +bool try_get_enote_record_v1_selfsend(const SpEnoteVariant &enote, + const crypto::x25519_pubkey &enote_ephemeral_pubkey, + const rct::key &input_context, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + const crypto::x25519_secret_key &xk_find_received, + const crypto::secret_key &s_generate_address, + const jamtis::jamtis_address_tag_cipher_context &cipher_context, + SpEnoteRecordV1 &record_out) +{ + // try to get an enote record with all the self-send types + for (unsigned char self_send_type{0}; + self_send_type <= static_cast(jamtis::JamtisSelfSendType::MAX); + ++self_send_type) + { + if (try_get_enote_record_v1_selfsend_for_type(enote, + enote_ephemeral_pubkey, + input_context, + jamtis_spend_pubkey, + k_view_balance, + xk_find_received, + s_generate_address, + cipher_context, + static_cast(self_send_type), + record_out)) + return true; + } + + return false; +} +//------------------------------------------------------------------------------------------------------------------- +bool try_get_enote_record_v1_selfsend(const SpEnoteVariant &enote, + const crypto::x25519_pubkey &enote_ephemeral_pubkey, + const rct::key &input_context, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + SpEnoteRecordV1 &record_out) +{ + // make generate-address secret and cipher context then get enote record + crypto::x25519_secret_key xk_find_received; + crypto::secret_key s_generate_address; + crypto::secret_key s_cipher_tag; + jamtis::make_jamtis_findreceived_key(k_view_balance, xk_find_received); + jamtis::make_jamtis_generateaddress_secret(k_view_balance, s_generate_address); + jamtis::make_jamtis_ciphertag_secret(s_generate_address, s_cipher_tag); + + const jamtis::jamtis_address_tag_cipher_context cipher_context{s_cipher_tag}; + + return try_get_enote_record_v1_selfsend(enote, + enote_ephemeral_pubkey, + input_context, + jamtis_spend_pubkey, + k_view_balance, + xk_find_received, + s_generate_address, + cipher_context, + record_out); +} +//------------------------------------------------------------------------------------------------------------------- +bool try_get_enote_record_v1(const SpEnoteVariant &enote, + const crypto::x25519_pubkey &enote_ephemeral_pubkey, + const rct::key &input_context, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + SpEnoteRecordV1 &record_out) +{ + // note: check for selfsend first since it is very fast for unowned enotes + // (assumes selfsends and plain enotes appear in similar quantities) + return try_get_enote_record_v1_selfsend(enote, + enote_ephemeral_pubkey, + input_context, + jamtis_spend_pubkey, + k_view_balance, + record_out) || + try_get_enote_record_v1_plain(enote, + enote_ephemeral_pubkey, + input_context, + jamtis_spend_pubkey, + k_view_balance, + record_out); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace sp diff --git a/src/seraphis_main/enote_record_utils.h b/src/seraphis_main/enote_record_utils.h new file mode 100644 index 0000000000..4701564105 --- /dev/null +++ b/src/seraphis_main/enote_record_utils.h @@ -0,0 +1,193 @@ +// Copyright (c) 2022, 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 making enote records from enotes. + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "crypto/x25519.h" +#include "enote_record_types.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/jamtis_address_tag_utils.h" +#include "seraphis_core/jamtis_support_types.h" +#include "tx_component_types.h" + +//third party headers + +//standard headers + +//forward declarations + + +namespace sp +{ + +/** +* brief: try_get_basic_enote_record_v1 - try to extract a basic enote record from an enote +* param: enote - +* param: enote_ephemeral_pubkey - +* param: input_context - +* param: sender_receiver_DH_derivation - +* outparam: basic_record_out - +* return: true if extraction succeeded +*/ +bool try_get_basic_enote_record_v1(const SpEnoteVariant &enote, + const crypto::x25519_pubkey &enote_ephemeral_pubkey, + const rct::key &input_context, + const crypto::x25519_pubkey &sender_receiver_DH_derivation, + SpBasicEnoteRecordV1 &basic_record_out); +bool try_get_basic_enote_record_v1(const SpEnoteVariant &enote, + const crypto::x25519_pubkey &enote_ephemeral_pubkey, + const rct::key &input_context, + const crypto::x25519_secret_key &xk_find_received, + SpBasicEnoteRecordV1 &basic_record_out); +/** +* brief: try_get_intermediate_enote_record_v1 - try to extract an intermediate enote record from an enote +* param: enote - +* param: enote_ephemeral_pubkey - +* param: input_context - +* param: jamtis_spend_pubkey - +* param: xk_unlock_amounts - +* param: xk_find_received - +* param: s_generate_address - +* param: cipher_context - +* outparam: record_out - +* return: true if extraction succeeded +*/ +bool try_get_intermediate_enote_record_v1(const SpEnoteVariant &enote, + const crypto::x25519_pubkey &enote_ephemeral_pubkey, + const rct::key &input_context, + const rct::key &jamtis_spend_pubkey, + const crypto::x25519_secret_key &xk_unlock_amounts, + const crypto::x25519_secret_key &xk_find_received, + const crypto::secret_key &s_generate_address, + const jamtis::jamtis_address_tag_cipher_context &cipher_context, + SpIntermediateEnoteRecordV1 &record_out); +bool try_get_intermediate_enote_record_v1(const SpEnoteVariant &enote, + const crypto::x25519_pubkey &enote_ephemeral_pubkey, + const rct::key &input_context, + const rct::key &jamtis_spend_pubkey, + const crypto::x25519_secret_key &xk_unlock_amounts, + const crypto::x25519_secret_key &xk_find_received, + const crypto::secret_key &s_generate_address, + SpIntermediateEnoteRecordV1 &record_out); +bool try_get_intermediate_enote_record_v1(const SpBasicEnoteRecordV1 &basic_record, + const rct::key &jamtis_spend_pubkey, + const crypto::x25519_secret_key &xk_unlock_amounts, + const crypto::x25519_secret_key &xk_find_received, + const crypto::secret_key &s_generate_address, + const jamtis::jamtis_address_tag_cipher_context &cipher_context, + SpIntermediateEnoteRecordV1 &record_out); +bool try_get_intermediate_enote_record_v1(const SpBasicEnoteRecordV1 &basic_record, + const rct::key &jamtis_spend_pubkey, + const crypto::x25519_secret_key &xk_unlock_amounts, + const crypto::x25519_secret_key &xk_find_received, + const crypto::secret_key &s_generate_address, + SpIntermediateEnoteRecordV1 &record_out); +/** +* brief: try_get_enote_record_v1_plain - try to extract an enote record from an enote +* - plain jamtis enote type attempt +* param: enote - +* param: enote_ephemeral_pubkey - +* param: input_context - +* param: jamtis_spend_pubkey - +* param: k_view_balance - +* outparam: record_out - +* return: true if extraction succeeded +*/ +bool try_get_enote_record_v1_plain(const SpEnoteVariant &enote, + const crypto::x25519_pubkey &enote_ephemeral_pubkey, + const rct::key &input_context, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + SpEnoteRecordV1 &record_out); +bool try_get_enote_record_v1_plain(const SpBasicEnoteRecordV1 &basic_record, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + const crypto::x25519_secret_key &xk_unlock_amounts, + const crypto::x25519_secret_key &xk_find_received, + const crypto::secret_key &s_generate_address, + const jamtis::jamtis_address_tag_cipher_context &cipher_context, + SpEnoteRecordV1 &record_out); +bool try_get_enote_record_v1_plain(const SpBasicEnoteRecordV1 &basic_record, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + SpEnoteRecordV1 &record_out); +bool try_get_enote_record_v1_plain(const SpIntermediateEnoteRecordV1 &intermediate_record, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + SpEnoteRecordV1 &record_out); +/** +* brief: try_get_enote_record_v1_selfsend - try to extract an enote record from an enote +* - selfsend jamtis enote type attempt +* param: enote - +* param: enote_ephemeral_pubkey - +* param: input_context - +* param: jamtis_spend_pubkey - +* param: k_view_balance - +* param: xk_find_received - +* param: s_generate_address - +* param: cipher_context - +* outparam: record_out - +* return: true if extraction succeeded +*/ +bool try_get_enote_record_v1_selfsend(const SpEnoteVariant &enote, + const crypto::x25519_pubkey &enote_ephemeral_pubkey, + const rct::key &input_context, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + const crypto::x25519_secret_key &xk_find_received, + const crypto::secret_key &s_generate_address, + const jamtis::jamtis_address_tag_cipher_context &cipher_context, + SpEnoteRecordV1 &record_out); +bool try_get_enote_record_v1_selfsend(const SpEnoteVariant &enote, + const crypto::x25519_pubkey &enote_ephemeral_pubkey, + const rct::key &input_context, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + SpEnoteRecordV1 &record_out); +/** +* brief: try_get_enote_record_v1 - try to extract an enote record from an enote (which can be any jamtis enote type) +* param: enote - +* param: enote_ephemeral_pubkey - +* param: input_context - +* param: jamtis_spend_pubkey - +* param: k_view_balance - +* outparam: record_out - +* return: true if extraction succeeded +*/ +bool try_get_enote_record_v1(const SpEnoteVariant &enote, + const crypto::x25519_pubkey &enote_ephemeral_pubkey, + const rct::key &input_context, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + SpEnoteRecordV1 &record_out); + +} //namespace sp diff --git a/src/seraphis_main/enote_record_utils_legacy.cpp b/src/seraphis_main/enote_record_utils_legacy.cpp new file mode 100644 index 0000000000..dc1e3ad97b --- /dev/null +++ b/src/seraphis_main/enote_record_utils_legacy.cpp @@ -0,0 +1,557 @@ +// Copyright (c) 2022, 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_record_utils_legacy.h" + +//local headers +#include "common/variant.h" +#include "crypto/crypto.h" +extern "C" +{ +#include "crypto/crypto-ops.h" +} +#include "cryptonote_basic/subaddress_index.h" +#include "device/device.hpp" +#include "enote_record_types.h" +#include "ringct/rctOps.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/legacy_core_utils.h" +#include "seraphis_core/legacy_enote_types.h" +#include "seraphis_core/legacy_enote_utils.h" +#include "seraphis_crypto/sp_crypto_utils.h" + +//third party headers +#include + +//standard headers +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis" + +namespace sp +{ +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +bool try_add_legacy_subaddress_spendkey(const boost::optional &address_index, + const rct::key &legacy_base_spend_pubkey, + const crypto::secret_key &legacy_view_privkey, + hw::device &hwdev, + std::unordered_map &legacy_subaddress_map_inout) +{ + // 1. check if there is an address index + if (!address_index) + return false; + + // 2. make the subaddress spendkey + rct::key subaddress_spendkey; + make_legacy_subaddress_spendkey(legacy_base_spend_pubkey, + legacy_view_privkey, + *address_index, + hwdev, + subaddress_spendkey); + + // 3. add it to the map + legacy_subaddress_map_inout[subaddress_spendkey] = *address_index; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool try_check_legacy_view_tag(const LegacyEnoteVariant &enote, + const rct::key &enote_ephemeral_pubkey, + const std::uint64_t tx_output_index, + const crypto::key_derivation &sender_receiver_DH_derivation, + hw::device &hwdev) +{ + // 1. obtain the view tag + // - only legacy enotes v4 and v5 have a view tag + struct visitor final : public tools::variant_static_visitor> + { + using variant_static_visitor::operator(); //for blank overload + boost::optional operator()(const LegacyEnoteV1 &enote) const { return boost::none; } + boost::optional operator()(const LegacyEnoteV2 &enote) const { return boost::none; } + boost::optional operator()(const LegacyEnoteV3 &enote) const { return boost::none; } + boost::optional operator()(const LegacyEnoteV4 &enote) const { return enote.view_tag; } + boost::optional operator()(const LegacyEnoteV5 &enote) const { return enote.view_tag; } + }; + + const boost::optional enote_view_tag{enote.visit(visitor{})}; + + if (!enote_view_tag) + return true; //check succeeds automatically for enotes with no view tag + + // 2. view_tag = H_1("view_tag", r K^v, t) + crypto::view_tag nominal_view_tag; + hwdev.derive_view_tag(sender_receiver_DH_derivation, tx_output_index, nominal_view_tag); + + // 3. check the view tag + if (nominal_view_tag == *enote_view_tag) + return true; + + return false; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool try_check_legacy_nominal_spendkey(const rct::key &onetime_address, + const std::uint64_t tx_output_index, + const crypto::key_derivation &sender_receiver_DH_derivation, + const rct::key &legacy_base_spend_pubkey, + const std::unordered_map &legacy_subaddress_map, + hw::device &hwdev, + boost::optional &address_index_out) +{ + // 1. nominal spendkey = Ko - Hn(r Kv, t) G + crypto::public_key nominal_spendkey; + hwdev.derive_subaddress_public_key(rct::rct2pk(onetime_address), + sender_receiver_DH_derivation, + tx_output_index, + nominal_spendkey); + + // 2. check base spendkey + if (rct::pk2rct(nominal_spendkey) == legacy_base_spend_pubkey) + { + address_index_out = boost::none; + return true; + } + + // 3. check subaddress map + if (legacy_subaddress_map.find(rct::pk2rct(nominal_spendkey)) != legacy_subaddress_map.end()) + { + address_index_out = legacy_subaddress_map.at(rct::pk2rct(nominal_spendkey)); + return true; + } + + return false; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool try_get_amount_commitment_information_v1(const rct::xmr_amount &enote_amount, + rct::xmr_amount &amount_out, + crypto::secret_key &amount_blinding_factor_out) +{ + amount_out = enote_amount; + amount_blinding_factor_out = rct::rct2sk(rct::I); + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool try_get_amount_commitment_information_v2(const rct::key &amount_commitment, + const rct::key &encoded_amount_mask, + const rct::key &encoded_amount, + const std::uint64_t tx_output_index, + const crypto::key_derivation &sender_receiver_DH_derivation, + hw::device &hwdev, + rct::xmr_amount &amount_out, + crypto::secret_key &amount_blinding_factor_out) +{ + // 1. Hn(k^v R_t, t) + crypto::secret_key sender_receiver_secret; + hwdev.derivation_to_scalar(sender_receiver_DH_derivation, tx_output_index, sender_receiver_secret); + + // 2. recover the amount mask and amount + if (!try_get_legacy_amount_v1(amount_commitment, + sender_receiver_secret, + encoded_amount_mask, + encoded_amount, + hwdev, + amount_blinding_factor_out, + amount_out)) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool try_get_amount_commitment_information_v3(const rct::key &amount_commitment, + const jamtis::encoded_amount_t &encoded_amount, + const std::uint64_t tx_output_index, + const crypto::key_derivation &sender_receiver_DH_derivation, + hw::device &hwdev, + rct::xmr_amount &amount_out, + crypto::secret_key &amount_blinding_factor_out) +{ + // 1. Hn(k^v R_t, t) + crypto::secret_key sender_receiver_secret; + hwdev.derivation_to_scalar(sender_receiver_DH_derivation, tx_output_index, sender_receiver_secret); + + // 2. recover the amount mask and amount + if (!try_get_legacy_amount_v2(amount_commitment, + sender_receiver_secret, + encoded_amount, + hwdev, + amount_blinding_factor_out, + amount_out)) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool try_get_amount_commitment_information(const LegacyEnoteVariant &enote, + const std::uint64_t tx_output_index, + const crypto::key_derivation &sender_receiver_DH_derivation, + hw::device &hwdev, + rct::xmr_amount &amount_out, + crypto::secret_key &amount_blinding_factor_out) +{ + if (const LegacyEnoteV1 *enote_ptr = enote.try_unwrap()) + { + return try_get_amount_commitment_information_v1(enote_ptr->amount, + amount_out, + amount_blinding_factor_out); + } + else if (const LegacyEnoteV2 *enote_ptr = enote.try_unwrap()) + { + return try_get_amount_commitment_information_v2(enote_ptr->amount_commitment, + enote_ptr->encoded_amount_blinding_factor, + enote_ptr->encoded_amount, + tx_output_index, + sender_receiver_DH_derivation, + hwdev, + amount_out, + amount_blinding_factor_out); + } + else if (const LegacyEnoteV3 *enote_ptr = enote.try_unwrap()) + { + return try_get_amount_commitment_information_v3(enote_ptr->amount_commitment, + enote_ptr->encoded_amount, + tx_output_index, + sender_receiver_DH_derivation, + hwdev, + amount_out, + amount_blinding_factor_out); + } + else if (const LegacyEnoteV4 *enote_ptr = enote.try_unwrap()) + { + return try_get_amount_commitment_information_v1(enote_ptr->amount, + amount_out, + amount_blinding_factor_out); + } + else if (const LegacyEnoteV5 *enote_ptr = enote.try_unwrap()) + { + return try_get_amount_commitment_information_v3(enote_ptr->amount_commitment, + enote_ptr->encoded_amount, + tx_output_index, + sender_receiver_DH_derivation, + hwdev, + amount_out, + amount_blinding_factor_out); + } + else + CHECK_AND_ASSERT_THROW_MES(false, "try get legacy amount commitment information: unknown enote type."); + + return false; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool try_get_intermediate_legacy_enote_record_info(const LegacyEnoteVariant &enote, + const rct::key &enote_ephemeral_pubkey, + const std::uint64_t tx_output_index, + const rct::key &legacy_base_spend_pubkey, + const std::unordered_map &legacy_subaddress_map, + const crypto::secret_key &legacy_view_privkey, + hw::device &hwdev, + crypto::secret_key &enote_view_extension_out, + rct::xmr_amount &amount_out, + crypto::secret_key &amount_blinding_factor_out, + boost::optional &subaddress_index_out) +{ + // 1. r K^v = k^v R + crypto::key_derivation sender_receiver_DH_derivation; + hwdev.generate_key_derivation(rct::rct2pk(enote_ephemeral_pubkey), + legacy_view_privkey, + sender_receiver_DH_derivation); + + // 2. check view tag (for enotes that have it) + if (!try_check_legacy_view_tag(enote, + enote_ephemeral_pubkey, + tx_output_index, + sender_receiver_DH_derivation, + hwdev)) + return false; + + // 3. nominal spendkey check (and get subaddress index if applicable) + if (!try_check_legacy_nominal_spendkey(onetime_address_ref(enote), + tx_output_index, + sender_receiver_DH_derivation, + legacy_base_spend_pubkey, + legacy_subaddress_map, + hwdev, + subaddress_index_out)) + return false; + + // 4. compute enote view privkey + make_legacy_enote_view_extension(tx_output_index, + sender_receiver_DH_derivation, + legacy_view_privkey, + subaddress_index_out, + hwdev, + enote_view_extension_out); + + // 5. try to get amount commitment information + if (!try_get_amount_commitment_information(enote, + tx_output_index, + sender_receiver_DH_derivation, + hwdev, + amount_out, + amount_blinding_factor_out)) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +bool try_get_legacy_basic_enote_record(const LegacyEnoteVariant &enote, + const rct::key &enote_ephemeral_pubkey, + const std::uint64_t tx_output_index, + const std::uint64_t unlock_time, + const crypto::key_derivation &sender_receiver_DH_derivation, + const rct::key &legacy_base_spend_pubkey, + const std::unordered_map &legacy_subaddress_map, + hw::device &hwdev, + LegacyBasicEnoteRecord &basic_record_out) +{ + // 1. check view tag (for enotes that have it) + if (!try_check_legacy_view_tag(enote, + enote_ephemeral_pubkey, + tx_output_index, + sender_receiver_DH_derivation, + hwdev)) + return false; + + // 2. nominal spendkey check (and get subaddress index if applicable) + if (!try_check_legacy_nominal_spendkey(onetime_address_ref(enote), + tx_output_index, + sender_receiver_DH_derivation, + legacy_base_spend_pubkey, + legacy_subaddress_map, + hwdev, + basic_record_out.address_index)) + return false; + + // 3. set miscellaneous fields + basic_record_out.enote = enote; + basic_record_out.enote_ephemeral_pubkey = enote_ephemeral_pubkey; + basic_record_out.tx_output_index = tx_output_index; + basic_record_out.unlock_time = unlock_time; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool try_get_legacy_basic_enote_record(const LegacyEnoteVariant &enote, + const rct::key &enote_ephemeral_pubkey, + const std::uint64_t tx_output_index, + const std::uint64_t unlock_time, + const rct::key &legacy_base_spend_pubkey, + const std::unordered_map &legacy_subaddress_map, + const crypto::secret_key &legacy_view_privkey, + hw::device &hwdev, + LegacyBasicEnoteRecord &basic_record_out) +{ + // 1. r K^v = k^v R + crypto::key_derivation sender_receiver_DH_derivation; + hwdev.generate_key_derivation(rct::rct2pk(enote_ephemeral_pubkey), + legacy_view_privkey, + sender_receiver_DH_derivation); + + // 2. finish getting the record + return try_get_legacy_basic_enote_record(enote, + enote_ephemeral_pubkey, + tx_output_index, + unlock_time, + sender_receiver_DH_derivation, + legacy_base_spend_pubkey, + legacy_subaddress_map, + hwdev, + basic_record_out); +} +//------------------------------------------------------------------------------------------------------------------- +bool try_get_legacy_intermediate_enote_record(const LegacyEnoteVariant &enote, + const rct::key &enote_ephemeral_pubkey, + const std::uint64_t tx_output_index, + const std::uint64_t unlock_time, + const rct::key &legacy_base_spend_pubkey, + const std::unordered_map &legacy_subaddress_map, + const crypto::secret_key &legacy_view_privkey, + hw::device &hwdev, + LegacyIntermediateEnoteRecord &record_out) +{ + // 1. try to get intermediate info + if (!try_get_intermediate_legacy_enote_record_info(enote, + enote_ephemeral_pubkey, + tx_output_index, + legacy_base_spend_pubkey, + legacy_subaddress_map, + legacy_view_privkey, + hwdev, + record_out.enote_view_extension, + record_out.amount, + record_out.amount_blinding_factor, + record_out.address_index)) + return false; + + // 2. collect miscellaneous pieces + record_out.enote = enote; + record_out.enote_ephemeral_pubkey = enote_ephemeral_pubkey; + record_out.tx_output_index = tx_output_index; + record_out.unlock_time = unlock_time; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool try_get_legacy_intermediate_enote_record(const LegacyBasicEnoteRecord &basic_record, + const rct::key &legacy_base_spend_pubkey, + const crypto::secret_key &legacy_view_privkey, + hw::device &hwdev, + LegacyIntermediateEnoteRecord &record_out) +{ + // 1. if the enote is owned by a subaddress, make the subaddress spendkey + std::unordered_map legacy_subaddress_map; + try_add_legacy_subaddress_spendkey(basic_record.address_index, + legacy_base_spend_pubkey, + legacy_view_privkey, + hwdev, + legacy_subaddress_map); + + // 2. finish getting the intermediate enote record + return try_get_legacy_intermediate_enote_record(basic_record.enote, + basic_record.enote_ephemeral_pubkey, + basic_record.tx_output_index, + basic_record.unlock_time, + legacy_base_spend_pubkey, + legacy_subaddress_map, + legacy_view_privkey, + hwdev, + record_out); +} +//------------------------------------------------------------------------------------------------------------------- +bool try_get_legacy_enote_record(const LegacyEnoteVariant &enote, + const rct::key &enote_ephemeral_pubkey, + const std::uint64_t tx_output_index, + const std::uint64_t unlock_time, + const rct::key &legacy_base_spend_pubkey, + const std::unordered_map &legacy_subaddress_map, + const crypto::secret_key &legacy_spend_privkey, + const crypto::secret_key &legacy_view_privkey, + hw::device &hwdev, + LegacyEnoteRecord &record_out) +{ + // 1. try to get intermediate info + if (!try_get_intermediate_legacy_enote_record_info(enote, + enote_ephemeral_pubkey, + tx_output_index, + legacy_base_spend_pubkey, + legacy_subaddress_map, + legacy_view_privkey, + hwdev, + record_out.enote_view_extension, + record_out.amount, + record_out.amount_blinding_factor, + record_out.address_index)) + return false; + + // 2. compute the key image + make_legacy_key_image(record_out.enote_view_extension, + legacy_spend_privkey, + onetime_address_ref(enote), + hwdev, + record_out.key_image); + + // 3. collect miscellaneous pieces + record_out.enote = enote; + record_out.enote_ephemeral_pubkey = enote_ephemeral_pubkey; + record_out.tx_output_index = tx_output_index; + record_out.unlock_time = unlock_time; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool try_get_legacy_enote_record(const LegacyBasicEnoteRecord &basic_record, + const rct::key &legacy_base_spend_pubkey, + const crypto::secret_key &legacy_spend_privkey, + const crypto::secret_key &legacy_view_privkey, + hw::device &hwdev, + LegacyEnoteRecord &record_out) +{ + // 1. if the enote is owned by a subaddress, make the subaddress spendkey + std::unordered_map legacy_subaddress_map; + try_add_legacy_subaddress_spendkey(basic_record.address_index, + legacy_base_spend_pubkey, + legacy_view_privkey, + hwdev, + legacy_subaddress_map); + + // 2. finish getting the full enote record + return try_get_legacy_enote_record(basic_record.enote, + basic_record.enote_ephemeral_pubkey, + basic_record.tx_output_index, + basic_record.unlock_time, + legacy_base_spend_pubkey, + legacy_subaddress_map, + legacy_spend_privkey, + legacy_view_privkey, + hwdev, + record_out); +} +//------------------------------------------------------------------------------------------------------------------- +void get_legacy_enote_record(const LegacyIntermediateEnoteRecord &intermediate_record, + const crypto::key_image &key_image, + LegacyEnoteRecord &record_out) +{ + record_out.enote = intermediate_record.enote; + record_out.enote_ephemeral_pubkey = intermediate_record.enote_ephemeral_pubkey; + record_out.enote_view_extension = intermediate_record.enote_view_extension; + record_out.amount = intermediate_record.amount; + record_out.amount_blinding_factor = intermediate_record.amount_blinding_factor; + record_out.key_image = key_image; + record_out.address_index = intermediate_record.address_index; + record_out.tx_output_index = intermediate_record.tx_output_index; + record_out.unlock_time = intermediate_record.unlock_time; +} +//------------------------------------------------------------------------------------------------------------------- +void get_legacy_enote_record(const LegacyIntermediateEnoteRecord &intermediate_record, + const crypto::secret_key &legacy_spend_privkey, + hw::device &hwdev, + LegacyEnoteRecord &record_out) +{ + // 1. make key image: ((view key stuff) + k^s) * Hp(Ko) + crypto::key_image key_image; + make_legacy_key_image(intermediate_record.enote_view_extension, + legacy_spend_privkey, + onetime_address_ref(intermediate_record.enote), + hwdev, + key_image); + + // 2. assemble data + get_legacy_enote_record(intermediate_record, key_image, record_out); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace sp diff --git a/src/seraphis_main/enote_record_utils_legacy.h b/src/seraphis_main/enote_record_utils_legacy.h new file mode 100644 index 0000000000..7899475efa --- /dev/null +++ b/src/seraphis_main/enote_record_utils_legacy.h @@ -0,0 +1,146 @@ +// Copyright (c) 2022, 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 obtaining legacy enote records. + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "cryptonote_basic/subaddress_index.h" +#include "device/device.hpp" +#include "enote_record_types.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/legacy_enote_types.h" + +//third party headers + +//standard headers +#include + +//forward declarations + + +namespace sp +{ + +/** +* brief: try_get_legacy_basic_enote_record - try to extract a legacy basic enote record from a legacy enote +* param: enote - +* param: enote_ephemeral_pubkey - +* param: tx_output_index - +* param: unlock_time - +* param: sender_receiver_DH_derivation - +* param: legacy_base_spend_pubkey - +* param: legacy_subaddress_map - +* inoutparam: hwdev - +* outparam: basic_record_out - +* return: true if an extraction succeeded +*/ +bool try_get_legacy_basic_enote_record(const LegacyEnoteVariant &enote, + const rct::key &enote_ephemeral_pubkey, + const std::uint64_t tx_output_index, + const std::uint64_t unlock_time, + const crypto::key_derivation &sender_receiver_DH_derivation, + const rct::key &legacy_base_spend_pubkey, + const std::unordered_map &legacy_subaddress_map, + hw::device &hwdev, + LegacyBasicEnoteRecord &basic_record_out); +bool try_get_legacy_basic_enote_record(const LegacyEnoteVariant &enote, + const rct::key &enote_ephemeral_pubkey, + const std::uint64_t tx_output_index, + const std::uint64_t unlock_time, + const rct::key &legacy_base_spend_pubkey, + const std::unordered_map &legacy_subaddress_map, + const crypto::secret_key &legacy_view_privkey, + hw::device &hwdev, + LegacyBasicEnoteRecord &basic_record_out); +/** +* brief: try_get_legacy_intermediate_enote_record - try to extract a legacy intermediate enote record from a legacy enote +* param: enote - +* param: enote_ephemeral_pubkey - +* param: tx_output_index - +* param: unlock_time - +* param: legacy_base_spend_pubkey - +* param: legacy_subaddress_map - +* param: legacy_view_privkey - +* outparam: record_out - +* return: true if an extraction succeeded +*/ +bool try_get_legacy_intermediate_enote_record(const LegacyEnoteVariant &enote, + const rct::key &enote_ephemeral_pubkey, + const std::uint64_t tx_output_index, + const std::uint64_t unlock_time, + const rct::key &legacy_base_spend_pubkey, + const std::unordered_map &legacy_subaddress_map, + const crypto::secret_key &legacy_view_privkey, + hw::device &hwdev, + LegacyIntermediateEnoteRecord &record_out); +bool try_get_legacy_intermediate_enote_record(const LegacyBasicEnoteRecord &basic_record, + const rct::key &legacy_base_spend_pubkey, + const crypto::secret_key &legacy_view_privkey, + hw::device &hwdev, + LegacyIntermediateEnoteRecord &record_out); +/** +* brief: try_get_legacy_enote_record - try to extract a legacy enote record from a legacy enote +* param: enote - +* param: enote_ephemeral_pubkey - +* param: tx_output_index - +* param: unlock_time - +* param: legacy_base_spend_pubkey - +* param: legacy_subaddress_map - +* param: legacy_spend_privkey - +* param: legacy_view_privkey - +* outparam: record_out - +* return: true if an extraction succeeded +*/ +bool try_get_legacy_enote_record(const LegacyEnoteVariant &enote, + const rct::key &enote_ephemeral_pubkey, + const std::uint64_t tx_output_index, + const std::uint64_t unlock_time, + const rct::key &legacy_base_spend_pubkey, + const std::unordered_map &legacy_subaddress_map, + const crypto::secret_key &legacy_spend_privkey, + const crypto::secret_key &legacy_view_privkey, + hw::device &hwdev, + LegacyEnoteRecord &record_out); +bool try_get_legacy_enote_record(const LegacyBasicEnoteRecord &basic_record, + const rct::key &legacy_base_spend_pubkey, + const crypto::secret_key &legacy_spend_privkey, + const crypto::secret_key &legacy_view_privkey, + hw::device &hwdev, + LegacyEnoteRecord &record_out); +void get_legacy_enote_record(const LegacyIntermediateEnoteRecord &intermediate_record, + const crypto::key_image &key_image, + LegacyEnoteRecord &record_out); +void get_legacy_enote_record(const LegacyIntermediateEnoteRecord &intermediate_record, + const crypto::secret_key &legacy_spend_privkey, + hw::device &hwdev, + LegacyEnoteRecord &record_out); + +} //namespace sp diff --git a/src/seraphis_main/scan_balance_recovery_utils.cpp b/src/seraphis_main/scan_balance_recovery_utils.cpp new file mode 100644 index 0000000000..73d9c0e038 --- /dev/null +++ b/src/seraphis_main/scan_balance_recovery_utils.cpp @@ -0,0 +1,856 @@ +// Copyright (c) 2022, 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 "scan_balance_recovery_utils.h" + +//local headers +#include "contextual_enote_record_types.h" +#include "contextual_enote_record_utils.h" +#include "crypto/crypto.h" +#include "crypto/x25519.h" +#include "cryptonote_basic/subaddress_index.h" +#include "device/device.hpp" +#include "enote_record_types.h" +#include "enote_record_utils.h" +#include "enote_record_utils_legacy.h" +#include "ringct/rctOps.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/jamtis_core_utils.h" +#include "seraphis_core/legacy_core_utils.h" +#include "seraphis_core/legacy_enote_types.h" +#include "seraphis_core/legacy_enote_utils.h" +#include "seraphis_core/tx_extra.h" +#include "seraphis_crypto/sp_crypto_utils.h" +#include "tx_component_types.h" + +//third party headers + +//standard headers +#include +#include +#include +#include +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis" + +namespace sp +{ +namespace scanning +{ +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool try_view_scan_legacy_enote_v1(const rct::key &legacy_base_spend_pubkey, + const std::unordered_map &legacy_subaddress_map, + const std::uint64_t block_index, + const std::uint64_t block_timestamp, + const rct::key &transaction_id, + const std::uint64_t total_enotes_before_tx, + const std::uint64_t enote_index, + const std::uint64_t unlock_time, + const TxExtra &tx_memo, + const LegacyEnoteVariant &legacy_enote, + const crypto::public_key &legacy_enote_ephemeral_pubkey, + const crypto::key_derivation &DH_derivation, + const SpEnoteOriginStatus origin_status, + hw::device &hwdev, + LegacyContextualBasicEnoteRecordV1 &contextual_record_out) +{ + // 1. view scan the enote (in try block in case the enote is malformed) + try + { + if (!try_get_legacy_basic_enote_record(legacy_enote, + rct::pk2rct(legacy_enote_ephemeral_pubkey), + enote_index, + unlock_time, + DH_derivation, + legacy_base_spend_pubkey, + legacy_subaddress_map, + hwdev, + contextual_record_out.record)) + return false; + } catch (...) { return false; } + + // 2. set the origin context + contextual_record_out.origin_context = + SpEnoteOriginContextV1{ + .block_index = block_index, + .block_timestamp = block_timestamp, + .transaction_id = transaction_id, + .enote_tx_index = enote_index, + .enote_ledger_index = total_enotes_before_tx + enote_index, + .origin_status = origin_status, + .memo = tx_memo + }; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void update_with_new_intermediate_record_legacy(const LegacyIntermediateEnoteRecord &new_enote_record, + const SpEnoteOriginContextV1 &new_record_origin_context, + std::unordered_map &found_enote_records_inout) +{ + // 1. add new intermediate legacy record to found enotes (or refresh if already there) + rct::key new_record_identifier; + get_legacy_enote_identifier(onetime_address_ref(new_enote_record.enote), + new_enote_record.amount, + new_record_identifier); + + found_enote_records_inout[new_record_identifier].record = new_enote_record; + + // 2. update the record's origin context + try_update_enote_origin_context_v1(new_record_origin_context, + found_enote_records_inout[new_record_identifier].origin_context); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void update_with_new_record_legacy(const LegacyEnoteRecord &new_enote_record, + const SpEnoteOriginContextV1 &new_record_origin_context, + const std::list &chunk_contextual_key_images, + std::unordered_map &found_enote_records_inout, + std::unordered_map &found_spent_key_images_inout) +{ + // 1. add new legacy record to found enotes (or refresh if already there) + rct::key new_record_identifier; + get_legacy_enote_identifier(onetime_address_ref(new_enote_record.enote), + new_enote_record.amount, + new_record_identifier); + + found_enote_records_inout[new_record_identifier].record = new_enote_record; + + // 2. if the enote is spent in this chunk, update its spent context + const crypto::key_image &new_record_key_image{new_enote_record.key_image}; + SpEnoteSpentContextV1 spent_context_update{}; + + auto contextual_key_images_of_record_spent_in_this_chunk = + std::find_if( + chunk_contextual_key_images.begin(), + chunk_contextual_key_images.end(), + [&new_record_key_image](const SpContextualKeyImageSetV1 &contextual_key_image_set) -> bool + { + return has_key_image(contextual_key_image_set, new_record_key_image); + } + ); + + if (contextual_key_images_of_record_spent_in_this_chunk != chunk_contextual_key_images.end()) + { + // a. record that the enote is spent in this chunk + found_spent_key_images_inout[new_record_key_image]; + + // b. update its spent context (update instead of assignment in case of duplicates) + try_update_enote_spent_context_v1(contextual_key_images_of_record_spent_in_this_chunk->spent_context, + found_spent_key_images_inout[new_record_key_image]); + + // c. save the record's current spent context + spent_context_update = found_spent_key_images_inout[new_record_key_image]; + } + + // 3. update the record's contexts + // note: multiple legacy enotes can have the same key image but different amounts; only one of those can be spent, + // so we should expect all of them to end up referencing the same spent context + update_contextual_enote_record_contexts_v1(new_record_origin_context, + spent_context_update, + found_enote_records_inout[new_record_identifier].origin_context, + found_enote_records_inout[new_record_identifier].spent_context); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void update_with_new_intermediate_record_sp(const SpIntermediateEnoteRecordV1 &new_enote_record, + const SpEnoteOriginContextV1 &new_record_origin_context, + std::unordered_map &found_enote_records_inout) +{ + // 1. add new seraphis record to found enotes (or refresh if already there) + const rct::key &new_record_onetime_address{onetime_address_ref(new_enote_record.enote)}; + + found_enote_records_inout[new_record_onetime_address].record = new_enote_record; + + // 2. update the record's origin context + try_update_enote_origin_context_v1(new_record_origin_context, + found_enote_records_inout[new_record_onetime_address].origin_context); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void update_with_new_record_sp(const SpEnoteRecordV1 &new_enote_record, + const SpEnoteOriginContextV1 &new_record_origin_context, + const std::list &chunk_contextual_key_images, + std::unordered_map &found_enote_records_inout, + std::unordered_map &found_spent_key_images_inout, + std::unordered_set &txs_have_spent_enotes_inout) +{ + // 1. add new record to found enotes (or refresh if already there) + const crypto::key_image &new_record_key_image{new_enote_record.key_image}; + + found_enote_records_inout[new_record_key_image].record = new_enote_record; + + // 2. if the enote is spent in this chunk, update its spent context + SpEnoteSpentContextV1 spent_context_update{}; + + auto contextual_key_images_of_record_spent_in_this_chunk = + std::find_if( + chunk_contextual_key_images.begin(), + chunk_contextual_key_images.end(), + [&new_record_key_image](const SpContextualKeyImageSetV1 &contextual_key_image_set) -> bool + { + return has_key_image(contextual_key_image_set, new_record_key_image); + } + ); + + if (contextual_key_images_of_record_spent_in_this_chunk != chunk_contextual_key_images.end()) + { + // a. record that the enote is spent in this chunk + found_spent_key_images_inout[new_record_key_image]; + + // b. update its spent context (update instead of assignment in case of duplicates) + try_update_enote_spent_context_v1(contextual_key_images_of_record_spent_in_this_chunk->spent_context, + found_spent_key_images_inout[new_record_key_image]); + + // c. save the record's current spent context + spent_context_update = found_spent_key_images_inout[new_record_key_image]; + + // d. save the tx id of the tx where this enote was spent (the tx is in this chunk) + // note: use the spent context of the contextual key images instead of the spent context update in case the + // update did not resolve to a tx in this chunk (probably a bug, but better safe than sorry here) + txs_have_spent_enotes_inout.insert( + contextual_key_images_of_record_spent_in_this_chunk->spent_context.transaction_id + ); + } + + // 3. update the record's contexts + update_contextual_enote_record_contexts_v1(new_record_origin_context, + spent_context_update, + found_enote_records_inout[new_record_key_image].origin_context, + found_enote_records_inout[new_record_key_image].spent_context); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void collect_legacy_key_images_from_tx(const rct::key &requested_tx_id, + const std::list &chunk_contextual_key_images, + std::unordered_map &legacy_key_images_in_tx_inout) +{ + // 1. find key images of the requested tx + auto contextual_key_images_of_requested_tx = + std::find_if( + chunk_contextual_key_images.begin(), + chunk_contextual_key_images.end(), + [&requested_tx_id](const SpContextualKeyImageSetV1 &contextual_key_image_set) -> bool + { + return contextual_key_image_set.spent_context.transaction_id == requested_tx_id; + } + ); + + CHECK_AND_ASSERT_THROW_MES(contextual_key_images_of_requested_tx != chunk_contextual_key_images.end(), + "enote scanning (collect legacy key images from tx): could not find tx's key images."); + + // 2. record legacy key images and their spent contexts + for (const crypto::key_image &legacy_key_image : contextual_key_images_of_requested_tx->legacy_key_images) + { + try_update_enote_spent_context_v1(contextual_key_images_of_requested_tx->spent_context, + legacy_key_images_in_tx_inout[legacy_key_image]); + } +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static std::unordered_set process_chunk_sp_selfsend_pass( + const std::unordered_set &txs_have_spent_enotes, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + const crypto::x25519_secret_key &xk_find_received, + const crypto::secret_key &s_generate_address, + const jamtis::jamtis_address_tag_cipher_context &cipher_context, + const std::unordered_map> &chunk_basic_records_per_tx, + const std::list &chunk_contextual_key_images, + std::unordered_map &found_enote_records_inout, + std::unordered_map &found_spent_sp_key_images_inout, + std::unordered_map &legacy_key_images_in_sp_selfspends_inout) +{ + // for each tx in this chunk that spends one of our enotes, check if any of the basic records attached to that + // tx contain a self-send enote owned by us + // - if any self-send enotes identified here are also spent in txs in this chunk, return those txs' ids so this + // function can be called in a loop (those txs will contain self-send enotes that need to be scanned and that may + // in turn be spent in this chunk) + std::unordered_set txs_have_spent_enotes_fresh; + SpEnoteRecordV1 new_enote_record; + + for (const rct::key &tx_with_spent_enotes : txs_have_spent_enotes) + { + CHECK_AND_ASSERT_THROW_MES(chunk_basic_records_per_tx.find(tx_with_spent_enotes) != + chunk_basic_records_per_tx.end(), + "enote scan process chunk (self-send passthroughs): tx with spent enotes not found in records map (bug)."); + + for (const ContextualBasicRecordVariant &contextual_basic_record : + chunk_basic_records_per_tx.at(tx_with_spent_enotes)) + { + if (!contextual_basic_record.is_type()) + continue; + + try + { + // a. check if the enote is owned by attempting to convert it to a full enote record (selfsend conversion) + if (!try_get_enote_record_v1_selfsend( + contextual_basic_record.unwrap().record.enote, + contextual_basic_record.unwrap().record + .enote_ephemeral_pubkey, + contextual_basic_record.unwrap().record + .input_context, + jamtis_spend_pubkey, + k_view_balance, + xk_find_received, + s_generate_address, + cipher_context, + new_enote_record)) + continue; + + // b. we found an owned enote, so handle it + // - this will also check if the enote was spent in this chunk, and update 'txs_have_spent_enotes' + // accordingly + update_with_new_record_sp(new_enote_record, + origin_context_ref(contextual_basic_record), + chunk_contextual_key_images, + found_enote_records_inout, + found_spent_sp_key_images_inout, + txs_have_spent_enotes_fresh); + + // c. record all legacy key images attached to this selfsend for the caller to deal with + // - all key images of legacy owned enotes spent in seraphis txs will be attached to seraphis + // txs with selfsend outputs, but during seraphis scanning it isn't guaranteed that we will be able + // to check if legacy key images attached to selfsend owned enotes are associated with owned legacy + // enotes; therefore we cache those legacy key images so they can be handled outside this scan process + collect_legacy_key_images_from_tx(origin_context_ref(contextual_basic_record).transaction_id, + chunk_contextual_key_images, + legacy_key_images_in_sp_selfspends_inout); + } catch (...) {} + } + } + + return txs_have_spent_enotes_fresh; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +bool try_find_legacy_enotes_in_tx(const rct::key &legacy_base_spend_pubkey, + const std::unordered_map &legacy_subaddress_map, + const crypto::secret_key &legacy_view_privkey, + const std::uint64_t block_index, + const std::uint64_t block_timestamp, + const rct::key &transaction_id, + const std::uint64_t total_enotes_before_tx, + const std::uint64_t unlock_time, + const TxExtra &tx_memo, + const std::vector &enotes_in_tx, + const SpEnoteOriginStatus origin_status, + hw::device &hwdev, + std::list &basic_records_in_tx_out) +{ + basic_records_in_tx_out.clear(); + + // 1. extract enote ephemeral pubkeys from the memo + crypto::public_key legacy_main_enote_ephemeral_pubkey; + std::vector legacy_additional_enote_ephemeral_pubkeys; + + extract_legacy_enote_ephemeral_pubkeys_from_tx_extra(tx_memo, + legacy_main_enote_ephemeral_pubkey, + legacy_additional_enote_ephemeral_pubkeys); + + // 2. check if there are a valid number of additional enote ephemeral pubkeys + if (legacy_additional_enote_ephemeral_pubkeys.size() > 0 && + legacy_additional_enote_ephemeral_pubkeys.size() != enotes_in_tx.size()) + return false; + + // 3. scan each enote in the tx using the 'additional enote ephemeral pubkeys' + // - this step is automatically skipped if legacy_additional_enote_ephemeral_pubkeys.size() == 0 + crypto::key_derivation temp_DH_derivation; + LegacyContextualBasicEnoteRecordV1 temp_contextual_record{}; + bool found_an_enote{false}; + + for (std::size_t enote_index{0}; enote_index < legacy_additional_enote_ephemeral_pubkeys.size(); ++enote_index) + { + // a. compute the DH derivation for this enote ephemeral pubkey + hwdev.generate_key_derivation(legacy_additional_enote_ephemeral_pubkeys[enote_index], + legacy_view_privkey, + temp_DH_derivation); + + // b. try to recover a contextual basic record from the enote + if (!try_view_scan_legacy_enote_v1(legacy_base_spend_pubkey, + legacy_subaddress_map, + block_index, + block_timestamp, + transaction_id, + total_enotes_before_tx, + enote_index, + unlock_time, + tx_memo, + enotes_in_tx[enote_index], + legacy_additional_enote_ephemeral_pubkeys[enote_index], + temp_DH_derivation, + origin_status, + hwdev, + temp_contextual_record)) + continue; + + // c. save the contextual basic record + // note: it is possible for enotes with duplicate onetime addresses to be added here; it is assumed the + // upstream caller will be able to handle those without problems + basic_records_in_tx_out.emplace_back(temp_contextual_record); + + // d. record that an owned enote has been found + found_an_enote = true; + } + + // 4. check if there is a main enote ephemeral pubkey + if (legacy_main_enote_ephemeral_pubkey == rct::rct2pk(rct::I)) + return found_an_enote; + + // 5. compute the key derivation for the main enote ephemeral pubkey + hwdev.generate_key_derivation(legacy_main_enote_ephemeral_pubkey, legacy_view_privkey, temp_DH_derivation); + + // 6. scan all enotes using the main key derivation + for (std::size_t enote_index{0}; enote_index < enotes_in_tx.size(); ++enote_index) + { + // a. try to recover a contextual basic record from the enote + if (!try_view_scan_legacy_enote_v1(legacy_base_spend_pubkey, + legacy_subaddress_map, + block_index, + block_timestamp, + transaction_id, + total_enotes_before_tx, + enote_index, + unlock_time, + tx_memo, + enotes_in_tx[enote_index], + legacy_main_enote_ephemeral_pubkey, + temp_DH_derivation, + origin_status, + hwdev, + temp_contextual_record)) + continue; + + // b. save the contextual basic record + // note: it is possible for enotes with duplicate onetime addresses to be added here; it is assumed the + // upstream caller will be able to handle those without problems + basic_records_in_tx_out.emplace_back(temp_contextual_record); + + // c. record that an owned enote has been found + found_an_enote = true; + } + + return found_an_enote; +} +//------------------------------------------------------------------------------------------------------------------- +bool try_find_sp_enotes_in_tx(const crypto::x25519_secret_key &xk_find_received, + const std::uint64_t block_index, + const std::uint64_t block_timestamp, + const rct::key &transaction_id, + const std::uint64_t total_enotes_before_tx, + const rct::key &input_context, + const SpTxSupplementV1 &tx_supplement, + const std::vector &enotes_in_tx, + const SpEnoteOriginStatus origin_status, + std::list &basic_records_in_tx_out) +{ + basic_records_in_tx_out.clear(); + + // 1. check if any enotes can be scanned + if (tx_supplement.output_enote_ephemeral_pubkeys.size() == 0 || + enotes_in_tx.size() == 0) + return false; + + // 2. find-received scan each enote in the tx + std::size_t ephemeral_pubkey_index{0}; + crypto::x25519_pubkey temp_DH_derivation; + SpContextualBasicEnoteRecordV1 temp_contextual_record{}; + bool found_an_enote{false}; + + for (std::size_t enote_index{0}; enote_index < enotes_in_tx.size(); ++enote_index) + { + // a. get the next Diffie-Hellman derivation + // - there can be fewer ephemeral pubkeys than enotes; when we get to the end, keep using the last one + if (enote_index < tx_supplement.output_enote_ephemeral_pubkeys.size()) + { + ephemeral_pubkey_index = enote_index; + crypto::x25519_scmul_key(xk_find_received, + tx_supplement.output_enote_ephemeral_pubkeys[ephemeral_pubkey_index], + temp_DH_derivation); + } + + // b, find-receive scan the enote (in try block in case enote is malformed) + try + { + if (!try_get_basic_enote_record_v1(enotes_in_tx[enote_index], + tx_supplement.output_enote_ephemeral_pubkeys[ephemeral_pubkey_index], + input_context, + temp_DH_derivation, + temp_contextual_record.record)) + continue; + } catch (...) { continue; } + + // c. set the origin context + temp_contextual_record.origin_context = + SpEnoteOriginContextV1{ + .block_index = block_index, + .block_timestamp = block_timestamp, + .transaction_id = transaction_id, + .enote_tx_index = enote_index, + .enote_ledger_index = total_enotes_before_tx + enote_index, + .origin_status = origin_status, + .memo = tx_supplement.tx_extra + }; + + // d. save the contextual basic record + // note: it is possible for enotes with duplicate onetime addresses to be added here; it is assumed the + // upstream caller will be able to handle those without problems + basic_records_in_tx_out.emplace_back(temp_contextual_record); + + // e. record that an enote was found + found_an_enote = true; + } + + return found_an_enote; +} +//------------------------------------------------------------------------------------------------------------------- +bool try_collect_key_images_from_tx(const std::uint64_t block_index, + const std::uint64_t block_timestamp, + const rct::key &transaction_id, + std::vector legacy_key_images_in_tx, + std::vector sp_key_images_in_tx, + const SpEnoteSpentStatus spent_status, + SpContextualKeyImageSetV1 &contextual_key_images_out) +{ + // 1. don't make the set if there are no key images + if (legacy_key_images_in_tx.size() == 0 && + sp_key_images_in_tx.size() == 0) + return false; + + // 2. make the set + contextual_key_images_out = SpContextualKeyImageSetV1{ + .legacy_key_images = std::move(legacy_key_images_in_tx), + .sp_key_images = std::move(sp_key_images_in_tx), + .spent_context = + SpEnoteSpentContextV1{ + .block_index = block_index, + .block_timestamp = block_timestamp, + .transaction_id = transaction_id, + .spent_status = spent_status + } + }; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +void process_chunk_intermediate_legacy(const rct::key &legacy_base_spend_pubkey, + const crypto::secret_key &legacy_view_privkey, + const std::function &check_key_image_is_known_func, + const std::unordered_map> &chunk_basic_records_per_tx, + const std::list &chunk_contextual_key_images, + hw::device &hwdev, + std::unordered_map &found_enote_records_out, + std::unordered_map &found_spent_key_images_out) +{ + found_enote_records_out.clear(); + found_spent_key_images_out.clear(); + + // 1. check if any legacy owned enotes have been spent in this chunk (key image matches) + auto key_image_handler = + [&](const SpEnoteSpentContextV1 &spent_context, const crypto::key_image &key_image) + { + // ask callback if key image is known (i.e. if the key image is attached to an owned enote acquired before + // this chunk) + if (check_key_image_is_known_func(key_image)) + { + // a. record the found spent key image + found_spent_key_images_out[key_image]; + + // b. update its spent context (use update instead of assignment in case of duplicates) + try_update_enote_spent_context_v1(spent_context, found_spent_key_images_out[key_image]); + } + }; + + for (const SpContextualKeyImageSetV1 &contextual_key_image_set : chunk_contextual_key_images) + { + for (const crypto::key_image &key_image : contextual_key_image_set.legacy_key_images) + key_image_handler(contextual_key_image_set.spent_context, key_image); + } + + // 2. check for legacy owned enotes in this chunk + LegacyIntermediateEnoteRecord new_enote_record; + + for (const auto &tx_basic_records : chunk_basic_records_per_tx) + { + for (const ContextualBasicRecordVariant &contextual_basic_record : tx_basic_records.second) + { + if (!contextual_basic_record.is_type()) + continue; + + try + { + // a. check if we own the enote by attempting to convert it to an intermediate enote record + if (!try_get_legacy_intermediate_enote_record( + contextual_basic_record.unwrap().record, + legacy_base_spend_pubkey, + legacy_view_privkey, + hwdev, + new_enote_record)) + continue; + + // b. we found an owned enote, so handle it + update_with_new_intermediate_record_legacy(new_enote_record, + origin_context_ref(contextual_basic_record), + found_enote_records_out); + } catch (...) {} + } + } +} +//------------------------------------------------------------------------------------------------------------------- +void process_chunk_full_legacy(const rct::key &legacy_base_spend_pubkey, + const crypto::secret_key &legacy_spend_privkey, + const crypto::secret_key &legacy_view_privkey, + const std::function &check_key_image_is_known_func, + const std::unordered_map> &chunk_basic_records_per_tx, + const std::list &chunk_contextual_key_images, + hw::device &hwdev, + std::unordered_map &found_enote_records_out, + std::unordered_map &found_spent_key_images_out) +{ + found_enote_records_out.clear(); + found_spent_key_images_out.clear(); + + // 1. check if any legacy owned enotes acquired before this chunk were spent in this chunk (key image matches) + auto key_image_handler = + [&](const SpEnoteSpentContextV1 &spent_context, const crypto::key_image &key_image) + { + // a. ask callback if key image is known (i.e. if the key image is attached to an owned enote acquired before + // this chunk) + if (check_key_image_is_known_func(key_image)) + { + // i. record the found spent key image + found_spent_key_images_out[key_image]; + + // ii. update its spent context (use update instead of assignment in case of duplicates) + try_update_enote_spent_context_v1(spent_context, found_spent_key_images_out[key_image]); + } + }; + + for (const SpContextualKeyImageSetV1 &contextual_key_image_set : chunk_contextual_key_images) + { + for (const crypto::key_image &key_image : contextual_key_image_set.legacy_key_images) + key_image_handler(contextual_key_image_set.spent_context, key_image); + } + + // 2. check for legacy owned enotes in this chunk + LegacyEnoteRecord new_enote_record; + + for (const auto &tx_basic_records : chunk_basic_records_per_tx) + { + for (const ContextualBasicRecordVariant &contextual_basic_record : tx_basic_records.second) + { + if (!contextual_basic_record.is_type()) + continue; + + try + { + // a. check if we own the enote by attempting to convert it to a full enote record + if (!try_get_legacy_enote_record( + contextual_basic_record.unwrap().record, + legacy_base_spend_pubkey, + legacy_spend_privkey, + legacy_view_privkey, + hwdev, + new_enote_record)) + continue; + + // b. we found an owned enote, so handle it + update_with_new_record_legacy(new_enote_record, + origin_context_ref(contextual_basic_record), + chunk_contextual_key_images, + found_enote_records_out, + found_spent_key_images_out); + } catch (...) {} + } + } +} +//------------------------------------------------------------------------------------------------------------------- +void process_chunk_intermediate_sp(const rct::key &jamtis_spend_pubkey, + const crypto::x25519_secret_key &xk_unlock_amounts, + const crypto::x25519_secret_key &xk_find_received, + const crypto::secret_key &s_generate_address, + const jamtis::jamtis_address_tag_cipher_context &cipher_context, + const std::unordered_map> &chunk_basic_records_per_tx, + std::unordered_map &found_enote_records_out) +{ + found_enote_records_out.clear(); + + // check for owned enotes in this chunk (non-self-send intermediate scanning pass) + SpIntermediateEnoteRecordV1 new_enote_record; + + for (const auto &tx_basic_records : chunk_basic_records_per_tx) + { + for (const ContextualBasicRecordVariant &contextual_basic_record : tx_basic_records.second) + { + if (!contextual_basic_record.is_type()) + continue; + + try + { + // a. check if we own the enote by attempting to convert it to an intermediate enote record + if (!try_get_intermediate_enote_record_v1( + contextual_basic_record.unwrap().record, + jamtis_spend_pubkey, + xk_unlock_amounts, + xk_find_received, + s_generate_address, + cipher_context, + new_enote_record)) + continue; + + // b. we found an owned enote, so handle it + update_with_new_intermediate_record_sp(new_enote_record, + origin_context_ref(contextual_basic_record), + found_enote_records_out); + } catch (...) {} + } + } +} +//------------------------------------------------------------------------------------------------------------------- +void process_chunk_full_sp(const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + const crypto::x25519_secret_key &xk_unlock_amounts, + const crypto::x25519_secret_key &xk_find_received, + const crypto::secret_key &s_generate_address, + const jamtis::jamtis_address_tag_cipher_context &cipher_context, + const std::function &check_key_image_is_known_func, + const std::unordered_map> &chunk_basic_records_per_tx, + const std::list &chunk_contextual_key_images, + std::unordered_map &found_enote_records_out, + std::unordered_map &found_spent_sp_key_images_out, + std::unordered_map &legacy_key_images_in_sp_selfspends_out) +{ + found_enote_records_out.clear(); + found_spent_sp_key_images_out.clear(); + legacy_key_images_in_sp_selfspends_out.clear(); + + // 1. check if any owned enotes acquired before this chunk were spent in this chunk (key image matches) + std::unordered_set txs_have_spent_enotes; + + auto key_image_handler = + [&](const SpEnoteSpentContextV1 &spent_context, const crypto::key_image &key_image) + { + // a. ask callback if key image is known (i.e. if the key image is attached to an owned enote acquired before + // this chunk) + if (check_key_image_is_known_func(key_image)) + { + // i. record the found spent key image + found_spent_sp_key_images_out[key_image]; + + // ii. update its spent context (use update instead of assignment in case of duplicates) + try_update_enote_spent_context_v1(spent_context, found_spent_sp_key_images_out[key_image]); + + // iii. record tx id of the tx that contains this key image (this tx spent an enote that we acquired + // before this chunk) + txs_have_spent_enotes.insert(spent_context.transaction_id); + } + }; + + for (const SpContextualKeyImageSetV1 &contextual_key_image_set : chunk_contextual_key_images) + { + // - We don't check if legacy key images are known from before this chunk because during a comprehensive view-only + // scan legacy key images are not computable by the legacy view key, so there may be owned legacy enotes with + // unknown key images. This means there may be txs in this chunk with our selfsends but only legacy key images + // that can't be identified - so we need to do a selfsend check on all of those txs. All legacy key images in + // txs that have both legacy key images and seraphis selfsends will be recorded along with their spent contexts + // for the caller to cache in preparation for when they are able to match key images with legacy enotes. + + // a. invoke the key image handler for seraphis key images in the chunk + for (const crypto::key_image &key_image : contextual_key_image_set.sp_key_images) + key_image_handler(contextual_key_image_set.spent_context, key_image); + + // b. save tx ids of txs that contain at least one legacy key image, so they can be examined by the selfsend pass + if (contextual_key_image_set.legacy_key_images.size() > 0) + txs_have_spent_enotes.insert(contextual_key_image_set.spent_context.transaction_id); + } + + // 2. check if this chunk contains owned enotes (non-self-send pass) + SpEnoteRecordV1 new_enote_record; + + for (const auto &tx_basic_records : chunk_basic_records_per_tx) + { + for (const ContextualBasicRecordVariant &contextual_basic_record : tx_basic_records.second) + { + if (!contextual_basic_record.is_type()) + continue; + + try + { + // a. check if we own the enote by attempting to convert it to a full enote record + if (!try_get_enote_record_v1_plain( + contextual_basic_record.unwrap().record, + jamtis_spend_pubkey, + k_view_balance, + xk_unlock_amounts, + xk_find_received, + s_generate_address, + cipher_context, + new_enote_record)) + continue; + + // b. we found an owned enote, so handle it + // - this will also check if the enote was spent in this chunk, and update 'txs_have_spent_enotes' + // accordingly + update_with_new_record_sp(new_enote_record, + origin_context_ref(contextual_basic_record), + chunk_contextual_key_images, + found_enote_records_out, + found_spent_sp_key_images_out, + txs_have_spent_enotes); + } catch (...) {} + } + } + + // 3. check for owned enotes in this chunk (self-send passes) + // - a selfsend pass identifies owned selfsend enotes in txs that have been flagged, and then flags txs where + // those enotes have been spent in this chunk + // - we loop through selfsend passes until no more txs are flagged + while (txs_have_spent_enotes.size() > 0) + { + txs_have_spent_enotes = + process_chunk_sp_selfsend_pass(txs_have_spent_enotes, + jamtis_spend_pubkey, + k_view_balance, + xk_find_received, + s_generate_address, + cipher_context, + chunk_basic_records_per_tx, + chunk_contextual_key_images, + found_enote_records_out, + found_spent_sp_key_images_out, + legacy_key_images_in_sp_selfspends_out); + } +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace scanning +} //namespace sp diff --git a/src/seraphis_main/scan_balance_recovery_utils.h b/src/seraphis_main/scan_balance_recovery_utils.h new file mode 100644 index 0000000000..709b1ae957 --- /dev/null +++ b/src/seraphis_main/scan_balance_recovery_utils.h @@ -0,0 +1,221 @@ +// Copyright (c) 2022, 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 performing balance recovery. + +#pragma once + +//local headers +#include "contextual_enote_record_types.h" +#include "crypto/crypto.h" +#include "crypto/x25519.h" +#include "cryptonote_basic/subaddress_index.h" +#include "device/device.hpp" +#include "enote_record_types.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/jamtis_address_tag_utils.h" +#include "seraphis_core/legacy_enote_types.h" +#include "seraphis_core/tx_extra.h" +#include "seraphis_crypto/sp_crypto_utils.h" + +//third party headers + +//standard headers +#include +#include +#include +#include + +//forward declarations + + +namespace sp +{ +namespace scanning +{ + +/** +* brief: try_find_legacy_enotes_in_tx - obtain contextual basic records from a legacy tx's contents +* param: legacy_base_spend_pubkey - +* param: legacy_subaddress_map - +* param: legacy_view_privkey - +* param: block_index - +* param: block_timestamp - +* param: transaction_id - +* param: total_enotes_before_tx - number of legacy enotes ordered before this tx (set to '0' if tx is non-ledger) +* param: unlock_time - +* param: tx_memo - +* param: enotes_in_tx - +* param: origin_status - +* inoutparam: hwdev - +* outparam: basic_records_in_tx_out - +*/ +bool try_find_legacy_enotes_in_tx(const rct::key &legacy_base_spend_pubkey, + const std::unordered_map &legacy_subaddress_map, + const crypto::secret_key &legacy_view_privkey, + const std::uint64_t block_index, + const std::uint64_t block_timestamp, + const rct::key &transaction_id, + const std::uint64_t total_enotes_before_tx, + const std::uint64_t unlock_time, + const TxExtra &tx_memo, + const std::vector &enotes_in_tx, + const SpEnoteOriginStatus origin_status, + hw::device &hwdev, + std::list &basic_records_in_tx_out); +/** +* brief: try_find_sp_enotes_in_tx - obtain contextual basic records from a seraphis tx's contents +* param: xk_find_received - +* param: block_index - +* param: block_timestamp - +* param: transaction_id - +* param: total_enotes_before_tx - number of seraphis enotes ordered before this tx (set to '0' if tx is non-ledger) +* param: input_context - +* param: tx_supplement - +* param: enotes_in_tx - +* param: origin_status - +* outparam: basic_records_in_tx_out - +*/ +bool try_find_sp_enotes_in_tx(const crypto::x25519_secret_key &xk_find_received, + const std::uint64_t block_index, + const std::uint64_t block_timestamp, + const rct::key &transaction_id, + const std::uint64_t total_enotes_before_tx, + const rct::key &input_context, + const SpTxSupplementV1 &tx_supplement, + const std::vector &enotes_in_tx, + const SpEnoteOriginStatus origin_status, + std::list &basic_records_in_tx_out); +/** +* brief: try_collect_key_images_from_tx - if a tx has key images, collect them into a contextual key image set +* param: block_index - +* param: block_timestamp - +* param: transaction_id - +* param: legacy_key_images_in_tx - +* param: sp_key_images_in_tx - +* param: spent_status - +* outparam: contextual_key_images_out - +* return: true if a set was made (there was at least one key image available) +*/ +bool try_collect_key_images_from_tx(const std::uint64_t block_index, + const std::uint64_t block_timestamp, + const rct::key &transaction_id, + std::vector legacy_key_images_in_tx, + std::vector sp_key_images_in_tx, + const SpEnoteSpentStatus spent_status, + SpContextualKeyImageSetV1 &contextual_key_images_out); +/** +* brief: process_chunk_intermediate_legacy - process a chunk of contextual basic records with a legacy view privkey +* param: legacy_base_spend_pubkey - +* param: legacy_view_privkey - +* param: check_key_image_is_known_func - callback for checking if a key image is known by the caller +* param: chunk_basic_records_per_tx - [ tx id : contextual basic record ] +* param: chunk_contextual_key_images - +* inoutparam: hwdev - +* outparam: found_enote_records_out - [ H32(Ko, a) : legacy contextual intermediate enote record ] +* note: mapped to H32(Ko, a) so enotes with the same key image but different amounts will be recovered +* outparam: found_spent_key_images_out - [ KI : spent context ] +*/ +void process_chunk_intermediate_legacy(const rct::key &legacy_base_spend_pubkey, + const crypto::secret_key &legacy_view_privkey, + const std::function &check_key_image_is_known_func, + const std::unordered_map> &chunk_basic_records_per_tx, + const std::list &chunk_contextual_key_images, + hw::device &hwdev, + // note: mapped to H32(Ko, a) so enotes with the same key image but different amounts will be recovered + std::unordered_map &found_enote_records_out, + std::unordered_map &found_spent_key_images_out); +/** +* brief: process_chunk_full_legacy - process a chunk of contextual basic records with legacy view and spend privkeys +* param: legacy_base_spend_pubkey - +* param: legacy_spend_privkey - +* param: legacy_view_privkey - +* param: check_key_image_is_known_func - callback for checking if a key image is known by the caller +* param: chunk_basic_records_per_tx - [ tx id : contextual basic record ] +* param: chunk_contextual_key_images - +* inoutparam: hwdev - +* outparam: found_enote_records_out - [ H32(Ko, a) : legacy contextual intermediate enote record ] +* note: mapped to H32(Ko, a) so enotes with the same key image but different amounts will be recovered +* outparam: found_spent_key_images_out - [ KI : spent context ] +*/ +void process_chunk_full_legacy(const rct::key &legacy_base_spend_pubkey, + const crypto::secret_key &legacy_spend_privkey, + const crypto::secret_key &legacy_view_privkey, + const std::function &check_key_image_is_known_func, + const std::unordered_map> &chunk_basic_records_per_tx, + const std::list &chunk_contextual_key_images, + hw::device &hwdev, + std::unordered_map &found_enote_records_out, + std::unordered_map &found_spent_key_images_out); +/** +* brief: process_chunk_intermediate_sp - process a chunk of contextual basic records with seraphis {kx_ua, kx_fr, s_ga} +* param: jamtis_spend_pubkey - +* param: xk_unlock_amounts - +* param: xk_find_received - +* param: s_generate_address - +* param: cipher_context - +* param: chunk_basic_records_per_tx - [ tx id : contextual basic record ] +* outparam: found_enote_records_out - [ Ko : legacy contextual intermediate enote record ] +*/ +void process_chunk_intermediate_sp(const rct::key &jamtis_spend_pubkey, + const crypto::x25519_secret_key &xk_unlock_amounts, + const crypto::x25519_secret_key &xk_find_received, + const crypto::secret_key &s_generate_address, + const jamtis::jamtis_address_tag_cipher_context &cipher_context, + const std::unordered_map> &chunk_basic_records_per_tx, + std::unordered_map &found_enote_records_out); +/** +* brief: process_chunk_full_sp - process a chunk of contextual basic records with seraphis view-balance privkey +* param: jamtis_spend_pubkey - +* param: k_view_balance - +* param: xk_unlock_amounts - +* param: xk_find_received - +* param: s_generate_address - +* param: cipher_context - +* param: check_key_image_is_known_func - callback for checking if a key image is known by the caller +* param: chunk_basic_records_per_tx - [ tx id : contextual basic record ] +* param: chunk_contextual_key_images - +* outparam: found_enote_records_out - [ seraphis KI : legacy contextual intermediate enote record ] +* outparam: found_spent_sp_key_images_out - [ seraphis KI : spent context ] +* outparam: legacy_key_images_in_sp_selfspends_out - [ legacy KI : spent context ] +*/ +void process_chunk_full_sp(const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + const crypto::x25519_secret_key &xk_unlock_amounts, + const crypto::x25519_secret_key &xk_find_received, + const crypto::secret_key &s_generate_address, + const jamtis::jamtis_address_tag_cipher_context &cipher_context, + const std::function &check_key_image_is_known_func, + const std::unordered_map> &chunk_basic_records_per_tx, + const std::list &chunk_contextual_key_images, + std::unordered_map &found_enote_records_out, + std::unordered_map &found_spent_sp_key_images_out, + std::unordered_map &legacy_key_images_in_sp_selfspends_out); + +} //namespace scanning +} //namespace sp diff --git a/src/seraphis_main/scan_chunk_consumer.h b/src/seraphis_main/scan_chunk_consumer.h new file mode 100644 index 0000000000..f30eec8747 --- /dev/null +++ b/src/seraphis_main/scan_chunk_consumer.h @@ -0,0 +1,93 @@ +// Copyright (c) 2022, 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. + +// Dependency injector for consuming data acquired by the candidacy phase of balance recovery. + +#pragma once + +//local headers +#include "contextual_enote_record_types.h" +#include "ringct/rctTypes.h" +#include "scan_ledger_chunk.h" + +//third party headers + +//standard headers +#include + +//forward declarations +namespace sp +{ +namespace scanning +{ + struct ChunkData; + struct ContiguityMarker; +} +} + +namespace sp +{ +namespace scanning +{ + +//// +// ChunkConsumer +// - provides an API for consuming chunks of enotes from find-received scanning +/// +class ChunkConsumer +{ +public: +//destructor + virtual ~ChunkConsumer() = default; + +//overloaded operators + /// disable copy/move (this is an abstract base class) + ChunkConsumer& operator=(ChunkConsumer&&) = delete; + +//member functions + /// get index of first block the consumer cares about + virtual std::uint64_t refresh_index() const = 0; + /// get index of first block the consumer wants to have scanned + virtual std::uint64_t desired_first_block() const = 0; + /// get a marker for the next block > the specified index + /// ERROR: return { -1, boost::none } if there is no such block + virtual ContiguityMarker get_next_block(const std::uint64_t block_index) const = 0; + /// get a marker for the nearest block <= the specified index + /// ERROR: return { refresh_index - 1, boost::none } if there is no such block + virtual ContiguityMarker get_nearest_block(const std::uint64_t block_index) const = 0; + + /// consume a chunk of basic enote records and save the results + virtual void consume_nonledger_chunk(const SpEnoteOriginStatus nonledger_origin_status, const ChunkData &data) = 0; + virtual void consume_onchain_chunk(const LedgerChunk &chunk, + const rct::key &alignment_block_id, + const std::uint64_t first_new_block, + const std::vector &new_block_ids) = 0; +}; + +} //namespace scanning +} //namespace sp diff --git a/src/seraphis_main/scan_context.h b/src/seraphis_main/scan_context.h new file mode 100644 index 0000000000..32dfa5dbe5 --- /dev/null +++ b/src/seraphis_main/scan_context.h @@ -0,0 +1,102 @@ +// Copyright (c) 2022, 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. + +// Dependency injectors for managing the find-received step of enote scanning. Intended to be stateful, managing +// a connection to a context that contains enotes and key images, and linking together successive 'get chunk' calls. + +#pragma once + +//local headers +#include "seraphis_main/scan_core_types.h" +#include "seraphis_main/scan_ledger_chunk.h" + +//third party headers + +//standard headers +#include + +//forward declarations + + +namespace sp +{ +namespace scanning +{ + +//// +// ScanContextNonLedger +// - manages a source of non-ledger-based enote scanning chunks +/// +class ScanContextNonLedger +{ +public: +//destructor + virtual ~ScanContextNonLedger() = default; + +//overloaded operators + /// disable copy/move (this is a virtual base class) + ScanContextNonLedger& operator=(ScanContextNonLedger&&) = delete; + +//member functions + /// get a scanning chunk for the nonledger txs associated with this context + virtual void get_nonledger_chunk(ChunkData &chunk_out) = 0; + /// test if scanning has been aborted + /// EXPECTATION: if this returns true then all subsequent calls to 'get chunk' should return an empty chunk + virtual bool is_aborted() const = 0; +}; + +//// +// ScanContextLedger +// - manages a source of ledger-based enote scanning chunks (i.e. finding potentially owned enotes in a ledger) +/// +class ScanContextLedger +{ +public: +//destructor + virtual ~ScanContextLedger() = default; + +//overloaded operators + /// disable copy/move (this is a virtual base class) + ScanContextLedger& operator=(ScanContextLedger&&) = delete; + +//member functions + /// tell the scanning context a block index to start scanning from + virtual void begin_scanning_from_index(const std::uint64_t initial_start_index, + const std::uint64_t max_chunk_size_hint) = 0; + /// get the next available onchain chunk (must be contiguous with the last chunk acquired since starting to scan) + /// note: if there is no chunk to return, return an empty chunk representing the top of the current chain + virtual std::unique_ptr get_onchain_chunk() = 0; + /// tell the scanning context to stop its scanning process (should be no-throw no-fail) + virtual void terminate_scanning() = 0; + /// test if scanning has been aborted + /// EXPECTATION: if this returns true then all subsequent calls to 'get chunk' should return an empty chunk + virtual bool is_aborted() const = 0; +}; + +} //namespace scanning +} //namespace sp diff --git a/src/seraphis_main/scan_core_types.h b/src/seraphis_main/scan_core_types.h new file mode 100644 index 0000000000..9e82a0bf58 --- /dev/null +++ b/src/seraphis_main/scan_core_types.h @@ -0,0 +1,96 @@ +// Copyright (c) 2022, 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 types for scanning enotes and key images to recover a user's balance. + +// PRECONDITIONS: +// 1. chunks must be built from an atomic view of the source cache (ledger, unconfirmed cache, offchain cache) +// 2. chunk data: contextual_key_images must reference a tx recorded in basic_records_per_tx (even if you +// need to add empty map entries to achieve that) +// 3. any call to get a chunk from a scanning context should produce a chunk that is at least as fresh as any +// other chunk obtained from that context (atomic ordering) +// 4. any call to consume a chunk in a chunk consumer should resolve all side-effects observable via the consumer's +// interface by the time the call is complete (e.g. any changes to block ids observable by get_nearest_block() need +// to be completed during the 'consume chunk' call) + +#pragma once + +//local headers +#include "ringct/rctTypes.h" +#include "seraphis_main/contextual_enote_record_types.h" + +//third party headers + +//standard headers +#include +#include + +//forward declarations + + +namespace sp +{ +namespace scanning +{ + +//// +// ChunkData +// - contextual basic enote records for owned enote candidates in a set of scanned txs (at a single point in time) +// - key images from each of the txs recorded in the basic records map +// - add empty entries to that map if you want to include the key images of txs without owned enote candidates, e.g. +// for legacy scanning where key images can appear in a tx even if none of the tx outputs were sent to you +// - LEGACY OPTIMIZATION (optional): only key images of rings which include a received enote MUST be collected +// - if filtering to get those key images is not possible then including all key images works too +/// +struct ChunkData final +{ + /// owned enote candidates in a set of scanned txs (mapped to tx id) + std::unordered_map> basic_records_per_tx; + /// key images from txs with owned enote candidates in the set of scanned txs + std::list contextual_key_images; +}; + +//// +// ChunkContext +// - prefix block id: id of block that comes before the chunk range, used for contiguity checks between chunks and with +// a chunk consumer +// - chunk range (in block indices): [start index, end index) +// - end index = start index + num blocks +/// +struct ChunkContext final +{ + /// block id at 'start index - 1' (implicitly ignored if start_index == 0) + rct::key prefix_block_id; + /// start index + std::uint64_t start_index; + /// block ids in range [start index, end index) + std::vector block_ids; +}; + +} //namespace scanning +} //namespace sp diff --git a/src/seraphis_main/scan_ledger_chunk.h b/src/seraphis_main/scan_ledger_chunk.h new file mode 100644 index 0000000000..98d61d058e --- /dev/null +++ b/src/seraphis_main/scan_ledger_chunk.h @@ -0,0 +1,79 @@ +// Copyright (c) 2022, 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. + +// Interface for implementing a ledger chunk. + +#pragma once + +//local headers +#include "misc_log_ex.h" +#include "ringct/rctTypes.h" + +//third party headers + +//standard headers +#include + +//forward declarations +namespace sp +{ +namespace scanning +{ + struct ChunkData; + struct ChunkContext; +} +} + +namespace sp +{ +namespace scanning +{ + +//// +// LedgerChunk +// - interface for implementing a ledger chunk; implementations may store data directly or asynchronously +// +// - chunk context: tracks where this chunk exists on-chain +// - chunk data: data obtained from scanning the chunk (per subconsumer) +// +// - subconsumers: a ledger chunk can store chunk data for multiple subconsumers (so they can share a chunk context) +/// +class LedgerChunk +{ +public: + virtual ~LedgerChunk() = default; + /// chunk context (includes chunk block range, prefix block id, and chunk block ids) + virtual const ChunkContext& get_context() const = 0; + /// chunk data (includes owned enote candidates and key image candidates) + virtual const ChunkData* try_get_data(const rct::key &subconsumer_id) const = 0; + /// set of subconsumers associated with this ledger chunk + virtual const std::vector& subconsumer_ids() const = 0; +}; + +} //namespace scanning +} //namespace sp diff --git a/src/seraphis_main/scan_machine.cpp b/src/seraphis_main/scan_machine.cpp new file mode 100644 index 0000000000..bdb02b9222 --- /dev/null +++ b/src/seraphis_main/scan_machine.cpp @@ -0,0 +1,621 @@ +// Copyright (c) 2022, 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 "scan_machine.h" + +//local headersx +#include "ringct/rctTypes.h" +#include "seraphis_crypto/math_utils.h" +#include "seraphis_main/scan_chunk_consumer.h" +#include "seraphis_main/scan_context.h" +#include "seraphis_main/scan_core_types.h" +#include "seraphis_main/scan_ledger_chunk.h" +#include "seraphis_main/scan_machine_types.h" +#include "seraphis_main/scan_misc_utils.h" + +//third party headers + +//standard headers +#include +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis" + +namespace sp +{ +namespace scanning +{ + +//// +// ContiguityCheckResult +/// +enum class ContiguityCheckResult : unsigned char +{ + NEED_PARTIALSCAN, + NEED_FULLSCAN, + SUCCESS +}; + +//------------------------------------------------------------------------------------------------------------------- +// reorg avoidance depth: this is the number of extra blocks to scan below our desired start index in case there was a +// reorg affecting blocks lower than that start index +// - we use an exponential back-off because if a fullscan fails then the true location of alignment divergence is +// unknown; the distance between the desired start index and the lowest scannable index may be very large, so if a +// fixed back-off were used it could take many fullscan attempts to find the point of divergence +//------------------------------------------------------------------------------------------------------------------- +static std::uint64_t get_reorg_avoidance_depth(const std::uint64_t reorg_avoidance_increment, + const std::uint64_t num_reorg_avoidance_backoffs) +{ + // 1. start at a depth of zero + // - this allows us to avoid accidentally reorging your data store if the scanning backend only has a portion + // of the blocks in your initial reorg avoidance depth range available when 'get chunk' is called (in the case + // where there wasn't actually a reorg and the backend is just catching up) + if (num_reorg_avoidance_backoffs == 0) + return 0; + + // 2. check that the increment is not 0 + // - check this after one backoff to support unit tests that set the increment to 0 + CHECK_AND_ASSERT_THROW_MES(reorg_avoidance_increment > 0, + "seraphis scan state machine (get reorg avoidance depth): requested a reorg avoidance backoff with zero " + "reorg avoidance increment."); + + // 3. 10 ^ (num requests - 1) * increment + return math::saturating_mul(math::uint_pow(10, num_reorg_avoidance_backoffs - 1), reorg_avoidance_increment, -1); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static std::uint64_t get_estimated_start_scan_index(const std::uint64_t reorg_avoidance_increment, + const std::uint64_t num_reorg_avoidance_backoffs, + const std::uint64_t lowest_scannable_index, + const std::uint64_t desired_start_index) +{ + // 1. set reorg avoidance depth + const std::uint64_t reorg_avoidance_depth{ + get_reorg_avoidance_depth(reorg_avoidance_increment, num_reorg_avoidance_backoffs) + }; + + // 2. initial block to scan = max(desired first block - reorg depth, chunk consumer's min scan index) + return math::saturating_sub(desired_start_index, reorg_avoidance_depth, lowest_scannable_index); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void set_initial_contiguity_marker(const std::uint64_t reorg_avoidance_increment, + const std::uint64_t num_reorg_avoidance_backoffs, + const ChunkConsumer &chunk_consumer, + ContiguityMarker &contiguity_marker_out) +{ + // 1. get index of the first block we want to scan + // - this is only an estimate since the chunk consumer may not have the block at this exact index cached + const std::uint64_t estimated_start_scan_index{ + get_estimated_start_scan_index(reorg_avoidance_increment, + num_reorg_avoidance_backoffs, + chunk_consumer.refresh_index(), + chunk_consumer.desired_first_block()) + }; + + // 2. set our initial point of contiguity is the consumer's block nearest to the block < our estimated start index, + // or the consumer's prefix block + contiguity_marker_out = chunk_consumer.get_nearest_block(estimated_start_scan_index - 1); + + CHECK_AND_ASSERT_THROW_MES(contiguity_marker_out.block_index + 1 >= chunk_consumer.refresh_index(), + "sp scan machine (set initial contiguity marker): contiguity marker is too far below refresh index."); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool contiguity_check(const ContiguityMarker &marker_A, const ContiguityMarker &marker_B) +{ + // 1. a marker with unspecified block id is contiguous with all markers below and equal to its index (but not + // contiguous with markers above it) + // note: this rule exists so that if the chain's top block is below our refresh index, we will be considered + // contiguous with it and won't erroneously think we have encountered a reorg (i.e. a broken contiguity); + // to see why that matters, change the '<=' to '==' then step through the unit tests that break + if (!marker_A.block_id && + marker_B.block_index + 1 <= marker_A.block_index + 1) + return true; + + if (!marker_B.block_id && + marker_A.block_index + 1 <= marker_B.block_index + 1) + return true; + + // 2. otherwise, indices must match + if (marker_A.block_index != marker_B.block_index) + return false; + + // 3. specified block ids must match + if (marker_A.block_id && + marker_B.block_id && + marker_A.block_id != marker_B.block_id) + return false; + + // 4. unspecified block ids automatically match with specified and unspecified block ids + return true; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static ContiguityCheckResult new_chunk_contiguity_check(const ContiguityMarker &contiguity_marker, + const ChunkContext &chunk_context, + const std::uint64_t first_contiguity_index) +{ + // 1. success case: check if this chunk is contiguous with our marker + if (contiguity_check( + contiguity_marker, + ContiguityMarker{ + chunk_context.start_index - 1, + chunk_context.start_index > 0 + ? boost::optional{chunk_context.prefix_block_id} + : boost::none + } + )) + return ContiguityCheckResult::SUCCESS; + + // 2. failure case: the chunk is not contiguous, check if we need to full scan + // - in this case, there was a reorg that affected our first expected point of contiguity (i.e. we obtained no new + // chunks that were contiguous with our existing known contiguous chain) + // note: +1 in case either index is '-1' + if (first_contiguity_index + 1 >= contiguity_marker.block_index + 1) + return ContiguityCheckResult::NEED_FULLSCAN; + + // 3. failure case: the chunk is not contiguous, but we don't need a full scan + // - there was a reorg detected but there is new chunk data that wasn't affected + return ContiguityCheckResult::NEED_PARTIALSCAN; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static ScanMachineState machine_state_from_contiguity_result(const ContiguityCheckResult contiguity_check_result, + const ScanMachineMetadata &metadata) +{ + CHECK_AND_ASSERT_THROW_MES(contiguity_check_result != ContiguityCheckResult::SUCCESS, + "seraphis scan machine (machine state from contiguity result): cannot convert success to a new machine state."); + + // convert the contiguity check result to a new machine state + if (contiguity_check_result == ContiguityCheckResult::NEED_PARTIALSCAN) + return ScanMachineNeedPartialscan{ .metadata = metadata }; + else if (contiguity_check_result == ContiguityCheckResult::NEED_FULLSCAN) + return ScanMachineNeedFullscan{ .metadata = metadata }; + + CHECK_AND_ASSERT_THROW_MES(false, "seraphis scan machine (machine state from contiguity result): unknown result."); + return ScanMachineTerminated{ .result = ScanMachineResult::FAIL }; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void update_alignment_marker(const ChunkConsumer &chunk_consumer, + const std::uint64_t start_index, + const std::vector &block_ids, + ContiguityMarker &alignment_marker_inout) +{ + // trace through the block ids to find the highest one that aligns with the chunk consumer's cached block ids + for (auto ids_it{block_ids.begin()}; ids_it != block_ids.end(); ++ids_it) + { + // a. get the chunk consumer's block index closest to this block (i.e. >= this block) in the input set + const std::uint64_t block_index{ + start_index + std::distance(block_ids.begin(), ids_it) + }; + const ContiguityMarker consumer_closest_block{chunk_consumer.get_next_block(block_index - 1)}; + + // b. exit if the consumer's block is not within the input block range + if (consumer_closest_block.block_index + 1 < start_index + 1 || + consumer_closest_block.block_index + 1 >= start_index + block_ids.size() + 1) + return; + + // c, sanity check + // - this is after the range check in case the consumer returned a null marker + CHECK_AND_ASSERT_THROW_MES(consumer_closest_block.block_index + 1 >= block_index + 1, + "seraphis scan state machine (update alignment marker): consumer's closest block index is below the " + "specified block index."); + + // d. move to the consumer's closest block's index + std::advance(ids_it, consumer_closest_block.block_index - block_index); + + // e. exit if the consumer is not aligned with this block + // - we are automatically aligned if the consumer's block id is null + if (consumer_closest_block.block_id && + !(*ids_it == *consumer_closest_block.block_id)) + return; + + // f. update the alignment marker + alignment_marker_inout.block_index = block_index; + alignment_marker_inout.block_id = *ids_it; + } +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static std::vector get_aligned_block_ids(const ChunkConsumer &chunk_consumer, + const ChunkContext &chunk_context, + ContiguityMarker &alignment_marker_inout) +{ + // 1. update the alignment marker + update_alignment_marker(chunk_consumer, + chunk_context.start_index, + chunk_context.block_ids, + alignment_marker_inout); + + // 2. sanity checks + CHECK_AND_ASSERT_THROW_MES(alignment_marker_inout.block_index + 1 >= chunk_context.start_index, + "seraphis scan state machine (align block ids): chunk start index exceeds the post-alignment block (bug)."); + CHECK_AND_ASSERT_THROW_MES(alignment_marker_inout.block_index + 1 - chunk_context.start_index <= + chunk_context.block_ids.size(), + "seraphis scan state machine (align block ids): the alignment range is larger than the chunk's block range " + "(bug)."); + + // 3. crop chunk block ids that are <= the alignment marker + return std::vector{ + std::next( + chunk_context.block_ids.begin(), + alignment_marker_inout.block_index + 1 - chunk_context.start_index + ), + chunk_context.block_ids.end() + }; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static ScanMachineState handle_nonempty_chunk(const ScanMachineMetadata &metadata, + const std::uint64_t first_contiguity_index, + const LedgerChunk &ledger_chunk, + const ContiguityMarker &contiguity_marker, + ChunkConsumer &chunk_consumer_inout) +{ + // note: we don't check if the scanning context is aborted here because the process could have been aborted after + // the chunk was acquired + const ChunkContext &chunk_context{ledger_chunk.get_context()}; + + // 1. verify this is a non-empty chunk + CHECK_AND_ASSERT_THROW_MES(!chunk_context_is_empty(chunk_context), + "seraphis scan state machine (handle nonempty chunk): chunk is empty unexpectedly."); + + // 2. check if this chunk is contiguous with the contiguity marker + // - if not contiguous then there must have been a reorg, so we need to rescan + const ContiguityCheckResult contiguity_check_result{ + new_chunk_contiguity_check(contiguity_marker, chunk_context, first_contiguity_index) + }; + + if (contiguity_check_result != ContiguityCheckResult::SUCCESS) + return machine_state_from_contiguity_result(contiguity_check_result, metadata); + + // 3. set alignment marker (assume we always start aligned) + // - alignment means a block id in a chunk matches the chunk consumer's block id at the alignment block index + ContiguityMarker alignment_marker{contiguity_marker}; + + // 4. align the chunk's block ids with the chunk consumer + // - update the point of alignment if this chunk overlaps with blocks known by the chunk consumer + // - crop the chunk's block ids to only include block ids unknown to the chunk consumer + const std::vector aligned_block_ids{ + get_aligned_block_ids(chunk_consumer_inout, chunk_context, alignment_marker) + }; + + // 5. validate chunk semantics + // - do this after checking the new chunk's scan status in case the chunk data is deferred; we don't want to block + // on accessing the data until we know we will need it + check_ledger_chunk_semantics(ledger_chunk, contiguity_marker.block_index); + + // 6. consume the chunk if it's not empty + // - if the chunk is empty after aligning, that means our chunk consumer already knows about the entire span + // of the chunk; we don't want to pass the chunk in, because there may be blocks in the NEXT chunk that + // our chunk consumer also knows about; we don't want the chunk consumer to think it needs to roll back its state + // to the top of this chunk + if (aligned_block_ids.size() > 0) + { + chunk_consumer_inout.consume_onchain_chunk(ledger_chunk, + alignment_marker.block_id ? *(alignment_marker.block_id) : rct::zero(), + alignment_marker.block_index + 1, + aligned_block_ids); + } + + // 7. set contiguity marker to last block of this chunk + CHECK_AND_ASSERT_THROW_MES(chunk_context.block_ids.size() > 0, + "seraphis scan state machine (handle nonempty chunk): no block ids (bug)."); + + const ContiguityMarker new_contiguity_marker{ + .block_index = chunk_context.start_index + chunk_context.block_ids.size() - 1, + .block_id = chunk_context.block_ids.back() + }; + + // 8. next scan state: scan another chunk + return ScanMachineDoScan{ + .metadata = metadata, + .contiguity_marker = new_contiguity_marker, + .first_contiguity_index = new_contiguity_marker.block_index + }; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static ScanMachineState handle_empty_chunk(const ScanMachineMetadata &metadata, + const std::uint64_t first_contiguity_index, + const LedgerChunk &ledger_chunk, + const ContiguityMarker &contiguity_marker, + ScanContextLedger &scan_context_inout, + ChunkConsumer &chunk_consumer_inout) +{ + const ChunkContext &chunk_context{ledger_chunk.get_context()}; + + // 1. verify that the chunk obtained is an empty chunk representing the top of the current blockchain + CHECK_AND_ASSERT_THROW_MES(chunk_context_is_empty(chunk_context), + "seraphis scan state machine (handle empty chunk): chunk is not empty as expected."); + + // 2. check if the scan process is aborted + // - when a scan process is aborted, the empty chunk returned may not represent the end of the chain, so we don't + // want to consume that chunk + if (scan_context_inout.is_aborted()) + return ScanMachineTerminated{ .result = ScanMachineResult::ABORTED }; + + // 3. verify that our termination chunk is contiguous with the chunks received so far + // - this can fail if a reorg dropped below our contiguity marker without replacing the dropped blocks, causing the + // first chunk obtained after the reorg to be this empty termination chunk + // note: this test won't fail if the chain's top index is below our contiguity marker when our contiguity marker has + // an unspecified block id; we don't care if the top index is lower than our scanning 'backstop' (i.e. + // lowest point in our chunk consumer) when we haven't actually scanned any blocks + const ContiguityCheckResult contiguity_check_result{ + new_chunk_contiguity_check(contiguity_marker, chunk_context, first_contiguity_index) + }; + + if (contiguity_check_result != ContiguityCheckResult::SUCCESS) + return machine_state_from_contiguity_result(contiguity_check_result, metadata); + + // 4. final update for our chunk consumer + // - we need to update with the termination chunk in case a reorg popped blocks, so the chunk consumer can roll back + // its state + chunk_consumer_inout.consume_onchain_chunk(ledger_chunk, + contiguity_marker.block_id ? *(contiguity_marker.block_id) : rct::zero(), + contiguity_marker.block_index + 1, + {}); + + // 5. no more scanning required + return ScanMachineTerminated{ .result = ScanMachineResult::SUCCESS }; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static ScanMachineState do_scan_pass(const ScanMachineMetadata &metadata, + const std::uint64_t first_contiguity_index, + const ContiguityMarker &contiguity_marker, + ScanContextLedger &scan_context_inout, + ChunkConsumer &chunk_consumer_inout) +{ + // 1. get a new chunk + std::unique_ptr new_chunk; + try + { + new_chunk = scan_context_inout.get_onchain_chunk(); + CHECK_AND_ASSERT_THROW_MES(new_chunk, "seraphis scan state machine (do scan pass): chunk obtained is null."); + } + catch (...) + { + LOG_ERROR("seraphis scan state machine (do scan pass): get chunk failed."); + throw; + } + + // 2. handle the chunk and return the next machine state + if (!chunk_context_is_empty(new_chunk->get_context())) + { + return handle_nonempty_chunk(metadata, + first_contiguity_index, + *new_chunk, + contiguity_marker, + chunk_consumer_inout); + } + else + { + return handle_empty_chunk(metadata, + first_contiguity_index, + *new_chunk, + contiguity_marker, + scan_context_inout, + chunk_consumer_inout); + } +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static ScanMachineState handle_need_fullscan(const ScanMachineNeedFullscan &state, const ChunkConsumer &chunk_consumer) +{ + // 1. set initial contiguity marker + ContiguityMarker start_scan_contiguity_marker; + set_initial_contiguity_marker(state.metadata.config.reorg_avoidance_increment, + state.metadata.fullscan_attempts, //exponential backoff as function of fullscan attempts, starting at 0 + chunk_consumer, + start_scan_contiguity_marker); + + // 2. record this scan attempt + ScanMachineMetadata next_metadata{state.metadata}; + next_metadata.fullscan_attempts += 1; + + // 3. fail if we have exceeded the max number of full scanning attempts (we appear to be in an infinite loop) + if (next_metadata.fullscan_attempts > 50) + { + LOG_ERROR("scan state machine (handle need fullscan): fullscan attempts exceeded 50 (sanity check fail)."); + return ScanMachineTerminated{ .result = ScanMachineResult::FAIL }; + } + + // 4. return the next state + return ScanMachineStartScan{ + .metadata = next_metadata, + .contiguity_marker = start_scan_contiguity_marker + }; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static ScanMachineState handle_need_partialscan(const ScanMachineNeedPartialscan &state, + const ChunkConsumer &chunk_consumer) +{ + // 1. set initial contiguity marker + ContiguityMarker start_scan_contiguity_marker; + set_initial_contiguity_marker(state.metadata.config.reorg_avoidance_increment, + 1, //in partial scans always back off by just one reorg avoidance increment + chunk_consumer, + start_scan_contiguity_marker); + + // 2. record this scan attempt + ScanMachineMetadata next_metadata{state.metadata}; + next_metadata.partialscan_attempts += 1; + + // 3. fail if we have exceeded the max number of partial scanning attempts (i.e. too many reorgs were detected, + // so now we abort) + if (next_metadata.partialscan_attempts > next_metadata.config.max_partialscan_attempts) + return ScanMachineTerminated{ .result = ScanMachineResult::FAIL }; + + // 4. return the next state + return ScanMachineStartScan{ + .metadata = next_metadata, + .contiguity_marker = start_scan_contiguity_marker + }; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static ScanMachineState handle_start_scan(const ScanMachineStartScan &state, + ScanContextLedger &scan_context_inout) +{ + try + { + // a. initialize the scanning context + scan_context_inout.begin_scanning_from_index(state.contiguity_marker.block_index + 1, + state.metadata.config.max_chunk_size_hint); + + // b. return the next state + return ScanMachineDoScan{ + .metadata = state.metadata, + .contiguity_marker = state.contiguity_marker, + .first_contiguity_index = state.contiguity_marker.block_index + }; + } + catch (...) { return ScanMachineTerminated{ .result = ScanMachineResult::FAIL }; } +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static ScanMachineState handle_do_scan(const ScanMachineDoScan &state, + ScanContextLedger &scan_context_inout, + ChunkConsumer &chunk_consumer_inout) +{ + // 1. perform one scan pass then update the status + ScanMachineState next_state; + try + { + next_state = do_scan_pass(state.metadata, + state.first_contiguity_index, + state.contiguity_marker, + scan_context_inout, + chunk_consumer_inout); + } + catch (...) { next_state = ScanMachineTerminated{ .result = ScanMachineResult::FAIL }; } + + // 2. try to terminate the scanning context if the next state is not another scan pass + try + { + if (!next_state.is_type()) + scan_context_inout.terminate_scanning(); + } catch (...) { LOG_ERROR("seraphis scan state machine (try handle do scan): scan context termination failed."); } + + return next_state; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool try_handle_need_fullscan(const ChunkConsumer &chunk_consumer, ScanMachineState &state_inout) +{ + const ScanMachineNeedFullscan *need_fullscan{state_inout.try_unwrap()}; + if (!need_fullscan) return false; + state_inout = handle_need_fullscan(*need_fullscan, chunk_consumer); + return true; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool try_handle_need_partialscan(const ChunkConsumer &chunk_consumer, ScanMachineState &state_inout) +{ + const ScanMachineNeedPartialscan *need_partialscan{state_inout.try_unwrap()}; + if (!need_partialscan) return false; + state_inout = handle_need_partialscan(*need_partialscan, chunk_consumer); + return true; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool try_handle_start_scan(ScanContextLedger &scan_context_inout, ScanMachineState &state_inout) +{ + const ScanMachineStartScan *start_scan{state_inout.try_unwrap()}; + if (!start_scan) return false; + state_inout = handle_start_scan(*start_scan, scan_context_inout); + return true; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool try_handle_do_scan(ScanContextLedger &scan_context_inout, + ChunkConsumer &chunk_consumer_inout, + ScanMachineState &state_inout) +{ + const ScanMachineDoScan *do_scan{state_inout.try_unwrap()}; + if (!do_scan) return false; + state_inout = handle_do_scan(*do_scan, scan_context_inout, chunk_consumer_inout); + return true; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool is_terminal_state_with_log(const ScanMachineState &state) +{ + const ScanMachineTerminated *terminated{state.try_unwrap()}; + + // 1. check if in a terminal state + if (!terminated) + return false; + + // 2. log error as needed + if (terminated->result == ScanMachineResult::FAIL) + LOG_ERROR("seraphis scan state machine (terminal state): scan failed!"); + else if (terminated->result == ScanMachineResult::ABORTED) + LOG_ERROR("seraphis scan state machine (terminal state): scan aborted!"); + else if (terminated->result != ScanMachineResult::SUCCESS) + LOG_ERROR("seraphis scan state machine (terminal state): unknown failure!"); + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +bool try_advance_state_machine(ScanContextLedger &scan_context_inout, + ChunkConsumer &chunk_consumer_inout, + ScanMachineState &state_inout) +{ + // check terminal states + if (is_terminal_state_with_log(state_inout)) + return false; + + // NEED_FULLSCAN + if (try_handle_need_fullscan(chunk_consumer_inout, state_inout)) + return true; + + // NEED_PARTIALSCAN + if (try_handle_need_partialscan(chunk_consumer_inout, state_inout)) + return true; + + // START_SCAN + if (try_handle_start_scan(scan_context_inout, state_inout)) + return true; + + // DO_SCAN + if (try_handle_do_scan(scan_context_inout, chunk_consumer_inout, state_inout)) + return true; + + return false; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace scanning +} //namespace sp diff --git a/src/seraphis_main/scan_machine.h b/src/seraphis_main/scan_machine.h new file mode 100644 index 0000000000..9f49b82af4 --- /dev/null +++ b/src/seraphis_main/scan_machine.h @@ -0,0 +1,67 @@ +// Copyright (c) 2022, 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. + +// State machine for scanning a LIFO chain of blocks by incrementally processing chunks of that chain. + +#pragma once + +//local headers +#include "scan_machine_types.h" + +//third party headers + +//standard headers + +//forward declarations +namespace sp +{ +namespace scanning +{ + class ScanContextLedger; + class ChunkConsumer; +} +} + +namespace sp +{ +namespace scanning +{ + +/** +* brief: try_advance_state_machine - advance the scan state machine to the next state +* inoutparam: scan_context_inout - +* inoutparam: chunk_consumer_inout - +* inoutparam: state_inout - +* return: true if the machine was advanced to a new non-terminal state, false if the machine is in a terminal state +*/ +bool try_advance_state_machine(ScanContextLedger &scan_context_inout, + ChunkConsumer &chunk_consumer_inout, + ScanMachineState &state_inout); + +} //namespace scanning +} //namespace sp diff --git a/src/seraphis_main/scan_machine_types.h b/src/seraphis_main/scan_machine_types.h new file mode 100644 index 0000000000..b14d556cb9 --- /dev/null +++ b/src/seraphis_main/scan_machine_types.h @@ -0,0 +1,176 @@ +// Copyright (c) 2022, 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. + +// Helper types for the scan state machine. + +#pragma once + +//local headers +#include "common/variant.h" +#include "ringct/rctTypes.h" + +//third party headers +#include + +//standard headers + +//forward declarations + + +namespace sp +{ +namespace scanning +{ + +//// +// ContiguityMarker +// - marks the end of a contiguous chain of blocks +// - if the contiguous chain is empty, then the block id will be unspecified and the block index will equal the chain's +// initial index minus one +// - a 'contiguous chain' does not have to start at 'block 0', it can start at any predefined block index where you +// want to start tracking contiguity +// - example: if your refresh index is 'block 101' and you haven't loaded/scanned any blocks, then your initial +// contiguity marker will start at 'block 100' with an unspecified block id; if you scanned blocks [101, 120], then +// your contiguity marker will be at block 120 with that block's block id +/// +struct ContiguityMarker final +{ + /// index of the block + std::uint64_t block_index; + /// id of the block (optional) + boost::optional block_id; +}; + +//// +// ScanMachineConfig +// - configuration details for the scan state machine +/// +struct ScanMachineConfig final +{ + /// increment for avoiding reorgs + /// - each fullscan attempt looks (10^attempts * increment) blocks below the requested start index + std::uint64_t reorg_avoidance_increment{10}; + /// max number of blocks per ledger chunk + /// - this is only a hint, the downstream scanning context is free to ignore it + std::uint64_t max_chunk_size_hint{100}; + /// maximum number of times to try rescanning if a partial reorg is detected + std::uint64_t max_partialscan_attempts{3}; +}; + +//// +// ScanMachineMetadata +// - metadata for the scan state machine +/// +struct ScanMachineMetadata final +{ + /// config details for the machine + ScanMachineConfig config; + + /// attempt counters: track history of the machine + std::size_t partialscan_attempts; + std::size_t fullscan_attempts; +}; + +//// +// ScanMachineResult +/// +enum class ScanMachineResult : unsigned char +{ + FAIL, + ABORTED, + SUCCESS +}; + +//// +// ScanMachineNeedFullscan +// - the machine needs to perform a full scan +/// +struct ScanMachineNeedFullscan final +{ + /// metadata for the machine + ScanMachineMetadata metadata; +}; + +//// +// ScanMachineNeedPartialscan +// - the machine needs to perform a partial scan +/// +struct ScanMachineNeedPartialscan final +{ + /// metadata for the machine + ScanMachineMetadata metadata; +}; + +//// +// ScanMachineStartScan +// - the machine needs to initialize a scan process +/// +struct ScanMachineStartScan final +{ + /// metadata for the machine + ScanMachineMetadata metadata; + + /// contiguity marker: keeps track of where in the ledger the machine is pointing to right now + ContiguityMarker contiguity_marker; +}; + +//// +// ScanMachineDoScan +// - the machine needs to scan one new chunk +/// +struct ScanMachineDoScan final +{ + /// metadata for the machine + ScanMachineMetadata metadata; + + /// contiguity context: keeps track of where in the ledger the machine is pointing to right now + ContiguityMarker contiguity_marker; + std::uint64_t first_contiguity_index; +}; + +//// +// ScanMachineTerminated +// - the machine has nothing more it can do +/// +struct ScanMachineTerminated final +{ + ScanMachineResult result; +}; + +/// variant of scan machine states +using ScanMachineState = + tools::variant< + ScanMachineNeedFullscan, + ScanMachineNeedPartialscan, + ScanMachineStartScan, + ScanMachineDoScan, + ScanMachineTerminated + >; + +} //namespace scanning +} //namespace sp diff --git a/src/seraphis_main/scan_misc_utils.cpp b/src/seraphis_main/scan_misc_utils.cpp new file mode 100644 index 0000000000..29c8b800b6 --- /dev/null +++ b/src/seraphis_main/scan_misc_utils.cpp @@ -0,0 +1,183 @@ +// Copyright (c) 2022, 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 "scan_misc_utils.h" + +//local headers +#include "misc_log_ex.h" +#include "seraphis_main/scan_core_types.h" +#include "seraphis_main/scan_ledger_chunk.h" +#include "seraphis_main/scan_machine_types.h" +#include "seraphis_main/scan_misc_utils.h" + +//third party headers + +//standard headers + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis" + +namespace sp +{ +namespace scanning +{ +//------------------------------------------------------------------------------------------------------------------- +std::size_t chunk_size(const ChunkContext &chunk_context) +{ + return chunk_context.block_ids.size(); +} +//------------------------------------------------------------------------------------------------------------------- +bool chunk_data_is_empty(const ChunkData &chunk_data) +{ + return chunk_data.basic_records_per_tx.size() == 0 && + chunk_data.contextual_key_images.size() == 0; +} +//------------------------------------------------------------------------------------------------------------------- +bool chunk_context_is_empty(const ChunkContext &chunk_context) +{ + return chunk_size(chunk_context) == 0; +} +//------------------------------------------------------------------------------------------------------------------- +void check_chunk_data_semantics(const ChunkData &chunk_data, + const SpEnoteOriginStatus expected_origin_status, + const SpEnoteSpentStatus expected_spent_status, + const std::uint64_t allowed_lowest_index, + const std::uint64_t allowed_highest_index) +{ + // 1. check contextual basic records + for (const auto &tx_basic_records : chunk_data.basic_records_per_tx) + { + for (const ContextualBasicRecordVariant &contextual_basic_record : tx_basic_records.second) + { + CHECK_AND_ASSERT_THROW_MES(origin_context_ref(contextual_basic_record).origin_status == + expected_origin_status, + "scan chunk data semantics check: contextual basic record doesn't have expected origin status."); + CHECK_AND_ASSERT_THROW_MES(origin_context_ref(contextual_basic_record).transaction_id == + tx_basic_records.first, + "scan chunk data semantics check: contextual basic record doesn't have origin tx id matching mapped id."); + CHECK_AND_ASSERT_THROW_MES(origin_context_ref(contextual_basic_record).block_index == + origin_context_ref(*tx_basic_records.second.begin()).block_index, + "scan chunk data semantics check: contextual record tx index doesn't match other records in tx."); + + CHECK_AND_ASSERT_THROW_MES( + origin_context_ref(contextual_basic_record).block_index >= allowed_lowest_index && + origin_context_ref(contextual_basic_record).block_index <= allowed_highest_index, + "scan chunk data semantics check: contextual record block index is out of the expected range."); + } + } + + // 2. check contextual key images + for (const auto &contextual_key_image_set : chunk_data.contextual_key_images) + { + CHECK_AND_ASSERT_THROW_MES(contextual_key_image_set.spent_context.spent_status == expected_spent_status, + "scan chunk data semantics check: contextual key image doesn't have expected spent status."); + + // notes: + // - in seraphis tx building, tx authors must always put a selfsend output enote in their txs; during balance + // recovery, the view tag check will pass for those selfsend enotes; this means to identify if your enotes are + // spent, you only need to look at key images in txs with view tag matches + // - in support of that expectation, we enforce that the key images in a scanning chunk must come from txs + // recorded in the 'basic records per tx' map, which will contain only owned enote candidates (in seraphis + // scanning, that's all the enotes that passed the view tag check) + // - if you want to include key images from txs that have no owned enote candidates, then you must add empty + // entries to the 'basic records per tx' map for those txs + // - when doing legacy scanning, you need to include all key images from the chain since legacy tx construction + // does/did not require all txs to have a self-send output + CHECK_AND_ASSERT_THROW_MES( + chunk_data.basic_records_per_tx.find(contextual_key_image_set.spent_context.transaction_id) != + chunk_data.basic_records_per_tx.end(), + "scan chunk data semantics check: contextual key image transaction id is not mirrored in basic records map."); + + CHECK_AND_ASSERT_THROW_MES( + contextual_key_image_set.spent_context.block_index >= allowed_lowest_index && + contextual_key_image_set.spent_context.block_index <= allowed_highest_index, + "scan chunk data semantics check: contextual key image block index is out of the expected range."); + } +} +//------------------------------------------------------------------------------------------------------------------- +void check_ledger_chunk_semantics(const LedgerChunk &ledger_chunk, const std::uint64_t expected_prefix_index) +{ + // 1. check context semantics + CHECK_AND_ASSERT_THROW_MES(ledger_chunk.get_context().start_index - 1 == expected_prefix_index, + "check ledger chunk semantics: chunk range doesn't start at expected prefix index."); + + const std::uint64_t num_blocks_in_chunk{ledger_chunk.get_context().block_ids.size()}; + CHECK_AND_ASSERT_THROW_MES(num_blocks_in_chunk >= 1, + "check ledger chunk semantics: chunk has no blocks."); + + // 2. get start and end block indices + // - start block = prefix block + 1 + const std::uint64_t allowed_lowest_index{ledger_chunk.get_context().start_index}; + // - end block + const std::uint64_t allowed_highest_index{allowed_lowest_index + num_blocks_in_chunk - 1}; + + // 3. check the chunk data semantics for each subconsumer + for (const rct::key &subconsumer_id : ledger_chunk.subconsumer_ids()) + { + // a. extract the chunk data + const ChunkData *chunk_data{ledger_chunk.try_get_data(subconsumer_id)}; + CHECK_AND_ASSERT_THROW_MES(chunk_data, + "check ledger chunk semantics: could not get chunk data for subconsumer."); + + // b. check the chunk data semantics + check_chunk_data_semantics(*chunk_data, + SpEnoteOriginStatus::ONCHAIN, + SpEnoteSpentStatus::SPENT_ONCHAIN, + allowed_lowest_index, + allowed_highest_index); + } +} +//------------------------------------------------------------------------------------------------------------------- +ScanMachineMetadata initialize_scan_machine_metadata(const ScanMachineConfig &scan_config) +{ + return ScanMachineMetadata{ + .config = scan_config, + .partialscan_attempts = 0, + .fullscan_attempts = 0 + }; +} +//------------------------------------------------------------------------------------------------------------------- +ScanMachineState initialize_scan_machine_state(const ScanMachineConfig &scan_config) +{ + return ScanMachineNeedFullscan{ .metadata = initialize_scan_machine_metadata(scan_config) }; +} +//------------------------------------------------------------------------------------------------------------------- +bool is_terminal_state(const ScanMachineState &state) +{ + return state.is_type(); +} +//------------------------------------------------------------------------------------------------------------------- +bool is_success_state(const ScanMachineState &state) +{ + const ScanMachineTerminated *terminated{state.try_unwrap()}; + return terminated && terminated->result == ScanMachineResult::SUCCESS; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace scanning +} //namespace sp diff --git a/src/seraphis_main/scan_misc_utils.h b/src/seraphis_main/scan_misc_utils.h new file mode 100644 index 0000000000..ca9b2d579a --- /dev/null +++ b/src/seraphis_main/scan_misc_utils.h @@ -0,0 +1,124 @@ +// Copyright (c) 2022, 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. + +// Miscellaneous utilities related to scanning. + +#pragma once + +//local headers +#include "seraphis_main/contextual_enote_record_types.h" +#include "seraphis_main/scan_machine_types.h" + +//third party headers + +//standard headers + +//forward declarations +namespace sp +{ +namespace scanning +{ + struct ChunkData; + struct ChunkContext; + class LedgerChunk; +} +} + +namespace sp +{ +namespace scanning +{ + +/** +* brief: chunk_size - get number of blocks in chunk +* param: chunk_context - +* return: number of blocks in chunk +*/ +std::size_t chunk_size(const ChunkContext &chunk_context); +/** +* brief: chunk_data_is_empty - check if a chunk data is empty (contains no records) +* param: chunk_data - +* return: true if the chunk data is empty +*/ +bool chunk_data_is_empty(const ChunkData &chunk_data); +/** +* brief: chunk_is_empty - check if a chunk context is empty (refers to no blocks) +* param: chunk_context - +* return: true if the chunk context is empty +*/ +bool chunk_context_is_empty(const ChunkContext &chunk_context); +/** +* brief: check_chunk_data_semantics - check semantics of chunk data +* - throws on failure +* param: chunk_data - +* param: expected_origin_status - +* param: expected_spent_status - +* param: allowed_lowest_index - lowest block index allowed in chunk data (e.g. origin block, spent block) +* param: allowed_highest_index - highest block index allowed in chunk data (e.g. origin block, spent block) +*/ +void check_chunk_data_semantics(const ChunkData &chunk_data, + const SpEnoteOriginStatus expected_origin_status, + const SpEnoteSpentStatus expected_spent_status, + const std::uint64_t allowed_lowest_index, + const std::uint64_t allowed_highest_index); +/** +* brief: check_ledger_chunk_semantics - check semantics of an on-chain chunk +* - expects the chunk context to be non-empty +* - throws on failure +* param: ledger_chunk - +* param: expected_prefix_index - +*/ +void check_ledger_chunk_semantics(const LedgerChunk &ledger_chunk, const std::uint64_t expected_prefix_index); +/** +* brief: initialize_scan_machine_metadata - initialize scan machine metadata with a specified configuration +* param: scan_config - +* return: initialized metadata +*/ +ScanMachineMetadata initialize_scan_machine_metadata(const ScanMachineConfig &scan_config); +/** +* brief: initialize_scan_machine_state - initialize a scan machine state with a specified configuration +* - initial state: need fullscan +* param: scan_config - +* return: initialized scan machine state +*/ +ScanMachineState initialize_scan_machine_state(const ScanMachineConfig &scan_config); +/** +* brief: is_terminal_state - test if a scan machine is in a terminal state +* param: state - +* return: true if state is terminal +*/ +bool is_terminal_state(const ScanMachineState &state); +/** +* brief: is_success_state - test if a scan machine is in a successful terminal state +* param: state - +* return: true if state is terminal +*/ +bool is_success_state(const ScanMachineState &state); + +} //namespace scanning +} //namespace sp diff --git a/src/seraphis_main/sp_knowledge_proof_types.h b/src/seraphis_main/sp_knowledge_proof_types.h new file mode 100644 index 0000000000..3a24188a57 --- /dev/null +++ b/src/seraphis_main/sp_knowledge_proof_types.h @@ -0,0 +1,236 @@ +// Copyright (c) 2023, 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 knowledge proof types. + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/jamtis_destination.h" +#include "seraphis_crypto/matrix_proof.h" +#include "seraphis_crypto/sp_composition_proof.h" +#include "seraphis_main/enote_record_types.h" +#include "seraphis_main/tx_builder_types.h" +#include "seraphis_main/tx_component_types.h" +#include "seraphis_main/tx_validation_context.h" + +//third party headers + +//standard headers +#include + +//forward declarations + + +namespace sp +{ +namespace knowledge_proofs +{ + +//// +// AddressOwnershipProofV1 +// - proof that an address K is constructed in the seraphis address style and is owned by the prover +// - {K = K_1} OR {K = K_s = k_vb X + k_m U} +// +// - INTERACTIVE PROOF: verifier must give a custom message to the prover, otherwise the prover can just copy-paste +// a pre-computed proof that he got from who-knows-where +// +// - VERIFIER: validate the seraphis composition proof on K +/// +struct AddressOwnershipProofV1 +{ + rct::key message; + rct::key K; + crypto::key_image addr_key_image; //'key image' of the address used in this proof + SpCompositionProof composition_proof; +}; + +//// +// AddressIndexProofV1 +// - proof that a jamtis address with spendkey K_1 was constructed from an index j from base spend key K_s +// +// - VERIFIER: recompute K_1 ?= [G/X/U spendkey extensions from {j, generator, K_s}] + K_s +/// +struct AddressIndexProofV1 +{ + rct::key K_s; + jamtis::address_index_t j; + rct::key generator; + rct::key K_1; +}; + +//// +// EnoteOwnershipProofV1 +// - proof an enote with onetime adress Ko is owned by an address K_1 +// - disclaimer: this does not prove that the owner of address K_1 can actually spend the enote; q could be computed in +// violation of the jamtis spec, in which case the owner of K_1 may never recover the enote and so the funds are +// effectively burned +// +// - VERIFIER: recompute Ko ?= [G/X/U sender extensions from {K_1, q, C}] + K_1 +/// +struct EnoteOwnershipProofV1 +{ + rct::key K_1; + rct::key q; + rct::key C; + rct::key Ko; +}; + +//// +// EnoteAmountProofV1 +// - proof an enote with amount commitment C has a particular amount a +// +// - VERIFIER: recompute C ?= x G + a H +/// +struct EnoteAmountProofV1 +{ + rct::xmr_amount a; + rct::key x; + rct::key C; +}; + +//// +// EnoteKeyImageProofV1 +// - proof a key image KI corresponds to a particular onetime address Ko +// +// - VERIFIER: +// - check that KI is in the prime-order subgroup +// - validate the seraphis composition proof on the provided {Ko, KI} +/// +struct EnoteKeyImageProofV1 +{ + rct::key Ko; + crypto::key_image KI; + SpCompositionProof composition_proof; +}; + +//// +// EnoteUnspentProofV1 +// - proof an enote with onetime address Ko was NOT spent by a tx input with key image test_KI +// +// pubkeys stored in the matrix proofs: +// Ko_g = k_g G +// Ko_x = (k_x + k_vb) X +// Ko_u = (k_u + k_m) U +// +// - VERIFIER: +// - recompute Ko ?= Ko_g + Ko_x + Ko_u +// - validate: +// - g_component_proof on base key G +// - x_component_transform_proof on base keys {X, test_KI} +// - u_component_proof on base key U +// - check: +// - if [x_component_transform_proof second proof key] == Ko_u then test_KI is the key image of Ko, otherwise +// it is not +// +// TODO: a more efficient version of this would make a proof on multiple test_KI at once +/// +struct EnoteUnspentProofV1 +{ + rct::key Ko; + crypto::key_image test_KI; + MatrixProof g_component_proof; //Ko_g on G + MatrixProof x_component_transform_proof; //{Ko_x, (k_x + k_vb)*test_KI} on {X, test_KI} + MatrixProof u_component_proof; //Ko_u on U +}; + +//// +// TxFundedProofV1 +// - proof that the prover owns the enote that was spent in a tx input with key image KI +// - this proof does not expose the enote, it just demonstrates that the prover can reproduce KI +// - note that this proof does not expose the input amount; if the prover cached the mask t_c in the original tx input, +// then they can make an EnoteAmountProofV1 on the input's masked amount commitment; otherwise they need an +// EnoteAmountProofV1 on the input enote's original amount commitment (which will expose which enote was spent by the tx) +// +// - INTERACTIVE PROOF: verifier must give a custom message to the prover +// +// - VERIFIER: validate the seraphis composition proof on the provided {K", KI} +/// +struct TxFundedProofV1 +{ + rct::key message; + rct::key masked_address; //K" = t_k G + Ko (using a different mask t_k than was used in the tx) + crypto::key_image KI; + SpCompositionProof composition_proof; +}; + +//// +// EnoteSentProofV1 +// - proof that an enote with amount a and onetime address Ko was sent to an address K_1 +// +// - VERIFIER: validate the EnoteOwnershipProofV1 and EnoteAmountProofV1 +/// +struct EnoteSentProofV1 +{ + EnoteOwnershipProofV1 enote_ownership_proof; + EnoteAmountProofV1 amount_proof; +}; + +//// +// ReservedEnoteProofV1 +// - proof that an enote with onetime address Ko is owned by address K_1, has amount a, has key image KI, is onchain, and +// is unspent +// +// - VERIFIER: +// - validate the EnoteOwnershipProofV1, EnoteAmountProofV1, and EnoteKeyImageProofV1 proofs +// - verify that {C, Ko} corresponds to an onchain enote using enote_ledger_index +// - verify the KI doesn't exist on-chain +/// +struct ReservedEnoteProofV1 +{ + EnoteOwnershipProofV1 enote_ownership_proof; + EnoteAmountProofV1 amount_proof; + EnoteKeyImageProofV1 KI_proof; + std::uint64_t enote_ledger_index; +}; + +//// +// ReserveProofV1 +// - proof that the prover has at least v = sum(a) unspent funds onchain +// +// - INTERACTIVE PROOF: verifier must give a custom message to the prover +// +// - VERIFIER: +// - validate the AddressOwnershipProofV1 proofs +// - check that the owning address K_1 in each of the reserved enote proofs corresponds to an address owned by the prover +// - check that the enotes referenced by the reserved enote proofs exist in the ledger +// - check that the key images in the reserved enote proofs do not exist in the ledger +// - validate the ReservedEnoteProofV1 proofs +// +// - OUTPUT: v = sum(amounts in the proofs) +/// +struct ReserveProofV1 +{ + std::vector address_ownership_proofs; + std::vector reserved_enote_proofs; +}; + +} //namespace knowledge_proofs +} //namespace sp diff --git a/src/seraphis_main/sp_knowledge_proof_utils.cpp b/src/seraphis_main/sp_knowledge_proof_utils.cpp new file mode 100644 index 0000000000..39aad9370c --- /dev/null +++ b/src/seraphis_main/sp_knowledge_proof_utils.cpp @@ -0,0 +1,978 @@ +// Copyright (c) 2023, 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 "sp_knowledge_proof_utils.h" + +//local headers +#include "common/container_helpers.h" +#include "crypto/crypto.h" +#include "crypto/generators.h" +#include "ringct/rctOps.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/jamtis_address_utils.h" +#include "seraphis_core/jamtis_core_utils.h" +#include "seraphis_core/jamtis_enote_utils.h" +#include "seraphis_core/jamtis_support_types.h" +#include "seraphis_core/sp_core_enote_utils.h" +#include "seraphis_core/sp_core_types.h" +#include "seraphis_crypto/matrix_proof.h" +#include "seraphis_crypto/sp_crypto_utils.h" +#include "seraphis_crypto/sp_hash_functions.h" +#include "seraphis_crypto/sp_transcript.h" +#include "seraphis_main/contextual_enote_record_types.h" +#include "seraphis_main/enote_record_types.h" +#include "seraphis_main/sp_knowledge_proof_types.h" +#include "seraphis_main/tx_component_types.h" +#include "seraphis_main/tx_validation_context.h" + +//third party headers +#include + +//standard headers +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis" + +namespace sp +{ +namespace knowledge_proofs +{ +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void make_address_ownership_proof_k_g_offset(const rct::key &K, crypto::secret_key &offset) +{ + // H_n(K) + SpFSTranscript transcript{ + config::HASH_KEY_SERAPHIS_ADDRESS_OWNERSHIP_PROOF_OFFSET_V1, + sizeof(rct::key), + }; + transcript.append("K", K); + + sp_hash_to_scalar(transcript.data(), transcript.size(),to_bytes(offset)); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void make_enote_key_image_proof_message_v1(const rct::key &onetime_address, + const crypto::key_image &KI, + rct::key &message_out) +{ + // H_32(Ko, KI) + SpFSTranscript transcript{ + config::HASH_KEY_SERAPHIS_ENOTE_KEY_IMAGE_PROOF_MESSAGE_V1, + 2*sizeof(rct::key), + }; + transcript.append("Ko", onetime_address); + transcript.append("KI", KI); + + sp_hash_to_32(transcript.data(), transcript.size(), message_out.bytes); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void make_enote_unspent_proof_message_v1(const rct::key &onetime_address, + const crypto::key_image &KI, + rct::key &message_out) +{ + // H_32(Ko, KI) + SpFSTranscript transcript{ + config::HASH_KEY_SERAPHIS_ENOTE_UNSPENT_PROOF_MESSAGE_V1, + 2*sizeof(rct::key), + }; + transcript.append("Ko", onetime_address); + transcript.append("KI", KI); + + sp_hash_to_32(transcript.data(), transcript.size(), message_out.bytes); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +void make_address_ownership_proof_v1(const rct::key &message, + const rct::key &address, + const crypto::secret_key &x, + const crypto::secret_key &y, + const crypto::secret_key &z, + AddressOwnershipProofV1 &proof_out) +{ + // 1. k_g_offset = H_n(K) + crypto::secret_key k_g_offset; + make_address_ownership_proof_k_g_offset(address, k_g_offset); + + // 2. K" = k_g_offset G + K + // note: we add an offset in case x == 0 (e.g. if K == K_s) + rct::key masked_address; + mask_key(k_g_offset, address, masked_address); + + // 3. x" = k_g_offset + x + crypto::secret_key x_factor; + sc_add(to_bytes(x_factor), to_bytes(k_g_offset), to_bytes(x)); + + // 4. make a composition proof on the masked address + SpCompositionProof proof; + make_sp_composition_proof(message, masked_address, x_factor, y, z, proof); + + // 5. prepare the address's 'key image' + crypto::key_image addr_key_image; + make_seraphis_key_image(y,z,addr_key_image); + + // 6. assemble the full proof + proof_out = AddressOwnershipProofV1{ + .message = message, + .K = address, + .addr_key_image = addr_key_image, + .composition_proof = proof + }; +} +//------------------------------------------------------------------------------------------------------------------- +void make_address_ownership_proof_v1(const rct::key &message, + const crypto::secret_key &sp_spend_privkey, + const crypto::secret_key &k_view_balance, + AddressOwnershipProofV1 &proof_out) +{ + // for address ownership of K_s + + // 1. prepare K_s = k_vb X + k_m U + rct::key jamtis_spend_pubkey; + make_seraphis_spendkey(k_view_balance, sp_spend_privkey, jamtis_spend_pubkey); + + // 2. finish the proof + make_address_ownership_proof_v1(message, + jamtis_spend_pubkey, + rct::rct2sk(rct::zero()), + k_view_balance, + sp_spend_privkey, + proof_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_address_ownership_proof_v1(const rct::key &message, + const crypto::secret_key &sp_spend_privkey, + const crypto::secret_key &k_view_balance, + const jamtis::address_index_t &j, + AddressOwnershipProofV1 &proof_out) +{ + // for address ownership of K_1 + + // 1. prepare privkey + crypto::secret_key s_generate_address; + jamtis::make_jamtis_generateaddress_secret(k_view_balance, s_generate_address); + + // 2. prepare K_s = k_vb X + k_m U + rct::key jamtis_spend_pubkey; + make_seraphis_spendkey(k_view_balance, sp_spend_privkey, jamtis_spend_pubkey); + + // 3. prepare address privkey components + // a. x = k^j_g + crypto::secret_key x; + jamtis::make_jamtis_spendkey_extension_g(jamtis_spend_pubkey, s_generate_address, j, x); //k^j_g + + // b. y = k^j_x + k_vb + crypto::secret_key y; + jamtis::make_jamtis_spendkey_extension_x(jamtis_spend_pubkey, s_generate_address, j, y); //k^j_x + sc_add(to_bytes(y), to_bytes(k_view_balance), to_bytes(y)); //+ k_vb + + // c. z = k^j_u + k_m + crypto::secret_key z; + jamtis::make_jamtis_spendkey_extension_u(jamtis_spend_pubkey, s_generate_address, j, z); //k^j_u + sc_add(to_bytes(z), to_bytes(sp_spend_privkey), to_bytes(z)); //+ k_m + + // 4. compute address + // K_1 = x G + y X + z U + rct::key jamtis_address_spend_key; + make_seraphis_spendkey(y, z, jamtis_address_spend_key); //y X + z U + mask_key(x, jamtis_address_spend_key, jamtis_address_spend_key); //+ x G + + // 5. finish the proof + make_address_ownership_proof_v1(message, jamtis_address_spend_key, x, y, z, proof_out); +} +//------------------------------------------------------------------------------------------------------------------- +bool verify_address_ownership_proof_v1(const AddressOwnershipProofV1 &proof, + const rct::key &expected_message, + const rct::key &expected_address) +{ + // 1. check the expected message + if (!(proof.message == expected_message)) + return false; + + // 2. check the expected address + if (!(proof.K == expected_address)) + return false; + + // 3. k_g_offset + crypto::secret_key k_g_offset; + make_address_ownership_proof_k_g_offset(proof.K, k_g_offset); + + // 4. K" = k_g_offset G + K + rct::key masked_address; + mask_key(k_g_offset, proof.K, masked_address); + + // 5. verify the composition proof + if (!verify_sp_composition_proof(proof.composition_proof, proof.message, masked_address, proof.addr_key_image)) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +void make_address_index_proof_v1(const rct::key &jamtis_spend_pubkey, + const jamtis::address_index_t &j, + const crypto::secret_key &s_generate_address, + AddressIndexProofV1 &proof_out) +{ + // 1. prepare the address index extension generator + crypto::secret_key generator; + jamtis::make_jamtis_index_extension_generator(s_generate_address, j, generator); + + // 2. compute K_1 + rct::key K_1; + jamtis::make_jamtis_address_spend_key(jamtis_spend_pubkey, s_generate_address, j, K_1); + + // 3. assemble the full proof + proof_out = AddressIndexProofV1{ + .K_s = jamtis_spend_pubkey, + .j = j, + .generator = rct::sk2rct(generator), + .K_1 = K_1 + }; +} +//------------------------------------------------------------------------------------------------------------------- +bool verify_address_index_proof_v1(const AddressIndexProofV1 &proof, const rct::key &expected_address) +{ + // 1. check the proof matches the expected address + if (!(proof.K_1 == expected_address)) + return false; + + // 2. reproduce the address index extensions + // a. k^j_u + crypto::secret_key address_extension_key_u; + make_jamtis_spendkey_extension(config::HASH_KEY_JAMTIS_SPENDKEY_EXTENSION_U, + proof.K_s, + proof.j, + rct::rct2sk(proof.generator), + address_extension_key_u); + + // b. k^j_x + crypto::secret_key address_extension_key_x; + make_jamtis_spendkey_extension(config::HASH_KEY_JAMTIS_SPENDKEY_EXTENSION_X, + proof.K_s, + proof.j, + rct::rct2sk(proof.generator), + address_extension_key_x); + + // c. k^j_g + crypto::secret_key address_extension_key_g; + make_jamtis_spendkey_extension(config::HASH_KEY_JAMTIS_SPENDKEY_EXTENSION_G, + proof.K_s, + proof.j, + rct::rct2sk(proof.generator), + address_extension_key_g); + + // 3. compute the nominal address spendkey + // K_1 = k^j_g G + k^j_x X + k^j_u U + K_s + rct::key nominal_address{proof.K_s}; //K_s + extend_seraphis_spendkey_u(address_extension_key_u, nominal_address); //k^j_u U + K_s + extend_seraphis_spendkey_x(address_extension_key_x, nominal_address); //k^j_x X + k^j_u U + K_s + mask_key(address_extension_key_g, nominal_address, nominal_address); //k^j_g G + k^j_x X + k^j_u U + K_s + + // 4. check that the proof address spendkey was recreated + if (!(nominal_address == proof.K_1)) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +void make_enote_ownership_proof_v1(const rct::key &jamtis_address_spend_key, + const rct::key &sender_receiver_secret, + const rct::key &amount_commitment, + const rct::key &onetime_address, + EnoteOwnershipProofV1 &proof_out) +{ + proof_out = EnoteOwnershipProofV1{ + .K_1 = jamtis_address_spend_key, + .q = sender_receiver_secret, + .C = amount_commitment, + .Ko = onetime_address + }; +} +//------------------------------------------------------------------------------------------------------------------- +void make_enote_ownership_proof_v1_sender_plain(const crypto::x25519_secret_key &enote_ephemeral_privkey, + const jamtis::JamtisDestinationV1 &recipient_destination, + const rct::key &input_context, + const rct::key &amount_commitment, + const rct::key &onetime_address, + EnoteOwnershipProofV1 &proof_out) +{ + // 1. compute the enote ephemeral pubkey + crypto::x25519_pubkey enote_ephemeral_pubkey; + jamtis::make_jamtis_enote_ephemeral_pubkey(enote_ephemeral_privkey, + recipient_destination.addr_K3, + enote_ephemeral_pubkey); + + // 2. prepare the sender-receiver secret + rct::key sender_receiver_secret; + jamtis::make_jamtis_sender_receiver_secret_plain(enote_ephemeral_privkey, + recipient_destination.addr_K2, + enote_ephemeral_pubkey, + input_context, + sender_receiver_secret); + + // 3. complete the proof + make_enote_ownership_proof_v1(recipient_destination.addr_K1, + sender_receiver_secret, + amount_commitment, + onetime_address, + proof_out); + + // 4. verify that the proof was created successfully + // - will fail if the enote is a jamtis selfsend type + CHECK_AND_ASSERT_THROW_MES(verify_enote_ownership_proof_v1(proof_out, amount_commitment, onetime_address), + "make enote ownership proof (v1 sender plain): failed to make proof."); +} +//------------------------------------------------------------------------------------------------------------------- +void make_enote_ownership_proof_v1_sender_selfsend(const crypto::x25519_pubkey &enote_ephemeral_pubkey, + const rct::key &jamtis_address_spend_key, + const rct::key &input_context, + const crypto::secret_key &k_view_balance, + const jamtis::JamtisSelfSendType self_send_type, + const rct::key &amount_commitment, + const rct::key &onetime_address, + EnoteOwnershipProofV1 &proof_out) +{ + // 1. prepare the sender-receiver secret + rct::key sender_receiver_secret; + jamtis::make_jamtis_sender_receiver_secret_selfsend(k_view_balance, + enote_ephemeral_pubkey, + input_context, + self_send_type, + sender_receiver_secret); + + // 2. complete the proof + make_enote_ownership_proof_v1(jamtis_address_spend_key, + sender_receiver_secret, + amount_commitment, + onetime_address, + proof_out); + + // 3. verify that the proof was created successfully + // - will fail if the enote is a jamtis plain type + CHECK_AND_ASSERT_THROW_MES(verify_enote_ownership_proof_v1(proof_out, amount_commitment, onetime_address), + "make enote ownership proof (v1 sender selfsend): failed to make proof."); +} +//------------------------------------------------------------------------------------------------------------------- +void make_enote_ownership_proof_v1_receiver(const SpEnoteRecordV1 &enote_record, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + EnoteOwnershipProofV1 &proof_out) +{ + // 1. helper privkeys + crypto::x25519_secret_key xk_find_received; + crypto::secret_key s_generate_address; + jamtis::make_jamtis_findreceived_key(k_view_balance, xk_find_received); + jamtis::make_jamtis_generateaddress_secret(k_view_balance, s_generate_address); + + // 2. get the owning address's spendkey K_1 + rct::key jamtis_address_spend_key; + jamtis::make_jamtis_address_spend_key(jamtis_spend_pubkey, + s_generate_address, + enote_record.address_index, + jamtis_address_spend_key); + + // 3. prepare the sender-receiver secret + rct::key sender_receiver_secret; + jamtis::JamtisSelfSendType self_send_type; + + if (jamtis::try_get_jamtis_self_send_type(enote_record.type, self_send_type)) + { + jamtis::make_jamtis_sender_receiver_secret_selfsend(k_view_balance, + enote_record.enote_ephemeral_pubkey, + enote_record.input_context, + self_send_type, + sender_receiver_secret); + } + else + { + jamtis::make_jamtis_sender_receiver_secret_plain(xk_find_received, + enote_record.enote_ephemeral_pubkey, + enote_record.enote_ephemeral_pubkey, + enote_record.input_context, + sender_receiver_secret); + } + + // 4. complete the proof + make_enote_ownership_proof_v1(jamtis_address_spend_key, + sender_receiver_secret, + amount_commitment_ref(enote_record.enote), + onetime_address_ref(enote_record.enote), + proof_out); + + // 5. verify that the proof was created successfully + CHECK_AND_ASSERT_THROW_MES(verify_enote_ownership_proof_v1(proof_out, + amount_commitment_ref(enote_record.enote), + onetime_address_ref(enote_record.enote)), + "make enote ownership proof (v1 recipient): failed to make proof."); +} +//------------------------------------------------------------------------------------------------------------------- +bool verify_enote_ownership_proof_v1(const EnoteOwnershipProofV1 &proof, + const rct::key &expected_amount_commitment, + const rct::key &expected_onetime_address) +{ + // 1. check the proof matches with the expected enote + if (!(proof.C == expected_amount_commitment)) + return false; + if (!(proof.Ko == expected_onetime_address)) + return false; + + // 2. reproduce the onetime address + rct::key reproduced_Ko; + jamtis::make_jamtis_onetime_address(proof.K_1, proof.q, proof.C, reproduced_Ko); + + // 3. check the reproduced onetime address matches the proof + if (!(proof.Ko == reproduced_Ko)) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +void make_enote_amount_proof_v1(const rct::xmr_amount &amount, + const crypto::secret_key &mask, + const rct::key &commitment, + EnoteAmountProofV1 &proof_out) +{ + proof_out = EnoteAmountProofV1{ + .a = amount, + .x = rct::sk2rct(mask), + .C = commitment + }; +} +//------------------------------------------------------------------------------------------------------------------- +bool verify_enote_amount_proof_v1(const EnoteAmountProofV1 &proof, const rct::key &expected_commitment) +{ + // 1. check the proof matches the expected amount commitment + if (!(proof.C == expected_commitment)) + return false; + + // 2. check the commitment can be reproduced + if (!(proof.C == rct::commit(proof.a, proof.x))) + return false;; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +void make_enote_key_image_proof_v1(const rct::key &onetime_address, + const crypto::secret_key &x, + const crypto::secret_key &y, + const crypto::secret_key &z, + EnoteKeyImageProofV1 &proof_out) +{ + // 1. prepare KI + crypto::key_image KI; + make_seraphis_key_image(y, z, KI); + + // 2. prepare the message to sign + rct::key message; + make_enote_key_image_proof_message_v1(onetime_address, KI, message); + + // 3. create the composition proof + SpCompositionProof composition_proof; + make_sp_composition_proof(message, onetime_address, x, y, z, composition_proof); + + // 4. assemble the full proof + proof_out = EnoteKeyImageProofV1{ + .Ko = onetime_address, + .KI = KI, + .composition_proof = composition_proof + }; +} +//------------------------------------------------------------------------------------------------------------------- +void make_enote_key_image_proof_v1(const SpEnoteRecordV1 &enote_record, + const crypto::secret_key &sp_spend_privkey, + const crypto::secret_key &k_view_balance, + EnoteKeyImageProofV1 &proof_out) +{ + // 1. y = k_x + k_vb + crypto::secret_key y; + sc_add(to_bytes(y), to_bytes(enote_record.enote_view_extension_x), to_bytes(k_view_balance)); + + // 2. z = k_u + k_m + crypto::secret_key z; + sc_add(to_bytes(z), to_bytes(enote_record.enote_view_extension_u), to_bytes(sp_spend_privkey)); + + // 3. complete the full proof + make_enote_key_image_proof_v1(onetime_address_ref(enote_record.enote), + enote_record.enote_view_extension_g, + y, + z, + proof_out); +} +//------------------------------------------------------------------------------------------------------------------- +bool verify_enote_key_image_proof_v1(const EnoteKeyImageProofV1 &proof, + const rct::key &expected_onetime_address, + const crypto::key_image &expected_KI) +{ + // 1. check the proof Ko matches the expected onetime address + if (!(proof.Ko == expected_onetime_address)) + return false; + + // 2. check the proof KI matches the expected key image + if (!(proof.KI == expected_KI)) + return false; + + // 3. verify that the key image is in the prime-order subgroup + if (!key_domain_is_prime_subgroup(rct::ki2rct(proof.KI))) + return false; + + // 4. validate the composition proof + rct::key message; + make_enote_key_image_proof_message_v1(proof.Ko, proof.KI, message); + + if (!verify_sp_composition_proof(proof.composition_proof, message, proof.Ko, proof.KI)) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +void make_enote_unspent_proof_v1(const SpEnoteRecordV1 &enote_record, + const crypto::secret_key &sp_spend_privkey, + const crypto::secret_key &k_view_balance, + const crypto::key_image &test_KI, + EnoteUnspentProofV1 &proof_out) +{ + // 1. prepare private key components + // note: pubkey components will be stored in the matrix proofs + // a. ko_g = k_g + const crypto::secret_key kog_skey{enote_record.enote_view_extension_g}; + + // b. ko_x = (k_x + k_vb) + crypto::secret_key kox_skey; + sc_add(to_bytes(kox_skey), to_bytes(enote_record.enote_view_extension_x), to_bytes(k_view_balance)); + + // c. ko_u = (k_u + k_m) + crypto::secret_key kou_skey; + sc_add(to_bytes(kou_skey), to_bytes(enote_record.enote_view_extension_u), to_bytes(sp_spend_privkey)); + + // 2. message to sign in the proofs + rct::key message; + make_enote_unspent_proof_message_v1(onetime_address_ref(enote_record.enote), test_KI, message); + + // 3. proof: k_g G on G + MatrixProof kog_proof; + make_matrix_proof(message, {crypto::get_G()}, {kog_skey}, kog_proof); + + // 4. proof: {ko_x X, (k_x + k_vb)*test_KI} on {X, test_KI} + MatrixProof kox_proof; + make_matrix_proof(message, {crypto::get_X(), rct::rct2pk(rct::ki2rct(test_KI))}, {kox_skey}, kox_proof); + + // 5. proof: ko_u U on U + MatrixProof kou_proof; + make_matrix_proof(message, {crypto::get_U()}, {kou_skey}, kou_proof); + + // 6. assemble full proof + proof_out = EnoteUnspentProofV1{ + .Ko = onetime_address_ref(enote_record.enote), + .test_KI = test_KI, + .g_component_proof = std::move(kog_proof), + .x_component_transform_proof = std::move(kox_proof), + .u_component_proof = std::move(kou_proof) + }; +} +//------------------------------------------------------------------------------------------------------------------- +bool verify_enote_unspent_proof_v1(const EnoteUnspentProofV1 &proof, + const rct::key &expected_onetime_address, + const crypto::key_image &expected_test_KI) +{ + // 1. check the proof matches with the expected onetime address + if (!(proof.Ko == expected_onetime_address)) + return false; + + // 2. check the proof matches with the expected test key image + if (!(proof.test_KI == expected_test_KI)) + return false; + + // 3. check that the onetime address can be reconstructed from internal proof components + if (proof.g_component_proof.M.size() != 1 || + proof.g_component_proof.M[0].size() != 1) + return false; + if (proof.x_component_transform_proof.M.size() != 2 || + proof.x_component_transform_proof.M[0].size() != 1 || + proof.x_component_transform_proof.M[1].size() != 1) + return false; + if (proof.u_component_proof.M.size() != 1 || + proof.u_component_proof.M[0].size() != 1) + return false; + + rct::key nominal_Ko{rct::pk2rct(proof.g_component_proof.M[0][0])}; //Ko_g + rct::addKeys(nominal_Ko, rct::pk2rct(proof.x_component_transform_proof.M[0][0]), nominal_Ko); //+ Ko_x + rct::addKeys(nominal_Ko, rct::pk2rct(proof.u_component_proof.M[0][0]), nominal_Ko); //+ Ko_u + nominal_Ko = rct::scalarmult8(nominal_Ko); + + if (!(proof.Ko == nominal_Ko)) + return false; + + // 4. message that should have been signed in the proofs + rct::key expected_message; + make_enote_unspent_proof_message_v1(proof.Ko, proof.test_KI, expected_message); + + // 5. validate proof on Ko_g + if (!(proof.g_component_proof.m == expected_message)) + return false; + if (!verify_matrix_proof(proof.g_component_proof, {crypto::get_G()})) + return false; + + // 6. validate proof on Ko_x + if (!(proof.x_component_transform_proof.m == expected_message)) + return false; + if (!verify_matrix_proof( + proof.x_component_transform_proof, + { + crypto::get_X(), + rct::rct2pk(rct::ki2rct(proof.test_KI)) + } + )) + return false; + + // 7. validate proof on Ko_u + if (!(proof.u_component_proof.m == expected_message)) + return false; + if (!verify_matrix_proof(proof.u_component_proof, {crypto::get_U()})) + return false; + + // 8. check if Ko_u == (k_x + k_vb)*test_KI + // - if so, then the test KI corresponds to the proof's enote, which implies the enote is spent (assuming only key + // images of spent enotes are tested) + if (rct::scalarmult8(rct::pk2rct(proof.u_component_proof.M[0][0])) == + rct::scalarmult8(rct::pk2rct(proof.x_component_transform_proof.M[1][0]))) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +void make_tx_funded_proof_v1(const rct::key &message, + const SpEnoteRecordV1 &enote_record, + const crypto::secret_key &sp_spend_privkey, + const crypto::secret_key &k_view_balance, + TxFundedProofV1 &proof_out) +{ + // 1. prepare a masked version of our enote's onetime address + const crypto::secret_key t_k_new{rct::rct2sk(rct::skGen())}; + + rct::key masked_address; + mask_key(t_k_new, onetime_address_ref(enote_record.enote), masked_address); //K" = t_k_new G + Ko + + // 2. prepare privkeys of K" + // a. x = t_k_new + k_g + crypto::secret_key x; + sc_add(to_bytes(x), to_bytes(t_k_new), to_bytes(enote_record.enote_view_extension_g)); + + // b. y = k_x + k_vb + crypto::secret_key y; + sc_add(to_bytes(y), to_bytes(enote_record.enote_view_extension_x), to_bytes(k_view_balance)); + + // c. z = k_u + k_m + crypto::secret_key z; + sc_add(to_bytes(z), to_bytes(enote_record.enote_view_extension_u), to_bytes(sp_spend_privkey)); + + // 3. make the composition proof + SpCompositionProof composition_proof; + make_sp_composition_proof(message, masked_address, x, y, z, composition_proof); + + // 4. assemble the full proof + proof_out = TxFundedProofV1{ + .message = message, + .masked_address = masked_address, + .KI = enote_record.key_image, + .composition_proof = composition_proof + }; +} +//------------------------------------------------------------------------------------------------------------------- +bool verify_tx_funded_proof_v1(const TxFundedProofV1 &proof, + const rct::key &expected_message, + const crypto::key_image &expected_KI) +{ + // 1. check the proof matches with the expected message + if (!(proof.message == expected_message)) + return false; + + // 2. check the proof matches with the expected key image + if (!(proof.KI == expected_KI)) + return false; + + // 3. validate the composition proof + if (!verify_sp_composition_proof(proof.composition_proof, proof.message, proof.masked_address, proof.KI)) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +void make_enote_sent_proof_v1(const EnoteOwnershipProofV1 &ownership_proof, + const EnoteAmountProofV1 &amount_proof, + EnoteSentProofV1 &proof_out) +{ + proof_out = EnoteSentProofV1{ + .enote_ownership_proof = ownership_proof, + .amount_proof = amount_proof + }; +} +//------------------------------------------------------------------------------------------------------------------- +bool verify_enote_sent_proof_v1(const EnoteSentProofV1 &proof, + const rct::key &expected_amount_commitment, + const rct::key &expected_onetime_address) +{ + // 1. verify the enote ownership proof + if (!verify_enote_ownership_proof_v1(proof.enote_ownership_proof, + expected_amount_commitment, + expected_onetime_address)) + return false; + + // 2. verify the amount proof + if (!verify_enote_amount_proof_v1(proof.amount_proof, expected_amount_commitment)) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +void make_reserved_enote_proof_v1(const EnoteOwnershipProofV1 &enote_ownership_proof, + const EnoteAmountProofV1 &amount_proof, + const EnoteKeyImageProofV1 &key_image_proof, + const std::uint64_t enote_ledger_index, + ReservedEnoteProofV1 &proof_out) +{ + proof_out = ReservedEnoteProofV1{ + .enote_ownership_proof = enote_ownership_proof, + .amount_proof = amount_proof, + .KI_proof = key_image_proof, + .enote_ledger_index = enote_ledger_index + }; +} +//------------------------------------------------------------------------------------------------------------------- +void make_reserved_enote_proof_v1(const SpContextualEnoteRecordV1 &contextual_record, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &sp_spend_privkey, + const crypto::secret_key &k_view_balance, + ReservedEnoteProofV1 &proof_out) +{ + // 1. make enote ownership proof + EnoteOwnershipProofV1 enote_ownership_proof; + make_enote_ownership_proof_v1_receiver(contextual_record.record, + jamtis_spend_pubkey, + k_view_balance, + enote_ownership_proof); + + // 2. make amount proof + EnoteAmountProofV1 amount_proof; + make_enote_amount_proof_v1(contextual_record.record.amount, + contextual_record.record.amount_blinding_factor, + amount_commitment_ref(contextual_record.record.enote), + amount_proof); + + // 3. make key image proof + EnoteKeyImageProofV1 key_image_proof; + make_enote_key_image_proof_v1(contextual_record.record, + sp_spend_privkey, + k_view_balance, + key_image_proof); + + // 4. complete full proof + make_reserved_enote_proof_v1(enote_ownership_proof, + amount_proof, + key_image_proof, + contextual_record.origin_context.enote_ledger_index, + proof_out); +} +//------------------------------------------------------------------------------------------------------------------- +bool verify_reserved_enote_proof_v1(const ReservedEnoteProofV1 &proof, + const rct::key &expected_amount_commitment, + const rct::key &expected_onetime_address, + const std::uint64_t expected_enote_ledger_index) +{ + // 1. verify the enote ownership proof + if (!verify_enote_ownership_proof_v1(proof.enote_ownership_proof, + expected_amount_commitment, + expected_onetime_address)) + return false; + + // 2. verify the enote amount proof + if (!verify_enote_amount_proof_v1(proof.amount_proof, expected_amount_commitment)) + return false; + + // 3. verify the key image proof + // note: we don't need an 'expected key image' here because our key image proof just needs to show that the proof's + // key image is derived from the onetime address of the reserved enote + if (!verify_enote_key_image_proof_v1(proof.KI_proof, expected_onetime_address, proof.KI_proof.KI)) + return false; + + // 4. check the proof matches the expected enote ledger index + if (proof.enote_ledger_index != expected_enote_ledger_index) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool reserved_enote_is_reserved_v1(const ReservedEnoteProofV1 &proof, const TxValidationContext &validation_context) +{ + // 1. try to get the squashed enote from the context + // - an enote is only 'reserved' if it exists onchain + rct::keyV squashed_enote_ref; + + try { validation_context.get_reference_set_proof_elements_v2({proof.enote_ledger_index}, squashed_enote_ref); } + catch (...) { return false; } + + if (squashed_enote_ref.size() != 1) + return false; + + // 2. compute the reserved enote's squashed enote representation + rct::key squashed_enote_representation; + make_seraphis_squashed_enote_Q(proof.enote_ownership_proof.Ko, + proof.enote_ownership_proof.C, + squashed_enote_representation); + + // 3. check that the squashed enote reference matches the representation + if (!(squashed_enote_ref[0] == squashed_enote_representation)) + return false; + + // 4. check that the key image is not in the context + // - an enote is only 'reserved' if it is unspent + if (validation_context.seraphis_key_image_exists(proof.KI_proof.KI)) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +void make_reserve_proof_v1(const rct::key &message, + const std::vector &reserved_enote_records, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &sp_spend_privkey, + const crypto::secret_key &k_view_balance, + ReserveProofV1 &proof_out) +{ + // 1. make randomized indices into the records + std::vector record_indices(reserved_enote_records.size(), 0); + std::iota(record_indices.begin(), record_indices.end(), 0); + std::shuffle(record_indices.begin(), record_indices.end(), crypto::random_device{}); + + // 2. make reserved enote proofs and collect addresses that need address ownership proofs + std::vector reserved_enote_proofs; + reserved_enote_proofs.reserve(reserved_enote_records.size()); + std::unordered_set address_indices; + address_indices.reserve(reserved_enote_records.size()); + + for (const std::size_t i : record_indices) + { + const SpContextualEnoteRecordV1 &record{reserved_enote_records[i]}; + + // a. skip records that aren't onchain + if (record.origin_context.origin_status != SpEnoteOriginStatus::ONCHAIN) + continue; + + // b. skip records that aren't unspent + if (record.spent_context.spent_status != SpEnoteSpentStatus::UNSPENT) + continue; + + // c. make a reserved enote proof + make_reserved_enote_proof_v1(record, + jamtis_spend_pubkey, + sp_spend_privkey, + k_view_balance, + tools::add_element(reserved_enote_proofs)); + + // d. save the address index + address_indices.insert(record.record.address_index); + } + + // 3. make address ownership proofs for all the unique addresses that own records in the reserve proof + std::vector address_ownership_proofs; + address_ownership_proofs.reserve(address_indices.size()); + + for (const jamtis::address_index_t &j : address_indices) + { + make_address_ownership_proof_v1(message, + sp_spend_privkey, + k_view_balance, + j, + tools::add_element(address_ownership_proofs)); + } + + // 4. assemble the full proof + proof_out = ReserveProofV1{ + .address_ownership_proofs = std::move(address_ownership_proofs), + .reserved_enote_proofs = std::move(reserved_enote_proofs) + }; +} +//------------------------------------------------------------------------------------------------------------------- +bool verify_reserve_proof_v1(const ReserveProofV1 &proof, + const rct::key &expected_message, + const TxValidationContext &validation_context) +{ + // 1. validate the address ownership proofs against the expected message + std::unordered_set found_addresses; + found_addresses.reserve(proof.address_ownership_proofs.size()); + + for (const AddressOwnershipProofV1 &address_ownership_proof : proof.address_ownership_proofs) + { + // a. verify the proof + // - we don't check expected addresses, since a reserve proof's goal is to demonstrate ownership of funds by + // 'any' addresses + if (!verify_address_ownership_proof_v1(address_ownership_proof, expected_message, address_ownership_proof.K)) + return false; + + // b. save the address from this proof + found_addresses.insert(address_ownership_proof.K); + } + + // 2. check all the reserved enote proofs + for (const ReservedEnoteProofV1 &reserved_enote_proof : proof.reserved_enote_proofs) + { + // a. check that the owning address K_1 in each of the reserved enote proofs corresponds to an address owned by + // the prover + if (found_addresses.find(reserved_enote_proof.enote_ownership_proof.K_1) == found_addresses.end()) + return false; + + // b. check that the enotes referenced by the reserved enote proofs are in the ledger and unspent + if (!reserved_enote_is_reserved_v1(reserved_enote_proof, validation_context)) + return false; + + // c. validate the reserved enote proofs + // - we don't check expected values because all we care about is validity (we already checked address consistency) + if (!verify_reserved_enote_proof_v1(reserved_enote_proof, + reserved_enote_proof.enote_ownership_proof.C, + reserved_enote_proof.enote_ownership_proof.Ko, + reserved_enote_proof.enote_ledger_index)) + return false; + } + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +boost::multiprecision::uint128_t total_reserve_amount(const ReserveProofV1 &proof) +{ + boost::multiprecision::uint128_t total_amount{0}; + + for (const ReservedEnoteProofV1 &reserved_enote_proof : proof.reserved_enote_proofs) + total_amount += reserved_enote_proof.amount_proof.a; + + return total_amount; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace knowledge_proofs +} //namespace sp diff --git a/src/seraphis_main/sp_knowledge_proof_utils.h b/src/seraphis_main/sp_knowledge_proof_utils.h new file mode 100644 index 0000000000..30d4323358 --- /dev/null +++ b/src/seraphis_main/sp_knowledge_proof_utils.h @@ -0,0 +1,331 @@ +// Copyright (c) 2023, 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 making and verifying seraphis knowledge proofs. + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/jamtis_destination.h" +#include "seraphis_core/jamtis_support_types.h" +#include "seraphis_main/contextual_enote_record_types.h" +#include "seraphis_main/enote_record_types.h" +#include "seraphis_main/sp_knowledge_proof_types.h" +#include "seraphis_main/tx_validation_context.h" + +//third party headers +#include + +//standard headers +#include + +//forward declarations + + +namespace sp +{ +namespace knowledge_proofs +{ + +/** +* brief: make an address ownership proof +* param: message - message provided by verifier +* param: address - address with the format xG + yX + zU (e.g. K_1 or K_s) +* param: x - secret key corresponding to base G +* param: y - secret key corresponding to base X +* param: z - secret key corresponding to base U +* outparam: proof_out - proof created +*/ +void make_address_ownership_proof_v1(const rct::key &message, + const rct::key &address, + const crypto::secret_key &x, + const crypto::secret_key &y, + const crypto::secret_key &z, + AddressOwnershipProofV1 &proof_out); +void make_address_ownership_proof_v1(const rct::key &message, //for K_s + const crypto::secret_key &sp_spend_privkey, + const crypto::secret_key &k_view_balance, + AddressOwnershipProofV1 &proof_out); +void make_address_ownership_proof_v1(const rct::key &message, //for K_1 + const crypto::secret_key &sp_spend_privkey, + const crypto::secret_key &k_view_balance, + const jamtis::address_index_t &j, + AddressOwnershipProofV1 &proof_out); +/** +* brief: verify address ownership proof +* param: proof - proof to verify +* param: expected_message - message expected in the proof +* param: expected_address - address expected in the proof +* return: true/false according to proof validity +*/ +bool verify_address_ownership_proof_v1(const AddressOwnershipProofV1 &proof, + const rct::key &expected_message, + const rct::key &expected_address); +/** +* brief: make an address index proof +* param: jamtis_spend_pubkey - K_s +* param: j - address index +* param: s_generate_address - s_ga +* outparam: proof_out - proof created +*/ +void make_address_index_proof_v1(const rct::key &jamtis_spend_pubkey, + const jamtis::address_index_t &j, + const crypto::secret_key &s_generate_address, + AddressIndexProofV1 &proof_out); +/** +* brief: verify address index proof +* param: proof - proof to verify +* param: expected_address - address this proof should be about +* return: true/false according to proof validity +*/ +bool verify_address_index_proof_v1(const AddressIndexProofV1 &proof, const rct::key &expected_address); +/** +* brief: make an enote ownership proof +* param: jamtis_address_spend_key - K_1 +* param: sender_receiver_secret - q +* param: amount_commitment - C +* param: onetime_address - Ko +* outparam: proof_out - proof created +*/ +void make_enote_ownership_proof_v1(const rct::key &jamtis_address_spend_key, + const rct::key &sender_receiver_secret, + const rct::key &amount_commitment, + const rct::key &onetime_address, + EnoteOwnershipProofV1 &proof_out); +void make_enote_ownership_proof_v1_sender_plain(const crypto::x25519_secret_key &enote_ephemeral_privkey, + const jamtis::JamtisDestinationV1 &recipient_destination, + const rct::key &input_context, + const rct::key &amount_commitment, + const rct::key &onetime_address, + EnoteOwnershipProofV1 &proof_out); +void make_enote_ownership_proof_v1_sender_selfsend(const crypto::x25519_pubkey &enote_ephemeral_pubkey, + const rct::key &jamtis_address_spend_key, + const rct::key &input_context, + const crypto::secret_key &k_view_balance, + const jamtis::JamtisSelfSendType self_send_type, + const rct::key &amount_commitment, + const rct::key &onetime_address, + EnoteOwnershipProofV1 &proof_out); +void make_enote_ownership_proof_v1_receiver(const SpEnoteRecordV1 &enote_record, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + EnoteOwnershipProofV1 &proof_out); +/** +* brief: verify enote ownership proof +* param: proof - proof to verify +* param: expected_amount_commitment - expected amount commitment of the proof enote +* param: expected_onetime_address - expected onetime address of the proof enote +* return: true/false according to proof validity +*/ +bool verify_enote_ownership_proof_v1(const EnoteOwnershipProofV1 &proof, + const rct::key &expected_amount_commitment, + const rct::key &expected_onetime_address); +/** +* brief: make an enote amount proof +* param: amount - xmr amount a +* param: mask - blinding factor x +* param: commitment - C = xG+aH +* outparam: proof_out - proof created +*/ +void make_enote_amount_proof_v1(const rct::xmr_amount &amount, + const crypto::secret_key &mask, + const rct::key &commitment, + EnoteAmountProofV1 &proof_out); +/** +* brief: verify enote amount proof +* param: proof - proof to verify +* param: expected_commitment - commitment expected to be in the proof +* return: true/false according to proof validity +*/ +bool verify_enote_amount_proof_v1(const EnoteAmountProofV1 &proof, const rct::key &expected_commitment); +/** +* brief: make an enote key image proof +* param: onetime_address - address which has the format xG + yX + zU. +* param: x - secret key corresponding to base G +* param: y - secret key corresponding to base X +* param: z - secret key corresponding to base U +* outparam: proof_out - proof created +*/ +void make_enote_key_image_proof_v1(const rct::key &onetime_address, + const crypto::secret_key &x, + const crypto::secret_key &y, + const crypto::secret_key &z, + EnoteKeyImageProofV1 &proof_out); +void make_enote_key_image_proof_v1(const SpEnoteRecordV1 &enote_record, + const crypto::secret_key &sp_spend_privkey, + const crypto::secret_key &k_view_balance, + EnoteKeyImageProofV1 &proof_out); +/** +* brief: verify enote key image proof +* param: proof - proof to verify +* param: expected_onetime_address - expected Ko in the proof +* param: expected_KI - expected KI in the proof +* return: true/false according to proof validity +*/ +bool verify_enote_key_image_proof_v1(const EnoteKeyImageProofV1 &proof, + const rct::key &expected_onetime_address, + const crypto::key_image &expected_KI); +/** +* brief: make an enote unspent proof +* param: enote_record - record of the enote for this proof +* param: sp_spend_privkey - k_m +* param: k_view_balance - k_vb +* param: test_KI - key image this proof shows does NOT correspond to the proof enote +* outparam: proof_out - proof created +*/ +void make_enote_unspent_proof_v1(const SpEnoteRecordV1 &enote_record, + const crypto::secret_key &sp_spend_privkey, + const crypto::secret_key &k_view_balance, + const crypto::key_image &test_KI, + EnoteUnspentProofV1 &proof_out); +/** +* brief: verify enote unspent proof +* param: proof - proof to verify +* param: expected_onetime_address - expected onetime address of the proof enote +* param: expected_test_KI - expected test key image +* return: true/false according to proof validity +*/ +bool verify_enote_unspent_proof_v1(const EnoteUnspentProofV1 &proof, + const rct::key &expected_onetime_address, + const crypto::key_image &expected_test_KI); +/** +* brief: make a funded tx proof +* param: message - message provided by verifier +* param: enote_record - enote_record containing all the mask openings +* param: onetime_address - address which has the format xG + yX + zU. +* param: k_vb - view_balance secret key +* param: k_m - master secret key +* outparam: proof_out - proof created +*/ +void make_tx_funded_proof_v1(const rct::key &message, + const SpEnoteRecordV1 &enote_record, + const crypto::secret_key &sp_spend_privkey, + const crypto::secret_key &k_view_balance, + TxFundedProofV1 &proof_out); +/** +* brief: verify funded tx proof +* param: proof - proof to verify +* param: expected_message - expected message to be signed by the proof +* param: expected_KI - expected key image for the proof +* return: true/false according to proof validity +*/ +bool verify_tx_funded_proof_v1(const TxFundedProofV1 &proof, + const rct::key &expected_message, + const crypto::key_image &expected_KI); +/** +* brief: make an enote sent proof +* param: ownership_proof - proof of enote ownership +* param: amount_proof - proof of enote amount +* outparam: proof_out - proof created +*/ +void make_enote_sent_proof_v1(const EnoteOwnershipProofV1 &ownership_proof, + const EnoteAmountProofV1 &amount_proof, + EnoteSentProofV1 &proof_out); +/** +* brief: verify enote sent proof +* param: proof - proof to verify +* param: expected_amount_commitment - expected amount commitment of the proof enote +* param: expected_onetime_address - expected onetime address of the proof enote +* return: true/false according to proof validity +*/ +bool verify_enote_sent_proof_v1(const EnoteSentProofV1 &proof, + const rct::key &expected_amount_commitment, + const rct::key &expected_onetime_address); +/** +* brief: make a reserved enote proof +* param: enote_ownership_proof - component proof +* param: amount_proof - component proof +* param: key_image_proof - component proof +* param: enote_ledger_index - ledger index of the reserved enote +* outparam: proof_out - proof created +*/ +void make_reserved_enote_proof_v1(const EnoteOwnershipProofV1 &enote_ownership_proof, + const EnoteAmountProofV1 &amount_proof, + const EnoteKeyImageProofV1 &key_image_proof, + const std::uint64_t enote_ledger_index, + ReservedEnoteProofV1 &proof_out); +void make_reserved_enote_proof_v1(const SpContextualEnoteRecordV1 &contextual_record, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &sp_spend_privkey, + const crypto::secret_key &k_view_balance, + ReservedEnoteProofV1 &proof_out); +/** +* brief: verify reserved enote proof +* param: proof - proof to verify +* param: expected_amount_commitment - commitment that should be in the proof +* param: expected_onetime_address - onetime address that should be in the proof +* return: true/false according to proof validity +*/ +bool verify_reserved_enote_proof_v1(const ReservedEnoteProofV1 &proof, + const rct::key &expected_amount_commitment, + const rct::key &expected_onetime_address, + const std::uint64_t expected_enote_ledger_index); +/** +* brief: check if the reserved enote in a reserved enote proof is onchain and unspent +* NOTE: does not verify the reserved enote proof +* param: proof - proof to check +* param: validation_context - context to check the proof against +* return: true if the enote is reserved (onchain and unspent) +*/ +bool reserved_enote_is_reserved_v1(const ReservedEnoteProofV1 &proof, const TxValidationContext &validation_context); +/** +* brief: make a reserve proof +* param: message - message provided by the verifier +* param: reserved_enote_records - enotes for the proof +* param: jamtis_spend_pubkey - K_s +* param: sp_spend_privkey - k_m +* param: k_view_balance - k_vb +* outparam: proof_out - proof created +*/ +void make_reserve_proof_v1(const rct::key &message, + const std::vector &reserved_enote_records, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &sp_spend_privkey, + const crypto::secret_key &k_view_balance, + ReserveProofV1 &proof_out); +/** +* brief: verify reserve proof +* param: proof - proof to verify +* param: expected_message - message that should be signed in the proof's address ownership proofs +* param: validation_context - context to check the reserved enotes against (to see if they are onchain and unspent) +* return: true/false according to proof validity +*/ +bool verify_reserve_proof_v1(const ReserveProofV1 &proof, + const rct::key &expected_message, + const TxValidationContext &validation_context); +/** +* brief: get the total amount in a reserve proof +* param: proof - proof to get amounts from +* return: total reserved amount +*/ +boost::multiprecision::uint128_t total_reserve_amount(const ReserveProofV1 &proof); + +} //namespace knowledge_proofs +} //namespace sp diff --git a/src/seraphis_main/tx_builder_types.cpp b/src/seraphis_main/tx_builder_types.cpp new file mode 100644 index 0000000000..2e34e07305 --- /dev/null +++ b/src/seraphis_main/tx_builder_types.cpp @@ -0,0 +1,258 @@ +// Copyright (c) 2022, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//paired header +#include "tx_builder_types.h" + +//local headers +#include "common/container_helpers.h" +#include "crypto/crypto.h" +#include "crypto/x25519.h" +#include "misc_log_ex.h" +#include "ringct/rctOps.h" +#include "ringct/rctTypes.h" +#include "tx_builders_inputs.h" +#include "tx_builders_mixed.h" +#include "tx_builders_outputs.h" +#include "tx_component_types.h" +#include "txtype_base.h" + +//third party headers + +//standard headers +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis" + +namespace sp +{ +//------------------------------------------------------------------------------------------------------------------- +rct::xmr_amount amount_ref(const SpInputProposalV1 &proposal) +{ + return proposal.core.amount; +} +//------------------------------------------------------------------------------------------------------------------- +const crypto::key_image& key_image_ref(const SpInputProposalV1 &proposal) +{ + return proposal.core.key_image; +} +//------------------------------------------------------------------------------------------------------------------- +rct::xmr_amount amount_ref(const SpCoinbaseOutputProposalV1 &proposal) +{ + return proposal.enote.core.amount; +} +//------------------------------------------------------------------------------------------------------------------- +rct::xmr_amount amount_ref(const SpOutputProposalV1 &proposal) +{ + return proposal.core.amount; +} +//------------------------------------------------------------------------------------------------------------------- +bool compare_Ko(const SpCoinbaseOutputProposalV1 &a, const SpCoinbaseOutputProposalV1 &b) +{ + return compare_Ko(a.enote, b.enote); +} +//------------------------------------------------------------------------------------------------------------------- +bool compare_Ko(const SpOutputProposalV1 &a, const SpOutputProposalV1 &b) +{ + return compare_Ko(a.core, b.core); +} +//------------------------------------------------------------------------------------------------------------------- +bool compare_KI(const SpInputProposalV1 &a, const SpInputProposalV1 &b) +{ + return compare_KI(a.core, b.core); +} +//------------------------------------------------------------------------------------------------------------------- +bool compare_KI(const SpPartialInputV1 &a, const SpPartialInputV1 &b) +{ + return compare_KI(a.input_image, b.input_image); +} +//------------------------------------------------------------------------------------------------------------------- +bool alignment_check(const SpAlignableMembershipProofV1 &a, const SpAlignableMembershipProofV1 &b) +{ + return a.masked_address == b.masked_address; +} +//------------------------------------------------------------------------------------------------------------------- +bool alignment_check(const SpAlignableMembershipProofV1 &proof, const rct::key &masked_address) +{ + return proof.masked_address == masked_address; +} +//------------------------------------------------------------------------------------------------------------------- +void get_enote_image_v1(const SpInputProposalV1 &proposal, SpEnoteImageV1 &image_out) +{ + get_enote_image_core(proposal.core, image_out.core); +} +//------------------------------------------------------------------------------------------------------------------- +void get_squash_prefix(const SpInputProposalV1 &proposal, rct::key &squash_prefix_out) +{ + get_squash_prefix(proposal.core, squash_prefix_out); +} +//------------------------------------------------------------------------------------------------------------------- +void get_enote_v1(const SpOutputProposalV1 &proposal, SpEnoteV1 &enote_out) +{ + // enote core + enote_out.core.onetime_address = proposal.core.onetime_address; + enote_out.core.amount_commitment = + rct::commit(amount_ref(proposal), rct::sk2rct(proposal.core.amount_blinding_factor)); + + // enote misc. details + enote_out.encoded_amount = proposal.encoded_amount; + enote_out.addr_tag_enc = proposal.addr_tag_enc; + enote_out.view_tag = proposal.view_tag; +} +//------------------------------------------------------------------------------------------------------------------- +void get_coinbase_output_proposals_v1(const SpCoinbaseTxProposalV1 &tx_proposal, + std::vector &output_proposals_out) +{ + // output proposals + output_proposals_out.clear(); + output_proposals_out.reserve(tx_proposal.normal_payment_proposals.size()); + + for (const jamtis::JamtisPaymentProposalV1 &payment_proposal : tx_proposal.normal_payment_proposals) + { + make_v1_coinbase_output_proposal_v1(payment_proposal, + tx_proposal.block_height, + tools::add_element(output_proposals_out)); + } + + // sort output proposals + std::sort(output_proposals_out.begin(), + output_proposals_out.end(), + tools::compare_func(compare_Ko)); +} +//------------------------------------------------------------------------------------------------------------------- +void get_output_proposals_v1(const SpTxProposalV1 &tx_proposal, + const crypto::secret_key &k_view_balance, + std::vector &output_proposals_out) +{ + CHECK_AND_ASSERT_THROW_MES(tx_proposal.normal_payment_proposals.size() + + tx_proposal.selfsend_payment_proposals.size() > 0, + "Tried to get output proposals for a tx proposal with no outputs!"); + + // input context + rct::key input_context; + make_standard_input_context_v1(tx_proposal.legacy_input_proposals, tx_proposal.sp_input_proposals, input_context); + + // output proposals + output_proposals_out.clear(); + output_proposals_out.reserve(tx_proposal.normal_payment_proposals.size() + + tx_proposal.selfsend_payment_proposals.size()); + + for (const jamtis::JamtisPaymentProposalV1 &normal_payment_proposal : tx_proposal.normal_payment_proposals) + make_v1_output_proposal_v1(normal_payment_proposal, input_context, tools::add_element(output_proposals_out)); + + for (const jamtis::JamtisPaymentProposalSelfSendV1 &selfsend_payment_proposal : + tx_proposal.selfsend_payment_proposals) + { + make_v1_output_proposal_v1(selfsend_payment_proposal, + k_view_balance, + input_context, + tools::add_element(output_proposals_out)); + } + + // sort output proposals + std::sort(output_proposals_out.begin(), + output_proposals_out.end(), + tools::compare_func(compare_Ko)); +} +//------------------------------------------------------------------------------------------------------------------- +void get_tx_proposal_prefix_v1(const SpTxProposalV1 &tx_proposal, + const tx_version_t &tx_version, + const crypto::secret_key &k_view_balance, + rct::key &tx_proposal_prefix_out) +{ + // get output proposals + std::vector output_proposals; + get_output_proposals_v1(tx_proposal, k_view_balance, output_proposals); + + // sanity check semantics + check_v1_output_proposal_set_semantics_v1(output_proposals); + + // make the proposal prefix + make_tx_proposal_prefix_v1(tx_version, + tx_proposal.legacy_input_proposals, + tx_proposal.sp_input_proposals, + output_proposals, + tx_proposal.tx_fee, + tx_proposal.partial_memo, + tx_proposal_prefix_out); +} +//------------------------------------------------------------------------------------------------------------------- +SpInputProposalV1 gen_sp_input_proposal_v1(const crypto::secret_key &sp_spend_privkey, + const crypto::secret_key &k_view_balance, + const rct::xmr_amount amount) +{ + SpInputProposalV1 temp; + temp.core = gen_sp_input_proposal_core(sp_spend_privkey, k_view_balance, amount); + return temp; +} +//------------------------------------------------------------------------------------------------------------------- +SpCoinbaseOutputProposalV1 gen_sp_coinbase_output_proposal_v1(const rct::xmr_amount amount, + const std::size_t num_random_memo_elements) +{ + SpCoinbaseOutputProposalV1 temp; + + // enote + temp.enote = gen_sp_coinbase_enote_v1(); + temp.enote.core.amount = amount; + + // enote ephemeral pubkey + temp.enote_ephemeral_pubkey = crypto::x25519_pubkey_gen(); + + // partial memo + std::vector memo_elements; + memo_elements.resize(num_random_memo_elements); + for (ExtraFieldElement &element: memo_elements) + element = gen_extra_field_element(); + make_tx_extra(std::move(memo_elements), temp.partial_memo); + + return temp; +} +//------------------------------------------------------------------------------------------------------------------- +SpOutputProposalV1 gen_sp_output_proposal_v1(const rct::xmr_amount amount, const std::size_t num_random_memo_elements) +{ + SpOutputProposalV1 temp; + + // gen base of destination + temp.core = gen_sp_output_proposal_core(amount); + + temp.enote_ephemeral_pubkey = crypto::x25519_pubkey_gen(); + crypto::rand(sizeof(temp.encoded_amount), temp.encoded_amount.bytes); + crypto::rand(sizeof(temp.addr_tag_enc), temp.addr_tag_enc.bytes); + temp.view_tag = crypto::rand_idx(0); + + std::vector memo_elements; + memo_elements.resize(num_random_memo_elements); + for (ExtraFieldElement &element: memo_elements) + element = gen_extra_field_element(); + make_tx_extra(std::move(memo_elements), temp.partial_memo); + + return temp; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace sp diff --git a/src/seraphis_main/tx_builder_types.h b/src/seraphis_main/tx_builder_types.h new file mode 100644 index 0000000000..7480913ea9 --- /dev/null +++ b/src/seraphis_main/tx_builder_types.h @@ -0,0 +1,330 @@ +// Copyright (c) 2022, 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 transaction-builder helper types. + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "crypto/x25519.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/discretized_fee.h" +#include "seraphis_core/jamtis_payment_proposal.h" +#include "seraphis_core/sp_core_types.h" +#include "seraphis_core/tx_extra.h" +#include "tx_builder_types_legacy.h" +#include "tx_component_types.h" +#include "tx_component_types_legacy.h" + +//third party headers + +//standard headers + +//forward declarations +namespace sp +{ +namespace jamtis +{ + struct JamtisPaymentProposalV1; + struct JamtisPaymentProposalSelfSendV1; +} + struct tx_version_t; +} + +namespace sp +{ + +//// +// SpInputProposalV1 +/// +struct SpInputProposalV1 final +{ + /// core of the proposal + SpInputProposalCore core; +}; + +/// get the proposal's amount +rct::xmr_amount amount_ref(const SpInputProposalV1 &proposal); +/// get the proposal's key image +const crypto::key_image& key_image_ref(const SpInputProposalV1 &proposal); + +//// +// SpCoinbaseOutputProposalV1 +/// +struct SpCoinbaseOutputProposalV1 final +{ + /// proposed enote + SpCoinbaseEnoteV1 enote; + + /// xK_e: enote ephemeral pubkey + crypto::x25519_pubkey enote_ephemeral_pubkey; + /// memo elements to add to the tx memo + TxExtra partial_memo; +}; + +/// get the proposal's amount +rct::xmr_amount amount_ref(const SpCoinbaseOutputProposalV1 &proposal); + +//// +// SpOutputProposalV1 +/// +struct SpOutputProposalV1 final +{ + /// core of the proposal + SpOutputProposalCore core; + + /// xK_e: enote ephemeral pubkey + crypto::x25519_pubkey enote_ephemeral_pubkey; + /// enc_a + jamtis::encoded_amount_t encoded_amount; + /// addr_tag_enc + jamtis::encrypted_address_tag_t addr_tag_enc; + /// view_tag + jamtis::view_tag_t view_tag; + + /// memo elements to add to the tx memo + TxExtra partial_memo; +}; + +/// get the proposal's amount +rct::xmr_amount amount_ref(const SpOutputProposalV1 &proposal); + +//// +// SpMembershipProofPrepV1 +// - data for producing a membership proof +/// +struct SpMembershipProofPrepV1 final +{ + /// ref set size = n^m + std::size_t ref_set_decomp_n; + std::size_t ref_set_decomp_m; + /// binned representation of ledger indices of enotes referenced by the proof + /// - only enotes in the ledger can have a membership proof + SpBinnedReferenceSetV1 binned_reference_set; + /// the referenced enotes (squashed representation) + std::vector referenced_enotes_squashed; + /// the real enote being referenced (plain enote representation) + SpEnoteCoreVariant real_reference_enote; + /// image masks for the real reference + crypto::secret_key address_mask; + crypto::secret_key commitment_mask; +}; + +//// +// SpAlignableMembershipProofV1 +// - the masked address can be used to match this membership proof with the corresponding input image +// - note: matching can fail if a masked address is reused in a tx, but that is almost definitely an implementation +// error! +/// +struct SpAlignableMembershipProofV1 final +{ + /// masked address used in the membership proof (for matching with corresponding input image) + rct::key masked_address; + /// the membership proof + SpMembershipProofV1 membership_proof; +}; + +//// +// SpCoinbaseTxProposalV1 +// - the proposed block height, reward, outputs, and miscellaneous memos +/// +struct SpCoinbaseTxProposalV1 final +{ + /// block height + std::uint64_t block_height; + /// block reward + rct::xmr_amount block_reward; + /// outputs (SORTED) + std::vector normal_payment_proposals; + /// partial memo + TxExtra partial_memo; +}; + +//// +// SpTxProposalV1 +// - the proposed set of inputs and outputs, with tx fee and miscellaneous memos +/// +struct SpTxProposalV1 final +{ + /// legacy input proposals (SORTED) + std::vector legacy_input_proposals; + /// seraphis input proposals (SORTED) + std::vector sp_input_proposals; + /// outputs (SORTED) + std::vector normal_payment_proposals; + std::vector selfsend_payment_proposals; + /// tx fee + DiscretizedFee tx_fee; + /// partial memo + TxExtra partial_memo; +}; + +//// +// SpPartialInputV1 +// - enote spent +// - cached amount and amount blinding factor, and image masks (for balance and membership proofs) +// - spend proof for input (and proof the input's key image is properly constructed) +// - proposal prefix (spend proof msg) [for consistency checks when handling this object] +/// +struct SpPartialInputV1 final +{ + /// input's image + SpEnoteImageV1 input_image; + /// input image's proof (demonstrates ownership of the underlying enote and that the key image is correct) + SpImageProofV1 image_proof; + /// image masks + crypto::secret_key address_mask; + crypto::secret_key commitment_mask; + + /// tx proposal prefix (represents the tx inputs/outputs/fee/memo; signed by this partial input's image proof) + rct::key tx_proposal_prefix; + + /// the input enote's core; used for making a membership proof + SpEnoteCoreVariant input_enote_core; + /// input amount + rct::xmr_amount input_amount; + /// input amount commitment's blinding factor; used for making the balance proof + crypto::secret_key input_amount_blinding_factor; +}; + +//// +// SpPartialTxV1 +// - everything needed for a tx except seraphis input membership proofs +/// +struct SpPartialTxV1 final +{ + /// legacy tx input images (spent legacy enotes) (SORTED) + std::vector legacy_input_images; + /// seraphis tx input images (spent seraphis enotes) (SORTED) + std::vector sp_input_images; + /// tx outputs (new enotes) (SORTED) + std::vector outputs; + /// balance proof (balance proof and range proofs) + SpBalanceProofV1 balance_proof; + /// legacy ring signatures: membership/ownership/unspentness for each legacy input (ALIGNED TO LEGACY INPUTS) + std::vector legacy_ring_signatures; + /// composition proofs: ownership/unspentness for each seraphis input (ALIGNED TO SERAPHIS INPUTS) + std::vector sp_image_proofs; + /// tx fee (discretized representation) + DiscretizedFee tx_fee; + /// supplemental data for tx + SpTxSupplementV1 tx_supplement; + + /// ring members for each legacy input; for validating ring signatures stored here (ALIGNED TO LEGACY INPUTS) + std::vector legacy_ring_signature_rings; + + /// seraphis input enotes; for creating seraphis input membership proofs (ALIGNED TO SERAPHIS INPUTS) + std::vector sp_input_enotes; + /// seraphis image masks; for creating seraphis input membership proofs (ALIGNED TO SERAPHIS INPUTS) + std::vector sp_address_masks; + std::vector sp_commitment_masks; +}; + +/// comparison method for sorting: a.Ko < b.Ko +bool compare_Ko(const SpCoinbaseOutputProposalV1 &a, const SpCoinbaseOutputProposalV1 &b); +bool compare_Ko(const SpOutputProposalV1 &a, const SpOutputProposalV1 &b); +/// comparison method for sorting: a.KI < b.KI +bool compare_KI(const SpInputProposalV1 &a, const SpInputProposalV1 &b); +bool compare_KI(const SpPartialInputV1 &a, const SpPartialInputV1 &b); +/// alignment checks for aligning seraphis membership proofs: test if masked addresses are equal +bool alignment_check(const SpAlignableMembershipProofV1 &a, const SpAlignableMembershipProofV1 &b); +bool alignment_check(const SpAlignableMembershipProofV1 &proof, const rct::key &masked_address); + +/** +* brief: get_enote_image_v1 - get the input proposal's enote image in the squashed enote model +* param: proposal - +* outparam: image_out - +*/ +void get_enote_image_v1(const SpInputProposalV1 &proposal, SpEnoteImageV1 &image_out); +/** +* brief: get_squash_prefix - get the input proposal's enote's squash prefix +* param: proposal - +* outparam: squash_prefix_out - H_n(Ko, C) +*/ +void get_squash_prefix(const SpInputProposalV1 &proposal, rct::key &squash_prefix_out); +/** +* brief: get_enote_v1 - extract the output proposal's enote +* param: proposal - +* outparam: enote_out - +*/ +void get_enote_v1(const SpOutputProposalV1 &proposal, SpEnoteV1 &enote_out); +/** +* brief: get_coinbase_output_proposals_v1 - convert the tx proposal's payment proposals into coinbase output proposals +* param: tx_proposal - +* outparam: output_proposals_out - +*/ +void get_coinbase_output_proposals_v1(const SpCoinbaseTxProposalV1 &tx_proposal, + std::vector &output_proposals_out); +/** +* brief: get_coinbase_output_proposals_v1 - convert the tx proposal's payment proposals into output proposals +* param: tx_proposal - +* param: k_view_balance - +* outparam: output_proposals_out - +*/ +void get_output_proposals_v1(const SpTxProposalV1 &tx_proposal, + const crypto::secret_key &k_view_balance, + std::vector &output_proposals_out); +/** +* brief: get_tx_proposal_prefix_v1 - get the message to be signed by input spend proofs +* param: tx_proposal - +* param: tx_version - +* param: k_view_balance - +* outparam: tx_proposal_prefix_out - +*/ +void get_tx_proposal_prefix_v1(const SpTxProposalV1 &tx_proposal, + const tx_version_t &tx_version, + const crypto::secret_key &k_view_balance, + rct::key &tx_proposal_prefix_out); +/** +* brief: gen_sp_input_proposal_v1 - generate an input proposal +* param: sp_spend_privkey - +* param: k_view_balance - +* param: amount - +* return: random input proposal +*/ +SpInputProposalV1 gen_sp_input_proposal_v1(const crypto::secret_key &sp_spend_privkey, + const crypto::secret_key &k_view_balance, + const rct::xmr_amount amount); +/** +* brief: gen_sp_coinbase_output_proposal_v1 - generate a coinbase output proposal +* param: amount - +* param: num_random_memo_elements - +* return: random coinbase output proposal +*/ +SpCoinbaseOutputProposalV1 gen_sp_coinbase_output_proposal_v1(const rct::xmr_amount amount, + const std::size_t num_random_memo_elements); +/** +* brief: gen_sp_output_proposal_v1 - generate an output proposal +* param: amount - +* param: num_random_memo_elements - +* return: random output proposal +*/ +SpOutputProposalV1 gen_sp_output_proposal_v1(const rct::xmr_amount amount, const std::size_t num_random_memo_elements); + +} //namespace sp diff --git a/src/seraphis_main/tx_builder_types_legacy.cpp b/src/seraphis_main/tx_builder_types_legacy.cpp new file mode 100644 index 0000000000..23ce914fc5 --- /dev/null +++ b/src/seraphis_main/tx_builder_types_legacy.cpp @@ -0,0 +1,98 @@ +// Copyright (c) 2022, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//paired header +#include "tx_builder_types_legacy.h" + +//local headers +#include "crypto/crypto.h" +#include "ringct/rctOps.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/legacy_core_utils.h" +#include "seraphis_crypto/sp_crypto_utils.h" +#include "tx_component_types_legacy.h" + +//third party headers + +//standard headers +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis" + +namespace sp +{ +//------------------------------------------------------------------------------------------------------------------- +rct::xmr_amount amount_ref(const LegacyInputProposalV1 &proposal) +{ + return proposal.amount; +} +//------------------------------------------------------------------------------------------------------------------- +bool compare_KI(const LegacyInputProposalV1 &a, const LegacyInputProposalV1 &b) +{ + return a.key_image < b.key_image; +} +//------------------------------------------------------------------------------------------------------------------- +bool compare_KI(const LegacyRingSignaturePrepV1 &a, const LegacyRingSignaturePrepV1 &b) +{ + return compare_KI(a.reference_image, b.reference_image); +} +//------------------------------------------------------------------------------------------------------------------- +bool compare_KI(const LegacyInputV1 &a, const LegacyInputV1 &b) +{ + return compare_KI(a.input_image, b.input_image); +} +//------------------------------------------------------------------------------------------------------------------- +void get_enote_image_v2(const LegacyInputProposalV1 &proposal, LegacyEnoteImageV2 &image_out) +{ + mask_key(proposal.commitment_mask, proposal.amount_commitment, image_out.masked_commitment); + image_out.key_image = proposal.key_image; +} +//------------------------------------------------------------------------------------------------------------------- +LegacyInputProposalV1 gen_legacy_input_proposal_v1(const crypto::secret_key &legacy_spend_privkey, + const rct::xmr_amount amount) +{ + LegacyInputProposalV1 temp; + + temp.enote_view_extension = rct::rct2sk(rct::skGen()); + temp.amount_blinding_factor = rct::rct2sk(rct::skGen()); + temp.amount = amount; + temp.commitment_mask = rct::rct2sk(rct::skGen()); + temp.onetime_address = rct::scalarmultBase(rct::sk2rct(legacy_spend_privkey)); + mask_key(temp.enote_view_extension, temp.onetime_address, temp.onetime_address); + temp.amount_commitment = rct::commit(temp.amount, rct::sk2rct(temp.amount_blinding_factor)); + make_legacy_key_image(temp.enote_view_extension, + legacy_spend_privkey, + temp.onetime_address, + hw::get_device("default"), + temp.key_image); + + return temp; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace sp diff --git a/src/seraphis_main/tx_builder_types_legacy.h b/src/seraphis_main/tx_builder_types_legacy.h new file mode 100644 index 0000000000..ba666bac2e --- /dev/null +++ b/src/seraphis_main/tx_builder_types_legacy.h @@ -0,0 +1,143 @@ +// Copyright (c) 2022, 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. + +// Legacy transaction-builder helper types. + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "ringct/rctTypes.h" +#include "tx_component_types_legacy.h" + +//third party headers + +//standard headers +#include + +//forward declarations + + +namespace sp +{ + +//// +// LegacyInputProposalV1 +/// +struct LegacyInputProposalV1 final +{ + /// core of the original enote + rct::key onetime_address; + rct::key amount_commitment; + /// the enote's key image + crypto::key_image key_image; + + /// Hn(k_v R_t, t) + [subaddresses: Hn(k_v, i)] (does not include legacy spend privkey k_s) + crypto::secret_key enote_view_extension; + /// x + crypto::secret_key amount_blinding_factor; + /// a + rct::xmr_amount amount; + + /// mask + crypto::secret_key commitment_mask; +}; + +/// get the proposal's amount +rct::xmr_amount amount_ref(const LegacyInputProposalV1 &proposal); + +//// +// LegacyRingSignaturePrepV1 +// - data for producing a legacy ring signature +/// +struct LegacyRingSignaturePrepV1 final +{ + /// tx proposal prefix (message to sign in the proof) + rct::key tx_proposal_prefix; + /// ledger indices of legacy enotes to be referenced by the proof + std::vector reference_set; + /// the referenced enotes ({Ko, C}((legacy)) representation) + rct::ctkeyV referenced_enotes; + /// the index of the real enote being referenced within the reference set + std::uint64_t real_reference_index; + /// enote image of the real reference (useful for sorting) + LegacyEnoteImageV2 reference_image; + /// enote view privkey of the real reference's onetime address + crypto::secret_key reference_view_privkey; + /// commitment mask applied to the reference amount commitment to produce the image's masked commitment + crypto::secret_key reference_commitment_mask; +}; + +//// +// LegacyInputV1 +// - legacy enote spent +// - legacy ring signature for the input +// - cached amount and masked amount commitment's blinding factor (for balance proof) +// - cached ring members (for validating the ring signature) +// - proposal prefix (spend proof msg) [for consistency checks when handling this object] +/// +struct LegacyInputV1 final +{ + /// input's image + LegacyEnoteImageV2 input_image; + /// input's ring signature (demonstrates ownership and membership of the underlying enote, and that the enote image + /// is correct) + LegacyRingSignatureV4 ring_signature; + + /// input amount + rct::xmr_amount input_amount; + /// input masked amount commitment's blinding factor; used for making the balance proof + crypto::secret_key input_masked_commitment_blinding_factor; + + /// cached ring members of the ring signature; used for validating the ring signature + rct::ctkeyV ring_members; + + /// tx proposal prefix (represents the inputs/outputs/fee/memo; signed by this input's ring signature) + rct::key tx_proposal_prefix; +}; + +/// comparison method for sorting: a.KI < b.KI +bool compare_KI(const LegacyInputProposalV1 &a, const LegacyInputProposalV1 &b); +bool compare_KI(const LegacyRingSignaturePrepV1 &a, const LegacyRingSignaturePrepV1 &b); +bool compare_KI(const LegacyInputV1 &a, const LegacyInputV1 &b); + +/** +* brief: get_enote_image_v2 - get this input's enote image +* outparam: image_out - +*/ +void get_enote_image_v2(const LegacyInputProposalV1 &proposal, LegacyEnoteImageV2 &image_out); +/** +* brief: gen_legacy_input_proposal_v1 - generate a legacy input proposal +* param: legacy_spend_privkey - +* param: amount - +* return: random proposal +*/ +LegacyInputProposalV1 gen_legacy_input_proposal_v1(const crypto::secret_key &legacy_spend_privkey, + const rct::xmr_amount amount); + +} //namespace sp diff --git a/src/seraphis_main/tx_builder_types_multisig.cpp b/src/seraphis_main/tx_builder_types_multisig.cpp new file mode 100644 index 0000000000..70cd3e5de6 --- /dev/null +++ b/src/seraphis_main/tx_builder_types_multisig.cpp @@ -0,0 +1,267 @@ +// Copyright (c) 2022, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//paired header +#include "tx_builder_types_multisig.h" + +//local headers +#include "common/container_helpers.h" +#include "crypto/crypto.h" +#include "cryptonote_basic/subaddress_index.h" +#include "enote_record_types.h" +#include "enote_record_utils_legacy.h" +#include "misc_log_ex.h" +#include "ringct/rctOps.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/legacy_core_utils.h" +#include "seraphis_core/sp_core_enote_utils.h" +#include "seraphis_core/tx_extra.h" +#include "seraphis_crypto/sp_crypto_utils.h" +#include "tx_builder_types.h" +#include "tx_builder_types_legacy.h" +#include "tx_builders_inputs.h" +#include "tx_builders_legacy_inputs.h" +#include "tx_builders_mixed.h" +#include "tx_component_types_legacy.h" + +//third party headers + +//standard headers +#include +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis" + +namespace sp +{ +//------------------------------------------------------------------------------------------------------------------- +bool compare_KI(const LegacyMultisigInputProposalV1 &a, const LegacyMultisigInputProposalV1 &b) +{ + return a.key_image < b.key_image; +} +//------------------------------------------------------------------------------------------------------------------- +void get_legacy_input_proposal_v1(const LegacyMultisigInputProposalV1 &multisig_input_proposal, + const rct::key &legacy_spend_pubkey, + const std::unordered_map &legacy_subaddress_map, + const crypto::secret_key &legacy_view_privkey, + LegacyInputProposalV1 &input_proposal_out) +{ + // extract legacy intermediate enote record from proposal + LegacyIntermediateEnoteRecord legacy_intermediate_record; + + CHECK_AND_ASSERT_THROW_MES(try_get_legacy_intermediate_enote_record(multisig_input_proposal.enote, + multisig_input_proposal.enote_ephemeral_pubkey, + multisig_input_proposal.tx_output_index, + multisig_input_proposal.unlock_time, + legacy_spend_pubkey, + legacy_subaddress_map, + legacy_view_privkey, + hw::get_device("default"), + legacy_intermediate_record), + "legacy multisig input proposal to legacy input proposal: could not recover intermediate enote record for input" + "proposal's enote."); + + // upgrade to full legacy enote record + LegacyEnoteRecord legacy_enote_record; + get_legacy_enote_record(legacy_intermediate_record, multisig_input_proposal.key_image, legacy_enote_record); + + // make the legacy input proposal + make_v1_legacy_input_proposal_v1(legacy_enote_record, multisig_input_proposal.commitment_mask, input_proposal_out); +} +//------------------------------------------------------------------------------------------------------------------- +void get_sp_input_proposal_v1(const SpMultisigInputProposalV1 &multisig_input_proposal, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + SpInputProposalV1 &input_proposal_out) +{ + CHECK_AND_ASSERT_THROW_MES(try_make_v1_input_proposal_v1(multisig_input_proposal.enote, + multisig_input_proposal.enote_ephemeral_pubkey, + multisig_input_proposal.input_context, + jamtis_spend_pubkey, + k_view_balance, + multisig_input_proposal.address_mask, + multisig_input_proposal.commitment_mask, + input_proposal_out), + "seraphis multisig input proposal to seraphis input proposal: conversion failed (wallet may not own this input)."); +} +//------------------------------------------------------------------------------------------------------------------- +void get_v1_tx_proposal_v1(const SpMultisigTxProposalV1 &multisig_tx_proposal, + const rct::key &legacy_spend_pubkey, + const std::unordered_map &legacy_subaddress_map, + const crypto::secret_key &legacy_view_privkey, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + SpTxProposalV1 &tx_proposal_out) +{ + // extract legacy input proposals + std::vector legacy_input_proposals; + legacy_input_proposals.reserve(multisig_tx_proposal.legacy_multisig_input_proposals.size()); + + for (const LegacyMultisigInputProposalV1 &multisig_input_proposal : + multisig_tx_proposal.legacy_multisig_input_proposals) + { + get_legacy_input_proposal_v1(multisig_input_proposal, + legacy_spend_pubkey, + legacy_subaddress_map, + legacy_view_privkey, + tools::add_element(legacy_input_proposals)); + } + + // extract seraphis input proposals + std::vector sp_input_proposals; + sp_input_proposals.reserve(multisig_tx_proposal.sp_multisig_input_proposals.size()); + + for (const SpMultisigInputProposalV1 &multisig_input_proposal : multisig_tx_proposal.sp_multisig_input_proposals) + { + get_sp_input_proposal_v1(multisig_input_proposal, + jamtis_spend_pubkey, + k_view_balance, + tools::add_element(sp_input_proposals)); + } + + // extract memo field elements + std::vector additional_memo_elements; + CHECK_AND_ASSERT_THROW_MES(try_get_extra_field_elements(multisig_tx_proposal.partial_memo, + additional_memo_elements), + "multisig tx proposal: could not parse partial memo."); + + // make the tx proposal + make_v1_tx_proposal_v1(std::move(legacy_input_proposals), + std::move(sp_input_proposals), + multisig_tx_proposal.normal_payment_proposals, + multisig_tx_proposal.selfsend_payment_proposals, + multisig_tx_proposal.tx_fee, + std::move(additional_memo_elements), + tx_proposal_out); +} +//------------------------------------------------------------------------------------------------------------------- +void get_tx_proposal_prefix_v1(const SpMultisigTxProposalV1 &multisig_tx_proposal, + const rct::key &legacy_spend_pubkey, + const std::unordered_map &legacy_subaddress_map, + const crypto::secret_key &legacy_view_privkey, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + rct::key &tx_proposal_prefix_out) +{ + // extract proposal + SpTxProposalV1 tx_proposal; + get_v1_tx_proposal_v1(multisig_tx_proposal, + legacy_spend_pubkey, + legacy_subaddress_map, + legacy_view_privkey, + jamtis_spend_pubkey, + k_view_balance, + tx_proposal); + + // get prefix from proposal + get_tx_proposal_prefix_v1(tx_proposal, multisig_tx_proposal.tx_version, k_view_balance, tx_proposal_prefix_out); +} +//------------------------------------------------------------------------------------------------------------------- +bool matches_with(const LegacyMultisigInputProposalV1 &multisig_input_proposal, + const multisig::CLSAGMultisigProposal &proof_proposal) +{ + // onetime address to sign + if (!(onetime_address_ref(multisig_input_proposal.enote) == main_proof_key_ref(proof_proposal))) + return false; + + // amount commitment to sign + const rct::key amount_commitment{amount_commitment_ref(multisig_input_proposal.enote)}; + if (!(amount_commitment == auxilliary_proof_key_ref(proof_proposal))) + return false; + + // pseudo-output commitment + rct::key masked_commitment; + mask_key(multisig_input_proposal.commitment_mask, amount_commitment, masked_commitment); + if (!(masked_commitment == proof_proposal.masked_C)) + return false; + + // key image + if (!(multisig_input_proposal.key_image == proof_proposal.KI)) + return false; + + // auxilliary key image + crypto::key_image auxilliary_key_image; + make_legacy_auxilliary_key_image_v1(multisig_input_proposal.commitment_mask, + onetime_address_ref(multisig_input_proposal.enote), + hw::get_device("default"), + auxilliary_key_image); + + if (!(auxilliary_key_image == proof_proposal.D)) + return false; + + // references line up 1:1 + if (multisig_input_proposal.reference_set.size() != proof_proposal.ring_members.size()) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool matches_with(const LegacyMultisigInputProposalV1 &multisig_input_proposal, const LegacyEnoteRecord &enote_record) +{ + // onetime address + if (!(onetime_address_ref(multisig_input_proposal.enote) == onetime_address_ref(enote_record.enote))) + return false; + + // amount commitment + if (!(amount_commitment_ref(multisig_input_proposal.enote) == amount_commitment_ref(enote_record.enote))) + return false; + + // key image + if (!(multisig_input_proposal.key_image == enote_record.key_image)) + return false; + + // misc + if (!(multisig_input_proposal.enote_ephemeral_pubkey == enote_record.enote_ephemeral_pubkey)) + return false; + if (!(multisig_input_proposal.tx_output_index == enote_record.tx_output_index)) + return false; + if (!(multisig_input_proposal.unlock_time <= enote_record.unlock_time)) //<= in case of duplicate enotes + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool matches_with(const SpMultisigInputProposalV1 &multisig_input_proposal, const SpEnoteRecordV1 &enote_record) +{ + // enote + if (!(multisig_input_proposal.enote == enote_record.enote)) + return false; + + // enote ephemeral pubkey + if (!(multisig_input_proposal.enote_ephemeral_pubkey == enote_record.enote_ephemeral_pubkey)) + return false; + + // input context + if (!(multisig_input_proposal.input_context == enote_record.input_context)) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace sp diff --git a/src/seraphis_main/tx_builder_types_multisig.h b/src/seraphis_main/tx_builder_types_multisig.h new file mode 100644 index 0000000000..e093ced8be --- /dev/null +++ b/src/seraphis_main/tx_builder_types_multisig.h @@ -0,0 +1,228 @@ +// Copyright (c) 2022, 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 transaction-builder helper types (multisig). + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "crypto/x25519.h" +#include "cryptonote_basic/subaddress_index.h" +#include "enote_record_types.h" +#include "multisig/multisig_clsag.h" +#include "multisig/multisig_signer_set_filter.h" +#include "multisig/multisig_sp_composition_proof.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/jamtis_payment_proposal.h" +#include "seraphis_core/legacy_enote_types.h" +#include "seraphis_core/sp_core_types.h" +#include "seraphis_core/tx_extra.h" +#include "tx_builder_types.h" +#include "tx_builder_types_legacy.h" +#include "tx_component_types.h" +#include "txtype_base.h" + +//third party headers + +//standard headers +#include +#include + +//forward declarations + + +namespace sp +{ + +//// +// LegacyMultisigRingSignaturePrepV1 +// - data for producing a legacy ring signature using multisig +// - this struct contains a subset of data found in LegacyRingSignaturePrepV1 because, in multisig, legacy ring signature +// preps need to be created before a tx proposal is available (this information is used to build multisig input proposals +// and multisig tx proposals) +/// +struct LegacyMultisigRingSignaturePrepV1 final +{ + /// ledger indices of legacy enotes referenced by the proof + std::vector reference_set; + /// the referenced enotes ({Ko, C}((legacy)) representation) + rct::ctkeyV referenced_enotes; + /// the index of the real enote being referenced within the reference set + std::uint64_t real_reference_index; + /// key image of the real reference + crypto::key_image key_image; +}; + +//// +// LegacyMultisigInputProposalV1 +// - propose a legacy tx input to be signed with multisig (for sending to other multisig participants) +/// +struct LegacyMultisigInputProposalV1 final +{ + /// the enote to spend + LegacyEnoteVariant enote; + /// the enote's key image + crypto::key_image key_image; + /// the enote's ephemeral pubkey + rct::key enote_ephemeral_pubkey; + /// t: the enote's output index in the tx that created it + std::uint64_t tx_output_index; + /// u: the enote's unlock time + std::uint64_t unlock_time; + + /// mask + crypto::secret_key commitment_mask; + + /// cached legacy enote indices for a legacy ring signature (should include a reference to this input proposal's enote) + std::vector reference_set; +}; + +//// +// SpMultisigInputProposalV1 +// - propose a seraphis tx input to be signed with multisig (for sending to other multisig participants) +/// +struct SpMultisigInputProposalV1 final +{ + /// enote to spend + SpEnoteVariant enote; + /// the enote's ephemeral pubkey + crypto::x25519_pubkey enote_ephemeral_pubkey; + /// the enote's input context + rct::key input_context; + + /// t_k + crypto::secret_key address_mask; + /// t_c + crypto::secret_key commitment_mask; +}; + +//// +// SpMultisigTxProposalV1 +// - propose to fund a set of outputs with multisig inputs +/// +struct SpMultisigTxProposalV1 final +{ + /// legacy tx inputs to sign with multisig (SORTED) + std::vector legacy_multisig_input_proposals; + /// seraphis tx inputs to sign with multisig (NOT SORTED; get sorted seraphis input proposals by converting to + /// a normal tx proposal) + std::vector sp_multisig_input_proposals; + /// legacy ring signature proposals (CLSAGs) for each legacy input proposal (ALIGNED TO SORTED LEGACY INPUTS) + std::vector legacy_input_proof_proposals; + /// composition proof proposals for each seraphis input proposal (ALIGNED TO SORTED SERAPHIS INPUTS) + std::vector sp_input_proof_proposals; + /// all multisig signers who may participate in signing this proposal + /// - the set may be larger than 'threshold', in which case every permutation of 'threshold' signers will attempt to sign + multisig::signer_set_filter aggregate_signer_set_filter; + + /// normal tx outputs (NOT SORTED) + std::vector normal_payment_proposals; + /// self-send tx outputs (NOT SORTED) + std::vector selfsend_payment_proposals; + /// proposed transaction fee + DiscretizedFee tx_fee; + /// miscellaneous memo elements to add to the tx memo + TxExtra partial_memo; + + /// encoding of intended tx version + tx_version_t tx_version; +}; + +/// comparison method for sorting: a.KI < b.KI +bool compare_KI(const LegacyMultisigInputProposalV1 &a, const LegacyMultisigInputProposalV1 &b); + +/** +* brief: get_legacy_input_proposal_v1 - convert a multisig input proposal to a legacy input proposal +* param: multisig_input_proposal - +* param: legacy_spend_pubkey - +* param: legacy_subaddress_map - +* param: legacy_view_privkey - +* outparam: input_proposal_out - +*/ +void get_legacy_input_proposal_v1(const LegacyMultisigInputProposalV1 &multisig_input_proposal, + const rct::key &legacy_spend_pubkey, + const std::unordered_map &legacy_subaddress_map, + const crypto::secret_key &legacy_view_privkey, + LegacyInputProposalV1 &input_proposal_out); +/** +* brief: get_sp_input_proposal_v1 - convert a multisig input proposal to a seraphis input proposal +* param: multisig_input_proposal - +* param: jamtis_spend_pubkey - +* param: k_view_balance - +* outparam: input_proposal_out - +*/ +void get_sp_input_proposal_v1(const SpMultisigInputProposalV1 &multisig_input_proposal, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + SpInputProposalV1 &input_proposal_out); +/** +* brief: get_v1_tx_proposal_v1 - convert a multisig tx proposal to a plain tx proposal +* param: multisig_tx_proposal - +* param: legacy_spend_pubkey - +* param: legacy_subaddress_map - +* param: legacy_view_privkey - +* param: jamtis_spend_pubkey - +* param: k_view_balance - +* outparam: tx_proposal_out - +*/ +void get_v1_tx_proposal_v1(const SpMultisigTxProposalV1 &multisig_tx_proposal, + const rct::key &legacy_spend_pubkey, + const std::unordered_map &legacy_subaddress_map, + const crypto::secret_key &legacy_view_privkey, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + SpTxProposalV1 &tx_proposal_out); +/** +* brief: get_tx_proposal_prefix_v1 - get the tx proposal prefix of a multisig tx proposal +* param: multisig_tx_proposal - +* param: legacy_spend_pubkey - +* param: legacy_subaddress_map - +* param: legacy_view_privkey - +* param: jamtis_spend_pubkey - +* param: k_view_balance - +* outparam: tx_proposal_prefix_out - +*/ +void get_tx_proposal_prefix_v1(const SpMultisigTxProposalV1 &multisig_tx_proposal, + const rct::key &legacy_spend_pubkey, + const std::unordered_map &legacy_subaddress_map, + const crypto::secret_key &legacy_view_privkey, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + rct::key &tx_proposal_prefix_out); +/** +* brief: matches_with - check if a multisig input proposal matches against other data types +* ... +* return: true if all alignment checks pass +*/ +bool matches_with(const LegacyMultisigInputProposalV1 &multisig_input_proposal, + const multisig::CLSAGMultisigProposal &proof_proposal); +bool matches_with(const LegacyMultisigInputProposalV1 &multisig_input_proposal, const LegacyEnoteRecord &enote_record); +bool matches_with(const SpMultisigInputProposalV1 &multisig_input_proposal, const SpEnoteRecordV1 &enote_record); + +} //namespace sp diff --git a/src/seraphis_main/tx_builders_inputs.cpp b/src/seraphis_main/tx_builders_inputs.cpp new file mode 100644 index 0000000000..5296887910 --- /dev/null +++ b/src/seraphis_main/tx_builders_inputs.cpp @@ -0,0 +1,692 @@ +// Copyright (c) 2022, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//paired header +#include "tx_builders_inputs.h" + +//local headers +#include "common/container_helpers.h" +#include "crypto/crypto.h" +#include "crypto/x25519.h" +extern "C" +{ +#include "crypto/crypto-ops.h" +} +#include "cryptonote_config.h" +#include "enote_record_types.h" +#include "enote_record_utils.h" +#include "misc_language.h" +#include "misc_log_ex.h" +#include "ringct/rctOps.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/binned_reference_set.h" +#include "seraphis_core/binned_reference_set_utils.h" +#include "seraphis_core/jamtis_enote_utils.h" +#include "seraphis_core/sp_core_enote_utils.h" +#include "seraphis_crypto/grootle.h" +#include "seraphis_crypto/math_utils.h" +#include "seraphis_crypto/sp_composition_proof.h" +#include "seraphis_crypto/sp_crypto_utils.h" +#include "seraphis_crypto/sp_hash_functions.h" +#include "seraphis_crypto/sp_transcript.h" +#include "tx_builder_types.h" +#include "tx_builder_types_legacy.h" +#include "tx_component_types.h" +#include "tx_component_types_legacy.h" + +//third party headers + +//standard headers +#include +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis" + +namespace sp +{ +//------------------------------------------------------------------------------------------------------------------- +void make_input_images_prefix_v1(const std::vector &legacy_enote_images, + const std::vector &sp_enote_images, + rct::key &input_images_prefix_out) +{ + // input images prefix = H_32({C", KI}((legacy)), {K", C", KI}) + SpFSTranscript transcript{ + config::HASH_KEY_SERAPHIS_INPUT_IMAGES_PREFIX_V1, + legacy_enote_images.size() * legacy_enote_image_v2_size_bytes() + + sp_enote_images.size() * sp_enote_image_v1_size_bytes() + }; + transcript.append("legacy_enote_images", legacy_enote_images); + transcript.append("sp_enote_images", sp_enote_images); + + sp_hash_to_32(transcript.data(), transcript.size(), input_images_prefix_out.bytes); +} +//------------------------------------------------------------------------------------------------------------------- +void check_v1_input_proposal_semantics_v1(const SpInputProposalCore &input_proposal, + const rct::key &sp_core_spend_pubkey, + const crypto::secret_key &k_view_balance) +{ + // 1. the onetime address must be reproducible + rct::key extended_spendkey{sp_core_spend_pubkey}; + extend_seraphis_spendkey_u(input_proposal.enote_view_extension_u, extended_spendkey); + + rct::key onetime_address_reproduced{extended_spendkey}; + extend_seraphis_spendkey_x(add_secrets(input_proposal.enote_view_extension_x, k_view_balance), + onetime_address_reproduced); + mask_key(input_proposal.enote_view_extension_g, onetime_address_reproduced, onetime_address_reproduced); + + CHECK_AND_ASSERT_THROW_MES(onetime_address_reproduced == onetime_address_ref(input_proposal.enote_core), + "input proposal v1 semantics check: could not reproduce the one-time address."); + + // 2. the key image must be reproducible and canonical + crypto::key_image key_image_reproduced; + make_seraphis_key_image(add_secrets(input_proposal.enote_view_extension_x, k_view_balance), + rct::rct2pk(extended_spendkey), + key_image_reproduced); + + CHECK_AND_ASSERT_THROW_MES(key_image_reproduced == input_proposal.key_image, + "input proposal v1 semantics check: could not reproduce the key image."); + CHECK_AND_ASSERT_THROW_MES(key_domain_is_prime_subgroup(rct::ki2rct(key_image_reproduced)), + "input proposal v1 semantics check: the key image is not canonical."); + + // 3. the amount commitment must be reproducible + const rct::key amount_commitment_reproduced{ + rct::commit(input_proposal.amount, rct::sk2rct(input_proposal.amount_blinding_factor)) + }; + + CHECK_AND_ASSERT_THROW_MES(amount_commitment_reproduced == amount_commitment_ref(input_proposal.enote_core), + "input proposal v1 semantics check: could not reproduce the amount commitment."); + + // 4. the masks should be canonical and > 1 + CHECK_AND_ASSERT_THROW_MES(sc_check(to_bytes(input_proposal.address_mask)) == 0, + "input proposal v1 semantics check: invalid address mask."); + CHECK_AND_ASSERT_THROW_MES(sc_isnonzero(to_bytes(input_proposal.address_mask)), + "input proposal v1 semantics check: address mask is zero."); + CHECK_AND_ASSERT_THROW_MES(!(rct::sk2rct(input_proposal.address_mask) == rct::identity()), + "input proposal v1 semantics check: address mask is 1."); + + CHECK_AND_ASSERT_THROW_MES(sc_check(to_bytes(input_proposal.commitment_mask)) == 0, + "input proposal v1 semantics check: invalid commitment mask."); + CHECK_AND_ASSERT_THROW_MES(sc_isnonzero(to_bytes(input_proposal.commitment_mask)), + "input proposal v1 semantics check: commitment mask is zero."); + CHECK_AND_ASSERT_THROW_MES(!(rct::sk2rct(input_proposal.commitment_mask) == rct::identity()), + "input proposal v1 semantics check: commitment mask is 1."); +} +//------------------------------------------------------------------------------------------------------------------- +void check_v1_input_proposal_semantics_v1(const SpInputProposalV1 &input_proposal, + const rct::key &sp_core_spend_pubkey, + const crypto::secret_key &k_view_balance) +{ + check_v1_input_proposal_semantics_v1(input_proposal.core, sp_core_spend_pubkey, k_view_balance); +} +//------------------------------------------------------------------------------------------------------------------- +void make_input_proposal(const SpEnoteCoreVariant &enote_core, + const crypto::key_image &key_image, + const crypto::secret_key &enote_view_extension_g, + const crypto::secret_key &enote_view_extension_x, + const crypto::secret_key &enote_view_extension_u, + const crypto::secret_key &input_amount_blinding_factor, + const rct::xmr_amount &input_amount, + const crypto::secret_key &address_mask, + const crypto::secret_key &commitment_mask, + SpInputProposalCore &proposal_out) +{ + // make an input proposal + proposal_out.enote_core = enote_core; + proposal_out.key_image = key_image; + proposal_out.enote_view_extension_g = enote_view_extension_g; + proposal_out.enote_view_extension_x = enote_view_extension_x; + proposal_out.enote_view_extension_u = enote_view_extension_u; + proposal_out.amount_blinding_factor = input_amount_blinding_factor; + proposal_out.amount = input_amount; + proposal_out.address_mask = address_mask; + proposal_out.commitment_mask = commitment_mask; +} +//------------------------------------------------------------------------------------------------------------------- +void make_v1_input_proposal_v1(const SpEnoteRecordV1 &enote_record, + const crypto::secret_key &address_mask, + const crypto::secret_key &commitment_mask, + SpInputProposalV1 &proposal_out) +{ + // make input proposal from enote record + make_input_proposal(core_ref(enote_record.enote), + enote_record.key_image, + enote_record.enote_view_extension_g, + enote_record.enote_view_extension_x, + enote_record.enote_view_extension_u, + enote_record.amount_blinding_factor, + enote_record.amount, + address_mask, + commitment_mask, + proposal_out.core); +} +//------------------------------------------------------------------------------------------------------------------- +bool try_make_v1_input_proposal_v1(const SpEnoteVariant &enote, + const crypto::x25519_pubkey &enote_ephemeral_pubkey, + const rct::key &input_context, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + const crypto::secret_key &address_mask, + const crypto::secret_key &commitment_mask, + SpInputProposalV1 &proposal_out) +{ + // try to extract info from enote then make an input proposal + SpEnoteRecordV1 enote_record; + if (!try_get_enote_record_v1(enote, + enote_ephemeral_pubkey, + input_context, + jamtis_spend_pubkey, + k_view_balance, + enote_record)) + return false; + + make_v1_input_proposal_v1(enote_record, address_mask, commitment_mask, proposal_out); + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +void make_standard_input_context_v1(const std::vector &legacy_input_proposals, + const std::vector &sp_input_proposals, + rct::key &input_context_out) +{ + // collect key images + std::vector legacy_key_images_collected; + std::vector sp_key_images_collected; + legacy_key_images_collected.reserve(legacy_input_proposals.size()); + sp_key_images_collected.reserve(sp_input_proposals.size()); + + for (const LegacyInputProposalV1 &legacy_input_proposal : legacy_input_proposals) + legacy_key_images_collected.emplace_back(legacy_input_proposal.key_image); + + for (const SpInputProposalV1 &sp_input_proposal : sp_input_proposals) + sp_key_images_collected.emplace_back(key_image_ref(sp_input_proposal)); + + // sort the key images + std::sort(legacy_key_images_collected.begin(), legacy_key_images_collected.end()); + std::sort(sp_key_images_collected.begin(), sp_key_images_collected.end()); + + // make the input context + jamtis::make_jamtis_input_context_standard(legacy_key_images_collected, sp_key_images_collected, input_context_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_standard_input_context_v1(const std::vector &legacy_input_images, + const std::vector &sp_input_images, + rct::key &input_context_out) +{ + // collect key images + std::vector legacy_key_images_collected; + std::vector sp_key_images_collected; + legacy_key_images_collected.reserve(legacy_input_images.size()); + sp_key_images_collected.reserve(sp_input_images.size()); + + for (const LegacyEnoteImageV2 &legacy_input_image : legacy_input_images) + legacy_key_images_collected.emplace_back(legacy_input_image.key_image); + + for (const SpEnoteImageV1 &sp_input_image : sp_input_images) + sp_key_images_collected.emplace_back(key_image_ref(sp_input_image)); + + // sort the key images + std::sort(legacy_key_images_collected.begin(), legacy_key_images_collected.end()); + std::sort(sp_key_images_collected.begin(), sp_key_images_collected.end()); + + // make the input context + jamtis::make_jamtis_input_context_standard(legacy_key_images_collected, sp_key_images_collected, input_context_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_v1_image_proof_v1(const SpInputProposalCore &input_proposal, + const rct::key &message, + const crypto::secret_key &sp_spend_privkey, + const crypto::secret_key &k_view_balance, + SpImageProofV1 &image_proof_out) +{ + // make image proof for an enote image in the squashed enote model + + // 1. the input enote + const SpEnoteCoreVariant &input_enote_core{input_proposal.enote_core}; + + // 2. the input enote image + SpEnoteImageCore input_enote_image_core; + get_enote_image_core(input_proposal, input_enote_image_core); + + // 3. prepare for proof (squashed enote model): x, y, z + // a. squash prefix: H_n(Ko,C) + rct::key squash_prefix; + make_seraphis_squash_prefix(onetime_address_ref(input_enote_core), + amount_commitment_ref(input_enote_core), + squash_prefix); // H_n(Ko,C) + + // b. x: t_k + H_n(Ko,C) (k_{g, sender} + k_{g, address}) + crypto::secret_key x; + sc_mul(to_bytes(x), squash_prefix.bytes, to_bytes(input_proposal.enote_view_extension_g)); + sc_add(to_bytes(x), to_bytes(input_proposal.address_mask), to_bytes(x)); + + // c. y: H_n(Ko,C) (k_{x, sender} + k_{x, address} + k_vb) + crypto::secret_key y; + sc_add(to_bytes(y), to_bytes(input_proposal.enote_view_extension_x), to_bytes(k_view_balance)); + sc_mul(to_bytes(y), squash_prefix.bytes, to_bytes(y)); + + // d. z: H_n(Ko,C) (k_{u, sender} + k_{u, address} + k_m) + crypto::secret_key z; + sc_add(to_bytes(z), to_bytes(input_proposal.enote_view_extension_u), to_bytes(sp_spend_privkey)); + sc_mul(to_bytes(z), squash_prefix.bytes, to_bytes(z)); + + // 4. make seraphis composition proof + make_sp_composition_proof(message, + input_enote_image_core.masked_address, + x, + y, + z, + image_proof_out.composition_proof); +} +//------------------------------------------------------------------------------------------------------------------- +void make_v1_image_proofs_v1(const std::vector &input_proposals, + const rct::key &message, + const crypto::secret_key &sp_spend_privkey, + const crypto::secret_key &k_view_balance, + std::vector &image_proofs_out) +{ + // make multiple image proofs + CHECK_AND_ASSERT_THROW_MES(input_proposals.size() > 0, "Tried to make image proofs for 0 inputs."); + + image_proofs_out.clear(); + image_proofs_out.reserve(input_proposals.size()); + + for (const SpInputProposalV1 &input_proposal : input_proposals) + { + make_v1_image_proof_v1(input_proposal.core, + message, + sp_spend_privkey, + k_view_balance, + tools::add_element(image_proofs_out)); + } +} +//------------------------------------------------------------------------------------------------------------------- +void check_v1_partial_input_semantics_v1(const SpPartialInputV1 &partial_input) +{ + // input amount commitment can be reconstructed + const rct::key reconstructed_amount_commitment{ + rct::commit(partial_input.input_amount, rct::sk2rct(partial_input.input_amount_blinding_factor)) + }; + + CHECK_AND_ASSERT_THROW_MES(reconstructed_amount_commitment == amount_commitment_ref(partial_input.input_enote_core), + "partial input semantics (v1): could not reconstruct amount commitment."); + + // input image masked address and commitment can be reconstructed + rct::key reconstructed_masked_address; + rct::key reconstructed_masked_commitment; + make_seraphis_enote_image_masked_keys(onetime_address_ref(partial_input.input_enote_core), + reconstructed_amount_commitment, + partial_input.address_mask, + partial_input.commitment_mask, + reconstructed_masked_address, + reconstructed_masked_commitment); + + CHECK_AND_ASSERT_THROW_MES(reconstructed_masked_address == masked_address_ref(partial_input.input_image), + "partial input semantics (v1): could not reconstruct masked address."); + CHECK_AND_ASSERT_THROW_MES(reconstructed_masked_commitment == masked_commitment_ref(partial_input.input_image), + "partial input semantics (v1): could not reconstruct masked commitment."); + + // image proof is valid + CHECK_AND_ASSERT_THROW_MES(verify_sp_composition_proof(partial_input.image_proof.composition_proof, + partial_input.tx_proposal_prefix, + reconstructed_masked_address, + key_image_ref(partial_input.input_image)), + "partial input semantics (v1): image proof is invalid."); +} +//------------------------------------------------------------------------------------------------------------------- +void make_v1_partial_input_v1(const SpInputProposalV1 &input_proposal, + const rct::key &tx_proposal_prefix, + SpImageProofV1 sp_image_proof, + const rct::key &sp_core_spend_pubkey, + const crypto::secret_key &k_view_balance, + SpPartialInputV1 &partial_input_out) +{ + // 1. check input proposal semantics + check_v1_input_proposal_semantics_v1(input_proposal, sp_core_spend_pubkey, k_view_balance); + + // 2. prepare input image + get_enote_image_v1(input_proposal, partial_input_out.input_image); + + // 3. set partial input pieces + partial_input_out.image_proof = std::move(sp_image_proof); + partial_input_out.address_mask = input_proposal.core.address_mask; + partial_input_out.commitment_mask = input_proposal.core.commitment_mask; + partial_input_out.tx_proposal_prefix = tx_proposal_prefix; + partial_input_out.input_enote_core = input_proposal.core.enote_core; + partial_input_out.input_amount = amount_ref(input_proposal); + partial_input_out.input_amount_blinding_factor = input_proposal.core.amount_blinding_factor; +} +//------------------------------------------------------------------------------------------------------------------- +void make_v1_partial_input_v1(const SpInputProposalV1 &input_proposal, + const rct::key &tx_proposal_prefix, + const crypto::secret_key &sp_spend_privkey, + const crypto::secret_key &k_view_balance, + SpPartialInputV1 &partial_input_out) +{ + // 1. initialization + rct::key sp_core_spend_pubkey; + make_seraphis_core_spendkey(sp_spend_privkey, sp_core_spend_pubkey); + + // 2. construct image proof + SpImageProofV1 sp_image_proof; + make_v1_image_proof_v1(input_proposal.core, tx_proposal_prefix, sp_spend_privkey, k_view_balance, sp_image_proof); + + // 3. finalize the partial input + make_v1_partial_input_v1(input_proposal, + tx_proposal_prefix, + std::move(sp_image_proof), + sp_core_spend_pubkey, + k_view_balance, + partial_input_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_v1_partial_inputs_v1(const std::vector &input_proposals, + const rct::key &tx_proposal_prefix, + const crypto::secret_key &sp_spend_privkey, + const crypto::secret_key &k_view_balance, + std::vector &partial_inputs_out) +{ + partial_inputs_out.clear(); + partial_inputs_out.reserve(input_proposals.size()); + + // make all inputs + for (const SpInputProposalV1 &input_proposal : input_proposals) + { + make_v1_partial_input_v1(input_proposal, + tx_proposal_prefix, + sp_spend_privkey, + k_view_balance, + tools::add_element(partial_inputs_out)); + } +} +//------------------------------------------------------------------------------------------------------------------- +void get_input_commitment_factors_v1(const std::vector &input_proposals, + std::vector &input_amounts_out, + std::vector &blinding_factors_out) +{ + // use input proposals to get amounts/blinding factors + blinding_factors_out.clear(); + blinding_factors_out.reserve(input_proposals.size()); + input_amounts_out.clear(); + input_amounts_out.reserve(input_proposals.size()); + + for (const SpInputProposalV1 &input_proposal : input_proposals) + { + // input image amount commitment blinding factor: t_c + x + sc_add(to_bytes(tools::add_element(blinding_factors_out)), + to_bytes(input_proposal.core.commitment_mask), // t_c + to_bytes(input_proposal.core.amount_blinding_factor)); // x + + // input amount: a + input_amounts_out.emplace_back(amount_ref(input_proposal)); + } +} +//------------------------------------------------------------------------------------------------------------------- +void get_input_commitment_factors_v1(const std::vector &partial_inputs, + std::vector &input_amounts_out, + std::vector &blinding_factors_out) +{ + // use partial inputs to get amounts/blinding factors + blinding_factors_out.clear(); + blinding_factors_out.reserve(partial_inputs.size()); + input_amounts_out.clear(); + input_amounts_out.reserve(partial_inputs.size()); + + for (const SpPartialInputV1 &partial_input : partial_inputs) + { + // input image amount commitment blinding factor: t_c + x + sc_add(to_bytes(tools::add_element(blinding_factors_out)), + to_bytes(partial_input.commitment_mask), // t_c + to_bytes(partial_input.input_amount_blinding_factor)); // x + + // input amount: a + input_amounts_out.emplace_back(partial_input.input_amount); + } +} +//------------------------------------------------------------------------------------------------------------------- +void make_binned_ref_set_generator_seed_v1(const rct::key &masked_address, + const rct::key &masked_commitment, + rct::key &generator_seed_out) +{ + // make binned reference set generator seed + + // seed = H_32(K", C") + SpKDFTranscript transcript{config::HASH_KEY_BINNED_REF_SET_GENERATOR_SEED, 2*sizeof(rct::key)}; + transcript.append("K_masked", masked_address); + transcript.append("C_masked", masked_commitment); + + // hash to the result + sp_hash_to_32(transcript.data(), transcript.size(), generator_seed_out.bytes); +} +//------------------------------------------------------------------------------------------------------------------- +void make_binned_ref_set_generator_seed_v1(const rct::key &onetime_address, + const rct::key &amount_commitment, + const crypto::secret_key &address_mask, + const crypto::secret_key &commitment_mask, + rct::key &generator_seed_out) +{ + // make binned reference set generator seed from pieces + + // masked address and commitment + rct::key masked_address; //K" = t_k G + H_n(Ko,C) Ko + rct::key masked_commitment; //C" = t_c G + C + make_seraphis_enote_image_masked_keys(onetime_address, + amount_commitment, + address_mask, + commitment_mask, + masked_address, + masked_commitment); + + // finish making the seed + make_binned_ref_set_generator_seed_v1(masked_address, masked_commitment, generator_seed_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_tx_membership_proof_message_v1(const SpBinnedReferenceSetV1 &binned_reference_set, rct::key &message_out) +{ + // m = H_32({binned reference set}) + SpFSTranscript transcript{ + config::HASH_KEY_SERAPHIS_MEMBERSHIP_PROOF_MESSAGE_V1, + sp_binned_ref_set_v1_size_bytes(binned_reference_set) + sp_binned_ref_set_config_v1_size_bytes() + }; + transcript.append("binned_reference_set", binned_reference_set); + + sp_hash_to_32(transcript.data(), transcript.size(), message_out.bytes); +} +//------------------------------------------------------------------------------------------------------------------- +void make_v1_membership_proof_v1(const std::size_t ref_set_decomp_n, + const std::size_t ref_set_decomp_m, + SpBinnedReferenceSetV1 binned_reference_set, + const std::vector &referenced_enotes_squashed, + const SpEnoteCoreVariant &real_reference_enote, + const crypto::secret_key &address_mask, + const crypto::secret_key &commitment_mask, + SpMembershipProofV1 &membership_proof_out) +{ + // make membership proof + + /// checks and initialization + + // 1. misc + const std::size_t ref_set_size{math::uint_pow(ref_set_decomp_n, ref_set_decomp_m)}; + + CHECK_AND_ASSERT_THROW_MES(referenced_enotes_squashed.size() == ref_set_size, + "make membership proof v1: ref set size doesn't match number of referenced enotes."); + CHECK_AND_ASSERT_THROW_MES(reference_set_size(binned_reference_set) == ref_set_size, + "make membership proof v1: ref set size doesn't match number of references in the binned reference set."); + + // 2. make the real reference's squashed representation for later + rct::key transformed_address; + make_seraphis_squashed_address_key(onetime_address_ref(real_reference_enote), + amount_commitment_ref(real_reference_enote), + transformed_address); //H_n(Ko,C) Ko + + rct::key real_Q; + rct::addKeys(real_Q, transformed_address, amount_commitment_ref(real_reference_enote)); //Hn(Ko,C) Ko + C + + // 3. check binned reference set generator + rct::key masked_address; + mask_key(address_mask, transformed_address, masked_address); //K" = t_k G + H_n(Ko,C) Ko + + rct::key masked_commitment; + mask_key(commitment_mask, amount_commitment_ref(real_reference_enote), masked_commitment); //C" = t_c G + C + + rct::key generator_seed_reproduced; + make_binned_ref_set_generator_seed_v1(masked_address, masked_commitment, generator_seed_reproduced); + + CHECK_AND_ASSERT_THROW_MES(generator_seed_reproduced == binned_reference_set.bin_generator_seed, + "make membership proof v1: unable to reproduce binned reference set generator seed."); + + + /// prepare to make proof + + // 1. find the real referenced enote's location in the reference set + const std::size_t real_spend_index_in_set{ + static_cast(std::distance( + referenced_enotes_squashed.begin(), + std::find(referenced_enotes_squashed.begin(), referenced_enotes_squashed.end(), real_Q) + )) + }; //l + + CHECK_AND_ASSERT_THROW_MES(real_spend_index_in_set < ref_set_size, + "make membership proof v1: could not find enote for membership proof in reference set."); + + // 2. proof offset (there is only one in the squashed enote model) + const rct::key image_offset{rct::addKeys(masked_address, masked_commitment)}; //Q" = K" + C" + + // 3. secret key of: Q[l] - Q" = -(t_k + t_c) G + crypto::secret_key proof_privkey; + sc_add(to_bytes(proof_privkey), to_bytes(address_mask), to_bytes(commitment_mask)); // t_k + t_c + sc_mul(to_bytes(proof_privkey), to_bytes(proof_privkey), minus_one().bytes); // -(t_k + t_c) + + // 4. proof message + rct::key message; + make_tx_membership_proof_message_v1(binned_reference_set, message); + + + /// make grootle proof + make_grootle_proof(message, + referenced_enotes_squashed, + real_spend_index_in_set, + image_offset, + proof_privkey, + ref_set_decomp_n, + ref_set_decomp_m, + membership_proof_out.grootle_proof); + + + /// copy miscellaneous components + membership_proof_out.binned_reference_set = std::move(binned_reference_set); + membership_proof_out.ref_set_decomp_n = ref_set_decomp_n; + membership_proof_out.ref_set_decomp_m = ref_set_decomp_m; +} +//------------------------------------------------------------------------------------------------------------------- +void make_v1_membership_proof_v1(SpMembershipProofPrepV1 membership_proof_prep, SpMembershipProofV1 &membership_proof_out) +{ + make_v1_membership_proof_v1(membership_proof_prep.ref_set_decomp_n, + membership_proof_prep.ref_set_decomp_m, + std::move(membership_proof_prep.binned_reference_set), + membership_proof_prep.referenced_enotes_squashed, + membership_proof_prep.real_reference_enote, + membership_proof_prep.address_mask, + membership_proof_prep.commitment_mask, + membership_proof_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_v1_membership_proofs_v1(std::vector membership_proof_preps, + std::vector &membership_proofs_out) +{ + // make multiple membership proofs + // note: this method is only useful if proof preps are pre-sorted, so alignable membership proofs are not needed + membership_proofs_out.clear(); + membership_proofs_out.reserve(membership_proof_preps.size()); + + for (SpMembershipProofPrepV1 &proof_prep : membership_proof_preps) + make_v1_membership_proof_v1(std::move(proof_prep), tools::add_element(membership_proofs_out)); +} +//------------------------------------------------------------------------------------------------------------------- +void make_v1_alignable_membership_proof_v1(SpMembershipProofPrepV1 membership_proof_prep, + SpAlignableMembershipProofV1 &alignable_membership_proof_out) +{ + // make alignable membership proof + + // save the masked address so the membership proof can be matched with its input image later + make_seraphis_squashed_address_key(onetime_address_ref(membership_proof_prep.real_reference_enote), + amount_commitment_ref(membership_proof_prep.real_reference_enote), + alignable_membership_proof_out.masked_address); //H_n(Ko,C) Ko + + mask_key(membership_proof_prep.address_mask, + alignable_membership_proof_out.masked_address, + alignable_membership_proof_out.masked_address); //t_k G + H_n(Ko,C) Ko + + // make the membership proof + make_v1_membership_proof_v1(std::move(membership_proof_prep), alignable_membership_proof_out.membership_proof); +} +//------------------------------------------------------------------------------------------------------------------- +void make_v1_alignable_membership_proofs_v1(std::vector membership_proof_preps, + std::vector &alignable_membership_proofs_out) +{ + // make multiple alignable membership proofs + alignable_membership_proofs_out.clear(); + alignable_membership_proofs_out.reserve(membership_proof_preps.size()); + + for (SpMembershipProofPrepV1 &proof_prep : membership_proof_preps) + make_v1_alignable_membership_proof_v1(std::move(proof_prep), tools::add_element(alignable_membership_proofs_out)); +} +//------------------------------------------------------------------------------------------------------------------- +void align_v1_membership_proofs_v1(const std::vector &input_images, + std::vector alignable_membership_proofs, + std::vector &membership_proofs_out) +{ + CHECK_AND_ASSERT_THROW_MES(input_images.size() == alignable_membership_proofs.size(), + "Mismatch between input image count and alignable membership proof count."); + + membership_proofs_out.clear(); + membership_proofs_out.reserve(alignable_membership_proofs.size()); + + for (const SpEnoteImageV1 &input_image : input_images) + { + // find the membership proof that matches with the input image at this index + auto membership_proof_match = + std::find_if( + alignable_membership_proofs.begin(), + alignable_membership_proofs.end(), + [&masked_address = masked_address_ref(input_image)](const SpAlignableMembershipProofV1 &a) -> bool + { + return alignment_check(a, masked_address); + } + ); + + CHECK_AND_ASSERT_THROW_MES(membership_proof_match != alignable_membership_proofs.end(), + "Could not find input image to match with an alignable membership proof."); + + membership_proof_match->masked_address = rct::zero(); //clear so duplicates will error out + membership_proofs_out.emplace_back(std::move(membership_proof_match->membership_proof)); + } +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace sp diff --git a/src/seraphis_main/tx_builders_inputs.h b/src/seraphis_main/tx_builders_inputs.h new file mode 100644 index 0000000000..d7a926640d --- /dev/null +++ b/src/seraphis_main/tx_builders_inputs.h @@ -0,0 +1,290 @@ +// Copyright (c) 2022, 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 tx-builder/component-builder implementations (tx inputs). + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "crypto/x25519.h" +#include "enote_record_types.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/binned_reference_set.h" +#include "seraphis_core/sp_core_types.h" +#include "tx_builder_types.h" +#include "tx_component_types.h" +#include "tx_component_types_legacy.h" + +//third party headers + +//standard headers +#include + +//forward declarations + + +namespace sp +{ + +/** +* brief: make_input_images_prefix_v1 - hash of enote images (for tx hashes) +* - H_32({C", KI}((legacy)), {K", C", KI}) +* param: legacy_enote_images - +* param: sp_enote_images - +* outparam: input_images_prefix_out - +*/ +void make_input_images_prefix_v1(const std::vector &legacy_enote_images, + const std::vector &sp_enote_images, + rct::key &input_images_prefix_out); +/** +* brief: check_v1_input_proposal_semantics_v1 - check the semantics of a seraphis v1 input proposal +* - throws on failure +* param: input_proposal - +* param: sp_core_spend_pubkey - +* param: k_view_balance - +*/ +void check_v1_input_proposal_semantics_v1(const SpInputProposalCore &input_proposal, + const rct::key &sp_core_spend_pubkey, + const crypto::secret_key &k_view_balance); +void check_v1_input_proposal_semantics_v1(const SpInputProposalV1 &input_proposal, + const rct::key &sp_core_spend_pubkey, + const crypto::secret_key &k_view_balance); +/** +* brief: make_input_proposal - make the core of a seraphis input proposal +* param: enote_core - +* param: key_image - +* param: enote_view_extension_g - +* param: enote_view_extension_x - +* param: enote_view_extension_u - +* param: input_amount_blinding_factor - +* param: input_amount - +* param: address_mask - +* param: commitment_mask - +* outparam: proposal_out - +*/ +void make_input_proposal(const SpEnoteCore &enote_core, + const crypto::key_image &key_image, + const crypto::secret_key &enote_view_extension_g, + const crypto::secret_key &enote_view_extension_x, + const crypto::secret_key &enote_view_extension_u, + const crypto::secret_key &input_amount_blinding_factor, + const rct::xmr_amount &input_amount, + const crypto::secret_key &address_mask, + const crypto::secret_key &commitment_mask, + SpInputProposalCore &proposal_out); +/** +* brief: make_v1_input_proposal_v1 - make a seraphis v1 input proposal +* param: enote_record - +* param: address_mask - +* param: commitment_mask - +* outparam: proposal_out - +*/ +void make_v1_input_proposal_v1(const SpEnoteRecordV1 &enote_record, + const crypto::secret_key &address_mask, + const crypto::secret_key &commitment_mask, + SpInputProposalV1 &proposal_out); +/** +* brief: try_make_v1_input_proposal_v1 - try to make a seraphis v1 input proposal from an enote +* param: enote - +* param: enote_ephemeral_pubkey - +* param: input_context - +* param: jamtis_spend_pubkey - +* param: k_view_balance - +* param: address_mask - +* param: commitment_mask - +* outparam: proposal_out - +*/ +bool try_make_v1_input_proposal_v1(const SpEnoteVariant &enote, + const crypto::x25519_pubkey &enote_ephemeral_pubkey, + const rct::key &input_context, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + const crypto::secret_key &address_mask, + const crypto::secret_key &commitment_mask, + SpInputProposalV1 &proposal_out); +/** +* brief: make_standard_input_context_v1 - compute an input context for non-coinbase transactions +* param: legacy_input_proposals - +* param: sp_input_proposals - +* outparam: input_context_out - +*/ +void make_standard_input_context_v1(const std::vector &legacy_input_proposals, + const std::vector &sp_input_proposals, + rct::key &input_context_out); +void make_standard_input_context_v1(const std::vector &legacy_input_images, + const std::vector &sp_input_images, + rct::key &input_context_out); +/** +* brief: make_v1_image_proof_v1 - make a seraphis composition proof for an enote image in the squashed enote model +* param: input_proposal - +* param: message - +* param: sp_spend_privkey - +* param: k_view_balance - +* outparam: image_proof_out - +*/ +void make_v1_image_proof_v1(const SpInputProposalCore &input_proposal, + const rct::key &message, + const crypto::secret_key &sp_spend_privkey, + const crypto::secret_key &k_view_balance, + SpImageProofV1 &image_proof_out); +/** +* brief: make_v1_image_proofs_v1 - make a set of seraphis composition proofs for enote images in the squashed enote model +* param: input_proposals - +* param: message - +* param: sp_spend_privkey - +* param: k_view_balance - +* outparam: image_proofs_out - +*/ +void make_v1_image_proofs_v1(const std::vector &input_proposals, + const rct::key &message, + const crypto::secret_key &sp_spend_privkey, + const crypto::secret_key &k_view_balance, + std::vector &image_proofs_out); +/** +* brief: check_v1_partial_input_semantics_v1 - check the semantics of a v1 partial seraphis input +* - throws on failure +* param: partial_input - +*/ +void check_v1_partial_input_semantics_v1(const SpPartialInputV1 &partial_input); +/** +* brief: make_v1_partial_input_v1 - make a v1 partial seraphis input +* param: input_proposal - +* param: tx_proposal_prefix - +* param: sp_image_proof - +* param: sp_core_spend_pubkey - +* param: k_view_balance - +* outparam: partial_input_out - +*/ +void make_v1_partial_input_v1(const SpInputProposalV1 &input_proposal, + const rct::key &tx_proposal_prefix, + SpImageProofV1 sp_image_proof, + const rct::key &sp_core_spend_pubkey, + const crypto::secret_key &k_view_balance, + SpPartialInputV1 &partial_input_out); +void make_v1_partial_input_v1(const SpInputProposalV1 &input_proposal, + const rct::key &tx_proposal_prefix, + const crypto::secret_key &sp_spend_privkey, + const crypto::secret_key &k_view_balance, + SpPartialInputV1 &partial_input_out); +/** +* brief: make_v1_partial_inputs_v1 - make a full set of v1 partial inputs +* param: input_proposals - +* param: tx_proposal_prefix - +* param: sp_spend_privkey - +* param: k_view_balance - +* outparam: partial_inputs_out - +*/ +void make_v1_partial_inputs_v1(const std::vector &input_proposals, + const rct::key &tx_proposal_prefix, + const crypto::secret_key &sp_spend_privkey, + const crypto::secret_key &k_view_balance, + std::vector &partial_inputs_out); +/** +* brief: get_input_commitment_factors_v1 - collect input amounts and input image amount commitment blinding factors +* param: input_proposals - +* outparam: input_amounts_out - +* outparam: blinding_factors_out - +*/ +void get_input_commitment_factors_v1(const std::vector &input_proposals, + std::vector &input_amounts_out, + std::vector &blinding_factors_out); +void get_input_commitment_factors_v1(const std::vector &partial_inputs, + std::vector &input_amounts_out, + std::vector &blinding_factors_out); +/** +* brief: make_binned_ref_set_generator_seed_v1 - compute a generator seed for making a binned reference set +* seed = H_32(K", C") +* note: depending on the enote image ensures the seed is a function of some 'random' information that is always +* available to both tx authors and validators (i.e. the masks, which are embedded in the image); seraphis +* membership proofs can be constructed in isolation, in which case only the real reference and the masks are +* available (so there are no other options for entropy without passing additional bytes around) +* param: masked_address - +* param: masked_commitment - +* outparam: generator_seed_out - +*/ +void make_binned_ref_set_generator_seed_v1(const rct::key &masked_address, + const rct::key &masked_commitment, + rct::key &generator_seed_out); +void make_binned_ref_set_generator_seed_v1(const rct::key &onetime_address, + const rct::key &amount_commitment, + const crypto::secret_key &address_mask, + const crypto::secret_key &commitment_mask, + rct::key &generator_seed_out); +/** +* brief: make_tx_membership_proof_message_v1 - message to sign in seraphis membership proofs used in a transaction +* - H_32({binned reference set}) +* param: binned_reference_set - +* outparam: message_out - the message to sign in a membership proof +*/ +void make_tx_membership_proof_message_v1(const SpBinnedReferenceSetV1 &binned_reference_set, rct::key &message_out); +/** +* brief: make_v1_membership_proof_v1 - make a grootle membership proof in the squashed enote model +* param: ref_set_decomp_n - +* param: ref_set_decomp_m - +* param: binned_reference_set - +* param: referenced_enotes_squashed - +* param: real_spend_index_in_set - +* param: real_reference_enote - +* param: image_address_mask - +* param: image_commitment_mask - +* outparam: membership_proof_out - +*/ +void make_v1_membership_proof_v1(const std::size_t ref_set_decomp_n, + const std::size_t ref_set_decomp_m, + SpBinnedReferenceSetV1 binned_reference_set, + const std::vector &referenced_enotes_squashed, + const std::size_t real_spend_index_in_set, + const SpEnoteCoreVariant &real_reference_enote, + const crypto::secret_key &image_address_mask, + const crypto::secret_key &image_commitment_mask, + SpMembershipProofV1 &membership_proof_out); +void make_v1_membership_proof_v1(SpMembershipProofPrepV1 membership_proof_prep, SpMembershipProofV1 &membership_proof_out); +void make_v1_membership_proofs_v1(std::vector membership_proof_preps, + std::vector &membership_proofs_out); +/** +* brief: make_v1_alignable_membership_proof_v1 - make an alignable membership proof (alignable means it can be aligned +* with the corresponding enote image at a later time) +* param: membership_proof_prep - +* outparam: alignable_membership_proof_out - +*/ +void make_v1_alignable_membership_proof_v1(SpMembershipProofPrepV1 membership_proof_prep, + SpAlignableMembershipProofV1 &alignable_membership_proof_out); +void make_v1_alignable_membership_proofs_v1(std::vector membership_proof_preps, + std::vector &alignable_membership_proof_out); +/** +* brief: align_v1_membership_proofs_v1 - rearrange seraphis membership proofs so they line up with a set of input images +* param: input_images - +* param: membership_proofs_alignable - +* outparam: membership_proofs_out - +*/ +void align_v1_membership_proofs_v1(const std::vector &input_images, + std::vector membership_proofs_alignable, + std::vector &membership_proofs_out); + +} //namespace sp diff --git a/src/seraphis_main/tx_builders_legacy_inputs.cpp b/src/seraphis_main/tx_builders_legacy_inputs.cpp new file mode 100644 index 0000000000..dcfb067ad9 --- /dev/null +++ b/src/seraphis_main/tx_builders_legacy_inputs.cpp @@ -0,0 +1,455 @@ +// Copyright (c) 2022, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//paired header +#include "tx_builders_legacy_inputs.h" + +//local headers +#include "common/container_helpers.h" +#include "crypto/crypto.h" +extern "C" +{ +#include "crypto/crypto-ops.h" +} +#include "cryptonote_config.h" +#include "device/device.hpp" +#include "enote_record_types.h" +#include "enote_record_utils.h" +#include "misc_language.h" +#include "misc_log_ex.h" +#include "ringct/rctOps.h" +#include "ringct/rctSigs.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/jamtis_enote_utils.h" +#include "seraphis_crypto/sp_crypto_utils.h" +#include "seraphis_crypto/sp_hash_functions.h" +#include "seraphis_crypto/sp_transcript.h" +#include "tx_builder_types_legacy.h" +#include "tx_component_types_legacy.h" + +//third party headers + +//standard headers +#include +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis" + +namespace sp +{ +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void prepare_clsag_proof_keys(const rct::ctkeyV &referenced_enotes, + const rct::key &masked_commitment, + rct::keyV &referenced_onetime_addresses_out, + rct::keyV &referenced_amount_commitments_out, + rct::keyV &nominal_commitments_to_zero_out) +{ + referenced_onetime_addresses_out.clear(); + referenced_onetime_addresses_out.reserve(referenced_enotes.size()); + referenced_amount_commitments_out.clear(); + referenced_amount_commitments_out.reserve(referenced_enotes.size()); + nominal_commitments_to_zero_out.clear(); + nominal_commitments_to_zero_out.reserve(referenced_enotes.size()); + + for (const rct::ctkey &referenced_enote : referenced_enotes) + { + referenced_onetime_addresses_out.emplace_back(referenced_enote.dest); + referenced_amount_commitments_out.emplace_back(referenced_enote.mask); + rct::subKeys(tools::add_element(nominal_commitments_to_zero_out), referenced_enote.mask, masked_commitment); + } +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +void check_v1_legacy_input_proposal_semantics_v1(const LegacyInputProposalV1 &input_proposal, + const rct::key &legacy_spend_pubkey) +{ + // 1. the onetime address must be reproducible + // Ko ?= k_v_stuff + k^s G + rct::key onetime_address_reproduced{legacy_spend_pubkey}; + mask_key(input_proposal.enote_view_extension, onetime_address_reproduced, onetime_address_reproduced); + + CHECK_AND_ASSERT_THROW_MES(onetime_address_reproduced == input_proposal.onetime_address, + "legacy input proposal v1 semantics check: could not reproduce the onetime address."); + + // 2. the key image must be canonical (note: legacy key image can't be reproduced in a semantics checker because it + // needs the legacy private spend key [assumed not available in semantics checkers]) + CHECK_AND_ASSERT_THROW_MES(key_domain_is_prime_subgroup(rct::ki2rct(input_proposal.key_image)), + "legacy input proposal v1 semantics check: the key image is not canonical."); + + // 3. the amount commitment must be reproducible + const rct::key amount_commitment_reproduced{ + rct::commit(input_proposal.amount, rct::sk2rct(input_proposal.amount_blinding_factor)) + }; + + CHECK_AND_ASSERT_THROW_MES(amount_commitment_reproduced == input_proposal.amount_commitment, + "legacy input proposal v1 semantics check: could not reproduce the amount commitment."); + + // 4. the commitment mask must be canonical and > 1 + CHECK_AND_ASSERT_THROW_MES(sc_check(to_bytes(input_proposal.commitment_mask)) == 0, + "legacy input proposal v1 semantics check: invalid commitment mask."); + CHECK_AND_ASSERT_THROW_MES(sc_isnonzero(to_bytes(input_proposal.commitment_mask)), + "legacy input proposal v1 semantics check: commitment mask is zero."); + CHECK_AND_ASSERT_THROW_MES(!(rct::sk2rct(input_proposal.commitment_mask) == rct::identity()), + "legacy input proposal v1 semantics check: commitment mask is 1."); +} +//------------------------------------------------------------------------------------------------------------------- +void make_v1_legacy_input_proposal_v1(const rct::key &onetime_address, + const rct::key &amount_commitment, + const crypto::key_image &key_image, + const crypto::secret_key &enote_view_extension, + const rct::xmr_amount &input_amount, + const crypto::secret_key &input_amount_blinding_factor, + const crypto::secret_key &commitment_mask, + LegacyInputProposalV1 &proposal_out) +{ + // make an input proposal + proposal_out.onetime_address = onetime_address; + proposal_out.amount_commitment = amount_commitment; + proposal_out.key_image = key_image; + proposal_out.enote_view_extension = enote_view_extension; + proposal_out.amount = input_amount; + proposal_out.amount_blinding_factor = input_amount_blinding_factor; + proposal_out.commitment_mask = commitment_mask; +} +//------------------------------------------------------------------------------------------------------------------- +void make_v1_legacy_input_proposal_v1(const LegacyEnoteRecord &enote_record, + const crypto::secret_key &commitment_mask, + LegacyInputProposalV1 &proposal_out) +{ + // make input proposal from enote record + make_v1_legacy_input_proposal_v1(onetime_address_ref(enote_record.enote), + amount_commitment_ref(enote_record.enote), + enote_record.key_image, + enote_record.enote_view_extension, + enote_record.amount, + enote_record.amount_blinding_factor, + commitment_mask, + proposal_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_tx_legacy_ring_signature_message_v1(const rct::key &tx_proposal_message, + const std::vector &reference_set_indices, + rct::key &message_out) +{ + // m = H_32(tx proposal message, {reference set indices}) + SpFSTranscript transcript{ + config::HASH_KEY_LEGACY_RING_SIGNATURES_MESSAGE_V1, + 32 + reference_set_indices.size() * 8 + }; + transcript.append("tx_proposal_message", tx_proposal_message); + transcript.append("reference_set_indices", reference_set_indices); + + sp_hash_to_32(transcript.data(), transcript.size(), message_out.bytes); +} +//------------------------------------------------------------------------------------------------------------------- +void make_v3_legacy_ring_signature(const rct::key &message, + std::vector reference_set, + const rct::ctkeyV &referenced_enotes, + const std::uint64_t real_reference_index, + const rct::key &masked_commitment, + const crypto::secret_key &reference_view_privkey, + const crypto::secret_key &reference_commitment_mask, + const crypto::secret_key &legacy_spend_privkey, + hw::device &hwdev, + LegacyRingSignatureV4 &ring_signature_out) +{ + // make ring signature + + /// checks + + // 1. reference sets + CHECK_AND_ASSERT_THROW_MES(tools::is_sorted_and_unique(reference_set), + "make v3 legacy ring signature: reference set indices are not sorted and unique."); + CHECK_AND_ASSERT_THROW_MES(reference_set.size() == referenced_enotes.size(), + "make v3 legacy ring signature: reference set indices don't match referenced enotes."); + CHECK_AND_ASSERT_THROW_MES(real_reference_index < referenced_enotes.size(), + "make v3 legacy ring signature: real reference index is outside range of referenced enotes."); + + // 2. reference onetime address is reproducible + rct::key onetime_address_reproduced{rct::scalarmultBase(rct::sk2rct(legacy_spend_privkey))}; + mask_key(reference_view_privkey, onetime_address_reproduced, onetime_address_reproduced); + + CHECK_AND_ASSERT_THROW_MES(onetime_address_reproduced == referenced_enotes[real_reference_index].dest, + "make v3 legacy ring signature: could not reproduce onetime address."); + + // 3. masked commitment is reproducible + rct::key masked_commitment_reproduced{referenced_enotes[real_reference_index].mask}; + mask_key(reference_commitment_mask, masked_commitment_reproduced, masked_commitment_reproduced); + + CHECK_AND_ASSERT_THROW_MES(masked_commitment_reproduced == masked_commitment, + "make v3 legacy ring signature: could not reproduce masked commitment (pseudo-output commitment)."); + + + /// prepare to make proof + + // 1. prepare proof pubkeys + rct::keyV referenced_onetime_addresses; + rct::keyV referenced_amount_commitments; + rct::keyV nominal_commitments_to_zero; + + prepare_clsag_proof_keys(referenced_enotes, + masked_commitment, + referenced_onetime_addresses, + referenced_amount_commitments, + nominal_commitments_to_zero); + + // 2. prepare signing key + crypto::secret_key signing_privkey; + sc_add(to_bytes(signing_privkey), to_bytes(reference_view_privkey), to_bytes(legacy_spend_privkey)); + + // 3. prepare commitment to zero key (negated mask): z + crypto::secret_key z; + sc_mul(to_bytes(z), minus_one().bytes, to_bytes(reference_commitment_mask)); + + + /// make clsag proof + ring_signature_out.clsag_proof = rct::CLSAG_Gen(message, + referenced_onetime_addresses, + rct::sk2rct(signing_privkey), + nominal_commitments_to_zero, + rct::sk2rct(z), + referenced_amount_commitments, + masked_commitment, + real_reference_index, + hwdev); + + + /// save the reference set + ring_signature_out.reference_set = std::move(reference_set); +} +//------------------------------------------------------------------------------------------------------------------- +void make_v3_legacy_ring_signature_v1(LegacyRingSignaturePrepV1 ring_signature_prep, + const crypto::secret_key &legacy_spend_privkey, + hw::device &hwdev, + LegacyRingSignatureV4 &ring_signature_out) +{ + // proof message + rct::key message; + make_tx_legacy_ring_signature_message_v1(ring_signature_prep.tx_proposal_prefix, + ring_signature_prep.reference_set, + message); + + // complete signature + make_v3_legacy_ring_signature(message, + std::move(ring_signature_prep.reference_set), + ring_signature_prep.referenced_enotes, + ring_signature_prep.real_reference_index, + ring_signature_prep.reference_image.masked_commitment, + ring_signature_prep.reference_view_privkey, + ring_signature_prep.reference_commitment_mask, + legacy_spend_privkey, + hwdev, + ring_signature_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_v3_legacy_ring_signatures_v1(std::vector ring_signature_preps, + const crypto::secret_key &legacy_spend_privkey, + hw::device &hwdev, + std::vector &ring_signatures_out) +{ + // only allow signatures on the same tx proposal + for (const LegacyRingSignaturePrepV1 &signature_prep : ring_signature_preps) + { + CHECK_AND_ASSERT_THROW_MES(signature_prep.tx_proposal_prefix == ring_signature_preps[0].tx_proposal_prefix, + "make v3 legacy ring signatures: inconsistent proposal prefixes."); + } + + // sort ring signature preps + std::sort(ring_signature_preps.begin(), + ring_signature_preps.end(), + tools::compare_func(compare_KI)); + + // make multiple ring signatures + ring_signatures_out.clear(); + ring_signatures_out.reserve(ring_signature_preps.size()); + + for (LegacyRingSignaturePrepV1 &signature_prep : ring_signature_preps) + { + make_v3_legacy_ring_signature_v1(std::move(signature_prep), + legacy_spend_privkey, + hwdev, + tools::add_element(ring_signatures_out)); + } +} +//------------------------------------------------------------------------------------------------------------------- +void check_v1_legacy_input_semantics_v1(const LegacyInputV1 &input) +{ + // 1. masked commitment can be reconstructed + const rct::key masked_commitment_reproduced{ + rct::commit(input.input_amount, rct::sk2rct(input.input_masked_commitment_blinding_factor)) + }; + + CHECK_AND_ASSERT_THROW_MES(masked_commitment_reproduced == input.input_image.masked_commitment, + "legacy input semantics (v1): could not reproduce masked commitment (pseudo-output commitment)."); + + // 2. key image is consistent between input image and cached value in the ring signature + CHECK_AND_ASSERT_THROW_MES(input.input_image.key_image == rct::rct2ki(input.ring_signature.clsag_proof.I), + "legacy input semantics (v1): key image is not consistent between input image and ring signature."); + + // 3. ring signature reference indices are sorted and unique and match with the cached reference enotes + CHECK_AND_ASSERT_THROW_MES(tools::is_sorted_and_unique(input.ring_signature.reference_set), + "legacy input semantics (v1): reference set indices are not sorted and unique."); + CHECK_AND_ASSERT_THROW_MES(input.ring_signature.reference_set.size() == input.ring_members.size(), + "legacy input semantics (v1): reference set indices don't match referenced enotes."); + + // 4. ring signature message + rct::key ring_signature_message; + make_tx_legacy_ring_signature_message_v1(input.tx_proposal_prefix, + input.ring_signature.reference_set, + ring_signature_message); + + // 4. ring signature is valid + CHECK_AND_ASSERT_THROW_MES(rct::verRctCLSAGSimple(ring_signature_message, + input.ring_signature.clsag_proof, + input.ring_members, + input.input_image.masked_commitment), + "legacy input semantics (v1): ring signature is invalid."); +} +//------------------------------------------------------------------------------------------------------------------- +void make_v1_legacy_input_v1(const rct::key &tx_proposal_prefix, + const LegacyInputProposalV1 &input_proposal, + LegacyRingSignatureV4 ring_signature, + rct::ctkeyV referenced_enotes, + const rct::key &legacy_spend_pubkey, + LegacyInputV1 &input_out) +{ + // 1. check input proposal semantics + check_v1_legacy_input_proposal_semantics_v1(input_proposal, legacy_spend_pubkey); + + // 2. prepare input image + get_enote_image_v2(input_proposal, input_out.input_image); + + // 3. set remaining legacy input info + input_out.ring_signature = std::move(ring_signature); + input_out.input_amount = input_proposal.amount; + sc_add(to_bytes(input_out.input_masked_commitment_blinding_factor), + to_bytes(input_proposal.commitment_mask), + to_bytes(input_proposal.amount_blinding_factor)); + input_out.ring_members = std::move(referenced_enotes); + input_out.tx_proposal_prefix = tx_proposal_prefix; +} +//------------------------------------------------------------------------------------------------------------------- +void make_v1_legacy_input_v1(const rct::key &tx_proposal_prefix, + const LegacyInputProposalV1 &input_proposal, + LegacyRingSignaturePrepV1 ring_signature_prep, + const crypto::secret_key &legacy_spend_privkey, + hw::device &hwdev, + LegacyInputV1 &input_out) +{ + // 1. ring signature prep must line up with specified proposal prefix + CHECK_AND_ASSERT_THROW_MES(tx_proposal_prefix == ring_signature_prep.tx_proposal_prefix, + "make v1 legacy input: ring signature prep does not have desired proposal prefix."); + + // 2. misc initialization + rct::ctkeyV referenced_enotes_copy{ring_signature_prep.referenced_enotes}; + const rct::key legacy_spend_pubkey{rct::scalarmultBase(rct::sk2rct(legacy_spend_privkey))}; + + // 3. construct ring signature + LegacyRingSignatureV4 ring_signature; + make_v3_legacy_ring_signature_v1(std::move(ring_signature_prep), legacy_spend_privkey, hwdev, ring_signature); + + // 4. finish making the input + make_v1_legacy_input_v1(tx_proposal_prefix, + input_proposal, + std::move(ring_signature), + std::move(referenced_enotes_copy), + legacy_spend_pubkey, + input_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_v1_legacy_inputs_v1(const rct::key &tx_proposal_prefix, + const std::vector &input_proposals, + std::vector ring_signature_preps, + const crypto::secret_key &legacy_spend_privkey, + hw::device &hwdev, + std::vector &inputs_out) +{ + // checks + CHECK_AND_ASSERT_THROW_MES(input_proposals.size() == ring_signature_preps.size(), + "make v1 legacy inputs: input proposals don't line up with ring signature preps."); + + inputs_out.clear(); + inputs_out.reserve(input_proposals.size()); + + // make all inputs + for (std::size_t input_index{0}; input_index < input_proposals.size(); ++input_index) + { + make_v1_legacy_input_v1(tx_proposal_prefix, + input_proposals[input_index], + std::move(ring_signature_preps[input_index]), + legacy_spend_privkey, + hwdev, + tools::add_element(inputs_out)); + } +} +//------------------------------------------------------------------------------------------------------------------- +void get_legacy_input_commitment_factors_v1(const std::vector &input_proposals, + std::vector &input_amounts_out, + std::vector &blinding_factors_out) +{ + // use legacy input proposals to get amounts/blinding factors + input_amounts_out.clear(); + input_amounts_out.reserve(input_proposals.size()); + blinding_factors_out.clear(); + blinding_factors_out.reserve(input_proposals.size()); + + for (const LegacyInputProposalV1 &input_proposal : input_proposals) + { + // input amount: a + input_amounts_out.emplace_back(input_proposal.amount); + + // input image amount commitment blinding factor: x" = mask + x + sc_add(to_bytes(tools::add_element(blinding_factors_out)), + to_bytes(input_proposal.commitment_mask), //mask + to_bytes(input_proposal.amount_blinding_factor)); //x + } +} +//------------------------------------------------------------------------------------------------------------------- +void get_legacy_input_commitment_factors_v1(const std::vector &inputs, + std::vector &input_amounts_out, + std::vector &blinding_factors_out) +{ + // use legacy inputs to get amounts/blinding factors + input_amounts_out.clear(); + input_amounts_out.reserve(inputs.size()); + blinding_factors_out.clear(); + blinding_factors_out.reserve(inputs.size()); + + for (const LegacyInputV1 &input : inputs) + { + // input amount: a + input_amounts_out.emplace_back(input.input_amount); + + // masked commitment blinding factor: x" = mask + x + blinding_factors_out.emplace_back(input.input_masked_commitment_blinding_factor); + } +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace sp diff --git a/src/seraphis_main/tx_builders_legacy_inputs.h b/src/seraphis_main/tx_builders_legacy_inputs.h new file mode 100644 index 0000000000..1f4f7a9d7b --- /dev/null +++ b/src/seraphis_main/tx_builders_legacy_inputs.h @@ -0,0 +1,170 @@ +// Copyright (c) 2022, 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 tx-builder/component-builder implementations (legacy tx inputs). + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "device/device.hpp" +#include "enote_record_types.h" +#include "ringct/rctTypes.h" +#include "tx_builder_types_legacy.h" +#include "tx_component_types_legacy.h" + +//third party headers + +//standard headers +#include + +//forward declarations + + +namespace sp +{ + +/** +* brief: check_v1_legacy_input_proposal_semantics_v1 - check semantics of a legacy v1 input proposal +* - throws on failure +* param: input_proposal - +* param: legacy_spend_pubkey - +*/ +void check_v1_legacy_input_proposal_semantics_v1(const LegacyInputProposalV1 &input_proposal, + const rct::key &legacy_spend_pubkey); +/** +* brief: make_v1_legacy_input_proposal_v1 - make a legacy v1 input proposal +* param: onetime_address - +* param: amount_commitment - +* param: key_image - +* param: enote_view_extension - +* param: input_amount - +* param: input_amount_blinding_factor - +* param: commitment_mask - +* outparam: proposal_out - +*/ +void make_v1_legacy_input_proposal_v1(const rct::key &onetime_address, + const rct::key &amount_commitment, + const crypto::key_image &key_image, + const crypto::secret_key &enote_view_extension, + const rct::xmr_amount &input_amount, + const crypto::secret_key &input_amount_blinding_factor, + const crypto::secret_key &commitment_mask, + SpInputProposalCore &proposal_out); +void make_v1_legacy_input_proposal_v1(const LegacyEnoteRecord &enote_record, + const crypto::secret_key &commitment_mask, + LegacyInputProposalV1 &proposal_out); +/** +* brief: make_tx_legacy_ring_signature_message_v1 - message to sign in legacy ring signatures used in a transaction +* - H_32(tx proposal message, {reference set indices}) +* param: tx_proposal_message - represents the transaction being signed (inputs, outputs, and memos), excluding proofs +* param: reference_set_indices - indices into the ledger's set of legacy enotes +* outparam: message_out - the message to sign in a legacy ring signature +*/ +void make_tx_legacy_ring_signature_message_v1(const rct::key &tx_proposal_message, + const std::vector &reference_set_indices, + rct::key &message_out); +/** +* brief: make_v3_legacy_ring_signature - make a legacy v3 ring signature +* param: message - +* param: reference_set - +* param: referenced_enotes - +* param: real_reference_index - +* param: masked_commitment - +* param: reference_view_privkey - +* param: reference_commitment_mask - +* param: legacy_spend_privkey - +* inoutparam: hwdev - +* outparam: ring_signature_out - +*/ +void make_v3_legacy_ring_signature(const rct::key &message, + std::vector reference_set, + const rct::ctkeyV &referenced_enotes, + const std::uint64_t real_reference_index, + const rct::key &masked_commitment, + const crypto::secret_key &reference_view_privkey, + const crypto::secret_key &reference_commitment_mask, + const crypto::secret_key &legacy_spend_privkey, + hw::device &hwdev, + LegacyRingSignatureV4 &ring_signature_out); +void make_v3_legacy_ring_signature_v1(LegacyRingSignaturePrepV1 ring_signature_prep, + const crypto::secret_key &legacy_spend_privkey, + hw::device &hwdev, + LegacyRingSignatureV4 &ring_signature_out); +void make_v3_legacy_ring_signatures_v1(std::vector ring_signature_preps, + const crypto::secret_key &legacy_spend_privkey, + hw::device &hwdev, + std::vector &ring_signatures_out); +/** +* brief: check_v1_legacy_input_semantics_v1 - check semantics of a legacy input +* - throws on failure +* param: input - +*/ +void check_v1_legacy_input_semantics_v1(const LegacyInputV1 &input); +/** +* brief: make_v1_legacy_input_v1 - make a legacy v1 input +* param: tx_proposal_prefix - +* param: input_proposal - +* param: ring_signature - +* param: referenced_enotes - +* param: legacy_spend_pubkey - +* inoutparam: hwdev - +* outparam: input_out - +*/ +void make_v1_legacy_input_v1(const rct::key &tx_proposal_prefix, + const LegacyInputProposalV1 &input_proposal, + LegacyRingSignatureV4 ring_signature, + rct::ctkeyV referenced_enotes, + const rct::key &legacy_spend_pubkey, + LegacyInputV1 &input_out); +void make_v1_legacy_input_v1(const rct::key &tx_proposal_prefix, + const LegacyInputProposalV1 &input_proposal, + LegacyRingSignaturePrepV1 ring_signature_prep, + const crypto::secret_key &legacy_spend_privkey, + hw::device &hwdev, + LegacyInputV1 &input_out); +void make_v1_legacy_inputs_v1(const rct::key &tx_proposal_prefix, + const std::vector &input_proposals, + std::vector ring_signature_preps, //must align with input_proposals + const crypto::secret_key &legacy_spend_privkey, + hw::device &hwdev, + std::vector &inputs_out); +/** +* brief: get_legacy_input_commitment_factors_v1 - collect input amounts and blinding factors +* param: input_proposals - +* outparam: input_amounts_out - +* outparam: blinding_factors_out - +*/ +void get_legacy_input_commitment_factors_v1(const std::vector &input_proposals, + std::vector &input_amounts_out, + std::vector &blinding_factors_out); +void get_legacy_input_commitment_factors_v1(const std::vector &inputs, + std::vector &input_amounts_out, + std::vector &blinding_factors_out); + +} //namespace sp diff --git a/src/seraphis_main/tx_builders_mixed.cpp b/src/seraphis_main/tx_builders_mixed.cpp new file mode 100644 index 0000000000..6ce0c9836b --- /dev/null +++ b/src/seraphis_main/tx_builders_mixed.cpp @@ -0,0 +1,1230 @@ +// Copyright (c) 2022, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//paired header +#include "tx_builders_mixed.h" + +//local headers +#include "common/container_helpers.h" +#include "contextual_enote_record_utils.h" +#include "crypto/crypto.h" +#include "cryptonote_config.h" +#include "misc_language.h" +#include "misc_log_ex.h" +#include "ringct/rctOps.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/binned_reference_set_utils.h" +#include "seraphis_core/jamtis_core_utils.h" +#include "seraphis_core/jamtis_support_types.h" +#include "seraphis_core/sp_core_enote_utils.h" +#include "seraphis_core/sp_ref_set_index_mapper_flat.h" +#include "seraphis_crypto/bulletproofs_plus2.h" +#include "seraphis_crypto/math_utils.h" +#include "seraphis_crypto/sp_crypto_utils.h" +#include "seraphis_crypto/sp_hash_functions.h" +#include "seraphis_crypto/sp_legacy_proof_helpers.h" +#include "seraphis_crypto/sp_transcript.h" +#include "tx_builder_types.h" +#include "tx_builders_inputs.h" +#include "tx_builders_legacy_inputs.h" +#include "tx_builders_outputs.h" +#include "tx_component_types.h" +#include "tx_component_types_legacy.h" +#include "tx_validators.h" +#include "txtype_base.h" +#include "txtype_squashed_v1.h" + +//third party headers +#include "boost/multiprecision/cpp_int.hpp" + +//standard headers +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis" + +namespace sp +{ +//// +// TxValidationContextSimple +// - assumes key images are not double-spent +// - stores manually-specified reference set elements (useful for validating partial txs) +/// +class TxValidationContextSimple final : public TxValidationContext +{ +public: +//constructors + TxValidationContextSimple(const std::unordered_map &legacy_reference_set_proof_elements, + const std::unordered_map &sp_reference_set_proof_elements) : + m_legacy_reference_set_proof_elements{legacy_reference_set_proof_elements}, + m_sp_reference_set_proof_elements{sp_reference_set_proof_elements} + {} + +//overloaded operators + /// disable copy/move (this is a scoped manager [reference wrapper]) + TxValidationContextSimple& operator=(TxValidationContextSimple&&) = delete; + +//member functions + /** + * brief: *_key_image_exists - check if a key image exists (always false here) + * ... + */ + bool cryptonote_key_image_exists(const crypto::key_image &key_image) const override + { + return false; + } + bool seraphis_key_image_exists(const crypto::key_image &key_image) const override + { + return false; + } + /** + * brief: get_reference_set_proof_elements_v1 - gets legacy {KI, C} pairs stored in the validation context + * param: indices - + * outparam: proof_elements_out - {KI, C} + */ + void get_reference_set_proof_elements_v1(const std::vector &indices, + rct::ctkeyV &proof_elements_out) const override + { + proof_elements_out.clear(); + proof_elements_out.reserve(indices.size()); + + for (const std::uint64_t index : indices) + { + if (m_legacy_reference_set_proof_elements.find(index) != m_legacy_reference_set_proof_elements.end()) + proof_elements_out.emplace_back(m_legacy_reference_set_proof_elements.at(index)); + else + proof_elements_out.emplace_back(rct::ctkey{}); + } + } + /** + * brief: get_reference_set_proof_elements_v2 - gets seraphis squashed enotes stored in the validation context + * param: indices - + * outparam: proof_elements_out - {squashed enote} + */ + void get_reference_set_proof_elements_v2(const std::vector &indices, + rct::keyV &proof_elements_out) const override + { + proof_elements_out.clear(); + proof_elements_out.reserve(indices.size()); + + for (const std::uint64_t index : indices) + { + if (m_sp_reference_set_proof_elements.find(index) != m_sp_reference_set_proof_elements.end()) + proof_elements_out.emplace_back(m_sp_reference_set_proof_elements.at(index)); + else + proof_elements_out.emplace_back(rct::key{}); + } + } + +//member variables +private: + const std::unordered_map &m_legacy_reference_set_proof_elements; + const std::unordered_map &m_sp_reference_set_proof_elements; +}; + +//------------------------------------------------------------------------------------------------------------------- +// convert a crypto::secret_key vector to an rct::key vector, and obtain a memwiper for the rct::key vector +//------------------------------------------------------------------------------------------------------------------- +static auto convert_skv_to_rctv(const std::vector &skv, rct::keyV &rctv_out) +{ + auto a_wiper = epee::misc_utils::create_scope_leave_handler( + [&rctv_out]() + { + memwipe(rctv_out.data(), rctv_out.size()*sizeof(rct::key)); + } + ); + + rctv_out.clear(); + rctv_out.reserve(skv.size()); + + for (const crypto::secret_key &skey : skv) + rctv_out.emplace_back(rct::sk2rct(skey)); + + return a_wiper; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool same_key_image(const LegacyInputV1 &input, const LegacyInputProposalV1 &input_proposal) +{ + return input.input_image.key_image == input_proposal.key_image; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool same_key_image(const SpPartialInputV1 &partial_input, const SpInputProposalV1 &input_proposal) +{ + return key_image_ref(partial_input.input_image) == key_image_ref(input_proposal); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void legacy_enote_records_to_input_proposals( + const std::vector &legacy_contextual_records, + std::vector &legacy_input_proposals_out) +{ + legacy_input_proposals_out.clear(); + legacy_input_proposals_out.reserve(legacy_contextual_records.size()); + + for (const LegacyContextualEnoteRecordV1 &legacy_contextual_input : legacy_contextual_records) + { + // convert legacy inputs to input proposals + make_v1_legacy_input_proposal_v1(legacy_contextual_input.record, + rct::rct2sk(rct::skGen()), + tools::add_element(legacy_input_proposals_out)); + } +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void sp_enote_records_to_input_proposals(const std::vector &sp_contextual_records, + std::vector &sp_input_proposals_out) +{ + sp_input_proposals_out.clear(); + sp_input_proposals_out.reserve(sp_contextual_records.size()); + + for (const SpContextualEnoteRecordV1 &sp_contextual_input : sp_contextual_records) + { + // convert seraphis inputs to input proposals + make_v1_input_proposal_v1(sp_contextual_input.record, + rct::rct2sk(rct::skGen()), + rct::rct2sk(rct::skGen()), + tools::add_element(sp_input_proposals_out)); + } +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void prepare_sp_membership_proof_prep_for_tx_simulation_v1(const rct::keyV &simulated_ledger_squashed_enotes, + const std::size_t real_reference_index, + const SpEnoteCoreVariant &real_reference_enote, + const crypto::secret_key &address_mask, + const crypto::secret_key &commitment_mask, + const std::size_t ref_set_decomp_n, + const std::size_t ref_set_decomp_m, + const SpBinnedReferenceSetConfigV1 &bin_config, + SpMembershipProofPrepV1 &prep_out) +{ + /// checks and initialization + const std::size_t ref_set_size{math::uint_pow(ref_set_decomp_n, ref_set_decomp_m)}; //n^m + + CHECK_AND_ASSERT_THROW_MES(simulated_ledger_squashed_enotes.size() > 0, + "prepare sp membership proof prep v1 (tx simulation): insufficient reference elements."); + CHECK_AND_ASSERT_THROW_MES(simulated_ledger_squashed_enotes.size() >= compute_bin_width(bin_config.bin_radius), + "prepare sp membership proof prep v1 (tx simulation): insufficient reference elements."); + CHECK_AND_ASSERT_THROW_MES(real_reference_index < simulated_ledger_squashed_enotes.size(), + "prepare sp membership proof prep v1 (tx simulation): real reference is out of bounds."); + CHECK_AND_ASSERT_THROW_MES(validate_bin_config_v1(ref_set_size, bin_config), + "prepare sp membership proof prep v1 (tx simulation): invalid binned reference set config."); + + + /// make binned reference set + + // 1. flat index mapper for mock-up + const SpRefSetIndexMapperFlat flat_index_mapper{0, simulated_ledger_squashed_enotes.size() - 1}; + + // 2. generator seed + rct::key generator_seed; + make_binned_ref_set_generator_seed_v1(onetime_address_ref(real_reference_enote), + amount_commitment_ref(real_reference_enote), + address_mask, + commitment_mask, + generator_seed); + + // 3. binned reference set + make_binned_reference_set_v1(flat_index_mapper, + bin_config, + generator_seed, + ref_set_size, + real_reference_index, + prep_out.binned_reference_set); + + + /// copy all referenced enotes from the simulated ledger (in squashed enote representation) + std::vector reference_indices; + CHECK_AND_ASSERT_THROW_MES(try_get_reference_indices_from_binned_reference_set_v1(prep_out.binned_reference_set, + reference_indices), + "prepare sp membership proof prep v1 (tx simulation): could not extract reference indices from binned " + "representation (bug)."); + + prep_out.referenced_enotes_squashed.clear(); + prep_out.referenced_enotes_squashed.reserve(reference_indices.size()); + + for (const std::uint64_t reference_index : reference_indices) + { + CHECK_AND_ASSERT_THROW_MES(reference_index < simulated_ledger_squashed_enotes.size(), + "prepare sp membership proof prep v1 (tx simulation): invalid index recovered from binned representation " + "(bug)."); + prep_out.referenced_enotes_squashed.emplace_back(simulated_ledger_squashed_enotes.at(reference_index)); + } + + + /// copy misc pieces + prep_out.ref_set_decomp_n = ref_set_decomp_n; + prep_out.ref_set_decomp_m = ref_set_decomp_m; + prep_out.real_reference_enote = real_reference_enote; + prep_out.address_mask = address_mask; + prep_out.commitment_mask = commitment_mask; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void prepare_sp_membership_proof_preps_for_tx_simulation_v1( + const std::vector &real_reference_enotes, + const std::vector &address_masks, + const std::vector &commitment_masks, + const std::size_t ref_set_decomp_n, + const std::size_t ref_set_decomp_m, + const SpBinnedReferenceSetConfigV1 &bin_config, + std::vector &preps_out, + std::unordered_map &sp_reference_set_proof_elements_out) +{ + preps_out.clear(); + sp_reference_set_proof_elements_out.clear(); + + /// checks + CHECK_AND_ASSERT_THROW_MES(real_reference_enotes.size() == address_masks.size(), + "prepare sp membership proof preps v1 (tx simulation): invalid number of address masks."); + CHECK_AND_ASSERT_THROW_MES(real_reference_enotes.size() == commitment_masks.size(), + "prepare sp membership proof preps v1 (tx simulation): invalid number of commitment masks."); + + + /// make preps + + // 1. convert real reference enotes to squashed representations + // - the enotes' indices in the input vectors will be treated as their indices in the simulated ledger + rct::keyV simulated_ledger_squashed_enotes; + simulated_ledger_squashed_enotes.reserve( + std::max(static_cast(real_reference_enotes.size()), compute_bin_width(bin_config.bin_radius)) + ); + + for (std::size_t proof_index{0}; proof_index < real_reference_enotes.size(); ++proof_index) + { + make_seraphis_squashed_enote_Q(onetime_address_ref(real_reference_enotes[proof_index]), + amount_commitment_ref(real_reference_enotes[proof_index]), + tools::add_element(simulated_ledger_squashed_enotes)); + + // save the [ index : squashed enote ] mapping + sp_reference_set_proof_elements_out[proof_index] = simulated_ledger_squashed_enotes.back(); + } + + // 2. pad the simulated ledger's squashed enotes so there are enough to satisfy the binning config + for (std::size_t ref_set_index{simulated_ledger_squashed_enotes.size()}; + ref_set_index < compute_bin_width(bin_config.bin_radius); + ++ref_set_index) + { + simulated_ledger_squashed_enotes.emplace_back(rct::pkGen()); + + // save the [ index : squashed enote ] mapping + sp_reference_set_proof_elements_out[ref_set_index] = simulated_ledger_squashed_enotes.back(); + } + + // 3. make each membership proof prep + for (std::size_t proof_index{0}; proof_index < real_reference_enotes.size(); ++proof_index) + { + // make the proof prep + prepare_sp_membership_proof_prep_for_tx_simulation_v1(simulated_ledger_squashed_enotes, + proof_index, + real_reference_enotes[proof_index], + address_masks[proof_index], + commitment_masks[proof_index], + ref_set_decomp_n, + ref_set_decomp_m, + bin_config, + tools::add_element(preps_out)); + } +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void check_tx_proposal_semantics_inputs_v1(const std::vector &legacy_input_proposals, + const std::vector &sp_input_proposals, + const rct::key &legacy_spend_pubkey, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + std::vector &in_amounts_out) +{ + // 1. there should be at least one input + CHECK_AND_ASSERT_THROW_MES(legacy_input_proposals.size() + sp_input_proposals.size() >= 1, + "Semantics check tx proposal inputs v1: there are no inputs."); + + // 2. input proposals should be sorted and unique + CHECK_AND_ASSERT_THROW_MES(tools::is_sorted_and_unique(legacy_input_proposals, compare_KI), + "Semantics check tx proposal inputs v1: legacy input proposals are not sorted and unique."); + CHECK_AND_ASSERT_THROW_MES(tools::is_sorted_and_unique(sp_input_proposals, compare_KI), + "Semantics check tx proposal inputs v1: seraphis input proposals are not sorted and unique."); + + // 3. legacy input proposal semantics should be valid + for (const LegacyInputProposalV1 &legacy_input_proposal : legacy_input_proposals) + check_v1_legacy_input_proposal_semantics_v1(legacy_input_proposal, legacy_spend_pubkey); + + // 4. seraphis input proposal semantics should be valid + rct::key sp_core_spend_pubkey{jamtis_spend_pubkey}; + reduce_seraphis_spendkey_x(k_view_balance, sp_core_spend_pubkey); + + for (const SpInputProposalV1 &sp_input_proposal : sp_input_proposals) + check_v1_input_proposal_semantics_v1(sp_input_proposal, sp_core_spend_pubkey, k_view_balance); + + // 5. collect input amounts + in_amounts_out.reserve(legacy_input_proposals.size() + sp_input_proposals.size()); + + for (const LegacyInputProposalV1 &legacy_input_proposal : legacy_input_proposals) + in_amounts_out.emplace_back(amount_ref(legacy_input_proposal)); + + for (const SpInputProposalV1 &sp_input_proposal : sp_input_proposals) + in_amounts_out.emplace_back(amount_ref(sp_input_proposal)); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void check_tx_proposal_semantics_selfsend_outputs_v1(const std::size_t num_normal_payment_proposals, + const std::vector &selfsend_payment_proposals, + const rct::key &input_context, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance) +{ + // 1. there must be at least one self-send output + CHECK_AND_ASSERT_THROW_MES(selfsend_payment_proposals.size() > 0, + "Semantics check tx proposal selfsends v1: there are no self-send outputs (at least one is expected)."); + + // 2. there cannot be two self-send outputs of the same type and no other outputs + // note: violations of this rule will cause both outputs to have the same sender-receiver shared secret, which + // can cause privacy issues for the tx author + if (num_normal_payment_proposals == 0 && + selfsend_payment_proposals.size() == 2) + { + CHECK_AND_ASSERT_THROW_MES(selfsend_payment_proposals[0].type != selfsend_payment_proposals[1].type, + "Semantics check tx proposal selfsends v1: there are two self-send outputs of the same type but no other " + "outputs (not allowed)."); + } + + // 3. all self-send destinations must be owned by the wallet + for (const jamtis::JamtisPaymentProposalSelfSendV1 &selfsend_payment_proposal : selfsend_payment_proposals) + { + check_jamtis_payment_proposal_selfsend_semantics_v1(selfsend_payment_proposal, + input_context, + jamtis_spend_pubkey, + k_view_balance); + } +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void check_tx_proposal_semantics_output_proposals_v1(const std::vector &output_proposals, + const TxExtra &partial_memo, + std::vector &output_amounts_out) +{ + // 1. check output proposal semantics + check_v1_output_proposal_set_semantics_v1(output_proposals); + + // 2. extract outputs from the output proposals + std::vector output_enotes; + std::vector output_amount_commitment_blinding_factors; + SpTxSupplementV1 tx_supplement; + + make_v1_outputs_v1(output_proposals, + output_enotes, + output_amounts_out, + output_amount_commitment_blinding_factors, + tx_supplement.output_enote_ephemeral_pubkeys); + + finalize_tx_extra_v1(partial_memo, output_proposals, tx_supplement.tx_extra); + + // 3. at least two outputs are expected + // note: this rule exists because the vast majority of txs normally have at least 2 outputs (i.e. 1+ outputs and + // change), so preventing 1-output txs improves tx uniformity + CHECK_AND_ASSERT_THROW_MES(output_enotes.size() >= 2, + "Semantics check tx proposal outputs v1: there are fewer than 2 outputs."); + + // 4. outputs should be sorted and unique + CHECK_AND_ASSERT_THROW_MES(tools::is_sorted_and_unique(output_enotes, compare_Ko), + "Semantics check tx proposal outputs v1: output onetime addresses are not sorted and unique."); + + // 5. onetime addresses should be canonical (sanity check so our tx outputs don't end up with duplicate key images) + for (const SpEnoteV1 &output_enote : output_enotes) + { + CHECK_AND_ASSERT_THROW_MES(onetime_address_is_canonical(output_enote.core), + "Semantics check tx proposal outputs v1: an output onetime address is not in the prime subgroup."); + } + + // 6. check that output amount commitments can be reproduced + CHECK_AND_ASSERT_THROW_MES(output_enotes.size() == output_amounts_out.size(), + "Semantics check tx proposal outputs v1: outputs don't line up with output amounts."); + CHECK_AND_ASSERT_THROW_MES(output_enotes.size() == output_amount_commitment_blinding_factors.size(), + "Semantics check tx proposal outputs v1: outputs don't line up with output amount commitment blinding factors."); + + for (std::size_t output_index{0}; output_index < output_enotes.size(); ++output_index) + { + CHECK_AND_ASSERT_THROW_MES(output_enotes[output_index].core.amount_commitment == + rct::commit(output_amounts_out[output_index], + rct::sk2rct(output_amount_commitment_blinding_factors[output_index])), + "Semantics check tx proposal outputs v1: could not reproduce an output's amount commitment."); + } + + // 7. check tx supplement (especially enote ephemeral pubkeys) + // note: require ephemeral pubkey optimization for normal txs + check_v1_tx_supplement_semantics_v2(tx_supplement, output_enotes.size()); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void collect_legacy_ring_signature_ring_members(const std::vector &legacy_ring_signatures, + const std::vector &legacy_ring_signature_rings, + std::unordered_map &legacy_reference_set_proof_elements_out) +{ + // map legacy ring members onto their on-chain legacy enote indices + CHECK_AND_ASSERT_THROW_MES(legacy_ring_signatures.size() == legacy_ring_signature_rings.size(), + "collect legacy ring signature ring members: legacy ring signatures don't line up with legacy ring signature " + "rings."); + + for (std::size_t legacy_input_index{0}; legacy_input_index < legacy_ring_signatures.size(); ++legacy_input_index) + { + CHECK_AND_ASSERT_THROW_MES(legacy_ring_signatures[legacy_input_index].reference_set.size() == + legacy_ring_signature_rings[legacy_input_index].size(), + "collect legacy ring signature ring members: a reference set doesn't line up with the corresponding ring."); + + for (std::size_t ring_index{0}; ring_index < legacy_ring_signature_rings[legacy_input_index].size(); ++ring_index) + { + legacy_reference_set_proof_elements_out[ + legacy_ring_signatures[legacy_input_index].reference_set[ring_index] + ] = legacy_ring_signature_rings[legacy_input_index][ring_index]; + } + } +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +void make_tx_proposal_prefix_v1(const tx_version_t &tx_version, + const std::vector &legacy_input_key_images, + const std::vector &sp_input_key_images, + const std::vector &output_enotes, + const rct::xmr_amount transaction_fee, + const SpTxSupplementV1 &tx_supplement, + rct::key &tx_proposal_prefix_out) +{ + // note: these were added due to hard-to-diagnose sorting bugs, however they do incur some cost for tx verification + CHECK_AND_ASSERT_THROW_MES(std::is_sorted(legacy_input_key_images.begin(), legacy_input_key_images.end()), + "tx proposal prefix (v1): legacy input key images are not sorted."); + CHECK_AND_ASSERT_THROW_MES(std::is_sorted(sp_input_key_images.begin(), sp_input_key_images.end()), + "tx proposal prefix (v1): seraphis input key images are not sorted."); + CHECK_AND_ASSERT_THROW_MES(std::is_sorted(output_enotes.begin(), + output_enotes.end(), + tools::compare_func(compare_Ko)), + "tx proposal prefix (v1): output enotes are not sorted."); + + // H_32(tx version, legacy input key images, seraphis input key images, output enotes, fee, tx supplement) + SpFSTranscript transcript{ + config::HASH_KEY_SERAPHIS_TX_PROPOSAL_MESSAGE_V1, + sizeof(tx_version) + + (legacy_input_key_images.size() + sp_input_key_images.size())*sizeof(crypto::key_image) + + output_enotes.size()*sp_enote_v1_size_bytes() + + sizeof(transaction_fee) + + sp_tx_supplement_v1_size_bytes(tx_supplement) + }; + transcript.append("tx_version", tx_version.bytes); + transcript.append("legacy_input_key_images", legacy_input_key_images); + transcript.append("sp_input_key_images", sp_input_key_images); + transcript.append("output_enotes", output_enotes); + transcript.append("transaction_fee", transaction_fee); + transcript.append("tx_supplement", tx_supplement); + + sp_hash_to_32(transcript.data(), transcript.size(), tx_proposal_prefix_out.bytes); +} +//------------------------------------------------------------------------------------------------------------------- +void make_tx_proposal_prefix_v1(const tx_version_t &tx_version, + const std::vector &legacy_input_key_images, + const std::vector &sp_input_key_images, + const std::vector &output_enotes, + const DiscretizedFee transaction_fee, + const SpTxSupplementV1 &tx_supplement, + rct::key &tx_proposal_prefix_out) +{ + // get raw fee value + rct::xmr_amount raw_transaction_fee; + CHECK_AND_ASSERT_THROW_MES(try_get_fee_value(transaction_fee, raw_transaction_fee), + "make image proposal prefix (v1): could not extract raw fee from discretized fee."); + + // get proposal prefix + make_tx_proposal_prefix_v1(tx_version, + legacy_input_key_images, + sp_input_key_images, + output_enotes, + raw_transaction_fee, + tx_supplement, + tx_proposal_prefix_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_tx_proposal_prefix_v1(const tx_version_t &tx_version, + const std::vector &input_legacy_enote_images, + const std::vector &input_sp_enote_images, + const std::vector &output_enotes, + const DiscretizedFee transaction_fee, + const SpTxSupplementV1 &tx_supplement, + rct::key &tx_proposal_prefix_out) +{ + // get key images from enote images + std::vector legacy_input_key_images; + std::vector sp_input_key_images; + legacy_input_key_images.reserve(input_legacy_enote_images.size()); + sp_input_key_images.reserve(input_sp_enote_images.size()); + + for (const LegacyEnoteImageV2 &legacy_enote_image : input_legacy_enote_images) + legacy_input_key_images.emplace_back(legacy_enote_image.key_image); + + for (const SpEnoteImageV1 &sp_enote_image : input_sp_enote_images) + sp_input_key_images.emplace_back(key_image_ref(sp_enote_image)); + + // get proposal prefix + make_tx_proposal_prefix_v1(tx_version, + legacy_input_key_images, + sp_input_key_images, + output_enotes, + transaction_fee, + tx_supplement, + tx_proposal_prefix_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_tx_proposal_prefix_v1(const tx_version_t &tx_version, + const std::vector &legacy_input_key_images, + const std::vector &sp_input_key_images, + const std::vector &output_proposals, + const DiscretizedFee transaction_fee, + const TxExtra &partial_memo, + rct::key &tx_proposal_prefix_out) +{ + // extract info from output proposals + std::vector output_enotes; + std::vector output_amounts; + std::vector output_amount_commitment_blinding_factors; + SpTxSupplementV1 tx_supplement; + + make_v1_outputs_v1(output_proposals, + output_enotes, + output_amounts, + output_amount_commitment_blinding_factors, + tx_supplement.output_enote_ephemeral_pubkeys); + + // collect full memo + finalize_tx_extra_v1(partial_memo, output_proposals, tx_supplement.tx_extra); + + // get proposal prefix + make_tx_proposal_prefix_v1(tx_version, + legacy_input_key_images, + sp_input_key_images, + output_enotes, + transaction_fee, + tx_supplement, + tx_proposal_prefix_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_tx_proposal_prefix_v1(const tx_version_t &tx_version, + const std::vector &legacy_inputs, + const std::vector &sp_partial_inputs, + const std::vector &output_proposals, + const DiscretizedFee transaction_fee, + const TxExtra &partial_memo, + rct::key &tx_proposal_prefix_out) +{ + // get key images from partial inputs + std::vector legacy_input_key_images; + std::vector sp_input_key_images; + legacy_input_key_images.reserve(legacy_inputs.size()); + sp_input_key_images.reserve(sp_partial_inputs.size()); + + for (const LegacyInputV1 &legacy_input : legacy_inputs) + legacy_input_key_images.emplace_back(legacy_input.input_image.key_image); + + for (const SpPartialInputV1 &sp_partial_input : sp_partial_inputs) + sp_input_key_images.emplace_back(key_image_ref(sp_partial_input.input_image)); + + // get proposal prefix + make_tx_proposal_prefix_v1(tx_version, + legacy_input_key_images, + sp_input_key_images, + output_proposals, + transaction_fee, + partial_memo, + tx_proposal_prefix_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_tx_proposal_prefix_v1(const tx_version_t &tx_version, + const std::vector &legacy_input_proposals, + const std::vector &sp_input_proposals, + const std::vector &output_proposals, + const DiscretizedFee transaction_fee, + const TxExtra &partial_memo, + rct::key &tx_proposal_prefix_out) +{ + // get key images from input proposals + std::vector legacy_input_key_images; + std::vector sp_input_key_images; + legacy_input_key_images.reserve(legacy_input_proposals.size()); + sp_input_key_images.reserve(sp_input_proposals.size()); + + for (const LegacyInputProposalV1 &legacy_input_proposal : legacy_input_proposals) + legacy_input_key_images.emplace_back(legacy_input_proposal.key_image); + + for (const SpInputProposalV1 &sp_input_proposal : sp_input_proposals) + sp_input_key_images.emplace_back(key_image_ref(sp_input_proposal)); + + // get proposal prefix + make_tx_proposal_prefix_v1(tx_version, + legacy_input_key_images, + sp_input_key_images, + output_proposals, + transaction_fee, + partial_memo, + tx_proposal_prefix_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_tx_proposal_prefix_v1(const SpTxSquashedV1 &tx, rct::key &tx_proposal_prefix_out) +{ + // get proposal prefix + make_tx_proposal_prefix_v1(tx_version_from(tx.tx_semantic_rules_version), + tx.legacy_input_images, + tx.sp_input_images, + tx.outputs, + tx.tx_fee, + tx.tx_supplement, + tx_proposal_prefix_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_tx_proofs_prefix_v1(const SpBalanceProofV1 &balance_proof, + const std::vector &legacy_ring_signatures, + const std::vector &sp_image_proofs, + const std::vector &sp_membership_proofs, + rct::key &tx_proofs_prefix_out) +{ + // H_32(balance proof, legacy ring signatures, seraphis image proofs, seraphis membership proofs) + SpFSTranscript transcript{ + config::HASH_KEY_SERAPHIS_TX_PROOFS_PREFIX_V1, + sp_balance_proof_v1_size_bytes(balance_proof) + + (legacy_ring_signatures.size() + ? legacy_ring_signatures.size() * legacy_ring_signature_v4_size_bytes(legacy_ring_signatures[0]) + : 0) + + sp_image_proofs.size() * sp_image_proof_v1_size_bytes() + + (sp_membership_proofs.size() + ? sp_membership_proofs.size() * sp_membership_proof_v1_size_bytes(sp_membership_proofs[0]) + : 0) + }; + transcript.append("balance_proof", balance_proof); + transcript.append("legacy_ring_signatures", legacy_ring_signatures); + transcript.append("sp_image_proofs", sp_image_proofs); + transcript.append("sp_membership_proofs", sp_membership_proofs); + + sp_hash_to_32(transcript.data(), transcript.size(), tx_proofs_prefix_out.bytes); +} +//------------------------------------------------------------------------------------------------------------------- +void make_tx_artifacts_merkle_root_v1(const rct::key &input_images_prefix, + const rct::key &tx_proofs_prefix, + rct::key &tx_artifacts_merkle_root_out) +{ + // H_32(input images prefix, tx proofs prefix) + SpFSTranscript transcript{ + config::HASH_KEY_SERAPHIS_TX_ARTIFACTS_MERKLE_ROOT_V1, + 2*sizeof(rct::key) + }; + transcript.append("input_images_prefix", input_images_prefix); + transcript.append("tx_proofs_prefix", tx_proofs_prefix); + + sp_hash_to_32(transcript.data(), transcript.size(), tx_artifacts_merkle_root_out.bytes); +} +//------------------------------------------------------------------------------------------------------------------- +void check_v1_coinbase_tx_proposal_semantics_v1(const SpCoinbaseTxProposalV1 &tx_proposal) +{ + // 1. extract output proposals from tx proposal (and check their semantics) + std::vector output_proposals; + get_coinbase_output_proposals_v1(tx_proposal, output_proposals); + + check_v1_coinbase_output_proposal_set_semantics_v1(output_proposals); + + // 2. extract outputs from the output proposals + std::vector output_enotes; + SpTxSupplementV1 tx_supplement; + + make_v1_coinbase_outputs_v1(output_proposals, output_enotes, tx_supplement.output_enote_ephemeral_pubkeys); + finalize_tx_extra_v1(tx_proposal.partial_memo, output_proposals, tx_supplement.tx_extra); + + // 3. outputs should be sorted and unique + CHECK_AND_ASSERT_THROW_MES(tools::is_sorted_and_unique(output_enotes, compare_Ko), + "Semantics check coinbase tx proposal v1: output onetime addresses are not sorted and unique."); + + // 4. onetime addresses should be canonical (sanity check so our tx outputs don't end up with duplicate key images) + for (const SpCoinbaseEnoteV1 &output_enote : output_enotes) + { + CHECK_AND_ASSERT_THROW_MES(onetime_address_is_canonical(output_enote.core), + "Semantics check coinbase tx proposal v1: an output onetime address is not in the prime subgroup."); + } + + // 5. check tx supplement (especially enote ephemeral pubkeys) + // note: there is no ephemeral pubkey optimization for coinbase txs + check_v1_tx_supplement_semantics_v1(tx_supplement, output_enotes.size()); + + // 6. check balance + CHECK_AND_ASSERT_THROW_MES(validate_sp_coinbase_amount_balance_v1(tx_proposal.block_reward, output_enotes), + "Semantics check coinbase tx proposal v1: outputs do not balance the block reward."); +} +//------------------------------------------------------------------------------------------------------------------- +void check_v1_tx_proposal_semantics_v1(const SpTxProposalV1 &tx_proposal, + const rct::key &legacy_spend_pubkey, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance) +{ + // 1. check inputs + std::vector in_amounts; + check_tx_proposal_semantics_inputs_v1(tx_proposal.legacy_input_proposals, + tx_proposal.sp_input_proposals, + legacy_spend_pubkey, + jamtis_spend_pubkey, + k_view_balance, + in_amounts); + + // 2. check self-send payment proposals + rct::key input_context; + make_standard_input_context_v1(tx_proposal.legacy_input_proposals, tx_proposal.sp_input_proposals, input_context); + + check_tx_proposal_semantics_selfsend_outputs_v1(tx_proposal.normal_payment_proposals.size(), + tx_proposal.selfsend_payment_proposals, + input_context, + jamtis_spend_pubkey, + k_view_balance); + + // 3. check output proposals + std::vector output_proposals; + get_output_proposals_v1(tx_proposal, k_view_balance, output_proposals); + + std::vector output_amounts; + check_tx_proposal_semantics_output_proposals_v1(output_proposals, tx_proposal.partial_memo, output_amounts); + + // 4. try to extract the fee value + rct::xmr_amount raw_transaction_fee; + CHECK_AND_ASSERT_THROW_MES(try_get_fee_value(tx_proposal.tx_fee, raw_transaction_fee), + "Semantics check tx proposal v1: could not extract fee value from discretized fee."); + + // 5. check balance: sum(input amnts) == sum(output amnts) + fee + CHECK_AND_ASSERT_THROW_MES(balance_check_in_out_amnts(in_amounts, output_amounts, raw_transaction_fee), + "Semantics check tx proposal v1: input/output amounts did not balance with desired fee."); +} +//------------------------------------------------------------------------------------------------------------------- +void make_v1_coinbase_tx_proposal_v1(const std::uint64_t block_height, + const rct::xmr_amount block_reward, + std::vector normal_payment_proposals, + std::vector additional_memo_elements, + SpCoinbaseTxProposalV1 &tx_proposal_out) +{ + // set fields + tx_proposal_out.block_height = block_height; + tx_proposal_out.block_reward = block_reward; + tx_proposal_out.normal_payment_proposals = std::move(normal_payment_proposals); + make_tx_extra(std::move(additional_memo_elements), tx_proposal_out.partial_memo); +} +//------------------------------------------------------------------------------------------------------------------- +void make_v1_tx_proposal_v1(std::vector legacy_input_proposals, + std::vector sp_input_proposals, + std::vector normal_payment_proposals, + std::vector selfsend_payment_proposals, + const DiscretizedFee discretized_transaction_fee, + std::vector additional_memo_elements, + SpTxProposalV1 &tx_proposal_out) +{ + // inputs should be sorted by key image + std::sort(legacy_input_proposals.begin(), + legacy_input_proposals.end(), + tools::compare_func(compare_KI)); + std::sort(sp_input_proposals.begin(), sp_input_proposals.end(), tools::compare_func(compare_KI)); + + // set fields + tx_proposal_out.legacy_input_proposals = std::move(legacy_input_proposals); + tx_proposal_out.sp_input_proposals = std::move(sp_input_proposals); + tx_proposal_out.normal_payment_proposals = std::move(normal_payment_proposals); + tx_proposal_out.selfsend_payment_proposals = std::move(selfsend_payment_proposals); + tx_proposal_out.tx_fee = discretized_transaction_fee; + make_tx_extra(std::move(additional_memo_elements), tx_proposal_out.partial_memo); +} +//------------------------------------------------------------------------------------------------------------------- +void make_v1_tx_proposal_v1(const std::vector &legacy_contextual_inputs, + const std::vector &sp_contextual_inputs, + std::vector normal_payment_proposals, + std::vector selfsend_payment_proposals, + const DiscretizedFee discretized_transaction_fee, + const TxExtra &partial_memo_for_tx, + SpTxProposalV1 &tx_proposal_out) +{ + // 1. legacy input proposals + std::vector legacy_input_proposals; + legacy_enote_records_to_input_proposals(legacy_contextual_inputs, legacy_input_proposals); + + // 2. seraphis input proposals + std::vector sp_input_proposals; + sp_enote_records_to_input_proposals(sp_contextual_inputs, sp_input_proposals); + + // 3. get memo elements + std::vector extra_field_elements; + CHECK_AND_ASSERT_THROW_MES(try_get_extra_field_elements(partial_memo_for_tx, extra_field_elements), + "make tx proposal for transfer (v1): unable to extract memo field elements for tx proposal."); + + // 4. assemble into tx proposal + make_v1_tx_proposal_v1(std::move(legacy_input_proposals), + std::move(sp_input_proposals), + std::move(normal_payment_proposals), + std::move(selfsend_payment_proposals), + discretized_transaction_fee, + std::move(extra_field_elements), + tx_proposal_out); +} +//------------------------------------------------------------------------------------------------------------------- +bool balance_check_in_out_amnts_v1(const rct::xmr_amount block_reward, + const std::vector &output_proposals) +{ + // output amounts + std::vector out_amounts; + out_amounts.reserve(output_proposals.size()); + + for (const SpCoinbaseOutputProposalV1 &output_proposal : output_proposals) + out_amounts.emplace_back(amount_ref(output_proposal)); + + // balance check + return balance_check_in_out_amnts({block_reward}, out_amounts, 0); +} +//------------------------------------------------------------------------------------------------------------------- +bool balance_check_in_out_amnts_v2(const std::vector &legacy_input_proposals, + const std::vector &sp_input_proposals, + const std::vector &output_proposals, + const DiscretizedFee discretized_transaction_fee) +{ + // input amounts + std::vector in_amounts; + in_amounts.reserve(legacy_input_proposals.size() + sp_input_proposals.size()); + + for (const LegacyInputProposalV1 &legacy_input_proposal : legacy_input_proposals) + in_amounts.emplace_back(amount_ref(legacy_input_proposal)); + + for (const SpInputProposalV1 &sp_input_proposal : sp_input_proposals) + in_amounts.emplace_back(amount_ref(sp_input_proposal)); + + // output amounts + std::vector out_amounts; + out_amounts.reserve(output_proposals.size()); + + for (const SpOutputProposalV1 &output_proposal : output_proposals) + out_amounts.emplace_back(amount_ref(output_proposal)); + + // fee + rct::xmr_amount raw_transaction_fee; + CHECK_AND_ASSERT_THROW_MES(try_get_fee_value(discretized_transaction_fee, raw_transaction_fee), + "balance check in out amnts v1: unable to extract transaction fee from discretized fee representation."); + + // balance check + return balance_check_in_out_amnts(in_amounts, out_amounts, raw_transaction_fee); +} +//------------------------------------------------------------------------------------------------------------------- +void make_v1_balance_proof_v1(const std::vector &legacy_input_amounts, + const std::vector &sp_input_amounts, + const std::vector &output_amounts, + const rct::xmr_amount transaction_fee, + const std::vector &legacy_input_image_amount_commitment_blinding_factors, + const std::vector &sp_input_image_amount_commitment_blinding_factors, + const std::vector &output_amount_commitment_blinding_factors, + SpBalanceProofV1 &balance_proof_out) +{ + // for squashed enote model + + // 1. check balance + std::vector all_in_amounts{legacy_input_amounts}; + all_in_amounts.insert(all_in_amounts.end(), sp_input_amounts.begin(), sp_input_amounts.end()); + + CHECK_AND_ASSERT_THROW_MES(balance_check_in_out_amnts(all_in_amounts, output_amounts, transaction_fee), + "make v1 balance proof (v1): amounts don't balance."); + + // 2. combine seraphis inputs and outputs for range proof (legacy input masked commitments are not range proofed) + std::vector range_proof_amounts{sp_input_amounts}; + range_proof_amounts.insert(range_proof_amounts.end(), output_amounts.begin(), output_amounts.end()); + + std::vector range_proof_blinding_factors{sp_input_image_amount_commitment_blinding_factors}; + range_proof_blinding_factors.insert(range_proof_blinding_factors.end(), + output_amount_commitment_blinding_factors.begin(), + output_amount_commitment_blinding_factors.end()); + + // 3. make range proofs + BulletproofPlus2 range_proofs; + + rct::keyV range_proof_amount_commitment_blinding_factors; + auto vec_wiper = convert_skv_to_rctv(range_proof_blinding_factors, range_proof_amount_commitment_blinding_factors); + make_bpp2_rangeproofs(range_proof_amounts, range_proof_amount_commitment_blinding_factors, range_proofs); + + balance_proof_out.bpp2_proof = std::move(range_proofs); + + // 4. set the remainder blinding factor + // blinding_factor = sum(legacy input blinding factors) + sum(sp input blinding factors) - sum(output blinding factors) + std::vector collected_input_blinding_factors{sp_input_image_amount_commitment_blinding_factors}; + collected_input_blinding_factors.insert(collected_input_blinding_factors.end(), + legacy_input_image_amount_commitment_blinding_factors.begin(), + legacy_input_image_amount_commitment_blinding_factors.end()); + + crypto::secret_key remainder_blinding_factor; + subtract_secret_key_vectors(collected_input_blinding_factors, + output_amount_commitment_blinding_factors, + remainder_blinding_factor); + + balance_proof_out.remainder_blinding_factor = rct::sk2rct(remainder_blinding_factor); +} +//------------------------------------------------------------------------------------------------------------------- +void check_v1_partial_tx_semantics_v1(const SpPartialTxV1 &partial_tx, + const SpTxSquashedV1::SemanticRulesVersion semantic_rules_version) +{ + // 1. get parameters for making mock seraphis ref sets (use minimum parameters for efficiency when possible) + const SemanticConfigSpRefSetV1 ref_set_config{semantic_config_sp_ref_sets_v1(semantic_rules_version)}; + const SpBinnedReferenceSetConfigV1 bin_config{ + .bin_radius = static_cast(ref_set_config.bin_radius_min), + .num_bin_members = static_cast(ref_set_config.num_bin_members_min), + }; + + // 2. make mock membership proof ref sets + std::vector sp_membership_proof_preps; + std::unordered_map sp_reference_set_proof_elements; + + prepare_sp_membership_proof_preps_for_tx_simulation_v1(partial_tx.sp_input_enotes, + partial_tx.sp_address_masks, + partial_tx.sp_commitment_masks, + ref_set_config.decomp_n_min, + ref_set_config.decomp_m_min, + bin_config, + sp_membership_proof_preps, + sp_reference_set_proof_elements); + + // 3. make the mock seraphis membership proofs + std::vector sp_membership_proofs; + make_v1_membership_proofs_v1(std::move(sp_membership_proof_preps), sp_membership_proofs); + + // 4. collect legacy ring signature ring members for mock validation context + std::unordered_map legacy_reference_set_proof_elements; + + collect_legacy_ring_signature_ring_members(partial_tx.legacy_ring_signatures, + partial_tx.legacy_ring_signature_rings, + legacy_reference_set_proof_elements); + + // 5. make tx (use raw constructor instead of partial tx constructor which would call this function in an infinite + // recursion) + SpTxSquashedV1 test_tx; + make_seraphis_tx_squashed_v1(semantic_rules_version, + partial_tx.legacy_input_images, + partial_tx.sp_input_images, + partial_tx.outputs, + partial_tx.balance_proof, + partial_tx.legacy_ring_signatures, + partial_tx.sp_image_proofs, + std::move(sp_membership_proofs), + partial_tx.tx_supplement, + partial_tx.tx_fee, + test_tx); + + // 6. validate tx + const TxValidationContextSimple tx_validation_context{ + legacy_reference_set_proof_elements, + sp_reference_set_proof_elements + }; + + CHECK_AND_ASSERT_THROW_MES(validate_tx(test_tx, tx_validation_context), + "v1 partial tx semantics check (v1): test transaction was invalid using requested semantics rules version!"); +} +//------------------------------------------------------------------------------------------------------------------- +void make_v1_partial_tx_v1(std::vector legacy_inputs, + std::vector sp_partial_inputs, + std::vector output_proposals, + const DiscretizedFee discretized_transaction_fee, + const TxExtra &partial_memo, + const tx_version_t &tx_version, + SpPartialTxV1 &partial_tx_out) +{ + /// preparation and checks + partial_tx_out = SpPartialTxV1{}; + + // 1. sort the inputs by key image + std::sort(legacy_inputs.begin(), legacy_inputs.end(), tools::compare_func(compare_KI)); + std::sort(sp_partial_inputs.begin(), sp_partial_inputs.end(), tools::compare_func(compare_KI)); + + // 2. sort the outputs by onetime address + std::sort(output_proposals.begin(), output_proposals.end(), tools::compare_func(compare_Ko)); + + // 3. semantics checks for inputs and outputs + for (const LegacyInputV1 &legacy_input : legacy_inputs) + check_v1_legacy_input_semantics_v1(legacy_input); + + for (const SpPartialInputV1 &partial_input : sp_partial_inputs) + check_v1_partial_input_semantics_v1(partial_input); + + check_v1_output_proposal_set_semantics_v1(output_proposals); //do this after sorting the proposals + + // 4. extract info from output proposals + std::vector output_enotes; + std::vector output_amounts; + std::vector output_amount_commitment_blinding_factors; + SpTxSupplementV1 tx_supplement; + + make_v1_outputs_v1(output_proposals, + output_enotes, + output_amounts, + output_amount_commitment_blinding_factors, + tx_supplement.output_enote_ephemeral_pubkeys); + + // 5. collect full memo + finalize_tx_extra_v1(partial_memo, output_proposals, tx_supplement.tx_extra); + + // 6. check: inputs and proposal must have consistent proposal prefixes + rct::key tx_proposal_prefix; + make_tx_proposal_prefix_v1(tx_version, + legacy_inputs, + sp_partial_inputs, + output_proposals, + discretized_transaction_fee, + partial_memo, + tx_proposal_prefix); + + for (const LegacyInputV1 &legacy_input : legacy_inputs) + { + CHECK_AND_ASSERT_THROW_MES(legacy_input.tx_proposal_prefix == tx_proposal_prefix, + "making partial tx v1: a legacy input's proposal prefix is invalid/inconsistent."); + } + + for (const SpPartialInputV1 &partial_input : sp_partial_inputs) + { + CHECK_AND_ASSERT_THROW_MES(partial_input.tx_proposal_prefix == tx_proposal_prefix, + "making partial tx v1: a seraphis partial input's proposal prefix is invalid/inconsistent."); + } + + + /// balance proof + + // 1. get input amounts and image amount commitment blinding factors + std::vector legacy_input_amounts; + std::vector legacy_input_image_amount_commitment_blinding_factors; + get_legacy_input_commitment_factors_v1(legacy_inputs, + legacy_input_amounts, + legacy_input_image_amount_commitment_blinding_factors); + + std::vector sp_input_amounts; + std::vector sp_input_image_amount_commitment_blinding_factors; + get_input_commitment_factors_v1(sp_partial_inputs, + sp_input_amounts, + sp_input_image_amount_commitment_blinding_factors); + + // 2. extract the fee + rct::xmr_amount raw_transaction_fee; + CHECK_AND_ASSERT_THROW_MES(try_get_fee_value(discretized_transaction_fee, raw_transaction_fee), + "making partial tx v1: could not extract a fee value from the discretized fee."); + + // 3. make balance proof + make_v1_balance_proof_v1(legacy_input_amounts, + sp_input_amounts, + output_amounts, + raw_transaction_fee, + legacy_input_image_amount_commitment_blinding_factors, + sp_input_image_amount_commitment_blinding_factors, + output_amount_commitment_blinding_factors, + partial_tx_out.balance_proof); + + + /// copy misc tx pieces + + // 1. gather legacy tx input parts + partial_tx_out.legacy_input_images.reserve(legacy_inputs.size()); + partial_tx_out.legacy_ring_signatures.reserve(legacy_inputs.size()); + partial_tx_out.legacy_ring_signature_rings.reserve(legacy_inputs.size()); + + for (LegacyInputV1 &legacy_input : legacy_inputs) + { + partial_tx_out.legacy_input_images.emplace_back(legacy_input.input_image); + partial_tx_out.legacy_ring_signatures.emplace_back(std::move(legacy_input.ring_signature)); + partial_tx_out.legacy_ring_signature_rings.emplace_back(std::move(legacy_input.ring_members)); + } + + // 2. gather seraphis tx input parts + partial_tx_out.sp_input_images.reserve(sp_partial_inputs.size()); + partial_tx_out.sp_image_proofs.reserve(sp_partial_inputs.size()); + partial_tx_out.sp_input_enotes.reserve(sp_partial_inputs.size()); + partial_tx_out.sp_address_masks.reserve(sp_partial_inputs.size()); + partial_tx_out.sp_commitment_masks.reserve(sp_partial_inputs.size()); + + for (SpPartialInputV1 &partial_input : sp_partial_inputs) + { + partial_tx_out.sp_input_images.emplace_back(partial_input.input_image); + partial_tx_out.sp_image_proofs.emplace_back(std::move(partial_input.image_proof)); + partial_tx_out.sp_input_enotes.emplace_back(partial_input.input_enote_core); + partial_tx_out.sp_address_masks.emplace_back(partial_input.address_mask); + partial_tx_out.sp_commitment_masks.emplace_back(partial_input.commitment_mask); + } + + // 3. gather tx output parts + partial_tx_out.outputs = std::move(output_enotes); + partial_tx_out.tx_fee = discretized_transaction_fee; + partial_tx_out.tx_supplement = std::move(tx_supplement); +} +//------------------------------------------------------------------------------------------------------------------- +void make_v1_partial_tx_v1(const SpTxProposalV1 &tx_proposal, + std::vector legacy_inputs, + std::vector sp_partial_inputs, + const tx_version_t &tx_version, + const rct::key &legacy_spend_pubkey, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + SpPartialTxV1 &partial_tx_out) +{ + // 1. validate tx proposal + check_v1_tx_proposal_semantics_v1(tx_proposal, legacy_spend_pubkey, jamtis_spend_pubkey, k_view_balance); + + // 2. sort the inputs by key image + std::sort(legacy_inputs.begin(), legacy_inputs.end(), tools::compare_func(compare_KI)); + std::sort(sp_partial_inputs.begin(), sp_partial_inputs.end(), tools::compare_func(compare_KI)); + + // 3. legacy inputs must line up with legacy input proposals in the tx proposal + CHECK_AND_ASSERT_THROW_MES(legacy_inputs.size() == tx_proposal.legacy_input_proposals.size(), + "making partial tx v1: number of legacy inputs doesn't match number of legacy input proposals."); + + for (std::size_t input_index{0}; input_index < legacy_inputs.size(); ++input_index) + { + CHECK_AND_ASSERT_THROW_MES(same_key_image(legacy_inputs[input_index], + tx_proposal.legacy_input_proposals[input_index]), + "making partial tx v1: legacy inputs and input proposals don't line up (inconsistent key images)."); + } + + // 4. seraphis partial inputs must line up with seraphis input proposals in the tx proposal + CHECK_AND_ASSERT_THROW_MES(sp_partial_inputs.size() == tx_proposal.sp_input_proposals.size(), + "making partial tx v1: number of seraphis partial inputs doesn't match number of seraphis input proposals."); + + for (std::size_t input_index{0}; input_index < sp_partial_inputs.size(); ++input_index) + { + CHECK_AND_ASSERT_THROW_MES(same_key_image(sp_partial_inputs[input_index], + tx_proposal.sp_input_proposals[input_index]), + "making partial tx v1: seraphis partial inputs and input proposals don't line up (inconsistent key " + "images)."); + } + + // 5. extract output proposals from tx proposal + std::vector output_proposals; + get_output_proposals_v1(tx_proposal, k_view_balance, output_proposals); + + // 6. construct partial tx + make_v1_partial_tx_v1(std::move(legacy_inputs), + std::move(sp_partial_inputs), + std::move(output_proposals), + tx_proposal.tx_fee, + tx_proposal.partial_memo, + tx_version, + partial_tx_out); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace sp diff --git a/src/seraphis_main/tx_builders_mixed.h b/src/seraphis_main/tx_builders_mixed.h new file mode 100644 index 0000000000..5043534616 --- /dev/null +++ b/src/seraphis_main/tx_builders_mixed.h @@ -0,0 +1,266 @@ +// Copyright (c) 2022, 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 tx-builder/component-builder implementations (those related to both inputs and outputs). + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/discretized_fee.h" +#include "tx_builder_types.h" +#include "tx_builder_types_legacy.h" +#include "tx_component_types.h" +#include "tx_component_types_legacy.h" +#include "tx_input_selection.h" +#include "txtype_base.h" +#include "txtype_squashed_v1.h" + +//third party headers + +//standard headers +#include + +//forward declarations + + +namespace sp +{ + +/** +* brief: make_tx_proposal_prefix_v1 - hash representing a tx proposal +* - H_32(tx version, legacy input key images, seraphis input key images, output enotes, fee, tx supplement) +* param: tx_version - +* param: legacy_input_key_images - +* param: sp_input_key_images - +* param: output_enotes - +* param: transaction_fee - +* param: tx_supplement - +* outparam: tx_proposal_prefix_out - hash representing a tx proposal +*/ +void make_tx_proposal_prefix_v1(const tx_version_t &tx_version, + const std::vector &legacy_input_key_images, + const std::vector &sp_input_key_images, + const std::vector &output_enotes, + const rct::xmr_amount transaction_fee, + const SpTxSupplementV1 &tx_supplement, + rct::key &tx_proposal_prefix_out); +void make_tx_proposal_prefix_v1(const tx_version_t &tx_version, + const std::vector &legacy_input_key_images, + const std::vector &sp_input_key_images, + const std::vector &output_enotes, + const DiscretizedFee transaction_fee, + const SpTxSupplementV1 &tx_supplement, + rct::key &tx_proposal_prefix_out); +void make_tx_proposal_prefix_v1(const tx_version_t &tx_version, + const std::vector &input_legacy_enote_images, + const std::vector &input_sp_enote_images, + const std::vector &output_enotes, + const DiscretizedFee transaction_fee, + const SpTxSupplementV1 &tx_supplement, + rct::key &tx_proposal_prefix_out); +void make_tx_proposal_prefix_v1(const tx_version_t &tx_version, + const std::vector &legacy_input_key_images, + const std::vector &sp_input_key_images, + const std::vector &output_proposals, + const DiscretizedFee transaction_fee, + const TxExtra &partial_memo, + rct::key &tx_proposal_prefix_out); +void make_tx_proposal_prefix_v1(const tx_version_t &tx_version, + const std::vector &legacy_inputs, + const std::vector &sp_partial_inputs, + const std::vector &output_proposals, + const DiscretizedFee transaction_fee, + const TxExtra &partial_memo, + rct::key &tx_proposal_prefix_out); +void make_tx_proposal_prefix_v1(const tx_version_t &tx_version, + const std::vector &legacy_input_proposals, + const std::vector &sp_input_proposals, + const std::vector &output_proposals, + const DiscretizedFee transaction_fee, + const TxExtra &partial_memo, + rct::key &tx_proposal_prefix_out); +void make_tx_proposal_prefix_v1(const SpTxSquashedV1 &tx, rct::key &tx_proposal_prefix_out); +/** +* brief: make_tx_proofs_prefix_v1 - hash of all proofs in a tx (e.g. for use in making a tx id) +* - H_32(balance proof, legacy ring signatures, seraphis image proofs, seraphis membership proofs) +* param: balance_proof - +* param: legacy_ring_signatures - +* param: sp_image_proofs - +* param: sp_membership_proofs - +* outparam: tx_proofs_prefix_out - +*/ +void make_tx_proofs_prefix_v1(const SpBalanceProofV1 &balance_proof, + const std::vector &legacy_ring_signatures, + const std::vector &sp_image_proofs, + const std::vector &sp_membership_proofs, + rct::key &tx_proofs_prefix_out); +/** +* brief: make_tx_artifacts_merkle_root_v1 - merkle root of transaction artifacts (input images and proofs) +* - H_32(input images prefix, tx proofs prefix) +* param: input_images_prefix - +* param: tx_proofs_prefix - +* outparam: tx_artifacts_merkle_root_out - +*/ +void make_tx_artifacts_merkle_root_v1(const rct::key &input_images_prefix, + const rct::key &tx_proofs_prefix, + rct::key &tx_artifacts_merkle_root_out); +/** +* brief: check_v1_coinbase_tx_proposal_semantics_v1 - check semantics of a coinbase tx proposal +* - throws if a check fails +* - NOTE: it is permitted for there to be no output coinbase enotes (i.e. for unit testing/mockups) +* param: tx_proposal - +*/ +void check_v1_coinbase_tx_proposal_semantics_v1(const SpCoinbaseTxProposalV1 &tx_proposal); +/** +* brief: check_v1_tx_proposal_semantics_v1 - check semantics of a tx proposal +* - throws if a check fails +* param: tx_proposal - +* param: legacy_spend_pubkey - +* param: jamtis_spend_pubkey - +* param: k_view_balance - +*/ +void check_v1_tx_proposal_semantics_v1(const SpTxProposalV1 &tx_proposal, + const rct::key &legacy_spend_pubkey, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance); +/** +* brief: make_v1_coinbase_tx_proposal_v1 - make v1 coinbase tx proposal +* param: block_height - +* param: block_reward - +* param: normal_payment_proposals - +* param: additional_memo_elements - +* outparam: tx_proposal_out - +*/ +void make_v1_coinbase_tx_proposal_v1(const std::uint64_t block_height, + const rct::xmr_amount block_reward, + std::vector normal_payment_proposals, + std::vector additional_memo_elements, + SpCoinbaseTxProposalV1 &tx_proposal_out); +/** +* brief: make_v1_tx_proposal_v1 - make v1 tx proposal +* param: legacy_input_proposals - +* param: sp_input_proposals - +* param: normal_payment_proposals - +* param: selfsend_payment_proposals - +* param: discretized_transaction_fee - +* param: additional_memo_elements - +* outparam: tx_proposal_out - +*/ +void make_v1_tx_proposal_v1(std::vector legacy_input_proposals, + std::vector sp_input_proposals, + std::vector normal_payment_proposals, + std::vector selfsend_payment_proposals, + const DiscretizedFee discretized_transaction_fee, + std::vector additional_memo_elements, + SpTxProposalV1 &tx_proposal_out); +void make_v1_tx_proposal_v1(const std::vector &legacy_contextual_inputs, + const std::vector &sp_contextual_inputs, + std::vector normal_payment_proposals, + std::vector selfsend_payment_proposals, + const DiscretizedFee discretized_transaction_fee, + const TxExtra &partial_memo_for_tx, + SpTxProposalV1 &tx_proposal_out); +/** +* brief: balance_check_in_out_amnts_v1 - verify that the block reward equals output amounts (coinbase txs) +* param: block_reward - +* param: output_proposals - +* return: true if amounts balance between block reward and outputs +*/ +bool balance_check_in_out_amnts_v1(const rct::xmr_amount block_reward, + const std::vector &output_proposals); +/** +* brief: balance_check_in_out_amnts_v2 - verify that input amounts equal output amounts + fee (normal txs) +* param: legacy_input_proposals - +* param: sp_input_proposals - +* param: output_proposals - +* param: discretized_transaction_fee - +* return: true if amounts balance between inputs and outputs (plus fee) +*/ +bool balance_check_in_out_amnts_v2(const std::vector &legacy_input_proposals, + const std::vector &sp_input_proposals, + const std::vector &output_proposals, + const DiscretizedFee discretized_transaction_fee); +/** +* brief: make_v1_balance_proof_v1 - make v1 tx balance proof (BP+ for range proofs; balance check is sum-to-zero) +* - range proofs: for seraphis input image amount commitments and output commitments (squashed enote model) +* param: legacy_input_amounts - +* param: sp_input_amounts - +* param: output_amounts - +* param: transaction_fee - +* param: legacy_input_image_amount_commitment_blinding_factors - +* param: sp_input_image_amount_commitment_blinding_factors - +* param: output_amount_commitment_blinding_factors - +* outparam: balance_proof_out - +*/ +void make_v1_balance_proof_v1(const std::vector &legacy_input_amounts, + const std::vector &sp_input_amounts, + const std::vector &output_amounts, + const rct::xmr_amount transaction_fee, + const std::vector &legacy_input_image_amount_commitment_blinding_factors, + const std::vector &sp_input_image_amount_commitment_blinding_factors, + const std::vector &output_amount_commitment_blinding_factors, + SpBalanceProofV1 &balance_proof_out); +/** +* brief: check_v1_partial_tx_semantics_v1 - check the semantics of a partial tx against SpTxSquashedV1 validation rules +* - throws if a check fails +* - makes a mock tx and validates it using the specified SpTxSquashedV1 semantics rules version +* param: partial_tx - +* param: semantic_rules_version - +*/ +void check_v1_partial_tx_semantics_v1(const SpPartialTxV1 &partial_tx, + const SpTxSquashedV1::SemanticRulesVersion semantic_rules_version); +/** +* brief: make_v1_partial_tx_v1 - make v1 partial transaction (everything ready for a full tx except seraphis membership +* proofs) +* param: legacy_inputs - +* param: sp_partial_inputs - +* param: output_proposals - +* param: discretized_transaction_fee - +* param: partial_memo - +* param: tx_version - +* outparam: partial_tx_out - +*/ +void make_v1_partial_tx_v1(std::vector legacy_inputs, + std::vector sp_partial_inputs, + std::vector output_proposals, + const DiscretizedFee discretized_transaction_fee, + const TxExtra &partial_memo, + const tx_version_t &tx_version, + SpPartialTxV1 &partial_tx_out); +void make_v1_partial_tx_v1(const SpTxProposalV1 &tx_proposal, + std::vector legacy_inputs, + std::vector sp_partial_inputs, + const tx_version_t &tx_version, + const rct::key &legacy_spend_pubkey, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + SpPartialTxV1 &partial_tx_out); + +} //namespace sp diff --git a/src/seraphis_main/tx_builders_multisig.cpp b/src/seraphis_main/tx_builders_multisig.cpp new file mode 100644 index 0000000000..150510a136 --- /dev/null +++ b/src/seraphis_main/tx_builders_multisig.cpp @@ -0,0 +1,1623 @@ +// Copyright (c) 2022, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//paired header +#include "tx_builders_multisig.h" + +//local headers +#include "common/container_helpers.h" +#include "contextual_enote_record_utils.h" +#include "crypto/crypto.h" +#include "crypto/x25519.h" +#include "crypto/generators.h" +#include "cryptonote_basic/subaddress_index.h" +#include "cryptonote_config.h" +#include "device/device.hpp" +#include "enote_record_types.h" +#include "enote_record_utils.h" +#include "misc_language.h" +#include "misc_log_ex.h" +#include "multisig/multisig_clsag.h" +#include "multisig/multisig_nonce_cache.h" +#include "multisig/multisig_partial_sig_makers.h" +#include "multisig/multisig_signer_set_filter.h" +#include "multisig/multisig_signing_helper_types.h" +#include "multisig/multisig_signing_helper_utils.h" +#include "multisig/multisig_sp_composition_proof.h" +#include "ringct/rctOps.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/discretized_fee.h" +#include "seraphis_core/jamtis_address_utils.h" +#include "seraphis_core/jamtis_core_utils.h" +#include "seraphis_core/jamtis_enote_utils.h" +#include "seraphis_core/legacy_core_utils.h" +#include "seraphis_core/sp_core_enote_utils.h" +#include "seraphis_crypto/sp_crypto_utils.h" +#include "tx_builder_types.h" +#include "tx_builder_types_legacy.h" +#include "tx_builder_types_multisig.h" +#include "tx_builders_inputs.h" +#include "tx_builders_legacy_inputs.h" +#include "tx_builders_mixed.h" +#include "tx_builders_outputs.h" +#include "tx_component_types.h" +#include "txtype_base.h" + +//third party headers + +//standard headers +#include +#include +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis" + +namespace sp +{ +//------------------------------------------------------------------------------------------------------------------- +// legacy proof contexts: [ legacy Ko : legacy input message ] +//------------------------------------------------------------------------------------------------------------------- +static void get_legacy_proof_contexts_v1(const rct::key &tx_proposal_prefix, + const std::vector &legacy_multisig_input_proposals, + std::unordered_map &proof_contexts_out) //[ proof key : proof message ] +{ + proof_contexts_out.clear(); + proof_contexts_out.reserve(legacy_multisig_input_proposals.size()); + + for (const LegacyMultisigInputProposalV1 &input_proposal : legacy_multisig_input_proposals) + { + make_tx_legacy_ring_signature_message_v1(tx_proposal_prefix, + input_proposal.reference_set, + proof_contexts_out[onetime_address_ref(input_proposal.enote)]); + } +} +//------------------------------------------------------------------------------------------------------------------- +// seraphis proof contexts: [ seraphis K" : tx proposal prefix ] +//------------------------------------------------------------------------------------------------------------------- +static void get_seraphis_proof_contexts_v1(const rct::key &tx_proposal_prefix, + const std::vector &sp_input_proposals, + std::unordered_map &proof_contexts_out) //[ proof key : proof message ] +{ + proof_contexts_out.clear(); + proof_contexts_out.reserve(sp_input_proposals.size()); + SpEnoteImageV1 enote_image_temp; + + for (const SpInputProposalV1 &input_proposal : sp_input_proposals) + { + get_enote_image_v1(input_proposal, enote_image_temp); + proof_contexts_out[masked_address_ref(enote_image_temp)] = tx_proposal_prefix; + } +} +//------------------------------------------------------------------------------------------------------------------- +// legacy proof base points: [ legacy Ko : {G, Hp(legacy Ko)} ] +//------------------------------------------------------------------------------------------------------------------- +static void get_legacy_proof_base_keys_v1(const std::vector &legacy_input_proposals, + std::unordered_map &legacy_proof_key_base_points_out) +{ + legacy_proof_key_base_points_out.clear(); + legacy_proof_key_base_points_out.reserve(legacy_input_proposals.size()); + crypto::key_image KI_base_temp; + + for (const LegacyInputProposalV1 &input_proposal : legacy_input_proposals) + { + // Hp(Ko) + crypto::generate_key_image(rct::rct2pk(input_proposal.onetime_address), rct::rct2sk(rct::I), KI_base_temp); + + // [ Ko : {G, Hp(Ko)} ] + legacy_proof_key_base_points_out[input_proposal.onetime_address] = + { + rct::G, + rct::ki2rct(KI_base_temp) + }; + } +} +//------------------------------------------------------------------------------------------------------------------- +// seraphis proof keys: [ seraphis K" : {U} ] +//------------------------------------------------------------------------------------------------------------------- +static void get_sp_proof_base_keys_v1(const std::vector &sp_input_proposals, + std::unordered_map &sp_proof_key_base_points_out) +{ + sp_proof_key_base_points_out.clear(); + sp_proof_key_base_points_out.reserve(sp_input_proposals.size()); + SpEnoteImageV1 enote_image_temp; + + for (const SpInputProposalV1 &input_proposal : sp_input_proposals) + { + get_enote_image_v1(input_proposal, enote_image_temp); + sp_proof_key_base_points_out[masked_address_ref(enote_image_temp)] = {rct::pk2rct(crypto::get_U())}; + } +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void check_semantics_legacy_multisig_input_material_v1(const rct::key &tx_proposal_prefix, + const LegacyMultisigInputProposalV1 &multisig_input_proposal, + const multisig::CLSAGMultisigProposal &input_proof_proposal) +{ + // 1. get legacy ring signature message + rct::key message; + make_tx_legacy_ring_signature_message_v1(tx_proposal_prefix, multisig_input_proposal.reference_set, message); + + // 2. input proof proposal message should equal the expected message + CHECK_AND_ASSERT_THROW_MES(input_proof_proposal.message == message, + "semantics check legacy multisig input material v1: legacy input proof proposal does not match the tx proposal " + "(unknown proof message)."); + + // 3. input proof proposal should match with the multisig input proposal + CHECK_AND_ASSERT_THROW_MES(matches_with(multisig_input_proposal, input_proof_proposal), + "semantics check legacy multisig input material v1: legacy multisig input proposal does not match input proof " + "proposal."); + + // 4. input proof proposal should be well formed + CHECK_AND_ASSERT_THROW_MES(input_proof_proposal.ring_members.size() == + input_proof_proposal.decoy_responses.size(), + "semantics check legacy multisig input material v1: legacy input proof proposal has invalid number of decoy " + "responses."); + CHECK_AND_ASSERT_THROW_MES(input_proof_proposal.l < input_proof_proposal.ring_members.size(), + "semantics check legacy multisig input material v1: legacy input proof proposal has out-of-range real index."); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void check_semantics_sp_multisig_input_material_v1(const rct::key &tx_proposal_prefix, + const SpInputProposalV1 &input_proposal, + const multisig::SpCompositionProofMultisigProposal &input_proof_proposal) +{ + // 1. input proof proposal messages should all equal the specified tx proposal prefix + CHECK_AND_ASSERT_THROW_MES(input_proof_proposal.message == tx_proposal_prefix, + "semantics check seraphis multisig input material v1: sp input proof proposal does not match the tx proposal " + "(different proposal prefix)."); + + // 2. input proof proposal proof key should match with the input proposal + SpEnoteImageV1 sp_enote_image; + get_enote_image_v1(input_proposal, sp_enote_image); + + CHECK_AND_ASSERT_THROW_MES(input_proof_proposal.K == masked_address_ref(sp_enote_image), + "semantics check seraphis multisig input material v1: sp input proof proposal does not match input proposal " + "(different proof keys)."); + + // 3. input proof proposal key image should match with the input proposal + CHECK_AND_ASSERT_THROW_MES(input_proof_proposal.KI == key_image_ref(sp_enote_image), + "semantics check seraphis multisig input material v1: sp input proof proposal does not match input proposal " + "(different key images)."); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void replace_legacy_input_proposal_destination_for_tx_simulation_v1( + const LegacyMultisigInputProposalV1 &multisig_input_proposal, + const multisig::CLSAGMultisigProposal &input_proof_proposal, + const crypto::secret_key &legacy_spend_privkey_mock, + LegacyInputProposalV1 &input_proposal_inout, + LegacyRingSignaturePrepV1 &legacy_ring_signature_prep_out) +{ + // 1. new onetime address privkey: k_view_stuff + k^s_mock + crypto::secret_key legacy_onetime_address_privkey; + sc_add(to_bytes(legacy_onetime_address_privkey), + to_bytes(input_proposal_inout.enote_view_extension), + to_bytes(legacy_spend_privkey_mock)); + + // 2. replace the onetime address + input_proposal_inout.onetime_address = rct::scalarmultBase(rct::sk2rct(legacy_onetime_address_privkey)); + + // 3. update the key image for the new onetime address + make_legacy_key_image(input_proposal_inout.enote_view_extension, + legacy_spend_privkey_mock, + input_proposal_inout.onetime_address, + hw::get_device("default"), + input_proposal_inout.key_image); + + // 4. make a legacy ring signature prep for this input + legacy_ring_signature_prep_out = + LegacyRingSignaturePrepV1{ + .tx_proposal_prefix = rct::I, //set this later + .reference_set = multisig_input_proposal.reference_set, + .referenced_enotes = input_proof_proposal.ring_members, + .real_reference_index = input_proof_proposal.l, + .reference_image = + LegacyEnoteImageV2{ + .masked_commitment = input_proof_proposal.masked_C, + .key_image = input_proposal_inout.key_image + }, + .reference_view_privkey = input_proposal_inout.enote_view_extension, + .reference_commitment_mask = input_proposal_inout.commitment_mask + }; + + // 4. replace the real-spend enote's onetime address in the reference set + legacy_ring_signature_prep_out + .referenced_enotes.at(legacy_ring_signature_prep_out.real_reference_index) + .dest = input_proposal_inout.onetime_address; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void replace_legacy_input_proposal_destinations_for_tx_simulation_v1( + const std::vector &multisig_input_proposals, + const std::vector &input_proof_proposals, + const crypto::secret_key &legacy_spend_privkey_mock, + std::vector &input_proposals_inout, + std::vector &legacy_ring_signature_preps_out) +{ + const std::size_t num_inputs{multisig_input_proposals.size()}; + CHECK_AND_ASSERT_THROW_MES(input_proof_proposals.size() == num_inputs, + "replace legacy input proposal destinations for tx sim v1: proof proposals size mismatch."); + CHECK_AND_ASSERT_THROW_MES(input_proposals_inout.size() == num_inputs, + "replace legacy input proposal destinations for tx sim v1: initial proposals size mismatch."); + + // 1. update the input proposals and make ring signature preps from the updated context + legacy_ring_signature_preps_out.clear(); + legacy_ring_signature_preps_out.reserve(num_inputs); + + for (std::size_t legacy_input_index{0}; legacy_input_index < num_inputs; ++legacy_input_index) + { + replace_legacy_input_proposal_destination_for_tx_simulation_v1(multisig_input_proposals[legacy_input_index], + input_proof_proposals[legacy_input_index], + legacy_spend_privkey_mock, + input_proposals_inout[legacy_input_index], + tools::add_element(legacy_ring_signature_preps_out)); + } + + // 2. repair legacy ring signature preps that may reference other preps' real enotes + // note: assume reference sets contain unique references and are all the same size + for (const LegacyRingSignaturePrepV1 &reference_prep : legacy_ring_signature_preps_out) + { + for (LegacyRingSignaturePrepV1 &prep_to_repair : legacy_ring_signature_preps_out) + { + // a. see if the reference prep's real reference is a decoy in this prep's reference set + auto ref_set_it = + std::find(prep_to_repair.reference_set.begin(), + prep_to_repair.reference_set.end(), + reference_prep.reference_set.at(reference_prep.real_reference_index)); + + // b. if not, skip it + if (ref_set_it == prep_to_repair.reference_set.end()) + continue; + + // c. otherwise, update the decoy's onetime address + prep_to_repair + .referenced_enotes + .at(std::distance(prep_to_repair.reference_set.begin(), ref_set_it)) + .dest = + reference_prep + .referenced_enotes + .at(reference_prep.real_reference_index) + .dest; + } + } + + // 3. make sure the updated input proposals are sorted + std::sort(input_proposals_inout.begin(), + input_proposals_inout.end(), + tools::compare_func(compare_KI)); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void replace_sp_input_proposal_destination_for_tx_simulation_v1(const rct::key &sp_core_spend_pubkey_mock, + const crypto::secret_key &k_view_balance, + SpInputProposalCore &sp_input_proposal_inout) +{ + // 1. save the amount commitment in a new temporary enote core shuttle variable + SpEnoteCore temp_enote_core; + temp_enote_core.amount_commitment = amount_commitment_ref(sp_input_proposal_inout.enote_core); + + // 2. extended spendkey + rct::key seraphis_extended_spendkey_temp{sp_core_spend_pubkey_mock}; //k_m U + extend_seraphis_spendkey_u(sp_input_proposal_inout.enote_view_extension_u, + seraphis_extended_spendkey_temp); //(k_u + k_m) U + + // 3. new onetime address + rct::key seraphis_onetime_address_temp{seraphis_extended_spendkey_temp}; //(k_u + k_m) U + extend_seraphis_spendkey_x(k_view_balance, seraphis_onetime_address_temp); //k_vb X + (k_u + k_m) U + extend_seraphis_spendkey_x(sp_input_proposal_inout.enote_view_extension_x, + seraphis_onetime_address_temp); //(k_x + k_vb) X + (k_u + k_m) U + mask_key(sp_input_proposal_inout.enote_view_extension_g, + seraphis_onetime_address_temp, + temp_enote_core.onetime_address); //k_g G + (k_x + k_vb) X + (k_u + k_m) U + + // 4. reset the proposal's enote core + sp_input_proposal_inout.enote_core = temp_enote_core; + + // 5. update key image for new onetime address + make_seraphis_key_image(add_secrets(sp_input_proposal_inout.enote_view_extension_x, k_view_balance), + rct::rct2pk(seraphis_extended_spendkey_temp), + sp_input_proposal_inout.key_image); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void replace_sp_input_proposal_destinations_for_tx_simulation_v1(const rct::key &sp_core_spend_pubkey_mock, + const crypto::secret_key &k_view_balance, + std::vector &sp_input_proposals_inout) +{ + // 1. update the input proposals + for (SpInputProposalV1 &sp_input_proposal : sp_input_proposals_inout) + { + replace_sp_input_proposal_destination_for_tx_simulation_v1(sp_core_spend_pubkey_mock, + k_view_balance, + sp_input_proposal.core); + } + + // 2. make sure the updated proposals are sorted + std::sort(sp_input_proposals_inout.begin(), + sp_input_proposals_inout.end(), + tools::compare_func(compare_KI)); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void prepare_legacy_input_proof_proposal_v1(const rct::key &tx_proposal_prefix, + const LegacyInputProposalV1 &legacy_input_proposal, + LegacyMultisigRingSignaturePrepV1 multisig_proof_prep, + multisig::CLSAGMultisigProposal &multisig_proposal_out) +{ + // 1. message to sign + rct::key legacy_ring_signature_message; + make_tx_legacy_ring_signature_message_v1(tx_proposal_prefix, + multisig_proof_prep.reference_set, + legacy_ring_signature_message); + + // 2. legacy enote image + LegacyEnoteImageV2 legacy_enote_image; + get_enote_image_v2(legacy_input_proposal, legacy_enote_image); + + // 3. legacy auxilliary key image: D + crypto::key_image auxilliary_key_image; + make_legacy_auxilliary_key_image_v1(legacy_input_proposal.commitment_mask, + legacy_input_proposal.onetime_address, + hw::get_device("default"), + auxilliary_key_image); + + // 4. legacy multisig proof proposal + multisig::make_clsag_multisig_proposal(legacy_ring_signature_message, + std::move(multisig_proof_prep.referenced_enotes), + legacy_enote_image.masked_commitment, + legacy_enote_image.key_image, + auxilliary_key_image, + multisig_proof_prep.real_reference_index, + multisig_proposal_out); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void prepare_legacy_clsag_privkeys_for_multisig(const crypto::secret_key &enote_view_extension, + const crypto::secret_key &commitment_mask, + crypto::secret_key &k_offset_out, + crypto::secret_key &z_out) +{ + // prepare k_offset: legacy enote view privkey + k_offset_out = enote_view_extension; + + // prepare z: - mask + // note: legacy commitments to zero are + // C_z = C[l] - C" + // = C[l] - (mask G + C[l]) + // = (- mask) G + sc_0(to_bytes(z_out)); + sc_sub(to_bytes(z_out), to_bytes(z_out), to_bytes(commitment_mask)); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void collect_legacy_clsag_privkeys_for_multisig(const std::vector &legacy_input_proposals, + std::vector &proof_privkeys_k_offset_out, + std::vector &proof_privkeys_z_out) +{ + CHECK_AND_ASSERT_THROW_MES(tools::is_sorted_and_unique(legacy_input_proposals, compare_KI), + "collect legacy clsag privkeys for multisig: legacy input proposals aren't sorted and unique."); + + proof_privkeys_k_offset_out.clear(); + proof_privkeys_k_offset_out.reserve(legacy_input_proposals.size()); + proof_privkeys_z_out.clear(); + proof_privkeys_z_out.reserve(legacy_input_proposals.size()); + + for (const LegacyInputProposalV1 &legacy_input_proposal : legacy_input_proposals) + { + prepare_legacy_clsag_privkeys_for_multisig(legacy_input_proposal.enote_view_extension, + legacy_input_proposal.commitment_mask, + tools::add_element(proof_privkeys_k_offset_out), + tools::add_element(proof_privkeys_z_out)); + } +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void prepare_sp_composition_proof_privkeys_for_multisig(const crypto::secret_key &k_view_balance, + const crypto::secret_key &enote_view_extension_g, + const crypto::secret_key &enote_view_extension_x, + const crypto::secret_key &enote_view_extension_u, + const crypto::secret_key &address_mask, + const rct::key &squash_prefix, + crypto::secret_key &x_out, + crypto::secret_key &y_out, + crypto::secret_key &z_offset_out, + crypto::secret_key &z_multiplier_out) +{ + // prepare x: t_k + Hn(Ko,C) * k_g + sc_mul(to_bytes(x_out), squash_prefix.bytes, to_bytes(enote_view_extension_g)); + sc_add(to_bytes(x_out), to_bytes(address_mask), to_bytes(x_out)); + + // prepare y: Hn(Ko,C) * (k_x + k_vb) + sc_add(to_bytes(y_out), to_bytes(enote_view_extension_x), to_bytes(k_view_balance)); + sc_mul(to_bytes(y_out), squash_prefix.bytes, to_bytes(y_out)); + + // prepare z_offset: k_u + z_offset_out = enote_view_extension_u; + + // prepare z_multiplier: Hn(Ko,C) + z_multiplier_out = rct::rct2sk(squash_prefix); + + // note: z = z_multiplier * (z_offset + sum_e(z_e)) + // = Hn(Ko,C) * (k_u + k_m ) +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void collect_sp_composition_proof_privkeys_for_multisig(const std::vector &sp_input_proposals, + const crypto::secret_key &k_view_balance, + std::vector &proof_privkeys_x_out, + std::vector &proof_privkeys_y_out, + std::vector &proof_privkeys_z_offset_out, + std::vector &proof_privkeys_z_multiplier_out) +{ + CHECK_AND_ASSERT_THROW_MES(tools::is_sorted_and_unique(sp_input_proposals, compare_KI), + "collect sp composition proof privkeys for multisig: sp input proposals aren't sorted and unique."); + + proof_privkeys_x_out.clear(); + proof_privkeys_x_out.reserve(sp_input_proposals.size()); + proof_privkeys_y_out.clear(); + proof_privkeys_y_out.reserve(sp_input_proposals.size()); + proof_privkeys_z_offset_out.clear(); + proof_privkeys_z_offset_out.reserve(sp_input_proposals.size()); + proof_privkeys_z_multiplier_out.clear(); + proof_privkeys_z_multiplier_out.reserve(sp_input_proposals.size()); + rct::key squash_prefix_temp; + + for (const SpInputProposalV1 &sp_input_proposal : sp_input_proposals) + { + // Hn(Ko,C) + get_squash_prefix(sp_input_proposal, squash_prefix_temp); + + // x, y, z_offset, z_multiplier + prepare_sp_composition_proof_privkeys_for_multisig(k_view_balance, + sp_input_proposal.core.enote_view_extension_g, + sp_input_proposal.core.enote_view_extension_x, + sp_input_proposal.core.enote_view_extension_u, + sp_input_proposal.core.address_mask, + squash_prefix_temp, + tools::add_element(proof_privkeys_x_out), + tools::add_element(proof_privkeys_y_out), + tools::add_element(proof_privkeys_z_offset_out), + tools::add_element(proof_privkeys_z_multiplier_out)); + } +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool try_make_v1_legacy_input_v1(const rct::key &tx_proposal_prefix, + const LegacyInputProposalV1 &input_proposal, + std::vector reference_set, + rct::ctkeyV referenced_enotes, + const rct::key &masked_commitment, + const std::vector &input_proof_partial_sigs, + const rct::key &legacy_spend_pubkey, + LegacyInputV1 &input_out) +{ + try + { + // 1. make legacy ring signature message + rct::key ring_signature_message; + make_tx_legacy_ring_signature_message_v1(tx_proposal_prefix, reference_set, ring_signature_message); + + // 2. all partial sigs must sign the expected message + for (const multisig::CLSAGMultisigPartial &partial_sig : input_proof_partial_sigs) + { + CHECK_AND_ASSERT_THROW_MES(partial_sig.message == ring_signature_message, + "multisig make partial legacy input v1: a partial signature's message does not match the expected " + "message."); + } + + // 3. assemble proof (will throw if partial sig assembly doesn't produce a valid proof) + LegacyRingSignatureV4 ring_signature; + multisig::finalize_clsag_multisig_proof(input_proof_partial_sigs, + referenced_enotes, + masked_commitment, + ring_signature.clsag_proof); + + ring_signature.reference_set = std::move(reference_set); + + // 4. make legacy input + make_v1_legacy_input_v1(tx_proposal_prefix, + input_proposal, + std::move(ring_signature), + std::move(referenced_enotes), + legacy_spend_pubkey, + input_out); + + // 5. validate semantics to minimize failure modes + check_v1_legacy_input_semantics_v1(input_out); + } + catch (...) { return false; } + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool try_make_v1_sp_partial_input_v1(const rct::key &tx_proposal_prefix, + const SpInputProposalV1 &input_proposal, + const std::vector &input_proof_partial_sigs, + const rct::key &sp_core_spend_pubkey, + const crypto::secret_key &k_view_balance, + SpPartialInputV1 &partial_input_out) +{ + try + { + // 1. all partial sigs must sign the expected message + for (const multisig::SpCompositionProofMultisigPartial &partial_sig : input_proof_partial_sigs) + { + CHECK_AND_ASSERT_THROW_MES(partial_sig.message == tx_proposal_prefix, + "multisig make partial seraphis input v1: a partial signature's message does not match the expected " + "message."); + } + + // 2. assemble proof (will throw if partial sig assembly doesn't produce a valid proof) + SpImageProofV1 sp_image_proof; + multisig::finalize_sp_composition_multisig_proof(input_proof_partial_sigs, sp_image_proof.composition_proof); + + // 3. make the partial input + make_v1_partial_input_v1(input_proposal, + tx_proposal_prefix, + std::move(sp_image_proof), + sp_core_spend_pubkey, + k_view_balance, + partial_input_out); + + // 4. validate semantics to minimize failure modes + check_v1_partial_input_semantics_v1(partial_input_out); + } + catch (...) { return false; } + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool try_make_legacy_inputs_for_multisig_v1(const rct::key &tx_proposal_prefix, + const std::vector &legacy_input_proposals, + const std::vector &legacy_multisig_input_proposals, + const std::vector &legacy_input_proof_proposals, + const std::vector &multisig_signers, + const std::unordered_map> + &legacy_input_partial_sigs_per_signer, + const rct::key &legacy_spend_pubkey, + std::list &multisig_errors_inout, + std::vector &legacy_inputs_out) +{ + // 1. process legacy input proposals + // - map legacy input proposals to their onetime addresses + // - map masked commitments to the corresponding onetime addresses + std::unordered_map mapped_legacy_input_proposals; + std::unordered_map mapped_masked_commitments; + + for (const LegacyInputProposalV1 &legacy_input_proposal : legacy_input_proposals) + { + mapped_legacy_input_proposals[legacy_input_proposal.onetime_address] = legacy_input_proposal; + mask_key(legacy_input_proposal.commitment_mask, + legacy_input_proposal.amount_commitment, + mapped_masked_commitments[legacy_input_proposal.onetime_address]); + } + + // 2. process multisig legacy input proposals + // - map ring signature messages to onetime addresses + // - map legacy reference sets to onetime addresses + std::unordered_map legacy_proof_contexts; //[ proof key : proof message ] + std::unordered_map> mapped_reference_sets; + rct::key message_temp; + + for (const LegacyMultisigInputProposalV1 &legacy_multisig_input_proposal : legacy_multisig_input_proposals) + { + // [ proof key : proof message ] + make_tx_legacy_ring_signature_message_v1(tx_proposal_prefix, + legacy_multisig_input_proposal.reference_set, + message_temp); + legacy_proof_contexts[onetime_address_ref(legacy_multisig_input_proposal.enote)] = message_temp; + + // [ proof key : reference set ] + mapped_reference_sets[onetime_address_ref(legacy_multisig_input_proposal.enote)] = + legacy_multisig_input_proposal.reference_set; + } + + // 3. map legacy ring members to onetime addresses + std::unordered_map mapped_ring_members; + + for (const multisig::CLSAGMultisigProposal &legacy_input_proof_proposal : legacy_input_proof_proposals) + mapped_ring_members[main_proof_key_ref(legacy_input_proof_proposal)] = legacy_input_proof_proposal.ring_members; + + // 4. filter the legacy partial signatures into a map + std::unordered_map>> collected_sigs_per_key_per_filter; + + multisig::filter_multisig_partial_signatures_for_combining_v1(multisig_signers, + legacy_proof_contexts, + multisig::MultisigPartialSigVariant::type_index_of(), + legacy_input_partial_sigs_per_signer, + multisig_errors_inout, + collected_sigs_per_key_per_filter); + + // 5. try to make one legacy input per input proposal (fails if can't make proofs for all inputs) + if (!multisig::try_assemble_multisig_partial_sigs_signer_group_attempts< + multisig::CLSAGMultisigPartial, + LegacyInputV1 + >( + legacy_input_proposals.size(), + collected_sigs_per_key_per_filter, + [&](const rct::key &proof_key, + const std::vector &partial_sigs, + LegacyInputV1 &legacy_input_out) -> bool + { + // sanity check + if (legacy_proof_contexts.find(proof_key) == legacy_proof_contexts.end()) + return false; + + // try to make the input + return try_make_v1_legacy_input_v1(tx_proposal_prefix, + mapped_legacy_input_proposals.at(proof_key), + mapped_reference_sets.at(proof_key), + mapped_ring_members.at(proof_key), + mapped_masked_commitments.at(proof_key), + partial_sigs, + legacy_spend_pubkey, + legacy_input_out); + }, + multisig_errors_inout, + legacy_inputs_out + )) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool try_make_sp_partial_inputs_for_multisig_v1(const rct::key &tx_proposal_prefix, + const std::vector &sp_input_proposals, + const std::vector &multisig_signers, + const std::unordered_map> + &sp_input_partial_sigs_per_signer, + const rct::key &sp_core_spend_pubkey, + const crypto::secret_key &k_view_balance, + std::list &multisig_errors_inout, + std::vector &sp_partial_inputs_out) +{ + // 1. process seraphis input proposals + // - collect seraphis masked addresses of input images + // - map seraphis input proposals to their masked addresses + std::unordered_map sp_proof_contexts; //[ proof key : proof message ] + std::unordered_map mapped_sp_input_proposals; + SpEnoteImageV1 enote_image_temp; + + for (const SpInputProposalV1 &sp_input_proposal : sp_input_proposals) + { + get_enote_image_v1(sp_input_proposal, enote_image_temp); + sp_proof_contexts[masked_address_ref(enote_image_temp)] = tx_proposal_prefix; + mapped_sp_input_proposals[masked_address_ref(enote_image_temp)] = sp_input_proposal; + } + + // 2. filter the seraphis partial signatures into a map + std::unordered_map>> collected_sigs_per_key_per_filter; + + multisig::filter_multisig_partial_signatures_for_combining_v1(multisig_signers, + sp_proof_contexts, + multisig::MultisigPartialSigVariant::type_index_of(), + sp_input_partial_sigs_per_signer, + multisig_errors_inout, + collected_sigs_per_key_per_filter); + + // 3. try to make one seraphis partial input per input proposal (fails if can't make proofs for all inputs) + if (!multisig::try_assemble_multisig_partial_sigs_signer_group_attempts< + multisig::SpCompositionProofMultisigPartial, + SpPartialInputV1 + >( + sp_input_proposals.size(), + collected_sigs_per_key_per_filter, + [&](const rct::key &proof_key, + const std::vector &partial_sigs, + SpPartialInputV1 &sp_partial_input_out) -> bool + { + // sanity check + if (sp_proof_contexts.find(proof_key) == sp_proof_contexts.end()) + return false; + + // try to make the partial input + return try_make_v1_sp_partial_input_v1(tx_proposal_prefix, + mapped_sp_input_proposals.at(proof_key), + partial_sigs, + sp_core_spend_pubkey, + k_view_balance, + sp_partial_input_out); + }, + multisig_errors_inout, + sp_partial_inputs_out + )) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +void check_v1_legacy_multisig_input_proposal_semantics_v1(const LegacyMultisigInputProposalV1 &multisig_input_proposal) +{ + CHECK_AND_ASSERT_THROW_MES(sc_isnonzero(to_bytes(multisig_input_proposal.commitment_mask)), + "semantics check legacy multisig input proposal v1: bad address mask (zero)."); + CHECK_AND_ASSERT_THROW_MES(sc_check(to_bytes(multisig_input_proposal.commitment_mask)) == 0, + "semantics check legacy multisig input proposal v1: bad address mask (not canonical)."); + CHECK_AND_ASSERT_THROW_MES(std::find(multisig_input_proposal.reference_set.begin(), + multisig_input_proposal.reference_set.end(), + multisig_input_proposal.tx_output_index) != + multisig_input_proposal.reference_set.end(), + "semantics check legacy multisig input proposal v1: referenced enote index is not in the reference set."); + CHECK_AND_ASSERT_THROW_MES(tools::is_sorted_and_unique(multisig_input_proposal.reference_set), + "semantics check legacy multisig input proposal v1: reference set indices are not sorted and unique."); +} +//------------------------------------------------------------------------------------------------------------------- +void make_v1_legacy_multisig_input_proposal_v1(const LegacyEnoteVariant &enote, + const crypto::key_image &key_image, + const rct::key &enote_ephemeral_pubkey, + const std::uint64_t tx_output_index, + const std::uint64_t unlock_time, + const crypto::secret_key &commitment_mask, + std::vector reference_set, + LegacyMultisigInputProposalV1 &proposal_out) +{ + // add components + proposal_out.enote = enote; + proposal_out.key_image = key_image; + proposal_out.enote_ephemeral_pubkey = enote_ephemeral_pubkey; + proposal_out.tx_output_index = tx_output_index; + proposal_out.unlock_time = unlock_time; + proposal_out.commitment_mask = commitment_mask; + proposal_out.reference_set = std::move(reference_set); +} +//------------------------------------------------------------------------------------------------------------------- +void make_v1_legacy_multisig_input_proposal_v1(const LegacyEnoteRecord &enote_record, + const crypto::secret_key &commitment_mask, + std::vector reference_set, + LegacyMultisigInputProposalV1 &proposal_out) +{ + make_v1_legacy_multisig_input_proposal_v1(enote_record.enote, + enote_record.key_image, + enote_record.enote_ephemeral_pubkey, + enote_record.tx_output_index, + enote_record.unlock_time, + commitment_mask, + std::move(reference_set), + proposal_out); +} +//------------------------------------------------------------------------------------------------------------------- +void check_v1_sp_multisig_input_proposal_semantics_v1(const SpMultisigInputProposalV1 &multisig_input_proposal) +{ + CHECK_AND_ASSERT_THROW_MES(sc_isnonzero(to_bytes(multisig_input_proposal.address_mask)), + "semantics check sp multisig input proposal v1: bad address mask (zero)."); + CHECK_AND_ASSERT_THROW_MES(sc_check(to_bytes(multisig_input_proposal.address_mask)) == 0, + "semantics check sp multisig input proposal v1: bad address mask (not canonical)."); + CHECK_AND_ASSERT_THROW_MES(sc_isnonzero(to_bytes(multisig_input_proposal.commitment_mask)), + "semantics check sp multisig input proposal v1: bad commitment mask (zero)."); + CHECK_AND_ASSERT_THROW_MES(sc_check(to_bytes(multisig_input_proposal.commitment_mask)) == 0, + "semantics check sp multisig input proposal v1: bad commitment mask (not canonical)."); +} +//------------------------------------------------------------------------------------------------------------------- +void make_v1_sp_multisig_input_proposal_v1(const SpEnoteVariant &enote, + const crypto::x25519_pubkey &enote_ephemeral_pubkey, + const rct::key &input_context, + const crypto::secret_key &address_mask, + const crypto::secret_key &commitment_mask, + SpMultisigInputProposalV1 &proposal_out) +{ + // add components + proposal_out.enote = enote; + proposal_out.enote_ephemeral_pubkey = enote_ephemeral_pubkey; + proposal_out.input_context = input_context; + proposal_out.address_mask = address_mask; + proposal_out.commitment_mask = commitment_mask; +} +//------------------------------------------------------------------------------------------------------------------- +void make_v1_sp_multisig_input_proposal_v1(const SpEnoteRecordV1 &enote_record, + const crypto::secret_key &address_mask, + const crypto::secret_key &commitment_mask, + SpMultisigInputProposalV1 &proposal_out) +{ + make_v1_sp_multisig_input_proposal_v1(enote_record.enote, + enote_record.enote_ephemeral_pubkey, + enote_record.input_context, + address_mask, + commitment_mask, + proposal_out); +} +//------------------------------------------------------------------------------------------------------------------- +void check_v1_multisig_tx_proposal_semantics_v1(const SpMultisigTxProposalV1 &multisig_tx_proposal, + const tx_version_t &expected_tx_version, + const std::uint32_t threshold, + const std::uint32_t num_signers, + const rct::key &legacy_spend_pubkey, + const std::unordered_map &legacy_subaddress_map, + const crypto::secret_key &legacy_view_privkey, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance) +{ + /// multisig signing config checks + + // 1. proposal should contain expected tx version encoding + CHECK_AND_ASSERT_THROW_MES(multisig_tx_proposal.tx_version == expected_tx_version, + "semantics check multisig tx proposal v1: intended tx version encoding is invalid."); + + // 2. signer set filter must be valid (at least 'threshold' signers allowed, format is valid) + CHECK_AND_ASSERT_THROW_MES(multisig::validate_aggregate_multisig_signer_set_filter(threshold, + num_signers, + multisig_tx_proposal.aggregate_signer_set_filter), + "semantics check multisig tx proposal v1: invalid aggregate signer set filter."); + + + /// input/output checks + + // 1. check the multisig input proposal semantics + // a. legacy + CHECK_AND_ASSERT_THROW_MES(tools::is_sorted_and_unique(multisig_tx_proposal.legacy_multisig_input_proposals, + compare_KI), + "semantics check multisig tx proposal v1: legacy multisig input proposals are not sorted and unique."); + + for (const LegacyMultisigInputProposalV1 &legacy_multisig_input_proposal : + multisig_tx_proposal.legacy_multisig_input_proposals) + check_v1_legacy_multisig_input_proposal_semantics_v1(legacy_multisig_input_proposal); + + // b. seraphis (these are NOT sorted) + for (const SpMultisigInputProposalV1 &sp_multisig_input_proposal : + multisig_tx_proposal.sp_multisig_input_proposals) + check_v1_sp_multisig_input_proposal_semantics_v1(sp_multisig_input_proposal); + + // 2. convert the proposal to a plain tx proposal and check its semantics (a comprehensive set of tests) + SpTxProposalV1 tx_proposal; + get_v1_tx_proposal_v1(multisig_tx_proposal, + legacy_spend_pubkey, + legacy_subaddress_map, + legacy_view_privkey, + jamtis_spend_pubkey, + k_view_balance, + tx_proposal); + + check_v1_tx_proposal_semantics_v1(tx_proposal, legacy_spend_pubkey, jamtis_spend_pubkey, k_view_balance); + + // 3. get tx proposal prefix + rct::key tx_proposal_prefix; + get_tx_proposal_prefix_v1(tx_proposal, multisig_tx_proposal.tx_version, k_view_balance, tx_proposal_prefix); + + + /// multisig-related input checks + + // 1. input proposals line up 1:1 with multisig input proof proposals + CHECK_AND_ASSERT_THROW_MES(tx_proposal.legacy_input_proposals.size() == + multisig_tx_proposal.legacy_input_proof_proposals.size(), + "semantics check multisig tx proposal v1: legacy input proposals don't line up with input proposal proofs."); + + CHECK_AND_ASSERT_THROW_MES(tx_proposal.sp_input_proposals.size() == + multisig_tx_proposal.sp_input_proof_proposals.size(), + "semantics check multisig tx proposal v1: sp input proposals don't line up with input proposal proofs."); + + // 2. assess each legacy input proof proposal + for (std::size_t legacy_input_index{0}; + legacy_input_index < multisig_tx_proposal.legacy_input_proof_proposals.size(); + ++legacy_input_index) + { + check_semantics_legacy_multisig_input_material_v1(tx_proposal_prefix, + multisig_tx_proposal.legacy_multisig_input_proposals[legacy_input_index], + multisig_tx_proposal.legacy_input_proof_proposals[legacy_input_index]); + } + + // 3. assess each seraphis input proof proposal (iterate through sorted input vectors; note that multisig + // input proposals are NOT sorted, but input proof proposals and input proposals obtained from + // a normal tx proposal ARE sorted) + for (std::size_t sp_input_index{0}; + sp_input_index < multisig_tx_proposal.sp_input_proof_proposals.size(); + ++sp_input_index) + { + check_semantics_sp_multisig_input_material_v1(tx_proposal_prefix, + tx_proposal.sp_input_proposals[sp_input_index], + multisig_tx_proposal.sp_input_proof_proposals[sp_input_index]); + } +} +//------------------------------------------------------------------------------------------------------------------- +bool try_simulate_tx_from_multisig_tx_proposal_v1(const SpMultisigTxProposalV1 &multisig_tx_proposal, + const SpTxSquashedV1::SemanticRulesVersion semantic_rules_version, + const std::uint32_t threshold, + const std::uint32_t num_signers, + const rct::key &legacy_spend_pubkey, + const std::unordered_map &legacy_subaddress_map, + const crypto::secret_key &legacy_view_privkey, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + hw::device &hwdev) +{ + try + { + // 1. get versioning of the proposed tx + const tx_version_t tx_version{tx_version_from(semantic_rules_version)}; + + // 2. validate the multisig tx proposal + check_v1_multisig_tx_proposal_semantics_v1(multisig_tx_proposal, + tx_version, + threshold, + num_signers, + legacy_spend_pubkey, + legacy_subaddress_map, + legacy_view_privkey, + jamtis_spend_pubkey, + k_view_balance); + + // 3. convert to a regular tx proposal + SpTxProposalV1 tx_proposal; + get_v1_tx_proposal_v1(multisig_tx_proposal, + legacy_spend_pubkey, + legacy_subaddress_map, + legacy_view_privkey, + jamtis_spend_pubkey, + k_view_balance, + tx_proposal); + + // 4. make mock legacy and jamtis spend private keys + const crypto::secret_key legacy_spend_privkey_mock{rct::rct2sk(rct::skGen())}; //k^s (legacy) + const crypto::secret_key sp_spend_privkey_mock{rct::rct2sk(rct::skGen())}; //k_m (seraphis) + const rct::key sp_core_spend_pubkey_mock{ + rct::scalarmultKey(rct::pk2rct(crypto::get_U()), rct::sk2rct(sp_spend_privkey_mock)) + }; //k_m U + + // 5. make simulated input proposals for the tx using the mock spend keys + // a. legacy input proposals + legacy input proof preps + // note: after this, the legacy input proof preps are unsorted and missing the message the proofs should sign + std::vector legacy_ring_signature_preps; + replace_legacy_input_proposal_destinations_for_tx_simulation_v1( + multisig_tx_proposal.legacy_multisig_input_proposals, + multisig_tx_proposal.legacy_input_proof_proposals, + legacy_spend_privkey_mock, + tx_proposal.legacy_input_proposals, + legacy_ring_signature_preps); + + // b. seraphis input proposals + replace_sp_input_proposal_destinations_for_tx_simulation_v1(sp_core_spend_pubkey_mock, + k_view_balance, + tx_proposal.sp_input_proposals); + + // note: at this point calling check_v1_tx_proposal_semantics_v1() would not work because the check assumes + // inputs will be signed by the same keys as selfsend outputs in the tx, but that is no longer the case + // for our simulation + + // 6. tx proposal prefix of modified tx proposal + rct::key tx_proposal_prefix; + get_tx_proposal_prefix_v1(tx_proposal, tx_version, k_view_balance, tx_proposal_prefix); + + // 7. finish preparing the legacy ring signature preps + for (LegacyRingSignaturePrepV1 &ring_signature_prep : legacy_ring_signature_preps) + ring_signature_prep.tx_proposal_prefix = tx_proposal_prefix; //now we can set this + + std::sort(legacy_ring_signature_preps.begin(), + legacy_ring_signature_preps.end(), + tools::compare_func(compare_KI)); + + // 8. convert the input proposals to inputs/partial inputs + // a. legacy inputs + std::vector legacy_inputs; + make_v1_legacy_inputs_v1(tx_proposal_prefix, + tx_proposal.legacy_input_proposals, + std::move(legacy_ring_signature_preps), //must be sorted + legacy_spend_privkey_mock, + hwdev, + legacy_inputs); + + // b. seraphis partial inputs + std::vector sp_partial_inputs; + make_v1_partial_inputs_v1(tx_proposal.sp_input_proposals, + tx_proposal_prefix, + sp_spend_privkey_mock, + k_view_balance, + sp_partial_inputs); + + // 9. convert the tx proposal payment proposals to output proposals + // note: we can't use the tx proposal directly to make a partial tx because doing so would invoke + // check_v1_tx_proposal_semantics_v1(), which won't work here + std::vector output_proposals; + get_output_proposals_v1(tx_proposal, k_view_balance, output_proposals); + + // 10. construct a partial tx + SpPartialTxV1 partial_tx; + make_v1_partial_tx_v1(std::move(legacy_inputs), + std::move(sp_partial_inputs), + std::move(output_proposals), + tx_proposal.tx_fee, + tx_proposal.partial_memo, + tx_version, + partial_tx); + + // 11. validate the partial tx (this internally simulates a full transaction) + check_v1_partial_tx_semantics_v1(partial_tx, semantic_rules_version); + } + catch (...) { return false; } + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +void make_v1_multisig_tx_proposal_v1(std::vector legacy_multisig_input_proposals, + std::vector sp_multisig_input_proposals, + std::unordered_map legacy_multisig_ring_signature_preps, + const multisig::signer_set_filter aggregate_signer_set_filter, + std::vector normal_payment_proposals, + std::vector selfsend_payment_proposals, + const DiscretizedFee discretized_transaction_fee, + std::vector additional_memo_elements, + const tx_version_t &tx_version, + const rct::key &legacy_spend_pubkey, + const std::unordered_map &legacy_subaddress_map, + const crypto::secret_key &legacy_view_privkey, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + SpMultisigTxProposalV1 &proposal_out) +{ + CHECK_AND_ASSERT_THROW_MES(tools::keys_match_internal_values(legacy_multisig_ring_signature_preps, + [](const crypto::key_image &key, const LegacyMultisigRingSignaturePrepV1 &prep) -> bool + { + return key == prep.key_image; + }), + "make v1 multisig tx proposal (v1): a legacy ring signature prep is mapped to the incorrect key image."); + + // 1. pre-sort legacy multisig input proposals + // note: they need to be sorted in the multisig tx proposal, and the tx proposal also calls sort on legacy input + // proposals so pre-sorting here means less work there + std::sort(legacy_multisig_input_proposals.begin(), + legacy_multisig_input_proposals.end(), + tools::compare_func(compare_KI)); + + // 2. convert legacy multisig input proposals to legacy input proposals + std::vector legacy_input_proposals; + legacy_input_proposals.reserve(legacy_multisig_input_proposals.size()); + + for (const LegacyMultisigInputProposalV1 &legacy_multisig_input_proposal : legacy_multisig_input_proposals) + { + get_legacy_input_proposal_v1(legacy_multisig_input_proposal, + legacy_spend_pubkey, + legacy_subaddress_map, + legacy_view_privkey, + tools::add_element(legacy_input_proposals)); + } + + // 3. convert seraphis multisig input proposals to seraphis input proposals + std::vector sp_input_proposals; + sp_input_proposals.reserve(sp_multisig_input_proposals.size()); + + for (const SpMultisigInputProposalV1 &sp_multisig_input_proposal : sp_multisig_input_proposals) + { + get_sp_input_proposal_v1(sp_multisig_input_proposal, + jamtis_spend_pubkey, + k_view_balance, + tools::add_element(sp_input_proposals)); + } + + // 4. make a temporary normal tx proposal + SpTxProposalV1 tx_proposal; + make_v1_tx_proposal_v1(std::move(legacy_input_proposals), + std::move(sp_input_proposals), + normal_payment_proposals, + selfsend_payment_proposals, + discretized_transaction_fee, + additional_memo_elements, + tx_proposal); + + // 5. sanity check the normal tx proposal + check_v1_tx_proposal_semantics_v1(tx_proposal, legacy_spend_pubkey, jamtis_spend_pubkey, k_view_balance); + + // 6. get proposal prefix + rct::key tx_proposal_prefix; + get_tx_proposal_prefix_v1(tx_proposal, tx_version, k_view_balance, tx_proposal_prefix); + + // 7. make sure the legacy proof preps align with legacy input proposals + // note: if the legacy input proposals contain duplicates, then the call to check_v1_tx_proposal_semantics_v1() + // will catch it + CHECK_AND_ASSERT_THROW_MES(legacy_multisig_ring_signature_preps.size() == + tx_proposal.legacy_input_proposals.size(), + "make v1 multisig tx proposal (v1): legacy ring signature preps don't line up with input proposals."); + + // 8. prepare legacy proof proposals + // note: using the tx proposal ensures proof proposals are sorted + proposal_out.legacy_input_proof_proposals.clear(); + proposal_out.legacy_input_proof_proposals.reserve(tx_proposal.legacy_input_proposals.size()); + + for (const LegacyInputProposalV1 &legacy_input_proposal : tx_proposal.legacy_input_proposals) + { + CHECK_AND_ASSERT_THROW_MES(legacy_multisig_ring_signature_preps.find(legacy_input_proposal.key_image) != + legacy_multisig_ring_signature_preps.end(), + "make v1 multisig tx proposal (v1): a legacy ring signature prep doesn't line up with an input proposal."); + + // a. prepare the proof proposal + prepare_legacy_input_proof_proposal_v1(tx_proposal_prefix, + legacy_input_proposal, + std::move(legacy_multisig_ring_signature_preps[legacy_input_proposal.key_image]), + tools::add_element(proposal_out.legacy_input_proof_proposals)); + + // b. clear this input's entry in the map so duplicate key images are handled better + legacy_multisig_ring_signature_preps.erase(legacy_input_proposal.key_image); + } + + // 9. prepare composition proof proposals for each seraphis input + // note: using the tx proposal ensures proof proposals are sorted + proposal_out.sp_input_proof_proposals.clear(); + proposal_out.sp_input_proof_proposals.reserve(tx_proposal.sp_input_proposals.size()); + SpEnoteImageV1 sp_enote_image_temp; + + for (const SpInputProposalV1 &sp_input_proposal : tx_proposal.sp_input_proposals) + { + get_enote_image_v1(sp_input_proposal, sp_enote_image_temp); + + multisig::make_sp_composition_multisig_proposal(tx_proposal_prefix, + masked_address_ref(sp_enote_image_temp), + key_image_ref(sp_enote_image_temp), + tools::add_element(proposal_out.sp_input_proof_proposals)); + } + + // 10. add miscellaneous components + proposal_out.legacy_multisig_input_proposals = std::move(legacy_multisig_input_proposals); + proposal_out.sp_multisig_input_proposals = std::move(sp_multisig_input_proposals); + proposal_out.aggregate_signer_set_filter = aggregate_signer_set_filter; + proposal_out.normal_payment_proposals = std::move(normal_payment_proposals); + proposal_out.selfsend_payment_proposals = std::move(selfsend_payment_proposals); + proposal_out.tx_fee = discretized_transaction_fee; + make_tx_extra(std::move(additional_memo_elements), proposal_out.partial_memo); + proposal_out.tx_version = tx_version; +} +//------------------------------------------------------------------------------------------------------------------- +void make_v1_multisig_tx_proposal_v1(const std::vector &legacy_contextual_inputs, + const std::vector &sp_contextual_inputs, + std::unordered_map legacy_multisig_ring_signature_preps, + const multisig::signer_set_filter aggregate_signer_set_filter, + std::vector normal_payment_proposals, + std::vector selfsend_payment_proposals, + const DiscretizedFee discretized_transaction_fee, + TxExtra partial_memo_for_tx, + const tx_version_t &tx_version, + const rct::key &legacy_spend_pubkey, + const std::unordered_map &legacy_subaddress_map, + const crypto::secret_key &legacy_view_privkey, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + SpMultisigTxProposalV1 &multisig_tx_proposal_out) +{ + CHECK_AND_ASSERT_THROW_MES(tools::keys_match_internal_values(legacy_multisig_ring_signature_preps, + [](const crypto::key_image &key, const LegacyMultisigRingSignaturePrepV1 &prep) -> bool + { + return key == prep.key_image; + }), + "make v1 multisig tx proposal (v1): a legacy ring signature prep is mapped to the incorrect key image."); + + // 1. convert legacy inputs to legacy multisig input proposals (inputs to spend) + CHECK_AND_ASSERT_THROW_MES(legacy_contextual_inputs.size() == legacy_multisig_ring_signature_preps.size(), + "make v1 multisig tx proposal (v1): legacy contextual inputs don't line up with ring signature preps."); + + std::vector legacy_multisig_input_proposals; + legacy_multisig_input_proposals.reserve(legacy_contextual_inputs.size()); + + for (const LegacyContextualEnoteRecordV1 &legacy_contextual_input : legacy_contextual_inputs) + { + CHECK_AND_ASSERT_THROW_MES(legacy_multisig_ring_signature_preps.find(key_image_ref(legacy_contextual_input)) != + legacy_multisig_ring_signature_preps.end(), + "make v1 multisig tx proposal (v1): a legacy contextual input doesn't have a corresponding multisig prep."); + + // convert inputs to multisig input proposals + make_v1_legacy_multisig_input_proposal_v1(legacy_contextual_input.record, + rct::rct2sk(rct::skGen()), + legacy_multisig_ring_signature_preps + .at(key_image_ref(legacy_contextual_input)) + .reference_set, //don't consume, the full prep needs to be consumed later + tools::add_element(legacy_multisig_input_proposals)); + } + + // 2. convert seraphis inputs to seraphis multisig input proposals (inputs to spend) + std::vector sp_multisig_input_proposals; + sp_multisig_input_proposals.reserve(sp_contextual_inputs.size()); + + for (const SpContextualEnoteRecordV1 &contextual_input : sp_contextual_inputs) + { + // convert inputs to multisig input proposals + make_v1_sp_multisig_input_proposal_v1(contextual_input.record, + rct::rct2sk(rct::skGen()), + rct::rct2sk(rct::skGen()), + tools::add_element(sp_multisig_input_proposals)); + } + + // 3. get memo elements + std::vector extra_field_elements; + CHECK_AND_ASSERT_THROW_MES(try_get_extra_field_elements(partial_memo_for_tx, extra_field_elements), + "make tx proposal for transfer (v1): unable to extract memo field elements for tx proposal."); + + // 4. finalize multisig tx proposal + make_v1_multisig_tx_proposal_v1(std::move(legacy_multisig_input_proposals), + std::move(sp_multisig_input_proposals), + std::move(legacy_multisig_ring_signature_preps), + aggregate_signer_set_filter, + std::move(normal_payment_proposals), + std::move(selfsend_payment_proposals), + discretized_transaction_fee, + std::move(extra_field_elements), + tx_version, + legacy_spend_pubkey, + legacy_subaddress_map, + legacy_view_privkey, + jamtis_spend_pubkey, + k_view_balance, + multisig_tx_proposal_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_v1_multisig_init_sets_for_inputs_v1(const crypto::public_key &signer_id, + const std::uint32_t threshold, + const std::vector &multisig_signers, + const SpMultisigTxProposalV1 &multisig_tx_proposal, + const tx_version_t &expected_tx_version, + const rct::key &legacy_spend_pubkey, + const std::unordered_map &legacy_subaddress_map, + const crypto::secret_key &legacy_view_privkey, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + multisig::MultisigNonceCache &nonce_record_inout, + //[ proof key : init set ] + std::unordered_map &legacy_input_init_set_collection_out, + //[ proof key : init set ] + std::unordered_map &sp_input_init_set_collection_out) +{ + // 1. validate multisig tx proposal + check_v1_multisig_tx_proposal_semantics_v1(multisig_tx_proposal, + expected_tx_version, + threshold, + multisig_signers.size(), + legacy_spend_pubkey, + legacy_subaddress_map, + legacy_view_privkey, + jamtis_spend_pubkey, + k_view_balance); + + CHECK_AND_ASSERT_THROW_MES(multisig_tx_proposal.legacy_multisig_input_proposals.size() + + multisig_tx_proposal.sp_multisig_input_proposals.size() > 0, + "make multisig input init sets v1: no inputs to initialize."); + + // 2. make tx proposal (to get sorted inputs and the tx proposal prefix) + SpTxProposalV1 tx_proposal; + get_v1_tx_proposal_v1(multisig_tx_proposal, + legacy_spend_pubkey, + legacy_subaddress_map, + legacy_view_privkey, + jamtis_spend_pubkey, + k_view_balance, + tx_proposal); + + // 3. tx proposal prefix + rct::key tx_proposal_prefix; + get_tx_proposal_prefix_v1(tx_proposal, multisig_tx_proposal.tx_version, k_view_balance, tx_proposal_prefix); + + // 4. prepare proof contexts and multisig proof base points + // a. legacy proof context [ legacy Ko : legacy input message ] + // b. legacy proof base points [ legacy Ko : {G, Hp(legacy Ko)} ] + std::unordered_map legacy_input_proof_contexts; + std::unordered_map legacy_proof_key_base_points; + get_legacy_proof_contexts_v1(tx_proposal_prefix, + multisig_tx_proposal.legacy_multisig_input_proposals, + legacy_input_proof_contexts); + get_legacy_proof_base_keys_v1(tx_proposal.legacy_input_proposals, legacy_proof_key_base_points); + + // c. seraphis proof context [ seraphis K" : tx proposal prefix ] + // d. seraphis proof base points [ seraphis K" : {U} ] + std::unordered_map sp_input_proof_contexts; + std::unordered_map sp_proof_key_base_points; + get_seraphis_proof_contexts_v1(tx_proposal_prefix, tx_proposal.sp_input_proposals, sp_input_proof_contexts); + get_sp_proof_base_keys_v1(tx_proposal.sp_input_proposals, sp_proof_key_base_points); + + // 5. finish making multisig input init sets + // a. legacy input init set + multisig::make_v1_multisig_init_set_collection_v1(threshold, + multisig_signers, + multisig_tx_proposal.aggregate_signer_set_filter, + signer_id, + legacy_input_proof_contexts, + legacy_proof_key_base_points, + nonce_record_inout, + legacy_input_init_set_collection_out); + + // b. seraphis input init set + multisig::make_v1_multisig_init_set_collection_v1(threshold, + multisig_signers, + multisig_tx_proposal.aggregate_signer_set_filter, + signer_id, + sp_input_proof_contexts, + sp_proof_key_base_points, + nonce_record_inout, + sp_input_init_set_collection_out); +} +//------------------------------------------------------------------------------------------------------------------- +bool try_make_v1_multisig_partial_sig_sets_for_legacy_inputs_v1(const multisig::multisig_account &signer_account, + const SpMultisigTxProposalV1 &multisig_tx_proposal, + const std::unordered_map &legacy_subaddress_map, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + const tx_version_t &expected_tx_version, + //[ proof key : init set ] + std::unordered_map local_input_init_set_collection, + //[ signer id : [ proof key : init set ] ] + std::unordered_map> + other_input_init_set_collections, + std::list &multisig_errors_inout, + multisig::MultisigNonceCache &nonce_record_inout, + std::vector &legacy_input_partial_sig_sets_out) +{ + legacy_input_partial_sig_sets_out.clear(); + + CHECK_AND_ASSERT_THROW_MES(signer_account.multisig_is_ready(), + "multisig legacy input partial sigs v1: signer account is not complete, so it can't make partial signatures."); + CHECK_AND_ASSERT_THROW_MES(signer_account.get_era() == cryptonote::account_generator_era::cryptonote, + "multisig legacy input partial sigs v1: signer account is not a cryptonote account, so it can't make legacy " + "partial signatures."); + + // early return if there are no legacy inputs in the multisig tx proposal + if (multisig_tx_proposal.legacy_multisig_input_proposals.size() == 0) + return true; + + + /// prepare pieces to use below + + // 1. misc. from account + const crypto::secret_key &legacy_view_privkey{signer_account.get_common_privkey()}; + const std::uint32_t threshold{signer_account.get_threshold()}; + const rct::key legacy_spend_pubkey{rct::pk2rct(signer_account.get_multisig_pubkey())}; + + // 2. make sure the multisig tx proposal is valid + check_v1_multisig_tx_proposal_semantics_v1(multisig_tx_proposal, + expected_tx_version, + threshold, + signer_account.get_signers().size(), + legacy_spend_pubkey, + legacy_subaddress_map, + legacy_view_privkey, + jamtis_spend_pubkey, + k_view_balance); + + // 3. normal tx proposal (to get tx proposal prefix and sorted inputs) + SpTxProposalV1 tx_proposal; + get_v1_tx_proposal_v1(multisig_tx_proposal, + legacy_spend_pubkey, + legacy_subaddress_map, + legacy_view_privkey, + jamtis_spend_pubkey, + k_view_balance, + tx_proposal); + + // 4. tx proposal prefix + rct::key tx_proposal_prefix; + get_tx_proposal_prefix_v1(tx_proposal, multisig_tx_proposal.tx_version, k_view_balance, tx_proposal_prefix); + + // 5. legacy proof contexts: [ onetime address : legacy input message ] + std::unordered_map input_proof_contexts; //[ proof key : proof message ] + get_legacy_proof_contexts_v1(tx_proposal_prefix, + multisig_tx_proposal.legacy_multisig_input_proposals, + input_proof_contexts); + + // 6. prepare legacy proof privkeys (non-multisig components) + std::vector proof_privkeys_k_offset; + std::vector proof_privkeys_z; + + collect_legacy_clsag_privkeys_for_multisig(tx_proposal.legacy_input_proposals, + proof_privkeys_k_offset, + proof_privkeys_z); + + // 7. signature maker for legacy CLSAG proofs + const multisig::MultisigPartialSigMakerCLSAG partial_sig_maker{ + threshold, + multisig_tx_proposal.legacy_input_proof_proposals, + proof_privkeys_k_offset, + proof_privkeys_z + }; + + + /// make the partial signatures + if (!multisig::try_make_v1_multisig_partial_sig_sets_v1(signer_account, + cryptonote::account_generator_era::cryptonote, + multisig_tx_proposal.aggregate_signer_set_filter, + input_proof_contexts, + 2, //legacy multisig: sign on G and Hp(Ko) + partial_sig_maker, + std::move(local_input_init_set_collection), + std::move(other_input_init_set_collections), + multisig_errors_inout, + nonce_record_inout, + legacy_input_partial_sig_sets_out)) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool try_make_v1_multisig_partial_sig_sets_for_sp_inputs_v1(const multisig::multisig_account &signer_account, + const SpMultisigTxProposalV1 &multisig_tx_proposal, + const rct::key &legacy_spend_pubkey, + const std::unordered_map &legacy_subaddress_map, + const crypto::secret_key &legacy_view_privkey, + const tx_version_t &expected_tx_version, + //[ proof key : init set ] + std::unordered_map local_input_init_set_collection, + //[ signer id : [ proof key : init set ] ] + std::unordered_map> + other_input_init_set_collections, + std::list &multisig_errors_inout, + multisig::MultisigNonceCache &nonce_record_inout, + std::vector &sp_input_partial_sig_sets_out) +{ + sp_input_partial_sig_sets_out.clear(); + + CHECK_AND_ASSERT_THROW_MES(signer_account.multisig_is_ready(), + "multisig input partial sigs: signer account is not complete, so it can't make partial signatures."); + CHECK_AND_ASSERT_THROW_MES(signer_account.get_era() == cryptonote::account_generator_era::seraphis, + "multisig input partial sigs: signer account is not a seraphis account, so it can't make seraphis partial " + "signatures."); + + // early return if there are no seraphis inputs in the multisig tx proposal + if (multisig_tx_proposal.sp_multisig_input_proposals.size() == 0) + return true; + + + /// prepare pieces to use below + + // 1. misc. from account + const crypto::secret_key &k_view_balance{signer_account.get_common_privkey()}; + const std::uint32_t threshold{signer_account.get_threshold()}; + + // 2. jamtis spend pubkey: k_vb X + k_m U + rct::key jamtis_spend_pubkey{rct::pk2rct(signer_account.get_multisig_pubkey())}; + extend_seraphis_spendkey_x(k_view_balance, jamtis_spend_pubkey); + + // 3. make sure the multisig tx proposal is valid + check_v1_multisig_tx_proposal_semantics_v1(multisig_tx_proposal, + expected_tx_version, + threshold, + signer_account.get_signers().size(), + legacy_spend_pubkey, + legacy_subaddress_map, + legacy_view_privkey, + jamtis_spend_pubkey, + k_view_balance); + + // 4. normal tx proposal (to get tx proposal prefix and sorted inputs) + SpTxProposalV1 tx_proposal; + get_v1_tx_proposal_v1(multisig_tx_proposal, + legacy_spend_pubkey, + legacy_subaddress_map, + legacy_view_privkey, + jamtis_spend_pubkey, + k_view_balance, + tx_proposal); + + // 5. tx proposal prefix + rct::key tx_proposal_prefix; + get_tx_proposal_prefix_v1(tx_proposal, multisig_tx_proposal.tx_version, k_view_balance, tx_proposal_prefix); + + // 6. seraphis proof contexts: [ masked address : tx proposal prefix ] + // note: all seraphis input image proofs sign the same message + std::unordered_map input_proof_contexts; //[ proof key : proof message ] + get_seraphis_proof_contexts_v1(tx_proposal_prefix, tx_proposal.sp_input_proposals, input_proof_contexts); + + // 7. prepare seraphis proof privkeys (non-multisig components) + std::vector proof_privkeys_x; + std::vector proof_privkeys_y; + std::vector proof_privkeys_z_offset; + std::vector proof_privkeys_z_multiplier; + + collect_sp_composition_proof_privkeys_for_multisig(tx_proposal.sp_input_proposals, + k_view_balance, + proof_privkeys_x, + proof_privkeys_y, + proof_privkeys_z_offset, + proof_privkeys_z_multiplier); + + // 8. signature maker for seraphis composition proofs + const multisig::MultisigPartialSigMakerSpCompositionProof partial_sig_maker{ + threshold, + multisig_tx_proposal.sp_input_proof_proposals, + proof_privkeys_x, + proof_privkeys_y, + proof_privkeys_z_offset, + proof_privkeys_z_multiplier + }; + + + /// make the partial signatures + if (!multisig::try_make_v1_multisig_partial_sig_sets_v1(signer_account, + cryptonote::account_generator_era::seraphis, + multisig_tx_proposal.aggregate_signer_set_filter, + input_proof_contexts, + 1, //sp multisig: sign on U + partial_sig_maker, + std::move(local_input_init_set_collection), + std::move(other_input_init_set_collections), + multisig_errors_inout, + nonce_record_inout, + sp_input_partial_sig_sets_out)) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool try_make_inputs_for_multisig_v1(const SpMultisigTxProposalV1 &multisig_tx_proposal, + const std::vector &multisig_signers, + const rct::key &legacy_spend_pubkey, + const std::unordered_map &legacy_subaddress_map, + const crypto::secret_key &legacy_view_privkey, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + const std::unordered_map> + &legacy_input_partial_sigs_per_signer, + const std::unordered_map> + &sp_input_partial_sigs_per_signer, + std::list &multisig_errors_inout, + std::vector &legacy_inputs_out, + std::vector &sp_partial_inputs_out) +{ + // note: we do not validate semantics of anything here, because this function is just optimistically attempting to + // combine partial sig sets into partial inputs if possible + + // 1. get tx proposal + SpTxProposalV1 tx_proposal; + get_v1_tx_proposal_v1(multisig_tx_proposal, + legacy_spend_pubkey, + legacy_subaddress_map, + legacy_view_privkey, + jamtis_spend_pubkey, + k_view_balance, + tx_proposal); + + // 2. the proof message is the tx's proposal prefix + rct::key tx_proposal_prefix; + get_tx_proposal_prefix_v1(tx_proposal, multisig_tx_proposal.tx_version, k_view_balance, tx_proposal_prefix); + + // 3. try to make legacy inputs + if (!try_make_legacy_inputs_for_multisig_v1(tx_proposal_prefix, + tx_proposal.legacy_input_proposals, + multisig_tx_proposal.legacy_multisig_input_proposals, + multisig_tx_proposal.legacy_input_proof_proposals, + multisig_signers, + legacy_input_partial_sigs_per_signer, + legacy_spend_pubkey, + multisig_errors_inout, + legacy_inputs_out)) + return false; + + // 4. try to make seraphis partial inputs + rct::key sp_core_spend_pubkey{jamtis_spend_pubkey}; + reduce_seraphis_spendkey_x(k_view_balance, sp_core_spend_pubkey); + + if (!try_make_sp_partial_inputs_for_multisig_v1(tx_proposal_prefix, + tx_proposal.sp_input_proposals, + multisig_signers, + sp_input_partial_sigs_per_signer, + sp_core_spend_pubkey, + k_view_balance, + multisig_errors_inout, + sp_partial_inputs_out)) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace sp diff --git a/src/seraphis_main/tx_builders_multisig.h b/src/seraphis_main/tx_builders_multisig.h new file mode 100644 index 0000000000..d6e9f76314 --- /dev/null +++ b/src/seraphis_main/tx_builders_multisig.h @@ -0,0 +1,369 @@ +// Copyright (c) 2022, 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 tx-builder/component-builder implementations (multisig). +// WARNING: Passing a semantic check here, or successfully making a component, does not guarantee that the +// component is well-formed (i.e. can ultimately be used to make a valid transaction). The checks should be +// considered sanity checks that only a malicious implementation can/will circumvent. Note that multisig +// is only assumed to work when a threshold of honest players are interacting. +// - The semantic checks SHOULD detect unintended behavior that would allow a successful transaction. For example, +// the checks prevent a multisig tx proposer from proposing a tx with no self-sends (which would make balance +// checks much more difficult). +// - If users encounter tx construction failures, it may be necessary to identify malicious players and +// exclude them. + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "crypto/x25519.h" +#include "cryptonote_basic/subaddress_index.h" +#include "device/device.hpp" +#include "enote_record_types.h" +#include "multisig/multisig_account.h" +#include "multisig/multisig_signer_set_filter.h" +#include "multisig/multisig_signing_errors.h" +#include "multisig/multisig_signing_helper_types.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/discretized_fee.h" +#include "seraphis_core/jamtis_destination.h" +#include "seraphis_core/jamtis_payment_proposal.h" +#include "seraphis_core/sp_core_types.h" +#include "seraphis_core/tx_extra.h" +#include "tx_builder_types.h" +#include "tx_builder_types_multisig.h" +#include "tx_component_types.h" +#include "tx_fee_calculator.h" +#include "tx_input_selection.h" +#include "txtype_squashed_v1.h" + +//third party headers + +//standard headers +#include +#include +#include + +//forward declarations +namespace multisig { class MultisigNonceCache; } +namespace sp { struct tx_version_t; } + +namespace sp +{ + +/** +* brief: check_v1_legacy_multisig_input_proposal_semantics_v1 - check semantics of a legacy multisig input proposal +* - throws if a check fails +* - note: does not verify that the caller owns the input's enote +* param: multisig_input_proposal - +*/ +void check_v1_legacy_multisig_input_proposal_semantics_v1(const LegacyMultisigInputProposalV1 &multisig_input_proposal); +/** +* brief: make_v1_legacy_multisig_input_proposal_v1 - make a legacy multisig input proposal (can be sent to other people) +* param: enote - +* param: key_image - +* param: enote_ephemeral_pubkey - +* param: tx_output_index - +* param: unlock_time - +* param: commitment_mask - +* param: reference_set - +* outparam: proposal_out - +*/ +void make_v1_legacy_multisig_input_proposal_v1(const LegacyEnoteVariant &enote, + const crypto::key_image &key_image, + const rct::key &enote_ephemeral_pubkey, + const std::uint64_t tx_output_index, + const std::uint64_t unlock_time, + const crypto::secret_key &commitment_mask, + std::vector reference_set, + LegacyMultisigInputProposalV1 &proposal_out); +void make_v1_legacy_multisig_input_proposal_v1(const LegacyEnoteRecord &enote_record, + const crypto::secret_key &commitment_mask, + std::vector reference_set, + LegacyMultisigInputProposalV1 &proposal_out); +/** +* brief: check_v1_sp_multisig_input_proposal_semantics_v1 - check semantics of a seraphis multisig input proposal +* - throws if a check fails +* - note: does not verify that the caller owns the input's enote +* param: multisig_input_proposal - +*/ +void check_v1_sp_multisig_input_proposal_semantics_v1(const SpMultisigInputProposalV1 &multisig_input_proposal); +/** +* brief: make_v1_sp_multisig_input_proposal_v1 - make a seraphis multisig input proposal (can be sent to other people) +* param: enote - +* param: enote_ephemeral_pubkey - +* param: input_context - +* param: address_mask - +* param: commitment_mask - +* outparam: proposal_out - +*/ +void make_v1_sp_multisig_input_proposal_v1(const SpEnoteV1 &enote, + const crypto::x25519_pubkey &enote_ephemeral_pubkey, + const rct::key &input_context, + const crypto::secret_key &address_mask, + const crypto::secret_key &commitment_mask, + SpMultisigInputProposalV1 &proposal_out); +void make_v1_sp_multisig_input_proposal_v1(const SpEnoteRecordV1 &enote_record, + const crypto::secret_key &address_mask, + const crypto::secret_key &commitment_mask, + SpMultisigInputProposalV1 &proposal_out); +/** +* brief: check_v1_multisig_tx_proposal_semantics_v1 - check semantics of a multisig tx proposal +* - throws if a check fails +* - not checked: semantics satisfy the desired tx semantic rules version (can check these with the simulate tx method) +* param: multisig_tx_proposal - +* param: expected_tx_version - +* param: threshold - +* param: num_signers - +* param: legacy_spend_pubkey - +* param: legacy_subaddress_map - +* param: legacy_view_privkey - +* param: jamtis_spend_pubkey - +* param: k_view_balance - +*/ +void check_v1_multisig_tx_proposal_semantics_v1(const SpMultisigTxProposalV1 &multisig_tx_proposal, + const tx_version_t &expected_tx_version, + const std::uint32_t threshold, + const std::uint32_t num_signers, + const rct::key &legacy_spend_pubkey, + const std::unordered_map &legacy_subaddress_map, + const crypto::secret_key &legacy_view_privkey, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance); +/** +* brief: try_simulate_tx_from_multisig_tx_proposal_v1 - try to simulate a squashed v1 tx from a multisig tx proposal +* - checks the proposal semantics then simulates a transaction and tries to fully validate it against the specified +* semantics rules +* - note: to check that a multisig tx proposal MAY ultimately succeed, combine this simulation with A) checks that +* all inputs are owned and spendable by the local user, B) checks that legacy ring members are valid references +* param: multisig_tx_proposal - +* param: semantic_rules_version - +* param: threshold - +* param: num_signers - +* param: legacy_spend_pubkey - +* param: legacy_subaddress_map - +* param: legacy_view_privkey - +* param: jamtis_spend_pubkey - +* param: k_view_balance - +* inoutparam: hwdev - +* return: true if the tx was successfully simulated +*/ +bool try_simulate_tx_from_multisig_tx_proposal_v1(const SpMultisigTxProposalV1 &multisig_tx_proposal, + const SpTxSquashedV1::SemanticRulesVersion semantic_rules_version, + const std::uint32_t threshold, + const std::uint32_t num_signers, + const rct::key &legacy_spend_pubkey, + const std::unordered_map &legacy_subaddress_map, + const crypto::secret_key &legacy_view_privkey, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + hw::device &hwdev); +/** +* brief: make_v1_multisig_tx_proposal_v1 - make a multisig tx proposal +* param: legacy_multisig_input_proposals - +* param: sp_multisig_input_proposals - +* param: legacy_ring_signature_preps - +* param: aggregate_signer_set_filter - +* param: normal_payment_proposals - +* param: selfsend_payment_proposals - +* param: discretized_transaction_fee - +* param: additional_memo_elements - +* param: tx_version - +* param: legacy_spend_pubkey - +* param: legacy_subaddress_map - +* param: legacy_view_privkey - +* param: jamtis_spend_pubkey +* param: k_view_balance - +* outparam: proposal_out - +*/ +void make_v1_multisig_tx_proposal_v1(std::vector legacy_multisig_input_proposals, + std::vector sp_multisig_input_proposals, + std::unordered_map legacy_ring_signature_preps, + const multisig::signer_set_filter aggregate_signer_set_filter, + std::vector normal_payment_proposals, + std::vector selfsend_payment_proposals, + const DiscretizedFee discretized_transaction_fee, + std::vector additional_memo_elements, + const tx_version_t &tx_version, + const rct::key &legacy_spend_pubkey, + const std::unordered_map &legacy_subaddress_map, + const crypto::secret_key &legacy_view_privkey, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + SpMultisigTxProposalV1 &proposal_out); +void make_v1_multisig_tx_proposal_v1(const std::vector &legacy_contextual_inputs, + const std::vector &sp_contextual_inputs, + std::unordered_map legacy_ring_signature_preps, + const multisig::signer_set_filter aggregate_signer_set_filter, + std::vector normal_payment_proposals, + std::vector selfsend_payment_proposals, + const DiscretizedFee discretized_transaction_fee, + TxExtra partial_memo_for_tx, + const tx_version_t &tx_version, + const rct::key &legacy_spend_pubkey, + const std::unordered_map &legacy_subaddress_map, + const crypto::secret_key &legacy_view_privkey, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + SpMultisigTxProposalV1 &multisig_tx_proposal_out); +/** +* brief: make_v1_multisig_init_sets_for_inputs_v1 - make init sets for legacy and seraphis multisig tx input proofs +* param: signer_id - +* param: threshold - +* param: multisig_signers - +* param: multisig_tx_proposal - +* param: expected_tx_version - +* param: legacy_spend_pubkey - +* param: legacy_subaddress_map - +* param: legacy_view_privkey - +* param: jamtis_spend_pubkey - +* param: k_view_balance - +* inoutparam: nonce_record_inout - +* outparam: legacy_input_init_set_collection_out - +* outparam: sp_input_init_set_collection_out - +*/ +void make_v1_multisig_init_sets_for_inputs_v1(const crypto::public_key &signer_id, + const std::uint32_t threshold, + const std::vector &multisig_signers, + const SpMultisigTxProposalV1 &multisig_tx_proposal, + const tx_version_t &expected_tx_version, + const rct::key &legacy_spend_pubkey, + const std::unordered_map &legacy_subaddress_map, + const crypto::secret_key &legacy_view_privkey, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + multisig::MultisigNonceCache &nonce_record_inout, + //[ proof key : init set ] + std::unordered_map &legacy_input_init_set_collection_out, + //[ proof key : init set ] + std::unordered_map &sp_input_init_set_collection_out); +/** +* brief: try_make_v1_multisig_partial_sig_sets_for_legacy_inputs_v1 - try to make multisig partial signatures for legacy +* tx inputs +* - weak preconditions: ignores invalid proof initializers from non-local signers +* - will throw if local signer is not in the aggregate signer filter (or has an invalid initializer) +* - will only succeed (return true) if a partial sig set can be made that includes each of the legacy inputs found in +* the multisig tx proposal +* param: signer_account - +* param: multisig_tx_proposal - +* param: legacy_subaddress_map - +* param: jamtis_spend_pubkey - +* param: k_view_balance - +* param: expected_tx_version - +* param: local_input_init_set_collection - +* param: other_input_init_set_collections - +* inoutparam: multisig_errors_inout - +* inoutparam: nonce_record_inout - +* outparam: legacy_input_partial_sig_sets_out - +* return: true if at least one set of partial signatures was created (containing a partial sig for each input) +*/ +bool try_make_v1_multisig_partial_sig_sets_for_legacy_inputs_v1(const multisig::multisig_account &signer_account, + const SpMultisigTxProposalV1 &multisig_tx_proposal, + const std::unordered_map &legacy_subaddress_map, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + const tx_version_t &expected_tx_version, + //[ proof key : init set ] + std::unordered_map local_input_init_set_collection, + //[ signer id : [ proof key : init set ] ] + std::unordered_map> + other_input_init_set_collections, + std::list &multisig_errors_inout, + multisig::MultisigNonceCache &nonce_record_inout, + std::vector &legacy_input_partial_sig_sets_out); +/** +* brief: try_make_v1_multisig_partial_sig_sets_for_sp_inputs_v1 - try to make multisig partial signatures for seraphis +* tx inputs +* - weak preconditions: ignores invalid proof initializers from non-local signers +* - will throw if local signer is not in the aggregate signer filter (or has an invalid initializer) +* - will only succeed (return true) if a partial sig set can be made that includes each of the seraphis inputs found in +* the multisig tx proposal +* param: signer_account - +* param: multisig_tx_proposal - +* param: legacy_spend_pubkey - +* param: legacy_subaddress_map - +* param: legacy_view_privkey - +* param: expected_tx_version - +* param: local_input_init_set_collection - +* param: other_input_init_set_collections - +* inoutparam: multisig_errors_inout - +* inoutparam: nonce_record_inout - +* outparam: sp_input_partial_sig_sets_out - +* return: true if at least one set of partial signatures was created (containing a partial sig for each input) +*/ +bool try_make_v1_multisig_partial_sig_sets_for_sp_inputs_v1(const multisig::multisig_account &signer_account, + const SpMultisigTxProposalV1 &multisig_tx_proposal, + const rct::key &legacy_spend_pubkey, + const std::unordered_map &legacy_subaddress_map, + const crypto::secret_key &legacy_view_privkey, + const tx_version_t &expected_tx_version, + //[ proof key : init set ] + std::unordered_map local_input_init_set_collection, + //[ signer id : [ proof key : init set ] ] + std::unordered_map> + other_input_init_set_collections, + std::list &multisig_errors_inout, + multisig::MultisigNonceCache &nonce_record_inout, + std::vector &sp_input_partial_sig_sets_out); +/** +* brief: try_make_inputs_for_multisig_v1 - try to make legacy inputs and seraphis partial inputs from a collection of +* multisig partial signatures +* - weak preconditions: ignores invalid partial signature sets (including sets that are only partially invalid) +* - will only succeed if a legacy input and seraphis partial input can be made for each of the inputs found in the +* multisig tx proposal +* param: multisig_tx_proposal - +* param: multisig_signers - +* param: legacy_spend_pubkey - +* param: legacy_subaddress_map - +* param: legacy_view_privkey - +* param: jamtis_spend_pubkey - +* param: k_view_balance - +* param: legacy_input_partial_sigs_per_signer - +* param: sp_input_partial_sigs_per_signer - +* inoutparam: multisig_errors_inout - +* outparam: legacy_inputs_out - +* outparam: sp_partial_inputs_out - +* return: true if legacy_inputs_out and sp_partial_inputs_out contain inputs/partial inputs corresponding to each input +* proposal in the multisig tx proposal +*/ +bool try_make_inputs_for_multisig_v1(const SpMultisigTxProposalV1 &multisig_tx_proposal, + const std::vector &multisig_signers, + const rct::key &legacy_spend_pubkey, + const std::unordered_map &legacy_subaddress_map, + const crypto::secret_key &legacy_view_privkey, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + const std::unordered_map> + &legacy_input_partial_sigs_per_signer, + const std::unordered_map> + &sp_input_partial_sigs_per_signer, + std::list &multisig_errors_inout, + std::vector &legacy_inputs_out, + std::vector &sp_partial_inputs_out); + +} //namespace sp diff --git a/src/seraphis_main/tx_builders_outputs.cpp b/src/seraphis_main/tx_builders_outputs.cpp new file mode 100644 index 0000000000..de9fdb4114 --- /dev/null +++ b/src/seraphis_main/tx_builders_outputs.cpp @@ -0,0 +1,818 @@ +// Copyright (c) 2022, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//paired header +#include "tx_builders_outputs.h" + +//local headers +#include "common/container_helpers.h" +#include "crypto/crypto.h" +#include "crypto/x25519.h" +#include "cryptonote_config.h" +#include "enote_record_types.h" +#include "enote_record_utils.h" +#include "misc_log_ex.h" +#include "ringct/rctOps.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/jamtis_core_utils.h" +#include "seraphis_core/jamtis_destination.h" +#include "seraphis_core/jamtis_payment_proposal.h" +#include "seraphis_core/jamtis_support_types.h" +#include "seraphis_core/tx_extra.h" +#include "seraphis_crypto/sp_crypto_utils.h" +#include "tx_builder_types.h" +#include "tx_component_types.h" + +//third party headers +#include "boost/multiprecision/cpp_int.hpp" +#include "boost/optional/optional.hpp" + +//standard headers +#include +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis" + +namespace sp +{ +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool ephemeral_pubkeys_are_unique(const std::vector &output_proposals) +{ + std::unordered_set enote_ephemeral_pubkeys; + enote_ephemeral_pubkeys.reserve(output_proposals.size()); + + for (const SpCoinbaseOutputProposalV1 &output_proposal : output_proposals) + enote_ephemeral_pubkeys.insert(output_proposal.enote_ephemeral_pubkey); + + return enote_ephemeral_pubkeys.size() == output_proposals.size(); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool ephemeral_pubkeys_are_unique(const std::vector &output_proposals) +{ + std::unordered_set enote_ephemeral_pubkeys; + enote_ephemeral_pubkeys.reserve(output_proposals.size()); + + for (const SpOutputProposalV1 &output_proposal : output_proposals) + enote_ephemeral_pubkeys.insert(output_proposal.enote_ephemeral_pubkey); + + return enote_ephemeral_pubkeys.size() == output_proposals.size(); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool ephemeral_pubkeys_are_unique(const std::vector &normal_payment_proposals, + const std::vector &selfsend_payment_proposals) +{ + std::unordered_set enote_ephemeral_pubkeys; + enote_ephemeral_pubkeys.reserve(normal_payment_proposals.size() + selfsend_payment_proposals.size()); + crypto::x25519_pubkey temp_enote_ephemeral_pubkey; + + for (const jamtis::JamtisPaymentProposalV1 &normal_proposal : normal_payment_proposals) + { + jamtis::get_enote_ephemeral_pubkey(normal_proposal, temp_enote_ephemeral_pubkey); + enote_ephemeral_pubkeys.insert(temp_enote_ephemeral_pubkey); + } + + for (const jamtis::JamtisPaymentProposalSelfSendV1 &selfsend_proposal : selfsend_payment_proposals) + { + jamtis::get_enote_ephemeral_pubkey(selfsend_proposal, temp_enote_ephemeral_pubkey); + enote_ephemeral_pubkeys.insert(temp_enote_ephemeral_pubkey); + } + + return enote_ephemeral_pubkeys.size() == normal_payment_proposals.size() + selfsend_payment_proposals.size(); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void make_additional_output_normal_dummy_v1(jamtis::JamtisPaymentProposalV1 &dummy_proposal_out) +{ + // make random payment proposal for a 'normal' dummy output + dummy_proposal_out.destination = jamtis::gen_jamtis_destination_v1(); + dummy_proposal_out.amount = 0; + dummy_proposal_out.enote_ephemeral_privkey = crypto::x25519_secret_key_gen(); + dummy_proposal_out.partial_memo = TxExtra{}; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void make_additional_output_special_dummy_v1(const crypto::x25519_pubkey &enote_ephemeral_pubkey, + jamtis::JamtisPaymentProposalV1 &dummy_proposal_out) +{ + // make random payment proposal for a 'special' dummy output that uses a shared enote ephemeral pubkey + dummy_proposal_out.destination = jamtis::gen_jamtis_destination_v1(); + crypto::x25519_invmul_key({crypto::x25519_eight()}, + enote_ephemeral_pubkey, + dummy_proposal_out.destination.addr_K3); //(1/8) * xK_e_other + dummy_proposal_out.amount = 0; + dummy_proposal_out.enote_ephemeral_privkey = crypto::x25519_eight(); //r = 8 (can't do r = 1 for x25519) + dummy_proposal_out.partial_memo = TxExtra{}; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void make_additional_output_normal_self_send_v1(const jamtis::JamtisSelfSendType self_send_type, + const jamtis::JamtisDestinationV1 &destination, + const rct::xmr_amount amount, + jamtis::JamtisPaymentProposalSelfSendV1 &selfsend_proposal_out) +{ + // build payment proposal for a 'normal' self-send + selfsend_proposal_out.destination = destination; + selfsend_proposal_out.amount = amount; + selfsend_proposal_out.type = self_send_type; + selfsend_proposal_out.enote_ephemeral_privkey = crypto::x25519_secret_key_gen(); + selfsend_proposal_out.partial_memo = TxExtra{}; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void make_additional_output_special_self_send_v1(const jamtis::JamtisSelfSendType self_send_type, + const crypto::x25519_pubkey &enote_ephemeral_pubkey, + const jamtis::JamtisDestinationV1 &destination, + const crypto::secret_key &k_view_balance, + const rct::xmr_amount amount, + jamtis::JamtisPaymentProposalSelfSendV1 &selfsend_proposal_out) +{ + // build payment proposal for a 'special' self-send that uses a shared enote ephemeral pubkey + + // 1. edit the destination to use adjusted DH keys so the proposal's ephemeral pubkey will match the input value + // while still allowing balance recovery with our xk_fr + crypto::x25519_secret_key xk_find_received; + jamtis::make_jamtis_findreceived_key(k_view_balance, xk_find_received); + + crypto::x25519_pubkey special_addr_K2; + crypto::x25519_scmul_key(xk_find_received, enote_ephemeral_pubkey, special_addr_K2); //xk_fr * xK_e_other + + selfsend_proposal_out.destination = destination; + crypto::x25519_invmul_key({crypto::x25519_eight()}, + special_addr_K2, + selfsend_proposal_out.destination.addr_K2); //(1/8) * xk_fr * xK_e_other + crypto::x25519_invmul_key({crypto::x25519_eight()}, + enote_ephemeral_pubkey, + selfsend_proposal_out.destination.addr_K3); //(1/8) * xK_e_other + + // 2. complete the proposal + selfsend_proposal_out.amount = amount; + selfsend_proposal_out.type = self_send_type; + selfsend_proposal_out.enote_ephemeral_privkey = crypto::x25519_eight(); //r = 8 (can't do r = 1 for x25519) + selfsend_proposal_out.partial_memo = TxExtra{}; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +void check_jamtis_payment_proposal_selfsend_semantics_v1( + const jamtis::JamtisPaymentProposalSelfSendV1 &selfsend_payment_proposal, + const rct::key &input_context, + const rct::key &spend_pubkey, + const crypto::secret_key &k_view_balance) +{ + // 1. convert to an output proposal + SpOutputProposalV1 output_proposal; + make_v1_output_proposal_v1(selfsend_payment_proposal, k_view_balance, input_context, output_proposal); + + // 2. extract enote from output proposal + SpEnoteV1 temp_enote; + get_enote_v1(output_proposal, temp_enote); + + // 3. try to get an enote record from the enote (via selfsend path) + SpEnoteRecordV1 temp_enote_record; + CHECK_AND_ASSERT_THROW_MES(try_get_enote_record_v1_selfsend(temp_enote, + output_proposal.enote_ephemeral_pubkey, + input_context, + spend_pubkey, + k_view_balance, + temp_enote_record), + "semantics check jamtis self-send payment proposal v1: failed to extract enote record from the proposal."); + + // 4. extract the self-send type + jamtis::JamtisSelfSendType dummy_type; + CHECK_AND_ASSERT_THROW_MES(jamtis::try_get_jamtis_self_send_type(temp_enote_record.type, dummy_type), + "semantics check jamtis self-send payment proposal v1: failed to convert enote type to self-send type (bug)."); +} +//------------------------------------------------------------------------------------------------------------------- +void check_v1_coinbase_output_proposal_semantics_v1(const SpCoinbaseOutputProposalV1 &output_proposal) +{ + std::vector additional_memo_elements; + CHECK_AND_ASSERT_THROW_MES(try_get_extra_field_elements(output_proposal.partial_memo, additional_memo_elements), + "coinbase output proposal semantics (v1): invalid partial memo."); +} +//------------------------------------------------------------------------------------------------------------------- +void check_v1_coinbase_output_proposal_set_semantics_v1(const std::vector &output_proposals) +{ + CHECK_AND_ASSERT_THROW_MES(output_proposals.size() >= 1, + "Semantics check coinbase output proposals v1: insufficient outputs."); + + // 1. output proposals should be internally valid + for (const SpCoinbaseOutputProposalV1 &output_proposal : output_proposals) + check_v1_coinbase_output_proposal_semantics_v1(output_proposal); + + // 2. all enote ephemeral pubkeys should be unique in coinbase output sets + CHECK_AND_ASSERT_THROW_MES(ephemeral_pubkeys_are_unique(output_proposals), + "Semantics check coinbase output proposals v1: enote ephemeral pubkeys aren't all unique."); + + // 3. proposals should be sorted and unique + CHECK_AND_ASSERT_THROW_MES(tools::is_sorted_and_unique(output_proposals, compare_Ko), + "Semantics check output proposals v1: output onetime addresses are not sorted and unique."); + + // 4. proposal onetime addresses should be canonical (sanity check so our tx outputs don't end up with duplicate + // key images) + for (const SpCoinbaseOutputProposalV1 &output_proposal : output_proposals) + { + CHECK_AND_ASSERT_THROW_MES(onetime_address_is_canonical(output_proposal.enote.core), + "Semantics check coinbase output proposals v1: an output onetime address is not in the prime subgroup."); + } +} +//------------------------------------------------------------------------------------------------------------------- +void check_v1_output_proposal_semantics_v1(const SpOutputProposalV1 &output_proposal) +{ + std::vector additional_memo_elements; + CHECK_AND_ASSERT_THROW_MES(try_get_extra_field_elements(output_proposal.partial_memo, additional_memo_elements), + "output proposal semantics (v1): invalid partial memo."); +} +//------------------------------------------------------------------------------------------------------------------- +void check_v1_output_proposal_set_semantics_v1(const std::vector &output_proposals) +{ + CHECK_AND_ASSERT_THROW_MES(output_proposals.size() >= 1, "Semantics check output proposals v1: insufficient outputs."); + + // 1. output proposals should be internally valid + for (const SpOutputProposalV1 &output_proposal : output_proposals) + check_v1_output_proposal_semantics_v1(output_proposal); + + // 2. if 2 proposals, must be a shared enote ephemeral pubkey + if (output_proposals.size() == 2) + { + CHECK_AND_ASSERT_THROW_MES(output_proposals[0].enote_ephemeral_pubkey == + output_proposals[1].enote_ephemeral_pubkey, + "Semantics check output proposals v1: there are 2 outputs but they don't share an enote ephemeral pubkey."); + } + + // 3. if >2 proposals, all enote ephemeral pubkeys should be unique + if (output_proposals.size() > 2) + { + CHECK_AND_ASSERT_THROW_MES(ephemeral_pubkeys_are_unique(output_proposals), + "Semantics check output proposals v1: there are >2 outputs but their enote ephemeral pubkeys aren't all " + "unique."); + } + + // 4. proposals should be sorted and unique + CHECK_AND_ASSERT_THROW_MES(tools::is_sorted_and_unique(output_proposals, compare_Ko), + "Semantics check output proposals v1: output onetime addresses are not sorted and unique."); + + // 5. proposal onetime addresses should be canonical (sanity check so our tx outputs don't end up with duplicate + // key images) + for (const SpOutputProposalV1 &output_proposal : output_proposals) + { + CHECK_AND_ASSERT_THROW_MES(onetime_address_is_canonical(output_proposal.core), + "Semantics check output proposals v1: an output onetime address is not in the prime subgroup."); + } +} +//------------------------------------------------------------------------------------------------------------------- +void make_v1_coinbase_output_proposal_v1(const jamtis::JamtisPaymentProposalV1 &proposal, + const std::uint64_t block_height, + SpCoinbaseOutputProposalV1 &output_proposal_out) +{ + jamtis::get_coinbase_output_proposal_v1(proposal, + block_height, + output_proposal_out.enote.core, + output_proposal_out.enote_ephemeral_pubkey, + output_proposal_out.enote.addr_tag_enc, + output_proposal_out.enote.view_tag, + output_proposal_out.partial_memo); +} +//------------------------------------------------------------------------------------------------------------------- +void make_v1_output_proposal_v1(const jamtis::JamtisPaymentProposalV1 &proposal, + const rct::key &input_context, + SpOutputProposalV1 &output_proposal_out) +{ + jamtis::get_output_proposal_v1(proposal, + input_context, + output_proposal_out.core, + output_proposal_out.enote_ephemeral_pubkey, + output_proposal_out.encoded_amount, + output_proposal_out.addr_tag_enc, + output_proposal_out.view_tag, + output_proposal_out.partial_memo); +} +//------------------------------------------------------------------------------------------------------------------- +void make_v1_output_proposal_v1(const jamtis::JamtisPaymentProposalSelfSendV1 &proposal, + const crypto::secret_key &k_view_balance, + const rct::key &input_context, + SpOutputProposalV1 &output_proposal_out) +{ + jamtis::get_output_proposal_v1(proposal, + k_view_balance, + input_context, + output_proposal_out.core, + output_proposal_out.enote_ephemeral_pubkey, + output_proposal_out.encoded_amount, + output_proposal_out.addr_tag_enc, + output_proposal_out.view_tag, + output_proposal_out.partial_memo); +} +//------------------------------------------------------------------------------------------------------------------- +void make_v1_coinbase_outputs_v1(const std::vector &output_proposals, + std::vector &outputs_out, + std::vector &output_enote_ephemeral_pubkeys_out) +{ + // 1. output proposal set should be valid + check_v1_coinbase_output_proposal_set_semantics_v1(output_proposals); + + // 2. extract tx output information from output proposals + outputs_out.clear(); + outputs_out.reserve(output_proposals.size()); + output_enote_ephemeral_pubkeys_out.clear(); + output_enote_ephemeral_pubkeys_out.reserve(output_proposals.size()); + + for (const SpCoinbaseOutputProposalV1 &output_proposal : output_proposals) + { + // a. convert to enote + outputs_out.emplace_back(output_proposal.enote); + + // b. copy unique enote pubkeys to tx supplement (note: the semantics checker should prevent duplicates) + if (std::find(output_enote_ephemeral_pubkeys_out.begin(), + output_enote_ephemeral_pubkeys_out.end(), + output_proposal.enote_ephemeral_pubkey) == output_enote_ephemeral_pubkeys_out.end()) + output_enote_ephemeral_pubkeys_out.emplace_back(output_proposal.enote_ephemeral_pubkey); + } +} +//------------------------------------------------------------------------------------------------------------------- +void make_v1_outputs_v1(const std::vector &output_proposals, + std::vector &outputs_out, + std::vector &output_amounts_out, + std::vector &output_amount_commitment_blinding_factors_out, + std::vector &output_enote_ephemeral_pubkeys_out) +{ + // 1. output proposal set should be valid + check_v1_output_proposal_set_semantics_v1(output_proposals); + + // 2. extract tx output information from output proposals + outputs_out.clear(); + outputs_out.reserve(output_proposals.size()); + output_amounts_out.clear(); + output_amounts_out.reserve(output_proposals.size()); + output_amount_commitment_blinding_factors_out.clear(); + output_amount_commitment_blinding_factors_out.reserve(output_proposals.size()); + output_enote_ephemeral_pubkeys_out.clear(); + output_enote_ephemeral_pubkeys_out.reserve(output_proposals.size()); + + for (const SpOutputProposalV1 &output_proposal : output_proposals) + { + // a. sanity check + // note: a blinding factor of 0 is allowed (but not recommended) + CHECK_AND_ASSERT_THROW_MES(sc_check(to_bytes(output_proposal.core.amount_blinding_factor)) == 0, + "making v1 outputs: invalid amount blinding factor (non-canonical)."); + + // b. convert to enote + get_enote_v1(output_proposal, tools::add_element(outputs_out)); + + // c. cache amount commitment information for range proofs + output_amounts_out.emplace_back(amount_ref(output_proposal)); + output_amount_commitment_blinding_factors_out.emplace_back(output_proposal.core.amount_blinding_factor); + + // d. copy unique enote pubkeys to tx supplement + if (std::find(output_enote_ephemeral_pubkeys_out.begin(), + output_enote_ephemeral_pubkeys_out.end(), + output_proposal.enote_ephemeral_pubkey) == output_enote_ephemeral_pubkeys_out.end()) + output_enote_ephemeral_pubkeys_out.emplace_back(output_proposal.enote_ephemeral_pubkey); + } +} +//------------------------------------------------------------------------------------------------------------------- +boost::optional try_get_additional_output_type_for_output_set_v1( + const std::size_t num_outputs, + const std::vector &self_send_output_types, + const bool output_ephemeral_pubkeys_are_unique, + const rct::xmr_amount change_amount) +{ + // 1. txs should have at least 1 non-change output + CHECK_AND_ASSERT_THROW_MES(num_outputs > 0, "Additional output type v1: 0 outputs specified. If you want to send " + "money to yourself, use a self-spend enote type instead of forcing it via a change enote type."); + + // 2. sanity check + CHECK_AND_ASSERT_THROW_MES(self_send_output_types.size() <= num_outputs, + "Additional output type v1: there are more self-send outputs than outputs."); + + // 3. if an extra output is needed, get it + if (num_outputs == 1) + { + if (change_amount == 0) + { + if (self_send_output_types.size() == 1) + { + // txs need at least 2 outputs; we already have a self-send, so make a random special dummy output + + // add a special dummy output + // - 0 amount + // - make sure the final proposal set will have 1 unique enote ephemeral pubkey + return OutputProposalSetExtraTypeV1::SPECIAL_DUMMY; + } + else //(no self-send) + { + // txs need at least 2 outputs, with at least 1 self-send enote type + + // add a special self-send dummy output + // - 0 amount + // - make sure the final proposal set will have 1 unique enote ephemeral pubkey + return OutputProposalSetExtraTypeV1::SPECIAL_SELF_SEND_DUMMY; + } + } + else if (/*change_amount > 0 &&*/ + self_send_output_types.size() == 1 && + self_send_output_types[0] == jamtis::JamtisSelfSendType::CHANGE) + { + // 2-out txs may not have 2 self-send type enotes of the same type from the same wallet, so since + // we already have a change output (for some dubious reason) we can't have a special change here + // reason: the outputs in a 2-out tx with 2 same-type self-sends would have the same sender-receiver shared + // secret, which could cause problems (e.g. the outputs would have the same view tags, and could even + // have the same onetime address if the destinations of the two outputs are the same) + + // two change outputs doesn't make sense, so just ban it + CHECK_AND_ASSERT_THROW_MES(false, "Additional output type v1: there is 1 change-type output already " + "specified, but the change amount is non-zero and a tx with just two change outputs is not allowed " + "for privacy reasons. If you want to make a tx with just two change outputs, avoid calling this function " + "(not recommended)."); + } + else //(change_amount > 0 && single output is not a self-send change) + { + // if there is 1 non-change output and non-zero change, then make a special change output that shares + // the other output's enote ephemeral pubkey + + // add a special change output + // - 'change' amount + // - make sure the final proposal set will have 1 unique enote ephemeral pubkey + return OutputProposalSetExtraTypeV1::SPECIAL_CHANGE; + } + } + else if (num_outputs == 2 && output_ephemeral_pubkeys_are_unique) + { + if (change_amount == 0) + { + // 2-out txs need 1 shared enote ephemeral pubkey; add a dummy output here since the outputs have different + // enote ephemeral pubkeys + + if (self_send_output_types.size() > 0) + { + // if we have at least 1 self-send already, we can just make a normal dummy output + + // add a normal dummy output + // - 0 amount + return OutputProposalSetExtraTypeV1::NORMAL_DUMMY; + } + else //(no self-sends) + { + // if there are no self-sends, then we need to add a dummy self-send + + // add a normal self-send dummy output + // - 0 amount + return OutputProposalSetExtraTypeV1::NORMAL_SELF_SEND_DUMMY; + } + } + else //(change_amount > 0) + { + // 2 separate outputs + 1 change output = a simple 3-out tx + + // add a normal change output + // - 'change' amount + return OutputProposalSetExtraTypeV1::NORMAL_CHANGE; + } + } + else if (num_outputs == 2 && !output_ephemeral_pubkeys_are_unique) + { + if (change_amount == 0) + { + if (self_send_output_types.size() == 2 && + self_send_output_types[0] == self_send_output_types[1]) + { + CHECK_AND_ASSERT_THROW_MES(false, "Additional output type v1: there are 2 self-send outputs with the " + "same type that share an enote ephemeral pubkey, but this can reduce user privacy. If you want to " + "send money to yourself, then make independent self-spend types, or avoid calling this function (not " + "recommended)."); + } + else if (self_send_output_types.size() > 0) + { + // do nothing: the proposal set is already 'final' + } + else //(no self-sends) + { + CHECK_AND_ASSERT_THROW_MES(false, "Additional output type v1: there are 2 normal outputs that share " + "an enote ephemeral pubkey, but every tx needs at least one self-send output (since the " + "2 outputs share an enote ephemeral pubkey, we can't add a dummy self-send). If you want " + "to make a 2-output tx with no self-sends, then avoid calling this function (not recommended)."); + } + } + else //(change_amount > 0) + { + CHECK_AND_ASSERT_THROW_MES(false, "Additional output type v1: there are 2 outputs that share " + "an enote ephemeral pubkey, but a non-zero change amount. In >2-out txs, all enote ephemeral pubkeys " + "should be unique, so adding a change output isn't feasible here. You need to make independent output " + "proposals, or avoid calling this function (not recommended)."); + } + } + else //(num_outputs > 2) + { + CHECK_AND_ASSERT_THROW_MES(output_ephemeral_pubkeys_are_unique, + "Additional output type v1: there are >2 outputs but their enote ephemeral pubkeys aren't all unique."); + + if (change_amount == 0) + { + if (self_send_output_types.size() > 0) + { + // do nothing: the proposal set is already 'final' + } + else //(no self-sends) + { + // every tx made by this function needs a self-send output, so make a dummy self-send here + + // add a normal self-send dummy output + // - 0 amount + return OutputProposalSetExtraTypeV1::NORMAL_SELF_SEND_DUMMY; + } + } + else //(change_amount > 0) + { + // >2 separate outputs + 1 change output = a simple tx with 4+ outputs + + // add a normal change output + // - 'change' amount + return OutputProposalSetExtraTypeV1::NORMAL_CHANGE; + } + } + + return boost::none; +} +//------------------------------------------------------------------------------------------------------------------- +void make_additional_output_dummy_v1(const OutputProposalSetExtraTypeV1 additional_output_type, + const crypto::x25519_pubkey &first_enote_ephemeral_pubkey, + jamtis::JamtisPaymentProposalV1 &normal_proposal_out) +{ + // choose which output type to make, and make it + if (additional_output_type == OutputProposalSetExtraTypeV1::NORMAL_DUMMY) + { + // normal dummy + // - 0 amount + make_additional_output_normal_dummy_v1(normal_proposal_out); + } + else if (additional_output_type == OutputProposalSetExtraTypeV1::SPECIAL_DUMMY) + { + // special dummy + // - 0 amount + // - shared enote ephemeral pubkey + make_additional_output_special_dummy_v1(first_enote_ephemeral_pubkey, normal_proposal_out); + } + else + { + CHECK_AND_ASSERT_THROW_MES(false, "Unknown output proposal set extra type (dummy)."); + } +} +//------------------------------------------------------------------------------------------------------------------- +void make_additional_output_selfsend_v1(const OutputProposalSetExtraTypeV1 additional_output_type, + const crypto::x25519_pubkey &first_enote_ephemeral_pubkey, + const jamtis::JamtisDestinationV1 &change_destination, + const jamtis::JamtisDestinationV1 &dummy_destination, + const crypto::secret_key &k_view_balance, + const rct::xmr_amount change_amount, + jamtis::JamtisPaymentProposalSelfSendV1 &selfsend_proposal_out) +{ + // choose which output type to make, and make it + if (additional_output_type == OutputProposalSetExtraTypeV1::NORMAL_SELF_SEND_DUMMY) + { + // normal self-send dummy + // - 0 amount + make_additional_output_normal_self_send_v1(jamtis::JamtisSelfSendType::DUMMY, + dummy_destination, + 0, + selfsend_proposal_out); + } + else if (additional_output_type == OutputProposalSetExtraTypeV1::NORMAL_CHANGE) + { + // normal change + // - 'change' amount + make_additional_output_normal_self_send_v1(jamtis::JamtisSelfSendType::CHANGE, + change_destination, + change_amount, + selfsend_proposal_out); + } + else if (additional_output_type == OutputProposalSetExtraTypeV1::SPECIAL_SELF_SEND_DUMMY) + { + // special self-send dummy + // - 0 amount + // - shared enote ephemeral pubkey + make_additional_output_special_self_send_v1(jamtis::JamtisSelfSendType::DUMMY, + first_enote_ephemeral_pubkey, + dummy_destination, + k_view_balance, + 0, + selfsend_proposal_out); + } + else if (additional_output_type == OutputProposalSetExtraTypeV1::SPECIAL_CHANGE) + { + // special change + // - 'change' amount + // - shared enote ephemeral pubkey + make_additional_output_special_self_send_v1(jamtis::JamtisSelfSendType::CHANGE, + first_enote_ephemeral_pubkey, + change_destination, + k_view_balance, + change_amount, + selfsend_proposal_out); + } + else + { + CHECK_AND_ASSERT_THROW_MES(false, "Unknown output proposal set extra type (self-send)."); + } +} +//------------------------------------------------------------------------------------------------------------------- +void make_additional_output_v1(const jamtis::JamtisDestinationV1 &change_destination, + const jamtis::JamtisDestinationV1 &dummy_destination, + const crypto::secret_key &k_view_balance, + const rct::xmr_amount change_amount, + const OutputProposalSetExtraTypeV1 additional_output_type, + const crypto::x25519_pubkey &first_enote_ephemeral_pubkey, + std::vector &normal_payment_proposals_inout, + std::vector &selfsend_payment_proposals_inout) +{ + if (additional_output_type == OutputProposalSetExtraTypeV1::NORMAL_DUMMY || + additional_output_type == OutputProposalSetExtraTypeV1::SPECIAL_DUMMY) + { + make_additional_output_dummy_v1(additional_output_type, + first_enote_ephemeral_pubkey, + tools::add_element(normal_payment_proposals_inout)); + } + else + { + make_additional_output_selfsend_v1(additional_output_type, + first_enote_ephemeral_pubkey, + change_destination, + dummy_destination, + k_view_balance, + change_amount, + tools::add_element(selfsend_payment_proposals_inout)); + } +} +//------------------------------------------------------------------------------------------------------------------- +void finalize_v1_output_proposal_set_v1(const boost::multiprecision::uint128_t &total_input_amount, + const rct::xmr_amount transaction_fee, + const jamtis::JamtisDestinationV1 &change_destination, + const jamtis::JamtisDestinationV1 &dummy_destination, + const crypto::secret_key &k_view_balance, + std::vector &normal_payment_proposals_inout, + std::vector &selfsend_payment_proposals_inout) +{ + // 1. get change amount + boost::multiprecision::uint128_t output_sum{transaction_fee}; + + for (const jamtis::JamtisPaymentProposalV1 &normal_proposal : normal_payment_proposals_inout) + output_sum += normal_proposal.amount; + + for (const jamtis::JamtisPaymentProposalSelfSendV1 &selfsend_proposal : selfsend_payment_proposals_inout) + output_sum += selfsend_proposal.amount; + + CHECK_AND_ASSERT_THROW_MES(total_input_amount >= output_sum, + "Finalize output proposals v1: input amount is too small."); + CHECK_AND_ASSERT_THROW_MES(total_input_amount - output_sum <= static_cast(-1), + "Finalize output proposals v1: change amount exceeds maximum value allowed."); + + const rct::xmr_amount change_amount{total_input_amount - output_sum}; + + // 2. collect self-send output types + std::vector self_send_output_types; + self_send_output_types.reserve(selfsend_payment_proposals_inout.size()); + + for (const jamtis::JamtisPaymentProposalSelfSendV1 &selfsend_proposal : selfsend_payment_proposals_inout) + self_send_output_types.emplace_back(selfsend_proposal.type); + + // 3. set the shared enote ephemeral pubkey here: it will always be the first one when it is needed + crypto::x25519_pubkey first_enote_ephemeral_pubkey{}; + + if (normal_payment_proposals_inout.size() > 0) + jamtis::get_enote_ephemeral_pubkey(normal_payment_proposals_inout[0], first_enote_ephemeral_pubkey); + else if (selfsend_payment_proposals_inout.size() > 0) + jamtis::get_enote_ephemeral_pubkey(selfsend_payment_proposals_inout[0], first_enote_ephemeral_pubkey); + + // 4. add an additional output if necessary + if (const auto additional_output_type = + try_get_additional_output_type_for_output_set_v1( + normal_payment_proposals_inout.size() + selfsend_payment_proposals_inout.size(), + self_send_output_types, + ephemeral_pubkeys_are_unique(normal_payment_proposals_inout, selfsend_payment_proposals_inout), + change_amount) + ) + { + make_additional_output_v1(change_destination, + dummy_destination, + k_view_balance, + change_amount, + *additional_output_type, + first_enote_ephemeral_pubkey, + normal_payment_proposals_inout, + selfsend_payment_proposals_inout); + } +} +//------------------------------------------------------------------------------------------------------------------- +void finalize_tx_extra_v1(const TxExtra &partial_memo, + const std::vector &output_proposals, + TxExtra &tx_extra_out) +{ + // 1. collect all memo elements + std::vector collected_memo_elements; + accumulate_extra_field_elements(partial_memo, collected_memo_elements); + + for (const SpCoinbaseOutputProposalV1 &output_proposal : output_proposals) + accumulate_extra_field_elements(output_proposal.partial_memo, collected_memo_elements); + + // 2. finalize the extra field + make_tx_extra(std::move(collected_memo_elements), tx_extra_out); +} +//------------------------------------------------------------------------------------------------------------------- +void finalize_tx_extra_v1(const TxExtra &partial_memo, + const std::vector &output_proposals, + TxExtra &tx_extra_out) +{ + // 1. collect all memo elements + std::vector collected_memo_elements; + accumulate_extra_field_elements(partial_memo, collected_memo_elements); + + for (const SpOutputProposalV1 &output_proposal : output_proposals) + accumulate_extra_field_elements(output_proposal.partial_memo, collected_memo_elements); + + // 2. finalize the extra field + make_tx_extra(std::move(collected_memo_elements), tx_extra_out); +} +//------------------------------------------------------------------------------------------------------------------- +void check_v1_tx_supplement_semantics_v1(const SpTxSupplementV1 &tx_supplement, const std::size_t num_outputs) +{ + // 1. num enote ephemeral pubkeys == num outputs + CHECK_AND_ASSERT_THROW_MES(tx_supplement.output_enote_ephemeral_pubkeys.size() == num_outputs, + "Semantics check tx supplement v1: there must be one enote pubkey for each output."); + + // 2. all enote pubkeys should be unique + CHECK_AND_ASSERT_THROW_MES(keys_are_unique(tx_supplement.output_enote_ephemeral_pubkeys), + "Semantics check tx supplement v1: enote pubkeys must be unique."); + + // 3. enote ephemeral pubkeys should not be zero + // note: this is an easy check to do, but in no way guarantees the enote ephemeral pubkeys are valid/usable + for (const crypto::x25519_pubkey &enote_ephemeral_pubkey : tx_supplement.output_enote_ephemeral_pubkeys) + { + CHECK_AND_ASSERT_THROW_MES(!(enote_ephemeral_pubkey == crypto::x25519_pubkey{}), + "Semantics check tx supplement v1: an enote ephemeral pubkey is zero."); + } + + // 4. the tx extra must be well-formed + std::vector extra_field_elements; + CHECK_AND_ASSERT_THROW_MES(try_get_extra_field_elements(tx_supplement.tx_extra, extra_field_elements), + "Semantics check tx supplement v1: could not extract extra field elements."); +} +//------------------------------------------------------------------------------------------------------------------- +void check_v1_tx_supplement_semantics_v2(const SpTxSupplementV1 &tx_supplement, const std::size_t num_outputs) +{ + // 1. there may be either 1 or 3+ enote pubkeys + if (num_outputs <= 2) + { + CHECK_AND_ASSERT_THROW_MES(tx_supplement.output_enote_ephemeral_pubkeys.size() == 1, + "Semantics check tx supplement v2: there must be 1 enote pubkey if there are <= 2 outputs."); + } + else + { + CHECK_AND_ASSERT_THROW_MES(tx_supplement.output_enote_ephemeral_pubkeys.size() == num_outputs, + "Semantics check tx supplement v2: there must be one enote pubkey for each output when there are > 2 outputs."); + } + + // 2. all enote pubkeys should be unique + CHECK_AND_ASSERT_THROW_MES(keys_are_unique(tx_supplement.output_enote_ephemeral_pubkeys), + "Semantics check tx supplement v2: enote pubkeys must be unique."); + + // 3. enote ephemeral pubkeys should not be zero + // note: this is an easy check to do, but in no way guarantees the enote ephemeral pubkeys are valid/usable + for (const crypto::x25519_pubkey &enote_ephemeral_pubkey : tx_supplement.output_enote_ephemeral_pubkeys) + { + CHECK_AND_ASSERT_THROW_MES(!(enote_ephemeral_pubkey == crypto::x25519_pubkey{}), + "Semantics check tx supplement v2: an enote ephemeral pubkey is zero."); + } + + // 4. the tx extra must be well-formed + std::vector extra_field_elements; + CHECK_AND_ASSERT_THROW_MES(try_get_extra_field_elements(tx_supplement.tx_extra, extra_field_elements), + "Semantics check tx supplement v2: could not extract extra field elements."); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace sp diff --git a/src/seraphis_main/tx_builders_outputs.h b/src/seraphis_main/tx_builders_outputs.h new file mode 100644 index 0000000000..ce61a44bc9 --- /dev/null +++ b/src/seraphis_main/tx_builders_outputs.h @@ -0,0 +1,248 @@ +// Copyright (c) 2022, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "crypto/x25519.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/jamtis_destination.h" +#include "seraphis_core/jamtis_payment_proposal.h" +#include "seraphis_core/jamtis_support_types.h" +#include "tx_builder_types.h" +#include "tx_component_types.h" + +//third party headers +#include "boost/multiprecision/cpp_int.hpp" +#include "boost/optional/optional.hpp" + +//standard headers +#include + +//forward declarations + + +namespace sp +{ + +enum class OutputProposalSetExtraTypeV1 +{ + // a plain dummy output (random recipient, random enote ephemeral pubkey, zero amount) + NORMAL_DUMMY, + // a self-send dummy output (self recipient, normal enote ephemeral pubkey, zero amount) + NORMAL_SELF_SEND_DUMMY, + // a normal change output (self recipient, normal enote ephemeral pubkey, non-zero amount) + NORMAL_CHANGE, + // a special dummy output (random recipient, shared enote ephemeral pubkey, zero amount) + SPECIAL_DUMMY, + // a special self-send dummy output (self recipient, shared enote ephemeral pubkey, zero amount) + SPECIAL_SELF_SEND_DUMMY, + // a special change output (self recipient, shared enote ephemeral pubkey, non-zero amount) + SPECIAL_CHANGE +}; + +/** +* brief: check_jamtis_payment_proposal_selfsend_semantics_v1 - validate semantics of a self-send payment proposal +* param: selfsend_payment_proposal - +* param: input_context - +* param: spend_pubkey - +* param: k_view_balance - +* return: true if it's a valid self-send proposal +*/ +void check_jamtis_payment_proposal_selfsend_semantics_v1( + const jamtis::JamtisPaymentProposalSelfSendV1 &selfsend_payment_proposal, + const rct::key &input_context, + const rct::key &spend_pubkey, + const crypto::secret_key &k_view_balance); +/** +* brief: check_v1_coinbase_output_proposal_semantics_v1 - check semantics of a coinbase output proposal +* - throws if a check fails +* param: output_proposal - +*/ +void check_v1_coinbase_output_proposal_semantics_v1(const SpCoinbaseOutputProposalV1 &output_proposal); +/** +* brief: check_v1_coinbase_output_proposal_set_semantics_v1 - check semantics of a set of coinbase output proposals +* - throws if a check fails +* param: output_proposals - +*/ +void check_v1_coinbase_output_proposal_set_semantics_v1(const std::vector &output_proposals); +/** +* brief: check_v1_output_proposal_semantics_v1 - check semantics of an output proposal +* - throws if a check fails +* param: output_proposal - +*/ +void check_v1_output_proposal_semantics_v1(const SpOutputProposalV1 &output_proposal); +/** +* brief: check_v1_output_proposal_set_semantics_v1 - check semantics of a set of output proposals +* - throws if a check fails +* param: output_proposals - +*/ +void check_v1_output_proposal_set_semantics_v1(const std::vector &output_proposals); +/** +* brief: make_v1_coinbase_output_proposal_v1 - convert a jamtis proposal to a coinbase output proposal +* param: proposal - +* param: block_height - height of the coinbase tx's block +* outparam: output_proposal_out - +*/ +void make_v1_coinbase_output_proposal_v1(const jamtis::JamtisPaymentProposalV1 &proposal, + const std::uint64_t block_height, + SpCoinbaseOutputProposalV1 &output_proposal_out); +/** +* brief: make_v1_output_proposal_v1 - convert a jamtis proposal to an output proposal +* param: proposal - +* param: input_context - +* outparam: output_proposal_out - +*/ +void make_v1_output_proposal_v1(const jamtis::JamtisPaymentProposalV1 &proposal, + const rct::key &input_context, + SpOutputProposalV1 &output_proposal_out); +/** +* brief: make_v1_output_proposal_v1 - convert a jamtis selfsend proposal to an output proposal +* param: proposal - +* param: k_view_balance - +* param: input_context - +* outparam: output_proposal_out - +*/ +void make_v1_output_proposal_v1(const jamtis::JamtisPaymentProposalSelfSendV1 &proposal, + const crypto::secret_key &k_view_balance, + const rct::key &input_context, + SpOutputProposalV1 &output_proposal_out); +/** +* brief: make_v1_coinbase_outputs_v1 - make v1 coinbase tx outputs +* param: output_proposals - +* outparam: outputs_out - +* outparam: output_enote_ephemeral_pubkeys_out - +*/ +void make_v1_coinbase_outputs_v1(const std::vector &output_proposals, + std::vector &outputs_out, + std::vector &output_enote_ephemeral_pubkeys_out); +/** +* brief: make_v1_outputs_v1 - make v1 tx outputs +* param: output_proposals - +* outparam: outputs_out - +* outparam: output_amounts_out - +* outparam: output_amount_commitment_blinding_factors_out - +* outparam: output_enote_ephemeral_pubkeys_out - +*/ +void make_v1_outputs_v1(const std::vector &output_proposals, + std::vector &outputs_out, + std::vector &output_amounts_out, + std::vector &output_amount_commitment_blinding_factors_out, + std::vector &output_enote_ephemeral_pubkeys_out); +/** +* brief: finalize_v1_output_proposal_set_v1 - finalize a set of output proposals by adding 0-1 new proposals +* (new proposals are appended) +* - NOT FOR COINBASE OUTPUT SETS (coinbase output sets don't need to be finalized) +* - add a change output if necessary +* - add a dummy output if appropriate +* - All output sets will contain at least 1 self-send, either from the original set passed in, or by adding a change +* or selfsend dummy here. +* - Only very rare txs should have more than two outputs and include a dummy output (i.e. have numerically more outputs +* than if this invariant weren't enforced; note that all txs must have at least two outputs). Only txs with at least +* two outputs and zero change amount and zero specified self-sends will acquire an additional dummy selfsend output. +* - A self-send dummy will only be made if there are no other self-sends; otherwise dummies will be purely random. +* - The goal of this function is for all txs made from output sets produced by this function to be identifiable by view +* tag checks. That way, a signer scanning for balance recovery only needs key images from txs that are flagged by a +* view tag check in order to A) identify all spent enotes, B) identify all of their self-send enotes in txs that use +* output sets from this function. This optimizes third-party view-tag scanning services, which only need to transmit +* key images from txs with view tag matches to the local client. Txs with no user-specified selfsends that don't use +* this function (or an equivalent) to define the output set WILL cause failures to identify spent enotes in that +* workflow. +* param: total_input_amount - +* param: transaction_fee - +* param: change_destination - +* param: dummy_destination - +* param: jamtis_spend_pubkey - +* param: k_view_balance - +* inoutparam: normal_payment_proposals_inout - +* inoutparam: selfsend_payment_proposals_inout - +*/ +boost::optional try_get_additional_output_type_for_output_set_v1( + const std::size_t num_outputs, + const std::vector &self_send_output_types, + const bool output_ephemeral_pubkeys_are_unique, + const rct::xmr_amount change_amount); +void make_additional_output_dummy_v1(const OutputProposalSetExtraTypeV1 additional_output_type, + const crypto::x25519_pubkey &first_enote_ephemeral_pubkey, + jamtis::JamtisPaymentProposalV1 &normal_proposal_out); //exposed for unit testing +void make_additional_output_selfsend_v1(const OutputProposalSetExtraTypeV1 additional_output_type, + const crypto::x25519_pubkey &first_enote_ephemeral_pubkey, + const jamtis::JamtisDestinationV1 &change_destination, + const jamtis::JamtisDestinationV1 &dummy_destination, + const crypto::secret_key &k_view_balance, + const rct::xmr_amount change_amount, + jamtis::JamtisPaymentProposalSelfSendV1 &selfsend_proposal_out); //exposed for unit testing +void make_additional_output_v1(const jamtis::JamtisDestinationV1 &change_destination, + const jamtis::JamtisDestinationV1 &dummy_destination, + const crypto::secret_key &k_view_balance, + const rct::xmr_amount change_amount, + const OutputProposalSetExtraTypeV1 additional_output_type, + const crypto::x25519_pubkey &first_enote_ephemeral_pubkey, + std::vector &normal_payment_proposals_inout, + std::vector &selfsend_payment_proposals_inout); //exposed for unit testing +void finalize_v1_output_proposal_set_v1(const boost::multiprecision::uint128_t &total_input_amount, + const rct::xmr_amount transaction_fee, + const jamtis::JamtisDestinationV1 &change_destination, + const jamtis::JamtisDestinationV1 &dummy_destination, + const crypto::secret_key &k_view_balance, + std::vector &normal_payment_proposals_inout, + std::vector &selfsend_payment_proposals_inout); +/** +* brief: finalize_tx_extra_v1 - combine partial memos into a complete tx extra field +* param: partial_memo - +* param: output_proposals - +* outparam: tx_extra_out - +*/ +void finalize_tx_extra_v1(const TxExtra &partial_memo, + const std::vector &output_proposals, + TxExtra &tx_extra_out); +void finalize_tx_extra_v1(const TxExtra &partial_memo, + const std::vector &output_proposals, + TxExtra &tx_extra_out); +/** +* brief: check_v1_tx_supplement_semantics_v1 - check semantics of a tx supplement (v1) +* - throws if a check fails +* - check: num enote ephemeral pubkeys == num outputs +* - check: all enote ephemeral pubkeys should be unique +* param: tx_supplement - +* param: num_outputs - +*/ +void check_v1_tx_supplement_semantics_v1(const SpTxSupplementV1 &tx_supplement, const std::size_t num_outputs); +/** +* brief: check_v1_tx_supplement_semantics_v2 - check semantics of a tx supplement (v2) +* - throws if a check fails +* - check: if num outputs == 2, there should be 1 enote ephemeral pubkey +* - check: otherwise, should be 'num_outputs' enote ephemeral pubkeys +* - check: all enote ephemeral pubkeys should be unique +* param: tx_supplement - +* param: num_outputs - +*/ +void check_v1_tx_supplement_semantics_v2(const SpTxSupplementV1 &tx_supplement, const std::size_t num_outputs); + +} //namespace sp diff --git a/src/seraphis_main/tx_component_types.cpp b/src/seraphis_main/tx_component_types.cpp new file mode 100644 index 0000000000..9b16305c64 --- /dev/null +++ b/src/seraphis_main/tx_component_types.cpp @@ -0,0 +1,381 @@ +// Copyright (c) 2022, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//paired header +#include "tx_component_types.h" + +//local headers +#include "common/variant.h" +#include "crypto/crypto.h" +#include "int-util.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/binned_reference_set.h" +#include "seraphis_core/jamtis_support_types.h" +#include "seraphis_core/sp_core_types.h" +#include "seraphis_crypto/math_utils.h" +#include "seraphis_crypto/sp_legacy_proof_helpers.h" +#include "seraphis_crypto/sp_transcript.h" + +//third party headers + +//standard headers +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis" + +namespace sp +{ +//------------------------------------------------------------------------------------------------------------------- +void append_to_transcript(const SpCoinbaseEnoteV1 &container, SpTranscriptBuilder &transcript_inout) +{ + transcript_inout.append("core", container.core); + transcript_inout.append("addr_tag_enc", container.addr_tag_enc.bytes); + transcript_inout.append("view_tag", container.view_tag); +} +//------------------------------------------------------------------------------------------------------------------- +std::size_t sp_coinbase_enote_v1_size_bytes() +{ + return sp_coinbase_enote_core_size_bytes() + + sizeof(jamtis::encrypted_address_tag_t) + + sizeof(jamtis::view_tag_t); +} +//------------------------------------------------------------------------------------------------------------------- +void append_to_transcript(const SpEnoteV1 &container, SpTranscriptBuilder &transcript_inout) +{ + transcript_inout.append("core", container.core); + transcript_inout.append("encoded_amount", container.encoded_amount.bytes); + transcript_inout.append("addr_tag_enc", container.addr_tag_enc.bytes); + transcript_inout.append("view_tag", container.view_tag); +} +//------------------------------------------------------------------------------------------------------------------- +std::size_t sp_enote_v1_size_bytes() +{ + return sp_enote_core_size_bytes() + + sizeof(jamtis::encoded_amount_t) + + sizeof(jamtis::encrypted_address_tag_t) + + sizeof(jamtis::view_tag_t); +} +//------------------------------------------------------------------------------------------------------------------- +SpEnoteCoreVariant core_ref(const SpEnoteVariant &variant) +{ + struct visitor final : public tools::variant_static_visitor + { + using variant_static_visitor::operator(); //for blank overload + SpEnoteCoreVariant operator()(const SpCoinbaseEnoteV1 &enote) const { return enote.core; } + SpEnoteCoreVariant operator()(const SpEnoteV1 &enote) const { return enote.core; } + }; + + return variant.visit(visitor{}); +} +//------------------------------------------------------------------------------------------------------------------- +const rct::key& onetime_address_ref(const SpEnoteVariant &variant) +{ + struct visitor final : public tools::variant_static_visitor + { + using variant_static_visitor::operator(); //for blank overload + const rct::key& operator()(const SpCoinbaseEnoteV1 &enote) const { return enote.core.onetime_address; } + const rct::key& operator()(const SpEnoteV1 &enote) const { return enote.core.onetime_address; } + }; + + return variant.visit(visitor{}); +} +//------------------------------------------------------------------------------------------------------------------- +rct::key amount_commitment_ref(const SpEnoteVariant &variant) +{ + struct visitor final : public tools::variant_static_visitor + { + using variant_static_visitor::operator(); //for blank overload + rct::key operator()(const SpCoinbaseEnoteV1 &enote) const { return rct::zeroCommit(enote.core.amount); } + rct::key operator()(const SpEnoteV1 &enote) const { return enote.core.amount_commitment; } + }; + + return variant.visit(visitor{}); +} +//------------------------------------------------------------------------------------------------------------------- +const jamtis::encrypted_address_tag_t& addr_tag_enc_ref(const SpEnoteVariant &variant) +{ + struct visitor final : public tools::variant_static_visitor + { + using variant_static_visitor::operator(); //for blank overload + const jamtis::encrypted_address_tag_t& operator()(const SpCoinbaseEnoteV1 &enote) const + { return enote.addr_tag_enc; } + const jamtis::encrypted_address_tag_t& operator()(const SpEnoteV1 &enote) const + { return enote.addr_tag_enc; } + }; + + return variant.visit(visitor{}); +} +//------------------------------------------------------------------------------------------------------------------- +jamtis::view_tag_t view_tag_ref(const SpEnoteVariant &variant) +{ + struct visitor final : public tools::variant_static_visitor + { + using variant_static_visitor::operator(); //for blank overload + jamtis::view_tag_t operator()(const SpCoinbaseEnoteV1 &enote) const { return enote.view_tag; } + jamtis::view_tag_t operator()(const SpEnoteV1 &enote) const { return enote.view_tag; } + }; + + return variant.visit(visitor{}); +} +//------------------------------------------------------------------------------------------------------------------- +const crypto::key_image& key_image_ref(const SpEnoteImageV1 &enote_image) +{ + return enote_image.core.key_image; +} +//------------------------------------------------------------------------------------------------------------------- +const rct::key& masked_address_ref(const SpEnoteImageV1 &enote_image) +{ + return enote_image.core.masked_address; +} +//------------------------------------------------------------------------------------------------------------------- +const rct::key& masked_commitment_ref(const SpEnoteImageV1 &enote_image) +{ + return enote_image.core.masked_commitment; +} +//------------------------------------------------------------------------------------------------------------------- +void append_to_transcript(const SpEnoteImageV1 &container, SpTranscriptBuilder &transcript_inout) +{ + transcript_inout.append("core", container.core); +} +//------------------------------------------------------------------------------------------------------------------- +void append_to_transcript(const SpMembershipProofV1 &container, SpTranscriptBuilder &transcript_inout) +{ + transcript_inout.append("grootle_proof", container.grootle_proof); + transcript_inout.append("binned_reference_set", container.binned_reference_set); + transcript_inout.append("n", container.ref_set_decomp_n); + transcript_inout.append("m", container.ref_set_decomp_m); +} +//------------------------------------------------------------------------------------------------------------------- +std::size_t sp_membership_proof_v1_size_bytes(const std::size_t n, + const std::size_t m, + const std::size_t num_bin_members) +{ + const std::size_t ref_set_size{math::uint_pow(n, m)}; + + return grootle_size_bytes(n, m) + + (num_bin_members > 0 + ? sp_binned_ref_set_v1_size_bytes(ref_set_size / num_bin_members) + : 0) + + 4 * 2; //decomposition parameters (assume these fit in 4 bytes each) +} +//------------------------------------------------------------------------------------------------------------------- +std::size_t sp_membership_proof_v1_size_bytes_compact(const std::size_t n, + const std::size_t m, + const std::size_t num_bin_members) +{ + const std::size_t ref_set_size{math::uint_pow(n, m)}; + + return grootle_size_bytes(n, m) + + (num_bin_members > 0 + ? sp_binned_ref_set_v1_size_bytes_compact(ref_set_size / num_bin_members) //compact binned ref set + : 0); //no decomposition parameters +} +//------------------------------------------------------------------------------------------------------------------- +std::size_t sp_membership_proof_v1_size_bytes(const SpMembershipProofV1 &proof) +{ + return sp_membership_proof_v1_size_bytes(proof.ref_set_decomp_n, + proof.ref_set_decomp_m, + proof.binned_reference_set.bin_config.num_bin_members); +} +//------------------------------------------------------------------------------------------------------------------- +std::size_t sp_membership_proof_v1_size_bytes_compact(const SpMembershipProofV1 &proof) +{ + return sp_membership_proof_v1_size_bytes_compact(proof.ref_set_decomp_n, + proof.ref_set_decomp_m, + proof.binned_reference_set.bin_config.num_bin_members); +} +//------------------------------------------------------------------------------------------------------------------- +void append_to_transcript(const SpImageProofV1 &container, SpTranscriptBuilder &transcript_inout) +{ + transcript_inout.append("composition_proof", container.composition_proof); +} +//------------------------------------------------------------------------------------------------------------------- +void append_to_transcript(const SpBalanceProofV1 &container, SpTranscriptBuilder &transcript_inout) +{ + append_bpp2_to_transcript(container.bpp2_proof, transcript_inout); + transcript_inout.append("remainder_blinding_factor", container.remainder_blinding_factor); +} +//------------------------------------------------------------------------------------------------------------------- +std::size_t sp_balance_proof_v1_size_bytes(const std::size_t num_range_proofs) +{ + std::size_t size{0}; + + // BP+ proof + size += bpp_size_bytes(num_range_proofs, true); //include commitments + + // remainder blinding factor + size += 32; + + return size; +} +//------------------------------------------------------------------------------------------------------------------- +std::size_t sp_balance_proof_v1_size_bytes(const SpBalanceProofV1 &proof) +{ + return sp_balance_proof_v1_size_bytes(proof.bpp2_proof.V.size()); +} +//------------------------------------------------------------------------------------------------------------------- +std::size_t sp_balance_proof_v1_size_bytes_compact(const std::size_t num_range_proofs) +{ + // proof size minus cached amount commitments + return sp_balance_proof_v1_size_bytes(num_range_proofs) - 32*(num_range_proofs); +} +//------------------------------------------------------------------------------------------------------------------- +std::size_t sp_balance_proof_v1_size_bytes_compact(const SpBalanceProofV1 &proof) +{ + return sp_balance_proof_v1_size_bytes_compact(proof.bpp2_proof.V.size()); +} +//------------------------------------------------------------------------------------------------------------------- +std::size_t sp_balance_proof_v1_weight(const std::size_t num_range_proofs) +{ + std::size_t weight{0}; + + // BP+ proof + weight += bpp_weight(num_range_proofs, false); //weight without cached amount commitments + + // remainder blinding factor + weight += 32; + + return weight; +} +//------------------------------------------------------------------------------------------------------------------- +std::size_t sp_balance_proof_v1_weight(const SpBalanceProofV1 &proof) +{ + return sp_balance_proof_v1_weight(proof.bpp2_proof.V.size()); +} +//------------------------------------------------------------------------------------------------------------------- +void append_to_transcript(const SpTxSupplementV1 &container, SpTranscriptBuilder &transcript_inout) +{ + transcript_inout.append("output_xK_e_keys", container.output_enote_ephemeral_pubkeys); + transcript_inout.append("tx_extra", container.tx_extra); +} +//------------------------------------------------------------------------------------------------------------------- +std::size_t sp_tx_supplement_v1_size_bytes(const std::size_t num_outputs, + const std::size_t tx_extra_size, + const bool use_shared_ephemeral_key_assumption) +{ + std::size_t size{0}; + + // enote ephemeral pubkeys + if (use_shared_ephemeral_key_assumption && + num_outputs == 2) + size += 32; + else + size += 32 * num_outputs; + + // tx extra + size += tx_extra_size; + + return size; +} +//------------------------------------------------------------------------------------------------------------------- +std::size_t sp_tx_supplement_v1_size_bytes(const SpTxSupplementV1 &tx_supplement) +{ + return 32 * tx_supplement.output_enote_ephemeral_pubkeys.size() + tx_supplement.tx_extra.size(); +} +//------------------------------------------------------------------------------------------------------------------- +bool operator==(const SpCoinbaseEnoteV1 &a, const SpCoinbaseEnoteV1 &b) +{ + return a.core == b.core && + a.addr_tag_enc == b.addr_tag_enc && + a.view_tag == b.view_tag; +} +//------------------------------------------------------------------------------------------------------------------- +bool operator==(const SpEnoteV1 &a, const SpEnoteV1 &b) +{ + return a.core == b.core && + a.encoded_amount == b.encoded_amount && + a.addr_tag_enc == b.addr_tag_enc && + a.view_tag == b.view_tag; +} +//------------------------------------------------------------------------------------------------------------------- +bool operator==(const SpEnoteVariant &variant1, const SpEnoteVariant &variant2) +{ + // check they have the same type + if (!SpEnoteVariant::same_type(variant1, variant2)) + return false; + + // use a visitor to test equality + struct visitor final : public tools::variant_static_visitor + { + visitor(const SpEnoteVariant &other_ref) : other{other_ref} {} + const SpEnoteVariant &other; + + using variant_static_visitor::operator(); //for blank overload + bool operator()(const SpCoinbaseEnoteV1 &enote) const { return enote == other.unwrap(); } + bool operator()(const SpEnoteV1 &enote) const { return enote == other.unwrap(); } + }; + + return variant1.visit(visitor{variant2}); +} +//------------------------------------------------------------------------------------------------------------------- +bool compare_Ko(const SpCoinbaseEnoteV1 &a, const SpCoinbaseEnoteV1 &b) +{ + return compare_Ko(a.core, b.core); +} +//------------------------------------------------------------------------------------------------------------------- +bool compare_Ko(const SpEnoteV1 &a, const SpEnoteV1 &b) +{ + return compare_Ko(a.core, b.core); +} +//------------------------------------------------------------------------------------------------------------------- +bool compare_KI(const SpEnoteImageV1 &a, const SpEnoteImageV1 &b) +{ + return compare_KI(a.core, b.core); +} +//------------------------------------------------------------------------------------------------------------------- +SpCoinbaseEnoteV1 gen_sp_coinbase_enote_v1() +{ + SpCoinbaseEnoteV1 temp; + + // gen base of enote + temp.core = gen_sp_coinbase_enote_core(); + + // extra pieces + crypto::rand(sizeof(jamtis::encrypted_address_tag_t), temp.addr_tag_enc.bytes); + temp.view_tag = crypto::rand_idx(0); + + return temp; +} +//------------------------------------------------------------------------------------------------------------------- +SpEnoteV1 gen_sp_enote_v1() +{ + SpEnoteV1 temp; + + // gen base of enote + temp.core = gen_sp_enote_core(); + + // extra pieces + crypto::rand(sizeof(temp.encoded_amount), temp.encoded_amount.bytes); + crypto::rand(sizeof(jamtis::encrypted_address_tag_t), temp.addr_tag_enc.bytes); + temp.view_tag = crypto::rand_idx(0); + + return temp; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace sp diff --git a/src/seraphis_main/tx_component_types.h b/src/seraphis_main/tx_component_types.h new file mode 100644 index 0000000000..f63864e60e --- /dev/null +++ b/src/seraphis_main/tx_component_types.h @@ -0,0 +1,240 @@ +// Copyright (c) 2022, 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 transaction component types. + +#pragma once + +//local headers +#include "common/variant.h" +#include "crypto/crypto.h" +#include "crypto/x25519.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/binned_reference_set.h" +#include "seraphis_core/jamtis_support_types.h" +#include "seraphis_core/sp_core_types.h" +#include "seraphis_core/tx_extra.h" +#include "seraphis_crypto/bulletproofs_plus2.h" +#include "seraphis_crypto/grootle.h" +#include "seraphis_crypto/sp_composition_proof.h" + +//third party headers +#include + +//standard headers + +//forward declarations +namespace sp { class SpTranscriptBuilder; } + +namespace sp +{ + +//// +// SpCoinbaseEnoteV1 +/// +struct SpCoinbaseEnoteV1 final +{ + /// enote core (onetime address, amount) + SpCoinbaseEnoteCore core; + + /// addr_tag_enc + jamtis::encrypted_address_tag_t addr_tag_enc; + /// view_tag + jamtis::view_tag_t view_tag; +}; +inline const boost::string_ref container_name(const SpCoinbaseEnoteV1&) { return "SpCoinbaseEnoteV1"; } +void append_to_transcript(const SpCoinbaseEnoteV1 &container, SpTranscriptBuilder &transcript_inout); + +/// get the size in bytes +std::size_t sp_coinbase_enote_v1_size_bytes(); + +//// +// SpEnoteV1 +/// +struct SpEnoteV1 final +{ + /// enote core (onetime address, amount commitment) + SpEnoteCore core; + + /// enc(a) + jamtis::encoded_amount_t encoded_amount; + /// addr_tag_enc + jamtis::encrypted_address_tag_t addr_tag_enc; + /// view_tag + jamtis::view_tag_t view_tag; +}; +inline const boost::string_ref container_name(const SpEnoteV1&) { return "SpEnoteV1"; } +void append_to_transcript(const SpEnoteV1 &container, SpTranscriptBuilder &transcript_inout); + +/// get the size in bytes +std::size_t sp_enote_v1_size_bytes(); + +//// +// SpEnoteVariant +// - variant of all seraphis enote types +// +// core_ref(): get a copy of the enote's core +// onetime_address_ref(): get the enote's onetime address +// amount_commitment_ref(): get the enote's amount commitment (this is a copy because coinbase enotes need to +// compute the commitment) +// addr_tag_enc_ref(): get the enote's encrypted address tag +// view_tag_ref(): get the enote's view tag (copies are cheap) +/// +using SpEnoteVariant = tools::variant; +SpEnoteCoreVariant core_ref(const SpEnoteVariant &variant); +const rct::key& onetime_address_ref(const SpEnoteVariant &variant); +rct::key amount_commitment_ref(const SpEnoteVariant &variant); +const jamtis::encrypted_address_tag_t& addr_tag_enc_ref(const SpEnoteVariant &variant); +jamtis::view_tag_t view_tag_ref(const SpEnoteVariant &variant); + +//// +// SpEnoteImageV1 +/// +struct SpEnoteImageV1 final +{ + /// enote image core (masked address, masked amount commitment, key image) + SpEnoteImageCore core; +}; +inline const boost::string_ref container_name(const SpEnoteImageV1&) { return "SpEnoteImageV1"; } +void append_to_transcript(const SpEnoteImageV1 &container, SpTranscriptBuilder &transcript_inout); + +/// get size in bytes +inline std::size_t sp_enote_image_v1_size_bytes() { return sp_enote_image_core_size_bytes(); } + +/// get the image components +const crypto::key_image& key_image_ref(const SpEnoteImageV1 &enote_image); +const rct::key& masked_address_ref(const SpEnoteImageV1 &enote_image); +const rct::key& masked_commitment_ref(const SpEnoteImageV1 &enote_image); + +//// +// SpMembershipProofV1 +// - grootle proof +/// +struct SpMembershipProofV1 final +{ + /// a grootle proof + GrootleProof grootle_proof; + /// binned representation of ledger indices of enotes referenced by the proof + SpBinnedReferenceSetV1 binned_reference_set; + /// ref set size = n^m + std::size_t ref_set_decomp_n; + std::size_t ref_set_decomp_m; +}; +inline const boost::string_ref container_name(const SpMembershipProofV1&) { return "SpMembershipProofV1"; } +void append_to_transcript(const SpMembershipProofV1 &container, SpTranscriptBuilder &transcript_inout); + +/// get size in bytes +/// - note: compact version excludes the decomposition parameters, and uses the compact size of the binned ref set +std::size_t sp_membership_proof_v1_size_bytes(const std::size_t n, + const std::size_t m, + const std::size_t num_bin_members); +std::size_t sp_membership_proof_v1_size_bytes_compact(const std::size_t n, + const std::size_t m, + const std::size_t num_bin_members); +std::size_t sp_membership_proof_v1_size_bytes(const SpMembershipProofV1 &proof); +std::size_t sp_membership_proof_v1_size_bytes_compact(const SpMembershipProofV1 &proof); + +//// +// SpImageProofV1 +// - ownership and legitimacy of the key image +// - seraphis composition proof +/// +struct SpImageProofV1 final +{ + /// a seraphis composition proof + SpCompositionProof composition_proof; +}; +inline const boost::string_ref container_name(const SpImageProofV1&) { return "SpImageProofV1"; } +void append_to_transcript(const SpImageProofV1 &container, SpTranscriptBuilder &transcript_inout); + +/// get size in bytes +inline std::size_t sp_image_proof_v1_size_bytes() { return sp_composition_size_bytes(); } + +//// +// SpBalanceProofV1 +// - balance proof: implicit with a remainder blinding factor: [sum(inputs) == sum(outputs) + remainder_blinding_factor*G] +// - range proofs: Bulletproofs+ v2 +/// +struct SpBalanceProofV1 final +{ + /// an aggregate set of BP+ proofs + BulletproofPlus2 bpp2_proof; + /// the remainder blinding factor + rct::key remainder_blinding_factor; +}; +inline const boost::string_ref container_name(const SpBalanceProofV1&) { return "SpBalanceProofV1"; } +void append_to_transcript(const SpBalanceProofV1 &container, SpTranscriptBuilder &transcript_inout); + +/// get the size in bytes +/// - note: the compact version does not include the bulletproof's cached amount commitments +std::size_t sp_balance_proof_v1_size_bytes(const std::size_t num_range_proofs); +std::size_t sp_balance_proof_v1_size_bytes(const SpBalanceProofV1 &proof); +std::size_t sp_balance_proof_v1_size_bytes_compact(const std::size_t num_range_proofs); +std::size_t sp_balance_proof_v1_size_bytes_compact(const SpBalanceProofV1 &proof); +/// get the proof weight (using compact size) +std::size_t sp_balance_proof_v1_weight(const std::size_t num_range_proofs); +std::size_t sp_balance_proof_v1_weight(const SpBalanceProofV1 &proof); + +//// +// SpTxSupplementV1 +// - supplementary info about a tx +// - enote ephemeral pubkeys (stored here instead of in enotes since enotes can share them) +// - tx memo +/// +struct SpTxSupplementV1 final +{ + /// xKe: enote ephemeral pubkeys for outputs + std::vector output_enote_ephemeral_pubkeys; + /// tx memo + TxExtra tx_extra; +}; +inline const boost::string_ref container_name(const SpTxSupplementV1&) { return "SpTxSupplementV1"; } +void append_to_transcript(const SpTxSupplementV1 &container, SpTranscriptBuilder &transcript_inout); + +/// get the size in bytes +std::size_t sp_tx_supplement_v1_size_bytes(const std::size_t num_outputs, + const std::size_t tx_extra_size, + const bool use_shared_ephemeral_key_assumption); +std::size_t sp_tx_supplement_v1_size_bytes(const SpTxSupplementV1 &tx_supplement); + +/// comparison operator for equivalence testing +bool operator==(const SpCoinbaseEnoteV1 &a, const SpCoinbaseEnoteV1 &b); +bool operator==(const SpEnoteV1 &a, const SpEnoteV1 &b); +bool operator==(const SpEnoteVariant &variant1, const SpEnoteVariant &variant2); +/// comparison method for sorting: a.Ko < b.Ko +bool compare_Ko(const SpCoinbaseEnoteV1 &a, const SpCoinbaseEnoteV1 &b); +bool compare_Ko(const SpEnoteV1 &a, const SpEnoteV1 &b); +/// comparison method for sorting: a.KI < b.KI +bool compare_KI(const SpEnoteImageV1 &a, const SpEnoteImageV1 &b); + +/// generate a dummy v1 coinbase enote +SpCoinbaseEnoteV1 gen_sp_coinbase_enote_v1(); +/// generate a dummy v1 enote +SpEnoteV1 gen_sp_enote_v1(); + +} //namespace sp diff --git a/src/seraphis_main/tx_component_types_legacy.cpp b/src/seraphis_main/tx_component_types_legacy.cpp new file mode 100644 index 0000000000..8e65d2a131 --- /dev/null +++ b/src/seraphis_main/tx_component_types_legacy.cpp @@ -0,0 +1,78 @@ +// Copyright (c) 2022, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//paired header +#include "tx_component_types_legacy.h" + +//local headers +#include "misc_log_ex.h" +#include "seraphis_crypto/sp_legacy_proof_helpers.h" +#include "seraphis_crypto/sp_transcript.h" + +//third party headers + +//standard headers + + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis" + +namespace sp +{ +//------------------------------------------------------------------------------------------------------------------- +void append_to_transcript(const LegacyEnoteImageV2 &container, SpTranscriptBuilder &transcript_inout) +{ + transcript_inout.append("C_masked", container.masked_commitment); + transcript_inout.append("KI", container.key_image); +} +//------------------------------------------------------------------------------------------------------------------- +void append_to_transcript(const LegacyRingSignatureV4 &container, SpTranscriptBuilder &transcript_inout) +{ + append_clsag_to_transcript(container.clsag_proof, transcript_inout); + transcript_inout.append("reference_set", container.reference_set); +} +//------------------------------------------------------------------------------------------------------------------- +std::size_t legacy_ring_signature_v4_size_bytes(const std::size_t num_ring_members) +{ + return clsag_size_bytes(num_ring_members) + num_ring_members * 8; +} +//------------------------------------------------------------------------------------------------------------------- +std::size_t legacy_ring_signature_v4_size_bytes(const LegacyRingSignatureV4 &ring_signature) +{ + CHECK_AND_ASSERT_THROW_MES(ring_signature.clsag_proof.s.size() == ring_signature.reference_set.size(), + "legacy ring signature v4 size: clsag proof doesn't match reference set size."); + + return legacy_ring_signature_v4_size_bytes(ring_signature.reference_set.size()); +} +//------------------------------------------------------------------------------------------------------------------- +bool compare_KI(const LegacyEnoteImageV2 &a, const LegacyEnoteImageV2 &b) +{ + return a.key_image < b.key_image; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace sp diff --git a/src/seraphis_main/tx_component_types_legacy.h b/src/seraphis_main/tx_component_types_legacy.h new file mode 100644 index 0000000000..d1cf12d25c --- /dev/null +++ b/src/seraphis_main/tx_component_types_legacy.h @@ -0,0 +1,108 @@ +// Copyright (c) 2022, 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 transaction component types. + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "ringct/rctTypes.h" + +//third party headers +#include + +//standard headers +#include + +//forward declarations +namespace sp { class SpTranscriptBuilder; } + +namespace sp +{ + +//// +// LegacyEnoteImageV1: not used in seraphis +// - key image only +/// + +//// +// LegacyEnoteImageV2 +// - masked commitment +// - key image +/// +struct LegacyEnoteImageV2 final +{ + /// masked commitment (aka 'pseudo-output commitment') + rct::key masked_commitment; + /// legacy key image + crypto::key_image key_image; +}; +inline const boost::string_ref container_name(const LegacyEnoteImageV2&) { return "LegacyEnoteImageV2"; } +void append_to_transcript(const LegacyEnoteImageV2 &container, SpTranscriptBuilder &transcript_inout); + +/// get the size in bytes +inline std::size_t legacy_enote_image_v2_size_bytes() { return 32 + 32; } + +//// +// LegacyRingSignatureV1: not used in seraphis +// - Cryptonote ring signature (using LegacyEnoteImageV1) +/// + +//// +// LegacyRingSignatureV2: not used in seraphis +// - MLSAG combined inputs (using LegacyEnoteImageV2) +/// + +//// +// LegacyRingSignatureV3: not used in seraphis +// - MLSAG split inputs (using LegacyEnoteImageV2) +/// + +//// +// LegacyRingSignatureV4 +// - CLSAG (using LegacyEnoteImageV2) +/// +struct LegacyRingSignatureV4 final +{ + /// a clsag proof + rct::clsag clsag_proof; + /// on-chain indices of the proof's ring members + std::vector reference_set; +}; +inline const boost::string_ref container_name(const LegacyRingSignatureV4&) { return "LegacyRingSignatureV4"; } +void append_to_transcript(const LegacyRingSignatureV4 &container, SpTranscriptBuilder &transcript_inout); + +/// get the size in bytes +std::size_t legacy_ring_signature_v4_size_bytes(const std::size_t num_ring_members); +std::size_t legacy_ring_signature_v4_size_bytes(const LegacyRingSignatureV4 &ring_signature); + +/// comparison method for sorting: a.KI < b.KI +bool compare_KI(const LegacyEnoteImageV2 &a, const LegacyEnoteImageV2 &b); + +} //namespace sp diff --git a/src/seraphis_main/tx_fee_calculator.h b/src/seraphis_main/tx_fee_calculator.h new file mode 100644 index 0000000000..f5cd9b788a --- /dev/null +++ b/src/seraphis_main/tx_fee_calculator.h @@ -0,0 +1,64 @@ +// Copyright (c) 2022, 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. + +// Interface for calculating a tx's fee given a fee/weight ratio and number of inputs/outputs. + +#pragma once + +//local headers +#include "ringct/rctTypes.h" + +//third party headers + +//standard headers +#include + +//forward declarations + + +namespace sp +{ + +class FeeCalculator +{ +public: +//destructor + virtual ~FeeCalculator() = default; + +//overloaded operators + /// disable copy/move (this is a pure virtual base class) + FeeCalculator& operator=(FeeCalculator&&) = delete; + +//member functions + virtual rct::xmr_amount compute_fee(const std::size_t fee_per_weight, + const std::size_t num_legacy_inputs, + const std::size_t num_sp_inputs, + const std::size_t num_outputs) const = 0; +}; + +} //namespace sp diff --git a/src/seraphis_main/tx_input_selection.cpp b/src/seraphis_main/tx_input_selection.cpp new file mode 100644 index 0000000000..4b63b7fda4 --- /dev/null +++ b/src/seraphis_main/tx_input_selection.cpp @@ -0,0 +1,848 @@ +// Copyright (c) 2022, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//paired header +#include "tx_input_selection.h" + +//local headers +#include "contextual_enote_record_types.h" +#include "misc_log_ex.h" +#include "ringct/rctTypes.h" +#include "tx_fee_calculator.h" +#include "tx_input_selection_output_context.h" + +//third party headers +#include "boost/container/map.hpp" +#include "boost/multiprecision/cpp_int.hpp" + +//standard headers +#include +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis" + +namespace sp +{ + +struct InputSelectionTypePair +{ + InputSelectionType added; + InputSelectionType candidate; +}; + +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static InputSelectionType input_selection_type(const ContextualRecordVariant &contextual_enote_record) +{ + struct visitor final : public tools::variant_static_visitor + { + using variant_static_visitor::operator(); //for blank overload + InputSelectionType operator()(const LegacyContextualEnoteRecordV1&) const { return InputSelectionType::LEGACY; } + InputSelectionType operator()(const SpContextualEnoteRecordV1&) const { return InputSelectionType::SERAPHIS; } + }; + + return contextual_enote_record.visit(visitor{}); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static std::size_t count_records(const input_set_tracker_t &input_set, const InputSelectionType type) +{ + if (input_set.find(type) == input_set.end()) + return 0; + + return input_set.at(type).size(); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static std::size_t total_inputs(const input_set_tracker_t &input_set) +{ + return count_records(input_set, InputSelectionType::LEGACY) + + count_records(input_set, InputSelectionType::SERAPHIS); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static rct::xmr_amount worst_amount_in_map( + const boost::container::multimap &map) +{ + if (map.size() == 0) + return 0; + + return map.begin()->first; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static rct::xmr_amount best_amount_in_map( + const boost::container::multimap &map) +{ + if (map.size() == 0) + return 0; + + return map.rbegin()->first; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static boost::multiprecision::uint128_t compute_total_amount(const input_set_tracker_t &input_set) +{ + boost::multiprecision::uint128_t amount_sum{0}; + + const auto legacy_set_it = input_set.find(InputSelectionType::LEGACY); + if (legacy_set_it != input_set.end()) + { + for (const auto &mapped_record : legacy_set_it->second) + amount_sum += mapped_record.first; + } + + const auto sp_set_it = input_set.find(InputSelectionType::SERAPHIS); + if (sp_set_it != input_set.end()) + { + for (const auto &mapped_record : sp_set_it->second) + amount_sum += mapped_record.first; + } + + return amount_sum; +} +//------------------------------------------------------------------------------------------------------------------- +// differential fee from removing one record of the specified type from the input set +//------------------------------------------------------------------------------------------------------------------- +static rct::xmr_amount diff_fee_of_removing_record(const input_set_tracker_t &input_set, + const InputSelectionType type, + const rct::xmr_amount fee_per_tx_weight, + const FeeCalculator &tx_fee_calculator, + const std::size_t num_outputs) +{ + if (count_records(input_set, type) == 0) + return -1; + + const std::size_t num_legacy_inputs_initial{count_records(input_set, InputSelectionType::LEGACY)}; + const std::size_t num_sp_inputs_initial{count_records(input_set, InputSelectionType::SERAPHIS)}; + const bool type_is_legacy{type == InputSelectionType::LEGACY}; + + const rct::xmr_amount initial_fee{ + tx_fee_calculator.compute_fee(fee_per_tx_weight, + num_legacy_inputs_initial, + num_sp_inputs_initial, + num_outputs) + }; + const rct::xmr_amount fee_after_input_removed{ + tx_fee_calculator.compute_fee(fee_per_tx_weight, + num_legacy_inputs_initial - (type_is_legacy ? 1 : 0), + num_sp_inputs_initial - (!type_is_legacy ? 1 : 0), + num_outputs) + }; + + CHECK_AND_ASSERT_THROW_MES(initial_fee >= fee_after_input_removed, + "input selection (diff fee of removing record): initial fee is lower than fee after input removed."); + + return initial_fee - fee_after_input_removed; +} +//------------------------------------------------------------------------------------------------------------------- +// differential fee from adding one record of the specified type to the input set +//------------------------------------------------------------------------------------------------------------------- +static rct::xmr_amount diff_fee_of_adding_record(const input_set_tracker_t &input_set, + const InputSelectionType type, + const rct::xmr_amount fee_per_tx_weight, + const FeeCalculator &tx_fee_calculator, + const std::size_t num_outputs) +{ + const std::size_t num_legacy_inputs_initial{count_records(input_set, InputSelectionType::LEGACY)}; + const std::size_t num_sp_inputs_initial{count_records(input_set, InputSelectionType::SERAPHIS)}; + const bool type_is_legacy{type == InputSelectionType::LEGACY}; + + const rct::xmr_amount initial_fee{ + tx_fee_calculator.compute_fee(fee_per_tx_weight, + num_legacy_inputs_initial, + num_sp_inputs_initial, + num_outputs) + }; + const rct::xmr_amount fee_after_input_added{ + tx_fee_calculator.compute_fee(fee_per_tx_weight, + num_legacy_inputs_initial + (type_is_legacy ? 1 : 0), + num_sp_inputs_initial + (!type_is_legacy ? 1 : 0), + num_outputs) + }; + + CHECK_AND_ASSERT_THROW_MES(fee_after_input_added >= initial_fee, + "input selection (diff fee of adding record): initial fee is greater than fee after input added."); + + return fee_after_input_added - initial_fee; +} +//------------------------------------------------------------------------------------------------------------------- +// differential fee from adding a record of one type to the input set after removing a record of another type +//------------------------------------------------------------------------------------------------------------------- +static rct::xmr_amount diff_fee_of_replacing_record(const input_set_tracker_t &input_set, + const InputSelectionType type_to_remove, + const InputSelectionType type_to_add, + const rct::xmr_amount fee_per_tx_weight, + const FeeCalculator &tx_fee_calculator, + const std::size_t num_outputs) +{ + if (count_records(input_set, type_to_remove) == 0) + return -1; + + // 1. calculate fee after input is removed + const bool removed_type_is_legacy{type_to_add == InputSelectionType::LEGACY}; + const std::size_t num_legacy_inputs_removed{ + count_records(input_set, InputSelectionType::LEGACY) - (removed_type_is_legacy ? 1 : 0) + }; + const std::size_t num_sp_inputs_removed{ + count_records(input_set, InputSelectionType::SERAPHIS) - (!removed_type_is_legacy ? 1 : 0) + }; + + const rct::xmr_amount fee_after_input_removed{ + tx_fee_calculator.compute_fee(fee_per_tx_weight, + num_legacy_inputs_removed, + num_sp_inputs_removed, + num_outputs) + }; + + // 2. calculate fee after input is added (after the removal step) + const bool new_type_is_legacy{type_to_add == InputSelectionType::LEGACY}; + const rct::xmr_amount fee_after_input_added{ + tx_fee_calculator.compute_fee(fee_per_tx_weight, + num_legacy_inputs_removed + (new_type_is_legacy ? 1 : 0), + num_sp_inputs_removed + (!new_type_is_legacy ? 1 : 0), + num_outputs) + }; + + // 3. return the marginal fee of the new input compared to before it was added + CHECK_AND_ASSERT_THROW_MES(fee_after_input_added >= fee_after_input_removed, + "input selection (fee of replacing record): new fee is lower than fee after input removed."); + + return fee_after_input_added - fee_after_input_removed; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool try_exclude_useless_input_of_type_v1(const InputSelectionType type, + const rct::xmr_amount fee_per_tx_weight, + const FeeCalculator &tx_fee_calculator, + const std::size_t num_outputs, + input_set_tracker_t &added_inputs_inout, + input_set_tracker_t &candidate_inputs_inout) +{ + // 1. fail if no added inputs to remove + if (count_records(added_inputs_inout, type) == 0) + return false; + + // 2. get the differential fee of the last input of the specified type + const rct::xmr_amount last_input_fee{ + diff_fee_of_removing_record(added_inputs_inout, + type, + fee_per_tx_weight, + tx_fee_calculator, + num_outputs) + }; + const rct::xmr_amount lowest_input_amount{ + worst_amount_in_map(added_inputs_inout.at(type)) + }; + + // 3. don't exclude if the smallest-amount input can cover its own differential fee + if (lowest_input_amount > last_input_fee) + return false; + + // 4. remove the input + candidate_inputs_inout[type].insert( + added_inputs_inout[type].extract(lowest_input_amount) + ); + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool try_update_added_inputs_exclude_useless_v1(const rct::xmr_amount fee_per_tx_weight, + const FeeCalculator &tx_fee_calculator, + const std::size_t num_outputs, + input_set_tracker_t &added_inputs_inout, + input_set_tracker_t &candidate_inputs_inout) +{ + // 1. fail if no added inputs to remove + const std::size_t total_inputs_initial{total_inputs(added_inputs_inout)}; + if (total_inputs_initial == 0) + return false; + + // 2. remove all useless added inputs + // - useless = an input doesn't exceed its own differential fee + std::size_t previous_total_inputs; + + do + { + previous_total_inputs = total_inputs(added_inputs_inout); + + // a. exclude useless legacy input + try_exclude_useless_input_of_type_v1(InputSelectionType::LEGACY, + fee_per_tx_weight, + tx_fee_calculator, + num_outputs, + added_inputs_inout, + candidate_inputs_inout); + + // b. exclude useless seraphis input + try_exclude_useless_input_of_type_v1(InputSelectionType::SERAPHIS, + fee_per_tx_weight, + tx_fee_calculator, + num_outputs, + added_inputs_inout, + candidate_inputs_inout); + } while (previous_total_inputs > total_inputs(added_inputs_inout)); + + // 3. fail if no inputs excluded + if (total_inputs(added_inputs_inout) == total_inputs_initial) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool try_swap_pair_v1(const InputSelectionType added_type_to_remove, + const InputSelectionType candidate_type_to_add, + const rct::xmr_amount fee_per_tx_weight, + const FeeCalculator &tx_fee_calculator, + const std::size_t num_outputs, + input_set_tracker_t &added_inputs_inout, + input_set_tracker_t &candidate_inputs_inout) +{ + // 1. fail if swap isn't possible + if (count_records(added_inputs_inout, added_type_to_remove) == 0 || + count_records(candidate_inputs_inout, candidate_type_to_add) == 0) + return false; + + // 2. differential fee from removing lowest-amount added + const boost::multiprecision::uint128_t differential_fee_replaceable{ + diff_fee_of_removing_record(added_inputs_inout, + added_type_to_remove, + fee_per_tx_weight, + tx_fee_calculator, + num_outputs) + }; + + // 3. differential fee from adding highest-amount candidate after added is removed + const boost::multiprecision::uint128_t differential_fee_candidate{ + diff_fee_of_replacing_record(added_inputs_inout, + added_type_to_remove, + candidate_type_to_add, + fee_per_tx_weight, + tx_fee_calculator, + num_outputs) + }; + + // 3. fail if this combination is not an improvement over the current added set + // replaceable_amnt - added_fee >= candidate_amnt - candidate_fee + // replaceable_amnt + candidate_fee >= candidate_amnt + added_fee (no overflow on subtraction) + const boost::multiprecision::uint128_t candidate_combination_cost{ + worst_amount_in_map(added_inputs_inout.at(added_type_to_remove)) + differential_fee_candidate + }; + const boost::multiprecision::uint128_t candidate_combination_reward{ + best_amount_in_map(candidate_inputs_inout.at(candidate_type_to_add)) + differential_fee_replaceable + }; + if (candidate_combination_cost >= candidate_combination_reward) + return false; + + // 4. swap + auto worst_added_input = + added_inputs_inout[added_type_to_remove].extract( + worst_amount_in_map(added_inputs_inout.at(added_type_to_remove)) + ); + auto best_candidate_input = + candidate_inputs_inout[candidate_type_to_add].extract( + best_amount_in_map(candidate_inputs_inout.at(candidate_type_to_add)) + ); + + added_inputs_inout[candidate_type_to_add].insert(std::move(best_candidate_input)); + candidate_inputs_inout[added_type_to_remove].insert(std::move(worst_added_input)); + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool try_update_added_inputs_replace_candidate_v1(const rct::xmr_amount fee_per_tx_weight, + const FeeCalculator &tx_fee_calculator, + const std::size_t num_outputs, + input_set_tracker_t &added_inputs_inout, + input_set_tracker_t &candidate_inputs_inout) +{ + // 1. fail if no added or candidate inputs + if (total_inputs(added_inputs_inout) == 0 || + total_inputs(candidate_inputs_inout) == 0) + return false; + + // 2. search for the best solution when removing one added input and adding one candidate input + // note: only perform one actual swap in case one swap is sufficient to solve the input selection game + bool found_replacement_combination{false}; + std::list test_combinations = + { + {InputSelectionType::LEGACY, InputSelectionType::LEGACY}, + {InputSelectionType::LEGACY, InputSelectionType::SERAPHIS}, + {InputSelectionType::SERAPHIS, InputSelectionType::LEGACY}, + {InputSelectionType::SERAPHIS, InputSelectionType::SERAPHIS} + }; + + for (const InputSelectionTypePair &test_combination : test_combinations) + { + // fall-through once a swap succeeds + found_replacement_combination = found_replacement_combination || + try_swap_pair_v1(test_combination.added, + test_combination.candidate, + fee_per_tx_weight, + tx_fee_calculator, + num_outputs, + added_inputs_inout, + candidate_inputs_inout); + } + + // 3. fail if no swaps occurred + if (!found_replacement_combination) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool try_add_candidate_of_type_v1(const InputSelectionType type, + const std::size_t max_inputs_allowed, + const rct::xmr_amount fee_per_tx_weight, + const FeeCalculator &tx_fee_calculator, + const std::size_t num_outputs, + input_set_tracker_t &added_inputs_inout, + input_set_tracker_t &candidate_inputs_inout) +{ + // 1. expect the inputs to not be full here + if (total_inputs(added_inputs_inout) >= max_inputs_allowed) + return false; + + // 2. fail if no candidate inputs available of the specified type + if (count_records(candidate_inputs_inout, type) == 0) + return false; + + // 3. get the differential fee and amount of the best candidate + const rct::xmr_amount next_input_fee_of_type{ + diff_fee_of_adding_record(added_inputs_inout, + type, + fee_per_tx_weight, + tx_fee_calculator, + num_outputs) + }; + const rct::xmr_amount best_candidate_amount_of_type{ + best_amount_in_map(candidate_inputs_inout.at(type)) + }; + + // 4. fail if the best candidate doesn't exceed the differential fee of adding it + if (next_input_fee_of_type >= best_candidate_amount_of_type) + return false; + + // 5. add the best candidate of this type + added_inputs_inout[type].insert( + candidate_inputs_inout[type].extract(best_candidate_amount_of_type) + ); + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool try_update_added_inputs_add_candidate_v1(const std::size_t max_inputs_allowed, + const rct::xmr_amount fee_per_tx_weight, + const FeeCalculator &tx_fee_calculator, + const std::size_t num_outputs, + input_set_tracker_t &added_inputs_inout, + input_set_tracker_t &candidate_inputs_inout) +{ + // 1. expect the inputs to not be full here + if (total_inputs(added_inputs_inout) >= max_inputs_allowed) + return false; + + // 2. fail if no candidate inputs available + if (total_inputs(candidate_inputs_inout) == 0) + return false; + + // 3. try to acquire a useful legacy input candidate + if (try_add_candidate_of_type_v1(InputSelectionType::LEGACY, + max_inputs_allowed, + fee_per_tx_weight, + tx_fee_calculator, + num_outputs, + added_inputs_inout, + candidate_inputs_inout)) + return true; + + // 4. try to acquire a useful seraphis input candidate + if (try_add_candidate_of_type_v1(InputSelectionType::SERAPHIS, + max_inputs_allowed, + fee_per_tx_weight, + tx_fee_calculator, + num_outputs, + added_inputs_inout, + candidate_inputs_inout)) + return true; + + return false; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool try_update_candidate_inputs_selection_v1(const boost::multiprecision::uint128_t output_amount, + const InputSelectorV1 &input_selector, + const rct::xmr_amount fee_per_tx_weight, + const FeeCalculator &tx_fee_calculator, + const std::size_t num_outputs, + const input_set_tracker_t &added_inputs, + input_set_tracker_t &candidate_inputs_inout) +{ + // 1. get current record parameters of the added inputs set + const std::size_t num_legacy_inputs{count_records(added_inputs, InputSelectionType::LEGACY)}; + const std::size_t num_sp_inputs{count_records(added_inputs, InputSelectionType::SERAPHIS)}; + + const rct::xmr_amount current_fee{ + tx_fee_calculator.compute_fee(fee_per_tx_weight, + num_legacy_inputs, + num_sp_inputs, + num_outputs) + }; + + // 2. get the reference amount for the input selection algorithm + // - this is only the current amount needed; the final amount will likely be higher due to a higher fee from + // adding more inputs + const boost::multiprecision::uint128_t selection_amount{output_amount + current_fee}; + + // 3. try to get a new input candidate from the selector + ContextualRecordVariant input_candidate; + + if (!input_selector.try_select_input_candidate_v1(selection_amount, + added_inputs, + candidate_inputs_inout, + input_candidate)) + return false; + + // 4. save the new candidate input - we will try to move it into the added pile in later passthroughs + candidate_inputs_inout[input_selection_type(input_candidate)].insert( + input_set_tracker_t::mapped_type::value_type{amount_ref(input_candidate), std::move(input_candidate)} + ); + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool try_add_inputs_range_of_type_v1(const InputSelectionType type, + const std::size_t max_inputs_allowed, + const rct::xmr_amount fee_per_tx_weight, + const FeeCalculator &tx_fee_calculator, + const std::size_t num_outputs, + input_set_tracker_t &added_inputs_inout, + input_set_tracker_t &candidate_inputs_inout) +{ + // 1. current tx fee + const std::size_t initial_inputs_count{total_inputs(added_inputs_inout)}; + std::size_t num_legacy_inputs{count_records(added_inputs_inout, InputSelectionType::LEGACY)}; + std::size_t num_sp_inputs{count_records(added_inputs_inout, InputSelectionType::SERAPHIS)}; + + const rct::xmr_amount current_fee{ + tx_fee_calculator.compute_fee(fee_per_tx_weight, + num_legacy_inputs, + num_sp_inputs, + num_outputs) + }; + + // 2. try to add a range of candidate inputs + boost::multiprecision::uint128_t range_sum{0}; + std::size_t range_size{0}; + + for (auto candidate_it = candidate_inputs_inout[type].rbegin(); + candidate_it != candidate_inputs_inout[type].rend(); + ++candidate_it) + { + range_sum += candidate_it->first; + ++range_size; + + // a. we have failed if our range exceeds the input limit + if (initial_inputs_count + range_size > max_inputs_allowed) + return false; + + // b. total fee including this range of inputs + if (type == InputSelectionType::LEGACY) + ++num_legacy_inputs; + else + ++num_sp_inputs; + + const rct::xmr_amount range_fee{ + tx_fee_calculator.compute_fee(fee_per_tx_weight, + num_legacy_inputs, + num_sp_inputs, + num_outputs) + }; + + // c. if range of candidate inputs can exceed the differential fee from those inputs, add them + CHECK_AND_ASSERT_THROW_MES(range_fee >= current_fee, + "input selection (candidate range): range fee is less than current fee (bug)."); + + if (range_sum > range_fee - current_fee) + { + for (std::size_t num_moved{0}; num_moved < range_size; ++num_moved) + { + CHECK_AND_ASSERT_THROW_MES(candidate_inputs_inout[type].size() != 0, + "input selection (candidate range): candidate inputs range smaller than expected (bug)."); + + added_inputs_inout[type].insert( + candidate_inputs_inout[type].extract(best_amount_in_map(candidate_inputs_inout[type])) + ); + } + + return true; + } + } + + return false; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool try_update_added_inputs_range_v1(const std::size_t max_inputs_allowed, + const rct::xmr_amount fee_per_tx_weight, + const FeeCalculator &tx_fee_calculator, + const std::size_t num_outputs, + input_set_tracker_t &added_inputs_inout, + input_set_tracker_t &candidate_inputs_inout) +{ + // note: this algorithm assumes only a range of same-type inputs can produce a solution; there may be range solutions + // created by combinations of legacy/seraphis inputs, but since discovering those is a brute force exercise, + // they are ignored here; in general, as seraphis enotes become relatively more common than legacy enotes, this + // algorithm is expected to produce relatively fewer false negatives + // note2: this algorithm also assumes there is no case where a range of added inputs might be usefully _replaced_ with + // a range of candidate inputs (if this case exists at all, it's probably a very niche edge-case) + + // 1. expect the added inputs list is not full + if (total_inputs(added_inputs_inout) >= max_inputs_allowed) + return false; + + // 2. try to add a range of candidate legacy inputs + if (try_add_inputs_range_of_type_v1(InputSelectionType::LEGACY, + max_inputs_allowed, + fee_per_tx_weight, + tx_fee_calculator, + num_outputs, + added_inputs_inout, + candidate_inputs_inout)) + return true; + + // 3. try to add a range of candidate seraphis inputs + if (try_add_inputs_range_of_type_v1(InputSelectionType::SERAPHIS, + max_inputs_allowed, + fee_per_tx_weight, + tx_fee_calculator, + num_outputs, + added_inputs_inout, + candidate_inputs_inout)) + return true; + + return false; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool try_select_inputs_v1(const boost::multiprecision::uint128_t output_amount, + const std::size_t max_inputs_allowed, + const InputSelectorV1 &input_selector, + const rct::xmr_amount fee_per_tx_weight, + const FeeCalculator &tx_fee_calculator, + const std::size_t num_outputs, + input_set_tracker_t initial_input_set, + input_set_tracker_t &input_set_out) +{ + CHECK_AND_ASSERT_THROW_MES(max_inputs_allowed > 0, "input selection: zero inputs were allowed."); + input_set_out.clear(); + + // update the input set until the output amount + fee is satisfied (or updating fails) + input_set_tracker_t added_inputs{std::move(initial_input_set)}; + input_set_tracker_t candidate_inputs; + + while (true) + { + // 1. exclude added inputs that don't pay for their differential fees + // note: this is a clean-up pass, so has precedence over checking for a solution + try_update_added_inputs_exclude_useless_v1(fee_per_tx_weight, + tx_fee_calculator, + num_outputs, + added_inputs, + candidate_inputs); + + // 2. check if we have a solution + CHECK_AND_ASSERT_THROW_MES(total_inputs(added_inputs) <= max_inputs_allowed, + "input selection: there are more inputs than the number allowed (bug)."); + + // a. compute current fee + const rct::xmr_amount current_fee{ + tx_fee_calculator.compute_fee(fee_per_tx_weight, + count_records(added_inputs, InputSelectionType::LEGACY), + count_records(added_inputs, InputSelectionType::SERAPHIS), + num_outputs) + }; + + // b. check if we have covered the required amount + if (compute_total_amount(added_inputs) >= output_amount + current_fee) + { + input_set_out = std::move(added_inputs); + return true; + } + + // 3. try to add the best candidate input to the added inputs set + if (try_update_added_inputs_add_candidate_v1(max_inputs_allowed, + fee_per_tx_weight, + tx_fee_calculator, + num_outputs, + added_inputs, + candidate_inputs)) + continue; + + // 4. try to replace an added input with a better candidate input + // - do this after trying to add an candidate input for better utilization of selected inputs; typically, + // after obtaining a new candidate input in step 5, it will be directly added to the input set in step 3 + // of the next update cycle; if this step were ordered before step 3, then new candidates would frequently + // be swapped with previously added inputs, and the final input set would always contain only the highest + // amounts from the selected inputs (even if the input selector was hoping for a different distribution) + // - the emergent behavior of the input selection process is overall rather opaque, but this ordering of + // steps should match the caller's expectations the best + if (try_update_added_inputs_replace_candidate_v1(fee_per_tx_weight, + tx_fee_calculator, + num_outputs, + added_inputs, + candidate_inputs)) + continue; + + // 5. try to obtain a new candidate input from the input selector + if (try_update_candidate_inputs_selection_v1(output_amount, + input_selector, + fee_per_tx_weight, + tx_fee_calculator, + num_outputs, + added_inputs, + candidate_inputs)) + continue; + + // 6. try to use a range of candidate inputs to get us closer to a solution + // note: this is an inefficient last-ditch effort, so we only attempt it after no more inputs can be selected + if (try_update_added_inputs_range_v1(max_inputs_allowed, + fee_per_tx_weight, + tx_fee_calculator, + num_outputs, + added_inputs, + candidate_inputs)) + continue; + + // 7. no attempts to update the added inputs worked, so we have failed + return false; + } + + return false; +} +//------------------------------------------------------------------------------------------------------------------- +bool try_get_input_set_v1(const OutputSetContextForInputSelection &output_set_context, + const std::size_t max_inputs_allowed, + const InputSelectorV1 &input_selector, + const rct::xmr_amount fee_per_tx_weight, + const FeeCalculator &tx_fee_calculator, + rct::xmr_amount &final_fee_out, + input_set_tracker_t &input_set_out) +{ + input_set_out.clear(); + + // 1. select inputs to cover requested output amount (assume 0 change) + const boost::multiprecision::uint128_t output_amount{output_set_context.total_amount()}; + const std::size_t num_outputs_nochange{output_set_context.num_outputs_nochange()}; + + if (!try_select_inputs_v1(output_amount, + max_inputs_allowed, + input_selector, + fee_per_tx_weight, + tx_fee_calculator, + num_outputs_nochange, + {}, + input_set_out)) + return false; + + // 2. compute fee for selected inputs + const std::size_t num_legacy_inputs_first_try{count_records(input_set_out, InputSelectionType::LEGACY)}; + const std::size_t num_sp_inputs_first_try{count_records(input_set_out, InputSelectionType::SERAPHIS)}; + + const rct::xmr_amount zero_change_fee{ + tx_fee_calculator.compute_fee(fee_per_tx_weight, + num_legacy_inputs_first_try, + num_sp_inputs_first_try, + num_outputs_nochange) + }; + + // 3. return if we are done (zero change is covered by input amounts) + // - very rare case + if (compute_total_amount(input_set_out) == output_amount + zero_change_fee) + { + final_fee_out = zero_change_fee; + return true; + } + + // 4. if non-zero change with computed fee, assume change must be non-zero (typical case) + // a. update fee assuming non-zero change + const std::size_t num_outputs_withchange{output_set_context.num_outputs_withchange()}; + + rct::xmr_amount nonzero_change_fee{ + tx_fee_calculator.compute_fee(fee_per_tx_weight, + num_legacy_inputs_first_try, + num_sp_inputs_first_try, + num_outputs_withchange) + }; + + CHECK_AND_ASSERT_THROW_MES(zero_change_fee <= nonzero_change_fee, + "getting an input set: adding a change output reduced the tx fee (bug)."); + + // b. if previously selected inputs are insufficient for non-zero change, select inputs again + // - very rare case + if (compute_total_amount(input_set_out) <= output_amount + nonzero_change_fee) + { + // i. select inputs + if (!try_select_inputs_v1(output_amount + 1, //+1 to force a non-zero change + max_inputs_allowed, + input_selector, + fee_per_tx_weight, + tx_fee_calculator, + num_outputs_withchange, + std::move(input_set_out), //reuse already-selected inputs + input_set_out)) + return false; + + // ii. update the fee + const std::size_t num_legacy_inputs_second_try{count_records(input_set_out, InputSelectionType::LEGACY)}; + const std::size_t num_sp_inputs_second_try{count_records(input_set_out, InputSelectionType::SERAPHIS)}; + + nonzero_change_fee = + tx_fee_calculator.compute_fee(fee_per_tx_weight, + num_legacy_inputs_second_try, + num_sp_inputs_second_try, + num_outputs_withchange); + } + + // c. we are done (non-zero change is covered by input amounts) + CHECK_AND_ASSERT_THROW_MES(compute_total_amount(input_set_out) > output_amount + nonzero_change_fee, + "getting an input set: selecting inputs for the non-zero change amount case failed (bug)."); + + final_fee_out = nonzero_change_fee; + return true; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace sp diff --git a/src/seraphis_main/tx_input_selection.h b/src/seraphis_main/tx_input_selection.h new file mode 100644 index 0000000000..871dc73534 --- /dev/null +++ b/src/seraphis_main/tx_input_selection.h @@ -0,0 +1,109 @@ +// Copyright (c) 2022, 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 selecting tx inputs from an enote storage. + +#pragma once + +//local headers +#include "contextual_enote_record_types.h" +#include "ringct/rctTypes.h" +#include "tx_fee_calculator.h" +#include "tx_input_selection_output_context.h" + +//third party headers +#include "boost/container/map.hpp" +#include "boost/multiprecision/cpp_int.hpp" + +//standard headers +#include + +//forward declarations + + +namespace sp +{ + +enum class InputSelectionType +{ + LEGACY, + SERAPHIS +}; + +using input_set_tracker_t = + std::unordered_map>; + +class InputSelectorV1 +{ +public: +//destructor + virtual ~InputSelectorV1() = default; + +//overloaded operators + /// disable copy/move (this is a pure virtual base class) + InputSelectorV1& operator=(InputSelectorV1&&) = delete; + +//member functions + /// select an available input + virtual bool try_select_input_candidate_v1(const boost::multiprecision::uint128_t desired_total_amount, + const input_set_tracker_t &added_inputs, + const input_set_tracker_t &candidate_inputs, + ContextualRecordVariant &selected_input_out) const = 0; +}; + +/** +* brief: try_get_input_set_v1 - try to select a set of inputs for a tx +* - This algorithm will fail to find a possible solution if there exist combinations that lead to 0-change successes, +* but the combination that was found has non-zero change that doesn't cover the differential fee of adding a change +* output (and there are no solutions that can cover that additional change output differential fee). Only an O(N!) +* brute force search can find the success solution(s) to that problem (e.g. on complete failures you could fall-back +* to brute force search on the 0-change case). However, that failure case will be extremely rare, so it probably +* isn't worthwhile to implement a brute force fall-back. +* - This algorithm includes a 'select range of inputs' trial pass that is implemented naively - only ranges of same-type +* candidate inputs are considered. A no-fail algorithm would use brute force to test all possible combinations of +* candiate inputs of different types. Brute force is O(N^2) instead of O(N) (for N = max inputs allowed), so it was +* not implemented here for efficiency. +* - The naive approach will have lower rates of false negatives as the proportion of seraphis to legacy enotes +* increases. +* param: output_set_context - +* param: max_inputs_allowed - +* param: input_selector - +* param: fee_per_tx_weight - +* param: tx_fee_calculator - +* outparam: final_fee_out - +* outparam: input_set_out - +*/ +bool try_get_input_set_v1(const OutputSetContextForInputSelection &output_set_context, + const std::size_t max_inputs_allowed, + const InputSelectorV1 &input_selector, + const rct::xmr_amount fee_per_tx_weight, + const FeeCalculator &tx_fee_calculator, + rct::xmr_amount &final_fee_out, + input_set_tracker_t &input_set_out); + +} //namespace sp diff --git a/src/seraphis_main/tx_input_selection_output_context.h b/src/seraphis_main/tx_input_selection_output_context.h new file mode 100644 index 0000000000..bf5486559d --- /dev/null +++ b/src/seraphis_main/tx_input_selection_output_context.h @@ -0,0 +1,66 @@ +// Copyright (c) 2022, 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. + +// Output set context for use during input selection. + +#pragma once + +//local headers + +//third party headers +#include "boost/multiprecision/cpp_int.hpp" + +//standard headers +#include + +//forward declarations + + +namespace sp +{ + +class OutputSetContextForInputSelection +{ +public: +//destructor + virtual ~OutputSetContextForInputSelection() = default; + +//overloaded operators + /// disable copy/move (this is a pure virtual base class) + OutputSetContextForInputSelection& operator=(OutputSetContextForInputSelection&&) = delete; + +//member functions + /// get total output amount + virtual boost::multiprecision::uint128_t total_amount() const = 0; + /// get number of outputs assuming no change + virtual std::size_t num_outputs_nochange() const = 0; + /// get number of outputs assuming non-zero change + virtual std::size_t num_outputs_withchange() const = 0; +}; + +} //namespace sp diff --git a/src/seraphis_main/tx_validation_context.h b/src/seraphis_main/tx_validation_context.h new file mode 100644 index 0000000000..2007d2cc74 --- /dev/null +++ b/src/seraphis_main/tx_validation_context.h @@ -0,0 +1,89 @@ +// Copyright (c) 2022, 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. + +// Interface for interacting with a context where a tx should be valid (e.g. a ledger). + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "ringct/rctTypes.h" + +//third party headers + +//standard headers +#include + +//forward declarations + + +namespace sp +{ + +class TxValidationContext +{ +public: +//destructor + virtual ~TxValidationContext() = default; + +//overloaded operators + /// disable copy/move (this is a pure virtual base class) + TxValidationContext& operator=(TxValidationContext&&) = delete; + +//member functions + /** + * brief: cryptonote_key_image_exists - checks if a cryptonote-style key image exists in the validation context + * param: key_image - + * return: true/false on check result + */ + virtual bool cryptonote_key_image_exists(const crypto::key_image &key_image) const = 0; + /** + * brief: seraphis_key_image_exists - checks if a seraphis-style key image exists in the validation context + * param: key_image - + * return: true/false on check result + */ + virtual bool seraphis_key_image_exists(const crypto::key_image &key_image) const = 0; + /** + * brief: get_reference_set_proof_elements_v1 - gets legacy {KI, C} pairs stored in the validation context + * - note: should only return elements that are valid to reference in a tx (e.g. locked elements are invalid) + * param: indices - + * outparam: proof_elements_out - {KI, C} + */ + virtual void get_reference_set_proof_elements_v1(const std::vector &indices, + rct::ctkeyV &proof_elements_out) const = 0; + /** + * brief: get_reference_set_proof_elements_v2 - gets seraphis squashed enotes stored in the validation context + * - note: should only return elements that are valid to reference in a tx (e.g. locked elements are invalid) + * param: indices - + * outparam: proof_elements_out - {squashed enote} + */ + virtual void get_reference_set_proof_elements_v2(const std::vector &indices, + rct::keyV &proof_elements_out) const = 0; +}; + +} //namespace sp diff --git a/src/seraphis_main/tx_validators.cpp b/src/seraphis_main/tx_validators.cpp new file mode 100644 index 0000000000..055437d67f --- /dev/null +++ b/src/seraphis_main/tx_validators.cpp @@ -0,0 +1,621 @@ +// Copyright (c) 2022, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//paired header +#include "tx_validators.h" + +//local headers +#include "common/container_helpers.h" +#include "crypto/crypto.h" +#include "ringct/rctOps.h" +#include "ringct/rctSigs.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/binned_reference_set_utils.h" +#include "seraphis_core/discretized_fee.h" +#include "seraphis_core/tx_extra.h" +#include "seraphis_crypto/bulletproofs_plus2.h" +#include "seraphis_crypto/math_utils.h" +#include "seraphis_crypto/grootle.h" +#include "seraphis_crypto/sp_composition_proof.h" +#include "seraphis_crypto/sp_crypto_utils.h" +#include "tx_builders_inputs.h" +#include "tx_builders_legacy_inputs.h" +#include "tx_component_types.h" +#include "tx_component_types_legacy.h" +#include "tx_validation_context.h" + +//third party headers +#include "boost/multiprecision/cpp_int.hpp" + +//standard headers +#include +#include +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis" + +namespace sp +{ +//------------------------------------------------------------------------------------------------------------------- +// helper for validating v1 balance proofs (balance equality check) +//------------------------------------------------------------------------------------------------------------------- +static bool validate_sp_amount_balance_equality_check_v1(const std::vector &legacy_input_images, + const std::vector &sp_input_images, + const std::vector &outputs, + const rct::xmr_amount transaction_fee, + const rct::key &remainder_blinding_factor) +{ + // the blinding factor should be a canonical scalar + if (sc_check(remainder_blinding_factor.bytes) != 0) + return false; + + // balance check + rct::keyV input_image_amount_commitments; + rct::keyV output_commitments; + input_image_amount_commitments.reserve(legacy_input_images.size() + sp_input_images.size()); + output_commitments.reserve(outputs.size() + 2); + + for (const LegacyEnoteImageV2 &legacy_input_image : legacy_input_images) + input_image_amount_commitments.emplace_back(legacy_input_image.masked_commitment); + + for (const SpEnoteImageV1 &sp_input_image : sp_input_images) + input_image_amount_commitments.emplace_back(masked_commitment_ref(sp_input_image)); + + for (const SpEnoteV1 &output : outputs) + output_commitments.emplace_back(output.core.amount_commitment); + + output_commitments.emplace_back(rct::commit(transaction_fee, rct::zero())); + + if (!(remainder_blinding_factor == rct::zero())) + output_commitments.emplace_back(rct::scalarmultBase(remainder_blinding_factor)); + + // sum(input masked commitments) ?= sum(output commitments) + transaction_fee*H + remainder_blinding_factor*G + return balance_check_equality(input_image_amount_commitments, output_commitments); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +bool validate_sp_semantics_coinbase_component_counts_v1(const SemanticConfigCoinbaseComponentCountsV1 &config, + const std::size_t num_outputs, + const std::size_t num_enote_pubkeys) +{ + // output count + if (num_outputs < config.min_outputs || + num_outputs > config.max_outputs) + return false; + + // outputs and enote pubkeys should be 1:1 (note: there are no 'shared' enote pubkeys in coinbase txs) + if (num_outputs != num_enote_pubkeys) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool validate_sp_semantics_component_counts_v1(const SemanticConfigComponentCountsV1 &config, + const std::size_t num_legacy_input_images, + const std::size_t num_sp_input_images, + const std::size_t num_legacy_ring_signatures, + const std::size_t num_sp_membership_proofs, + const std::size_t num_sp_image_proofs, + const std::size_t num_outputs, + const std::size_t num_enote_pubkeys, + const std::size_t num_range_proofs) +{ + // input count + if (num_legacy_input_images + num_sp_input_images < config.min_inputs || + num_legacy_input_images + num_sp_input_images > config.max_inputs) + return false; + + // legacy input images and ring signatures should be 1:1 + if (num_legacy_input_images != num_legacy_ring_signatures) + return false; + + // seraphis input images and image proofs should be 1:1 + if (num_sp_input_images != num_sp_image_proofs) + return false; + + // seraphis input images and membership proofs should be 1:1 + if (num_sp_input_images != num_sp_membership_proofs) + return false; + + // output count + if (num_outputs < config.min_outputs || + num_outputs > config.max_outputs) + return false; + + // range proofs should be 1:1 with seraphis input image amount commitments and outputs + if (num_range_proofs != num_sp_input_images + num_outputs) + return false; + + // outputs and enote pubkeys should be 1:1 + // - except for 2-out txs, which should have only one enote pubkey + if (num_outputs == 2) + { + if (num_enote_pubkeys != 1) + return false; + } + else if (num_outputs != num_enote_pubkeys) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool validate_sp_semantics_legacy_reference_sets_v1(const SemanticConfigLegacyRefSetV1 &config, + const std::vector &legacy_ring_signatures) +{ + // assume valid if no signatures + if (legacy_ring_signatures.size() == 0) + return true; + + // check ring size in each ring signature + for (const LegacyRingSignatureV4 &legacy_ring_signature : legacy_ring_signatures) + { + // reference set + if (legacy_ring_signature.reference_set.size() < config.ring_size_min || + legacy_ring_signature.reference_set.size() > config.ring_size_max) + return false; + + // CLSAG signature size + if (legacy_ring_signature.reference_set.size() != legacy_ring_signature.clsag_proof.s.size()) + return false; + } + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool validate_sp_semantics_sp_reference_sets_v1(const SemanticConfigSpRefSetV1 &config, + const std::vector &sp_membership_proofs) +{ + // assume valid if no proofs + if (sp_membership_proofs.size() == 0) + return true; + + // check ref set decomp + const std::size_t ref_set_decomp_n{sp_membership_proofs[0].ref_set_decomp_n}; + const std::size_t ref_set_decomp_m{sp_membership_proofs[0].ref_set_decomp_m}; + + if (ref_set_decomp_n < config.decomp_n_min || + ref_set_decomp_n > config.decomp_n_max) + return false; + + if (ref_set_decomp_m < config.decomp_m_min || + ref_set_decomp_m > config.decomp_m_max) + return false; + + // check binned reference set configuration + const SpBinnedReferenceSetConfigV1 bin_config{sp_membership_proofs[0].binned_reference_set.bin_config}; + + if (bin_config.bin_radius < config.bin_radius_min || + bin_config.bin_radius > config.bin_radius_max) + return false; + + if (bin_config.num_bin_members < config.num_bin_members_min || + bin_config.num_bin_members > config.num_bin_members_max) + return false; + + // check seraphis membership proofs + for (const SpMembershipProofV1 &sp_proof : sp_membership_proofs) + { + // proof ref set decomposition (n^m) should match number of referenced enotes + const std::size_t ref_set_size{math::uint_pow(sp_proof.ref_set_decomp_n, sp_proof.ref_set_decomp_m)}; + + if (ref_set_size != reference_set_size(sp_proof.binned_reference_set)) + return false; + + // all proofs should have same ref set decomp (and implicitly: same ref set size) + if (sp_proof.ref_set_decomp_n != ref_set_decomp_n) + return false; + if (sp_proof.ref_set_decomp_m != ref_set_decomp_m) + return false; + + // all proofs should have the same bin config + if (sp_proof.binned_reference_set.bin_config != bin_config) + return false; + } + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool validate_sp_semantics_output_serialization_v1(const std::vector &output_enotes) +{ + ge_p3 temp_deserialized; + + // onetime addresses must be deserializable + for (const SpCoinbaseEnoteV1 &output_enote : output_enotes) + { + if (ge_frombytes_vartime(&temp_deserialized, output_enote.core.onetime_address.bytes) != 0) + return false; + } + + // note: all possible serializations of x25519 public keys are valid, so we don't validate enote ephemeral pubkeys here + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool validate_sp_semantics_output_serialization_v2(const std::vector &output_enotes) +{ + ge_p3 temp_deserialized; + + // onetime addresses must be deserializable + for (const SpEnoteV1 &output_enote : output_enotes) + { + if (ge_frombytes_vartime(&temp_deserialized, output_enote.core.onetime_address.bytes) != 0) + return false; + } + + // note: all possible serializations of x25519 public keys are valid, so we don't validate enote ephemeral pubkeys here + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool validate_sp_semantics_input_images_v1(const std::vector &legacy_input_images, + const std::vector &sp_input_images) +{ + for (const LegacyEnoteImageV2 &legacy_image : legacy_input_images) + { + // input linking tags must be in the prime subgroup: l*KI = identity + if (!sp::key_domain_is_prime_subgroup(rct::ki2rct(legacy_image.key_image))) + return false; + + // image parts must not be identity + if (legacy_image.masked_commitment == rct::identity()) + return false; + if (rct::ki2rct(legacy_image.key_image) == rct::identity()) + return false; + } + + for (const SpEnoteImageV1 &sp_image : sp_input_images) + { + // input linking tags must be in the prime subgroup: l*KI = identity + if (!sp::key_domain_is_prime_subgroup(rct::ki2rct(key_image_ref(sp_image)))) + return false; + + // image parts must not be identity + if (masked_address_ref(sp_image) == rct::identity()) + return false; + if (masked_commitment_ref(sp_image) == rct::identity()) + return false; + if (rct::ki2rct(key_image_ref(sp_image)) == rct::identity()) + return false; + } + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool validate_sp_semantics_coinbase_layout_v1(const std::vector &outputs, + const std::vector &enote_ephemeral_pubkeys, + const TxExtra &tx_extra) +{ + // output enotes should be sorted by onetime address with byte-wise comparisons (ascending), and unique + if (!tools::is_sorted_and_unique(outputs, compare_Ko)) + return false; + + // enote ephemeral pubkeys should be unique (they don't need to be sorted) + if (!keys_are_unique(enote_ephemeral_pubkeys)) + return false; + + // tx extra fields should be in sorted TLV (Type-Length-Value) format + std::vector extra_field_elements; + if (!try_get_extra_field_elements(tx_extra, extra_field_elements)) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool validate_sp_semantics_layout_v1(const std::vector &legacy_ring_signatures, + const std::vector &sp_membership_proofs, + const std::vector &legacy_input_images, + const std::vector &sp_input_images, + const std::vector &outputs, + const std::vector &enote_ephemeral_pubkeys, + const TxExtra &tx_extra) +{ + // legacy reference sets should be sorted (ascending) without duplicates + for (const LegacyRingSignatureV4 &legacy_ring_signature : legacy_ring_signatures) + { + if (!tools::is_sorted_and_unique(legacy_ring_signature.reference_set)) + return false; + } + + // seraphis membership proof binned reference set bins should be sorted (ascending) + // note: duplicate bin locations are allowed + for (const SpMembershipProofV1 &sp_proof : sp_membership_proofs) + { + if (!std::is_sorted(sp_proof.binned_reference_set.bin_loci.begin(), + sp_proof.binned_reference_set.bin_loci.end())) + return false; + } + + // legacy input images should be sorted by key image with byte-wise comparisons (ascending), and unique + if (!tools::is_sorted_and_unique(legacy_input_images, compare_KI)) + return false; + + // seraphis input images should be sorted by key image with byte-wise comparisons (ascending), and unique + if (!tools::is_sorted_and_unique(sp_input_images, compare_KI)) + return false; + + // legacy and seraphis input images should not have any matching key images + // note: it is not necessary to check this because overlapping key images is impossible if the input proofs are valid + + // output enotes should be sorted by onetime address with byte-wise comparisons (ascending), and unique + if (!tools::is_sorted_and_unique(outputs, compare_Ko)) + return false; + + // enote ephemeral pubkeys should be unique (they don't need to be sorted) + if (!keys_are_unique(enote_ephemeral_pubkeys)) + return false; + + // tx extra fields should be in sorted TLV (Type-Length-Value) format + std::vector extra_field_elements; + if (!try_get_extra_field_elements(tx_extra, extra_field_elements)) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool validate_sp_semantics_fee_v1(const DiscretizedFee discretized_transaction_fee) +{ + rct::xmr_amount raw_transaction_fee; + if (!try_get_fee_value(discretized_transaction_fee, raw_transaction_fee)) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool validate_sp_key_images_v1(const std::vector &legacy_input_images, + const std::vector &sp_input_images, + const TxValidationContext &tx_validation_context) +{ + // check no legacy duplicates in ledger context + for (const LegacyEnoteImageV2 &legacy_input_image : legacy_input_images) + { + if (tx_validation_context.cryptonote_key_image_exists(legacy_input_image.key_image)) + return false; + } + + // check no seraphis duplicates in ledger context + for (const SpEnoteImageV1 &sp_input_image : sp_input_images) + { + if (tx_validation_context.seraphis_key_image_exists(key_image_ref(sp_input_image))) + return false; + } + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool validate_sp_coinbase_amount_balance_v1(const rct::xmr_amount block_reward, + const std::vector &outputs) +{ + // add together output amounts (use uint128_t to prevent malicious overflow) + boost::multiprecision::uint128_t output_amount_sum{0}; + + for (const SpCoinbaseEnoteV1 &output : outputs) + output_amount_sum += output.core.amount; + + // expect output amount equals coinbase block reward + if (block_reward != output_amount_sum) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool validate_sp_amount_balance_v1(const std::vector &legacy_input_images, + const std::vector &sp_input_images, + const std::vector &outputs, + const DiscretizedFee discretized_transaction_fee, + const SpBalanceProofV1 &balance_proof) +{ + const BulletproofPlus2 &range_proofs = balance_proof.bpp2_proof; + + // sanity check + if (range_proofs.V.size() == 0) + return false; + + // try to extract the fee + rct::xmr_amount raw_transaction_fee; + if (!try_get_fee_value(discretized_transaction_fee, raw_transaction_fee)) + return false; + + // check that amount commitments balance + if (!validate_sp_amount_balance_equality_check_v1(legacy_input_images, + sp_input_images, + outputs, + raw_transaction_fee, + balance_proof.remainder_blinding_factor)) + return false; + + // check that commitments in range proofs line up with seraphis input image and output commitments + if (sp_input_images.size() + outputs.size() != range_proofs.V.size()) + return false; + + for (std::size_t input_commitment_index{0}; input_commitment_index < sp_input_images.size(); ++input_commitment_index) + { + // the two stored copies of input image commitments must match + if (!(masked_commitment_ref(sp_input_images[input_commitment_index]) == + rct::scalarmult8(range_proofs.V[input_commitment_index]))) + return false; + } + + for (std::size_t output_commitment_index{0}; output_commitment_index < outputs.size(); ++output_commitment_index) + { + // the two stored copies of output commitments must match + if (!(outputs[output_commitment_index].core.amount_commitment == + rct::scalarmult8(range_proofs.V[sp_input_images.size() + output_commitment_index]))) + return false; + } + + // BP+: deferred for batch-verification + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool validate_sp_legacy_input_proofs_v1(const std::vector &legacy_ring_signatures, + const std::vector &legacy_input_images, + const rct::key &tx_proposal_prefix, + const TxValidationContext &tx_validation_context) +{ + // sanity check + if (legacy_ring_signatures.size() != legacy_input_images.size()) + return false; + + // legacy ring signatures and input images should have the same main key images stored + for (std::size_t legacy_input_index{0}; legacy_input_index < legacy_ring_signatures.size(); ++legacy_input_index) + { + if (rct::rct2ki(legacy_ring_signatures[legacy_input_index].clsag_proof.I) != + legacy_input_images[legacy_input_index].key_image) + return false; + } + + // validate each legacy ring signature + rct::ctkeyV ring_members_temp; + rct::key ring_signature_message_temp; + + for (std::size_t legacy_input_index{0}; legacy_input_index < legacy_ring_signatures.size(); ++legacy_input_index) + { + // collect CLSAG ring members + ring_members_temp.clear(); + tx_validation_context.get_reference_set_proof_elements_v1( + legacy_ring_signatures[legacy_input_index].reference_set, + ring_members_temp); + + // make legacy proof message + make_tx_legacy_ring_signature_message_v1(tx_proposal_prefix, + legacy_ring_signatures[legacy_input_index].reference_set, + ring_signature_message_temp); + + // verify CLSAG proof + if (!rct::verRctCLSAGSimple(ring_signature_message_temp, + legacy_ring_signatures[legacy_input_index].clsag_proof, + ring_members_temp, + legacy_input_images[legacy_input_index].masked_commitment)) + return false; + } + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool validate_sp_composition_proofs_v1(const std::vector &sp_image_proofs, + const std::vector &sp_input_images, + const rct::key &tx_proposal_prefix) +{ + // sanity check + if (sp_image_proofs.size() != sp_input_images.size()) + return false; + + // validate each composition proof + for (std::size_t input_index{0}; input_index < sp_input_images.size(); ++input_index) + { + if (!sp::verify_sp_composition_proof(sp_image_proofs[input_index].composition_proof, + tx_proposal_prefix, + masked_address_ref(sp_input_images[input_index]), + key_image_ref(sp_input_images[input_index]))) + return false; + } + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool try_get_sp_membership_proofs_v1_validation_data(const std::vector &sp_membership_proofs, + const std::vector &sp_input_images, + const TxValidationContext &tx_validation_context, + std::list &validation_data_out) +{ + const std::size_t num_proofs{sp_membership_proofs.size()}; + validation_data_out.clear(); + + // sanity check + if (num_proofs != sp_input_images.size()) + return false; + + // assume valid if no proofs + if (num_proofs == 0) + return true; + + // get batched validation data + std::vector proofs; + std::vector membership_proof_keys; + rct::keyV offsets; + rct::keyV messages; + proofs.reserve(num_proofs); + membership_proof_keys.reserve(num_proofs); + offsets.reserve(num_proofs); + messages.reserve(num_proofs); + + rct::key generator_seed_reproduced; + std::vector reference_indices; + + for (std::size_t proof_index{0}; proof_index < num_proofs; ++proof_index) + { + // sanity check + if (!sp_membership_proofs[proof_index] || + !sp_input_images[proof_index]) + return false; + + // the binned reference set's generator seed should be reproducible + make_binned_ref_set_generator_seed_v1(sp_input_images[proof_index]->masked_address, + sp_input_images[proof_index]->masked_commitment, + generator_seed_reproduced); + + if (!(generator_seed_reproduced == sp_membership_proofs[proof_index]->binned_reference_set.bin_generator_seed)) + return false; + + // extract the references + if(!try_get_reference_indices_from_binned_reference_set_v1(sp_membership_proofs[proof_index]->binned_reference_set, + reference_indices)) + return false; + + // get proof keys from enotes stored in the ledger + tx_validation_context.get_reference_set_proof_elements_v2(reference_indices, + tools::add_element(membership_proof_keys)); + + // offset (input image masked keys squashed: Q" = K" + C") + rct::addKeys(tools::add_element(offsets), + sp_input_images[proof_index]->masked_address, + sp_input_images[proof_index]->masked_commitment); + + // proof message + make_tx_membership_proof_message_v1(sp_membership_proofs[proof_index]->binned_reference_set, + tools::add_element(messages)); + + // save the proof + proofs.emplace_back(&(sp_membership_proofs[proof_index]->grootle_proof)); + } + + // get verification data + sp::get_grootle_verification_data(proofs, + messages, + membership_proof_keys, + offsets, + sp_membership_proofs[0]->ref_set_decomp_n, + sp_membership_proofs[0]->ref_set_decomp_m, + validation_data_out); + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace sp diff --git a/src/seraphis_main/tx_validators.h b/src/seraphis_main/tx_validators.h new file mode 100644 index 0000000000..ce6acad4c6 --- /dev/null +++ b/src/seraphis_main/tx_validators.h @@ -0,0 +1,303 @@ +// Copyright (c) 2022, 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 tx validator implementations. + +#pragma once + +//local headers +#include "crypto/x25519.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/discretized_fee.h" +#include "seraphis_core/tx_extra.h" +#include "tx_component_types.h" +#include "tx_component_types_legacy.h" + +//third party headers + +//standard headers +#include +#include + +//forward declarations +namespace sp +{ + class SpMultiexpBuilder; + class TxValidationContext; +} + +namespace sp +{ + +/// semantic validation config: component counts +struct SemanticConfigCoinbaseComponentCountsV1 final +{ + std::size_t min_outputs; + std::size_t max_outputs; +}; + +/// semantic validation config: component counts +struct SemanticConfigComponentCountsV1 final +{ + std::size_t min_inputs; + std::size_t max_inputs; + std::size_t min_outputs; + std::size_t max_outputs; +}; + +/// semantic validation config: legacy reference sets +struct SemanticConfigLegacyRefSetV1 final +{ + std::size_t ring_size_min; + std::size_t ring_size_max; +}; + +/// semantic validation config: seraphis reference sets +struct SemanticConfigSpRefSetV1 final +{ + std::size_t decomp_n_min; + std::size_t decomp_n_max; + std::size_t decomp_m_min; + std::size_t decomp_m_max; + std::size_t bin_radius_min; + std::size_t bin_radius_max; + std::size_t num_bin_members_min; + std::size_t num_bin_members_max; +}; + +/** +* brief: validate_sp_semantics_coinbase_component_counts_v1 - check coinbase tx component counts are valid +* - min_outputs <= num(outputs) <= max_outputs +* - num(enote pubkeys) == num(outputs) +* +* param: config - +* param: num_outputs - +* param: num_enote_pubkeys - +* return: true/false on validation result +*/ +bool validate_sp_semantics_coinbase_component_counts_v1(const SemanticConfigCoinbaseComponentCountsV1 &config, + const std::size_t num_outputs, + const std::size_t num_enote_pubkeys); +/** +* brief: validate_sp_semantics_component_counts_v1 - check tx component counts are valid +* - min_inputs <= num(legacy and seraphis input images) <= max_inputs +* - num(legacy ring signatures) == num(legacy input images) +* - num(seraphis membership proofs) == num(seraphis image proofs) == num(seraphis input images) +* - min_outputs <= num(outputs) <= max_outputs +* - num(range proofs) == num(seraphis input images) + num(outputs) +* - if (num(outputs) == 2), num(enote pubkeys) == 1, else num(enote pubkeys) == num(outputs) +* +* param: config - +* param: num_legacy_input_images - +* param: num_sp_input_images - +* param: num_legacy_ring_signatures - +* param: num_sp_membership_proofs - +* param: num_sp_image_proofs - +* param: num_outputs - +* param: num_enote_pubkeys - +* param: num_range_proofs - +* return: true/false on validation result +*/ +bool validate_sp_semantics_component_counts_v1(const SemanticConfigComponentCountsV1 &config, + const std::size_t num_legacy_input_images, + const std::size_t num_sp_input_images, + const std::size_t num_legacy_ring_signatures, + const std::size_t num_sp_membership_proofs, + const std::size_t num_sp_image_proofs, + const std::size_t num_outputs, + const std::size_t num_enote_pubkeys, + const std::size_t num_range_proofs); +/** +* brief: validate_sp_semantics_legacy_reference_sets_v1 - check legacy ring signatures have consistent and +* valid reference sets +* - ring_size_min <= ring_size <= ring_size_max +* - CLSAG proof matches the stored ring member indices +* param: config +* param: legacy_ring_signatures - +* return: true/false on validation result +*/ +bool validate_sp_semantics_legacy_reference_sets_v1(const SemanticConfigLegacyRefSetV1 &config, + const std::vector &legacy_ring_signatures); +/** +* brief: validate_sp_semantics_sp_reference_sets_v1 - check seraphis membership proofs have consistent and +* valid reference sets +* - decomp_n_min <= decomp_n <= decom_n_max +* - decomp_m_min <= decomp_m <= decom_m_max +* - bin_radius_min <= bin_radius <= bin_radius_max +* - num_bin_members_min <= num_bin_members <= num_bin_members_max +* - ref set size from decomposition == ref set size from binned reference set +* param: config +* param: sp_membership_proofs - +* return: true/false on validation result +*/ +bool validate_sp_semantics_sp_reference_sets_v1(const SemanticConfigSpRefSetV1 &config, + const std::vector &sp_membership_proofs); +/** +* brief: validate_sp_semantics_output_serialization_v1 - check output enotes are properly serialized +* - onetime addresses are deserializable (note: amount commitment serialization is checked in the balance proof) +* param: output_enotes - +* return: true/false on validation result +*/ +bool validate_sp_semantics_output_serialization_v1(const std::vector &output_enotes); +bool validate_sp_semantics_output_serialization_v2(const std::vector &output_enotes); +/** +* brief: validate_sp_semantics_input_images_v1 - check input images are well-formed +* - key images are in the prime-order EC subgroup: l*KI == identity +* - key images, masked addresses, and masked commitments are not identity +* param: legacy_input_images - +* param: sp_input_images - +* return: true/false on validation result +*/ +bool validate_sp_semantics_input_images_v1(const std::vector &legacy_input_images, + const std::vector &sp_input_images); +/** +* brief: validate_sp_semantics_coinbase_layout_v1 - check coinbase tx components have the proper layout +* - output enotes sorted by onetime addresses with byte-wise comparisons (ascending) +* - onetime addresses are all unique +* - enote ephemeral pubkeys are unique +* - extra field is in sorted TLV (Type-Length-Value) format +* param: outputs - +* param: enote_ephemeral_pubkeys - +* param: tx_extra - +* return: true/false on validation result +*/ +bool validate_sp_semantics_coinbase_layout_v1(const std::vector &outputs, + const std::vector &enote_ephemeral_pubkeys, + const TxExtra &tx_extra); +/** +* brief: validate_sp_semantics_layout_v1 - check tx components have the proper layout +* - legacy reference sets are sorted (ascending) +* - legacy reference set indices are unique +* - seraphis membership proof binned reference set bins are sorted (ascending) +* - legacy input images sorted by key image with byte-wise comparisons (ascending) +* - seraphis input images sorted by key image with byte-wise comparisons (ascending) +* - legacy and seraphis input key images are all unique +* - output enotes sorted by onetime addresses with byte-wise comparisons (ascending) +* - onetime addresses are all unique +* - enote ephemeral pubkeys are unique +* - extra field is in sorted TLV (Type-Length-Value) format +* param: legacy_ring_signatures - +* param: sp_membership_proofs - +* param: legacy_input_images - +* param: sp_input_images - +* param: outputs - +* param: enote_ephemeral_pubkeys - +* param: tx_extra - +* return: true/false on validation result +*/ +bool validate_sp_semantics_layout_v1(const std::vector &legacy_ring_signatures, + const std::vector &sp_membership_proofs, + const std::vector &legacy_input_images, + const std::vector &sp_input_images, + const std::vector &outputs, + const std::vector &enote_ephemeral_pubkeys, + const TxExtra &tx_extra); +/** +* brief: validate_sp_semantics_fee_v1 - check that a discretized fee is a valid fee representation +* param: discretized_transaction_fee +* return: true/false on validation result +*/ +bool validate_sp_semantics_fee_v1(const DiscretizedFee discretized_transaction_fee); +/** +* brief: validate_sp_key_images_v1 - check tx does not double spend +* - no key image duplicates in ledger +* param: legacy_input_images - +* param: sp_input_images - +* param: tx_validation_context - +* return: true/false on validation result +*/ +bool validate_sp_key_images_v1(const std::vector &legacy_input_images, + const std::vector &sp_input_images, + const TxValidationContext &tx_validation_context); +/** +* brief: validate_sp_coinbase_amount_balance_v1 - check that amounts balance in the coinbase tx (block reward == outputs) +* - check block_reward == sum(output amounts) +* param: block_reward - +* param: outputs - +* return: true/false on validation result +*/ +bool validate_sp_coinbase_amount_balance_v1(const rct::xmr_amount block_reward, + const std::vector &outputs); +/** +* brief: validate_sp_amount_balance_v1 - check that amounts balance in the tx (inputs == outputs) +* - check sum(input image masked commitments) == sum(output commitments) + fee*H + remainder*G +* - note: BP+ verification is NOT done here (deferred for batch-verification) +* param: legacy_input_images - +* param: sp_input_images - +* param: outputs - +* param: discretized_transaction_fee - +* param: balance_proof - +* return: true/false on validation result +*/ +bool validate_sp_amount_balance_v1(const std::vector &legacy_input_images, + const std::vector &sp_input_images, + const std::vector &outputs, + const DiscretizedFee discretized_transaction_fee, + const SpBalanceProofV1 &balance_proof); +/** +* brief: validate_sp_composition_proofs_v1 - check that spending legacy tx inputs is authorized by their owners, +* key images are properly constructed, and the legacy inputs exist in the ledger +* - check legacy CLSAG proofs +* param: legacy_ring_signatures - +* param: legacy_input_images - +* param: tx_proposal_prefix - +* param: tx_validation_context - +* return: true/false on validation result +*/ +bool validate_sp_legacy_input_proofs_v1(const std::vector &legacy_ring_signatures, + const std::vector &legacy_input_images, + const rct::key &tx_proposal_prefix, + const TxValidationContext &tx_validation_context); +/** +* brief: validate_sp_composition_proofs_v1 - check that spending seraphis tx inputs is authorized by their owners, +* and that key images are properly constructed +* - check seraphis composition proofs +* param: sp_image_proofs - +* param: sp_input_images - +* param: tx_proposal_prefix - +* return: true/false on validation result +*/ +bool validate_sp_composition_proofs_v1(const std::vector &sp_image_proofs, + const std::vector &sp_input_images, + const rct::key &tx_proposal_prefix); +/** +* brief: try_get_sp_membership_proofs_v1_validation_data - get verification data to verify that seraphis tx inputs +* exist in the ledger +* - try to get referenced enotes from the ledger in 'squashed enote' form +* - get verification data for grootle proofs (membership proofs) +* param: membership_proofs - +* param: input_images - +* param: tx_validation_context - +* outparam: validation_data_out - +*/ +bool try_get_sp_membership_proofs_v1_validation_data(const std::vector &membership_proofs, + const std::vector &input_images, + const TxValidationContext &tx_validation_context, + std::list &validation_data_out); + +} //namespace sp diff --git a/src/seraphis_main/txtype_base.cpp b/src/seraphis_main/txtype_base.cpp new file mode 100644 index 0000000000..00e0b5dfaf --- /dev/null +++ b/src/seraphis_main/txtype_base.cpp @@ -0,0 +1,105 @@ +// Copyright (c) 2022, 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 "txtype_base.h" + +//local headers + +//third party headers + +//standard headers +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis" + +namespace sp +{ +//------------------------------------------------------------------------------------------------------------------- +// brief: validate_txs_impl - validate a set of tx (use batching if possible) +// type: SpTxType - transaction type +// param: txs - set of tx pointers +// param: tx_validation_context - injected validation context (e.g. for obtaining ledger-related information) +// return: true/false on verification result +//------------------------------------------------------------------------------------------------------------------- +template +static bool validate_txs_impl(const std::vector &txs, const TxValidationContext &tx_validation_context) +{ + try + { + // validate non-batchable + for (const SpTxType *tx : txs) + { + if (!tx) + return false; + + if (!validate_tx_semantics(*tx)) + return false; + + if (!validate_tx_key_images(*tx, tx_validation_context)) + return false; + + if (!validate_tx_amount_balance(*tx)) + return false; + + if (!validate_tx_input_proofs(*tx, tx_validation_context)) + return false; + } + + // validate batchable + if (!validate_txs_batchable(txs, tx_validation_context)) + return false; + } + catch (...) { return false; } + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +bool validate_tx(const SpTxCoinbaseV1 &tx, const TxValidationContext &tx_validation_context) +{ + return validate_txs_impl({&tx}, tx_validation_context); +} +//------------------------------------------------------------------------------------------------------------------- +bool validate_txs(const std::vector &txs, const TxValidationContext &tx_validation_context) +{ + return validate_txs_impl(txs, tx_validation_context); +} +//------------------------------------------------------------------------------------------------------------------- +bool validate_tx(const SpTxSquashedV1 &tx, const TxValidationContext &tx_validation_context) +{ + return validate_txs_impl({&tx}, tx_validation_context); +} +//------------------------------------------------------------------------------------------------------------------- +bool validate_txs(const std::vector &txs, const TxValidationContext &tx_validation_context) +{ + return validate_txs_impl(txs, tx_validation_context); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace sp diff --git a/src/seraphis_main/txtype_base.h b/src/seraphis_main/txtype_base.h new file mode 100644 index 0000000000..91634284a9 --- /dev/null +++ b/src/seraphis_main/txtype_base.h @@ -0,0 +1,145 @@ +// Copyright (c) 2022, 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. + +// Base tx interface for Seraphis. +// WARNING: This file MUST NOT acquire more includes (may open a hole for overload injection). + +#pragma once + +//local headers + +//third party headers + +//standard headers +#include +#include + +//forward declarations +namespace rct { using xmr_amount = uint64_t; } +namespace sp +{ + struct SpTxCoinbaseV1; + struct SpTxSquashedV1; + class TxValidationContext; +} + +namespace sp +{ + +//// must be implemented by each tx type + +/// short description of the tx type (e.g. 'SpSquashedV1') +template +std::string tx_descriptor(); + +/// tx structure version (e.g. from struct TxStructureVersionSp) +template +unsigned char tx_structure_version(); + +/// transaction validators +template +bool validate_tx_semantics(const SpTxType &tx); +template +bool validate_tx_key_images(const SpTxType &tx, const TxValidationContext &tx_validation_context); +template +bool validate_tx_amount_balance(const SpTxType &tx); +template +bool validate_tx_input_proofs(const SpTxType &tx, const TxValidationContext &tx_validation_context); +template +bool validate_txs_batchable(const std::vector &txs, const TxValidationContext &tx_validation_context); + + +//// Versioning + +/// Transaction protocol era: following CryptoNote (1) and RingCT (2) +constexpr unsigned char TxEraSp{3}; + +/// Transaction structure types: tx types within era 'TxEraSp' +enum class TxStructureVersionSp : unsigned char +{ + /// coinbase transaction + TxTypeSpCoinbaseV1 = 0, + /// normal transaction: squashed v1 + TxTypeSpSquashedV1 = 1 +}; + +/// get the tx version: era | format | semantic rules +struct tx_version_t final +{ + unsigned char bytes[3]; +}; +inline bool operator==(const tx_version_t &a, const tx_version_t &b) +{ return (a.bytes[0] == b.bytes[0]) && (a.bytes[1] == b.bytes[1]) && (a.bytes[2] == b.bytes[2]); } + +inline tx_version_t tx_version_tx_base_from(const unsigned char tx_era_version, + const unsigned char tx_structure_version, + const unsigned char tx_semantic_rules_version) +{ + tx_version_t tx_version; + + /// era of the tx (e.g. CryptoNote/RingCT/Seraphis) + tx_version.bytes[0] = tx_era_version; + /// structure version of the tx within its era + tx_version.bytes[1] = tx_structure_version; + /// a tx format's validation rules version + tx_version.bytes[2] = tx_semantic_rules_version; + + return tx_version; +} + +/// get the tx version for seraphis txs: TxEraSp | format | semantic rules +inline tx_version_t tx_version_seraphis_base_from(const unsigned char tx_structure_version, + const unsigned char tx_semantic_rules_version) +{ + return tx_version_tx_base_from(TxEraSp, tx_structure_version, tx_semantic_rules_version); +} + +/// get the tx version for a specific seraphis tx type +template +tx_version_t tx_version_from(const unsigned char tx_semantic_rules_version) +{ + return tx_version_seraphis_base_from(tx_structure_version(), tx_semantic_rules_version); +} + + +//// core validators + +/// specialize the following functions with definitions in txtype_base.cpp, so the validate_txs_impl() function from that +/// file will be explicitly instantiated using the formula written there (this way maliciously injected overloads +/// of validate_txs_impl() won't be available to the compiler) +/// bool validate_tx(const SpTxType &tx, const TxValidationContext &tx_validation_context); +/// bool validate_txs(const std::vector &txs, const TxValidationContext &tx_validation_context); + +/// SpTxCoinbaseV1 +bool validate_tx(const SpTxCoinbaseV1 &tx, const TxValidationContext &tx_validation_context); +bool validate_txs(const std::vector &txs, const TxValidationContext &tx_validation_context); +/// SpTxSquashedV1 +bool validate_tx(const SpTxSquashedV1 &tx, const TxValidationContext &tx_validation_context); +bool validate_txs(const std::vector &txs, const TxValidationContext &tx_validation_context); + +} //namespace sp diff --git a/src/seraphis_main/txtype_coinbase_v1.cpp b/src/seraphis_main/txtype_coinbase_v1.cpp new file mode 100644 index 0000000000..7bc098197d --- /dev/null +++ b/src/seraphis_main/txtype_coinbase_v1.cpp @@ -0,0 +1,270 @@ +// Copyright (c) 2022, 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 "txtype_coinbase_v1.h" + +//local headers +#include "cryptonote_config.h" +#include "misc_log_ex.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/sp_core_enote_utils.h" +#include "seraphis_core/sp_core_types.h" +#include "seraphis_crypto/sp_crypto_utils.h" +#include "seraphis_crypto/sp_hash_functions.h" +#include "seraphis_crypto/sp_transcript.h" +#include "tx_builder_types.h" +#include "tx_builders_mixed.h" +#include "tx_builders_outputs.h" +#include "tx_component_types.h" +#include "tx_validation_context.h" +#include "tx_validators.h" + +//third party headers + +//standard headers +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis" + +namespace sp +{ +//------------------------------------------------------------------------------------------------------------------- +std::size_t sp_tx_coinbase_v1_size_bytes(const std::size_t num_outputs, const std::size_t tx_extra_size) +{ + // size of the transaction as represented in C++ + std::size_t size{0}; + + // coinbase input (block height and block reward) + size += 8 + 8; + + // outputs + size += num_outputs * sp_coinbase_enote_v1_size_bytes(); + + // extra data in tx + size += sp_tx_supplement_v1_size_bytes(num_outputs, tx_extra_size, false); //without shared ephemeral pubkey assumption + + return size; +} +//------------------------------------------------------------------------------------------------------------------- +std::size_t sp_tx_coinbase_v1_size_bytes(const SpTxCoinbaseV1 &tx) +{ + return sp_tx_coinbase_v1_size_bytes(tx.outputs.size(), tx.tx_supplement.tx_extra.size()); +} +//------------------------------------------------------------------------------------------------------------------- +std::size_t sp_tx_coinbase_v1_weight(const std::size_t num_outputs, const std::size_t tx_extra_size) +{ + return sp_tx_coinbase_v1_size_bytes(num_outputs, tx_extra_size); +} +//------------------------------------------------------------------------------------------------------------------- +std::size_t sp_tx_coinbase_v1_weight(const SpTxCoinbaseV1 &tx) +{ + return sp_tx_coinbase_v1_weight(tx.outputs.size(), tx.tx_supplement.tx_extra.size()); +} +//------------------------------------------------------------------------------------------------------------------- +void get_sp_tx_coinbase_v1_txid(const SpTxCoinbaseV1 &tx, rct::key &tx_id_out) +{ + // tx_id = H_32(tx version, block height, block reward, output enotes, tx supplement) + const tx_version_t tx_version{tx_version_from(tx.tx_semantic_rules_version)}; + + SpFSTranscript transcript{ + config::HASH_KEY_SERAPHIS_TRANSACTION_TYPE_COINBASE_V1, + sizeof(tx_version) + + 16 + + tx.outputs.size()*sp_coinbase_enote_v1_size_bytes() + + sp_tx_supplement_v1_size_bytes(tx.tx_supplement) + }; + transcript.append("tx_version", tx_version.bytes); + transcript.append("block_height", tx.block_height); + transcript.append("block_reward", tx.block_reward); + transcript.append("output_enotes", tx.outputs); + transcript.append("tx_supplement", tx.tx_supplement); + + sp_hash_to_32(transcript.data(), transcript.size(), tx_id_out.bytes); +} +//------------------------------------------------------------------------------------------------------------------- +void make_seraphis_tx_coinbase_v1(const SpTxCoinbaseV1::SemanticRulesVersion semantic_rules_version, + const std::uint64_t block_height, + const rct::xmr_amount block_reward, + std::vector outputs, + SpTxSupplementV1 tx_supplement, + SpTxCoinbaseV1 &tx_out) +{ + tx_out.tx_semantic_rules_version = semantic_rules_version; + tx_out.block_height = block_height; + tx_out.block_reward = block_reward; + tx_out.outputs = std::move(outputs); + tx_out.tx_supplement = std::move(tx_supplement); + + CHECK_AND_ASSERT_THROW_MES(validate_tx_semantics(tx_out), "Failed to assemble an SpTxCoinbaseV1."); +} +//------------------------------------------------------------------------------------------------------------------- +void make_seraphis_tx_coinbase_v1(const SpTxCoinbaseV1::SemanticRulesVersion semantic_rules_version, + const SpCoinbaseTxProposalV1 &tx_proposal, + SpTxCoinbaseV1 &tx_out) +{ + // 1. check tx proposal semantics + check_v1_coinbase_tx_proposal_semantics_v1(tx_proposal); + + // 2. extract outputs from the tx proposal + std::vector output_proposals; + get_coinbase_output_proposals_v1(tx_proposal, output_proposals); + + // 3. extract info from output proposals + std::vector output_enotes; + SpTxSupplementV1 tx_supplement; + make_v1_coinbase_outputs_v1(output_proposals, output_enotes, tx_supplement.output_enote_ephemeral_pubkeys); + + // 4. collect full memo + finalize_tx_extra_v1(tx_proposal.partial_memo, output_proposals, tx_supplement.tx_extra); + + // 5. finish tx + make_seraphis_tx_coinbase_v1(semantic_rules_version, + tx_proposal.block_height, + tx_proposal.block_reward, + std::move(output_enotes), + std::move(tx_supplement), + tx_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_seraphis_tx_coinbase_v1(const SpTxCoinbaseV1::SemanticRulesVersion semantic_rules_version, + const std::uint64_t block_height, + const rct::xmr_amount block_reward, + std::vector normal_payment_proposals, + std::vector additional_memo_elements, + SpTxCoinbaseV1 &tx_out) +{ + // make a coinbase tx proposal + SpCoinbaseTxProposalV1 tx_proposal; + make_v1_coinbase_tx_proposal_v1(block_height, + block_reward, + std::move(normal_payment_proposals), + std::move(additional_memo_elements), + tx_proposal); + + // finish tx + make_seraphis_tx_coinbase_v1(semantic_rules_version, tx_proposal, tx_out); +} +//------------------------------------------------------------------------------------------------------------------- +SemanticConfigCoinbaseComponentCountsV1 semantic_config_coinbase_component_counts_v1( + const SpTxCoinbaseV1::SemanticRulesVersion tx_semantic_rules_version) +{ + SemanticConfigCoinbaseComponentCountsV1 config{}; + + if (tx_semantic_rules_version == SpTxCoinbaseV1::SemanticRulesVersion::MOCK) + { + config.min_outputs = 1; + config.max_outputs = 100000; + } + else if (tx_semantic_rules_version == SpTxCoinbaseV1::SemanticRulesVersion::ONE) + { + config.min_outputs = 1; + config.max_outputs = config::SP_MAX_COINBASE_OUTPUTS_V1; + } + else //unknown semantic rules version + { + CHECK_AND_ASSERT_THROW_MES(false, "Tried to get semantic config for component counts with unknown rules version."); + } + + return config; +} +//------------------------------------------------------------------------------------------------------------------- +template <> +bool validate_tx_semantics(const SpTxCoinbaseV1 &tx) +{ + // validate component counts (num outputs, etc.) + if (!validate_sp_semantics_coinbase_component_counts_v1( + semantic_config_coinbase_component_counts_v1(tx.tx_semantic_rules_version), + tx.outputs.size(), + tx.tx_supplement.output_enote_ephemeral_pubkeys.size())) + return false; + + // validate output serialization semantics + if (!validate_sp_semantics_output_serialization_v1(tx.outputs)) + return false; + + // validate layout (sorting, uniqueness) of outputs and tx supplement + if (!validate_sp_semantics_coinbase_layout_v1(tx.outputs, + tx.tx_supplement.output_enote_ephemeral_pubkeys, + tx.tx_supplement.tx_extra)) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +template <> +bool validate_tx_key_images(const SpTxCoinbaseV1&, const TxValidationContext&) +{ + // coinbase txs have no key images + return true; +} +//------------------------------------------------------------------------------------------------------------------- +template <> +bool validate_tx_amount_balance(const SpTxCoinbaseV1 &tx) +{ + // balance proof + if (!validate_sp_coinbase_amount_balance_v1(tx.block_reward, tx.outputs)) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +template <> +bool validate_tx_input_proofs(const SpTxCoinbaseV1&, const TxValidationContext&) +{ + // coinbase txs have no input prooofs + return true; +} +//------------------------------------------------------------------------------------------------------------------- +template <> +bool validate_txs_batchable(const std::vector&, const TxValidationContext&) +{ + // coinbase txs have no batchable proofs to verify + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool try_get_tx_contextual_validation_id(const SpTxCoinbaseV1 &tx, const TxValidationContext&, rct::key &validation_id_out) +{ + try + { + // 1. tx id + rct::key tx_id; + get_sp_tx_coinbase_v1_txid(tx, tx_id); + + // 2. validation_id = H_32(tx_id) + SpFSTranscript transcript{config::HASH_KEY_SERAPHIS_TX_CONTEXTUAL_VALIDATION_ID_V1, sizeof(tx_id)}; + transcript.append("tx_id", tx_id); + + sp_hash_to_32(transcript.data(), transcript.size(), validation_id_out.bytes); + } catch (...) { return false; } + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace sp diff --git a/src/seraphis_main/txtype_coinbase_v1.h b/src/seraphis_main/txtype_coinbase_v1.h new file mode 100644 index 0000000000..d19d6c81f1 --- /dev/null +++ b/src/seraphis_main/txtype_coinbase_v1.h @@ -0,0 +1,165 @@ +// Copyright (c) 2022, 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 coinbase Seraphis transaction. + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "misc_log_ex.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/jamtis_payment_proposal.h" +#include "seraphis_core/sp_core_types.h" +#include "tx_builder_types.h" +#include "tx_component_types.h" +#include "tx_validation_context.h" +#include "tx_validators.h" +#include "txtype_base.h" + +//third party headers + +//standard headers +#include +#include + +//forward declarations + + +namespace sp +{ + +//// +// Seraphis coinbase tx +// - input: block height + block reward amount +// - outputs: cleartext amounts +// - memo field: sorted TLV format +/// +struct SpTxCoinbaseV1 final +{ + enum class SemanticRulesVersion : unsigned char + { + MOCK = 0, + ONE = 1 + }; + + /// semantic rules version + SemanticRulesVersion tx_semantic_rules_version; + + /// height of the block whose block reward this coinbase tx disperses + std::uint64_t block_height; + /// block reward dispersed by this coinbase tx + rct::xmr_amount block_reward; + /// tx outputs (new coinbase enotes) + std::vector outputs; + /// supplemental data for tx + SpTxSupplementV1 tx_supplement; +}; + +/// get size of a possible tx +std::size_t sp_tx_coinbase_v1_size_bytes(const std::size_t num_outputs, const std::size_t tx_extra_size); +/// get size of the tx +std::size_t sp_tx_coinbase_v1_size_bytes(const SpTxCoinbaseV1 &tx); +/// get weight of a possible tx (weight == size) +std::size_t sp_tx_coinbase_v1_weight(const std::size_t num_outputs, const TxExtra &tx_extra); +/// get weight of the tx (weight == size) +std::size_t sp_tx_coinbase_v1_weight(const SpTxCoinbaseV1 &tx); + +/** +* brief: get_sp_tx_coinbase_v1_txid - get the transaction id +* param: tx - +* outparam: tx_id_out - +*/ +void get_sp_tx_coinbase_v1_txid(const SpTxCoinbaseV1 &tx, rct::key &tx_id_out); +/** +* brief: make_seraphis_tx_coinbase_v1 - make an SpTxCoinbaseV1 transaction +* ... +* outparam: tx_out - +*/ +void make_seraphis_tx_coinbase_v1(const SpTxCoinbaseV1::SemanticRulesVersion semantic_rules_version, + const std::uint64_t block_height, + const rct::xmr_amount block_reward, + std::vector outputs, + SpTxSupplementV1 tx_supplement, + SpTxCoinbaseV1 &tx_out); +void make_seraphis_tx_coinbase_v1(const SpTxCoinbaseV1::SemanticRulesVersion semantic_rules_version, + const SpCoinbaseTxProposalV1 &tx_proposal, + SpTxCoinbaseV1 &tx_out); +void make_seraphis_tx_coinbase_v1(const SpTxCoinbaseV1::SemanticRulesVersion semantic_rules_version, + const std::uint64_t block_height, + const rct::xmr_amount block_reward, + std::vector normal_payment_proposals, + std::vector additional_memo_elements, + SpTxCoinbaseV1 &tx_out); + +/** +* brief: semantic_config_coinbase_component_counts_v1 - component count configuration for a given semantics rule version +* param: tx_semantic_rules_version - +* return: allowed component counts for the given semantics rules version +*/ +SemanticConfigCoinbaseComponentCountsV1 semantic_config_coinbase_component_counts_v1( + const SpTxCoinbaseV1::SemanticRulesVersion tx_semantic_rules_version); + + +//// tx base concept implementations + +/// short descriptor of the tx type +template <> +inline std::string tx_descriptor() { return "SpCoinbaseV1"; } + +/// tx structure version +template <> +inline unsigned char tx_structure_version() +{ + return static_cast(TxStructureVersionSp::TxTypeSpCoinbaseV1); +} + +/// versioning for an SpTxCoinbaseV1 tx +inline tx_version_t tx_version_from(const SpTxCoinbaseV1::SemanticRulesVersion tx_semantic_rules_version) +{ + return tx_version_from(static_cast(tx_semantic_rules_version)); +} + +/// transaction validators +template <> +bool validate_tx_semantics(const SpTxCoinbaseV1 &tx); +template <> +bool validate_tx_key_images(const SpTxCoinbaseV1&, const TxValidationContext&); +template <> +bool validate_tx_amount_balance(const SpTxCoinbaseV1 &tx); +template <> +bool validate_tx_input_proofs(const SpTxCoinbaseV1&, const TxValidationContext&); +template <> +bool validate_txs_batchable(const std::vector&, const TxValidationContext&); + +/// contextual validation id +/// - can be used for checking if an already-validated tx (whose contextual validation id was recorded) is still valid +/// against a validation context that may have changed (e.g. due to a reorg) +bool try_get_tx_contextual_validation_id(const SpTxCoinbaseV1 &tx, const TxValidationContext&, rct::key &validation_id_out); + +} //namespace sp diff --git a/src/seraphis_main/txtype_squashed_v1.cpp b/src/seraphis_main/txtype_squashed_v1.cpp new file mode 100644 index 0000000000..dd32d19502 --- /dev/null +++ b/src/seraphis_main/txtype_squashed_v1.cpp @@ -0,0 +1,729 @@ +// Copyright (c) 2022, 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 "txtype_squashed_v1.h" + +//local headers +#include "common/container_helpers.h" +#include "cryptonote_config.h" +#include "misc_log_ex.h" +#include "ringct/multiexp.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/binned_reference_set.h" +#include "seraphis_core/binned_reference_set_utils.h" +#include "seraphis_core/discretized_fee.h" +#include "seraphis_core/jamtis_payment_proposal.h" +#include "seraphis_core/sp_core_enote_utils.h" +#include "seraphis_core/sp_core_types.h" +#include "seraphis_crypto/bulletproofs_plus2.h" +#include "seraphis_crypto/sp_crypto_utils.h" +#include "seraphis_crypto/sp_hash_functions.h" +#include "seraphis_crypto/sp_multiexp.h" +#include "seraphis_crypto/sp_transcript.h" +#include "tx_builder_types.h" +#include "tx_builders_inputs.h" +#include "tx_builders_legacy_inputs.h" +#include "tx_builders_mixed.h" +#include "tx_builders_outputs.h" +#include "tx_component_types.h" +#include "tx_component_types_legacy.h" +#include "tx_validation_context.h" +#include "tx_validators.h" + +//third party headers + +//standard headers +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis" + +namespace sp +{ +//------------------------------------------------------------------------------------------------------------------- +std::size_t sp_tx_squashed_v1_size_bytes(const std::size_t num_legacy_inputs, + const std::size_t num_sp_inputs, + const std::size_t num_outputs, + const std::size_t legacy_ring_size, + const std::size_t ref_set_decomp_n, + const std::size_t ref_set_decomp_m, + const std::size_t num_bin_members, + const std::size_t tx_extra_size) +{ + // size of the transaction as represented in C++ (it is likely ~5-15% smaller when serialized) + // note: configs and derived data that are cached post-deserialization are NOT included (e.g. binned reference set + // config and seed) + std::size_t size{0}; + + // legacy input images + size += num_legacy_inputs * legacy_enote_image_v2_size_bytes(); + + // seraphis input images + size += num_sp_inputs * sp_enote_image_v1_size_bytes(); + + // outputs + size += num_outputs * sp_enote_v1_size_bytes(); + + // balance proof (note: only seraphis inputs and outputs are range proofed) + size += sp_balance_proof_v1_size_bytes_compact(num_sp_inputs + num_outputs); + + // legacy ring signatures + size += num_legacy_inputs * legacy_ring_signature_v4_size_bytes(legacy_ring_size); + + // ownership/key-image-legitimacy proof for all seraphis inputs + size += num_sp_inputs * sp_image_proof_v1_size_bytes(); + + // membership proofs for seraphis inputs + size += num_sp_inputs * sp_membership_proof_v1_size_bytes_compact(ref_set_decomp_n, ref_set_decomp_m, num_bin_members); + + // extra data in tx + size += sp_tx_supplement_v1_size_bytes(num_outputs, tx_extra_size, true); //with shared ephemeral pubkey assumption + + // tx fee + size += discretized_fee_size_bytes(); + + return size; +} +//------------------------------------------------------------------------------------------------------------------- +std::size_t sp_tx_squashed_v1_size_bytes(const SpTxSquashedV1 &tx) +{ + const std::size_t legacy_ring_size{ + tx.legacy_ring_signatures.size() + ? tx.legacy_ring_signatures[0].reference_set.size() + : 0 + }; + const std::size_t ref_set_decomp_n{ + tx.sp_membership_proofs.size() + ? tx.sp_membership_proofs[0].ref_set_decomp_n + : 0 + }; + const std::size_t ref_set_decomp_m{ + tx.sp_membership_proofs.size() + ? tx.sp_membership_proofs[0].ref_set_decomp_m + : 0 + }; + const std::size_t num_bin_members{ + tx.sp_membership_proofs.size() + ? tx.sp_membership_proofs[0].binned_reference_set.bin_config.num_bin_members + : 0u + }; + + return sp_tx_squashed_v1_size_bytes(tx.legacy_input_images.size(), + tx.sp_input_images.size(), + tx.outputs.size(), + legacy_ring_size, + ref_set_decomp_n, + ref_set_decomp_m, + num_bin_members, + tx.tx_supplement.tx_extra.size()); +} +//------------------------------------------------------------------------------------------------------------------- +std::size_t sp_tx_squashed_v1_weight(const std::size_t num_legacy_inputs, + const std::size_t num_sp_inputs, + const std::size_t num_outputs, + const std::size_t legacy_ring_size, + const std::size_t ref_set_decomp_n, + const std::size_t ref_set_decomp_m, + const std::size_t num_bin_members, + const std::size_t tx_extra_size) +{ + // tx weight = tx size + balance proof clawback + std::size_t weight{ + sp_tx_squashed_v1_size_bytes(num_legacy_inputs, + num_sp_inputs, + num_outputs, + legacy_ring_size, + ref_set_decomp_n, + ref_set_decomp_m, + num_bin_members, + tx_extra_size) + }; + + // subtract balance proof size and add its weight + weight -= sp_balance_proof_v1_size_bytes_compact(num_sp_inputs + num_outputs); + weight += sp_balance_proof_v1_weight(num_sp_inputs + num_outputs); + + return weight; +} +//------------------------------------------------------------------------------------------------------------------- +std::size_t sp_tx_squashed_v1_weight(const SpTxSquashedV1 &tx) +{ + const std::size_t legacy_ring_size{ + tx.legacy_ring_signatures.size() + ? tx.legacy_ring_signatures[0].reference_set.size() + : 0 + }; + const std::size_t ref_set_decomp_n{ + tx.sp_membership_proofs.size() + ? tx.sp_membership_proofs[0].ref_set_decomp_n + : 0 + }; + const std::size_t ref_set_decomp_m{ + tx.sp_membership_proofs.size() + ? tx.sp_membership_proofs[0].ref_set_decomp_m + : 0 + }; + const std::size_t num_bin_members{ + tx.sp_membership_proofs.size() + ? tx.sp_membership_proofs[0].binned_reference_set.bin_config.num_bin_members + : 0u + }; + + return sp_tx_squashed_v1_weight(tx.legacy_input_images.size(), + tx.sp_input_images.size(), + tx.outputs.size(), + legacy_ring_size, + ref_set_decomp_n, + ref_set_decomp_m, + num_bin_members, + tx.tx_supplement.tx_extra.size()); +} +//------------------------------------------------------------------------------------------------------------------- +void get_sp_tx_squashed_v1_txid(const SpTxSquashedV1 &tx, rct::key &tx_id_out) +{ + // tx_id = H_32(tx_proposal_prefix, tx_artifacts_merkle_root) + + // 1. tx proposal prefix + // H_32(tx version, legacy input key images, seraphis input key images, output enotes, fee, tx supplement) + rct::key tx_proposal_prefix; + make_tx_proposal_prefix_v1(tx, tx_proposal_prefix); + + // 2. input images prefix + // - note: key images are represented in the tx id twice (tx proposal prefix and input images + // - the reasons are: A) decouple proposals from the enote image structure, B) don't require proposals to commit + // to input commitment masks + // H_32({C", KI}((legacy)), {K", C", KI}((seraphis))) + rct::key input_images_prefix; + make_input_images_prefix_v1(tx.legacy_input_images, tx.sp_input_images, input_images_prefix); + + // 3. tx proofs prefix + // H_32(balance proof, legacy ring signatures, image proofs, seraphis membership proofs) + rct::key tx_proofs_prefix; + make_tx_proofs_prefix_v1(tx.balance_proof, + tx.legacy_ring_signatures, + tx.sp_image_proofs, + tx.sp_membership_proofs, + tx_proofs_prefix); + + // 4. tx artifacts prefix + // H_32(input images prefix, tx proofs prefix) + rct::key tx_artifacts_merkle_root; + make_tx_artifacts_merkle_root_v1(input_images_prefix, tx_proofs_prefix, tx_artifacts_merkle_root); + + // 5. tx id + // tx_id = H_32(tx_proposal_prefix, tx_artifacts_merkle_root) + SpFSTranscript transcript{config::HASH_KEY_SERAPHIS_TRANSACTION_TYPE_SQUASHED_V1, 2*sizeof(rct::key)}; + transcript.append("prefix", tx_proposal_prefix); + transcript.append("artifacts", tx_artifacts_merkle_root); + + assert(transcript.size() <= 128 && "sp squashed v1 tx id must fit within one blake2b block (128 bytes)."); + sp_hash_to_32(transcript.data(), transcript.size(), tx_id_out.bytes); +} +//------------------------------------------------------------------------------------------------------------------- +void make_seraphis_tx_squashed_v1(const SpTxSquashedV1::SemanticRulesVersion semantic_rules_version, + std::vector legacy_input_images, + std::vector sp_input_images, + std::vector outputs, + SpBalanceProofV1 balance_proof, + std::vector legacy_ring_signatures, + std::vector sp_image_proofs, + std::vector sp_membership_proofs, + SpTxSupplementV1 tx_supplement, + const DiscretizedFee discretized_transaction_fee, + SpTxSquashedV1 &tx_out) +{ + tx_out.tx_semantic_rules_version = semantic_rules_version; + tx_out.legacy_input_images = std::move(legacy_input_images); + tx_out.sp_input_images = std::move(sp_input_images); + tx_out.outputs = std::move(outputs); + tx_out.balance_proof = std::move(balance_proof); + tx_out.legacy_ring_signatures = std::move(legacy_ring_signatures); + tx_out.sp_image_proofs = std::move(sp_image_proofs); + tx_out.sp_membership_proofs = std::move(sp_membership_proofs); + tx_out.tx_supplement = std::move(tx_supplement); + tx_out.tx_fee = discretized_transaction_fee; + + CHECK_AND_ASSERT_THROW_MES(validate_tx_semantics(tx_out), "Failed to assemble an SpTxSquashedV1."); +} +//------------------------------------------------------------------------------------------------------------------- +void make_seraphis_tx_squashed_v1(const SpTxSquashedV1::SemanticRulesVersion semantic_rules_version, + SpPartialTxV1 partial_tx, + std::vector sp_membership_proofs, + SpTxSquashedV1 &tx_out) +{ + // check partial tx semantics + check_v1_partial_tx_semantics_v1(partial_tx, semantic_rules_version); + + // note: seraphis membership proofs cannot be validated without the ledger used to construct them, so there is no + // check here + + // finish tx + make_seraphis_tx_squashed_v1(semantic_rules_version, + std::move(partial_tx.legacy_input_images), + std::move(partial_tx.sp_input_images), + std::move(partial_tx.outputs), + std::move(partial_tx.balance_proof), + std::move(partial_tx.legacy_ring_signatures), + std::move(partial_tx.sp_image_proofs), + std::move(sp_membership_proofs), + std::move(partial_tx.tx_supplement), + partial_tx.tx_fee, + tx_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_seraphis_tx_squashed_v1(const SpTxSquashedV1::SemanticRulesVersion semantic_rules_version, + SpPartialTxV1 partial_tx, + std::vector alignable_membership_proofs, + SpTxSquashedV1 &tx_out) +{ + // line up the the membership proofs with the partial tx's input images (which are sorted) + std::vector tx_membership_proofs; + align_v1_membership_proofs_v1(partial_tx.sp_input_images, + std::move(alignable_membership_proofs), + tx_membership_proofs); + + // finish tx + make_seraphis_tx_squashed_v1(semantic_rules_version, std::move(partial_tx), std::move(tx_membership_proofs), tx_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_seraphis_tx_squashed_v1(const SpTxSquashedV1::SemanticRulesVersion semantic_rules_version, + const SpTxProposalV1 &tx_proposal, + std::vector legacy_inputs, + std::vector sp_partial_inputs, + std::vector sp_membership_proof_preps, + const rct::key &legacy_spend_pubkey, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + SpTxSquashedV1 &tx_out) +{ + // partial tx + SpPartialTxV1 partial_tx; + make_v1_partial_tx_v1(tx_proposal, + std::move(legacy_inputs), + std::move(sp_partial_inputs), + tx_version_from(semantic_rules_version), + legacy_spend_pubkey, + jamtis_spend_pubkey, + k_view_balance, + partial_tx); + + // seraphis membership proofs (assumes the caller prepared to make a membership proof for each input) + std::vector alignable_membership_proofs; + make_v1_alignable_membership_proofs_v1(std::move(sp_membership_proof_preps), alignable_membership_proofs); + + // finish tx + make_seraphis_tx_squashed_v1(semantic_rules_version, + std::move(partial_tx), + std::move(alignable_membership_proofs), + tx_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_seraphis_tx_squashed_v1(const SpTxSquashedV1::SemanticRulesVersion semantic_rules_version, + const SpTxProposalV1 &tx_proposal, + std::vector legacy_ring_signature_preps, + std::vector sp_membership_proof_preps, + const crypto::secret_key &legacy_spend_privkey, + const crypto::secret_key &sp_spend_privkey, + const crypto::secret_key &k_view_balance, + hw::device &hwdev, + SpTxSquashedV1 &tx_out) +{ + // tx proposal prefix + rct::key tx_proposal_prefix; + get_tx_proposal_prefix_v1(tx_proposal, tx_version_from(semantic_rules_version), k_view_balance, tx_proposal_prefix); + + // legacy inputs + std::vector legacy_inputs; + make_v1_legacy_inputs_v1(tx_proposal_prefix, + tx_proposal.legacy_input_proposals, + std::move(legacy_ring_signature_preps), + legacy_spend_privkey, + hwdev, + legacy_inputs); + + // seraphis partial inputs + std::vector sp_partial_inputs; + make_v1_partial_inputs_v1(tx_proposal.sp_input_proposals, + tx_proposal_prefix, + sp_spend_privkey, + k_view_balance, + sp_partial_inputs); + + // legacy spend pubkey + const rct::key legacy_spend_pubkey{rct::scalarmultBase(rct::sk2rct(legacy_spend_privkey))}; + + // jamtis spend pubkey + rct::key jamtis_spend_pubkey; + make_seraphis_spendkey(k_view_balance, sp_spend_privkey, jamtis_spend_pubkey); + + // finish tx + make_seraphis_tx_squashed_v1(semantic_rules_version, + tx_proposal, + std::move(legacy_inputs), + std::move(sp_partial_inputs), + std::move(sp_membership_proof_preps), + legacy_spend_pubkey, + jamtis_spend_pubkey, + k_view_balance, + tx_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_seraphis_tx_squashed_v1(const SpTxSquashedV1::SemanticRulesVersion semantic_rules_version, + std::vector normal_payment_proposals, + std::vector selfsend_payment_proposals, + const DiscretizedFee discretized_transaction_fee, + std::vector legacy_input_proposals, + std::vector sp_input_proposals, + std::vector additional_memo_elements, + std::vector legacy_ring_signature_preps, + std::vector sp_membership_proof_preps, + const crypto::secret_key &legacy_spend_privkey, + const crypto::secret_key &sp_spend_privkey, + const crypto::secret_key &k_view_balance, + hw::device &hwdev, + SpTxSquashedV1 &tx_out) +{ + // tx proposal + SpTxProposalV1 tx_proposal; + make_v1_tx_proposal_v1(std::move(legacy_input_proposals), + std::move(sp_input_proposals), + std::move(normal_payment_proposals), + std::move(selfsend_payment_proposals), + discretized_transaction_fee, + std::move(additional_memo_elements), + tx_proposal); + + // finish tx + make_seraphis_tx_squashed_v1(semantic_rules_version, + tx_proposal, + std::move(legacy_ring_signature_preps), + std::move(sp_membership_proof_preps), + legacy_spend_privkey, + sp_spend_privkey, + k_view_balance, + hwdev, + tx_out); +} +//------------------------------------------------------------------------------------------------------------------- +SemanticConfigComponentCountsV1 semantic_config_component_counts_v1( + const SpTxSquashedV1::SemanticRulesVersion tx_semantic_rules_version) +{ + SemanticConfigComponentCountsV1 config{}; + + // note: in the squashed model, inputs + outputs must be <= the BP+ pre-generated generator array size ('maxM') + if (tx_semantic_rules_version == SpTxSquashedV1::SemanticRulesVersion::MOCK) + { + config.min_inputs = 1; + config.max_inputs = 100000; + config.min_outputs = 1; + config.max_outputs = 100000; + } + else if (tx_semantic_rules_version == SpTxSquashedV1::SemanticRulesVersion::ONE) + { + config.min_inputs = 1; + config.max_inputs = config::SP_MAX_INPUTS_V1; + config.min_outputs = 2; + config.max_outputs = config::SP_MAX_OUTPUTS_V1; + } + else //unknown semantic rules version + { + CHECK_AND_ASSERT_THROW_MES(false, "Tried to get semantic config for component counts with unknown rules version."); + } + + return config; +} +//------------------------------------------------------------------------------------------------------------------- +SemanticConfigLegacyRefSetV1 semantic_config_legacy_ref_sets_v1( + const SpTxSquashedV1::SemanticRulesVersion tx_semantic_rules_version) +{ + SemanticConfigLegacyRefSetV1 config{}; + + if (tx_semantic_rules_version == SpTxSquashedV1::SemanticRulesVersion::MOCK) + { + config.ring_size_min = 1; + config.ring_size_max = 1000; + } + else if (tx_semantic_rules_version == SpTxSquashedV1::SemanticRulesVersion::ONE) + { + config.ring_size_min = config::LEGACY_RING_SIZE_V1; + config.ring_size_max = config::LEGACY_RING_SIZE_V1; + } + else //unknown semantic rules version + { + CHECK_AND_ASSERT_THROW_MES(false, + "Tried to get semantic config for legacy ref set sizes with unknown rules version."); + } + + return config; +} +//------------------------------------------------------------------------------------------------------------------- +SemanticConfigSpRefSetV1 semantic_config_sp_ref_sets_v1( + const SpTxSquashedV1::SemanticRulesVersion tx_semantic_rules_version) +{ + SemanticConfigSpRefSetV1 config{}; + + if (tx_semantic_rules_version == SpTxSquashedV1::SemanticRulesVersion::MOCK) + { + // note: if n*m exceeds GROOTLE_MAX_MN, an exception will be thrown + config.decomp_n_min = 2; + config.decomp_n_max = 100000; + config.decomp_m_min = 2; + config.decomp_m_max = 100000; + config.bin_radius_min = 0; + config.bin_radius_max = 30000; + config.num_bin_members_min = 1; + config.num_bin_members_max = 60000; + } + else if (tx_semantic_rules_version == SpTxSquashedV1::SemanticRulesVersion::ONE) + { + config.decomp_n_min = config::SP_GROOTLE_N_V1; + config.decomp_n_max = config::SP_GROOTLE_N_V1; + config.decomp_m_min = config::SP_GROOTLE_M_V1; + config.decomp_m_max = config::SP_GROOTLE_M_V1; + config.bin_radius_min = config::SP_REF_SET_BIN_RADIUS_V1; + config.bin_radius_max = config::SP_REF_SET_BIN_RADIUS_V1; + config.num_bin_members_min = config::SP_REF_SET_NUM_BIN_MEMBERS_V1; + config.num_bin_members_max = config::SP_REF_SET_NUM_BIN_MEMBERS_V1; + } + else //unknown semantic rules version + { + CHECK_AND_ASSERT_THROW_MES(false, "Tried to get semantic config for ref set sizes with unknown rules version."); + } + + return config; +} +//------------------------------------------------------------------------------------------------------------------- +template <> +bool validate_tx_semantics(const SpTxSquashedV1 &tx) +{ + // validate component counts (num inputs/outputs/etc.) + if (!validate_sp_semantics_component_counts_v1(semantic_config_component_counts_v1(tx.tx_semantic_rules_version), + tx.legacy_input_images.size(), + tx.sp_input_images.size(), + tx.legacy_ring_signatures.size(), + tx.sp_membership_proofs.size(), + tx.sp_image_proofs.size(), + tx.outputs.size(), + tx.tx_supplement.output_enote_ephemeral_pubkeys.size(), + tx.balance_proof.bpp2_proof.V.size())) + return false; + + // validate legacy input proof reference set sizes + if (!validate_sp_semantics_legacy_reference_sets_v1(semantic_config_legacy_ref_sets_v1(tx.tx_semantic_rules_version), + tx.legacy_ring_signatures)) + return false; + + // validate seraphis input proof reference set sizes + if (!validate_sp_semantics_sp_reference_sets_v1(semantic_config_sp_ref_sets_v1(tx.tx_semantic_rules_version), + tx.sp_membership_proofs)) + return false; + + // validate output serialization semantics + if (!validate_sp_semantics_output_serialization_v2(tx.outputs)) + return false; + + // validate input image semantics + if (!validate_sp_semantics_input_images_v1(tx.legacy_input_images, tx.sp_input_images)) + return false; + + // validate layout (sorting, uniqueness) of input images, membership proof ref sets, outputs, and tx supplement + if (!validate_sp_semantics_layout_v1(tx.legacy_ring_signatures, + tx.sp_membership_proofs, + tx.legacy_input_images, + tx.sp_input_images, + tx.outputs, + tx.tx_supplement.output_enote_ephemeral_pubkeys, + tx.tx_supplement.tx_extra)) + return false; + + // validate the tx fee is well-formed + if (!validate_sp_semantics_fee_v1(tx.tx_fee)) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +template <> +bool validate_tx_key_images(const SpTxSquashedV1 &tx, const TxValidationContext &tx_validation_context) +{ + // unspentness proof: check that key images are not in the ledger + if (!validate_sp_key_images_v1(tx.legacy_input_images, tx.sp_input_images, tx_validation_context)) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +template <> +bool validate_tx_amount_balance(const SpTxSquashedV1 &tx) +{ + // balance proof + if (!validate_sp_amount_balance_v1(tx.legacy_input_images, + tx.sp_input_images, + tx.outputs, + tx.tx_fee, + tx.balance_proof)) + return false; + + // deferred for batching: range proofs + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +template <> +bool validate_tx_input_proofs(const SpTxSquashedV1 &tx, const TxValidationContext &tx_validation_context) +{ + // prepare image proofs message + rct::key tx_proposal_prefix; + make_tx_proposal_prefix_v1(tx, tx_proposal_prefix); + + // ownership, membership, and key image validity of legacy inputs + if (!validate_sp_legacy_input_proofs_v1(tx.legacy_ring_signatures, + tx.legacy_input_images, + tx_proposal_prefix, + tx_validation_context)) + return false; + + // ownership proof (and proof that key images are well-formed) + if (!validate_sp_composition_proofs_v1(tx.sp_image_proofs, tx.sp_input_images, tx_proposal_prefix)) + return false; + + // deferred for batching: seraphis membership proofs + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +template <> +bool validate_txs_batchable(const std::vector &txs, + const TxValidationContext &tx_validation_context) +{ + std::vector sp_membership_proof_ptrs; + std::vector sp_input_image_ptrs; + std::vector range_proof_ptrs; + sp_membership_proof_ptrs.reserve(txs.size()*20); //heuristic... (most txs have 1-2 seraphis inputs) + sp_input_image_ptrs.reserve(txs.size()*20); + range_proof_ptrs.reserve(txs.size()); + + // prepare for batch-verification + for (const SpTxSquashedV1 *tx : txs) + { + if (!tx) + return false; + + // gather membership proof pieces + for (const SpMembershipProofV1 &sp_membership_proof : tx->sp_membership_proofs) + sp_membership_proof_ptrs.push_back(&sp_membership_proof); + + for (const SpEnoteImageV1 &sp_input_image : tx->sp_input_images) + sp_input_image_ptrs.push_back(&(sp_input_image.core)); + + // gather range proofs + range_proof_ptrs.push_back(&(tx->balance_proof.bpp2_proof)); + } + + // batch verification: collect pippenger data sets for an aggregated multiexponentiation + + // seraphis membership proofs + std::list validation_data_sp_membership_proofs; + if (!try_get_sp_membership_proofs_v1_validation_data(sp_membership_proof_ptrs, + sp_input_image_ptrs, + tx_validation_context, + validation_data_sp_membership_proofs)) + return false; + + // range proofs + std::list validation_data_range_proofs; + if (!try_get_bulletproof_plus2_verification_data(range_proof_ptrs, validation_data_range_proofs)) + return false; + + // batch verify + std::list validation_data{std::move(validation_data_sp_membership_proofs)}; + validation_data.splice(validation_data.end(), validation_data_range_proofs); + + if (!SpMultiexp{validation_data}.evaluates_to_point_at_infinity()) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool try_get_tx_contextual_validation_id(const SpTxSquashedV1 &tx, + const TxValidationContext &tx_validation_context, + rct::key &validation_id_out) +{ + try + { + // 1. check key images + if (!validate_sp_key_images_v1(tx.legacy_input_images, tx.sp_input_images, tx_validation_context)) + return false; + + // 2. get legacy ring members + std::vector legacy_ring_members; + legacy_ring_members.reserve(tx.legacy_ring_signatures.size()); + + for (const LegacyRingSignatureV4 &legacy_ring_signature : tx.legacy_ring_signatures) + { + // get the legacy ring members + tx_validation_context.get_reference_set_proof_elements_v1( + legacy_ring_signature.reference_set, + tools::add_element(legacy_ring_members)); + } + + // 3. get seraphis reference set elements + std::vector sp_reference_indices_temp; + std::vector sp_membership_proof_refs; + sp_membership_proof_refs.reserve(tx.sp_membership_proofs.size()); + + for (const SpMembershipProofV1 &sp_membership_proof : tx.sp_membership_proofs) + { + // a. decompress the reference set indices + if(!try_get_reference_indices_from_binned_reference_set_v1(sp_membership_proof.binned_reference_set, + sp_reference_indices_temp)) + return false; + + // b. get the seraphis reference set elements + tx_validation_context.get_reference_set_proof_elements_v2(sp_reference_indices_temp, + tools::add_element(sp_membership_proof_refs)); + } + + // 4. transaction id + rct::key tx_id; + get_sp_tx_squashed_v1_txid(tx, tx_id); + + // 5. validation_id = H_32(tx_id, legacy ring members, seraphis membership proof reference elements) + SpFSTranscript transcript{config::HASH_KEY_SERAPHIS_TX_CONTEXTUAL_VALIDATION_ID_V2, sizeof(tx_id)}; + transcript.append("tx_id", tx_id); + transcript.append("legacy_ring_members", legacy_ring_members); + transcript.append("sp_membership_proof_refs", sp_membership_proof_refs); + + sp_hash_to_32(transcript.data(), transcript.size(), validation_id_out.bytes); + } catch (...) { return false; } + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace sp diff --git a/src/seraphis_main/txtype_squashed_v1.h b/src/seraphis_main/txtype_squashed_v1.h new file mode 100644 index 0000000000..53e7653f06 --- /dev/null +++ b/src/seraphis_main/txtype_squashed_v1.h @@ -0,0 +1,252 @@ +// Copyright (c) 2022, 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 normal Seraphis transaction implemented in the 'squashed enote' model. + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "device/device.hpp" +#include "misc_log_ex.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/discretized_fee.h" +#include "seraphis_core/jamtis_payment_proposal.h" +#include "seraphis_core/sp_core_types.h" +#include "tx_builder_types.h" +#include "tx_component_types.h" +#include "tx_component_types_legacy.h" +#include "tx_validation_context.h" +#include "tx_validators.h" +#include "txtype_base.h" + +//third party headers + +//standard headers +#include +#include + +//forward declarations + + +namespace sp +{ + +//// +// Normal Seraphis tx in the squashed enote model +// - input membership/ownership/key image validity (legacy): clsag proofs (one per input) +// - input membership (seraphis): grootle proofs (one per input) +// - input ownership/key image validity (seraphis): seraphis composition proofs (one per input) +// - input reference sets (legacy): set of on-chain indices +// - input reference sets (seraphis): binned reference sets +// - outputs: seraphis enotes +// - range proofs: Bulletproof+ (aggregated range proofs for all seraphis inputs' masked commitments and new output +// enotes' commitments) +// - fees: discretized +// - memo field: sorted TLV format +/// +struct SpTxSquashedV1 final +{ + enum class SemanticRulesVersion : unsigned char + { + MOCK = 0, + ONE = 1 + }; + + /// semantic rules version + SemanticRulesVersion tx_semantic_rules_version; + + /// legacy tx input images (spent legacy enotes) + std::vector legacy_input_images; + /// seraphis tx input images (spent seraphis enotes) + std::vector sp_input_images; + /// tx outputs (new seraphis enotes) + std::vector outputs; + /// balance proof (balance proof and range proofs) + SpBalanceProofV1 balance_proof; + /// ring signature proofs: membership and ownership/key-image-legitimacy for each legacy input + std::vector legacy_ring_signatures; + /// composition proofs: ownership/key-image-legitimacy for each seraphis input + std::vector sp_image_proofs; + /// Grootle proofs on squashed enotes: membership for each seraphis input + std::vector sp_membership_proofs; + /// supplemental data for tx + SpTxSupplementV1 tx_supplement; + /// the transaction fee (discretized representation) + DiscretizedFee tx_fee; +}; + +/// get size of a possible tx (assuming compact components) +std::size_t sp_tx_squashed_v1_size_bytes(const std::size_t num_legacy_inputs, + const std::size_t num_sp_inputs, + const std::size_t num_outputs, + const std::size_t legacy_ring_size, + const std::size_t ref_set_decomp_n, + const std::size_t ref_set_decomp_m, + const std::size_t num_bin_members, + const std::size_t tx_extra_size); +/// get size of the tx (assuming compact components) +std::size_t sp_tx_squashed_v1_size_bytes(const SpTxSquashedV1 &tx); +/// get weight of a possible tx (assuming compact components) +std::size_t sp_tx_squashed_v1_weight(const std::size_t num_legacy_inputs, + const std::size_t num_sp_inputs, + const std::size_t num_outputs, + const std::size_t legacy_ring_size, + const std::size_t ref_set_decomp_n, + const std::size_t ref_set_decomp_m, + const std::size_t num_bin_members, + const std::size_t tx_extra_size); +/// get weight of the tx (assuming compact components) +std::size_t sp_tx_squashed_v1_weight(const SpTxSquashedV1 &tx); + +/** +* brief: get_sp_tx_squashed_v1_txid - get the transaction id +* param: tx - +* outparam: tx_id_out - +*/ +void get_sp_tx_squashed_v1_txid(const SpTxSquashedV1 &tx, rct::key &tx_id_out); +/** +* brief: make_seraphis_tx_squashed_v1 - make an SpTxSquashedV1 transaction +* ... +* outparam: tx_out - +*/ +void make_seraphis_tx_squashed_v1(const SpTxSquashedV1::SemanticRulesVersion semantic_rules_version, + std::vector legacy_input_images, + std::vector sp_input_images, + std::vector outputs, + SpBalanceProofV1 balance_proof, + std::vector legacy_ring_signatures, + std::vector sp_image_proofs, + std::vector sp_membership_proofs, + SpTxSupplementV1 tx_supplement, + const DiscretizedFee discretized_transaction_fee, + SpTxSquashedV1 &tx_out); +void make_seraphis_tx_squashed_v1(const SpTxSquashedV1::SemanticRulesVersion semantic_rules_version, + SpPartialTxV1 partial_tx, + std::vector sp_membership_proofs, + SpTxSquashedV1 &tx_out); +void make_seraphis_tx_squashed_v1(const SpTxSquashedV1::SemanticRulesVersion semantic_rules_version, + SpPartialTxV1 partial_tx, + std::vector alignable_membership_proofs, + SpTxSquashedV1 &tx_out); +void make_seraphis_tx_squashed_v1(const SpTxSquashedV1::SemanticRulesVersion semantic_rules_version, + const SpTxProposalV1 &tx_proposal, + std::vector legacy_inputs, + std::vector sp_partial_inputs, + std::vector sp_membership_proof_preps, + const rct::key &legacy_spend_pubkey, + const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + SpTxSquashedV1 &tx_out); +void make_seraphis_tx_squashed_v1(const SpTxSquashedV1::SemanticRulesVersion semantic_rules_version, + const SpTxProposalV1 &tx_proposal, + std::vector legacy_ring_signature_preps, + std::vector sp_membership_proof_preps, + const crypto::secret_key &legacy_spend_privkey, + const crypto::secret_key &sp_spend_privkey, + const crypto::secret_key &k_view_balance, + hw::device &hwdev, + SpTxSquashedV1 &tx_out); +void make_seraphis_tx_squashed_v1(const SpTxSquashedV1::SemanticRulesVersion semantic_rules_version, + std::vector normal_payment_proposals, + std::vector selfsend_payment_proposals, + const DiscretizedFee discretized_transaction_fee, + std::vector legacy_input_proposals, + std::vector sp_input_proposals, + std::vector additional_memo_elements, + std::vector legacy_ring_signature_preps, + std::vector sp_membership_proof_preps, + const crypto::secret_key &legacy_spend_privkey, + const crypto::secret_key &sp_spend_privkey, + const crypto::secret_key &k_view_balance, + hw::device &hwdev, + SpTxSquashedV1 &tx_out); + +/** +* brief: semantic_config_component_counts_v1 - component count configuration for a given semantics rule version +* param: tx_semantic_rules_version - +* return: allowed component counts for the given semantics rules version +*/ +SemanticConfigComponentCountsV1 semantic_config_component_counts_v1( + const SpTxSquashedV1::SemanticRulesVersion tx_semantic_rules_version); +/** +* brief: semantic_config_legacy_ref_sets_v1 - legacy reference set configuration for a given semantics rule version +* param: tx_semantic_rules_version - +* return: allowed reference set configuration for the given semantics rules version +*/ +SemanticConfigLegacyRefSetV1 semantic_config_legacy_ref_sets_v1( + const SpTxSquashedV1::SemanticRulesVersion tx_semantic_rules_version); +/** +* brief: semantic_config_sp_ref_sets_v1 - seraphis reference set configuration for a given semantics rule version +* param: tx_semantic_rules_version - +* return: allowed reference set configuration for the given semantics rules version +*/ +SemanticConfigSpRefSetV1 semantic_config_sp_ref_sets_v1( + const SpTxSquashedV1::SemanticRulesVersion tx_semantic_rules_version); + + +//// tx base concept implementations + +/// short descriptor of the tx type +template <> +inline std::string tx_descriptor() { return "SpSquashedV1"; } + +/// tx structure version +template <> +inline unsigned char tx_structure_version() +{ + return static_cast(TxStructureVersionSp::TxTypeSpSquashedV1); +} + +/// version of an SpTxSquashedV1 tx +inline tx_version_t tx_version_from(const SpTxSquashedV1::SemanticRulesVersion tx_semantic_rules_version) +{ + return tx_version_from(static_cast(tx_semantic_rules_version)); +} + +/// transaction validators +template <> +bool validate_tx_semantics(const SpTxSquashedV1 &tx); +template <> +bool validate_tx_key_images(const SpTxSquashedV1 &tx, const TxValidationContext &tx_validation_context); +template <> +bool validate_tx_amount_balance(const SpTxSquashedV1 &tx); +template <> +bool validate_tx_input_proofs(const SpTxSquashedV1 &tx, const TxValidationContext &tx_validation_context); +template <> +bool validate_txs_batchable(const std::vector &txs, + const TxValidationContext &tx_validation_context); + +/// contextual validation id +/// - can be used for checking if an already-validated tx (whose contextual validation id was recorded) is still valid +/// against a validation context that may have changed (e.g. due to a reorg) +bool try_get_tx_contextual_validation_id(const SpTxSquashedV1 &tx, + const TxValidationContext &tx_validation_context, + rct::key &validation_id_out); + +} //namespace sp diff --git a/src/seraphis_mocks/CMakeLists.txt b/src/seraphis_mocks/CMakeLists.txt new file mode 100644 index 0000000000..ed4796678c --- /dev/null +++ b/src/seraphis_mocks/CMakeLists.txt @@ -0,0 +1,69 @@ +# Copyright (c) 2021, 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(seraphis_mocks_sources + enote_finding_context_mocks.cpp + enote_store_mock_simple_v1.cpp + jamtis_mock_keys.cpp + legacy_mock_keys.cpp + make_mock_tx.cpp + mock_ledger_context.cpp + mock_offchain_context.cpp + mock_send_receive.cpp + mock_tx_builders_inputs.cpp + mock_tx_builders_legacy_inputs.cpp + mock_tx_builders_outputs.cpp + scan_chunk_consumer_mocks.cpp + tx_input_selector_mocks.cpp) + +monero_find_all_headers(seraphis_mocks_headers, "${CMAKE_CURRENT_SOURCE_DIR}") + +monero_add_library(seraphis_mocks + ${seraphis_mocks_sources} + ${seraphis_mocks_headers}) + +target_link_libraries(seraphis_mocks + PUBLIC + cncrypto + common + cryptonote_basic + device + epee + ringct + seraphis_core + seraphis_crypto + seraphis_impl + seraphis_main + PRIVATE + ${EXTRA_LIBRARIES}) + +target_include_directories(seraphis_mocks + PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}" + PRIVATE + ${Boost_INCLUDE_DIRS}) diff --git a/src/seraphis_mocks/enote_finding_context_mocks.cpp b/src/seraphis_mocks/enote_finding_context_mocks.cpp new file mode 100644 index 0000000000..bcf7e68641 --- /dev/null +++ b/src/seraphis_mocks/enote_finding_context_mocks.cpp @@ -0,0 +1,106 @@ +// Copyright (c) 2022, 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. + +// NOT FOR PRODUCTION + +//paired header +#include "enote_finding_context_mocks.h" + +//local headers +#include "seraphis_impl/scan_ledger_chunk_simple.h" +#include "seraphis_main/scan_core_types.h" + +//third party headers + +//standard headers +#include + + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis_mocks" + +namespace sp +{ +namespace mocks +{ +//------------------------------------------------------------------------------------------------------------------- +std::unique_ptr EnoteFindingContextLedgerMockLegacy::get_onchain_chunk( + const std::uint64_t chunk_start_index, + const std::uint64_t chunk_max_size) const +{ + scanning::ChunkContext chunk_context; + scanning::ChunkData chunk_data; + + m_mock_ledger_context.get_onchain_chunk_legacy(chunk_start_index, + chunk_max_size, + m_legacy_base_spend_pubkey, + m_legacy_subaddress_map, + m_legacy_view_privkey, + m_legacy_scan_mode, + chunk_context, + chunk_data); + + return std::make_unique( + std::move(chunk_context), + std::vector{std::move(chunk_data)}, + std::vector{rct::zero()} + ); +} +//------------------------------------------------------------------------------------------------------------------- +std::unique_ptr EnoteFindingContextLedgerMockSp::get_onchain_chunk( + const std::uint64_t chunk_start_index, + const std::uint64_t chunk_max_size) const +{ + scanning::ChunkContext chunk_context; + scanning::ChunkData chunk_data; + + m_mock_ledger_context.get_onchain_chunk_sp(chunk_start_index, + chunk_max_size, + m_xk_find_received, + chunk_context, + chunk_data); + + return std::make_unique( + std::move(chunk_context), + std::vector{std::move(chunk_data)}, + std::vector{rct::zero()} + ); +} +//------------------------------------------------------------------------------------------------------------------- +void EnoteFindingContextUnconfirmedMockSp::get_nonledger_chunk(scanning::ChunkData &chunk_out) const +{ + m_mock_ledger_context.get_unconfirmed_chunk_sp(m_xk_find_received, chunk_out); +} +//------------------------------------------------------------------------------------------------------------------- +void EnoteFindingContextOffchainMockSp::get_nonledger_chunk(scanning::ChunkData &chunk_out) const +{ + m_mock_offchain_context.get_offchain_chunk_sp(m_xk_find_received, chunk_out); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace mocks +} //namespace sp diff --git a/src/seraphis_mocks/enote_finding_context_mocks.h b/src/seraphis_mocks/enote_finding_context_mocks.h new file mode 100644 index 0000000000..dbc163591b --- /dev/null +++ b/src/seraphis_mocks/enote_finding_context_mocks.h @@ -0,0 +1,191 @@ +// Copyright (c) 2022, 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. + +// NOT FOR PRODUCTION + +// Dependency injectors for the find-received step of enote scanning (mock-ups). + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "crypto/x25519.h" +#include "mock_ledger_context.h" +#include "mock_offchain_context.h" +#include "ringct/rctTypes.h" +#include "seraphis_main/enote_finding_context.h" +#include "seraphis_main/scan_core_types.h" +#include "seraphis_main/scan_ledger_chunk.h" + +//third party headers + +//standard headers +#include +#include + +//forward declarations + + +namespace sp +{ +namespace mocks +{ + +/// LegacyScanMode: convenience enum for specifying legacy scan mode ('scan' or 'only process legacy key images') +enum class LegacyScanMode : unsigned char +{ + SCAN, + KEY_IMAGES_ONLY +}; + +//// +// EnoteFindingContextLedgerMockLegacy +// - wraps a mock ledger context, produces chunks of potentially owned enotes (from legacy view scanning) +// - note: if the legacy_scan_mode is set to KEY_IMAGES_ONLY, then chunks found will contain only key images +/// +class EnoteFindingContextLedgerMockLegacy final : public EnoteFindingContextLedger +{ +public: +//constructors + EnoteFindingContextLedgerMockLegacy(const MockLedgerContext &mock_ledger_context, + const rct::key &legacy_base_spend_pubkey, + const std::unordered_map &legacy_subaddress_map, + const crypto::secret_key &legacy_view_privkey, + const LegacyScanMode legacy_scan_mode) : + m_mock_ledger_context{mock_ledger_context}, + m_legacy_base_spend_pubkey{legacy_base_spend_pubkey}, + m_legacy_subaddress_map{legacy_subaddress_map}, + m_legacy_view_privkey{legacy_view_privkey}, + m_legacy_scan_mode{legacy_scan_mode} + {} + +//overloaded operators + /// disable copy/move (this is a scoped manager [reference wrapper]) + EnoteFindingContextLedgerMockLegacy& operator=(EnoteFindingContextLedgerMockLegacy&&) = delete; + +//member functions + /// get an onchain chunk (or empty chunk representing top of current chain) + std::unique_ptr get_onchain_chunk(const std::uint64_t chunk_start_index, + const std::uint64_t chunk_max_size) const override; + +//member variables +private: + const MockLedgerContext &m_mock_ledger_context; + const rct::key &m_legacy_base_spend_pubkey; + const std::unordered_map &m_legacy_subaddress_map; + const crypto::secret_key &m_legacy_view_privkey; + const LegacyScanMode m_legacy_scan_mode; +}; + +//// +// EnoteFindingContextLedgerMockSp +// - wraps a mock ledger context, produces chunks of potentially owned enotes (from find-received scanning) +/// +class EnoteFindingContextLedgerMockSp final : public EnoteFindingContextLedger +{ +public: +//constructors + EnoteFindingContextLedgerMockSp(const MockLedgerContext &mock_ledger_context, + const crypto::x25519_secret_key &xk_find_received) : + m_mock_ledger_context{mock_ledger_context}, + m_xk_find_received{xk_find_received} + {} + +//overloaded operators + /// disable copy/move (this is a scoped manager [reference wrapper]) + EnoteFindingContextLedgerMockSp& operator=(EnoteFindingContextLedgerMockSp&&) = delete; + +//member functions + /// get an onchain chunk (or empty chunk representing top of current chain) + std::unique_ptr get_onchain_chunk(const std::uint64_t chunk_start_index, + const std::uint64_t chunk_max_size) const override; + +//member variables +private: + const MockLedgerContext &m_mock_ledger_context; + const crypto::x25519_secret_key &m_xk_find_received; +}; + +//// +// EnoteFindingContextUnconfirmedMockSp +// - wraps a mock ledger context, produces chunks of potentially owned unconfirmed enotes (from find-received scanning) +/// +class EnoteFindingContextUnconfirmedMockSp final : public EnoteFindingContextNonLedger +{ +public: +//constructors + EnoteFindingContextUnconfirmedMockSp(const MockLedgerContext &mock_ledger_context, + const crypto::x25519_secret_key &xk_find_received) : + m_mock_ledger_context{mock_ledger_context}, + m_xk_find_received{xk_find_received} + {} + +//overloaded operators + /// disable copy/move (this is a scoped manager [reference wrapper]) + EnoteFindingContextUnconfirmedMockSp& operator=(EnoteFindingContextUnconfirmedMockSp&&) = delete; + +//member functions + /// get a fresh unconfirmed chunk + void get_nonledger_chunk(scanning::ChunkData &chunk_out) const override; + +//member variables +private: + const MockLedgerContext &m_mock_ledger_context; + const crypto::x25519_secret_key &m_xk_find_received; +}; + +//// +// EnoteFindingContextOffchainMockSp +// - wraps a mock offchain context, produces chunks of potentially owned enotes (from find-received scanning) +/// +class EnoteFindingContextOffchainMockSp final : public EnoteFindingContextNonLedger +{ +public: +//constructors + EnoteFindingContextOffchainMockSp(const MockOffchainContext &mock_offchain_context, + const crypto::x25519_secret_key &xk_find_received) : + m_mock_offchain_context{mock_offchain_context}, + m_xk_find_received{xk_find_received} + {} + +//overloaded operators + /// disable copy/move (this is a scoped manager [reference wrapper]) + EnoteFindingContextOffchainMockSp& operator=(EnoteFindingContextOffchainMockSp&&) = delete; + +//member functions + /// get a fresh offchain chunk + void get_nonledger_chunk(scanning::ChunkData &chunk_out) const override; + +//member variables +private: + const MockOffchainContext &m_mock_offchain_context; + const crypto::x25519_secret_key &m_xk_find_received; +}; + +} //namespace mocks +} //namespace sp diff --git a/src/seraphis_mocks/enote_store_mock_simple_v1.cpp b/src/seraphis_mocks/enote_store_mock_simple_v1.cpp new file mode 100644 index 0000000000..2bbfaea190 --- /dev/null +++ b/src/seraphis_mocks/enote_store_mock_simple_v1.cpp @@ -0,0 +1,61 @@ +// Copyright (c) 2022, 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. + +// NOT FOR PRODUCTION + +//paired header +#include "enote_store_mock_simple_v1.h" + +//local headers +#include "seraphis_main/contextual_enote_record_types.h" + +//third party headers + +//standard headers + + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis_mocks" + +namespace sp +{ +namespace mocks +{ +//------------------------------------------------------------------------------------------------------------------- +void SpEnoteStoreMockSimpleV1::add_record(const LegacyContextualEnoteRecordV1 &new_record) +{ + m_legacy_contextual_enote_records.emplace_back(new_record); +} +//------------------------------------------------------------------------------------------------------------------- +void SpEnoteStoreMockSimpleV1::add_record(const SpContextualEnoteRecordV1 &new_record) +{ + m_sp_contextual_enote_records.emplace_back(new_record); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace mocks +} //namespace sp diff --git a/src/seraphis_mocks/enote_store_mock_simple_v1.h b/src/seraphis_mocks/enote_store_mock_simple_v1.h new file mode 100644 index 0000000000..e75310df78 --- /dev/null +++ b/src/seraphis_mocks/enote_store_mock_simple_v1.h @@ -0,0 +1,75 @@ +// Copyright (c) 2022, 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. + +// NOT FOR PRODUCTION + +// Simple mock enote store that just stores enote records. + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "seraphis_main/contextual_enote_record_types.h" + +//third party headers +#include "boost/multiprecision/cpp_int.hpp" + +//standard headers +#include + +//forward declarations + + +namespace sp +{ +namespace mocks +{ + +//// +// SpEnoteStoreMockSimpleV1 +// - records are stored in the order they are added (useful for deterministic input selection for unit tests) +/// +class SpEnoteStoreMockSimpleV1 final +{ + friend class InputSelectorMockSimpleV1; + +public: + /// add a record + void add_record(const LegacyContextualEnoteRecordV1 &new_record); + void add_record(const SpContextualEnoteRecordV1 &new_record); + +//member variables +protected: + /// legacy enotes + std::list m_legacy_contextual_enote_records; + /// seraphis enotes + std::list m_sp_contextual_enote_records; +}; + +} //namespace mocks +} //namespace sp diff --git a/src/seraphis_mocks/jamtis_mock_keys.cpp b/src/seraphis_mocks/jamtis_mock_keys.cpp new file mode 100644 index 0000000000..12494336cb --- /dev/null +++ b/src/seraphis_mocks/jamtis_mock_keys.cpp @@ -0,0 +1,84 @@ +// Copyright (c) 2022, 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. + +// NOT FOR PRODUCTION + +//paired header +#include "jamtis_mock_keys.h" + +//local headers +#include "crypto/crypto.h" +#include "crypto/x25519.h" +#include "ringct/rctOps.h" +#include "seraphis_core/jamtis_core_utils.h" +#include "seraphis_core/sp_core_enote_utils.h" + +//third party headers + +//standard headers + + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis_mocks" + +namespace sp +{ +namespace jamtis +{ +namespace mocks +{ +//------------------------------------------------------------------------------------------------------------------- +void make_jamtis_mock_keys(jamtis_mock_keys &keys_out) +{ + keys_out.k_m = rct::rct2sk(rct::skGen()); + keys_out.k_vb = rct::rct2sk(rct::skGen()); + make_jamtis_unlockamounts_key(keys_out.k_vb, keys_out.xk_ua); + make_jamtis_findreceived_key(keys_out.k_vb, keys_out.xk_fr); + make_jamtis_generateaddress_secret(keys_out.k_vb, keys_out.s_ga); + make_jamtis_ciphertag_secret(keys_out.s_ga, keys_out.s_ct); + make_seraphis_spendkey(keys_out.k_vb, keys_out.k_m, keys_out.K_1_base); + make_jamtis_unlockamounts_pubkey(keys_out.xk_ua, keys_out.xK_ua); + make_jamtis_findreceived_pubkey(keys_out.xk_fr, keys_out.xK_ua, keys_out.xK_fr); +} +//------------------------------------------------------------------------------------------------------------------- +void make_random_address_for_user(const jamtis_mock_keys &user_keys, JamtisDestinationV1 &user_address_out) +{ + address_index_t address_index; + address_index = gen_address_index(); + + make_jamtis_destination_v1(user_keys.K_1_base, + user_keys.xK_ua, + user_keys.xK_fr, + user_keys.s_ga, + address_index, + user_address_out); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace mocks +} //namespace jamtis +} //namespace sp diff --git a/src/seraphis_mocks/jamtis_mock_keys.h b/src/seraphis_mocks/jamtis_mock_keys.h new file mode 100644 index 0000000000..593c26ba1d --- /dev/null +++ b/src/seraphis_mocks/jamtis_mock_keys.h @@ -0,0 +1,83 @@ +// Copyright (c) 2022, 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. + +// NOT FOR PRODUCTION + +//// +// Mock jamtis keys +// +// reference: https://gist.github.com/tevador/50160d160d24cfc6c52ae02eb3d17024 +/// + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "crypto/x25519.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/jamtis_destination.h" + +//third party headers + +//standard headers +#include + +//forward declarations + + +namespace sp +{ +namespace jamtis +{ +namespace mocks +{ + +//// +// A set of jamtis keys for mock-ups/unit testing +/// +struct jamtis_mock_keys +{ + crypto::secret_key k_m; //master + crypto::secret_key k_vb; //view-balance + crypto::x25519_secret_key xk_ua; //unlock-amounts + crypto::x25519_secret_key xk_fr; //find-received + crypto::secret_key s_ga; //generate-address + crypto::secret_key s_ct; //cipher-tag + rct::key K_1_base; //jamtis spend base = k_vb X + k_m U + crypto::x25519_pubkey xK_ua; //unlock-amounts pubkey = xk_ua xG + crypto::x25519_pubkey xK_fr; //find-received pubkey = xk_fr xk_ua xG +}; + +/// make a set of mock jamtis keys (for mock-ups/unit testing) +void make_jamtis_mock_keys(jamtis_mock_keys &keys_out); +/// make a random jamtis address for the given privkeys +void make_random_address_for_user(const jamtis_mock_keys &user_keys, JamtisDestinationV1 &user_address_out); + +} //namespace mocks +} //namespace jamtis +} //namespace sp diff --git a/src/seraphis_mocks/legacy_mock_keys.cpp b/src/seraphis_mocks/legacy_mock_keys.cpp new file mode 100644 index 0000000000..fd88763015 --- /dev/null +++ b/src/seraphis_mocks/legacy_mock_keys.cpp @@ -0,0 +1,84 @@ +// Copyright (c) 2022, 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. + +// NOT FOR PRODUCTION + +//paired header +#include "legacy_mock_keys.h" + +//local headers +#include "crypto/crypto.h" +#include "cryptonote_basic/subaddress_index.h" +#include "ringct/rctOps.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/legacy_core_utils.h" + +//third party headers + +//standard headers + + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis_mocks" + +namespace sp +{ +namespace mocks +{ +//------------------------------------------------------------------------------------------------------------------- +void make_legacy_mock_keys(legacy_mock_keys &keys_out) +{ + keys_out.k_s = rct::rct2sk(rct::skGen()); + keys_out.k_v = rct::rct2sk(rct::skGen()); + keys_out.Ks = rct::scalarmultBase(rct::sk2rct(keys_out.k_s)); + keys_out.Kv = rct::scalarmultBase(rct::sk2rct(keys_out.k_v)); +} +//------------------------------------------------------------------------------------------------------------------- +void gen_legacy_subaddress(const rct::key &legacy_base_spend_pubkey, + const crypto::secret_key &legacy_view_privkey, + rct::key &subaddr_spendkey_out, + rct::key &subaddr_viewkey_out, + cryptonote::subaddress_index &subaddr_index_out) +{ + // random subaddress index: i + crypto::rand(sizeof(subaddr_index_out.minor), reinterpret_cast(&subaddr_index_out.minor)); + crypto::rand(sizeof(subaddr_index_out.major), reinterpret_cast(&subaddr_index_out.major)); + + // subaddress spendkey: (Hn(k^v, i) + k^s) G + make_legacy_subaddress_spendkey(legacy_base_spend_pubkey, + legacy_view_privkey, + subaddr_index_out, + hw::get_device("default"), + subaddr_spendkey_out); + + // subaddress viewkey: k^v * K^{s,i} + rct::scalarmultKey(subaddr_viewkey_out, subaddr_spendkey_out, rct::sk2rct(legacy_view_privkey)); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace mocks +} //namespace sp diff --git a/src/seraphis_mocks/legacy_mock_keys.h b/src/seraphis_mocks/legacy_mock_keys.h new file mode 100644 index 0000000000..d6a2288bf7 --- /dev/null +++ b/src/seraphis_mocks/legacy_mock_keys.h @@ -0,0 +1,73 @@ +// Copyright (c) 2022, 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. + +// NOT FOR PRODUCTION + +// Legacy mock keys. + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "cryptonote_basic/subaddress_index.h" +#include "ringct/rctTypes.h" + +//third party headers + +//standard headers + +//forward declarations + + +namespace sp +{ +namespace mocks +{ + +//// +// A set of legacy keys for mock-ups/unit testing +/// +struct legacy_mock_keys final +{ + crypto::secret_key k_s; //spend privkey + crypto::secret_key k_v; //view privkey + rct::key Ks; //main spend pubkey: Ks = k_s G + rct::key Kv; //main view pubkey: Kv = k_v G +}; + +/// make a set of mock legacy keys (for mock-ups/unit testing) +void make_legacy_mock_keys(legacy_mock_keys &keys_out); +/// generate a legacy subaddress for the given keys +void gen_legacy_subaddress(const rct::key &legacy_base_spend_pubkey, + const crypto::secret_key &legacy_view_privkey, + rct::key &subaddr_spendkey_out, + rct::key &subaddr_viewkey_out, + cryptonote::subaddress_index &subaddr_index_out); + +} //namespace mocks +} //namespace sp diff --git a/src/seraphis_mocks/make_mock_tx.cpp b/src/seraphis_mocks/make_mock_tx.cpp new file mode 100644 index 0000000000..2d243b8623 --- /dev/null +++ b/src/seraphis_mocks/make_mock_tx.cpp @@ -0,0 +1,267 @@ +// Copyright (c) 2022, 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. + +// NOT FOR PRODUCTION + +//paired header +#include "make_mock_tx.h" + +//local headers +#include "common/container_helpers.h" +#include "crypto/crypto.h" +#include "mock_ledger_context.h" +#include "mock_tx_builders_inputs.h" +#include "mock_tx_builders_legacy_inputs.h" +#include "mock_tx_builders_outputs.h" +#include "ringct/rctOps.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/discretized_fee.h" +#include "seraphis_core/tx_extra.h" +#include "seraphis_main/tx_builder_types.h" +#include "seraphis_main/tx_builder_types_legacy.h" +#include "seraphis_main/tx_builders_inputs.h" +#include "seraphis_main/tx_builders_legacy_inputs.h" +#include "seraphis_main/tx_builders_mixed.h" +#include "seraphis_main/tx_builders_outputs.h" +#include "seraphis_main/txtype_coinbase_v1.h" +#include "seraphis_main/txtype_squashed_v1.h" + +//third party headers + +//standard headers +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis_mocks" + +namespace sp +{ +namespace mocks +{ +//------------------------------------------------------------------------------------------------------------------- +template <> +void make_mock_tx(const SpTxParamPackV1 ¶ms, + const std::vector &legacy_in_amounts, + const std::vector &sp_in_amounts, + const std::vector &out_amounts, + const DiscretizedFee discretized_transaction_fee, + MockLedgerContext &ledger_context_inout, + SpTxCoinbaseV1 &tx_out) +{ + CHECK_AND_ASSERT_THROW_MES(out_amounts.size() > 0, "SpTxCoinbaseV1: tried to make mock tx without any outputs."); + CHECK_AND_ASSERT_THROW_MES(discretized_transaction_fee == rct::xmr_amount{0}, + "SpTxCoinbaseV1: tried to make mock tx with nonzero fee."); + + // 1. mock semantics version + const SpTxCoinbaseV1::SemanticRulesVersion semantic_rules_version{SpTxCoinbaseV1::SemanticRulesVersion::MOCK}; + + // 2. set block reward from input amounts + rct::xmr_amount block_reward{0}; + + for (const rct::xmr_amount &input_amount : legacy_in_amounts) + block_reward += input_amount; + for (const rct::xmr_amount &input_amount : sp_in_amounts) + block_reward += input_amount; + + // 3. make mock outputs + std::vector output_proposals{ + gen_mock_sp_coinbase_output_proposals_v1(out_amounts, params.num_random_memo_elements) + }; + + // 4. expect amounts to balance + CHECK_AND_ASSERT_THROW_MES(balance_check_in_out_amnts_v1(block_reward, output_proposals), + "SpTxCoinbaseV1: tried to make mock tx with unbalanced amounts."); + + // 5. make partial memo + std::vector additional_memo_elements; + additional_memo_elements.resize(params.num_random_memo_elements); + + for (ExtraFieldElement &element : additional_memo_elements) + element= gen_extra_field_element(); + + TxExtra partial_memo; + make_tx_extra(std::move(additional_memo_elements), partial_memo); + + // 6. extract info from output proposals + std::vector output_enotes; + SpTxSupplementV1 tx_supplement; + make_v1_coinbase_outputs_v1(output_proposals, output_enotes, tx_supplement.output_enote_ephemeral_pubkeys); + + // 7. collect full memo + finalize_tx_extra_v1(partial_memo, output_proposals, tx_supplement.tx_extra); + + // 8. finish tx + make_seraphis_tx_coinbase_v1(semantic_rules_version, + ledger_context_inout.chain_height() + 1, //next block + block_reward, + std::move(output_enotes), + std::move(tx_supplement), + tx_out); +} +//------------------------------------------------------------------------------------------------------------------- +template <> +void make_mock_tx(const SpTxParamPackV1 ¶ms, + const std::vector &legacy_in_amounts, + const std::vector &sp_in_amounts, + const std::vector &out_amounts, + const DiscretizedFee discretized_transaction_fee, + MockLedgerContext &ledger_context_inout, + SpTxSquashedV1 &tx_out) +{ + CHECK_AND_ASSERT_THROW_MES(legacy_in_amounts.size() + sp_in_amounts.size() > 0, + "SpTxSquashedV1: tried to make mock tx without any inputs."); + CHECK_AND_ASSERT_THROW_MES(out_amounts.size() > 0, "SpTxSquashedV1: tried to make mock tx without any outputs."); + + // 1. mock semantics version + const SpTxSquashedV1::SemanticRulesVersion semantic_rules_version{SpTxSquashedV1::SemanticRulesVersion::MOCK}; + + // 2. make legacy spend privkey + const crypto::secret_key legacy_spend_privkey{rct::rct2sk(rct::skGen())}; + + // 3. make seraphis core privkeys (spend and view key) + const crypto::secret_key sp_spend_privkey{rct::rct2sk(rct::skGen())}; + const crypto::secret_key k_view_balance{rct::rct2sk(rct::skGen())}; + + // 4. make mock legacy inputs + std::vector legacy_input_proposals{ + gen_mock_legacy_input_proposals_v1(legacy_spend_privkey, legacy_in_amounts) + }; + std::sort(legacy_input_proposals.begin(), + legacy_input_proposals.end(), + tools::compare_func(compare_KI)); + + // 5. make mock seraphis inputs + std::vector sp_input_proposals{ + gen_mock_sp_input_proposals_v1(sp_spend_privkey, k_view_balance, sp_in_amounts) + }; + std::sort(sp_input_proposals.begin(), sp_input_proposals.end(), tools::compare_func(compare_KI)); + + // 6. make mock outputs + std::vector output_proposals{ + gen_mock_sp_output_proposals_v1(out_amounts, params.num_random_memo_elements) + }; + + // 7. for 2-out txs, the enote ephemeral pubkey is shared by both outputs + if (output_proposals.size() == 2) + output_proposals[1].enote_ephemeral_pubkey = output_proposals[0].enote_ephemeral_pubkey; + + // 8. expect amounts to balance + CHECK_AND_ASSERT_THROW_MES(balance_check_in_out_amnts_v2(legacy_input_proposals, + sp_input_proposals, + output_proposals, + discretized_transaction_fee), + "SpTxSquashedV1: tried to make mock tx with unbalanced amounts."); + + // 9. make partial memo + std::vector additional_memo_elements; + additional_memo_elements.resize(params.num_random_memo_elements); + + for (ExtraFieldElement &element : additional_memo_elements) + element = gen_extra_field_element(); + + TxExtra partial_memo; + make_tx_extra(std::move(additional_memo_elements), partial_memo); + + // 10. versioning for proofs + const tx_version_t tx_version{tx_version_from(semantic_rules_version)}; + + // 11. proposal prefix + rct::key tx_proposal_prefix; + make_tx_proposal_prefix_v1(tx_version, + legacy_input_proposals, + sp_input_proposals, + output_proposals, + discretized_transaction_fee, + partial_memo, + tx_proposal_prefix); + + // 12. make legacy ring signature preps + std::vector legacy_ring_signature_preps{ + gen_mock_legacy_ring_signature_preps_v1(tx_proposal_prefix, + legacy_input_proposals, + params.legacy_ring_size, + ledger_context_inout) + }; + std::sort(legacy_ring_signature_preps.begin(), + legacy_ring_signature_preps.end(), + tools::compare_func(compare_KI)); + + // 13. make legacy inputs + std::vector legacy_inputs; + + make_v1_legacy_inputs_v1(tx_proposal_prefix, + legacy_input_proposals, + std::move(legacy_ring_signature_preps), + legacy_spend_privkey, + hw::get_device("default"), + legacy_inputs); + std::sort(legacy_inputs.begin(), legacy_inputs.end(), tools::compare_func(compare_KI)); + + // 14. make seraphis partial inputs + std::vector sp_partial_inputs; + + make_v1_partial_inputs_v1(sp_input_proposals, + tx_proposal_prefix, + sp_spend_privkey, + k_view_balance, + sp_partial_inputs); + std::sort(sp_partial_inputs.begin(), sp_partial_inputs.end(), tools::compare_func(compare_KI)); + + // 15. prepare partial tx + SpPartialTxV1 partial_tx; + + make_v1_partial_tx_v1(std::move(legacy_inputs), + std::move(sp_partial_inputs), + std::move(output_proposals), + discretized_transaction_fee, + partial_memo, + tx_version, + partial_tx); + + // 16. make mock seraphis membership proof ref sets + std::vector sp_membership_proof_preps{ + gen_mock_sp_membership_proof_preps_v1(sp_input_proposals, + params.ref_set_decomp_n, + params.ref_set_decomp_m, + params.bin_config, + ledger_context_inout) + }; + + // 17. seraphis membership proofs + std::vector sp_alignable_membership_proofs; + make_v1_alignable_membership_proofs_v1(std::move(sp_membership_proof_preps), sp_alignable_membership_proofs); + + // 18. make tx + make_seraphis_tx_squashed_v1(semantic_rules_version, + std::move(partial_tx), + std::move(sp_alignable_membership_proofs), + tx_out); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace mocks +} //namespace sp diff --git a/src/seraphis_mocks/make_mock_tx.h b/src/seraphis_mocks/make_mock_tx.h new file mode 100644 index 0000000000..6010f2808f --- /dev/null +++ b/src/seraphis_mocks/make_mock_tx.h @@ -0,0 +1,114 @@ +// Copyright (c) 2022, 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. + +// NOT FOR PRODUCTION + +// Mock seraphis transaction builders. + +#pragma once + +//local headers +#include "seraphis_core/binned_reference_set.h" + +//third party headers + +//standard headers +#include + +//forward declarations +namespace rct { using xmr_amount = uint64_t; } +namespace sp +{ + struct DiscretizedFee; + struct SpTxCoinbaseV1; + struct SpTxSquashedV1; + class TxValidationContext; +namespace mocks +{ + class MockLedgerContext; +} +} + +namespace sp +{ +namespace mocks +{ + +/** +* brief: make_mock_tx - make a mock transaction (template) +* type: SpTxType - +* type: SpTxParamsT - +* param: params - +* param: legacy_in_amounts - +* param: sp_in_amounts - +* param: out_amounts - +* param: discretized_transaction_fee - +* inoutparam: ledger_context_inout - +* outparam: tx_out - +*/ +template +void make_mock_tx(const SpTxParamsT ¶ms, + const std::vector &legacy_in_amounts, + const std::vector &sp_in_amounts, + const std::vector &out_amounts, + const DiscretizedFee discretized_transaction_fee, + MockLedgerContext &ledger_context_inout, + SpTxType &tx_out); + +//// +/// SpTxParamPackV1 - parameter pack (for unit tests/mockups/etc.) +/// +struct SpTxParamPackV1 +{ + std::size_t legacy_ring_size{0}; + std::size_t ref_set_decomp_n{0}; + std::size_t ref_set_decomp_m{0}; + std::size_t num_random_memo_elements{0}; + SpBinnedReferenceSetConfigV1 bin_config{0, 0}; +}; +/// make an SpTxCoinbaseV1 transaction +template <> +void make_mock_tx(const SpTxParamPackV1 ¶ms, + const std::vector &legacy_in_amounts, + const std::vector &sp_in_amounts, + const std::vector &out_amounts, + const DiscretizedFee discretized_transaction_fee, + MockLedgerContext &ledger_context_inout, + SpTxCoinbaseV1 &tx_out); +/// make an SpTxSquashedV1 transaction +template <> +void make_mock_tx(const SpTxParamPackV1 ¶ms, + const std::vector &legacy_in_amounts, + const std::vector &sp_in_amounts, + const std::vector &out_amounts, + const DiscretizedFee discretized_transaction_fee, + MockLedgerContext &ledger_context_inout, + SpTxSquashedV1 &tx_out); + +} //namespace mocks +} //namespace sp diff --git a/src/seraphis_mocks/mock_ledger_context.cpp b/src/seraphis_mocks/mock_ledger_context.cpp new file mode 100644 index 0000000000..2c705efb7c --- /dev/null +++ b/src/seraphis_mocks/mock_ledger_context.cpp @@ -0,0 +1,1046 @@ +// Copyright (c) 2022, 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. + +// NOT FOR PRODUCTION + +//paired header +#include "mock_ledger_context.h" + +//local headers +#include "crypto/crypto.h" +#include "crypto/x25519.h" +#include "cryptonote_basic/subaddress_index.h" +#include "device/device.hpp" +#include "enote_finding_context_mocks.h" +#include "misc_log_ex.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/jamtis_enote_utils.h" +#include "seraphis_core/legacy_enote_types.h" +#include "seraphis_core/sp_core_enote_utils.h" +#include "seraphis_crypto/sp_crypto_utils.h" +#include "seraphis_main/scan_balance_recovery_utils.h" +#include "seraphis_main/scan_core_types.h" +#include "seraphis_main/tx_component_types.h" +#include "seraphis_main/txtype_coinbase_v1.h" +#include "seraphis_main/txtype_squashed_v1.h" + +//third party headers +#include +#include + +//standard headers +#include +#include +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis_mocks" + +namespace sp +{ +namespace mocks +{ +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +template +static void erase_ledger_cache_map_from_index(const std::uint64_t pop_index, MapT &map_inout) +{ + static_assert(std::is_same::value, "erase ledger map key is not uint64_t"); + + if (map_inout.size() == 0) + return; + + // erase entire map if pop index is below first known index, otherwise erase from pop index directly + const std::uint64_t index_to_erase_from = + map_inout.begin()->first >= pop_index + ? map_inout.begin()->first + : pop_index; + + map_inout.erase(map_inout.find(index_to_erase_from), map_inout.end()); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +MockLedgerContext::MockLedgerContext(const std::uint64_t first_seraphis_allowed_block, + const std::uint64_t first_seraphis_only_block) : + m_first_seraphis_allowed_block{first_seraphis_allowed_block}, + m_first_seraphis_only_block{first_seraphis_only_block} +{ + CHECK_AND_ASSERT_THROW_MES(m_first_seraphis_allowed_block <= m_first_seraphis_only_block, + "mock ledger context (constructor): invalid seraphis tx era range."); +} +//------------------------------------------------------------------------------------------------------------------- +std::uint64_t MockLedgerContext::top_block_index() const +{ + return m_block_infos.size() - 1; +} +//------------------------------------------------------------------------------------------------------------------- +std::uint64_t MockLedgerContext::chain_height() const +{ + return this->top_block_index() + 1; +} +//------------------------------------------------------------------------------------------------------------------- +bool MockLedgerContext::cryptonote_key_image_exists_unconfirmed(const crypto::key_image &key_image) const +{ + return m_unconfirmed_legacy_key_images.find(key_image) != m_unconfirmed_legacy_key_images.end(); +} +//------------------------------------------------------------------------------------------------------------------- +bool MockLedgerContext::seraphis_key_image_exists_unconfirmed(const crypto::key_image &key_image) const +{ + return m_unconfirmed_sp_key_images.find(key_image) != m_unconfirmed_sp_key_images.end(); +} +//------------------------------------------------------------------------------------------------------------------- +bool MockLedgerContext::cryptonote_key_image_exists_onchain(const crypto::key_image &key_image) const +{ + return m_legacy_key_images.find(key_image) != m_legacy_key_images.end(); +} +//------------------------------------------------------------------------------------------------------------------- +bool MockLedgerContext::seraphis_key_image_exists_onchain(const crypto::key_image &key_image) const +{ + return m_sp_key_images.find(key_image) != m_sp_key_images.end(); +} +//------------------------------------------------------------------------------------------------------------------- +void MockLedgerContext::get_reference_set_proof_elements_v1(const std::vector &indices, + rct::ctkeyV &proof_elements_out) const +{ + // get legacy enotes: {KI, C} + proof_elements_out.clear(); + proof_elements_out.reserve(indices.size()); + + for (const std::uint64_t index : indices) + { + CHECK_AND_ASSERT_THROW_MES(index < m_legacy_enote_references.size(), + "Tried to get legacy enote that doesn't exist."); + proof_elements_out.emplace_back(m_legacy_enote_references.at(index)); + } +} +//------------------------------------------------------------------------------------------------------------------- +void MockLedgerContext::get_reference_set_proof_elements_v2(const std::vector &indices, + rct::keyV &proof_elements_out) const +{ + // get squashed enotes + proof_elements_out.clear(); + proof_elements_out.reserve(indices.size()); + + for (const std::uint64_t index : indices) + { + CHECK_AND_ASSERT_THROW_MES(index < m_sp_squashed_enotes.size(), "Tried to get squashed enote that doesn't exist."); + proof_elements_out.emplace_back(m_sp_squashed_enotes.at(index)); + } +} +//------------------------------------------------------------------------------------------------------------------- +std::uint64_t MockLedgerContext::max_legacy_enote_index() const +{ + return m_legacy_enote_references.size() - 1; +} +//------------------------------------------------------------------------------------------------------------------- +std::uint64_t MockLedgerContext::max_sp_enote_index() const +{ + return m_sp_squashed_enotes.size() - 1; +} +//------------------------------------------------------------------------------------------------------------------- +void MockLedgerContext::remove_tx_from_unconfirmed_cache(const rct::key &tx_id) +{ + // clear key images + if (m_unconfirmed_tx_key_images.find(tx_id) != m_unconfirmed_tx_key_images.end()) + { + for (const crypto::key_image &key_image : std::get<0>(m_unconfirmed_tx_key_images[tx_id])) + m_unconfirmed_legacy_key_images.erase(key_image); + for (const crypto::key_image &key_image : std::get<1>(m_unconfirmed_tx_key_images[tx_id])) + m_unconfirmed_sp_key_images.erase(key_image); + + m_unconfirmed_tx_key_images.erase(tx_id); + } + + // clear output contents + m_unconfirmed_tx_output_contents.erase(tx_id); +} +//------------------------------------------------------------------------------------------------------------------- +void MockLedgerContext::clear_unconfirmed_cache() +{ + m_unconfirmed_legacy_key_images.clear(); + m_unconfirmed_sp_key_images.clear(); + m_unconfirmed_tx_key_images.clear(); + m_unconfirmed_tx_output_contents.clear(); +} +//------------------------------------------------------------------------------------------------------------------- +std::uint64_t MockLedgerContext::add_legacy_coinbase(const rct::key &tx_id, + const std::uint64_t unlock_time, + TxExtra memo, + std::vector legacy_key_images_for_block, + std::vector output_enotes) +{ + /// checks + + // a. can only add blocks with a mock legacy coinbase tx prior to first seraphis-enabled block + CHECK_AND_ASSERT_THROW_MES(this->top_block_index() + 1 < m_first_seraphis_only_block, + "mock tx ledger (adding legacy coinbase tx): chain index is above last block that can have a legacy coinbase " + "tx."); + + // b. accumulated output count is consistent + const std::uint64_t accumulated_output_count = + m_accumulated_legacy_output_counts.size() + ? (m_accumulated_legacy_output_counts.rbegin())->second //last block's accumulated legacy output count + : 0; + + CHECK_AND_ASSERT_THROW_MES(accumulated_output_count == m_legacy_enote_references.size(), + "mock tx ledger (adding legacy coinbase tx): inconsistent number of accumulated outputs (bug)."); + + + /// update state + const std::uint64_t new_index{this->top_block_index() + 1}; + + // 1. add legacy key images (mockup: force key images into chain as part of coinbase tx) + for (const crypto::key_image &legacy_key_image : legacy_key_images_for_block) + m_legacy_key_images.insert(legacy_key_image); + + m_blocks_of_tx_key_images[new_index][tx_id] = {std::move(legacy_key_images_for_block), {}}; + + // 2. add tx outputs + + // a. initialize with current total legacy output count + std::uint64_t total_legacy_output_count{m_legacy_enote_references.size()}; + + // b. insert all legacy enotes to the reference set + for (const LegacyEnoteVariant &enote : output_enotes) + { + m_legacy_enote_references[total_legacy_output_count] = {onetime_address_ref(enote), amount_commitment_ref(enote)}; + + ++total_legacy_output_count; + } + + // c. add this block's accumulated output count + m_accumulated_legacy_output_counts[new_index] = total_legacy_output_count; + + if (new_index >= m_first_seraphis_allowed_block) + m_accumulated_sp_output_counts[new_index] = m_sp_squashed_enotes.size(); + + // d. add this block's tx output contents + m_blocks_of_legacy_tx_output_contents[new_index][tx_id] = {unlock_time, std::move(memo), std::move(output_enotes)}; + + if (new_index >= m_first_seraphis_allowed_block) + m_blocks_of_sp_tx_output_contents[new_index]; + + // 3. add block info (random block ID and zero timestamp in mockup) + m_block_infos[new_index] = {rct::pkGen(), 0}; + + // 4. clear unconfirmed cache + this->clear_unconfirmed_cache(); + + return new_index; +} +//------------------------------------------------------------------------------------------------------------------- +bool MockLedgerContext::try_add_unconfirmed_coinbase_v1(const rct::key &coinbase_tx_id, + const rct::key &input_context, + SpTxSupplementV1 tx_supplement, + std::vector output_enotes) +{ + /// check failure modes + + // 1. fail if tx id is duplicated (bug since coinbase block index check should prevent this) + CHECK_AND_ASSERT_THROW_MES(m_unconfirmed_tx_key_images.find(coinbase_tx_id) == m_unconfirmed_tx_key_images.end(), + "mock tx ledger (adding unconfirmed coinbase tx): tx id already exists in key image map (bug)."); + CHECK_AND_ASSERT_THROW_MES(m_unconfirmed_tx_output_contents.find(coinbase_tx_id) == + m_unconfirmed_tx_output_contents.end(), + "mock tx ledger (adding unconfirmed coinbase tx): tx id already exists in output contents map (bug)."); + + + /// update state + + // 1. add key images (there are none, but we want an entry in the map) + m_unconfirmed_tx_key_images[coinbase_tx_id]; + + // 2. add tx outputs + m_unconfirmed_tx_output_contents[coinbase_tx_id] = + { + input_context, + std::move(tx_supplement), + std::move(output_enotes) + }; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool MockLedgerContext::try_add_unconfirmed_tx_v1(const SpTxSquashedV1 &tx) +{ + /// check failure modes + + // 1. fail if new tx overlaps with cached key images: unconfirmed, onchain + std::vector legacy_key_images_collected; + std::vector sp_key_images_collected; + + for (const LegacyEnoteImageV2 &legacy_enote_image : tx.legacy_input_images) + { + if (this->cryptonote_key_image_exists_unconfirmed(legacy_enote_image.key_image) || + this->cryptonote_key_image_exists_onchain(legacy_enote_image.key_image)) + return false; + + legacy_key_images_collected.emplace_back(legacy_enote_image.key_image); + } + + for (const SpEnoteImageV1 &sp_enote_image : tx.sp_input_images) + { + if (this->seraphis_key_image_exists_unconfirmed(key_image_ref(sp_enote_image)) || + this->seraphis_key_image_exists_onchain(key_image_ref(sp_enote_image))) + return false; + + sp_key_images_collected.emplace_back(key_image_ref(sp_enote_image)); + } + + // 2. fail if tx id is duplicated (bug since key image check should prevent this) + rct::key tx_id; + get_sp_tx_squashed_v1_txid(tx, tx_id); + + CHECK_AND_ASSERT_THROW_MES(m_unconfirmed_tx_key_images.find(tx_id) == m_unconfirmed_tx_key_images.end(), + "mock tx ledger (adding unconfirmed tx): tx id already exists in key image map (bug)."); + CHECK_AND_ASSERT_THROW_MES(m_unconfirmed_tx_output_contents.find(tx_id) == m_unconfirmed_tx_output_contents.end(), + "mock tx ledger (adding unconfirmed tx): tx id already exists in output contents map (bug)."); + + // 3. prepare input context + rct::key input_context; + jamtis::make_jamtis_input_context_standard(legacy_key_images_collected, sp_key_images_collected, input_context); + + + /// update state + + // 1. add key images + for (const crypto::key_image &legacy_key_image : legacy_key_images_collected) + m_unconfirmed_legacy_key_images.insert(legacy_key_image); + + for (const crypto::key_image &sp_key_image : sp_key_images_collected) + m_unconfirmed_sp_key_images.insert(sp_key_image); + + m_unconfirmed_tx_key_images[tx_id] = {std::move(legacy_key_images_collected), std::move(sp_key_images_collected)}; + + // 2. add tx outputs + std::vector output_enote_variants; + output_enote_variants.reserve(tx.outputs.size()); + + for (const SpEnoteV1 &enote : tx.outputs) + output_enote_variants.emplace_back(enote); + + m_unconfirmed_tx_output_contents[tx_id] = {input_context, tx.tx_supplement, output_enote_variants}; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +std::uint64_t MockLedgerContext::commit_unconfirmed_txs_v1(const rct::key &coinbase_tx_id, + const rct::key &mock_coinbase_input_context, + SpTxSupplementV1 mock_coinbase_tx_supplement, + std::vector mock_coinbase_output_enotes) +{ + /// sanity checks: check unconfirmed key images and txids + for (const auto &tx_key_images : m_unconfirmed_tx_key_images) + { + // a. tx ids are present in both unconfirmed data maps + CHECK_AND_ASSERT_THROW_MES(m_unconfirmed_tx_output_contents.find(tx_key_images.first) != + m_unconfirmed_tx_output_contents.end(), + "mock tx ledger (committing unconfirmed txs): tx id not in all unconfirmed data maps (bug)."); + + // b. tx ids are not present onchain + for (const auto &block_tx_key_images : m_blocks_of_tx_key_images) + { + CHECK_AND_ASSERT_THROW_MES(block_tx_key_images.second.find(tx_key_images.first) == + block_tx_key_images.second.end(), + "mock tx ledger (committing unconfirmed txs): unconfirmed tx id found in ledger (bug)."); + } + + for (const auto &block_tx_outputs : m_blocks_of_sp_tx_output_contents) + { + CHECK_AND_ASSERT_THROW_MES(block_tx_outputs.second.find(tx_key_images.first) == block_tx_outputs.second.end(), + "mock tx ledger (committing unconfirmed txs): unconfirmed tx id found in ledger (bug)."); + } + + // c. legacy key images are not present onchain + for (const crypto::key_image &key_image : std::get<0>(tx_key_images.second)) + { + CHECK_AND_ASSERT_THROW_MES(!this->cryptonote_key_image_exists_onchain(key_image), + "mock tx ledger (committing unconfirmed txs): unconfirmed legacy tx key image exists in ledger (bug)."); + } + + // d. seraphis key images are not present onchain + for (const crypto::key_image &key_image : std::get<1>(tx_key_images.second)) + { + CHECK_AND_ASSERT_THROW_MES(!this->seraphis_key_image_exists_onchain(key_image), + "mock tx ledger (committing unconfirmed txs): unconfirmed seraphis tx key image exists in ledger (bug)."); + } + } + + // d. unconfirmed maps line up + CHECK_AND_ASSERT_THROW_MES(m_unconfirmed_tx_key_images.size() == m_unconfirmed_tx_output_contents.size(), + "mock tx ledger (committing unconfirmed txs): unconfirmed data maps mismatch (bug)."); + + // e. accumulated output count is consistent + const std::uint64_t accumulated_output_count = + m_accumulated_sp_output_counts.size() + ? (m_accumulated_sp_output_counts.rbegin())->second //last block's accumulated output count + : 0; + + CHECK_AND_ASSERT_THROW_MES(accumulated_output_count == m_sp_squashed_enotes.size(), + "mock tx ledger (committing unconfirmed txs): inconsistent number of accumulated outputs (bug)."); + + // f. can only add blocks with seraphis txs at first seraphis-enabled block + CHECK_AND_ASSERT_THROW_MES(this->top_block_index() + 1 >= m_first_seraphis_allowed_block, + "mock tx ledger (committing unconfirmed txs): cannot make seraphis block because block index is too low."); + + + /// add mock coinbase tx to unconfirmed cache + // note: this should not invalidate the result of any of the prior checks + CHECK_AND_ASSERT_THROW_MES(this->try_add_unconfirmed_coinbase_v1(coinbase_tx_id, + mock_coinbase_input_context, + std::move(mock_coinbase_tx_supplement), + std::move(mock_coinbase_output_enotes)), + "mock tx ledger (committing unconfirmed txs): unable to add mock coinbase tx to unconfirmed cache (bug)."); + + + /// update state + const std::uint64_t new_index{this->top_block_index() + 1}; + + // 1. add key images + m_legacy_key_images.insert(m_unconfirmed_legacy_key_images.begin(), m_unconfirmed_legacy_key_images.end()); + m_sp_key_images.insert(m_unconfirmed_sp_key_images.begin(), m_unconfirmed_sp_key_images.end()); + m_blocks_of_tx_key_images[new_index] = std::move(m_unconfirmed_tx_key_images); + + // 2. add tx outputs + + // a. initialize with current total output count + std::uint64_t total_sp_output_count{m_sp_squashed_enotes.size()}; + + // b. insert all squashed enotes to the reference set + for (const auto &tx_info : m_unconfirmed_tx_output_contents) + { + const auto &tx_enotes = std::get>(tx_info.second); + for (const SpEnoteVariant &enote : tx_enotes) + { + make_seraphis_squashed_enote_Q(onetime_address_ref(enote), + amount_commitment_ref(enote), + m_sp_squashed_enotes[total_sp_output_count]); + + ++total_sp_output_count; + } + } + + // c. add this block's accumulated output count + m_accumulated_sp_output_counts[new_index] = total_sp_output_count; + + if (new_index < m_first_seraphis_only_block) + m_accumulated_legacy_output_counts[new_index] = m_legacy_enote_references.size(); + + // d. steal the unconfirmed cache's tx output contents + m_blocks_of_sp_tx_output_contents[new_index] = std::move(m_unconfirmed_tx_output_contents); + + if (new_index < m_first_seraphis_only_block) + m_blocks_of_legacy_tx_output_contents[new_index]; + + // 3. add block info (random block ID and zero timestamp in mockup) + m_block_infos[new_index] = {rct::pkGen(), 0}; + + // 4. clear unconfirmed chache + this->clear_unconfirmed_cache(); + + return new_index; +} +//------------------------------------------------------------------------------------------------------------------- +std::uint64_t MockLedgerContext::commit_unconfirmed_txs_v1(const SpTxCoinbaseV1 &coinbase_tx) +{ + /// checks + CHECK_AND_ASSERT_THROW_MES(coinbase_tx.block_height == this->chain_height() + 1, + "mock tx ledger (committing a coinbase tx): coinbase tx's block height does not match chain height."); + + + /// commit a new block + + // 1. convert output enotes to type-erased enote variants + std::vector coinbase_output_enotes; + coinbase_output_enotes.reserve(coinbase_tx.outputs.size()); + + for (const SpCoinbaseEnoteV1 &coinbase_enote : coinbase_tx.outputs) + coinbase_output_enotes.emplace_back(coinbase_enote); + + // 2. compute coinbase input context + rct::key coinbase_input_context; + jamtis::make_jamtis_input_context_coinbase(coinbase_tx.block_height, coinbase_input_context); + + // 3. coinbase tx id + rct::key coinbase_tx_id; + get_sp_tx_coinbase_v1_txid(coinbase_tx, coinbase_tx_id); + + // 3. punt to mock commit function + return this->commit_unconfirmed_txs_v1(coinbase_tx_id, + coinbase_input_context, + coinbase_tx.tx_supplement, + std::move(coinbase_output_enotes)); +} +//------------------------------------------------------------------------------------------------------------------- +std::uint64_t MockLedgerContext::pop_chain_at_index(const std::uint64_t pop_index) +{ + if (pop_index + 1 > this->top_block_index() + 1 || + pop_index + 1 == 0) + return 0; + + const std::uint64_t num_blocks_to_pop{this->top_block_index() - pop_index + 1}; + + // 1. remove key images + for (std::uint64_t index_to_pop{pop_index}; index_to_pop < pop_index + num_blocks_to_pop; ++index_to_pop) + { + if (m_blocks_of_tx_key_images.find(index_to_pop) != m_blocks_of_tx_key_images.end()) + { + for (const auto &tx_key_images : m_blocks_of_tx_key_images[index_to_pop]) + { + for (const crypto::key_image &key_image : std::get<0>(tx_key_images.second)) + m_legacy_key_images.erase(key_image); + for (const crypto::key_image &key_image : std::get<1>(tx_key_images.second)) + m_sp_key_images.erase(key_image); + } + } + } + + // 2. remove legacy enote references + if (m_accumulated_legacy_output_counts.size() > 0) + { + // sanity check + if (pop_index > 0) + { + CHECK_AND_ASSERT_THROW_MES(m_accumulated_legacy_output_counts.find(pop_index - 1) != + m_accumulated_legacy_output_counts.end(), + "mock ledger context (popping chain): accumulated legacy output counts has a hole (bug)."); + } + + // remove all outputs starting in the pop_index block + const std::uint64_t first_output_to_remove = + pop_index > 0 + ? m_accumulated_legacy_output_counts[pop_index - 1] + : 0; + + m_legacy_enote_references.erase(m_legacy_enote_references.find(first_output_to_remove), + m_legacy_enote_references.end()); + } + + // 3. remove squashed enotes + if (m_accumulated_sp_output_counts.size() > 0) + { + // sanity check + if (pop_index > m_first_seraphis_allowed_block) + { + CHECK_AND_ASSERT_THROW_MES(m_accumulated_sp_output_counts.find(pop_index - 1) != + m_accumulated_sp_output_counts.end(), + "mock ledger context (popping chain): accumulated seraphis output counts has a hole (bug)."); + } + + // remove all outputs starting in the pop_index block + const std::uint64_t first_output_to_remove = + pop_index > m_first_seraphis_allowed_block + ? m_accumulated_sp_output_counts[pop_index - 1] + : 0; + + m_sp_squashed_enotes.erase(m_sp_squashed_enotes.find(first_output_to_remove), m_sp_squashed_enotes.end()); + } + + // 4. clean up block maps + erase_ledger_cache_map_from_index(pop_index, m_blocks_of_tx_key_images); + erase_ledger_cache_map_from_index(pop_index, m_accumulated_legacy_output_counts); + erase_ledger_cache_map_from_index(pop_index, m_accumulated_sp_output_counts); + erase_ledger_cache_map_from_index(pop_index, m_blocks_of_legacy_tx_output_contents); + erase_ledger_cache_map_from_index(pop_index, m_blocks_of_sp_tx_output_contents); + erase_ledger_cache_map_from_index(pop_index, m_block_infos); + + return num_blocks_to_pop; +} +//------------------------------------------------------------------------------------------------------------------- +std::uint64_t MockLedgerContext::pop_blocks(const std::size_t num_blocks) +{ + const std::uint64_t top_block_index{this->top_block_index()}; + return this->pop_chain_at_index(top_block_index + 1 >= num_blocks ? top_block_index + 1 - num_blocks : 0); +} +//------------------------------------------------------------------------------------------------------------------- +void MockLedgerContext::get_unconfirmed_chunk_sp(const crypto::x25519_secret_key &xk_find_received, + scanning::ChunkData &chunk_data_out) const +{ + chunk_data_out.basic_records_per_tx.clear(); + chunk_data_out.contextual_key_images.clear(); + + // no chunk if no txs to scan + if (m_unconfirmed_tx_output_contents.size() == 0) + return; + + // optimization: reserve capacity in the chunk records map + // - on average, one tx per sizeof(jamtis view tag) enotes will have a record in the chunk; we add 40% to account + // for typical variance plus uncertainty in the number of enotes + chunk_data_out.basic_records_per_tx.reserve( + m_unconfirmed_tx_output_contents.size() * 2 * 140 / 100 / sizeof(sp::jamtis::view_tag_t) + ); + + // find-received scan each tx in the unconfirmed chache + std::list collected_records; + SpContextualKeyImageSetV1 collected_key_images; + + for (const auto &tx_with_output_contents : m_unconfirmed_tx_output_contents) + { + const rct::key &tx_id{sortable2rct(tx_with_output_contents.first)}; + + // if this tx contains at least one view-tag match, then add the tx's key images to the chunk + if (scanning::try_find_sp_enotes_in_tx(xk_find_received, + -1, + -1, + tx_id, + 0, + std::get(tx_with_output_contents.second), + std::get(tx_with_output_contents.second), + std::get>(tx_with_output_contents.second), + SpEnoteOriginStatus::UNCONFIRMED, + collected_records)) + { + // splice juuust in case a tx id is duplicated as part of a mockup + chunk_data_out.basic_records_per_tx[tx_id] + .splice(chunk_data_out.basic_records_per_tx[tx_id].end(), collected_records); + + CHECK_AND_ASSERT_THROW_MES(m_unconfirmed_tx_key_images.find(tx_with_output_contents.first) != + m_unconfirmed_tx_key_images.end(), + "unconfirmed chunk find-received scanning (mock ledger context): key image map missing tx (bug)."); + + if (scanning::try_collect_key_images_from_tx(-1, + -1, + tx_id, + std::get<0>(m_unconfirmed_tx_key_images.at(tx_with_output_contents.first)), + std::get<1>(m_unconfirmed_tx_key_images.at(tx_with_output_contents.first)), + SpEnoteSpentStatus::SPENT_UNCONFIRMED, + collected_key_images)) + chunk_data_out.contextual_key_images.emplace_back(std::move(collected_key_images)); + } + } +} +//------------------------------------------------------------------------------------------------------------------- +void MockLedgerContext::get_onchain_chunk_legacy(const std::uint64_t chunk_start_index, + const std::uint64_t chunk_max_size, + const rct::key &legacy_base_spend_pubkey, + const std::unordered_map &legacy_subaddress_map, + const crypto::secret_key &legacy_view_privkey, + const LegacyScanMode legacy_scan_mode, + scanning::ChunkContext &chunk_context_out, + scanning::ChunkData &chunk_data_out) const +{ + chunk_data_out.basic_records_per_tx.clear(); + chunk_data_out.contextual_key_images.clear(); + chunk_context_out.block_ids.clear(); + + /// 1. failure cases + if (this->top_block_index() + 1 == 0 || + chunk_start_index >= m_first_seraphis_only_block || + chunk_start_index > this->top_block_index() || + chunk_max_size == 0) + { + // set empty chunk info: top of the legacy-enabled chain + chunk_context_out.start_index = std::min(m_first_seraphis_only_block, this->top_block_index() + 1); + + if (chunk_context_out.start_index > 0) + { + CHECK_AND_ASSERT_THROW_MES(m_block_infos.find(chunk_context_out.start_index - 1) != + m_block_infos.end(), + "onchain chunk legacy-view scanning (mock ledger context): block ids map incorrect indexing (bug)."); + + chunk_context_out.prefix_block_id = std::get(m_block_infos.at(chunk_context_out.start_index - 1)); + } + else + chunk_context_out.prefix_block_id = rct::zero(); + + return; + } + + + /// 2. set block information + // a. block range (cap on the lowest of: chain index, seraphis-only range begins, chunk size) + chunk_context_out.start_index = chunk_start_index; + const std::uint64_t chunk_end_index{ + std::min({ + this->top_block_index() + 1, + m_first_seraphis_only_block, + chunk_start_index + chunk_max_size + }) + }; + + CHECK_AND_ASSERT_THROW_MES(chunk_end_index > chunk_context_out.start_index, + "onchain chunk legacy-view scanning (mock ledger context): chunk has no blocks below failure tests (bug)."); + CHECK_AND_ASSERT_THROW_MES(m_block_infos.find(chunk_context_out.start_index) != m_block_infos.end() && + m_block_infos.find(chunk_end_index - 1) != m_block_infos.end(), + "onchain chunk legacy-view scanning (mock ledger context): block range outside of block ids map (bug)."); + + // b. prefix block id + chunk_context_out.prefix_block_id = + chunk_start_index > 0 + ? std::get(m_block_infos.at(chunk_start_index - 1)) + : rct::zero(); + + // c. block ids in the range + chunk_context_out.block_ids.reserve(chunk_end_index - chunk_context_out.start_index); + + std::for_each( + m_block_infos.find(chunk_context_out.start_index), + m_block_infos.find(chunk_end_index), + [&](const auto &mapped_block_info) + { + chunk_context_out.block_ids.emplace_back(std::get(mapped_block_info.second)); + } + ); + + CHECK_AND_ASSERT_THROW_MES(chunk_context_out.block_ids.size() == + chunk_end_index - chunk_context_out.start_index, + "onchain chunk legacy-view scanning (mock ledger context): invalid number of block ids acquired (bug)."); + + + /// 3. scan blocks in the chunk range that may contain legacy enotes or key images + // a. early return if chunk doesn't cover any legacy enabled blocks + // - we did this in failure tests above + + // b. get adjusted chunk end + // - we did this when defining the chunk end + + CHECK_AND_ASSERT_THROW_MES(m_blocks_of_legacy_tx_output_contents.find(chunk_context_out.start_index) != + m_blocks_of_legacy_tx_output_contents.end(), + "onchain chunk legacy-view scanning (mock ledger context): start of chunk not known in tx outputs map (bug)."); + CHECK_AND_ASSERT_THROW_MES(m_blocks_of_legacy_tx_output_contents.find(chunk_end_index - 1) != + m_blocks_of_legacy_tx_output_contents.end(), + "onchain chunk legacy-view scanning (mock ledger context): end of chunk not known in tx outputs map (bug)."); + CHECK_AND_ASSERT_THROW_MES(m_blocks_of_tx_key_images.find(chunk_context_out.start_index) != + m_blocks_of_tx_key_images.end(), + "onchain chunk legacy-view scanning (mock ledger context): start of chunk not known in key images map (bug)."); + CHECK_AND_ASSERT_THROW_MES(m_blocks_of_tx_key_images.find(chunk_end_index - 1) != + m_blocks_of_tx_key_images.end(), + "onchain chunk legacy-view scanning (mock ledger context): end of chunk not known in key images map (bug)."); + + // a. initialize output count to the total number of legacy enotes in the ledger before the first block to scan + std::uint64_t total_output_count_before_tx{0}; + + if (chunk_context_out.start_index > 0) + { + CHECK_AND_ASSERT_THROW_MES(m_accumulated_legacy_output_counts.find(chunk_context_out.start_index - 1) != + m_accumulated_legacy_output_counts.end(), + "onchain chunk legacy-view scanning (mock ledger context): output counts missing a block (bug)."); + + total_output_count_before_tx = m_accumulated_legacy_output_counts.at(chunk_context_out.start_index - 1); + } + + // b. optimization: reserve size in the chunk map + // - use output counts as a proxy for the number of txs in the chunk range + if (chunk_context_out.block_ids.size() > 0) + { + CHECK_AND_ASSERT_THROW_MES(m_accumulated_legacy_output_counts.find(chunk_end_index - 1) != + m_accumulated_legacy_output_counts.end(), + "onchain chunk legacy-view scanning (mock ledger context): output counts missing a block (bug)."); + + chunk_data_out.basic_records_per_tx.reserve( + (m_accumulated_legacy_output_counts.at(chunk_end_index - 1) - total_output_count_before_tx) / 2 + ); + } + + // c. legacy-view scan each block in the range + std::list collected_records; + SpContextualKeyImageSetV1 collected_key_images; + + std::for_each( + m_blocks_of_legacy_tx_output_contents.find(chunk_context_out.start_index), + m_blocks_of_legacy_tx_output_contents.find(chunk_end_index), + [&](const auto &block_of_tx_output_contents) + { + CHECK_AND_ASSERT_THROW_MES(m_block_infos.find(block_of_tx_output_contents.first) != m_block_infos.end(), + "onchain chunk legacy-view scanning (mock ledger context): block infos map missing index (bug)."); + + for (const auto &tx_with_output_contents : block_of_tx_output_contents.second) + { + const rct::key &tx_id{sortable2rct(tx_with_output_contents.first)}; + + // legacy view-scan the tx if in scan mode + if (legacy_scan_mode == LegacyScanMode::SCAN) + { + if (scanning::try_find_legacy_enotes_in_tx(legacy_base_spend_pubkey, + legacy_subaddress_map, + legacy_view_privkey, + block_of_tx_output_contents.first, + std::get(m_block_infos.at(block_of_tx_output_contents.first)), + tx_id, + total_output_count_before_tx, + std::get(tx_with_output_contents.second), + std::get(tx_with_output_contents.second), + std::get>(tx_with_output_contents.second), + SpEnoteOriginStatus::ONCHAIN, + hw::get_device("default"), + collected_records)) + { + // splice juuust in case a tx id is duplicated as part of a mockup + chunk_data_out.basic_records_per_tx[tx_id] + .splice(chunk_data_out.basic_records_per_tx[tx_id].end(), collected_records); + } + } + + // always add an entry for this tx in the basic records map (since we save key images for every tx) + chunk_data_out.basic_records_per_tx[sortable2rct(tx_with_output_contents.first)]; + + // collect key images from the tx (always do this for legacy txs) + // - optimization not implemented here: only key images of rings which include a received + // enote MUST be collected; filtering to get those key images is not possible here so we + // include all key images + CHECK_AND_ASSERT_THROW_MES( + m_blocks_of_tx_key_images + .at(block_of_tx_output_contents.first).find(tx_with_output_contents.first) != + m_blocks_of_tx_key_images + .at(block_of_tx_output_contents.first).end(), + "onchain chunk legacy-view scanning (mock ledger context): key image map missing tx (bug)."); + + if (scanning::try_collect_key_images_from_tx(block_of_tx_output_contents.first, + std::get(m_block_infos.at(block_of_tx_output_contents.first)), + sortable2rct(tx_with_output_contents.first), + std::get<0>(m_blocks_of_tx_key_images + .at(block_of_tx_output_contents.first) + .at(tx_with_output_contents.first)), + std::get<1>(m_blocks_of_tx_key_images + .at(block_of_tx_output_contents.first) + .at(tx_with_output_contents.first)), + SpEnoteSpentStatus::SPENT_ONCHAIN, + collected_key_images)) + chunk_data_out.contextual_key_images.emplace_back(std::move(collected_key_images)); + + // add this tx's number of outputs to the total output count + total_output_count_before_tx += + std::get>(tx_with_output_contents.second).size(); + } + } + ); + + for (const SpContextualKeyImageSetV1 &key_image_set : chunk_data_out.contextual_key_images) + { + CHECK_AND_ASSERT_THROW_MES(key_image_set.sp_key_images.size() == 0, + "onchain chunk legacy-view scanning (mock ledger context): a legacy tx has sp key images (bug)."); + } +} +//------------------------------------------------------------------------------------------------------------------- +void MockLedgerContext::get_onchain_chunk_sp(const std::uint64_t chunk_start_index, + const std::uint64_t chunk_max_size, + const crypto::x25519_secret_key &xk_find_received, + scanning::ChunkContext &chunk_context_out, + scanning::ChunkData &chunk_data_out) const +{ + chunk_data_out.basic_records_per_tx.clear(); + chunk_data_out.contextual_key_images.clear(); + chunk_context_out.block_ids.clear(); + + /// 1. failure cases + if (this->top_block_index() + 1 == 0 || + chunk_start_index > this->top_block_index() || + chunk_max_size == 0) + { + // set empty chunk info: top of the chain + chunk_context_out.start_index = this->top_block_index() + 1; + + if (chunk_context_out.start_index > 0) + { + CHECK_AND_ASSERT_THROW_MES(m_block_infos.find(chunk_context_out.start_index - 1) != + m_block_infos.end(), + "onchain chunk find-received scanning (mock ledger context): block ids map incorrect indexing (bug)."); + + chunk_context_out.prefix_block_id = std::get(m_block_infos.at(chunk_context_out.start_index - 1)); + } + else + chunk_context_out.prefix_block_id = rct::zero(); + + return; + } + + + /// 2. set block information + // a. block range + chunk_context_out.start_index = chunk_start_index; + const std::uint64_t chunk_end_index{ + std::min( + this->top_block_index() + 1, + chunk_start_index + chunk_max_size + ) + }; + + CHECK_AND_ASSERT_THROW_MES(chunk_end_index > chunk_context_out.start_index, + "onchain chunk find-received scanning (mock ledger context): chunk has no blocks below failure tests (bug)."); + CHECK_AND_ASSERT_THROW_MES(m_block_infos.find(chunk_context_out.start_index) != m_block_infos.end() && + m_block_infos.find(chunk_end_index - 1) != m_block_infos.end(), + "onchain chunk find-received scanning (mock ledger context): block range outside of block ids map (bug)."); + + // b. prefix block id + chunk_context_out.prefix_block_id = + chunk_start_index > 0 + ? std::get(m_block_infos.at(chunk_start_index - 1)) + : rct::zero(); + + // c. block ids in the range + chunk_context_out.block_ids.reserve(chunk_end_index - chunk_context_out.start_index); + + std::for_each( + m_block_infos.find(chunk_context_out.start_index), + m_block_infos.find(chunk_end_index), + [&](const auto &mapped_block_info) + { + chunk_context_out.block_ids.emplace_back(std::get(mapped_block_info.second)); + } + ); + + CHECK_AND_ASSERT_THROW_MES(chunk_context_out.block_ids.size() == + chunk_end_index - chunk_context_out.start_index, + "onchain chunk find-received scanning (mock ledger context): invalid number of block ids acquired (bug)."); + + + /// 3. scan blocks in the chunk range that may contain seraphis enotes or key images + // a. early return if chunk doesn't cover any seraphis enabled blocks + if (chunk_end_index <= m_first_seraphis_allowed_block) + return; + + // b. get adjusted chunk start + const std::uint64_t chunk_start_adjusted{ + std::max(chunk_context_out.start_index + 1, m_first_seraphis_allowed_block + 1) - 1 + }; + + CHECK_AND_ASSERT_THROW_MES(m_blocks_of_sp_tx_output_contents.find(chunk_start_adjusted) != + m_blocks_of_sp_tx_output_contents.end(), + "onchain chunk find-received scanning (mock ledger context): start of chunk not known in tx outputs map (bug)."); + CHECK_AND_ASSERT_THROW_MES(m_blocks_of_sp_tx_output_contents.find(chunk_end_index - 1) != + m_blocks_of_sp_tx_output_contents.end(), + "onchain chunk find-received scanning (mock ledger context): end of chunk not known in tx outputs map (bug)."); + CHECK_AND_ASSERT_THROW_MES(m_blocks_of_tx_key_images.find(chunk_start_adjusted) != + m_blocks_of_tx_key_images.end(), + "onchain chunk find-received scanning (mock ledger context): start of chunk not known in key images map (bug)."); + CHECK_AND_ASSERT_THROW_MES(m_blocks_of_tx_key_images.find(chunk_end_index - 1) != + m_blocks_of_tx_key_images.end(), + "onchain chunk find-received scanning (mock ledger context): end of chunk not known in key images map (bug)."); + + // c. initialize output count to the total number of seraphis enotes in the ledger before the first block to scan + std::uint64_t total_output_count_before_tx{0}; + + if (chunk_start_adjusted > m_first_seraphis_allowed_block) + { + CHECK_AND_ASSERT_THROW_MES(m_accumulated_sp_output_counts.find(chunk_start_adjusted - 1) != + m_accumulated_sp_output_counts.end(), + "onchain chunk find-received scanning (mock ledger context): output counts missing a block (bug)."); + + total_output_count_before_tx = m_accumulated_sp_output_counts.at(chunk_start_adjusted - 1); + } + + // d. optimization: reserve size in the chunk map + // - on average, one tx per sizeof(jamtis view tag) enotes will have a record in the chunk; we add 20% to account + // for typical variance + if (chunk_context_out.block_ids.size() > 0) + { + CHECK_AND_ASSERT_THROW_MES(m_accumulated_sp_output_counts.find(chunk_end_index - 1) != + m_accumulated_sp_output_counts.end(), + "onchain chunk find-received scanning (mock ledger context): output counts missing a block (bug)."); + + chunk_data_out.basic_records_per_tx.reserve( + ((m_accumulated_sp_output_counts.at(chunk_end_index - 1) - total_output_count_before_tx) * 120 / 100 / + sizeof(sp::jamtis::view_tag_t)) + ); + } + + // e. find-received scan each block in the range + std::list collected_records; + SpContextualKeyImageSetV1 collected_key_images; + + std::for_each( + m_blocks_of_sp_tx_output_contents.find(chunk_start_adjusted), + m_blocks_of_sp_tx_output_contents.find(chunk_end_index), + [&](const auto &block_of_tx_output_contents) + { + CHECK_AND_ASSERT_THROW_MES(m_block_infos.find(block_of_tx_output_contents.first) != m_block_infos.end(), + "onchain chunk find-received scanning (mock ledger context): block infos map missing index (bug)."); + + for (const auto &tx_with_output_contents : block_of_tx_output_contents.second) + { + const rct::key &tx_id{sortable2rct(tx_with_output_contents.first)}; + + // if this tx contains at least one view-tag match, then add the tx's key images to the chunk + if (scanning::try_find_sp_enotes_in_tx(xk_find_received, + block_of_tx_output_contents.first, + std::get(m_block_infos.at(block_of_tx_output_contents.first)), + tx_id, + total_output_count_before_tx, + std::get(tx_with_output_contents.second), + std::get(tx_with_output_contents.second), + std::get>(tx_with_output_contents.second), + SpEnoteOriginStatus::ONCHAIN, + collected_records)) + { + // splice juuust in case a tx id is duplicated as part of a mockup + chunk_data_out.basic_records_per_tx[tx_id] + .splice(chunk_data_out.basic_records_per_tx[tx_id].end(), collected_records); + + CHECK_AND_ASSERT_THROW_MES( + m_blocks_of_tx_key_images + .at(block_of_tx_output_contents.first).find(tx_with_output_contents.first) != + m_blocks_of_tx_key_images + .at(block_of_tx_output_contents.first).end(), + "onchain chunk find-received scanning (mock ledger context): key image map missing tx " + "(bug)."); + + if (scanning::try_collect_key_images_from_tx(block_of_tx_output_contents.first, + std::get(m_block_infos.at(block_of_tx_output_contents.first)), + tx_id, + std::get<0>(m_blocks_of_tx_key_images + .at(block_of_tx_output_contents.first) + .at(tx_with_output_contents.first)), + std::get<1>(m_blocks_of_tx_key_images + .at(block_of_tx_output_contents.first) + .at(tx_with_output_contents.first)), + SpEnoteSpentStatus::SPENT_ONCHAIN, + collected_key_images)) + chunk_data_out.contextual_key_images.emplace_back(std::move(collected_key_images)); + } + + // add this tx's number of outputs to the total output count + total_output_count_before_tx += + std::get>(tx_with_output_contents.second).size(); + } + } + ); +} +//------------------------------------------------------------------------------------------------------------------- + +//------------------------------------------------------------------------------------------------------------------- +// free functions +//------------------------------------------------------------------------------------------------------------------- +bool try_add_tx_to_ledger(const SpTxCoinbaseV1 &tx_to_add, MockLedgerContext &ledger_context_inout) +{ + return false; +} +//------------------------------------------------------------------------------------------------------------------- +bool try_add_tx_to_ledger(const SpTxSquashedV1 &tx_to_add, MockLedgerContext &ledger_context_inout) +{ + if (!ledger_context_inout.try_add_unconfirmed_tx_v1(tx_to_add)) + return false; + + ledger_context_inout.commit_unconfirmed_txs_v1(rct::pkGen(), + rct::pkGen(), + SpTxSupplementV1{}, + std::vector{}); + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace mocks +} //namespace sp diff --git a/src/seraphis_mocks/mock_ledger_context.h b/src/seraphis_mocks/mock_ledger_context.h new file mode 100644 index 0000000000..d579194cb4 --- /dev/null +++ b/src/seraphis_mocks/mock_ledger_context.h @@ -0,0 +1,360 @@ +// Copyright (c) 2022, 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. + +// NOT FOR PRODUCTION + +// Mock ledger context. +// WARNING: txs added to the mock ledger aren't auto-validated (aside from key image checks) +// WARNING: reference set proof element getters do NOT check if the elements are spendable (i.e. if they are unlocked) +// WARNING: this object is not inherently thread-safe; use a read/write lock to manage its lifetime if needed + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "crypto/x25519.h" +#include "cryptonote_basic/subaddress_index.h" +#include "ringct/rctOps.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/legacy_enote_types.h" +#include "seraphis_crypto/sp_crypto_utils.h" +#include "seraphis_main/scan_core_types.h" +#include "seraphis_main/tx_component_types.h" + +//third party headers + +//standard headers +#include +#include +#include +#include + +//forward declarations +namespace sp +{ +namespace mocks +{ + enum class LegacyScanMode : unsigned char; +} + struct SpTxCoinbaseV1; + struct SpTxSquashedV1; +} + +namespace sp +{ +namespace mocks +{ + +class MockLedgerContext final +{ +public: +//constructors + /// define tx era ranges (legacy: [0, first seraphis only); seraphis: [first seraphis allowed,) ) + /// NOTE: blocks with mock legacy coinbase txs are allowed before the first seraphis-only block, but in practice + // legacy coinbases should stop appearing at the first seraphis-allowed block + MockLedgerContext(const std::uint64_t first_seraphis_allowed_block, const std::uint64_t first_seraphis_only_block); + +//member functions + /** + * brief: top_block_index - get index of the chain's top block + * - returns uint64{-1} if there are no blocks + * return: top block index (num blocks - 1) + */ + std::uint64_t top_block_index() const; + /** + * brief: chain_height - get size of the chain + * - returns 0 if there are no blocks + * return: current chain height + */ + std::uint64_t chain_height() const; + /** + * brief: *key_image_exists* - checks if a key image exists in the cache + * param: key_image - + * return: true/false on check result + */ + bool cryptonote_key_image_exists_unconfirmed(const crypto::key_image &key_image) const; + bool seraphis_key_image_exists_unconfirmed(const crypto::key_image &key_image) const; + bool cryptonote_key_image_exists_onchain(const crypto::key_image &key_image) const; + bool seraphis_key_image_exists_onchain(const crypto::key_image &key_image) const; + /** + * brief: get_reference_set_proof_elements_v1 - get legacy enotes stored in the ledger (for a membership proof) + * param: indices - + * outparam: proof_elements_out - {KI, C} + */ + void get_reference_set_proof_elements_v1(const std::vector &indices, + rct::ctkeyV &proof_elements_out) const; + /** + * brief: get_reference_set_proof_elements_v2 - get seraphis squashed enotes stored in the ledger + * param: indices - + * outparam: proof_elements_out - {squashed enote} + */ + void get_reference_set_proof_elements_v2(const std::vector &indices, + rct::keyV &proof_elements_out) const; + /** + * brief: max_legacy_enote_index - highest index of a legacy enote in the ledger + * return: highest legacy enote index (defaults to std::uint64_t::max if no enotes) + */ + std::uint64_t max_legacy_enote_index() const; + /** + * brief: max_sp_enote_index - highest index of a seraphis enote in the ledger + * return: highest seraphis enote index (defaults to std::uint64_t::max if no enotes) + */ + std::uint64_t max_sp_enote_index() const; + /** + * brief: num_legacy_enotes - number of legacy enotes in the ledger + * return: number of legacy enotes in the ledger + */ + std::uint64_t num_legacy_enotes() const { return max_legacy_enote_index() + 1; } + /** + * brief: num_sp_enotes - number of seraphis enotes in the ledger + * return: number of seraphis enotes in the ledger + */ + std::uint64_t num_sp_enotes() const { return max_sp_enote_index() + 1; } + /** + * brief: clear_unconfirmed_cache - remove all data stored in unconfirmed cache + */ + void clear_unconfirmed_cache(); + /** + * brief: remove_tx_from_unconfirmed_cache - remove a tx from the unconfirmed cache + * param: tx_id - tx id of tx to remove + */ + void remove_tx_from_unconfirmed_cache(const rct::key &tx_id); + /** + * brief: add_legacy_coinbase - make a block with a mock legacy coinbase tx (containing legacy key images) + * param: tx_id - + * param: unlock_time - + * param: memo - + * param: legacy_key_images_for_block - + * param: output_enotes - + * return: block index of newly added block + */ + std::uint64_t add_legacy_coinbase(const rct::key &tx_id, + const std::uint64_t unlock_time, + TxExtra memo, + std::vector legacy_key_images_for_block, + std::vector output_enotes); + /** + * brief: try_add_unconfirmed_coinbase_v1 - try to add a mock seraphis coinbase tx to the 'unconfirmed' tx cache + * param: coinbase_tx_id - + * param: input_context - + * param: tx_supplement - + * param: output_enotes - + * return: true if adding the tx succeeded + */ + bool try_add_unconfirmed_coinbase_v1(const rct::key &coinbase_tx_id, + const rct::key &input_context, + SpTxSupplementV1 tx_supplement, + std::vector output_enotes); + /** + * brief: try_add_unconfirmed_tx_v1 - try to add a full transaction to the 'unconfirmed' tx cache + * - fails if there are key image duplicates with: unconfirmed, onchain + * - auto-removes any offchain entries that have overlapping key images with this tx + * param: tx - + * return: true if adding succeeded + */ + bool try_add_unconfirmed_tx_v1(const SpTxSquashedV1 &tx); + /** + * brief: commit_unconfirmed_cache_v1 - move all unconfirmed txs onto the chain in a new block, with new mock coinbase tx + * - clears the unconfirmed tx cache + * - note: currently does NOT validate if coinbase enotes are sorted properly + * - note2: permits seraphis enotes of any type (coinbase or regular enotes) for convenience in mockups + * param: coinbase_tx_id - + * param: mock_coinbase_input_context - + * param: mock_coinbase_tx_supplement - + * param: mock_coinbase_output_enotes - + * return: block index of newly added block + */ + std::uint64_t commit_unconfirmed_txs_v1(const rct::key &coinbase_tx_id, + const rct::key &mock_coinbase_input_context, + SpTxSupplementV1 mock_coinbase_tx_supplement, + std::vector mock_coinbase_output_enotes); + /** + * brief: commit_unconfirmed_cache_v1 - move all unconfirmed txs onto the chain in a new block, with new + * coinbase tx + * - throws if the coinbase tx's block index does not equal the ledger's next block index + * - clears the unconfirmed tx cache + * - note: currently does NOT validate the coinbase tx + * - note2: currently does nothing with the block reward + * param: coinbase_tx - + * return: block index of newly added block + */ + std::uint64_t commit_unconfirmed_txs_v1(const SpTxCoinbaseV1 &coinbase_tx); + /** + * brief: pop_chain_at_index - remove all blocks >= the specified block index from the chain + * param: pop_index - first block to pop from the chain + * return: number of blocks popped + */ + std::uint64_t pop_chain_at_index(const std::uint64_t pop_index); + /** + * brief: pop_blocks - remove a specified number of blocks from the chain + * param: num_blocks - number of blocks to remove + * return: number of blocks popped + */ + std::uint64_t pop_blocks(const std::size_t num_blocks); + /** + * brief: get_unconfirmed_chunk_sp - try to find-received scan the unconfirmed tx cache + * param: xk_find_received - + * outparam: chunk_data_out - + */ + void get_unconfirmed_chunk_sp(const crypto::x25519_secret_key &xk_find_received, + scanning::ChunkData &chunk_data_out) const; + /** + * brief: get_onchain_chunk_legacy - legacy view scan a chunk of blocks + * param: chunk_start_index - + * param: chunk_max_size - + * param: legacy_base_spend_pubkey - + * param: legacy_subaddress_map - + * param: legacy_view_privkey - + * param: legacy_scan_mode - + * outparam: chunk_context_out - chunk of scanned blocks (or empty chunk representing top of current chain) + * outparam: chunk_data_out - + */ + void get_onchain_chunk_legacy(const std::uint64_t chunk_start_index, + const std::uint64_t chunk_max_size, + const rct::key &legacy_base_spend_pubkey, + const std::unordered_map &legacy_subaddress_map, + const crypto::secret_key &legacy_view_privkey, + const LegacyScanMode legacy_scan_mode, + scanning::ChunkContext &chunk_context_out, + scanning::ChunkData &chunk_data_out) const; + /** + * brief: get_onchain_chunk_sp - find-received scan a chunk of blocks + * param: chunk_start_index - + * param: chunk_max_size - + * param: xk_find_received - + * outparam: chunk_context_out - chunk of scanned blocks (or empty chunk representing top of current chain) + * outparam: chunk_data_out - + */ + void get_onchain_chunk_sp(const std::uint64_t chunk_start_index, + const std::uint64_t chunk_max_size, + const crypto::x25519_secret_key &xk_find_received, + scanning::ChunkContext &chunk_context_out, + scanning::ChunkData &chunk_data_out) const; + +private: + /// first block where a seraphis tx is allowed (this block and all following must have a seraphis coinbase tx) + std::uint64_t m_first_seraphis_allowed_block; + /// first block where only seraphis txs are allowed + std::uint64_t m_first_seraphis_only_block; + + + //// UNCONFIRMED TXs + + /// cryptonote key images (legacy) + std::unordered_set m_unconfirmed_legacy_key_images; + /// seraphis key images + std::unordered_set m_unconfirmed_sp_key_images; + /// map of tx key images + std::map< + sortable_key, // tx id + std::pair< + std::vector, // legacy key images in tx + std::vector // seraphis key images in tx + > + > m_unconfirmed_tx_key_images; + /// map of seraphis tx outputs + std::map< + sortable_key, // tx id + std::tuple< // tx output contents + rct::key, // input context + SpTxSupplementV1, // tx supplement + std::vector // output enotes + > + > m_unconfirmed_tx_output_contents; + + + //// ON-CHAIN BLOCKS & TXs + + /// Cryptonote key images (legacy) + std::unordered_set m_legacy_key_images; + /// seraphis key images + std::unordered_set m_sp_key_images; + /// map of tx key images + std::map< + std::uint64_t, // block index + std::map< + sortable_key, // tx id + std::pair< + std::vector, // legacy key images in tx + std::vector // seraphis key images in tx + > + > + > m_blocks_of_tx_key_images; + /// legacy enote references {KI, C} (mapped to output index) + std::map m_legacy_enote_references; + /// seraphis squashed enotes (mapped to output index) + std::map m_sp_squashed_enotes; + /// map of accumulated output counts (legacy) + std::map< + std::uint64_t, // block index + std::uint64_t // total number of legacy enotes including those in this block + > m_accumulated_legacy_output_counts; + /// map of accumulated output counts (seraphis) + std::map< + std::uint64_t, // block index + std::uint64_t // total number of seraphis enotes including those in this block + > m_accumulated_sp_output_counts; + /// map of legacy tx outputs + std::map< + std::uint64_t, // block index + std::map< + sortable_key, // tx id + std::tuple< // tx output contents + std::uint64_t, // unlock time + TxExtra, // tx memo + std::vector // output enotes + > + > + > m_blocks_of_legacy_tx_output_contents; + /// map of seraphis tx outputs + std::map< + std::uint64_t, // block index + std::map< + sortable_key, // tx id + std::tuple< // tx output contents + rct::key, // input context + SpTxSupplementV1, // tx supplement + std::vector // output enotes + > + > + > m_blocks_of_sp_tx_output_contents; + /// map of block info + std::map< + std::uint64_t, // block index + std::tuple< + rct::key, // block ID + std::uint64_t // block timestamp + > + > m_block_infos; +}; + +bool try_add_tx_to_ledger(const SpTxCoinbaseV1 &tx_to_add, MockLedgerContext &ledger_context_inout); +bool try_add_tx_to_ledger(const SpTxSquashedV1 &tx_to_add, MockLedgerContext &ledger_context_inout); + +} //namespace mocks +} //namespace sp diff --git a/src/seraphis_mocks/mock_offchain_context.cpp b/src/seraphis_mocks/mock_offchain_context.cpp new file mode 100644 index 0000000000..fb36dbe7b3 --- /dev/null +++ b/src/seraphis_mocks/mock_offchain_context.cpp @@ -0,0 +1,252 @@ +// Copyright (c) 2022, 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. + +// NOT FOR PRODUCTION + +//paired header +#include "mock_offchain_context.h" + +//local headers +#include "crypto/crypto.h" +#include "crypto/x25519.h" +#include "misc_log_ex.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/jamtis_enote_utils.h" +#include "seraphis_core/sp_core_enote_utils.h" +#include "seraphis_main/scan_balance_recovery_utils.h" +#include "seraphis_main/scan_core_types.h" +#include "seraphis_main/tx_component_types.h" +#include "seraphis_main/tx_component_types_legacy.h" +#include "seraphis_main/txtype_squashed_v1.h" + +//third party headers + +//standard headers +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis_mocks" + +namespace sp +{ +namespace mocks +{ +//------------------------------------------------------------------------------------------------------------------- +bool MockOffchainContext::cryptonote_key_image_exists(const crypto::key_image &key_image) const +{ + return m_legacy_key_images.find(key_image) != m_legacy_key_images.end(); +} +//------------------------------------------------------------------------------------------------------------------- +bool MockOffchainContext::seraphis_key_image_exists(const crypto::key_image &key_image) const +{ + return m_sp_key_images.find(key_image) != m_sp_key_images.end(); +} +//------------------------------------------------------------------------------------------------------------------- +bool MockOffchainContext::try_add_partial_tx_v1(const SpPartialTxV1 &partial_tx) +{ + return this->try_add_v1_impl(partial_tx.legacy_input_images, + partial_tx.sp_input_images, + partial_tx.tx_supplement, + partial_tx.outputs); +} +//------------------------------------------------------------------------------------------------------------------- +bool MockOffchainContext::try_add_tx_v1(const SpTxSquashedV1 &tx) +{ + return this->try_add_v1_impl(tx.legacy_input_images, tx.sp_input_images, tx.tx_supplement, tx.outputs); +} +//------------------------------------------------------------------------------------------------------------------- +void MockOffchainContext::remove_tx_from_cache(const rct::key &input_context) +{ + // 1. clear key images + if (m_tx_key_images.find(input_context) != m_tx_key_images.end()) + { + for (const crypto::key_image &key_image : std::get<0>(m_tx_key_images[input_context])) + m_legacy_key_images.erase(key_image); + for (const crypto::key_image &key_image : std::get<1>(m_tx_key_images[input_context])) + m_sp_key_images.erase(key_image); + + m_tx_key_images.erase(input_context); + } + + // 2. clear output contents + m_output_contents.erase(input_context); +} +//------------------------------------------------------------------------------------------------------------------- +void MockOffchainContext::remove_tx_with_key_image_from_cache(const crypto::key_image &key_image) +{ + // 1. early return if key image isn't cached + if (m_sp_key_images.find(key_image) == m_sp_key_images.end() && + m_legacy_key_images.find(key_image) == m_legacy_key_images.end()) + return; + + // 2. remove the tx that has this key image (there should only be one) + auto tx_key_images_search_it = std::find_if(m_tx_key_images.begin(), m_tx_key_images.end(), + [&key_image](const auto &tx_key_images) -> bool + { + // check legacy key images + if (std::find(std::get<0>(tx_key_images.second).begin(), + std::get<0>(tx_key_images.second).end(), + key_image) != + std::get<0>(tx_key_images.second).end()) + return true; + + // check seraphis key images + if (std::find(std::get<1>(tx_key_images.second).begin(), + std::get<1>(tx_key_images.second).end(), + key_image) != + std::get<1>(tx_key_images.second).end()) + return true; + + return false; + } + ); + + if (tx_key_images_search_it != m_tx_key_images.end()) + this->remove_tx_from_cache(tx_key_images_search_it->first); +} +//------------------------------------------------------------------------------------------------------------------- +void MockOffchainContext::clear_cache() +{ + m_legacy_key_images.clear(); + m_sp_key_images.clear(); + m_output_contents.clear(); + m_tx_key_images.clear(); +} +//------------------------------------------------------------------------------------------------------------------- +void MockOffchainContext::get_offchain_chunk_sp(const crypto::x25519_secret_key &xk_find_received, + scanning::ChunkData &chunk_data_out) const +{ + chunk_data_out.basic_records_per_tx.clear(); + chunk_data_out.contextual_key_images.clear(); + + // 1. no chunk if no txs to scan + if (m_output_contents.size() == 0) + return; + + // 2. find-received scan each tx in the unconfirmed chache + std::list collected_records; + SpContextualKeyImageSetV1 collected_key_images; + + for (const auto &tx_with_output_contents : m_output_contents) + { + const rct::key &tx_id{tx_with_output_contents.first}; //use input context as proxy for tx id + + // if this tx contains at least one view-tag match, then add the tx's key images to the chunk + if (scanning::try_find_sp_enotes_in_tx(xk_find_received, + -1, + -1, + tx_id, + 0, + tx_with_output_contents.first, + std::get(tx_with_output_contents.second), + std::get>(tx_with_output_contents.second), + SpEnoteOriginStatus::OFFCHAIN, + collected_records)) + { + chunk_data_out.basic_records_per_tx[tx_id] + .splice(chunk_data_out.basic_records_per_tx[tx_id].end(), collected_records); + + CHECK_AND_ASSERT_THROW_MES(m_tx_key_images.find(tx_with_output_contents.first) != m_tx_key_images.end(), + "offchain find-received scanning (mock offchain context): key image map missing input context (bug)."); + + if (scanning::try_collect_key_images_from_tx(-1, + -1, + tx_id, + std::get<0>(m_tx_key_images.at(tx_with_output_contents.first)), + std::get<1>(m_tx_key_images.at(tx_with_output_contents.first)), + SpEnoteSpentStatus::SPENT_OFFCHAIN, + collected_key_images)) + chunk_data_out.contextual_key_images.emplace_back(std::move(collected_key_images)); + } + } +} +//------------------------------------------------------------------------------------------------------------------- +// internal implementation details +//------------------------------------------------------------------------------------------------------------------- +bool MockOffchainContext::try_add_v1_impl(const std::vector &legacy_input_images, + const std::vector &sp_input_images, + const SpTxSupplementV1 &tx_supplement, + const std::vector &output_enotes) +{ + /// check failure modes + + // 1. fail if new tx overlaps with cached key images: offchain + std::vector legacy_key_images_collected; + std::vector sp_key_images_collected; + + for (const LegacyEnoteImageV2 &legacy_enote_image : legacy_input_images) + { + if (this->cryptonote_key_image_exists(legacy_enote_image.key_image)) + return false; + + legacy_key_images_collected.emplace_back(legacy_enote_image.key_image); + } + + for (const SpEnoteImageV1 &sp_enote_image : sp_input_images) + { + if (this->seraphis_key_image_exists(key_image_ref(sp_enote_image))) + return false; + + sp_key_images_collected.emplace_back(key_image_ref(sp_enote_image)); + } + + rct::key input_context; + jamtis::make_jamtis_input_context_standard(legacy_key_images_collected, sp_key_images_collected, input_context); + + // 2. fail if input context is duplicated (bug since key image check should prevent this) + CHECK_AND_ASSERT_THROW_MES(m_tx_key_images.find(input_context) == m_tx_key_images.end(), + "mock tx ledger (adding offchain tx): input context already exists in key image map (bug)."); + CHECK_AND_ASSERT_THROW_MES(m_output_contents.find(input_context) == m_output_contents.end(), + "mock tx ledger (adding offchain tx): input context already exists in output contents map (bug)."); + + + /// update state + + // 1. add key images + for (const crypto::key_image &legacy_key_image : legacy_key_images_collected) + m_legacy_key_images.insert(legacy_key_image); + + for (const crypto::key_image &sp_key_image : sp_key_images_collected) + m_sp_key_images.insert(sp_key_image); + + m_tx_key_images[input_context] = {std::move(legacy_key_images_collected), std::move(sp_key_images_collected)}; + + // 2. add tx outputs + std::vector output_enote_variants; + output_enote_variants.reserve(output_enotes.size()); + + for (const SpEnoteV1 &enote : output_enotes) + output_enote_variants.emplace_back(enote); + + m_output_contents[input_context] = {tx_supplement, output_enote_variants}; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace mocks +} //namespace sp diff --git a/src/seraphis_mocks/mock_offchain_context.h b/src/seraphis_mocks/mock_offchain_context.h new file mode 100644 index 0000000000..82c02f1d42 --- /dev/null +++ b/src/seraphis_mocks/mock_offchain_context.h @@ -0,0 +1,149 @@ +// Copyright (c) 2022, 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. + +// NOT FOR PRODUCTION + +// Mock offchain context. +// note: the input context is used as a proxy for tx id in the maps, because the tx id is not known for partial txs + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "crypto/x25519.h" +#include "ringct/rctOps.h" +#include "ringct/rctTypes.h" +#include "seraphis_main/tx_component_types.h" +#include "seraphis_main/tx_component_types_legacy.h" + +//third party headers + +//standard headers +#include +#include +#include +#include + +//forward declarations +namespace sp +{ + struct SpPartialTxV1; + struct SpTxSquashedV1; +namespace scanning +{ + struct ChunkData; +} +} + +namespace sp +{ +namespace mocks +{ + +class MockOffchainContext final +{ +public: + /** + * brief: cryptonote_key_image_exists - checks if a cryptonote key image exists in the offchain context + * param: key_image - + * return: true/false on check result + */ + bool cryptonote_key_image_exists(const crypto::key_image &key_image) const; + /** + * brief: seraphis_key_image_exists - checks if a seraphis key image exists in the offchain context + * param: key_image - + * return: true/false on check result + */ + bool seraphis_key_image_exists(const crypto::key_image &key_image) const; + /** + * brief: try_add_partial_tx_v1 - try to add a partial transaction to the offchain tx cache + * - fails if there are key image duplicates with: offchain + * param: partial_tx - + * return: true if adding succeeded + */ + bool try_add_partial_tx_v1(const SpPartialTxV1 &partial_tx); + /** + * brief: try_add_tx_v1 - try to add a full transaction to the offchain tx cache + * - fails if there are key image duplicates with: offchain + * param: tx - + * return: true if adding succeeded + */ + bool try_add_tx_v1(const SpTxSquashedV1 &tx); + /** + * brief: remove_tx_from_cache - remove a tx or partial tx from the offchain cache + * param: input_context - input context of tx/partial tx to remove + */ + void remove_tx_from_cache(const rct::key &input_context); + /** + * brief: remove_tx_with_key_image_from_cache - remove the tx with a specified key image from the offchain cache + * param: key_image - key image in tx/partial tx to remove + */ + void remove_tx_with_key_image_from_cache(const crypto::key_image &key_image); + /** + * brief: clear_cache - remove all data stored in offchain cache + */ + void clear_cache(); + /** + * brief: get_offchain_chunk_sp - find-received scan the offchain tx cache + * param: xk_find_received - + * outparam: chunk_data_out - + * return: true if chunk is not empty + */ + void get_offchain_chunk_sp(const crypto::x25519_secret_key &xk_find_received, + scanning::ChunkData &chunk_data_out) const; + +private: + bool try_add_v1_impl(const std::vector &legacy_input_images, + const std::vector &sp_input_images, + const SpTxSupplementV1 &tx_supplement, + const std::vector &output_enotes); + + /// legacy key images + std::unordered_set m_legacy_key_images; + /// Seraphis key images + std::unordered_set m_sp_key_images; + /// map of tx outputs + std::unordered_map< + rct::key, // input context + std::tuple< // tx output contents + SpTxSupplementV1, // tx supplement + std::vector // output enotes + > + > m_output_contents; + /// map of tx key images + std::unordered_map< + rct::key, // input context + std::pair< + std::vector, // legacy key images in tx + std::vector // seraphis key images in tx + > + > m_tx_key_images; +}; + +} //namespace mocks +} //namespace sp diff --git a/src/seraphis_mocks/mock_send_receive.cpp b/src/seraphis_mocks/mock_send_receive.cpp new file mode 100644 index 0000000000..777c134f89 --- /dev/null +++ b/src/seraphis_mocks/mock_send_receive.cpp @@ -0,0 +1,538 @@ +// Copyright (c) 2022, 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. + +// NOT FOR PRODUCTION + +//paired header +#include "mock_send_receive.h" + +//local headers +#include "common/container_helpers.h" +#include "crypto/crypto.h" +#include "cryptonote_basic/subaddress_index.h" +#include "enote_finding_context_mocks.h" +#include "misc_log_ex.h" +#include "mock_ledger_context.h" +#include "mock_tx_builders_inputs.h" +#include "mock_tx_builders_legacy_inputs.h" +#include "ringct/rctOps.h" +#include "ringct/rctTypes.h" +#include "scan_chunk_consumer_mocks.h" +#include "seraphis_core/binned_reference_set.h" +#include "seraphis_core/binned_reference_set_utils.h" +#include "seraphis_core/legacy_core_utils.h" +#include "seraphis_core/legacy_enote_utils.h" +#include "seraphis_core/sp_core_types.h" +#include "seraphis_impl/scan_context_simple.h" +#include "seraphis_impl/scan_process_basic.h" +#include "seraphis_impl/tx_builder_utils.h" +#include "seraphis_main/contextual_enote_record_utils.h" +#include "seraphis_main/scan_machine_types.h" +#include "seraphis_main/tx_builder_types.h" +#include "seraphis_main/tx_builders_inputs.h" +#include "seraphis_main/tx_builders_mixed.h" +#include "seraphis_main/tx_component_types.h" +#include "seraphis_main/txtype_coinbase_v1.h" +#include "seraphis_main/txtype_squashed_v1.h" +#include "tx_validation_context_mock.h" + +//third party headers + +//standard headers +#include +#include +#include +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis_mocks" + +namespace sp +{ +namespace mocks +{ +//------------------------------------------------------------------------------------------------------------------- +void convert_outlay_to_payment_proposal(const rct::xmr_amount outlay_amount, + const jamtis::JamtisDestinationV1 &destination, + const TxExtra &partial_memo_for_destination, + jamtis::JamtisPaymentProposalV1 &payment_proposal_out) +{ + payment_proposal_out = jamtis::JamtisPaymentProposalV1{ + .destination = destination, + .amount = outlay_amount, + .enote_ephemeral_privkey = crypto::x25519_secret_key_gen(), + .partial_memo = partial_memo_for_destination + }; +} +//------------------------------------------------------------------------------------------------------------------- +void send_legacy_coinbase_amounts_to_user(const std::vector &coinbase_amounts, + const rct::key &destination_subaddr_spend_pubkey, + const rct::key &destination_subaddr_view_pubkey, + MockLedgerContext &ledger_context_inout) +{ + // 1. prepare mock coinbase enotes + std::vector coinbase_enotes; + std::vector collected_enote_ephemeral_pubkeys; + TxExtra tx_extra; + coinbase_enotes.reserve(coinbase_amounts.size()); + coinbase_enotes.reserve(coinbase_amounts.size()); + + LegacyEnoteV5 enote_temp; + + for (std::size_t amount_index{0}; amount_index < coinbase_amounts.size(); ++amount_index) + { + // a. legacy enote ephemeral pubkey + const crypto::secret_key enote_ephemeral_privkey{rct::rct2sk(rct::skGen())}; + collected_enote_ephemeral_pubkeys.emplace_back( + rct::scalarmultKey(destination_subaddr_spend_pubkey, rct::sk2rct(enote_ephemeral_privkey)) + ); + + // b. make legacy coinbase enote + make_legacy_enote_v5(destination_subaddr_spend_pubkey, + destination_subaddr_view_pubkey, + coinbase_amounts[amount_index], + amount_index, + enote_ephemeral_privkey, + enote_temp); + + coinbase_enotes.emplace_back(enote_temp); + } + + // 2. set tx extra + CHECK_AND_ASSERT_THROW_MES(try_append_legacy_enote_ephemeral_pubkeys_to_tx_extra(collected_enote_ephemeral_pubkeys, + tx_extra), + "send legacy coinbase amounts to user: appending enote ephemeral pubkeys to tx extra failed."); + + // 3. commit coinbase enotes as new block + ledger_context_inout.add_legacy_coinbase(rct::pkGen(), 0, std::move(tx_extra), {}, std::move(coinbase_enotes)); +} +//------------------------------------------------------------------------------------------------------------------- +void send_sp_coinbase_amounts_to_user(const std::vector &coinbase_amounts, + const jamtis::JamtisDestinationV1 &user_address, + MockLedgerContext &ledger_context_inout) +{ + // 1. prepare payment proposals + std::vector payment_proposals; + payment_proposals.reserve(coinbase_amounts.size()); + rct::xmr_amount block_reward{0}; + + for (const rct::xmr_amount coinbase_amount : coinbase_amounts) + { + // a. make payment proposal + convert_outlay_to_payment_proposal(coinbase_amount, + user_address, + TxExtra{}, + tools::add_element(payment_proposals)); + + // b. accumulate the block reward + block_reward += coinbase_amount; + } + + // 2. make a coinbase tx + SpTxCoinbaseV1 coinbase_tx; + make_seraphis_tx_coinbase_v1(SpTxCoinbaseV1::SemanticRulesVersion::MOCK, + ledger_context_inout.chain_height() + 1, + block_reward, + std::move(payment_proposals), + {}, + coinbase_tx); + + // 3. validate the coinbase tx + const TxValidationContextMock tx_validation_context{ledger_context_inout}; + CHECK_AND_ASSERT_THROW_MES(validate_tx(coinbase_tx, tx_validation_context), + "send sp coinbase amounts to user (mock): failed to validate coinbase tx."); + + // 4. commit coinbase tx as new block + ledger_context_inout.commit_unconfirmed_txs_v1(coinbase_tx); +} +//------------------------------------------------------------------------------------------------------------------- +void send_sp_coinbase_amounts_to_users(const std::vector> &coinbase_amounts_per_user, + const std::vector &user_addresses, + MockLedgerContext &ledger_context_inout) +{ + CHECK_AND_ASSERT_THROW_MES(coinbase_amounts_per_user.size() == user_addresses.size(), + "send sp coinbase amounts to users (mock): amount : address mismatch."); + + // 1. prepare payment proposals + std::vector payment_proposals; + payment_proposals.reserve(coinbase_amounts_per_user.size()); + rct::xmr_amount block_reward{0}; + + for (std::size_t user_index{0}; user_index < user_addresses.size(); ++user_index) + { + for (const rct::xmr_amount user_amount : coinbase_amounts_per_user[user_index]) + { + // a .make payment proposal + convert_outlay_to_payment_proposal(user_amount, + user_addresses[user_index], + TxExtra{}, + tools::add_element(payment_proposals)); + + // b. accumulate the block reward + block_reward += user_amount; + } + } + + // 2. make a coinbase tx + SpTxCoinbaseV1 coinbase_tx; + make_seraphis_tx_coinbase_v1(SpTxCoinbaseV1::SemanticRulesVersion::MOCK, + ledger_context_inout.chain_height() + 1, + block_reward, + std::move(payment_proposals), + {}, + coinbase_tx); + + // 3. validate the coinbase tx + const TxValidationContextMock tx_validation_context{ledger_context_inout}; + CHECK_AND_ASSERT_THROW_MES(validate_tx(coinbase_tx, tx_validation_context), + "send sp coinbase amounts to user (mock): failed to validate coinbase tx."); + + // 4. commit coinbase tx as new block + ledger_context_inout.commit_unconfirmed_txs_v1(coinbase_tx); +} +//------------------------------------------------------------------------------------------------------------------- +void construct_tx_for_mock_ledger_v1(const legacy_mock_keys &local_user_legacy_keys, + const jamtis::mocks::jamtis_mock_keys &local_user_sp_keys, + const InputSelectorV1 &local_user_input_selector, + const FeeCalculator &tx_fee_calculator, + const rct::xmr_amount fee_per_tx_weight, + const std::size_t max_inputs, + const std::vector> &outlays, + const std::size_t legacy_ring_size, + const std::size_t ref_set_decomp_n, + const std::size_t ref_set_decomp_m, + const SpBinnedReferenceSetConfigV1 &bin_config, + const MockLedgerContext &ledger_context, + SpTxSquashedV1 &tx_out) +{ + /// build transaction + + // 1. prepare dummy and change addresses + jamtis::JamtisDestinationV1 change_address; + jamtis::JamtisDestinationV1 dummy_address; + make_random_address_for_user(local_user_sp_keys, change_address); + make_random_address_for_user(local_user_sp_keys, dummy_address); + + // 2. convert outlays to normal payment proposals + std::vector normal_payment_proposals; + normal_payment_proposals.reserve(outlays.size()); + + for (const auto &outlay : outlays) + { + convert_outlay_to_payment_proposal(std::get(outlay), + std::get(outlay), + std::get(outlay), + tools::add_element(normal_payment_proposals)); + } + + // 3. prepare inputs and finalize outputs + std::vector legacy_contextual_inputs; + std::vector sp_contextual_inputs; + std::vector selfsend_payment_proposals; //note: no user-defined selfsends + DiscretizedFee discretized_transaction_fee; + CHECK_AND_ASSERT_THROW_MES(try_prepare_inputs_and_outputs_for_transfer_v1(change_address, + dummy_address, + local_user_input_selector, + tx_fee_calculator, + fee_per_tx_weight, + max_inputs, + std::move(normal_payment_proposals), + std::move(selfsend_payment_proposals), + local_user_sp_keys.k_vb, + legacy_contextual_inputs, + sp_contextual_inputs, + normal_payment_proposals, + selfsend_payment_proposals, + discretized_transaction_fee), + "construct tx for mock ledger (v1): preparing inputs and outputs failed."); + + // 4. tx proposal + SpTxProposalV1 tx_proposal; + make_v1_tx_proposal_v1(legacy_contextual_inputs, + sp_contextual_inputs, + std::move(normal_payment_proposals), + std::move(selfsend_payment_proposals), + discretized_transaction_fee, + TxExtra{}, + tx_proposal); + + // 5. tx proposal prefix + const tx_version_t tx_version{tx_version_from(SpTxSquashedV1::SemanticRulesVersion::MOCK)}; + + rct::key tx_proposal_prefix; + get_tx_proposal_prefix_v1(tx_proposal, tx_version, local_user_sp_keys.k_vb, tx_proposal_prefix); + + // 6. get ledger mappings for the input membership proofs + // note: do this after making the tx proposal to demo that inputs don't have to be on-chain when proposing a tx + std::unordered_map legacy_input_ledger_mappings; + std::unordered_map sp_input_ledger_mappings; + try_get_membership_proof_real_reference_mappings(legacy_contextual_inputs, legacy_input_ledger_mappings); + try_get_membership_proof_real_reference_mappings(sp_contextual_inputs, sp_input_ledger_mappings); + + // 7. prepare for legacy ring signatures + std::vector legacy_ring_signature_preps; + make_mock_legacy_ring_signature_preps_for_inputs_v1(tx_proposal_prefix, + legacy_input_ledger_mappings, + tx_proposal.legacy_input_proposals, + legacy_ring_size, + ledger_context, + legacy_ring_signature_preps); + + // 8. prepare for membership proofs + std::vector sp_membership_proof_preps; + make_mock_sp_membership_proof_preps_for_inputs_v1(sp_input_ledger_mappings, + tx_proposal.sp_input_proposals, + ref_set_decomp_n, + ref_set_decomp_m, + bin_config, + ledger_context, + sp_membership_proof_preps); + + // 9. complete tx + make_seraphis_tx_squashed_v1(SpTxSquashedV1::SemanticRulesVersion::MOCK, + tx_proposal, + std::move(legacy_ring_signature_preps), + std::move(sp_membership_proof_preps), + local_user_legacy_keys.k_s, + local_user_sp_keys.k_m, + local_user_sp_keys.k_vb, + hw::get_device("default"), + tx_out); +} +//------------------------------------------------------------------------------------------------------------------- +void transfer_funds_single_mock_v1_unconfirmed_sp_only(const jamtis::mocks::jamtis_mock_keys &local_user_sp_keys, + const InputSelectorV1 &local_user_input_selector, + const FeeCalculator &tx_fee_calculator, + const rct::xmr_amount fee_per_tx_weight, + const std::size_t max_inputs, + const std::vector> &outlays, + const std::size_t ref_set_decomp_n, + const std::size_t ref_set_decomp_m, + const SpBinnedReferenceSetConfigV1 &bin_config, + MockLedgerContext &ledger_context_inout) +{ + // 1. make one tx + SpTxSquashedV1 single_tx; + construct_tx_for_mock_ledger_v1(legacy_mock_keys{}, //no legacy inputs + local_user_sp_keys, + local_user_input_selector, + tx_fee_calculator, + fee_per_tx_weight, + max_inputs, + outlays, + 0, + ref_set_decomp_n, + ref_set_decomp_m, + bin_config, + ledger_context_inout, + single_tx); + + // 2. validate and submit to the mock ledger + const TxValidationContextMock tx_validation_context{ledger_context_inout}; + CHECK_AND_ASSERT_THROW_MES(validate_tx(single_tx, tx_validation_context), + "transfer funds single mock unconfirmed: validating tx failed."); + CHECK_AND_ASSERT_THROW_MES(ledger_context_inout.try_add_unconfirmed_tx_v1(single_tx), + "transfer funds single mock unconfirmed: adding unconfirmed tx to mock ledger failed."); +} +//------------------------------------------------------------------------------------------------------------------- +void transfer_funds_single_mock_v1_unconfirmed(const legacy_mock_keys &local_user_legacy_keys, + const jamtis::mocks::jamtis_mock_keys &local_user_sp_keys, + const InputSelectorV1 &local_user_input_selector, + const FeeCalculator &tx_fee_calculator, + const rct::xmr_amount fee_per_tx_weight, + const std::size_t max_inputs, + const std::vector> &outlays, + const std::size_t legacy_ring_size, + const std::size_t ref_set_decomp_n, + const std::size_t ref_set_decomp_m, + const SpBinnedReferenceSetConfigV1 &bin_config, + MockLedgerContext &ledger_context_inout) +{ + // 1. make one tx + SpTxSquashedV1 single_tx; + construct_tx_for_mock_ledger_v1(local_user_legacy_keys, + local_user_sp_keys, + local_user_input_selector, + tx_fee_calculator, + fee_per_tx_weight, + max_inputs, + outlays, + legacy_ring_size, + ref_set_decomp_n, + ref_set_decomp_m, + bin_config, + ledger_context_inout, + single_tx); + + // 2. validate and submit to the mock ledger + const TxValidationContextMock tx_validation_context{ledger_context_inout}; + CHECK_AND_ASSERT_THROW_MES(validate_tx(single_tx, tx_validation_context), + "transfer funds single mock unconfirmed sp only: validating tx failed."); + CHECK_AND_ASSERT_THROW_MES(ledger_context_inout.try_add_unconfirmed_tx_v1(single_tx), + "transfer funds single mock unconfirmed sp only: validating tx failed."); +} +//------------------------------------------------------------------------------------------------------------------- +void transfer_funds_single_mock_v1(const legacy_mock_keys &local_user_legacy_keys, + const jamtis::mocks::jamtis_mock_keys &local_user_sp_keys, + const InputSelectorV1 &local_user_input_selector, + const FeeCalculator &tx_fee_calculator, + const rct::xmr_amount fee_per_tx_weight, + const std::size_t max_inputs, + const std::vector> &outlays, + const std::size_t legacy_ring_size, + const std::size_t ref_set_decomp_n, + const std::size_t ref_set_decomp_m, + const SpBinnedReferenceSetConfigV1 &bin_config, + MockLedgerContext &ledger_context_inout) +{ + // 1. make one tx + SpTxSquashedV1 single_tx; + construct_tx_for_mock_ledger_v1(local_user_legacy_keys, + local_user_sp_keys, + local_user_input_selector, + tx_fee_calculator, + fee_per_tx_weight, + max_inputs, + outlays, + legacy_ring_size, + ref_set_decomp_n, + ref_set_decomp_m, + bin_config, + ledger_context_inout, + single_tx); + + // 2, validate and submit to the mock ledger + const TxValidationContextMock tx_validation_context{ledger_context_inout}; + CHECK_AND_ASSERT_THROW_MES(validate_tx(single_tx, tx_validation_context), + "transfer funds single mock: validating tx failed."); + CHECK_AND_ASSERT_THROW_MES(try_add_tx_to_ledger(single_tx, ledger_context_inout), + "transfer funds single mock: adding tx to mock ledger failed."); +} +//------------------------------------------------------------------------------------------------------------------- +void refresh_user_enote_store_legacy_intermediate(const rct::key &legacy_base_spend_pubkey, + const std::unordered_map &legacy_subaddress_map, + const crypto::secret_key &legacy_view_privkey, + const LegacyScanMode legacy_scan_mode, + const scanning::ScanMachineConfig &refresh_config, + const MockLedgerContext &ledger_context, + SpEnoteStore &user_enote_store_inout) +{ + const EnoteFindingContextLedgerMockLegacy enote_finding_context{ + ledger_context, + legacy_base_spend_pubkey, + legacy_subaddress_map, + legacy_view_privkey, + legacy_scan_mode + }; + scanning::ScanContextNonLedgerDummy scan_context_nonledger{}; + scanning::ScanContextLedgerSimple scan_context_ledger{enote_finding_context}; + ChunkConsumerMockLegacyIntermediate chunk_consumer{ + legacy_base_spend_pubkey, + legacy_view_privkey, + legacy_scan_mode, + user_enote_store_inout + }; + + sp::refresh_enote_store(refresh_config, + scan_context_nonledger, + scan_context_ledger, + chunk_consumer); +} +//------------------------------------------------------------------------------------------------------------------- +void refresh_user_enote_store_legacy_full(const rct::key &legacy_base_spend_pubkey, + const std::unordered_map &legacy_subaddress_map, + const crypto::secret_key &legacy_spend_privkey, + const crypto::secret_key &legacy_view_privkey, + const scanning::ScanMachineConfig &refresh_config, + const MockLedgerContext &ledger_context, + SpEnoteStore &user_enote_store_inout) +{ + const EnoteFindingContextLedgerMockLegacy enote_finding_context{ + ledger_context, + legacy_base_spend_pubkey, + legacy_subaddress_map, + legacy_view_privkey, + LegacyScanMode::SCAN + }; + scanning::ScanContextNonLedgerDummy scan_context_nonledger{}; + scanning::ScanContextLedgerSimple scan_context_ledger{enote_finding_context}; + ChunkConsumerMockLegacy chunk_consumer{ + legacy_base_spend_pubkey, + legacy_spend_privkey, + legacy_view_privkey, + user_enote_store_inout + }; + + sp::refresh_enote_store(refresh_config, + scan_context_nonledger, + scan_context_ledger, + chunk_consumer); +} +//------------------------------------------------------------------------------------------------------------------- +void refresh_user_enote_store_PV(const jamtis::mocks::jamtis_mock_keys &user_keys, + const scanning::ScanMachineConfig &refresh_config, + const MockLedgerContext &ledger_context, + SpEnoteStorePaymentValidator &user_enote_store_inout) +{ + const EnoteFindingContextUnconfirmedMockSp enote_finding_context_unconfirmed{ledger_context, user_keys.xk_fr}; + const EnoteFindingContextLedgerMockSp enote_finding_context_ledger{ledger_context, user_keys.xk_fr}; + scanning::ScanContextNonLedgerSimple scan_context_unconfirmed{enote_finding_context_unconfirmed}; + scanning::ScanContextLedgerSimple scan_context_ledger{enote_finding_context_ledger}; + ChunkConsumerMockSpIntermediate chunk_consumer{ + user_keys.K_1_base, + user_keys.xk_ua, + user_keys.xk_fr, + user_keys.s_ga, + user_enote_store_inout + }; + + sp::refresh_enote_store(refresh_config, + scan_context_unconfirmed, + scan_context_ledger, + chunk_consumer); +} +//------------------------------------------------------------------------------------------------------------------- +void refresh_user_enote_store(const jamtis::mocks::jamtis_mock_keys &user_keys, + const scanning::ScanMachineConfig &refresh_config, + const MockLedgerContext &ledger_context, + SpEnoteStore &user_enote_store_inout) +{ + const EnoteFindingContextUnconfirmedMockSp enote_finding_context_unconfirmed{ledger_context, user_keys.xk_fr}; + const EnoteFindingContextLedgerMockSp enote_finding_context_ledger{ledger_context, user_keys.xk_fr}; + scanning::ScanContextNonLedgerSimple scan_context_unconfirmed{enote_finding_context_unconfirmed}; + scanning::ScanContextLedgerSimple scan_context_ledger{enote_finding_context_ledger}; + ChunkConsumerMockSp chunk_consumer{user_keys.K_1_base, user_keys.k_vb, user_enote_store_inout}; + + sp::refresh_enote_store(refresh_config, + scan_context_unconfirmed, + scan_context_ledger, + chunk_consumer); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace mocks +} //namespace sp diff --git a/src/seraphis_mocks/mock_send_receive.h b/src/seraphis_mocks/mock_send_receive.h new file mode 100644 index 0000000000..25668cbea0 --- /dev/null +++ b/src/seraphis_mocks/mock_send_receive.h @@ -0,0 +1,160 @@ +// Copyright (c) 2022, 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. + +// NOT FOR PRODUCTION + +// Seraphis tx-builder/component-builder mockups (tx inputs). + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "cryptonote_basic/subaddress_index.h" +#include "enote_finding_context_mocks.h" +#include "jamtis_mock_keys.h" +#include "legacy_mock_keys.h" +#include "mock_ledger_context.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/binned_reference_set.h" +#include "seraphis_core/jamtis_payment_proposal.h" +#include "seraphis_core/tx_extra.h" +#include "seraphis_impl/enote_store.h" +#include "seraphis_impl/enote_store_payment_validator.h" +#include "seraphis_main/scan_machine_types.h" +#include "seraphis_main/tx_builder_types.h" +#include "seraphis_main/tx_fee_calculator.h" +#include "seraphis_main/tx_input_selection.h" +#include "seraphis_main/txtype_coinbase_v1.h" +#include "seraphis_main/txtype_squashed_v1.h" + +//third party headers + +//standard headers +#include +#include +#include + +//forward declarations + + +namespace sp +{ +namespace mocks +{ + +/// make a payment proposal +void convert_outlay_to_payment_proposal(const rct::xmr_amount outlay_amount, + const jamtis::JamtisDestinationV1 &destination, + const TxExtra &partial_memo_for_destination, + jamtis::JamtisPaymentProposalV1 &payment_proposal_out); +/// send funds as coinbase enotes +void send_legacy_coinbase_amounts_to_user(const std::vector &coinbase_amounts, + const rct::key &destination_subaddr_spend_pubkey, + const rct::key &destination_subaddr_view_pubkey, + MockLedgerContext &ledger_context_inout); +void send_sp_coinbase_amounts_to_user(const std::vector &coinbase_amounts, + const jamtis::JamtisDestinationV1 &user_address, + MockLedgerContext &ledger_context_inout); +void send_sp_coinbase_amounts_to_users(const std::vector> &coinbase_amounts_per_user, + const std::vector &user_addresses, + MockLedgerContext &ledger_context_inout); +/// create a seraphis transaction +void construct_tx_for_mock_ledger_v1(const legacy_mock_keys &local_user_legacy_keys, + const jamtis::mocks::jamtis_mock_keys &local_user_sp_keys, + const InputSelectorV1 &local_user_input_selector, + const FeeCalculator &tx_fee_calculator, + const rct::xmr_amount fee_per_tx_weight, + const std::size_t max_inputs, + const std::vector> &outlays, + const std::size_t legacy_ring_size, + const std::size_t ref_set_decomp_n, + const std::size_t ref_set_decomp_m, + const SpBinnedReferenceSetConfigV1 &bin_config, + const MockLedgerContext &ledger_context, + SpTxSquashedV1 &tx_out); +/// create transactions and submit them to a mock ledger +void transfer_funds_single_mock_v1_unconfirmed_sp_only(const jamtis::mocks::jamtis_mock_keys &local_user_sp_keys, + const InputSelectorV1 &local_user_input_selector, + const FeeCalculator &tx_fee_calculator, + const rct::xmr_amount fee_per_tx_weight, + const std::size_t max_inputs, + const std::vector> &outlays, + const std::size_t ref_set_decomp_n, + const std::size_t ref_set_decomp_m, + const SpBinnedReferenceSetConfigV1 &bin_config, + MockLedgerContext &ledger_context_inout); +void transfer_funds_single_mock_v1_unconfirmed(const legacy_mock_keys &local_user_legacy_keys, + const jamtis::mocks::jamtis_mock_keys &local_user_sp_keys, + const InputSelectorV1 &local_user_input_selector, + const FeeCalculator &tx_fee_calculator, + const rct::xmr_amount fee_per_tx_weight, + const std::size_t max_inputs, + const std::vector> &outlays, + const std::size_t legacy_ring_size, + const std::size_t ref_set_decomp_n, + const std::size_t ref_set_decomp_m, + const SpBinnedReferenceSetConfigV1 &bin_config, + MockLedgerContext &ledger_context_inout); +void transfer_funds_single_mock_v1(const legacy_mock_keys &local_user_legacy_keys, + const jamtis::mocks::jamtis_mock_keys &local_user_sp_keys, + const InputSelectorV1 &local_user_input_selector, + const FeeCalculator &tx_fee_calculator, + const rct::xmr_amount fee_per_tx_weight, + const std::size_t max_inputs, + const std::vector> &outlays, + const std::size_t legacy_ring_size, + const std::size_t ref_set_decomp_n, + const std::size_t ref_set_decomp_m, + const SpBinnedReferenceSetConfigV1 &bin_config, + MockLedgerContext &ledger_context_inout); +/// refresh an enote store +void refresh_user_enote_store_legacy_intermediate(const rct::key &legacy_base_spend_pubkey, + const std::unordered_map &legacy_subaddress_map, + const crypto::secret_key &legacy_view_privkey, + const LegacyScanMode legacy_scan_mode, + const scanning::ScanMachineConfig &refresh_config, + const MockLedgerContext &ledger_context, + SpEnoteStore &user_enote_store_inout); +void refresh_user_enote_store_legacy_full(const rct::key &legacy_base_spend_pubkey, + const std::unordered_map &legacy_subaddress_map, + const crypto::secret_key &legacy_spend_privkey, + const crypto::secret_key &legacy_view_privkey, + const scanning::ScanMachineConfig &refresh_config, + const MockLedgerContext &ledger_context, + SpEnoteStore &user_enote_store_inout); +void refresh_user_enote_store_PV(const jamtis::mocks::jamtis_mock_keys &user_keys, + const scanning::ScanMachineConfig &refresh_config, + const MockLedgerContext &ledger_context, + SpEnoteStorePaymentValidator &user_enote_store_inout); +void refresh_user_enote_store(const jamtis::mocks::jamtis_mock_keys &user_keys, + const scanning::ScanMachineConfig &refresh_config, + const MockLedgerContext &ledger_context, + SpEnoteStore &user_enote_store_inout); + +} //namespace mocks +} //namespace sp diff --git a/src/seraphis_mocks/mock_tx_builders_inputs.cpp b/src/seraphis_mocks/mock_tx_builders_inputs.cpp new file mode 100644 index 0000000000..7c76d5f0b4 --- /dev/null +++ b/src/seraphis_mocks/mock_tx_builders_inputs.cpp @@ -0,0 +1,300 @@ +// Copyright (c) 2022, 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. + +// NOT FOR PRODUCTION + +//paired header +#include "mock_tx_builders_inputs.h" + +//local headers +#include "common/container_helpers.h" +#include "crypto/crypto.h" +#include "misc_log_ex.h" +#include "mock_ledger_context.h" +#include "ringct/rctOps.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/binned_reference_set.h" +#include "seraphis_core/binned_reference_set_utils.h" +#include "seraphis_core/sp_core_types.h" +#include "seraphis_core/sp_ref_set_index_mapper_flat.h" +#include "seraphis_crypto/math_utils.h" +#include "seraphis_main/tx_builder_types.h" +#include "seraphis_main/tx_builders_inputs.h" +#include "seraphis_main/tx_component_types.h" + +//third party headers + +//standard headers +#include +#include +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis_mocks" + +namespace sp +{ +namespace mocks +{ +//------------------------------------------------------------------------------------------------------------------- +std::vector gen_mock_sp_input_proposals_v1(const crypto::secret_key &sp_spend_privkey, + const crypto::secret_key &k_view_balance, + const std::vector &in_amounts) +{ + // generate random inputs + std::vector input_proposals; + input_proposals.reserve(in_amounts.size()); + + for (const rct::xmr_amount in_amount : in_amounts) + tools::add_element(input_proposals) = gen_sp_input_proposal_v1(sp_spend_privkey, k_view_balance, in_amount); + + return input_proposals; +} +//------------------------------------------------------------------------------------------------------------------- +SpMembershipProofPrepV1 gen_mock_sp_membership_proof_prep_for_enote_at_pos_v1( + const SpEnoteCoreVariant &real_reference_enote, + const std::uint64_t &real_reference_index_in_ledger, + const crypto::secret_key &address_mask, + const crypto::secret_key &commitment_mask, + const std::size_t ref_set_decomp_n, + const std::size_t ref_set_decomp_m, + const SpBinnedReferenceSetConfigV1 &bin_config, + const MockLedgerContext &ledger_context) +{ + // generate a mock membership proof prep + + /// checks and initialization + const std::size_t ref_set_size{math::uint_pow(ref_set_decomp_n, ref_set_decomp_m)}; // n^m + + CHECK_AND_ASSERT_THROW_MES(validate_bin_config_v1(ref_set_size, bin_config), + "gen mock membership proof prep: invalid binned reference set config."); + + + /// make binned reference set + SpMembershipProofPrepV1 proof_prep; + + // 1. flat index mapper for mock-up + const SpRefSetIndexMapperFlat flat_index_mapper{0, ledger_context.max_sp_enote_index()}; + + // 2. generator seed + rct::key generator_seed; + make_binned_ref_set_generator_seed_v1(onetime_address_ref(real_reference_enote), + amount_commitment_ref(real_reference_enote), + address_mask, + commitment_mask, + generator_seed); + + // 3. binned reference set + make_binned_reference_set_v1(flat_index_mapper, + bin_config, + generator_seed, + ref_set_size, + real_reference_index_in_ledger, + proof_prep.binned_reference_set); + + + /// copy all referenced enotes from the ledger (in squashed enote representation) + std::vector reference_indices; + CHECK_AND_ASSERT_THROW_MES(try_get_reference_indices_from_binned_reference_set_v1(proof_prep.binned_reference_set, + reference_indices), + "gen mock membership proof prep: could not extract reference indices from binned representation (bug)."); + + ledger_context.get_reference_set_proof_elements_v2(reference_indices, proof_prep.referenced_enotes_squashed); + + + /// copy misc pieces + proof_prep.ref_set_decomp_n = ref_set_decomp_n; + proof_prep.ref_set_decomp_m = ref_set_decomp_m; + proof_prep.real_reference_enote = real_reference_enote; + proof_prep.address_mask = address_mask; + proof_prep.commitment_mask = commitment_mask; + + return proof_prep; +} +//------------------------------------------------------------------------------------------------------------------- +SpMembershipProofPrepV1 gen_mock_sp_membership_proof_prep_v1( + const SpEnoteCoreVariant &real_reference_enote, + const crypto::secret_key &address_mask, + const crypto::secret_key &commitment_mask, + const std::size_t ref_set_decomp_n, + const std::size_t ref_set_decomp_m, + const SpBinnedReferenceSetConfigV1 &bin_config, + MockLedgerContext &ledger_context_inout) +{ + // generate a mock membership proof prep + + /// add fake enotes to the ledger (2x the ref set size), with the real one at a random location + + // 1. make fake enotes + const std::size_t ref_set_size{math::uint_pow(ref_set_decomp_n, ref_set_decomp_m)}; // n^m + const std::size_t num_enotes_to_add{ref_set_size * 2}; + const std::size_t add_real_at_pos{crypto::rand_idx(num_enotes_to_add)}; + std::vector mock_enotes; + mock_enotes.reserve(num_enotes_to_add); + + for (std::size_t enote_to_add{0}; enote_to_add < num_enotes_to_add; ++enote_to_add) + { + if (enote_to_add == add_real_at_pos) + { + if (const SpCoinbaseEnoteCore *enote_ptr = real_reference_enote.try_unwrap()) + mock_enotes.emplace_back(SpCoinbaseEnoteV1{.core = *enote_ptr}); + else if (const SpEnoteCore *enote_ptr = real_reference_enote.try_unwrap()) + mock_enotes.emplace_back(SpEnoteV1{.core = *enote_ptr}); + else + CHECK_AND_ASSERT_THROW_MES(false, "gen mock sp membership proof prep: invalid real reference enote type."); + } + else + mock_enotes.emplace_back(gen_sp_enote_v1()); + } + + // 2. clear any txs lingering unconfirmed + ledger_context_inout.commit_unconfirmed_txs_v1(rct::pkGen(), + rct::pkGen(), + SpTxSupplementV1{}, + std::vector{}); + + // 3. add mock enotes as the outputs of a mock coinbase tx + const std::uint64_t real_reference_index_in_ledger{ledger_context_inout.max_sp_enote_index() + add_real_at_pos + 1}; + ledger_context_inout.commit_unconfirmed_txs_v1(rct::pkGen(), + rct::pkGen(), + SpTxSupplementV1{}, + std::move(mock_enotes)); + + + /// finish making the proof prep + return gen_mock_sp_membership_proof_prep_for_enote_at_pos_v1(real_reference_enote, + real_reference_index_in_ledger, + address_mask, + commitment_mask, + ref_set_decomp_n, + ref_set_decomp_m, + bin_config, + ledger_context_inout); +} +//------------------------------------------------------------------------------------------------------------------- +std::vector gen_mock_sp_membership_proof_preps_v1( + const std::vector &real_referenced_enotes, + const std::vector &address_masks, + const std::vector &commitment_masks, + const std::size_t ref_set_decomp_n, + const std::size_t ref_set_decomp_m, + const SpBinnedReferenceSetConfigV1 &bin_config, + MockLedgerContext &ledger_context_inout) +{ + // make mock membership ref sets from input enotes + CHECK_AND_ASSERT_THROW_MES(real_referenced_enotes.size() == address_masks.size(), + "gen mock membership proof preps: input enotes don't line up with address masks."); + CHECK_AND_ASSERT_THROW_MES(real_referenced_enotes.size() == commitment_masks.size(), + "gen mock membership proof preps: input enotes don't line up with commitment masks."); + + std::vector proof_preps; + proof_preps.reserve(real_referenced_enotes.size()); + + for (std::size_t input_index{0}; input_index < real_referenced_enotes.size(); ++input_index) + { + proof_preps.emplace_back( + gen_mock_sp_membership_proof_prep_v1(real_referenced_enotes[input_index], + address_masks[input_index], + commitment_masks[input_index], + ref_set_decomp_n, + ref_set_decomp_m, + bin_config, + ledger_context_inout) + ); + } + + return proof_preps; +} +//------------------------------------------------------------------------------------------------------------------- +std::vector gen_mock_sp_membership_proof_preps_v1( + const std::vector &input_proposals, + const std::size_t ref_set_decomp_n, + const std::size_t ref_set_decomp_m, + const SpBinnedReferenceSetConfigV1 &bin_config, + MockLedgerContext &ledger_context_inout) +{ + // make mock membership ref sets from input proposals + std::vector input_enotes; + std::vector address_masks; + std::vector commitment_masks; + input_enotes.reserve(input_proposals.size()); + address_masks.reserve(input_proposals.size()); + commitment_masks.reserve(input_proposals.size()); + + for (const SpInputProposalV1 &input_proposal : input_proposals) + { + input_enotes.emplace_back(input_proposal.core.enote_core); + address_masks.emplace_back(input_proposal.core.address_mask); + commitment_masks.emplace_back(input_proposal.core.commitment_mask); + } + + return gen_mock_sp_membership_proof_preps_v1(input_enotes, + address_masks, + commitment_masks, + ref_set_decomp_n, + ref_set_decomp_m, + bin_config, + ledger_context_inout); +} +//------------------------------------------------------------------------------------------------------------------- +void make_mock_sp_membership_proof_preps_for_inputs_v1( + const std::unordered_map &input_ledger_mappings, + const std::vector &input_proposals, + const std::size_t ref_set_decomp_n, + const std::size_t ref_set_decomp_m, + const SpBinnedReferenceSetConfigV1 &bin_config, + const MockLedgerContext &ledger_context, + std::vector &membership_proof_preps_out) +{ + CHECK_AND_ASSERT_THROW_MES(input_ledger_mappings.size() == input_proposals.size(), + "make mock membership proof preps: input proposals don't line up with their enotes' ledger indices."); + + membership_proof_preps_out.clear(); + membership_proof_preps_out.reserve(input_proposals.size()); + + for (const SpInputProposalV1 &input_proposal : input_proposals) + { + CHECK_AND_ASSERT_THROW_MES( + input_ledger_mappings.find(key_image_ref(input_proposal)) != input_ledger_mappings.end(), + "make mock membership proof preps: the enote ledger indices map is missing an expected key image."); + + membership_proof_preps_out.emplace_back( + gen_mock_sp_membership_proof_prep_for_enote_at_pos_v1(input_proposal.core.enote_core, + input_ledger_mappings.at(key_image_ref(input_proposal)), + input_proposal.core.address_mask, + input_proposal.core.commitment_mask, + ref_set_decomp_n, + ref_set_decomp_m, + bin_config, + ledger_context) + ); + } +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace mocks +} //namespace sp diff --git a/src/seraphis_mocks/mock_tx_builders_inputs.h b/src/seraphis_mocks/mock_tx_builders_inputs.h new file mode 100644 index 0000000000..6daa11f769 --- /dev/null +++ b/src/seraphis_mocks/mock_tx_builders_inputs.h @@ -0,0 +1,126 @@ +// Copyright (c) 2022, 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. + +// NOT FOR PRODUCTION + +// Seraphis tx-builder/component-builder mockups (tx inputs). + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "mock_ledger_context.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/binned_reference_set.h" +#include "seraphis_main/tx_builder_types.h" + +//third party headers + +//standard headers +#include +#include + +//forward declarations + + +namespace sp +{ +namespace mocks +{ + +/** +* brief: gen_mock_sp_input_proposals_v1 - create random mock inputs +* param: sp_spend_privkey - +* param: k_view_balance - +* param: in_amounts - +* return: set of transaction inputs ready to spend +*/ +std::vector gen_mock_sp_input_proposals_v1(const crypto::secret_key &sp_spend_privkey, + const crypto::secret_key &k_view_balance, + const std::vector &in_amounts); +/** +* brief: gen_mock_sp_membership_proof_prep_v1 - create a random reference set for an enote, with real spend at a +* random index, and update mock ledger to include all members of the reference set (including squashed enotes) +* param: input_enote - +* param: ref_set_decomp_n - +* param: ref_set_decomp_m - +* inoutparam: ledger_context_inout - +* return: a reference set that can be used to make a membership proof +*/ +SpMembershipProofPrepV1 gen_mock_sp_membership_proof_prep_for_enote_at_pos_v1( + const SpEnoteCoreVariant &real_reference_enote, + const std::uint64_t &real_reference_index_in_ledger, + const crypto::secret_key &address_mask, + const crypto::secret_key &commitment_mask, + const std::size_t ref_set_decomp_n, + const std::size_t ref_set_decomp_m, + const SpBinnedReferenceSetConfigV1 &bin_config, + const MockLedgerContext &ledger_context); +SpMembershipProofPrepV1 gen_mock_sp_membership_proof_prep_v1( + const SpEnoteCoreVariant &real_reference_enote, + const crypto::secret_key &address_mask, + const crypto::secret_key &commitment_mask, + const std::size_t ref_set_decomp_n, + const std::size_t ref_set_decomp_m, + const SpBinnedReferenceSetConfigV1 &bin_config, + MockLedgerContext &ledger_context_inout); +std::vector gen_mock_sp_membership_proof_preps_v1( + const std::vector &real_referenced_enotes, + const std::vector &address_masks, + const std::vector &commitment_masks, + const std::size_t ref_set_decomp_n, + const std::size_t ref_set_decomp_m, + const SpBinnedReferenceSetConfigV1 &bin_config, + MockLedgerContext &ledger_context_inout); +std::vector gen_mock_sp_membership_proof_preps_v1( + const std::vector &input_proposals, + const std::size_t ref_set_decomp_n, + const std::size_t ref_set_decomp_m, + const SpBinnedReferenceSetConfigV1 &bin_config, + MockLedgerContext &ledger_context_inout); +/** +* brief: make_mock_sp_membership_proof_preps_for_inputs_v1 - prepare membership proofs for enotes in a mock ledger +* param: input_ledger_mappings - +* param: input_proposals - +* param: ref_set_decomp_n - +* param: ref_set_decomp_m - +* param: bin_config - +* param: ledger_context - +* outparam: membership_proof_preps_out - +*/ +void make_mock_sp_membership_proof_preps_for_inputs_v1( + const std::unordered_map &input_ledger_mappings, + const std::vector &input_proposals, + const std::size_t ref_set_decomp_n, + const std::size_t ref_set_decomp_m, + const SpBinnedReferenceSetConfigV1 &bin_config, + const MockLedgerContext &ledger_context, + std::vector &membership_proof_preps_out); + +} //namespace mocks +} //namespace sp diff --git a/src/seraphis_mocks/mock_tx_builders_legacy_inputs.cpp b/src/seraphis_mocks/mock_tx_builders_legacy_inputs.cpp new file mode 100644 index 0000000000..f83ab18001 --- /dev/null +++ b/src/seraphis_mocks/mock_tx_builders_legacy_inputs.cpp @@ -0,0 +1,324 @@ +// Copyright (c) 2022, 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. + +// NOT FOR PRODUCTION + +//paired header +#include "mock_tx_builders_legacy_inputs.h" + +//local headers +#include "common/container_helpers.h" +#include "crypto/crypto.h" +#include "misc_log_ex.h" +#include "mock_ledger_context.h" +#include "ringct/rctOps.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/legacy_decoy_selector_flat.h" +#include "seraphis_core/legacy_enote_types.h" +#include "seraphis_main/contextual_enote_record_types.h" +#include "seraphis_main/contextual_enote_record_utils.h" +#include "seraphis_main/tx_builder_types_legacy.h" +#include "seraphis_main/tx_builder_types_multisig.h" +#include "seraphis_main/tx_component_types_legacy.h" + +//third party headers + +//standard headers +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis_mocks" + +namespace sp +{ +namespace mocks +{ +//------------------------------------------------------------------------------------------------------------------- +std::vector gen_mock_legacy_input_proposals_v1(const crypto::secret_key &legacy_spend_privkey, + const std::vector &input_amounts) +{ + // generate random inputs + std::vector input_proposals; + input_proposals.reserve(input_amounts.size()); + + for (const rct::xmr_amount in_amount : input_amounts) + tools::add_element(input_proposals) = gen_legacy_input_proposal_v1(legacy_spend_privkey, in_amount); + + return input_proposals; +} +//------------------------------------------------------------------------------------------------------------------- +void gen_mock_legacy_ring_signature_members_for_enote_at_pos_v1(const std::uint64_t real_reference_index_in_ledger, + const std::uint64_t ring_size, + const MockLedgerContext &ledger_context, + std::vector &reference_set_out, + rct::ctkeyV &referenced_enotes_out, + std::uint64_t &real_reference_index_out) +{ + // generate ring members for a mock legacy ring signature for a legacy enote at a known position in the mock ledger + + /// make reference set + LegacyRingSignaturePrepV1 proof_prep; + + // 1. flat decoy selector for mock-up + const LegacyDecoySelectorFlat decoy_selector{0, ledger_context.max_legacy_enote_index()}; + + // 2. reference set + CHECK_AND_ASSERT_THROW_MES(ring_size > 0, + "gen mock legacy ring signature members (for enote at pos): ring size of 0 is not allowed."); + + decoy_selector.get_ring_members(real_reference_index_in_ledger, + ring_size, + reference_set_out, + real_reference_index_out); + + CHECK_AND_ASSERT_THROW_MES(real_reference_index_out < reference_set_out.size(), + "gen mock legacy ring signature members (for enote at pos): real reference index is outside of reference set."); + + + /// copy all referenced legacy enotes from the ledger + ledger_context.get_reference_set_proof_elements_v1(reference_set_out, referenced_enotes_out); + + CHECK_AND_ASSERT_THROW_MES(reference_set_out.size() == referenced_enotes_out.size(), + "gen mock legacy ring signature members (for enote at pos): reference set doesn't line up with reference " + "enotes."); +} +//------------------------------------------------------------------------------------------------------------------- +LegacyRingSignaturePrepV1 gen_mock_legacy_ring_signature_prep_for_enote_at_pos_v1(const rct::key &tx_proposal_prefix, + const std::uint64_t real_reference_index_in_ledger, + const LegacyEnoteImageV2 &real_reference_image, + const crypto::secret_key &real_reference_view_privkey, + const crypto::secret_key &commitment_mask, + const std::uint64_t ring_size, + const MockLedgerContext &ledger_context) +{ + // generate a mock ring signature prep for a legacy enote at a known position in the mock ledger + LegacyRingSignaturePrepV1 proof_prep; + + // 1. generate ring members + gen_mock_legacy_ring_signature_members_for_enote_at_pos_v1(real_reference_index_in_ledger, + ring_size, + ledger_context, + proof_prep.reference_set, + proof_prep.referenced_enotes, + proof_prep.real_reference_index); + + // 2. copy misc pieces + proof_prep.tx_proposal_prefix = tx_proposal_prefix; + proof_prep.reference_image = real_reference_image; + proof_prep.reference_view_privkey = real_reference_view_privkey; + proof_prep.reference_commitment_mask = commitment_mask; + + return proof_prep; +} +//------------------------------------------------------------------------------------------------------------------- +LegacyRingSignaturePrepV1 gen_mock_legacy_ring_signature_prep_v1(const rct::key &tx_proposal_prefix, + const rct::ctkey &real_reference_enote, + const LegacyEnoteImageV2 &real_reference_image, + const crypto::secret_key &real_reference_view_privkey, + const crypto::secret_key &commitment_mask, + const std::uint64_t ring_size, + MockLedgerContext &ledger_context_inout) +{ + // generate a mock ring signature prep + + /// add fake enotes to the ledger (2x the ring size), with the real one at a random location + + // 1. make fake legacy enotes + const std::size_t num_enotes_to_add{ring_size * 2}; + const std::size_t add_real_at_pos{crypto::rand_idx(num_enotes_to_add)}; + std::vector mock_enotes; + mock_enotes.reserve(num_enotes_to_add); + + for (std::size_t enote_to_add{0}; enote_to_add < num_enotes_to_add; ++enote_to_add) + { + LegacyEnoteV5 temp{gen_legacy_enote_v5()}; + + if (enote_to_add == add_real_at_pos) + { + temp.onetime_address = real_reference_enote.dest; + temp.amount_commitment = real_reference_enote.mask; + } + + mock_enotes.emplace_back(temp); + } + + // 2. add mock legacy enotes as the outputs of a mock legacy coinbase tx + const std::uint64_t real_reference_index_in_ledger{ + ledger_context_inout.max_legacy_enote_index() + add_real_at_pos + 1 + }; + ledger_context_inout.add_legacy_coinbase(rct::pkGen(), 0, TxExtra{}, {}, std::move(mock_enotes)); + + + /// finish making the proof prep + return gen_mock_legacy_ring_signature_prep_for_enote_at_pos_v1(tx_proposal_prefix, + real_reference_index_in_ledger, + real_reference_image, + real_reference_view_privkey, + commitment_mask, + ring_size, + ledger_context_inout); +} +//------------------------------------------------------------------------------------------------------------------- +std::vector gen_mock_legacy_ring_signature_preps_v1(const rct::key &tx_proposal_prefix, + const rct::ctkeyV &real_referenced_enotes, + const std::vector &real_reference_images, + const std::vector &real_reference_view_privkeys, + const std::vector &commitment_masks, + const std::uint64_t ring_size, + MockLedgerContext &ledger_context_inout) +{ + // make mock legacy ring signatures from input enotes + CHECK_AND_ASSERT_THROW_MES(real_referenced_enotes.size() == real_reference_images.size(), + "gen mock legacy ring signature preps: input enotes don't line up with input images."); + CHECK_AND_ASSERT_THROW_MES(real_referenced_enotes.size() == real_reference_view_privkeys.size(), + "gen mock legacy ring signature preps: input enotes don't line up with input enote view privkeys."); + CHECK_AND_ASSERT_THROW_MES(real_referenced_enotes.size() == commitment_masks.size(), + "gen mock legacy ring signature preps: input enotes don't line up with commitment masks."); + + std::vector proof_preps; + proof_preps.reserve(real_referenced_enotes.size()); + + for (std::size_t input_index{0}; input_index < real_referenced_enotes.size(); ++input_index) + { + proof_preps.emplace_back( + gen_mock_legacy_ring_signature_prep_v1(tx_proposal_prefix, + real_referenced_enotes[input_index], + real_reference_images[input_index], + real_reference_view_privkeys[input_index], + commitment_masks[input_index], + ring_size, + ledger_context_inout) + ); + } + + return proof_preps; +} +//------------------------------------------------------------------------------------------------------------------- +std::vector gen_mock_legacy_ring_signature_preps_v1(const rct::key &tx_proposal_prefix, + const std::vector &input_proposals, + const std::uint64_t ring_size, + MockLedgerContext &ledger_context_inout) +{ + // make mock legacy ring signatures from input proposals + rct::ctkeyV input_enotes; + std::vector input_images; + std::vector input_enote_view_extensions; + std::vector commitment_masks; + input_enotes.reserve(input_proposals.size()); + input_images.reserve(input_proposals.size()); + input_enote_view_extensions.reserve(input_proposals.size()); + commitment_masks.reserve(input_proposals.size()); + + for (const LegacyInputProposalV1 &input_proposal : input_proposals) + { + input_enotes.emplace_back( + rct::ctkey{ .dest = input_proposal.onetime_address, .mask = input_proposal.amount_commitment} + ); + input_images.emplace_back(); + + input_images.back().key_image = input_proposal.key_image; + mask_key(input_proposal.commitment_mask, + input_proposal.amount_commitment, + input_images.back().masked_commitment); + + input_enote_view_extensions.emplace_back(input_proposal.enote_view_extension); + commitment_masks.emplace_back(input_proposal.commitment_mask); + } + + return gen_mock_legacy_ring_signature_preps_v1(tx_proposal_prefix, + input_enotes, + input_images, + input_enote_view_extensions, + commitment_masks, + ring_size, + ledger_context_inout); +} +//------------------------------------------------------------------------------------------------------------------- +void make_mock_legacy_ring_signature_preps_for_inputs_v1(const rct::key &tx_proposal_prefix, + const std::unordered_map &input_ledger_mappings, + const std::vector &input_proposals, + const std::uint64_t ring_size, + const MockLedgerContext &ledger_context, + std::vector &ring_signature_preps_out) +{ + CHECK_AND_ASSERT_THROW_MES(input_ledger_mappings.size() == input_proposals.size(), + "make mock legacy ring signature preps: input proposals don't line up with their enotes' ledger indices."); + + ring_signature_preps_out.clear(); + ring_signature_preps_out.reserve(input_proposals.size()); + + for (const LegacyInputProposalV1 &input_proposal : input_proposals) + { + CHECK_AND_ASSERT_THROW_MES(input_ledger_mappings.find(input_proposal.key_image) != input_ledger_mappings.end(), + "make mock legacy ring signature preps: the enote ledger indices map is missing an expected key image."); + + rct::key masked_commitment; + mask_key(input_proposal.commitment_mask, input_proposal.amount_commitment, masked_commitment); + + ring_signature_preps_out.emplace_back( + gen_mock_legacy_ring_signature_prep_for_enote_at_pos_v1(tx_proposal_prefix, + input_ledger_mappings.at(input_proposal.key_image), + LegacyEnoteImageV2{masked_commitment, input_proposal.key_image}, + input_proposal.enote_view_extension, + input_proposal.commitment_mask, + ring_size, + ledger_context) + ); + } +} +//------------------------------------------------------------------------------------------------------------------- +bool try_gen_legacy_multisig_ring_signature_preps_v1(const std::vector &contextual_records, + const std::uint64_t legacy_ring_size, + const MockLedgerContext &ledger_context, + std::unordered_map &mapped_preps_out) +{ + // 1. extract map [ legacy KI : enote ledger index ] from contextual records + std::unordered_map enote_ledger_mappings; + + if (!try_get_membership_proof_real_reference_mappings(contextual_records, enote_ledger_mappings)) + return false; + + // 2. generate legacy multisig ring signature preps for each legacy enote requested + for (const auto &enote_ledger_mapping : enote_ledger_mappings) + { + LegacyMultisigRingSignaturePrepV1 &prep = mapped_preps_out[enote_ledger_mapping.first]; + prep.key_image = enote_ledger_mapping.first; + + gen_mock_legacy_ring_signature_members_for_enote_at_pos_v1(enote_ledger_mapping.second, + legacy_ring_size, + ledger_context, + prep.reference_set, + prep.referenced_enotes, + prep.real_reference_index); + } + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace mocks +} //namespace sp diff --git a/src/seraphis_mocks/mock_tx_builders_legacy_inputs.h b/src/seraphis_mocks/mock_tx_builders_legacy_inputs.h new file mode 100644 index 0000000000..b9cd921914 --- /dev/null +++ b/src/seraphis_mocks/mock_tx_builders_legacy_inputs.h @@ -0,0 +1,104 @@ +// Copyright (c) 2022, 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. + +// NOT FOR PRODUCTION + +// Seraphis tx-builder/component-builder mockups (legacy tx inputs). + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "mock_ledger_context.h" +#include "ringct/rctTypes.h" +#include "seraphis_main/contextual_enote_record_types.h" +#include "seraphis_main/tx_builder_types_legacy.h" +#include "seraphis_main/tx_builder_types_multisig.h" + +//third party headers + +//standard headers +#include + +//forward declarations + + +namespace sp +{ +namespace mocks +{ + +/// create random mock inputs +std::vector gen_mock_legacy_input_proposals_v1(const crypto::secret_key &legacy_spend_privkey, + const std::vector &input_amounts); +/// make mock legacy ring signature preps +void gen_mock_legacy_ring_signature_members_for_enote_at_pos_v1(const std::uint64_t real_reference_index_in_ledger, + const std::uint64_t ring_size, + const MockLedgerContext &ledger_context, + std::vector &reference_set_out, + rct::ctkeyV &referenced_enotes_out, + std::uint64_t &real_reference_index_out); +LegacyRingSignaturePrepV1 gen_mock_legacy_ring_signature_prep_for_enote_at_pos_v1(const rct::key &tx_proposal_prefix, + const std::uint64_t real_reference_index_in_ledger, + const LegacyEnoteImageV2 &real_reference_image, + const crypto::secret_key &real_reference_view_privkey, + const crypto::secret_key &commitment_mask, + const std::uint64_t ring_size, + const MockLedgerContext &ledger_context); +LegacyRingSignaturePrepV1 gen_mock_legacy_ring_signature_prep_v1(const rct::key &tx_proposal_prefix, + const rct::ctkey &real_reference_enote, + const LegacyEnoteImageV2 &real_reference_image, + const crypto::secret_key &real_reference_view_privkey, + const crypto::secret_key &commitment_mask, + const std::uint64_t ring_size, + MockLedgerContext &ledger_context_inout); +std::vector gen_mock_legacy_ring_signature_preps_v1(const rct::key &tx_proposal_prefix, + const rct::ctkeyV &real_referenced_enotes, + const std::vector &real_reference_images, + const std::vector &real_reference_view_privkeys, + const std::vector &commitment_masks, + const std::uint64_t ring_size, + MockLedgerContext &ledger_context_inout); +std::vector gen_mock_legacy_ring_signature_preps_v1(const rct::key &tx_proposal_prefix, + const std::vector &input_proposals, + const std::uint64_t ring_size, + MockLedgerContext &ledger_context_inout); +/// prepare membership proofs for enotes in a mock ledger +void make_mock_legacy_ring_signature_preps_for_inputs_v1(const rct::key &tx_proposal_prefix, + const std::unordered_map &input_ledger_mappings, + const std::vector &input_proposals, + const std::uint64_t ring_size, + const MockLedgerContext &ledger_context, + std::vector &ring_signature_preps_out); +bool try_gen_legacy_multisig_ring_signature_preps_v1(const std::vector &contextual_records, + const std::uint64_t legacy_ring_size, + const MockLedgerContext &ledger_context, + std::unordered_map &mapped_preps_out); + +} //namespace mocks +} //namespace sp diff --git a/src/seraphis_mocks/mock_tx_builders_outputs.cpp b/src/seraphis_mocks/mock_tx_builders_outputs.cpp new file mode 100644 index 0000000000..1654fadf83 --- /dev/null +++ b/src/seraphis_mocks/mock_tx_builders_outputs.cpp @@ -0,0 +1,89 @@ +// Copyright (c) 2022, 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. + +// NOT FOR PRODUCTION + +//paired header +#include "mock_tx_builders_outputs.h" + +//local headers +#include "common/container_helpers.h" +#include "ringct/rctTypes.h" +#include "seraphis_main/tx_builder_types.h" + +//third party headers + +//standard headers +#include +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis_mocks" + +namespace sp +{ +namespace mocks +{ +//------------------------------------------------------------------------------------------------------------------- +std::vector gen_mock_sp_coinbase_output_proposals_v1( + const std::vector &out_amounts, + const std::size_t num_random_memo_elements) +{ + // 1. generate random output proposals + std::vector output_proposals; + output_proposals.reserve(out_amounts.size()); + + for (const rct::xmr_amount out_amount : out_amounts) + output_proposals.emplace_back(gen_sp_coinbase_output_proposal_v1(out_amount, num_random_memo_elements)); + + // 2. sort them + std::sort(output_proposals.begin(), + output_proposals.end(), + tools::compare_func(compare_Ko)); + + return output_proposals; +} +//------------------------------------------------------------------------------------------------------------------- +std::vector gen_mock_sp_output_proposals_v1(const std::vector &out_amounts, + const std::size_t num_random_memo_elements) +{ + // 1. generate random output proposals + std::vector output_proposals; + output_proposals.reserve(out_amounts.size()); + + for (const rct::xmr_amount out_amount : out_amounts) + output_proposals.emplace_back(gen_sp_output_proposal_v1(out_amount, num_random_memo_elements)); + + // 2. sort them + std::sort(output_proposals.begin(), output_proposals.end(), tools::compare_func(compare_Ko)); + + return output_proposals; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace mocks +} //namespace sp diff --git a/src/seraphis_mocks/mock_tx_builders_outputs.h b/src/seraphis_mocks/mock_tx_builders_outputs.h new file mode 100644 index 0000000000..6c64c0f6de --- /dev/null +++ b/src/seraphis_mocks/mock_tx_builders_outputs.h @@ -0,0 +1,71 @@ +// Copyright (c) 2022, 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. + +// NOT FOR PRODUCTION + +// Seraphis tx-builder/component-builder mockups (tx outputs). + +#pragma once + +//local headers +#include "ringct/rctTypes.h" +#include "seraphis_main/tx_builder_types.h" + +//third party headers + +//standard headers +#include + +//forward declarations + + +namespace sp +{ +namespace mocks +{ + +/** +* brief: gen_mock_sp_coinbase_output_proposals_v1 - create random coinbase output proposals +* param: out_amounts - +* param: num_random_memo_elements - +* return: set of generated output proposals +*/ +std::vector gen_mock_sp_coinbase_output_proposals_v1( + const std::vector &out_amounts, + const std::size_t num_random_memo_elements); +/** +* brief: gen_mock_sp_output_proposals_v1 - create random output proposals +* param: out_amounts - +* param: num_random_memo_elements - +* return: set of generated output proposals +*/ +std::vector gen_mock_sp_output_proposals_v1(const std::vector &out_amounts, + const std::size_t num_random_memo_elements); + +} //namespace mocks +} //namespace sp diff --git a/src/seraphis_mocks/scan_chunk_consumer_mocks.cpp b/src/seraphis_mocks/scan_chunk_consumer_mocks.cpp new file mode 100644 index 0000000000..e164c95ec1 --- /dev/null +++ b/src/seraphis_mocks/scan_chunk_consumer_mocks.cpp @@ -0,0 +1,471 @@ +// Copyright (c) 2022, 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. + +// NOT FOR PRODUCTION + +//paired header +#include "scan_chunk_consumer_mocks.h" + +//local headers +#include "crypto/crypto.h" +#include "crypto/x25519.h" +#include "enote_finding_context_mocks.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/jamtis_core_utils.h" +#include "seraphis_impl/enote_store.h" +#include "seraphis_impl/enote_store_utils.h" +#include "seraphis_main/enote_record_types.h" +#include "seraphis_main/scan_balance_recovery_utils.h" +#include "seraphis_main/scan_core_types.h" +#include "seraphis_main/scan_machine_types.h" + +//third party headers + +//standard headers +#include +#include +#include +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis_mocks" + +namespace sp +{ +namespace mocks +{ +//------------------------------------------------------------------------------------------------------------------- +// Legacy Intermediate +//------------------------------------------------------------------------------------------------------------------- +ChunkConsumerMockLegacyIntermediate::ChunkConsumerMockLegacyIntermediate( + const rct::key &legacy_base_spend_pubkey, + const crypto::secret_key &legacy_view_privkey, + const LegacyScanMode legacy_scan_mode, + SpEnoteStore &enote_store) : + m_legacy_base_spend_pubkey{legacy_base_spend_pubkey}, + m_legacy_view_privkey{legacy_view_privkey}, + m_legacy_scan_mode{legacy_scan_mode}, + m_enote_store{enote_store} +{} +//------------------------------------------------------------------------------------------------------------------- +std::uint64_t ChunkConsumerMockLegacyIntermediate::refresh_index() const +{ + return m_enote_store.legacy_refresh_index(); +} +//------------------------------------------------------------------------------------------------------------------- +std::uint64_t ChunkConsumerMockLegacyIntermediate::desired_first_block() const +{ + if (m_legacy_scan_mode == LegacyScanMode::KEY_IMAGES_ONLY) + return m_enote_store.top_legacy_fullscanned_block_index() + 1; + else + return m_enote_store.top_legacy_partialscanned_block_index() + 1; +} +//------------------------------------------------------------------------------------------------------------------- +scanning::ContiguityMarker ChunkConsumerMockLegacyIntermediate::get_next_block(const std::uint64_t block_index) const +{ + if (m_legacy_scan_mode == LegacyScanMode::KEY_IMAGES_ONLY) + return get_next_legacy_fullscanned_block(m_enote_store, block_index); + else + return get_next_legacy_partialscanned_block(m_enote_store, block_index); +} +//------------------------------------------------------------------------------------------------------------------- +scanning::ContiguityMarker ChunkConsumerMockLegacyIntermediate::get_nearest_block(const std::uint64_t block_index) const +{ + if (m_legacy_scan_mode == LegacyScanMode::KEY_IMAGES_ONLY) + return get_nearest_legacy_fullscanned_block(m_enote_store, block_index); + else + return get_nearest_legacy_partialscanned_block(m_enote_store, block_index); +} +//------------------------------------------------------------------------------------------------------------------- +void ChunkConsumerMockLegacyIntermediate::consume_nonledger_chunk(const SpEnoteOriginStatus nonledger_origin_status, + const scanning::ChunkData &chunk_data) +{ + // 1. process the chunk + std::unordered_map found_enote_records; + std::unordered_map found_spent_key_images; + + scanning::process_chunk_intermediate_legacy(m_legacy_base_spend_pubkey, + m_legacy_view_privkey, + [this](const crypto::key_image &key_image) -> bool + { + return this->m_enote_store.has_enote_with_key_image(key_image); + }, + chunk_data.basic_records_per_tx, + chunk_data.contextual_key_images, + hw::get_device("default"), + found_enote_records, + found_spent_key_images); + + // 2. save the results + std::list events; + if (m_legacy_scan_mode == LegacyScanMode::KEY_IMAGES_ONLY) + m_enote_store.update_with_intermediate_legacy_found_spent_key_images(found_spent_key_images, events); + else + { + m_enote_store.update_with_intermediate_legacy_records_from_nonledger(nonledger_origin_status, + found_enote_records, + found_spent_key_images, + events); + } +} +//------------------------------------------------------------------------------------------------------------------- +void ChunkConsumerMockLegacyIntermediate::consume_onchain_chunk(const scanning::LedgerChunk &chunk, + const rct::key &alignment_block_id, + const std::uint64_t first_new_block, + const std::vector &new_block_ids) +{ + // 1. extract the data + const scanning::ChunkData *chunk_data{chunk.try_get_data(rct::zero())}; + CHECK_AND_ASSERT_THROW_MES(chunk_data, "chunk consumer mock legacy intermediate: no chunk data."); + + // 2. process the chunk + std::unordered_map found_enote_records; + std::unordered_map found_spent_key_images; + + scanning::process_chunk_intermediate_legacy(m_legacy_base_spend_pubkey, + m_legacy_view_privkey, + [this](const crypto::key_image &key_image) -> bool + { + return this->m_enote_store.has_enote_with_key_image(key_image); + }, + chunk_data->basic_records_per_tx, + chunk_data->contextual_key_images, + hw::get_device("default"), + found_enote_records, + found_spent_key_images); + + // 3. save the results + std::list events; + if (m_legacy_scan_mode == LegacyScanMode::KEY_IMAGES_ONLY) + m_enote_store.update_with_intermediate_legacy_found_spent_key_images(found_spent_key_images, events); + else + { + m_enote_store.update_with_intermediate_legacy_records_from_ledger(alignment_block_id, + first_new_block, + new_block_ids, + found_enote_records, + found_spent_key_images, + events); + } +} +//------------------------------------------------------------------------------------------------------------------- +// Legacy +//------------------------------------------------------------------------------------------------------------------- +ChunkConsumerMockLegacy::ChunkConsumerMockLegacy(const rct::key &legacy_base_spend_pubkey, + const crypto::secret_key &legacy_spend_privkey, + const crypto::secret_key &legacy_view_privkey, + SpEnoteStore &enote_store) : + m_legacy_base_spend_pubkey{legacy_base_spend_pubkey}, + m_legacy_spend_privkey{legacy_spend_privkey}, + m_legacy_view_privkey{legacy_view_privkey}, + m_enote_store{enote_store} +{} +//------------------------------------------------------------------------------------------------------------------- +std::uint64_t ChunkConsumerMockLegacy::refresh_index() const +{ + return m_enote_store.legacy_refresh_index(); +} +//------------------------------------------------------------------------------------------------------------------- +std::uint64_t ChunkConsumerMockLegacy::desired_first_block() const +{ + return m_enote_store.top_legacy_fullscanned_block_index() + 1; +} +//------------------------------------------------------------------------------------------------------------------- +scanning::ContiguityMarker ChunkConsumerMockLegacy::get_next_block(const std::uint64_t block_index) const +{ + return get_next_legacy_fullscanned_block(m_enote_store, block_index); +} +//------------------------------------------------------------------------------------------------------------------- +scanning::ContiguityMarker ChunkConsumerMockLegacy::get_nearest_block(const std::uint64_t block_index) const +{ + return get_nearest_legacy_fullscanned_block(m_enote_store, block_index); +} +//------------------------------------------------------------------------------------------------------------------- +void ChunkConsumerMockLegacy::consume_nonledger_chunk(const SpEnoteOriginStatus nonledger_origin_status, + const scanning::ChunkData &chunk_data) +{ + // 1. process the chunk + std::unordered_map found_enote_records; + std::unordered_map found_spent_key_images; + + scanning::process_chunk_full_legacy(m_legacy_base_spend_pubkey, + m_legacy_spend_privkey, + m_legacy_view_privkey, + [this](const crypto::key_image &key_image) -> bool + { + return this->m_enote_store.has_enote_with_key_image(key_image); + }, + chunk_data.basic_records_per_tx, + chunk_data.contextual_key_images, + hw::get_device("default"), + found_enote_records, + found_spent_key_images); + + // 2. save the results + std::list events; + m_enote_store.update_with_legacy_records_from_nonledger(nonledger_origin_status, + found_enote_records, + found_spent_key_images, + events); +} +//------------------------------------------------------------------------------------------------------------------- +void ChunkConsumerMockLegacy::consume_onchain_chunk(const scanning::LedgerChunk &chunk, + const rct::key &alignment_block_id, + const std::uint64_t first_new_block, + const std::vector &new_block_ids) +{ + // 1. extract the data + const scanning::ChunkData *chunk_data{chunk.try_get_data(rct::zero())}; + CHECK_AND_ASSERT_THROW_MES(chunk_data, "chunk consumer mock legacy: no chunk data."); + + // 2. process the chunk + std::unordered_map found_enote_records; + std::unordered_map found_spent_key_images; + + scanning::process_chunk_full_legacy(m_legacy_base_spend_pubkey, + m_legacy_spend_privkey, + m_legacy_view_privkey, + [this](const crypto::key_image &key_image) -> bool + { + return this->m_enote_store.has_enote_with_key_image(key_image); + }, + chunk_data->basic_records_per_tx, + chunk_data->contextual_key_images, + hw::get_device("default"), + found_enote_records, + found_spent_key_images); + + // 3. save the results + std::list events; + m_enote_store.update_with_legacy_records_from_ledger(alignment_block_id, + first_new_block, + new_block_ids, + found_enote_records, + found_spent_key_images, + events); +} +//------------------------------------------------------------------------------------------------------------------- +// Seraphis Intermediate +//------------------------------------------------------------------------------------------------------------------- +ChunkConsumerMockSpIntermediate::ChunkConsumerMockSpIntermediate(const rct::key &jamtis_spend_pubkey, + const crypto::x25519_secret_key &xk_unlock_amounts, + const crypto::x25519_secret_key &xk_find_received, + const crypto::secret_key &s_generate_address, + SpEnoteStorePaymentValidator &enote_store) : + m_jamtis_spend_pubkey{jamtis_spend_pubkey}, + m_xk_unlock_amounts{xk_unlock_amounts}, + m_xk_find_received{xk_find_received}, + m_s_generate_address{s_generate_address}, + m_enote_store{enote_store} +{ + jamtis::make_jamtis_ciphertag_secret(m_s_generate_address, m_s_cipher_tag); + + m_cipher_context = std::make_unique(m_s_cipher_tag); +} +//------------------------------------------------------------------------------------------------------------------- +std::uint64_t ChunkConsumerMockSpIntermediate::refresh_index() const +{ + return m_enote_store.refresh_index(); +} +//------------------------------------------------------------------------------------------------------------------- +std::uint64_t ChunkConsumerMockSpIntermediate::desired_first_block() const +{ + return m_enote_store.top_block_index() + 1; +} +//------------------------------------------------------------------------------------------------------------------- +scanning::ContiguityMarker ChunkConsumerMockSpIntermediate::get_next_block(const std::uint64_t block_index) const +{ + return get_next_sp_scanned_block(m_enote_store, block_index); +} +//------------------------------------------------------------------------------------------------------------------- +scanning::ContiguityMarker ChunkConsumerMockSpIntermediate::get_nearest_block(const std::uint64_t block_index) const +{ + return get_nearest_sp_scanned_block(m_enote_store, block_index); +} +//------------------------------------------------------------------------------------------------------------------- +void ChunkConsumerMockSpIntermediate::consume_nonledger_chunk(const SpEnoteOriginStatus nonledger_origin_status, + const scanning::ChunkData &chunk_data) +{ + // 1. process the chunk + std::unordered_map found_enote_records; + + scanning::process_chunk_intermediate_sp(m_jamtis_spend_pubkey, + m_xk_unlock_amounts, + m_xk_find_received, + m_s_generate_address, + *m_cipher_context, + chunk_data.basic_records_per_tx, + found_enote_records); + + // 2. save the results + std::list events; + m_enote_store.update_with_sp_records_from_nonledger(nonledger_origin_status, found_enote_records, events); +} +//------------------------------------------------------------------------------------------------------------------- +void ChunkConsumerMockSpIntermediate::consume_onchain_chunk(const scanning::LedgerChunk &chunk, + const rct::key &alignment_block_id, + const std::uint64_t first_new_block, + const std::vector &new_block_ids) +{ + // 1. extract the data + const scanning::ChunkData *chunk_data{chunk.try_get_data(rct::zero())}; + CHECK_AND_ASSERT_THROW_MES(chunk_data, "chunk consumer mock sp intermediate: no chunk data."); + + // 2. process the chunk + std::unordered_map found_enote_records; + + scanning::process_chunk_intermediate_sp(m_jamtis_spend_pubkey, + m_xk_unlock_amounts, + m_xk_find_received, + m_s_generate_address, + *m_cipher_context, + chunk_data->basic_records_per_tx, + found_enote_records); + + // 3. save the results + std::list events; + m_enote_store.update_with_sp_records_from_ledger(alignment_block_id, + first_new_block, + new_block_ids, + found_enote_records, + events); +} +//------------------------------------------------------------------------------------------------------------------- +// Seraphis +//------------------------------------------------------------------------------------------------------------------- +ChunkConsumerMockSp::ChunkConsumerMockSp(const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + SpEnoteStore &enote_store) : + m_jamtis_spend_pubkey{jamtis_spend_pubkey}, + m_k_view_balance{k_view_balance}, + m_enote_store{enote_store} +{ + jamtis::make_jamtis_unlockamounts_key(m_k_view_balance, m_xk_unlock_amounts); + jamtis::make_jamtis_findreceived_key(m_k_view_balance, m_xk_find_received); + jamtis::make_jamtis_generateaddress_secret(m_k_view_balance, m_s_generate_address); + jamtis::make_jamtis_ciphertag_secret(m_s_generate_address, m_s_cipher_tag); + + m_cipher_context = std::make_unique(m_s_cipher_tag); +} +//------------------------------------------------------------------------------------------------------------------- +std::uint64_t ChunkConsumerMockSp::refresh_index() const +{ + return m_enote_store.sp_refresh_index(); +} +//------------------------------------------------------------------------------------------------------------------- +std::uint64_t ChunkConsumerMockSp::desired_first_block() const +{ + return m_enote_store.top_sp_scanned_block_index() + 1; +} +//------------------------------------------------------------------------------------------------------------------- +scanning::ContiguityMarker ChunkConsumerMockSp::get_next_block(const std::uint64_t block_index) const +{ + return get_next_sp_scanned_block(m_enote_store, block_index); +} +//------------------------------------------------------------------------------------------------------------------- +scanning::ContiguityMarker ChunkConsumerMockSp::get_nearest_block(const std::uint64_t block_index) const +{ + return get_nearest_sp_scanned_block(m_enote_store, block_index); +} +//------------------------------------------------------------------------------------------------------------------- +void ChunkConsumerMockSp::consume_nonledger_chunk(const SpEnoteOriginStatus nonledger_origin_status, + const scanning::ChunkData &chunk_data) +{ + // 1. process the chunk + std::unordered_map found_enote_records; + std::unordered_map found_spent_key_images; + std::unordered_map legacy_key_images_in_sp_selfsends; + + scanning::process_chunk_full_sp(m_jamtis_spend_pubkey, + m_k_view_balance, + m_xk_unlock_amounts, + m_xk_find_received, + m_s_generate_address, + *m_cipher_context, + [this](const crypto::key_image &key_image) -> bool + { + return this->m_enote_store.has_enote_with_key_image(key_image); + }, + chunk_data.basic_records_per_tx, + chunk_data.contextual_key_images, + found_enote_records, + found_spent_key_images, + legacy_key_images_in_sp_selfsends); + + // 2. save the results + std::list events; + m_enote_store.update_with_sp_records_from_nonledger(nonledger_origin_status, + found_enote_records, + found_spent_key_images, + legacy_key_images_in_sp_selfsends, + events); +} +//------------------------------------------------------------------------------------------------------------------- +void ChunkConsumerMockSp::consume_onchain_chunk(const scanning::LedgerChunk &chunk, + const rct::key &alignment_block_id, + const std::uint64_t first_new_block, + const std::vector &new_block_ids) +{ + // 1. extract the data + const scanning::ChunkData *chunk_data{chunk.try_get_data(rct::zero())}; + CHECK_AND_ASSERT_THROW_MES(chunk_data, "chunk consumer mock sp: no chunk data."); + + // 2. process the chunk + std::unordered_map found_enote_records; + std::unordered_map found_spent_key_images; + std::unordered_map legacy_key_images_in_sp_selfsends; + + scanning::process_chunk_full_sp(m_jamtis_spend_pubkey, + m_k_view_balance, + m_xk_unlock_amounts, + m_xk_find_received, + m_s_generate_address, + *m_cipher_context, + [this](const crypto::key_image &key_image) -> bool + { + return this->m_enote_store.has_enote_with_key_image(key_image); + }, + chunk_data->basic_records_per_tx, + chunk_data->contextual_key_images, + found_enote_records, + found_spent_key_images, + legacy_key_images_in_sp_selfsends); + + // 2. save the results + std::list events; + m_enote_store.update_with_sp_records_from_ledger(alignment_block_id, + first_new_block, + new_block_ids, + found_enote_records, + found_spent_key_images, + legacy_key_images_in_sp_selfsends, + events); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace mocks +} //namespace sp diff --git a/src/seraphis_mocks/scan_chunk_consumer_mocks.h b/src/seraphis_mocks/scan_chunk_consumer_mocks.h new file mode 100644 index 0000000000..0683e2da8e --- /dev/null +++ b/src/seraphis_mocks/scan_chunk_consumer_mocks.h @@ -0,0 +1,246 @@ +// Copyright (c) 2022, 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. + +// NOT FOR PRODUCTION + +// Chunk consumers for these enote scanning workflows: +// - legacy view-only (view-scan or key image collection) +// - legacy full-scan +// - seraphis payment validator scan +// - seraphis full-scan + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "crypto/x25519.h" +#include "enote_finding_context_mocks.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/jamtis_address_tag_utils.h" +#include "seraphis_impl/enote_store.h" +#include "seraphis_impl/enote_store_payment_validator.h" +#include "seraphis_main/enote_record_types.h" +#include "seraphis_main/scan_chunk_consumer.h" +#include "seraphis_main/scan_machine_types.h" + +//third party headers + +//standard headers +#include +#include +#include +#include +#include + +//forward declarations + + +namespace sp +{ +namespace mocks +{ + +class ChunkConsumerMockLegacyIntermediate final : public scanning::ChunkConsumer +{ +public: +//constructors + /// normal constructor + ChunkConsumerMockLegacyIntermediate(const rct::key &legacy_base_spend_pubkey, + const crypto::secret_key &legacy_view_privkey, + const LegacyScanMode legacy_scan_mode, + SpEnoteStore &enote_store); + +//overloaded operators + /// disable copy/move (this is a scoped manager [reference wrapper]) + ChunkConsumerMockLegacyIntermediate& operator=(ChunkConsumerMockLegacyIntermediate&&) = delete; + +//member functions + /// get index of first block the enote store cares about + std::uint64_t refresh_index() const override; + /// get index of first block the updater wants to have scanned + std::uint64_t desired_first_block() const override; + /// get a marker for the next block > the specified index + scanning::ContiguityMarker get_next_block(const std::uint64_t block_index) const override; + /// get a marker for the nearest block <= the specified index + scanning::ContiguityMarker get_nearest_block(const std::uint64_t block_index) const override; + + /// consume a chunk of basic enote records and save the results + void consume_nonledger_chunk(const SpEnoteOriginStatus nonledger_origin_status, + const scanning::ChunkData &chunk_data) override; + void consume_onchain_chunk(const scanning::LedgerChunk &chunk, + const rct::key &alignment_block_id, + const std::uint64_t first_new_block, + const std::vector &new_block_ids) override; + +//member variables +private: + /// If this is set to KEY_IMAGES_ONLY, then desired_first_block() will be defined from the last block that was legacy + /// view-scanned AND where legacy key images were fully handled (i.e. the last fullscanned index). Otherwise, it will + /// be defined from the last block that was only legacy view-scanned. + /// - Goal: when scanning for legacy key images, expect the enote scanner to return key images for all blocks that + /// were legacy view-scanned but that didn't have key images handled (i.e. because key images weren't available + /// during a previous scan). + const LegacyScanMode m_legacy_scan_mode; + + const rct::key &m_legacy_base_spend_pubkey; + const crypto::secret_key &m_legacy_view_privkey; + SpEnoteStore &m_enote_store; +}; + +class ChunkConsumerMockLegacy final : public scanning::ChunkConsumer +{ +public: +//constructors + /// normal constructor + ChunkConsumerMockLegacy(const rct::key &legacy_base_spend_pubkey, + const crypto::secret_key &legacy_spend_privkey, + const crypto::secret_key &legacy_view_privkey, + SpEnoteStore &enote_store); + +//overloaded operators + /// disable copy/move (this is a scoped manager [reference wrapper]) + ChunkConsumerMockLegacy& operator=(ChunkConsumerMockLegacy&&) = delete; + +//member functions + /// get index of first block the enote store cares about + std::uint64_t refresh_index() const override; + /// get index of first block the updater wants to have scanned + std::uint64_t desired_first_block() const override; + /// get a marker for the next block > the specified index + scanning::ContiguityMarker get_next_block(const std::uint64_t block_index) const override; + /// get a marker for the nearest block <= the specified index + scanning::ContiguityMarker get_nearest_block(const std::uint64_t block_index) const override; + + /// consume a chunk of basic enote records and save the results + void consume_nonledger_chunk(const SpEnoteOriginStatus nonledger_origin_status, + const scanning::ChunkData &chunk_data) override; + void consume_onchain_chunk(const scanning::LedgerChunk &chunk, + const rct::key &alignment_block_id, + const std::uint64_t first_new_block, + const std::vector &new_block_ids) override; + +//member variables +private: + const rct::key &m_legacy_base_spend_pubkey; + const crypto::secret_key &m_legacy_spend_privkey; + const crypto::secret_key &m_legacy_view_privkey; + + SpEnoteStore &m_enote_store; +}; + +class ChunkConsumerMockSpIntermediate final : public scanning::ChunkConsumer +{ +public: +//constructors + /// normal constructor + ChunkConsumerMockSpIntermediate(const rct::key &jamtis_spend_pubkey, + const crypto::x25519_secret_key &xk_unlock_amounts, + const crypto::x25519_secret_key &xk_find_received, + const crypto::secret_key &s_generate_address, + SpEnoteStorePaymentValidator &enote_store); + +//overloaded operators + /// disable copy/move (this is a scoped manager [reference wrapper]) + ChunkConsumerMockSpIntermediate& operator=(ChunkConsumerMockSpIntermediate&&) = delete; + +//member functions + /// get index of first block the enote store cares about + std::uint64_t refresh_index() const override; + /// get index of first block the updater wants to have scanned + std::uint64_t desired_first_block() const override; + /// get a marker for the next block > the specified index + scanning::ContiguityMarker get_next_block(const std::uint64_t block_index) const override; + /// get a marker for the nearest block <= the specified index + scanning::ContiguityMarker get_nearest_block(const std::uint64_t block_index) const override; + + /// consume a chunk of basic enote records and save the results + void consume_nonledger_chunk(const SpEnoteOriginStatus nonledger_origin_status, + const scanning::ChunkData &chunk_data) override; + void consume_onchain_chunk(const scanning::LedgerChunk &chunk, + const rct::key &alignment_block_id, + const std::uint64_t first_new_block, + const std::vector &new_block_ids) override; + +//member variables +private: + const rct::key &m_jamtis_spend_pubkey; + const crypto::x25519_secret_key &m_xk_unlock_amounts; + const crypto::x25519_secret_key &m_xk_find_received; + const crypto::secret_key &m_s_generate_address; + SpEnoteStorePaymentValidator &m_enote_store; + + crypto::secret_key m_s_cipher_tag; + std::unique_ptr m_cipher_context; +}; + +class ChunkConsumerMockSp final : public scanning::ChunkConsumer +{ +public: +//constructors + /// normal constructor + ChunkConsumerMockSp(const rct::key &jamtis_spend_pubkey, + const crypto::secret_key &k_view_balance, + SpEnoteStore &enote_store); + +//overloaded operators + /// disable copy/move (this is a scoped manager [reference wrapper]) + ChunkConsumerMockSp& operator=(ChunkConsumerMockSp&&) = delete; + +//member functions + /// get index of first block the enote store cares about + std::uint64_t refresh_index() const override; + /// get index of first block the updater wants to have scanned + std::uint64_t desired_first_block() const override; + /// get a marker for the next block > the specified index + scanning::ContiguityMarker get_next_block(const std::uint64_t block_index) const override; + /// get a marker for the nearest block <= the specified index + scanning::ContiguityMarker get_nearest_block(const std::uint64_t block_index) const override; + + /// consume a chunk of basic enote records and save the results + void consume_nonledger_chunk(const SpEnoteOriginStatus nonledger_origin_status, + const scanning::ChunkData &chunk_data) override; + void consume_onchain_chunk(const scanning::LedgerChunk &chunk, + const rct::key &alignment_block_id, + const std::uint64_t first_new_block, + const std::vector &new_block_ids) override; + +//member variables +private: + const rct::key &m_jamtis_spend_pubkey; + const crypto::secret_key &m_k_view_balance; + SpEnoteStore &m_enote_store; + + crypto::x25519_secret_key m_xk_unlock_amounts; + crypto::x25519_secret_key m_xk_find_received; + crypto::secret_key m_s_generate_address; + crypto::secret_key m_s_cipher_tag; + std::unique_ptr m_cipher_context; +}; + +} //namespace mocks +} //namespace sp diff --git a/src/seraphis_mocks/seraphis_mocks.h b/src/seraphis_mocks/seraphis_mocks.h new file mode 100644 index 0000000000..a2875328a1 --- /dev/null +++ b/src/seraphis_mocks/seraphis_mocks.h @@ -0,0 +1,51 @@ +// Copyright (c) 2022, 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. + +// NOT FOR PRODUCTION + +// Aggregate header for seraphis mockups. + + +#pragma once + +#include "enote_finding_context_mocks.h" +#include "enote_store_mock_simple_v1.h" +#include "jamtis_mock_keys.h" +#include "legacy_mock_keys.h" +#include "make_mock_tx.h" +#include "mock_ledger_context.h" +#include "mock_offchain_context.h" +#include "mock_send_receive.h" +#include "mock_tx_builders_inputs.h" +#include "mock_tx_builders_legacy_inputs.h" +#include "mock_tx_builders_outputs.h" +#include "scan_chunk_consumer_mocks.h" +#include "tx_fee_calculator_mocks.h" +#include "tx_input_selection_output_context_mocks.h" +#include "tx_input_selector_mocks.h" +#include "tx_validation_context_mock.h" diff --git a/src/seraphis_mocks/tx_fee_calculator_mocks.h b/src/seraphis_mocks/tx_fee_calculator_mocks.h new file mode 100644 index 0000000000..dc255926ca --- /dev/null +++ b/src/seraphis_mocks/tx_fee_calculator_mocks.h @@ -0,0 +1,100 @@ +// Copyright (c) 2022, 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. + +// NOT FOR PRODUCTION + +// Calculate a tx fee (mock-ups for testing). + +#pragma once + +//local headers +#include "ringct/rctTypes.h" +#include "seraphis_main/tx_fee_calculator.h" + +//third party headers + +//standard headers +#include + +//forward declarations + + +namespace sp +{ +namespace mocks +{ + +/// fee = fee_per_weight +class FeeCalculatorMockTrivial final : public FeeCalculator +{ +public: +//member functions + rct::xmr_amount compute_fee(const std::size_t fee_per_weight, + const std::size_t num_legacy_inputs, + const std::size_t num_sp_inputs, + const std::size_t num_outputs) const override + { + return fee_per_weight; + } +}; + +/// fee = fee_per_weight * (num_inputs + num_outputs) +class FeeCalculatorMockSimple final : public FeeCalculator +{ +public: +//member functions + rct::xmr_amount compute_fee(const std::size_t fee_per_weight, + const std::size_t num_legacy_inputs, + const std::size_t num_sp_inputs, + const std::size_t num_outputs) const override + { + return fee_per_weight * (num_legacy_inputs + num_sp_inputs + num_outputs); + } +}; + +/// fee = fee_per_weight * (num_inputs / step_size + num_outputs) +class FeeCalculatorMockInputsStepped final : public FeeCalculator +{ +public: +//constructors + FeeCalculatorMockInputsStepped(const std::size_t step_size) : m_step_size{step_size > 0 ? step_size : 1} {} +//member functions + rct::xmr_amount compute_fee(const std::size_t fee_per_weight, + const std::size_t num_legacy_inputs, + const std::size_t num_sp_inputs, + const std::size_t num_outputs) const override + { + return fee_per_weight * ((num_legacy_inputs + num_sp_inputs) / m_step_size + num_outputs); + } +//member variables +private: + std::size_t m_step_size; +}; + +} //namespace mocks +} //namespace sp diff --git a/src/seraphis_mocks/tx_input_selection_output_context_mocks.h b/src/seraphis_mocks/tx_input_selection_output_context_mocks.h new file mode 100644 index 0000000000..ea22bcbe4a --- /dev/null +++ b/src/seraphis_mocks/tx_input_selection_output_context_mocks.h @@ -0,0 +1,83 @@ +// Copyright (c) 2022, 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. + +// NOT FOR PRODUCTION + +// Simple mock output set context for use in input selection. + +#pragma once + +//local headers +#include "ringct/rctTypes.h" +#include "seraphis_main/tx_input_selection_output_context.h" + +//third party headers +#include "boost/multiprecision/cpp_int.hpp" + +//standard headers + +//forward declarations + + +namespace sp +{ +namespace mocks +{ + +class OutputSetContextForInputSelectionMockSimple final : public OutputSetContextForInputSelection +{ +public: +//constructors + OutputSetContextForInputSelectionMockSimple(const std::vector &output_amounts, + const std::size_t num_additional_with_change) : + m_num_outputs{output_amounts.size()}, + m_num_additional_with_change{num_additional_with_change} + { + m_output_amount = 0; + + for (const rct::xmr_amount output_amount : output_amounts) + m_output_amount += output_amount; + } + +//member functions + /// get total output amount + boost::multiprecision::uint128_t total_amount() const override { return m_output_amount; } + /// get number of outputs assuming no change + std::size_t num_outputs_nochange() const override { return m_num_outputs; } + /// get number of outputs assuming non-zero change + std::size_t num_outputs_withchange() const override { return m_num_outputs + m_num_additional_with_change; } + +//member variables +private: + std::size_t m_num_outputs; + boost::multiprecision::uint128_t m_output_amount; + std::size_t m_num_additional_with_change; +}; + +} //namespace mocks +} //namespace sp diff --git a/src/seraphis_mocks/tx_input_selector_mocks.cpp b/src/seraphis_mocks/tx_input_selector_mocks.cpp new file mode 100644 index 0000000000..f2cbf89593 --- /dev/null +++ b/src/seraphis_mocks/tx_input_selector_mocks.cpp @@ -0,0 +1,257 @@ +// Copyright (c) 2022, 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. + +// NOT FOR PRODUCTION + +//paired header +#include "tx_input_selector_mocks.h" + +//local headers +#include "seraphis_main/contextual_enote_record_types.h" +#include "seraphis_main/contextual_enote_record_utils.h" +#include "seraphis_main/tx_input_selection.h" + +//third party headers +#include "boost/container/map.hpp" +#include "boost/multiprecision/cpp_int.hpp" + +//standard headers +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis_mocks" + +namespace sp +{ +namespace mocks +{ +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool pred_has_match(const boost::container::multimap &input_set, + const std::function &comparison_record)> &predicate) +{ + return std::find_if(input_set.begin(), input_set.end(), predicate) != input_set.end(); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool pred_has_match(const input_set_tracker_t &input_set, + const InputSelectionType input_type, + const std::function &comparison_record)> &predicate) +{ + if (input_set.find(input_type) == input_set.end()) + return false; + + return pred_has_match(input_set.at(input_type), predicate); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +bool InputSelectorMockSimpleV1::try_select_input_candidate_v1(const boost::multiprecision::uint128_t desired_total_amount, + const input_set_tracker_t &added_inputs, + const input_set_tracker_t &candidate_inputs, + ContextualRecordVariant &selected_input_out) const +{ + // 1. try to select a legacy input + for (const LegacyContextualEnoteRecordV1 &contextual_enote_record : m_enote_store.m_legacy_contextual_enote_records) + { + // a. only consider unspent enotes + if (!has_spent_status(contextual_enote_record, SpEnoteSpentStatus::UNSPENT)) + continue; + + // b. prepare record finder + auto record_finder = + [&contextual_enote_record](const std::pair &comparison_record) + -> bool + { + if (!comparison_record.second.is_type()) + return false; + + return have_same_destination( + contextual_enote_record, + comparison_record.second.unwrap() + ); + }; + + // c. ignore already added legacy inputs + if (pred_has_match(added_inputs, InputSelectionType::LEGACY, record_finder)) + continue; + + // d. ignore legacy input candidates + if (pred_has_match(candidate_inputs, InputSelectionType::LEGACY, record_finder)) + continue; + + selected_input_out = contextual_enote_record; + return true; + } + + // 2. try to select a seraphis input + for (const SpContextualEnoteRecordV1 &contextual_enote_record : m_enote_store.m_sp_contextual_enote_records) + { + // a. only consider unspent enotes + if (!has_spent_status(contextual_enote_record, SpEnoteSpentStatus::UNSPENT)) + continue; + + // b. prepare record finder + auto record_finder = + [&contextual_enote_record](const std::pair &comparison_record) + -> bool + { + if (!comparison_record.second.is_type()) + return false; + + return have_same_destination( + contextual_enote_record, + comparison_record.second.unwrap() + ); + }; + + // c. ignore already added seraphis inputs + if (pred_has_match(added_inputs, InputSelectionType::SERAPHIS, record_finder)) + continue; + + // d. ignore already seraphis input candidates + if (pred_has_match(candidate_inputs, InputSelectionType::SERAPHIS, record_finder)) + continue; + + selected_input_out = contextual_enote_record; + return true; + } + + return false; +} +//------------------------------------------------------------------------------------------------------------------- +bool InputSelectorMockV1::try_select_input_candidate_v1(const boost::multiprecision::uint128_t desired_total_amount, + const input_set_tracker_t &added_inputs, + const input_set_tracker_t &candidate_inputs, + ContextualRecordVariant &selected_input_out) const +{ + // 1. try to select from legacy enotes + const std::unordered_map &mapped_legacy_contextual_enote_records{ + m_enote_store.legacy_records() + }; + const std::unordered_map> &legacy_onetime_address_identifier_map{ + m_enote_store.legacy_onetime_address_identifier_map() + }; + for (const auto &mapped_enote_record : mapped_legacy_contextual_enote_records) + { + // a. only consider unspent enotes + if (!has_spent_status(mapped_enote_record.second, SpEnoteSpentStatus::UNSPENT)) + continue; + + // b. prepare record finder + auto record_finder = + [&mapped_enote_record](const std::pair &comparison_record) -> bool + { + if (!comparison_record.second.is_type()) + return false; + + return have_same_destination( + mapped_enote_record.second, + comparison_record.second.unwrap() + ); + }; + + // c. ignore already added legacy inputs + if (pred_has_match(added_inputs, InputSelectionType::LEGACY, record_finder)) + continue; + + // d. ignore existing legacy input candidates + if (pred_has_match(candidate_inputs, InputSelectionType::LEGACY, record_finder)) + continue; + + // e. if this legacy enote shares a onetime address with any other legacy enotes, only proceed if this one + // has the highest amount + if (!legacy_enote_has_highest_amount_in_set(mapped_enote_record.first, + mapped_enote_record.second.record.amount, + {SpEnoteOriginStatus::OFFCHAIN, SpEnoteOriginStatus::UNCONFIRMED, SpEnoteOriginStatus::ONCHAIN}, + legacy_onetime_address_identifier_map.at( + onetime_address_ref(mapped_enote_record.second.record.enote) + ), + [&mapped_legacy_contextual_enote_records](const rct::key &identifier) -> const SpEnoteOriginStatus& + { + CHECK_AND_ASSERT_THROW_MES(mapped_legacy_contextual_enote_records.find(identifier) != + mapped_legacy_contextual_enote_records.end(), + "input selector (mock): tracked legacy duplicates has an entry that doesn't line up " + "1:1 with the legacy map even though it should (bug)."); + + return mapped_legacy_contextual_enote_records.at(identifier).origin_context.origin_status; + }, + [&mapped_legacy_contextual_enote_records](const rct::key &identifier) -> rct::xmr_amount + { + CHECK_AND_ASSERT_THROW_MES(mapped_legacy_contextual_enote_records.find(identifier) != + mapped_legacy_contextual_enote_records.end(), + "input selector (mock): tracked legacy duplicates has an entry that doesn't line up " + "1:1 with the legacy map even though it should (bug)."); + + return mapped_legacy_contextual_enote_records.at(identifier).record.amount; + })) + continue; + + selected_input_out = mapped_enote_record.second; + return true; + } + + // 2. try to select from seraphis enotes + const std::unordered_map &mapped_sp_contextual_enote_records{ + m_enote_store.sp_records() + }; + for (const auto &mapped_enote_record : mapped_sp_contextual_enote_records) + { + // a. only consider unspent enotes + if (!has_spent_status(mapped_enote_record.second, SpEnoteSpentStatus::UNSPENT)) + continue; + + // b. prepare record finder + auto record_finder = + [&mapped_enote_record](const std::pair &comparison_record) -> bool + { + if (!comparison_record.second.is_type()) + return false; + + return have_same_destination( + mapped_enote_record.second, + comparison_record.second.unwrap() + ); + }; + + // c. ignore already added seraphis inputs + if (pred_has_match(added_inputs, InputSelectionType::SERAPHIS, record_finder)) + continue; + + // d. ignore already excluded seraphis inputs + if (pred_has_match(candidate_inputs, InputSelectionType::SERAPHIS, record_finder)) + continue; + + selected_input_out = mapped_enote_record.second; + return true; + } + + return false; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace mocks +} //namespace sp diff --git a/src/seraphis_mocks/tx_input_selector_mocks.h b/src/seraphis_mocks/tx_input_selector_mocks.h new file mode 100644 index 0000000000..d4374302ba --- /dev/null +++ b/src/seraphis_mocks/tx_input_selector_mocks.h @@ -0,0 +1,119 @@ +// Copyright (c) 2022, 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. + +// NOT FOR PRODUCTION + +// Utilities for selecting tx inputs from an enote storage (mock-ups for unit testing). + +#pragma once + +//local headers +#include "enote_store_mock_simple_v1.h" +#include "seraphis_impl/enote_store.h" +#include "seraphis_main/contextual_enote_record_types.h" +#include "seraphis_main/tx_input_selection.h" + +//third party headers +#include "boost/multiprecision/cpp_int.hpp" + +//standard headers + +//forward declarations + + +namespace sp +{ +namespace mocks +{ + +/// simple input selector +/// - select the next available input in the enote store (input selection with this is not thread-safe) +class InputSelectorMockSimpleV1 final : public InputSelectorV1 +{ +public: +//constructors + /// normal constructor + InputSelectorMockSimpleV1(const SpEnoteStoreMockSimpleV1 &enote_store) : + m_enote_store{enote_store} + { + // in practice, lock the enote store with an 'input selection' mutex here for thread-safe input selection that + // prevents two tx attempts from using the same inputs (take a reader-writer lock when selecting an input) + } + +//overloaded operators + /// disable copy/move (this is a scoped manager [reference wrapper]) + InputSelectorMockSimpleV1& operator=(InputSelectorMockSimpleV1&&) = delete; + +//member functions + /// select the next available input + bool try_select_input_candidate_v1(const boost::multiprecision::uint128_t desired_total_amount, + const input_set_tracker_t &added_inputs, + const input_set_tracker_t &candidate_inputs, + ContextualRecordVariant &selected_input_out) const override; + +//member variables +private: + /// read-only reference to an enote storage + const SpEnoteStoreMockSimpleV1 &m_enote_store; +}; + +/// mock input selector +/// - select a pseudo-random available input in the enote store (input selection with this is not thread-safe) +class InputSelectorMockV1 final : public InputSelectorV1 +{ +public: +//constructors + /// normal constructor + InputSelectorMockV1(const SpEnoteStore &enote_store) : + m_enote_store{enote_store} + { + // in practice, lock the enote store with an 'input selection' mutex here for thread-safe input selection that + // prevents two tx attempts from using the same inputs (take a reader-writer lock when selecting an input) + } + +//overloaded operators + /// disable copy/move (this is a scoped manager [reference wrapper]) + InputSelectorMockV1& operator=(InputSelectorMockV1&&) = delete; + +//member functions + /// select the next available input + /// NOTE: this is a mock-up; a real input selector would contain many complicated mechanisms, e.g. the option to ignore + /// locked enotes, heuristics to avoid input timing correlations, etc. + bool try_select_input_candidate_v1(const boost::multiprecision::uint128_t desired_total_amount, + const input_set_tracker_t &added_inputs, + const input_set_tracker_t &candidate_inputs, + ContextualRecordVariant &selected_input_out) const override; + +//member variables +private: + /// read-only reference to an enote storage + const SpEnoteStore &m_enote_store; +}; + +} //namespace mocks +} //namespace sp diff --git a/src/seraphis_mocks/tx_validation_context_mock.h b/src/seraphis_mocks/tx_validation_context_mock.h new file mode 100644 index 0000000000..4205ce4d4a --- /dev/null +++ b/src/seraphis_mocks/tx_validation_context_mock.h @@ -0,0 +1,112 @@ +// Copyright (c) 2022, 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. + +// NOT FOR PRODUCTION + +// Mock-up of interface for interacting with a mock ledger context where txs should be valid. + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "mock_ledger_context.h" +#include "ringct/rctTypes.h" +#include "seraphis_main/tx_validation_context.h" + +//third party headers + +//standard headers +#include + +//forward declarations + + +namespace sp +{ +namespace mocks +{ + +class TxValidationContextMock final : public TxValidationContext +{ +public: +//constructors + TxValidationContextMock(const MockLedgerContext &mock_ledger_context) : + m_mock_ledger_context{mock_ledger_context} + {} + +//overloaded operators + /// disable copy/move (this is a scoped manager [reference wrapper]) + TxValidationContextMock& operator=(TxValidationContextMock&&) = delete; + +//member functions + /** + * brief: cryptonote_key_image_exists - checks if a cryptonote key image exists in the mock ledger + * param: key_image - + * return: true/false on check result + */ + bool cryptonote_key_image_exists(const crypto::key_image &key_image) const override + { + return m_mock_ledger_context.cryptonote_key_image_exists_onchain(key_image); + } + /** + * brief: seraphis_key_image_exists - checks if a seraphis key image exists in the mock ledger + * param: key_image - + * return: true/false on check result + */ + bool seraphis_key_image_exists(const crypto::key_image &key_image) const override + { + return m_mock_ledger_context.seraphis_key_image_exists_onchain(key_image); + } + /** + * brief: get_reference_set_proof_elements_v1 - gets legacy {KI, C} pairs stored in the mock ledger + * param: indices - + * outparam: proof_elements_out - {KI, C} + */ + void get_reference_set_proof_elements_v1(const std::vector &indices, + rct::ctkeyV &proof_elements_out) const override + { + m_mock_ledger_context.get_reference_set_proof_elements_v1(indices, proof_elements_out); + } + /** + * brief: get_reference_set_proof_elements_v2 - gets seraphis squashed enotes stored in the mock ledger + * param: indices - + * outparam: proof_elements_out - {squashed enote} + */ + void get_reference_set_proof_elements_v2(const std::vector &indices, + rct::keyV &proof_elements_out) const override + { + m_mock_ledger_context.get_reference_set_proof_elements_v2(indices, proof_elements_out); + } + +//member variables +private: + const MockLedgerContext &m_mock_ledger_context; +}; + +} //namespace mocks +} //namespace sp diff --git a/src/seraphis_wallet/CMakeLists.txt b/src/seraphis_wallet/CMakeLists.txt new file mode 100644 index 0000000000..9aa69b934a --- /dev/null +++ b/src/seraphis_wallet/CMakeLists.txt @@ -0,0 +1,67 @@ +#Copyright(c) 2014 - 2022, 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(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) + +set(wallet_sources + key_container.cpp + jamtis_keys.cpp) + +monero_find_all_headers(wallet_private_headers "${CMAKE_CURRENT_SOURCE_DIR}") + +monero_private_headers(seraphis_wallet + ${wallet_private_headers}) +monero_add_library(seraphis_wallet + ${wallet_sources} + ${wallet_private_headers}) +target_link_libraries(seraphis_wallet + PUBLIC + # seraphis_lib + cncrypto + common + cryptonote_basic + device + epee + ringct + seraphis_core + seraphis_crypto + rpc_base + multisig + # cryptonote_core + # mnemonics + # device_trezor + net + ${LMDB_LIBRARY} + ${Boost_CHRONO_LIBRARY} + ${Boost_SERIALIZATION_LIBRARY} + ${Boost_FILESYSTEM_LIBRARY} + ${Boost_SYSTEM_LIBRARY} + ${Boost_THREAD_LIBRARY} + ${Boost_REGEX_LIBRARY} + PRIVATE + ${EXTRA_LIBRARIES}) diff --git a/src/seraphis_wallet/encrypted_file.h b/src/seraphis_wallet/encrypted_file.h new file mode 100644 index 0000000000..3070c420f5 --- /dev/null +++ b/src/seraphis_wallet/encrypted_file.h @@ -0,0 +1,120 @@ +#pragma once + +#include "crypto/chacha.h" +#include "crypto/crypto.h" +#include "file_io_utils.h" +#include "serialization/binary_archive.h" +#include "serialization/containers.h" +#include "serialization/crypto.h" +#include "serialization/serialization.h" +#include "serialization/string.h" +#include "storages/portable_storage_template_helper.h" +#include "string_coding.h" + +#include + +struct encrypted_file +{ + std::string encrypted_data; + crypto::chacha_iv iv; + + BEGIN_SERIALIZE_OBJECT() + VERSION_FIELD(0) + FIELD(encrypted_data) + FIELD(iv) + END_SERIALIZE() +}; + +template bool read_encrypted_file(std::string path, const crypto::chacha_key &key, T &struct_out) +{ + std::string buf; + if (!epee::file_io_utils::load_file_to_string(path, buf)) + return false; + + encrypted_file file; + + binary_archive file_ar{epee::strspan(buf)}; + if (!::serialization::serialize(file_ar, file)) + return false; + + std::string decrypted_data; + decrypted_data.resize(file.encrypted_data.size()); + crypto::chacha20(file.encrypted_data.data(), file.encrypted_data.size(), key, file.iv, &decrypted_data[0]); + + binary_archive ar{epee::strspan(decrypted_data)}; + if (!::serialization::serialize(ar, struct_out)) + return false; + + return true; +} + +// NOTE: if this were c++20, Concepts and `require` could be used to make this one function +template bool read_encrypted_file_json(std::string path, const crypto::chacha_key &key, T &struct_out) +{ + std::string buf; + if (!epee::file_io_utils::load_file_to_string(path, buf)) + return false; + + encrypted_file file; + + binary_archive file_ar{epee::strspan(buf)}; + if (!::serialization::serialize(file_ar, file)) + return false; + + std::string decrypted_data; + decrypted_data.resize(file.encrypted_data.size()); + crypto::chacha20(file.encrypted_data.data(), file.encrypted_data.size(), key, file.iv, &decrypted_data[0]); + + std::string decoded = epee::string_encoding::base64_decode(decrypted_data); + return epee::serialization::load_t_from_json(struct_out, decoded); +} + +template bool write_encrypted_file(std::string path, const crypto::chacha_key &key, T &struct_in) +{ + std::stringstream data_oss; + binary_archive data_ar(data_oss); + if (!::serialization::serialize(data_ar, struct_in)) + return false; + + std::string buf = data_oss.str(); + + encrypted_file file = {}; + file.iv = crypto::rand(); + + std::string encrypted_data; + encrypted_data.resize(buf.size()); + + crypto::chacha20(buf.data(), buf.size(), key, file.iv, &encrypted_data[0]); + + file.encrypted_data = encrypted_data; + + std::stringstream file_oss; + binary_archive file_ar(file_oss); + if (!::serialization::serialize(file_ar, file)) + return false; + + return epee::file_io_utils::save_string_to_file(path, file_oss.str()); +} + +template bool write_encrypted_file_json(std::string path, const crypto::chacha_key &key, T &struct_in) +{ + std::string struct_json = epee::serialization::store_t_to_json(struct_in); + std::string data = epee::string_encoding::base64_encode(struct_json); + + encrypted_file file = {}; + file.iv = crypto::rand(); + + std::string encrypted_data; + encrypted_data.resize(data.size()); + + crypto::chacha20(data.data(), data.size(), key, file.iv, &encrypted_data[0]); + + file.encrypted_data = encrypted_data; + + std::stringstream file_oss; + binary_archive file_ar(file_oss); + if (!::serialization::serialize(file_ar, file)) + return false; + + return epee::file_io_utils::save_string_to_file(path, file_oss.str()); +} diff --git a/src/seraphis_wallet/jamtis_keys.cpp b/src/seraphis_wallet/jamtis_keys.cpp new file mode 100644 index 0000000000..3bfbcbff63 --- /dev/null +++ b/src/seraphis_wallet/jamtis_keys.cpp @@ -0,0 +1,225 @@ +// Copyright (c) 2022, 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 "jamtis_keys.h" + +//local headers +#include "crypto/chacha.h" +#include "crypto/crypto.h" +#include "crypto/x25519.h" +#include "ringct/rctOps.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/jamtis_account_secrets.h" +#include "seraphis_core/sp_core_enote_utils.h" +#include "seraphis_wallet/key_container.h" +#include +#include +#include + +//third party headers + +//standard headers + + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis_wallet" + +namespace { + +bool is_zero(crypto::secret_key k) +{ + return crypto_verify_32((unsigned char*)k.data, rct::zero().bytes); +} + +bool is_zero(crypto::x25519_secret_key k) +{ + return crypto_verify_32(k.data, rct::zero().bytes); +} + +bool is_one(crypto::secret_key k) +{ + return crypto_verify_32((unsigned char*)k.data, rct::identity().bytes); +} + +bool is_one(crypto::x25519_secret_key k) +{ + return crypto_verify_32((unsigned char*)k.data, rct::identity().bytes); +} +void derive_key(const crypto::chacha_key &base_key, crypto::chacha_key &key) +{ + static_assert(sizeof(base_key) == sizeof(crypto::hash), "chacha key and hash should be the same size"); + + epee::mlocked> data; + memcpy(data.data(), &base_key, sizeof(base_key)); + data[sizeof(base_key)] = 'k'; + crypto::generate_chacha_key(data.data(), sizeof(data), key, 1); +} + +epee::wipeable_string get_key_stream(const crypto::chacha_key &base_key, const crypto::chacha_iv &iv, size_t bytes) +{ + // derive a new key + crypto::chacha_key key; + derive_key(base_key, key); + + // chacha + epee::wipeable_string buffer0(std::string(bytes, '\0')); + epee::wipeable_string buffer1 = buffer0; + crypto::chacha20(buffer0.data(), buffer0.size(), key, iv, buffer1.data()); + return buffer1; +} + +} // namespace + +namespace sp +{ +namespace jamtis +{ + +//------------------------------------------------------------------------------------------------------------------- +void make_jamtis_keys(JamtisKeys &keys_out) +{ + keys_out.k_m = rct::rct2sk(rct::skGen()); + keys_out.k_vb = rct::rct2sk(rct::skGen()); + make_jamtis_viewreceived_key(keys_out.k_vb, keys_out.d_vr); + make_jamtis_filterassist_key(keys_out.d_vr, keys_out.d_fa); + make_jamtis_generateaddress_secret(keys_out.d_vr, keys_out.s_ga); + make_jamtis_ciphertag_secret(keys_out.s_ga, keys_out.s_ct); + make_seraphis_spendkey(keys_out.k_vb, keys_out.k_m, keys_out.K_s_base); + make_jamtis_exchangebase_pubkey(keys_out.d_vr, keys_out.D_base); + make_jamtis_viewreceived_pubkey(keys_out.d_vr, keys_out.D_base, keys_out.D_vr); + make_jamtis_filterassist_pubkey(keys_out.d_fa, keys_out.D_base, keys_out.D_fa); +} +//------------------------------------------------------------------------------------------------------------------- +// See wiki page +seraphis_wallet::WalletType get_wallet_type(const JamtisKeys &keys) +{ + if (!is_zero(keys.k_m)) + { + assert(!is_zero(keys.k_vb)); + return seraphis_wallet::WalletType::Master; + } + + if (!is_zero(keys.k_vb)) + { + return seraphis_wallet::WalletType::ViewAll; + } + + if (!is_zero(keys.d_vr)) { + return seraphis_wallet::WalletType::PaymentValidator; + } + + if (is_one(keys.d_fa) && is_one(keys.s_ga)) + { + return seraphis_wallet::WalletType::FilterAssistAndAddressGen; + } + + if (is_zero(keys.d_fa) && is_one(keys.s_ga)) + { + return seraphis_wallet::WalletType::AddressGenerator; + } + + if (is_zero(keys.d_fa) && is_zero(keys.s_ga)) + { + return seraphis_wallet::WalletType::FilterAssist; + } + + return seraphis_wallet::WalletType::Empty; +} +//------------------------------------------------------------------------------------------------------------------- +void derive_jamtis_keys_from_existing(JamtisKeys &keys) +{ + make_jamtis_viewreceived_key(keys.k_vb, keys.d_vr); + make_jamtis_filterassist_key(keys.d_vr, keys.d_fa); + make_jamtis_generateaddress_secret(keys.d_vr, keys.s_ga); + make_jamtis_ciphertag_secret(keys.s_ga, keys.s_ct); + make_seraphis_spendkey(keys.k_vb, keys.k_m, keys.K_s_base); + make_jamtis_exchangebase_pubkey(keys.d_vr, keys.D_base); + make_jamtis_viewreceived_pubkey(keys.d_vr, keys.D_base, keys.D_vr); + make_jamtis_filterassist_pubkey(keys.d_fa, keys.D_base, keys.D_fa); +} +//------------------------------------------------------------------------------------------------------------------- +void make_address_for_user(const JamtisKeys &user_keys, + const address_index_t &j, + JamtisDestinationV1 &user_address_out) +{ + make_jamtis_destination_v1(user_keys.K_s_base, + user_keys.D_fa, + user_keys.D_vr, + user_keys.D_base, + user_keys.s_ga, + j, + user_address_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_random_address_for_user(const JamtisKeys &user_keys, JamtisDestinationV1 &user_address_out) +{ + const address_index_t random_j = gen_address_index(); + + make_address_for_user(user_keys, random_j, user_address_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_jamtis_amount(const JamtisKeys &user_keys, JamtisDestinationV1 &user_address_out) +{ + const address_index_t random_j = gen_address_index(); + + make_address_for_user(user_keys, random_j, user_address_out); +} +//------------------------------------------------------------------------------------------------------------------- +void xor_with_key_stream(const crypto::chacha_key &chacha_key, + const crypto::chacha_iv chacha_iv, + JamtisKeys &keys) +{ + // we have 6 private keys + epee::wipeable_string key_stream = get_key_stream(chacha_key, chacha_iv, 6 * sizeof(crypto::secret_key)); + const char *ptr = key_stream.data(); + + for (size_t i = 0; i < sizeof(crypto::secret_key); ++i) keys.k_m.data[i] ^= *ptr++; + for (size_t i = 0; i < sizeof(crypto::secret_key); ++i) keys.k_vb.data[i] ^= *ptr++; + for (size_t i = 0; i < sizeof(crypto::secret_key); ++i) keys.d_fa.data[i] ^= *ptr++; + for (size_t i = 0; i < sizeof(crypto::secret_key); ++i) keys.d_vr.data[i] ^= *ptr++; + for (size_t i = 0; i < sizeof(crypto::secret_key); ++i) keys.s_ga.data[i] ^= *ptr++; + for (size_t i = 0; i < sizeof(crypto::secret_key); ++i) keys.s_ct.data[i] ^= *ptr++; +} +//------------------------------------------------------------------------------------------------------------------- +bool jamtis_keys_equal(const JamtisKeys &keys, const JamtisKeys &other) +{ + return (keys.k_m == other.k_m) && + (keys.k_vb == other.k_vb) && + (keys.d_vr == other.d_vr) && + (keys.d_fa == other.d_fa) && + (keys.s_ga == other.s_ga) && + (keys.s_ct == other.s_ct) && + (keys.K_s_base == other.K_s_base) && + (keys.D_vr == other.D_vr) && + (keys.D_fa == other.D_fa) && + (keys.D_base == other.D_base); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace jamtis +} //namespace sp diff --git a/src/seraphis_wallet/jamtis_keys.h b/src/seraphis_wallet/jamtis_keys.h new file mode 100644 index 0000000000..89e409224f --- /dev/null +++ b/src/seraphis_wallet/jamtis_keys.h @@ -0,0 +1,100 @@ +// Copyright (c) 2022, 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. + +//// +// Jamtis keys +// +// reference: https://gist.github.com/tevador/50160d160d24cfc6c52ae02eb3d17024 +/// + +#pragma once + +//local headers +#include "crypto/chacha.h" +#include "crypto/crypto.h" +#include "crypto/x25519.h" +#include "ringct/rctTypes.h" +#include "seraphis_core/jamtis_destination.h" + +//third party headers + +//standard headers + +//forward declarations +namespace seraphis_wallet { + +enum class WalletType; + +} + +namespace sp +{ +namespace jamtis +{ + +//// +// A set of jamtis keys for mock-ups/unit testing +/// +struct JamtisKeys +{ + crypto::secret_key k_m; //master + crypto::secret_key k_vb; //view-balance + crypto::x25519_secret_key d_vr; //view-received + crypto::x25519_secret_key d_fa; //filter-assist + crypto::secret_key s_ga; //generate-address + crypto::secret_key s_ct; //cipher-tag + rct::key K_s_base; //jamtis spend base = k_vb X + k_m U + crypto::x25519_pubkey D_vr; //view-received pubkey = d_vr D_base + crypto::x25519_pubkey D_fa; //filter-assist pubkey = d_fa D_base + crypto::x25519_pubkey D_base; //exchange-base pubkey = d_vr xG +}; + +/// make a set of jamtis keys +void make_jamtis_keys(JamtisKeys &keys_out); +/// derive a set of jamtis keys from existing non-zero entries +void derive_jamtis_keys(JamtisKeys &keys); +/// make a jamtis address for the given privkeys and address index +void make_address_for_user(const JamtisKeys &user_keys, + const address_index_t &j, + JamtisDestinationV1 &user_address_out); +/// make a random jamtis address for the given privkeys +void make_random_address_for_user(const JamtisKeys &user_keys, + JamtisDestinationV1 &user_address_out); +/// encrypt a set of jamtis keys in-place +void xor_with_key_stream(const crypto::chacha_key &chacha_key, + const crypto::chacha_iv chacha_iv, + JamtisKeys &keys); + +/// get keys' wallet type from the existing keys +seraphis_wallet::WalletType get_wallet_type(const JamtisKeys &keys); + +/// compare two key structures; both should be in the same decrypted/encrypted state +bool jamtis_keys_equal(const JamtisKeys &keys, const JamtisKeys &other); + +} //namespace jamtis +} //namespace sp diff --git a/src/seraphis_wallet/key_container.cpp b/src/seraphis_wallet/key_container.cpp new file mode 100644 index 0000000000..ac85be4f67 --- /dev/null +++ b/src/seraphis_wallet/key_container.cpp @@ -0,0 +1,225 @@ +// 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 "key_container.h" + +// local headers +#include "crypto/chacha.h" +#include "jamtis_keys.h" +#include "seraphis_wallet/encrypted_file.h" +#include "jamtis_keys.h" + +// standard headers + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "seraphis_wallet" + +namespace seraphis_wallet +{ +//------------------------------------------------------------------------------------------------------------------- +KeyContainer::KeyContainer(sp::jamtis::JamtisKeys &&keys, const crypto::chacha_key &key) : + m_keys{keys}, + m_encrypted{false}, + m_encryption_iv{} +{ + encrypt(key); +} +//------------------------------------------------------------------------------------------------------------------- +KeyContainer::KeyContainer(sp::jamtis::JamtisKeys &&keys, bool encrypted, const crypto::chacha_iv encryption_iv) : + m_keys{keys}, + m_encrypted{encrypted}, + m_encryption_iv{encryption_iv} +{ +} +//------------------------------------------------------------------------------------------------------------------- +bool KeyContainer::load_from_keys_file(const std::string &path, const crypto::chacha_key &chacha_key) +{ + // 1. define serializable + ser_JamtisKeys ser_keys; + + // 2. get the keys in the encrypted file into the serializable + CHECK_AND_ASSERT_THROW_MES( + read_encrypted_file(path, chacha_key, ser_keys), "load_from_keys_file: failed reading encrypted file."); + + // 3. recover jamtis keys + sp::jamtis::JamtisKeys recovered_keys{}; + recover_jamtis_keys(ser_keys, recovered_keys); + + // 4. check if keys are valid and move to m_keys if so + CHECK_AND_ASSERT_THROW_MES(jamtis_keys_valid(recovered_keys, chacha_key), "load_from_keys_file: failed validating jamtis keys."); + m_keys = std::move(recovered_keys); + + // 5. encrypt keys in memory + encrypt(chacha_key); + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool KeyContainer::jamtis_keys_valid(const sp::jamtis::JamtisKeys &keys, const crypto::chacha_key &chacha_key) +{ + // 1. copy original keys + sp::jamtis::JamtisKeys test_keys{keys}; + + // 2. derive keys + sp::jamtis::derive_jamtis_keys(test_keys); + + // 3. check if the given keys match + return sp::jamtis::jamtis_keys_equal(test_keys, keys); +} +//------------------------------------------------------------------------------------------------------------------- +bool KeyContainer::encrypt(const crypto::chacha_key &chacha_key) +{ + // 1. return false if already encrypted + if (m_encrypted) + return false; + + // 2. generate new iv + m_encryption_iv = crypto::rand(); + + // 3. encrypt keys with chacha_key and iv + sp::jamtis::xor_with_key_stream(chacha_key, m_encryption_iv, m_keys); + + // 4. set encrypted flag true + m_encrypted = true; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool KeyContainer::decrypt(const crypto::chacha_key &chacha_key) +{ + // 1. return false if already decrypted + if (!m_encrypted) + return false; + + // 2. decrypt keys with chacha_key and iv + sp::jamtis::xor_with_key_stream(chacha_key, m_encryption_iv, m_keys); + + // 3. set encrypted flag false + m_encrypted = false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +void KeyContainer::generate_keys(const crypto::chacha_key &chacha_key) +{ + // 1. generate new keys and store to m_keys + make_jamtis_keys(m_keys); + + // 2. encrypt keys if they are decrypted + if (!m_encrypted) + encrypt(chacha_key); +} +//------------------------------------------------------------------------------------------------------------------- +sp::jamtis::JamtisKeys &KeyContainer::get_keys(const crypto::chacha_key &chacha_key) { + if (m_encrypted) + decrypt(chacha_key); + + return m_keys; +} +//------------------------------------------------------------------------------------------------------------------- +KeyGuard KeyContainer::get_keys_guard(const crypto::chacha_key &chacha_key) { + return KeyGuard{*this, chacha_key}; +} +//------------------------------------------------------------------------------------------------------------------- +WalletType KeyContainer::get_wallet_type() +{ + return sp::jamtis::get_wallet_type(m_keys); +} +//------------------------------------------------------------------------------------------------------------------- +void KeyContainer::make_serializable_jamtis_keys(ser_JamtisKeys &serializable_keys) +{ + serializable_keys.k_m = m_keys.k_m; + serializable_keys.k_vb = m_keys.k_vb; + serializable_keys.d_vr = m_keys.d_vr; + serializable_keys.d_fa = m_keys.d_fa; + serializable_keys.s_ga = m_keys.s_ga; + serializable_keys.s_ct = m_keys.s_ct; + serializable_keys.K_s_base = m_keys.K_s_base; + serializable_keys.D_vr = m_keys.D_vr; + serializable_keys.D_fa = m_keys.D_fa; +} +//------------------------------------------------------------------------------------------------------------------- +void KeyContainer::recover_jamtis_keys(const ser_JamtisKeys &ser_keys, sp::jamtis::JamtisKeys &keys_out) +{ + keys_out.k_m = ser_keys.k_m; + keys_out.k_vb = ser_keys.k_vb; + keys_out.d_vr = ser_keys.d_vr; + keys_out.d_fa = ser_keys.d_fa; + keys_out.s_ga = ser_keys.s_ga; + keys_out.s_ct = ser_keys.s_ct; + keys_out.K_s_base = ser_keys.K_s_base; + keys_out.D_vr = ser_keys.D_vr; + keys_out.D_fa = ser_keys.D_fa; +} +//------------------------------------------------------------------------------------------------------------------- +bool KeyContainer::compare_keys(KeyContainer &other, const crypto::chacha_key &chacha_key) +{ + // 1. decrypt keys if they are encrypted in memory + other.decrypt(chacha_key); + // 2. decrypt if encrypted in memory + decrypt(chacha_key); + + bool r = sp::jamtis::jamtis_keys_equal(m_keys, other.m_keys); + + // 3. encrypt in memory + other.encrypt(chacha_key); + + // 4. encrypt in memory + encrypt(chacha_key); + + // 5. return result of comparison + return r; +} +//------------------------------------------------------------------------------------------------------------------- +// KeyGuard +//------------------------------------------------------------------------------------------------------------------- +KeyGuard::KeyGuard(const KeyGuard &other) : + m_ref{other.m_ref + 1}, + m_container{other.m_container}, + m_key{other.m_key} +{ +} +//------------------------------------------------------------------------------------------------------------------- +KeyGuard::KeyGuard(KeyContainer &container, const crypto::chacha_key &key) : + m_container{container}, + m_ref{1}, + m_key{key} +{ + m_container.decrypt(key); +} +//------------------------------------------------------------------------------------------------------------------- +KeyGuard::~KeyGuard() +{ + if (m_ref == 1) + { + m_container.encrypt(m_key); + } +} +//------------------------------------------------------------------------------------------------------------------- +} // namespace seraphis_wallet diff --git a/src/seraphis_wallet/key_container.h b/src/seraphis_wallet/key_container.h new file mode 100644 index 0000000000..42c1a1dc3f --- /dev/null +++ b/src/seraphis_wallet/key_container.h @@ -0,0 +1,169 @@ +// Copyright (c) 2024, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +// local headers +#include "crypto/chacha.h" +#include "crypto/crypto.h" +#include "serialization/crypto.h" +#include "seraphis_wallet/jamtis_keys.h" +#include "serialization/serialization.h" + +// third party headers + +// standard headers +#include + +// forward declarations + +struct ser_JamtisKeys +{ + crypto::secret_key k_m; //master + crypto::secret_key k_vb; //view-balance + crypto::x25519_secret_key d_vr; //view-received + crypto::x25519_secret_key d_fa; //filter-assist + crypto::secret_key s_ga; //generate-address + crypto::secret_key s_ct; //cipher-tag + rct::key K_s_base; //jamtis spend base = k_vb X + k_m U + crypto::x25519_pubkey D_vr; //view-received pubkey = d_vr D_base + crypto::x25519_pubkey D_fa; //filter-assist pubkey = d_fa D_base + crypto::x25519_pubkey D_base; //exchange-base pubkey = d_vr xG + + BEGIN_SERIALIZE() + FIELD(k_m) + FIELD(k_vb) + FIELD(d_vr) + FIELD(d_fa) + FIELD(s_ga) + FIELD(s_ct) + FIELD(K_s_base) + FIELD(D_vr) + FIELD(D_fa) + FIELD(D_base) + END_SERIALIZE() +}; + +BLOB_SERIALIZER(ser_JamtisKeys); + +namespace seraphis_wallet +{ + +// forward declaration +class KeyGuard; + +enum class WalletType +{ + Empty, + FilterAssist, + AddressGenerator, + FilterAssistAndAddressGen, + PaymentValidator, + ViewAll, + Master, +}; + +/// KeyContainer +// - it handles (store, load, etc) the private keys. +/// +class KeyContainer +{ +public: + KeyContainer(sp::jamtis::JamtisKeys &&keys, const crypto::chacha_key &key); + + KeyContainer() : m_keys{}, m_encryption_iv{}, m_encrypted{false} {} + + KeyContainer(sp::jamtis::JamtisKeys &&keys, + bool encrypted, + const crypto::chacha_iv encryption_iv); + + // member functions + + /// verify if is encrypted + bool is_encrypted() { return m_encrypted; } + + /// load keys from a file and ensure their validity + bool load_from_keys_file(const std::string &path, const crypto::chacha_key &chacha_key); + + /// check if keys are valid + bool jamtis_keys_valid(const sp::jamtis::JamtisKeys &keys, const crypto::chacha_key &chacha_key); + + sp::jamtis::JamtisKeys &get_keys(const crypto::chacha_key &chacha_key); + + /// get keys, protected by a guard + KeyGuard get_keys_guard(const crypto::chacha_key &chacha_key); + + /// encrypt the keys in-memory + bool encrypt(const crypto::chacha_key &chacha_key); + + /// decrypt the keys in-memory + bool decrypt(const crypto::chacha_key &chacha_key); + + /// generate new keys + void generate_keys(const crypto::chacha_key &chacha_key); + + /// get the wallet type of the loaded keys + WalletType get_wallet_type(); + + /// make jamtis_keys serializable + void make_serializable_jamtis_keys(ser_JamtisKeys &serializable_keys); + + /// recover keys from serializable + void recover_jamtis_keys(const ser_JamtisKeys &ser_keys, sp::jamtis::JamtisKeys &keys_out); + + /// compare the keys of two containers that have the same chacha_key + bool compare_keys(KeyContainer &other, const crypto::chacha_key &chacha_key); + +private: + /// initialization vector + crypto::chacha_iv m_encryption_iv; + + /// struct that contains the keys so that + /// they wouldn't get swapped out of memory + epee::mlocked m_keys; + + /// true if keys are encrypted in memory + bool m_encrypted; +}; + +class KeyGuard +{ +public: + KeyGuard(KeyContainer &, const crypto::chacha_key &); + + KeyGuard(const KeyGuard &other); + + ~KeyGuard(); + +private: + const crypto::chacha_key &m_key; + int m_ref; + KeyContainer &m_container; +}; + +} // namespace seraphis_wallet diff --git a/src/serialization/crypto.h b/src/serialization/crypto.h index 896d00583b..4b05d3bcf8 100644 --- a/src/serialization/crypto.h +++ b/src/serialization/crypto.h @@ -37,6 +37,8 @@ #include "crypto/chacha.h" #include "crypto/crypto.h" #include "crypto/hash.h" +#include "crypto/x25519.h" +#include "binary_archive.h" // read template