diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index de92d905c15..6aeaae92bcc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ env: REMOVE_BUNDLED_PACKAGES : sudo rm -rf /usr/local # ARCH="default" (not "native") ensures, that a different execution host can execute binaries compiled elsewhere. BUILD_DEFAULT_LINUX: 'cmake -S . -B build -D ARCH="default" -D BUILD_TESTS=ON -D ENABLE_FUZZ_TEST=ON -D CMAKE_BUILD_TYPE=Release && cmake --build build --target all && cmake --build build --target wallet_api' - APT_INSTALL_LINUX: 'apt -y install build-essential cargo cmake libboost-all-dev miniupnpc libunbound-dev graphviz doxygen libunwind8-dev pkg-config libssl-dev libzmq3-dev libsodium-dev libhidapi-dev libusb-1.0-0-dev libprotobuf-dev protobuf-compiler ccache curl git' + APT_INSTALL_LINUX: 'apt -y install build-essential cmake libboost-all-dev miniupnpc libunbound-dev graphviz doxygen libunwind8-dev pkg-config libssl-dev libzmq3-dev libsodium-dev libhidapi-dev libusb-1.0-0-dev libprotobuf-dev protobuf-compiler ccache curl git' APT_SET_CONF: | tee -a /etc/apt/apt.conf.d/80-custom << EOF Acquire::Retries "3"; @@ -26,6 +26,12 @@ env: ccache --max-size=150M ccache --set-config=compression=true USE_DEVICE_TREZOR_MANDATORY: ON + INSTALL_RUST: | + curl -O https://static.rust-lang.org/rustup/archive/1.29.0/x86_64-unknown-linux-gnu/rustup-init + echo "4acc9acc76d5079515b46346a485974457b5a79893cfb01112423c89aeb5aa10 rustup-init" | sha256sum -c + chmod +x rustup-init + ./rustup-init -y --default-toolchain 1.93 + echo "$HOME/.cargo/bin" >> $GITHUB_PATH jobs: build-macos: @@ -116,13 +122,7 @@ jobs: - name: install monero dependencies run: ${{env.APT_INSTALL_LINUX}} - name: install rust - # Debian 11 ships Rust 1.48.0. We need >=1.69 to build FCMP++. - run: | - curl -O https://static.rust-lang.org/rustup/archive/1.27.1/x86_64-unknown-linux-gnu/rustup-init - echo "6aeece6993e902708983b209d04c0d1dbb14ebb405ddb87def578d41f920f56d rustup-init" | sha256sum -c - chmod +x rustup-init - ./rustup-init -y --default-toolchain 1.69 - echo "$HOME/.cargo/bin" >> $GITHUB_PATH + run: ${{env.INSTALL_RUST}} - name: configure git run: git config --global --add safe.directory '*' - uses: actions/checkout@v5 @@ -157,6 +157,8 @@ jobs: run: apt update - name: install monero dependencies run: ${{env.APT_INSTALL_LINUX}} + - name: install rust + run: ${{env.INSTALL_RUST}} - name: configure git run: git config --global --add safe.directory '*' - uses: actions/checkout@v5 @@ -200,6 +202,8 @@ jobs: run: apt update - name: install monero dependencies run: ${{env.APT_INSTALL_LINUX}} + - name: install rust + run: ${{env.INSTALL_RUST}} - name: install pip run: apt install -y python3-pip - name: install Python dependencies diff --git a/.github/workflows/depends.yml b/.github/workflows/depends.yml index 31bbb55da9b..3ed8cbccfb6 100644 --- a/.github/workflows/depends.yml +++ b/.github/workflows/depends.yml @@ -26,7 +26,7 @@ jobs: build-cross: runs-on: ubuntu-latest container: - image: ubuntu:22.04 + image: ${{ matrix.toolchain.container || 'ubuntu:22.04' }} env: DEBIAN_FRONTEND: noninteractive CCACHE_TEMPDIR: /tmp/.ccache-temp @@ -38,6 +38,7 @@ jobs: host: "riscv64-linux-gnu" rust_host: "riscv64gc-unknown-linux-gnu" packages: "g++-riscv64-linux-gnu" + container: "ubuntu:26.04" # RandomX v2 requires GCC 14 or above for riscv64 - name: "ARM v8" host: "aarch64-linux-gnu" rust_host: "aarch64-unknown-linux-gnu" @@ -50,6 +51,7 @@ jobs: host: "x86_64-w64-mingw32" rust_host: "x86_64-pc-windows-gnu" packages: "g++-mingw-w64-x86-64" + container: "ubuntu:24.04" # Rust >= 1.70 requires binutils >= 2.40 - name: "x86_64 Linux" host: "x86_64-unknown-linux-gnu" rust_host: "x86_64-unknown-linux-gnu" @@ -91,14 +93,11 @@ jobs: - name: install dependencies run: apt update; apt -y install build-essential cmake pkg-config git ccache ${{ matrix.toolchain.packages }} - name: install rust - # We can't use the latest Rust due to LLVM 17 not working with old `ld`s (such as in Ubuntu 20.04) for RISC-V - # We could update ld (a pain), update Ubuntu (requires a large amount of changes), or downgrade Rust - # We can't use Rust 1.70 due to LLVM 16 requiring ld >= 2.40 when building for Windows run: | - curl -O https://static.rust-lang.org/rustup/archive/1.27.1/x86_64-unknown-linux-gnu/rustup-init - echo "6aeece6993e902708983b209d04c0d1dbb14ebb405ddb87def578d41f920f56d rustup-init" | sha256sum -c + curl -O https://static.rust-lang.org/rustup/archive/1.29.0/x86_64-unknown-linux-gnu/rustup-init + echo "4acc9acc76d5079515b46346a485974457b5a79893cfb01112423c89aeb5aa10 rustup-init" | sha256sum -c chmod +x rustup-init - ./rustup-init -y --default-toolchain 1.69 --target ${{ matrix.toolchain.rust_host }} + ./rustup-init -y --default-toolchain 1.93 --target ${{ matrix.toolchain.rust_host }} echo "$HOME/.cargo/bin" >> $GITHUB_PATH - name: configure git run: git config --global --add safe.directory '*' diff --git a/.gitmodules b/.gitmodules index f711a555bb5..40fb2b826e8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -14,3 +14,10 @@ [submodule "external/gtest"] path = external/gtest url = https://github.com/google/googletest.git +[submodule "external/fcmp-plus-plus"] + path = external/fcmp-plus-plus + url = https://github.com/kayabaNerve/fcmp-plus-plus.git +[submodule "external/mx25519"] + path = external/mx25519 + url = https://github.com/jeffro256/mx25519 + branch = unclamped diff --git a/CMakeLists.txt b/CMakeLists.txt index 2e58c1c8c69..4d96243d4c9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -382,6 +382,7 @@ if(NOT MANUAL_SUBMODULES) check_submodule(external/rapidjson) check_submodule(external/randomx) check_submodule(external/supercop) + check_submodule(external/mx25519) endif() endif() @@ -454,7 +455,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(MINGW) set(DEFAULT_STATIC true) @@ -481,6 +482,7 @@ set(PIC_FLAG "-fPIC") if(STATIC) set(CMAKE_FIND_LIBRARY_SUFFIXES .a ${CMAKE_FIND_LIBRARY_SUFFIXES}) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DZMQ_STATIC") + add_definitions("-DMX25519_STATIC") endif() option(SANITIZE "Use ASAN memory sanitizer" OFF) @@ -1056,6 +1058,9 @@ endif() add_definitions(-DBOOST_ASIO_ENABLE_SEQUENTIAL_STRAND_ALLOCATION) add_definitions(-DBOOST_NO_AUTO_PTR) add_definitions(-DBOOST_UUID_DISABLE_ALIGNMENT) # This restores UUID's std::has_unique_object_representations property +# Boost has two conflicting save/load impls for `std::variant`, one in serialization/variant.hpp, +# and one in serialization/std_variant.hpp. This macro disables the one in variant.hpp. +add_definitions(-DBOOST_NO_CXX17_HDR_VARIANT) include_directories(SYSTEM ${Boost_INCLUDE_DIRS}) if(MINGW) diff --git a/contrib/epee/include/math_helper.h b/contrib/epee/include/math_helper.h index 00d6951799d..f38ecf84532 100644 --- a/contrib/epee/include/math_helper.h +++ b/contrib/epee/include/math_helper.h @@ -27,6 +27,8 @@ #pragma once #include // uint64_t +#include // NULL +#include #ifdef _WIN32 #include // GetSystemTimeAsFileTime @@ -41,6 +43,7 @@ namespace math_helper template class once_a_time { + get_interval m_get_interval; uint64_t get_time() const { #ifdef _WIN32 @@ -61,11 +64,12 @@ namespace math_helper void set_next_interval() { - m_interval = get_interval()(); + m_interval = m_get_interval(); } public: - once_a_time() + once_a_time(get_interval gi = get_interval()) + : m_get_interval(std::move(gi)) { m_last_worked_time = 0; if(!start_immediate) diff --git a/contrib/epee/include/net/http_protocol_handler.h b/contrib/epee/include/net/http_protocol_handler.h index 8b73964dd2b..5b8aa0eb6fe 100644 --- a/contrib/epee/include/net/http_protocol_handler.h +++ b/contrib/epee/include/net/http_protocol_handler.h @@ -36,6 +36,7 @@ #include "net_utils_base.h" #include "http_auth.h" #include "http_base.h" +#include "syncobj.h" #undef MONERO_DEFAULT_LOG_CATEGORY #define MONERO_DEFAULT_LOG_CATEGORY "net.http" diff --git a/contrib/epee/include/net/levin_protocol_handler_async.h b/contrib/epee/include/net/levin_protocol_handler_async.h index 9e410f59b52..1c05f31d99d 100644 --- a/contrib/epee/include/net/levin_protocol_handler_async.h +++ b/contrib/epee/include/net/levin_protocol_handler_async.h @@ -40,6 +40,8 @@ #include "time_helper.h" #include "int-util.h" +#include "cryptonote_basic/connection_context.h" + #include #include @@ -480,7 +482,7 @@ class async_protocol_handler buff_to_invoke = {buff_to_invoke.data(), std::size_t(inner_size)}; - const size_t max_bytes = m_connection_context.get_max_bytes(m_current_head.m_command); + const size_t max_bytes = cryptonote::get_command_max_bytes(m_current_head.m_command); if(buff_to_invoke.size() > std::min(max_packet_size, max_bytes)) { MERROR(m_connection_context << "Maximum packet size exceed!, m_max_packet_size = " << std::min(max_packet_size, max_bytes) @@ -580,7 +582,7 @@ class async_protocol_handler m_cache_in_buffer.erase(sizeof(bucket_head2)); m_state = stream_state_body; m_oponent_protocol_ver = m_current_head.m_protocol_version; - const size_t max_bytes = m_connection_context.get_max_bytes(m_current_head.m_command); + const size_t max_bytes = cryptonote::get_command_max_bytes(m_current_head.m_command); if(m_current_head.m_cb > std::min(max_packet_size, max_bytes)) { LOG_ERROR_CC(m_connection_context, "Maximum packet size exceed!, m_max_packet_size = " << std::min(max_packet_size, max_bytes) diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index dd65593ccb4..b16723b63f0 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -67,3 +67,4 @@ add_subdirectory(db_drivers) add_subdirectory(easylogging++) add_subdirectory(qrcodegen) add_subdirectory(randomx EXCLUDE_FROM_ALL) +add_subdirectory(mx25519) diff --git a/external/mx25519 b/external/mx25519 new file mode 160000 index 00000000000..e808a6406b2 --- /dev/null +++ b/external/mx25519 @@ -0,0 +1 @@ +Subproject commit e808a6406b254091f4ed83bf8ea35f032da7f0b7 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6190b40f830..f43fec4dedd 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -83,12 +83,15 @@ endfunction () include(Version) monero_add_library(version SOURCES ${CMAKE_BINARY_DIR}/version.cpp DEPENDS genversion) +add_subdirectory(carrot_core) +add_subdirectory(carrot_impl) add_subdirectory(common) add_subdirectory(crypto) add_subdirectory(ringct) add_subdirectory(checkpoints) add_subdirectory(cryptonote_basic) add_subdirectory(cryptonote_core) +add_subdirectory(fcmp_pp) add_subdirectory(lmdb) add_subdirectory(multisig) add_subdirectory(net) diff --git a/src/blockchain_db/CMakeLists.txt b/src/blockchain_db/CMakeLists.txt index 1e2f06809d8..df979d96822 100644 --- a/src/blockchain_db/CMakeLists.txt +++ b/src/blockchain_db/CMakeLists.txt @@ -46,6 +46,7 @@ target_link_libraries(blockchain_db common cncrypto cryptonote_basic + fcmp_pp ringct_basic ${LMDB_LIBRARY} ${Boost_FILESYSTEM_LIBRARY} diff --git a/src/blockchain_db/blockchain_db.cpp b/src/blockchain_db/blockchain_db.cpp index 72492f9ae98..23cb1b051dd 100644 --- a/src/blockchain_db/blockchain_db.cpp +++ b/src/blockchain_db/blockchain_db.cpp @@ -179,7 +179,7 @@ void BlockchainDB::pop_block() pop_block(blk, txs); } -void BlockchainDB::add_transaction(const crypto::hash& blk_hash, const transaction& tx, const epee::span blob, const crypto::hash* tx_hash_ptr, const crypto::hash* tx_prunable_hash_ptr) +void BlockchainDB::add_transaction(const crypto::hash& blk_hash, const transaction& tx, epee::span blob, const std::unordered_map &transparent_amount_commitments, const crypto::hash* tx_hash_ptr, const crypto::hash* tx_prunable_hash_ptr) { bool miner_tx = false; crypto::hash tx_hash, tx_prunable_hash; @@ -233,10 +233,12 @@ void BlockchainDB::add_transaction(const crypto::hash& blk_hash, const transacti if (miner_tx && tx.version == 2) { cryptonote::tx_out vout = tx.vout[i]; - rct::key commitment = rct::zeroCommit(vout.amount); + const auto commitment_it = transparent_amount_commitments.find(vout.amount); + if (commitment_it == transparent_amount_commitments.end()) + throw std::runtime_error("Failed to get miner tx commitment, aborting"); vout.amount = 0; amount_output_indices[i] = add_output(tx_hash, vout, i, tx.unlock_time, - &commitment); + &commitment_it->second); } else { @@ -244,6 +246,7 @@ void BlockchainDB::add_transaction(const crypto::hash& blk_hash, const transacti tx.version > 1 ? &tx.rct_signatures.outPk[i].mask : NULL); } } + add_tx_amount_output_indices(tx_id, amount_output_indices); } @@ -253,6 +256,7 @@ uint64_t BlockchainDB::add_block( const std::pair& blck , const difficulty_type& cumulative_difficulty , const uint64_t& coins_generated , const std::vector>& txs + , const std::unordered_map& transparent_amount_commitments ) { const block &blk = blck.first; @@ -274,7 +278,7 @@ uint64_t BlockchainDB::add_block( const std::pair& blck uint64_t num_rct_outs = 0; blobdata miner_bd = tx_to_blob(blk.miner_tx); - add_transaction(blk_hash, blk.miner_tx, epee::strspan(miner_bd)); + add_transaction(blk_hash, blk.miner_tx, epee::strspan(miner_bd), transparent_amount_commitments); if (blk.miner_tx.version == 2) num_rct_outs += blk.miner_tx.vout.size(); int tx_i = 0; @@ -282,7 +286,7 @@ uint64_t BlockchainDB::add_block( const std::pair& blck for (const std::pair& tx : txs) { tx_hash = blk.tx_hashes[tx_i]; - add_transaction(blk_hash, tx.first, epee::strspan(tx.second), &tx_hash); + add_transaction(blk_hash, tx.first, epee::strspan(tx.second), transparent_amount_commitments, &tx_hash); for (const auto &vout: tx.first.vout) { if (vout.amount == 0) @@ -303,7 +307,311 @@ uint64_t BlockchainDB::add_block( const std::pair& blck ++num_calls; - return prev_height; + return ++prev_height; +} + +void BlockchainDB::advance_tree(const uint64_t blk_idx, const std::vector &known_new_outputs) +{ + LOG_PRINT_L3("BlockchainDB::" << __func__); + + // Get the earliest possible last locked block of outputs created in blk_idx + const uint64_t earliest_last_locked_block = cryptonote::get_default_last_locked_block_index(blk_idx); + + // If we're advancing the genesis block, make sure to initialize the tree + if (blk_idx == 0) + { + // Expected: tree meta table is currently empty + + // We grow the first blocks with empty outputs, since no outputs in this range should be spendable yet + for (uint64_t new_blk_idx = blk_idx; new_blk_idx < earliest_last_locked_block; ++new_blk_idx) + { + this->grow_tree(new_blk_idx, {}); + } + } + // Expected: earliest_last_locked_block == last block idx + 1 in tree meta + + // Now we can advance the tree 1 block + auto unlocked_outputs = this->get_outs_at_last_locked_block_idx(earliest_last_locked_block); + + // Include known new outputs if provided + unlocked_outputs.insert(unlocked_outputs.end(), known_new_outputs.begin(), known_new_outputs.end()); + + // Grow the tree with outputs that are spendable once the earliest_last_locked_block is in the chain + this->grow_tree(earliest_last_locked_block, std::move(unlocked_outputs)); + + // Now that we've used the unlocked leaves to grow the tree, we delete them from the locked outputs table + this->del_locked_outs_at_block_idx(earliest_last_locked_block); +} + +void BlockchainDB::grow_tree(const uint64_t blk_idx, std::vector &&new_outputs) +{ + LOG_PRINT_L3("BlockchainDB::" << __func__); + + MDEBUG("Growing tree usable once block " << blk_idx << " is in the chain"); + + // Get the number of leaf tuples that exist in the current tree + const uint64_t old_n_leaf_tuples = this->get_n_leaf_tuples(); + + if (blk_idx == 0) + CHECK_AND_ASSERT_THROW_MES(old_n_leaf_tuples == 0, "Tree is not empty at blk idx 0"); + + // Get the prev block's tree edge (i.e. the current tree edge before growing) + std::vector prev_tree_edge; + uint64_t prev_blk_idx = 0; + if (blk_idx > 0) + { + prev_blk_idx = blk_idx - 1; + + // Make sure tree tip lines up to expected block + const uint64_t tree_block_idx = this->get_tree_block_idx(); + + CHECK_AND_ASSERT_THROW_MES(tree_block_idx == prev_blk_idx, + "Unexpected tree block idx mismatch to prev block (" + std::to_string(tree_block_idx) + " vs " + std::to_string(prev_blk_idx) + ")"); + + prev_tree_edge = this->get_tree_edge(prev_blk_idx); + } + + // We re-save the prev tree edge at this next block if the tree doesn't grow + const auto save_prev_tree_edge = [&, this]() { this->save_tree_meta(blk_idx, old_n_leaf_tuples, prev_tree_edge); }; + if (new_outputs.empty()) + { + save_prev_tree_edge(); + return; + } + + // Set the tree's existing last hashes from the existing edge + const auto last_hashes = m_curve_trees->tree_edge_to_last_hashes(prev_tree_edge); + + // Use the number of leaf tuples and the existing last hashes to get a struct we can use to extend the tree + auto tree_extension = m_curve_trees->get_tree_extension(old_n_leaf_tuples, last_hashes, {std::move(new_outputs)}, false/*use_fast_torsion_check*/); + if (tree_extension.leaves.tuples.empty()) + { + save_prev_tree_edge(); + return; + } + + const auto compressed_tree_extension = m_curve_trees->compress_tree_extension(std::move(tree_extension)); + const auto tree_edge = this->grow_with_tree_extension(compressed_tree_extension); + + const uint64_t new_n_leaf_tuples = compressed_tree_extension.leaves.tuples.size() + old_n_leaf_tuples; + this->save_tree_meta(blk_idx, new_n_leaf_tuples, tree_edge); +} + +void BlockchainDB::trim_block() +{ + LOG_PRINT_L3("BlockchainDB::" << __func__); + + const uint64_t n_blocks = this->height(); + if (n_blocks == 0) + return; + + const uint64_t removing_block_idx = n_blocks - 1; + + // Get the earliest possible last locked block of outputs created in removing_block_idx + const uint64_t default_last_locked_block = cryptonote::get_default_last_locked_block_index(removing_block_idx); + const uint64_t tree_block_idx = this->get_tree_block_idx(); + + CHECK_AND_ASSERT_THROW_MES(tree_block_idx > 0, "tree block idx must be >0"); + CHECK_AND_ASSERT_THROW_MES(tree_block_idx == default_last_locked_block, + "Unexpected tree block idx mismatch (" + std::to_string(tree_block_idx) + " vs " + std::to_string(default_last_locked_block) + ")"); + + const uint64_t prev_tree_block_idx = tree_block_idx - 1; + + MDEBUG("Trimming tree to block " << prev_tree_block_idx << " (removing block " << removing_block_idx << ")"); + + // Read n leaf tuples from the prev tree block to see how how many leaves + // should remain in the tree after trimming a block from the tree. + const uint64_t new_n_leaf_tuples = this->get_block_n_leaf_tuples(prev_tree_block_idx); + + // Trim the tree to the new n leaf tuples + this->trim_tree(new_n_leaf_tuples, tree_block_idx); + + // Remove block from tree meta + this->del_tree_meta(tree_block_idx); +} + +void BlockchainDB::trim_tree(const uint64_t new_n_leaf_tuples, const uint64_t trim_block_idx) +{ + LOG_PRINT_L3("BlockchainDB::" << __func__); + + const uint64_t old_n_leaf_tuples = this->trim_leaves(new_n_leaf_tuples, trim_block_idx); + + // If nothing to trim, return + if (old_n_leaf_tuples == new_n_leaf_tuples) + return; + + if (new_n_leaf_tuples == 0) + { + // Empty the tree + this->trim_layers(new_n_leaf_tuples, {}/*n_elems_per_layer*/, {}/*prev_tree_edge*/, 0/*expected_root_idx*/); + return; + } + + // Trim the expected layers + const auto n_elems_per_layer = m_curve_trees->n_elems_per_layer(new_n_leaf_tuples); + const auto prev_tree_edge = this->get_tree_edge(trim_block_idx - 1); + const uint64_t expected_root_idx = m_curve_trees->n_layers(new_n_leaf_tuples) - 1; + this->trim_layers(new_n_leaf_tuples, n_elems_per_layer, prev_tree_edge, expected_root_idx); +} + +std::pair BlockchainDB::get_last_path(const uint64_t block_idx) const +{ + LOG_PRINT_L3("BlockchainDB::" << __func__); + + db_rtxn_guard rtxn_guard(this); + + // See how many leaves were in the tree at the given block + const uint64_t block_n_leaf_tuples = this->get_block_n_leaf_tuples(block_idx); + if (block_n_leaf_tuples == 0) + return { 0, {} }; + + // Get path elems from the block's last path (gets *current* path elem state) + const auto last_path_indexes = m_curve_trees->get_path_indexes(block_n_leaf_tuples, block_n_leaf_tuples - 1); + auto path = this->get_path(last_path_indexes); + + // See what the last hashes at every layer for the provided block were (gets *old* path elem state) + const std::vector tree_edge = this->get_tree_edge(block_idx); + CHECK_AND_ASSERT_THROW_MES(tree_edge.size(), "get_last_path: empty tree edge"); + CHECK_AND_ASSERT_THROW_MES(tree_edge.size() == path.layer_chunks.size(), "get_last_path: mismatched tree edge size to path layer chunks"); + + // Use tree edge at the provided block to set the last hash for each layer (so path state reflects old state) + for (std::size_t i = 0; i < path.layer_chunks.size(); ++i) + { + CHECK_AND_ASSERT_THROW_MES(path.layer_chunks[i].elems.size(), "get_last_path: empty path"); + path.layer_chunks[i].elems.back() = tree_edge[i]; + } + + return { block_n_leaf_tuples, path }; +} + +uint64_t BlockchainDB::get_path_by_unified_id(const std::vector &unified_ids, + const uint64_t as_of_n_blocks, + std::vector &leaf_idxs_out, + std::vector &paths_out) const +{ + LOG_PRINT_L3("BlockchainDB::" << __func__); + + db_rtxn_guard rtxn_guard(this); + + // Initialize result vectors with 0 values. If outptut is not in the tree, + // result vectors kept as 0 values + leaf_idxs_out = std::vector(unified_ids.size(), 0); + paths_out = std::vector(unified_ids.size(), fcmp_pp::CompressedPath{}); + + if (unified_ids.empty()) + return 0; + + const uint64_t cur_n_blocks = this->height(); + if (cur_n_blocks == 0) + return 0; + + // We're getting path data assuming chain tip is as_of_block_idx + const uint64_t as_of_block_idx = as_of_n_blocks ? (as_of_n_blocks - 1) : (cur_n_blocks - 1); + + CHECK_AND_ASSERT_THROW_MES(as_of_block_idx <= this->get_tree_block_idx(), "get_path_by_unified_id: as_of_block_idx is higher than highest tree block idx"); + + // TODO: de-duplicate db reads where possible (steps 1, 2, 3, 5, 6 can all be de-dup'd especially 6) + // TODO: return consolidated path + // Note: a table mapping output id -> leaf idx would allow skipping to step 5 + + // 1. Read DB for tx out indexes with global output id + std::vector tois; + tois.reserve(unified_ids.size()); + for (const auto &uid : unified_ids) + { + // Throws if output not found + tois.emplace_back(this->get_output_tx_and_index_from_unified(uid)); + } + + // 2. Read DB for output metadata {unlock_time, created block idx} + std::vector out_keys; + out_keys.reserve(unified_ids.size()); + for (const auto &tois : tois) + { + cryptonote::transaction _; + const auto tx_out_keys = this->get_tx_output_data(tois.first, _); + CHECK_AND_ASSERT_THROW_MES(tois.second < tx_out_keys.size(), "get_path_by_unified_id: tx out keys too small"); + out_keys.emplace_back(tx_out_keys.at(tois.second)); + } + + // 3. Determine each output's last locked block + std::vector last_locked_block_idxs; + last_locked_block_idxs.reserve(out_keys.size()); + for (const auto &outkey : out_keys) + { + const uint64_t last_locked_block_idx = cryptonote::get_last_locked_block_index(outkey.data.unlock_time, outkey.data.height); + last_locked_block_idxs.emplace_back(last_locked_block_idx); + } + + // 4. Get n leaf tuples at output's last locked block and next block + std::vector> n_leaf_tuple_ranges; + std::vector not_expected_in_tree; + n_leaf_tuple_ranges.reserve(last_locked_block_idxs.size()); + not_expected_in_tree.reserve(last_locked_block_idxs.size()); + for (const uint64_t block_idx : last_locked_block_idxs) + { + if (block_idx > as_of_block_idx) + { + not_expected_in_tree.push_back(true); + n_leaf_tuple_ranges.push_back({0, 0}); + continue; + } + + const uint64_t n_leaf_tuples = this->get_block_n_leaf_tuples(block_idx); + + const uint64_t next_block_idx = block_idx + 1; + const uint64_t next_n_leaf_tuples = this->get_block_n_leaf_tuples(next_block_idx); + + n_leaf_tuple_ranges.push_back({n_leaf_tuples, next_n_leaf_tuples}); + + // Output is still locked if there are no leaf tuples for the block it's in + not_expected_in_tree.push_back(n_leaf_tuples == 0 && next_n_leaf_tuples == 0); + } + + // 5. Find leaf idxs by output id, using leaf tuple ranges to narrow search + for (std::size_t i = 0; i < out_keys.size(); ++i) + { + // If the output is still expected locked, then it won't have a leaf idx + if (not_expected_in_tree.at(i)) + continue; + const uint64_t unified_id = out_keys.at(i).unified_id; + const auto tuple_range = n_leaf_tuple_ranges.at(i); + leaf_idxs_out.at(i) = this->find_leaf_idx_by_unified_id_bounded_search(unified_id, tuple_range.first, tuple_range.second); + } + + // 6. Use leaf idxs to get paths + const uint64_t n_leaf_tuples = this->get_block_n_leaf_tuples(as_of_block_idx); + const auto last_path_idxs = m_curve_trees->get_path_indexes(n_leaf_tuples, n_leaf_tuples - 1); + const auto last_path = this->get_last_path(as_of_block_idx); + for (std::size_t i = 0; i < leaf_idxs_out.size(); ++i) + { + if (not_expected_in_tree.at(i)) + continue; + + // Read path from the db using path indexes + const auto path_idxs = m_curve_trees->get_path_indexes(n_leaf_tuples, leaf_idxs_out.at(i)); + auto path = this->get_path(path_idxs); + + CHECK_AND_ASSERT_THROW_MES(path.leaves.size() && path.layer_chunks.size(), "get_path_by_unified_id: empty path"); + + // If the path is part of the last path, then we'll need to update the last + // elem in each layer + for (uint8_t i = 0; i < path_idxs.layers.size(); ++i) + { + if (last_path_idxs.layers.at(i).second != path_idxs.layers.at(i).second) + continue; + + CHECK_AND_ASSERT_THROW_MES(path.layer_chunks.at(i).elems.size(), "get_path_by_unified_id: empty layer in path"); + CHECK_AND_ASSERT_THROW_MES(path.layer_chunks.at(i).elems.size() == last_path.second.layer_chunks.at(i).elems.size(), + "get_path_by_unified_id: unexpected size of last path"); + + path.layer_chunks.at(i).elems.back() = last_path.second.layer_chunks.at(i).elems.back(); + } + + paths_out.at(i) = std::move(path); + } + + return n_leaf_tuples; } void BlockchainDB::set_hard_fork(HardFork* hf) diff --git a/src/blockchain_db/blockchain_db.h b/src/blockchain_db/blockchain_db.h index e3888487d9d..02f6b6cec9d 100644 --- a/src/blockchain_db/blockchain_db.h +++ b/src/blockchain_db/blockchain_db.h @@ -32,6 +32,8 @@ #include #include +#include +#include #include #include "common/command_line.h" #include "crypto/hash.h" @@ -40,6 +42,8 @@ #include "cryptonote_basic/difficulty.h" #include "cryptonote_basic/hardfork.h" #include "cryptonote_protocol/enums.h" +#include "fcmp_pp/curve_trees.h" +#include "fcmp_pp/fcmp_pp_types.h" /** \file * Cryptonote Blockchain Database Interface @@ -116,6 +120,16 @@ enum class relay_category : uint8_t bool matches_category(relay_method method, relay_category category) noexcept; +#pragma pack(push, 1) +// This MUST be identical to output_data_t, without the extra rct data at the end +struct pre_rct_output_data_t +{ + crypto::public_key pubkey; //!< the output's public key (for spend verification) + uint64_t unlock_time; //!< the output's unlock time (or height) + uint64_t height; //!< the height of the block which created the output +}; +#pragma pack(pop) + #pragma pack(push, 1) /** @@ -130,6 +144,18 @@ struct output_data_t }; #pragma pack(pop) +typedef struct pre_rct_outkey { + uint64_t amount_index; + uint64_t unified_id; + pre_rct_output_data_t data; +} pre_rct_outkey; + +typedef struct outkey { + uint64_t amount_index; + uint64_t unified_id; + output_data_t data; +} outkey; + #pragma pack(push, 1) struct tx_data_t { @@ -194,6 +220,7 @@ struct txpool_tx_meta_t static_assert(sizeof(txpool_tx_meta_t) == 192, "possible DB migration needed for changes to txpool_tx_meta_t"); static_assert(offsetof(txpool_tx_meta_t, valid_input_verification_id) == 160, "verif ID wrong alignment"); + #define DBF_SAFE 1 #define DBF_FAST 2 #define DBF_FASTEST 4 @@ -477,8 +504,9 @@ class BlockchainDB * future, this tracking (of the number, at least) should be moved to * this class, as it is necessary and the same among all BlockchainDB. * - * It returns an amount output index, which is the index of the output - * for its specified amount. + * It returns the output indexes, which contains an amount output index (the + * index of the output for its specified amount) and output id (the global + * index of the output among all outputs of any amount). * * This data should be stored in such a manner that the only thing needed to * reverse the process is the tx_out. @@ -534,6 +562,35 @@ class BlockchainDB */ virtual void remove_spent_key(const crypto::key_image& k_image) = 0; + // + // Curve tree related db calls (private) + // + + // TODO: descriptions + virtual std::vector get_outs_at_last_locked_block_idx(uint64_t block_id) const = 0; + + virtual void del_locked_outs_at_block_idx(uint64_t block_idx) = 0; + + virtual uint64_t get_tree_block_idx() const = 0; + + virtual std::vector get_tree_edge(uint64_t block_id) const = 0; + + virtual uint64_t trim_leaves(const uint64_t new_n_leaf_tuples, const uint64_t trim_block_idx) = 0; + + virtual void trim_layers(const uint64_t new_n_leaf_tuples, + const std::vector &n_elems_per_layer, + const std::vector &prev_tree_edge, + const uint64_t expected_root_idx) = 0; + + virtual void save_tree_meta(const uint64_t block_idx, const uint64_t n_leaf_tuples, const std::vector &tree_edge) = 0; + + virtual void del_tree_meta(const uint64_t block_idx) = 0; + + virtual std::vector grow_with_tree_extension(const fcmp_pp::CompressedTreeExtension &tree_extension) = 0; + + virtual fcmp_pp::CompressedPath get_path(const fcmp_pp::PathIndexes &path_indexes) const = 0; + + virtual uint64_t find_leaf_idx_by_unified_id_bounded_search(uint64_t unified_id, uint64_t leaf_idx_start, uint64_t leaf_idx_end) const = 0; /********************************************************************* * private concrete members @@ -573,10 +630,11 @@ class BlockchainDB * @param blk_hash hash of the block which has the transaction * @param tx the transaction to add * @param blob for `tx` + * @param transparent_amount_commitments pre-calculated transparent amount commitments * @param tx_hash_ptr the hash of the transaction, if already calculated * @param tx_prunable_hash_ptr the hash of the prunable part of the transaction, if already calculated */ - void add_transaction(const crypto::hash& blk_hash, const transaction& tx, epee::span blob, const crypto::hash* tx_hash_ptr = NULL, const crypto::hash* tx_prunable_hash_ptr = NULL); + void add_transaction(const crypto::hash& blk_hash, const transaction& tx, epee::span blob, const std::unordered_map &transparent_amount_commitments, const crypto::hash* tx_hash_ptr = NULL, const crypto::hash* tx_prunable_hash_ptr = NULL); mutable uint64_t time_tx_exists = 0; //!< a performance metric uint64_t time_commit1 = 0; //!< a performance metric @@ -584,12 +642,14 @@ class BlockchainDB HardFork* m_hardfork; + std::shared_ptr m_curve_trees; + public: /** * @brief An empty constructor. */ - BlockchainDB(): m_hardfork(NULL), m_open(false) { } + BlockchainDB(): m_hardfork(NULL), m_open(false), m_curve_trees() { } /** * @brief An empty destructor. @@ -822,6 +882,7 @@ class BlockchainDB * @param cumulative_difficulty the accumulated difficulty after this block * @param coins_generated the number of coins generated total after this block * @param txs the transactions in the block + * @param transparent_amount_commitments pre-calculated transparent amount commitments * * @return the height of the chain post-addition */ @@ -831,6 +892,7 @@ class BlockchainDB , const difficulty_type& cumulative_difficulty , const uint64_t& coins_generated , const std::vector>& txs + , const std::unordered_map& transparent_amount_commitments ); /** @@ -1384,6 +1446,16 @@ class BlockchainDB */ virtual uint64_t get_tx_block_height(const crypto::hash& h) const = 0; + // returns the total number of outputs in the chain (of all amounts) + /** + * @brief fetches the number of outputs in the chain + * + * The subclass should return a count of outputs, or zero if there are none. + * + * @return the total number of outputs in the chain (of all amounts) + */ + virtual uint64_t num_outputs() const = 0; + // returns the total number of outputs of amount /** * @brief fetches the number of outputs of a given amount @@ -1423,19 +1495,19 @@ class BlockchainDB * * @return the requested output data */ - virtual output_data_t get_output_key(const uint64_t& amount, const uint64_t& index, bool include_commitmemt = true) const = 0; + virtual outkey get_output_key(const uint64_t& amount, const uint64_t& index, bool include_commitmemt = true) const = 0; /** * @brief gets an output's tx hash and index * * The subclass should return the hash of the transaction which created the - * output with the global index given, as well as its index in that transaction. + * output with the unified index given, as well as its index in that transaction. * - * @param index an output's global index + * @param index an output's unified index * * @return the tx hash and output index */ - virtual tx_out_index get_output_tx_and_index_from_global(const uint64_t& index) const = 0; + virtual tx_out_index get_output_tx_and_index_from_unified(const uint64_t& index) const = 0; /** * @brief gets an output's tx hash and index @@ -1476,7 +1548,19 @@ class BlockchainDB * @param outputs return-by-reference a list of outputs' metadata */ virtual void get_output_key(const epee::span &amounts, const std::vector &offsets, std::vector &outputs, bool allow_partial = false) const = 0; - + + /** + * @brief fetches tx out data with the given hash + * + * If the transaction does not exist, the subclass should throw TX_DNE. + * + * @param h the hash to look for + * @param tx return-by-reference pruned transaction + * + * @return the transaction's associated output data + */ + virtual std::vector get_tx_output_data(const crypto::hash& h, cryptonote::transaction &tx) const = 0; + /* * FIXME: Need to check with git blame and ask what this does to * document it @@ -1766,6 +1850,79 @@ class BlockchainDB */ virtual bool for_all_alt_blocks(std::function f, bool include_blob = false) const = 0; + // + // Curve tree related db calls (public) + // + + // TODO: descriptions + virtual void advance_tree(const uint64_t block_idx, const std::vector &known_new_outputs); + + void grow_tree(const uint64_t block_idx, std::vector &&new_outputs); + + void trim_block(); + + void trim_tree(const uint64_t new_n_leaf_tuples, const uint64_t trim_block_idx); + + std::pair get_last_path(const uint64_t block_idx) const; + + uint64_t get_path_by_unified_id(const std::vector &unified_ids, + const uint64_t as_of_n_blocks, + std::vector &leaf_idxs_out, + std::vector &paths_out) const; + + /** + * @brief add outs to locked outputs tables + * + * If any of this cannot be done, the subclass should throw the corresponding + * subclass of DB_EXCEPTION + * + * @param outs_by_last_locked_block outs grouped by last locked block + * @param timelocked_outputs custom timelocked outputs + * + */ + virtual void add_locked_outs(const fcmp_pp::OutsByLastLockedBlock& outs_by_last_locked_block, const std::unordered_map& timelocked_outputs) = 0; + + // TODO: descriptions + virtual bool audit_tree(const uint64_t expected_n_leaf_tuples) const = 0; + virtual uint64_t get_n_leaf_tuples() const = 0; + virtual uint64_t get_block_n_leaf_tuples(const uint64_t block_idx) const = 0; + + /** + * @brief return tree's root and n_tree_layers at a specific block idx + * + * Gets the tree root and n_tree_layers composed of all valid spendable + * outputs when blk_idx is the tip of the chain. + * + * If the chain tip is block index n, and `blk_idx == n`, then this will + * return the tree root and n layers in a tree composed of all valid + * spendable outputs in the chain at that time. + * + * If the chain tip is block index n, and `blk_idx == n-1`, then this will + * return the tree root and n layers in a tree composed of all valid + * spendable outputs in the chain *when the chain tip was block index n - 1*. + * + * Note that the tree stored in the database may not match up with the tree + * root returned here, since the tree stored in the db may have grown past + * the chain tip's tree, with outputs that will unlock in future blocks. + * + * This function throws if the db does not have a tree root stored for the + * given blk_idx. + * + * @param blk_idx the state of the tree as of this block index + * @param tree_root_out return-by-reference tree root + * + * @return n tree layers when blk_idx was chain tip + */ + virtual uint8_t get_tree_root_at_blk_idx(const uint64_t blk_idx, crypto::ec_point &tree_root_out) const = 0; + + /** + * @brief return custom timelocked outputs after the provided block idx + * + * @param start_block_idx + * + * @return custom timelocked outputs grouped by last locked block + */ + virtual fcmp_pp::OutsByLastLockedBlock get_custom_timelocked_outputs(uint64_t start_block_idx) const = 0; // // Hard fork related storage @@ -1848,22 +2005,45 @@ class BlockchainDB }; // class BlockchainDB -class db_txn_guard +class db_rtxn_guard { public: - db_txn_guard(BlockchainDB *db, bool readonly): db(db), readonly(readonly), active(false) + db_rtxn_guard(const BlockchainDB *db): db(db), active(false) { - if (readonly) - { - active = db->block_rtxn_start(); - } - else + active = db->block_rtxn_start(); + } + virtual ~db_rtxn_guard() + { + stop(); + } + void stop() + { + if (active) { - db->block_wtxn_start(); - active = true; + db->block_rtxn_stop(); + active = false; } } - virtual ~db_txn_guard() + void abort() + { + db->block_rtxn_abort(); + active = false; + } + +private: + const BlockchainDB *db; + bool active; +}; + +class db_wtxn_guard +{ +public: + db_wtxn_guard(BlockchainDB *db): db(db), active(false) + { + db->block_wtxn_start(); + active = true; + } + virtual ~db_wtxn_guard() { stop(); } @@ -1871,31 +2051,21 @@ class db_txn_guard { if (active) { - if (readonly) - db->block_rtxn_stop(); - else - db->block_wtxn_stop(); + db->block_wtxn_stop(); active = false; } } void abort() { - if (readonly) - db->block_rtxn_abort(); - else - db->block_wtxn_abort(); + db->block_wtxn_abort(); active = false; } private: BlockchainDB *db; - bool readonly; bool active; }; -class db_rtxn_guard: public db_txn_guard { public: db_rtxn_guard(BlockchainDB *db): db_txn_guard(db, true) {} }; -class db_wtxn_guard: public db_txn_guard { public: db_wtxn_guard(BlockchainDB *db): db_txn_guard(db, false) {} }; - BlockchainDB *new_db(); } // namespace cryptonote diff --git a/src/blockchain_db/lmdb/db_lmdb.cpp b/src/blockchain_db/lmdb/db_lmdb.cpp index 82de0a8cfaa..1bf196a4845 100644 --- a/src/blockchain_db/lmdb/db_lmdb.cpp +++ b/src/blockchain_db/lmdb/db_lmdb.cpp @@ -43,6 +43,7 @@ #include "common/pruning.h" #include "cryptonote_basic/cryptonote_format_utils.h" #include "crypto/crypto.h" +#include "fcmp_pp/fcmp_pp_serialization.h" #include "profile_tools.h" #include "ringct/rctOps.h" @@ -58,21 +59,11 @@ using epee::string_tools::pod_to_hex; using namespace crypto; // Increase when the DB structure changes -#define VERSION 5 +#define VERSION 6 namespace { -#pragma pack(push, 1) -// This MUST be identical to output_data_t, without the extra rct data at the end -struct pre_rct_output_data_t -{ - crypto::public_key pubkey; //!< the output's public key (for spend verification) - uint64_t unlock_time; //!< the output's unlock time (or height) - uint64_t height; //!< the height of the block which created the output -}; -#pragma pack(pop) - template inline void throw0(const T &e) { @@ -149,6 +140,14 @@ int BlockchainLMDB::compare_uint64(const MDB_val *a, const MDB_val *b) return (va < vb) ? -1 : va > vb; } +int BlockchainLMDB::compare_uint8(const MDB_val *a, const MDB_val *b) +{ + uint8_t va, vb; + memcpy(&va, a->mv_data, sizeof(va)); + memcpy(&vb, b->mv_data, sizeof(vb)); + return (va < vb) ? -1 : va > vb; +} + int BlockchainLMDB::compare_hash32(const MDB_val *a, const MDB_val *b) { uint32_t *va = (uint32_t*) a->mv_data; @@ -203,6 +202,14 @@ namespace * * spent_keys input hash - * + * locked_outputs block ID [{UnifiedOutput}...] + * leaves leaf_idx {mdb_leaf} + * layers layer_idx [{child_chunk_idx, child_chunk_hash}...] + * tree_edges block ID [child_chunk_hash] + * tree_meta block ID n_leaf_tuples + * + * timelocked_outputs block ID [{UnifiedOutput}...] + * * txpool_meta txn hash txn metadata * txpool_blob txn hash txn blob * @@ -214,7 +221,8 @@ namespace * attached as a prefix on the Data to serve as the DUPSORT key. * (DUPFIXED saves 8 bytes per record.) * - * The output_amounts table doesn't use a dummy key, but uses DUPSORT. + * The output_amounts, locked_outputs, layers, and timelocked_outputs + * tables don't use a dummy key, but use DUPSORT. */ const char* const LMDB_BLOCKS = "blocks"; const char* const LMDB_BLOCK_HEIGHTS = "block_heights"; @@ -232,6 +240,15 @@ const char* const LMDB_OUTPUT_TXS = "output_txs"; const char* const LMDB_OUTPUT_AMOUNTS = "output_amounts"; const char* const LMDB_SPENT_KEYS = "spent_keys"; +// Curve trees merkle tree tables +const char* const LMDB_LOCKED_OUTPUTS = "locked_outputs"; +const char* const LMDB_LEAVES = "leaves"; +const char* const LMDB_LAYERS = "layers"; +const char* const LMDB_TREE_EDGES = "tree_edges"; +const char* const LMDB_TREE_META = "tree_meta"; + +const char* const LMDB_TIMELOCKED_OUTPUTS = "timelocked_outputs"; + const char* const LMDB_TXPOOL_META = "txpool_meta"; const char* const LMDB_TXPOOL_BLOB = "txpool_blob"; @@ -337,24 +354,30 @@ typedef struct blk_height { uint64_t bh_height; } blk_height; -typedef struct pre_rct_outkey { - uint64_t amount_index; - uint64_t output_id; - pre_rct_output_data_t data; -} pre_rct_outkey; - -typedef struct outkey { - uint64_t amount_index; - uint64_t output_id; - output_data_t data; -} outkey; - typedef struct outtx { - uint64_t output_id; + uint64_t unified_id; crypto::hash tx_hash; uint64_t local_index; } outtx; +#pragma pack(push, 1) +typedef struct mdb_leaf { + uint64_t leaf_idx; + uint64_t unified_id; +} mdb_leaf; +#pragma pack(pop) +static_assert(sizeof(mdb_leaf) == (8+8), "mdb_leaf unexpected size"); + +typedef struct layer_val { + uint64_t child_chunk_idx; + crypto::ec_point child_chunk_hash; +} layer_val; +static_assert(sizeof(layer_val) == (8+32), "layer_val unexpected size"); + +typedef struct mdb_tree_meta { + uint64_t n_leaf_tuples; +} mdb_tree_meta; + std::atomic mdb_txn_safe::num_active_txns{0}; std::atomic_flag mdb_txn_safe::creation_gate = ATOMIC_FLAG_INIT; @@ -865,6 +888,9 @@ void BlockchainLMDB::remove_block() CURSOR(block_info) CURSOR(block_heights) CURSOR(blocks) + + BlockchainDB::trim_block(); + MDB_val_copy k(m_height - 1); MDB_val h = k; if ((result = mdb_cursor_get(m_cur_block_info, (MDB_val *)&zerokval, &h, MDB_GET_BOTH))) @@ -1093,7 +1119,7 @@ uint64_t BlockchainLMDB::add_output(const crypto::hash& tx_hash, throw0(DB_ERROR(lmdb_error("Failed to get output amount in db transaction: ", result).c_str())); else ok.amount_index = 0; - ok.output_id = m_num_outputs; + ok.unified_id = m_num_outputs; ok.data.pubkey = output_public_key; ok.data.unlock_time = unlock_time; ok.data.height = m_height; @@ -1152,141 +1178,1381 @@ void BlockchainLMDB::remove_tx_outputs(const uint64_t tx_id, const transaction& throw0(DB_ERROR("tx has outputs, but no output indices found")); } - bool is_pseudo_rct = tx.version >= 2 && tx.vin.size() == 1 && tx.vin[0].type() == typeid(txin_gen); - for (size_t i = tx.vout.size(); i-- > 0;) - { - uint64_t amount = is_pseudo_rct ? 0 : tx.vout[i].amount; - remove_output(amount, amount_output_indices[i]); + bool is_pseudo_rct = tx.version >= 2 && tx.vin.size() == 1 && tx.vin[0].type() == typeid(txin_gen); + for (size_t i = tx.vout.size(); i-- > 0;) + { + uint64_t amount = is_pseudo_rct ? 0 : tx.vout[i].amount; + remove_output(amount, amount_output_indices[i]); + } +} + +void BlockchainLMDB::remove_output(const uint64_t amount, const uint64_t& out_index) +{ + LOG_PRINT_L3("BlockchainLMDB::" << __func__); + check_open(); + mdb_txn_cursors *m_cursors = &m_wcursors; + CURSOR(output_amounts); + CURSOR(output_txs); + + MDB_val_set(k, amount); + MDB_val_set(v, out_index); + + auto result = mdb_cursor_get(m_cur_output_amounts, &k, &v, MDB_GET_BOTH); + if (result == MDB_NOTFOUND) + throw1(OUTPUT_DNE("Attempting to get an output index by amount and amount index, but amount not found")); + else if (result) + throw0(DB_ERROR(lmdb_error("DB error attempting to get an output", result).c_str())); + + const pre_rct_outkey *ok = (const pre_rct_outkey *)v.mv_data; + MDB_val_set(otxk, ok->unified_id); + result = mdb_cursor_get(m_cur_output_txs, (MDB_val *)&zerokval, &otxk, MDB_GET_BOTH); + if (result == MDB_NOTFOUND) + { + throw0(DB_ERROR("Unexpected: global output index not found in m_output_txs")); + } + else if (result) + { + throw1(DB_ERROR(lmdb_error("Error adding removal of output tx to db transaction", result).c_str())); + } + + // Remove output from locked outputs table if present. We expect all valid + // outputs to be in the locked outputs table because remove_output is called + // when removing the top block from the chain, and all outputs from the top + // block are expected to be locked until they are at least 10 blocks old (10 + // is the lower bound). An output might not be in the locked outputs table if + // it is invalid, then gets removed from the locked outputs table upon growing + // the tree. + // TODO: test case where we add an invalid output to the chain, grow the tree + // in the block in which that output unlocks, pop blocks to remove that output + // from the chain, then progress the chain again. + CURSOR(locked_outputs); + + const uint64_t last_locked_block = cryptonote::get_last_locked_block_index(ok->data.unlock_time, ok->data.height); + + MDB_val_set(k_block_id, last_locked_block); + MDB_val_set(v_output, ok->unified_id); + + result = mdb_cursor_get(m_cur_locked_outputs, &k_block_id, &v_output, MDB_GET_BOTH); + if (result == MDB_NOTFOUND) + { + // We expect this output is invalid + } + else if (result) + { + throw1(DB_ERROR(lmdb_error("Error adding removal of locked output to db transaction", result).c_str())); + } + else + { + result = mdb_cursor_del(m_cur_locked_outputs, 0); + if (result) + throw0(DB_ERROR(lmdb_error(std::string("Error deleting locked output index ").append(boost::lexical_cast(out_index).append(": ")).c_str(), result).c_str())); + } + + // Remove output from custom timelocked outputs table if present + CURSOR(timelocked_outputs); + + MDB_val_set(k_timelocked_block_id, last_locked_block); + MDB_val_set(v_timelocked_output, ok->unified_id); + + result = mdb_cursor_get(m_cur_timelocked_outputs, &k_timelocked_block_id, &v_timelocked_output, MDB_GET_BOTH); + if (result == MDB_NOTFOUND) + { + // Output is either not timelocked or is invalid + } + else if (result) + { + throw1(DB_ERROR(lmdb_error("Error adding removal of timelocked output to db transaction", result).c_str())); + } + else + { + result = mdb_cursor_del(m_cur_timelocked_outputs, 0); + if (result) + throw0(DB_ERROR(lmdb_error(std::string("Error deleting timelocked output index ").append(boost::lexical_cast(out_index).append(": ")).c_str(), result).c_str())); + } + + result = mdb_cursor_del(m_cur_output_txs, 0); + if (result) + throw0(DB_ERROR(lmdb_error(std::string("Error deleting output index ").append(boost::lexical_cast(out_index).append(": ")).c_str(), result).c_str())); + + // now delete the amount + result = mdb_cursor_del(m_cur_output_amounts, 0); + if (result) + throw0(DB_ERROR(lmdb_error(std::string("Error deleting amount for output index ").append(boost::lexical_cast(out_index).append(": ")).c_str(), result).c_str())); +} + +void BlockchainLMDB::prune_outputs(uint64_t amount) +{ + LOG_PRINT_L3("BlockchainLMDB::" << __func__); + check_open(); + mdb_txn_cursors *m_cursors = &m_wcursors; + CURSOR(output_amounts); + CURSOR(output_txs); + + MINFO("Pruning outputs for amount " << amount); + + MDB_val v; + MDB_val_set(k, amount); + int result = mdb_cursor_get(m_cur_output_amounts, &k, &v, MDB_SET); + if (result == MDB_NOTFOUND) + return; + if (result) + throw0(DB_ERROR(lmdb_error("Error looking up outputs: ", result).c_str())); + + // gather output ids + mdb_size_t num_elems; + mdb_cursor_count(m_cur_output_amounts, &num_elems); + MINFO(num_elems << " outputs found"); + std::vector unified_ids; + unified_ids.reserve(num_elems); + while (1) + { + const pre_rct_outkey *okp = (const pre_rct_outkey *)v.mv_data; + unified_ids.push_back(okp->unified_id); + MDEBUG("output id " << okp->unified_id); + result = mdb_cursor_get(m_cur_output_amounts, &k, &v, MDB_NEXT_DUP); + if (result == MDB_NOTFOUND) + break; + if (result) + throw0(DB_ERROR(lmdb_error("Error counting outputs: ", result).c_str())); + } + if (unified_ids.size() != num_elems) + throw0(DB_ERROR("Unexpected number of outputs")); + + result = mdb_cursor_del(m_cur_output_amounts, MDB_NODUPDATA); + if (result) + throw0(DB_ERROR(lmdb_error("Error deleting outputs: ", result).c_str())); + + for (uint64_t unified_id: unified_ids) + { + MDB_val_set(v, unified_id); + result = mdb_cursor_get(m_cur_output_txs, (MDB_val *)&zerokval, &v, MDB_GET_BOTH); + if (result) + throw0(DB_ERROR(lmdb_error("Error looking up output: ", result).c_str())); + result = mdb_cursor_del(m_cur_output_txs, 0); + if (result) + throw0(DB_ERROR(lmdb_error("Error deleting output: ", result).c_str())); + } +} + +void BlockchainLMDB::add_spent_key(const crypto::key_image& k_image) +{ + LOG_PRINT_L3("BlockchainLMDB::" << __func__); + check_open(); + mdb_txn_cursors *m_cursors = &m_wcursors; + + CURSOR(spent_keys) + + MDB_val k = {sizeof(k_image), (void *)&k_image}; + if (auto result = mdb_cursor_put(m_cur_spent_keys, (MDB_val *)&zerokval, &k, MDB_NODUPDATA)) { + if (result == MDB_KEYEXIST) + throw1(KEY_IMAGE_EXISTS("Attempting to add spent key image that's already in the db")); + else + throw1(DB_ERROR(lmdb_error("Error adding spent key image to db transaction: ", result).c_str())); + } +} + +void BlockchainLMDB::remove_spent_key(const crypto::key_image& k_image) +{ + LOG_PRINT_L3("BlockchainLMDB::" << __func__); + check_open(); + mdb_txn_cursors *m_cursors = &m_wcursors; + + CURSOR(spent_keys) + + MDB_val k = {sizeof(k_image), (void *)&k_image}; + auto result = mdb_cursor_get(m_cur_spent_keys, (MDB_val *)&zerokval, &k, MDB_GET_BOTH); + if (result != 0 && result != MDB_NOTFOUND) + throw1(DB_ERROR(lmdb_error("Error finding spent key to remove", result).c_str())); + if (!result) + { + result = mdb_cursor_del(m_cur_spent_keys, 0); + if (result) + throw1(DB_ERROR(lmdb_error("Error adding removal of key image to db transaction", result).c_str())); + } +} + +void BlockchainLMDB::add_locked_outs(const fcmp_pp::OutsByLastLockedBlock& outs_by_last_locked_block, const std::unordered_map& timelocked_outputs) +{ + LOG_PRINT_L3("BlockchainLMDB::" << __func__); + check_open(); + mdb_txn_cursors *m_cursors = &m_wcursors; + + CURSOR(locked_outputs) + CURSOR(timelocked_outputs) + + // Add to the locked outputs and custom timelocked tables, keyed by last locked block + for (const auto &last_locked_block : outs_by_last_locked_block) + { + const uint64_t last_locked_block_idx = last_locked_block.first; + for (const fcmp_pp::UnifiedOutput &locked_output : last_locked_block.second) + { + const cryptonote::blobdata output_blob = cryptonote::t_serializable_object_to_blob(locked_output); + + MDB_val_set(k_block_id, last_locked_block_idx); + MDB_val_sized(v_output, output_blob); + int result = mdb_cursor_put(m_cur_locked_outputs, &k_block_id, &v_output, MDB_APPENDDUP); + if (result != MDB_SUCCESS) + throw0(DB_ERROR(lmdb_error("Failed to add locked output: ", result).c_str())); + + if (timelocked_outputs.find(locked_output.unified_id) == timelocked_outputs.end()) + continue; + + // Add to custom timelocked outputs table also so it does not get removed in del_locked_outs_at_block_idx + MDB_val_set(k_timelocked_block_id, last_locked_block_idx); + MDB_val_sized(v_timelocked_output, output_blob); + result = mdb_cursor_put(m_cur_timelocked_outputs, &k_timelocked_block_id, &v_timelocked_output, MDB_APPENDDUP); + if (result != MDB_SUCCESS) + throw0(DB_ERROR(lmdb_error("Failed to add timelocked output: ", result).c_str())); + } + } +} + +void BlockchainLMDB::del_locked_outs_at_block_idx(uint64_t block_idx) +{ + LOG_PRINT_L3("BlockchainLMDB::" << __func__); + check_open(); + mdb_txn_cursors *m_cursors = &m_wcursors; + + CURSOR(locked_outputs) + + MDB_val_set(k_block_idx, block_idx); + + int result = mdb_cursor_get(m_cur_locked_outputs, &k_block_idx, NULL, MDB_SET); + if (result == MDB_NOTFOUND) + return; + if (result != MDB_SUCCESS) + throw1(DB_ERROR(lmdb_error("Error finding locked outputs to remove: ", result).c_str())); + + result = mdb_cursor_del(m_cur_locked_outputs, MDB_NODUPDATA); + if (result) + throw1(DB_ERROR(lmdb_error("Error removing locked outputs: ", result).c_str())); +} + +std::vector BlockchainLMDB::grow_with_tree_extension(const fcmp_pp::CompressedTreeExtension &tree_extension) +{ + LOG_PRINT_L3("BlockchainLMDB::" << __func__); + check_open(); + mdb_txn_cursors *m_cursors = &m_wcursors; + + CHECK_AND_ASSERT_THROW_MES(m_write_txn != nullptr, "Must have m_write_txn set to grow tree"); + CHECK_AND_ASSERT_THROW_MES(m_curve_trees != nullptr, "curve trees must be set"); + + CURSOR(leaves) + + // Insert the leaves + auto &leaves = tree_extension.leaves; + for (uint64_t i = 0; i < leaves.tuples.size(); ++i) + { + const uint64_t leaf_idx = i + leaves.start_leaf_tuple_idx; + mdb_leaf val{.leaf_idx = leaf_idx, .unified_id = leaves.tuples.at(i).unified_id}; + MDB_val_set(v, val); + + int result = mdb_cursor_put(m_cur_leaves, (MDB_val *)&zerokval, &v, MDB_APPENDDUP); + if (result != MDB_SUCCESS) + throw0(DB_ERROR(lmdb_error("Failed to add leaf: ", result).c_str())); + } + + // Grow the layers + const auto &layer_extensions = tree_extension.layer_extensions; + if (layer_extensions.empty()) + throw0(DB_ERROR("Unexpected 0 n layers")); + + std::vector tree_edge; + tree_edge.reserve(layer_extensions.size()); + for (uint64_t layer_idx = 0; layer_idx < layer_extensions.size(); ++layer_idx) + { + MTRACE("Growing layer " << layer_idx); + tree_edge.emplace_back(this->grow_layer(layer_extensions.at(layer_idx), layer_idx)); + } + + return tree_edge; +} + +crypto::ec_point BlockchainLMDB::grow_layer(const fcmp_pp::CompressedLayerExtension &layer_extension, + const uint64_t layer_idx) +{ + LOG_PRINT_L3("BlockchainLMDB::" << __func__); + check_open(); + mdb_txn_cursors *m_cursors = &m_wcursors; + + CURSOR(layers) + + CHECK_AND_ASSERT_THROW_MES(!layer_extension.hashes.empty(), "empty layer extension"); + std::vector hashes; + hashes.reserve(layer_extension.hashes.size()); + + // Expected: layer_extension.start_idx should line up with the end of the layer + + MDB_val_copy k(layer_idx); + + // 1. Update the existing last hash if necessary + if (layer_extension.update_existing_last_hash) + { + hashes.emplace_back(layer_extension.hashes.front()); + + // We updated the last hash, so update it + layer_val lv; + lv.child_chunk_idx = layer_extension.start_idx; + lv.child_chunk_hash = hashes.back(); + MDB_val_set(v, lv); + + // We expect to overwrite the existing hash + // Expected: the hash should already exist and be the expected existing last hash + int result = mdb_cursor_put(m_cur_layers, &k, &v, 0); + if (result != MDB_SUCCESS) + throw0(DB_ERROR(lmdb_error("Failed to update chunk hash: ", result).c_str())); + } + + // 2. Add all the new hashes found in the extension + for (uint64_t i = layer_extension.update_existing_last_hash ? 1 : 0; i < layer_extension.hashes.size(); ++i) + { + hashes.emplace_back(layer_extension.hashes[i]); + + layer_val lv; + lv.child_chunk_idx = i + layer_extension.start_idx; + lv.child_chunk_hash = hashes.back(); + MDB_val_set(v, lv); + + int result = mdb_cursor_put(m_cur_layers, &k, &v, MDB_APPENDDUP); + if (result != MDB_SUCCESS) + throw0(DB_ERROR(lmdb_error("Failed to add hash: ", result).c_str())); + } + + return hashes.back(); +} + +void BlockchainLMDB::save_tree_meta(const uint64_t block_idx, const uint64_t n_leaf_tuples, const std::vector &tree_edge) +{ + LOG_PRINT_L3("BlockchainLMDB::" << __func__); + check_open(); + mdb_txn_cursors *m_cursors = &m_wcursors; + + CURSOR(tree_edges); + CURSOR(tree_meta); + + if (tree_edge.size() > std::numeric_limits::max()) + throw0(DB_ERROR("too many tree edge members")); + if (tree_edge.size() != m_curve_trees->n_layers(n_leaf_tuples)) + throw0(DB_ERROR("Number of layers in the tree edge does not match expected for the saved n leaf tuples")); + + // 1. Save the tree edge + MDB_val_copy k_tee(block_idx); + MDB_val v; + const std::size_t n_layers = tree_edge.size(); + v.mv_data = n_layers ? (void *)tree_edge.data() : (void*)""; + v.mv_size = sizeof(crypto::ec_point) * n_layers; + + int result = mdb_cursor_put(m_cur_tree_edges, &k_tee, &v, MDB_NOOVERWRITE); + if (result != MDB_SUCCESS) + throw0(DB_ERROR(lmdb_error("Failed to set last hash: ", result).c_str())); + + // 2. Save the tree meta + MDB_val_copy k_meta(block_idx); + mdb_tree_meta tree_meta; + tree_meta.n_leaf_tuples = n_leaf_tuples; + MDB_val_set(v_meta, tree_meta); + + MDEBUG("Saving tree meta for block idx " << block_idx << " (n_leaf_tuples=" << n_leaf_tuples + << ", root=" << (tree_edge.size() ? tree_edge.back() : crypto::ec_point{}) << ")"); + + result = mdb_cursor_put(m_cur_tree_meta, &k_meta, &v_meta, MDB_NOOVERWRITE); + if (result != MDB_SUCCESS) + throw1(DB_ERROR(lmdb_error("Error setting tree meta: ", result).c_str())); +} + +void BlockchainLMDB::del_tree_meta(const uint64_t block_idx) +{ + LOG_PRINT_L3("BlockchainLMDB::" << __func__); + check_open(); + mdb_txn_cursors *m_cursors = &m_wcursors; + + CURSOR(tree_edges) + CURSOR(tree_meta) + + MDB_val_set(k_block_id, block_idx); + + // 1. Delete tree edge at this block + int result = mdb_cursor_get(m_cur_tree_edges, &k_block_id, NULL, MDB_SET); + if (result != MDB_SUCCESS) + throw1(DB_ERROR(lmdb_error("Error finding tree edge to remove at block " + std::to_string(block_idx) + ": ", result).c_str())); + result = mdb_cursor_del(m_cur_tree_edges, 0); + if (result) + throw1(DB_ERROR(lmdb_error("Error removing tree edge: ", result).c_str())); + + // 2. Delete tree meta at this block + result = mdb_cursor_get(m_cur_tree_meta, &k_block_id, NULL, MDB_SET); + if (result != MDB_SUCCESS) + throw1(DB_ERROR(lmdb_error("Error finding tree meta to remove at block " + std::to_string(block_idx) + ": ", result).c_str())); + result = mdb_cursor_del(m_cur_tree_meta, 0); + if (result) + throw1(DB_ERROR(lmdb_error("Error removing tree meta: ", result).c_str())); +} + +std::vector BlockchainLMDB::get_tree_edge(uint64_t block_id) const +{ + LOG_PRINT_L3("BlockchainLMDB::" << __func__); + check_open(); + + TXN_PREFIX_RDONLY(); + RCURSOR(tree_edges) + + MDB_val_set(k_block_id, block_id); + MDB_val v; + int result = mdb_cursor_get(m_cur_tree_edges, &k_block_id, &v, MDB_SET); + if (result != MDB_SUCCESS) + throw1(DB_ERROR(lmdb_error("Error finding tree edge at block " + std::to_string(block_id) + ": ", result).c_str())); + + crypto::ec_point* tree_edge = (crypto::ec_point*)v.mv_data; + const std::size_t n_layers = v.mv_size / sizeof(crypto::ec_point); + + std::vector res; + res.reserve(n_layers); + for (std::size_t i = 0; i < n_layers; ++i) + res.emplace_back(std::move(tree_edge[i])); + + TXN_POSTFIX_RDONLY(); + + return res; +} + +fcmp_pp::CompressedPath BlockchainLMDB::get_path(const fcmp_pp::PathIndexes &path_indexes) const +{ + LOG_PRINT_L3("BlockchainLMDB::" << __func__); + check_open(); + + TXN_PREFIX_RDONLY(); + RCURSOR(leaves) + RCURSOR(layers) + + fcmp_pp::CompressedPath path_bytes; + + auto &leaves_out = path_bytes.leaves; + auto &layer_chunks_out = path_bytes.layer_chunks; + + // Get the leaves + // TODO: separate function for leaves + { + if (path_indexes.leaf_range.second > path_indexes.leaf_range.first) + { + std::vector unified_ids; + unified_ids.reserve(path_indexes.leaf_range.second - path_indexes.leaf_range.first); + + uint64_t idx = path_indexes.leaf_range.first; + + MDB_val k = zerokval; + MDB_val_copy v(idx); + + MDB_cursor_op leaf_op = MDB_GET_BOTH; + do + { + int result = mdb_cursor_get(m_cur_leaves, &k, &v, leaf_op); + leaf_op = MDB_NEXT; + if (result == MDB_NOTFOUND) + throw0(DB_ERROR("leaf not found")); // TODO: specific error type instead of DB_ERROR + if (result != MDB_SUCCESS) + throw0(DB_ERROR(lmdb_error("Failed to get leaf: ", result).c_str())); + + const auto *db_leaf = (mdb_leaf *)v.mv_data; + unified_ids.push_back(db_leaf->unified_id); + + ++idx; + } + while (idx < path_indexes.leaf_range.second); + + leaves_out = this->get_unified_output_by_id(unified_ids); + } + } + + // Traverse the tree layer-by-layer starting at the layer closest to leaf layer + // TODO: separate function for layers + std::size_t layer_idx = 0; + for (const auto &layer_idx_range : path_indexes.layers) + { + fcmp_pp::CompressedChunk chunk; + + MDB_val_set(k, layer_idx); + MDB_val_set(v, layer_idx_range.first); + MDB_cursor_op op = MDB_GET_BOTH; + for (std::size_t i = layer_idx_range.first; i < layer_idx_range.second; ++i) + { + MTRACE("Getting child at layer_idx: " << layer_idx << " , idx: " << i); + + int result = mdb_cursor_get(m_cur_layers, &k, &v, op); + op = MDB_NEXT_DUP; + if (result == MDB_NOTFOUND) + throw0(DB_ERROR("layer elem not found")); // TODO: specific error type instead of DB_ERROR + if (result != MDB_SUCCESS) + throw0(DB_ERROR(lmdb_error("Failed to get layer elem: ", result).c_str())); + + auto *lv = (layer_val *)v.mv_data; + chunk.elems.emplace_back(std::move(lv->child_chunk_hash)); + } + + layer_chunks_out.emplace_back(std::move(chunk)); + ++layer_idx; + } + + TXN_POSTFIX_RDONLY(); + + return path_bytes; +} + +std::vector BlockchainLMDB::get_unified_output_by_id( + const std::vector &unified_ids) const +{ + LOG_PRINT_L3("BlockchainLMDB::" << __func__); + check_open(); + + TXN_PREFIX_RDONLY(); + + std::vector unified_outputs; + unified_outputs.reserve(unified_ids.size()); + + // Collect tx hashes from output id's, reading db output_txs + std::vector tois; + this->get_output_tx_and_index_from_unified(unified_ids, tois); + + // Collect pruned txs from tx hashes, reading db tx_indices, and txs_pruned + std::unordered_map txs; + for (const auto &toi : tois) + { + if (txs.find(toi.first) == txs.end()) + txs[toi.first] = this->get_pruned_tx(toi.first); + } + + // Collect unified outputs from pruned tx data + std::unordered_map transparent_amount_commitments; + for (std::size_t i = 0; i < unified_ids.size(); ++i) + { + const uint64_t unified_id = unified_ids.at(i); + const auto &toi = tois.at(i); + + const auto tx_it = txs.find(toi.first); + if (tx_it == txs.end()) + throw0(DB_ERROR("Missing tx for provided output id.")); + const auto &tx = tx_it->second; + + // Amount commitment + const auto &out = tx.vout.at(toi.second); + rct::key commitment; + if (out.amount == 0) + { + commitment = tx.rct_signatures.outPk.at(toi.second).mask; + } + else + { + if (transparent_amount_commitments.find(out.amount) == transparent_amount_commitments.end()) + transparent_amount_commitments[out.amount] = rct::zeroCommitVartime(out.amount); + commitment = transparent_amount_commitments[out.amount]; + } + + unified_outputs.emplace_back(fcmp_pp::UnifiedOutput{ + .unified_id = unified_id, + .output_pair = cryptonote::to_output_pair(out.target, commitment) + }); + } + + if (unified_ids.size() != unified_outputs.size()) + throw0(DB_ERROR("get_unified_output_by_id: unified_ids <> unified_outputs size mismatch")); + + TXN_POSTFIX_RDONLY(); + + return unified_outputs; +} + +uint64_t BlockchainLMDB::find_leaf_idx_by_unified_id_bounded_search(uint64_t unified_id, uint64_t leaf_idx_start, uint64_t leaf_idx_end) const +{ + LOG_PRINT_L3("BlockchainLMDB::" << __func__); + check_open(); + + TXN_PREFIX_RDONLY(); + RCURSOR(leaves) + + MDB_val k = zerokval; + MDB_val_copy v_leaf(leaf_idx_start); + + uint64_t n_leaves_checked = 0; + uint64_t leaf_idx = 0; + bool found_leaf_idx = false; + bool done = false; + MDB_cursor_op op = MDB_SET; + while (!done) + { + int result = mdb_cursor_get(m_cur_leaves, &k, &v_leaf, op); + if (result == MDB_NOTFOUND) + break; + if (result != MDB_SUCCESS) + throw0(DB_ERROR(lmdb_error("Failed to get leaf idx by output id: ", result).c_str())); + op = MDB_NEXT_MULTIPLE; + + const auto range_begin = ((const mdb_leaf*)v_leaf.mv_data); + const auto range_end = range_begin + v_leaf.mv_size / sizeof(mdb_leaf); + + auto it = range_begin; + + // The first MDB_NEXT_MULTIPLE includes the val from MDB_SET, so skip it + if (n_leaves_checked == 1) + ++it; + + while (it < range_end) + { + found_leaf_idx = it->unified_id == unified_id; + if (found_leaf_idx) + { + leaf_idx = it->leaf_idx; + done = true; + break; + } + + done = leaf_idx_end > 0 && it->leaf_idx >= leaf_idx_end; + if (done) + break; + + ++n_leaves_checked; + ++it; + } + } + + if (!found_leaf_idx) + throw0(DB_ERROR("Did not find leaf idx by output id")); + + TXN_POSTFIX_RDONLY(); + + return leaf_idx; +} + +uint64_t BlockchainLMDB::trim_leaves(const uint64_t new_n_leaf_tuples, const uint64_t trim_block_idx) +{ + LOG_PRINT_L3("BlockchainLMDB::" << __func__); + check_open(); + mdb_txn_cursors *m_cursors = &m_wcursors; + + CURSOR(leaves) + CURSOR(locked_outputs) + + CHECK_AND_ASSERT_THROW_MES(m_write_txn != nullptr, "Must have m_write_txn set to trim tree"); + + const uint64_t old_n_leaf_tuples = this->get_n_leaf_tuples(); + if (new_n_leaf_tuples > old_n_leaf_tuples) + throw1(DB_ERROR("Cannot have more leaves in tree after trimming than exist in the tree already")); + + // Return if we don't need to trim any leaves + if (new_n_leaf_tuples == old_n_leaf_tuples) + return old_n_leaf_tuples; + + // Trim the leaves, re-adding to locked outputs table + std::vector unified_ids; + unified_ids.reserve(old_n_leaf_tuples - new_n_leaf_tuples); + for (uint64_t i = new_n_leaf_tuples; i < old_n_leaf_tuples; ++i) + { + MDB_val_copy k(i); + MDB_val v = k; + int result = mdb_cursor_get(m_cur_leaves, (MDB_val *)&zerokval, &v, MDB_GET_BOTH); + if (result == MDB_NOTFOUND) + throw0(DB_ERROR("leaf not found")); + if (result != MDB_SUCCESS) + throw0(DB_ERROR(lmdb_error("Failed to get leaf: ", result).c_str())); + + const auto *o = (mdb_leaf *)v.mv_data; + unified_ids.push_back(o->unified_id); + + // Delete the leaf + result = mdb_cursor_del(m_cur_leaves, 0); + if (result != MDB_SUCCESS) + throw0(DB_ERROR(lmdb_error("Error removing leaf: ", result).c_str())); + + MDEBUG("Successfully removed leaf at leaf_tuple_idx: " << i); + } + + const auto unified_outputs = this->get_unified_output_by_id(unified_ids); + + MDB_val_set(k_block_id, trim_block_idx); + for (const fcmp_pp::UnifiedOutput &unified_output : unified_outputs) + { + // Re-add the output to the locked output table in order. The output should + // still be in the outputs tables. + const cryptonote::blobdata output_blob = cryptonote::t_serializable_object_to_blob(unified_output); + MDB_val_sized(v_output, output_blob); + MDEBUG("Re-adding locked unified_id: " << unified_output.unified_id << " , last locked block: " << trim_block_idx); + int result = mdb_cursor_put(m_cur_locked_outputs, &k_block_id, &v_output, MDB_APPENDDUP); + if (result != MDB_SUCCESS) + throw0(DB_ERROR(lmdb_error("Failed to re-add locked output: ", result).c_str())); + } + + return old_n_leaf_tuples; +} + +void BlockchainLMDB::trim_layers(const uint64_t new_n_leaf_tuples, + const std::vector &n_elems_per_layer, + const std::vector &prev_tree_edge, + const uint64_t expected_root_idx) +{ + LOG_PRINT_L3("BlockchainLMDB::" << __func__); + check_open(); + mdb_txn_cursors *m_cursors = &m_wcursors; + + CURSOR(layers) + + CHECK_AND_ASSERT_THROW_MES(m_write_txn != nullptr, "Must have m_write_txn set for trim_layers"); + + if (n_elems_per_layer.size() != prev_tree_edge.size()) + throw1(DB_ERROR("trim_layers: n_elems_per_layer.size() != prev_tree_edge.size()")); + + // If the tree is supposed to be empty, empty the tree + if (new_n_leaf_tuples == 0) + { + // Empty the layers table, no elems should remain + int result = mdb_drop(*m_write_txn, m_layers, 0); + if (result != MDB_SUCCESS) + throw0(DB_ERROR(lmdb_error("Error emptying layers table: ", result).c_str())); + return; + } + + // Shrink the layer sizes and update the last hash in each layer using the prev block's cached tree edge + for (std::size_t layer_idx = 0; layer_idx < n_elems_per_layer.size(); ++layer_idx) + { + const uint64_t n_new_elems = n_elems_per_layer[layer_idx]; + if (n_new_elems == 0) + throw0(DB_ERROR("Unexpected 0 new elems")); + if (layer_idx >= prev_tree_edge.size()) + throw0(DB_ERROR("Tree edge is too small")); + + // Delete all excess elems in layer + this->trim_layer(n_new_elems, layer_idx); + + // Set the new last elem using the old tree edge elem + MDB_val_copy k_layer_idx(layer_idx); + layer_val lv; + lv.child_chunk_idx = n_new_elems - 1; + lv.child_chunk_hash = std::move(prev_tree_edge.at(layer_idx)); + MDB_val_set(v_lv, lv); + + // Overwrite layer last elem + MDEBUG("Re-setting elem " << lv.child_chunk_idx << " at layer idx " << layer_idx << ": " << epee::string_tools::pod_to_hex(lv.child_chunk_hash)); + int result = mdb_cursor_put(m_cur_layers, &k_layer_idx, &v_lv, 0); + if (result != MDB_SUCCESS) + throw0(DB_ERROR(lmdb_error("Failed to update chunk hash: ", result).c_str())); + } + + // Delete any remaining layers in layers after the root + while (1) + { + MDB_val k, v; + int result = mdb_cursor_get(m_cur_layers, &k, &v, MDB_LAST); + if (result != MDB_SUCCESS) + throw0(DB_ERROR(lmdb_error("Failed to get last elem: ", result).c_str())); + + const uint64_t last_layer_idx = *(uint64_t *)k.mv_data; + if (last_layer_idx > expected_root_idx) + { + // Delete all elements in layers after the root + result = mdb_cursor_del(m_cur_layers, MDB_NODUPDATA); + if (result != MDB_SUCCESS) + throw0(DB_ERROR(lmdb_error("Error removing elems after root: ", result).c_str())); + } + else if (last_layer_idx < expected_root_idx) + { + throw0(DB_ERROR("Encountered unexpected last elem in tree before the root")); + } + else // last_layer_idx == expected_root_idx + { + // We've trimmed all layers past the root, we're done + break; + } + } +} + +void BlockchainLMDB::trim_layer(const uint64_t new_n_elems_in_layer, const uint64_t layer_idx) +{ + LOG_PRINT_L3("BlockchainLMDB::" << __func__); + check_open(); + mdb_txn_cursors *m_cursors = &m_wcursors; + + CURSOR(layers) + + MDEBUG("Trimming layer " << layer_idx << ", new elems in layer: " << new_n_elems_in_layer); + MDB_val_copy k(layer_idx); + + // Get the number of existing elements in the layer + // TODO: get_num_elems_in_layer + uint64_t old_n_elems_in_layer = 0; + { + // Get the first record in a layer so we can then get the last record + MDB_val v; + int result = mdb_cursor_get(m_cur_layers, &k, &v, MDB_SET); + if (result != MDB_SUCCESS) + throw0(DB_ERROR(lmdb_error("Failed to get first record in layer: ", result).c_str())); + + result = mdb_cursor_get(m_cur_layers, &k, &v, MDB_LAST_DUP); + if (result != MDB_SUCCESS) + throw0(DB_ERROR(lmdb_error("Failed to get layer last elem on trim: ", result).c_str())); + + const auto *lv = (layer_val *)v.mv_data; + old_n_elems_in_layer = (1 + lv->child_chunk_idx); + } + + CHECK_AND_ASSERT_THROW_MES(old_n_elems_in_layer >= new_n_elems_in_layer, "unexpected old n elems in layer"); + const uint64_t trim_n_elems_in_layer = old_n_elems_in_layer - new_n_elems_in_layer; + + // Delete the elements + for (uint64_t i = 0; i < trim_n_elems_in_layer; ++i) + { + uint64_t last_elem_idx = (old_n_elems_in_layer - 1 - i); + MDB_val_set(v, last_elem_idx); + + int result = mdb_cursor_get(m_cur_layers, &k, &v, MDB_GET_BOTH); + if (result == MDB_NOTFOUND) + throw0(DB_ERROR("leaf not found")); // TODO: specific error type instead of DB_ERROR + if (result != MDB_SUCCESS) + throw0(DB_ERROR(lmdb_error("Failed to get elem: ", result).c_str())); + + result = mdb_cursor_del(m_cur_layers, 0); + if (result != MDB_SUCCESS) + throw0(DB_ERROR(lmdb_error("Error removing elem: ", result).c_str())); + + MDEBUG("Successfully removed elem at layer_idx: " << layer_idx << " , last_elem_idx: " << last_elem_idx); + } +} + +uint64_t BlockchainLMDB::get_n_leaf_tuples() const +{ + LOG_PRINT_L3("BlockchainLMDB::" << __func__); + check_open(); + + TXN_PREFIX_RDONLY(); + RCURSOR(leaves) + + // Get the number of leaf tuples in the tree + std::uint64_t n_leaf_tuples = 0; + + { + MDB_val k, v; + int result = mdb_cursor_get(m_cur_leaves, &k, &v, MDB_LAST); + if (result == MDB_NOTFOUND) + n_leaf_tuples = 0; + else if (result == MDB_SUCCESS) + n_leaf_tuples = 1 + ((const mdb_leaf*)v.mv_data)->leaf_idx; + else + throw0(DB_ERROR(lmdb_error("Failed to get last leaf: ", result).c_str())); + } + + TXN_POSTFIX_RDONLY(); + + return n_leaf_tuples; +} + +uint64_t BlockchainLMDB::get_block_n_leaf_tuples(const uint64_t block_idx) const +{ + LOG_PRINT_L3("BlockchainLMDB::" << __func__); + check_open(); + + TXN_PREFIX_RDONLY(); + RCURSOR(tree_meta); + + MDB_val_set(k_block_id, block_idx); + MDB_val v; + int result = mdb_cursor_get(m_cur_tree_meta, &k_block_id, &v, MDB_SET); + if (result == MDB_NOTFOUND) + throw0(BLOCK_DNE(std::string("Attempt to get tree meta from blk idx ").append(boost::lexical_cast(block_idx)).append(" failed -- tree meta not in db").c_str())); + if (result != MDB_SUCCESS) + throw1(DB_ERROR(lmdb_error("Error getting tree meta n leaf tuples: ", result).c_str())); + + uint64_t n_leaf_tuples = ((mdb_tree_meta *)v.mv_data)->n_leaf_tuples; + + TXN_POSTFIX_RDONLY(); + + return n_leaf_tuples; +} + +uint8_t BlockchainLMDB::get_tree_root_at_blk_idx(const uint64_t blk_idx, crypto::ec_point &tree_root_out) const +{ + const std::vector tree_edge = this->get_tree_edge(blk_idx); + if (tree_edge.empty()) + { + tree_root_out = crypto::ec_point{}; + return 0; + } + tree_root_out = tree_edge.back(); + static_assert(sizeof(std::size_t) >= sizeof(uint8_t), "unexpected size of size_t"); + return (uint8_t) tree_edge.size(); +} + +bool BlockchainLMDB::audit_tree(const uint64_t expected_n_leaf_tuples) const +{ + LOG_PRINT_L3("BlockchainLMDB::" << __func__); + check_open(); + + TXN_PREFIX_RDONLY(); + RCURSOR(leaves) + RCURSOR(layers) + + const uint64_t actual_n_leaf_tuples = this->get_n_leaf_tuples(); + CHECK_AND_ASSERT_MES(actual_n_leaf_tuples == expected_n_leaf_tuples, false, "unexpected num leaf tuples"); + + MDEBUG("Auditing tree with " << actual_n_leaf_tuples << " leaf tuples"); + + if (actual_n_leaf_tuples == 0) + { + // Make sure layers table is also empty + MDB_stat db_stats; + int result = mdb_stat(m_txn, m_layers, &db_stats); + if (result) + throw0(DB_ERROR(lmdb_error("Failed to query m_layers: ", result).c_str())); + CHECK_AND_ASSERT_MES(db_stats.ms_entries == 0, false, "unexpected num layer entries"); + return true; + } + + CHECK_AND_ASSERT_THROW_MES(m_curve_trees != nullptr, "curve trees must be set"); + + // Check chunks of leaves hash into first layer as expected + uint64_t layer_idx = 0; + uint64_t child_chunk_idx = 0; + MDB_cursor_op leaf_op = MDB_FIRST; + MDB_cursor_op parent_op = MDB_FIRST; + + MDB_val_copy k_parent(layer_idx); + MDB_val_set(v_parent, child_chunk_idx); + + while (1) + { + // Get next leaf chunk + if (child_chunk_idx && child_chunk_idx % 1000 == 0) + MINFO("Auditing layer " << layer_idx << ", child_chunk_idx " << child_chunk_idx); + + // Iterate until chunk is full or we get to the end of all leaves, collecting leaf output ID's + std::vector unified_ids; + unified_ids.reserve(m_curve_trees->m_c1_width); + MDB_val k_leaf, v_leaf; + while (1) + { + int result = mdb_cursor_get(m_cur_leaves, &k_leaf, &v_leaf, leaf_op); + leaf_op = MDB_NEXT; + if (result == MDB_NOTFOUND) + break; + if (result != MDB_SUCCESS) + throw0(DB_ERROR(lmdb_error("Failed to add leaf: ", result).c_str())); + + const auto *o = (mdb_leaf *)v_leaf.mv_data; + unified_ids.push_back(o->unified_id); + + if (unified_ids.size() == m_curve_trees->m_c1_width) + break; + } + + // Get leaves from output ID's + std::vector leaf_tuples_chunk; + leaf_tuples_chunk.reserve(m_curve_trees->m_c1_width); + const auto unified_outputs = this->get_unified_output_by_id(unified_ids); + for (const auto &unified_output : unified_outputs) + { + auto leaf_tuple = m_curve_trees->leaf_tuple(unified_output.output_pair); + leaf_tuples_chunk.emplace_back(std::move(leaf_tuple)); + } + + // Get the actual leaf chunk hash from the db + MDEBUG("Getting leaf chunk hash starting at child_chunk_idx " << child_chunk_idx); + int result = mdb_cursor_get(m_cur_layers, &k_parent, &v_parent, parent_op); + parent_op = MDB_NEXT_DUP; + + // Check end condition: no more leaf tuples in the leaf layer + if (leaf_tuples_chunk.empty()) + { + // No more leaves, expect to be done with parent chunks as well + if (result != MDB_NOTFOUND) + throw0(DB_ERROR(lmdb_error("unexpected leaf chunk parent result found at child_chunk_idx " + + std::to_string(child_chunk_idx), result).c_str())); + + break; + } + + if (result != MDB_SUCCESS) + throw0(DB_ERROR(lmdb_error("Failed to get parent in first layer: ", result).c_str())); + if (layer_idx != *(uint64_t*)k_parent.mv_data || child_chunk_idx != ((layer_val *)v_parent.mv_data)->child_chunk_idx) + throw0(DB_ERROR("unexpected parent encountered")); + + // Get the expected leaf chunk hash + const auto leaves = m_curve_trees->flatten_leaves(std::move(leaf_tuples_chunk)); + const fcmp_pp::curve_trees::Selene::Chunk chunk{leaves.data(), leaves.size()}; + + // Hash the chunk of leaves + const auto chunk_hash = fcmp_pp::curve_trees::get_new_parent(m_curve_trees->m_c1, chunk); + const auto expected_chunk_hash = m_curve_trees->m_c1->to_string(chunk_hash); + MDEBUG("chunk_hash " << expected_chunk_hash << " , hash init point: " + << m_curve_trees->m_c1->to_string(m_curve_trees->m_c1->hash_init_point()) << " (" << leaves.size() << " leaves)"); + + // Now compare to value from the db + const auto *lv = (layer_val *)v_parent.mv_data; + const auto actual_chunk_hash = epee::string_tools::pod_to_hex(lv->child_chunk_hash); + MDEBUG("Actual leaf chunk hash " << actual_chunk_hash); + + CHECK_AND_ASSERT_MES(expected_chunk_hash == actual_chunk_hash, false, "unexpected leaf chunk hash"); + CHECK_AND_ASSERT_MES(lv->child_chunk_idx == child_chunk_idx, false, "unexpected child chunk idx"); + + ++child_chunk_idx; + } + + MDEBUG("Successfully audited leaf layer"); + + // Traverse up the tree auditing each layer until we've audited every layer in the tree + bool audit_complete = false; + while (!audit_complete) + { + MDEBUG("Auditing layer " << layer_idx); + + // Alternate starting with c2 as parent (we already audited c1 leaf parents), then c1 as parent, then c2, etc. + const bool parent_is_c2 = layer_idx % 2 == 0; + if (parent_is_c2) + { + audit_complete = this->audit_layer( + /*c_child*/ m_curve_trees->m_c1, + /*c_parent*/ m_curve_trees->m_c2, + layer_idx, + /*chunk_width*/ m_curve_trees->m_c2_width); + } + else + { + audit_complete = this->audit_layer( + /*c_child*/ m_curve_trees->m_c2, + /*c_parent*/ m_curve_trees->m_c1, + layer_idx, + /*chunk_width*/ m_curve_trees->m_c1_width); + } + + ++layer_idx; + } + + TXN_POSTFIX_RDONLY(); + + return true; +} + +template +bool BlockchainLMDB::audit_layer(const std::unique_ptr &c_child, + const std::unique_ptr &c_parent, + const uint64_t child_layer_idx, + const uint64_t chunk_width) const +{ + LOG_PRINT_L3("BlockchainLMDB::" << __func__); + check_open(); + + TXN_PREFIX_RDONLY(); + + // Open two separate cursors for child and parent layer + MDB_cursor *child_layer_cursor, *parent_layer_cursor; + + int c_result = mdb_cursor_open(m_txn, m_layers, &child_layer_cursor); + if (c_result) + throw0(DB_ERROR(lmdb_error("Failed to open child cursor: ", c_result).c_str())); + int p_result = mdb_cursor_open(m_txn, m_layers, &parent_layer_cursor); + if (p_result) + throw0(DB_ERROR(lmdb_error("Failed to open parent cursor: ", p_result).c_str())); + + // Set the cursors to the start of each layer + const uint64_t parent_layer_idx = child_layer_idx + 1; + + MDB_val_set(k_child, child_layer_idx); + MDB_val_set(k_parent, parent_layer_idx); + + MDB_val v_child, v_parent; + + c_result = mdb_cursor_get(child_layer_cursor, &k_child, &v_child, MDB_SET); + p_result = mdb_cursor_get(parent_layer_cursor, &k_parent, &v_parent, MDB_SET); + + if (c_result != MDB_SUCCESS) + throw0(DB_ERROR(lmdb_error("Failed to get child: ", c_result).c_str())); + if (p_result != MDB_SUCCESS && p_result != MDB_NOTFOUND) + throw0(DB_ERROR(lmdb_error("Failed to get parent: ", p_result).c_str())); + + // Begin to audit the layer + MDB_cursor_op op_child = MDB_FIRST_DUP; + MDB_cursor_op op_parent = MDB_FIRST_DUP; + bool audit_complete = false; + uint64_t child_chunk_idx = 0; + while (1) + { + if (child_chunk_idx && child_chunk_idx % 1000 == 0) + MINFO("Auditing layer " << parent_layer_idx << ", child_chunk_idx " << child_chunk_idx); + + // Get next child chunk + std::vector child_chunk; + child_chunk.reserve(chunk_width); + while (1) + { + int result = mdb_cursor_get(child_layer_cursor, &k_child, &v_child, op_child); + op_child = MDB_NEXT_DUP; + if (result == MDB_NOTFOUND) + break; + if (result != MDB_SUCCESS) + throw0(DB_ERROR(lmdb_error("Failed to get child: ", result).c_str())); + + const auto *lv = (layer_val *)v_child.mv_data; + auto child_point = c_child->from_bytes(lv->child_chunk_hash); + + child_chunk.emplace_back(std::move(child_point)); + + if (child_chunk.size() == chunk_width) + break; + } + + // Get the actual chunk hash from the db + int result = mdb_cursor_get(parent_layer_cursor, &k_parent, &v_parent, op_parent); + op_parent = MDB_NEXT_DUP; + + // Check for end conditions + // End condition A (audit_complete=false): finished auditing layer and ready to move up a layer + // End condition B (audit_complete=true ): finished auditing the tree, no more layers remaining + + // End condition A: check if finished auditing this layer + if (child_chunk.empty()) + { + // No more children, expect to be done auditing layer and ready to move up a layer + if (result != MDB_NOTFOUND) + throw0(DB_ERROR(lmdb_error("unexpected parent result at parent_layer_idx " + std::to_string(parent_layer_idx) + + " , child_chunk_idx " + std::to_string(child_chunk_idx) + " : ", result).c_str())); + + MDEBUG("Finished auditing layer " << child_layer_idx); + audit_complete = false; + break; + } + + // End condition B: check if finished auditing the tree + if (child_chunk_idx == 0 && child_chunk.size() == 1) + { + if (p_result != MDB_NOTFOUND) + throw0(DB_ERROR(lmdb_error("unexpected parent of root at parent_layer_idx " + std::to_string(parent_layer_idx) + + " , child_chunk_idx " + std::to_string(child_chunk_idx) + " : ", result).c_str())); + + MDEBUG("Encountered root at layer_idx " << child_layer_idx); + audit_complete = true; + break; + } + + if (result != MDB_SUCCESS) + throw0(DB_ERROR(lmdb_error("Failed to get parent: ", result).c_str())); + + if (child_layer_idx != *(uint64_t*)k_child.mv_data) + throw0(DB_ERROR("unexpected child encountered")); + if (parent_layer_idx != *(uint64_t*)k_parent.mv_data) + throw0(DB_ERROR("unexpected parent encountered")); + + // Get the expected chunk hash + std::vector child_scalars; + child_scalars.reserve(child_chunk.size()); + for (const auto &child : child_chunk) + child_scalars.emplace_back(c_child->point_to_cycle_scalar(child)); + const typename C_PARENT::Chunk chunk{child_scalars.data(), child_scalars.size()}; + + for (uint64_t i = 0; i < child_scalars.size(); ++i) + MDEBUG("Hashing " << c_parent->to_string(child_scalars[i])); + + const auto chunk_hash = fcmp_pp::curve_trees::get_new_parent(c_parent, chunk); + const auto expected_chunk_hash = c_parent->to_string(chunk_hash); + MDEBUG("Expected chunk_hash " << expected_chunk_hash << " (" << child_scalars.size() << " children)"); + + const auto *lv = (layer_val *)v_parent.mv_data; + const auto actual_chunk_hash = epee::string_tools::pod_to_hex(lv->child_chunk_hash); + MDEBUG("Actual chunk hash " << actual_chunk_hash); + + if (expected_chunk_hash != actual_chunk_hash) + throw0(DB_ERROR(("unexpected hash at child_chunk_idx " + std::to_string(child_chunk_idx)).c_str())); + if (lv->child_chunk_idx != child_chunk_idx) + throw0(DB_ERROR(("unexpected child_chunk_idx, epxected " + std::to_string(child_chunk_idx)).c_str())); + + ++child_chunk_idx; + } + + TXN_POSTFIX_RDONLY(); + + return audit_complete; +} + +std::vector BlockchainLMDB::get_outs_at_last_locked_block_idx(uint64_t block_idx) const +{ + LOG_PRINT_L3("BlockchainLMDB::" << __func__); + check_open(); + + TXN_PREFIX_RDONLY(); + RCURSOR(locked_outputs) + + MDB_val_set(k_block_idx, block_idx); + MDB_val v_output; + + // Get all the locked outputs at the provided block id + std::vector outs; + + MDB_cursor_op op = MDB_SET; + while (1) + { + int result = mdb_cursor_get(m_cur_locked_outputs, &k_block_idx, &v_output, op); + if (result == MDB_NOTFOUND) + break; + if (result != MDB_SUCCESS) + throw0(DB_ERROR(lmdb_error("Failed to get next locked outputs: ", result).c_str())); + op = MDB_NEXT_MULTIPLE; + + const uint64_t blk_id = *(const uint64_t*)k_block_idx.mv_data; + if (blk_id != block_idx) + throw0(DB_ERROR(("Blk id " + std::to_string(blk_id) + " not the expected" + std::to_string(block_idx)).c_str())); + + const char *range_begin = (const char*)v_output.mv_data; + const char *range_end = range_begin + v_output.mv_size; + + auto it = range_begin; + + static_assert(SIZEOF_SERIALIZED_UNIFIED_OUTPUT == 73, "Unified output is stored serialized in 73 bytes"); + + // The first MDB_NEXT_MULTIPLE includes the val from MDB_SET, so skip it + if (outs.size() == 1) + it += SIZEOF_SERIALIZED_UNIFIED_OUTPUT; + + while (it < range_end) + { + if ((it + SIZEOF_SERIALIZED_UNIFIED_OUTPUT) > range_end) + throw0(DB_ERROR("Out of bounds reading locked outputs")); + + cryptonote::blobdata bd; + bd.assign(it, SIZEOF_SERIALIZED_UNIFIED_OUTPUT); + + fcmp_pp::UnifiedOutput out; + if (!cryptonote::t_serializable_object_from_blob(out, bd)) + throw0(DB_ERROR("Failed to de-serialize locked output")); + + outs.emplace_back(std::move(out)); + it += SIZEOF_SERIALIZED_UNIFIED_OUTPUT; + } } + + TXN_POSTFIX_RDONLY(); + + return outs; } -void BlockchainLMDB::remove_output(const uint64_t amount, const uint64_t& out_index) +uint64_t BlockchainLMDB::get_tree_block_idx() const { LOG_PRINT_L3("BlockchainLMDB::" << __func__); check_open(); - mdb_txn_cursors *m_cursors = &m_wcursors; - CURSOR(output_amounts); - CURSOR(output_txs); - MDB_val_set(k, amount); - MDB_val_set(v, out_index); + TXN_PREFIX_RDONLY(); - auto result = mdb_cursor_get(m_cur_output_amounts, &k, &v, MDB_GET_BOTH); - if (result == MDB_NOTFOUND) - throw1(OUTPUT_DNE("Attempting to get an output index by amount and amount index, but amount not found")); - else if (result) - throw0(DB_ERROR(lmdb_error("DB error attempting to get an output", result).c_str())); + RCURSOR(tree_meta) - const pre_rct_outkey *ok = (const pre_rct_outkey *)v.mv_data; - MDB_val_set(otxk, ok->output_id); - result = mdb_cursor_get(m_cur_output_txs, (MDB_val *)&zerokval, &otxk, MDB_GET_BOTH); - if (result == MDB_NOTFOUND) - { - throw0(DB_ERROR("Unexpected: global output index not found in m_output_txs")); - } - else if (result) - { - throw1(DB_ERROR(lmdb_error("Error adding removal of output tx to db transaction", result).c_str())); - } - result = mdb_cursor_del(m_cur_output_txs, 0); - if (result) - throw0(DB_ERROR(lmdb_error(std::string("Error deleting output index ").append(boost::lexical_cast(out_index).append(": ")).c_str(), result).c_str())); + MDB_val k; + MDB_val v; + int result = mdb_cursor_get(m_cur_tree_meta, &k, &v, MDB_LAST); + if (result != MDB_SUCCESS) + throw1(DB_ERROR(lmdb_error("Error finding last tree meta: ", result).c_str())); - // now delete the amount - result = mdb_cursor_del(m_cur_output_amounts, 0); - if (result) - throw0(DB_ERROR(lmdb_error(std::string("Error deleting amount for output index ").append(boost::lexical_cast(out_index).append(": ")).c_str(), result).c_str())); + uint64_t block_idx = *(uint64_t *)k.mv_data; + + TXN_POSTFIX_RDONLY(); + + return block_idx; } -void BlockchainLMDB::prune_outputs(uint64_t amount) +fcmp_pp::OutsByLastLockedBlock BlockchainLMDB::get_custom_timelocked_outputs(uint64_t start_block_idx) const { LOG_PRINT_L3("BlockchainLMDB::" << __func__); check_open(); - mdb_txn_cursors *m_cursors = &m_wcursors; - CURSOR(output_amounts); - CURSOR(output_txs); - MINFO("Pruning outputs for amount " << amount); + TXN_PREFIX_RDONLY(); + RCURSOR(timelocked_outputs) - MDB_val v; - MDB_val_set(k, amount); - int result = mdb_cursor_get(m_cur_output_amounts, &k, &v, MDB_SET); - if (result == MDB_NOTFOUND) - return; - if (result) - throw0(DB_ERROR(lmdb_error("Error looking up outputs: ", result).c_str())); + fcmp_pp::OutsByLastLockedBlock outs; - // gather output ids - mdb_size_t num_elems; - mdb_cursor_count(m_cur_output_amounts, &num_elems); - MINFO(num_elems << " outputs found"); - std::vector output_ids; - output_ids.reserve(num_elems); + /* + We expect the timelocked outputs table to be sorted primarily by key, i.e. + last locked block idx. For a given key, we expect timelocked outputs to be + sorted based on output id. Output id's increase monotonically + based on when the output is added to the chain. + */ + + // 1. Get all custom timelocked outputs with last locked block start_block_idx or higher + MDB_cursor_op op = MDB_LAST; while (1) { - const pre_rct_outkey *okp = (const pre_rct_outkey *)v.mv_data; - output_ids.push_back(okp->output_id); - MDEBUG("output id " << okp->output_id); - result = mdb_cursor_get(m_cur_output_amounts, &k, &v, MDB_NEXT_DUP); + MDB_val k, v; + int result = mdb_cursor_get(m_cur_timelocked_outputs, &k, &v, op); if (result == MDB_NOTFOUND) break; - if (result) - throw0(DB_ERROR(lmdb_error("Error counting outputs: ", result).c_str())); + if (result != MDB_SUCCESS) + throw0(DB_ERROR(lmdb_error("Failed to get last timelocked output: ", result).c_str())); + + const uint64_t last_locked_block_idx = *(const uint64_t*)k.mv_data; + + // We can stop as soon as we encounter a timelocked output with last locked + // block < start_block_idx, since the table should be sorted primarily by + // last locked block, and we're iterating in reverse. + if (last_locked_block_idx < start_block_idx) + break; + + // Include the out in the result container + cryptonote::blobdata bd; + bd.assign(reinterpret_cast(v.mv_data), v.mv_size); + + fcmp_pp::UnifiedOutput timelocked_output; + if (!cryptonote::t_serializable_object_from_blob(timelocked_output, bd)) + throw0(DB_ERROR("Failed to de-serialize timelocked output")); + + outs[last_locked_block_idx].emplace_back(std::move(timelocked_output)); + + op = MDB_PREV; } - if (output_ids.size() != num_elems) - throw0(DB_ERROR("Unexpected number of outputs")); - result = mdb_cursor_del(m_cur_output_amounts, MDB_NODUPDATA); - if (result) - throw0(DB_ERROR(lmdb_error("Error deleting outputs: ", result).c_str())); + // TODO: If we store the created block idx in timelocked outputs table, can avoid all this below - for (uint64_t output_id: output_ids) + // 2. Only return outputs that were created BEFORE start_block_idx + // 2a. Place all output id's in a flat vector + using unified_id_pair_t = std::pair; + std::vector unified_ids; + for (const auto &outs_by_last_locked_block : outs) { - MDB_val_set(v, output_id); - result = mdb_cursor_get(m_cur_output_txs, (MDB_val *)&zerokval, &v, MDB_GET_BOTH); - if (result) - throw0(DB_ERROR(lmdb_error("Error looking up output: ", result).c_str())); - result = mdb_cursor_del(m_cur_output_txs, 0); - if (result) - throw0(DB_ERROR(lmdb_error("Error deleting output: ", result).c_str())); + for (const auto &o : outs_by_last_locked_block.second) + unified_ids.push_back({ o.unified_id, outs_by_last_locked_block.first }); } -} -void BlockchainLMDB::add_spent_key(const crypto::key_image& k_image) -{ - LOG_PRINT_L3("BlockchainLMDB::" << __func__); - check_open(); - mdb_txn_cursors *m_cursors = &m_wcursors; + // 2b. Sort the vector in descending order by output ID, so outputs are ordered most recently created first + std::sort(unified_ids.begin(), unified_ids.end(), [](const unified_id_pair_t &a, const unified_id_pair_t &b) + {return a.first > b.first; }); - CURSOR(spent_keys) + // 2c. Remove all outputs from the result container that were created at height start_block_idx or higher + for (const auto &unified_id : unified_ids) + { + const auto output_tx = this->get_output_tx_and_index_from_unified(unified_id.first); + const uint64_t created_block_idx = this->get_tx_block_height(output_tx.first); - MDB_val k = {sizeof(k_image), (void *)&k_image}; - if (auto result = mdb_cursor_put(m_cur_spent_keys, (MDB_val *)&zerokval, &k, MDB_NODUPDATA)) { - if (result == MDB_KEYEXIST) - throw1(KEY_IMAGE_EXISTS("Attempting to add spent key image that's already in the db")); - else - throw1(DB_ERROR(lmdb_error("Error adding spent key image to db transaction: ", result).c_str())); - } -} + // As soon as we encounter an output created before start_block_idx, we're done, we've removed all we needed to + if (created_block_idx < start_block_idx) + break; -void BlockchainLMDB::remove_spent_key(const crypto::key_image& k_image) -{ - LOG_PRINT_L3("BlockchainLMDB::" << __func__); - check_open(); - mdb_txn_cursors *m_cursors = &m_wcursors; + // Find the output in the outs container + auto blk_it = outs.find(unified_id.second/*last_locked_block*/); - CURSOR(spent_keys) + // The first one in the vec should be the output id since outputs were added from the table in descending order + if (blk_it == outs.end()) + throw0(DB_ERROR("Missing output's last locked block")); + if (blk_it->second.empty()) + throw0(DB_ERROR("last locked block missing outputs")); + if (blk_it->second.front().unified_id != unified_id.first) + throw0(DB_ERROR("Output id is not first in outs by last locked block")); - MDB_val k = {sizeof(k_image), (void *)&k_image}; - auto result = mdb_cursor_get(m_cur_spent_keys, (MDB_val *)&zerokval, &k, MDB_GET_BOTH); - if (result != 0 && result != MDB_NOTFOUND) - throw1(DB_ERROR(lmdb_error("Error finding spent key to remove", result).c_str())); - if (!result) + // Remove the output from outs container + blk_it->second.erase(blk_it->second.begin()); + if (blk_it->second.empty()) + outs.erase(blk_it); + } + + // 3. Sort outputs of each last locked block by output id + for (auto &outs_by_last_locked_block : outs) { - result = mdb_cursor_del(m_cur_spent_keys, 0); - if (result) - throw1(DB_ERROR(lmdb_error("Error adding removal of key image to db transaction", result).c_str())); + auto &unsorted = outs_by_last_locked_block.second; + std::sort(unsorted.begin(), unsorted.end(), + [](const fcmp_pp::UnifiedOutput &a, const fcmp_pp::UnifiedOutput &b) + {return a.unified_id < b.unified_id; }); } + + TXN_POSTFIX_RDONLY(); + + return outs; } BlockchainLMDB::~BlockchainLMDB() @@ -1303,7 +2569,7 @@ BlockchainLMDB::~BlockchainLMDB() BlockchainLMDB::close(); } -BlockchainLMDB::BlockchainLMDB(bool batch_transactions): BlockchainDB() +BlockchainLMDB::BlockchainLMDB(bool batch_transactions, std::shared_ptr curve_trees): BlockchainDB() { LOG_PRINT_L3("BlockchainLMDB::" << __func__); // initialize folder to something "safe" just in case @@ -1320,6 +2586,8 @@ BlockchainLMDB::BlockchainLMDB(bool batch_transactions): BlockchainDB() // reset may also need changing when initialize things here m_hardfork = nullptr; + + m_curve_trees = curve_trees; } #ifdef WIN32 @@ -1380,6 +2648,9 @@ void BlockchainLMDB::open(const std::string& filename, const int db_flags) if (m_open) throw0(DB_OPEN_FAILURE("Attempted to open db, but it's already open")); + if (m_curve_trees == nullptr) + throw0(DB_OPEN_FAILURE("curve trees not set yet, must be set before opening db")); + boost::filesystem::path direc(filename); if (!boost::filesystem::exists(direc) && !boost::filesystem::create_directories(direc)) { @@ -1498,6 +2769,14 @@ void BlockchainLMDB::open(const std::string& filename, const int db_flags) lmdb_db_open(txn, LMDB_SPENT_KEYS, MDB_INTEGERKEY | MDB_CREATE | MDB_DUPSORT | MDB_DUPFIXED, m_spent_keys, "Failed to open db handle for m_spent_keys"); + lmdb_db_open(txn, LMDB_LOCKED_OUTPUTS, MDB_INTEGERKEY | MDB_DUPSORT | MDB_DUPFIXED | MDB_CREATE, m_locked_outputs, "Failed to open db handle for m_locked_outputs"); + lmdb_db_open(txn, LMDB_LEAVES, MDB_INTEGERKEY | MDB_DUPSORT | MDB_DUPFIXED | MDB_CREATE, m_leaves, "Failed to open db handle for m_leaves"); + lmdb_db_open(txn, LMDB_LAYERS, MDB_INTEGERKEY | MDB_DUPSORT | MDB_DUPFIXED | MDB_CREATE, m_layers, "Failed to open db handle for m_layers"); + lmdb_db_open(txn, LMDB_TREE_EDGES, MDB_INTEGERKEY | MDB_CREATE, m_tree_edges, "Failed to open db handle for m_tree_edges"); + lmdb_db_open(txn, LMDB_TREE_META, MDB_INTEGERKEY | MDB_CREATE, m_tree_meta, "Failed to open db handle for m_tree_meta"); + + lmdb_db_open(txn, LMDB_TIMELOCKED_OUTPUTS, MDB_INTEGERKEY | MDB_DUPSORT | MDB_DUPFIXED | MDB_CREATE, m_timelocked_outputs, "Failed to open db handle for m_timelocked_outputs"); + lmdb_db_open(txn, LMDB_TXPOOL_META, MDB_CREATE, m_txpool_meta, "Failed to open db handle for m_txpool_meta"); lmdb_db_open(txn, LMDB_TXPOOL_BLOB, MDB_CREATE, m_txpool_blob, "Failed to open db handle for m_txpool_blob"); @@ -1517,6 +2796,12 @@ void BlockchainLMDB::open(const std::string& filename, const int db_flags) mdb_set_dupsort(txn, m_block_heights, compare_hash32); mdb_set_dupsort(txn, m_tx_indices, compare_hash32); mdb_set_dupsort(txn, m_output_amounts, compare_uint64); + mdb_set_dupsort(txn, m_locked_outputs, compare_uint64); + mdb_set_dupsort(txn, m_leaves, compare_uint64); + mdb_set_dupsort(txn, m_layers, compare_uint64); + mdb_set_compare(txn, m_tree_edges, compare_uint64); + mdb_set_compare(txn, m_tree_meta, compare_uint64); + mdb_set_dupsort(txn, m_timelocked_outputs, compare_uint64); mdb_set_dupsort(txn, m_output_txs, compare_uint64); mdb_set_dupsort(txn, m_block_info, compare_uint64); if (!(mdb_flags & MDB_RDONLY)) @@ -1573,7 +2858,10 @@ void BlockchainLMDB::open(const std::string& filename, const int db_flags) // We don't handle the old format previous to that commit. txn.commit(); m_open = true; + // Decrement num active txs so db can resize if needed + mdb_txn_safe::increment_txns(-1); migrate(db_version); + mdb_txn_safe::increment_txns(1); return; } #endif @@ -1694,6 +2982,18 @@ void BlockchainLMDB::reset() throw0(DB_ERROR(lmdb_error("Failed to drop m_output_amounts: ", result).c_str())); if (auto result = mdb_drop(txn, m_spent_keys, 0)) throw0(DB_ERROR(lmdb_error("Failed to drop m_spent_keys: ", result).c_str())); + if (auto result = mdb_drop(txn, m_locked_outputs, 0)) + throw0(DB_ERROR(lmdb_error("Failed to drop m_locked_outputs: ", result).c_str())); + if (auto result = mdb_drop(txn, m_leaves, 0)) + throw0(DB_ERROR(lmdb_error("Failed to drop m_leaves: ", result).c_str())); + if (auto result = mdb_drop(txn, m_layers, 0)) + throw0(DB_ERROR(lmdb_error("Failed to drop m_layers: ", result).c_str())); + if (auto result = mdb_drop(txn, m_tree_edges, 0)) + throw0(DB_ERROR(lmdb_error("Failed to drop m_tree_edges: ", result).c_str())); + if (auto result = mdb_drop(txn, m_tree_meta, 0)) + throw0(DB_ERROR(lmdb_error("Failed to drop m_tree_meta: ", result).c_str())); + if (auto result = mdb_drop(txn, m_timelocked_outputs, 0)) + throw0(DB_ERROR(lmdb_error("Failed to drop m_timelocked_outputs: ", result).c_str())); (void)mdb_drop(txn, m_hf_starting_heights, 0); // this one is dropped in new code if (auto result = mdb_drop(txn, m_hf_versions, 0)) throw0(DB_ERROR(lmdb_error("Failed to drop m_hf_versions: ", result).c_str())); @@ -2936,7 +4236,7 @@ uint64_t BlockchainLMDB::num_outputs() const if (result == MDB_NOTFOUND) num = 0; else if (result == 0) - num = 1 + ((const outtx*)v.mv_data)->output_id; + num = 1 + ((const outtx*)v.mv_data)->unified_id; else throw0(DB_ERROR(lmdb_error("Failed to query m_output_txs: ", result).c_str())); @@ -3429,7 +4729,7 @@ uint64_t BlockchainLMDB::get_num_outputs(const uint64_t& amount) const return num_elems; } -output_data_t BlockchainLMDB::get_output_key(const uint64_t& amount, const uint64_t& index, bool include_commitmemt) const +outkey BlockchainLMDB::get_output_key(const uint64_t& amount, const uint64_t& index, bool include_commitmemt) const { LOG_PRINT_L3("BlockchainLMDB::" << __func__); check_open(); @@ -3446,24 +4746,25 @@ output_data_t BlockchainLMDB::get_output_key(const uint64_t& amount, const uint6 else if (get_result) throw0(DB_ERROR("Error attempting to retrieve an output pubkey from the db")); - output_data_t ret; + outkey ret; if (amount == 0) { - const outkey *okp = (const outkey *)v.mv_data; - ret = okp->data; + ret = *(const outkey *)v.mv_data; } else { const pre_rct_outkey *okp = (const pre_rct_outkey *)v.mv_data; - memcpy(&ret, &okp->data, sizeof(pre_rct_output_data_t)); + ret.amount_index = okp->amount_index; + ret.unified_id = okp->unified_id; + memcpy(&ret.data, &okp->data, sizeof(pre_rct_output_data_t)); if (include_commitmemt) - ret.commitment = rct::zeroCommit(amount); + ret.data.commitment = rct::zeroCommitVartime(amount); } TXN_POSTFIX_RDONLY(); return ret; } -tx_out_index BlockchainLMDB::get_output_tx_and_index_from_global(const uint64_t& output_id) const +tx_out_index BlockchainLMDB::get_output_tx_and_index_from_unified(const uint64_t& unified_id) const { LOG_PRINT_L3("BlockchainLMDB::" << __func__); check_open(); @@ -3471,7 +4772,7 @@ tx_out_index BlockchainLMDB::get_output_tx_and_index_from_global(const uint64_t& TXN_PREFIX_RDONLY(); RCURSOR(output_txs); - MDB_val_set(v, output_id); + MDB_val_set(v, unified_id); auto get_result = mdb_cursor_get(m_cur_output_txs, (MDB_val *)&zerokval, &v, MDB_GET_BOTH); if (get_result == MDB_NOTFOUND) @@ -3749,7 +5050,7 @@ bool BlockchainLMDB::for_all_outputs(std::functionoutput_id); + tx_out_index toi = get_output_tx_and_index_from_unified(ok->unified_id); if (!f(amount, toi.first, ok->data.height, toi.second)) { fret = false; break; @@ -4083,7 +5384,7 @@ void BlockchainLMDB::block_rtxn_abort() const } uint64_t BlockchainLMDB::add_block(const std::pair& blk, size_t block_weight, uint64_t long_term_block_weight, const difficulty_type& cumulative_difficulty, const uint64_t& coins_generated, - const std::vector>& txs) + const std::vector>& txs, const std::unordered_map& transparent_amount_commitments) { LOG_PRINT_L3("BlockchainLMDB::" << __func__); check_open(); @@ -4101,7 +5402,7 @@ uint64_t BlockchainLMDB::add_block(const std::pair& blk, size_t try { - BlockchainDB::add_block(blk, block_weight, long_term_block_weight, cumulative_difficulty, coins_generated, txs); + BlockchainDB::add_block(blk, block_weight, long_term_block_weight, cumulative_difficulty, coins_generated, txs, transparent_amount_commitments); } catch (const DB_ERROR_TXN_START& e) { @@ -4130,20 +5431,20 @@ void BlockchainLMDB::pop_block(block& blk, std::vector& txs) } } -void BlockchainLMDB::get_output_tx_and_index_from_global(const std::vector &global_indices, +void BlockchainLMDB::get_output_tx_and_index_from_unified(const std::vector &unified_ids, std::vector &tx_out_indices) const { LOG_PRINT_L3("BlockchainLMDB::" << __func__); check_open(); tx_out_indices.clear(); - tx_out_indices.reserve(global_indices.size()); + tx_out_indices.reserve(unified_ids.size()); TXN_PREFIX_RDONLY(); RCURSOR(output_txs); - for (const uint64_t &output_id : global_indices) + for (const uint64_t &unified_id : unified_ids) { - MDB_val_set(v, output_id); + MDB_val_set(v, unified_id); auto get_result = mdb_cursor_get(m_cur_output_txs, (MDB_val *)&zerokval, &v, MDB_GET_BOTH); if (get_result == MDB_NOTFOUND) @@ -4203,7 +5504,7 @@ void BlockchainLMDB::get_output_key(const epee::span &amounts, c outputs.resize(outputs.size() + 1); output_data_t &data = outputs.back(); memcpy(&data, &okp->data, sizeof(pre_rct_output_data_t)); - data.commitment = rct::zeroCommit(amount); + data.commitment = rct::zeroCommitVartime(amount); } } @@ -4237,18 +5538,78 @@ void BlockchainLMDB::get_output_tx_and_index(const uint64_t& amount, const std:: throw0(DB_ERROR(lmdb_error("Error attempting to retrieve an output from the db", get_result).c_str())); const outkey *okp = (const outkey *)v.mv_data; - tx_indices.push_back(okp->output_id); + tx_indices.push_back(okp->unified_id); } TIME_MEASURE_START(db3); if(tx_indices.size() > 0) { - get_output_tx_and_index_from_global(tx_indices, indices); + get_output_tx_and_index_from_unified(tx_indices, indices); } TIME_MEASURE_FINISH(db3); LOG_PRINT_L3("db3: " << db3); } +std::vector BlockchainLMDB::get_tx_output_data(const crypto::hash& h, cryptonote::transaction &tx) const +{ + LOG_PRINT_L3("BlockchainLMDB::" << __func__); + check_open(); + + TXN_PREFIX_RDONLY(); + RCURSOR(tx_indices); + RCURSOR(tx_outputs); + + // Fetch the pruned tx + cryptonote::blobdata bd; + if (!this->get_pruned_tx_blob(h, bd)) + throw1(TX_DNE(std::string("tx with hash ").append(epee::string_tools::pod_to_hex(h)).append(" not found in db").c_str())); + if (!parse_and_validate_tx_base_from_blob(bd, tx)) + throw0(DB_ERROR("Failed to parse tx from blob retrieved from the db")); + + // Get txid from tx_indices table + MDB_val_set(v, h); + int result = mdb_cursor_get(m_cur_tx_indices, (MDB_val *)&zerokval, &v, MDB_GET_BOTH); + if (result == MDB_NOTFOUND) + throw1(TX_DNE(lmdb_error(std::string("tx data with hash ") + epee::string_tools::pod_to_hex(h) + " not found in db: ", result).c_str())); + else if (result) + throw0(DB_ERROR(lmdb_error("DB error attempting to fetch tx data from hash: ", result).c_str())); + txindex *tip = (txindex *)v.mv_data; + + // Get tx amount output indices from tx_outputs table + MDB_val_set(k_tx_id, tip->data.tx_id); + MDB_val v_tx_output; + + result = mdb_cursor_get(m_cur_tx_outputs, &k_tx_id, &v_tx_output, MDB_SET); + if (result == MDB_NOTFOUND) + { + LOG_PRINT_L0("WARNING: Unexpected: tx has no amount indices stored in " + "tx_outputs, but it should have an empty entry even if it's a tx without " + "outputs"); + return {}; + } + else if (result) + throw0(DB_ERROR(lmdb_error("DB error attempting to get data for tx_outputs[tx_index]", result).c_str())); + + const uint64_t* amount_output_indices = (const uint64_t*)v_tx_output.mv_data; + const size_t num_amount_output_indices = v_tx_output.mv_size / sizeof(uint64_t); + + // Get tx output amounts from pruned tx + std::vector amounts; + amounts.reserve(tx.vout.size()); + for (const auto &out : tx.vout) + amounts.push_back(tx.version >= 2 ? 0 : out.amount); + if (amounts.size() != num_amount_output_indices) + throw0(DB_ERROR("Unexpected size mismatch amount output indices <> vouts")); + + // Get output data + std::vector outs; + for (size_t i = 0; i < amounts.size(); ++i) + outs.emplace_back(this->get_output_key(amounts[i], amount_output_indices[i], true/*include_commitment*/)); + + TXN_POSTFIX_RDONLY(); + return outs; +} + std::map> BlockchainLMDB::get_output_histogram(const std::vector &amounts, bool unlocked, uint64_t recent_cutoff, uint64_t min_count) const { LOG_PRINT_L3("BlockchainLMDB::" << __func__); @@ -5099,7 +6460,8 @@ void BlockchainLMDB::migrate_0_1() throw0(DB_ERROR("Failed to parse block from blob retrieved from the db")); const auto miner_blob = tx_to_blob(b.miner_tx); - add_transaction(null_hash, b.miner_tx, epee::strspan(miner_blob)); + // Empty transparent amount commitments is ok bc only needed for v2 txs, which v0 db's can't have + add_transaction(null_hash, b.miner_tx, epee::strspan(miner_blob), {}); for (unsigned int j = 0; j(v.mv_data), v.mv_size}; if (!parse_and_validate_tx_from_blob(bd, tx)) throw0(DB_ERROR("Failed to parse tx from blob retrieved from the db")); - add_transaction(null_hash, std::move(tx), epee::strspan(bd), &b.tx_hashes[j]); + add_transaction(null_hash, std::move(tx), epee::strspan(bd), {}, &b.tx_hashes[j]); result = mdb_cursor_del(c_txs, 0); if (result) throw0(DB_ERROR(lmdb_error("Failed to get record from txs: ", result).c_str())); @@ -5706,6 +7068,347 @@ void BlockchainLMDB::migrate_4_5() txn.commit(); } +void BlockchainLMDB::migrate_5_6() +{ + LOG_PRINT_L3("BlockchainLMDB::" << __func__); + uint64_t i; + int result; + mdb_txn_safe txn(false); + + MGINFO_YELLOW("Migrating blockchain from DB version 5 to 6 - this may take a while:"); + + MDB_dbi m_tmp_last_output; + do + { + // 1. Prepare all valid outputs to be inserted into the merkle tree and + // place them in a locked outputs table. The key to this new table is the + // block id in which the outputs unlock. + { + MGINFO("Setting up a locked outputs table (step 1/2 of the full-chain membership proof migration)"); + + result = mdb_txn_begin(m_env, NULL, 0, txn); + if (result) + throw0(DB_ERROR(lmdb_error("Failed to create a transaction for the db: ", result).c_str())); + lmdb_db_open(txn, "tmp_last_output", MDB_INTEGERKEY | MDB_CREATE, m_tmp_last_output, "Failed to open db handle for m_tmp_last_output"); + txn.commit(); + + if (!m_batch_transactions) + set_batch_transactions(true); + const std::size_t BATCH_SIZE = 10000; + batch_start(); + txn.m_txn = m_write_txn->m_txn; + + // Use this cache to know how to restart the migration if the process is killed + struct tmp_output_cache { uint64_t n_outputs_read; uint64_t amount; outkey ok; }; + tmp_output_cache last_output; + + MDB_cursor *c_output_amounts, *c_locked_outputs, *c_tmp_last_output, *c_timelocked_outputs; + MDB_val k, v; + + i = 0; + const uint64_t n_outputs = this->num_outputs(); + std::size_t progress_width = 0; + MDB_cursor_op op = MDB_FIRST; + while (1) + { + if (!(i % BATCH_SIZE)) + { + if (i) + { + LOGIF(el::Level::Info) + { + const uint64_t percent = std::min((i * 100) / n_outputs, (uint64_t)99); + const std::string progress_line = std::to_string(i) + " / " + std::to_string(n_outputs) + + " outputs (" + std::to_string(percent) + "% of step 1/2)"; + std::cout << '\r' << std::string(progress_width, ' ') << '\r' << progress_line << std::flush; + progress_width = std::max(progress_width, progress_line.size()); + } + + // Update last output read + MDB_val_set(v_last_output, last_output); + result = mdb_cursor_put(c_tmp_last_output, (MDB_val*)&zerokval, &v_last_output, 0); + if (result) + throw0(DB_ERROR(lmdb_error("Failed to update max output id: ", result).c_str())); + + // Commit and start a new txn + batch_stop(); + batch_start(); + txn.m_txn = m_write_txn->m_txn; + + // Reset k and v so we continue migration from the last output + k = {sizeof(last_output.amount), (void *)&last_output.amount}; + + const std::size_t outkey_size = (last_output.amount == 0) ? sizeof(outkey) : sizeof(pre_rct_outkey); + v = {outkey_size, (void *)&last_output.ok}; + } + + // Open all cursors + result = mdb_cursor_open(txn, m_output_amounts, &c_output_amounts); + if (result) + throw0(DB_ERROR(lmdb_error("Failed to open a cursor for output amounts: ", result).c_str())); + result = mdb_cursor_open(txn, m_locked_outputs, &c_locked_outputs); + if (result) + throw0(DB_ERROR(lmdb_error("Failed to open a cursor for locked outputs: ", result).c_str())); + result = mdb_cursor_open(txn, m_tmp_last_output, &c_tmp_last_output); + if (result) + throw0(DB_ERROR(lmdb_error("Failed to open a cursor for temp last output: ", result).c_str())); + result = mdb_cursor_open(txn, m_timelocked_outputs, &c_timelocked_outputs); + if (result) + throw0(DB_ERROR(lmdb_error("Failed to open a cursor for timelocked outputs: ", result).c_str())); + + // Get the cached last output from the db + bool found_cached_output = false; + tmp_output_cache cached_last_o; + if (i == 0) + { + MDB_val v_last_output; + result = mdb_cursor_get(c_tmp_last_output, (MDB_val*)&zerokval, &v_last_output, MDB_SET); + if (result != MDB_SUCCESS && result != MDB_NOTFOUND) + throw0(DB_ERROR(lmdb_error("Failed to get max output id: ", result).c_str())); + if (result != MDB_NOTFOUND) + { + cached_last_o = *(const tmp_output_cache*)v_last_output.mv_data; + + if (n_outputs < cached_last_o.n_outputs_read) + throw0(DB_ERROR("Unexpected n_outputs_read on cached last output")); + if (n_outputs == cached_last_o.n_outputs_read) + break; + + MDEBUG("Found cached output " << cached_last_o.ok.unified_id + << ", migrated " << cached_last_o.n_outputs_read << " outputs already"); + found_cached_output = true; + + // Set k and v so we can continue the migration from that output + k = {sizeof(cached_last_o.amount), (void *)&cached_last_o.amount}; + + const std::size_t outkey_size = (cached_last_o.amount == 0) ? sizeof(outkey) : sizeof(pre_rct_outkey); + v = {outkey_size, (void *)&cached_last_o.ok}; + + i = cached_last_o.n_outputs_read; + op = MDB_NEXT; + } + } + + // Advance the output_amounts cursor to the last output read + if (i || found_cached_output) + { + result = mdb_cursor_get(c_output_amounts, &k, &v, MDB_GET_BOTH); + if (result) + throw0(DB_ERROR(lmdb_error("Failed to advance cursor for output amounts: ", result).c_str())); + } + } + + // Get the next output from the db + result = mdb_cursor_get(c_output_amounts, &k, &v, op); + op = MDB_NEXT; + if (result == MDB_NOTFOUND) + { + // Indicate we've read all outputs so we know the migration step is complete + last_output.n_outputs_read = n_outputs; + MDB_val_set(v_last_output, last_output); + result = mdb_cursor_put(c_tmp_last_output, (MDB_val*)&zerokval, &v_last_output, 0); + if (result) + throw0(DB_ERROR(lmdb_error("Failed to update max output id: ", result).c_str())); + + if (progress_width) + std::cout << '\r' << std::string(progress_width, ' ') << '\r' << std::flush; + //std::cout << " 150000000 / 150000000 outputs (100% of step 1/2)\r" << std::flush; // just showing that chars are erased effectively + batch_stop(); + break; + } + if (result != MDB_SUCCESS) + throw0(DB_ERROR(lmdb_error("Failed to get a record from output amounts: ", result).c_str())); + + ++i; + const bool commit_next_iter = i && !(i % BATCH_SIZE); + + // Read the output data + uint64_t amount = *(const uint64_t*)k.mv_data; + output_data_t output_data; + uint64_t unified_id; + if (amount == 0) + { + const outkey *okp = (const outkey *)v.mv_data; + output_data = okp->data; + unified_id = okp->unified_id; + if (commit_next_iter) + memcpy(&last_output.ok, okp, sizeof(outkey)); + } + else + { + const pre_rct_outkey *okp = (const pre_rct_outkey *)v.mv_data; + memcpy(&output_data, &okp->data, sizeof(pre_rct_output_data_t)); + output_data.commitment = rct::zeroCommitVartime(amount); + unified_id = okp->unified_id; + if (commit_next_iter) + memcpy(&last_output.ok, okp, sizeof(pre_rct_outkey)); + } + + if (commit_next_iter) + { + // Set last output metadata + last_output.amount = amount; + last_output.n_outputs_read = i; + } + + // Prepare the output for insertion to the tree (all outputs in the db at this point must be legacy) + fcmp_pp::LegacyOutputPair output_pair{{ + output_data.pubkey, + rct::rct2pt(output_data.commitment), + }}; + + const fcmp_pp::UnifiedOutput unified_output{ + .unified_id = unified_id, + .output_pair = std::move(output_pair) + }; + const cryptonote::blobdata output_blob = cryptonote::t_serializable_object_to_blob(unified_output); + + // Get the output's last locked block + const uint64_t last_locked_block = cryptonote::get_last_locked_block_index(output_data.unlock_time, output_data.height); + + // Add the output to the locked outputs table + MDB_val_set(k_block_id, last_locked_block); + MDB_val_sized(v_output, output_blob); + + // MDB_NODUPDATA because all output id's should be unique + // Can't use MDB_APPENDDUP because outputs aren't inserted in order sorted by unified_id + result = mdb_cursor_put(c_locked_outputs, &k_block_id, &v_output, MDB_NODUPDATA); + if (result != MDB_SUCCESS) + throw0(DB_ERROR(lmdb_error("Failed to add locked output: ", result).c_str())); + + // Check if the output is a coinbase output + bool is_coinbase = false; + const bool has_coinbase_last_locked_block = last_locked_block == (output_data.height + CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW - 1); + if (has_coinbase_last_locked_block) + { + // Only coinbase outputs could potentially have the coinbase last locked block (see prevalidate_miner_transaction) + auto toi = this->get_output_tx_and_index_from_unified(unified_id); + auto tx = this->get_pruned_tx(toi.first); + is_coinbase = cryptonote::is_coinbase(tx); + } + + // Add custom timelocked outputs to the timelocked outputs table + if (!cryptonote::is_custom_timelocked(is_coinbase, last_locked_block, output_data.height)) + continue; + + MDB_val_set(k_timelocked_block_id, last_locked_block); + MDB_val_sized(v_timelocked_output, output_blob); + + // MDB_NODUPDATA because all output id's should be unique + // Can't use MDB_APPENDDUP because outputs aren't inserted in order sorted by unified_id + result = mdb_cursor_put(c_timelocked_outputs, &k_timelocked_block_id, &v_timelocked_output, MDB_NODUPDATA); + if (result != MDB_SUCCESS) + throw0(DB_ERROR(lmdb_error("Failed to add timelocked output: ", result).c_str())); + } + } + + // 2. Set up the curve trees merkle tree by growing the tree block by block, + // with leaves that are spendable in each respective block + { + MGINFO("Setting up a merkle tree using existing cryptonote outputs (step 2/2 of the full-chain membership proof migration)"); + + if (!m_batch_transactions) + set_batch_transactions(true); + const std::size_t BATCH_SIZE = 50; + batch_start(); + txn.m_txn = m_write_txn->m_txn; + + MDB_cursor *c_locked_outputs; + + i = 0; + const uint64_t n_blocks = height(); + std::size_t progress_width = 0; + while (i < n_blocks) + { + if (!(i % BATCH_SIZE)) + { + if (i) + { + LOGIF(el::Level::Info) + { + const uint64_t percent = std::min((i * 100) / n_blocks, (uint64_t)99); + const std::string progress_line = std::to_string(i) + " / " + std::to_string(n_blocks) + + " blocks (" + std::to_string(percent) + "% of step 2/2)"; + std::cout << '\r' << std::string(progress_width, ' ') << '\r' << progress_line << std::flush; + progress_width = std::max(progress_width, progress_line.size()); + } + + batch_stop(); + batch_start(); + txn.m_txn = m_write_txn->m_txn; + } + + // Open all cursors + result = mdb_cursor_open(txn, m_locked_outputs, &c_locked_outputs); + if (result) + throw0(DB_ERROR(lmdb_error("Failed to open a cursor for locked outputs: ", result).c_str())); + + // See what the last block inserted into the new table was + if (i == 0) + { + MDB_stat db_stats; + result = mdb_stat(txn, m_tree_meta, &db_stats); + if (result) + throw0(DB_ERROR(lmdb_error("Failed to query m_tree_meta: ", result).c_str())); + const uint64_t n_tree_blocks = db_stats.ms_entries; + const uint64_t tree_block_idx = n_tree_blocks - std::min(1, n_tree_blocks); + const uint64_t last_added_block_idx = tree_block_idx - std::min((CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE - 1), tree_block_idx); + CHECK_AND_ASSERT_THROW_MES(n_tree_blocks == 0 || cryptonote::get_default_last_locked_block_index(last_added_block_idx) == tree_block_idx, + "Unexpected tree block idx mismatch to last added block idx"); + if (last_added_block_idx) + { + MINFO("Continuing from last added block " << last_added_block_idx); + i = last_added_block_idx + 1; + } + if (i == n_blocks) + break; + } + } + + this->advance_tree(i, {}); + + LOGIF(el::Level::Info) + { + if ((i % 1000) == 0) + { + const uint64_t n_leaf_tuples = this->get_block_n_leaf_tuples(i); + crypto::ec_point tree_root; + this->get_tree_root_at_blk_idx(i, tree_root); + const std::string tree_root_str = epee::string_tools::pod_to_hex(tree_root); + MINFO("Block: " << i << ", tree root: " << tree_root_str << ", leaves: " << n_leaf_tuples); + } + } + + ++i; + if (i == n_blocks) + if (progress_width) + std::cout << '\r' << std::string(progress_width, ' ') << '\r' << std::flush; + } + batch_stop(); + } + } while(0); + + // Update db version + uint32_t version = 6; + MDB_val v; + v.mv_data = (void *)&version; + v.mv_size = sizeof(version); + MDB_val_str(vk, "version"); + result = mdb_txn_begin(m_env, NULL, 0, txn); + if (result) + throw0(DB_ERROR(lmdb_error("Failed to create a transaction for the db: ", result).c_str())); + result = mdb_put(txn, m_properties, &vk, &v, 0); + if (result) + throw0(DB_ERROR(lmdb_error("Failed to update version for the db: ", result).c_str())); + + // We only needed the temp last output table for this migration, drop it + result = mdb_drop(txn, m_tmp_last_output, 1); + if (result) + throw0(DB_ERROR(lmdb_error("Failed to drop temp last output table: ", result).c_str())); + + txn.commit(); +} + void BlockchainLMDB::migrate(const uint32_t oldversion) { if (oldversion < 1) @@ -5718,6 +7421,8 @@ void BlockchainLMDB::migrate(const uint32_t oldversion) migrate_3_4(); if (oldversion < 5) migrate_4_5(); + if (oldversion < 6) + migrate_5_6(); } } // namespace cryptonote diff --git a/src/blockchain_db/lmdb/db_lmdb.h b/src/blockchain_db/lmdb/db_lmdb.h index a3924190a2a..195f442eafc 100644 --- a/src/blockchain_db/lmdb/db_lmdb.h +++ b/src/blockchain_db/lmdb/db_lmdb.h @@ -30,6 +30,7 @@ #include "blockchain_db/blockchain_db.h" #include "cryptonote_basic/blobdatatype.h" // for type blobdata +#include "fcmp_pp/curve_trees.h" #include "ringct/rctTypes.h" #include @@ -64,6 +65,14 @@ typedef struct mdb_txn_cursors MDB_cursor *m_txc_spent_keys; + MDB_cursor *m_txc_locked_outputs; + MDB_cursor *m_txc_leaves; + MDB_cursor *m_txc_layers; + MDB_cursor *m_txc_tree_edges; + MDB_cursor *m_txc_tree_meta; + + MDB_cursor *m_txc_timelocked_outputs; + MDB_cursor *m_txc_txpool_meta; MDB_cursor *m_txc_txpool_blob; @@ -87,6 +96,12 @@ typedef struct mdb_txn_cursors #define m_cur_tx_indices m_cursors->m_txc_tx_indices #define m_cur_tx_outputs m_cursors->m_txc_tx_outputs #define m_cur_spent_keys m_cursors->m_txc_spent_keys +#define m_cur_locked_outputs m_cursors->m_txc_locked_outputs +#define m_cur_leaves m_cursors->m_txc_leaves +#define m_cur_layers m_cursors->m_txc_layers +#define m_cur_tree_edges m_cursors->m_txc_tree_edges +#define m_cur_tree_meta m_cursors->m_txc_tree_meta +#define m_cur_timelocked_outputs m_cursors->m_txc_timelocked_outputs #define m_cur_txpool_meta m_cursors->m_txc_txpool_meta #define m_cur_txpool_blob m_cursors->m_txc_txpool_blob #define m_cur_alt_blocks m_cursors->m_txc_alt_blocks @@ -109,6 +124,12 @@ typedef struct mdb_rflags bool m_rf_tx_indices; bool m_rf_tx_outputs; bool m_rf_spent_keys; + bool m_rf_locked_outputs; + bool m_rf_leaves; + bool m_rf_layers; + bool m_rf_tree_edges; + bool m_rf_tree_meta; + bool m_rf_timelocked_outputs; bool m_rf_txpool_meta; bool m_rf_txpool_blob; bool m_rf_alt_blocks; @@ -183,7 +204,7 @@ struct mdb_txn_safe class BlockchainLMDB final: public BlockchainDB { public: - BlockchainLMDB(bool batch_transactions=true); + BlockchainLMDB(bool batch_transactions=true, std::shared_ptr curve_trees = fcmp_pp::curve_trees::curve_trees_v1()); ~BlockchainLMDB(); void open(const std::string& filename, const int mdb_flags=0) override; @@ -258,6 +279,8 @@ class BlockchainLMDB final: public BlockchainDB bool get_prunable_tx_blob(const crypto::hash& h, cryptonote::blobdata &tx) const override; bool get_prunable_tx_hash(const crypto::hash& tx_hash, crypto::hash &prunable_hash) const override; + std::vector get_tx_output_data(const crypto::hash& h, cryptonote::transaction &tx) const override; + std::vector get_txids_loose(const crypto::hash& h, std::uint32_t bits, uint64_t max_num_txs = 0) override; uint64_t get_tx_count() const override; @@ -268,11 +291,11 @@ class BlockchainLMDB final: public BlockchainDB uint64_t get_num_outputs(const uint64_t& amount) const override; - output_data_t get_output_key(const uint64_t& amount, const uint64_t& index, bool include_commitmemt) const override; + outkey get_output_key(const uint64_t& amount, const uint64_t& index, bool include_commitmemt) const override; void get_output_key(const epee::span &amounts, const std::vector &offsets, std::vector &outputs, bool allow_partial = false) const override; - tx_out_index get_output_tx_and_index_from_global(const uint64_t& index) const override; - void get_output_tx_and_index_from_global(const std::vector &global_indices, + tx_out_index get_output_tx_and_index_from_unified(const uint64_t& index) const override; + void get_output_tx_and_index_from_unified(const std::vector &unified_ids, std::vector &tx_out_indices) const; tx_out_index get_output_tx_and_index(const uint64_t& amount, const uint64_t& index) const override; @@ -318,6 +341,7 @@ class BlockchainLMDB final: public BlockchainDB , const difficulty_type& cumulative_difficulty , const uint64_t& coins_generated , const std::vector>& txs + , const std::unordered_map& transparent_amount_commitments ) override; void set_batch_transactions(bool batch_transactions) override; @@ -355,9 +379,12 @@ class BlockchainLMDB final: public BlockchainDB // helper functions static int compare_uint64(const MDB_val *a, const MDB_val *b); + static int compare_uint8(const MDB_val *a, const MDB_val *b); static int compare_hash32(const MDB_val *a, const MDB_val *b); static int compare_string(const MDB_val *a, const MDB_val *b); + bool audit_tree(const uint64_t expected_n_leaf_tuples) const override; + private: void do_resize(uint64_t size_increase=0); @@ -401,7 +428,58 @@ class BlockchainLMDB final: public BlockchainDB void remove_spent_key(const crypto::key_image& k_image) override; - uint64_t num_outputs() const; + uint64_t num_outputs() const override; + + // + // Curve tree related db calls (private) + // + + void add_locked_outs(const fcmp_pp::OutsByLastLockedBlock& outs_by_last_locked_block, const std::unordered_map& timelocked_outputs) override; + + void del_locked_outs_at_block_idx(uint64_t block_idx) override; + + std::vector grow_with_tree_extension(const fcmp_pp::CompressedTreeExtension &tree_extension) override; + + crypto::ec_point grow_layer(const fcmp_pp::CompressedLayerExtension &layer_extension, const uint64_t layer_idx); + + uint64_t trim_leaves(const uint64_t new_n_leaf_tuples, const uint64_t trim_block_idx) override; + + void trim_layers(const uint64_t new_n_leaf_tuples, + const std::vector &n_elems_per_layer, + const std::vector &prev_tree_edge, + const uint64_t expected_root_idx) override; + + void trim_layer(const uint64_t new_n_elems_in_layer, const uint64_t layer_idx); + + void save_tree_meta(const uint64_t block_idx, const uint64_t n_leaf_tuples, const std::vector &tree_edge) override; + + void del_tree_meta(const uint64_t block_idx) override; + + uint64_t get_n_leaf_tuples() const override; + + uint64_t get_block_n_leaf_tuples(uint64_t block_idx) const override; + + uint8_t get_tree_root_at_blk_idx(const uint64_t blk_idx, crypto::ec_point &tree_root_out) const override; + + std::vector get_tree_edge(uint64_t block_id) const override; + + fcmp_pp::CompressedPath get_path(const fcmp_pp::PathIndexes &path_indexes) const override; + + template + bool audit_layer(const std::unique_ptr &c_child, + const std::unique_ptr &c_parent, + const uint64_t child_layer_idx, + const uint64_t chunk_width) const; + + std::vector get_outs_at_last_locked_block_idx(uint64_t block_id) const override; + + uint64_t get_tree_block_idx() const override; + + fcmp_pp::OutsByLastLockedBlock get_custom_timelocked_outputs(uint64_t start_block_idx) const override; + + std::vector get_unified_output_by_id(const std::vector &unified_ids) const; + + uint64_t find_leaf_idx_by_unified_id_bounded_search(uint64_t unified_id, uint64_t leaf_idx_start, uint64_t leaf_idx_end) const override; // Hard fork void set_hard_fork_version(uint64_t height, uint8_t version) override; @@ -440,6 +518,9 @@ class BlockchainLMDB final: public BlockchainDB // migrate from DB version 4 to 5 void migrate_4_5(); + // migrate from DB version 5 to 6 + void migrate_5_6(); + void cleanup_batch(); private: @@ -462,6 +543,14 @@ class BlockchainLMDB final: public BlockchainDB MDB_dbi m_spent_keys; + MDB_dbi m_locked_outputs; + MDB_dbi m_leaves; + MDB_dbi m_layers; + MDB_dbi m_tree_edges; + MDB_dbi m_tree_meta; + + MDB_dbi m_timelocked_outputs; + MDB_dbi m_txpool_meta; MDB_dbi m_txpool_blob; diff --git a/src/blockchain_db/testdb.h b/src/blockchain_db/testdb.h index 5b1614e904d..ddf331bae4c 100644 --- a/src/blockchain_db/testdb.h +++ b/src/blockchain_db/testdb.h @@ -101,13 +101,15 @@ class BaseTestDB: public cryptonote::BlockchainDB { virtual uint64_t get_tx_count() const override { return 0; } virtual std::vector get_tx_list(const std::vector& hlist) const override { return std::vector(); } virtual uint64_t get_tx_block_height(const crypto::hash& h) const override { return 0; } + virtual uint64_t num_outputs() const override { return 1; } virtual uint64_t get_num_outputs(const uint64_t& amount) const override { return 1; } virtual uint64_t get_indexing_base() const override { return 0; } - virtual cryptonote::output_data_t get_output_key(const uint64_t& amount, const uint64_t& index, bool include_commitmemt) const override { return cryptonote::output_data_t(); } - virtual cryptonote::tx_out_index get_output_tx_and_index_from_global(const uint64_t& index) const override { return cryptonote::tx_out_index(); } + virtual cryptonote::outkey get_output_key(const uint64_t& amount, const uint64_t& index, bool include_commitmemt) const override { return cryptonote::outkey(); } + virtual cryptonote::tx_out_index get_output_tx_and_index_from_unified(const uint64_t& index) const override { return cryptonote::tx_out_index(); } virtual cryptonote::tx_out_index get_output_tx_and_index(const uint64_t& amount, const uint64_t& index) const override { return cryptonote::tx_out_index(); } virtual void get_output_tx_and_index(const uint64_t& amount, const std::vector &offsets, std::vector &indices) const override {} virtual void get_output_key(const epee::span &amounts, const std::vector &offsets, std::vector &outputs, bool allow_partial = false) const override {} + virtual std::vector get_tx_output_data(const crypto::hash& h, cryptonote::transaction &tx) const override { return std::vector{}; } virtual bool can_thread_bulk_indices() const override { return false; } virtual std::vector> get_tx_amount_output_indices(const uint64_t tx_index, size_t n_txes) const override { return std::vector>(); } virtual bool has_key_image(const crypto::key_image& img) const override { return false; } @@ -119,6 +121,25 @@ class BaseTestDB: public cryptonote::BlockchainDB { virtual void add_spent_key(const crypto::key_image& k_image) override {} virtual void remove_spent_key(const crypto::key_image& k_image) override {} + virtual void add_locked_outs(const fcmp_pp::OutsByLastLockedBlock& outs_by_last_locked_block, const std::unordered_map& timelocked_outputs) override {}; + virtual void advance_tree(const uint64_t block_idx, const std::vector &known_new_outputs) override {}; + virtual std::vector get_outs_at_last_locked_block_idx(uint64_t block_id) const override { return std::vector{}; }; + virtual void del_locked_outs_at_block_idx(uint64_t block_idx) override {}; + virtual uint64_t get_tree_block_idx() const override { return 0; }; + virtual std::vector get_tree_edge(uint64_t block_id) const override { return {}; }; + virtual void save_tree_meta(const uint64_t block_idx, const uint64_t n_leaf_tuples, const std::vector &tree_edge) override {}; + virtual void del_tree_meta(const uint64_t block_idx) override {}; + virtual std::vector grow_with_tree_extension(const fcmp_pp::CompressedTreeExtension &tree_extension) override { return std::vector{}; }; + virtual fcmp_pp::CompressedPath get_path(const fcmp_pp::PathIndexes &path_indexes) const override { return fcmp_pp::CompressedPath{}; }; + virtual uint64_t find_leaf_idx_by_unified_id_bounded_search(uint64_t unified_id, uint64_t leaf_idx_start, uint64_t leaf_idx_end) const override { return 0; }; + virtual uint64_t trim_leaves(const uint64_t new_n_leaf_tuples, const uint64_t trim_block_idx) override { return 0; }; + virtual void trim_layers(const uint64_t new_n_leaf_tuples, const std::vector &n_elems_per_layer, const std::vector &prev_tree_edge, const uint64_t expected_root_idx) override {}; + virtual bool audit_tree(const uint64_t expected_n_leaf_tuples) const override { return false; }; + virtual uint8_t get_tree_root_at_blk_idx(const uint64_t blk_idx, crypto::ec_point &tree_root_out) const override { return {}; }; + virtual uint64_t get_n_leaf_tuples() const override { return 0; }; + virtual uint64_t get_block_n_leaf_tuples(const uint64_t block_idx) const override { return 0; }; + virtual fcmp_pp::OutsByLastLockedBlock get_custom_timelocked_outputs(uint64_t start_block_idx) const override { return {{}}; }; + virtual bool for_all_key_images(std::function) const override { return true; } virtual bool for_blocks_range(const uint64_t&, const uint64_t&, std::function) const override { return true; } virtual bool for_all_transactions(std::function, bool pruned) const override { return true; } diff --git a/src/blockchain_utilities/blockchain_ancestry.cpp b/src/blockchain_utilities/blockchain_ancestry.cpp index bdf5a12195f..112f262c50d 100644 --- a/src/blockchain_utilities/blockchain_ancestry.cpp +++ b/src/blockchain_utilities/blockchain_ancestry.cpp @@ -283,7 +283,7 @@ static bool get_output_txid(ancestry_state_t &state, BlockchainDB *db, uint64_t return true; } - const output_data_t od = db->get_output_key(amount, offset, false); + const output_data_t od = db->get_output_key(amount, offset, false).data; cryptonote::block b; if (!get_block_from_height(state, db, od.height, b)) return false; diff --git a/src/blockchain_utilities/blockchain_depth.cpp b/src/blockchain_utilities/blockchain_depth.cpp index c59052b7647..723c14f6a5c 100644 --- a/src/blockchain_utilities/blockchain_depth.cpp +++ b/src/blockchain_utilities/blockchain_depth.cpp @@ -220,7 +220,7 @@ int main(int argc, char* argv[]) auto absolute_offsets = cryptonote::relative_output_offsets_to_absolute(txin.key_offsets); for (uint64_t offset: absolute_offsets) { - const output_data_t od = db->get_output_key(amount, offset); + const output_data_t od = db->get_output_key(amount, offset).data; const crypto::hash block_hash = db->get_block_hash_from_height(od.height); bd = db->get_block_blob(block_hash); cryptonote::block b; diff --git a/src/blockchain_utilities/blockchain_import.cpp b/src/blockchain_utilities/blockchain_import.cpp index 82ff2dedbfb..c05a0d8df3e 100644 --- a/src/blockchain_utilities/blockchain_import.cpp +++ b/src/blockchain_utilities/blockchain_import.cpp @@ -175,7 +175,8 @@ int check_flush(cryptonote::core &core, std::vector &block tx_verification_context tvc = AUTO_VAL_INIT(tvc); CHECK_AND_ASSERT_THROW_MES(tx_blob.prunable_hash == crypto::null_hash, "block entry must not contain pruned txs"); - core.handle_incoming_tx(tx_blob.blob, tvc, relay_method::block, true); + crypto::hash txid; + core.handle_incoming_tx(tx_blob.blob, tvc, relay_method::block, true, txid); if(tvc.m_verifivation_failed) { cryptonote::transaction transaction; @@ -487,10 +488,15 @@ int import_from_file(cryptonote::core& core, const std::string& import_file_path cumulative_difficulty = bp.cumulative_difficulty; coins_generated = bp.coins_generated; + const uint64_t first_unified_id = core.get_blockchain_storage().get_db().num_outputs(); + std::unordered_map transparent_amount_commitments; + const auto tx_refs = cryptonote::collect_transparent_amount_commitments(b.miner_tx, txs, transparent_amount_commitments); + try { uint64_t long_term_block_weight = core.get_blockchain_storage().get_next_long_term_block_weight(block_weight); - core.get_blockchain_storage().get_db().add_block(std::make_pair(b, block_to_blob(b)), block_weight, long_term_block_weight, cumulative_difficulty, coins_generated, txs); + const uint64_t new_height = core.get_blockchain_storage().get_db().add_block(std::make_pair(b, block_to_blob(b)), block_weight, long_term_block_weight, cumulative_difficulty, coins_generated, txs, transparent_amount_commitments); + cryptonote::handle_fcmp_tree(&core.get_blockchain_storage().get_db(), new_height - 1, first_unified_id, tx_refs, transparent_amount_commitments); } catch (const std::exception& e) { diff --git a/src/blockchain_utilities/blockchain_prune.cpp b/src/blockchain_utilities/blockchain_prune.cpp index bc5b6b62da2..93a334f1eab 100644 --- a/src/blockchain_utilities/blockchain_prune.cpp +++ b/src/blockchain_utilities/blockchain_prune.cpp @@ -53,7 +53,7 @@ static std::string db_path; static uint64_t records_per_sync = 16 * 65536; static const size_t slack = 512 * 1024 * 1024; -static constexpr uint32_t MAX_SUPPORTED_DB_VERSION = 5; +static constexpr uint32_t MAX_SUPPORTED_DB_VERSION = 6; static std::vector is_v1; @@ -709,6 +709,12 @@ int main(int argc, char* argv[]) copy_table(env0, env1, "alt_blocks", 0, 0, BlockchainLMDB::compare_hash32); copy_table(env0, env1, "hf_versions", MDB_INTEGERKEY, 0); copy_table(env0, env1, "properties", 0, 0, BlockchainLMDB::compare_string); + copy_table(env0, env1, "locked_outputs", MDB_INTEGERKEY | MDB_DUPSORT | MDB_DUPFIXED, 0, BlockchainLMDB::compare_uint64); + copy_table(env0, env1, "leaves", MDB_INTEGERKEY | MDB_DUPSORT | MDB_DUPFIXED, 0, BlockchainLMDB::compare_uint64); + copy_table(env0, env1, "layers", MDB_INTEGERKEY | MDB_DUPSORT | MDB_DUPFIXED, 0, BlockchainLMDB::compare_uint64); + copy_table(env0, env1, "tree_edges", MDB_INTEGERKEY, 0, BlockchainLMDB::compare_uint64); + copy_table(env0, env1, "tree_meta", MDB_INTEGERKEY, 0, BlockchainLMDB::compare_uint64); + copy_table(env0, env1, "timelocked_outputs", MDB_INTEGERKEY | MDB_DUPSORT | MDB_DUPFIXED, 0, BlockchainLMDB::compare_uint64); if (already_pruned) { MINFO("Copying already-pruned tables..."); diff --git a/src/carrot_core/CMakeLists.txt b/src/carrot_core/CMakeLists.txt new file mode 100644 index 00000000000..bf0afa26f55 --- /dev/null +++ b/src/carrot_core/CMakeLists.txt @@ -0,0 +1,64 @@ +# Copyright (c) 2024, The Monero Project +# +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, are +# permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this list of +# conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, this list +# of conditions and the following disclaimer in the documentation and/or other +# materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors may be +# used to endorse or promote products derived from this software without specific +# prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +# THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +set(carrot_core_sources + account_secrets.cpp + address_utils.cpp + carrot_enote_types.cpp + core_types.cpp + destination.cpp + device.cpp + device_ram_borrowed.cpp + enote_utils.cpp + hash_functions.cpp + output_set_finalization.cpp + payment_proposal.cpp + scan.cpp + scan_unsafe.cpp) + +monero_find_all_headers(carrot_core_headers, "${CMAKE_CURRENT_SOURCE_DIR}") + +monero_add_library(carrot_core + ${carrot_core_sources} + ${carrot_core_headers}) + +target_link_libraries(carrot_core + PUBLIC + cncrypto + epee + mx25519_static + ringct_basic + seraphis_crypto + PRIVATE + ${EXTRA_LIBRARIES}) + +target_include_directories(carrot_core + PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}" + PRIVATE + ${Boost_INCLUDE_DIRS}) diff --git a/src/carrot_core/account_secrets.cpp b/src/carrot_core/account_secrets.cpp new file mode 100644 index 00000000000..e9fd451a090 --- /dev/null +++ b/src/carrot_core/account_secrets.cpp @@ -0,0 +1,119 @@ +// Copyright (c) 2024, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//paired header +#include "account_secrets.h" + +//local headers +#include "config.h" +#include "crypto/generators.h" +#include "hash_functions.h" +#include "ringct/rctOps.h" +#include "transcript_fixed.h" + +//third party headers + +//standard headers + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "carrot" + +namespace carrot +{ +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_provespend_key(const crypto::secret_key &s_master, + crypto::secret_key &k_prove_spend_out) +{ + // k_ps = H_n[s_m]() + const auto transcript = make_fixed_transcript(); + derive_scalar(transcript.data(), transcript.size(), &s_master, to_bytes(k_prove_spend_out)); +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_partial_spend_pubkey(const crypto::secret_key &k_prove_spend, + crypto::public_key &partial_spend_pubkey_out) +{ + // K_ps = k_ps T + partial_spend_pubkey_out = rct::rct2pk(rct::scalarmultKey(rct::pk2rct(crypto::get_T()), + rct::sk2rct(k_prove_spend))); +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_viewbalance_secret(const crypto::secret_key &s_master, + crypto::secret_key &s_view_balance_out) +{ + // s_vb = H_32[s_m]() + const auto transcript = make_fixed_transcript(); + derive_bytes_32(transcript.data(), transcript.size(), &s_master, to_bytes(s_view_balance_out)); +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_generateimage_preimage(const crypto::secret_key &s_view_balance, + crypto::secret_key &s_generate_image_preimage_out) +{ + // s_gp = H_n[s_vb]() + const auto transcript = make_fixed_transcript(); + derive_bytes_32(transcript.data(), transcript.size(), &s_view_balance, to_bytes(s_generate_image_preimage_out)); +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_generateimage_key(const crypto::secret_key &s_generate_image_preimage, + const crypto::public_key &partial_spend_pubkey, + crypto::secret_key &k_generate_image_out) +{ + // k_gi = H_n[s_gp](K_ps) + const auto transcript = make_fixed_transcript(partial_spend_pubkey); + derive_scalar(transcript.data(), transcript.size(), &s_generate_image_preimage, to_bytes(k_generate_image_out)); +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_viewincoming_key(const crypto::secret_key &s_view_balance, + crypto::secret_key &k_view_out) +{ + // k_v = H_n[s_vb]() + const auto transcript = make_fixed_transcript(); + derive_scalar(transcript.data(), transcript.size(), &s_view_balance, to_bytes(k_view_out)); +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_generateaddress_secret(const crypto::secret_key &s_view_balance, + crypto::secret_key &s_generate_address_out) +{ + // s_ga = H_32[s_vb]() + const auto transcript = make_fixed_transcript(); + derive_bytes_32(transcript.data(), transcript.size(), &s_view_balance, to_bytes(s_generate_address_out)); +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_spend_pubkey(const crypto::secret_key &k_generate_image, + const crypto::secret_key &k_prove_spend, + crypto::public_key &spend_pubkey_out) +{ + // k_ps T + rct::key tmp; + rct::scalarmultKey(tmp, rct::pk2rct(crypto::get_T()), rct::sk2rct(k_prove_spend)); + + // K_s = k_gi G + k_ps T + rct::addKeys1(tmp, rct::sk2rct(k_generate_image), tmp); + spend_pubkey_out = rct::rct2pk(tmp); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace carrot diff --git a/src/carrot_core/account_secrets.h b/src/carrot_core/account_secrets.h new file mode 100644 index 00000000000..725e047fa33 --- /dev/null +++ b/src/carrot_core/account_secrets.h @@ -0,0 +1,121 @@ +// Copyright (c) 2024, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//// +// Core implementation details for making Carrot privkeys, secrets, and pubkeys. +// - Carrot is a specification for FCMP-RingCT compatible addressing +// +// references: +// * https://github.com/jeffro256/carrot/blob/master/carrot.md +/// + +#pragma once + +//local headers +#include "crypto/crypto.h" + +//third party headers + +//standard headers + +//forward declarations + + +namespace carrot +{ + +/** +* brief: make_carrot_provespend_key - prove-spend key, for signing input proofs to spend enotes +* k_ps = H_n[s_m]() +* param: s_master - s_m +* outparam: k_prove_spend_out - k_ps +*/ +void make_carrot_provespend_key(const crypto::secret_key &s_master, + crypto::secret_key &k_prove_spend_out); +/** + * brief: make_carrot_partial_spend_pubkey - partial spend pubkey, for deriving the generate-image key + * K_ps = k_ps T + * param: k_prove_spend - k_ps + * outparam: partial_spend_pubkey_out - K_ps + */ +void make_carrot_partial_spend_pubkey(const crypto::secret_key &k_prove_spend, + crypto::public_key &partial_spend_pubkey_out); +/** +* brief: make_carrot_viewbalance_secret - view-balance secret, for viewing all balance information +* s_vb = H_n[s_m]() +* param: s_master - s_m +* outparam: s_view_balance_out - s_vb +*/ +void make_carrot_viewbalance_secret(const crypto::secret_key &s_master, + crypto::secret_key &s_view_balance_out); +/** +* brief: make_carrot_generateimage_preimage - generate-image key preimage +* s_gp = H_n[s_vb]() +* param: s_view_balance - s_vb +* outparam: s_generate_image_preimage_out - s_gp +*/ +void make_carrot_generateimage_preimage(const crypto::secret_key &s_view_balance, + crypto::secret_key &s_generate_image_preimage_out); + /** +* brief: make_carrot_generateimage_key - generate-image key, for identifying enote spends +* k_gi = H_n[s_gp](K_ps) +* param: s_generate_image_preimage - s_gp +* param: partial_spend_pubkey - K_ps +* outparam: k_generate_image_out - k_gi +*/ +void make_carrot_generateimage_key(const crypto::secret_key &s_generate_image_preimage, + const crypto::public_key &partial_spend_pubkey, + crypto::secret_key &k_generate_image_out); +/** +* brief: make_carrot_viewincoming_key - view-incoming key, for identifying received external enotes +* k_v = H_n[s_vb]() +* param: s_view_balance - s_vb +* outparam: k_view_out - k_v +*/ +void make_carrot_viewincoming_key(const crypto::secret_key &s_view_balance, + crypto::secret_key &k_view_out); +/** +* brief: make_carrot_generateaddress_secret - generate-address secret, for generating addresses +* s_ga = H_32[s_vb]() +* param: s_view_balance - s_vb +* outparam: s_generate_address_out - s_ga +*/ +void make_carrot_generateaddress_secret(const crypto::secret_key &s_view_balance, + crypto::secret_key &s_generate_address_out); +/** + * brief: make_carrot_spend_pubkey - base public spendkey for rerandomizable RingCT + * K_s = k_gi G + k_ps T + * param: k_generate_image - k_gi + * param: k_prove_spend - k_ps + * outparam: spend_pubkey_out - K_s +*/ +void make_carrot_spend_pubkey(const crypto::secret_key &k_generate_image, + const crypto::secret_key &k_prove_spend, + crypto::public_key &spend_pubkey_out); + +} //namespace carrot diff --git a/src/carrot_core/address_utils.cpp b/src/carrot_core/address_utils.cpp new file mode 100644 index 00000000000..718ac4f0f33 --- /dev/null +++ b/src/carrot_core/address_utils.cpp @@ -0,0 +1,80 @@ +// Copyright (c) 2024, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//paired header +#include "address_utils.h" + +//local headers +#include "config.h" +#include "hash_functions.h" +#include "ringct/rctOps.h" +#include "transcript_fixed.h" + +//third party headers + +//standard headers + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "carrot" + +namespace carrot +{ +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_address_index_preimage_1(const crypto::secret_key &s_generate_address, + const std::uint32_t j_major, + const std::uint32_t j_minor, + crypto::secret_key &address_index_preimage_1_out) +{ + // s^j_ap1 = H_32[s_ga](j_major, j_minor) + const auto transcript = make_fixed_transcript(j_major, j_minor); + derive_bytes_32(transcript.data(), transcript.size(), &s_generate_address, to_bytes(address_index_preimage_1_out)); +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_address_index_preimage_2(const crypto::secret_key &address_index_preimage_1, + const std::uint32_t j_major, + const std::uint32_t j_minor, + const crypto::public_key &account_spend_pubkey, + const crypto::public_key &account_view_pubkey, + crypto::secret_key &address_index_preimage_2_out) +{ + // s^j_ap2 = H_32[s^j_ap1](j_major, j_minor, K_s, K_v) + const auto transcript = make_fixed_transcript(j_major, j_minor, + account_spend_pubkey, account_view_pubkey); + derive_bytes_32(transcript.data(), transcript.size(), &address_index_preimage_1, to_bytes(address_index_preimage_2_out)); +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_subaddress_scalar(const crypto::secret_key &address_index_preimage_2, + const crypto::public_key &account_spend_pubkey, + crypto::secret_key &subaddress_scalar_out) +{ + // k^j_subscal = H_n[s^j_ap2](K_s) + const auto transcript = make_fixed_transcript(account_spend_pubkey); + derive_scalar(transcript.data(), transcript.size(), &address_index_preimage_2, to_bytes(subaddress_scalar_out)); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace carrot diff --git a/src/carrot_core/address_utils.h b/src/carrot_core/address_utils.h new file mode 100644 index 00000000000..238e3625261 --- /dev/null +++ b/src/carrot_core/address_utils.h @@ -0,0 +1,93 @@ +// Copyright (c) 2024, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// Utilities for building carrot addresses. + +#pragma once + +//local headers +#include "crypto/crypto.h" + +//third party headers + +//standard headers + +//forward declarations + + +namespace carrot +{ + +/** +* brief: is_main_address_index - determine whether j=(j_major, j_minor) represents the main address +*/ +static constexpr bool is_main_address_index(const std::uint32_t j_major, const std::uint32_t j_minor) +{ + return !(j_major || j_minor); +} + +/** +* brief: make_carrot_address_index_preimage_1 - s^j_ap1 +* s^j_ap1 = H_32[s_ga](j_major, j_minor) +* param: s_generate_address - s_ga +* param: j_major - +* param: j_minor - +* outparam: address_index_preimage_1_out - s^j_ap1 +*/ +void make_carrot_address_index_preimage_1(const crypto::secret_key &s_generate_address, + const std::uint32_t j_major, + const std::uint32_t j_minor, + crypto::secret_key &address_index_preimage_1_out); +/** +* brief: make_carrot_address_index_preimage_2 - s^j_ap2 +* s^j_ap2 = H_32[s^j_ap1](j_major, j_minor, K_s, K_v) +* param: address_index_preimage_1 - s^j_ap1 +* param: j_major - +* param: j_minor - +* param: account_spend_pubkey - K_s +* param: account_view_pubkey - K_v +* outparam: address_index_preimage_2 - s^j_ap2 +*/ +void make_carrot_address_index_preimage_2(const crypto::secret_key &address_index_preimage_1, + const std::uint32_t j_major, + const std::uint32_t j_minor, + const crypto::public_key &account_spend_pubkey, + const crypto::public_key &account_view_pubkey, + crypto::secret_key &address_index_preimage_2_out); +/** +* brief: make_carrot_address_privkey - k^j_subscal +* k^j_subscal = H_n[s^j_ap2](K_s) +* param: address_index_preimage_2 - s^j_ap2 +* param: account_spend_pubkey - K_s = k_gi G + k_ps T +* outparam: subaddress_scalar_out - k^j_subscal +*/ +void make_carrot_subaddress_scalar(const crypto::secret_key &address_index_preimage_2, + const crypto::public_key &account_spend_pubkey, + crypto::secret_key &subaddress_scalar_out); + +} //namespace carrot diff --git a/src/carrot_core/carrot_enote_types.cpp b/src/carrot_core/carrot_enote_types.cpp new file mode 100644 index 00000000000..61bd8020d01 --- /dev/null +++ b/src/carrot_core/carrot_enote_types.cpp @@ -0,0 +1,75 @@ +// Copyright (c) 2024, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// Utilities for scanning carrot enotes. + +//paired header +#include "carrot_enote_types.h" + +//local headers + +//third party headers + +//standard headers + +/* +Expected: +- onetime address +- amount commitment +- encrypted amount +- encrypted janus anchor +- view tag +- ephemeral pubkey +- tx first key image +*/ + +namespace carrot +{ +//------------------------------------------------------------------------------------------------------------------- +bool operator==(const CarrotEnoteV1 &a, const CarrotEnoteV1 &b) +{ + return a.onetime_address == b.onetime_address && + a.amount_commitment == b.amount_commitment && + a.amount_enc == b.amount_enc && + a.anchor_enc == b.anchor_enc && + a.view_tag == b.view_tag && + a.tx_first_key_image == b.tx_first_key_image && + memcmp(a.enote_ephemeral_pubkey.data, b.enote_ephemeral_pubkey.data, sizeof(mx25519_pubkey)) == 0; +} +//------------------------------------------------------------------------------------------------------------------- +bool operator==(const CarrotCoinbaseEnoteV1 &a, const CarrotCoinbaseEnoteV1 &b) +{ + return a.onetime_address == b.onetime_address && + a.amount == b.amount && + a.anchor_enc == b.anchor_enc && + a.view_tag == b.view_tag && + a.block_index == b.block_index && + memcmp(a.enote_ephemeral_pubkey.data, b.enote_ephemeral_pubkey.data, sizeof(mx25519_pubkey)) == 0; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace carrot \ No newline at end of file diff --git a/src/carrot_core/carrot_enote_types.h b/src/carrot_core/carrot_enote_types.h new file mode 100644 index 00000000000..88aedee6237 --- /dev/null +++ b/src/carrot_core/carrot_enote_types.h @@ -0,0 +1,90 @@ +// Copyright (c) 2024, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// Carrot core types. + +#pragma once + +//local headers +#include "core_types.h" +#include "mx25519.h" +#include "ringct/rctTypes.h" + +//third party headers + +//standard headers + +//forward declarations + + +namespace carrot +{ + +struct CarrotEnoteV1 final +{ + /// K_o + crypto::public_key onetime_address; + /// C_a + rct::key amount_commitment; + /// a_enc + encrypted_amount_t amount_enc; + /// anchor_enc + encrypted_janus_anchor_t anchor_enc; + /// view_tag + view_tag_t view_tag; + /// D_e + mx25519_pubkey enote_ephemeral_pubkey; + /// L_0 + crypto::key_image tx_first_key_image; +}; + +/// equality operators +bool operator==(const CarrotEnoteV1 &a, const CarrotEnoteV1 &b); +static inline bool operator!=(const CarrotEnoteV1 &a, const CarrotEnoteV1 &b) { return !(a == b); } + +struct CarrotCoinbaseEnoteV1 final +{ + /// K_o + crypto::public_key onetime_address; + /// a + rct::xmr_amount amount; + /// anchor_enc + encrypted_janus_anchor_t anchor_enc; + /// view_tag + view_tag_t view_tag; + /// D_e + mx25519_pubkey enote_ephemeral_pubkey; + /// block_index + std::uint64_t block_index; +}; + +/// equality operators +bool operator==(const CarrotCoinbaseEnoteV1 &a, const CarrotCoinbaseEnoteV1 &b); +static inline bool operator!=(const CarrotCoinbaseEnoteV1 &a, const CarrotCoinbaseEnoteV1 &b) { return !(a == b); } + +} //namespace carrot diff --git a/src/carrot_core/config.h b/src/carrot_core/config.h new file mode 100644 index 00000000000..5974214b3bd --- /dev/null +++ b/src/carrot_core/config.h @@ -0,0 +1,78 @@ +// Copyright (c) 2024, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//! @file Constants used in Carrot + +#pragma once + +//local headers + +//third party headers + +//standard headers + +//forward declarations + +namespace carrot +{ +// Personalization +static constexpr const unsigned char CARROT_PERSONAL_STRING[] = "Monero"; + +// Carrot addressing protocol domain separators +static constexpr const unsigned char CARROT_DOMAIN_SEP_AMOUNT_BLINDING_FACTOR[] = "Carrot commitment mask"; +static constexpr const unsigned char CARROT_DOMAIN_SEP_ONETIME_EXTENSION_G_COINBASE[] = "Carrot coinbase extension G"; +static constexpr const unsigned char CARROT_DOMAIN_SEP_ONETIME_EXTENSION_T_COINBASE[] = "Carrot coinbase extension T"; +static constexpr const unsigned char CARROT_DOMAIN_SEP_ONETIME_EXTENSION_G[] = "Carrot key extension G"; +static constexpr const unsigned char CARROT_DOMAIN_SEP_ONETIME_EXTENSION_T[] = "Carrot key extension T"; +static constexpr const unsigned char CARROT_DOMAIN_SEP_ENCRYPTION_MASK_ANCHOR[] = "Carrot encryption mask anchor"; +static constexpr const unsigned char CARROT_DOMAIN_SEP_ENCRYPTION_MASK_AMOUNT[] = "Carrot encryption mask a"; +static constexpr const unsigned char CARROT_DOMAIN_SEP_ENCRYPTION_MASK_PAYMENT_ID[] = "Carrot encryption mask pid"; +static constexpr const unsigned char CARROT_DOMAIN_SEP_JANUS_ANCHOR_SPECIAL[] = "Carrot janus anchor special"; +static constexpr const unsigned char CARROT_DOMAIN_SEP_EPHEMERAL_PRIVKEY[] = "Carrot sending key normal"; +static constexpr const unsigned char CARROT_DOMAIN_SEP_VIEW_TAG[] = "Carrot view tag"; +static constexpr const unsigned char CARROT_DOMAIN_SEP_SENDER_RECEIVER_SECRET[] = "Carrot sender-receiver secret"; +static constexpr const unsigned char CARROT_DOMAIN_SEP_INPUT_CONTEXT_COINBASE = 'C'; +static constexpr const unsigned char CARROT_DOMAIN_SEP_INPUT_CONTEXT_RINGCT = 'R'; + +// Carrot account secret domain separators +static constexpr const unsigned char CARROT_DOMAIN_SEP_PROVE_SPEND_KEY[] = "Carrot prove-spend key"; +static constexpr const unsigned char CARROT_DOMAIN_SEP_VIEW_BALANCE_SECRET[] = "Carrot view-balance secret"; +static constexpr const unsigned char CARROT_DOMAIN_SEP_GENERATE_IMAGE_PREIMAGE[] = "Carrot generate-image preimage secret"; +static constexpr const unsigned char CARROT_DOMAIN_SEP_GENERATE_IMAGE_KEY[] = "Carrot generate-image key"; +static constexpr const unsigned char CARROT_DOMAIN_SEP_INCOMING_VIEW_KEY[] = "Carrot incoming view key"; +static constexpr const unsigned char CARROT_DOMAIN_SEP_GENERATE_ADDRESS_SECRET[] = "Carrot generate-address secret"; + +// Carrot address domain separators +static constexpr const unsigned char CARROT_DOMAIN_SEP_ADDRESS_INDEX_PREIMAGE_1[] = "Carrot address index preimage 1"; +static constexpr const unsigned char CARROT_DOMAIN_SEP_ADDRESS_INDEX_PREIMAGE_2[] = "Carrot address index preimage 2"; +static constexpr const unsigned char CARROT_DOMAIN_SEP_SUBADDRESS_SCALAR[] = "Carrot subaddress scalar"; + +// Carrot misc constants +static constexpr const unsigned int CARROT_MIN_TX_OUTPUTS = 2; +static constexpr const unsigned int CARROT_MIN_TX_INPUTS = 1; +} //namespace carrot diff --git a/src/carrot_core/core_types.cpp b/src/carrot_core/core_types.cpp new file mode 100644 index 00000000000..6eab9b1f451 --- /dev/null +++ b/src/carrot_core/core_types.cpp @@ -0,0 +1,162 @@ +// Copyright (c) 2024, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//paired header +#include "core_types.h" + +//local headers +#include "crypto/crypto.h" +extern "C" +{ +#include "crypto/crypto-ops.h" +} + +//third party headers +#include + +//standard headers + +namespace carrot +{ +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +template +static void xor_bytes(const unsigned char(&a)[Sz], const unsigned char(&b)[Sz], unsigned char(&c_out)[Sz]) +{ + for (std::size_t i{0}; i < Sz; ++i) + c_out[i] = a[i] ^ b[i]; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +template +static T xor_bytes(const T &a, const T &b) +{ + T temp; + xor_bytes(a.bytes, b.bytes, temp.bytes); + return temp; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +bool operator==(const janus_anchor_t &a, const janus_anchor_t &b) +{ + return memcmp(&a, &b, sizeof(janus_anchor_t)) == 0; +} +//------------------------------------------------------------------------------------------------------------------- +janus_anchor_t operator^(const janus_anchor_t &a, const janus_anchor_t &b) +{ + return xor_bytes(a, b); +} +//------------------------------------------------------------------------------------------------------------------- +bool operator==(const encrypted_amount_t &a, const encrypted_amount_t &b) +{ + return memcmp(&a, &b, sizeof(encrypted_amount_t)) == 0; +} +//------------------------------------------------------------------------------------------------------------------- +encrypted_amount_t operator^(const encrypted_amount_t &a, const encrypted_amount_t &b) +{ + return xor_bytes(a, b); +} +//------------------------------------------------------------------------------------------------------------------- +bool operator==(const payment_id_t &a, const payment_id_t &b) +{ + return memcmp(&a, &b, sizeof(payment_id_t)) == 0; +} +//------------------------------------------------------------------------------------------------------------------- +payment_id_t operator^(const payment_id_t &a, const payment_id_t &b) +{ + return xor_bytes(a, b); +} +//------------------------------------------------------------------------------------------------------------------- +bool operator==(const encrypted_payment_id_t &a, const encrypted_payment_id_t &b) +{ + return memcmp(&a, &b, sizeof(encrypted_payment_id_t)) == 0; +} +//------------------------------------------------------------------------------------------------------------------- +encrypted_payment_id_t operator^(const encrypted_payment_id_t &a, const encrypted_payment_id_t &b) +{ + return xor_bytes(a, b); +} +//------------------------------------------------------------------------------------------------------------------- +bool operator==(const input_context_t &a, const input_context_t &b) +{ + return memcmp(&a, &b, sizeof(input_context_t)) == 0; +} +//------------------------------------------------------------------------------------------------------------------- +bool operator==(const view_tag_t &a, const view_tag_t &b) +{ + return memcmp(&a, &b, sizeof(view_tag_t)) == 0; +} +//------------------------------------------------------------------------------------------------------------------- +janus_anchor_t gen_janus_anchor() +{ + return crypto::rand(); +} +//------------------------------------------------------------------------------------------------------------------- +encrypted_amount_t gen_encrypted_amount() +{ + return crypto::rand(); +} +//------------------------------------------------------------------------------------------------------------------- +payment_id_t gen_payment_id() +{ + while (true) + { + const payment_id_t res = crypto::rand(); + if (res != null_payment_id) + return res; + } +} +//------------------------------------------------------------------------------------------------------------------- +encrypted_payment_id_t gen_encrypted_payment_id() +{ + return crypto::rand(); +} +//------------------------------------------------------------------------------------------------------------------- +view_tag_t gen_view_tag() +{ + return crypto::rand(); +} +//------------------------------------------------------------------------------------------------------------------- +input_context_t gen_input_context() +{ + return crypto::rand(); +} +//------------------------------------------------------------------------------------------------------------------- +mx25519_pubkey gen_x25519_pubkey() +{ + unsigned char sc64[64]; + crypto::rand(sizeof(sc64), sc64); + sc_reduce(sc64); + ge_p3 P; + ge_scalarmult_base(&P, sc64); + mx25519_pubkey P_x25519; + ge_p3_to_x25519(P_x25519.data, &P); + return P_x25519; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace carrot diff --git a/src/carrot_core/core_types.h b/src/carrot_core/core_types.h new file mode 100644 index 00000000000..c65339ef968 --- /dev/null +++ b/src/carrot_core/core_types.h @@ -0,0 +1,146 @@ +// Copyright (c) 2024, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//! @file Supporting types for Carrot (anchor, view tag, etc.). + +#pragma once + +//local headers +#include "mx25519.h" + +//third party headers + +//standard headers +#include +#include + +//forward declarations + +namespace carrot +{ + +constexpr std::size_t JANUS_ANCHOR_BYTES{16}; + +/// Encodes either randomness for deriving the ephemeral key (normal), an HMAC of the view key plus +/// ephemeral pubkey (external selfsends), or a custom message (internal selfsends). +struct janus_anchor_t final +{ + unsigned char bytes[JANUS_ANCHOR_BYTES]; +}; + +/// carrot janus anchor XORd with a user-defined secret +using encrypted_janus_anchor_t = janus_anchor_t; + +/// carrot enote types +enum class CarrotEnoteType : unsigned char +{ + PAYMENT = 0, + CHANGE = 1 +}; + +/// carrot encrypted amount +constexpr std::size_t ENCRYPTED_AMOUNT_BYTES{8}; +struct encrypted_amount_t final +{ + unsigned char bytes[ENCRYPTED_AMOUNT_BYTES]; +}; + +/// legacy payment ID +constexpr std::size_t PAYMENT_ID_BYTES{8}; +struct payment_id_t final +{ + unsigned char bytes[PAYMENT_ID_BYTES]; +}; +static constexpr payment_id_t null_payment_id{{0}}; + +/// legacy encrypted payment ID +struct encrypted_payment_id_t final +{ + unsigned char bytes[PAYMENT_ID_BYTES]; +}; + +/// carrot view tags +constexpr std::size_t VIEW_TAG_BYTES{3}; +struct view_tag_t final +{ + unsigned char bytes[VIEW_TAG_BYTES]; +}; + +static_assert(sizeof(view_tag_t) < 32, "uint8_t cannot index all view tag bits"); + +/// carrot input context +constexpr std::size_t INPUT_CONTEXT_BYTES{1 + 32}; +struct input_context_t final +{ + unsigned char bytes[INPUT_CONTEXT_BYTES]; +}; + +/// overloaded operators: janus anchor +bool operator==(const janus_anchor_t &a, const janus_anchor_t &b); +static inline bool operator!=(const janus_anchor_t &a, const janus_anchor_t &b) { return !(a == b); } +janus_anchor_t operator^(const janus_anchor_t &a, const janus_anchor_t &b); + +/// overloaded operators: encrypted amount +bool operator==(const encrypted_amount_t &a, const encrypted_amount_t &b); +static inline bool operator!=(const encrypted_amount_t &a, const encrypted_amount_t &b) { return !(a == b); } +encrypted_amount_t operator^(const encrypted_amount_t &a, const encrypted_amount_t &b); + +/// overloaded operators: payment ID +bool operator==(const payment_id_t &a, const payment_id_t &b); +static inline bool operator!=(const payment_id_t &a, const payment_id_t &b) { return !(a == b); } +payment_id_t operator^(const payment_id_t &a, const payment_id_t &b); + +/// overloaded operators: encrypted payment ID +bool operator==(const encrypted_payment_id_t &a, const encrypted_payment_id_t &b); +static inline bool operator!=(const encrypted_payment_id_t &a, const encrypted_payment_id_t &b) { return !(a == b); } +encrypted_payment_id_t operator^(const encrypted_payment_id_t &a, const encrypted_payment_id_t &b); + +/// overloaded operators: input context +bool operator==(const input_context_t &a, const input_context_t &b); +static inline bool operator!=(const input_context_t &a, const input_context_t &b) { return !(a == b); } + +/// overloaded operators: view tag +bool operator==(const view_tag_t &a, const view_tag_t &b); +static inline bool operator!=(const view_tag_t &a, const view_tag_t &b) { return !(a == b); } + +/// generate a random janus anchor +janus_anchor_t gen_janus_anchor(); +/// generate a random encrypted amount +encrypted_amount_t gen_encrypted_amount(); +/// generate a random (non-null) payment ID +payment_id_t gen_payment_id(); +/// generate a random encrypted payment ID +encrypted_payment_id_t gen_encrypted_payment_id(); +/// generate a random view tag +view_tag_t gen_view_tag(); +/// generate a random input context +input_context_t gen_input_context(); +/// generate a random X25519 pubkey (unclamped) +mx25519_pubkey gen_x25519_pubkey(); + +} //namespace carrot diff --git a/src/carrot_core/destination.cpp b/src/carrot_core/destination.cpp new file mode 100644 index 00000000000..52daa33c63f --- /dev/null +++ b/src/carrot_core/destination.cpp @@ -0,0 +1,159 @@ +// Copyright (c) 2024, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//paired header +#include "destination.h" + +//local headers +#include "address_utils.h" +#include "exceptions.h" +#include "misc_log_ex.h" +#include "ringct/rctOps.h" + +//third party headers + +//standard headers + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "carrot.dest" + +namespace carrot +{ +//------------------------------------------------------------------------------------------------------------------- +bool operator==(const CarrotDestinationV1 &a, const CarrotDestinationV1 &b) +{ + return a.address_spend_pubkey == b.address_spend_pubkey && + a.address_view_pubkey == b.address_view_pubkey && + a.is_subaddress == b.is_subaddress && + a.payment_id == b.payment_id; +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_main_address_v1(const crypto::public_key &account_spend_pubkey, + const crypto::public_key &primary_address_view_pubkey, + CarrotDestinationV1 &destination_out) +{ + destination_out = CarrotDestinationV1{ + .address_spend_pubkey = account_spend_pubkey, + .address_view_pubkey = primary_address_view_pubkey, + .is_subaddress = false, + .payment_id = null_payment_id + }; +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_subaddress_v1(const crypto::public_key &account_spend_pubkey, + const crypto::public_key &account_view_pubkey, + const generate_address_secret_device &s_generate_address_dev, + const std::uint32_t j_major, + const std::uint32_t j_minor, + CarrotDestinationV1 &destination_out) +{ + CARROT_CHECK_AND_THROW(j_major || j_minor, + bad_address_type, "j cannot be 0 for a subaddress, only for main addresses"); + + // s^j_ap1 = H_32[s_ga](j_major, j_minor) + crypto::secret_key address_index_preimage_1; + s_generate_address_dev.make_address_index_preimage_1(j_major, j_minor, address_index_preimage_1); + + // s^j_ap2 = H_32[s^j_ap1](j_major, j_minor, K_s, K_v) + crypto::secret_key address_index_preimage_2; + make_carrot_address_index_preimage_2(address_index_preimage_1, + j_major, + j_minor, + account_spend_pubkey, + account_view_pubkey, + address_index_preimage_2); + + // k^j_subscal = H_n[s^j_ap2](K_s) + crypto::secret_key subaddress_scalar; + make_carrot_subaddress_scalar(address_index_preimage_2, account_spend_pubkey, subaddress_scalar); + + // K^j_s = k^j_subscal * K_s + const rct::key address_spend_pubkey = + rct::scalarmultKey(rct::pk2rct(account_spend_pubkey), rct::sk2rct(subaddress_scalar)); + + // K^j_v = k^j_subscal * K_v + const rct::key address_view_pubkey = + rct::scalarmultKey(rct::pk2rct(account_view_pubkey), rct::sk2rct(subaddress_scalar)); + + destination_out = CarrotDestinationV1{ + .address_spend_pubkey = rct::rct2pk(address_spend_pubkey), + .address_view_pubkey = rct::rct2pk(address_view_pubkey), + .is_subaddress = true, + .payment_id = null_payment_id + }; +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_integrated_address_v1(const crypto::public_key &account_spend_pubkey, + const crypto::public_key &primary_address_view_pubkey, + const payment_id_t payment_id, + CarrotDestinationV1 &destination_out) +{ + destination_out = CarrotDestinationV1{ + .address_spend_pubkey = account_spend_pubkey, + .address_view_pubkey = primary_address_view_pubkey, + .is_subaddress = false, + .payment_id = payment_id + }; +} +//------------------------------------------------------------------------------------------------------------------- +CarrotDestinationV1 gen_carrot_main_address_v1() +{ + return CarrotDestinationV1{ + .address_spend_pubkey = rct::rct2pk(rct::pkGen()), + .address_view_pubkey = rct::rct2pk(rct::pkGen()), + .is_subaddress = false, + .payment_id = null_payment_id + }; +} +//------------------------------------------------------------------------------------------------------------------- +CarrotDestinationV1 gen_carrot_subaddress_v1() +{ + return CarrotDestinationV1{ + .address_spend_pubkey = rct::rct2pk(rct::pkGen()), + .address_view_pubkey = rct::rct2pk(rct::pkGen()), + .is_subaddress = true, + .payment_id = null_payment_id + }; +} +//------------------------------------------------------------------------------------------------------------------- +CarrotDestinationV1 gen_carrot_integrated_address_v1() +{ + // force generate non-zero payment id + payment_id_t payment_id{gen_payment_id()}; + while (payment_id == null_payment_id) + payment_id = gen_payment_id(); + + return CarrotDestinationV1{ + .address_spend_pubkey = rct::rct2pk(rct::pkGen()), + .address_view_pubkey = rct::rct2pk(rct::pkGen()), + .is_subaddress = false, + .payment_id = payment_id + }; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace carrot diff --git a/src/carrot_core/destination.h b/src/carrot_core/destination.h new file mode 100644 index 00000000000..e2d955a4890 --- /dev/null +++ b/src/carrot_core/destination.h @@ -0,0 +1,117 @@ +// Copyright (c) 2024, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// A 'payment proposal' is a proposal to make an enote sending funds to a Carrot address. +// Carrot: Cryptonote Address For Rerandomizable-RingCT-Output Transactions + +#pragma once + +//local headers +#include "core_types.h" +#include "crypto/crypto.h" +#include "device.h" + +//third party headers + +//standard headers + +//forward declarations + + +namespace carrot +{ + +//// +// CarrotDestinationV1 +// - for creating an output proposal to send an amount to someone +/// +struct CarrotDestinationV1 final +{ + /// K^j_s + crypto::public_key address_spend_pubkey; + /// K^j_v + crypto::public_key address_view_pubkey; + /// is a subaddress? + bool is_subaddress; + /// legacy payment id pid: non-null only for integrated addresses (main address + payment id) + payment_id_t payment_id; +}; + +/// equality operators +bool operator==(const CarrotDestinationV1 &a, const CarrotDestinationV1 &b); +static inline bool operator!=(const CarrotDestinationV1 &a, const CarrotDestinationV1 &b) { return !(a == b); } + +/** +* brief: make_carrot_main_address_v1 - make a destination address +* param: account_spend_pubkey - K_s +* param: primary_address_view_pubkey - K^0_v = k_v G +* outparam: destination_out - the full main address +*/ +void make_carrot_main_address_v1(const crypto::public_key &account_spend_pubkey, + const crypto::public_key &primary_address_view_pubkey, + CarrotDestinationV1 &destination_out); +/** +* brief: make_carrot_subaddress_v1 - make a destination address +* param: account_spend_pubkey - K_s +* param: account_view_pubkey - K_v = k_v K_s +* param: s_generate_address_dev - device for s_ga +* param: j_major - +* param: j_minor - +* outparam: destination_out - the full subaddress +*/ +void make_carrot_subaddress_v1(const crypto::public_key &account_spend_pubkey, + const crypto::public_key &account_view_pubkey, + const generate_address_secret_device &s_generate_address_dev, + const std::uint32_t j_major, + const std::uint32_t j_minor, + CarrotDestinationV1 &destination_out); +/** +* brief: make_carrot_integrated_address_v1 - make a destination address +* param: account_spend_pubkey - K_s +* param: primary_address_view_pubkey - K^0_v = k_v G +* param: payment_id - pid +* outparam: destination_out - the full main address +*/ +void make_carrot_integrated_address_v1(const crypto::public_key &account_spend_pubkey, + const crypto::public_key &primary_address_view_pubkey, + const payment_id_t payment_id, + CarrotDestinationV1 &destination_out); +/** +* brief: gen_carrot_main_address_v1 - generate a random main address +*/ +CarrotDestinationV1 gen_carrot_main_address_v1(); +/** +* brief: gen_carrot_subaddress_v1 - generate a random subaddress +*/ +CarrotDestinationV1 gen_carrot_subaddress_v1(); +/** +* brief: gen_carrot_integrated_address_v1 - generate a random integrated address +*/ +CarrotDestinationV1 gen_carrot_integrated_address_v1(); + +} //namespace carrot diff --git a/src/carrot_core/device.cpp b/src/carrot_core/device.cpp new file mode 100644 index 00000000000..f4e218a2c95 --- /dev/null +++ b/src/carrot_core/device.cpp @@ -0,0 +1,57 @@ +// Copyright (c) 2025, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//paired header +#include "device.h" + +//local headers +#include "ringct/rctOps.h" + +//third party headers + +//standard headers + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "carrot.device" + +namespace carrot +{ +//------------------------------------------------------------------------------------------------------------------- +bool view_incoming_key_device::view_key_scalar_mult8_ed25519(const crypto::public_key &P, + crypto::public_key &kv8P) const +{ + // Is slow b/c it does 2 compressions, override if you want speed + + if (!this->view_key_scalar_mult_ed25519(P, kv8P)) + return false; + + kv8P = rct::rct2pk(rct::scalarmult8(rct::pk2rct(kv8P))); + return true; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace carrot diff --git a/src/carrot_core/device.h b/src/carrot_core/device.h new file mode 100644 index 00000000000..8a0c369121c --- /dev/null +++ b/src/carrot_core/device.h @@ -0,0 +1,211 @@ +// Copyright (c) 2024, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//! @file Abstract interfaces for performing scanning without revealing account keys + +#pragma once + +//local headers +#include "core_types.h" +#include "crypto/crypto.h" +#include "mx25519.h" + +//third party headers + +//standard headers +#include +#include +#include + +//forward declarations + + +/** + * These device interfaces were written primarily to be as lean as possible, for ease of the + * implementor, so long as account keys don't leak. As such, the interfaces do not shield + * sender-receiver secrets, and thus temporary access to this device interface can expose + * transaction content permanently in a provable manner. The device interface currently used in + * Monero (hw::device) also exposes transaction content, which can be saved permanently, but it + * wouldn't necessarily be provable. Thus, in the case of a breach, the original user has some + * plausible deniability with (hw::device), which cannot be said of the interfaces in this file. + * It's not impossible to make carrot scanning happen completely on-device, but it is significantly + * more involved. + */ + +namespace carrot +{ +/** + * brief: base exception type for reporting carrot device errors. + * note: devices should only throw this exception or derived classes + */ +struct device_error: public std::runtime_error +{ + /** + * param: dev_make - e.g. "Trezor", "Ledger" + * param: dev_model - e.g. "Model T", "Nano X" + * param: func_called - verbatim device interface method name, e.g. "view_key_scalar_mult_x25519" + * param: msg - arbitrary error message + * param: code - arbitrary error code + */ + device_error(std::string &&dev_make, + std::string &&dev_model, + std::string &&func_called, + std::string &&msg, + const int code) + : std::runtime_error(make_formatted_message(dev_make, dev_model, func_called, msg, code)), + dev_make(dev_make), dev_model(dev_model), func_called(func_called), msg(msg), code(code) + {} + + static std::string make_formatted_message(const std::string &dev_make, + const std::string &dev_model, + const std::string &func_called, + const std::string &msg, + const int code) + { + char buf[384]; + snprintf(buf, sizeof(buf), + "%s %s device error (%d), at %s(): %s", + dev_make.c_str(), dev_model.c_str(), code, func_called.c_str(), msg.c_str()); + return {buf}; + } + + const std::string dev_make; + const std::string dev_model; + const std::string func_called; + const std::string msg; + const int code; +}; + +struct view_incoming_key_device +{ + /** + * brief: view_key_scalar_mult_ed25519 - do an Ed25519 scalar mult against the incoming view key + * kvP = k_v * P + * param: P - Ed25519 base point + * outparam: kvP + * return: true on success, false on failure (e.g. unable to decompress point) + */ + virtual bool view_key_scalar_mult_ed25519(const crypto::public_key &P, crypto::public_key &kvP) const = 0; + + /** + * brief: view_key_scalar_mult8_ed25519 - do an Ed25519 scalar mult against the incoming view key, w/ cofactor clear + * kv8P = 8 * k_v * P + * param: P - Ed25519 base point + * outparam: kv8P + * return: true on success, false on failure (e.g. unable to decompress point) + * + * Used in pre-Carrot sender-receiver ECDH exchange. + */ + virtual bool view_key_scalar_mult8_ed25519(const crypto::public_key &P, crypto::public_key &kv8P) const; + + /** + * brief: view_key_scalar_mult_x25519 - do an X25519 scalar mult against the incoming view key + * kvD = k_v * D + * param: D - X25519 base point + * outparam: kvD + * return: true on success, false on failure (e.g. unable to decompress point) + */ + virtual bool view_key_scalar_mult_x25519(const mx25519_pubkey &D, mx25519_pubkey &kvD) const = 0; + + /** + * brief: make_janus_anchor_special - make a janus anchor for "special" enotes + * anchor_sp = H_16(D_e, input_context, Ko, k_v) + * param: enote_ephemeral_pubkey - D_e + * param: input_context - input_context + * param: account_spend_pubkey - K_s + * outparam: anchor_special_out - anchor_sp + */ + virtual void make_janus_anchor_special(const mx25519_pubkey &enote_ephemeral_pubkey, + const input_context_t &input_context, + const crypto::public_key &onetime_address, + janus_anchor_t &anchor_special_out) const = 0; + + virtual ~view_incoming_key_device() = default; +}; + +struct view_balance_secret_device +{ + /** + * brief: make_internal_view_tag - make an internal view tag, given non-secret data + * vt = H_3(s_vb || input_context || Ko) + * param: input_context - input_context + * param: onetime_address - Ko + * outparam: view_tag_out - vt + */ + virtual void make_internal_view_tag(const input_context_t &input_context, + const crypto::public_key &onetime_address, + view_tag_t &view_tag_out) const = 0; + + /** + * brief: make_internal_sender_receiver_secret - make internal sender-receiver secret, given non-secret data + * s^ctx_sr = H_32(s_sr, D_e, input_context) + * param: enote_ephemeral_pubkey - D_e + * param: input_context - input_context + * outparam: s_sender_receiver_ctx_out - s^ctx_sr + */ + virtual void make_internal_sender_receiver_secret(const mx25519_pubkey &enote_ephemeral_pubkey, + const input_context_t &input_context, + crypto::hash &s_sender_receiver_ctx_out) const = 0; + + virtual ~view_balance_secret_device() = default; +}; + +struct generate_address_secret_device +{ + /** + * brief: make carrot address index preimage 1 s^j_ap1 + * s^j_ap1 = H_32[s_ga](j_major, j_minor) + * param: major_index - j_major + * param: minor_index - j_minor + * outparam: address_index_preimage_1 - s^j_ap1 + */ + virtual void make_address_index_preimage_1(const std::uint32_t major_index, + const std::uint32_t minor_index, + crypto::secret_key &address_index_preimage_1) const = 0; + + virtual ~generate_address_secret_device() = default; +}; + +struct generate_image_key_device +{ + /** + * brief: generate_image_scalar_mult_hash_to_point - + * [carrot] L_partial = k_gi Hp(K_o) + * [legacy] L_partial = k_s Hp(K_o) + * param: onetime_address - K_o + * param: use_biased - true if should use biased hash-to-point function + * return: L_partial + */ + virtual crypto::ec_point generate_image_scalar_mult_hash_to_point( + const crypto::public_key &onetime_address, + const bool use_biased) const = 0; + + virtual ~generate_image_key_device() = default; +}; + +} //namespace carrot diff --git a/src/carrot_core/device_ram_borrowed.cpp b/src/carrot_core/device_ram_borrowed.cpp new file mode 100644 index 00000000000..87adf4d7b5e --- /dev/null +++ b/src/carrot_core/device_ram_borrowed.cpp @@ -0,0 +1,109 @@ +// Copyright (c) 2024, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//pair header +#include "device_ram_borrowed.h" + +//local headers +#include "address_utils.h" +#include "enote_utils.h" +#include "ringct/rctOps.h" + +//third party headers + +//standard headers + + +namespace carrot +{ +//------------------------------------------------------------------------------------------------------------------- +bool view_incoming_key_ram_borrowed_device::view_key_scalar_mult_ed25519(const crypto::public_key &P, + crypto::public_key &kvP) const +{ + kvP = rct::rct2pk(rct::scalarmultKey(rct::pk2rct(P), rct::sk2rct(m_k_view_incoming))); + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool view_incoming_key_ram_borrowed_device::view_key_scalar_mult_x25519(const mx25519_pubkey &D, + mx25519_pubkey &kvD) const +{ + return try_make_carrot_shared_key_receiver(m_k_view_incoming, D, kvD); +} +//------------------------------------------------------------------------------------------------------------------- +void view_incoming_key_ram_borrowed_device::make_janus_anchor_special( + const mx25519_pubkey &enote_ephemeral_pubkey, + const input_context_t &input_context, + const crypto::public_key &onetime_address, + janus_anchor_t &anchor_special_out) const +{ + return make_carrot_janus_anchor_special(enote_ephemeral_pubkey, + input_context, + onetime_address, + m_k_view_incoming, + anchor_special_out); +} +//------------------------------------------------------------------------------------------------------------------- +void view_balance_secret_ram_borrowed_device::make_internal_view_tag(const input_context_t &input_context, + const crypto::public_key &onetime_address, + view_tag_t &view_tag_out) const +{ + make_carrot_view_tag(to_bytes(m_s_view_balance), input_context, onetime_address, view_tag_out); +} +//------------------------------------------------------------------------------------------------------------------- +void view_balance_secret_ram_borrowed_device::make_internal_sender_receiver_secret( + const mx25519_pubkey &enote_ephemeral_pubkey, + const input_context_t &input_context, + crypto::hash &s_sender_receiver_ctx_out) const +{ + make_carrot_contextualized_sender_receiver_secret(to_bytes(m_s_view_balance), + enote_ephemeral_pubkey, + input_context, + s_sender_receiver_ctx_out); +} +//------------------------------------------------------------------------------------------------------------------- +void generate_address_secret_ram_borrowed_device::make_address_index_preimage_1( + const std::uint32_t major_index, + const std::uint32_t minor_index, + crypto::secret_key &address_index_preimage_1_out) const +{ + make_carrot_address_index_preimage_1(m_s_generate_address, major_index, minor_index, address_index_preimage_1_out); +} +//------------------------------------------------------------------------------------------------------------------- +crypto::ec_point generate_image_key_ram_borrowed_device::generate_image_scalar_mult_hash_to_point( + const crypto::public_key &onetime_address, + const bool use_biased) const +{ + // I = Hp(K_o) + crypto::ec_point key_image_generator; + crypto::derive_key_image_generator(onetime_address, use_biased, key_image_generator); + + // L_partial = k_gi I + return rct::rct2pt(rct::scalarmultKey(rct::pt2rct(key_image_generator), rct::sk2rct(m_k_generate_image))); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace carrot diff --git a/src/carrot_core/device_ram_borrowed.h b/src/carrot_core/device_ram_borrowed.h new file mode 100644 index 00000000000..ff5b53f0a9f --- /dev/null +++ b/src/carrot_core/device_ram_borrowed.h @@ -0,0 +1,113 @@ +// Copyright (c) 2024, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//! @file Carrot device implementations for in-memory keys & secrets + +#pragma once + +//local headers +#include "device.h" + +//third party headers + +//standard headers + +//forward declarations + + +namespace carrot +{ + +class view_incoming_key_ram_borrowed_device : virtual public view_incoming_key_device +{ +public: + view_incoming_key_ram_borrowed_device(const crypto::secret_key &k_view_incoming): + m_k_view_incoming(k_view_incoming) {} + + bool view_key_scalar_mult_ed25519(const crypto::public_key &P, + crypto::public_key &kvP) const override; + + bool view_key_scalar_mult_x25519(const mx25519_pubkey &D, + mx25519_pubkey &kvD) const override; + + void make_janus_anchor_special(const mx25519_pubkey &enote_ephemeral_pubkey, + const input_context_t &input_context, + const crypto::public_key &onetime_address, + janus_anchor_t &anchor_special_out) const override; + +protected: + const crypto::secret_key &m_k_view_incoming; +}; + +class view_balance_secret_ram_borrowed_device final : public view_balance_secret_device +{ +public: + view_balance_secret_ram_borrowed_device(const crypto::secret_key &s_view_balance): + m_s_view_balance(s_view_balance) {} + + void make_internal_view_tag(const input_context_t &input_context, + const crypto::public_key &onetime_address, + view_tag_t &view_tag_out) const override; + + void make_internal_sender_receiver_secret(const mx25519_pubkey &enote_ephemeral_pubkey, + const input_context_t &input_context, + crypto::hash &s_sender_receiver_ctx_out) const override; + +protected: + const crypto::secret_key &m_s_view_balance; +}; + +class generate_address_secret_ram_borrowed_device final : public generate_address_secret_device +{ +public: + generate_address_secret_ram_borrowed_device(const crypto::secret_key &s_generate_address): + m_s_generate_address(s_generate_address) {} + + void make_address_index_preimage_1(const std::uint32_t major_index, + const std::uint32_t minor_index, + crypto::secret_key &address_index_preimage_1_out) const override; + +protected: + const crypto::secret_key &m_s_generate_address; +}; + +class generate_image_key_ram_borrowed_device final : public generate_image_key_device +{ +public: + generate_image_key_ram_borrowed_device(const crypto::secret_key &k_generate_image): + m_k_generate_image(k_generate_image) {} + + crypto::ec_point generate_image_scalar_mult_hash_to_point( + const crypto::public_key &onetime_address, + const bool use_biased) const override; + +protected: + const crypto::secret_key &m_k_generate_image; +}; + +} //namespace carrot diff --git a/src/carrot_core/enote_utils.cpp b/src/carrot_core/enote_utils.cpp new file mode 100644 index 00000000000..b0fc3c62eb5 --- /dev/null +++ b/src/carrot_core/enote_utils.cpp @@ -0,0 +1,606 @@ +// Copyright (c) 2024, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//paired header +#include "enote_utils.h" + +//local headers +#include "config.h" +extern "C" +{ +#include "crypto/crypto-ops.h" +} +#include "crypto/generators.h" +#include "crypto/wallet/crypto.h" +#include "hash_functions.h" +#include "int-util.h" +#include "misc_language.h" +#include "mx25519.h" +#include "ringct/rctOps.h" +#include "transcript_fixed.h" + +//third party headers + +//standard headers +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "carrot" + +namespace carrot +{ +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static const mx25519_impl* get_mx25519_impl() +{ + static std::once_flag of; + static const mx25519_impl *impl; + std::call_once(of, [&](){ impl = mx25519_select_impl(MX25519_TYPE_AUTO); }); + if (impl == nullptr) + throw std::runtime_error("failed to obtain an mx25519 implementation"); + return impl; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static encrypted_amount_t enc_amount(const rct::xmr_amount amount, const encrypted_amount_t &mask) +{ + static_assert(sizeof(rct::xmr_amount) == sizeof(encrypted_amount_t), ""); + + // little_endian(amount) XOR H_8(q, Ko) + encrypted_amount_t amount_LE; + memcpy_swap64le(amount_LE.bytes, &amount, 1); + return amount_LE ^ mask; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static rct::xmr_amount dec_amount(const encrypted_amount_t &encrypted_amount, const encrypted_amount_t &mask) +{ + static_assert(sizeof(rct::xmr_amount) == sizeof(encrypted_amount_t), ""); + + // system_endian(encrypted_amount XOR H_8(q, Ko)) + const encrypted_amount_t decrypted_amount{encrypted_amount ^ mask}; + rct::xmr_amount amount; + memcpy_swap64le(&amount, &decrypted_amount, 1); + return amount; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +template , encrypted_payment_id_t, payment_id_t>> +static OtherPid convert_payment_id(const Pid &v) +{ + static_assert(sizeof(Pid) == PAYMENT_ID_BYTES); + OtherPid conv; + memcpy(&conv, &v, PAYMENT_ID_BYTES); + return conv; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +/** +* brief: create a coinbase FCMP++ onetime address extension pubkey +* K^o_ext = k^o_g G + k^o_t T +* param: s_sender_receiver_ctx - s^ctx_sr +* param: amount - a +* param: main_address_spend_pubkey - K^0_s +* outparam: sender_extension_pubkey_out - K^o_ext +*/ +static void make_carrot_sender_extension_pubkey_coinbase(const crypto::hash &s_sender_receiver_ctx, + const rct::xmr_amount amount, + const crypto::public_key &main_address_spend_pubkey, + crypto::public_key &sender_extension_pubkey_out) +{ + // k^o_g = H_n[s^ctx_sr]("..g..", a, K^0_s) + crypto::secret_key sender_extension_g; + make_carrot_sender_extension_g_coinbase(s_sender_receiver_ctx, amount, main_address_spend_pubkey, sender_extension_g); + + // k^o_t = H_n[s^ctx_sr]("..t..", a, K^0_s) + crypto::secret_key sender_extension_t; + make_carrot_sender_extension_t_coinbase(s_sender_receiver_ctx, amount, main_address_spend_pubkey, sender_extension_t); + + // K^o_ext = k^o_g G + k^o_t T + rct::key sender_extension_pubkey_tmp; + rct::addKeys2(sender_extension_pubkey_tmp, + rct::sk2rct(sender_extension_g), + rct::sk2rct(sender_extension_t), + rct::pk2rct(crypto::get_T())); + + sender_extension_pubkey_out = rct::rct2pk(sender_extension_pubkey_tmp); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +/** +* brief: create a non-coinbase FCMP++ onetime address extension pubkey +* K^o_ext = k^o_g G + k^o_t T +* param: s_sender_receiver_ctx - s^ctx_sr +* param: amount_commitment - C_a +* outparam: sender_extension_pubkey_out - K^o_ext +*/ +static void make_carrot_sender_extension_pubkey(const crypto::hash &s_sender_receiver_ctx, + const rct::key &amount_commitment, + crypto::public_key &sender_extension_pubkey_out) +{ + // k^o_g = H_n("..g..", s^ctx_sr, C_a) + crypto::secret_key sender_extension_g; + make_carrot_sender_extension_g(s_sender_receiver_ctx, amount_commitment, sender_extension_g); + + // k^o_t = H_n("..t..", s^ctx_sr, C_a) + crypto::secret_key sender_extension_t; + make_carrot_sender_extension_t(s_sender_receiver_ctx, amount_commitment, sender_extension_t); + + // K^o_ext = k^o_g G + k^o_t T + rct::key sender_extension_pubkey_tmp; + rct::addKeys2(sender_extension_pubkey_tmp, + rct::sk2rct(sender_extension_g), + rct::sk2rct(sender_extension_t), + rct::pk2rct(crypto::get_T())); + + sender_extension_pubkey_out = rct::rct2pk(sender_extension_pubkey_tmp); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_enote_ephemeral_privkey(const janus_anchor_t &anchor_norm, + const input_context_t &input_context, + const crypto::public_key &address_spend_pubkey, + const payment_id_t payment_id, + crypto::secret_key &enote_ephemeral_privkey_out) +{ + // d_e = (H_64(anchor_norm, input_context, K^j_s, pid)) mod l + const auto transcript = make_fixed_transcript( + anchor_norm, input_context, address_spend_pubkey, payment_id); + derive_scalar(transcript.data(), transcript.size(), nullptr, &enote_ephemeral_privkey_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_enote_ephemeral_pubkey_cryptonote(const crypto::secret_key &enote_ephemeral_privkey, + mx25519_pubkey &enote_ephemeral_pubkey_out) +{ + // D_e = d_e B + mx25519_scmul_base(get_mx25519_impl(), + &enote_ephemeral_pubkey_out, + reinterpret_cast(&enote_ephemeral_privkey)); +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_enote_ephemeral_pubkey_subaddress(const crypto::secret_key &enote_ephemeral_privkey, + const crypto::public_key &address_spend_pubkey, + mx25519_pubkey &enote_ephemeral_pubkey_out) +{ + // deserialize K^j_s + ge_p3 address_spend_pubkey_p3; + ge_frombytes_vartime(&address_spend_pubkey_p3, to_bytes(address_spend_pubkey)); + + // K_e = d_e K^j_s + ge_p3 D_e_in_ed25519; + ge_scalarmult_p3(&D_e_in_ed25519, to_bytes(enote_ephemeral_privkey), &address_spend_pubkey_p3); + + // D_e = ConvertPointE(K_e) + ge_p3_to_x25519(enote_ephemeral_pubkey_out.data, &D_e_in_ed25519); +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_enote_ephemeral_pubkey(const crypto::secret_key &enote_ephemeral_privkey, + const crypto::public_key &address_spend_pubkey, + const bool is_subaddress, + mx25519_pubkey &enote_ephemeral_pubkey_out) +{ + if (is_subaddress) + { + // D_e = d_e ConvertPointE(K^j_s) + make_carrot_enote_ephemeral_pubkey_subaddress(enote_ephemeral_privkey, + address_spend_pubkey, + enote_ephemeral_pubkey_out); + } + else // !is_subaddress + { + // D_e = d_e B + make_carrot_enote_ephemeral_pubkey_cryptonote(enote_ephemeral_privkey, enote_ephemeral_pubkey_out); + } +} +//------------------------------------------------------------------------------------------------------------------- +bool try_make_carrot_shared_key_receiver(const crypto::secret_key &k_view, + const mx25519_pubkey &enote_ephemeral_pubkey, + mx25519_pubkey &s_sender_receiver_out) +{ + // s_sr = k_v D_e + mx25519_scmul_key(get_mx25519_impl(), + &s_sender_receiver_out, + reinterpret_cast(&k_view), + &enote_ephemeral_pubkey); + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool try_make_carrot_shared_key_sender(const crypto::secret_key &enote_ephemeral_privkey, + const crypto::public_key &address_view_pubkey, + mx25519_pubkey &s_sender_receiver_out) +{ + // if K^j_v not in prime order subgroup, then FAIL + ge_p3 address_view_pubkey_p3; + if (!rct::toPointCheckOrder(&address_view_pubkey_p3, to_bytes(address_view_pubkey))) + return false; + + // D^j_v = ConvertPointE(K^j_v) + mx25519_pubkey address_view_pubkey_x25519; + ge_p3_to_x25519(address_view_pubkey_x25519.data, &address_view_pubkey_p3); + + // s_sr = d_e D^j_v + mx25519_scmul_key(get_mx25519_impl(), + &s_sender_receiver_out, + reinterpret_cast(&enote_ephemeral_privkey), + &address_view_pubkey_x25519); + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_view_tag(const unsigned char s_sender_receiver[32], + const input_context_t &input_context, + const crypto::public_key &onetime_address, + view_tag_t &view_tag_out) +{ + // vt = H_3[s_sr](input_context || Ko) + const auto transcript = make_fixed_transcript(input_context, onetime_address); + derive_bytes_3(transcript.data(), transcript.size(), s_sender_receiver, &view_tag_out); +} +//------------------------------------------------------------------------------------------------------------------- +input_context_t make_carrot_input_context_coinbase(const std::uint64_t block_index) +{ + // input_context = "C" || IntToBytes256(block_index) + input_context_t input_context{}; + input_context.bytes[0] = CARROT_DOMAIN_SEP_INPUT_CONTEXT_COINBASE; + memcpy_swap64le(input_context.bytes + 1, &block_index, 1); + return input_context; +} +//------------------------------------------------------------------------------------------------------------------- +input_context_t make_carrot_input_context(const crypto::key_image &first_rct_key_image) +{ + // input_context = "R" || KI_1 + input_context_t input_context{}; + input_context.bytes[0] = CARROT_DOMAIN_SEP_INPUT_CONTEXT_RINGCT; + memcpy(input_context.bytes + 1, first_rct_key_image.data, sizeof(crypto::key_image)); + return input_context; +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_contextualized_sender_receiver_secret(const unsigned char s_sender_receiver[32], + const mx25519_pubkey &enote_ephemeral_pubkey, + const input_context_t &input_context, + crypto::hash &s_sender_receiver_ctx_out) +{ + // s^ctx_sr = H_32[s_sr](D_e, input_context) + const auto transcript = make_fixed_transcript( + enote_ephemeral_pubkey, input_context); + derive_bytes_32(transcript.data(), transcript.size(), s_sender_receiver, &s_sender_receiver_ctx_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_sender_extension_g_coinbase(const crypto::hash &s_sender_receiver_ctx, + const rct::xmr_amount amount, + const crypto::public_key &main_address_spend_pubkey, + crypto::secret_key &sender_extension_out) +{ + // k^o_g = H_n[s^ctx_sr]("..g..", a, K^0_s) + const auto transcript = make_fixed_transcript(amount, + main_address_spend_pubkey); + derive_scalar(transcript.data(), transcript.size(), &s_sender_receiver_ctx, &sender_extension_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_sender_extension_t_coinbase(const crypto::hash &s_sender_receiver_ctx, + const rct::xmr_amount amount, + const crypto::public_key &main_address_spend_pubkey, + crypto::secret_key &sender_extension_out) +{ + // k^o_t = H_n[s^ctx_sr]("..t..", a, K^0_s) + const auto transcript = make_fixed_transcript(amount, + main_address_spend_pubkey); + derive_scalar(transcript.data(), transcript.size(), &s_sender_receiver_ctx, &sender_extension_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_onetime_address_coinbase(const crypto::public_key &main_address_spend_pubkey, + const crypto::hash &s_sender_receiver_ctx, + const rct::xmr_amount amount, + crypto::public_key &onetime_address_out) +{ + // K^o_ext = k^o_g G + k^o_t T + crypto::public_key sender_extension_pubkey; + make_carrot_sender_extension_pubkey_coinbase(s_sender_receiver_ctx, amount, main_address_spend_pubkey, + sender_extension_pubkey); + + // Ko = K^0_s + K^o_ext + onetime_address_out = rct::rct2pk(rct::addKeys( + rct::pk2rct(main_address_spend_pubkey), rct::pk2rct(sender_extension_pubkey))); +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_sender_extension_g(const crypto::hash &s_sender_receiver_ctx, + const rct::key &amount_commitment, + crypto::secret_key &sender_extension_out) +{ + // k^o_g = H_n[s^ctx_sr]("..g..", C_a) + const auto transcript = make_fixed_transcript(amount_commitment); + derive_scalar(transcript.data(), transcript.size(), &s_sender_receiver_ctx, &sender_extension_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_sender_extension_t(const crypto::hash &s_sender_receiver_ctx, + const rct::key &amount_commitment, + crypto::secret_key &sender_extension_out) +{ + // k^o_t = H_n[s^ctx_sr]("..t..", C_a) + const auto transcript = make_fixed_transcript(amount_commitment); + derive_scalar(transcript.data(), transcript.size(), &s_sender_receiver_ctx, &sender_extension_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_onetime_address(const crypto::public_key &address_spend_pubkey, + const crypto::hash &s_sender_receiver_ctx, + const rct::key &amount_commitment, + crypto::public_key &onetime_address_out) +{ + // K^o_ext = k^o_g G + k^o_t T + crypto::public_key sender_extension_pubkey; + make_carrot_sender_extension_pubkey(s_sender_receiver_ctx, amount_commitment, sender_extension_pubkey); + + // Ko = K^j_s + K^o_ext + onetime_address_out = rct::rct2pk(rct::addKeys( + rct::pk2rct(address_spend_pubkey), rct::pk2rct(sender_extension_pubkey))); +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_amount_blinding_factor(const crypto::hash &s_sender_receiver_ctx, + const rct::xmr_amount amount, + const crypto::public_key &address_spend_pubkey, + const CarrotEnoteType enote_type, + crypto::secret_key &amount_blinding_factor_out) +{ + // k_a = H_n[s^ctx_sr](a, K^j_s, enote_type) + const auto transcript = make_fixed_transcript( + amount, address_spend_pubkey, static_cast(enote_type)); + derive_scalar(transcript.data(), transcript.size(), &s_sender_receiver_ctx, &amount_blinding_factor_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_anchor_encryption_mask(const crypto::hash &s_sender_receiver_ctx, + const crypto::public_key &onetime_address, + encrypted_janus_anchor_t &anchor_encryption_mask_out) +{ + // m_anchor = H_16[s^ctx_sr](Ko) + const auto transcript = make_fixed_transcript(onetime_address); + derive_bytes_16(transcript.data(), transcript.size(), &s_sender_receiver_ctx, &anchor_encryption_mask_out); +} +//------------------------------------------------------------------------------------------------------------------- +encrypted_janus_anchor_t encrypt_carrot_anchor(const janus_anchor_t &anchor, + const crypto::hash &s_sender_receiver_ctx, + const crypto::public_key &onetime_address) +{ + // m_anchor = H_16[s^ctx_sr](Ko) + encrypted_janus_anchor_t mask; + make_carrot_anchor_encryption_mask(s_sender_receiver_ctx, onetime_address, mask); + + // anchor_enc = anchor XOR m_anchor + return anchor ^ mask; +} +//------------------------------------------------------------------------------------------------------------------- +janus_anchor_t decrypt_carrot_anchor(const encrypted_janus_anchor_t &encrypted_anchor, + const crypto::hash &s_sender_receiver_ctx, + const crypto::public_key &onetime_address) +{ + // m_anchor = H_16[s^ctx_sr](Ko) + encrypted_janus_anchor_t mask; + make_carrot_anchor_encryption_mask(s_sender_receiver_ctx, onetime_address, mask); + + // anchor = anchor_enc XOR m_anchor + return encrypted_anchor ^ mask; +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_amount_encryption_mask(const crypto::hash &s_sender_receiver_ctx, + const crypto::public_key &onetime_address, + encrypted_amount_t &amount_encryption_mask_out) +{ + // m_a = H_8[s^ctx_sr](Ko) + const auto transcript = make_fixed_transcript(onetime_address); + derive_bytes_8(transcript.data(), transcript.size(), &s_sender_receiver_ctx, &amount_encryption_mask_out); +} +//------------------------------------------------------------------------------------------------------------------- +encrypted_amount_t encrypt_carrot_amount(const rct::xmr_amount amount, + const crypto::hash &s_sender_receiver_ctx, + const crypto::public_key &onetime_address) +{ + // m_a = H_8[s^ctx_sr](Ko) + encrypted_amount_t mask; + make_carrot_amount_encryption_mask(s_sender_receiver_ctx, onetime_address, mask); + + // a_enc = a XOR m_a [paying attention to system endianness] + return enc_amount(amount, mask); +} +//------------------------------------------------------------------------------------------------------------------- +rct::xmr_amount decrypt_carrot_amount(const encrypted_amount_t encrypted_amount, + const crypto::hash &s_sender_receiver_ctx, + const crypto::public_key &onetime_address) +{ + // m_a = H_8[s^ctx_sr](Ko) + encrypted_amount_t mask; + make_carrot_amount_encryption_mask(s_sender_receiver_ctx, onetime_address, mask); + + // a = a_enc XOR m_a [paying attention to system endianness] + return dec_amount(encrypted_amount, mask); +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_payment_id_encryption_mask(const crypto::hash &s_sender_receiver_ctx, + const crypto::public_key &onetime_address, + encrypted_payment_id_t &payment_id_encryption_mask_out) +{ + // m_pid = H_8[s^ctx_sr](Ko) + const auto transcript = make_fixed_transcript(onetime_address); + derive_bytes_8(transcript.data(), transcript.size(), &s_sender_receiver_ctx, &payment_id_encryption_mask_out); +} +//------------------------------------------------------------------------------------------------------------------- +encrypted_payment_id_t encrypt_legacy_payment_id(const payment_id_t payment_id, + const crypto::hash &s_sender_receiver_ctx, + const crypto::public_key &onetime_address) +{ + // m_pid = H_8[s^ctx_sr](Ko) + encrypted_payment_id_t mask; + make_carrot_payment_id_encryption_mask(s_sender_receiver_ctx, onetime_address, mask); + + // pid_enc = pid XOR m_pid + return convert_payment_id(payment_id) ^ mask; +} +//------------------------------------------------------------------------------------------------------------------- +payment_id_t decrypt_legacy_payment_id(const encrypted_payment_id_t encrypted_payment_id, + const crypto::hash &s_sender_receiver_ctx, + const crypto::public_key &onetime_address) +{ + // m_pid = H_8[s^ctx_sr](Ko) + encrypted_payment_id_t mask; + make_carrot_payment_id_encryption_mask(s_sender_receiver_ctx, onetime_address, mask); + + // pid = pid_enc XOR m_pid + return convert_payment_id(encrypted_payment_id ^ mask); +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_janus_anchor_special(const mx25519_pubkey &enote_ephemeral_pubkey, + const input_context_t &input_context, + const crypto::public_key &onetime_address, + const crypto::secret_key &k_view, + janus_anchor_t &anchor_special_out) +{ + // anchor_sp = H_16[k_v](D_e, input_context, Ko) + const auto transcript = make_fixed_transcript( + enote_ephemeral_pubkey, input_context, onetime_address); + derive_bytes_16(transcript.data(), transcript.size(), &k_view, &anchor_special_out); +} +//------------------------------------------------------------------------------------------------------------------- +void recover_address_spend_pubkey(const crypto::public_key &onetime_address, + const crypto::secret_key &sender_extension_g, + const crypto::secret_key &sender_extension_t, + crypto::public_key &address_spend_key_out) +{ + // K^o_ext = k^o_g G + k^o_t T + rct::key sender_extension_pubkey; + rct::addKeys2(sender_extension_pubkey, + rct::sk2rct(sender_extension_g), + rct::sk2rct(sender_extension_t), + rct::pk2rct(crypto::get_T())); + + // K^j_s = Ko - K^o_ext + rct::key res_tmp; + rct::subKeys(res_tmp, rct::pk2rct(onetime_address), sender_extension_pubkey); + address_spend_key_out = rct::rct2pk(res_tmp); +} +//------------------------------------------------------------------------------------------------------------------- +bool test_carrot_view_tag(const unsigned char s_sender_receiver[32], + const input_context_t input_context, + const crypto::public_key &onetime_address, + const view_tag_t view_tag) +{ + // vt' = H_3[s_sr](input_context || Ko) + view_tag_t nominal_view_tag; + make_carrot_view_tag(s_sender_receiver, input_context, onetime_address, nominal_view_tag); + + // vt' ?= vt + return nominal_view_tag == view_tag; +} +//------------------------------------------------------------------------------------------------------------------- +bool try_recompute_carrot_amount_commitment(const crypto::hash &s_sender_receiver_ctx, + const rct::xmr_amount nominal_amount, + const crypto::public_key &nominal_address_spend_pubkey, + const CarrotEnoteType nominal_enote_type, + const rct::key &amount_commitment, + crypto::secret_key &amount_blinding_factor_out) +{ + // k_a' = H_n[s^ctx_sr](a', K^j_s', enote_type') + make_carrot_amount_blinding_factor(s_sender_receiver_ctx, + nominal_amount, + nominal_address_spend_pubkey, + nominal_enote_type, + amount_blinding_factor_out); + + // C_a' = k_a' G + a' H + const rct::key nominal_amount_commitment = rct::commit(nominal_amount, rct::sk2rct(amount_blinding_factor_out)); + + // C_a' ?= C_a + return nominal_amount_commitment == amount_commitment; +} +//------------------------------------------------------------------------------------------------------------------- +bool try_get_carrot_amount(const crypto::hash &s_sender_receiver_ctx, + const encrypted_amount_t &encrypted_amount, + const crypto::public_key &onetime_address, + const crypto::public_key &address_spend_pubkey, + const rct::key &amount_commitment, + CarrotEnoteType &enote_type_out, + rct::xmr_amount &amount_out, + crypto::secret_key &amount_blinding_factor_out) +{ + // a' = a_enc XOR m_a + amount_out = decrypt_carrot_amount(encrypted_amount, s_sender_receiver_ctx, onetime_address); + + // set enote_type <- "payment" + enote_type_out = CarrotEnoteType::PAYMENT; + + // if C_a ?= k_a' G + a' H, then PASS + if (try_recompute_carrot_amount_commitment(s_sender_receiver_ctx, + amount_out, + address_spend_pubkey, + enote_type_out, + amount_commitment, + amount_blinding_factor_out)) + return true; + + // set enote_type <- "change" + enote_type_out = CarrotEnoteType::CHANGE; + + // if C_a ?= k_a' G + a' H, then PASS + if (try_recompute_carrot_amount_commitment(s_sender_receiver_ctx, + amount_out, + address_spend_pubkey, + enote_type_out, + amount_commitment, + amount_blinding_factor_out)) + return true; + + // neither attempt at recomputing passed: so FAIL + return false; +} +//------------------------------------------------------------------------------------------------------------------- +bool verify_carrot_normal_janus_protection(const janus_anchor_t &nominal_anchor, + const input_context_t &input_context, + const crypto::public_key &nominal_address_spend_pubkey, + const bool is_subaddress, + const payment_id_t nominal_payment_id, + const mx25519_pubkey &enote_ephemeral_pubkey) +{ + // d_e' = H_n(anchor_norm, input_context, K^j_s, pid) + crypto::secret_key nominal_enote_ephemeral_privkey; + make_carrot_enote_ephemeral_privkey(nominal_anchor, + input_context, + nominal_address_spend_pubkey, + nominal_payment_id, + nominal_enote_ephemeral_privkey); + + // recompute D_e' for d_e' and address type + mx25519_pubkey nominal_enote_ephemeral_pubkey; + make_carrot_enote_ephemeral_pubkey(nominal_enote_ephemeral_privkey, + nominal_address_spend_pubkey, + is_subaddress, + nominal_enote_ephemeral_pubkey); + + // D_e' ?= D_e + return 0 == memcmp(&nominal_enote_ephemeral_pubkey, &enote_ephemeral_pubkey, sizeof(mx25519_pubkey)); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace carrot diff --git a/src/carrot_core/enote_utils.h b/src/carrot_core/enote_utils.h new file mode 100644 index 00000000000..00ba458baeb --- /dev/null +++ b/src/carrot_core/enote_utils.h @@ -0,0 +1,423 @@ +// Copyright (c) 2024, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// @file Utilities for making and handling enotes with carrot. + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "core_types.h" +#include "mx25519.h" +#include "ringct/rctTypes.h" + +//third party headers + +//standard headers + +//forward declarations + +namespace carrot +{ + +/** + * brief: make_carrot_enote_ephemeral_privkey - enote ephemeral privkey d_e for Carrot enotes + * d_e = H_n(anchor_norm, input_context, K^j_s, pid) + * param: anchor_norm - normal Janus anchor + * param: input_context - input_context + * param: address_spend_pubkey - K^j_s + * param: payment_id - pid + * outparam: enote_ephemeral_privkey_out - k_e + */ +void make_carrot_enote_ephemeral_privkey(const janus_anchor_t &anchor_norm, + const input_context_t &input_context, + const crypto::public_key &address_spend_pubkey, + const payment_id_t payment_id, + crypto::secret_key &enote_ephemeral_privkey_out); +/** + * brief: make_carrot_enote_ephemeral_pubkey_main - make enote ephemeral pubkey D_e for a main address + * D_e = d_e B + * param: enote_ephemeral_privkey - d_e + * outparam: enote_ephemeral_pubkey_out - D_e + */ +void make_carrot_enote_ephemeral_pubkey_cryptonote(const crypto::secret_key &enote_ephemeral_privkey, + mx25519_pubkey &enote_ephemeral_pubkey_out); +/** + * brief: make_carrot_enote_ephemeral_pubkey_subaddress - make enote ephemeral pubkey D_e for a subaddress + * D_e = d_e ConvertPointE(K^j_s) + * param: enote_ephemeral_privkey - d_e + * param: address_spend_pubkey - K^j_s + * outparam: enote_ephemeral_pubkey_out - D_e + */ +void make_carrot_enote_ephemeral_pubkey_subaddress(const crypto::secret_key &enote_ephemeral_privkey, + const crypto::public_key &address_spend_pubkey, + mx25519_pubkey &enote_ephemeral_pubkey_out); +/** + * brief: make_carrot_enote_ephemeral_pubkey - make enote ephemeral pubkey D_e for either address type + * [is_subaddress]: D_e = d_e ConvertPointE(K^j_s) + * [!is_subaddress]: D_e = d_e B + * param: enote_ephemeral_privkey - d_e + * param: address_spend_pubkey - K^j_s + * param: is_subaddress - + * outparam: enote_ephemeral_pubkey_out - D_e + */ +void make_carrot_enote_ephemeral_pubkey(const crypto::secret_key &enote_ephemeral_privkey, + const crypto::public_key &address_spend_pubkey, + const bool is_subaddress, + mx25519_pubkey &enote_ephemeral_pubkey_out); +/** + * brief: try_make_carrot_shared_key_receiver - perform the receiver-side ECDH exchange for Carrot enotes + * s_sr = k_v D_e + * param: k_view - k_v + * param: enote_ephemeral_pubkey - D_e + * outparam: s_sender_receiver_out - s_sr + * return: true if successful, false if a failure occurred in point decompression + */ +bool try_make_carrot_shared_key_receiver(const crypto::secret_key &k_view, + const mx25519_pubkey &enote_ephemeral_pubkey, + mx25519_pubkey &s_sender_receiver_out); +/** + * brief: try_make_carrot_shared_key_sender - perform the sender-side ECDH exchange for Carrot enotes + * s_sr = d_e ConvertPointE(K^j_v) + * param: enote_ephemeral_privkey - d_e + * param: address_view_pubkey - K^j_v + * outparam: s_sender_receiver_out - s_sr + * return: true if successful, false if a failure occurred in point decompression + */ +bool try_make_carrot_shared_key_sender(const crypto::secret_key &enote_ephemeral_privkey, + const crypto::public_key &address_view_pubkey, + mx25519_pubkey &s_sender_receiver_out); +/** +* brief: make_carrot_view_tag - used for optimized identification of enotes +* vt = H_3[s_sr](input_context || Ko) +* param: s_sender_receiver - s_sr +* param: input_context - input_context +* param: onetime_address - Ko +* outparam: view_tag_out - vt +*/ +void make_carrot_view_tag(const unsigned char s_sender_receiver[32], + const input_context_t &input_context, + const crypto::public_key &onetime_address, + view_tag_t &view_tag_out); +/** +* brief: make_carrot_input_context_coinbase - input context for a sender-receiver secret (coinbase txs) +* input_context = "C" || IntToBytes256(block_index) +* param: block_index - block index of the coinbase tx +* return: input_context +*/ +input_context_t make_carrot_input_context_coinbase(const std::uint64_t block_index); +/** +* brief: make_carrot_input_context - input context for a sender-receiver secret (standard RingCT txs) +* input_context = "R" || KI_1 +* param: first_rct_key_image - KI_1, the first spent RingCT key image in a tx +* return: input_context +*/ +input_context_t make_carrot_input_context(const crypto::key_image &first_rct_key_image); +/** +* brief: make_carrot_contextualized_sender_receiver_secret - contextualized sender-receiver secret s^ctx_sr +* s^ctx_sr = H_32[s_sr](D_e, input_context) +* param: s_sender_receiver - s_sr +* param: enote_ephemeral_pubkey - D_e +* param: input_context - [standard: KI_1] [coinbase: block index] +* outparam: s_sender_receiver_ctx_out - s^ctx_sr +* - note: this is 'crypto::hash' instead of 'crypto::secret_key' for better performance in multithreaded environments +*/ +void make_carrot_contextualized_sender_receiver_secret(const unsigned char s_sender_receiver[32], + const mx25519_pubkey &enote_ephemeral_pubkey, + const input_context_t &input_context, + crypto::hash &s_sender_receiver_ctx_out); +/** +* brief: extension over G for transforming a receiver's spendkey into a coinbase one-time address +* k^o_g = H_n[s^ctx_sr]("..g..", a, K^0_s) +* param: s_sender_receiver_ctx - s^ctx_sr +* param: amount - a +* param: main_address_spend_pubkey - K^0_s +* outparam: sender_extension_out - k^o_g +*/ +void make_carrot_sender_extension_g_coinbase(const crypto::hash &s_sender_receiver_ctx, + const rct::xmr_amount amount, + const crypto::public_key &main_address_spend_pubkey, + crypto::secret_key &sender_extension_out); +/** +* brief: extension over T for transforming a receiver's spendkey into a coinbase one-time address +* k^o_t = H_n[s^ctx_sr]("..t..", a, K^0_s) +* param: s_sender_receiver_ctx - s^ctx_sr +* param: amount - a +* param: main_address_spend_pubkey - K^0_s +* outparam: sender_extension_out - k^o_t +*/ +void make_carrot_sender_extension_t_coinbase(const crypto::hash &s_sender_receiver_ctx, + const rct::xmr_amount amount, + const crypto::public_key &main_address_spend_pubkey, + crypto::secret_key &sender_extension_out); +/** +* brief: make_carrot_onetime_address - create a FCMP++ coinbase onetime address +* Ko = K^0_s + K^o_ext = K^j_s + (k^o_g G + k^o_t T) +* param: main_address_spend_pubkey - K^0_s +* param: s_sender_receiver_ctx - s^ctx_sr +* param: amount - a +* outparam: onetime_address_out - Ko +*/ +void make_carrot_onetime_address_coinbase(const crypto::public_key &main_address_spend_pubkey, + const crypto::hash &s_sender_receiver_ctx, + const rct::xmr_amount amount, + crypto::public_key &onetime_address_out); +/** +* brief: extension over G for transforming a receiver's spendkey into a non-coinbase one-time address +* k^o_g = H_n[s^ctx_sr]("..g..", C_a) +* param: s_sender_receiver_ctx - s^ctx_sr +* param: amount_commitment - C_a +* outparam: sender_extension_out - k^o_g +*/ +void make_carrot_sender_extension_g(const crypto::hash &s_sender_receiver_ctx, + const rct::key &amount_commitment, + crypto::secret_key &sender_extension_out); +/** +* brief: extension over T for transforming a receiver's spendkey into a non-coinbase one-time address +* k^o_t = H_n[s^ctx_sr]("..t..", C_a) +* param: s_sender_receiver_ctx - s^ctx_sr +* param: amount_commitment - C_a +* outparam: sender_extension_out - k^o_t +*/ +void make_carrot_sender_extension_t(const crypto::hash &s_sender_receiver_ctx, + const rct::key &amount_commitment, + crypto::secret_key &sender_extension_out); +/** +* brief: make_carrot_onetime_address - create a FCMP++ non-coinbase onetime address +* Ko = K^j_s + K^o_ext = K^j_s + (k^o_g G + k^o_t T) +* param: address_spend_pubkey - K^j_s +* param: s_sender_receiver_ctx - s^ctx_sr +* param: amount_commitment - C_a +* outparam: onetime_address_out - Ko +*/ +void make_carrot_onetime_address(const crypto::public_key &address_spend_pubkey, + const crypto::hash &s_sender_receiver_ctx, + const rct::key &amount_commitment, + crypto::public_key &onetime_address_out); +/** +* brief: make_carrot_amount_blinding_factor - create blinding factor for enote's amount commitment C_a +* k_a = H_n[s^ctx_sr](a, K^j_s, enote_type) +* param: s_sender_receiver_ctx - s^ctx_sr +* param: amount - a +* param: address_spend_pubkey - K^j_s +* param: enote_type - enote_type +* outparam: amount_blinding_factor_out - k_a +*/ +void make_carrot_amount_blinding_factor(const crypto::hash &s_sender_receiver_ctx, + const rct::xmr_amount amount, + const crypto::public_key &address_spend_pubkey, + const CarrotEnoteType enote_type, + crypto::secret_key &amount_blinding_factor_out); +/** +* brief: make_carrot_anchor_encryption_mask - create XOR encryption mask for enote's anchor +* m_anchor = H_16[s^ctx_sr](Ko) +* param: s_sender_receiver_ctx - s^ctx_sr +* param: onetime_address - Ko +* outparam: anchor_encryption_mask_out - m_anchor +*/ +void make_carrot_anchor_encryption_mask(const crypto::hash &s_sender_receiver_ctx, + const crypto::public_key &onetime_address, + encrypted_janus_anchor_t &anchor_encryption_mask_out); +/** +* brief: encrypt_carrot_anchor - encrypt a Janus anchor for an enote +* anchor_enc = anchor XOR m_anchor +* param: anchor - +* param: s_sender_receiver_ctx - s^ctx_sr +* param: onetime_address - Ko +* return: anchor_enc +*/ +encrypted_janus_anchor_t encrypt_carrot_anchor(const janus_anchor_t &anchor, + const crypto::hash &s_sender_receiver_ctx, + const crypto::public_key &onetime_address); +/** +* brief: decrypt_carrot_address_tag - decrypt a Janus anchor from an enote +* anchor = anchor_enc XOR m_anchor +* param: encrypted_anchor - anchor_enc +* param: s_sender_receiver_ctx - s^ctx_sr +* param: onetime_address - Ko +* return: anchor +*/ +janus_anchor_t decrypt_carrot_anchor(const encrypted_janus_anchor_t &encrypted_anchor, + const crypto::hash &s_sender_receiver_ctx, + const crypto::public_key &onetime_address); +/** +* brief: make_carrot_amount_encryption_mask - create XOR encryption mask for enote's amount +* m_a = H_8[s^ctx_sr](Ko) +* param: s_sender_receiver_ctx - s^ctx_sr +* param: onetime_address - Ko +* outparam: amount_encryption_mask_out - m_a +*/ +void make_carrot_amount_encryption_mask(const crypto::hash &s_sender_receiver_ctx, + const crypto::public_key &onetime_address, + encrypted_amount_t &amount_encryption_mask_out); +/** +* brief: encrypt_carrot_amount - encrypt an amount for an enote +* a_enc = a XOR m_a +* param: amount - a +* param: s_sender_receiver_ctx - s^ctx_sr +* param: onetime_address - Ko +* return: a_enc +*/ +encrypted_amount_t encrypt_carrot_amount(const rct::xmr_amount amount, + const crypto::hash &s_sender_receiver_ctx, + const crypto::public_key &onetime_address); +/** +* brief: decrypt_carrot_amount - decrypt an amount from an enote +* a = a_enc XOR m_a +* param: encrypted_amount - a_enc +* param: s_sender_receiver_ctx - s^ctx_sr +* param: onetime_address - Ko +* return: a +*/ +rct::xmr_amount decrypt_carrot_amount(const encrypted_amount_t encrypted_amount, + const crypto::hash &s_sender_receiver_ctx, + const crypto::public_key &onetime_address); +/** +* brief: make_carrot_payment_id_encryption_mask - create XOR encryption mask for enote's payment ID +* m_pid = H_8[s^ctx_sr](Ko) +* param: s_sender_receiver_ctx - s^ctx_sr +* param: onetime_address - Ko +* outparam: payment_id_encryption_mask_out - m_pid +*/ +void make_carrot_payment_id_encryption_mask(const crypto::hash &s_sender_receiver_ctx, + const crypto::public_key &onetime_address, + encrypted_payment_id_t &payment_id_encryption_mask_out); +/** +* brief: encrypt_legacy_payment_id - encrypt a payment ID from an enote +* pid_enc = pid XOR m_pid +* param: payment_id - pid +* param: s_sender_receiver_ctx - s^ctx_sr +* param: onetime_address - Ko +* return: pid_enc +*/ +encrypted_payment_id_t encrypt_legacy_payment_id(const payment_id_t payment_id, + const crypto::hash &s_sender_receiver_ctx, + const crypto::public_key &onetime_address); +/** +* brief: decrypt_legacy_payment_id - decrypt a payment ID from an enote +* pid = pid_enc XOR m_pid +* param: encrypted_payment_id - pid_enc +* param: s_sender_receiver_ctx - s^ctx_sr +* param: onetime_address - Ko +* return: pid +*/ +payment_id_t decrypt_legacy_payment_id(const encrypted_payment_id_t encrypted_payment_id, + const crypto::hash &s_sender_receiver_ctx, + const crypto::public_key &onetime_address); +/** + * brief: make_carrot_janus_anchor_special - make a janus anchor for "special" enotes + * anchor_sp = H_16[k_v](D_e, input_context, Ko) + * param: enote_ephemeral_pubkey - D_e + * param: input_context - + * param: onetime_address - Ko + * param: k_view - k_v + * outparam: anchor_special_out - anchor_sp + */ +void make_carrot_janus_anchor_special(const mx25519_pubkey &enote_ephemeral_pubkey, + const input_context_t &input_context, + const crypto::public_key &onetime_address, + const crypto::secret_key &k_view, + janus_anchor_t &anchor_special_out); +/** +* brief: recover_address_spend_pubkey - get the receiver's spend key for which this one-time address +* can be reconstructed as 'owned' by +* K^j_s = Ko - K^o_ext = Ko - (k^o_g G + k^o_t T) +* param: onetime_address - Ko +* param: sender_extension_g - k^g_o +* param: sender_extension_t - k^t_o +* outparam: address_spend_key_out: - K^j_s +*/ +void recover_address_spend_pubkey(const crypto::public_key &onetime_address, + const crypto::secret_key &sender_extension_g, + const crypto::secret_key &sender_extension_t, + crypto::public_key &address_spend_key_out); +/** +* brief: test_carrot_view_tag - test carrot view tag +* param: s_sender_receiver - s_sr +* param: input_context - +* param: onetime_address - Ko +* param: view_tag - vt +* return: true if successfully recomputed the view tag +*/ +bool test_carrot_view_tag(const unsigned char s_sender_receiver[32], + const input_context_t input_context, + const crypto::public_key &onetime_address, + const view_tag_t view_tag); +/** +* brief: try_recompute_carrot_amount_commitment - test recreating the amount commitment for given enote_type and amount +* param: s_sender_receiver_ctx - s^ctx_sr +* param: nominal_amount - a' +* param: nominal_address_spend_pubkey - K^j_s' +* param: nominal_enote_type - enote_type' +* param: amount_commitment - C_a +* outparam: amount_blinding_factor_out - k_a' = H_n[s^ctx_sr](a', K^j_s', enote_type') +* return: true if successfully recomputed the amount commitment (C_a ?= k_a' G + a' H) +*/ +bool try_recompute_carrot_amount_commitment(const crypto::hash &s_sender_receiver_ctx, + const rct::xmr_amount nominal_amount, + const crypto::public_key &nominal_address_spend_pubkey, + const CarrotEnoteType nominal_enote_type, + const rct::key &amount_commitment, + crypto::secret_key &amount_blinding_factor_out); +/** +* brief: try_get_carrot_amount - test decrypting the amount and recomputing the amount commitment +* param: s_sender_receiver_ctx - s^ctx_sr +* param: encrypted_amount - a_enc +* param: onetime_address - Ko +* param: address_spend_pubkey - K^j_s +* param: amount_commitment - C_a +* outparam: enote_type_out - enote_type' +* outparam: amount_out - a' = a_enc XOR m_a +* outparam: amount_blinding_factor_out - k_a' = H_n[s^ctx_sr](a', K^j_s', enote_type') +* return: true if successfully recomputed the amount commitment (C_a ?= k_a' G + a' H) +*/ +bool try_get_carrot_amount(const crypto::hash &s_sender_receiver_ctx, + const encrypted_amount_t &encrypted_amount, + const crypto::public_key &onetime_address, + const crypto::public_key &address_spend_pubkey, + const rct::key &amount_commitment, + CarrotEnoteType &enote_type_out, + rct::xmr_amount &amount_out, + crypto::secret_key &amount_blinding_factor_out); +/** + * brief: verify_carrot_normal_janus_protection - check normal external enote is Janus safe (i.e. can recompute D_e) + * param: nominal_anchor - anchor' + * param: input_context - + * param: nominal_address_spend_pubkey - K^j_s' + * param: is_subaddress - + * param: nominal_payment_id - pid' + * param: enote_ephemeral_pubkey - D_e + * return: true if this normal external enote is safe from Janus attacks + */ +bool verify_carrot_normal_janus_protection(const janus_anchor_t &nominal_anchor, + const input_context_t &input_context, + const crypto::public_key &nominal_address_spend_pubkey, + const bool is_subaddress, + const payment_id_t nominal_payment_id, + const mx25519_pubkey &enote_ephemeral_pubkey); +} //namespace carrot diff --git a/src/carrot_core/exceptions.h b/src/carrot_core/exceptions.h new file mode 100644 index 00000000000..1048cb4a929 --- /dev/null +++ b/src/carrot_core/exceptions.h @@ -0,0 +1,79 @@ +// Copyright (c) 2025, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//! @file Exceptions thrown by Carrot + +#pragma once + +//local headers + +//third party headers + +//standard headers +#include + +//forward declarations + +namespace carrot +{ +#define CARROT_DEFINE_SIMPLE_ERROR_TYPE(e, b) class e: public b { using b::b; }; + +class carrot_logic_error: public std::logic_error { using std::logic_error::logic_error; }; + +CARROT_DEFINE_SIMPLE_ERROR_TYPE(bad_address_type, carrot_logic_error) +CARROT_DEFINE_SIMPLE_ERROR_TYPE(component_out_of_order, carrot_logic_error) +CARROT_DEFINE_SIMPLE_ERROR_TYPE(invalid_point, carrot_logic_error) +CARROT_DEFINE_SIMPLE_ERROR_TYPE(missing_components, carrot_logic_error) +CARROT_DEFINE_SIMPLE_ERROR_TYPE(missing_randomness, carrot_logic_error) +CARROT_DEFINE_SIMPLE_ERROR_TYPE(missing_ephemeral_key, carrot_logic_error) +CARROT_DEFINE_SIMPLE_ERROR_TYPE(too_few_inputs, carrot_logic_error) +CARROT_DEFINE_SIMPLE_ERROR_TYPE(too_many_inputs, carrot_logic_error) +CARROT_DEFINE_SIMPLE_ERROR_TYPE(too_few_outputs, carrot_logic_error) +CARROT_DEFINE_SIMPLE_ERROR_TYPE(too_many_outputs, carrot_logic_error) +CARROT_DEFINE_SIMPLE_ERROR_TYPE(integer_overflow, carrot_logic_error) + +class carrot_runtime_error: public std::runtime_error { using std::runtime_error::runtime_error; }; + +CARROT_DEFINE_SIMPLE_ERROR_TYPE(crypto_function_failed, carrot_runtime_error) +CARROT_DEFINE_SIMPLE_ERROR_TYPE(not_enough_money, carrot_runtime_error) +CARROT_DEFINE_SIMPLE_ERROR_TYPE(not_enough_usable_money, carrot_runtime_error) +CARROT_DEFINE_SIMPLE_ERROR_TYPE(unexpected_scan_failure, carrot_runtime_error) +CARROT_DEFINE_SIMPLE_ERROR_TYPE(burnt_enote, carrot_runtime_error) + +/// one needs to include misc_log_ex.h to use the following macros +#define CARROT_THROW(errtype, message) { \ + std::stringstream ss; \ + ss << message; \ + const std::string msg_str = ss.str(); \ + LOG_ERROR(msg_str); \ + throw errtype(msg_str); \ + } +#define CARROT_CHECK_AND_THROW(expr, errtype, message) if (!(expr)) { CARROT_THROW(errtype, message) } + +#undef CARROT_DEFINE_SIMPLE_ERROR_TYPE +} //namespace carrot diff --git a/src/carrot_core/hash_functions.cpp b/src/carrot_core/hash_functions.cpp new file mode 100644 index 00000000000..4f3b9ba3fbf --- /dev/null +++ b/src/carrot_core/hash_functions.cpp @@ -0,0 +1,151 @@ +// Copyright (c) 2024, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//paired header +#include "hash_functions.h" + +//local headers +#include "config.h" +extern "C" +{ +#include "crypto/crypto-ops.h" +} +#include "crypto/blake2b.h" +#include "exceptions.h" +#include "memwipe.h" +#include "misc_log_ex.h" + +//third party headers + +//standard headers +#include +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "carrot" + +namespace carrot +{ +//------------------------------------------------------------------------------------------------------------------- +// H_x[k](data) +// - if derivation_key == nullptr, then the hash is NOT keyed +//------------------------------------------------------------------------------------------------------------------- +static void hash_base(const void *derivation_key, //32 bytes + const void *data, + const std::size_t data_length, + void *hash_out, + const std::size_t out_length) +{ + CARROT_CHECK_AND_THROW(out_length && out_length <= BLAKE2B_OUTBYTES, + carrot_logic_error, "Invalid blake2b digest length"); + + const bool is_keyed = derivation_key != nullptr; + + // Typical blake2b setup with digest length and key length set + static_assert(std::has_unique_object_representations_v); + static_assert(static_cast(BLAKE2B_OUTBYTES) < std::numeric_limits::max()); + blake2b_param b2b_param{}; + b2b_param.digest_length = static_cast(out_length); + b2b_param.key_length = is_keyed ? 32 : 0; + b2b_param.fanout = 1; + b2b_param.depth = 1; + + // Set personal string! + static_assert(sizeof(CARROT_PERSONAL_STRING) <= BLAKE2B_PERSONALBYTES); + static_assert(CARROT_PERSONAL_STRING[sizeof(CARROT_PERSONAL_STRING) - 1] == 0, "str missing null terminator"); + memcpy(b2b_param.personal, CARROT_PERSONAL_STRING, sizeof(CARROT_PERSONAL_STRING) - 1); // exclude null term + + // init state, wiped on leave (TODO: can be sped up to wipe only on bad leave) + tools::scrubbed state; + CARROT_CHECK_AND_THROW(0 == blake2b_init_param(&state, &b2b_param), + crypto_function_failed, "carrot hash base: blake2b init failed"); + + // update with key block if applicable + if (is_keyed) + { + uint8_t block[BLAKE2B_BLOCKBYTES]; + memset(block, 0, BLAKE2B_BLOCKBYTES); + memcpy(block, derivation_key, 32); + const int r = blake2b_update(&state, block, BLAKE2B_BLOCKBYTES); + // wipe the key from stack + memwipe(block, sizeof(block)); + // only check success after wiping key block + CARROT_CHECK_AND_THROW(0 == r, crypto_function_failed, "carrot hash base: blake2b key update failed"); + } + + // update with data blocks + CARROT_CHECK_AND_THROW(0 == blake2b_update(&state, data, data_length), + crypto_function_failed, "carrot hash base: blake2b update failed"); + + // finalize + CARROT_CHECK_AND_THROW(0 == blake2b_final(&state, hash_out, out_length), + crypto_function_failed, "carrot hash base: blake2b final failed"); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +void derive_bytes_3(const void *data, const std::size_t data_length, const void *key, void *hash_out) +{ + // H_3(x): 3-byte output + hash_base(key, data, data_length, hash_out, 3); +} +//------------------------------------------------------------------------------------------------------------------- +void derive_bytes_8(const void *data, const std::size_t data_length, const void *key, void *hash_out) +{ + // H_8(x): 8-byte output + hash_base(key, data, data_length, hash_out, 8); +} +//------------------------------------------------------------------------------------------------------------------- +void derive_bytes_16(const void *data, const std::size_t data_length, const void *key, void *hash_out) +{ + // H_16(x): 16-byte output + hash_base(key, data, data_length, hash_out, 16); +} +//------------------------------------------------------------------------------------------------------------------- +void derive_bytes_32(const void *data, const std::size_t data_length, const void *key, void *hash_out) +{ + // H_32(x): 32-byte output + hash_base(key, data, data_length, hash_out, 32); +} +//------------------------------------------------------------------------------------------------------------------- +void derive_bytes_64(const void *data, const std::size_t data_length, const void *key, void *hash_out) +{ + // H_64(x): 64-byte output + hash_base(key, data, data_length, hash_out, 64); +} +//------------------------------------------------------------------------------------------------------------------- +void derive_scalar(const void *data, const std::size_t data_length, const void *key, void *hash_out) +{ + // H_n(x): Ed25519 group scalar output (32 bytes) + // note: hash to 64 bytes then mod l + unsigned char temp[64]; + hash_base(key, data, data_length, temp, 64); + sc_reduce(temp); //mod l + memcpy(hash_out, temp, 32); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace carrot diff --git a/src/carrot_core/hash_functions.h b/src/carrot_core/hash_functions.h new file mode 100644 index 00000000000..0053e8b5955 --- /dev/null +++ b/src/carrot_core/hash_functions.h @@ -0,0 +1,59 @@ +// Copyright (c) 2024, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// Core hash functions for Carrot. + +#pragma once + +//local headers + +//third party headers + +//standard headers +#include + +//forward declarations + + +namespace carrot +{ + +/// H_3(x): 3-byte output +void derive_bytes_3(const void *data, const std::size_t data_length, const void *key, void *hash_out); +/// H_8(x): 8-byte output +void derive_bytes_8(const void *data, const std::size_t data_length, const void* key, void *hash_out); +/// H_16(x): 16-byte output +void derive_bytes_16(const void *data, const std::size_t data_length, const void *key, void *hash_out); +/// H_32(x): 32-byte output +void derive_bytes_32(const void *data, const std::size_t data_length, const void *key, void *hash_out); +/// H_64(x): 64-byte output +void derive_bytes_64(const void *data, const std::size_t data_length, const void *key, void *hash_out); +/// H_n(x): unclamped Curve25519/Ed25519 group scalar output (32 bytes) +void derive_scalar(const void *data, const std::size_t data_length, const void *key, void *hash_out); + +} //namespace carrot diff --git a/src/carrot_core/output_set_finalization.cpp b/src/carrot_core/output_set_finalization.cpp new file mode 100644 index 00000000000..89bef4a6ab9 --- /dev/null +++ b/src/carrot_core/output_set_finalization.cpp @@ -0,0 +1,398 @@ +// Copyright (c) 2024, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//paired header +#include "output_set_finalization.h" + +//local headers +#include "common/container_helpers.h" +#include "enote_utils.h" +#include "exceptions.h" +#include "misc_log_ex.h" +#include "ringct/rctOps.h" + +//third party headers + +//standard headers +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "carrot.osf" + +namespace carrot +{ +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +template +struct compare_memcmp{ bool operator()(const T &a, const T &b) const { return memcmp(&a, &b, sizeof(T)) < 0; } }; +template +using memcmp_set = std::set>; +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +std::optional get_additional_output_type(const size_t num_outgoing, + const size_t num_selfsend, + const bool need_change_output, + const bool have_payment_type_selfsend) +{ + const size_t num_outputs = num_outgoing + num_selfsend; + const bool already_completed = num_outputs >= 2 && num_selfsend >= 1 && !need_change_output; + if (num_outputs == 0) + { + CARROT_THROW(too_few_outputs, "set contains 0 outputs"); + } + else if (already_completed) + { + return std::nullopt; + } + else if (num_outputs == 1) + { + if (num_selfsend == 0) + { + return AdditionalOutputType::CHANGE_SHARED; + } + else if (!need_change_output) + { + return AdditionalOutputType::DUMMY; + } + else // num_selfsend == 1 && need_change_output + { + if (have_payment_type_selfsend) + { + return AdditionalOutputType::CHANGE_SHARED; + } + else + { + return AdditionalOutputType::PAYMENT_SHARED; + } + } + } + else + { + return AdditionalOutputType::CHANGE_UNIQUE; + } +} +//------------------------------------------------------------------------------------------------------------------- +std::variant get_additional_payment_proposal( + const size_t num_outgoing, + const size_t num_selfsend, + const rct::xmr_amount needed_change_amount, + const bool have_payment_type_selfsend, + const crypto::public_key &change_address_spend_pubkey) +{ + const bool need_change_output = needed_change_amount != 0; + const std::optional additional_output_type = get_additional_output_type( + num_outgoing, + num_selfsend, + need_change_output, + have_payment_type_selfsend + ); + + if (!additional_output_type) + return std::nullopt; + + switch (*additional_output_type) + { + case AdditionalOutputType::PAYMENT_SHARED: + return CarrotPaymentProposalSelfSendV1{ + .destination_address_spend_pubkey = change_address_spend_pubkey, + .amount = needed_change_amount, + .enote_type = CarrotEnoteType::PAYMENT, + .enote_ephemeral_pubkey = std::nullopt + }; + case AdditionalOutputType::CHANGE_SHARED: + return CarrotPaymentProposalSelfSendV1{ + .destination_address_spend_pubkey = change_address_spend_pubkey, + .amount = needed_change_amount, + .enote_type = CarrotEnoteType::CHANGE, + .enote_ephemeral_pubkey = std::nullopt + }; + case AdditionalOutputType::CHANGE_UNIQUE: + return CarrotPaymentProposalSelfSendV1{ + .destination_address_spend_pubkey = change_address_spend_pubkey, + .amount = needed_change_amount, + .enote_type = CarrotEnoteType::CHANGE, + .enote_ephemeral_pubkey = std::nullopt + }; + case AdditionalOutputType::DUMMY: + return CarrotPaymentProposalV1{ + .destination = gen_carrot_main_address_v1(), + .amount = 0, + .randomness = gen_janus_anchor() + }; + } + + CARROT_THROW(std::invalid_argument, "unrecognized additional output type"); +} +//------------------------------------------------------------------------------------------------------------------- +void get_output_enote_proposals(const std::vector &normal_payment_proposals, + const std::vector &selfsend_payment_proposals, + const std::optional &dummy_encrypted_payment_id, + const view_balance_secret_device *s_view_balance_dev, + const view_incoming_key_device *k_view_dev, + const crypto::key_image &tx_first_key_image, + std::vector &output_enote_proposals_out, + encrypted_payment_id_t &encrypted_payment_id_out, + std::vector> *payment_proposal_order_out) +{ + output_enote_proposals_out.clear(); + encrypted_payment_id_out = {{0}}; + if (payment_proposal_order_out) + payment_proposal_order_out->clear(); + + // assert payment proposals numbers + const size_t num_selfsend_proposals = selfsend_payment_proposals.size(); + const size_t num_proposals = normal_payment_proposals.size() + num_selfsend_proposals; + CARROT_CHECK_AND_THROW(num_proposals >= CARROT_MIN_TX_OUTPUTS, too_few_outputs, "too few payment proposals"); + CARROT_CHECK_AND_THROW(num_selfsend_proposals, too_few_outputs, "no selfsend payment proposal"); + + // assert there is a max of 1 integrated address payment proposals + size_t num_integrated = 0; + for (const CarrotPaymentProposalV1 &normal_payment_proposal : normal_payment_proposals) + if (normal_payment_proposal.destination.payment_id != null_payment_id) + ++num_integrated; + CARROT_CHECK_AND_THROW(num_integrated <= 1, + bad_address_type, "only one integrated address is allowed per tx output set"); + + // assert anchor_norm != 0 for normal payments + for (const CarrotPaymentProposalV1 &normal_payment_proposal : normal_payment_proposals) + CARROT_CHECK_AND_THROW(normal_payment_proposal.randomness != janus_anchor_t{}, + missing_randomness, "normal payment proposal has unset anchor_norm AKA randomness"); + + // assert uniqueness of randomness for each normal payment + memcmp_set randomnesses; + for (const CarrotPaymentProposalV1 &normal_payment_proposal : normal_payment_proposals) + randomnesses.insert(normal_payment_proposal.randomness); + const bool has_unique_randomness = randomnesses.size() == normal_payment_proposals.size(); + CARROT_CHECK_AND_THROW(has_unique_randomness, + missing_randomness, "normal payment proposals contain duplicate anchor_norm AKA randomness"); + + // for each output: (enote proposal , is ss?, payment idx ) + std::vector>> sortable_data; + sortable_data.reserve(num_proposals); + + // D^other_e + std::optional other_enote_ephemeral_pubkey; + + // construct normal enotes + for (size_t i = 0; i < normal_payment_proposals.size(); ++i) + { + auto &output_entry = sortable_data.emplace_back(); + output_entry.second = {false, i}; + + encrypted_payment_id_t encrypted_payment_id; + get_output_proposal_normal_v1(normal_payment_proposals[i], + tx_first_key_image, + output_entry.first, + encrypted_payment_id); + + // if 1 normal and 1 self-send, set D^other_e equal to this D_e + if (num_proposals == 2) + other_enote_ephemeral_pubkey = output_entry.first.enote.enote_ephemeral_pubkey; + + // set pid_enc from integrated address proposal pic_enc + const bool is_integrated = normal_payment_proposals[i].destination.payment_id != null_payment_id; + if (is_integrated) + encrypted_payment_id_out = encrypted_payment_id; + } + + // in the case that there is no required pid_enc, set it to the provided dummy + if (0 == num_integrated) + { + CARROT_CHECK_AND_THROW(dummy_encrypted_payment_id, + missing_components, "missing encrypted payment ID: no integrated address nor provided dummy"); + encrypted_payment_id_out = *dummy_encrypted_payment_id; + } + + // if 0 normal, and 2 self-send, set D^other_e equal to whichever *has* a D_e + if (num_proposals == 2 && num_selfsend_proposals == 2) + { + const size_t present_ephem_pk_index = selfsend_payment_proposals.at(0).enote_ephemeral_pubkey ? 0 : 1; + other_enote_ephemeral_pubkey = selfsend_payment_proposals.at(present_ephem_pk_index).enote_ephemeral_pubkey; + CARROT_CHECK_AND_THROW(other_enote_ephemeral_pubkey, + missing_ephemeral_key, "missing ephemeral key: 2-out tx with 2 selfsends needs 1 non-null D_e"); + } + + // construct selfsend enotes, preferring internal enotes over special enotes when possible + for (size_t i = 0; i < num_selfsend_proposals; ++i) + { + const CarrotPaymentProposalSelfSendV1 &selfsend_payment_proposal = selfsend_payment_proposals.at(i); + + auto &output_entry = sortable_data.emplace_back(); + output_entry.second = {true, i}; + + if (s_view_balance_dev != nullptr) + { + get_output_proposal_internal_v1(selfsend_payment_proposal, + *s_view_balance_dev, + tx_first_key_image, + other_enote_ephemeral_pubkey, + output_entry.first); + } + else if (k_view_dev != nullptr) + { + get_output_proposal_special_v1(selfsend_payment_proposal, + *k_view_dev, + tx_first_key_image, + other_enote_ephemeral_pubkey, + output_entry.first); + } + else // neither k_v nor s_vb device passed + { + CARROT_THROW(std::invalid_argument, "neither a view-balance nor view-incoming device was provided"); + } + } + + // sort enotes by K_o + const auto sort_output_enote_proposal = [](const auto &a, const auto &b) -> bool + { return a.first.enote.onetime_address < b.first.enote.onetime_address; }; + std::sort(sortable_data.begin(), sortable_data.end(), sort_output_enote_proposal); + + // collect output_enote_proposals_out and payment_proposal_order_out + output_enote_proposals_out.reserve(num_proposals); + if (payment_proposal_order_out) + payment_proposal_order_out->reserve(num_proposals); + for (const auto &output_entry : sortable_data) + { + output_enote_proposals_out.push_back(output_entry.first); + if (payment_proposal_order_out) + payment_proposal_order_out->push_back(output_entry.second); + } + + // assert uniqueness of D_e if >2-out, shared otherwise. also check D_e is not trivial + memcmp_set ephemeral_pubkeys; + for (const RCTOutputEnoteProposal &p : output_enote_proposals_out) + { + const bool trivial_enote_ephemeral_pubkey = memcmp(p.enote.enote_ephemeral_pubkey.data, + mx25519_pubkey{}.data, + sizeof(mx25519_pubkey)) == 0; + CARROT_CHECK_AND_THROW(!trivial_enote_ephemeral_pubkey, missing_randomness, + "this set contains enote ephemeral pubkeys with x=0"); + ephemeral_pubkeys.insert(p.enote.enote_ephemeral_pubkey); + } + const bool has_unique_ephemeral_pubkeys = ephemeral_pubkeys.size() == output_enote_proposals_out.size(); + CARROT_CHECK_AND_THROW(!(num_proposals == 2 && has_unique_ephemeral_pubkeys), + component_out_of_order, "this 2-out set needs to share their ephemeral pubkey"); + CARROT_CHECK_AND_THROW(!(num_proposals != 2 && !has_unique_ephemeral_pubkeys), + missing_randomness, "this >2-out set contains duplicate enote ephemeral pubkeys"); + + // assert uniqueness of K_o + CARROT_CHECK_AND_THROW(tools::is_sorted_and_unique(sortable_data, sort_output_enote_proposal), + component_out_of_order, "this set contains duplicate onetime addresses"); + + // assert all K_o lie in prime order subgroup + for (const RCTOutputEnoteProposal &output_enote_proposal : output_enote_proposals_out) + { + CARROT_CHECK_AND_THROW(rct::isInMainSubgroup(rct::pk2rct(output_enote_proposal.enote.onetime_address)), + invalid_point, "this set contains an invalid onetime address"); + } + + // assert unique and non-trivial k_a + memcmp_set amount_blinding_factors; + for (const RCTOutputEnoteProposal &output_enote_proposal : output_enote_proposals_out) + { + CARROT_CHECK_AND_THROW(output_enote_proposal.amount_blinding_factor != crypto::null_skey, + missing_randomness, "this set contains a trivial amount blinding factor"); + + amount_blinding_factors.insert(output_enote_proposal.amount_blinding_factor); + } + CARROT_CHECK_AND_THROW(amount_blinding_factors.size() == num_proposals, missing_randomness, + "this set contains duplicate amount blinding factors"); +} +//------------------------------------------------------------------------------------------------------------------- +void get_coinbase_output_enotes(const std::vector &normal_payment_proposals, + const uint64_t block_index, + std::vector &output_coinbase_enotes_out) +{ + output_coinbase_enotes_out.clear(); + + // assert payment proposals numbers + const size_t num_proposals = normal_payment_proposals.size(); + + // assert there are no subaddress or integrated address payment proposals + for (const CarrotPaymentProposalV1 &normal_payment_proposal : normal_payment_proposals) + { + const CarrotDestinationV1 &destination = normal_payment_proposal.destination; + CARROT_CHECK_AND_THROW(destination.payment_id == null_payment_id && !destination.is_subaddress, + bad_address_type, "get coinbase output enotes: no integrated addresses or subaddresses allowed"); + } + + // assert anchor_norm != 0 for payments + for (const CarrotPaymentProposalV1 &normal_payment_proposal : normal_payment_proposals) + CARROT_CHECK_AND_THROW(normal_payment_proposal.randomness != janus_anchor_t{}, + missing_randomness, "normal payment proposal has unset anchor_norm AKA randomness"); + + // assert uniqueness of randomness for each payment + memcmp_set randomnesses; + for (const CarrotPaymentProposalV1 &normal_payment_proposal : normal_payment_proposals) + randomnesses.insert(normal_payment_proposal.randomness); + const bool has_unique_randomness = randomnesses.size() == normal_payment_proposals.size(); + CARROT_CHECK_AND_THROW(has_unique_randomness, + missing_randomness, "normal payment proposals contain duplicate anchor_norm AKA randomness"); + + // construct normal enotes + output_coinbase_enotes_out.reserve(num_proposals); + for (size_t i = 0; i < normal_payment_proposals.size(); ++i) + { + get_coinbase_enote_v1(normal_payment_proposals[i], + block_index, + output_coinbase_enotes_out.emplace_back()); + } + + // assert uniqueness and non-trivial-ness of D_e + memcmp_set ephemeral_pubkeys; + for (const CarrotCoinbaseEnoteV1 &enote : output_coinbase_enotes_out) + { + const bool trivial_enote_ephemeral_pubkey = memcmp(enote.enote_ephemeral_pubkey.data, + mx25519_pubkey{}.data, + sizeof(mx25519_pubkey)) == 0; + CARROT_CHECK_AND_THROW(!trivial_enote_ephemeral_pubkey, + missing_randomness, "this set contains enote ephemeral pubkeys with x=0"); + ephemeral_pubkeys.insert(enote.enote_ephemeral_pubkey); + } + const bool has_unique_ephemeral_pubkeys = ephemeral_pubkeys.size() == output_coinbase_enotes_out.size(); + CARROT_CHECK_AND_THROW(has_unique_ephemeral_pubkeys, + missing_randomness, "a coinbase enote set needs unique ephemeral pubkeys, but this set isn't unique"); + + // sort enotes by K_o + const auto sort_output_enote_proposal = [](const CarrotCoinbaseEnoteV1 &a, const CarrotCoinbaseEnoteV1 &b) + -> bool { return a.onetime_address < b.onetime_address; }; + std::sort(output_coinbase_enotes_out.begin(), output_coinbase_enotes_out.end(), sort_output_enote_proposal); + + // assert uniqueness of K_o + CARROT_CHECK_AND_THROW(tools::is_sorted_and_unique(output_coinbase_enotes_out, sort_output_enote_proposal), + component_out_of_order, "this set contains duplicate onetime addresses"); + + // assert all K_o lie in prime order subgroup + for (const CarrotCoinbaseEnoteV1 &output_enote : output_coinbase_enotes_out) + { + CARROT_CHECK_AND_THROW(rct::isInMainSubgroup(rct::pk2rct(output_enote.onetime_address)), + invalid_point, "this set contains an invalid onetime address"); + } +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace carrot diff --git a/src/carrot_core/output_set_finalization.h b/src/carrot_core/output_set_finalization.h new file mode 100644 index 00000000000..56d5e56cfb0 --- /dev/null +++ b/src/carrot_core/output_set_finalization.h @@ -0,0 +1,125 @@ +// Copyright (c) 2024, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//! @file Utilities for constructing output proposal sets that adhere to Carrot rules + +#pragma once + +//local headers +#include "carrot_enote_types.h" +#include "config.h" +#include "payment_proposal.h" +#include "ringct/rctTypes.h" + +//third party headers + +//standard headers +#include +#include +#include + +//forward declarations + + +namespace carrot +{ +enum class AdditionalOutputType +{ + PAYMENT_SHARED, // selfsend proposal with enote_type="payment" with a shared D_e + CHANGE_SHARED, // selfsend proposal with enote_type="change" with a shared D_e + CHANGE_UNIQUE, // selfsend proposal with enote_type="change" with a unique D_e + DUMMY // outgoing proposal to a random address +}; + +/** + * brief: get_additional_output_type - get the type of the additional enote needed to finalize an output set + * param: num_outgoing - number of outgoing transfers + * param: num_selfsend - number of selfsend transfers + * param: need_change_output - whether an additional change output needs to be included for balance + * param: have_payment_type_selfsend - true if the enote set has a selfsend enote with enote_type="payment" + * return: AdditionalOutputType if need an additional enote, else std::nullopt + * throw: std::runtime_error if the output set is in a state where it cannot be finalized + */ +std::optional get_additional_output_type(const size_t num_outgoing, + const size_t num_selfsend, + const bool need_change_output, + const bool have_payment_type_selfsend); +/** + * brief: get_additional_payment_proposal - get an additional payment proposal to complete an output set + * param: num_outgoing - number of outgoing transfers + * param: num_selfsend - number of selfsend transfers + * param: needed_change_amount - the amount of leftover change needed to be included + * param: have_payment_type_selfsend - true if the enote set has a selfsend enote with enote_type="payment" + * param: change_address_spend_pubkey - K^j_s of our change address + * return: an output proposal if need an additional enote, else none + * throw: std::runtime_error if the output set is in a state where it cannot be finalized + */ +std::variant get_additional_payment_proposal( + const size_t num_outgoing, + const size_t num_selfsend, + const rct::xmr_amount needed_change_amount, + const bool have_payment_type_selfsend, + const crypto::public_key &change_address_spend_pubkey); +/** + * brief: get_output_enote_proposals - convert a *finalized* set of payment proposals into output enote proposals + * param: normal_payment_proposals - + * param: selfsend_payment_proposals - + * param: dummy_encrypted_payment_id - random pid_enc, required if no integrated addresses in normal payment proposals + * param: s_view_balance_dev - pointer to view-balance device (OPTIONAL) + * param: k_view_dev - pointer to view-incoming device (OPTIONAL) + * param: tx_first_key_image - KI_1 + * outparam: output_enote_proposals_out - + * outparam: encrypted_payment_id_out - pid_enc + * outparam: payment_proposal_order_out - (is self-send, payment idx) pairs which specify order of proposals + * converted to output enotes (OPTIONAL) + * throw: std::runtime_error if the payment proposals do not represent a valid tx output set, or if no devices + * + * If s_view_balance_dev is not NULL, then the selfsend payments are converted into *internal* enotes. + * Otherwise, if k_view_dev is not NULL, then the selfsend payments are converted into *external* enotes. + */ +void get_output_enote_proposals(const std::vector &normal_payment_proposals, + const std::vector &selfsend_payment_proposals, + const std::optional &dummy_encrypted_payment_id, + const view_balance_secret_device *s_view_balance_dev, + const view_incoming_key_device *k_view_dev, + const crypto::key_image &tx_first_key_image, + std::vector &output_enote_proposals_out, + encrypted_payment_id_t &encrypted_payment_id_out, + std::vector> *payment_proposal_order_out = nullptr); +/** + * brief: get_coinbase_output_enotes - convert a *finalized* set of payment proposals into coinbase output enotes + * param: normal_payment_proposals - + * param: block_index - + * outparam: output_coinbase_enotes_out - + * throw: std::runtime_error if the payment proposals do not represent a valid tx output set, or if no devices + */ +void get_coinbase_output_enotes(const std::vector &normal_payment_proposals, + const uint64_t block_index, + std::vector &output_coinbase_enotes_out); + +} //namespace carrot diff --git a/src/carrot_core/payment_proposal.cpp b/src/carrot_core/payment_proposal.cpp new file mode 100644 index 00000000000..b262c332aff --- /dev/null +++ b/src/carrot_core/payment_proposal.cpp @@ -0,0 +1,497 @@ +// Copyright (c) 2024, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//paired header +#include "payment_proposal.h" + +//local headers +#include "int-util.h" +#include "enote_utils.h" +#include "exceptions.h" +#include "misc_language.h" +#include "misc_log_ex.h" +#include "ringct/rctOps.h" + +//third party headers + +//standard headers + + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "carrot.pp" + +namespace carrot +{ +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static const janus_anchor_t null_anchor{{0}}; +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +template +static auto auto_wiper(T &obj) +{ + static_assert(std::is_trivially_copyable()); + return epee::misc_utils::create_scope_leave_handler([&]{ memwipe(&obj, sizeof(T)); }); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static crypto::secret_key get_enote_ephemeral_privkey(const janus_anchor_t randomness, + const CarrotDestinationV1 &destination, + const input_context_t &input_context) +{ + // d_e = H_n(anchor_norm, input_context, K^j_s, pid) + crypto::secret_key enote_ephemeral_privkey; + make_carrot_enote_ephemeral_privkey(randomness, + input_context, + destination.address_spend_pubkey, + destination.payment_id, + enote_ephemeral_privkey); + + return enote_ephemeral_privkey; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static mx25519_pubkey get_enote_ephemeral_pubkey(const janus_anchor_t randomness, + const CarrotDestinationV1 &destination, + const input_context_t &input_context) +{ + // d_e = H_n(anchor_norm, input_context, K^j_s, pid) + const crypto::secret_key enote_ephemeral_privkey{get_enote_ephemeral_privkey(randomness, + destination, + input_context)}; + + mx25519_pubkey enote_ephemeral_pubkey; + if (destination.is_subaddress) + // D_e = d_e ConvertPointE(K^j_s) + make_carrot_enote_ephemeral_pubkey_subaddress(enote_ephemeral_privkey, + destination.address_spend_pubkey, + enote_ephemeral_pubkey); + else + // D_e = d_e B + make_carrot_enote_ephemeral_pubkey_cryptonote(enote_ephemeral_privkey, + enote_ephemeral_pubkey); + + return enote_ephemeral_pubkey; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void get_normal_proposal_ecdh_parts(const CarrotPaymentProposalV1 &proposal, + const input_context_t &input_context, + mx25519_pubkey &enote_ephemeral_pubkey_out, + mx25519_pubkey &s_sender_receiver_out) +{ + // 1. d_e = H_n(anchor_norm, input_context, K^j_s, pid) + const crypto::secret_key enote_ephemeral_privkey = get_enote_ephemeral_privkey(proposal.randomness, + proposal.destination, + input_context); + + // 2. make D_e + enote_ephemeral_pubkey_out = get_enote_ephemeral_pubkey(proposal, input_context); + + // 3. s_sr = d_e ConvertPointE(K^j_v) + CARROT_CHECK_AND_THROW( + try_make_carrot_shared_key_sender(enote_ephemeral_privkey, + proposal.destination.address_view_pubkey, + s_sender_receiver_out), + invalid_point, "invalid address view pubkey point"); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void get_output_proposal_parts(const crypto::hash &s_sender_receiver_ctx, + const crypto::public_key &destination_spend_pubkey, + const payment_id_t payment_id, + const rct::xmr_amount amount, + const CarrotEnoteType enote_type, + const mx25519_pubkey &enote_ephemeral_pubkey, + const input_context_t &input_context, + const bool coinbase_amount_commitment, + crypto::secret_key &amount_blinding_factor_out, + rct::key &amount_commitment_out, + crypto::public_key &onetime_address_out, + encrypted_amount_t &encrypted_amount_out, + encrypted_payment_id_t &encrypted_payment_id_out) +{ + // 1. k_a = H_n[s^ctx_sr](a, K^j_s, enote_type) if !coinbase, else 1 + if (coinbase_amount_commitment) + sc_1(to_bytes(amount_blinding_factor_out)); + else + make_carrot_amount_blinding_factor(s_sender_receiver_ctx, + amount, + destination_spend_pubkey, + enote_type, + amount_blinding_factor_out); + + // 2. C_a = k_a G + a H + amount_commitment_out = rct::commit(amount, rct::sk2rct(amount_blinding_factor_out)); + + // 3. Ko = K^j_s + K^o_ext = K^j_s + (k^o_g G + k^o_t T) + if (coinbase_amount_commitment) + make_carrot_onetime_address_coinbase(destination_spend_pubkey, + s_sender_receiver_ctx, + amount, + onetime_address_out); + else + make_carrot_onetime_address(destination_spend_pubkey, + s_sender_receiver_ctx, + amount_commitment_out, + onetime_address_out); + + // 4. a_enc = a XOR m_a + encrypted_amount_out = encrypt_carrot_amount(amount, + s_sender_receiver_ctx, + onetime_address_out); + + // 5. pid_enc = pid XOR m_pid + encrypted_payment_id_out = encrypt_legacy_payment_id(payment_id, s_sender_receiver_ctx, onetime_address_out); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void get_external_output_proposal_parts(const mx25519_pubkey &s_sender_receiver, + const crypto::public_key &destination_spend_pubkey, + const payment_id_t payment_id, + const rct::xmr_amount amount, + const CarrotEnoteType enote_type, + const mx25519_pubkey &enote_ephemeral_pubkey, + const input_context_t &input_context, + const bool coinbase_amount_commitment, + crypto::hash &s_sender_receiver_ctx_out, + crypto::secret_key &amount_blinding_factor_out, + rct::key &amount_commitment_out, + crypto::public_key &onetime_address_out, + encrypted_amount_t &encrypted_amount_out, + encrypted_payment_id_t &encrypted_payment_id_out, + view_tag_t &view_tag_out) +{ + // 1. s^ctx_sr = H_32[s_sr](D_e, input_context) + make_carrot_contextualized_sender_receiver_secret(s_sender_receiver.data, + enote_ephemeral_pubkey, + input_context, + s_sender_receiver_ctx_out); + + // 2. get other parts: k_a, C_a, Ko, a_enc, pid_enc + get_output_proposal_parts(s_sender_receiver_ctx_out, + destination_spend_pubkey, + payment_id, + amount, + enote_type, + enote_ephemeral_pubkey, + input_context, + coinbase_amount_commitment, + amount_blinding_factor_out, + amount_commitment_out, + onetime_address_out, + encrypted_amount_out, + encrypted_payment_id_out); + + // 3. vt = H_3[s_sr](input_context || Ko) + make_carrot_view_tag(s_sender_receiver.data, input_context, onetime_address_out, view_tag_out); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +bool operator==(const CarrotPaymentProposalV1 &a, const CarrotPaymentProposalV1 &b) +{ + return a.destination == b.destination && + a.amount == b.amount && + a.randomness == b.randomness; +} +//------------------------------------------------------------------------------------------------------------------- +bool operator==(const CarrotPaymentProposalSelfSendV1 &a, const CarrotPaymentProposalSelfSendV1 &b) +{ + return a.destination_address_spend_pubkey == b.destination_address_spend_pubkey && + a.amount == b.amount && + a.enote_type == b.enote_type && + a.internal_message == b.internal_message && + 0 == memcmp(&a.enote_ephemeral_pubkey, &b.enote_ephemeral_pubkey, sizeof(mx25519_pubkey)); +} +//------------------------------------------------------------------------------------------------------------------- +crypto::secret_key get_enote_ephemeral_privkey(const CarrotPaymentProposalV1 &proposal, + const input_context_t &input_context) +{ + return get_enote_ephemeral_privkey(proposal.randomness, proposal.destination, input_context); +} +//------------------------------------------------------------------------------------------------------------------- +mx25519_pubkey get_enote_ephemeral_pubkey(const CarrotPaymentProposalV1 &proposal, + const input_context_t &input_context) +{ + return get_enote_ephemeral_pubkey(proposal.randomness, proposal.destination, input_context); +} +//------------------------------------------------------------------------------------------------------------------- +void get_coinbase_enote_v1(const CarrotPaymentProposalV1 &proposal, + const std::uint64_t block_index, + CarrotCoinbaseEnoteV1 &output_enote_out) +{ + // 1. sanity checks + CARROT_CHECK_AND_THROW(proposal.randomness != null_anchor, + missing_randomness, "invalid randomness for janus anchor (zero)."); + CARROT_CHECK_AND_THROW(!proposal.destination.is_subaddress, + bad_address_type, "subaddresses aren't allowed as destinations of coinbase outputs"); + CARROT_CHECK_AND_THROW(proposal.destination.payment_id == null_payment_id, + bad_address_type, "integrated addresses aren't allowed as destinations of coinbase outputs"); + + // 2. coinbase input context + const input_context_t input_context = make_carrot_input_context_coinbase(block_index); + + // 3. make D_e and do external ECDH + mx25519_pubkey s_sender_receiver; auto dhe_wiper = auto_wiper(s_sender_receiver); + get_normal_proposal_ecdh_parts(proposal, + input_context, + output_enote_out.enote_ephemeral_pubkey, + s_sender_receiver); + + // 4. build the output enote address pieces + crypto::hash s_sender_receiver_ctx; auto q_wiper = auto_wiper(s_sender_receiver_ctx); + crypto::secret_key dummy_amount_blinding_factor; + rct::key dummy_amount_commitment; + encrypted_amount_t dummy_encrypted_amount; + encrypted_payment_id_t dummy_encrypted_payment_id; + get_external_output_proposal_parts(s_sender_receiver, + proposal.destination.address_spend_pubkey, + null_payment_id, + proposal.amount, + CarrotEnoteType::PAYMENT, + output_enote_out.enote_ephemeral_pubkey, + input_context, + true, // coinbase_amount_commitment + s_sender_receiver_ctx, + dummy_amount_blinding_factor, + dummy_amount_commitment, + output_enote_out.onetime_address, + dummy_encrypted_amount, + dummy_encrypted_payment_id, + output_enote_out.view_tag); + + // 5. anchor_enc = anchor XOR m_anchor + output_enote_out.anchor_enc = encrypt_carrot_anchor(proposal.randomness, + s_sender_receiver_ctx, + output_enote_out.onetime_address); + + // 6. save the amount and block index + output_enote_out.amount = proposal.amount; + output_enote_out.block_index = block_index; +} +//------------------------------------------------------------------------------------------------------------------- +void get_output_proposal_normal_v1(const CarrotPaymentProposalV1 &proposal, + const crypto::key_image &tx_first_key_image, + RCTOutputEnoteProposal &output_enote_out, + encrypted_payment_id_t &encrypted_payment_id_out) +{ + // 1. sanity checks + CARROT_CHECK_AND_THROW(proposal.randomness != null_anchor, + missing_randomness, "invalid randomness for janus anchor (zero)."); + + // 2. input context: input_context = "R" || KI_1 + const input_context_t input_context = make_carrot_input_context(tx_first_key_image); + + // 3. make D_e and do external ECDH + mx25519_pubkey s_sender_receiver; auto dhe_wiper = auto_wiper(s_sender_receiver); + get_normal_proposal_ecdh_parts(proposal, + input_context, + output_enote_out.enote.enote_ephemeral_pubkey, + s_sender_receiver); + + // 4. build the output enote address pieces + crypto::hash s_sender_receiver_ctx; auto q_wiper = auto_wiper(s_sender_receiver_ctx); + get_external_output_proposal_parts(s_sender_receiver, + proposal.destination.address_spend_pubkey, + proposal.destination.payment_id, + proposal.amount, + CarrotEnoteType::PAYMENT, + output_enote_out.enote.enote_ephemeral_pubkey, + input_context, + false, // coinbase_amount_commitment + s_sender_receiver_ctx, + output_enote_out.amount_blinding_factor, + output_enote_out.enote.amount_commitment, + output_enote_out.enote.onetime_address, + output_enote_out.enote.amount_enc, + encrypted_payment_id_out, + output_enote_out.enote.view_tag); + + // 5. anchor_enc = anchor XOR m_anchor + output_enote_out.enote.anchor_enc = encrypt_carrot_anchor(proposal.randomness, + s_sender_receiver_ctx, + output_enote_out.enote.onetime_address); + + // 6. save the amount and first key image + output_enote_out.amount = proposal.amount; + output_enote_out.enote.tx_first_key_image = tx_first_key_image; +} +//------------------------------------------------------------------------------------------------------------------- +void get_output_proposal_special_v1(const CarrotPaymentProposalSelfSendV1 &proposal, + const view_incoming_key_device &k_view_dev, + const crypto::key_image &tx_first_key_image, + const std::optional &other_enote_ephemeral_pubkey, + RCTOutputEnoteProposal &output_enote_out) +{ + // 1. sanity checks + CARROT_CHECK_AND_THROW(!proposal.internal_message, + component_out_of_order, "internal messages are only for internal selfsends, not special selfsends"); + + // 2. input context: input_context = "R" || KI_1 + const input_context_t input_context = make_carrot_input_context(tx_first_key_image); + + // 3. D_e + const bool missing_enote_ephemeral_pubkeys = !proposal.enote_ephemeral_pubkey && !other_enote_ephemeral_pubkey; + const bool mismatched_enote_ephemeral_pubkeys = proposal.enote_ephemeral_pubkey && + other_enote_ephemeral_pubkey && + memcmp(&*proposal.enote_ephemeral_pubkey, &*other_enote_ephemeral_pubkey, sizeof(mx25519_pubkey)); + CARROT_CHECK_AND_THROW(!missing_enote_ephemeral_pubkeys, + missing_components, "no enote ephemeral pubkey provided"); + CARROT_CHECK_AND_THROW(!mismatched_enote_ephemeral_pubkeys, + component_out_of_order, "mismatched enote ephemeral pubkeys provided"); + const mx25519_pubkey enote_ephemeral_pubkey = proposal.enote_ephemeral_pubkey.value_or( + other_enote_ephemeral_pubkey.value_or(mx25519_pubkey{})); + + // 4. s_sr = k_v D_e + mx25519_pubkey s_sender_receiver; auto ecdh_wiper = auto_wiper(s_sender_receiver); + CARROT_CHECK_AND_THROW(k_view_dev.view_key_scalar_mult_x25519(enote_ephemeral_pubkey, s_sender_receiver), + crypto_function_failed, "HW device failed to perform ECDH with ephemeral pubkey"); + + // 5. build the output enote address pieces + crypto::hash s_sender_receiver_ctx; auto q_wiper = auto_wiper(s_sender_receiver_ctx); + encrypted_payment_id_t dummy_encrypted_payment_id; + get_external_output_proposal_parts(s_sender_receiver, + proposal.destination_address_spend_pubkey, + null_payment_id, + proposal.amount, + proposal.enote_type, + enote_ephemeral_pubkey, + input_context, + false, // coinbase_amount_commitment + s_sender_receiver_ctx, + output_enote_out.amount_blinding_factor, + output_enote_out.enote.amount_commitment, + output_enote_out.enote.onetime_address, + output_enote_out.enote.amount_enc, + dummy_encrypted_payment_id, + output_enote_out.enote.view_tag); + + // 6. make special janus anchor: anchor_sp = H_16[k_v](D_e, input_context, Ko) + janus_anchor_t janus_anchor_special; + k_view_dev.make_janus_anchor_special(enote_ephemeral_pubkey, + input_context, + output_enote_out.enote.onetime_address, + janus_anchor_special); + + // 7. encrypt special anchor: anchor_enc = anchor XOR m_anchor + output_enote_out.enote.anchor_enc = encrypt_carrot_anchor(janus_anchor_special, + s_sender_receiver_ctx, + output_enote_out.enote.onetime_address); + + // 8. save the enote ephemeral pubkey, first tx key image, and amount + output_enote_out.enote.enote_ephemeral_pubkey = enote_ephemeral_pubkey; + output_enote_out.enote.tx_first_key_image = tx_first_key_image; + output_enote_out.amount = proposal.amount; +} +//------------------------------------------------------------------------------------------------------------------- +void get_output_proposal_internal_v1(const CarrotPaymentProposalSelfSendV1 &proposal, + const view_balance_secret_device &s_view_balance_dev, + const crypto::key_image &tx_first_key_image, + const std::optional &other_enote_ephemeral_pubkey, + RCTOutputEnoteProposal &output_enote_out) +{ + // 1. sanity checks + // @TODO + + // 2. input_context = "R" || KI_1 + const input_context_t input_context = make_carrot_input_context(tx_first_key_image); + + // 3. D_e + const bool missing_enote_ephemeral_pubkeys = !proposal.enote_ephemeral_pubkey && !other_enote_ephemeral_pubkey; + const bool mismatched_enote_ephemeral_pubkeys = proposal.enote_ephemeral_pubkey && + other_enote_ephemeral_pubkey && + memcmp(&*proposal.enote_ephemeral_pubkey, &*other_enote_ephemeral_pubkey, sizeof(mx25519_pubkey)); + CARROT_CHECK_AND_THROW(!missing_enote_ephemeral_pubkeys, + missing_components, "no enote ephemeral pubkey provided"); + CARROT_CHECK_AND_THROW(!mismatched_enote_ephemeral_pubkeys, + component_out_of_order, "mismatched enote ephemeral pubkeys provided"); + const mx25519_pubkey enote_ephemeral_pubkey = proposal.enote_ephemeral_pubkey.value_or( + other_enote_ephemeral_pubkey.value_or(mx25519_pubkey{})); + + // 4. s^ctx_sr = H_32[s_vb](D_e, input_context) + crypto::hash s_sender_receiver_ctx; auto q_wiper = auto_wiper(s_sender_receiver_ctx); + s_view_balance_dev.make_internal_sender_receiver_secret(enote_ephemeral_pubkey, + input_context, + s_sender_receiver_ctx); + + // 5. build the output enote address pieces + encrypted_payment_id_t dummy_encrypted_payment_id; + get_output_proposal_parts(s_sender_receiver_ctx, + proposal.destination_address_spend_pubkey, + null_payment_id, + proposal.amount, + proposal.enote_type, + enote_ephemeral_pubkey, + input_context, + false, // coinbase_amount_commitment + output_enote_out.amount_blinding_factor, + output_enote_out.enote.amount_commitment, + output_enote_out.enote.onetime_address, + output_enote_out.enote.amount_enc, + dummy_encrypted_payment_id); + + // 6. vt = H_3[s_vb](input_context || Ko) + s_view_balance_dev.make_internal_view_tag(input_context, + output_enote_out.enote.onetime_address, + output_enote_out.enote.view_tag); + + // 7. anchor = given message OR 0s, if not available + const janus_anchor_t anchor = proposal.internal_message.value_or(janus_anchor_t{}); + + // 8. encrypt anchor: anchor_enc = anchor XOR m_anchor + output_enote_out.enote.anchor_enc = encrypt_carrot_anchor(anchor, + s_sender_receiver_ctx, + output_enote_out.enote.onetime_address); + + // 9. save the enote ephemeral pubkey, first tx key image, and amount + output_enote_out.enote.enote_ephemeral_pubkey = enote_ephemeral_pubkey; + output_enote_out.enote.tx_first_key_image = tx_first_key_image; + output_enote_out.amount = proposal.amount; +} +//------------------------------------------------------------------------------------------------------------------- +CarrotPaymentProposalV1 gen_carrot_payment_proposal_v1(const bool is_subaddress, + const bool has_payment_id, + const rct::xmr_amount amount, + const std::size_t num_random_memo_elements) +{ + CarrotPaymentProposalV1 temp; + + if (is_subaddress) + temp.destination = gen_carrot_subaddress_v1(); + else if (has_payment_id) + temp.destination = gen_carrot_integrated_address_v1(); + else + temp.destination = gen_carrot_main_address_v1(); + + temp.amount = amount; + temp.randomness = gen_janus_anchor(); + + return temp; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace carrot diff --git a/src/carrot_core/payment_proposal.h b/src/carrot_core/payment_proposal.h new file mode 100644 index 00000000000..98f2720531c --- /dev/null +++ b/src/carrot_core/payment_proposal.h @@ -0,0 +1,171 @@ +// Copyright (c) 2024, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// A 'payment proposal' is a proposal to make an enote sending funds to a Carrot address. +// Carrot: Cryptonote Address For Rerandomizable-RingCT-Output Transactions + +#pragma once + +//local headers +#include "carrot_enote_types.h" +#include "destination.h" +#include "device.h" +#include "ringct/rctTypes.h" + +//third party headers + +//standard headers +#include + +//forward declarations + + +namespace carrot +{ + +//// +// CarrotPaymentProposalV1 +// - for creating an output proposal to send an amount to someone +/// +struct CarrotPaymentProposalV1 final +{ + /// user address + CarrotDestinationV1 destination; + /// b + rct::xmr_amount amount; + /// anchor_norm: secret randomness for Janus anchor + janus_anchor_t randomness; +}; + +//// +// CarrotPaymentProposalSelfSendV1 +// - for creating an output proposal to send change to yourself +/// +struct CarrotPaymentProposalSelfSendV1 final +{ + /// one of our own address spend pubkeys: K^j_s + crypto::public_key destination_address_spend_pubkey; + /// a + rct::xmr_amount amount; + + /// enote_type + CarrotEnoteType enote_type; + /// enote ephemeral pubkey: D_e + std::optional enote_ephemeral_pubkey; + /// anchor: arbitrary, pre-encrypted message for _internal_ selfsends + std::optional internal_message; +}; + +struct RCTOutputEnoteProposal final +{ + CarrotEnoteV1 enote; + + // we need this opening information to make amount range proofs + rct::xmr_amount amount; + crypto::secret_key amount_blinding_factor; +}; + +/// equality operators +bool operator==(const CarrotPaymentProposalV1 &a, const CarrotPaymentProposalV1 &b); +/// equality operators +bool operator==(const CarrotPaymentProposalSelfSendV1 &a, const CarrotPaymentProposalSelfSendV1 &b); + +/** +* brief: get_enote_ephemeral_privkey - get the proposal's enote ephemeral privkey d_e +* param: proposal - +* param: input_context - +* return: d_e +*/ +crypto::secret_key get_enote_ephemeral_privkey(const CarrotPaymentProposalV1 &proposal, + const input_context_t &input_context); +/** +* brief: get_enote_ephemeral_pubkey - get the proposal's enote ephemeral pubkey D_e +* param: proposal - +* param: input_context - +* return: D_e +*/ +mx25519_pubkey get_enote_ephemeral_pubkey(const CarrotPaymentProposalV1 &proposal, + const input_context_t &input_context); +/** +* brief: get_coinbase_enote_v1 - convert the carrot proposal to a coinbase output enote +* param: proposal - +* param: block_index - index of the coinbase tx's block +* outparam: output_enote_out - +*/ +void get_coinbase_enote_v1(const CarrotPaymentProposalV1 &proposal, + const std::uint64_t block_index, + CarrotCoinbaseEnoteV1 &output_enote_out); +/** +* brief: get_output_proposal_normal_v1 - convert the carrot proposal to an output proposal +* param: proposal - +* param: tx_first_key_image - +* outparam: output_enote_out - +* outparam: encrypted_payment_id_out - pid_enc +*/ +void get_output_proposal_normal_v1(const CarrotPaymentProposalV1 &proposal, + const crypto::key_image &tx_first_key_image, + RCTOutputEnoteProposal &output_enote_out, + encrypted_payment_id_t &encrypted_payment_id_out); +/** +* brief: get_output_proposal_special_v1 - convert the carrot proposal to an output proposal (external selfsend) +* param: proposal - +* param: k_view_dev - +* param: tx_first_key_image - +* param: other_enote_ephemeral_pubkey - +* outparam: output_enote_out - +*/ +void get_output_proposal_special_v1(const CarrotPaymentProposalSelfSendV1 &proposal, + const view_incoming_key_device &k_view_dev, + const crypto::key_image &tx_first_key_image, + const std::optional &other_enote_ephemeral_pubkey, + RCTOutputEnoteProposal &output_enote_out); +/** +* brief: get_output_proposal_internal_v1 - convert the carrot proposal to an output proposal (internal) +* param: proposal - +* param: s_view_balance_dev - +* param: tx_first_key_image - +* param: other_enote_ephemeral_pubkey - +* outparam: output_enote_out - +*/ +void get_output_proposal_internal_v1(const CarrotPaymentProposalSelfSendV1 &proposal, + const view_balance_secret_device &s_view_balance_dev, + const crypto::key_image &tx_first_key_image, + const std::optional &other_enote_ephemeral_pubkey, + RCTOutputEnoteProposal &output_enote_out); +/** +* brief: gen_carrot_payment_proposal_v1 - generate a random proposal +* param: is_subaddress - whether to generate a proposal to subaddress +* param: has_payment_id - true to generate non-zero payment ID, false for null payment ID +* param: amount - +* return: a random proposal +*/ +CarrotPaymentProposalV1 gen_carrot_payment_proposal_v1(const bool is_subaddress, + const bool has_payment_id, + const rct::xmr_amount amount); + +} //namespace carrot diff --git a/src/carrot_core/scan.cpp b/src/carrot_core/scan.cpp new file mode 100644 index 00000000000..0a4206102e2 --- /dev/null +++ b/src/carrot_core/scan.cpp @@ -0,0 +1,403 @@ +// Copyright (c) 2025, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// Utilities for scanning carrot enotes + +//paired header +#include "scan.h" + +//local headers +#include "destination.h" +#include "enote_utils.h" +#include "ringct/rctOps.h" +#include "scan_unsafe.h" + +//third party headers + +//standard headers + + +namespace carrot +{ +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool is_main_address_spend_pubkey(const crypto::public_key &address_spend_pubkey, + const epee::span main_address_spend_pubkeys) +{ + for (const crypto::public_key &main_address_spend_pubkey : main_address_spend_pubkeys) + if (address_spend_pubkey == main_address_spend_pubkey) + return true; + return false; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static crypto::secret_key get_enote_ephemeral_privkey_sender(const janus_anchor_t &anchor_norm, + const CarrotDestinationV1 &destination, + const input_context_t &input_context) +{ + // d_e = H_n(anchor_norm, input_context, K^j_s, pid) + crypto::secret_key enote_ephemeral_privkey; + make_carrot_enote_ephemeral_privkey(anchor_norm, + input_context, + destination.address_spend_pubkey, + destination.payment_id, + enote_ephemeral_privkey); + return enote_ephemeral_privkey; +} +//------------------------------------------------------------------------------------------------------------------- +static bool try_scan_carrot_coinbase_enote_checked( + const CarrotCoinbaseEnoteV1 &enote, + const mx25519_pubkey &s_sender_receiver, + const epee::span main_address_spend_pubkeys, + crypto::secret_key &sender_extension_g_out, + crypto::secret_key &sender_extension_t_out, + crypto::public_key &address_spend_pubkey_out) +{ + // k^g_o, k^g_t, K^j_s, pid, anchor + janus_anchor_t nominal_janus_anchor; + if (!try_scan_carrot_coinbase_enote_no_janus(enote, + s_sender_receiver, + main_address_spend_pubkeys, + sender_extension_g_out, + sender_extension_t_out, + address_spend_pubkey_out, + nominal_janus_anchor)) + return false; + + return verify_carrot_normal_janus_protection(nominal_janus_anchor, + make_carrot_input_context_coinbase(enote.block_index), + address_spend_pubkey_out, + /*is_subaddress=*/false, + null_payment_id, + enote.enote_ephemeral_pubkey); +} +//------------------------------------------------------------------------------------------------------------------- +static bool try_scan_carrot_enote_external_normal_checked(const CarrotEnoteV1 &enote, + const std::optional &encrypted_payment_id, + const mx25519_pubkey &s_sender_receiver, + const epee::span main_address_spend_pubkeys, + crypto::secret_key &sender_extension_g_out, + crypto::secret_key &sender_extension_t_out, + crypto::public_key &address_spend_pubkey_out, + rct::xmr_amount &amount_out, + crypto::secret_key &amount_blinding_factor_out, + payment_id_t &payment_id_out, + CarrotEnoteType &enote_type_out, + janus_anchor_t &nominal_janus_anchor_out, + bool &verified_normal_janus) +{ + if (!try_scan_carrot_enote_external_no_janus(enote, + encrypted_payment_id, + s_sender_receiver, + sender_extension_g_out, + sender_extension_t_out, + address_spend_pubkey_out, + amount_out, + amount_blinding_factor_out, + payment_id_out, + enote_type_out, + nominal_janus_anchor_out)) + return false; + + verified_normal_janus = verify_carrot_normal_janus_protection( + make_carrot_input_context(enote.tx_first_key_image), + address_spend_pubkey_out, + !is_main_address_spend_pubkey(address_spend_pubkey_out, main_address_spend_pubkeys), + enote.enote_ephemeral_pubkey, + nominal_janus_anchor_out, + payment_id_out); + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool try_make_carrot_shared_key_receiver( + const view_incoming_key_device &k_view_dev, + const mx25519_pubkey &enote_ephemeral_pubkey, + mx25519_pubkey &s_sender_receiver_out) +{ + return k_view_dev.view_key_scalar_mult_x25519(enote_ephemeral_pubkey, s_sender_receiver_out); +} +//------------------------------------------------------------------------------------------------------------------- +bool try_scan_carrot_coinbase_enote_sender( + const CarrotCoinbaseEnoteV1 &enote, + const CarrotDestinationV1 &destination, + const janus_anchor_t &anchor_norm, + crypto::secret_key &sender_extension_g_out, + crypto::secret_key &sender_extension_t_out) +{ + const crypto::secret_key enote_ephemeral_privkey = get_enote_ephemeral_privkey_sender(anchor_norm, + destination, + make_carrot_input_context_coinbase(enote.block_index)); + + return try_scan_carrot_coinbase_enote_sender(enote, + destination, + enote_ephemeral_privkey, + sender_extension_g_out, + sender_extension_t_out); +} +//------------------------------------------------------------------------------------------------------------------- +bool try_scan_carrot_coinbase_enote_sender( + const CarrotCoinbaseEnoteV1 &enote, + const CarrotDestinationV1 &destination, + const crypto::secret_key &enote_ephemeral_privkey, + crypto::secret_key &sender_extension_g_out, + crypto::secret_key &sender_extension_t_out) +{ + // s_sr = d_e ConvertPointE(K^j_v) + mx25519_pubkey s_sender_receiver; + if (!try_make_carrot_shared_key_sender(enote_ephemeral_privkey, + destination.address_view_pubkey, + s_sender_receiver)) + return false; + + crypto::public_key dummy_main_address_spend_pubkey; + if (!try_scan_carrot_coinbase_enote_checked(enote, + s_sender_receiver, + {&destination.address_spend_pubkey, 1}, + sender_extension_g_out, + sender_extension_t_out, + dummy_main_address_spend_pubkey)) + return false; + + // this should've already been checked, but just for good measure... + return dummy_main_address_spend_pubkey == destination.address_spend_pubkey; +} +//------------------------------------------------------------------------------------------------------------------- +bool try_scan_carrot_coinbase_enote_receiver( + const CarrotCoinbaseEnoteV1 &enote, + const mx25519_pubkey &s_sender_receiver, + const epee::span main_address_spend_pubkeys, + crypto::secret_key &sender_extension_g_out, + crypto::secret_key &sender_extension_t_out, + crypto::public_key &main_address_spend_pubkey_out) +{ + return try_scan_carrot_coinbase_enote_checked(enote, + s_sender_receiver, + main_address_spend_pubkeys, + sender_extension_g_out, + sender_extension_t_out, + main_address_spend_pubkey_out); +} +//------------------------------------------------------------------------------------------------------------------- +bool try_scan_carrot_coinbase_enote_receiver( + const CarrotCoinbaseEnoteV1 &enote, + const mx25519_pubkey &s_sender_receiver, + const crypto::public_key &main_address_spend_pubkey, + crypto::secret_key &sender_extension_g_out, + crypto::secret_key &sender_extension_t_out) +{ + crypto::public_key dummy_main_address_spend_pubkey; + return try_scan_carrot_coinbase_enote_receiver( + enote, + s_sender_receiver, + {&main_address_spend_pubkey, 1}, + sender_extension_g_out, + sender_extension_t_out, + dummy_main_address_spend_pubkey); +} +//------------------------------------------------------------------------------------------------------------------- +bool try_scan_carrot_enote_external_sender(const CarrotEnoteV1 &enote, + const std::optional &encrypted_payment_id, + const CarrotDestinationV1 &destination, + const janus_anchor_t &anchor_norm, + crypto::secret_key &sender_extension_g_out, + crypto::secret_key &sender_extension_t_out, + rct::xmr_amount &amount_out, + crypto::secret_key &amount_blinding_factor_out, + CarrotEnoteType &enote_type_out, + const bool check_pid) +{ + const crypto::secret_key enote_ephemeral_privkey = get_enote_ephemeral_privkey_sender(anchor_norm, + destination, + make_carrot_input_context(enote.tx_first_key_image)); + + return try_scan_carrot_enote_external_sender(enote, + encrypted_payment_id, + destination, + enote_ephemeral_privkey, + sender_extension_g_out, + sender_extension_t_out, + amount_out, + amount_blinding_factor_out, + enote_type_out, + check_pid); +} +//------------------------------------------------------------------------------------------------------------------- +bool try_scan_carrot_enote_external_sender(const CarrotEnoteV1 &enote, + const std::optional &encrypted_payment_id, + const CarrotDestinationV1 &destination, + const crypto::secret_key &enote_ephemeral_privkey, + crypto::secret_key &sender_extension_g_out, + crypto::secret_key &sender_extension_t_out, + rct::xmr_amount &amount_out, + crypto::secret_key &amount_blinding_factor_out, + CarrotEnoteType &enote_type_out, + const bool check_pid) +{ + // s_sr = d_e ConvertPointE(K^j_v) + mx25519_pubkey s_sender_receiver; + if (!try_make_carrot_shared_key_sender(enote_ephemeral_privkey, + destination.address_view_pubkey, + s_sender_receiver)) + return false; + + return try_scan_carrot_enote_external_sender(enote, + encrypted_payment_id, + destination, + s_sender_receiver, + sender_extension_g_out, + sender_extension_t_out, + amount_out, + amount_blinding_factor_out, + enote_type_out, + check_pid); +} +//------------------------------------------------------------------------------------------------------------------- +bool try_scan_carrot_enote_external_sender(const CarrotEnoteV1 &enote, + const std::optional &encrypted_payment_id, + const CarrotDestinationV1 &destination, + const mx25519_pubkey &s_sender_receiver, + crypto::secret_key &sender_extension_g_out, + crypto::secret_key &sender_extension_t_out, + rct::xmr_amount &amount_out, + crypto::secret_key &amount_blinding_factor_out, + CarrotEnoteType &enote_type_out, + const bool check_pid) +{ + crypto::public_key recovered_address_spend_pubkey; + payment_id_t recovered_payment_id; + CarrotEnoteType recovered_enote_type; + janus_anchor_t dummy_janus_anchor; + bool verified_normal_janus = false; + if (!try_scan_carrot_enote_external_normal_checked(enote, + encrypted_payment_id, + s_sender_receiver, + {&destination.address_spend_pubkey, 1}, + sender_extension_g_out, + sender_extension_t_out, + recovered_address_spend_pubkey, + amount_out, + amount_blinding_factor_out, + recovered_payment_id, + recovered_enote_type, + dummy_janus_anchor, + verified_normal_janus)) + return false; + else if (!verified_normal_janus) + return false; + else if (recovered_address_spend_pubkey != destination.address_spend_pubkey) + return false; + else if (check_pid && recovered_payment_id != destination.payment_id) + return false; + else if (recovered_enote_type != CarrotEnoteType::PAYMENT) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool try_scan_carrot_enote_external_receiver(const CarrotEnoteV1 &enote, + const std::optional &encrypted_payment_id, + const mx25519_pubkey &s_sender_receiver, + const epee::span main_address_spend_pubkeys, + const view_incoming_key_device &k_view_dev, + crypto::secret_key &sender_extension_g_out, + crypto::secret_key &sender_extension_t_out, + crypto::public_key &address_spend_pubkey_out, + rct::xmr_amount &amount_out, + crypto::secret_key &amount_blinding_factor_out, + payment_id_t &payment_id_out, + CarrotEnoteType &enote_type_out) +{ + janus_anchor_t nominal_janus_anchor; + bool verified_normal_janus = false; + if (!try_scan_carrot_enote_external_normal_checked(enote, + encrypted_payment_id, + s_sender_receiver, + main_address_spend_pubkeys, + sender_extension_g_out, + sender_extension_t_out, + address_spend_pubkey_out, + amount_out, + amount_blinding_factor_out, + payment_id_out, + enote_type_out, + nominal_janus_anchor, + verified_normal_janus)) + return false; + + if (!verified_normal_janus && !verify_carrot_special_janus_protection(enote.tx_first_key_image, + enote.enote_ephemeral_pubkey, + enote.onetime_address, + k_view_dev, + nominal_janus_anchor)) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool try_scan_carrot_enote_internal_receiver(const CarrotEnoteV1 &enote, + const view_balance_secret_device &s_view_balance_dev, + crypto::secret_key &sender_extension_g_out, + crypto::secret_key &sender_extension_t_out, + crypto::public_key &address_spend_pubkey_out, + rct::xmr_amount &amount_out, + crypto::secret_key &amount_blinding_factor_out, + CarrotEnoteType &enote_type_out, + janus_anchor_t &internal_message_out) +{ + // input_context + const input_context_t input_context = make_carrot_input_context(enote.tx_first_key_image); + + // vt = H_3[s_vb](input_context || Ko) + view_tag_t nominal_view_tag; + s_view_balance_dev.make_internal_view_tag(input_context, enote.onetime_address, nominal_view_tag); + + // test view tag + if (nominal_view_tag != enote.view_tag) + return false; + + // s^ctx_sr = H_32[s_vb](D_e, input_context) + crypto::hash s_sender_receiver_ctx; + s_view_balance_dev.make_internal_sender_receiver_secret(enote.enote_ephemeral_pubkey, + input_context, + s_sender_receiver_ctx); + + return try_scan_carrot_enote_internal_burnt(enote, + s_sender_receiver_ctx, + sender_extension_g_out, + sender_extension_t_out, + address_spend_pubkey_out, + amount_out, + amount_blinding_factor_out, + enote_type_out, + internal_message_out); + + // janus protection checks are not needed for internal scans +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace carrot diff --git a/src/carrot_core/scan.h b/src/carrot_core/scan.h new file mode 100644 index 00000000000..5c6b1fbd762 --- /dev/null +++ b/src/carrot_core/scan.h @@ -0,0 +1,191 @@ +// Copyright (c) 2025, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//! @file Utilities for scanning carrot enotes + +/** + * The "scan process" mentioned in this file is described in Section 8.1 "Enote Scan" of the Carrot specification: + * https://github.com/jeffro256/carrot/blob/master/carrot.md#81-enote-scan + */ + +#pragma once + +//local headers +#include "carrot_enote_types.h" +#include "device.h" +#include "span.h" + +//third party headers + +//standard headers +#include + +//forward declarations +namespace carrot { struct CarrotDestinationV1; } + + +namespace carrot +{ +/** + * brief: try_make_carrot_shared_key_receiver - perform the receiver-side ECDH exchange for Carrot enotes + * s_sr = k_v D_e + * param: k_view_dev - + * param: enote_ephemeral_pubkey - D_e + * outparam: s_sender_receiver_out - s_sr + * return: true if successful, false if a failure occurred in point decompression + */ +bool try_make_carrot_shared_key_receiver( + const view_incoming_key_device &k_view_dev, + const mx25519_pubkey &enote_ephemeral_pubkey, + mx25519_pubkey &s_sender_receiver_out); +/** + * brief: try_scan_carrot_coinbase_enote_[sender/receiver] - attempt scan process on coinbase enote + * param: enote - + * param: destination - (is_subaddress, K^j_s, K^j_v, pid) + * param: anchor_norm - anchor_norm + * param: enote_ephemeral_privkey - d_e + * param: s_sender_receiver - s_sr + * param: main_address_spend_pubkeys - {K^0_s, ...} + * outparam: sender_extension_g_out - k^g_o + * outparam: sender_extension_t_out - k^t_o + * outparam: main_address_spend_pubkey_out - K^0_s, which will be one of main_address_spend_pubkeys + * return: true iff the scan process succeeded + */ +bool try_scan_carrot_coinbase_enote_sender( + const CarrotCoinbaseEnoteV1 &enote, + const CarrotDestinationV1 &destination, + const janus_anchor_t &anchor_norm, + crypto::secret_key &sender_extension_g_out, + crypto::secret_key &sender_extension_t_out); +bool try_scan_carrot_coinbase_enote_sender( + const CarrotCoinbaseEnoteV1 &enote, + const CarrotDestinationV1 &destination, + const crypto::secret_key &enote_ephemeral_privkey, + crypto::secret_key &sender_extension_g_out, + crypto::secret_key &sender_extension_t_out); +bool try_scan_carrot_coinbase_enote_receiver( + const CarrotCoinbaseEnoteV1 &enote, + const mx25519_pubkey &s_sender_receiver, + const epee::span main_address_spend_pubkeys, + crypto::secret_key &sender_extension_g_out, + crypto::secret_key &sender_extension_t_out, + crypto::public_key &main_address_spend_pubkey_out); +bool try_scan_carrot_coinbase_enote_receiver( + const CarrotCoinbaseEnoteV1 &enote, + const mx25519_pubkey &s_sender_receiver, + const crypto::public_key &main_address_spend_pubkey, + crypto::secret_key &sender_extension_g_out, + crypto::secret_key &sender_extension_t_out); +/** + * brief: try_scan_carrot_enote_external_[sender/receiver] - attempt scan process on external enote + * param: enote - + * param: encrypted_payment_id - pid_enc + * param: destination - (is_subaddress, K^j_s, K^j_v, pid) + * param: anchor_norm - anchor_norm + * param: enote_ephemeral_privkey - d_e + * param: s_sender_receiver - s_sr + * param: main_address_spend_pubkeys - {K^0_s, ...} + * param: k_view_dev - + * outparam: sender_extension_g_out - k^g_o + * outparam: sender_extension_t_out - k^t_o + * outparam: address_spend_pubkey_out - K^j_s + * outparam: amount_out - a + * outparam: amount_blinding_factor_out - k_a + * outparam: payment_id_out - pid + * outparam: enote_type_out - enote_type + * return: true iff the scan process succeeded + * + * warning: the _sender overloads cannot check for Janus protection on special enotes + */ +bool try_scan_carrot_enote_external_sender(const CarrotEnoteV1 &enote, + const std::optional &encrypted_payment_id, + const CarrotDestinationV1 &destination, + const janus_anchor_t &anchor_norm, + crypto::secret_key &sender_extension_g_out, + crypto::secret_key &sender_extension_t_out, + rct::xmr_amount &amount_out, + crypto::secret_key &amount_blinding_factor_out, + CarrotEnoteType &enote_type_out, + const bool check_pid = true); +bool try_scan_carrot_enote_external_sender(const CarrotEnoteV1 &enote, + const std::optional &encrypted_payment_id, + const CarrotDestinationV1 &destination, + const crypto::secret_key &enote_ephemeral_privkey, + crypto::secret_key &sender_extension_g_out, + crypto::secret_key &sender_extension_t_out, + rct::xmr_amount &amount_out, + crypto::secret_key &amount_blinding_factor_out, + CarrotEnoteType &enote_type_out, + const bool check_pid = true); +bool try_scan_carrot_enote_external_sender(const CarrotEnoteV1 &enote, + const std::optional &encrypted_payment_id, + const CarrotDestinationV1 &destination, + const mx25519_pubkey &s_sender_receiver, + crypto::secret_key &sender_extension_g_out, + crypto::secret_key &sender_extension_t_out, + rct::xmr_amount &amount_out, + crypto::secret_key &amount_blinding_factor_out, + CarrotEnoteType &enote_type_out, + const bool check_pid = true); +bool try_scan_carrot_enote_external_receiver(const CarrotEnoteV1 &enote, + const std::optional &encrypted_payment_id, + const mx25519_pubkey &s_sender_receiver, + const epee::span main_address_spend_pubkeys, + const view_incoming_key_device &k_view_dev, + crypto::secret_key &sender_extension_g_out, + crypto::secret_key &sender_extension_t_out, + crypto::public_key &address_spend_pubkey_out, + rct::xmr_amount &amount_out, + crypto::secret_key &amount_blinding_factor_out, + payment_id_t &payment_id_out, + CarrotEnoteType &enote_type_out); +/** + * brief: try_scan_carrot_enote_internal_receiver - attempt scan process on internal enote + * param: enote - + * param: s_view_balance_dev - + * outparam: sender_extension_g_out - k^g_o + * outparam: sender_extension_t_out - k^t_o + * outparam: address_spend_pubkey_out - K^j_s + * outparam: amount_out - a + * outparam: amount_blinding_factor_out - k_a + * outparam: enote_type_out - enote_type + * outparam: internal_message_out - anchor' + * return: true iff the scan process succeeded + */ +bool try_scan_carrot_enote_internal_receiver(const CarrotEnoteV1 &enote, + const view_balance_secret_device &s_view_balance_dev, + crypto::secret_key &sender_extension_g_out, + crypto::secret_key &sender_extension_t_out, + crypto::public_key &address_spend_pubkey_out, + rct::xmr_amount &amount_out, + crypto::secret_key &amount_blinding_factor_out, + CarrotEnoteType &enote_type_out, + janus_anchor_t &internal_message_out); +//! @TODO: try_scan_carrot_enote_internal_sender(): can't validate burning w/o passing s_sr = s_vb + +} //namespace carrot diff --git a/src/carrot_core/scan_unsafe.cpp b/src/carrot_core/scan_unsafe.cpp new file mode 100644 index 00000000000..e04a78cf7ea --- /dev/null +++ b/src/carrot_core/scan_unsafe.cpp @@ -0,0 +1,272 @@ +// Copyright (c) 2025, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// Utilities for scanning carrot enotes + +//paired header +#include "scan_unsafe.h" + +//local headers +#include "enote_utils.h" +#include "ringct/rctOps.h" + +//third party headers + +//standard headers + + +namespace carrot +{ +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool scan_non_coinbase_info(const CarrotEnoteV1 &enote, + const std::optional &encrypted_payment_id, + const crypto::hash &s_sender_receiver_ctx, + crypto::secret_key &sender_extension_g_out, + crypto::secret_key &sender_extension_t_out, + crypto::public_key &address_spend_pubkey_out, + payment_id_t &nominal_payment_id_out, + janus_anchor_t &nominal_janus_anchor_out, + CarrotEnoteType &enote_type_out, + rct::xmr_amount &amount_out, + crypto::secret_key &amount_blinding_factor_out) +{ + // k^o_g = H_n[s^ctx_sr]("..g..", C_a) + make_carrot_sender_extension_g(s_sender_receiver_ctx, + enote.amount_commitment, + sender_extension_g_out); + + // k^o_t = H_n[s^ctx_sr]("..t..", C_a) + make_carrot_sender_extension_t(s_sender_receiver_ctx, + enote.amount_commitment, + sender_extension_t_out); + + // K^j_s = Ko - K^o_ext = Ko - (k^o_g G + k^o_t T) + recover_address_spend_pubkey(enote.onetime_address, + sender_extension_g_out, + sender_extension_t_out, + address_spend_pubkey_out); + + // pid = pid_enc XOR m_pid, if applicable + if (encrypted_payment_id) + nominal_payment_id_out = decrypt_legacy_payment_id(*encrypted_payment_id, + s_sender_receiver_ctx, + enote.onetime_address); + else + nominal_payment_id_out = null_payment_id; + + // anchor = anchor_enc XOR m_anchor + nominal_janus_anchor_out = decrypt_carrot_anchor(enote.anchor_enc, + s_sender_receiver_ctx, + enote.onetime_address); + + return try_get_carrot_amount(s_sender_receiver_ctx, + enote.amount_enc, + enote.onetime_address, + address_spend_pubkey_out, + enote.amount_commitment, + enote_type_out, + amount_out, + amount_blinding_factor_out); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +bool try_scan_carrot_coinbase_enote_no_janus( + const CarrotCoinbaseEnoteV1 &enote, + const mx25519_pubkey &s_sender_receiver, + const epee::span main_address_spend_pubkeys, + crypto::secret_key &sender_extension_g_out, + crypto::secret_key &sender_extension_t_out, + crypto::public_key &nominal_address_spend_pubkey_out, + janus_anchor_t &nominal_janus_anchor_out) +{ + // input_context + const input_context_t input_context = make_carrot_input_context_coinbase(enote.block_index); + + // if vt' != vt, then FAIL + if (!test_carrot_view_tag(s_sender_receiver.data, input_context, enote.onetime_address, enote.view_tag)) + return false; + + // s^ctx_sr = H_32[s_sr](D_e, input_context) + crypto::hash s_sender_receiver_ctx; + make_carrot_contextualized_sender_receiver_secret(s_sender_receiver.data, + enote.enote_ephemeral_pubkey, + input_context, + s_sender_receiver_ctx); + + bool recovered_main_pubkey = false; + for (const crypto::public_key &main_address_spend_pubkey : main_address_spend_pubkeys) + { + // k^o_g = H_n[s^ctx_sr]("..g..", a, K^0_s) + make_carrot_sender_extension_g_coinbase(s_sender_receiver_ctx, + enote.amount, + main_address_spend_pubkey, + sender_extension_g_out); + + // k^o_t = H_n[s^ctx_sr]("..t..", a, K^0_s) + make_carrot_sender_extension_t_coinbase(s_sender_receiver_ctx, + enote.amount, + main_address_spend_pubkey, + sender_extension_t_out); + + // K^j_s = Ko - K^o_ext = Ko - (k^o_g G + k^o_t T) + recover_address_spend_pubkey(enote.onetime_address, + sender_extension_g_out, + sender_extension_t_out, + nominal_address_spend_pubkey_out); + + if (nominal_address_spend_pubkey_out == main_address_spend_pubkey) + { + recovered_main_pubkey = true; + break; + } + } + + if (!recovered_main_pubkey) + return false; + + // anchor = anchor_enc XOR m_anchor + nominal_janus_anchor_out = decrypt_carrot_anchor(enote.anchor_enc, + s_sender_receiver_ctx, + enote.onetime_address); + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool try_scan_carrot_enote_external_no_janus(const CarrotEnoteV1 &enote, + const std::optional &encrypted_payment_id, + const mx25519_pubkey &s_sender_receiver, + crypto::secret_key &sender_extension_g_out, + crypto::secret_key &sender_extension_t_out, + crypto::public_key &address_spend_pubkey_out, + rct::xmr_amount &amount_out, + crypto::secret_key &amount_blinding_factor_out, + payment_id_t &nominal_payment_id_out, + CarrotEnoteType &enote_type_out, + janus_anchor_t &nominal_janus_anchor_out) +{ + // input_context + const input_context_t input_context = make_carrot_input_context(enote.tx_first_key_image); + + // if vt' != vt, then FAIL + if (!test_carrot_view_tag(s_sender_receiver.data, input_context, enote.onetime_address, enote.view_tag)) + return false; + + // s^ctx_sr = H_32[s_sr](D_e, input_context) + crypto::hash s_sender_receiver_ctx; + make_carrot_contextualized_sender_receiver_secret(s_sender_receiver.data, + enote.enote_ephemeral_pubkey, + input_context, + s_sender_receiver_ctx); + + // k^g_o, k^g_t, K^j_s, pid, a, z + return scan_non_coinbase_info(enote, + encrypted_payment_id, + s_sender_receiver_ctx, + sender_extension_g_out, + sender_extension_t_out, + address_spend_pubkey_out, + nominal_payment_id_out, + nominal_janus_anchor_out, + enote_type_out, + amount_out, + amount_blinding_factor_out); +} +//------------------------------------------------------------------------------------------------------------------- +bool try_scan_carrot_enote_internal_burnt(const CarrotEnoteV1 &enote, + const crypto::hash &s_sender_receiver_ctx, + crypto::secret_key &sender_extension_g_out, + crypto::secret_key &sender_extension_t_out, + crypto::public_key &address_spend_pubkey_out, + rct::xmr_amount &amount_out, + crypto::secret_key &amount_blinding_factor_out, + CarrotEnoteType &enote_type_out, + janus_anchor_t &internal_message_out) +{ + // k^g_o, k^t_o, K^j_s', pid', anchor', a, z + payment_id_t dummy_payment_id; + return scan_non_coinbase_info(enote, + std::nullopt, + s_sender_receiver_ctx, + sender_extension_g_out, + sender_extension_t_out, + address_spend_pubkey_out, + dummy_payment_id, + internal_message_out, + enote_type_out, + amount_out, + amount_blinding_factor_out); +} +//------------------------------------------------------------------------------------------------------------------- +bool verify_carrot_normal_janus_protection(const input_context_t &input_context, + const crypto::public_key &nominal_address_spend_pubkey, + const bool is_subaddress, + const mx25519_pubkey &enote_ephemeral_pubkey, + const janus_anchor_t &nominal_janus_anchor, + payment_id_t &nominal_payment_id_inout) +{ + // if can recompute D_e with pid', then PASS + if (verify_carrot_normal_janus_protection(nominal_janus_anchor, + input_context, + nominal_address_spend_pubkey, + is_subaddress, + nominal_payment_id_inout, + enote_ephemeral_pubkey)) + return true; + + // if can recompute D_e with null pid, then PASS + nominal_payment_id_inout = null_payment_id; + return verify_carrot_normal_janus_protection(nominal_janus_anchor, + input_context, + nominal_address_spend_pubkey, + is_subaddress, + nominal_payment_id_inout, + enote_ephemeral_pubkey); +} +//------------------------------------------------------------------------------------------------------------------- +bool verify_carrot_special_janus_protection(const crypto::key_image &tx_first_key_image, + const mx25519_pubkey &enote_ephemeral_pubkey, + const crypto::public_key &onetime_address, + const view_incoming_key_device &k_view_dev, + const janus_anchor_t &nominal_janus_anchor) +{ + // input_context = "R" || KI_1 + const input_context_t input_context = make_carrot_input_context(tx_first_key_image); + + // anchor_sp = H_16[k_v](D_e, input_context, Ko) + janus_anchor_t expected_special_anchor; + k_view_dev.make_janus_anchor_special(enote_ephemeral_pubkey, + input_context, + onetime_address, + expected_special_anchor); + + // attempt special janus check: anchor_sp ?= anchor' + return expected_special_anchor == nominal_janus_anchor; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace carrot diff --git a/src/carrot_core/scan_unsafe.h b/src/carrot_core/scan_unsafe.h new file mode 100644 index 00000000000..68e7b821621 --- /dev/null +++ b/src/carrot_core/scan_unsafe.h @@ -0,0 +1,159 @@ +// Copyright (c) 2025, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//! @file Low-level helper utilities for scanning carrot enotes + +/** + * These functions should not be used directly unless you know what you're doing. Using these + * functions incorrectly can result in Janus attacks, incorrect PIDs, and view tag skips. For + * safer high-level scanning functions, see the scan.h header. + */ + +#pragma once + +//local headers +#include "carrot_enote_types.h" +#include "device.h" +#include "span.h" + +//third party headers + +//standard headers +#include + +//forward declarations + + +namespace carrot +{ +/** + * brief: try_scan_carrot_coinbase_enote_no_janus - attempt scan process on coinbase enote w/o Janus protection + * param: enote - + * param: s_sender_receiver - s_sr + * param: main_address_spend_pubkeys - {K^0_s, ...} + * outparam: sender_extension_g_out - k^g_o + * outparam: sender_extension_t_out - k^t_o + * outparam: nominal_address_spend_pubkey - K^j_s' + * outparam: nominal_janus_anchor_out - anchor' + * return: true iff the scan process (w/o Janus checks) succeeded + */ +bool try_scan_carrot_coinbase_enote_no_janus( + const CarrotCoinbaseEnoteV1 &enote, + const mx25519_pubkey &s_sender_receiver, + const epee::span main_address_spend_pubkeys, + crypto::secret_key &sender_extension_g_out, + crypto::secret_key &sender_extension_t_out, + crypto::public_key &nominal_address_spend_pubkey_out, + janus_anchor_t &nominal_janus_anchor_out); +/** + * brief: try_scan_carrot_enote_external_no_janus - attempt scan process on external enote, + * w/o Janus protection nor correct PID + * param: enote - + * param: encrypted_payment_id - pid_enc + * param: s_sender_receiver - s_sr + * outparam: sender_extension_g_out - k^g_o + * outparam: sender_extension_t_out - k^t_o + * outparam: address_spend_pubkey_out - K^j_s + * outparam: amount_out - a + * outparam: amount_blinding_factor_out - k_a + * outparam: nominal_payment_id_out - pid + * outparam: enote_type_out - enote_type + * outparam: nominal_janus_anchor_out - anchor' + * return: true iff the scan process (w/o Janus or PID checks) succeeded + */ +bool try_scan_carrot_enote_external_no_janus(const CarrotEnoteV1 &enote, + const std::optional &encrypted_payment_id, + const mx25519_pubkey &s_sender_receiver, + crypto::secret_key &sender_extension_g_out, + crypto::secret_key &sender_extension_t_out, + crypto::public_key &address_spend_pubkey_out, + rct::xmr_amount &amount_out, + crypto::secret_key &amount_blinding_factor_out, + payment_id_t &nominal_payment_id_out, + CarrotEnoteType &enote_type_out, + janus_anchor_t &nominal_janus_anchor_out); +/** + * brief: try_scan_carrot_enote_internal_burnt - attempt scan process on internal enote w/o + * input_context burning bug check or view tag check + * param: enote - + * param: s_sender_receiver_ctx - s^ctx_sr, SHOULD BE function of its respective input_context + * outparam: sender_extension_g_out - k^g_o + * outparam: sender_extension_t_out - k^t_o + * outparam: address_spend_pubkey_out - K^j_s + * outparam: amount_out - a + * outparam: amount_blinding_factor_out - k_a + * outparam: enote_type_out - enote_type + * outparam: internal_message_out - anchor' + * return: true iff the scan process (w/o view tag check) succeeded + * + * The amount commitment recompute burning bug *is* checked, but `s^ctx_sr` + * cannot be validated as being a function of the `input_context`, so that + * burning bug check is not done. The view tag is not checked either. + */ +bool try_scan_carrot_enote_internal_burnt(const CarrotEnoteV1 &enote, + const crypto::hash &s_sender_receiver_ctx, + crypto::secret_key &sender_extension_g_out, + crypto::secret_key &sender_extension_t_out, + crypto::public_key &address_spend_pubkey_out, + rct::xmr_amount &amount_out, + crypto::secret_key &amount_blinding_factor_out, + CarrotEnoteType &enote_type_out, + janus_anchor_t &internal_message_out); +/** + * brief: verify_carrot_normal_janus_protection - verify that scanned normal enote is not attempting a + * Janus attack, and check pid + * param: input_context - input_context + * param: nominal_address_spend_pubkey - K^j_s' + * param: is_subaddress - true iff K^j_s' corresponds to a subaddress + * param: enote_ephemeral_pubkey - D_e + * param: nominal_janus_anchor - anchor' + * outparam: nominal_payment_id_inout - takes pid', sets to nullpid if not associated to this enote + * return: true iff it is computationally intractable for a sender to succeed at a Janus attack + */ +bool verify_carrot_normal_janus_protection(const input_context_t &input_context, + const crypto::public_key &nominal_address_spend_pubkey, + const bool is_subaddress, + const mx25519_pubkey &enote_ephemeral_pubkey, + const janus_anchor_t &nominal_janus_anchor, + payment_id_t &nominal_payment_id_inout); +/** + * brief: verify_carrot_special_janus_protection - verify that scanned special enote is not attempting a Janus attack + * param: tx_first_key_image - KI_1 + * param: enote_ephemeral_pubkey - D_e + * param: onetime_address - K_o + * param: k_view_dev - + * param: nominal_janus_anchor - anchor' + * return: true iff it is computationally intractable for a sender to succeed at a Janus attack + */ +bool verify_carrot_special_janus_protection(const crypto::key_image &tx_first_key_image, + const mx25519_pubkey &enote_ephemeral_pubkey, + const crypto::public_key &onetime_address, + const view_incoming_key_device &k_view_dev, + const janus_anchor_t &nominal_janus_anchor); + +} //namespace carrot diff --git a/src/carrot_core/transcript_fixed.h b/src/carrot_core/transcript_fixed.h new file mode 100644 index 00000000000..75d72df47b5 --- /dev/null +++ b/src/carrot_core/transcript_fixed.h @@ -0,0 +1,186 @@ +// Copyright (c) 2024, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// Transcript class for assembling data that needs to be hashed. + +#pragma once + +//local headers +#include "int-util.h" +#include "memwipe.h" + +//third party headers + +//standard headers +#include +#include +#include + +//forward declarations + + +namespace carrot +{ +namespace detail +{ +template +constexpr size_t sizeof_sum() +{ + return (sizeof(Ts) + ...); +} + +template <> +constexpr size_t sizeof_sum<>() +{ + return 0; +} +} //namespace detail + +//// +// FixedTransport +// - build a transcript of a fixed bytesize and input types, enforced at compile time +// - written to be the simplest correct transcript of data possible +// - requires domain separators at compile-time as well +// - ensures that no two transcripts with different domain separators will ever be equal +// - does not use dynamic allocation +// - unsigned integers are added to the transcript in little-endian form +// - signed integers are not allowed +// - domain separator is length-prefixed with a single unsigned byte at the beginning +// - passed domain separator can be null terminated or not, null bytes and after will be dropped +// - does not include an environment-specific prefix string; see the `carrot::derive_*()` hash functions +/// +template +class FixedTransport final +{ +public: +//constructors + /// normal constructor + FixedTransport(const Ts&... args) + { + // copy domain separator length prefix + m_transcript[0] = static_cast(domain_sep_size()); + + // copy domain separator + memcpy(m_transcript + 1, domain_sep, domain_sep_size()); + + // copy types into buffer + append<1 + domain_sep_size()>(args...); + } + +//overloaded operators + /// disable copy/move + FixedTransport& operator=(const FixedTransport&) = delete; + FixedTransport& operator=(FixedTransport&&) = delete; + +//member functions + constexpr const void* data() const noexcept { return m_transcript; } + + static constexpr std::size_t size() + { + return 1 + domain_sep_size() + detail::sizeof_sum(); + } + +//destructors + ~FixedTransport() + { + // wipe the buffer on leave in case it contains sensitive data + memwipe(m_transcript, sizeof(m_transcript)); + } + +private: +//member functions + template + void append() {} + + template + void append(const U0 &arg0, const Us&... args) + { + // write current argument to buffer + write(arg0); + + // call append for next argument + static constexpr size_t new_offset = offset + sizeof(arg0); + append(args...); + } + + template + void write(const U &val) + { + static_assert(std::has_unique_object_representations_v); + static_assert(std::is_standard_layout_v); + static_assert(!std::is_signed_v || std::is_same_v); + static_assert(alignof(U) == 1); + static_assert(!std::is_pointer_v); + + memcpy(m_transcript + offset, &val, sizeof(val)); + } + + template + void write(std::uint16_t val) + { + val = SWAP16LE(val); + memcpy(m_transcript + offset, &val, sizeof(val)); + } + + template + void write(std::uint32_t val) + { + val = SWAP32LE(val); + memcpy(m_transcript + offset, &val, sizeof(val)); + } + + template + void write(std::uint64_t val) + { + val = SWAP64LE(val); + memcpy(m_transcript + offset, &val, sizeof(val)); + } + + static constexpr std::size_t domain_sep_size() + { + for (std::size_t i = 0; i < N; ++i) + if (domain_sep[i] == '\0') + return i; + + return N; + } + + static_assert(domain_sep_size() <= 255, "domain separator must be less than 256 characters long"); + +//member variables + /// the transcript buffer + unsigned char m_transcript[size()]; +}; + +template +auto make_fixed_transcript(const Ts&... args) +{ + return FixedTransport(args...); +} + +} //namespace carrot diff --git a/src/carrot_impl/CMakeLists.txt b/src/carrot_impl/CMakeLists.txt new file mode 100644 index 00000000000..c4045d117e1 --- /dev/null +++ b/src/carrot_impl/CMakeLists.txt @@ -0,0 +1,64 @@ +# Copyright (c) 2024, The Monero Project +# +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, are +# permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this list of +# conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, this list +# of conditions and the following disclaimer in the documentation and/or other +# materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors may be +# used to endorse or promote products derived from this software without specific +# prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +# THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +set(carrot_impl_sources + address_device_hierarchies.cpp + address_device_ram_borrowed.cpp + address_utils.cpp + format_utils.cpp + input_selection.cpp + key_image_device_composed.cpp + key_image_device_precomputed.cpp + multi_tx_proposal_utils.cpp + output_opening_types.cpp + spend_device_ram_borrowed.cpp + subaddress_map_legacy.cpp + tx_builder_inputs.cpp + tx_builder_outputs.cpp + tx_proposal_utils.cpp +) + +monero_find_all_headers(carrot_impl_headers, "${CMAKE_CURRENT_SOURCE_DIR}") + +monero_add_library(carrot_impl + ${carrot_impl_sources} + ${carrot_impl_headers}) + +target_link_libraries(carrot_impl + PUBLIC + carrot_core + cryptonote_basic + fcmp_pp + PRIVATE + ${EXTRA_LIBRARIES}) + +target_include_directories(carrot_impl + PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}" + PRIVATE + ${Boost_INCLUDE_DIRS}) diff --git a/src/carrot_impl/address_device.h b/src/carrot_impl/address_device.h new file mode 100644 index 00000000000..44cc040642d --- /dev/null +++ b/src/carrot_impl/address_device.h @@ -0,0 +1,74 @@ +// Copyright (c) 2025, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +//local headers +#include "carrot_core/device.h" +#include "subaddress_index.h" + +//third party headers + +//standard headers + +//forward declarations + +namespace carrot +{ +static constexpr const int E_UNSUPPORTED_ADDRESS_TYPE = 1; + +struct address_device +{ + /** + * brief: get K^j_s given j + * param: subaddr_index - j + * outparam: address_spend_pubkey_out - K^j_s + */ + virtual void get_address_spend_pubkey(const subaddress_index_extended &subaddr_index, + crypto::public_key &address_spend_pubkey_out) const = 0; + + /** + * brief: get (K^j_s, K^j_v) given j + * param: subaddr_index - j + * outparam: address_spend_pubkey_out - K^j_s + * outparam: address_view_pubkey_out - K^j_v + */ + virtual void get_address_pubkeys(const subaddress_index_extended &subaddr_index, + crypto::public_key &address_spend_pubkey_out, + crypto::public_key &address_view_pubkey_out) const = 0; + + /** + * get (k^j_subext, k^j_subscalar) given j s.t. K^j_s = k^j_subscalar K_s + k^j_subext G + */ + virtual void get_address_openings(const subaddress_index_extended &subaddr_index, + crypto::secret_key &address_extension_g_out, + crypto::secret_key &address_scalar_out) const = 0; + + virtual ~address_device() = default; +}; +} //namespace carrot diff --git a/src/carrot_impl/address_device_hierarchies.cpp b/src/carrot_impl/address_device_hierarchies.cpp new file mode 100644 index 00000000000..2f7818b49dd --- /dev/null +++ b/src/carrot_impl/address_device_hierarchies.cpp @@ -0,0 +1,385 @@ +// Copyright (c) 2025, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//paired header +#include "address_device_hierarchies.h" + +//local headers +#include "carrot_core/address_utils.h" +#include "carrot_core/exceptions.h" +#include "crypto/crypto.h" +#include "crypto/generators.h" +extern "C" +{ +#include "crypto/crypto-ops.h" +} +#include "misc_log_ex.h" + +//third party headers + +//standard headers + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "carrot_impl" + +namespace +{ +static void assert_single_derive_type(const carrot::AddressDeriveType expected_derive_type, + const carrot::AddressDeriveType actual_derive_type, + const char * const dev_model, + const char * const func_called) +{ + const auto make_error = [dev_model, func_called](std::string msg) + { + return carrot::device_error(std::string("Default"), + std::string(dev_model), + std::string(func_called), + std::move(msg), + carrot::E_UNSUPPORTED_ADDRESS_TYPE); + }; + + if (actual_derive_type != expected_derive_type && actual_derive_type != carrot::AddressDeriveType::Auto) + { + CARROT_THROW(make_error, "Expected derive type " << static_cast(expected_derive_type) + << ", got derive type " << static_cast(actual_derive_type)); + } +} +} //anonymous namespace + +namespace carrot +{ +//------------------------------------------------------------------------------------------------------------------- +cryptonote_hierarchy_address_device::cryptonote_hierarchy_address_device( + std::shared_ptr k_view_incoming_dev, + const crypto::public_key &cryptonote_account_spend_pubkey) +: + m_k_view_incoming_dev(std::move(k_view_incoming_dev)), + m_cryptonote_account_spend_pubkey(cryptonote_account_spend_pubkey) +{ + assert(this->m_k_view_incoming_dev); +} +//------------------------------------------------------------------------------------------------------------------- +void cryptonote_hierarchy_address_device::get_address_spend_pubkey(const subaddress_index_extended &subaddr_index, + crypto::public_key &address_spend_pubkey_out) const +{ + this->assert_derive_type(subaddr_index, "get_address_spend_pubkey"); + + if (!subaddr_index.index.is_subaddress()) + { + address_spend_pubkey_out = this->m_cryptonote_account_spend_pubkey; + return; + } + + // k^j_subext = ScalarDeriveLegacy("SubAddr" || IntToBytes8(0) || k_v || IntToBytes32(j_major) || IntToBytes32(j_minor)) + crypto::secret_key k_subaddress_extension; + this->m_k_view_incoming_dev->make_legacy_subaddress_extension(subaddr_index.index.major, + subaddr_index.index.minor, k_subaddress_extension); + + // K^j_subext = k^j_subext G + ge_p3 subaddress_extension_p3; + ge_scalarmult_base(&subaddress_extension_p3, to_bytes(k_subaddress_extension)); + + // decompress K_S + ge_p3 account_spend_pubkey_p3; + ge_frombytes_vartime(&account_spend_pubkey_p3, to_bytes(this->m_cryptonote_account_spend_pubkey)); // discard result + ge_cached account_spend_pubkey_cached; + ge_p3_to_cached(&account_spend_pubkey_cached, &account_spend_pubkey_p3); + + // K^j_s = K_s + K^j_subext + ge_p1p1 address_spend_pubkey_p1p1; + ge_add(&address_spend_pubkey_p1p1, &subaddress_extension_p3, &account_spend_pubkey_cached); + ge_p2 address_spend_pubkey_p2; + ge_p1p1_to_p2(&address_spend_pubkey_p2, &address_spend_pubkey_p1p1); + ge_tobytes(to_bytes(address_spend_pubkey_out), &address_spend_pubkey_p2); +} +//------------------------------------------------------------------------------------------------------------------- +void cryptonote_hierarchy_address_device::get_address_pubkeys(const subaddress_index_extended &subaddr_index, + crypto::public_key &address_spend_pubkey_out, + crypto::public_key &address_view_pubkey_out) const +{ + this->assert_derive_type(subaddr_index, "get_address_pubkeys"); + this->get_address_spend_pubkey(subaddr_index, address_spend_pubkey_out); + const crypto::public_key view_base_pubkey = subaddr_index.index.is_subaddress() + ? address_spend_pubkey_out : crypto::get_G(); + this->m_k_view_incoming_dev->view_key_scalar_mult_ed25519(view_base_pubkey, address_view_pubkey_out); +} +//------------------------------------------------------------------------------------------------------------------- +void cryptonote_hierarchy_address_device::get_address_openings(const subaddress_index_extended &subaddr_index, + crypto::secret_key &address_extension_g_out, + crypto::secret_key &address_scalar_out) const +{ + this->assert_derive_type(subaddr_index, "get_address_openings"); + + if (subaddr_index.index.is_subaddress()) + { + // k^j_subext = ScalarDeriveLegacy("SubAddr" || IntToBytes8(0) || k_v || IntToBytes32(j_major) || IntToBytes32(j_minor)) + this->m_k_view_incoming_dev->make_legacy_subaddress_extension(subaddr_index.index.major, + subaddr_index.index.minor, + address_extension_g_out); + } + else // main address + { + // k^j_subext = 0 + address_extension_g_out = crypto::null_skey; + } + + // k^j_subscal = 1 + address_scalar_out = crypto::secret_key{{1}}; +} +//------------------------------------------------------------------------------------------------------------------- +bool cryptonote_hierarchy_address_device::view_key_scalar_mult_ed25519(const crypto::public_key &P, + crypto::public_key &kvP) const +{ + return this->m_k_view_incoming_dev->view_key_scalar_mult_ed25519(P, kvP); +} +//------------------------------------------------------------------------------------------------------------------- +bool cryptonote_hierarchy_address_device::view_key_scalar_mult8_ed25519(const crypto::public_key &P, + crypto::public_key &kv8P) const +{ + return this->m_k_view_incoming_dev->view_key_scalar_mult8_ed25519(P, kv8P); +} +//------------------------------------------------------------------------------------------------------------------- +bool cryptonote_hierarchy_address_device::view_key_scalar_mult_x25519(const mx25519_pubkey &D, + mx25519_pubkey &kvD) const +{ + return this->m_k_view_incoming_dev->view_key_scalar_mult_x25519(D, kvD); +} +//------------------------------------------------------------------------------------------------------------------- +void cryptonote_hierarchy_address_device::make_janus_anchor_special(const mx25519_pubkey &enote_ephemeral_pubkey, + const input_context_t &input_context, + const crypto::public_key &onetime_address, + janus_anchor_t &anchor_special_out) const +{ + return this->m_k_view_incoming_dev->make_janus_anchor_special(enote_ephemeral_pubkey, input_context, + onetime_address, anchor_special_out); +} +//------------------------------------------------------------------------------------------------------------------- +void cryptonote_hierarchy_address_device::make_legacy_subaddress_extension( + const std::uint32_t major_index, + const std::uint32_t minor_index, + crypto::secret_key &legacy_subaddress_extension_out) const +{ + return this->m_k_view_incoming_dev->make_legacy_subaddress_extension(major_index, minor_index, + legacy_subaddress_extension_out); +} +//------------------------------------------------------------------------------------------------------------------- +void cryptonote_hierarchy_address_device::assert_derive_type(const subaddress_index_extended &subaddr_index, + const char * const func_called) const +{ + assert_single_derive_type(AddressDeriveType::PreCarrot, subaddr_index.derive_type, + "cryptonote_hierarchy_address_device", func_called); +} +//------------------------------------------------------------------------------------------------------------------- +carrot_hierarchy_address_device::carrot_hierarchy_address_device( + std::shared_ptr s_generate_address_dev, + const crypto::public_key &carrot_account_spend_pubkey, + const crypto::public_key &carrot_account_view_pubkey) +: + m_s_generate_address_dev(std::move(s_generate_address_dev)), + m_carrot_account_spend_pubkey(carrot_account_spend_pubkey), + m_carrot_account_view_pubkey(carrot_account_view_pubkey) +{ + assert(this->m_s_generate_address_dev); +} +//------------------------------------------------------------------------------------------------------------------- +void carrot_hierarchy_address_device::get_address_spend_pubkey(const subaddress_index_extended &subaddr_index, + crypto::public_key &address_spend_pubkey_out) const +{ + this->assert_derive_type(subaddr_index, "get_address_spend_pubkey"); + + if (!subaddr_index.index.is_subaddress()) + { + address_spend_pubkey_out = this->m_carrot_account_spend_pubkey; + return; + } + + const crypto::secret_key subaddress_scalar = this->get_subaddress_scalar(subaddr_index.index); + + // decompress K_s + ge_p3 account_spend_pubkey_p3; + ge_frombytes_vartime(&account_spend_pubkey_p3, to_bytes(this->m_carrot_account_spend_pubkey)); + + // K^j_s = k^j_subscal K_s + ge_p2 tmp_p2; + ge_scalarmult(&tmp_p2, to_bytes(subaddress_scalar), &account_spend_pubkey_p3); + ge_tobytes(to_bytes(address_spend_pubkey_out), &tmp_p2); +} +//------------------------------------------------------------------------------------------------------------------- +void carrot_hierarchy_address_device::get_address_pubkeys(const subaddress_index_extended &subaddr_index, + crypto::public_key &address_spend_pubkey_out, + crypto::public_key &address_view_pubkey_out) const +{ + this->assert_derive_type(subaddr_index, "get_address_pubkeys"); + + if (!subaddr_index.index.is_subaddress()) + { + address_spend_pubkey_out = this->m_carrot_account_spend_pubkey; + return; + } + + const crypto::secret_key subaddress_scalar = this->get_subaddress_scalar(subaddr_index.index); + + // decompress K_s + ge_p3 tmp_p3; + ge_frombytes_vartime(&tmp_p3, to_bytes(this->m_carrot_account_spend_pubkey)); + + // K^j_s = k^j_subscal K_s + ge_p2 tmp_p2; + ge_scalarmult(&tmp_p2, to_bytes(subaddress_scalar), &tmp_p3); + ge_tobytes(to_bytes(address_spend_pubkey_out), &tmp_p2); + + // decompress K_v + ge_frombytes_vartime(&tmp_p3, to_bytes(this->m_carrot_account_view_pubkey)); + + // K^j_v = k^j_subscal K_v + ge_scalarmult(&tmp_p2, to_bytes(subaddress_scalar), &tmp_p3); + ge_tobytes(to_bytes(address_view_pubkey_out), &tmp_p2); +} +//------------------------------------------------------------------------------------------------------------------- +void carrot_hierarchy_address_device::get_address_openings(const subaddress_index_extended &subaddr_index, + crypto::secret_key &address_extension_g_out, + crypto::secret_key &address_scalar_out) const +{ + this->assert_derive_type(subaddr_index, "get_address_openings"); + + // k^j_subext = 0 + address_extension_g_out = crypto::null_skey; + + // [main] k^j_subscal = 1 + // [subaddress] k^j_subscal = H_n[s^j_gen](K_s, K_v, j_major, j_minor) + address_scalar_out = this->get_subaddress_scalar(subaddr_index.index); +} +//------------------------------------------------------------------------------------------------------------------- +crypto::secret_key carrot_hierarchy_address_device::get_subaddress_scalar(const subaddress_index &subaddr_index) const +{ + if (subaddr_index.is_subaddress()) + { + // s^j_ap1 = H_32[s_ga](j_major, j_minor) + crypto::secret_key res; + this->m_s_generate_address_dev->make_address_index_preimage_1(subaddr_index.major, + subaddr_index.minor, + res); + + // s^j_ap2 = H_32[s^j_ap1](j_major, j_minor, K_s, K_v) + make_carrot_address_index_preimage_2(res, + subaddr_index.major, + subaddr_index.minor, + this->m_carrot_account_spend_pubkey, + this->m_carrot_account_view_pubkey, + res); + + // k^j_subscal = H_n[s^j_ap2](K_s) + make_carrot_subaddress_scalar(res, + this->m_carrot_account_spend_pubkey, + res); + + return res; + } + else // main address + { + // k^j_subscal = 1 + return crypto::secret_key{{1}}; + } +} +//------------------------------------------------------------------------------------------------------------------- +void carrot_hierarchy_address_device::assert_derive_type(const subaddress_index_extended &subaddr_index, + const char * const func_called) const +{ + assert_single_derive_type(AddressDeriveType::Carrot, subaddr_index.derive_type, + "carrot_hierarchy_address_device", func_called); +} +//------------------------------------------------------------------------------------------------------------------- +hybrid_hierarchy_address_device::hybrid_hierarchy_address_device(std::shared_ptr carrot_addr_dev, + std::shared_ptr cryptonote_addr_dev) +: + m_carrot_addr_dev(std::move(carrot_addr_dev)), + m_cryptonote_addr_dev(std::move(cryptonote_addr_dev)) +{} +//------------------------------------------------------------------------------------------------------------------- +void hybrid_hierarchy_address_device::get_address_spend_pubkey(const subaddress_index_extended &subaddr_index, + crypto::public_key &address_spend_pubkey_out) const +{ + this->resolve_address_device(subaddr_index.derive_type, "get_address_spend_pubkey") + .get_address_spend_pubkey(subaddr_index, address_spend_pubkey_out); +} +//------------------------------------------------------------------------------------------------------------------- +void hybrid_hierarchy_address_device::get_address_pubkeys(const subaddress_index_extended &subaddr_index, + crypto::public_key &address_spend_pubkey_out, + crypto::public_key &address_view_pubkey_out) const +{ + this->resolve_address_device(subaddr_index.derive_type, "get_address_pubkeys") + .get_address_pubkeys(subaddr_index, address_spend_pubkey_out, address_view_pubkey_out); +} +//------------------------------------------------------------------------------------------------------------------- +void hybrid_hierarchy_address_device::get_address_openings(const subaddress_index_extended &subaddr_index, + crypto::secret_key &address_extension_g_out, + crypto::secret_key &address_scalar_out) const +{ + this->resolve_address_device(subaddr_index.derive_type, "get_address_openings") + .get_address_openings(subaddr_index, address_extension_g_out, address_scalar_out); +} +//------------------------------------------------------------------------------------------------------------------- +const address_device& hybrid_hierarchy_address_device::resolve_address_device(const AddressDeriveType derive_type, + const char * const func_called) const +{ + const auto make_error = [func_called](std::string msg) + { + return carrot::device_error("Default", + "hybrid_hierarchy_address_device", + func_called, + std::move(msg), + E_UNSUPPORTED_ADDRESS_TYPE); + }; + + const address_device *p_resolved_addr_dev = nullptr; + switch (derive_type) + { + case AddressDeriveType::Auto: + if (this->m_carrot_addr_dev) + p_resolved_addr_dev = this->m_carrot_addr_dev.get(); + else + p_resolved_addr_dev = this->m_cryptonote_addr_dev.get(); + break; + case AddressDeriveType::PreCarrot: + p_resolved_addr_dev = this->m_cryptonote_addr_dev.get(); + break; + case AddressDeriveType::Carrot: + p_resolved_addr_dev = this->m_carrot_addr_dev.get(); + break; + default: + CARROT_THROW(make_error, "unrecognized derive type: " << static_cast(derive_type)); + break; + }; + + CARROT_CHECK_AND_THROW(p_resolved_addr_dev, + make_error, "Derive type not supported for this hybrid address device: " << static_cast(derive_type)); + + return *p_resolved_addr_dev; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace carrot diff --git a/src/carrot_impl/address_device_hierarchies.h b/src/carrot_impl/address_device_hierarchies.h new file mode 100644 index 00000000000..3665f62a2c1 --- /dev/null +++ b/src/carrot_impl/address_device_hierarchies.h @@ -0,0 +1,177 @@ +// Copyright (c) 2025, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +//local headers +#include "address_device.h" + +//third party headers + +//standard headers +#include + +//forward declarations + +namespace carrot +{ +/// @brief extends the k_v device to allow Cryptonote-style address derivation +struct cryptonote_view_incoming_key_device: virtual public view_incoming_key_device +{ + /** + * brief: make_legacy_subaddress_extension - k^j_subext + * k^j_subext = ScalarDeriveLegacy("SubAddr" || IntToBytes8(0) || k_v || IntToBytes32(j_major) || IntToBytes32(j_minor)) + * param: major_index - j_major + * param: minor_index - j_minor + * outparam: legacy_subaddress_extension_out - k^j_subext + */ + virtual void make_legacy_subaddress_extension( + const std::uint32_t major_index, + const std::uint32_t minor_index, + crypto::secret_key &legacy_subaddress_extension_out) const = 0; +}; + +/// @brief takes a CN k_v device and K_s to derive addresses in Cryptonote style +/// @note will fail if passed derive type is not ::PreCarrot or ::Auto +class cryptonote_hierarchy_address_device: public address_device, public cryptonote_view_incoming_key_device +{ +public: +//constructor + cryptonote_hierarchy_address_device( + std::shared_ptr k_view_incoming_dev, + const crypto::public_key &cryptonote_account_spend_pubkey); + +//address_device + void get_address_spend_pubkey(const subaddress_index_extended &subaddr_index, + crypto::public_key &address_spend_pubkey_out) const override; + + void get_address_pubkeys(const subaddress_index_extended &subaddr_index, + crypto::public_key &address_spend_pubkey_out, + crypto::public_key &address_view_pubkey_out) const override; + + void get_address_openings(const subaddress_index_extended &subaddr_index, + crypto::secret_key &address_extension_g_out, + crypto::secret_key &address_scalar_out) const override; + +//cryptonote_view_incoming_key_device + bool view_key_scalar_mult_ed25519(const crypto::public_key &P, crypto::public_key &kvP) const override; + + bool view_key_scalar_mult8_ed25519(const crypto::public_key &P, crypto::public_key &kv8P) const override; + + bool view_key_scalar_mult_x25519(const mx25519_pubkey &D, mx25519_pubkey &kvD) const override; + + void make_janus_anchor_special(const mx25519_pubkey &enote_ephemeral_pubkey, + const input_context_t &input_context, + const crypto::public_key &onetime_address, + janus_anchor_t &anchor_special_out) const override; + + void make_legacy_subaddress_extension( + const std::uint32_t major_index, + const std::uint32_t minor_index, + crypto::secret_key &legacy_subaddress_extension_out) const override; + +protected: +//member fields + std::shared_ptr m_k_view_incoming_dev; + crypto::public_key m_cryptonote_account_spend_pubkey; + +private: +//member functions + void assert_derive_type(const subaddress_index_extended &subaddr_index, + const char * const func_called) const; +}; + +/// @brief takes a s_ga device and (K_s, K_v) to derive addresses in Carrot style +/// @note will fail if passed derive type is not ::Carrot or ::Auto +class carrot_hierarchy_address_device: public address_device +{ +public: +//constructor + carrot_hierarchy_address_device( + std::shared_ptr s_generate_address_dev, + const crypto::public_key &carrot_account_spend_pubkey, + const crypto::public_key &carrot_account_view_pubkey); + +//address_device + void get_address_spend_pubkey(const subaddress_index_extended &subaddr_index, + crypto::public_key &address_spend_pubkey_out) const override; + + void get_address_pubkeys(const subaddress_index_extended &subaddr_index, + crypto::public_key &address_spend_pubkey_out, + crypto::public_key &address_view_pubkey_out) const override; + + void get_address_openings(const subaddress_index_extended &subaddr_index, + crypto::secret_key &address_extension_g_out, + crypto::secret_key &address_scalar_out) const override; + +protected: +//member fields + std::shared_ptr m_s_generate_address_dev; + crypto::public_key m_carrot_account_spend_pubkey; + crypto::public_key m_carrot_account_view_pubkey; + +private: +//member functions + crypto::secret_key get_subaddress_scalar(const subaddress_index &subaddr_index) const; + + void assert_derive_type(const subaddress_index_extended &subaddr_index, + const char * const func_called) const; +}; + +/// @brief takes a CN and/or a Carrot address device and dispatches derivation according to the passed derive type +/// @note resolves to Carrot on ::Auto derive type if available +class hybrid_hierarchy_address_device: public address_device +{ +public: +//constructor + hybrid_hierarchy_address_device(std::shared_ptr carrot_addr_dev, + std::shared_ptr cryptonote_addr_dev); + +//address_device + void get_address_spend_pubkey(const subaddress_index_extended &subaddr_index, + crypto::public_key &address_spend_pubkey_out) const override; + + void get_address_pubkeys(const subaddress_index_extended &subaddr_index, + crypto::public_key &address_spend_pubkey_out, + crypto::public_key &address_view_pubkey_out) const override; + + void get_address_openings(const subaddress_index_extended &subaddr_index, + crypto::secret_key &address_extension_g_out, + crypto::secret_key &address_scalar_out) const override; + +protected: +//member fields + std::shared_ptr m_carrot_addr_dev; + std::shared_ptr m_cryptonote_addr_dev; + +private: +//member functions + const address_device& resolve_address_device(const AddressDeriveType derive_type, + const char * const func_called) const; +}; +} //namespace carrot diff --git a/src/carrot_impl/address_device_ram_borrowed.cpp b/src/carrot_impl/address_device_ram_borrowed.cpp new file mode 100644 index 00000000000..8360fafd09d --- /dev/null +++ b/src/carrot_impl/address_device_ram_borrowed.cpp @@ -0,0 +1,62 @@ +// Copyright (c) 2024, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//paired header +#include "address_device_ram_borrowed.h" + +//local headers +#include "address_utils.h" + +//third party headers + +//standard headers + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "carrot_impl" + +namespace carrot +{ +//------------------------------------------------------------------------------------------------------------------- +cryptonote_view_incoming_key_ram_borrowed_device::cryptonote_view_incoming_key_ram_borrowed_device( + const crypto::secret_key &k_view_incoming) +: + view_incoming_key_ram_borrowed_device(k_view_incoming) +{} +//------------------------------------------------------------------------------------------------------------------- +void cryptonote_view_incoming_key_ram_borrowed_device::make_legacy_subaddress_extension( + const std::uint32_t major_index, + const std::uint32_t minor_index, + crypto::secret_key &legacy_subaddress_extension_out) const +{ + carrot::make_legacy_subaddress_extension(m_k_view_incoming, + major_index, + minor_index, + legacy_subaddress_extension_out); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace carrot diff --git a/src/carrot_impl/address_device_ram_borrowed.h b/src/carrot_impl/address_device_ram_borrowed.h new file mode 100644 index 00000000000..51b5e676ad1 --- /dev/null +++ b/src/carrot_impl/address_device_ram_borrowed.h @@ -0,0 +1,55 @@ +// Copyright (c) 2025, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +//local headers +#include "address_device_hierarchies.h" +#include "carrot_core/device_ram_borrowed.h" + +//third party headers + +//standard headers + +//forward declarations + +namespace carrot +{ +class cryptonote_view_incoming_key_ram_borrowed_device: + virtual public cryptonote_view_incoming_key_device, + virtual public view_incoming_key_ram_borrowed_device +{ +public: + cryptonote_view_incoming_key_ram_borrowed_device(const crypto::secret_key &k_view_incoming); + + void make_legacy_subaddress_extension( + const std::uint32_t major_index, + const std::uint32_t minor_index, + crypto::secret_key &legacy_subaddress_extension_out) const override; +}; +} //namespace carrot diff --git a/src/carrot_impl/address_utils.cpp b/src/carrot_impl/address_utils.cpp new file mode 100644 index 00000000000..093afeba53d --- /dev/null +++ b/src/carrot_impl/address_utils.cpp @@ -0,0 +1,114 @@ +// Copyright (c) 2024, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//paired header +#include "address_utils.h" + +//local headers +#include "address_device.h" +#include "crypto/generators.h" +#include "cryptonote_config.h" +#include "int-util.h" +#include "ringct/rctOps.h" + +//third party headers + +//standard headers + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "carrot_impl" + +namespace carrot +{ +//------------------------------------------------------------------------------------------------------------------- +void make_legacy_subaddress_extension(const crypto::secret_key &k_view, + const std::uint32_t major_index, + const std::uint32_t minor_index, + crypto::secret_key &legacy_subaddress_extension_out) +{ + if (!major_index && !minor_index) + { + sc_0(to_bytes(legacy_subaddress_extension_out)); + return; + } + + char data[sizeof(config::HASH_KEY_SUBADDRESS) + sizeof(crypto::secret_key) + 2 * sizeof(uint32_t)]; + + // "Subaddr" || IntToBytes(0) + memcpy(data, config::HASH_KEY_SUBADDRESS, sizeof(config::HASH_KEY_SUBADDRESS)); + + // ... || k_v + memcpy(data + sizeof(config::HASH_KEY_SUBADDRESS), &k_view, sizeof(crypto::secret_key)); + + // ... || IntToBytes32(j_major) + uint32_t idx = SWAP32LE(major_index); + memcpy(data + sizeof(config::HASH_KEY_SUBADDRESS) + sizeof(crypto::secret_key), &idx, sizeof(uint32_t)); + + // ... || IntToBytes32(j_minor) + idx = SWAP32LE(minor_index); + memcpy(data + sizeof(config::HASH_KEY_SUBADDRESS) + sizeof(crypto::secret_key) + sizeof(uint32_t), + &idx, + sizeof(uint32_t)); + + // k^j_subext = ScalarDeriveLegacy("SubAddr" || IntToBytes8(0) || k_v || IntToBytes32(j_major) || IntToBytes32(j_minor)) + crypto::hash_to_scalar(data, sizeof(data), legacy_subaddress_extension_out); +} +//------------------------------------------------------------------------------------------------------------------- +std::size_t get_all_main_address_spend_pubkeys(const address_device &addr_dev, + crypto::public_key main_address_spend_pubkeys_out[2]) +{ + memset(main_address_spend_pubkeys_out, 0, 2*sizeof(main_address_spend_pubkeys_out[0])); + + std::size_t n_main_addrs = 0; + try + { + addr_dev.get_address_spend_pubkey({{}, AddressDeriveType::PreCarrot}, + main_address_spend_pubkeys_out[n_main_addrs]); + ++n_main_addrs; + } + catch (...) {} + try + { + addr_dev.get_address_spend_pubkey({{}, AddressDeriveType::Carrot}, + main_address_spend_pubkeys_out[n_main_addrs]); + ++n_main_addrs; + } + catch (...) {} + + return n_main_addrs; +} +//------------------------------------------------------------------------------------------------------------------- +epee::span get_all_main_address_spend_pubkeys_span( + const address_device &addr_dev, + crypto::public_key main_address_spend_pubkeys_out[2]) +{ + const std::size_t n_main_addrs = get_all_main_address_spend_pubkeys(addr_dev, main_address_spend_pubkeys_out); + return {main_address_spend_pubkeys_out, n_main_addrs}; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace carrot diff --git a/src/carrot_impl/address_utils.h b/src/carrot_impl/address_utils.h new file mode 100644 index 00000000000..6ed6ce6558e --- /dev/null +++ b/src/carrot_impl/address_utils.h @@ -0,0 +1,72 @@ +// Copyright (c) 2025, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "span.h" + +//third party headers + +//standard headers +#include + +//forward declarations +namespace carrot +{ +struct address_device; +} + +namespace carrot +{ +/** + * brief: make_legacy_subaddress_extension - k^j_subext + * k^j_subext = ScalarDeriveLegacy("SubAddr" || IntToBytes8(0) || k_v || IntToBytes32(j_major) || IntToBytes32(j_minor)) + * param: k_view - k_v + * param: major_index - j_major + * param: minor_index - j_minor + * outparam: legacy_subaddress_extension_out - k^j_subext + */ +void make_legacy_subaddress_extension(const crypto::secret_key &k_view, + const std::uint32_t major_index, + const std::uint32_t minor_index, + crypto::secret_key &legacy_subaddress_extension_out); +/** + * brief: get all supported main address spend pubkeys K_s from a hybrid address device + * param: addr_dev - + * outparam: main_address_spend_pubkeys_out - + * return: number of supported pubkeys or span to supported main_address_spend_pubkeys_out + */ +std::size_t get_all_main_address_spend_pubkeys( + const address_device &addr_dev, + crypto::public_key main_address_spend_pubkeys_out[2]); +epee::span get_all_main_address_spend_pubkeys_span( + const address_device &addr_dev, + crypto::public_key main_address_spend_pubkeys_out[2]); +} //namespace carrot diff --git a/src/carrot_impl/carrot_chain_serialization.h b/src/carrot_impl/carrot_chain_serialization.h new file mode 100644 index 00000000000..d3b7cf0ca1b --- /dev/null +++ b/src/carrot_impl/carrot_chain_serialization.h @@ -0,0 +1,43 @@ +// Copyright (c) 2025, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +//local headers +#include "carrot_core/core_types.h" +#include "serialization/serialization.h" + +//third party headers + +//standard headers + +//forward declarations + +BLOB_SERIALIZER(carrot::view_tag_t); +BLOB_SERIALIZER(carrot::encrypted_janus_anchor_t); +BLOB_SERIALIZER(carrot::encrypted_payment_id_t); diff --git a/src/carrot_impl/carrot_offchain_serialization.h b/src/carrot_impl/carrot_offchain_serialization.h new file mode 100644 index 00000000000..09c4e9f642d --- /dev/null +++ b/src/carrot_impl/carrot_offchain_serialization.h @@ -0,0 +1,163 @@ +// Copyright (c) 2025, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +//local headers +#include "carrot_chain_serialization.h" +#include "carrot_core/payment_proposal.h" +#include "serialization/binary_archive.h" +#include "serialization/crypto.h" +#include "serialization/optional.h" +#include "serialization/variant.h" +#include "subaddress_index.h" +#include "tx_proposal.h" + +//third party headers + +//standard headers + +//forward declarations + +BLOB_SERIALIZER(carrot::encrypted_amount_t); +BLOB_SERIALIZER(carrot::payment_id_t); + +BEGIN_SERIALIZE_OBJECT_FN(carrot::CarrotDestinationV1) + static constexpr uint8_t flag_is_subaddr = 1 << 0; + static constexpr uint8_t flag_has_pid = 1 << 1; + uint8_t flags = 0; + if (v.is_subaddress) flags |= flag_is_subaddr; + if (v.payment_id != carrot::null_payment_id) flags |= flag_has_pid; + FIELD(flags) + v.is_subaddress = flags & flag_is_subaddr; + + FIELD_F(address_spend_pubkey) + FIELD_F(address_view_pubkey) + if (flags & flag_has_pid) + { + FIELD_F(payment_id) + } + else // no pid + { + v.payment_id = carrot::null_payment_id; + } +END_SERIALIZE() + +BEGIN_SERIALIZE_OBJECT_FN(carrot::CarrotPaymentProposalV1) + FIELD_F(destination) + VARINT_FIELD_F(amount) + FIELD_F(randomness) +END_SERIALIZE() + +BEGIN_SERIALIZE_OBJECT_FN(carrot::CarrotPaymentProposalSelfSendV1) + FIELD_F(destination_address_spend_pubkey) + VARINT_FIELD_F(amount) + VARINT_FIELD_F(enote_type) + FIELD_F(enote_ephemeral_pubkey) + FIELD_F(internal_message) +END_SERIALIZE() + +BEGIN_SERIALIZE_OBJECT_FN(carrot::subaddress_index) + VARINT_FIELD_F(major) + VARINT_FIELD_F(minor) +END_SERIALIZE() + +BEGIN_SERIALIZE_OBJECT_FN(carrot::subaddress_index_extended) + FIELD_F(index) + VARINT_FIELD_F(derive_type) +END_SERIALIZE() + +BEGIN_SERIALIZE_OBJECT_FN(carrot::CarrotEnoteV1) + FIELD_F(onetime_address) + FIELD_F(amount_commitment) + FIELD_F(amount_enc) + FIELD_F(anchor_enc) + FIELD_F(view_tag) + FIELD_F(enote_ephemeral_pubkey) + FIELD_F(tx_first_key_image) +END_SERIALIZE() + +BEGIN_SERIALIZE_OBJECT_FN(carrot::CarrotCoinbaseEnoteV1) + FIELD_F(onetime_address) + VARINT_FIELD_F(amount) + FIELD_F(anchor_enc) + FIELD_F(view_tag) + FIELD_F(enote_ephemeral_pubkey) + VARINT_FIELD_F(block_index) +END_SERIALIZE() + +BEGIN_SERIALIZE_OBJECT_FN(carrot::LegacyOutputOpeningHintV1) + FIELD_F(onetime_address) + FIELD_F(ephemeral_tx_pubkey) + FIELD_F(subaddr_index) + VARINT_FIELD_F(amount) + FIELD_F(amount_blinding_factor) + VARINT_FIELD_F(local_output_index) +END_SERIALIZE() + +BEGIN_SERIALIZE_OBJECT_FN(carrot::CarrotOutputOpeningHintV1) + FIELD_F(source_enote) + FIELD_F(encrypted_payment_id) + FIELD_F(subaddr_index) +END_SERIALIZE() + +BEGIN_SERIALIZE_OBJECT_FN(carrot::CarrotOutputOpeningHintV2) + FIELD_F(onetime_address) + FIELD_F(amount_commitment) + FIELD_F(anchor_enc) + FIELD_F(view_tag) + FIELD_F(enote_ephemeral_pubkey) + FIELD_F(tx_first_key_image) + VARINT_FIELD_F(amount) + FIELD_F(encrypted_payment_id) + FIELD_F(subaddr_index) +END_SERIALIZE() + +BEGIN_SERIALIZE_OBJECT_FN(carrot::CarrotCoinbaseOutputOpeningHintV1) + FIELD_F(source_enote) + VARINT_FIELD_F(derive_type) +END_SERIALIZE() + +BEGIN_SERIALIZE_OBJECT_FN(carrot::CarrotPaymentProposalVerifiableSelfSendV1) + FIELD_F(proposal) + FIELD_F(subaddr_index) +END_SERIALIZE() + +BEGIN_SERIALIZE_OBJECT_FN(carrot::CarrotTransactionProposalV1) + FIELD_F(input_proposals) + FIELD_F(normal_payment_proposals) + FIELD_F(selfsend_payment_proposals) + FIELD_F(dummy_encrypted_payment_id) + VARINT_FIELD_F(fee) + FIELD_F(extra) +END_SERIALIZE() + +VARIANT_TAG(binary_archive, carrot::LegacyOutputOpeningHintV1, 0x80); +VARIANT_TAG(binary_archive, carrot::CarrotOutputOpeningHintV1, 0x81); +VARIANT_TAG(binary_archive, carrot::CarrotOutputOpeningHintV2, 0x82); +VARIANT_TAG(binary_archive, carrot::CarrotCoinbaseOutputOpeningHintV1, 0x83); diff --git a/src/carrot_impl/format_utils.cpp b/src/carrot_impl/format_utils.cpp new file mode 100644 index 00000000000..fdec572cf39 --- /dev/null +++ b/src/carrot_impl/format_utils.cpp @@ -0,0 +1,593 @@ +// 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 "format_utils.h" + +//local headers +#include "carrot_core/config.h" +#include "carrot_core/enote_utils.h" +#include "carrot_core/exceptions.h" +#include "carrot_core/payment_proposal.h" +#include "cryptonote_basic/cryptonote_format_utils.h" +#include "cryptonote_config.h" +#include "ringct/rctOps.h" +#include "serialization/binary_utils.h" + +//third party headers + +//standard headers + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "carrot_impl.format_utils" + +static_assert(sizeof(mx25519_pubkey) == sizeof(crypto::public_key), + "cannot use crypto::public_key as storage for X25519 keys since size is different"); + +namespace carrot +{ +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +template +static void store_carrot_ephemeral_pubkeys_to_extra(const EnoteContainer &enotes, std::vector &extra_inout) +{ + const size_t nouts = enotes.size(); + const bool use_shared_ephemeral_pubkey = nouts == 1 || (nouts == 2 && 0 == memcmp( + &enotes.front().enote_ephemeral_pubkey, + &enotes.back().enote_ephemeral_pubkey, + sizeof(mx25519_pubkey))); + bool success = true; + if (use_shared_ephemeral_pubkey) + { + const mx25519_pubkey &enote_ephemeral_pubkey = enotes.at(0).enote_ephemeral_pubkey; + const crypto::public_key tx_pubkey = raw_byte_convert(enote_ephemeral_pubkey); + success = success && cryptonote::add_tx_pub_key_to_extra(extra_inout, tx_pubkey); + } + else // !use_shared_ephemeral_pubkey + { + std::vector tx_pubkeys(nouts); + for (size_t i = 0; i < nouts; ++i) + { + const mx25519_pubkey &enote_ephemeral_pubkey = enotes.at(i).enote_ephemeral_pubkey; + tx_pubkeys[i] = raw_byte_convert(enote_ephemeral_pubkey); + } + success = success && cryptonote::add_additional_tx_pub_keys_to_extra(extra_inout, tx_pubkeys); + } + CHECK_AND_ASSERT_THROW_MES(success, "add carrot ephemeral pubkeys to extra: failed to add tx_extra fields"); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool try_load_carrot_ephemeral_pubkeys_from_extra(const std::vector &extra_fields, + std::vector &enote_ephemeral_pubkeys_out) +{ + //! @TODO: skip extra vector allocation for tx_pubkeys and copy directly + //! @TODO: longterm: skip allocating into std::vector before copying + + cryptonote::tx_extra_pub_key tx_pubkey; + cryptonote::tx_extra_additional_pub_keys tx_pubkeys; + if (cryptonote::find_tx_extra_field_by_type(extra_fields, tx_pubkey)) + { + enote_ephemeral_pubkeys_out = {raw_byte_convert(tx_pubkey.pub_key)}; + return true; + } + else if (cryptonote::find_tx_extra_field_by_type(extra_fields, tx_pubkeys)) + { + enote_ephemeral_pubkeys_out.resize(tx_pubkeys.data.size()); + + static_assert(sizeof(mx25519_pubkey) == sizeof(crypto::public_key)); + static_assert(std::is_trivially_copyable_v); + static_assert(std::is_trivially_copyable_v); + + memcpy(enote_ephemeral_pubkeys_out.data(), + tx_pubkeys.data.data(), + tx_pubkeys.data.size() * sizeof(mx25519_pubkey)); + + return true; + } + + return false; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +bool is_carrot_transaction_v1(const cryptonote::transaction_prefix &tx_prefix) +{ + if (tx_prefix.vout.empty()) + return false; + return tx_prefix.vout.at(0).target.type() == typeid(cryptonote::txout_to_carrot_v1); +} +//------------------------------------------------------------------------------------------------------------------- +input_context_t parse_carrot_input_context(const cryptonote::txin_gen &txin) +{ + return make_carrot_input_context_coinbase(txin.height); +} +//------------------------------------------------------------------------------------------------------------------- +input_context_t parse_carrot_input_context(const cryptonote::txin_to_key &txin) +{ + return make_carrot_input_context(txin.k_image); +} +//------------------------------------------------------------------------------------------------------------------- +bool parse_carrot_input_context(const cryptonote::txin_v &txin, input_context_t &input_context_out) +{ + struct parse_carrot_input_context_v_visitor + { + bool operator()(const cryptonote::txin_gen &txin) const + { input_context_out = parse_carrot_input_context(txin); return true; } + bool operator()(const cryptonote::txin_to_key &txin) const + { input_context_out = parse_carrot_input_context(txin); return true; } + bool operator()(const cryptonote::txin_to_script&) const { return false; } + bool operator()(const cryptonote::txin_to_scripthash&) const { return false; } + + input_context_t &input_context_out; + }; + + return boost::apply_visitor(parse_carrot_input_context_v_visitor{input_context_out}, txin); +} +//------------------------------------------------------------------------------------------------------------------- +bool parse_carrot_input_context(const cryptonote::transaction_prefix &tx_prefix, input_context_t &input_context_out) +{ + CHECK_AND_ASSERT_MES(!tx_prefix.vin.empty(), false, "parse_carrot_input_context: no input available"); + return parse_carrot_input_context(tx_prefix.vin.at(0), input_context_out); +} +//------------------------------------------------------------------------------------------------------------------- +std::uint64_t get_carrot_default_tx_extra_size(const std::size_t n_outputs) +{ + CARROT_CHECK_AND_THROW(n_outputs <= FCMP_PLUS_PLUS_MAX_OUTPUTS, + too_many_outputs, "n_outputs too high: " << n_outputs); + CARROT_CHECK_AND_THROW(n_outputs >= CARROT_MIN_TX_OUTPUTS, + too_few_outputs, " n_outputs too low: " << n_outputs); + + static constexpr std::uint64_t enc_pid_extra_field_size = + 8 /*pid*/ + + 1 /*TX_EXTRA_NONCE_ENCRYPTED_PAYMENT_ID*/ + + 1 /*nonce.size()*/ + + 1 /*tx_extra_field variant tag*/; + + static constexpr std::uint64_t x25519_pubkey_size = 32; + const std::size_t n_ephemeral = (n_outputs == 2) ? 1 : n_outputs; + const bool use_additional = n_ephemeral > 1; + const std::uint64_t ephemeral_pubkeys_field_size = + 1 /*tx_extra_field variant tag*/ + + (use_additional ? 1 : 0) /*vector size if applicable*/ + + (n_ephemeral * x25519_pubkey_size) /*actual pubkeys*/; + + return enc_pid_extra_field_size + ephemeral_pubkeys_field_size; +} +//------------------------------------------------------------------------------------------------------------------- +std::map get_fee_by_input_count(const std::size_t n_outputs, + const std::size_t extra_extra_len, + const std::uint64_t fee_per_weight) +{ + CARROT_CHECK_AND_THROW(extra_extra_len <= MAX_TX_EXTRA_SIZE, + integer_overflow, "extra extra len is too high"); + const std::uint64_t extra_len = get_carrot_default_tx_extra_size(n_outputs) + extra_extra_len; + CARROT_CHECK_AND_THROW(extra_len <= MAX_TX_EXTRA_SIZE, + integer_overflow, "total tx extra len after default fields is too high"); + + std::map fee_by_input_count; + for (std::size_t n_inputs = CARROT_MIN_TX_INPUTS; n_inputs <= FCMP_PLUS_PLUS_MAX_INPUTS; ++n_inputs) + { + const uint64_t tx_weight = cryptonote::get_fcmp_pp_transaction_weight_v1(n_inputs, n_outputs, extra_len); + CARROT_CHECK_AND_THROW(std::numeric_limits::max() / tx_weight > fee_per_weight, + integer_overflow, "fee_per_weight is too high and caused fee integer overflow"); + fee_by_input_count[n_inputs] = tx_weight * fee_per_weight; + } + + return fee_by_input_count; +} +//------------------------------------------------------------------------------------------------------------------- +bool try_load_carrot_extra_v1( + const std::vector &tx_extra, + std::vector &enote_ephemeral_pubkeys_out, + std::optional &encrypted_payment_id_out) +{ + std::vector tx_extra_fields; + cryptonote::parse_tx_extra(tx_extra, tx_extra_fields); + + return try_load_carrot_extra_v1(tx_extra_fields, enote_ephemeral_pubkeys_out, encrypted_payment_id_out); +} +//------------------------------------------------------------------------------------------------------------------- +bool try_load_carrot_extra_v1( + const std::vector &tx_extra_fields, + std::vector &enote_ephemeral_pubkeys_out, + std::optional &encrypted_payment_id_out) +{ + //ephemeral pubkeys: D_e + if (!try_load_carrot_ephemeral_pubkeys_from_extra(tx_extra_fields, enote_ephemeral_pubkeys_out)) + return false; + + //encrypted payment ID: pid_enc + encrypted_payment_id_out = std::nullopt; + cryptonote::tx_extra_nonce extra_nonce; + if (cryptonote::find_tx_extra_field_by_type(tx_extra_fields, extra_nonce)) + { + crypto::hash8 pid_enc_8; + if (cryptonote::get_encrypted_payment_id_from_tx_extra_nonce(extra_nonce.nonce, pid_enc_8)) + { + encrypted_payment_id_t &pid_enc = encrypted_payment_id_out.emplace(); + pid_enc = raw_byte_convert(pid_enc_8); + } + } + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +cryptonote::transaction store_carrot_to_transaction_v1(const std::vector &enotes, + const std::vector &key_images, + const rct::xmr_amount fee, + const encrypted_payment_id_t encrypted_payment_id) +{ + const size_t nins = key_images.size(); + const size_t nouts = enotes.size(); + + cryptonote::transaction tx; + tx.pruned = true; + tx.version = 2; + tx.unlock_time = 0; + tx.vin.reserve(nins); + tx.vout.reserve(nouts); + tx.extra.reserve(MAX_TX_EXTRA_SIZE); + tx.rct_signatures.type = carrot_v1_rct_type; + tx.rct_signatures.txnFee = fee; + tx.rct_signatures.ecdhInfo.reserve(nouts); + tx.rct_signatures.outPk.reserve(nouts); + + //inputs + for (const crypto::key_image &ki : key_images) + { + //L + tx.vin.emplace_back(cryptonote::txin_to_key{ //@TODO: can save 2 bytes by using slim input type + .amount = 0, + .key_offsets = {}, + .k_image = ki + }); + } + + //outputs + for (const CarrotEnoteV1 &enote : enotes) + { + //K_o,vt,anchor_enc + tx.vout.push_back(cryptonote::tx_out{0, cryptonote::txout_to_carrot_v1{ + .key = enote.onetime_address, + .view_tag = enote.view_tag, + .encrypted_janus_anchor = enote.anchor_enc + }}); + + //a_enc + rct::ecdhTuple &ecdh_tuple = tx.rct_signatures.ecdhInfo.emplace_back(); + memcpy(ecdh_tuple.amount.bytes, enote.amount_enc.bytes, sizeof(ecdh_tuple.amount)); + + //C_a + tx.rct_signatures.outPk.push_back(rct::ctkey{rct::key{}, enote.amount_commitment}); + } + + //ephemeral pubkeys: D_e + store_carrot_ephemeral_pubkeys_to_extra(enotes, tx.extra); + + //encrypted payment id: pid_enc + const crypto::hash8 pid_enc_8 = raw_byte_convert(encrypted_payment_id); + cryptonote::blobdata extra_nonce; + cryptonote::set_encrypted_payment_id_to_tx_extra_nonce(extra_nonce, pid_enc_8); + CHECK_AND_ASSERT_THROW_MES(cryptonote::add_extra_nonce_to_tx_extra(tx.extra, extra_nonce), + "store carrot to transaction v1: failed to add encrypted payment ID to tx_extra"); + + //finalize tx_extra + CHECK_AND_ASSERT_THROW_MES(cryptonote::sort_tx_extra(tx.extra, tx.extra, /*allow_partial=*/false), + "store carrot to transaction v1: failed to sort tx_extra"); + + return tx; +} +//------------------------------------------------------------------------------------------------------------------- +bool try_load_carrot_enote_from_transaction_v1(const cryptonote::transaction &tx, + const epee::span enote_ephemeral_pubkeys, + const std::size_t local_output_index, + CarrotEnoteV1 &enote_out) +{ + const rct::rctSigBase &rv = tx.rct_signatures; + + const size_t nins = tx.vin.size(); + const size_t nouts = tx.vout.size(); + const bool shared_ephemeral_pubkey = enote_ephemeral_pubkeys.size() == 1; + const size_t ephemeral_pubkey_index = shared_ephemeral_pubkey ? 0 : local_output_index; + + CHECK_AND_ASSERT_MES(nins, false, "try_load_carrot_enote_from_transaction_v1: no inputs"); + CHECK_AND_ASSERT_MES(ephemeral_pubkey_index < enote_ephemeral_pubkeys.size(), + false, + "try_load_carrot_enote_from_transaction_v1: not enough ephemeral pubkeys"); + CHECK_AND_ASSERT_MES(local_output_index < nouts, + false, + "try_load_carrot_enote_from_transaction_v1: not enough outputs"); + CHECK_AND_ASSERT_MES(nouts == rv.ecdhInfo.size(), + false, + "try_load_carrot_enote_from_transaction_v1: ecdhInfo wrong size"); + CHECK_AND_ASSERT_MES(nouts == rv.outPk.size(), + false, + "try_load_carrot_enote_from_transaction_v1: outPk wrong size"); + + const cryptonote::txout_target_v &t = tx.vout.at(local_output_index).target; + const cryptonote::txout_to_carrot_v1 * const c = boost::strict_get(&t); + CHECK_AND_ASSERT_MES(c, false, "try_load_carrot_enote_from_transaction_v1: wrong output type"); + + const cryptonote::txin_to_key * const inp = boost::strict_get(&tx.vin.at(0)); + CHECK_AND_ASSERT_MES(inp, false, "try_load_carrot_enote_from_transaction_v1: wrong input type"); + + //K_o + enote_out.onetime_address = c->key; + + //vt + enote_out.view_tag = c->view_tag; + + //anchor_enc + enote_out.anchor_enc = c->encrypted_janus_anchor; + + //L_1 + enote_out.tx_first_key_image = inp->k_image; + + //a_enc + memcpy(enote_out.amount_enc.bytes, rv.ecdhInfo.at(local_output_index).amount.bytes, sizeof(encrypted_amount_t)); + + //C_a + enote_out.amount_commitment = rv.outPk.at(local_output_index).mask; + + //D_e + enote_out.enote_ephemeral_pubkey = enote_ephemeral_pubkeys[ephemeral_pubkey_index]; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool try_load_carrot_from_transaction_v1(const cryptonote::transaction &tx, + std::vector &enotes_out, + std::vector &key_images_out, + rct::xmr_amount &fee_out, + std::optional &encrypted_payment_id_out) +{ + const rct::rctSigBase &rv = tx.rct_signatures; + fee_out = rv.txnFee; + + const size_t nins = tx.vin.size(); + const size_t nouts = tx.vout.size(); + + //inputs + key_images_out.resize(nins); + for (size_t i = 0; i < nins; ++i) + { + const cryptonote::txin_to_key * const k = boost::strict_get(&tx.vin.at(i)); + if (nullptr == k) + return false; + + //L + key_images_out[i] = k->k_image; + } + + //D_e, pid_enc + std::vector enote_ephemeral_pubkeys; + if (!try_load_carrot_extra_v1(tx.extra, enote_ephemeral_pubkeys, encrypted_payment_id_out)) + return false; + + const size_t n_ephemeral = enote_ephemeral_pubkeys.size(); + if (n_ephemeral == 0 || n_ephemeral > nouts) + return false; + + //outputs + enotes_out.resize(nouts); + for (size_t i = 0; i < nouts; ++i) + if (!try_load_carrot_enote_from_transaction_v1(tx, epee::to_span(enote_ephemeral_pubkeys), i, enotes_out[i])) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +cryptonote::transaction store_carrot_to_coinbase_transaction_v1( + const std::vector &enotes, + const cryptonote::blobdata &extra_nonce) +{ + const size_t nouts = enotes.size(); + const std::uint64_t block_index = enotes.at(0).block_index; + + cryptonote::transaction tx; + tx.pruned = false; + tx.version = 2; + tx.unlock_time = block_index + CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW; + tx.vin.reserve(1); + tx.vout.reserve(nouts); + tx.extra.reserve(MAX_TX_EXTRA_SIZE); + tx.rct_signatures.type = rct::RCTTypeNull; + + //input + tx.vin.emplace_back(cryptonote::txin_gen{.height = static_cast(block_index)}); + + //outputs + for (const CarrotCoinbaseEnoteV1 &enote : enotes) + { + //K_o,vt,anchor_enc,a + tx.vout.push_back(cryptonote::tx_out{enote.amount, + cryptonote::txout_to_carrot_v1{ + .key = enote.onetime_address, + .view_tag = enote.view_tag, + .encrypted_janus_anchor = enote.anchor_enc + } + }); + } + + //ephemeral pubkeys: D_e + store_carrot_ephemeral_pubkeys_to_extra(enotes, tx.extra); + + //add extra_nonce to tx_extra + CHECK_AND_ASSERT_THROW_MES(cryptonote::add_extra_nonce_to_tx_extra(tx.extra, extra_nonce), + "store_carrot_to_coinbase_transaction_v1: failed to add extra nonce to tx_extra"); + + // sort tx_extra + CHECK_AND_ASSERT_THROW_MES(cryptonote::sort_tx_extra(tx.extra, tx.extra), + "store_carrot_to_coinbase_transaction_v1: failed to sort tx_extra"); + + return tx; +} +//------------------------------------------------------------------------------------------------------------------- +cryptonote::transaction make_single_enote_carrot_coinbase_transaction_v1(const CarrotDestinationV1 &destination, + const rct::xmr_amount block_reward, + const std::uint64_t block_index, + const cryptonote::blobdata &extra_nonce) +{ + CHECK_AND_ASSERT_THROW_MES(!destination.is_subaddress, + "make_single_enote_carrot_coinbase_transaction_v1: subaddress are not allowed in miner transactions"); + CHECK_AND_ASSERT_THROW_MES(destination.payment_id == null_payment_id, + "make_single_enote_carrot_coinbase_transaction_v1: integrated addresses are not allowed in miner transactions"); + + const CarrotPaymentProposalV1 payment_proposal{ + .destination = destination, + .amount = block_reward, + .randomness = gen_janus_anchor() + }; + + std::vector enotes(1); + get_coinbase_enote_v1(payment_proposal, block_index, enotes.front()); + + return store_carrot_to_coinbase_transaction_v1(enotes, extra_nonce); +} +//------------------------------------------------------------------------------------------------------------------- +bool try_load_carrot_coinbase_enote_from_transaction_v1(const cryptonote::transaction &tx, + const epee::span enote_ephemeral_pubkeys, + const std::size_t local_output_index, + CarrotCoinbaseEnoteV1 &enote_out) +{ + CHECK_AND_ASSERT_MES(!tx.vin.empty(), false, "try_load_carrot_coinbase_enote_from_transaction_v1: no inputs"); + const cryptonote::txin_gen * const inp = boost::strict_get(&tx.vin.at(0)); + CHECK_AND_ASSERT_MES(inp, false, "try_load_carrot_coinbase_enote_from_transaction_v1: wrong input type"); + + //block_index + enote_out.block_index = inp->height; + + CHECK_AND_ASSERT_MES(local_output_index < tx.vout.size(), + false, + "try_load_carrot_coinbase_enote_from_transaction_v1: not enough outputs"); + const cryptonote::tx_out &o = tx.vout.at(local_output_index); + + //a + enote_out.amount = o.amount; + + const cryptonote::txout_to_carrot_v1 * const c = boost::strict_get(&o.target); + CHECK_AND_ASSERT_MES(c, false, "try_load_carrot_coinbase_enote_from_transaction_v1: wrong output type"); + + //K_o + enote_out.onetime_address = c->key; + + //vt + enote_out.view_tag = c->view_tag; + + //anchor_enc + enote_out.anchor_enc = c->encrypted_janus_anchor; + + CHECK_AND_ASSERT_MES(local_output_index < enote_ephemeral_pubkeys.size(), + false, + "try_load_carrot_coinbase_enote_from_transaction_v1: no enough ephemeral pubkeys"); + + //D_e + enote_out.enote_ephemeral_pubkey = enote_ephemeral_pubkeys[local_output_index]; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +bool try_load_carrot_from_coinbase_transaction_v1(const cryptonote::transaction &tx, + std::vector &enotes_out) +{ + const size_t nouts = tx.vout.size(); + + //D_e, pid_enc + std::vector enote_ephemeral_pubkeys; + std::optional dummy_encrypted_payment_id; + if (!try_load_carrot_extra_v1(tx.extra, enote_ephemeral_pubkeys, dummy_encrypted_payment_id)) + return false; + else if (enote_ephemeral_pubkeys.size() != nouts) + return false; + + //outputs + enotes_out.resize(nouts); + for (size_t i = 0; i < nouts; ++i) + if (!try_load_carrot_coinbase_enote_from_transaction_v1(tx, + epee::to_span(enote_ephemeral_pubkeys), + i, + enotes_out[i])) + return false; + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +rct::rctSigPrunable store_fcmp_proofs_to_rct_prunable_v1( + rct::BulletproofPlus &&bulletproof_plus, + const std::vector &rerandomized_outputs, + const std::vector &sal_proofs, + const fcmp_pp::FcmpMembershipProof &membership_proof, + const std::uint64_t fcmp_reference_block, + const std::uint8_t n_tree_layers) +{ + fcmp_pp::FcmpPpProof fcmp_pp_proof_bytes = fcmp_pp::fcmp_pp_proof_from_parts_v1(rerandomized_outputs, + sal_proofs, + membership_proof, + n_tree_layers); + + // extract C~ + const size_t n_inputs = rerandomized_outputs.size(); + rct::keyV pseudoOuts(n_inputs); + for (size_t i = 0; i < n_inputs; ++i) + memcpy(pseudoOuts[i].bytes, rerandomized_outputs.at(i).input.C_tilde, sizeof(rct::key)); + + return rct::rctSigPrunable{ + .rangeSigs = {}, + .bulletproofs = {}, + .bulletproofs_plus = {bulletproof_plus}, + .MGs = {}, + .CLSAGs = {}, + .pseudoOuts = std::move(pseudoOuts), + .reference_block = fcmp_reference_block, + .n_tree_layers = n_tree_layers, + .fcmp_pp = std::move(fcmp_pp_proof_bytes), + .fcmp_ver_helper_data = {} + }; +} +//------------------------------------------------------------------------------------------------------------------- +crypto::hash calculate_signable_fcmp_pp_transaction_hash(const cryptonote::transaction &tx) +{ + // see: rct::get_mlsag_prehash() + + CHECK_AND_ASSERT_THROW_MES(tx.rct_signatures.type == rct::RCTTypeFcmpPlusPlus, + "calculate_signable_fcmp_pp_transaction_hash() is not valid for non-FCMP++ txs"); + + std::stringstream ss; + binary_archive ar(ss); + CHECK_AND_ASSERT_THROW_MES(const_cast(tx.rct_signatures).serialize_rctsig_base(ar, 0, tx.vout.size()), + "could not calculate signable tx hash: failed to serialize rctSigBase"); + const std::string sig_base_blob = ss.str(); + + const std::vector subhashes = { + rct::hash2rct(cryptonote::get_transaction_prefix_hash(tx)), + rct::hash2rct(crypto::cn_fast_hash(sig_base_blob.data(), sig_base_blob.size())) + }; + + return rct::rct2hash(rct::cn_fast_hash(subhashes)); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace carrot diff --git a/src/carrot_impl/format_utils.h b/src/carrot_impl/format_utils.h new file mode 100644 index 00000000000..ee95c3c41ed --- /dev/null +++ b/src/carrot_impl/format_utils.h @@ -0,0 +1,221 @@ +// Copyright (c) 2024, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +//local headers +#include "carrot_core/carrot_enote_types.h" +#include "carrot_core/destination.h" +#include "cryptonote_basic/blobdatatype.h" +#include "cryptonote_basic/cryptonote_basic.h" +#include "cryptonote_basic/tx_extra.h" + +//third party headers + +//standard headers +#include +#include +#include +#include + +//forward declarations + +namespace carrot +{ +static constexpr std::uint8_t carrot_v1_rct_type = rct::RCTTypeFcmpPlusPlus; + +template +static inline T raw_byte_convert(const U &u) +{ + static_assert(sizeof(T) == sizeof(U)); + static_assert(std::is_trivially_copyable_v); + static_assert(std::has_unique_object_representations_v); + static_assert(std::has_unique_object_representations_v); + static_assert(alignof(T) == 1); + static_assert(alignof(U) == 1); + + T t; + memcpy(&t, &u, sizeof(T)); + return t; +} + +/** + * is_carrot_transaction_v1 - determine whether a transaction uses the Carrot addressing protocol + * param: tx_prefix - + * return: true iff this tx_prefix represents a v1 Carrot tx + * throw: too_few_outputs - iff tx_prefix.vout.empty() + */ +bool is_carrot_transaction_v1(const cryptonote::transaction_prefix &tx_prefix); +/** + * brief: parse_carrot_input_context - try parsing carrot input context from cryptonote transaction components + */ +input_context_t parse_carrot_input_context(const cryptonote::txin_gen &txin); +input_context_t parse_carrot_input_context(const cryptonote::txin_to_key &txin); +bool parse_carrot_input_context(const cryptonote::txin_v &txin, input_context_t &input_context_out); +bool parse_carrot_input_context(const cryptonote::transaction_prefix &tx_prefix, input_context_t &input_context_out); +/** + * brief: get_carrot_default_tx_extra_size - get default size of tx_extra for a Carrot v1 tx with no custom fields + * param: n_outputs - + * return: default size of tx_extra for a Carrot v1 tx with no custom fields + */ +std::uint64_t get_carrot_default_tx_extra_size(const std::size_t n_outputs); +/** + * brief: get_fee_by_input_count - get the fees for a carrot tx given # of outputs, extra size, and fee/weight rate + * param: n_outputs - + * param: extra_extra_len - byte size of extra fields, not including standard ones + * param: fee_per_weight - + * return: map of (# inputs, fee) pairs + * throw: carrot::integer_overflow if extra_extra_len or fee_per_weight are too high + * throw: too_few_outputs if n_outputs < CARROT_MIN_TX_OUTPUTS + * throw: too_many_outputs if n_outputs > FCMP_PLUS_PLUS_MAX_OUTPUTS + * + * This uses the weight function cryptonote::get_fcmp_pp_transaction_weight_v1() assuming that the + * default tx.extra fields are of size get_carrot_default_tx_extra_size(n_outputs). The fee is + * simply calculated as weight * fee_per_weight, overflow checked. + */ +std::map get_fee_by_input_count(const std::size_t n_outputs, + const std::size_t extra_extra_len, + const std::uint64_t fee_per_weight); +/** + * brief: try_load_carrot_extra_v1 - load Carrot info which is stored in tx_extra + * param: tx_extra_fields - + * outparam: enote_ephemeral_pubkeys_out - D_e + * outparam: encrypted_payment_id_out - pid_enc + */ +bool try_load_carrot_extra_v1( + const std::vector &tx_extra, + std::vector &enote_ephemeral_pubkeys_out, + std::optional &encrypted_payment_id_out); +bool try_load_carrot_extra_v1( + const std::vector &tx_extra_fields, + std::vector &enote_ephemeral_pubkeys_out, + std::optional &encrypted_payment_id_out); +/** + * brief: store_carrot_to_transaction_v1 - store non-coinbase Carrot info to a cryptonote::transaction + * param: enotes - + * param: key_images - + * param: fee - + * param: encrypted_payment_id - pid_enc + * return: a fully populated, pruned, non-coinbase transaction containing given Carrot information + */ +cryptonote::transaction store_carrot_to_transaction_v1(const std::vector &enotes, + const std::vector &key_images, + const rct::xmr_amount fee, + const encrypted_payment_id_t encrypted_payment_id); +/** + * brief: try_load_carrot_enote_from_transaction_v1 - load one non-coinbase Carrot enote from a cryptonote::transaction + * param: tx - + * param: enote_ephemeral_pubkeys - D_e + * param: local_output_index - + * outparam: enote_out - + * return: true iff enote was successfully parsed + */ +bool try_load_carrot_enote_from_transaction_v1(const cryptonote::transaction &tx, + const epee::span enote_ephemeral_pubkeys, + const std::size_t local_output_index, + CarrotEnoteV1 &enote_out); +/** + * brief: load_carrot_from_transaction_v1 - load non-coinbase Carrot info from a cryptonote::transaction + * param: tx - + * outparam: enotes_out - + * outparam: key_images_out - + * outparam: fee_out - + * outparam: encrypted_payment_id_out - + * return: Carrot enotes, key images, fee, and encrypted pid contained within a non-coinbase transaction + */ +bool try_load_carrot_from_transaction_v1(const cryptonote::transaction &tx, + std::vector &enotes_out, + std::vector &key_images_out, + rct::xmr_amount &fee_out, + std::optional &encrypted_payment_id_out); +/** + * brief: store_carrot_to_coinbase_transaction_v1 - store coinbase Carrot info to a cryptonote::transaction + * param: enotes - + * param: extra_nonce - + * return: a full coinbase transaction containing given Carrot information + */ +cryptonote::transaction store_carrot_to_coinbase_transaction_v1( + const std::vector &enotes, + const cryptonote::blobdata &extra_nonce); +/** + * brief: make_single_enote_carrot_coinbase_transaction_v1 - store one coinbase Carrot enote to a cryptonote::transaction + * param: destination - + * param: block_reward - + * param: block_index - + * param: extra_nonce - + * return: a full coinbase transaction containing given Carrot information + */ +cryptonote::transaction make_single_enote_carrot_coinbase_transaction_v1(const CarrotDestinationV1 &destination, + const rct::xmr_amount block_reward, + const std::uint64_t block_index, + const cryptonote::blobdata &extra_nonce); +/** + * brief: try_load_carrot_coinbase_enote_from_transaction_v1 - load one coinbase Carrot enote from a cryptonote::transaction + * param: tx - + * param: enote_ephemeral_pubkeys - + * param: local_output_index - + * outparam: enote_out - + * return: true iff enote was successfully parsed + */ +bool try_load_carrot_coinbase_enote_from_transaction_v1(const cryptonote::transaction &tx, + const epee::span enote_ephemeral_pubkeys, + const std::size_t local_output_index, + CarrotCoinbaseEnoteV1 &enote_out); +/** + * brief: try_load_carrot_from_coinbase_transaction_v1 - load coinbase Carrot info from a cryptonote::transaction + * param: tx - + * outparam: enotes_out - + * return: Carrot coinbase enotes and block index contained within a coinbase transaction + */ +bool try_load_carrot_from_coinbase_transaction_v1(const cryptonote::transaction &tx, + std::vector &enotes_out); +/** + * brief: store_fcmp_proofs_to_rct_prunable_v1 - + * param: bulletproof_plus - + * param: rerandomized_outputs - + * param: sal_proofs - + * param: membership_proof - + * param: fcmp_reference_block - + * param: n_tree_layers - + * return: prunable RCT signature data that can be attached to corresponding pruned tx + */ +rct::rctSigPrunable store_fcmp_proofs_to_rct_prunable_v1( + rct::BulletproofPlus &&bulletproof_plus, + const std::vector &rerandomized_outputs, + const std::vector &sal_proofs, + const fcmp_pp::FcmpMembershipProof &membership_proof, + const std::uint64_t fcmp_reference_block, + const std::uint8_t n_tree_layers); +/** + * brief: calculate_signable_transaction_hash - + * param: tx - pruned or full FCMP++ transaction + * throw: std::runtime_error if `tx` is not FCMP++ or fails to serialize + */ +crypto::hash calculate_signable_fcmp_pp_transaction_hash(const cryptonote::transaction &tx); + +} //namespace carrot diff --git a/src/carrot_impl/input_selection.cpp b/src/carrot_impl/input_selection.cpp new file mode 100644 index 00000000000..40a48680e2e --- /dev/null +++ b/src/carrot_impl/input_selection.cpp @@ -0,0 +1,609 @@ +// Copyright (c) 2024, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//paired header +#include "input_selection.h" + +//local headers +#include "carrot_core/config.h" +#include "carrot_core/exceptions.h" +#include "common/container_helpers.h" +#include "cryptonote_basic/cryptonote_format_utils.h" +#include "misc_log_ex.h" + +//third party headers +#include + +//standard headers +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "carrot_impl" + +namespace carrot +{ +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static std::ostream &operator<<(std::ostream &os, const CarrotSelectedInput &input) +{ + os << "{ onetime address = '" << onetime_address_ref(input.input) + << "', amount = " << cryptonote::print_money(input.amount) << " }"; + return os; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static std::ostream &operator<<(std::ostream &os, const InputCandidate &input) +{ + os << "{ core = " << input.core << ", block_index = " << input.block_index + << ", carrot = " << !input.is_pre_carrot << ", external = " << input.is_external << " }"; + return os; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static std::set set_union(const std::set &a, const std::set &b) +{ + std::set c = a; + c.merge(std::set(b)); + return c; +}; +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void stable_sort_indices_by_amount(const epee::span input_candidates, + std::vector &indices_inout) +{ + std::stable_sort(indices_inout.begin(), indices_inout.end(), + [input_candidates](const std::size_t a, const std::size_t b) -> bool + { + CARROT_CHECK_AND_THROW(a < input_candidates.size() && b < input_candidates.size(), + std::out_of_range, "input candidate index out of range"); + return input_candidates[a].core.amount < input_candidates[b].core.amount; + }); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static std::pair input_count_for_max_usable_money( + const epee::span input_candidates, + const std::set &selectable_inputs, + const std::size_t max_num_input_count, + const std::map &fee_by_input_count) +{ + const auto deref_amount = [input_candidates](const std::size_t idx) { + CARROT_CHECK_AND_THROW(idx < input_candidates.size(), + component_out_of_order, "selectable_inputs out of range of input candidates"); + return input_candidates[idx].core.amount; + }; + + return get_input_count_for_max_usable_money( + boost::make_transform_iterator(selectable_inputs.cbegin(), deref_amount), + boost::make_transform_iterator(selectable_inputs.cend(), deref_amount), + max_num_input_count, + fee_by_input_count); +} +//------------------------------------------------------------------------------------------------------------------- +template +static constexpr bool is_power_of_2(T v) +{ + static_assert(std::is_integral_v); + return (v > 0) && ((v & (v - 1)) == 0); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +int compare_input_candidate_same_ota(const InputCandidate &lhs, const InputCandidate &rhs) +{ + CARROT_CHECK_AND_THROW(onetime_address_ref(lhs.core.input) == onetime_address_ref(rhs.core.input), + component_out_of_order, "this function is not meant to compare inputs of different key images"); + + // First prefer the higher amount, + if (lhs.core.amount < rhs.core.amount) + return -1; + else if (lhs.core.amount > rhs.core.amount) + return 1; + + // Then prefer older, + if (lhs.block_index < rhs.block_index) + return 1; + else if (lhs.block_index > rhs.block_index) + return -1; + + // Then prefer Carrot over pre-Carrot. It should be computationally intractable for + // lhs.is_pre_carrot != rhs.is_pre_carrot, when they both successfully scan, but I haven't + // looked into it too deeply. + if (lhs.is_pre_carrot && !rhs.is_pre_carrot) + return -1; + else if (!lhs.is_pre_carrot && rhs.is_pre_carrot) + return 1; + + // Then prefer internal over external. Same tractability note as with is_pre_carrot. + if (lhs.is_external && !rhs.is_external) + return -1; + else if (!lhs.is_external && rhs.is_external) + return 1; + + return 0; +} +//------------------------------------------------------------------------------------------------------------------- +std::vector> form_preferred_input_candidate_subsets( + const epee::span input_candidates, + const std::uint32_t flags, + const bool is_normal_transfer) +{ + using namespace InputSelectionFlags; + + // Sanity check flags + const bool confused_qfs = (flags & ALLOW_PRE_CARROT_INPUTS_IN_NORMAL_TRANSFERS) && + !(flags & ALLOW_EXTERNAL_INPUTS_IN_NORMAL_TRANSFERS); + CARROT_CHECK_AND_THROW(!confused_qfs, std::invalid_argument, + "It does not make sense to allow pre-carrot inputs in normal transfers, but not external carrot inputs."); + + // 1. Compile map of best input candidates by onetime address to mitigate the "burning bug" for legacy enotes + std::unordered_map best_input_by_onetime_address; + for (size_t i = 0; i < input_candidates.size(); ++i) + { + const InputCandidate &input_candidate = input_candidates[i]; + const crypto::public_key onetime_address = onetime_address_ref(input_candidate.core.input); + auto it = best_input_by_onetime_address.find(onetime_address); + if (it == best_input_by_onetime_address.end()) + { + best_input_by_onetime_address[onetime_address] = i; + } + else + { + const InputCandidate &other_input_candidate = input_candidates[it->second]; + if (compare_input_candidate_same_ota(other_input_candidate, input_candidate) < 0) + it->second = i; + } + } + + // 2. Collect set of non-burned inputs + std::set all_non_burned_inputs; + for (const auto &best_input : best_input_by_onetime_address) + all_non_burned_inputs.insert(best_input.second); + + // 3. Partition into: + // a) Pre-carrot (no quantum forward secrecy) + // b) External carrot (quantum forward secret if public address not known) + // c) Internal carrot (always quantum forward secret unless secret keys known) + std::set pre_carrot_inputs; + std::set external_carrot_inputs; + std::set internal_inputs; + for (std::size_t candidate_idx : all_non_burned_inputs) + { + if (input_candidates[candidate_idx].is_pre_carrot) + pre_carrot_inputs.insert(candidate_idx); + else if (input_candidates[candidate_idx].is_external) + external_carrot_inputs.insert(candidate_idx); + else + internal_inputs.insert(candidate_idx); + } + + // 4. Calculate misc features + const bool must_use_internal = !(flags & ALLOW_EXTERNAL_INPUTS_IN_NORMAL_TRANSFERS) && is_normal_transfer; + const bool allow_mixed_externality = (flags & ALLOW_MIXED_INTERNAL_EXTERNAL) && !must_use_internal; + const bool must_use_carrot = !(flags & ALLOW_PRE_CARROT_INPUTS_IN_NORMAL_TRANSFERS) && is_normal_transfer; + const bool allow_mixed_carrotness = (flags & ALLOW_MIXED_CARROT_PRE_CARROT) && !must_use_carrot; + + // 5. We should prefer to spend non-forward-secret enotes in transactions where all the outputs + // are going back to ourself. Otherwise, if we spend these enotes while transferring money to + // another entity, an external observer who A) has a quantum computer, and B) knows one of their + // public addresses, will be able to trace the money transfer. Such an observer will always be + // able to tell which view-incoming keys / accounts these non-forward-secrets enotes belong to, + // their amounts, and where they're spent. So since they already know that information, churning + // back to oneself doesn't actually reveal that much more additional information. + const bool prefer_non_fs = !is_normal_transfer; + CARROT_CHECK_AND_THROW(!must_use_internal || !prefer_non_fs, + carrot_logic_error, "bug: must_use_internal AND prefer_non_fs are true"); + + // There is no "prefer pre-carrot" variable since in the case that we prefer spending + // non-forward-secret, we always prefer first spending pre-carrot over carrot, if it is allowed + + // 6. Define input_candidate_subsets and how to add to it + std::vector> input_candidate_subsets; + input_candidate_subsets.reserve(8); + const auto push_subset = [&input_candidate_subsets](const std::set &subset) + { + if (subset.empty()) return; + const auto subset_it = std::find(input_candidate_subsets.cbegin(), input_candidate_subsets.cend(), subset); + if (subset_it != input_candidate_subsets.cend()) return; // subset already present (could be more efficient) + input_candidate_subsets.push_back(subset); + }; + + // 7. Try dispatching for non-forward-secret input subsets, if preferred in this context + if (prefer_non_fs) + { + // try getting rid of pre-carrot enotes first, if allowed + if (!must_use_carrot) + push_subset(pre_carrot_inputs); + + // ... then external carrot + push_subset(external_carrot_inputs); + } + + // 8. Try dispatching for internal + push_subset(internal_inputs); + + // 9. Try dispatching for non-FS *after* internal, if allowed and not already tried + if (!must_use_internal || !prefer_non_fs) + { + // Spending non-FS inputs in a normal transfer transaction is not ideal, but at least + // when partition it like this, we aren't "dirtying" the carrot with the pre-carrot, and + // the internal with the external + if (!must_use_carrot) + push_subset(pre_carrot_inputs); + push_subset(external_carrot_inputs); + } + + // 10. Try dispatching for all non-FS (mixed pre-carrot & carrot external), if allowed + if (allow_mixed_carrotness) + { + // We're mixing carrot/pre-carrot spends here, but avoiding "dirtying" the internal + push_subset(set_union(pre_carrot_inputs, external_carrot_inputs)); + } + + // 11. Try dispatching for all carrot, if allowed + if (allow_mixed_externality) + { + // We're mixing internal & external carrot spends here, but avoiding "dirtying" the + // carrot spends with pre-carrot spends. This will be quantum forward secret iff the + // adversary doesn't know one of your public addresses + push_subset(set_union(external_carrot_inputs, internal_inputs)); + } + + //! @TODO: MRL discussion about whether step 11 or step 12 should go first. In other words, + // do we prefer to avoid dirtying internal, and protect against quantum adversaries + // who know your public addresses? Or do we prefer to avoid dirtying w/ pre-carrot, + // and protect against quantum adversaries with no special knowledge of your public + // addresses, but whose attacks are only relevant when spending pre-FCMP++ enotes? + + // 12. Try dispatching for everything, if allowed + if (allow_mixed_carrotness && allow_mixed_externality) + push_subset(all_non_burned_inputs); + + // Notice that we don't combine just the pre_carrot_inputs and internal_inputs by themselves + + return input_candidate_subsets; +} +//------------------------------------------------------------------------------------------------------------------- +std::vector get_input_counts_in_preferred_order(std::size_t max_n_inputs) +{ + // 1 or 2 randomly, then + // other ascending powers of 2, then + // other ascending positive numbers + + //! @TODO: MRL discussion about 2 vs 1 default input count when 1 input can pay. If we default + // to 1, then that may reveal more information about the amount, and reveals that one can't pay + // with 1 output when using 2. Vice versa, if we default to 2, then that means that one only + // owns 1 output when using 1. It may be the most advantageous to randomly switch between + // preferring 1 vs 2. See: https://lavalle.pl/planning/node437.html. Con to this approach: if we + // default to 1 over 2 always then there's scenarios where we net save tx fees and proving time. + + if (0 == max_n_inputs) + max_n_inputs = FCMP_PLUS_PLUS_MAX_INPUTS; + + CARROT_CHECK_AND_THROW(max_n_inputs <= FCMP_PLUS_PLUS_MAX_INPUTS, + too_many_inputs, "Max input count for input selection higher than consensus limit"); + + if (1 == max_n_inputs) + return {1}; + + const bool random_bit = 0 == (crypto::rand() & 0x01); + std::vector preferred_counts = random_bit + ? std::vector{2, 1} + : std::vector{1, 2}; + + // Get all powers of 2, > 2, up to max inputs. Prefer powers of 2 over non-powers of 2 for tx uniformity + std::vector powers_of_2; + std::size_t cur_power_of_2 = 4; + while (cur_power_of_2 <= max_n_inputs) + { + preferred_counts.push_back(cur_power_of_2); + cur_power_of_2 <<= 1; + } + + // Now get the remaining non-powers of 2 + for (std::size_t i = 3; i <= max_n_inputs; ++i) + { + if (is_power_of_2(i)) + continue; + preferred_counts.push_back(i); + } + + CHECK_AND_ASSERT_THROW_MES(preferred_counts.size() == max_n_inputs, "unexpected preferred counts"); + return preferred_counts; +} +//------------------------------------------------------------------------------------------------------------------- +select_inputs_func_t make_single_transfer_input_selector( + const epee::span input_candidates, + const epee::span policies, + const std::uint32_t flags, + const std::size_t max_n_inputs, + std::set *selected_input_indices_out) +{ + // input selector :D + return [=](const boost::multiprecision::uint128_t &nominal_output_sum, + const std::map &fee_by_input_count, + const std::size_t num_normal_payment_proposals, + const std::size_t num_selfsend_payment_proposals, + std::vector &selected_inputs_out) + { + using namespace InputSelectionFlags; + + // 1. Sanity checks valid arguments + const std::size_t n_candidates = input_candidates.size(); + CARROT_CHECK_AND_THROW(!fee_by_input_count.empty(), missing_components, "no provided allowed input count"); + CARROT_CHECK_AND_THROW(!policies.empty(), missing_components, "no input selection policies provided"); + CARROT_CHECK_AND_THROW(n_candidates, not_enough_money, "no input candidates provided"); + + // 2. Log + MDEBUG("Running single transfer input selector with " << input_candidates.size() << " candidates and " + << policies.size() << " policies, for " << num_normal_payment_proposals << " normal payment proposals, " + << num_selfsend_payment_proposals << " self-send payment proposals, " + << cryptonote::print_money(nominal_output_sum) + << " output sum, and fee range " << cryptonote::print_money(fee_by_input_count.cbegin()->second) + << "-" << cryptonote::print_money(fee_by_input_count.crbegin()->second)); + + // 3. Calculate minimum required input money sum for a given input count + const bool subtract_fee = flags & IS_KNOWN_FEE_SUBTRACTABLE; + std::map required_money_by_input_count; + for (const auto &fee_and_input_count : fee_by_input_count) + { + required_money_by_input_count[fee_and_input_count.first] = + nominal_output_sum + (subtract_fee ? 0 : fee_and_input_count.second); + } + const boost::multiprecision::uint128_t absolute_minimum_required_money + = required_money_by_input_count.cbegin()->second; + + // 4. Quick check of total money and single tx input count limited total money + boost::multiprecision::uint128_t total_candidate_money = 0; + for (const InputCandidate &input_candidate : input_candidates) + total_candidate_money += input_candidate.core.amount; + CARROT_CHECK_AND_THROW(total_candidate_money >= absolute_minimum_required_money, + not_enough_money, + "Not enough money in all inputs (" << cryptonote::print_money(total_candidate_money) + << ") to fund minimum output sum (" << cryptonote::print_money(absolute_minimum_required_money) << ')'); + + std::set all_idxs; for (std::size_t i = 0; i < input_candidates.size(); ++i) all_idxs.insert(i); + const std::pair max_usable_money = + input_count_for_max_usable_money(input_candidates, all_idxs, FCMP_PLUS_PLUS_MAX_INPUTS, fee_by_input_count); + CARROT_CHECK_AND_THROW(max_usable_money.second >= absolute_minimum_required_money, + not_enough_usable_money, + "Not enough usable money in top " << max_usable_money.first << " inputs (" + << cryptonote::print_money(max_usable_money.second) << ") to fund minimum output sum (" + << cryptonote::print_money(absolute_minimum_required_money) << ')'); + + // 5. Get preferred input candidate subsets + //! @TODO: dummy check num_normal_payment_proposals + const std::vector> input_candidate_subsets = form_preferred_input_candidate_subsets( + input_candidates, + flags, + num_normal_payment_proposals); + + // 6. Get preferred transaction input counts + const std::vector input_counts = get_input_counts_in_preferred_order(max_n_inputs); + + // 7. For each input candidate subset... + std::set selected_inputs_indices; + for (const std::set &input_candidate_subset : input_candidate_subsets) + { + if (selected_inputs_indices.size()) break; + + // Skip if not enough money in this selectable set for max number of tx inputs... + const auto max_usable_money = input_count_for_max_usable_money(input_candidates, + input_candidate_subset, FCMP_PLUS_PLUS_MAX_INPUTS, fee_by_input_count); + if (!max_usable_money.first) + continue; + else if (max_usable_money.second < required_money_by_input_count.at(max_usable_money.first)) + continue; + + // Debug log input candidate subset + MDEBUG("Trying to dispatch input selection on " << input_candidate_subset.size() << + "-input subset with tx max usable money: " << cryptonote::print_money(max_usable_money.second)); + for (const std::size_t selectable_index : input_candidate_subset) + { + const InputCandidate &input_candidate = input_candidates[selectable_index]; + MDEBUG(" " << input_candidate); + } + + // For each transaction input count... + for (const std::size_t n_inputs : input_counts) + { + if (selected_inputs_indices.size()) break; + + const boost::multiprecision::uint128_t &required_money = required_money_by_input_count.at(n_inputs); + + // Skip if not enough money in this selectable set for exact number of inputs... + const auto max_usable_money = input_count_for_max_usable_money(input_candidates, + input_candidate_subset, n_inputs, fee_by_input_count); + if (max_usable_money.first != n_inputs) + continue; + else if (max_usable_money.second < required_money) + continue; + + // After this point, we expect one of the policies to succeed, otherwise all input selection fails + + // at least one call to an input selection subroutine has enough usable money to work with + MDEBUG("Trying input selection with " << n_inputs << " tx inputs"); + + // Filter all dust out of subset unless ALLOW_DUST flag is provided + std::set candidate_subset_filtered = input_candidate_subset; + if (!(flags & ALLOW_DUST)) + { + const rct::xmr_amount dust_threshold = fee_by_input_count.at(n_inputs) + - (n_inputs > CARROT_MIN_TX_INPUTS ? fee_by_input_count.at(n_inputs - 1) : 0); + for (auto it = candidate_subset_filtered.cbegin(); it != candidate_subset_filtered.cend();) + { + if (*it >= input_candidates.size() || input_candidates[*it].core.amount <= dust_threshold) + it = candidate_subset_filtered.erase(it); + else + ++it; + } + } + + // For each input selection policy... + for (const input_selection_policy_t &policy : policies) + { + if (selected_inputs_indices.size()) break; + + policy(input_candidates, + candidate_subset_filtered, + n_inputs, + required_money, + selected_inputs_indices); + } + + // Check nominal success + CARROT_CHECK_AND_THROW(selected_inputs_indices.size(), + carrot_runtime_error, "provided input selection policies failed with enough usable money"); + CARROT_CHECK_AND_THROW(selected_inputs_indices.size() == n_inputs, + carrot_logic_error, "bug in policy: selected wrong number of inputs"); + + // Check selected indices were actually selectable + for (const std::size_t selected_inputs_index : selected_inputs_indices) + CARROT_CHECK_AND_THROW(candidate_subset_filtered.count(selected_inputs_index), + carrot_logic_error, "bug in policy: returned unselectable index"); + } + }; + + // 8. Sanity check indices + CARROT_CHECK_AND_THROW(!selected_inputs_indices.empty(), + not_enough_usable_money, + "No single allowed subset of candidates had enough money to fund payment proposals and fees for inputs"); + CARROT_CHECK_AND_THROW(*selected_inputs_indices.crbegin() < input_candidates.size(), + carrot_logic_error, "bug: selected inputs index out of range"); + + // 9. Check the sum of input amounts is great enough + const std::size_t num_selected = selected_inputs_indices.size(); + const boost::multiprecision::uint128_t required_money = required_money_by_input_count.at(num_selected); + boost::multiprecision::uint128_t input_amount_sum = 0; + for (const std::size_t idx : selected_inputs_indices) + input_amount_sum += input_candidates[idx].core.amount; + CARROT_CHECK_AND_THROW(input_amount_sum >= required_money, + carrot_logic_error, "bug: input selection returned successful without enough funds"); + + // 10. Collect selected inputs + selected_inputs_out.clear(); + selected_inputs_out.reserve(num_selected); + for (size_t selected_input_index : selected_inputs_indices) + selected_inputs_out.push_back(input_candidates[selected_input_index].core); + + if (selected_input_indices_out != nullptr) + *selected_input_indices_out = std::move(selected_inputs_indices); + }; +} +//------------------------------------------------------------------------------------------------------------------- +namespace ispolicy +{ +//------------------------------------------------------------------------------------------------------------------- +void select_greedy_aging(const epee::span input_candidates, + const std::set &selectable_inputs, + const std::size_t n_inputs, + const boost::multiprecision::uint128_t &required_money, + std::set &selected_inputs_indices_out) +{ + MTRACE(__func__ << ": n_inputs=" << n_inputs << ", selectable_inputs.size()=" << selectable_inputs.size()); + + selected_inputs_indices_out.clear(); + + CHECK_AND_ASSERT_MES(n_inputs,, "select_greedy_aging: n_inputs must be non-zero"); + CHECK_AND_ASSERT_MES(n_inputs <= selectable_inputs.size(),, + "select_greedy_aging: not enough inputs: " << selectable_inputs.size() << '/' << n_inputs); + + // Sort selectable inputs by amount + std::vector selectable_inputs_by_amount(selectable_inputs.cbegin(), selectable_inputs.cend()); + stable_sort_indices_by_amount(input_candidates, selectable_inputs_by_amount); + + // Select highest amount inputs and collect ordered multi-map of block indices of current selected inputs + boost::multiprecision::uint128_t input_amount_sum = 0; + std::multimap selected_indices_by_block_index; + for (size_t i = 0; i < n_inputs; ++i) + { + const std::size_t selectable_idx = selectable_inputs_by_amount.at(selectable_inputs_by_amount.size() - i - 1); + const InputCandidate &input = input_candidates[selectable_idx]; + input_amount_sum += input.core.amount; + selected_inputs_indices_out.insert(selectable_idx); + selected_indices_by_block_index.emplace(input.block_index, selectable_idx); + } + + // Check enough money + if (input_amount_sum < required_money) + { + MDEBUG("not enough money in " << n_inputs << " inputs: " << cryptonote::print_money(input_amount_sum)); + selected_inputs_indices_out.clear(); + return; + } + + // Right now, we have the highest amount inputs selected. Perform a greedy search to replace the newest inputs + // with the oldest possible input that still provides enough money + for (auto bi_it = selected_indices_by_block_index.rbegin(); bi_it != selected_indices_by_block_index.rend();) + { + std::uint64_t min_block_index = bi_it->first; + std::size_t input_of_min_block_index_input = bi_it->second; + const boost::multiprecision::uint128_t surplus = input_amount_sum - required_money; + const rct::xmr_amount currently_selected_amount = input_candidates[bi_it->second].core.amount; + const rct::xmr_amount lowest_replacement_amount = (currently_selected_amount > surplus) + ? boost::numeric_cast(currently_selected_amount - surplus) : 0; + const auto lower_amount_it = std::lower_bound(selectable_inputs_by_amount.cbegin(), + selectable_inputs_by_amount.cend(), lowest_replacement_amount, + [&input_candidates](const std::size_t selectable_idx, const rct::xmr_amount lowest_replacement_amount) + { + CARROT_CHECK_AND_THROW(selectable_idx < input_candidates.size(), + std::out_of_range, "input candidate index out of range"); + return input_candidates[selectable_idx].core.amount < lowest_replacement_amount; + }); + for (auto amount_it = lower_amount_it; amount_it != selectable_inputs_by_amount.cend(); ++amount_it) + { + const std::size_t potential_replacement_idx = *amount_it; + if (selected_inputs_indices_out.count(potential_replacement_idx)) + continue; + const InputCandidate &potential_replacement_input = input_candidates[potential_replacement_idx]; + if (potential_replacement_input.block_index < min_block_index) + { + min_block_index = potential_replacement_input.block_index; + input_of_min_block_index_input = potential_replacement_idx; + } + } + + if (input_of_min_block_index_input != bi_it->second) // i.e. found a replacement + { + selected_inputs_indices_out.erase(bi_it->second); + selected_inputs_indices_out.insert(input_of_min_block_index_input); + bi_it = tools::reverse_erase(selected_indices_by_block_index, bi_it); + selected_indices_by_block_index.emplace(min_block_index, input_of_min_block_index_input); + input_amount_sum -= currently_selected_amount; + input_amount_sum += input_candidates[input_of_min_block_index_input].core.amount; + CARROT_CHECK_AND_THROW(input_amount_sum >= required_money, + carrot_logic_error, "BUG: replaced an input with one of too low amount"); + } + else // no replacement, go to next input + { + ++bi_it; + } + } +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace ispolicy +} //namespace carrot diff --git a/src/carrot_impl/input_selection.h b/src/carrot_impl/input_selection.h new file mode 100644 index 00000000000..4c0dec4d5ca --- /dev/null +++ b/src/carrot_impl/input_selection.h @@ -0,0 +1,220 @@ +// Copyright (c) 2025, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +//local headers +#include "tx_proposal_utils.h" + +//third party headers + +//standard headers +#include + +//forward declarations + +namespace carrot +{ +struct InputCandidate +{ + CarrotSelectedInput core; + + bool is_pre_carrot; + bool is_external; + uint64_t block_index; +}; + +namespace InputSelectionFlags +{ + // Quantum forward secrecy (ON = unsafe) + static constexpr std::uint32_t ALLOW_EXTERNAL_INPUTS_IN_NORMAL_TRANSFERS = 1 << 0; + static constexpr std::uint32_t ALLOW_PRE_CARROT_INPUTS_IN_NORMAL_TRANSFERS = 1 << 1; + static constexpr std::uint32_t ALLOW_MIXED_INTERNAL_EXTERNAL = 1 << 2; + static constexpr std::uint32_t ALLOW_MIXED_CARROT_PRE_CARROT = 1 << 3; + + // Amount handling + static constexpr std::uint32_t IS_KNOWN_FEE_SUBTRACTABLE = 1 << 4; + static constexpr std::uint32_t ALLOW_DUST = 1 << 5; +} + +/** + * brief: input_selection_policy_t - a functor which implements N-input selection on a subset of candidates + * param: input_candidates - + * param: selectable_input_indices - subset of indices in `input_candidates` allowed to select from + * param: n_inputs - exact number of selected inputs should return + * param: required_money - sum of amounts of selected inputs should be greater than or equal to this value + * outparam: selected_inputs_indices_out - `n_inputs` subset of `selectable_input_indices`, of selected input indices + * + * To signify selection failure, `selected_inputs_indices_out` should be empty after end of call. + */ +using input_selection_policy_t = std::function input_candidates, + const std::set &selectable_input_indices, + std::size_t n_inputs, + const boost::multiprecision::uint128_t &required_money, + std::set &selected_inputs_indices_out +)>; + +/** + * brief: get_input_count_for_max_usable_money - get optimal number of inputs to maximum money minus fees + * param: user_amount_begin - rct::xmr_amount LegacyForwardIterator to beginning of user-defined amounts container + * param: user_amount_end - rct::xmr_amount LegacyForwardIterator to end of user-defined amounts container + * param: max_num_input_count - maximum number of inputs + * param: fee_by_input_count - fee indexed by number of inputs + * return: (N, X) where the X is the sum of the greatest N <= max_num_input_count amounts, + * maximizing X - F(N). F(N) is the fee for this transaction, given input count N + * + * This should correctly handle "almost-dust": inputs which are less than the fee, but greater than + * or equal to the difference of the fee compared to excluding that input, even if less than the + * difference of the fee compared to excluding that input plus more inputs. If this function returns + * N == 0, then there aren't enough usable funds, i.e. no N exists such that X - F(N) > 0. + */ +template +std::pair get_input_count_for_max_usable_money( + AmountFwdIt user_amount_begin, + const AmountFwdIt &user_amount_end, + const std::size_t max_num_input_count, + const std::map &fee_by_input_count); +/** + * brief: compare_input_candidate_same_ota - compare two input candidates who share a OTA; we can only choose one! + * param: lhs - + * param: rhs - + * return: 1 if lhs is better, -1 if rhs is better, 0 if neutral + * throw: component_out_of_order - iff rhs.core.key_image != lhs.core.key_image + * + * The better candidate is determined by criteria in descending order of importance as follows: + * 1. Amount (higher is better, duh) + * 2. Age (older is better for protection against double spend attacks) + * 3. Is pre-Carrot enote? (`false` is better for spending QFS) + * 4. Is external enote? (`false` is better for spending QFS) + */ +int compare_input_candidate_same_ota(const InputCandidate &lhs, const InputCandidate &rhs); +/** + * brief: form_preferred_input_candidate_subsets - make subsets of input candidates to try selection in preferred order + * param: input_candidates - slice to user-provided input candidates + * param: flags - see InputSelectionFlags namespace + * param: is_normal_transfer - true iff num normal non-dummy payments in tx to perform selection for is >= 1 + * return: ordered list of subsets (represented by 0-based indices) of input_candidates to try selection on + * + * This function also performs a burning bug check; no indices returned in any subset will reference + * an input candidate when another input candidate shares the same key image but is "better" as + * determined by compare_input_candidate_same_ki. + * + * Purpose: Mainly due to quantum forward secrecy properties of spending different types of Monero + * enotes, it isn't always preferable to be able to select all usable input candidates together in + * the same transaction. For example, spending internal Carrot enotes is always quantum forward + * secret, even when one's public Monero address is known. By contrast, spending a pre-Carrot enote + * is never quantum forward secret, even with no public address knowledge. As such, a spender should + * prefer not to spend these enotes in the same transaction, since the pre-Carrot enote will "taint" + * the quantum forward spending secrecy of the internal Carrot enote. Based on certain heuristics + * and user-provided flags, this function creates a list of subsets of input_candidates, in + * preferred order, to perform input selection on. + * + * Flags: + * * ALLOW_EXTERNAL_INPUTS_IN_NORMAL_TRANSFERS - external inputs in normal txs are allowed iff=1 + * * ALLOW_PRE_CARROT_INPUTS_IN_NORMAL_TRANSFERS - pre-carrot inputs in normal txs are allowed iff=1 + * * ALLOW_MIXED_INTERNAL_EXTERNAL - mixing internal/external inputs in any txs is allowed iff=1 + * * ALLOW_MIXED_CARROT_PRE_CARROT - mixing pre-carrot/carrot inputs in any txs is allowed iff=1 + * + * General rules (not in any specific order): + * * It should be preferred NOT to use external inputs in normal transfers + * * It should be preferred NOT to use pre-carrot inputs in normal transfers + * * It should be preferred NOT to mix internal/external inputs + * * It should be preferred NOT to mix pre-carrot/carrot inputs + * * It should be preferred YES to use external & pre-carrots inputs in self-send transactions + * + * In scenarios where a user-provided flag allows a non-preferred subset of input candidates, this + * function will FIRST add the subset of input candidates as if the user didn't provide that + * flag, and THEN add the non-preferred subset. For example, let's say that you pass input + * candidates span {A, B}, flags=ALLOW_EXTERNAL_INPUTS_IN_NORMAL_TRANSFERS, and + * is_normal_transfer=true. In this example, A is internal and B is external. The return + * value of this function will be {{0}, {1}, {0, 1}}. This means: "try input selection on 0 (A) + * first, then 1 (B), and then both {0, 1} (A & B)". In this example, although external inputs are + * *allowed* in normal transfers by the flag provided by the user, they are not *preferred*, so + * {A} comes before {B} in the subset list. Mixing is the least preferred, so {A, B} is the last + * subset. + * + * If unsure which flags to use, flags=0 is the "safest" option for input selection. Note that this + * completely disallows normal transfers for legacy key hierarchies, since inputs will never be + * internal due to the lack of the view-balance secret s_vb in legacy key hierarchies. + */ +std::vector> form_preferred_input_candidate_subsets( + const epee::span input_candidates, + const std::uint32_t flags, + const bool is_normal_transfer); +/** + * brief: get_input_counts_in_preferred_order - return list of tx input counts in order that we should prefer to select + * + * Transaction input counts are trivially observable on-chain, so picking a wrong input count when + * given the chance between multiple choices can have privacy consequences. The purpose of this + * function is to determine the order in which we should try to select a certain number of inputs. + * + * param: max_n_inputs - maximum number of inputs to use in selection, set 0 to to use consensus limit + */ +std::vector get_input_counts_in_preferred_order(std::size_t max_n_inputs); +/** + * brief: make_single_transfer_input_selector - a customizable input selector for single (i.e. not batched) transfers + * param: input_candidates - + * param: policies - slice of ISPs to attempt selection on, in order of user's preference + * param: flags - see InputSelectionFlags namespace + * outparam: selected_input_indices_out - selected indices into `input_candidates` (optional) + * return: input selector functor + * + * The returned input selector considers provided input candidates, and creates subsets of the + * candidates as according to `form_preferred_input_candidate_subsets`. Then, in the order of input + * counts as according to `get_input_counts_in_preferred_order`, finds the first pair (subset, input + * count) that contains enough "usable" money: a sum of money great enough to pay the nominal output + * sum, plus any required fees as according to the input count. Once that is found, ISPs are + * dispatched in provided user order until one succeeds. If none succeed for that pair, then the + * whole of input selection fails, we do not move onto the next (subset, input count) pair. + * + * SAFETY: The lifetime of objects referenced by `input_candidates` and `selected_input_indices_out` + * (if not null) must be valid at least as long as as the last call to the returned functor. + */ +select_inputs_func_t make_single_transfer_input_selector( + const epee::span input_candidates, + const epee::span policies, + const std::uint32_t flags, + const std::size_t max_n_inputs, + std::set *selected_input_indices_out); + +namespace ispolicy +{ +/** + * brief: select_greedy_aging - an ISP which generally attempts to select old outputs, but isn't necessarily optimal + */ +void select_greedy_aging(const epee::span, + const std::set&, + std::size_t, + const boost::multiprecision::uint128_t&, + std::set&); + +} //namespace ispolicy +} //namespace carrot + +#include "input_selection.inl" diff --git a/src/carrot_impl/input_selection.inl b/src/carrot_impl/input_selection.inl new file mode 100644 index 00000000000..18de27c267c --- /dev/null +++ b/src/carrot_impl/input_selection.inl @@ -0,0 +1,101 @@ +// Copyright (c) 2025, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +//local headers +#include "ringct/rctTypes.h" + +//third party headers +#include + +//standard headers +#include +#include +#include + +//forward declarations + +namespace carrot +{ +//------------------------------------------------------------------------------------------------------------------- +template +std::pair get_input_count_for_max_usable_money( + AmountFwdIt user_amount_begin, + const AmountFwdIt &user_amount_end, + const std::size_t max_num_input_count, + const std::map &fee_by_input_count) +{ + // maintain list of top `max_num_input_count` amounts + std::multiset top_amounts; + std::size_t n_amounts = 0; + for (;user_amount_begin != user_amount_end; ++user_amount_begin, ++n_amounts) + { + top_amounts.insert(*user_amount_begin); + if (top_amounts.size() > max_num_input_count) + top_amounts.erase(top_amounts.cbegin()); + } + + std::size_t input_count = 0; + boost::multiprecision::uint128_t cumulative_input_sum = 0; + std::size_t best_input_count = 0; + boost::multiprecision::uint128_t best_cumulative_input_sum = 0; + boost::multiprecision::uint128_t best_net_input_sum = 0; + // for all valid input counts (or existing amount counts, whichever is fewer)... + for (auto top_amount_it = top_amounts.crbegin(); top_amount_it != top_amounts.crend(); ++top_amount_it) + { + // in order of largest amount to least... + const rct::xmr_amount amount = *top_amount_it; + + // get fee of next number of inputs if available in F(N), else stop. + cumulative_input_sum += amount; + ++input_count; + const auto fee_it = fee_by_input_count.find(input_count); + if (fee_it == fee_by_input_count.cend()) + break; + const rct::xmr_amount current_fee = fee_it->second; + + // if the input amount total is greater than fee for that number of inputs... + if (cumulative_input_sum > current_fee) + { + const boost::multiprecision::uint128_t net_input_sum = cumulative_input_sum - current_fee; + // and if that input amount total minus its fees is better than any other total observed yet... + if (net_input_sum > best_net_input_sum) + { + // set that as best. + best_input_count = input_count; + best_cumulative_input_sum = cumulative_input_sum; + best_net_input_sum = net_input_sum; + } + } + } + + return {best_input_count, best_cumulative_input_sum}; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace carrot diff --git a/src/carrot_impl/key_image_device.h b/src/carrot_impl/key_image_device.h new file mode 100644 index 00000000000..27100b48d18 --- /dev/null +++ b/src/carrot_impl/key_image_device.h @@ -0,0 +1,68 @@ +// Copyright (c) 2025, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +//local headers +#include "output_opening_types.h" + +//third party headers + +//standard headers + +//forward declarations + +namespace carrot +{ +struct key_image_device +{ + /** + * brief: derive a key image for an enote given its opening hint + * param: opening_hint - + * return: L = x Hp(O) given O and some y s.t. O = x G + y T + * + * The _implementor_ is expected to validate that this enote passes the burning bug check, Janus check, etc. + */ + virtual crypto::key_image derive_key_image(const OutputOpeningHintVariant &opening_hint) const = 0; + + /** + * brief: derive a key image for an enote given it's sender opening and address index + * param: sender_extension_g - k^g_o + * param: onetime_address - K_o + * param: subaddr_index - j + * param: use_biased - true if should use biased hash-to-point function + * return: L = x Hp(K_o) given K_o and some y s.t. K_o = x G + y T + * + * The _caller_ is expected to validate that this enote passes the burning bug check, Janus check, etc. + */ + virtual crypto::key_image derive_key_image_prescanned(const crypto::secret_key &sender_extension_g, + const crypto::public_key &onetime_address, + const subaddress_index_extended &subaddr_index, + const bool use_biased) const = 0; +}; +} //namespace carrot diff --git a/src/carrot_impl/key_image_device_composed.cpp b/src/carrot_impl/key_image_device_composed.cpp new file mode 100644 index 00000000000..9df8ab18534 --- /dev/null +++ b/src/carrot_impl/key_image_device_composed.cpp @@ -0,0 +1,174 @@ +// 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_image_device_composed.h" + +//local headers +#include "address_utils.h" +#include "carrot_core/address_utils.h" +#include "carrot_core/exceptions.h" +#include "misc_log_ex.h" +#include "ringct/rctOps.h" + +//third party headers + +//standard headers + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "carrot_impl.device" + +namespace +{ +struct make_local_device_error +{ + int code; + std::string func; + + make_local_device_error(int code, std::string func): code(code), func(std::move(func)) {} + + carrot::device_error operator()(std::string msg) + { + return carrot::device_error("Default", "key_image_device_composed", std::move(func), std::move(msg), code); + } +}; +} //anonymous namespace + +namespace carrot +{ +//------------------------------------------------------------------------------------------------------------------- +key_image_device_composed::key_image_device_composed(std::shared_ptr k_generate_image_dev, + std::shared_ptr addr_dev, + std::shared_ptr s_view_balance_dev, + std::shared_ptr k_view_incoming_dev): + m_legacy_k_generate_image_dev(s_view_balance_dev ? + std::shared_ptr{} : std::move(k_generate_image_dev)), + m_carrot_k_generate_image_dev(s_view_balance_dev + ? std::move(k_generate_image_dev) : std::shared_ptr{}), + m_addr_dev(std::move(addr_dev)), + m_s_view_balance_dev(std::move(s_view_balance_dev)), + m_k_view_incoming_dev(std::move(k_view_incoming_dev)) +{} +//------------------------------------------------------------------------------------------------------------------- +key_image_device_composed::key_image_device_composed( + std::shared_ptr legacy_k_generate_image_dev, + std::shared_ptr carrot_k_generate_image_dev, + std::shared_ptr addr_dev, + std::shared_ptr s_view_balance_dev, + std::shared_ptr k_view_incoming_dev): + m_legacy_k_generate_image_dev(std::move(legacy_k_generate_image_dev)), + m_carrot_k_generate_image_dev(std::move(carrot_k_generate_image_dev)), + m_addr_dev(std::move(addr_dev)), + m_s_view_balance_dev(std::move(s_view_balance_dev)), + m_k_view_incoming_dev(std::move(k_view_incoming_dev)) +{} +//------------------------------------------------------------------------------------------------------------------- +crypto::key_image key_image_device_composed::derive_key_image(const OutputOpeningHintVariant &opening_hint) const +{ + const crypto::public_key onetime_address = onetime_address_ref(opening_hint); + const subaddress_index_extended subaddr_index = subaddress_index_ref(opening_hint); + + crypto::public_key main_address_spend_pubkeys[2]; + const std::size_t n_main_addrs = get_all_main_address_spend_pubkeys(*m_addr_dev, main_address_spend_pubkeys); + CARROT_CHECK_AND_THROW(n_main_addrs > 0, make_local_device_error(-4, "derive_key_image"), + "Address device supports no known address derivation scheme"); + + // get k^g_o, k^t_o + crypto::secret_key sender_extension_g; + crypto::secret_key sender_extension_t; + if (!try_scan_opening_hint_sender_extensions(opening_hint, + {main_address_spend_pubkeys, n_main_addrs}, + m_k_view_incoming_dev.get(), + m_s_view_balance_dev.get(), + sender_extension_g, + sender_extension_t)) + { + throw make_local_device_error{-3, "derive_key_image"}("enote scan failed"); + } + + return this->derive_key_image_prescanned(sender_extension_g, + onetime_address, + subaddr_index, + use_biased_hash_to_point(opening_hint)); +} +//------------------------------------------------------------------------------------------------------------------- +crypto::key_image key_image_device_composed::derive_key_image_prescanned(const crypto::secret_key &sender_extension_g, + const crypto::public_key &onetime_address, + const subaddress_index_extended &subaddr_index, + const bool use_biased) const +{ + // resolve generate-image device + const generate_image_key_device *used_k_generate_image_dev = nullptr; + switch (subaddr_index.derive_type) + { + case AddressDeriveType::Auto: + CARROT_THROW(make_local_device_error(-11, "derive_key_image_prescanned"), + "Cannot use Auto derive type for opening key images"); + break; + case AddressDeriveType::PreCarrot: + used_k_generate_image_dev = m_legacy_k_generate_image_dev.get(); + break; + case AddressDeriveType::Carrot: + used_k_generate_image_dev = m_carrot_k_generate_image_dev.get(); + break; + default: + CARROT_THROW(make_local_device_error(-9, "derive_key_image_prescanned"), + "Unrecognized subaddress index derive type"); + } + CARROT_CHECK_AND_THROW(used_k_generate_image_dev != nullptr, + make_local_device_error(-10, "derive_key_image_prescanned"), + "No generate-image device present for given subaddress index type"); + + // [legacy] L_partial = k_s Hp(K_o) + // [carrot] L_partial = k_gi Hp(K_o) + rct::key partial_key_image + = rct::pt2rct(used_k_generate_image_dev->generate_image_scalar_mult_hash_to_point(onetime_address, use_biased)); + + // I = Hp(K_o) + crypto::ec_point key_image_generator; + crypto::derive_key_image_generator(onetime_address, use_biased, key_image_generator); + + // get k^j_subscal, k^j_subext + crypto::secret_key subaddr_extension_g; + crypto::secret_key carrot_subaddr_scalar; + m_addr_dev->get_address_openings(subaddr_index, subaddr_extension_g, carrot_subaddr_scalar); + + // L_partial = k^j_subscal L_partial + partial_key_image = rct::scalarmultKey(partial_key_image, rct::sk2rct(carrot_subaddr_scalar)); + + // L_partial = k^j_subext I + L_partial + rct::key tmp; + tmp = rct::scalarmultKey(rct::pt2rct(key_image_generator), rct::sk2rct(subaddr_extension_g)); + partial_key_image = rct::addKeys(tmp, partial_key_image); + + // L = k^g_o I + L_partial + tmp = rct::scalarmultKey(rct::pt2rct(key_image_generator), rct::sk2rct(sender_extension_g)); + return rct::rct2ki(rct::addKeys(tmp, partial_key_image)); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace carrot diff --git a/src/carrot_impl/key_image_device_composed.h b/src/carrot_impl/key_image_device_composed.h new file mode 100644 index 00000000000..ac8f4653654 --- /dev/null +++ b/src/carrot_impl/key_image_device_composed.h @@ -0,0 +1,74 @@ +// Copyright (c) 2025, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +//local headers +#include "address_device.h" +#include "key_image_device.h" + +//third party headers + +//standard headers +#include + +//forward declarations + +namespace carrot +{ +class key_image_device_composed: public key_image_device +{ +public: + /// @brief single-type derivation device + key_image_device_composed(std::shared_ptr k_generate_image_dev, + std::shared_ptr addr_dev, + std::shared_ptr s_view_balance_dev, + std::shared_ptr k_view_incoming_dev); + + /// @brief hybrid-type derivation device + key_image_device_composed(std::shared_ptr legacy_k_generate_image_dev, + std::shared_ptr carrot_k_generate_image_dev, + std::shared_ptr addr_dev, + std::shared_ptr s_view_balance_dev, + std::shared_ptr k_view_incoming_dev); + + crypto::key_image derive_key_image(const OutputOpeningHintVariant &opening_hint) const override; + + crypto::key_image derive_key_image_prescanned(const crypto::secret_key &sender_extension_g, + const crypto::public_key &onetime_address, + const subaddress_index_extended &subaddr_index, + const bool use_biased) const override; + +protected: + std::shared_ptr m_legacy_k_generate_image_dev; + std::shared_ptr m_carrot_k_generate_image_dev; + std::shared_ptr m_addr_dev; + std::shared_ptr m_s_view_balance_dev; + std::shared_ptr m_k_view_incoming_dev; +}; +} //namespace carrot diff --git a/src/carrot_impl/key_image_device_precomputed.cpp b/src/carrot_impl/key_image_device_precomputed.cpp new file mode 100644 index 00000000000..07b38b8ad3b --- /dev/null +++ b/src/carrot_impl/key_image_device_precomputed.cpp @@ -0,0 +1,77 @@ +// 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_image_device_precomputed.h" + +//local headers +#include "carrot_core/device.h" +#include "carrot_core/exceptions.h" +#include "misc_log_ex.h" + +//third party headers + +//standard headers + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "carrot_impl" + +namespace carrot +{ +//------------------------------------------------------------------------------------------------------------------- +crypto::key_image key_image_device_precompted::derive_key_image(const OutputOpeningHintVariant &opening_hint) const +{ + const auto local_get_device_error = [](std::string msg) { + return device_error("Default", "key_image_device_precompted", "derive_key_image", std::move(msg), -1); + }; + + const crypto::public_key onetime_address = onetime_address_ref(opening_hint); + const auto it = m_key_image_map.find(onetime_address); + CARROT_CHECK_AND_THROW(it != m_key_image_map.cend(), + local_get_device_error, "missing onetime address in map: " << onetime_address); + return it->second; +} +//------------------------------------------------------------------------------------------------------------------- +crypto::key_image key_image_device_precompted::derive_key_image_prescanned( + const crypto::secret_key &sender_extension_g, + const crypto::public_key &onetime_address, + const subaddress_index_extended &subaddr_index, + const bool use_biased) const +{ + const auto local_get_device_error = [](std::string msg) { + return device_error("Default", "key_image_device_precompted", "derive_key_image_prescanned", std::move(msg), + -1); + }; + + const auto it = m_key_image_map.find(onetime_address); + CARROT_CHECK_AND_THROW(it != m_key_image_map.cend(), + local_get_device_error, "missing onetime address in map: " << onetime_address); + return it->second; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace carrot diff --git a/src/carrot_impl/key_image_device_precomputed.h b/src/carrot_impl/key_image_device_precomputed.h new file mode 100644 index 00000000000..26e895f1697 --- /dev/null +++ b/src/carrot_impl/key_image_device_precomputed.h @@ -0,0 +1,60 @@ +// Copyright (c) 2025, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +//local headers +#include "key_image_device.h" + +//third party headers + +//standard headers +#include + +//forward declarations + +namespace carrot +{ +class key_image_device_precompted: public key_image_device +{ +public: + key_image_device_precompted(std::unordered_map &&key_image_map): + m_key_image_map(std::move(key_image_map)) + {} + + crypto::key_image derive_key_image(const OutputOpeningHintVariant &opening_hint) const override; + + crypto::key_image derive_key_image_prescanned(const crypto::secret_key &sender_extension_g, + const crypto::public_key &onetime_address, + const subaddress_index_extended &subaddr_index, + const bool use_biased) const override; + +protected: + std::unordered_map m_key_image_map; +}; +} //namespace carrot diff --git a/src/carrot_impl/multi_tx_proposal_utils.cpp b/src/carrot_impl/multi_tx_proposal_utils.cpp new file mode 100644 index 00000000000..b92388120fc --- /dev/null +++ b/src/carrot_impl/multi_tx_proposal_utils.cpp @@ -0,0 +1,253 @@ +// Copyright (c) 2025, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//paired header +#include "multi_tx_proposal_utils.h" + +//local headers +#include "carrot_core/config.h" +#include "carrot_core/exceptions.h" +#include "common/container_helpers.h" +#include "format_utils.h" +#include "misc_log_ex.h" + +//third party headers +#include + +//standard headers + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "carrot_impl" + +namespace carrot +{ +//------------------------------------------------------------------------------------------------------------------- +void make_multiple_carrot_transaction_proposals_transfer( + std::vector &&normal_payment_proposals, + std::vector &&selfsend_payment_proposals, + const rct::xmr_amount fee_per_weight, + const std::vector &extra, + std::vector &&input_candidates, + const epee::span input_selection_policies, + const std::uint32_t input_selection_flags, + const std::size_t max_n_inputs, + const crypto::public_key &change_address_spend_pubkey, + const subaddress_index_extended &change_address_index, + const std::set &subtractable_normal_payment_proposals, + const std::set &subtractable_selfsend_payment_proposals, + std::vector &tx_proposals_out) +{ + tx_proposals_out.clear(); + + // clear out 0-amount input candidates to speed things up / reduce logging down the line + tools::for_all_in_vector_erase_no_preserve_order_if(input_candidates, + [](const InputCandidate &ic) { return ic.core.amount == 0; } + ); + + static constexpr std::size_t max_n_dsts_per_tx = FCMP_PLUS_PLUS_MAX_OUTPUTS - 1; + + std::size_t n_dsts = normal_payment_proposals.size() + selfsend_payment_proposals.size(); + tx_proposals_out.reserve((n_dsts + max_n_dsts_per_tx - 1) / max_n_dsts_per_tx); + while (n_dsts) + { + // build payment proposals and subtractable info + std::vector tx_normal_payment_proposals; + std::vector tx_selfsend_payment_proposals; + std::set tx_subtractable_normal_payment_proposals; + std::set tx_subtractable_selfsend_payment_proposals; + std::size_t n_tx_dsts = 0; + while (n_dsts && n_tx_dsts < max_n_dsts_per_tx) + { + if (normal_payment_proposals.size()) + { + tx_normal_payment_proposals.push_back(normal_payment_proposals.back()); + normal_payment_proposals.pop_back(); + if (subtractable_normal_payment_proposals.count(normal_payment_proposals.size())) + tx_subtractable_normal_payment_proposals.insert(tx_normal_payment_proposals.size() - 1); + } + else // selfsend_payment_proposals.size() + { + CARROT_CHECK_AND_THROW(selfsend_payment_proposals.size(), + carrot::carrot_logic_error, "bug in payment proposal counting"); + tx_selfsend_payment_proposals.push_back(selfsend_payment_proposals.back()); + selfsend_payment_proposals.pop_back(); + if (subtractable_selfsend_payment_proposals.count(selfsend_payment_proposals.size())) + tx_subtractable_selfsend_payment_proposals.insert(tx_selfsend_payment_proposals.size() - 1); + } + + ++n_tx_dsts; + --n_dsts; + } + + // make input selector + std::set selected_transfer_indices; + carrot::select_inputs_func_t select_inputs = make_single_transfer_input_selector( + epee::to_span(input_candidates), + input_selection_policies, + input_selection_flags, + max_n_inputs, + &selected_transfer_indices); + + // make proposal + carrot::CarrotTransactionProposalV1 &tx_proposal = tx_proposals_out.emplace_back(); + carrot::make_carrot_transaction_proposal_v1_transfer( + tx_normal_payment_proposals, + tx_selfsend_payment_proposals, + fee_per_weight, + extra, + std::move(select_inputs), + change_address_spend_pubkey, + change_address_index, + tx_subtractable_normal_payment_proposals, + tx_subtractable_selfsend_payment_proposals, + tx_proposal); + + // update `input_candidates` for next proposal by removing already-selected one-time addresses + std::unordered_set used_otas; + for (const carrot::InputProposalV1 &input_proposal : tx_proposal.input_proposals) + used_otas.insert(onetime_address_ref(input_proposal)); + tools::for_all_in_vector_erase_no_preserve_order_if(input_candidates, + [&used_otas](const auto &ic) -> bool { return used_otas.count(onetime_address_ref(ic.core.input)); } + ); + } +} +//------------------------------------------------------------------------------------------------------------------- +void make_multiple_carrot_transaction_proposals_sweep( + const std::vector &normal_payment_proposals, + const std::vector &selfsend_payment_proposals, + const rct::xmr_amount fee_per_weight, + const std::vector &extra, + std::vector &&selected_inputs, + const crypto::public_key &change_address_spend_pubkey, + const subaddress_index_extended &change_address_index, + const bool ignore_dust, + std::vector &tx_proposals_out) +{ + tx_proposals_out.clear(); + + const std::size_t n_dests_per_tx = normal_payment_proposals.size() + selfsend_payment_proposals.size(); + CARROT_CHECK_AND_THROW(selected_inputs.size(), carrot::too_few_inputs, "no inputs provided"); + CARROT_CHECK_AND_THROW(n_dests_per_tx, carrot::too_few_outputs, "sweep must have at least one destination"); + CARROT_CHECK_AND_THROW(n_dests_per_tx <= FCMP_PLUS_PLUS_MAX_OUTPUTS, + carrot::too_many_outputs, "too many sweep destinations per transaction"); + + // Check for duplicate one-time addresses in `selected_inputs` + std::unordered_set input_onetime_address; + for (const CarrotSelectedInput &selected_input : selected_inputs) + input_onetime_address.insert(onetime_address_ref(selected_input.input)); + CARROT_CHECK_AND_THROW(input_onetime_address.size() == selected_inputs.size(), + component_out_of_order, "found duplicate one-time addressess when sweeping"); + + // Sort `selected_inputs` by amount, descending. We do this so that we can try to pair + // small inputs with big inputs so that hopefully each tx can pay for its own fees + std::sort(selected_inputs.begin(), selected_inputs.end(), + [](const carrot::CarrotSelectedInput &a, const carrot::CarrotSelectedInput &b) { return a.amount > b.amount; }); + + const std::size_t n_outputs = std::max(CARROT_MIN_TX_OUTPUTS, normal_payment_proposals.size() + + std::max(1, selfsend_payment_proposals.size())); + + const std::map fee_by_input_count = get_fee_by_input_count(n_outputs, extra.size(), fee_per_weight); + + tx_proposals_out.reserve((selected_inputs.size() + FCMP_PLUS_PLUS_MAX_INPUTS - 1) / FCMP_PLUS_PLUS_MAX_INPUTS); + + // callback for calling `get_input_count_for_max_usable_money()` on some slice of inputs starting at `window_offset` + const auto get_input_count_for_max_usable_money_in_window = + [&selected_inputs, &fee_by_input_count](const std::size_t window_offset) + { + CARROT_CHECK_AND_THROW(window_offset <= selected_inputs.size(), + carrot_logic_error, "selected_offset out of range"); + const std::size_t window_size = std::min(FCMP_PLUS_PLUS_MAX_INPUTS, + selected_inputs.size() - window_offset); + + const auto deref_amount = [](const CarrotSelectedInput &i) { return i.amount; }; + + const auto window_begin = selected_inputs.cbegin() + window_offset; + const auto window_end = window_begin + window_size; + + return carrot::get_input_count_for_max_usable_money( + boost::make_transform_iterator(window_begin, deref_amount), + boost::make_transform_iterator(window_end, deref_amount), + window_size, + fee_by_input_count).first; + }; + + // To try to maximize the total amount of money sent in a sweep, we first order the amounts in + // ascending order. Then from low to high, we slide a "window" over a slice of contiguous + // amounts and call `get_input_count_for_max_usable_money_in_window` over that window. That call + // will tell how many of the inputs in that window are dusty. If any are dusty, we should keep + // sliding the window until we can't. Once that window stops, we use that window for the inputs + // for the current transaction. Repeat until no inputs are left or only dusty inputs are left. + + // while some selection of inputs of the highest amounts at some input count yields a net positive output sum... + while (get_input_count_for_max_usable_money_in_window(0)) + { + // slide a window in ascending amount order until all inputs in that window yield a net positive output sum... + const std::size_t max_window_size = std::min(FCMP_PLUS_PLUS_MAX_INPUTS, selected_inputs.size()); + std::size_t window_offset = selected_inputs.size() - max_window_size; + std::size_t n_tx_inputs = 0; + do + { + n_tx_inputs = get_input_count_for_max_usable_money_in_window(window_offset); + if (0 == window_offset || max_window_size == n_tx_inputs) + break; + --window_offset; + } + while (1); + + // if that doesn't happen, stop early + if (!n_tx_inputs) + break; + + // calculate iterators into `selected_inputs` based on the input count returned by `get_input_count_for_max_...` + const std::size_t window_offset_end = window_offset + n_tx_inputs; + CARROT_CHECK_AND_THROW(window_offset_end <= selected_inputs.size(), + carrot_logic_error, "BUG: window_offset_end out of range"); + const auto tx_input_begin = selected_inputs.cbegin() + window_offset; + const auto tx_input_end = selected_inputs.cbegin() + window_offset_end; + + // pop those selected inputs out of `selected_inputs` into `tx_selected_inputs` + std::vector tx_selected_inputs; + tx_selected_inputs.reserve(window_offset_end - window_offset); + tx_selected_inputs.insert(tx_selected_inputs.cend(), tx_input_begin, tx_input_end); + selected_inputs.erase(tx_input_begin, tx_input_end); + + carrot::make_carrot_transaction_proposal_v1_sweep(normal_payment_proposals, + selfsend_payment_proposals, + fee_per_weight, + extra, + std::move(tx_selected_inputs), + change_address_spend_pubkey, + change_address_index, + tx_proposals_out.emplace_back()); + } + + CARROT_CHECK_AND_THROW(ignore_dust || selected_inputs.empty(), + carrot::not_enough_usable_money, "some inputs couldn't pay for their own marginal fee and `ignore_dust`=false"); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace carrot diff --git a/src/carrot_impl/multi_tx_proposal_utils.h b/src/carrot_impl/multi_tx_proposal_utils.h new file mode 100644 index 00000000000..8ed6025cb44 --- /dev/null +++ b/src/carrot_impl/multi_tx_proposal_utils.h @@ -0,0 +1,106 @@ +// Copyright (c) 2025, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +//local headers +#include "input_selection.h" + +//third party headers + +//standard headers +#include +#include + +//forward declarations + +namespace carrot +{ +/** + * brief: make_multiple_carrot_transaction_proposals_transfer - make multiple "transfer-style" Carrot tx proposals + * param: normal_payment_proposals - normal payment proposals to be included *once* in the tx set + * param: selfsend_payment_proposals - selfsend payment proposals to be included *once* in the tx set + * param: fee_per_weight - concrete fee is calculated as transaction weight times this value + * param: extra - truly "extra" fields to be included in *every* tx_extra, doesn't include ephemeral tx pubkeys or PIDs + * param: input_candidates - list of potential input candidates to choose from + * param: input_selection_policies - span of ISPs (see `make_single_transfer_input_selector` for more info) + * param: input_selection_flags - flags passed to `make_single_transfer_input_selector` + * param: change_address_spend_pubkey - address spend pubkey to send to for change selfsend enotes + * param: change_address_index - subaddress index of change_address_spend_pubkey in your account + * param: subtractable_normal_payment_proposals - indices of normal payment proposals which are "fee subtractable" + * param: subtractable_selfsend_payment_proposals - indices of selfsend payment proposals which are "fee subtractable" + * outparam: tx_proposals_out - set of fully formed Carrot transaction proposal which satisfies all payments + * + * Creates as many transactions as is necessary to fulfill all of the passed payment proposals. + * Internally, this function uses a greedy loop, selecting inputs with + * `make_single_transfer_input_selector` for input selection for a single transaction, marking them + * as used, and then moves onto the next single transaction in isolation. This is technically not + * optimal, but optimality in this case our problem is NP-complete, which means that achieving + * optimality requires a non-polynomial runtime. Also, using `make_single_transfer_input_selector` + * for pulling single transactions at a time means that it's easier to verify the input selection + * rules are being upheld for each individual transaction in the set. + */ +void make_multiple_carrot_transaction_proposals_transfer( + std::vector &&normal_payment_proposals, + std::vector &&selfsend_payment_proposals, + const rct::xmr_amount fee_per_weight, + const std::vector &extra, + std::vector &&input_candidates, + const epee::span input_selection_policies, + const std::uint32_t input_selection_flags, + const std::size_t max_n_inputs, + const crypto::public_key &change_address_spend_pubkey, + const subaddress_index_extended &change_address_index, + const std::set &subtractable_normal_payment_proposals, + const std::set &subtractable_selfsend_payment_proposals, + std::vector &tx_proposals_out); +/** + * brief: make_multiple_carrot_transaction_proposals_sweep - make multiple "sweep-style" Carrot transaction proposals + * param: normal_payment_proposals - normal payment proposals to be included in *every* tx in the set + * param: selfsend_payment_proposals - selfsend payment proposals to be included in *every* tx in the set + * param: fee_per_weight - concrete fee is calculated as transaction weight times this value + * param: extra - truly "extra" fields to be included in tx_extra, doesn't include ephemeral tx pubkeys or PIDs + * param: selected_inputs - explicitly provided inputs + * param: change_address_spend_pubkey - address spend pubkey to send to for change selfsend enotes + * param: change_address_index - subaddress index of change_address_spend_pubkey in your account + * outparam: tx_proposal_out - set of fully formed Carrot transaction proposal which spend all inputs + * + * Creates as many sweep transaction as is necessary to spend all of `selected_inputs`, including + * the provided payment proposals in every transaction. + */ +void make_multiple_carrot_transaction_proposals_sweep( + const std::vector &normal_payment_proposals, + const std::vector &selfsend_payment_proposals, + const rct::xmr_amount fee_per_weight, + const std::vector &extra, + std::vector &&selected_inputs, + const crypto::public_key &change_address_spend_pubkey, + const subaddress_index_extended &change_address_index, + const bool ignore_dust, + std::vector &tx_proposals_out); +} //namespace carrot diff --git a/src/carrot_impl/output_opening_types.cpp b/src/carrot_impl/output_opening_types.cpp new file mode 100644 index 00000000000..2e986d4564d --- /dev/null +++ b/src/carrot_impl/output_opening_types.cpp @@ -0,0 +1,429 @@ +// Copyright (c) 2025, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//paired header +#include "output_opening_types.h" + +//local headers +#include "carrot_core/enote_utils.h" +#include "carrot_core/scan.h" +#include "ringct/rctOps.h" + +//third party headers + +//standard headers + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "carrot_impl" + +namespace carrot +{ +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static CarrotEnoteV1 make_carrot_enote_from_v2_opening_hint(const CarrotOutputOpeningHintV2 &opening_hint, + const crypto::hash &s_sender_receiver_ctx) +{ + return CarrotEnoteV1{ + .onetime_address = opening_hint.onetime_address, + .amount_commitment = opening_hint.amount_commitment, + .amount_enc = encrypt_carrot_amount(opening_hint.amount, s_sender_receiver_ctx, opening_hint.onetime_address), + .anchor_enc = opening_hint.anchor_enc, + .view_tag = opening_hint.view_tag, + .enote_ephemeral_pubkey = opening_hint.enote_ephemeral_pubkey, + .tx_first_key_image = opening_hint.tx_first_key_image + }; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool try_opening_hint_scan_on_carrot_enote(const CarrotEnoteV1 &enote, + const std::optional &encrypted_payment_id, + const epee::span main_address_spend_pubkeys, + const view_incoming_key_device *k_view_incoming_dev, + const view_balance_secret_device *s_view_balance_dev, + crypto::secret_key &sender_extension_g_out, + crypto::secret_key &sender_extension_t_out, + rct::xmr_amount &amount_out, + rct::key &amount_blinding_factor_out) +{ + crypto::public_key dummy_address_spend_pubkey; + crypto::secret_key amount_blinding_factor_sk; + CarrotEnoteType dummy_enote_type; + janus_anchor_t dummy_internal_message; + payment_id_t dummy_payment_id; + if (s_view_balance_dev != nullptr) + { + if (try_scan_carrot_enote_internal_receiver(enote, + *s_view_balance_dev, + sender_extension_g_out, + sender_extension_t_out, + dummy_address_spend_pubkey, + amount_out, + amount_blinding_factor_sk, + dummy_enote_type, + dummy_internal_message)) + { + amount_blinding_factor_out = rct::sk2rct(amount_blinding_factor_sk); + return true; + } + } + if (k_view_incoming_dev != nullptr) + { + mx25519_pubkey s_sender_receiver; + if (try_make_carrot_shared_key_receiver(*k_view_incoming_dev, + enote.enote_ephemeral_pubkey, + s_sender_receiver)) + { + if (try_scan_carrot_enote_external_receiver(enote, + encrypted_payment_id, + s_sender_receiver, + main_address_spend_pubkeys, + *k_view_incoming_dev, + sender_extension_g_out, + sender_extension_t_out, + dummy_address_spend_pubkey, + amount_out, + amount_blinding_factor_sk, + dummy_payment_id, + dummy_enote_type)) + { + amount_blinding_factor_out = rct::sk2rct(amount_blinding_factor_sk); + return true; + } + } + } + + return false; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static bool try_scan_opening_hint(const OutputOpeningHintVariant &opening_hint, + const epee::span main_address_spend_pubkeys, + const view_incoming_key_device *k_view_incoming_dev, + const view_balance_secret_device *s_view_balance_dev, + crypto::secret_key &sender_extension_g_out, + crypto::secret_key &sender_extension_t_out, + rct::xmr_amount &amount_out, + rct::key &amount_blinding_factor_out) +{ + struct try_scan_opening_hint_visitor + { + bool operator()(const LegacyOutputOpeningHintV1 &hint) const + { + sender_extension_g_out = crypto::null_skey; + sender_extension_t_out = crypto::null_skey; + amount_out = hint.amount; + amount_blinding_factor_out = hint.amount_blinding_factor; + + if (k_view_incoming_dev == nullptr) + return false; + + // k_v K_e + crypto::public_key kd_tors; + if (!k_view_incoming_dev->view_key_scalar_mult_ed25519(hint.ephemeral_tx_pubkey, kd_tors)) + return false; + + // 8 k_v K_e + kd_tors = rct::rct2pk(rct::scalarmult8(rct::pk2rct(kd_tors))); + crypto::key_derivation kd; + memcpy(&kd, &kd_tors, sizeof(kd)); + + // d = k_o = Hs1(8 k_v K_e, i) + crypto::derivation_to_scalar(kd, hint.local_output_index, unwrap(unwrap(sender_extension_g_out))); + + return true; + } + + bool operator()(const CarrotOutputOpeningHintV1 &hint) const + { + return try_opening_hint_scan_on_carrot_enote(hint.source_enote, + hint.encrypted_payment_id, + main_address_spend_pubkeys, + k_view_incoming_dev, + s_view_balance_dev, + sender_extension_g_out, + sender_extension_t_out, + amount_out, + amount_blinding_factor_out); + } + + bool operator()(const CarrotOutputOpeningHintV2 &hint) const + { + // input_context = "R" || KI_1 + const input_context_t input_context = carrot::make_carrot_input_context(hint.tx_first_key_image); + + // s^ctx_sr = H_32(s_sr, D_e, input_context) for internal&external s_sr + crypto::hash s_sender_receiver_ctx[2]; //! @TODO: wipe + std::size_t n_keys_available = 0; + if (s_view_balance_dev != nullptr) + { + // s^ctx_sr = H_32(s_vb, D_e, input_context) + s_view_balance_dev->make_internal_sender_receiver_secret(hint.enote_ephemeral_pubkey, + input_context, s_sender_receiver_ctx[n_keys_available++]); + } + if (k_view_incoming_dev != nullptr) + { + // s_sr = k_v D_e + mx25519_pubkey s_sender_receiver; + if (!carrot::try_make_carrot_shared_key_receiver(*k_view_incoming_dev, + hint.enote_ephemeral_pubkey, + s_sender_receiver)) + return false; + + // s^ctx_sr = H_32(k_v D_e, D_e, input_context) + carrot::make_carrot_contextualized_sender_receiver_secret( + s_sender_receiver.data, + hint.enote_ephemeral_pubkey, + input_context, + s_sender_receiver_ctx[n_keys_available++]); + } + + for (std::size_t i = 0; i < n_keys_available; ++i) + { + if (try_opening_hint_scan_on_carrot_enote( + make_carrot_enote_from_v2_opening_hint(hint, s_sender_receiver_ctx[i]), + hint.encrypted_payment_id, + main_address_spend_pubkeys, + k_view_incoming_dev, + s_view_balance_dev, + sender_extension_g_out, + sender_extension_t_out, + amount_out, + amount_blinding_factor_out)) + return true; + } + + return false; + } + + bool operator()(const CarrotCoinbaseOutputOpeningHintV1 &hint) const + { + amount_out = hint.source_enote.amount; + amount_blinding_factor_out = rct::I; + + mx25519_pubkey s_sender_receiver; + if (try_make_carrot_shared_key_receiver(*k_view_incoming_dev, + hint.source_enote.enote_ephemeral_pubkey, + s_sender_receiver)) + { + crypto::public_key dummy_address_spend_pubkey; + return try_scan_carrot_coinbase_enote_receiver(hint.source_enote, + s_sender_receiver, + main_address_spend_pubkeys, + sender_extension_g_out, + sender_extension_t_out, + dummy_address_spend_pubkey); + } + + return false; + } + + epee::span main_address_spend_pubkeys; + const view_incoming_key_device *k_view_incoming_dev; + const view_balance_secret_device *s_view_balance_dev; + crypto::secret_key &sender_extension_g_out; + crypto::secret_key &sender_extension_t_out; + rct::xmr_amount &amount_out; + rct::key &amount_blinding_factor_out; + }; + + return std::visit(try_scan_opening_hint_visitor{ + main_address_spend_pubkeys, + k_view_incoming_dev, + s_view_balance_dev, + sender_extension_g_out, + sender_extension_t_out, + amount_out, + amount_blinding_factor_out}, + opening_hint); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +bool operator==(const LegacyOutputOpeningHintV1 &a, const LegacyOutputOpeningHintV1 &b) +{ + return a.onetime_address == b.onetime_address + && a.ephemeral_tx_pubkey == b.ephemeral_tx_pubkey + && a.subaddr_index == b.subaddr_index + && a.amount == b.amount + && a.amount_blinding_factor == b.amount_blinding_factor + && a.local_output_index == b.local_output_index; +} +//------------------------------------------------------------------------------------------------------------------- +bool operator==(const CarrotOutputOpeningHintV1 &a, const CarrotOutputOpeningHintV1 &b) +{ + return a.source_enote == b.source_enote + && a.encrypted_payment_id == b.encrypted_payment_id + && a.subaddr_index == b.subaddr_index; +} +//------------------------------------------------------------------------------------------------------------------- +bool operator==(const CarrotOutputOpeningHintV2 &a, const CarrotOutputOpeningHintV2 &b) +{ + return a.onetime_address == b.onetime_address + && a.amount_commitment == b.amount_commitment + && a.anchor_enc == b.anchor_enc + && a.view_tag == b.view_tag + && memcmp(&a.enote_ephemeral_pubkey, &b.enote_ephemeral_pubkey, sizeof(a.enote_ephemeral_pubkey)) == 0 + && a.tx_first_key_image == b.tx_first_key_image + && a.amount == b.amount + && a.encrypted_payment_id == b.encrypted_payment_id + && a.subaddr_index == b.subaddr_index; +} +//------------------------------------------------------------------------------------------------------------------- +bool operator==(const CarrotCoinbaseOutputOpeningHintV1 &a, const CarrotCoinbaseOutputOpeningHintV1 &b) +{ + return a.source_enote == b.source_enote + && a.derive_type == b.derive_type; +} +//------------------------------------------------------------------------------------------------------------------- +const crypto::public_key &onetime_address_ref(const OutputOpeningHintVariant &opening_hint) +{ + struct onetime_address_ref_visitor + { + const crypto::public_key &operator()(const LegacyOutputOpeningHintV1 &h) const + { return h.onetime_address; } + const crypto::public_key &operator()(const CarrotOutputOpeningHintV1 &h) const + { return h.source_enote.onetime_address; } + const crypto::public_key &operator()(const CarrotOutputOpeningHintV2 &h) const + { return h.onetime_address; } + const crypto::public_key &operator()(const CarrotCoinbaseOutputOpeningHintV1 &h) const + { return h.source_enote.onetime_address; } + }; + + return std::visit(onetime_address_ref_visitor{}, opening_hint); +} +//------------------------------------------------------------------------------------------------------------------- +rct::key amount_commitment_ref(const OutputOpeningHintVariant &opening_hint) +{ + struct amount_commitment_ref_visitor + { + rct::key operator()(const LegacyOutputOpeningHintV1 &h) const + { return rct::commit(h.amount, h.amount_blinding_factor); } + rct::key operator()(const CarrotOutputOpeningHintV1 &h) const + { return h.source_enote.amount_commitment; } + rct::key operator()(const CarrotOutputOpeningHintV2 &h) const + { return h.amount_commitment; } + rct::key operator()(const CarrotCoinbaseOutputOpeningHintV1 &h) const + { return rct::zeroCommitVartime(h.source_enote.amount); } + }; + + return std::visit(amount_commitment_ref_visitor{}, opening_hint); +} +//------------------------------------------------------------------------------------------------------------------- +subaddress_index_extended subaddress_index_ref(const OutputOpeningHintVariant &opening_hint) +{ + struct subaddress_index_ref_visitor + { + subaddress_index_extended operator()(const LegacyOutputOpeningHintV1 &h) const + { return {h.subaddr_index, AddressDeriveType::PreCarrot}; } + subaddress_index_extended operator()(const CarrotOutputOpeningHintV1 &h) const + { return h.subaddr_index; } + subaddress_index_extended operator()(const CarrotOutputOpeningHintV2 &h) const + { return h.subaddr_index; } + subaddress_index_extended operator()(const CarrotCoinbaseOutputOpeningHintV1 &h) const + { return {{0, 0}, h.derive_type}; } + }; + + return std::visit(subaddress_index_ref_visitor{}, opening_hint); +} +//------------------------------------------------------------------------------------------------------------------- +bool use_biased_hash_to_point(const OutputOpeningHintVariant &opening_hint) +{ + struct hint_visitor + { + bool operator()(const LegacyOutputOpeningHintV1&) const + { return true; } + bool operator()(const CarrotOutputOpeningHintV1&) const + { return false; } + bool operator()(const CarrotOutputOpeningHintV2&) const + { return false; } + bool operator()(const CarrotCoinbaseOutputOpeningHintV1&) const + { return false; } + }; + + return std::visit(hint_visitor{}, opening_hint); +} +//------------------------------------------------------------------------------------------------------------------- +fcmp_pp::OutputPair to_output_pair(const OutputOpeningHintVariant &opening_hint) +{ + struct hint_visitor + { + const crypto::public_key &O; + const crypto::ec_point &C; + + fcmp_pp::OutputPair operator()(const LegacyOutputOpeningHintV1&) const + { return fcmp_pp::LegacyOutputPair{{O, C}}; } + fcmp_pp::OutputPair operator()(const CarrotOutputOpeningHintV1&) const + { return fcmp_pp::CarrotOutputPairV1{{O, C}}; } + fcmp_pp::OutputPair operator()(const CarrotOutputOpeningHintV2&) const + { return fcmp_pp::CarrotOutputPairV1{{O, C}}; } + fcmp_pp::OutputPair operator()(const CarrotCoinbaseOutputOpeningHintV1&) const + { return fcmp_pp::CarrotOutputPairV1{{O, C}}; } + }; + + const crypto::public_key &O = onetime_address_ref(opening_hint); + const rct::key &C = amount_commitment_ref(opening_hint); + return std::visit(hint_visitor{O, rct::rct2pt(C)}, opening_hint); +} +//------------------------------------------------------------------------------------------------------------------- +bool try_scan_opening_hint_sender_extensions(const OutputOpeningHintVariant &opening_hint, + const epee::span main_address_spend_pubkeys, + const view_incoming_key_device *k_view_incoming_dev, + const view_balance_secret_device *s_view_balance_dev, + crypto::secret_key &sender_extension_g_out, + crypto::secret_key &sender_extension_t_out) +{ + rct::xmr_amount amount; + rct::key amount_blinding_factor; + return try_scan_opening_hint(opening_hint, + main_address_spend_pubkeys, + k_view_incoming_dev, + s_view_balance_dev, + sender_extension_g_out, + sender_extension_t_out, + amount, + amount_blinding_factor); +} +//------------------------------------------------------------------------------------------------------------------- +bool try_scan_opening_hint_amount(const OutputOpeningHintVariant &opening_hint, + const epee::span main_address_spend_pubkeys, + const view_incoming_key_device *k_view_incoming_dev, + const view_balance_secret_device *s_view_balance_dev, + rct::xmr_amount &amount_out, + rct::key &amount_blinding_factor_out) +{ + crypto::secret_key sender_extension_g, sender_extension_t; + return try_scan_opening_hint(opening_hint, + main_address_spend_pubkeys, + k_view_incoming_dev, + s_view_balance_dev, + sender_extension_g, + sender_extension_t, + amount_out, + amount_blinding_factor_out); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace carrot diff --git a/src/carrot_impl/output_opening_types.h b/src/carrot_impl/output_opening_types.h new file mode 100644 index 00000000000..9ccbd47115c --- /dev/null +++ b/src/carrot_impl/output_opening_types.h @@ -0,0 +1,180 @@ +// Copyright (c) 2025, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +//local headers + +//third party headers +#include "carrot_core/carrot_enote_types.h" +#include "fcmp_pp/fcmp_pp_types.h" +#include "span.h" +#include "subaddress_index.h" + +//standard headers +#include +#include + +//forward declarations +namespace carrot +{ +struct view_incoming_key_device; +struct view_balance_secret_device; +} + +namespace carrot +{ +struct LegacyOutputOpeningHintV1 +{ + // WARNING: Using this opening hint is unsafe and enables for HW devices to + // accidentally burn XMR if an attacker controls the hot wallet and + // can publish a new enote with the same K_o as an existing enote, + // but with a different amount. However, it is unavoidable for + // legacy enotes, since the computation of K_o is not directly nor + // indirectly bound to the amount. + + // Informs remote prover (implied to know opening of K^j_s given j) how to open O, C such that: + // O = K^j_s + Hs1(8 k_v K_e, i) G + // C = z G + a H + + // O + crypto::public_key onetime_address; + + // K_e + crypto::public_key ephemeral_tx_pubkey; + + // j (legacy only) + subaddress_index subaddr_index; + + // a + rct::xmr_amount amount; + + // z + rct::key amount_blinding_factor; + + // i + std::size_t local_output_index; +}; +bool operator==(const LegacyOutputOpeningHintV1&, const LegacyOutputOpeningHintV1&); + +struct CarrotOutputOpeningHintV1 +{ + // source enote + CarrotEnoteV1 source_enote; + + // pid_enc + std::optional encrypted_payment_id; + + // j, derive type + subaddress_index_extended subaddr_index; +}; +bool operator==(const CarrotOutputOpeningHintV1&, const CarrotOutputOpeningHintV1&); + +struct CarrotOutputOpeningHintV2 +{ + /// K_o + crypto::public_key onetime_address; + /// C_a + rct::key amount_commitment; + /// anchor_enc + encrypted_janus_anchor_t anchor_enc; + /// view_tag + view_tag_t view_tag; + /// D_e + mx25519_pubkey enote_ephemeral_pubkey; + /// L_0 + crypto::key_image tx_first_key_image; + + /// a + rct::xmr_amount amount; + + // pid_enc + std::optional encrypted_payment_id; + + // j, derive type + subaddress_index_extended subaddr_index; +}; +bool operator==(const CarrotOutputOpeningHintV2&, const CarrotOutputOpeningHintV2&); + +struct CarrotCoinbaseOutputOpeningHintV1 +{ + // source enote + CarrotCoinbaseEnoteV1 source_enote; + + // no encrypted pids for coinbase transactions + + // subaddress index is assumed to be (0, 0) in coinbase transactions + AddressDeriveType derive_type; +}; +bool operator==(const CarrotCoinbaseOutputOpeningHintV1&, const CarrotCoinbaseOutputOpeningHintV1&); + +using OutputOpeningHintVariant = std::variant< + LegacyOutputOpeningHintV1, + CarrotOutputOpeningHintV1, + CarrotOutputOpeningHintV2, + CarrotCoinbaseOutputOpeningHintV1 + >; +const crypto::public_key &onetime_address_ref(const OutputOpeningHintVariant&); +rct::key amount_commitment_ref(const OutputOpeningHintVariant&); +subaddress_index_extended subaddress_index_ref(const OutputOpeningHintVariant&); + +bool use_biased_hash_to_point(const OutputOpeningHintVariant&); + +fcmp_pp::OutputPair to_output_pair(const OutputOpeningHintVariant &opening_hint); + +/** + * brief: try_scan_opening_hint_sender_extensions - scan sender extensions for given opening hint + * param: opening_hint + * param: k_view_incoming_dev - k_v [OPTIONAL] + * param: s_view_balance_dev - s_vb [OPTIONAL] + * outparam: sender_extension_g_out - k^g_o + * outparam: sender_extension_t_out - k^t_o + * return: true iff Carrot enote scan was successful, or if nominal legacy derivation-to-scalar didn't fail + */ +bool try_scan_opening_hint_sender_extensions(const OutputOpeningHintVariant &opening_hint, + const epee::span main_address_spend_pubkeys, + const view_incoming_key_device *k_view_incoming_dev, + const view_balance_secret_device *s_view_balance_dev, + crypto::secret_key &sender_extension_g_out, + crypto::secret_key &sender_extension_t_out); +/** + * brief: try_scan_opening_hint_amount - scan amount and blinding factor for given opening hint + * param: opening_hint + * param: k_view_incoming_dev - k_v [OPTIONAL] + * param: s_view_balance_dev - s_vb [OPTIONAL] + * outparam: amount_out - a + * outparam: amount_blinding_factor_out - k_a + * return: true iff Carrot enote scan was successful, or if nominal legacy derivation-to-scalar didn't fail + */ +bool try_scan_opening_hint_amount(const OutputOpeningHintVariant &opening_hint, + const epee::span main_address_spend_pubkeys, + const view_incoming_key_device *k_view_incoming_dev, + const view_balance_secret_device *s_view_balance_dev, + rct::xmr_amount &amount_out, + rct::key &amount_blinding_factor_out); +} //namespace carrot diff --git a/src/carrot_impl/spend_device.h b/src/carrot_impl/spend_device.h new file mode 100644 index 00000000000..748f691284b --- /dev/null +++ b/src/carrot_impl/spend_device.h @@ -0,0 +1,57 @@ +// Copyright (c) 2025, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +//local headers +#include "key_image_device.h" +#include "tx_proposal.h" + +//third party headers + +//standard headers +#include + +//forward declarations + +namespace carrot +{ +struct spend_device: public key_image_device +{ + // maps KI -> (OTA, SA/L) in consensus ordering + using signed_input_set_t = std::map, + std::greater>; + + virtual bool try_sign_carrot_transaction_proposal_v1(const CarrotTransactionProposalV1 &tx_proposal, + const std::unordered_map &rerandomized_outputs, + crypto::hash &signable_tx_hash_out, + signed_input_set_t &signed_inputs_out + ) const = 0; +}; +} //namespace carrot diff --git a/src/carrot_impl/spend_device_ram_borrowed.cpp b/src/carrot_impl/spend_device_ram_borrowed.cpp new file mode 100644 index 00000000000..3ba81dbf450 --- /dev/null +++ b/src/carrot_impl/spend_device_ram_borrowed.cpp @@ -0,0 +1,190 @@ +// Copyright (c) 2025, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//paired header +#include "spend_device_ram_borrowed.h" + +//local headers +#include "address_device_ram_borrowed.h" +#include "carrot_core/account_secrets.h" +#include "carrot_core/device_ram_borrowed.h" +#include "carrot_core/exceptions.h" +#include "crypto/generators.h" +#include "key_image_device_composed.h" +#include "misc_log_ex.h" +#include "tx_builder_inputs.h" +#include "tx_builder_outputs.h" + +//third party headers + +//standard headers + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "carrot_impl" + +#define DEFINE_SUB_DEVICES() \ + std::shared_ptr legacy_k_generate_image_dev; \ + std::shared_ptr carrot_k_generate_image_dev( \ + new generate_image_key_ram_borrowed_device(this->m_privkey_g)); \ + if (!this->m_s_view_balance_dev) \ + std::swap(legacy_k_generate_image_dev, carrot_k_generate_image_dev); \ + const carrot::key_image_device_composed key_image_dev( \ + std::move(legacy_k_generate_image_dev), \ + std::move(carrot_k_generate_image_dev), \ + this->m_address_dev, \ + this->m_s_view_balance_dev, \ + this->m_k_view_incoming_dev); + +namespace carrot +{ +//------------------------------------------------------------------------------------------------------------------- +spend_device_ram_borrowed::spend_device_ram_borrowed( + std::shared_ptr k_view_incoming_dev, + std::shared_ptr s_view_balance_dev, + std::shared_ptr address_dev, + const crypto::secret_key &privkey_g, + const crypto::secret_key &privkey_t) +: + m_k_view_incoming_dev(k_view_incoming_dev), + m_s_view_balance_dev(s_view_balance_dev), + m_address_dev(address_dev), + m_privkey_g(privkey_g), + m_privkey_t(privkey_t) +{ + assert(this->m_k_view_incoming_dev); +} +//------------------------------------------------------------------------------------------------------------------- +spend_device_ram_borrowed::spend_device_ram_borrowed( + const crypto::secret_key &k_spend, + const crypto::secret_key &k_view) +: + m_k_view_incoming_dev(), //assigned in ctor + m_s_view_balance_dev(), + m_address_dev(), //assigned in ctor + m_privkey_g(k_spend), + m_privkey_t(crypto::null_skey) +{ + // K_s = k_s G + crypto::public_key cryptonote_account_spend_pubkey; + crypto::secret_key_to_public_key(k_spend, cryptonote_account_spend_pubkey); + + auto k_view_incoming_dev = std::make_shared(k_view); + m_k_view_incoming_dev = k_view_incoming_dev; + + m_address_dev.reset(new carrot::cryptonote_hierarchy_address_device(std::move(k_view_incoming_dev), + cryptonote_account_spend_pubkey)); +} +//------------------------------------------------------------------------------------------------------------------- +bool spend_device_ram_borrowed::try_sign_carrot_transaction_proposal_v1( + const CarrotTransactionProposalV1 &tx_proposal, + const std::unordered_map &rerandomized_outputs, + crypto::hash &signable_tx_hash_out, + signed_input_set_t &signed_inputs_out) const +{ + signable_tx_hash_out = crypto::null_hash; + signed_inputs_out.clear(); + + DEFINE_SUB_DEVICES() + + // get sorted tx key images and insert into `signed_inputs_out` + std::vector sorted_input_key_images; + std::vector key_image_order; + carrot::get_sorted_input_key_images_from_proposal_v1(tx_proposal, + key_image_dev, + sorted_input_key_images, + &key_image_order); + for (std::size_t tx_input_idx = 0; tx_input_idx < sorted_input_key_images.size(); ++tx_input_idx) + { + const std::size_t input_proposal_idx = key_image_order.at(tx_input_idx); + const crypto::public_key ota = onetime_address_ref(tx_proposal.input_proposals.at(input_proposal_idx)); + const crypto::key_image &ki = sorted_input_key_images.at(tx_input_idx); + signed_inputs_out[ki].first = ota; + } + + // calculate signable tx hash + make_signable_tx_hash_from_proposal_v1(tx_proposal, + /*s_view_balance_dev=*/nullptr, + this->m_k_view_incoming_dev.get(), + sorted_input_key_images, + signable_tx_hash_out); + + // prove SA/L + for (auto &p : signed_inputs_out) + { + const crypto::public_key &onetime_address = p.second.first; + + const auto input_proposal_it = std::find_if(tx_proposal.input_proposals.cbegin(), + tx_proposal.input_proposals.cend(), + [&onetime_address](const auto &ip) { return onetime_address_ref(ip) == onetime_address; }); + CARROT_CHECK_AND_THROW(input_proposal_it != tx_proposal.input_proposals.cend(), + carrot::component_out_of_order, + "could not find input proposal for given one-time address"); + + const auto rerandomized_output_it = rerandomized_outputs.find(onetime_address); + CARROT_CHECK_AND_THROW(rerandomized_output_it != rerandomized_outputs.cend(), + carrot::component_out_of_order, + "could not find rerandomized output for given one-time address"); + + crypto::key_image ki; + carrot::make_sal_proof_any_to_hybrid_v1(signable_tx_hash_out, + rerandomized_output_it->second, + *input_proposal_it, + this->m_privkey_g, + this->m_privkey_t, + this->m_s_view_balance_dev.get(), + *this->m_k_view_incoming_dev, + *this->m_address_dev, + p.second.second, + ki); + + CARROT_CHECK_AND_THROW(ki == p.first, + carrot::component_out_of_order, "key image mismatch during SA/L proving") + } + + return true; +} +//------------------------------------------------------------------------------------------------------------------- +crypto::key_image spend_device_ram_borrowed::derive_key_image(const OutputOpeningHintVariant &opening_hint) const +{ + DEFINE_SUB_DEVICES() + + return key_image_dev.derive_key_image(opening_hint); +} +//------------------------------------------------------------------------------------------------------------------- +crypto::key_image spend_device_ram_borrowed::derive_key_image_prescanned( + const crypto::secret_key &sender_extension_g, + const crypto::public_key &onetime_address, + const subaddress_index_extended &subaddr_index, + const bool use_biased) const +{ + DEFINE_SUB_DEVICES() + + return key_image_dev.derive_key_image_prescanned(sender_extension_g, onetime_address, subaddr_index, use_biased); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace carrot diff --git a/src/carrot_impl/spend_device_ram_borrowed.h b/src/carrot_impl/spend_device_ram_borrowed.h new file mode 100644 index 00000000000..6ca97677221 --- /dev/null +++ b/src/carrot_impl/spend_device_ram_borrowed.h @@ -0,0 +1,77 @@ +// Copyright (c) 2025, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +//local headers +#include "address_device.h" +#include "crypto/crypto.h" +#include "spend_device.h" + +//third party headers + +//standard headers + +//forward declarations + +namespace carrot +{ +class spend_device_ram_borrowed: public spend_device +{ +public: + /// @brief device composed (except k_s, k_ps, k_gi) + spend_device_ram_borrowed(std::shared_ptr k_view_incoming_dev, + std::shared_ptr s_view_balance_dev, + std::shared_ptr address_dev, + const crypto::secret_key &privkey_g, + const crypto::secret_key &privkey_t); + + /// @brief cryptonote-derived & ram borrowed from k_s, k_v + spend_device_ram_borrowed(const crypto::secret_key &k_spend, const crypto::secret_key &k_view); + + bool try_sign_carrot_transaction_proposal_v1(const CarrotTransactionProposalV1 &tx_proposal, + const std::unordered_map &rerandomized_outputs, + crypto::hash &signable_tx_hash_out, + signed_input_set_t &signed_inputs_out + ) const override; + + crypto::key_image derive_key_image(const OutputOpeningHintVariant &opening_hint) const override; + + crypto::key_image derive_key_image_prescanned(const crypto::secret_key &sender_extension_g, + const crypto::public_key &onetime_address, + const subaddress_index_extended &subaddr_index, + const bool use_biased) const override; + +protected: + std::shared_ptr m_k_view_incoming_dev; + std::shared_ptr m_s_view_balance_dev; + std::shared_ptr m_address_dev; + const crypto::secret_key &m_privkey_g; + const crypto::secret_key &m_privkey_t; +}; +} //namespace carrot diff --git a/src/carrot_impl/subaddress_index.h b/src/carrot_impl/subaddress_index.h new file mode 100644 index 00000000000..1f201dc6983 --- /dev/null +++ b/src/carrot_impl/subaddress_index.h @@ -0,0 +1,126 @@ +// Copyright (c) 2025, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +//local headers +#include "common/hash_combine.h" + +//third party headers + +//standard headers +#include + +//forward declarations + +namespace carrot +{ +/** + * brief: subaddress_index - + */ +struct subaddress_index +{ + // j_major + std::uint32_t major; + // j_minor + std::uint32_t minor; + + bool is_subaddress() const + { + return major || minor; + } +}; +static inline bool operator==(const subaddress_index a, const subaddress_index b) +{ + return a.major == b.major && a.minor == b.minor; +} +static inline bool operator!=(const subaddress_index a, const subaddress_index b) +{ + return !(a == b); +} + +/** + * brief: AddressDeriveType - used in hybrid key hierarchies to specify how to derive subaddresses for the same index + */ +enum class AddressDeriveType +{ + Auto, + PreCarrot, + Carrot +}; + +struct subaddress_index_extended +{ + subaddress_index index; + AddressDeriveType derive_type; +}; +static inline bool operator==(const subaddress_index_extended &a, const subaddress_index_extended &b) +{ + return a.index == b.index && a.derive_type == b.derive_type; +} +static inline bool operator!=(const subaddress_index_extended &a, const subaddress_index_extended &b) +{ + return !(a == b); +} +} //namespace carrot + +namespace std +{ +template<> +struct hash +{ + std::size_t operator()(const carrot::subaddress_index &i) const + { + std::size_t h = 0; + tools::hash_combine(h, i.major); + tools::hash_combine(h, i.minor); + return h; + } +}; + +template<> +struct hash +{ + std::size_t operator()(const carrot::AddressDeriveType &a) const + { + return static_cast(a); + } +}; + +template<> +struct hash +{ + std::size_t operator()(const carrot::subaddress_index_extended &i) const + { + std::size_t h = 0; + tools::hash_combine(h, i.index); + tools::hash_combine(h, i.derive_type); + return h; + } +}; +} diff --git a/src/carrot_impl/subaddress_map.h b/src/carrot_impl/subaddress_map.h new file mode 100644 index 00000000000..6e37d76b2dd --- /dev/null +++ b/src/carrot_impl/subaddress_map.h @@ -0,0 +1,64 @@ +// Copyright (c) 2025, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +//local headers +#include "crypto/crypto.h" +#include "subaddress_index.h" + +//third party headers + +//standard headers +#include + +//forward declarations + +namespace carrot +{ +struct subaddress_map +{ + /** + * brief: Get the index and derivation type of an address given its spend pubkey + * param: address_spend_pubkey - K^j_s + * return: j for K^j_s, std::nullopt if unable + */ + virtual std::optional get_index_for_address_spend_pubkey( + const crypto::public_key &address_spend_pubkey) const = 0; + + /** + * brief: Get the spend pubkey of an address given index and derivation type + * param: subaddr_index - j + * return: K^j_s for j, std::nullopt if unable + */ + virtual std::optional get_address_spend_pubkey_for_index( + const subaddress_index_extended &subaddr_index) const = 0; + + virtual ~subaddress_map() = default; +}; +} //namespace carrot diff --git a/src/carrot_impl/subaddress_map_legacy.cpp b/src/carrot_impl/subaddress_map_legacy.cpp new file mode 100644 index 00000000000..0788fa23cac --- /dev/null +++ b/src/carrot_impl/subaddress_map_legacy.cpp @@ -0,0 +1,82 @@ +// Copyright (c) 2025, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//paired header +#include "subaddress_map_legacy.h" + +//local headers + +//third party headers + +//standard headers +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "carrot_impl" + +namespace carrot +{ +//------------------------------------------------------------------------------------------------------------------- +subaddress_map_legacy::subaddress_map_legacy( + const std::unordered_map &subaddress_map_legacy) +: + m_subaddress_map_legacy(subaddress_map_legacy) +{} +//------------------------------------------------------------------------------------------------------------------- +std::optional subaddress_map_legacy::get_index_for_address_spend_pubkey( + const crypto::public_key &address_spend_pubkey) const +{ + const auto it = m_subaddress_map_legacy.find(address_spend_pubkey); + if (it == m_subaddress_map_legacy.cend()) + return std::nullopt; + const cryptonote::subaddress_index &subaddr_index = it->second; + return subaddress_index_extended{ + .index = subaddress_index{ + .major = subaddr_index.major, + .minor = subaddr_index.minor + }, + .derive_type = AddressDeriveType::PreCarrot + }; +} +//------------------------------------------------------------------------------------------------------------------- +std::optional subaddress_map_legacy::get_address_spend_pubkey_for_index( + const subaddress_index_extended &subaddr_index) const +{ + if (subaddr_index.derive_type != AddressDeriveType::PreCarrot) + return std::nullopt; + const auto it = std::find_if(m_subaddress_map_legacy.cbegin(), m_subaddress_map_legacy.cend(), + [&subaddr_index](const std::pair &e) { + return e.second.major == subaddr_index.index.major && e.second.minor == subaddr_index.index.minor; + } + ); + if (it == m_subaddress_map_legacy.cend()) + return std::nullopt; + return it->first; +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace carrot diff --git a/src/carrot_impl/subaddress_map_legacy.h b/src/carrot_impl/subaddress_map_legacy.h new file mode 100644 index 00000000000..302bd61b4ac --- /dev/null +++ b/src/carrot_impl/subaddress_map_legacy.h @@ -0,0 +1,59 @@ +// Copyright (c) 2025, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +//local headers +#include "cryptonote_basic/subaddress_index.h" +#include "subaddress_map.h" + +//third party headers + +//standard headers +#include + +//forward declarations + +namespace carrot +{ +class subaddress_map_legacy: public subaddress_map +{ +public: + subaddress_map_legacy( + const std::unordered_map &subaddress_map_legacy); + + std::optional get_index_for_address_spend_pubkey( + const crypto::public_key &address_spend_pubkey) const override; + + std::optional get_address_spend_pubkey_for_index( + const subaddress_index_extended &subaddr_index) const override; + +private: + const std::unordered_map &m_subaddress_map_legacy; +}; +} //namespace carrot diff --git a/src/carrot_impl/tx_builder_inputs.cpp b/src/carrot_impl/tx_builder_inputs.cpp new file mode 100644 index 00000000000..c928162bf90 --- /dev/null +++ b/src/carrot_impl/tx_builder_inputs.cpp @@ -0,0 +1,325 @@ +// 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 "tx_builder_inputs.h" + +//local headers +#include "carrot_core/account_secrets.h" +#include "carrot_core/address_utils.h" +#include "carrot_core/config.h" +#include "carrot_core/enote_utils.h" +#include "carrot_core/scan.h" +#include "carrot_impl/address_utils.h" +#include "crypto/generators.h" +#include "fcmp_pp/prove.h" +#include "misc_log_ex.h" +#include "ringct/rctOps.h" + +//third party headers + +//standard headers +#include + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "carrot_impl" + +namespace carrot +{ +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static rct::key load_key(const std::uint8_t bytes[32]) +{ + rct::key k; + memcpy(k.bytes, bytes, sizeof(k)); + return k; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static FcmpInputCompressed calculate_fcmp_input_for_rerandomizations(const crypto::public_key &onetime_address, + const rct::key &amount_commitment, + const bool use_biased_hash_to_point, + const rct::key &r_o, + const rct::key &r_i, + const rct::key &r_r_i, + const rct::key &r_c) +{ + return fcmp_pp::calculate_fcmp_input_for_rerandomizations(onetime_address, + rct::rct2pt(amount_commitment), + use_biased_hash_to_point, + rct::rct2sk(r_o), + rct::rct2sk(r_i), + rct::rct2sk(r_r_i), + rct::rct2sk(r_c)); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void make_sal_proof_nominal_address(const crypto::hash &signable_tx_hash, + const FcmpRerandomizedOutputCompressed &rerandomized_output, + const crypto::secret_key &address_privkey_g, + const crypto::secret_key &address_privkey_t, + const OutputOpeningHintVariant &opening_hint, + const epee::span main_address_spend_pubkeys, + const view_incoming_key_device *k_view_incoming_dev, + const view_balance_secret_device *s_view_balance_dev, + fcmp_pp::FcmpPpSalProof &sal_proof_out, + crypto::key_image &key_image_out) +{ + // O = x G + y T + CHECK_AND_ASSERT_THROW_MES(verify_rerandomized_output_basic(rerandomized_output, + onetime_address_ref(opening_hint), + amount_commitment_ref(opening_hint), + use_biased_hash_to_point(opening_hint)), + "Could not make SA/L proof: failed to verify rerandomized output against opening hint"); + + // scan k^g_o, k^t_o + crypto::secret_key sender_extension_g; + crypto::secret_key sender_extension_t; + CHECK_AND_ASSERT_THROW_MES(try_scan_opening_hint_sender_extensions(opening_hint, + main_address_spend_pubkeys, + k_view_incoming_dev, + s_view_balance_dev, + sender_extension_g, + sender_extension_t), + "Could not make SA/L proof: failed to scan opening hint"); + + // x = k^{j,g}_addr + k^g_o + crypto::secret_key x; + sc_add(to_bytes(x), + to_bytes(address_privkey_g), + to_bytes(sender_extension_g)); + + // y = k^{j,t}_addr + k^t_o + crypto::secret_key y; + sc_add(to_bytes(y), + to_bytes(address_privkey_t), + to_bytes(sender_extension_t)); + + std::tie(sal_proof_out, key_image_out) = fcmp_pp::prove_sal(signable_tx_hash, + x, + y, + rerandomized_output); +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_rerandomized_outputs_nonrefundable(const std::vector &input_onetime_addresses, + const std::vector &input_amount_commitments, + const std::vector &input_uses_biased_hash_to_point, + const std::vector &input_amount_blinding_factors, + const std::vector &output_amount_blinding_factors, + std::vector &rerandomized_outputs_out) +{ + // collect input_amount_commitments as crypto::ec_point + std::vector input_amount_commitments_pt; + input_amount_commitments_pt.reserve(input_amount_commitments.size()); + for (const rct::key &input_amount_commitment : input_amount_commitments) + input_amount_commitments_pt.push_back(rct::rct2pt(input_amount_commitment)); + + // collect input_amount_blinding_factors as crypto::secret_key + std::vector input_amount_blinding_factors_sk; + input_amount_blinding_factors_sk.reserve(input_amount_blinding_factors_sk.size()); + for (const rct::key &input_amount_blinding_factor : input_amount_blinding_factors) + input_amount_blinding_factors_sk.push_back(rct::rct2sk(input_amount_blinding_factor)); + + // generate random r_o + std::vector r_o(input_onetime_addresses.size()); + for (size_t i = 0; i < input_onetime_addresses.size(); ++i) + crypto::random32_unbiased(to_bytes(r_o[i])); + + // calculate output_amount_blinding_factor_sum = sum(output_amount_blinding_factors) + crypto::secret_key output_amount_blinding_factor_sum; + sc_0(to_bytes(output_amount_blinding_factor_sum)); + for (const rct::key &output_amount_blinding_factor : output_amount_blinding_factors) + sc_add(to_bytes(output_amount_blinding_factor_sum), + to_bytes(output_amount_blinding_factor_sum), + output_amount_blinding_factor.bytes); + + fcmp_pp::make_balanced_rerandomized_output_set(input_onetime_addresses, + input_amount_commitments_pt, + input_uses_biased_hash_to_point, + input_amount_blinding_factors_sk, + r_o, + output_amount_blinding_factor_sum, + rerandomized_outputs_out); +} +//------------------------------------------------------------------------------------------------------------------- +bool verify_rerandomized_output_basic(const FcmpRerandomizedOutputCompressed &rerandomized_output, + const crypto::public_key &onetime_address, + const rct::key &amount_commitment, + const bool use_biased_hash_to_point) +{ + const FcmpInputCompressed recomputed_input = calculate_fcmp_input_for_rerandomizations( + onetime_address, + amount_commitment, + use_biased_hash_to_point, + load_key(rerandomized_output.r_o), + load_key(rerandomized_output.r_i), + load_key(rerandomized_output.r_r_i), + load_key(rerandomized_output.r_c)); + + return 0 == memcmp(&recomputed_input, &rerandomized_output.input, sizeof(FcmpInputCompressed)); +} +//------------------------------------------------------------------------------------------------------------------- +void make_sal_proof_any_to_legacy_v1(const crypto::hash &signable_tx_hash, + const FcmpRerandomizedOutputCompressed &rerandomized_output, + const OutputOpeningHintVariant &opening_hint, + const crypto::secret_key &k_spend, + const cryptonote_hierarchy_address_device &addr_dev, + fcmp_pp::FcmpPpSalProof &sal_proof_out, + crypto::key_image &key_image_out) +{ + // get K_s + crypto::public_key main_address_spend_pubkey; + addr_dev.get_address_spend_pubkey({}, main_address_spend_pubkey); + + // k^j_subext = ScalarDeriveLegacy("SubAddr" || IntToBytes8(0) || k_v || IntToBytes32(j_major) || IntToBytes32(j_minor)) + const subaddress_index_extended subaddr_index = subaddress_index_ref(opening_hint); + crypto::secret_key address_privkey_g; + crypto::secret_key dummy_subaddress_scalar; + addr_dev.get_address_openings(subaddr_index, address_privkey_g, dummy_subaddress_scalar); + + // k^j_g = k^j_subext + k_s + sc_add(to_bytes(address_privkey_g), to_bytes(address_privkey_g), to_bytes(k_spend)); + + make_sal_proof_nominal_address(signable_tx_hash, + rerandomized_output, + address_privkey_g, + crypto::null_skey, + opening_hint, + {&main_address_spend_pubkey, 1}, + &addr_dev, + /*s_view_balance_dev=*/nullptr, + sal_proof_out, + key_image_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_sal_proof_any_to_carrot_v1(const crypto::hash &signable_tx_hash, + const FcmpRerandomizedOutputCompressed &rerandomized_output, + const OutputOpeningHintVariant &opening_hint, + const crypto::secret_key &k_prove_spend, + const crypto::secret_key &k_generate_image, + const view_balance_secret_device &s_view_balance_dev, + const view_incoming_key_device &k_view_incoming_dev, + const generate_address_secret_device &s_generate_address_dev, + fcmp_pp::FcmpPpSalProof &sal_proof_out, + crypto::key_image &key_image_out) +{ + // K_s = k_gi G + k_ps T + crypto::public_key main_address_spend_pubkey; + carrot::make_carrot_spend_pubkey(k_generate_image, k_prove_spend, main_address_spend_pubkey); + + // K_v = k_v K_s + crypto::public_key account_view_pubkey; + k_view_incoming_dev.view_key_scalar_mult_ed25519(main_address_spend_pubkey, account_view_pubkey); + + // s^j_ap1 = H_32[s_ga](j_major, j_minor) + const subaddress_index_extended subaddr_index = subaddress_index_ref(opening_hint); + crypto::secret_key address_index_preimage_1; + s_generate_address_dev.make_address_index_preimage_1(subaddr_index.index.major, + subaddr_index.index.minor, + address_index_preimage_1); + + // s^j_ap2 = H_32[s^j_ap1](j_major, j_minor, K_s, K_v) + crypto::secret_key address_index_preimage_2; + make_carrot_address_index_preimage_2(address_index_preimage_1, + subaddr_index.index.major, + subaddr_index.index.minor, + main_address_spend_pubkey, + account_view_pubkey, + address_index_preimage_2); + + // k^j_subscal = H_n(K_s, j_major, j_minor, s^j_gen) + crypto::secret_key subaddress_scalar; + if (subaddr_index.index.is_subaddress()) + { + make_carrot_subaddress_scalar(address_index_preimage_2, + main_address_spend_pubkey, + subaddress_scalar); + } + else // main address + { + sc_1(to_bytes(subaddress_scalar)); + } + + // k^j_g = k_gi * k^j_subscal + crypto::secret_key address_privkey_g; + sc_mul(to_bytes(address_privkey_g), to_bytes(k_generate_image), to_bytes(subaddress_scalar)); + + // k^j_t = k_ps * k^j_subscal + crypto::secret_key address_privkey_t; + sc_mul(to_bytes(address_privkey_t), to_bytes(k_prove_spend), to_bytes(subaddress_scalar)); + + make_sal_proof_nominal_address(signable_tx_hash, + rerandomized_output, + address_privkey_g, + address_privkey_t, + opening_hint, + {&main_address_spend_pubkey, 1}, + &k_view_incoming_dev, + &s_view_balance_dev, + sal_proof_out, + key_image_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_sal_proof_any_to_hybrid_v1(const crypto::hash &signable_tx_hash, + const FcmpRerandomizedOutputCompressed &rerandomized_output, + const OutputOpeningHintVariant &opening_hint, + const crypto::secret_key &k_privkey_g, + const crypto::secret_key &k_privkey_t, + const view_balance_secret_device *s_view_balance_dev, + const view_incoming_key_device &k_view_incoming_dev, + const address_device &addr_dev, + fcmp_pp::FcmpPpSalProof &sal_proof_out, + crypto::key_image &key_image_out) +{ + crypto::secret_key subaddress_extention_g; + crypto::secret_key subaddress_scalar; + addr_dev.get_address_openings(subaddress_index_ref(opening_hint), subaddress_extention_g, subaddress_scalar); + + // k^j_g = k_g * k^j_subscal + k^j_subext + crypto::secret_key address_privkey_g; + sc_muladd(to_bytes(address_privkey_g), to_bytes(k_privkey_g), + to_bytes(subaddress_scalar), to_bytes(subaddress_extention_g)); + + // k^j_t = k_t * k^j_subscal + crypto::secret_key address_privkey_t; + sc_mul(to_bytes(address_privkey_t), to_bytes(k_privkey_t), to_bytes(subaddress_scalar)); + + crypto::public_key main_address_spend_pubkeys[2]; + make_sal_proof_nominal_address(signable_tx_hash, + rerandomized_output, + address_privkey_g, + address_privkey_t, + opening_hint, + get_all_main_address_spend_pubkeys_span(addr_dev, main_address_spend_pubkeys), + &k_view_incoming_dev, + s_view_balance_dev, + sal_proof_out, + key_image_out); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace carrot diff --git a/src/carrot_impl/tx_builder_inputs.h b/src/carrot_impl/tx_builder_inputs.h new file mode 100644 index 00000000000..f07663602bd --- /dev/null +++ b/src/carrot_impl/tx_builder_inputs.h @@ -0,0 +1,93 @@ +// 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 "address_device_hierarchies.h" +#include "carrot_core/carrot_enote_types.h" +#include "fcmp_pp/curve_trees.h" +#include "output_opening_types.h" +#include "span.h" +#include "tx_proposal.h" + +//third party headers + +//standard headers +#include + +//forward declarations + +namespace carrot +{ +void make_carrot_rerandomized_outputs_nonrefundable(const std::vector &input_onetime_addresses, + const std::vector &input_amount_commitments, + const std::vector &input_uses_biased_hash_to_point, + const std::vector &input_amount_blinding_factors, + const std::vector &output_amount_blinding_factors, + std::vector &rerandomized_outputs_out); + +bool verify_rerandomized_output_basic(const FcmpRerandomizedOutputCompressed &rerandomized_output, + const crypto::public_key &onetime_address, + const rct::key &amount_commitment, + const bool use_biased_hash_to_point); + +// spend any enote addressed to a legacy address +void make_sal_proof_any_to_legacy_v1(const crypto::hash &signable_tx_hash, + const FcmpRerandomizedOutputCompressed &rerandomized_output, + const OutputOpeningHintVariant &opening_hint, + const crypto::secret_key &k_spend, + const cryptonote_hierarchy_address_device &addr_dev, + fcmp_pp::FcmpPpSalProof &sal_proof_out, + crypto::key_image &key_image_out); + +// spend any enote addressed to a carrot address +void make_sal_proof_any_to_carrot_v1(const crypto::hash &signable_tx_hash, + const FcmpRerandomizedOutputCompressed &rerandomized_output, + const OutputOpeningHintVariant &opening_hint, + const crypto::secret_key &k_prove_spend, + const crypto::secret_key &k_generate_image, + const view_balance_secret_device &s_view_balance_dev, + const view_incoming_key_device &k_view_incoming_dev, + const generate_address_secret_device &s_generate_address_dev, + fcmp_pp::FcmpPpSalProof &sal_proof_out, + crypto::key_image &key_image_out); + +// spend any enote addressed to a carrot or legacy address +void make_sal_proof_any_to_hybrid_v1(const crypto::hash &signable_tx_hash, + const FcmpRerandomizedOutputCompressed &rerandomized_output, + const OutputOpeningHintVariant &opening_hint, + const crypto::secret_key &k_privkey_g, + const crypto::secret_key &k_privkey_t, + const view_balance_secret_device *s_view_balance_dev, + const view_incoming_key_device &k_view_incoming_dev, + const address_device &addr_dev, + fcmp_pp::FcmpPpSalProof &sal_proof_out, + crypto::key_image &key_image_out); + +} //namespace carrot diff --git a/src/carrot_impl/tx_builder_outputs.cpp b/src/carrot_impl/tx_builder_outputs.cpp new file mode 100644 index 00000000000..1285512b26c --- /dev/null +++ b/src/carrot_impl/tx_builder_outputs.cpp @@ -0,0 +1,295 @@ +// 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 "tx_builder_outputs.h" + +//local headers +#include "carrot_core/enote_utils.h" +#include "carrot_core/exceptions.h" +#include "carrot_core/output_set_finalization.h" +#include "common/container_helpers.h" +#include "cryptonote_basic/cryptonote_format_utils.h" +#include "format_utils.h" +#include "ringct/rctSigs.h" + +//third party headers + +//standard headers + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "carrot_impl.tbo" + +namespace carrot +{ +//------------------------------------------------------------------------------------------------------------------- +void get_sorted_input_key_images_from_proposal_v1(const CarrotTransactionProposalV1 &tx_proposal, + const key_image_device &key_image_dev, + std::vector &sorted_key_images_out, + std::vector *order_out) +{ + const std::size_t n_inputs = tx_proposal.input_proposals.size(); + sorted_key_images_out.clear(); + sorted_key_images_out.reserve(n_inputs); + if (order_out) + { + order_out->clear(); + order_out->reserve(n_inputs); + } + + // derive key images + std::vector> sortable_data; + sortable_data.reserve(tx_proposal.input_proposals.size()); + for (std::size_t i = 0; i < n_inputs; ++i) + sortable_data.emplace_back(key_image_dev.derive_key_image(tx_proposal.input_proposals.at(i)), i); + + // sort key images + std::sort(sortable_data.begin(), sortable_data.end(), std::greater{}); + + // collect result + for (const auto &p : sortable_data) + { + sorted_key_images_out.push_back(p.first); + if (order_out) + order_out->push_back(p.second); + } +} +//------------------------------------------------------------------------------------------------------------------- +void get_output_enote_proposals_from_proposal_v1(const CarrotTransactionProposalV1 &tx_proposal, + const view_balance_secret_device *s_view_balance_dev, + const view_incoming_key_device *k_view_dev, + const crypto::key_image &tx_first_key_image, + std::vector &output_enote_proposals_out, + encrypted_payment_id_t &encrypted_payment_id_out, + std::vector> *payment_proposal_order_out) +{ + // collect self-sends proposal cores + std::vector selfsend_payment_proposal_cores; + selfsend_payment_proposal_cores.reserve(tx_proposal.selfsend_payment_proposals.size()); + for (const auto &selfsend_payment_proposal : tx_proposal.selfsend_payment_proposals) + selfsend_payment_proposal_cores.push_back(selfsend_payment_proposal.proposal); + + // derive enote proposals + get_output_enote_proposals(tx_proposal.normal_payment_proposals, + selfsend_payment_proposal_cores, + tx_proposal.dummy_encrypted_payment_id, + s_view_balance_dev, + k_view_dev, + tx_first_key_image, + output_enote_proposals_out, + encrypted_payment_id_out, + payment_proposal_order_out); +} +//------------------------------------------------------------------------------------------------------------------- +void get_output_enote_proposals_from_proposal_v1(const CarrotTransactionProposalV1 &tx_proposal, + const view_balance_secret_device *s_view_balance_dev, + const view_incoming_key_device *k_view_dev, + const key_image_device &key_image_dev, + std::vector &output_enote_proposals_out, + encrypted_payment_id_t &encrypted_payment_id_out, + std::vector> *payment_proposal_order_out) +{ + // derive input key images + std::vector sorted_input_key_images; + get_sorted_input_key_images_from_proposal_v1(tx_proposal, key_image_dev, sorted_input_key_images); + CARROT_CHECK_AND_THROW(!sorted_input_key_images.empty(), too_few_inputs, "No inputs in proposal"); + + get_output_enote_proposals_from_proposal_v1(tx_proposal, + s_view_balance_dev, + k_view_dev, + sorted_input_key_images.at(0), + output_enote_proposals_out, + encrypted_payment_id_out, + payment_proposal_order_out); +} +//------------------------------------------------------------------------------------------------------------------- +void get_sender_receiver_secrets_from_proposal_v1(const std::vector &normal_payment_proposals, + const std::vector &selfsend_payment_proposals, + const view_balance_secret_device *s_view_balance_dev, + const view_incoming_key_device *k_view_dev, + const crypto::key_image &tx_first_key_image, + std::vector &s_sender_receiver_out, + std::vector> &payment_proposal_order_out) +{ + s_sender_receiver_out.clear(); + + const std::size_t n_outputs = normal_payment_proposals.size() + selfsend_payment_proposals.size(); + s_sender_receiver_out.reserve(n_outputs); + + // collect self-sends proposal cores + std::vector selfsend_payment_proposal_cores; + selfsend_payment_proposal_cores.reserve(selfsend_payment_proposals.size()); + for (const auto &selfsend_payment_proposal : selfsend_payment_proposals) + selfsend_payment_proposal_cores.push_back(selfsend_payment_proposal.proposal); + + // derive enote proposals (only to get proposal order in output set) + std::vector output_enote_proposals_out; + encrypted_payment_id_t encrypted_payment_id; + get_output_enote_proposals(normal_payment_proposals, + selfsend_payment_proposal_cores, + encrypted_payment_id_t{}, + s_view_balance_dev, + k_view_dev, + tx_first_key_image, + output_enote_proposals_out, + encrypted_payment_id, + &payment_proposal_order_out); + + // special case: 2-out, 2-selfsend tx + if (n_outputs == 2 && selfsend_payment_proposals.size() == 2) + { + s_sender_receiver_out.push_back(rct::rct2sk(rct::identity())); //! @TODO + return; + } + + // derive s_sr in finalized output order + for (const std::pair &payment_proposal_idx : payment_proposal_order_out) + { + const bool is_selfsend = payment_proposal_idx.first; + if (is_selfsend) + { + if (n_outputs == 2) + continue; + s_sender_receiver_out.push_back(rct::rct2sk(rct::identity())); //! @TODO + } + else + { + const auto &normal_payment_proposal = normal_payment_proposals.at(payment_proposal_idx.second); + // d_e = H_n(anchor_norm, input_context, K^j_s, pid)) + const crypto::secret_key enote_ephemeral_privkey = get_enote_ephemeral_privkey(normal_payment_proposal, + make_carrot_input_context(tx_first_key_image)); + // s_sr = d_e ConvertPointE(K^j_v) + mx25519_pubkey s_sender_receiver; + CARROT_CHECK_AND_THROW(try_make_carrot_shared_key_sender(enote_ephemeral_privkey, + normal_payment_proposal.destination.address_view_pubkey, s_sender_receiver), + invalid_point, "Invalid address view pubkey"); + crypto::secret_key &s_sender_receiver_sk = s_sender_receiver_out.emplace_back(); + memcpy(&unwrap(unwrap(s_sender_receiver_sk)), &s_sender_receiver, sizeof(s_sender_receiver_sk)); + } + } +} +//------------------------------------------------------------------------------------------------------------------- +void make_signable_tx_hash_from_proposal_v1(const CarrotTransactionProposalV1 &tx_proposal, + const view_balance_secret_device *s_view_balance_dev, + const view_incoming_key_device *k_view_dev, + const std::vector &sorted_input_key_images, + crypto::hash &signable_tx_hash_out) +{ + //! @TODO: there's a more efficient way to do this than constructing&serializing a whole cryptonote::transaction + // HW devices will need to implement this function to sign tx proposals, and most of these devices don't have a lot of memory + + cryptonote::transaction pruned_tx; + make_pruned_transaction_from_proposal_v1(tx_proposal, + s_view_balance_dev, + k_view_dev, + sorted_input_key_images, + pruned_tx); + + signable_tx_hash_out = calculate_signable_fcmp_pp_transaction_hash(pruned_tx); +} +//------------------------------------------------------------------------------------------------------------------- +void make_signable_tx_hash_from_proposal_v1(const CarrotTransactionProposalV1 &tx_proposal, + const view_balance_secret_device *s_view_balance_dev, + const view_incoming_key_device *k_view_dev, + const key_image_device &key_image_dev, + crypto::hash &signable_tx_hash_out) +{ + // derive input key images + std::vector sorted_input_key_images; + get_sorted_input_key_images_from_proposal_v1(tx_proposal, key_image_dev, sorted_input_key_images); + CARROT_CHECK_AND_THROW(!sorted_input_key_images.empty(), too_few_inputs, "No inputs in proposal"); + + make_signable_tx_hash_from_proposal_v1(tx_proposal, + s_view_balance_dev, + k_view_dev, + sorted_input_key_images, + signable_tx_hash_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_pruned_transaction_from_proposal_v1(const CarrotTransactionProposalV1 &tx_proposal, + const view_balance_secret_device *s_view_balance_dev, + const view_incoming_key_device *k_view_dev, + const std::vector &sorted_input_key_images, + cryptonote::transaction &pruned_tx_out) +{ + const std::size_t n_inputs = tx_proposal.input_proposals.size(); + CARROT_CHECK_AND_THROW(n_inputs, too_few_inputs, "No inputs in proposal"); + CARROT_CHECK_AND_THROW(sorted_input_key_images.size() == n_inputs, + too_few_inputs, "Mismatching size of transaction input proposal and passed input key images"); + + // derive enote proposals + std::vector output_enote_proposals; + encrypted_payment_id_t encrypted_payment_id; + get_output_enote_proposals_from_proposal_v1(tx_proposal, + s_view_balance_dev, + k_view_dev, + sorted_input_key_images.at(0), + output_enote_proposals, + encrypted_payment_id); + + // collect enotes + std::vector enotes; + enotes.reserve(output_enote_proposals.size()); + for (const RCTOutputEnoteProposal &output_enote_proposal : output_enote_proposals) + enotes.push_back(output_enote_proposal.enote); + + // serialize tx + pruned_tx_out = store_carrot_to_transaction_v1(enotes, + sorted_input_key_images, + tx_proposal.fee, + encrypted_payment_id); + + // add extra payload and sort + if (!tx_proposal.extra.empty()) + { + std::vector sorted_extra; + pruned_tx_out.extra.insert(pruned_tx_out.extra.end(), tx_proposal.extra.cbegin(), tx_proposal.extra.cend()); + CHECK_AND_ASSERT_THROW_MES(cryptonote::sort_tx_extra(pruned_tx_out.extra, sorted_extra), + "make_pruned_transaction_from_proposal_v1: failed to sort tx extra"); + pruned_tx_out.extra = std::move(sorted_extra); + } +} +//------------------------------------------------------------------------------------------------------------------- +void make_pruned_transaction_from_proposal_v1(const CarrotTransactionProposalV1 &tx_proposal, + const view_balance_secret_device *s_view_balance_dev, + const view_incoming_key_device *k_view_dev, + const key_image_device &key_image_dev, + cryptonote::transaction &pruned_tx_out) +{ + // derive input key images + std::vector sorted_input_key_images; + get_sorted_input_key_images_from_proposal_v1(tx_proposal, key_image_dev, sorted_input_key_images); + + make_pruned_transaction_from_proposal_v1(tx_proposal, + s_view_balance_dev, + k_view_dev, + sorted_input_key_images, + pruned_tx_out); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace carrot diff --git a/src/carrot_impl/tx_builder_outputs.h b/src/carrot_impl/tx_builder_outputs.h new file mode 100644 index 00000000000..7272e067c72 --- /dev/null +++ b/src/carrot_impl/tx_builder_outputs.h @@ -0,0 +1,133 @@ +// Copyright (c) 2025, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +//local headers +#include "cryptonote_basic/cryptonote_basic.h" +#include "key_image_device.h" +#include "tx_proposal.h" + +//third party headers + +//standard headers + +//forward declarations + +namespace carrot +{ +/** + * brief: get_sorted_input_key_images_from_proposal_v1 - + * param: tx_proposal - + * param: key_image_dev - + * outparam: sorted_key_images_out - + * outparam: order_out - order of input proposals in key image list [OPTIONAL] + */ +void get_sorted_input_key_images_from_proposal_v1(const CarrotTransactionProposalV1 &tx_proposal, + const key_image_device &key_image_dev, + std::vector &sorted_key_images_out, + std::vector *order_out = nullptr); +/** + * brief: get_output_enote_proposals_from_proposal_v1 - get_output_enote_proposals for transaction proposals + * param: tx_proposal - transaction proposal + * param: s_view_balance_dev - device for s_vb (optional) + * param: k_view_dev - device for k_v (optional) + * outparam: output_enote_proposals_out - resultant output enote set + * putparam: encrypted_payment_id_out - resultant pid_enc + * outparam: payment_proposal_order_out - order of payment proposals in resultant output enote set + */ +void get_output_enote_proposals_from_proposal_v1(const CarrotTransactionProposalV1 &tx_proposal, + const view_balance_secret_device *s_view_balance_dev, + const view_incoming_key_device *k_view_dev, + const crypto::key_image &tx_first_key_image, + std::vector &output_enote_proposals_out, + encrypted_payment_id_t &encrypted_payment_id_out, + std::vector> *payment_proposal_order_out = nullptr); +void get_output_enote_proposals_from_proposal_v1(const CarrotTransactionProposalV1 &tx_proposal, + const view_balance_secret_device *s_view_balance_dev, + const view_incoming_key_device *k_view_dev, + const key_image_device &key_image_dev, + std::vector &output_enote_proposals_out, + encrypted_payment_id_t &encrypted_payment_id_out, + std::vector> *payment_proposal_order_out = nullptr); +/** + * brief: get_sender_receiver_secrets_from_proposal_v1 - get s_sr for all enotes in finalized order + * param: normal_payment_proposals - + * param: selfsend_payment_proposals - + * param: s_view_balance_dev - device for s_vb (optional) + * param: k_view_dev - device for k_v (optional) + * param: tx_first_key_image - KI_1 + * outparam: s_sender_receiver_out - s_sr for each enote in order of output enote enote set + * outparam: payment_proposal_order_out - order of payment proposals in resultant output enote set + */ +void get_sender_receiver_secrets_from_proposal_v1(const std::vector &normal_payment_proposals, + const std::vector &selfsend_payment_proposals, + const view_balance_secret_device *s_view_balance_dev, + const view_incoming_key_device *k_view_dev, + const crypto::key_image &tx_first_key_image, + std::vector &s_sender_receiver_out, + std::vector> &payment_proposal_order_out); +/** + * brief: make_signable_tx_hash_from_proposal_v1 - make signable transaction hash from tx proposal and keys + * param: tx_proposal - transaction proposal + * param: s_view_balance_dev - device for s_vb (optional) + * param: k_view_dev - device for k_v (optional) + * param: sorted_input_key_images - + * param: key_image_dev - device for deriving key images + * outparam: signable_tx_hash_out - signable transaction hash + */ +void make_signable_tx_hash_from_proposal_v1(const CarrotTransactionProposalV1 &tx_proposal, + const view_balance_secret_device *s_view_balance_dev, + const view_incoming_key_device *k_view_dev, + const std::vector &sorted_input_key_images, + crypto::hash &signable_tx_hash_out); +void make_signable_tx_hash_from_proposal_v1(const CarrotTransactionProposalV1 &tx_proposal, + const view_balance_secret_device *s_view_balance_dev, + const view_incoming_key_device *k_view_dev, + const key_image_device &key_image_dev, + crypto::hash &signable_tx_hash_out); +/** + * brief: make_pruned_transaction_from_proposal_v1 - make pruned Carrot/FCMP++ transaction from tx proposal and keys + * param: tx_proposal - transaction proposal + * param: s_view_balance_dev - device for s_vb (optional) + * param: k_view_dev - device for k_v (optional) + * param: sorted_input_key_images - + * param: key_image_dev - device for deriving key images + * outparam: pruned_tx_out - pruned Carrot/FCMP++ transaction represented by transaction proposal + */ +void make_pruned_transaction_from_proposal_v1(const CarrotTransactionProposalV1 &tx_proposal, + const view_balance_secret_device *s_view_balance_dev, + const view_incoming_key_device *k_view_dev, + const std::vector &sorted_input_key_images, + cryptonote::transaction &pruned_tx_out); +void make_pruned_transaction_from_proposal_v1(const CarrotTransactionProposalV1 &tx_proposal, + const view_balance_secret_device *s_view_balance_dev, + const view_incoming_key_device *k_view_dev, + const key_image_device &key_image_dev, + cryptonote::transaction &pruned_tx_out); +} //namespace carrot diff --git a/src/carrot_impl/tx_proposal.h b/src/carrot_impl/tx_proposal.h new file mode 100644 index 00000000000..cf5ad2f967e --- /dev/null +++ b/src/carrot_impl/tx_proposal.h @@ -0,0 +1,92 @@ +// Copyright (c) 2025, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +//local headers +#include "carrot_core/payment_proposal.h" +#include "output_opening_types.h" + +//third party headers + +//standard headers +#include + +//forward declarations + +namespace carrot +{ + +using InputProposalV1 = OutputOpeningHintVariant; + +/** + * brief: CarrotPaymentProposalVerifiableSelfSendV1 - A selfsend payment proposal, verified to an owned address + * + * The `subaddr_index` field is intended to be be used to derive + * `proposal.destination_address_spend_pubkey`, without the need for a subaddress lookahead table. + */ +struct CarrotPaymentProposalVerifiableSelfSendV1 +{ + CarrotPaymentProposalSelfSendV1 proposal; + subaddress_index_extended subaddr_index; +}; + +/** + * brief: CarrotTransactionProposalV1 - A specification on how to construct a Carrot transaction, minus key material + * + * The fields in `CarrotTransactionProposalV1` are chosen as what is the absolute minimum amount of + * information required to verifiably, in a human-meaningful way, reconstruct the "signable + * transaction hash". The signable transaction hash, (usually written `signable_tx_hash` in code), + * is the actual 32-byte message that the FCMP++ Spend-Authorization & Linkability (SA/L) proofs + * sign and verify against. In situations where the signing device may be separate from the device + * formulating the transaction (e.g. cold signing, hardware devices, multisig, etc), this struct + * can be passed amongst signers so that the signers can actually verifying what they are signing. + * + * For exact details on what goes into the signable transaction hash, see `rct::get_pre_mlsag_hash`. + */ +struct CarrotTransactionProposalV1 +{ + /// Spent enote and corresponding opening info per input + std::vector input_proposals; + + /// Payment proposals to be converted into output enotes + std::vector normal_payment_proposals; + std::vector selfsend_payment_proposals; + + /// This field should be uniformly randomly generated. It is used to populate the encrypted + /// payment ID field in the transaction when none of `normal_payment_proposals` are addressed + /// to integrated addresses. + encrypted_payment_id_t dummy_encrypted_payment_id; + /// Fee to miner + rct::xmr_amount fee; + + /// This field is truly "extra". It should contain only tx.extra fields that aren't present in a + /// normal Carrot transaction, i.e. NOT ephemeral pubkeys nor encrypted PIDs + std::vector extra; +}; +} //namespace carrot diff --git a/src/carrot_impl/tx_proposal_utils.cpp b/src/carrot_impl/tx_proposal_utils.cpp new file mode 100644 index 00000000000..882ea52b2be --- /dev/null +++ b/src/carrot_impl/tx_proposal_utils.cpp @@ -0,0 +1,464 @@ +// 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 "tx_proposal_utils.h" + +//local headers +#include "carrot_core/exceptions.h" +#include "carrot_core/output_set_finalization.h" +#include "cryptonote_basic/cryptonote_format_utils.h" +#include "format_utils.h" +#include "misc_log_ex.h" + +//third party headers + +//standard headers +#include "optional" +#include "variant" + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "carrot_impl.tpu" + +namespace carrot +{ +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static void append_additional_payment_proposal_if_necessary( + std::vector& normal_payment_proposals_inout, + std::vector &selfsend_payment_proposals_inout, + const crypto::public_key &change_address_spend_pubkey, + const subaddress_index_extended &change_subaddr_index) +{ + struct append_additional_payment_proposal_if_necessary_visitor + { + void operator()(std::nullopt_t) const {} + void operator()(const CarrotPaymentProposalV1 &p) const { normal_proposals_inout.push_back(p); } + void operator()(const CarrotPaymentProposalSelfSendV1 &p) const + { + selfsend_proposals_inout.push_back(CarrotPaymentProposalVerifiableSelfSendV1{ + .proposal = p, + .subaddr_index = change_subaddr_index + }); + } + + std::vector& normal_proposals_inout; + std::vector &selfsend_proposals_inout; + const subaddress_index_extended &change_subaddr_index; + }; + + bool have_payment_type_selfsend = false; + for (const CarrotPaymentProposalVerifiableSelfSendV1 &selfsend_payment_proposal : selfsend_payment_proposals_inout) + if (selfsend_payment_proposal.proposal.enote_type == CarrotEnoteType::PAYMENT) + have_payment_type_selfsend = true; + + const auto additional_payment_proposal = get_additional_payment_proposal(normal_payment_proposals_inout.size(), + selfsend_payment_proposals_inout.size(), + /*needed_change_amount=*/0, + have_payment_type_selfsend, + change_address_spend_pubkey); + + std::visit(append_additional_payment_proposal_if_necessary_visitor{ + normal_payment_proposals_inout, + selfsend_payment_proposals_inout, + change_subaddr_index + }, additional_payment_proposal); +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_transaction_proposal_v1(const std::vector &normal_payment_proposals_in, + const std::vector &selfsend_payment_proposals_in, + const rct::xmr_amount fee_per_weight, + const std::vector &extra, + select_inputs_func_t &&select_inputs, + carve_fees_and_balance_func_t &&carve_fees_and_balance, + const crypto::public_key &change_address_spend_pubkey, + const subaddress_index_extended &change_address_index, + CarrotTransactionProposalV1 &tx_proposal_out) +{ + tx_proposal_out.extra = extra; + + std::vector &normal_payment_proposals + = tx_proposal_out.normal_payment_proposals + = normal_payment_proposals_in; + std::vector &selfsend_payment_proposals + = tx_proposal_out.selfsend_payment_proposals + = selfsend_payment_proposals_in; + + // add an additional payment proposal to satisfy scanning/consensus rules, if applicable + append_additional_payment_proposal_if_necessary(normal_payment_proposals, + selfsend_payment_proposals, + change_address_spend_pubkey, + change_address_index); + + const size_t num_outs = normal_payment_proposals.size() + selfsend_payment_proposals.size(); + CHECK_AND_ASSERT_THROW_MES(num_outs >= CARROT_MIN_TX_OUTPUTS, + "make_carrot_transaction_proposal_v1: too few outputs"); + + // generate random X25519 ephemeral pubkeys for selfsend proposals if: + // a. not explicitly provided in a >2-out tx, OR + // b. not explicitly provided in a 2-out 2-self-send tx and the other is also missing + const bool should_gen_selfsend_ephemeral_pubkeys = num_outs != 2 || + (normal_payment_proposals.empty() + && !selfsend_payment_proposals.at(0).proposal.enote_ephemeral_pubkey + && !selfsend_payment_proposals.at(1).proposal.enote_ephemeral_pubkey); + if (should_gen_selfsend_ephemeral_pubkeys) + { + for (size_t i = 0; i < selfsend_payment_proposals.size(); ++i) + { + // should not provide two different D_e in a 2-out tx, so skip the second D_e in a 2-out + const bool should_skip_generating = num_outs == 2 && i == 1; + if (should_skip_generating) + continue; + CarrotPaymentProposalVerifiableSelfSendV1 &selfsend_payment_proposal = selfsend_payment_proposals[i]; + if (!selfsend_payment_proposal.proposal.enote_ephemeral_pubkey) + selfsend_payment_proposal.proposal.enote_ephemeral_pubkey = gen_x25519_pubkey(); + } + } + + // generate random dummy encrypted payment ID for if none of the normal payment proposals are integrated + tx_proposal_out.dummy_encrypted_payment_id = gen_encrypted_payment_id(); + + // calculate the final size of tx.extra + const size_t tx_extra_size = get_carrot_default_tx_extra_size(num_outs) + extra.size(); + + // calculate the concrete fee for this transaction for each possible valid input count + std::map fee_per_input_count; + for (size_t num_ins = CARROT_MIN_TX_INPUTS; num_ins <= FCMP_PLUS_PLUS_MAX_INPUTS; ++num_ins) + { + const uint64_t tx_weight = cryptonote::get_fcmp_pp_transaction_weight_v1(num_ins, num_outs, tx_extra_size); + CHECK_AND_ASSERT_THROW_MES(tx_weight != std::numeric_limits::max(), + "make_carrot_transaction_proposal_v1: invalid weight returned for ins=" << num_ins + << " outs=" << num_outs << " extra_size=" << tx_extra_size); + CHECK_AND_ASSERT_THROW_MES(std::numeric_limits::max() / tx_weight > fee_per_weight, + "make_carrot_transaction_proposal_v1: overflow in fee calculation"); + const rct::xmr_amount fee = tx_weight * fee_per_weight; + fee_per_input_count.emplace(num_ins, fee); + } + + // calculate sum of payment proposal amounts before fee carving + boost::multiprecision::uint128_t nominal_output_amount_sum = 0; + for (const CarrotPaymentProposalV1 &normal_proposal : normal_payment_proposals) + nominal_output_amount_sum += normal_proposal.amount; + for (const CarrotPaymentProposalVerifiableSelfSendV1 &selfsend_proposal : selfsend_payment_proposals) + nominal_output_amount_sum += selfsend_proposal.proposal.amount; + + // callback to select inputs given nominal output sum and fee per input count + std::vector selected_inputs; + select_inputs(nominal_output_amount_sum, + fee_per_input_count, + normal_payment_proposals.size(), + selfsend_payment_proposals.size(), + selected_inputs); + + // get fee given the number of selected inputs + const std::size_t n_inputs = selected_inputs.size(); + CARROT_CHECK_AND_THROW(n_inputs >= CARROT_MIN_TX_INPUTS, + too_few_inputs, "input selection returned too few inputs: " << n_inputs); + CARROT_CHECK_AND_THROW(n_inputs <= FCMP_PLUS_PLUS_MAX_INPUTS, + too_many_inputs, "input selection returned too many inputs: " << n_inputs); + CARROT_CHECK_AND_THROW(fee_per_input_count.count(n_inputs), + carrot_logic_error, "BUG: fee_per_input_count populated with holes, missing: " << n_inputs); + tx_proposal_out.fee = fee_per_input_count.at(n_inputs); + + // calculate input amount sum + boost::multiprecision::uint128_t input_amount_sum = 0; + for (const CarrotSelectedInput &selected_input : selected_inputs) + input_amount_sum += selected_input.amount; + + // callback to balance the outputs with the fee and input sum + carve_fees_and_balance(input_amount_sum, tx_proposal_out.fee, normal_payment_proposals, selfsend_payment_proposals); + + // sanity check balance + input_amount_sum -= tx_proposal_out.fee; + for (const CarrotPaymentProposalV1 &normal_payment_proposal : normal_payment_proposals) + input_amount_sum -= normal_payment_proposal.amount; + for (const CarrotPaymentProposalVerifiableSelfSendV1 &selfsend_payment_proposal : selfsend_payment_proposals) + input_amount_sum -= selfsend_payment_proposal.proposal.amount; + CHECK_AND_ASSERT_THROW_MES(input_amount_sum == 0, + "make_carrot_transaction_proposal_v1: post-carved transaction does not balance"); + + // collect input proposals + tx_proposal_out.input_proposals.reserve(selected_inputs.size()); + for (const CarrotSelectedInput &selected_input : selected_inputs) + tx_proposal_out.input_proposals.push_back(selected_input.input); +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_transaction_proposal_v1_transfer( + const std::vector &normal_payment_proposals, + const std::vector &selfsend_payment_proposals_in, + const rct::xmr_amount fee_per_weight, + const std::vector &extra, + select_inputs_func_t &&select_inputs, + const crypto::public_key &change_address_spend_pubkey, + const subaddress_index_extended &change_address_index, + const std::set &subtractable_normal_payment_proposals, + const std::set &subtractable_selfsend_payment_proposals, + CarrotTransactionProposalV1 &tx_proposal_out) +{ + std::vector selfsend_payment_proposals = selfsend_payment_proposals_in; + + // always add implicit selfsend enote, so resultant enotes' amounts mirror given payments set close as possible + // note: we always do this, even if the amount ends up being 0 and we already have a selfsend. this is because if we + // realize later that the change output we added here has a 0 amount, and we try removing it, then the fee + // would go down and then the change amount *wouldn't* be 0, so it must stay. Although technically, + // the scenario could arise where a change in input selection changes the input sum amount and fee exactly + // such that we could remove the implicit change output and it happens to balance. IMO, handling this edge + // case isn't worth the additional code complexity, and may cause unexpected uniformity issues. The calling + // code might expect that transfers to N destinations always produces a transaction with N+1 outputs + const bool add_payment_type_selfsend = normal_payment_proposals.empty() && + selfsend_payment_proposals.size() == 1 && + selfsend_payment_proposals.at(0).proposal.enote_type == CarrotEnoteType::CHANGE; + + selfsend_payment_proposals.push_back(CarrotPaymentProposalVerifiableSelfSendV1{ + .proposal = CarrotPaymentProposalSelfSendV1{ + .destination_address_spend_pubkey = change_address_spend_pubkey, + .amount = 0, + .enote_type = add_payment_type_selfsend ? CarrotEnoteType::PAYMENT : CarrotEnoteType::CHANGE + }, + .subaddr_index = change_address_index + }); + + // define carves fees and balance callback + carve_fees_and_balance_func_t carve_fees_and_balance = + [ + &subtractable_normal_payment_proposals, + &subtractable_selfsend_payment_proposals + ] + ( + const boost::multiprecision::uint128_t &input_sum_amount, + const rct::xmr_amount fee, + std::vector &normal_payment_proposals, + std::vector &selfsend_payment_proposals + ) + { + // shadow subtractable_selfsend_payment_proposals and adjust for default case (no subtractable provided) + const auto &subtractable_selfsend_payment_proposals_ref = subtractable_selfsend_payment_proposals; + std::set subtractable_selfsend_payment_proposals = subtractable_selfsend_payment_proposals_ref; + if (subtractable_normal_payment_proposals.empty() && subtractable_selfsend_payment_proposals.empty()) + subtractable_selfsend_payment_proposals.insert(selfsend_payment_proposals.size() - 1); + + const bool has_subbable_normal = !subtractable_normal_payment_proposals.empty(); + const bool has_subbable_selfsend = !subtractable_selfsend_payment_proposals.empty(); + const size_t num_normal = normal_payment_proposals.size(); + const size_t num_selfsend = selfsend_payment_proposals.size(); + + // check subbable indices invariants + CHECK_AND_ASSERT_THROW_MES( + !has_subbable_normal || *subtractable_normal_payment_proposals.crbegin() < num_normal, + "make unsigned transaction transfer subtractable: subtractable normal proposal index out of bounds"); + CHECK_AND_ASSERT_THROW_MES( + !has_subbable_selfsend || *subtractable_selfsend_payment_proposals.crbegin() < num_selfsend, + "make unsigned transaction transfer subtractable: subtractable selfsend proposal index out of bounds"); + CHECK_AND_ASSERT_THROW_MES(has_subbable_normal || has_subbable_selfsend, + "make unsigned transaction transfer subtractable: no subtractable indices"); + + // check selfsend proposal invariants + CHECK_AND_ASSERT_THROW_MES(!selfsend_payment_proposals.empty(), + "make unsigned transaction transfer subtractable: missing a selfsend proposal"); + CHECK_AND_ASSERT_THROW_MES(selfsend_payment_proposals.back().proposal.amount == 0, + "make unsigned transaction transfer subtractable: bug: added implicit change output has non-zero amount"); + + // start by setting the last selfsend amount equal to (inputs - outputs), before fee + boost::multiprecision::uint128_t implicit_change_amount = input_sum_amount; + for (const CarrotPaymentProposalV1 &normal_payment_proposal : normal_payment_proposals) + implicit_change_amount -= normal_payment_proposal.amount; + for (const CarrotPaymentProposalVerifiableSelfSendV1 &selfsend_payment_proposal : selfsend_payment_proposals) + implicit_change_amount -= selfsend_payment_proposal.proposal.amount; + + selfsend_payment_proposals.back().proposal.amount = + boost::numeric_cast(implicit_change_amount); + + // deduct an even fee amount from all subtractable outputs + const size_t num_subtractble_normal = subtractable_normal_payment_proposals.size(); + const size_t num_subtractable_selfsend = subtractable_selfsend_payment_proposals.size(); + const size_t num_subtractable = num_subtractble_normal + num_subtractable_selfsend; + const rct::xmr_amount minimum_subtraction = fee / num_subtractable; // no div by 0 since we checked subtractable + for (size_t normal_sub_idx : subtractable_normal_payment_proposals) + { + CarrotPaymentProposalV1 &normal_payment_proposal = normal_payment_proposals[normal_sub_idx]; + CHECK_AND_ASSERT_THROW_MES(normal_payment_proposal.amount >= minimum_subtraction, + "make unsigned transaction transfer subtractable: not enough funds in subtractable payment"); + normal_payment_proposal.amount -= minimum_subtraction; + } + for (size_t selfsend_sub_idx : subtractable_selfsend_payment_proposals) + { + CarrotPaymentProposalSelfSendV1 &selfsend_payment_proposal = + selfsend_payment_proposals[selfsend_sub_idx].proposal; + CHECK_AND_ASSERT_THROW_MES(selfsend_payment_proposal.amount >= minimum_subtraction, + "make unsigned transaction transfer subtractable: not enough funds in subtractable payment"); + selfsend_payment_proposal.amount -= minimum_subtraction; + } + + // deduct 1 at a time from selfsend proposals + rct::xmr_amount fee_remainder = fee % num_subtractable; + for (size_t selfsend_sub_idx : subtractable_selfsend_payment_proposals) + { + if (fee_remainder == 0) + break; + + CarrotPaymentProposalSelfSendV1 &selfsend_payment_proposal = + selfsend_payment_proposals[selfsend_sub_idx].proposal; + CHECK_AND_ASSERT_THROW_MES(selfsend_payment_proposal.amount >= 1, + "make unsigned transaction transfer subtractable: not enough funds in subtractable payment"); + selfsend_payment_proposal.amount -= 1; + fee_remainder -= 1; + } + + // now deduct 1 at a time from normal proposals, shuffled + if (fee_remainder != 0) + { + // create vector of shuffled subtractble normal payment indices + // note: we do this to hide the order that the normal payment proposals were described in this call, in case + // the recipients collude + std::vector shuffled_normal_subtractable(subtractable_normal_payment_proposals.cbegin(), + subtractable_normal_payment_proposals.cend()); + std::shuffle(shuffled_normal_subtractable.begin(), + shuffled_normal_subtractable.end(), + crypto::random_device{}); + + for (size_t normal_sub_idx : shuffled_normal_subtractable) + { + if (fee_remainder == 0) + break; + + CarrotPaymentProposalV1 &normal_payment_proposal = normal_payment_proposals[normal_sub_idx]; + CHECK_AND_ASSERT_THROW_MES(normal_payment_proposal.amount >= 1, + "make unsigned transaction transfer subtractable: not enough funds in subtractable payment"); + normal_payment_proposal.amount -= 1; + fee_remainder -= 1; + } + } + + CHECK_AND_ASSERT_THROW_MES(fee_remainder == 0, + "make unsigned transaction transfer subtractable: bug: fee remainder at end of carve function"); + }; //end carve_fees_and_balance + + // make unsigned transaction with fee carving callback + make_carrot_transaction_proposal_v1(normal_payment_proposals, + selfsend_payment_proposals, + fee_per_weight, + extra, + std::forward(select_inputs), + std::move(carve_fees_and_balance), + change_address_spend_pubkey, + change_address_index, + tx_proposal_out); +} +//------------------------------------------------------------------------------------------------------------------- +void make_carrot_transaction_proposal_v1_sweep( + const std::vector &normal_payment_proposals, + const std::vector &selfsend_payment_proposals, + const rct::xmr_amount fee_per_weight, + const std::vector &extra, + std::vector &&selected_inputs, + const crypto::public_key &change_address_spend_pubkey, + const subaddress_index_extended &change_address_index, + CarrotTransactionProposalV1 &tx_proposal_out) +{ + // sanity check payment proposals are provided + CHECK_AND_ASSERT_THROW_MES(normal_payment_proposals.size() || selfsend_payment_proposals.size(), + "make carrot transaction proposal v1 sweep: no payment proposals provided"); + + // sanity check that all payment proposal amounts are 0 + for (const CarrotPaymentProposalV1 &normal_payment_proposal : normal_payment_proposals) + { + CHECK_AND_ASSERT_THROW_MES(normal_payment_proposal.amount == 0, + "make carrot transaction proposal v1 sweep: payment proposal amount not 0"); + } + for (const CarrotPaymentProposalVerifiableSelfSendV1 &selfsend_payment_proposal : selfsend_payment_proposals) + { + CHECK_AND_ASSERT_THROW_MES(selfsend_payment_proposal.proposal.amount == 0, + "make carrot transaction proposal v1 sweep: payment proposal amount not 0"); + } + + // sanity check that either normal payment proposals XOR selfsend are provided, not both + CHECK_AND_ASSERT_THROW_MES(bool(normal_payment_proposals.size()) ^ bool(selfsend_payment_proposals.size()), + "make carrot transaction proposal v1 sweep: both normal and self-send payment proposals are provided"); + + const bool is_selfsend_sweep = !selfsend_payment_proposals.empty(); + + // define input selection callback, which is just a shuttle for `selected_inputs` + select_inputs_func_t select_inputs = [&selected_inputs] + ( + const boost::multiprecision::uint128_t&, + const std::map&, + const std::size_t, + const std::size_t, + std::vector &selected_inputs_out + ) + { + selected_inputs_out = std::move(selected_inputs); + }; //end select_inputs + + // define carves fees and balance callback + carve_fees_and_balance_func_t carve_fees_and_balance = [is_selfsend_sweep] + ( + const boost::multiprecision::uint128_t &input_sum_amount, + const rct::xmr_amount fee, + std::vector &normal_payment_proposals, + std::vector &selfsend_payment_proposals + ) + { + // get pointers to proposal amounts and shuffle, excluding implicit selfsend + const size_t n_outputs = normal_payment_proposals.size() + selfsend_payment_proposals.size(); + std::vector amount_ptrs; + amount_ptrs.reserve(n_outputs); + if (is_selfsend_sweep) + for (CarrotPaymentProposalVerifiableSelfSendV1 &selfsend_payment_proposal : selfsend_payment_proposals) + amount_ptrs.push_back(&selfsend_payment_proposal.proposal.amount); + else + for (CarrotPaymentProposalV1 &normal_payment_proposal : normal_payment_proposals) + amount_ptrs.push_back(&normal_payment_proposal.amount); + std::shuffle(amount_ptrs.begin(), amount_ptrs.end(), crypto::random_device{}); + + // disburse amount equally amongst modifiable amounts + const boost::multiprecision::uint128_t output_sum_amount = input_sum_amount - fee; + const rct::xmr_amount minimum_sweep_amount = + boost::numeric_cast(output_sum_amount / amount_ptrs.size()); + const size_t num_remaining = + boost::numeric_cast(output_sum_amount % amount_ptrs.size()); + CHECK_AND_ASSERT_THROW_MES(num_remaining < amount_ptrs.size(), + "make carrot transaction proposal v1 sweep: bug: num_remaining >= n_outputs"); + for (size_t i = 0; i < amount_ptrs.size(); ++i) + *amount_ptrs.at(i) = minimum_sweep_amount + (i < num_remaining ? 1 : 0); + }; //end carve_fees_and_balance + + // make unsigned transaction with sweep carving callback and selected inputs + make_carrot_transaction_proposal_v1(normal_payment_proposals, + selfsend_payment_proposals, + fee_per_weight, + extra, + std::move(select_inputs), + std::move(carve_fees_and_balance), + change_address_spend_pubkey, + change_address_index, + tx_proposal_out); +} +//------------------------------------------------------------------------------------------------------------------- +} //namespace carrot diff --git a/src/carrot_impl/tx_proposal_utils.h b/src/carrot_impl/tx_proposal_utils.h new file mode 100644 index 00000000000..b0f2a86ef06 --- /dev/null +++ b/src/carrot_impl/tx_proposal_utils.h @@ -0,0 +1,178 @@ +// Copyright (c) 2025, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//! @file Utilities for creating Carrot transaction proposals + +#pragma once + +//local headers +#include "tx_proposal.h" + +//third party headers +#include + +//standard headers +#include + +//forward declarations + +namespace carrot +{ +struct CarrotSelectedInput +{ + rct::xmr_amount amount; + InputProposalV1 input; +}; +static inline bool operator==(const CarrotSelectedInput &a, const CarrotSelectedInput &b) +{ + return a.amount == b.amount && onetime_address_ref(a.input) == onetime_address_ref(b.input); +} +static inline bool operator!=(const CarrotSelectedInput &a, const CarrotSelectedInput &b) +{ + return !(a == b); +} + +using select_inputs_func_t = std::function&, // absolute fee per input count + const std::size_t, // number of normal payment proposals + const std::size_t, // number of self-send payment proposals + std::vector& // selected inputs result + )>; + +using carve_fees_and_balance_func_t = std::function&, // normal payment proposals [inout] + std::vector& // selfsend payment proposals [inout] + )>; + +/** + * brief: make_carrot_transaction_proposal_v1 - generic core function for forming single Carrot transaction proposals + * param: normal_payment_proposals - normal payment proposals to be included in the tx + * param: selfsend_payment_proposals - selfsend payment proposals to be included in the tx + * param: fee_per_weight - concrete fee is calculated as transaction weight times this value + * param: extra - truly "extra" fields to be included in tx_extra, doesn't include ephemeral tx pubkeys or PIDs + * param: select_inputs - input selection callback (see more below) + * param: carve_fees_and_balance - fee carving callback (see more below) + * param: change_address_spend_pubkey - address spend pubkey to send to for change selfsend enotes + * param: change_address_index - subaddress index of change_address_spend_pubkey in your account + * outparam: tx_proposal_out - a fully formed Carrot transaction proposal + * + * This function will add a selfsend payment proposal if no other selfsend payment proposal is + * passed in, so that all Carrot transactions contain at least 1 selfsend enote, conforming to the + * "Mandatory self-send enote rule." This function also fills in all random fields where applicable + * so that generating enotes and signable transaction hashes from this transaction proposal is + * deterministic. + * + * `select_inputs`: takes in four arguments: 1) the "nominal output sum", which is the sum of the + * amounts in the passed normal and selfsend payment proposals, excluding fee, 2) the concrete fee + * for the transaction indexed by number of inputs, 3) the number of normal payment proposals, and + * 4) the number of selfsend payment proposals, including additional ones added inside the body of + * `make_carrot_transaction_proposal_v1`. `select_inputs` outputs a list of "selected inputs", in + * no particular order, which are each an opening hint for a spent enote, and a corresponding + * amount. Because the weight of a FCMP++ transaction is simply a function of number of inputs, + * number of outputs, and tx_extra size, the exact concrete fee for each potential input count is + * calculated and passed to the input selection callback for ease of algorithms. + * + * `carve_fees_and_balance`: takes in the sum of input amounts, the concrete fee, and the payment + * proposals by reference. It should modify the *amounts* of the payment proposals such that + * the sum of amounts of all payment proposals + fee = input amount sum. Any selfsend payment + * proposal created in the body of this function will appear at the end of selfsend list. + */ +void make_carrot_transaction_proposal_v1(const std::vector &normal_payment_proposals, + const std::vector &selfsend_payment_proposals, + const rct::xmr_amount fee_per_weight, + const std::vector &extra, + select_inputs_func_t &&select_inputs, + carve_fees_and_balance_func_t &&carve_fees_and_balance, + const crypto::public_key &change_address_spend_pubkey, + const subaddress_index_extended &change_address_index, + CarrotTransactionProposalV1 &tx_proposal_out); +/** + * brief: make_carrot_transaction_proposal_v1_transfer - make a "transfer-style" Carrot transaction proposal + * param: normal_payment_proposals - normal payment proposals to be included in the tx + * param: selfsend_payment_proposals - selfsend payment proposals to be included in the tx + * param: fee_per_weight - concrete fee is calculated as transaction weight times this value + * param: extra - truly "extra" fields to be included in tx_extra, doesn't include ephemeral tx pubkeys or PIDs + * param: select_inputs - input selection callback (see make_carrot_transaction_proposal_v1 for info) + * param: change_address_spend_pubkey - address spend pubkey to send to for change selfsend enotes + * param: change_address_index - subaddress index of change_address_spend_pubkey in your account + * param: subtractable_normal_payment_proposals - indices of normal payment proposals which are "fee subtractable" + * param: subtractable_selfsend_payment_proposals - indices of selfsend payment proposals which are "fee subtractable" + * outparam: tx_proposal_out - a fully formed Carrot transaction proposal + * + * This function *always* adds an additional selfsend enote not in `selfsend_payment_proposals`, + * even if not strictly needed. Any leftover "change" XMR after fulfilling the passed payment + * proposals is assigned to the aforementioned additional selfsend proposal. + * + * All passed payment proposals in `normal_payment_proposals` and `selfsend_payment_proposals` are + * included in final transaction proposal as-is with their passed amount, *unless* they are marked + * as "fee subtractable" using `subtractable_normal_payment_proposals` or + * `subtractable_selfsend_payment_proposals`, respectively. If at least one payment proposal is + * marked as fee subtractable, then the fee is split evenly amongst those payments are subtracted + * from their respective amount. If no payment proposal is marked as fee subtractable, then the fee + * is subtracted from the additional selfsend proposal. + */ +void make_carrot_transaction_proposal_v1_transfer( + const std::vector &normal_payment_proposals, + const std::vector &selfsend_payment_proposals, + const rct::xmr_amount fee_per_weight, + const std::vector &extra, + select_inputs_func_t &&select_inputs, + const crypto::public_key &change_address_spend_pubkey, + const subaddress_index_extended &change_address_index, + const std::set &subtractable_normal_payment_proposals, + const std::set &subtractable_selfsend_payment_proposals, + CarrotTransactionProposalV1 &tx_proposal_out); +/** + * brief: make_carrot_transaction_proposal_v1_sweep - make a "sweep-style" Carrot transaction proposal + * param: normal_payment_proposals - normal payment proposals to be included in the tx (with amount=0) + * param: selfsend_payment_proposals - selfsend payment proposals to be included in the tx (with amount=0) + * param: fee_per_weight - concrete fee is calculated as transaction weight times this value + * param: extra - truly "extra" fields to be included in tx_extra, doesn't include ephemeral tx pubkeys or PIDs + * param: selected_inputs - explicitly provided inputs + * param: change_address_spend_pubkey - address spend pubkey to send to for change selfsend enotes + * param: change_address_index - subaddress index of change_address_spend_pubkey in your account + * outparam: tx_proposal_out - a fully formed Carrot transaction proposal + * + * Unlike with "transfer-style" transactions, this function does *not* add an additional selfsend + * proposal if one is already passed in `selfsend_payment_proposals`. The fee and input amount sum + * is divided equally amongst all payment proposals. + */ +void make_carrot_transaction_proposal_v1_sweep( + const std::vector &normal_payment_proposals, + const std::vector &selfsend_payment_proposals, + const rct::xmr_amount fee_per_weight, + const std::vector &extra, + std::vector &&selected_inputs, + const crypto::public_key &change_address_spend_pubkey, + const subaddress_index_extended &change_address_index, + CarrotTransactionProposalV1 &tx_proposal_out); + +} //namespace carrot diff --git a/src/common/container_helpers.h b/src/common/container_helpers.h index 45d29a42df7..e19d6089a1a 100644 --- a/src/common/container_helpers.h +++ b/src/common/container_helpers.h @@ -160,4 +160,44 @@ void for_all_in_map_erase_if(std::unordered_map &map_inout, PredT } } +/// convenience erasor for vectors, with total O(N) runtime. Erases all elements X when predicate(X) +/// returns true. No guarantees are made about order of resulting vector if predicate(X) returns +// true for any element X in vec_inout. Otherwise, vec_inout is not modified +template +void for_all_in_vector_erase_no_preserve_order_if(std::vector &vec_inout, PredT predicate) +{ + static_assert( + std::is_same< + bool, + decltype(predicate(std::declval>())) + >::value, + "invalid callable - expected callable in form bool(Value)" + ); + + for (auto vec_it = vec_inout.begin(); vec_it != vec_inout.end();) + { + if (predicate(*vec_it)) + { + const auto it_to_last = vec_inout.end() - 1; + if (vec_it != it_to_last) // i.e. not last element + { + *vec_it = std::move(*it_to_last); + } + vec_inout.pop_back(); + } + else + { + ++vec_it; + } + } +} + +/// erase with a reverse iterator +/// returned iterator is (pos + 1) if pos hadn't been erased +template +std::reverse_iterator reverse_erase(ContainerT &container, std::reverse_iterator pos) +{ + return std::reverse_iterator(container.erase((++pos).base())); +} + } //namespace tools diff --git a/src/common/merge_sorted_vectors.h b/src/common/merge_sorted_vectors.h new file mode 100644 index 00000000000..e3ffb8bcda7 --- /dev/null +++ b/src/common/merge_sorted_vectors.h @@ -0,0 +1,82 @@ +// Copyright (c) 2024, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include +#include + +namespace tools +{ + +template +bool merge_sorted_vectors(const std::vector &a, const std::vector &b, const U &sort_fn, std::vector &v_out) +{ + v_out.clear(); + v_out.reserve(a.size() + b.size()); + + if (!std::is_sorted(a.begin(), a.end(), sort_fn)) + return false; + if (!std::is_sorted(b.begin(), b.end(), sort_fn)) + return false; + + auto a_it = a.begin(); + auto b_it = b.begin(); + + while (a_it != a.end() || b_it != b.end()) + { + if (a_it == a.end()) + { + v_out.push_back(*b_it); + ++b_it; + continue; + } + + if (b_it == b.end()) + { + v_out.push_back(*a_it); + ++a_it; + continue; + } + + if (sort_fn(*a_it, *b_it)) + { + v_out.push_back(*a_it); + ++a_it; + continue; + } + + v_out.push_back(*b_it); + ++b_it; + } + + assert(std::is_sorted(v_out.begin(), v_out.end(), sort_fn)); + return true; +} + +}//namespace tools diff --git a/src/common/perf_timer.h b/src/common/perf_timer.h index 3e53adcb800..a140bb4802d 100644 --- a/src/common/perf_timer.h +++ b/src/common/perf_timer.h @@ -79,7 +79,7 @@ void set_performance_timer_log_level(el::Level level); #define PERF_TIMER_NAME(name) pt_##name #define PERF_TIMER_UNIT(name, unit) tools::LoggingPerformanceTimer PERF_TIMER_NAME(name)(#name, "perf." MONERO_DEFAULT_LOG_CATEGORY, unit, tools::performance_timer_log_level) -#define PERF_TIMER_UNIT_L(name, unit, l) tools::LoggingPerformanceTimer PERF_TIMER_NAME(name)t_##name(#name, "perf." MONERO_DEFAULT_LOG_CATEGORY, unit, l) +#define PERF_TIMER_UNIT_L(name, unit, l) tools::LoggingPerformanceTimer PERF_TIMER_NAME(name)(#name, "perf." MONERO_DEFAULT_LOG_CATEGORY, unit, l) #define PERF_TIMER(name) PERF_TIMER_UNIT(name, 1000000) #define PERF_TIMER_L(name, l) PERF_TIMER_UNIT_L(name, 1000000, l) #define PERF_TIMER_START_UNIT(name, unit) std::unique_ptr PERF_TIMER_NAME(name)(new tools::LoggingPerformanceTimer(#name, "perf." MONERO_DEFAULT_LOG_CATEGORY, unit, el::Level::Info)) diff --git a/src/crypto/CMakeLists.txt b/src/crypto/CMakeLists.txt index b0006faba87..0f8edecb535 100644 --- a/src/crypto/CMakeLists.txt +++ b/src/crypto/CMakeLists.txt @@ -76,6 +76,9 @@ target_link_libraries(cncrypto ${sodium_LIBRARIES} PRIVATE ${EXTRA_LIBRARIES}) +target_include_directories(cncrypto + PRIVATE + ${MX25519_INCLUDE}) if (ARM) option(NO_OPTIMIZED_MULTIPLY_ON_ARM diff --git a/src/crypto/crypto-ops-data.c b/src/crypto/crypto-ops-data.c index edaa4644fd2..af13b94edfa 100644 --- a/src/crypto/crypto-ops-data.c +++ b/src/crypto/crypto-ops-data.c @@ -38,6 +38,13 @@ const fe fe_d = {-10913610, 13857413, -15372611, 6949391, 114729, -8787816, -627 const fe fe_sqrtm1 = {-32595792, -7943725, 9377950, 3500415, 12389472, -272473, -25146209, -2005654, 326686, 11406482}; /* sqrt(-1) */ const fe fe_d2 = {-21827239, -5839606, -30745221, 13898782, 229458, 15978800, -12551817, -6495438, 29715968, 9444199}; /* 2 * d */ +/* a = -1 */ +// TODO: double check these consts +const fe fe_a_sub_d = {10913609, -13857413, 15372611, -6949391, -114729, 8787816, 6275908, 3247719, 18696448, 12055116}; /* a - d */ +const fe fe_a0 = {-21827241, -5839606, -30745221, 13898782, 229458, 15978800, -12551817, -6495438, 29715968, 9444199}; /* A0 = 2 * (a + d) */ +const fe fe_ap = {-23454401, 11679213, -5618422, 5756869, -458917, 1596832, 25103633, 12990876, 7676928, 14666033}; /* Ap = -2 * A0 */ +const fe fe_msqrt2b = {-1359796, -3165658, 8463188, -8916281, -9242332, 8801166, -2887120, 14417306, 28934311, 6371549}; + /* base[i][j] = (j+1)*256^i*B */ const ge_precomp ge_base[32][8] = { { @@ -870,6 +877,11 @@ const fe fe_fffb1 = {-31702527, -2466483, -26106795, -12203692, -12169197, -3210 const fe fe_fffb2 = {8166131, -6741800, -17040804, 3154616, 21461005, 1466302, -30876704, -6368709, 10503587, -13363080}; /* sqrt(2 * A * (A + 2)) */ const fe fe_fffb3 = {-13620103, 14639558, 4532995, 7679154, 16815101, -15883539, -22863840, -14813421, 13716513, -6477756}; /* sqrt(-sqrt(-1) * A * (A + 2)) */ const fe fe_fffb4 = {-21786234, -12173074, 21573800, 4524538, -4645904, 16204591, 8012863, -8444712, 3212926, 6885324}; /* sqrt(sqrt(-1) * A * (A + 2)) */ +const fe fe_a_inv_3 = {-22207407, 11184811, 22369621, -11184811, -22369621, 11184811, 22369621, -11184811, -22369621, 11184811}; /* A / 3*/ +const fe fe_c = {-12222970, -8312128, -11511410, 9067497, -15300785, -241793, 25456130, 14121551, -12187136, 3972024}; /* sqrt(-(A + 2))*/ +const fe fe_one = {1, 0, 0, 0, 0, 0, 0, 0, 0, 0}; +const fe fe_m1 = {-1, 0, 0, 0, 0, 0, 0, 0, 0, 0}; +const fe fe_inv2 = {10, 0, 0, 0, 0, 0, 0, 0, 0, -16777216}; /* 1 / 2 */ const ge_p3 ge_p3_identity = { {0}, {1, 0}, {1, 0}, {0} }; const ge_p3 ge_p3_H = { {7329926, -15101362, 31411471, 7614783, 27996851, -3197071, -11157635, -6878293, 466949, -7986503}, diff --git a/src/crypto/crypto-ops.c b/src/crypto/crypto-ops.c index 08556ab1bef..2dbecedb4ad 100644 --- a/src/crypto/crypto-ops.c +++ b/src/crypto/crypto-ops.c @@ -38,11 +38,9 @@ DISABLE_VS_WARNINGS(4146 4244) /* Predeclarations */ -static void fe_sq(fe, const fe); static void ge_madd(ge_p1p1 *, const ge_p3 *, const ge_precomp *); static void ge_msub(ge_p1p1 *, const ge_p3 *, const ge_precomp *); static void ge_p2_0(ge_p2 *); -static void ge_p3_dbl(ge_p1p1 *, const ge_p3 *); static void fe_divpowm1(fe, const fe, const fe); /* Common functions */ @@ -90,7 +88,7 @@ void fe_0(fe h) { h = 1 */ -static void fe_1(fe h) { +void fe_1(fe h) { h[0] = 1; h[1] = 0; h[2] = 0; @@ -230,7 +228,7 @@ static void fe_cmov(fe f, const fe g, unsigned int b) { h = f */ -static void fe_copy(fe h, const fe f) { +void fe_copy(fe h, const fe f) { int32_t f0 = f[0]; int32_t f1 = f[1]; int32_t f2 = f[2]; @@ -313,6 +311,56 @@ void fe_invert(fe out, const fe z) { return; } +// return 1 if a == b, else 0 +int fe_equals(const fe a, const fe b) { + unsigned char a_bytes[32]; + unsigned char b_bytes[32]; + fe_tobytes(a_bytes, a); + fe_tobytes(b_bytes, b); + + int r = 1; + for (int i = 0; i < 32; ++i) { + r &= a_bytes[i] == b_bytes[i]; + } + return r; +} + +// Montgomery's trick +// https://iacr.org/archive/pkc2004/29470042/29470042.pdf 2.2 +void fe_batch_invert(fe *out, const fe *in, const int n) { + if (n == 0) { + return; + } + + assert(out); + assert(in); +#ifndef NDEBUG + { + // Overlap of `out` and `in` sections not allowed + const fe *pmin = out < in ? out : in; + const fe *pmax = out > in ? out : in; + assert(pmin + n < pmax); + } +#endif + + // Step 1: collect initial muls + fe_copy(out[0], in[0]); + for (int i = 1; i < n; ++i) { + fe_mul(out[i], out[i-1], in[i]); + } + + // Step 2: get the inverse of all elems multiplied together + fe a; + fe_invert(a, out[n-1]); + + // Step 3: get each inverse + for (int i = n; i > 1; --i) { + fe_mul(out[i-1], a, out[i-2]); + fe_mul(a, a, in[i-1]); + } + fe_copy(out[0], a); +} + /* From fe_isnegative.c */ /* @@ -323,7 +371,7 @@ return 0 if f is in {0,2,4,...,q-1} |f| bounded by 1.1*2^26,1.1*2^25,1.1*2^26,1.1*2^25,etc. */ -static int fe_isnegative(const fe f) { +int fe_isnegative(const fe f) { unsigned char s[32]; fe_tobytes(s, f); return s[0] & 1; @@ -604,7 +652,7 @@ h = -f |h| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. */ -static void fe_neg(fe h, const fe f) { +void fe_neg(fe h, const fe f) { int32_t f0 = f[0]; int32_t f1 = f[1]; int32_t f2 = f[2]; @@ -654,7 +702,7 @@ Can overlap h with f. See fe_mul.c for discussion of implementation strategy. */ -static void fe_sq(fe h, const fe f) { +void fe_sq(fe h, const fe f) { int32_t f0 = f[0]; int32_t f1 = f[1]; int32_t f2 = f[2]; @@ -958,7 +1006,7 @@ Can overlap h with f or g. |h| bounded by 1.1*2^26,1.1*2^25,1.1*2^26,1.1*2^25,etc. */ -static void fe_sub(fe h, const fe f, const fe g) { +void fe_sub(fe h, const fe f, const fe g) { int32_t f0 = f[0]; int32_t f1 = f[1]; int32_t f2 = f[2]; @@ -1328,16 +1376,9 @@ void ge_double_scalarmult_base_vartime_p3(ge_p3 *r3, const unsigned char *a, con } } -/* From ge_frombytes.c, modified */ - -int ge_frombytes_vartime(ge_p3 *h, const unsigned char *s) { - fe u; - fe v; - fe vxx; - fe check; - - /* From fe_frombytes.c */ +/* From fe_frombytes.c */ +int fe_frombytes_vartime(fe y, const unsigned char *s) { int64_t h0 = load_4(s); int64_t h1 = load_3(s + 4) << 6; int64_t h2 = load_3(s + 7) << 5; @@ -1378,18 +1419,31 @@ int ge_frombytes_vartime(ge_p3 *h, const unsigned char *s) { carry6 = (h6 + (int64_t) (1<<25)) >> 26; h7 += carry6; h6 -= carry6 << 26; carry8 = (h8 + (int64_t) (1<<25)) >> 26; h9 += carry8; h8 -= carry8 << 26; - h->Y[0] = h0; - h->Y[1] = h1; - h->Y[2] = h2; - h->Y[3] = h3; - h->Y[4] = h4; - h->Y[5] = h5; - h->Y[6] = h6; - h->Y[7] = h7; - h->Y[8] = h8; - h->Y[9] = h9; + y[0] = h0; + y[1] = h1; + y[2] = h2; + y[3] = h3; + y[4] = h4; + y[5] = h5; + y[6] = h6; + y[7] = h7; + y[8] = h8; + y[9] = h9; - /* End fe_frombytes.c */ + return 0; +} + +/* From ge_frombytes.c, modified */ + +int ge_frombytes_vartime(ge_p3 *h, const unsigned char *s) { + fe u; + fe v; + fe vxx; + fe check; + + if (fe_frombytes_vartime(h->Y, s) != 0) { + return -1; + } fe_1(h->Z); fe_sq(u, h->Y); @@ -1529,7 +1583,7 @@ static void ge_p3_0(ge_p3 *h) { r = 2 * p */ -static void ge_p3_dbl(ge_p1p1 *r, const ge_p3 *p) { +void ge_p3_dbl(ge_p1p1 *r, const ge_p3 *p) { ge_p2 q; ge_p3_to_p2(&q, p); ge_p2_dbl(r, &q); @@ -1953,26 +2007,18 @@ void sc_reduce(unsigned char *s) { s[31] = s11 >> 17; } -/* New code */ +/* From fe_pow22523.c */ -static void fe_divpowm1(fe r, const fe u, const fe v) { - fe v3, uv7, t0, t1, t2; +void fe_pow22523(fe out, const fe z) { + fe t0; + fe t1; + fe t2; int i; - fe_sq(v3, v); - fe_mul(v3, v3, v); /* v3 = v^3 */ - fe_sq(uv7, v3); - fe_mul(uv7, uv7, v); - fe_mul(uv7, uv7, u); /* uv7 = uv^7 */ - - /*fe_pow22523(uv7, uv7);*/ - - /* From fe_pow22523.c */ - - fe_sq(t0, uv7); + fe_sq(t0, z); fe_sq(t1, t0); fe_sq(t1, t1); - fe_mul(t1, uv7, t1); + fe_mul(t1, z, t1); fe_mul(t0, t0, t1); fe_sq(t0, t0); fe_mul(t0, t1, t0); @@ -2011,12 +2057,24 @@ static void fe_divpowm1(fe r, const fe u, const fe v) { fe_mul(t0, t1, t0); fe_sq(t0, t0); fe_sq(t0, t0); - fe_mul(t0, t0, uv7); + fe_mul(out, t0, z); +} + +/* New code */ - /* End fe_pow22523.c */ - /* t0 = (uv^7)^((q-5)/8) */ - fe_mul(t0, t0, v3); - fe_mul(r, t0, u); /* u^(m+1)v^(-(m+1)) */ +static void fe_divpowm1(fe r, const fe u, const fe v) { + fe v3, uv7; + + fe_sq(v3, v); + fe_mul(v3, v3, v); /* v3 = v^3 */ + fe_sq(uv7, v3); + fe_mul(uv7, uv7, v); + fe_mul(uv7, uv7, u); /* uv7 = uv^7 */ + + fe_pow22523(r, uv7); /* (uv^7)^((q-5)/8) */ + + fe_mul(r, r, v3); + fe_mul(r, r, u); /* u^(m+1)v^(-(m+1)) */ } static void ge_cached_0(ge_cached *r) { @@ -2429,6 +2487,14 @@ void sc_0(unsigned char *s) { } } +void sc_1(unsigned char *s) { + int i; + s[0] = 1; + for (i = 1; i < 32; i++) { + s[i] = 0; + } +} + void sc_reduce32(unsigned char *s) { int64_t s0 = 2097151 & load_3(s); int64_t s1 = 2097151 & (load_4(s + 2) >> 5); @@ -3847,6 +3913,92 @@ int sc_isnonzero(const unsigned char *s) { s[27] | s[28] | s[29] | s[30] | s[31]) - 1) >> 8) + 1; } +static void edwardsYZ_to_x25519(unsigned char *xbytes, const fe Y, const fe Z) { + // y = Y/Z + // x_mont = (1 + y) / (1 - y) + // = (1 + Y/Z) / (1 - Y/Z) + // = (Z + Y) / (Z - Y) + + fe tmp0; + fe tmp1; + fe_add(tmp0, Z, Y); // Z + Y + fe_sub(tmp1, Z, Y); // Z - Y + fe_invert(tmp1, tmp1); // 1/(Z - Y) + fe_mul(tmp0, tmp0, tmp1); // (Z + Y) / (Z - Y) + fe_tobytes(xbytes, tmp0); // tobytes((Z + Y) / (Z - Y)) +} + +void ge_p3_to_x25519(unsigned char *xbytes, const ge_p3 *h) +{ + edwardsYZ_to_x25519(xbytes, h->Y, h->Z); +} + +int edwards_bytes_to_x25519_vartime(unsigned char *xbytes, const unsigned char *s) +{ + /* From fe_frombytes.c */ + + int64_t h0 = load_4(s); + int64_t h1 = load_3(s + 4) << 6; + int64_t h2 = load_3(s + 7) << 5; + int64_t h3 = load_3(s + 10) << 3; + int64_t h4 = load_3(s + 13) << 2; + int64_t h5 = load_4(s + 16); + int64_t h6 = load_3(s + 20) << 7; + int64_t h7 = load_3(s + 23) << 5; + int64_t h8 = load_3(s + 26) << 4; + int64_t h9 = (load_3(s + 29) & 8388607) << 2; + int64_t carry0; + int64_t carry1; + int64_t carry2; + int64_t carry3; + int64_t carry4; + int64_t carry5; + int64_t carry6; + int64_t carry7; + int64_t carry8; + int64_t carry9; + + /* Validate the number to be canonical */ + if (h9 == 33554428 && h8 == 268435440 && h7 == 536870880 && h6 == 2147483520 && + h5 == 4294967295 && h4 == 67108860 && h3 == 134217720 && h2 == 536870880 && + h1 == 1073741760 && h0 >= 4294967277) { + return -1; + } + + carry9 = (h9 + (int64_t) (1<<24)) >> 25; h0 += carry9 * 19; h9 -= carry9 << 25; + carry1 = (h1 + (int64_t) (1<<24)) >> 25; h2 += carry1; h1 -= carry1 << 25; + carry3 = (h3 + (int64_t) (1<<24)) >> 25; h4 += carry3; h3 -= carry3 << 25; + carry5 = (h5 + (int64_t) (1<<24)) >> 25; h6 += carry5; h5 -= carry5 << 25; + carry7 = (h7 + (int64_t) (1<<24)) >> 25; h8 += carry7; h7 -= carry7 << 25; + + carry0 = (h0 + (int64_t) (1<<25)) >> 26; h1 += carry0; h0 -= carry0 << 26; + carry2 = (h2 + (int64_t) (1<<25)) >> 26; h3 += carry2; h2 -= carry2 << 26; + carry4 = (h4 + (int64_t) (1<<25)) >> 26; h5 += carry4; h4 -= carry4 << 26; + carry6 = (h6 + (int64_t) (1<<25)) >> 26; h7 += carry6; h6 -= carry6 << 26; + carry8 = (h8 + (int64_t) (1<<25)) >> 26; h9 += carry8; h8 -= carry8 << 26; + + fe Y; + Y[0] = h0; + Y[1] = h1; + Y[2] = h2; + Y[3] = h3; + Y[4] = h4; + Y[5] = h5; + Y[6] = h6; + Y[7] = h7; + Y[8] = h8; + Y[9] = h9; + + /* End fe_frombytes.c */ + + fe Z; + fe_1(Z); + + edwardsYZ_to_x25519(xbytes, Y, Z); + + return 0; +} + int ge_p3_is_point_at_infinity_vartime(const ge_p3 *p) { // https://eprint.iacr.org/2008/522 // X == T == 0 and Y/Z == 1 @@ -3895,3 +4047,67 @@ int ge_p3_is_point_at_infinity_vartime(const ge_p3 *p) { // Y/Z = 0/0 return 0; } + +// https://www.ietf.org/archive/id/draft-ietf-lwig-curve-representations-02.pdf E.2 +static void fe_ed_derivatives_to_wei_x(unsigned char *wei_x, const fe inv_one_minus_y, const fe one_plus_y) +{ + // (1/(1-y))*(1+y) + fe inv_one_minus_y_mul_one_plus_y; + fe_mul(inv_one_minus_y_mul_one_plus_y, inv_one_minus_y, one_plus_y); + + // wei x = (1/(1-y))*(1+y) + (A/3) + fe wei_x_fe; + fe_add(wei_x_fe, inv_one_minus_y_mul_one_plus_y, fe_a_inv_3); + fe_tobytes(wei_x, wei_x_fe); +} + +// https://www.ietf.org/archive/id/draft-ietf-lwig-curve-representations-02.pdf E.2 +void fe_ed_derivatives_to_wei_x_y(unsigned char *wei_x, unsigned char *wei_y, const fe inv_one_minus_y, const fe one_plus_y, const fe inv_one_minus_y_mul_x) +{ + fe_ed_derivatives_to_wei_x(wei_x, inv_one_minus_y, one_plus_y); + + // c*(1+y) + fe fe_c_mul_one_plus_y; + fe_mul(fe_c_mul_one_plus_y, fe_c, one_plus_y); + + // wei y = c * (1+y) * (1/((1-y)*x)) + fe wei_y_fe; + fe_mul(wei_y_fe, fe_c_mul_one_plus_y, inv_one_minus_y_mul_x); + fe_tobytes(wei_y, wei_y_fe); +} + +/* +Since fe_add and fe_sub enforce the following conditions: + +Preconditions: + |f| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. + |g| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. + +Postconditions: + |h| bounded by 1.1*2^26,1.1*2^25,1.1*2^26,1.1*2^25,etc. + +We sometimes need to "reduce" field elems when they are in the poscondition's +larger domain to match the precondition domain. This way we can take the output +of fe_add or fe_sub and use it as input to another call to fe_add or fe_sub. + +We reduce by converting the field elem to its byte repr, then re-deriving the +field elem from the byte repr. +*/ +void fe_reduce(fe reduced_f, const fe f) +{ + unsigned char f_bytes[32]; + fe_tobytes(f_bytes, f); + fe_frombytes_vartime(reduced_f, f_bytes); +} + +void fe_dbl(fe h, const fe f) +{ + // Reduce the input for safety to ensure we meet the preconditions for fe_add + fe f_reduced; + fe_reduce(f_reduced, f); + fe h_res; + fe_add(h_res, f_reduced, f_reduced); + // Reduce the output for safety to ensure the result can be used as input to + // fe_add or fe_sub without an extra call to fe_reduce + fe_reduce(h, h_res); +} diff --git a/src/crypto/crypto-ops.h b/src/crypto/crypto-ops.h index c103f1f789d..652c1165847 100644 --- a/src/crypto/crypto-ops.h +++ b/src/crypto/crypto-ops.h @@ -88,6 +88,7 @@ void ge_double_scalarmult_base_vartime_p3(ge_p3 *, const unsigned char *, const extern const fe fe_sqrtm1; extern const fe fe_d; +int fe_frombytes_vartime(fe, const unsigned char *); int ge_frombytes_vartime(ge_p3 *, const unsigned char *); /* From ge_p1p1_to_p2.c */ @@ -102,6 +103,10 @@ void ge_p1p1_to_p3(ge_p3 *, const ge_p1p1 *); void ge_p2_dbl(ge_p1p1 *, const ge_p2 *); +/* From ge_p3_dbl.c */ + +void ge_p3_dbl(ge_p1p1 *r, const ge_p3 *p); + /* From ge_p3_to_cached.c */ extern const fe fe_d2; @@ -128,6 +133,10 @@ void ge_tobytes(unsigned char *, const ge_p2 *); void sc_reduce(unsigned char *); +/* From fe_pow22523.c */ + +void fe_pow22523(fe, const fe); + /* New code */ void ge_scalarmult(ge_p2 *, const unsigned char *, const ge_p3 *); @@ -137,16 +146,26 @@ void ge_triple_scalarmult_precomp_vartime(ge_p2 *, const unsigned char *, const void ge_double_scalarmult_precomp_vartime2(ge_p2 *, const unsigned char *, const ge_dsmp, const unsigned char *, const ge_dsmp); void ge_double_scalarmult_precomp_vartime2_p3(ge_p3 *, const unsigned char *, const ge_dsmp, const unsigned char *, const ge_dsmp); void ge_mul8(ge_p1p1 *, const ge_p2 *); +extern const fe fe_a_sub_d; +extern const fe fe_a0; +extern const fe fe_ap; +extern const fe fe_msqrt2b; extern const fe fe_ma2; extern const fe fe_ma; extern const fe fe_fffb1; extern const fe fe_fffb2; extern const fe fe_fffb3; extern const fe fe_fffb4; +extern const fe fe_a_inv_3; +extern const fe fe_c; +extern const fe fe_one; +extern const fe fe_m1; +extern const fe fe_inv2; extern const ge_p3 ge_p3_identity; extern const ge_p3 ge_p3_H; void ge_fromfe_frombytes_vartime(ge_p2 *, const unsigned char *); void sc_0(unsigned char *); +void sc_1(unsigned char *); void sc_reduce32(unsigned char *); void sc_add(unsigned char *, const unsigned char *, const unsigned char *); void sc_sub(unsigned char *, const unsigned char *, const unsigned char *); @@ -156,14 +175,41 @@ void sc_muladd(unsigned char *s, const unsigned char *a, const unsigned char *b, int sc_check(const unsigned char *); int sc_isnonzero(const unsigned char *); /* Doesn't normalize */ +/** + * brief: Convert Ed25519 y-coord to X25519 x-coord, AKA "ConvertPointE()" in the Carrot spec + */ +void ge_p3_to_x25519(unsigned char *xbytes, const ge_p3 *h); +int edwards_bytes_to_x25519_vartime(unsigned char *xbytes, const unsigned char *s); + // internal uint64_t load_3(const unsigned char *in); uint64_t load_4(const unsigned char *in); void ge_sub(ge_p1p1 *r, const ge_p3 *p, const ge_cached *q); void fe_add(fe h, const fe f, const fe g); +void fe_neg(fe h, const fe f); void fe_tobytes(unsigned char *, const fe); +void fe_copy(fe h, const fe f); +int fe_isnegative(const fe f); void fe_invert(fe out, const fe z); +int fe_equals(const fe a, const fe b); + +/** +@brief: out[i] = 1/in[i] for i in [0, n). Uses Montgomery's trick +@return: 0 on success, some other value otherwise + +Unlike other crypto functions, `out` and `in` memory sections CANNOT be aliased. +If `out` and `in` overlap, it will cause undefined output. +**/ +void fe_batch_invert(fe *out, const fe *in, const int n); void fe_mul(fe out, const fe, const fe); +void fe_sq(fe h, const fe f); +void fe_sub(fe h, const fe f, const fe g); void fe_0(fe h); +void fe_1(fe h); int ge_p3_is_point_at_infinity_vartime(const ge_p3 *p); + +void fe_ed_derivatives_to_wei_x_y(unsigned char *wei_x, unsigned char *wei_y, const fe inv_one_minus_y, const fe one_plus_y, const fe inv_one_minus_y_mul_x); + +void fe_reduce(fe reduced_f, const fe f); +void fe_dbl(fe h, const fe f); diff --git a/src/crypto/crypto.cpp b/src/crypto/crypto.cpp index f6c94fa0393..32ad4174f17 100644 --- a/src/crypto/crypto.cpp +++ b/src/crypto/crypto.cpp @@ -42,6 +42,7 @@ #include "warnings.h" #include "crypto.h" #include "hash.h" +#include "blake2b.h" #include "cryptonote_config.h" @@ -608,7 +609,7 @@ namespace crypto { return sc_isnonzero(&c2) == 0; } - static void hash_to_ec(const public_key &key, ge_p3 &res) { + static void biased_hash_to_ec(const public_key &key, ge_p3 &res) { hash h; ge_p2 point; ge_p1p1 point2; @@ -618,11 +619,56 @@ namespace crypto { ge_p1p1_to_p3(&res, &point2); } + void crypto_ops::unbiased_hash_to_ec(const unsigned char *preimage, const size_t length, ec_point &res) { + uint8_t hash[64]; + blake2b(std::addressof(hash), 64, preimage, length, NULL, 0); + + ge_p2 first; + ge_fromfe_frombytes_vartime(&first, reinterpret_cast(&hash)); + ge_p1p1 first_p1p1; + ge_mul8(&first_p1p1, &first); + ge_p3 first_p3; + ge_p1p1_to_p3(&first_p3, &first_p1p1); + + ge_p2 second; + ge_fromfe_frombytes_vartime(&second, reinterpret_cast(&hash) + 32); + ge_p1p1 second_p1p1; + ge_mul8(&second_p1p1, &second); + ge_p3 second_p3; + ge_p1p1_to_p3(&second_p3, &second_p1p1); + ge_cached second_cached; + ge_p3_to_cached(&second_cached, &second_p3); + + ge_p1p1 point; + ge_add(&point, &first_p3, &second_cached); + + ge_p3 res_ge_p3; + ge_p1p1_to_p3(&res_ge_p3, &point); + ge_p3_tobytes(&res, &res_ge_p3); + } + + static void biased_derive_key_image_generator(const public_key &pub, ec_point &ki_gen) { + ge_p3 point; + biased_hash_to_ec(pub, point); + ge_p3_tobytes(&ki_gen, &point); + } + + static void unbiased_derive_key_image_generator(const public_key &pub, ec_point &ki_gen) { + unbiased_hash_to_ec(&pub, sizeof(public_key), ki_gen); + } + + void crypto_ops::derive_key_image_generator(const public_key &pub, const bool biased, ec_point &ki_gen) { + if (biased) + biased_derive_key_image_generator(pub, ki_gen); + else + unbiased_derive_key_image_generator(pub, ki_gen); + } + void crypto_ops::generate_key_image(const public_key &pub, const secret_key &sec, key_image &image) { ge_p3 point; ge_p2 point2; assert(sc_check(&sec) == 0); - hash_to_ec(pub, point); + biased_hash_to_ec(pub, point); ge_scalarmult(&point2, &unwrap(sec), &point); ge_tobytes(&image, &point2); } @@ -683,7 +729,7 @@ POP_WARNINGS random_scalar(k); ge_scalarmult_base(&tmp3, &k); ge_p3_tobytes(&buf->ab[i].a, &tmp3); - hash_to_ec(*pubs[i], tmp3); + biased_hash_to_ec(*pubs[i], tmp3); ge_scalarmult(&tmp2, &k, &tmp3); ge_tobytes(&buf->ab[i].b, &tmp2); } else { @@ -695,7 +741,7 @@ POP_WARNINGS } ge_double_scalarmult_base_vartime(&tmp2, &sig[i].c, &tmp3, &sig[i].r); ge_tobytes(&buf->ab[i].a, &tmp2); - hash_to_ec(*pubs[i], tmp3); + biased_hash_to_ec(*pubs[i], tmp3); ge_double_scalarmult_precomp_vartime(&tmp2, &sig[i].r, &tmp3, &sig[i].c, image_pre); ge_tobytes(&buf->ab[i].b, &tmp2); sc_add(&sum, &sum, &sig[i].c); @@ -740,7 +786,7 @@ POP_WARNINGS } ge_double_scalarmult_base_vartime(&tmp2, &sig[i].c, &tmp3, &sig[i].r); ge_tobytes(&buf->ab[i].a, &tmp2); - hash_to_ec(*pubs[i], tmp3); + biased_hash_to_ec(*pubs[i], tmp3); ge_double_scalarmult_precomp_vartime(&tmp2, &sig[i].r, &tmp3, &sig[i].c, image_pre); ge_tobytes(&buf->ab[i].b, &tmp2); sc_add(&sum, &sum, &sig[i].c); diff --git a/src/crypto/crypto.h b/src/crypto/crypto.h index 01def5dea7d..1813466db7e 100644 --- a/src/crypto/crypto.h +++ b/src/crypto/crypto.h @@ -60,6 +60,11 @@ namespace crypto { char data[32]; }; + // x or y coordinate + POD_CLASS ec_coord { + char data[32]; + }; + POD_CLASS public_key: ec_point { friend class crypto_ops; }; @@ -129,6 +134,8 @@ namespace crypto { friend void generate_tx_proof_v1(const hash &, const public_key &, const public_key &, const boost::optional &, const public_key &, const secret_key &, signature &); static bool check_tx_proof(const hash &, const public_key &, const public_key &, const boost::optional &, const public_key &, const signature &, const int); friend bool check_tx_proof(const hash &, const public_key &, const public_key &, const boost::optional &, const public_key &, const signature &, const int); + static void derive_key_image_generator(const public_key &, bool, ec_point &); + friend void derive_key_image_generator(const public_key &, bool, ec_point &); static void generate_key_image(const public_key &, const secret_key &, key_image &); friend void generate_key_image(const public_key &, const secret_key &, key_image &); static void generate_ring_signature(const hash &, const key_image &, @@ -141,6 +148,8 @@ namespace crypto { const public_key *const *, std::size_t, const signature *); static void derive_view_tag(const key_derivation &, std::size_t, view_tag &); friend void derive_view_tag(const key_derivation &, std::size_t, view_tag &); + static void unbiased_hash_to_ec(const unsigned char *, const size_t, ec_point &); + friend void unbiased_hash_to_ec(const unsigned char *, const size_t, ec_point &); }; void generate_random_bytes_thread_safe(size_t N, uint8_t *bytes); @@ -254,6 +263,10 @@ namespace crypto { return crypto_ops::check_tx_proof(prefix_hash, R, A, B, D, sig, version); } + inline void derive_key_image_generator(const public_key &pub, const bool biased, ec_point &ki_gen) { + crypto_ops::derive_key_image_generator(pub, biased, ki_gen); + } + /* To send money to a key: * * The sender generates an ephemeral key and includes it in transaction output. * * To spend the money, the receiver generates a key image from it. @@ -297,6 +310,10 @@ namespace crypto { crypto_ops::derive_view_tag(derivation, output_index, vt); } + inline void unbiased_hash_to_ec(const unsigned char *preimage, const size_t length, ec_point &res) { + crypto_ops::unbiased_hash_to_ec(preimage, length, res); + } + inline std::ostream &operator <<(std::ostream &o, const crypto::public_key &v) { epee::to_hex::formatted(o, epee::as_byte_span(v)); return o; } @@ -321,6 +338,9 @@ namespace crypto { inline std::ostream &operator <<(std::ostream &o, const crypto::view_tag &v) { epee::to_hex::formatted(o, epee::as_byte_span(v)); return o; } + inline std::ostream &operator <<(std::ostream &o, const crypto::ec_point &v) { + epee::to_hex::formatted(o, epee::as_byte_span(v)); return o; + } const extern crypto::public_key null_pkey; const extern crypto::secret_key null_skey; @@ -329,6 +349,19 @@ namespace crypto { inline bool operator>(const public_key &p1, const public_key &p2) { return p2 < p1; } inline bool operator<(const key_image &p1, const key_image &p2) { return memcmp(&p1, &p2, sizeof(key_image)) < 0; } inline bool operator>(const key_image &p1, const key_image &p2) { return p2 < p1; } + + static const ec_point EC_I = {1}; + + static const ec_point EC_INV_EIGHT = {{ + static_cast(0x79), static_cast(0x2f), static_cast(0xdc), static_cast(0xe2), + static_cast(0x29), static_cast(0xe5), static_cast(0x06), static_cast(0x61), + static_cast(0xd0), static_cast(0xda), static_cast(0x1c), static_cast(0x7d), + static_cast(0xb3), static_cast(0x9d), static_cast(0xd3), static_cast(0x07), + static_cast(0x00), static_cast(0x00), static_cast(0x00), static_cast(0x00), + static_cast(0x00), static_cast(0x00), static_cast(0x00), static_cast(0x00), + static_cast(0x00), static_cast(0x00), static_cast(0x00), static_cast(0x00), + static_cast(0x00), static_cast(0x00), static_cast(0x00), static_cast(0x06) + }}; } // type conversions for easier calls to sc_add(), sc_sub(), hash functions @@ -336,7 +369,10 @@ inline unsigned char* to_bytes(crypto::ec_scalar &scalar) { return &reinterpret_ inline const unsigned char* to_bytes(const crypto::ec_scalar &scalar) { return &reinterpret_cast(scalar); } inline unsigned char* to_bytes(crypto::ec_point &point) { return &reinterpret_cast(point); } inline const unsigned char* to_bytes(const crypto::ec_point &point) { return &reinterpret_cast(point); } +inline unsigned char* to_bytes(crypto::ec_coord &coord) { return &reinterpret_cast(coord); } +inline const unsigned char* to_bytes(const crypto::ec_coord &coord) { return &reinterpret_cast(coord); } +CRYPTO_MAKE_HASHABLE(ec_point) CRYPTO_MAKE_HASHABLE(public_key) CRYPTO_MAKE_HASHABLE_CONSTANT_TIME(secret_key) CRYPTO_MAKE_HASHABLE_CONSTANT_TIME(public_key_memsafe) diff --git a/src/crypto/generators.cpp b/src/crypto/generators.cpp index 493d18334d9..ad151b13fb6 100644 --- a/src/crypto/generators.cpp +++ b/src/crypto/generators.cpp @@ -39,6 +39,7 @@ extern "C" #include #include #include +#include namespace crypto { @@ -68,14 +69,36 @@ constexpr public_key G = bytes_to({ 0x58, 0x66, 0x66, 0x66, 0x66, 0x //pedersen commitment generator H: toPoint(cn_fast_hash(G)) constexpr public_key H = bytes_to({ 0x8b, 0x65, 0x59, 0x70, 0x15, 0x37, 0x99, 0xaf, 0x2a, 0xea, 0xdc, 0x9f, 0xf1, 0xad, 0xd0, 0xea, 0x6c, 0x72, 0x51, 0xd5, 0x41, 0x54, 0xcf, 0xa9, 0x2c, 0x17, 0x3a, 0x0d, 0xd3, 0x9c, 0x1f, 0x94 }); +//FCMP++ generator T: unbiased_hash_to_ec(keccak("Monero Generator T")) +constexpr public_key T = bytes_to({ 97, 183, 54, 206, 147, 182, 42, 61, 55, 120, 171, 32, 77, 168, 93, 59, 76, + 220, 7, 37, 15, 93, 167, 227, 223, 38, 41, 146, 129, 52, 213, 38 }); +//FCMP++ generator U: unbiased_hash_to_ec(keccak("Monero FCMP++ Generator U")) +constexpr public_key U = bytes_to({ 80, 107, 35, 246, 214, 229, 48, 153, 122, 188, 172, 198, 253, 52, 119, 52, + 177, 76, 43, 215, 155, 234, 0, 238, 176, 72, 87, 232, 234, 221, 26, 138 }); +//FCMP++ generator V: unbiased_hash_to_ec(keccak("Monero FCMP++ Generator V")) +constexpr public_key V = bytes_to({ 105, 53, 244, 19, 248, 49, 9, 19, 138, 122, 20, 180, 9, 85, 45, 59, 118, + 216, 143, 202, 129, 187, 89, 39, 233, 161, 225, 48, 205, 254, 41, 249 }); static ge_p3 G_p3; static ge_p3 H_p3; +static ge_p3 T_p3; +static ge_p3 U_p3; +static ge_p3 V_p3; static ge_cached G_cached; static ge_cached H_cached; +static ge_cached T_cached; +static ge_cached U_cached; +static ge_cached V_cached; // misc static std::once_flag init_gens_once_flag; +//------------------------------------------------------------------------------------------------------------------- +// hash-to-point: H_p(x) = unbiased_hash_to_ec(x) +//------------------------------------------------------------------------------------------------------------------- +static void hash_to_point(const crypto::hash &x, crypto::ec_point &point_out) +{ + unbiased_hash_to_ec((const unsigned char*)x.data, sizeof(crypto::hash), point_out); +} //------------------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------------------- static public_key reproduce_generator_G() @@ -120,6 +143,39 @@ static public_key reproduce_generator_H() return reproduced_H; } //------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static public_key reproduce_generator_T() +{ + // T = H_p(keccak("Monero Generator T")) + const std::string_view T_seed{"Monero Generator T"}; + public_key reproduced_T; + hash_to_point(cn_fast_hash(T_seed.data(), T_seed.size()), reproduced_T); + + return reproduced_T; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static public_key reproduce_generator_U() +{ + // U = H_p(keccak("Monero FCMP++ Generator U")) + const std::string_view U_seed{"Monero FCMP++ Generator U"}; + public_key reproduced_U; + hash_to_point(cn_fast_hash(U_seed.data(), U_seed.size()), reproduced_U); + + return reproduced_U; +} +//------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +static public_key reproduce_generator_V() +{ + // V = H_p(keccak("Monero FCMP++ Generator V")) + const std::string_view V_seed{"Monero FCMP++ Generator V"}; + public_key reproduced_V; + hash_to_point(cn_fast_hash(V_seed.data(), V_seed.size()), reproduced_V); + + return reproduced_V; +} +//------------------------------------------------------------------------------------------------------------------- // Make generators, but only once //------------------------------------------------------------------------------------------------------------------- static void init_gens() @@ -130,21 +186,36 @@ static void init_gens() // sanity check the generators static_assert(static_cast(G.data[0]) == 0x58, "compile-time constant sanity check"); static_assert(static_cast(H.data[0]) == 0x8b, "compile-time constant sanity check"); + static_assert(static_cast(T.data[0]) == 97, "compile-time constant sanity check"); + static_assert(static_cast(U.data[0]) == 80, "compile-time constant sanity check"); + static_assert(static_cast(V.data[0]) == 105, "compile-time constant sanity check"); // build ge_p3 representations of generators const int G_deserialize = ge_frombytes_vartime(&G_p3, to_bytes(G)); const int H_deserialize = ge_frombytes_vartime(&H_p3, to_bytes(H)); + const int T_deserialize = ge_frombytes_vartime(&T_p3, to_bytes(T)); + const int U_deserialize = ge_frombytes_vartime(&U_p3, to_bytes(U)); + const int V_deserialize = ge_frombytes_vartime(&V_p3, to_bytes(V)); (void)G_deserialize; assert(G_deserialize == 0); (void)H_deserialize; assert(H_deserialize == 0); + (void)T_deserialize; assert(T_deserialize == 0); + (void)U_deserialize; assert(U_deserialize == 0); + (void)V_deserialize; assert(V_deserialize == 0); // get cached versions ge_p3_to_cached(&G_cached, &G_p3); ge_p3_to_cached(&H_cached, &H_p3); + ge_p3_to_cached(&T_cached, &T_p3); + ge_p3_to_cached(&U_cached, &U_p3); + ge_p3_to_cached(&V_cached, &V_p3); // in debug mode, check that generators are reproducible (void)reproduce_generator_G; assert(reproduce_generator_G() == G); (void)reproduce_generator_H; assert(reproduce_generator_H() == H); + (void)reproduce_generator_T; assert(reproduce_generator_T() == T); + (void)reproduce_generator_U; assert(reproduce_generator_U() == U); + (void)reproduce_generator_V; assert(reproduce_generator_V() == V); }); } @@ -159,6 +230,21 @@ public_key get_H() return H; } //------------------------------------------------------------------------------------------------------------------- +public_key get_T() +{ + return T; +} +//------------------------------------------------------------------------------------------------------------------- +public_key get_U() +{ + return U; +} +//------------------------------------------------------------------------------------------------------------------- +public_key get_V() +{ + return V; +} +//------------------------------------------------------------------------------------------------------------------- ge_p3 get_G_p3() { init_gens(); @@ -171,6 +257,24 @@ ge_p3 get_H_p3() return H_p3; } //------------------------------------------------------------------------------------------------------------------- +ge_p3 get_T_p3() +{ + init_gens(); + return T_p3; +} +//------------------------------------------------------------------------------------------------------------------- +ge_p3 get_U_p3() +{ + init_gens(); + return U_p3; +} +//------------------------------------------------------------------------------------------------------------------- +ge_p3 get_V_p3() +{ + init_gens(); + return V_p3; +} +//------------------------------------------------------------------------------------------------------------------- ge_cached get_G_cached() { init_gens(); @@ -183,4 +287,22 @@ ge_cached get_H_cached() return H_cached; } //------------------------------------------------------------------------------------------------------------------- +ge_cached get_T_cached() +{ + init_gens(); + return T_cached; +} +//------------------------------------------------------------------------------------------------------------------- +ge_cached get_U_cached() +{ + init_gens(); + return U_cached; +} +//------------------------------------------------------------------------------------------------------------------- +ge_cached get_V_cached() +{ + init_gens(); + return V_cached; +} +//------------------------------------------------------------------------------------------------------------------- } //namespace crypto diff --git a/src/crypto/generators.h b/src/crypto/generators.h index c7d5e693e60..15f9285d400 100644 --- a/src/crypto/generators.h +++ b/src/crypto/generators.h @@ -39,9 +39,18 @@ namespace crypto public_key get_G(); public_key get_H(); +public_key get_T(); +public_key get_U(); +public_key get_V(); ge_p3 get_G_p3(); ge_p3 get_H_p3(); +ge_p3 get_T_p3(); +ge_p3 get_U_p3(); +ge_p3 get_V_p3(); ge_cached get_G_cached(); ge_cached get_H_cached(); +ge_cached get_T_cached(); +ge_cached get_U_cached(); +ge_cached get_V_cached(); } //namespace crypto diff --git a/src/crypto/hash.h b/src/crypto/hash.h index 3e0c54c6c85..bbc7bcadba7 100644 --- a/src/crypto/hash.h +++ b/src/crypto/hash.h @@ -106,5 +106,8 @@ namespace crypto { inline bool operator>(const hash &lhs, const hash &rhs) noexcept { return rhs < lhs; } } +inline const unsigned char* to_bytes(const crypto::hash &h) { return &reinterpret_cast(h); } +inline unsigned char* to_bytes(crypto::hash &h) { return &reinterpret_cast(h); } + CRYPTO_MAKE_HASHABLE(hash) CRYPTO_MAKE_COMPARABLE(hash8) diff --git a/src/cryptonote_basic/account.cpp b/src/cryptonote_basic/account.cpp index 686796ce497..3ba5638bfa9 100644 --- a/src/cryptonote_basic/account.cpp +++ b/src/cryptonote_basic/account.cpp @@ -175,8 +175,8 @@ DISABLE_VS_WARNINGS(4244 4345) struct tm timestamp = {0}; timestamp.tm_year = 2014 - 1900; // year 2014 - timestamp.tm_mon = 6 - 1; // month june - timestamp.tm_mday = 8; // 8th of june + timestamp.tm_mon = 4 - 1; // month april + timestamp.tm_mday = 15; // 15th of june timestamp.tm_hour = 0; timestamp.tm_min = 0; timestamp.tm_sec = 0; diff --git a/src/cryptonote_basic/connection_context.cpp b/src/cryptonote_basic/connection_context.cpp index 10457141709..be4bd15f665 100644 --- a/src/cryptonote_basic/connection_context.cpp +++ b/src/cryptonote_basic/connection_context.cpp @@ -30,47 +30,9 @@ #include "connection_context.h" #include -#include "cryptonote_protocol/cryptonote_protocol_defs.h" -#include "p2p/p2p_protocol_defs.h" namespace cryptonote { - std::size_t cryptonote_connection_context::get_max_bytes(const int command) noexcept - { - switch (command) - { - case nodetool::COMMAND_HANDSHAKE_T::ID: - return 65536; - case nodetool::COMMAND_TIMED_SYNC_T::ID: - return 65536; - case nodetool::COMMAND_PING::ID: - return 4096; - case nodetool::COMMAND_REQUEST_SUPPORT_FLAGS::ID: - return 4096; - case cryptonote::NOTIFY_NEW_BLOCK::ID: - return 1024 * 1024 * 128; // 128 MB (max packet is a bit less than 100 MB though) - case cryptonote::NOTIFY_NEW_TRANSACTIONS::ID: - return 1024 * 1024 * 128; // 128 MB (max packet is a bit less than 100 MB though) - case cryptonote::NOTIFY_REQUEST_GET_OBJECTS::ID: - return 1024 * 1024 * 2; // 2 MB - case cryptonote::NOTIFY_RESPONSE_GET_OBJECTS::ID: - return 1024 * 1024 * 128; // 128 MB (max packet is a bit less than 100 MB though) - case cryptonote::NOTIFY_REQUEST_CHAIN::ID: - return 512 * 1024; // 512 kB - case cryptonote::NOTIFY_RESPONSE_CHAIN_ENTRY::ID: - return 1024 * 1024 * 4; // 4 MB - case cryptonote::NOTIFY_NEW_FLUFFY_BLOCK::ID: - return 1024 * 1024 * 4; // 4 MB, but it does not includes transaction data - case cryptonote::NOTIFY_REQUEST_FLUFFY_MISSING_TX::ID: - return 1024 * 1024; // 1 MB - case cryptonote::NOTIFY_GET_TXPOOL_COMPLEMENT::ID: - return 1024 * 1024 * 4; // 4 MB - default: - break; - }; - return std::numeric_limits::max(); - } - void cryptonote_connection_context::set_state_normal() { m_state = state_normal; diff --git a/src/cryptonote_basic/connection_context.h b/src/cryptonote_basic/connection_context.h index 2252897fa3b..c71468cacf6 100644 --- a/src/cryptonote_basic/connection_context.h +++ b/src/cryptonote_basic/connection_context.h @@ -32,14 +32,59 @@ #pragma once #include #include -#include +#include +#include #include #include -#include "net/net_utils_base.h" #include "crypto/hash.h" +#include "cryptonote_protocol/cryptonote_protocol_defs.h" +#include "net/net_utils_base.h" +#include "p2p/p2p_protocol_defs.h" +#include "syncobj.h" namespace cryptonote { + //! \return Maximum number of bytes permissible for `command`. + constexpr std::size_t get_command_max_bytes(const int command) noexcept + { + switch (command) + { + case nodetool::COMMAND_HANDSHAKE_T::ID: + return 65536; + case nodetool::COMMAND_TIMED_SYNC_T::ID: + return 65536; + case nodetool::COMMAND_PING::ID: + return 4096; + case nodetool::COMMAND_REQUEST_SUPPORT_FLAGS::ID: + return 4096; + case cryptonote::NOTIFY_NEW_BLOCK::ID: + return 1024 * 1024 * 128; // 128 MB (max packet is a bit less than 100 MB though) + case cryptonote::NOTIFY_NEW_TRANSACTIONS::ID: + return 1024 * 1024 * 128; // 128 MB (max packet is a bit less than 100 MB though) + case cryptonote::NOTIFY_REQUEST_GET_OBJECTS::ID: + return 1024 * 1024 * 2; // 2 MB + case cryptonote::NOTIFY_RESPONSE_GET_OBJECTS::ID: + return 1024 * 1024 * 128; // 128 MB (max packet is a bit less than 100 MB though) + case cryptonote::NOTIFY_REQUEST_CHAIN::ID: + return 512 * 1024; // 512 kB + case cryptonote::NOTIFY_RESPONSE_CHAIN_ENTRY::ID: + return 1024 * 1024 * 4; // 4 MB + case cryptonote::NOTIFY_NEW_FLUFFY_BLOCK::ID: + return 1024 * 1024 * 128; // 128 MB (max packet is a bit less than 100 MB though, fluffy blocks can be full) + case cryptonote::NOTIFY_REQUEST_FLUFFY_MISSING_TX::ID: + return 1024 * 1024; // 1 MB + case cryptonote::NOTIFY_GET_TXPOOL_COMPLEMENT::ID: + return 1024 * 1024 * 4; // 4 MB + case cryptonote::NOTIFY_TX_POOL_HASH::ID: + return 1024 * 1024 * 2; // 2 MB + case cryptonote::NOTIFY_REQUEST_TX_POOL_TXS::ID: + return 1024 * 1024 * 2; // 2 MB + default: + break; + }; + return std::numeric_limits::max(); + } + struct cryptonote_connection_context: public epee::net_utils::connection_context_base { cryptonote_connection_context(): m_state(state_before_handshake), m_remote_blockchain_height(0), m_last_response_height(0), @@ -89,9 +134,6 @@ namespace cryptonote static constexpr int handshake_command() noexcept { return 1001; } bool handshake_complete() const noexcept { return m_state != state_before_handshake; } - //! \return Maximum number of bytes permissible for `command`. - static size_t get_max_bytes(int command) noexcept; - //! Use this instead of `m_state = state_normal`. void set_state_normal(); diff --git a/src/cryptonote_basic/cryptonote_basic.h b/src/cryptonote_basic/cryptonote_basic.h index 9379e5d2743..047088ad1ac 100644 --- a/src/cryptonote_basic/cryptonote_basic.h +++ b/src/cryptonote_basic/cryptonote_basic.h @@ -43,6 +43,8 @@ #include "serialization/debug_archive.h" #include "serialization/crypto.h" #include "serialization/keyvalue_serialization.h" // eepe named serialization +#include "carrot_core/core_types.h" +#include "carrot_impl/carrot_chain_serialization.h" #include "cryptonote_config.h" #include "crypto/crypto.h" #include "crypto/hash.h" @@ -58,14 +60,19 @@ namespace cryptonote /* outputs */ - struct txout_to_script + struct txout_to_carrot_v1 { - std::vector keys; - std::vector script; + crypto::public_key key; // K_o + carrot::view_tag_t view_tag; // vt + carrot::encrypted_janus_anchor_t encrypted_janus_anchor; // anchor_enc + + // Encrypted amount a_enc and amount commitment C_a are stored in rct::rctSigBase + // This allows for reuse of this output type between coinbase and non-coinbase txs BEGIN_SERIALIZE_OBJECT() - FIELD(keys) - FIELD(script) + FIELD(key) + FIELD(view_tag) + FIELD(encrypted_janus_anchor) END_SERIALIZE() }; @@ -122,16 +129,7 @@ namespace cryptonote struct txin_to_scripthash { - crypto::hash prev; - size_t prevout; - txout_to_script script; - std::vector sigset; - BEGIN_SERIALIZE_OBJECT() - FIELD(prev) - VARINT_FIELD(prevout) - FIELD(script) - FIELD(sigset) END_SERIALIZE() }; @@ -151,7 +149,7 @@ namespace cryptonote typedef boost::variant txin_v; - typedef boost::variant txout_target_v; + typedef boost::variant txout_target_v; //typedef std::pair out_t; struct tx_out @@ -167,6 +165,22 @@ namespace cryptonote }; + inline const crypto::public_key &output_pubkey_cref(const txout_target_v &tx_out) + { + struct tx_out_visitor + { + const crypto::public_key &operator()(const cryptonote::txout_to_carrot_v1 &out) const + { return out.key; } + const crypto::public_key &operator()(const cryptonote::txout_to_tagged_key &out) const + { return out.key; } + const crypto::public_key &operator()(const cryptonote::txout_to_key &out) const + { return out.key; } + const crypto::public_key &operator()(const cryptonote::txout_to_scripthash&) const + { throw std::runtime_error("Unexpected usage of txout to scripthash"); } + }; + return boost::apply_visitor(tx_out_visitor{}, tx_out); + } + class transaction_prefix { @@ -308,7 +322,8 @@ namespace cryptonote ar.tag("rctsig_prunable"); ar.begin_object(); r = rct_signatures.p.serialize_rctsig_prunable(ar, rct_signatures.type, vin.size(), vout.size(), - vin.size() > 0 && vin[0].type() == typeid(txin_to_key) ? boost::get(vin[0]).key_offsets.size() - 1 : 0); + (vin.empty() || vin[0].type() != typeid(txin_to_key) || rct_signatures.type == rct::RCTTypeFcmpPlusPlus) + ? 0 : boost::get(vin[0]).key_offsets.size() - 1); if (!r || !ar.good()) return false; ar.end_object(); } @@ -512,6 +527,8 @@ namespace cryptonote hash_valid(b.is_hash_valid()), miner_tx(b.miner_tx), tx_hashes(b.tx_hashes), + fcmp_pp_n_tree_layers(b.fcmp_pp_n_tree_layers), + fcmp_pp_tree_root(b.fcmp_pp_tree_root), hash(b.hash) {} block(block &&b): @@ -519,6 +536,8 @@ namespace cryptonote hash_valid(b.is_hash_valid()), miner_tx(std::move(b.miner_tx)), tx_hashes(std::move(b.tx_hashes)), + fcmp_pp_n_tree_layers(std::move(b.fcmp_pp_n_tree_layers)), + fcmp_pp_tree_root(std::move(b.fcmp_pp_tree_root)), hash(std::move(b.hash)) { b.miner_tx.set_null(); @@ -532,6 +551,8 @@ namespace cryptonote hash_valid = b.is_hash_valid(); miner_tx = b.miner_tx; tx_hashes = b.tx_hashes; + fcmp_pp_n_tree_layers = b.fcmp_pp_n_tree_layers; + fcmp_pp_tree_root = b.fcmp_pp_tree_root; hash = b.hash; } return *this; @@ -544,6 +565,8 @@ namespace cryptonote hash_valid = b.is_hash_valid(); miner_tx = std::move(b.miner_tx); tx_hashes = std::move(b.tx_hashes); + fcmp_pp_n_tree_layers = b.fcmp_pp_n_tree_layers; + fcmp_pp_tree_root = b.fcmp_pp_tree_root; hash = std::move(b.hash); b.miner_tx.set_null(); b.tx_hashes.clear(); @@ -558,6 +581,10 @@ namespace cryptonote transaction miner_tx; std::vector tx_hashes; + // We include both n tree layers and the root so SPV nodes can verify FCMP++ proofs + uint8_t fcmp_pp_n_tree_layers; + crypto::ec_point fcmp_pp_tree_root; + // hash cash mutable crypto::hash hash; @@ -570,6 +597,13 @@ namespace cryptonote FIELD(tx_hashes) if (tx_hashes.size() > CRYPTONOTE_MAX_TX_PER_BLOCK) return false; + if (major_version >= HF_VERSION_FCMP_PLUS_PLUS) + { + FIELD(fcmp_pp_n_tree_layers) + if (fcmp_pp_n_tree_layers > FCMP_PLUS_PLUS_MAX_LAYERS) + return false; + FIELD(fcmp_pp_tree_root) + } END_SERIALIZE() }; @@ -642,7 +676,7 @@ VARIANT_TAG(binary_archive, cryptonote::txin_gen, 0xff); VARIANT_TAG(binary_archive, cryptonote::txin_to_script, 0x0); VARIANT_TAG(binary_archive, cryptonote::txin_to_scripthash, 0x1); VARIANT_TAG(binary_archive, cryptonote::txin_to_key, 0x2); -VARIANT_TAG(binary_archive, cryptonote::txout_to_script, 0x0); +VARIANT_TAG(binary_archive, cryptonote::txout_to_carrot_v1, 0x0); VARIANT_TAG(binary_archive, cryptonote::txout_to_scripthash, 0x1); VARIANT_TAG(binary_archive, cryptonote::txout_to_key, 0x2); VARIANT_TAG(binary_archive, cryptonote::txout_to_tagged_key, 0x3); @@ -653,7 +687,7 @@ VARIANT_TAG(json_archive, cryptonote::txin_gen, "gen"); VARIANT_TAG(json_archive, cryptonote::txin_to_script, "script"); VARIANT_TAG(json_archive, cryptonote::txin_to_scripthash, "scripthash"); VARIANT_TAG(json_archive, cryptonote::txin_to_key, "key"); -VARIANT_TAG(json_archive, cryptonote::txout_to_script, "script"); +VARIANT_TAG(json_archive, cryptonote::txout_to_carrot_v1, "carrot_v1"); VARIANT_TAG(json_archive, cryptonote::txout_to_scripthash, "scripthash"); VARIANT_TAG(json_archive, cryptonote::txout_to_key, "key"); VARIANT_TAG(json_archive, cryptonote::txout_to_tagged_key, "tagged_key"); @@ -664,7 +698,7 @@ VARIANT_TAG(debug_archive, cryptonote::txin_gen, "gen"); VARIANT_TAG(debug_archive, cryptonote::txin_to_script, "script"); VARIANT_TAG(debug_archive, cryptonote::txin_to_scripthash, "scripthash"); VARIANT_TAG(debug_archive, cryptonote::txin_to_key, "key"); -VARIANT_TAG(debug_archive, cryptonote::txout_to_script, "script"); +VARIANT_TAG(debug_archive, cryptonote::txout_to_carrot_v1, "carrot_v1"); VARIANT_TAG(debug_archive, cryptonote::txout_to_scripthash, "scripthash"); VARIANT_TAG(debug_archive, cryptonote::txout_to_key, "key"); VARIANT_TAG(debug_archive, cryptonote::txout_to_tagged_key, "tagged_key"); diff --git a/src/cryptonote_basic/cryptonote_basic_impl.cpp b/src/cryptonote_basic/cryptonote_basic_impl.cpp index bd79a7b183e..d3565477122 100644 --- a/src/cryptonote_basic/cryptonote_basic_impl.cpp +++ b/src/cryptonote_basic/cryptonote_basic_impl.cpp @@ -72,7 +72,9 @@ namespace cryptonote { return CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V1; if (version < 5) return CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V2; - return CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5; + if (version < HF_VERSION_2026_SCALING) + return CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5; + return CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V17; } //----------------------------------------------------------------------------------------------- size_t get_max_tx_size() diff --git a/src/cryptonote_basic/cryptonote_boost_serialization.h b/src/cryptonote_basic/cryptonote_boost_serialization.h index 8948c650cd4..13c605c5209 100644 --- a/src/cryptonote_basic/cryptonote_boost_serialization.h +++ b/src/cryptonote_basic/cryptonote_boost_serialization.h @@ -91,12 +91,18 @@ namespace boost { a & reinterpret_cast(x); } + template + inline void serialize(Archive &a, crypto::ec_point &x, const boost::serialization::version_type ver) + { + a & reinterpret_cast(x); + } template - inline void serialize(Archive &a, cryptonote::txout_to_script &x, const boost::serialization::version_type ver) + inline void serialize(Archive &a, cryptonote::txout_to_carrot_v1 &x, const boost::serialization::version_type ver) { - a & x.keys; - a & x.script; + a & x.key; + a & x.view_tag.bytes; + a & x.encrypted_janus_anchor.bytes; } @@ -136,10 +142,6 @@ namespace boost template inline void serialize(Archive &a, cryptonote::txin_to_scripthash &x, const boost::serialization::version_type ver) { - a & x.prev; - a & x.prevout; - a & x.script; - a & x.sigset; } template @@ -199,6 +201,11 @@ namespace boost //------------------ a & b.miner_tx; a & b.tx_hashes; + if (b.major_version >= HF_VERSION_FCMP_PLUS_PLUS) + { + a & b.fcmp_pp_n_tree_layers; + a & b.fcmp_pp_tree_root; + } } template @@ -330,7 +337,7 @@ namespace boost a & x.type; if (x.type == rct::RCTTypeNull) return; - if (x.type != rct::RCTTypeFull && x.type != rct::RCTTypeSimple && x.type != rct::RCTTypeBulletproof && x.type != rct::RCTTypeBulletproof2 && x.type != rct::RCTTypeCLSAG && x.type != rct::RCTTypeBulletproofPlus) + if (x.type != rct::RCTTypeFull && x.type != rct::RCTTypeSimple && x.type != rct::RCTTypeBulletproof && x.type != rct::RCTTypeBulletproof2 && x.type != rct::RCTTypeCLSAG && x.type != rct::RCTTypeBulletproofPlus && x.type != rct::RCTTypeFcmpPlusPlus) throw boost::archive::archive_exception(boost::archive::archive_exception::other_exception, "Unsupported rct type"); // a & x.message; message is not serialized, as it can be reconstructed from the tx data // a & x.mixRing; mixRing is not serialized, as it can be reconstructed from the offsets @@ -354,6 +361,12 @@ namespace boost a & x.MGs; if (ver >= 1u) a & x.CLSAGs; + if (ver >= 3u) + { + a & x.reference_block; + a & x.n_tree_layers; + a & x.fcmp_pp; + } if (x.rangeSigs.empty()) a & x.pseudoOuts; } @@ -364,7 +377,7 @@ namespace boost a & x.type; if (x.type == rct::RCTTypeNull) return; - if (x.type != rct::RCTTypeFull && x.type != rct::RCTTypeSimple && x.type != rct::RCTTypeBulletproof && x.type != rct::RCTTypeBulletproof2 && x.type != rct::RCTTypeCLSAG && x.type != rct::RCTTypeBulletproofPlus) + if (x.type != rct::RCTTypeFull && x.type != rct::RCTTypeSimple && x.type != rct::RCTTypeBulletproof && x.type != rct::RCTTypeBulletproof2 && x.type != rct::RCTTypeCLSAG && x.type != rct::RCTTypeBulletproofPlus && x.type != rct::RCTTypeFcmpPlusPlus) throw boost::archive::archive_exception(boost::archive::archive_exception::other_exception, "Unsupported rct type"); // a & x.message; message is not serialized, as it can be reconstructed from the tx data // a & x.mixRing; mixRing is not serialized, as it can be reconstructed from the offsets @@ -384,7 +397,13 @@ namespace boost a & x.p.MGs; if (ver >= 1u) a & x.p.CLSAGs; - if (x.type == rct::RCTTypeBulletproof || x.type == rct::RCTTypeBulletproof2 || x.type == rct::RCTTypeCLSAG || x.type == rct::RCTTypeBulletproofPlus) + if (ver >= 3u) + { + a & x.p.reference_block; + a & x.p.n_tree_layers; + a & x.p.fcmp_pp; + } + if (x.type == rct::RCTTypeBulletproof || x.type == rct::RCTTypeBulletproof2 || x.type == rct::RCTTypeCLSAG || x.type == rct::RCTTypeBulletproofPlus || x.type == rct::RCTTypeFcmpPlusPlus) a & x.p.pseudoOuts; } @@ -425,6 +444,6 @@ namespace boost } } -BOOST_CLASS_VERSION(rct::rctSigPrunable, 2) -BOOST_CLASS_VERSION(rct::rctSig, 2) +BOOST_CLASS_VERSION(rct::rctSigPrunable, 3) +BOOST_CLASS_VERSION(rct::rctSig, 3) BOOST_CLASS_VERSION(rct::multisig_out, 1) diff --git a/src/cryptonote_basic/cryptonote_format_utils.cpp b/src/cryptonote_basic/cryptonote_format_utils.cpp index 7e4a35c2398..519289ba806 100644 --- a/src/cryptonote_basic/cryptonote_format_utils.cpp +++ b/src/cryptonote_basic/cryptonote_format_utils.cpp @@ -79,13 +79,10 @@ namespace cryptonote ge_p1p1_to_p3(&A2, &tmp3); ge_p3_tobytes(&AB, &A2); } - - uint64_t get_transaction_weight_clawback(const transaction &tx, size_t n_padded_outputs) + //--------------------------------------------------------------- + static uint64_t get_transaction_weight_clawback(const bool plus, const size_t n_outputs, const size_t n_padded_outputs) { - const rct::rctSig &rv = tx.rct_signatures; - const bool plus = rv.type == rct::RCTTypeBulletproofPlus; const uint64_t bp_base = (32 * ((plus ? 6 : 9) + 7 * 2)) / 2; // notional size of a 2 output proof, normalized to 1 proof (ie, divided by 2) - const size_t n_outputs = tx.vout.size(); if (n_padded_outputs <= 2) return 0; size_t nlr = 0; @@ -99,6 +96,64 @@ namespace cryptonote const uint64_t bp_clawback = (bp_base * n_padded_outputs - bp_size) * 4 / 5; return bp_clawback; } + + static void get_tree_hash(const std::vector& tx_hashes, crypto::hash& h) + { + tree_hash(tx_hashes.data(), tx_hashes.size(), h); + } + + static crypto::hash get_tree_hash(const std::vector& tx_hashes) + { + crypto::hash h = null_hash; + get_tree_hash(tx_hashes, h); + return h; + } + //--------------------------------------------------------------- + static uint64_t get_transaction_weight_clawback(const transaction &tx, const size_t n_padded_outputs) + { + const rct::rctSig &rv = tx.rct_signatures; + const bool plus = rv.type == rct::RCTTypeBulletproofPlus || rv.type == rct::RCTTypeFcmpPlusPlus; + const size_t n_outputs = tx.vout.size(); + return get_transaction_weight_clawback(plus, n_outputs, n_padded_outputs); + } + //--------------------------------------------------------------- + // Helper function to group outputs by last locked block idx + static uint64_t set_tx_outs_by_last_locked_block(const cryptonote::transaction &tx, + const std::unordered_map &transparent_amount_commitments, + const uint64_t &first_unified_id, + const uint64_t block_idx, + fcmp_pp::OutsByLastLockedBlock &outs_by_last_locked_block_inout, + std::unordered_map &timelocked_outputs_inout) + { + const uint64_t last_locked_block = cryptonote::get_last_locked_block_index(tx.unlock_time, block_idx); + const bool has_custom_timelock = cryptonote::is_custom_timelocked(cryptonote::is_coinbase(tx), + last_locked_block, + block_idx); + + for (std::size_t i = 0; i < tx.vout.size(); ++i) + { + const uint64_t unified_id = first_unified_id + i; + const auto &out = tx.vout[i]; + + rct::key commitment; + CHECK_AND_ASSERT_THROW_MES(cryptonote::get_commitment(tx, i, transparent_amount_commitments, commitment), + "failed to get tx commitment"); + + const fcmp_pp::UnifiedOutput unified_output{ + .unified_id = unified_id, + .output_pair = cryptonote::to_output_pair(out.target, commitment) + }; + + if (has_custom_timelock) + { + timelocked_outputs_inout[unified_id] = last_locked_block; + } + + outs_by_last_locked_block_inout[last_locked_block].emplace_back(unified_output); + } + + return tx.vout.size(); + } //--------------------------------------------------------------- } @@ -419,11 +474,126 @@ namespace cryptonote return string_tools::get_xtype_from_string(amount, str_amount); } //--------------------------------------------------------------- + uint64_t get_fcmp_pp_prefix_weight_v1(const size_t n_inputs, const size_t n_outputs, const size_t extra_len) + { + MTRACE(__func__ << "(n_inputs=" << n_inputs << ", n_outputs=" << n_outputs << ", extra_len=" << extra_len); + + CHECK_AND_ASSERT_MES(n_inputs && n_inputs <= FCMP_PLUS_PLUS_MAX_INPUTS, + std::numeric_limits::max(), + "invalid n_inputs: " << n_inputs); + CHECK_AND_ASSERT_MES(n_outputs >= 2 && n_outputs <= FCMP_PLUS_PLUS_MAX_OUTPUTS, + std::numeric_limits::max(), + "invalid n_outputs: " << n_outputs); + CHECK_AND_ASSERT_MES(extra_len <= MAX_TX_EXTRA_SIZE, + std::numeric_limits::max(), + "invalid extra_len: " << extra_len); + + static constexpr uint64_t txin_to_key_weight = 1 /*amount=0*/ + 1 /*key_offsets.size()=0*/ + 32 /*k_image*/; + static constexpr uint64_t txout_to_carrot_weight = 32 /*key*/ + 3 /*view_tag*/ + 16 /*encrypted_janus_anchor*/; + static constexpr uint64_t tx_out_weight = 1 /*amount=0*/ + txout_to_carrot_weight + 1 /*txout_target_v tag*/; + + // varint len bumps from 2 to 3 at 16384 + static_assert(16384 > FCMP_PLUS_PLUS_MAX_INPUTS, "16384 expected > FCMP_PLUS_PLUS_MAX_INPUTS"); + static_assert(16384 > MAX_TX_EXTRA_SIZE, "16384 expected > MAX_TX_EXTRA_SIZE"); + + // varint len bumps from 1 to 2 at 128 + static_assert(128 > FCMP_PLUS_PLUS_MAX_OUTPUTS, "128 expected > FCMP_PLUS_PLUS_MAX_OUTPUTS"); + + return + 1 /*version=2*/ + + 1 /*unlock_time=0*/ + + (n_inputs >= 128 ? 2 : 1) /*vin.size()<=FCMP_PLUS_PLUS_MAX_INPUTS*/ + + n_inputs * (txin_to_key_weight /*txin_to_key*/ + 1 /*txin_v tag*/) + + 1 /*vout.size()<=FCMP_PLUS_PLUS_MAX_OUTPUTS*/ + + (n_outputs * tx_out_weight /*tx_out*/) + + (extra_len >= 128 ? 2 : 1) /*extra.size()*/ + + extra_len; + } + //--------------------------------------------------------------- + uint64_t get_fcmp_pp_unprunable_weight_v1(const size_t n_inputs, const size_t n_outputs, const size_t extra_len) + { + MTRACE(__func__ << "(n_inputs=" << n_inputs << ", n_outputs=" << n_outputs << ", extra_len=" << extra_len); + + const uint64_t prefix_weight = get_fcmp_pp_prefix_weight_v1(n_inputs, n_outputs, extra_len); + if (prefix_weight == std::numeric_limits::max()) + return prefix_weight; + + static constexpr uint64_t max_u64_varint_len = 10; // size of varint storing 2**64-1 + static constexpr uint64_t rct_sig_base_per_out_weight = 8 /*ecdhInfo.at(i).amount*/ + 32 /*outPk.at(i).mask*/; + + return prefix_weight + + 1 /*type*/ + + max_u64_varint_len /*txnFee*/ + + (n_outputs * rct_sig_base_per_out_weight); + } + //--------------------------------------------------------------- + uint64_t get_fcmp_pp_transaction_weight_v1(const size_t n_inputs, const size_t n_outputs, const size_t extra_len) + { + MTRACE(__func__ << "(n_inputs=" << n_inputs << ", n_outputs=" << n_outputs << ", extra_len=" << extra_len); + + const uint64_t unprunable_weight = get_fcmp_pp_unprunable_weight_v1(n_inputs, n_outputs, extra_len); + if (unprunable_weight == std::numeric_limits::max()) + return unprunable_weight; + + static constexpr uint64_t max_block_index_varint_len = 5; // size of varint storing CRYPTONOTE_MAX_BLOCK_NUMBER + + static constexpr uint64_t rerandomized_output_weight = FCMP_PP_INPUT_TUPLE_SIZE_V1 + 32 /*C~ AKA pseudoOut*/; + + const uint64_t total_sal_weight = n_inputs * (rerandomized_output_weight + FCMP_PP_SAL_PROOF_SIZE_V1); + const uint64_t misc_fcmp_pp_weight = max_block_index_varint_len /*reference_block*/ + 1 /*n_tree_layers*/; + + // Calculate deterministic bulletproofs size (assumes canonical BP format) + size_t nrl = 0, n_padded_outputs; + while ((n_padded_outputs = (1u << nrl)) < n_outputs) + ++nrl; + nrl += 6; + uint64_t bp_weight = 32 * (6 + 2 * nrl) + 2; + bp_weight += 1 /*nbp*/; + + // There's a few reasons why we treat n_tree_layers as a fixed value for weight calculation: + // a. If we took n_tree_layers into account when calculating weight, then fee calculation + // would be a function of the number of layers in the FCMP tree. This has a couple + // implications: + // i. To determine the "correct" fee in multi-signer/cold-signer contexts, signers + // would have to transmit and agree upon what the current n_tree_layers value is, + // which complicates these protocols, and is inherently difficult to validate + // for offline signers. It also just complicates the process for normal wallets. + // ii. If signers need guarantees that a signature for a transaction proposal with a + // certain fee isn't reused for similar transaction but with a different + // n_tree_layers, and thus weight, then n_tree_layers would have to be included + // in rctSigBase and hashed into the signable_tx_hash, which means an extra byte + // per pruned transaction when wallets are refreshing. Also, more subjectively, + // putting n_tree_layers into rctSigBase feels misplaced. + // b. Dropping the weight for low values of n_tree_layers directly incentivizes spenders of + // old enotes to use as small a value of n_tree_layers as possible, which hurts their + // anonymity. + // + // We chose 7 specifically because at the time of writing (9 April 2025), the current layer size + // of the Monero mainnet would be 6. 7 is approaching relatively quickly, and would be the value + // for many decades at the current tx volume. + static constexpr size_t fake_n_tree_layers = 7; + const uint64_t fcmp_weight = fcmp_pp::membership_proof_len(n_inputs, fake_n_tree_layers); + + const uint64_t rct_sig_prunable_weight = bp_weight + total_sal_weight + misc_fcmp_pp_weight + fcmp_weight; + + return unprunable_weight + rct_sig_prunable_weight; + } + //--------------------------------------------------------------- + uint64_t get_fcmp_pp_transaction_weight_v1(const transaction_prefix &tx_prefix) + { + return get_fcmp_pp_transaction_weight_v1(tx_prefix.vin.size(), tx_prefix.vout.size(), tx_prefix.extra.size()); + } + //--------------------------------------------------------------- uint64_t get_transaction_weight(const transaction &tx, size_t blob_size) { CHECK_AND_ASSERT_MES(!tx.pruned, std::numeric_limits::max(), "get_transaction_weight does not support pruned txes"); + CHECK_AND_ASSERT_MES(tx.rct_signatures.type <= rct::RCTTypeFcmpPlusPlus, + std::numeric_limits::max(), + "get_transaction_weight does not support transactions newer than FCMP++ v1"); if (tx.version < 2) return blob_size; + else if (tx.rct_signatures.type == rct::RCTTypeFcmpPlusPlus) + return get_fcmp_pp_transaction_weight_v1(tx); const rct::rctSig &rv = tx.rct_signatures; const bool bulletproof = rct::is_rct_bulletproof(rv.type); const bool bulletproof_plus = rct::is_rct_bulletproof_plus(rv.type); @@ -439,6 +609,10 @@ namespace cryptonote { CHECK_AND_ASSERT_MES(tx.pruned, std::numeric_limits::max(), "get_pruned_transaction_weight does not support non pruned txes"); CHECK_AND_ASSERT_MES(tx.version >= 2, std::numeric_limits::max(), "get_pruned_transaction_weight does not support v1 txes"); + + if (tx.rct_signatures.type == rct::RCTTypeFcmpPlusPlus) + return get_fcmp_pp_transaction_weight_v1(tx); + CHECK_AND_ASSERT_MES(tx.rct_signatures.type == rct::RCTTypeBulletproof2 || tx.rct_signatures.type == rct::RCTTypeCLSAG || tx.rct_signatures.type == rct::RCTTypeBulletproofPlus, std::numeric_limits::max(), "Unsupported rct_signatures type in get_pruned_transaction_weight"); CHECK_AND_ASSERT_MES(!tx.vin.empty(), std::numeric_limits::max(), "empty vin"); @@ -462,6 +636,7 @@ namespace cryptonote weight += extra; // calculate deterministic CLSAG/MLSAG data size + // TODO: update for fcmp_pp const size_t ring_size = boost::get(tx.vin[0]).key_offsets.size(); if (rct::is_rct_clsag(tx.rct_signatures.type)) extra = tx.vin.size() * (ring_size + 2) * 32; @@ -911,10 +1086,13 @@ namespace cryptonote { // before HF_VERSION_VIEW_TAGS, outputs with public keys are of type txout_to_key // after HF_VERSION_VIEW_TAGS, outputs with public keys are of type txout_to_tagged_key + // after HF_VERSION_FCMP_PLUS_PLUS, outputs with public keys are of type txout_to_carrot_v1 if (out.target.type() == typeid(txout_to_key)) output_public_key = boost::get< txout_to_key >(out.target).key; else if (out.target.type() == typeid(txout_to_tagged_key)) output_public_key = boost::get< txout_to_tagged_key >(out.target).key; + else if (out.target.type() == typeid(txout_to_carrot_v1)) + output_public_key = boost::get< txout_to_carrot_v1 >(out.target).key; else { LOG_ERROR("Unexpected output target type found: " << out.target.type().name()); @@ -931,6 +1109,28 @@ namespace cryptonote : boost::optional(); } //--------------------------------------------------------------- + bool get_commitment(const transaction& tx, std::size_t o_idx, const std::unordered_map &transparent_amount_commitments, rct::key &c_out) + { + static_assert(CURRENT_TRANSACTION_VERSION == 2, "This section of code was written with 2 tx versions in mind. " + "Revisit this section and update for the new tx version."); + CHECK_AND_ASSERT_THROW_MES(tx.version == 1 || tx.version == 2, "encountered unexpected tx version"); + + if (tx.version >= 2 && !cryptonote::is_coinbase(tx)) + { + CHECK_AND_ASSERT_MES(tx.rct_signatures.outPk.size() > o_idx, false, "get_commitment: o_idx must be < tx.rct_signatures.outPk.size()"); + c_out = tx.rct_signatures.outPk.at(o_idx).mask; + return true; + } + + // tx version 1 OR miner tx + // return the pre-calculated transparent amount commitment + CHECK_AND_ASSERT_MES(tx.vout.size() > o_idx, false, "get_commitment: o_idx must be < tx.vout.size()"); + const auto it = transparent_amount_commitments.find(tx.vout.at(o_idx).amount); + CHECK_AND_ASSERT_MES(it != transparent_amount_commitments.end(), false, "get_commitment: transparent amount commitment missing"); + c_out = it->second; + return true; + } + //--------------------------------------------------------------- std::string short_hash_str(const crypto::hash& h) { std::string res = string_tools::pod_to_hex(h); @@ -960,33 +1160,47 @@ namespace cryptonote //--------------------------------------------------------------- bool check_output_types(const transaction& tx, const uint8_t hf_version) { + if (tx.vout.empty()) + return true; + + // require all outputs in a tx be of the same type + const std::type_info &o_type = tx.vout.at(0).target.type(); for (const auto &o: tx.vout) { - if (hf_version > HF_VERSION_VIEW_TAGS) - { - // from v15, require outputs have view tags - CHECK_AND_ASSERT_MES(o.target.type() == typeid(txout_to_tagged_key), false, "wrong variant type: " - << o.target.type().name() << ", expected txout_to_tagged_key in transaction id=" << get_transaction_hash(tx)); - } - else if (hf_version < HF_VERSION_VIEW_TAGS) - { - // require outputs to be of type txout_to_key - CHECK_AND_ASSERT_MES(o.target.type() == typeid(txout_to_key), false, "wrong variant type: " - << o.target.type().name() << ", expected txout_to_key in transaction id=" << get_transaction_hash(tx)); - } - else //(hf_version == HF_VERSION_VIEW_TAGS) - { - // require outputs be of type txout_to_key OR txout_to_tagged_key - // to allow grace period before requiring all to be txout_to_tagged_key - CHECK_AND_ASSERT_MES(o.target.type() == typeid(txout_to_key) || o.target.type() == typeid(txout_to_tagged_key), false, "wrong variant type: " - << o.target.type().name() << ", expected txout_to_key or txout_to_tagged_key in transaction id=" << get_transaction_hash(tx)); - - // require all outputs in a tx be of the same type - CHECK_AND_ASSERT_MES(o.target.type() == tx.vout[0].target.type(), false, "non-matching variant types: " - << o.target.type().name() << " and " << tx.vout[0].target.type().name() << ", " - << "expected matching variant types in transaction id=" << get_transaction_hash(tx)); - } + const std::type_info &cur_type = o.target.type(); + CHECK_AND_ASSERT_MES(cur_type == o_type, false, "non-matching variant types: " + << o_type.name() << " and " << cur_type.name() << ", " + << "expected matching variant types in transaction id=" << get_transaction_hash(tx)); } + + const bool is_coinbase = cryptonote::is_coinbase(tx); + + bool is_correct_output_type = false; + if (hf_version < HF_VERSION_VIEW_TAGS) + is_correct_output_type = o_type == typeid(txout_to_key); + else if (hf_version == HF_VERSION_VIEW_TAGS) + is_correct_output_type = (o_type == typeid(txout_to_key) || o_type == typeid(txout_to_tagged_key)); + else if (hf_version < HF_VERSION_CARROT) + is_correct_output_type = o_type == typeid(txout_to_tagged_key); + else if (hf_version == HF_VERSION_CARROT) + is_correct_output_type = (o_type == typeid(txout_to_tagged_key) && !is_coinbase) + || (o_type == typeid(txout_to_carrot_v1)); + else // (hf_version > HF_VERSION_CARROT) + is_correct_output_type = o_type == typeid(txout_to_carrot_v1); + + CHECK_AND_ASSERT_MES(is_correct_output_type, false, + "wrong " << (is_coinbase ? "" : "non-") << "coinbase transaction output type '" << o_type.name() + << "' for fork v" << hf_version << " in transaction id=" << get_transaction_hash(tx)); + + // during v17, require non-coinbase carrot txs use FCMP++ and legacy use BP+ + if (hf_version == HF_VERSION_CARROT && !is_coinbase) + { + CHECK_AND_ASSERT_MES( + (o_type == typeid(txout_to_carrot_v1) && tx.rct_signatures.type == rct::RCTTypeFcmpPlusPlus) || + (o_type == typeid(txout_to_tagged_key) && tx.rct_signatures.type == rct::RCTTypeBulletproofPlus), + false, "mismatched output type to tx proof type in transaction id=" << get_transaction_hash(tx)); + } + return true; } //--------------------------------------------------------------- @@ -1045,7 +1259,14 @@ namespace cryptonote return false; } //--------------------------------------------------------------- - boost::optional is_out_to_acc_precomp(const std::unordered_map& subaddresses, const crypto::public_key& out_key, const crypto::key_derivation& derivation, const std::vector& additional_derivations, size_t output_index, hw::device &hwdev, const boost::optional& view_tag_opt) + boost::optional is_out_to_acc_precomp( + const std::unordered_map& subaddresses, + const crypto::public_key& out_key, + const crypto::key_derivation& derivation, + const epee::span additional_derivations, + size_t output_index, + hw::device &hwdev, + const boost::optional& view_tag_opt) { // try the shared tx pubkey crypto::public_key subaddress_spendkey; @@ -1072,6 +1293,24 @@ namespace cryptonote return boost::none; } //--------------------------------------------------------------- + boost::optional is_out_to_acc_precomp( + const std::unordered_map& subaddresses, + const crypto::public_key& out_key, + const crypto::key_derivation& derivation, + const std::vector& additional_derivations, + size_t output_index, + hw::device &hwdev, + const boost::optional& view_tag_opt) + { + return is_out_to_acc_precomp(subaddresses, + out_key, + derivation, + epee::to_span(additional_derivations), + output_index, + hwdev, + view_tag_opt); + } + //--------------------------------------------------------------- bool lookup_acc_outs(const account_keys& acc, const transaction& tx, std::vector& outs, uint64_t& money_transfered) { crypto::public_key tx_pub_key = get_tx_pub_key_from_extra(tx); @@ -1286,7 +1525,8 @@ namespace cryptonote binary_archive ba(ss); const size_t inputs = t.vin.size(); const size_t outputs = t.vout.size(); - const size_t mixin = t.vin.empty() ? 0 : t.vin[0].type() == typeid(txin_to_key) ? boost::get(t.vin[0]).key_offsets.size() - 1 : 0; + const size_t mixin = (t.vin.empty() || t.rct_signatures.type == rct::RCTTypeFcmpPlusPlus || t.vin[0].type() != typeid(txin_to_key)) + ? 0 : boost::get(t.vin[0]).key_offsets.size() - 1; bool r = tt.rct_signatures.p.serialize_rctsig_prunable(ba, t.rct_signatures.type, inputs, outputs, mixin); CHECK_AND_ASSERT_MES(r, false, "Failed to serialize rct signatures prunable"); cryptonote::get_blob_hash(ss.str(), res); @@ -1440,8 +1680,8 @@ namespace cryptonote blobdata get_block_hashing_blob(const block& b) { blobdata blob = t_serializable_object_to_blob(static_cast(b)); - crypto::hash tree_root_hash = get_tx_tree_hash(b); - blob.append(reinterpret_cast(&tree_root_hash), sizeof(tree_root_hash)); + crypto::hash block_content_hash = get_block_content_hash(b); + blob.append(reinterpret_cast(&block_content_hash), sizeof(block_content_hash)); blob.append(tools::get_varint_data(b.tx_hashes.size()+1)); return blob; } @@ -1583,29 +1823,31 @@ namespace cryptonote return t_serializable_object_to_blob(tx, b_blob); } //--------------------------------------------------------------- - void get_tx_tree_hash(const std::vector& tx_hashes, crypto::hash& h) - { - tree_hash(tx_hashes.data(), tx_hashes.size(), h); - } - //--------------------------------------------------------------- - crypto::hash get_tx_tree_hash(const std::vector& tx_hashes) + crypto::hash get_block_content_hash(const block& b) { - crypto::hash h = null_hash; - get_tx_tree_hash(tx_hashes, h); - return h; - } - //--------------------------------------------------------------- - crypto::hash get_tx_tree_hash(const block& b) - { - std::vector txs_ids; - txs_ids.reserve(1 + b.tx_hashes.size()); - crypto::hash h = null_hash; - size_t bl_sz = 0; - CHECK_AND_ASSERT_THROW_MES(get_transaction_hash(b.miner_tx, h, bl_sz), "Failed to calculate transaction hash"); - txs_ids.push_back(h); + // The miner transaction goes first in the Merkle tree so that mining software can update nonces + // within, and then the block content hash can be recalculated by re-hashing the left-hand side + // of the tree. The number of FCMP tree layers and FCMP tree root go in the following slots so + // that they are at a fixed index. + + std::vector hashes; + hashes.reserve(1 + 1 + 1 + b.tx_hashes.size()); + // 1. Miner tx + hashes.push_back(get_transaction_hash(b.miner_tx)); + if (b.major_version >= HF_VERSION_FCMP_PLUS_PLUS) + { + // 2. n tree layers in FCMP++ tree + static_assert(sizeof(crypto::hash) >= sizeof(uint8_t), "crypto::hash is too small"); + hashes.push_back(crypto::hash{static_cast(b.fcmp_pp_n_tree_layers)}); + // 3. FCMP++ tree root + crypto::hash &h = hashes.emplace_back(); + static_assert(sizeof(crypto::hash) == sizeof(crypto::ec_point), "hash/ec_point size mismatch"); + memcpy(&h, &b.fcmp_pp_tree_root, sizeof(crypto::hash)); + } + // 4. All other txs for(auto& th: b.tx_hashes) - txs_ids.push_back(th); - return get_tx_tree_hash(txs_ids); + hashes.push_back(th); + return get_tree_hash(hashes); } //--------------------------------------------------------------- crypto::hash get_block_longhash(const blobdata_ref block_hashing_blob, @@ -1670,4 +1912,120 @@ namespace cryptonote sc_sub((unsigned char*)key.data, (const unsigned char*)key.data, (const unsigned char*)hash.data); return key; } + //--------------------------------------------------------------- + uint64_t get_default_last_locked_block_index(const uint64_t block_included_in_chain) + { + static_assert(CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE > 0, "unexpected default spendable age"); + return block_included_in_chain + (CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE - 1); + } + //--------------------------------------------------------------- + // TODO: write tests for this func that match with current daemon logic + uint64_t get_last_locked_block_index(uint64_t unlock_time, uint64_t block_included_in_chain) + { + uint64_t last_locked_block_index = 0; + + const uint64_t default_block_index = get_default_last_locked_block_index(block_included_in_chain); + + if (unlock_time == 0) + { + last_locked_block_index = default_block_index; + } + else if (unlock_time < CRYPTONOTE_MAX_BLOCK_NUMBER) + { + // The unlock_time in this case is supposed to be the chain height at which the output unlocks + // The chain height is 1 higher than the highest block index, so we subtract 1 for this delta + last_locked_block_index = unlock_time > 0 ? (unlock_time - 1) : 0; + } + else + { + // Interpret the unlock_time as time + // TODO: hardcode correct times for each network and take in nettype + const auto hf_v15_time = 1656629118; + const auto hf_v15_height = 2689608; + + // Use the last hard fork's time and block combo to convert the time-based timelock into an last locked block + // TODO: consider taking into account 60s block times when that was consensus + if (hf_v15_time > unlock_time) + { + const auto seconds_since_unlock = hf_v15_time - unlock_time; + const auto blocks_since_unlock = seconds_since_unlock / DIFFICULTY_TARGET_V2; + + last_locked_block_index = hf_v15_height > blocks_since_unlock + ? (hf_v15_height - blocks_since_unlock) + : default_block_index; + } + else + { + const auto seconds_until_unlock = unlock_time - hf_v15_time; + const auto blocks_until_unlock = seconds_until_unlock / DIFFICULTY_TARGET_V2; + last_locked_block_index = hf_v15_height + blocks_until_unlock; + } + + /* Note: since this function was introduced for the hf that included fcmp's, it's possible for an output to be + spent before it reaches the last_locked_block_index going by the old rules; this is ok. It can't be spent again b/c + it'll have a duplicate key image. It's also possible for an output to unlock by old rules, and then re-lock + again at the fork. This is also ok, we just need to be sure that the new hf rules use this last_locked_block_index + starting at the fork for fcmp's. + */ + + // TODO: double check the accuracy of this calculation + MDEBUG("unlock time: " << unlock_time << " , last_locked_block_index: " << last_locked_block_index); + } + + // Can't unlock earlier than the default last locked block + return std::max(last_locked_block_index, default_block_index); + } + //--------------------------------------------------------------- + bool is_custom_timelocked(bool is_coinbase, uint64_t last_locked_block_idx, uint64_t block_included_in_chain) + { + if (is_coinbase) + return false; + + return last_locked_block_idx > cryptonote::get_default_last_locked_block_index(block_included_in_chain); + } + //--------------------------------------------------------------- + OutsByLastLockedBlockMeta get_outs_by_last_locked_block( + const std::vector> &txs, + const std::unordered_map &transparent_amount_commitments, + const uint64_t first_unified_id, + const uint64_t block_idx) + { + OutsByLastLockedBlockMeta outs_by_last_locked_block_meta_out; + outs_by_last_locked_block_meta_out.next_unified_id = first_unified_id; + + for (const auto &tx : txs) + { + outs_by_last_locked_block_meta_out.next_unified_id += set_tx_outs_by_last_locked_block( + tx.get(), + transparent_amount_commitments, + outs_by_last_locked_block_meta_out.next_unified_id, + block_idx, + outs_by_last_locked_block_meta_out.outs_by_last_locked_block, + outs_by_last_locked_block_meta_out.timelocked_outputs); + } + + return outs_by_last_locked_block_meta_out; + } + //--------------------------------------------------------------- + fcmp_pp::OutputPair to_output_pair(const cryptonote::txout_target_v &tx_out, const rct::key &commitment) + { + struct tx_out_visitor + { + const crypto::public_key &O; + const crypto::ec_point &C; + + fcmp_pp::OutputPair operator()(const cryptonote::txout_to_carrot_v1&) const + { return fcmp_pp::CarrotOutputPairV1{{O, C}}; } + fcmp_pp::OutputPair operator()(const cryptonote::txout_to_tagged_key&) const + { return fcmp_pp::LegacyOutputPair{{O, C}}; } + fcmp_pp::OutputPair operator()(const cryptonote::txout_to_key&) const + { return fcmp_pp::LegacyOutputPair{{O, C}}; } + fcmp_pp::OutputPair operator()(const cryptonote::txout_to_scripthash&) const + { return fcmp_pp::LegacyOutputPair{{O, C}}; } + }; + + const crypto::public_key &O = cryptonote::output_pubkey_cref(tx_out); + const crypto::ec_point &C = rct::rct2pt(commitment); + return boost::apply_visitor(tx_out_visitor{O, C}, tx_out); + } } diff --git a/src/cryptonote_basic/cryptonote_format_utils.h b/src/cryptonote_basic/cryptonote_format_utils.h index 64383ca9475..d7dfdd49bd2 100644 --- a/src/cryptonote_basic/cryptonote_format_utils.h +++ b/src/cryptonote_basic/cryptonote_format_utils.h @@ -37,8 +37,12 @@ #include "include_base_utils.h" #include "crypto/crypto.h" #include "crypto/hash.h" +#include "fcmp_pp/curve_trees.h" +#include "fcmp_pp/fcmp_pp_types.h" +#include "span.h" #include #include +#include namespace epee { @@ -53,6 +57,7 @@ namespace cryptonote void get_transaction_prefix_hash(const transaction_prefix& tx, crypto::hash& h); crypto::hash get_transaction_prefix_hash(const transaction_prefix& tx); bool parse_and_validate_tx_prefix_from_blob(const blobdata_ref& tx_blob, transaction_prefix& tx); + bool expand_transaction_1(transaction &tx, bool base_only); bool parse_and_validate_tx_from_blob(const blobdata_ref& tx_blob, transaction& tx, crypto::hash& tx_hash, crypto::hash& tx_prefix_hash); bool parse_and_validate_tx_from_blob(const blobdata_ref& tx_blob, transaction& tx, crypto::hash& tx_hash); bool parse_and_validate_tx_from_blob(const blobdata_ref& tx_blob, transaction& tx); @@ -98,6 +103,7 @@ namespace cryptonote subaddress_index index; crypto::key_derivation derivation; }; + boost::optional is_out_to_acc_precomp(const std::unordered_map& subaddresses, const crypto::public_key& out_key, const crypto::key_derivation& derivation, const epee::span additional_derivations, size_t output_index, hw::device &hwdev, const boost::optional& view_tag_opt = boost::optional()); boost::optional is_out_to_acc_precomp(const std::unordered_map& subaddresses, const crypto::public_key& out_key, const crypto::key_derivation& derivation, const std::vector& additional_derivations, size_t output_index, hw::device &hwdev, const boost::optional& view_tag_opt = boost::optional()); bool lookup_acc_outs(const account_keys& acc, const transaction& tx, const crypto::public_key& tx_pub_key, const std::vector& additional_tx_public_keys, std::vector& outs, uint64_t& money_transfered); bool lookup_acc_outs(const account_keys& acc, const transaction& tx, std::vector& outs, uint64_t& money_transfered); @@ -130,10 +136,15 @@ namespace cryptonote bool get_inputs_money_amount(const transaction& tx, uint64_t& money); uint64_t get_outs_money_amount(const transaction& tx); bool get_output_public_key(const cryptonote::tx_out& out, crypto::public_key& output_public_key); + bool get_commitment(const transaction& tx, std::size_t o_idx, const std::unordered_map &transparent_amount_commitments, rct::key &c_out); boost::optional get_output_view_tag(const cryptonote::tx_out& out); bool check_inputs_types_supported(const transaction& tx); bool check_outs_valid(const transaction& tx); bool parse_amount(uint64_t& amount, const std::string& str_amount); + uint64_t get_fcmp_pp_prefix_weight_v1(const size_t n_inputs, const size_t n_outputs, const size_t extra_len); + uint64_t get_fcmp_pp_unprunable_weight_v1(const size_t n_inputs, const size_t n_outputs, const size_t extra_len); + uint64_t get_fcmp_pp_transaction_weight_v1(const size_t n_inputs, const size_t n_outputs, const size_t extra_len); + uint64_t get_fcmp_pp_transaction_weight_v1(const transaction_prefix &tx_prefix); uint64_t get_transaction_weight(const transaction &tx); uint64_t get_transaction_weight(const transaction &tx, size_t blob_size); uint64_t get_pruned_transaction_weight(const transaction &tx); @@ -265,11 +276,50 @@ namespace cryptonote const uint64_t height, const uint8_t major_version, const crypto::hash &seed_hash); + crypto::hash get_block_content_hash(const block& b); bool is_valid_decomposed_amount(uint64_t amount); void get_hash_stats(uint64_t &tx_hashes_calculated, uint64_t &tx_hashes_cached, uint64_t &block_hashes_calculated, uint64_t & block_hashes_cached); crypto::secret_key encrypt_key(crypto::secret_key key, const epee::wipeable_string &passphrase); crypto::secret_key decrypt_key(crypto::secret_key key, const epee::wipeable_string &passphrase); + + uint64_t get_default_last_locked_block_index(const uint64_t block_included_in_chain); + // Returns the last locked block index for the provided unlock_time + uint64_t get_last_locked_block_index(uint64_t unlock_time, uint64_t block_included_in_chain); + bool is_custom_timelocked(bool is_coinbase, uint64_t last_locked_block_idx, uint64_t block_included_in_chain); + + struct OutsByLastLockedBlockMeta + { + fcmp_pp::OutsByLastLockedBlock outs_by_last_locked_block; + std::unordered_map timelocked_outputs; + uint64_t next_unified_id; + }; + + OutsByLastLockedBlockMeta get_outs_by_last_locked_block( + const std::vector> &txs, + const std::unordered_map &transparent_amount_commitments, + const uint64_t first_unified_id, + const uint64_t block_idx); + + inline bool output_checked_for_torsion(const cryptonote::txout_target_v &tx_out) + { + struct tx_out_visitor + { + bool operator()(const cryptonote::txout_to_carrot_v1&) const + { return true; } + bool operator()(const cryptonote::txout_to_tagged_key&) const + { return false; } + bool operator()(const cryptonote::txout_to_key&) const + { return false; } + bool operator()(const cryptonote::txout_to_scripthash&) const + { return false; } + }; + + return boost::apply_visitor(tx_out_visitor{}, tx_out); + } + + fcmp_pp::OutputPair to_output_pair(const cryptonote::txout_target_v &tx_out, const rct::key &commitment); + #define CHECKED_GET_SPECIFIC_VARIANT(variant_var, specific_type, variable_name, fail_return_val) \ CHECK_AND_ASSERT_MES(variant_var.type() == typeid(specific_type), fail_return_val, "wrong variant type: " << variant_var.type().name() << ", expected " << typeid(specific_type).name()); \ specific_type& variable_name = boost::get(variant_var); diff --git a/src/cryptonote_basic/tx_extra.h b/src/cryptonote_basic/tx_extra.h index 74f5105ee62..f5a8a5d2941 100644 --- a/src/cryptonote_basic/tx_extra.h +++ b/src/cryptonote_basic/tx_extra.h @@ -100,6 +100,7 @@ namespace cryptonote struct tx_extra_pub_key { + // while marked `crypto::public_key`, which usually means Ed25519, this will hold an X25519 pubkey in Carrot txs crypto::public_key pub_key; BEGIN_SERIALIZE() @@ -167,6 +168,7 @@ namespace cryptonote // per-output additional tx pubkey for multi-destination transfers involving at least one subaddress struct tx_extra_additional_pub_keys { + // same as tx_extra_pub_key, this is a vector of X25519 pubkeys in Carrot txs std::vector data; BEGIN_SERIALIZE() diff --git a/src/cryptonote_config.h b/src/cryptonote_config.h index 35b86d6e9dd..5fb1e27becf 100644 --- a/src/cryptonote_config.h +++ b/src/cryptonote_config.h @@ -59,8 +59,10 @@ #define CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V2 60000 //size of block (bytes) after which reward for block calculated using block size #define CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V1 20000 //size of block (bytes) after which reward for block calculated using block size - before first fork #define CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5 300000 //size of block (bytes) after which reward for block calculated using block size - second change, from v5 +#define CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V17 625000 //size of block (bytes) after which reward for block calculated using block size - third change, from v17 #define CRYPTONOTE_LONG_TERM_BLOCK_WEIGHT_WINDOW_SIZE 100000 // size in blocks of the long term block weight median window -#define CRYPTONOTE_SHORT_TERM_BLOCK_WEIGHT_SURGE_FACTOR 50 +#define CRYPTONOTE_SHORT_TERM_BLOCK_SURGE_FACTOR_V10 50 +#define CRYPTONOTE_SHORT_TERM_BLOCK_SURGE_FACTOR_V17 8 #define CRYPTONOTE_COINBASE_BLOB_RESERVED_SIZE 600 #define CRYPTONOTE_DISPLAY_DECIMAL_POINT 12 // COIN - number of smallest units in one coin @@ -72,7 +74,8 @@ #define DYNAMIC_FEE_PER_KB_BASE_FEE ((uint64_t)2000000000) // 2 * pow(10,9) #define DYNAMIC_FEE_PER_KB_BASE_BLOCK_REWARD ((uint64_t)10000000000000) // 10 * pow(10,12) #define DYNAMIC_FEE_PER_KB_BASE_FEE_V5 ((uint64_t)2000000000 * (uint64_t)CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V2 / CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5) -#define DYNAMIC_FEE_REFERENCE_TRANSACTION_WEIGHT ((uint64_t)3000) +#define DYNAMIC_FEE_REFERENCE_TRANSACTION_WEIGHT_V8 ((uint64_t)3000) +#define DYNAMIC_FEE_REFERENCE_TRANSACTION_WEIGHT_V17 ((uint64_t)12500) #define ORPHANED_BLOCKS_MAX_COUNT 100 @@ -145,6 +148,7 @@ #define P2P_DEFAULT_PEERS_IN_HANDSHAKE 250 #define P2P_MAX_PEERS_IN_HANDSHAKE 250 #define P2P_DEFAULT_CONNECTION_TIMEOUT 5000 //5 seconds +#define P2P_DEFAULT_REQUEST_TIMEOUT (P2P_DEFAULT_CONNECTION_TIMEOUT*6) // 30 seconds #define P2P_DEFAULT_SOCKS_CONNECT_TIMEOUT 45 // seconds #define P2P_DEFAULT_PING_CONNECTION_TIMEOUT 2000 //2 seconds #define P2P_DEFAULT_INVOKE_TIMEOUT 60*2*1000 //2 minutes @@ -154,6 +158,8 @@ #define P2P_DEFAULT_SYNC_SEARCH_CONNECTIONS_COUNT 2 #define P2P_DEFAULT_LIMIT_RATE_UP 8192 // kB/s #define P2P_DEFAULT_LIMIT_RATE_DOWN 32768 // kB/s +#define P2P_REQUEST_FAILURE_THRESHOLD_PERCENTAGE 70 // if more than 70% of requests fail, the peer is dropped +#define P2P_MIN_SAMPLE_SIZE_FOR_DROPPING 5 // minimum number of requests to consider dropping a peer for failed requests #define P2P_FAILED_ADDR_FORGET_SECONDS (60*60) //1 hour #define P2P_IP_BLOCKTIME (60*60*24) //24 hour @@ -161,7 +167,8 @@ #define P2P_IDLE_CONNECTION_KILL_INTERVAL (5*60) //5 minutes #define P2P_SUPPORT_FLAG_FLUFFY_BLOCKS 0x01 -#define P2P_SUPPORT_FLAGS P2P_SUPPORT_FLAG_FLUFFY_BLOCKS +#define P2P_SUPPORT_FLAG_TX_RELAY_V2 0x02 +#define P2P_SUPPORT_FLAGS (P2P_SUPPORT_FLAG_FLUFFY_BLOCKS | P2P_SUPPORT_FLAG_TX_RELAY_V2) #define RPC_IP_FAILS_BEFORE_BLOCK 3 @@ -196,6 +203,14 @@ #define HF_VERSION_BULLETPROOF_PLUS 15 #define HF_VERSION_VIEW_TAGS 15 #define HF_VERSION_2021_SCALING 15 +#define HF_VERSION_FCMP_PLUS_PLUS 17 +#define HF_VERSION_CARROT 17 +#define HF_VERSION_REJECT_UNLOCK_TIME 17 +#define HF_VERSION_REJECT_LARGE_EXTRA 17 +#define HF_VERSION_REJECT_UNMIXABLE_V1 17 +#define HF_VERSION_REJECT_MANY_MINER_OUTPUTS 17 +#define HF_VERSION_2026_SCALING 17 +#define MAX_HF_VERSION 18 #define PER_KB_FEE_QUANTIZATION_DECIMALS 8 #define CRYPTONOTE_SCALING_2021_FEE_ROUNDING_PLACES 2 @@ -207,6 +222,21 @@ #define BULLETPROOF_MAX_OUTPUTS 16 #define BULLETPROOF_PLUS_MAX_OUTPUTS 16 +// TODO: settle on figures here +// https://gist.github.com/kayabaNerve/dbbadf1f2b0f4e04732fc5ac559745b7 +// https://gist.github.com/Rucknium/784b243d75184333144a92b3258788f6 +// Discussions on PoW-enabled relay for high input txs: +// https://libera.monerologs.net/monero-research-lab/20250430#c522449-c522790 +// https://libera.monerologs.net/no-wallet-left-behind/20250505#c523568-c523686 +#define FCMP_PLUS_PLUS_MAX_INPUTS 128 +#define FCMP_PLUS_PLUS_MAX_OUTPUTS 16 +#define FCMP_PLUS_PLUS_MAX_MINER_OUTPUTS 10000 +#define FCMP_PLUS_PLUS_MAX_TX_WEIGHT 203599 + +// Restricting n layers keeps the proof_len table size very small and portable +// 12 layers means the tree can support over 100 quadrillion outputs +#define FCMP_PLUS_PLUS_MAX_LAYERS 12 + #define CRYPTONOTE_PRUNING_STRIPE_SIZE 4096 // the smaller, the smoother the increase #define CRYPTONOTE_PRUNING_LOG_STRIPES 3 // the higher, the more space saved #define CRYPTONOTE_PRUNING_TIP_BLOCKS 5500 // the smaller, the more space saved @@ -265,6 +295,7 @@ 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_TXHASH_AND_TREE_ROOT[] = "txhash_and_tree_root"; // Multisig const uint32_t MULTISIG_MAX_SIGNERS{16}; diff --git a/src/cryptonote_core/CMakeLists.txt b/src/cryptonote_core/CMakeLists.txt index 50bc11de095..65fdbb582bd 100644 --- a/src/cryptonote_core/CMakeLists.txt +++ b/src/cryptonote_core/CMakeLists.txt @@ -48,12 +48,15 @@ monero_add_library(cryptonote_core target_link_libraries(cryptonote_core PUBLIC version + carrot_core + carrot_impl common cncrypto blockchain_db ringct device hardforks + fcmp_pp ${Boost_DATE_TIME_LIBRARY} ${Boost_PROGRAM_OPTIONS_LIBRARY} ${Boost_SERIALIZATION_LIBRARY} diff --git a/src/cryptonote_core/blockchain.cpp b/src/cryptonote_core/blockchain.cpp index 387bae9a3b9..df71753e4de 100644 --- a/src/cryptonote_core/blockchain.cpp +++ b/src/cryptonote_core/blockchain.cpp @@ -29,6 +29,7 @@ // Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers #include +#include #include #include #include @@ -58,6 +59,8 @@ #include "common/varint.h" #include "common/pruning.h" #include "common/data_cache.h" +#include "ringct/rctSigs.h" +#include "tx_verification_utils.h" #include "time_helper.h" #undef MONERO_DEFAULT_LOG_CATEGORY @@ -67,6 +70,8 @@ using namespace crypto; +static constexpr const std::uint8_t RCT_CACHE_TYPE = rct::RCTTypeFcmpPlusPlus; + //#include "serialization/json_archive.h" /* TODO: @@ -87,6 +92,33 @@ DISABLE_VS_WARNINGS(4267) // used to overestimate the block reward when estimating a per kB to use #define BLOCK_REWARD_OVERESTIMATE (10 * 1000000000000) +namespace +{ +//------------------------------------------------------------------ +static bool get_fcmp_tx_tree_root(const BlockchainDB *db, const cryptonote::transaction &tx, crypto::ec_point &tree_root_out) +{ + tree_root_out = crypto::ec_point{}; + if (!rct::is_rct_fcmp(tx.rct_signatures.type)) + return true; + CHECK_AND_ASSERT_MES(!tx.pruned, false, "can't get root for pruned FCMP txs"); + + // Make sure reference block exists in the chain + CHECK_AND_NO_ASSERT_MES_L1(tx.rct_signatures.p.reference_block < db->height(), false, + "tx " << get_transaction_hash(tx) << " included reference block that was too high"); + + // Get the tree root and n tree layers at provided block + const uint8_t n_tree_layers = db->get_tree_root_at_blk_idx(tx.rct_signatures.p.reference_block, tree_root_out); + + // Make sure the provided n tree layers matches expected + // IMPORTANT! + CHECK_AND_ASSERT_MES(tx.rct_signatures.p.n_tree_layers == n_tree_layers, false, + "tx " << get_transaction_hash(tx) << " included incorrect number of tree layers"); + + return true; +} +//------------------------------------------------------------------ +} //anonymous namespace + //------------------------------------------------------------------ Blockchain::Blockchain(tx_memory_pool& tx_pool) : m_db(), m_tx_pool(tx_pool), m_hardfork(NULL), m_timestamps_and_difficulties_height(0), m_reset_timestamps_and_difficulties_height(true), m_current_block_cumul_weight_limit(0), m_current_block_cumul_weight_median(0), @@ -222,7 +254,7 @@ bool Blockchain::scan_outputkeys_for_indexes(size_t tx_version, const txin_to_ke if (count < outputs.size()) output_index = outputs.at(count); else - output_index = m_db->get_output_key(tx_in_to_key.amount, i); + output_index = m_db->get_output_key(tx_in_to_key.amount, i).data; // call to the passed boost visitor to grab the public key for the output if (!vis.handle_output(output_index.unlock_time, output_index.pubkey, output_index.commitment)) @@ -452,8 +484,15 @@ bool Blockchain::init(BlockchainDB* db, const network_type nettype, bool offline recalculate_difficulties(difficulty_recalc_height); } + if (m_db->is_read_only()) + { + db_rtxn_guard txn_guard(m_db); + if (!update_next_cumulative_weight_limit()) + return false; + } + else { - db_txn_guard txn_guard(m_db, m_db->is_read_only()); + db_wtxn_guard txn_guard(m_db); if (!update_next_cumulative_weight_limit()) return false; } @@ -642,55 +681,78 @@ block Blockchain::pop_block_from_blockchain() // in hf_versions. uint8_t version = get_ideal_hard_fork_version(m_db->height()); - // At time of popping, we know all of the referenced mix ring data for popped transactions, - // and since they are already in the chain, and not pruned, we assume that the ring signature - // input verification succeeded for these transactions. We can deference each each mix ring, - // calculate the verification ID for that (tx, ring) pair, then add to the mempool with that - // input verification ID. This speeds up re-org handling by allowing to skip verifying ring - // signatures which were previously verified. - const crypto::hash tx_prefix_hash = get_transaction_prefix_hash(tx); - - struct outputs_visitor + // At time of popping, we know all of the referenced mix ring / FCMP root data for popped + // transactions, and since they are already in the chain, and not pruned, we assume that the + // ring signature / FCMP++ input verification succeeded for these transactions. We can + // dereference each each mix ring / FCMP tree root, calculate the verification ID for that + // (tx, ref data) pair, then add to the mempool with that input verification ID. This speeds + // up re-org handling by allowing to skip verifying ring signatures / FCMP++s which were + // previously verified. + crypto::hash valid_input_verification_id = crypto::null_hash; + const bool uses_ring_signatures = tx.version == 1 + || (tx.version == 2 && tx.rct_signatures.type <= rct::RCTTypeBulletproofPlus); + if (uses_ring_signatures) { - rct::ctkeyV ˚ - bool handle_output(uint64_t, const crypto::public_key &pubkey, const rct::key &commitment) + const crypto::hash tx_prefix_hash = get_transaction_prefix_hash(tx); + + struct outputs_visitor { - ring.push_back({rct::pk2rct(pubkey), commitment}); - return true; - } - }; + rct::ctkeyV ˚ + bool handle_output(uint64_t, const crypto::public_key &pubkey, const rct::key &commitment) + { + ring.push_back({rct::pk2rct(pubkey), commitment}); + return true; + } + }; - rct::ctkeyM dereferenced_mix_ring; - dereferenced_mix_ring.reserve(tx.vin.size()); - for (const txin_v &txin : tx.vin) - { - const txin_to_key *pin = boost::get(&txin); - if (nullptr == pin || pin->key_offsets.empty()) + rct::ctkeyM dereferenced_mix_ring; + dereferenced_mix_ring.reserve(tx.vin.size()); + for (const txin_v &txin : tx.vin) { - dereferenced_mix_ring.clear(); - break; - } + const txin_to_key *pin = boost::get(&txin); + if (nullptr == pin || pin->key_offsets.empty()) + { + dereferenced_mix_ring.clear(); + break; + } + + rct::ctkeyV &curr_ring = dereferenced_mix_ring.emplace_back(); + curr_ring.reserve(pin->key_offsets.size()); + outputs_visitor vis{curr_ring}; - rct::ctkeyV &curr_ring = dereferenced_mix_ring.emplace_back(); - curr_ring.reserve(pin->key_offsets.size()); - outputs_visitor vis{curr_ring}; + if (!scan_outputkeys_for_indexes(tx.version, *pin, vis, tx_prefix_hash)) + { + dereferenced_mix_ring.clear(); + break; + } + } - if (!scan_outputkeys_for_indexes(tx.version, *pin, vis, tx_prefix_hash)) + if (!dereferenced_mix_ring.empty()) { - dereferenced_mix_ring.clear(); - break; + valid_input_verification_id = make_input_verification_id(get_transaction_hash(tx), dereferenced_mix_ring); + } + else + { + MWARNING("Failed to fetch ring signature input data for popped transaction, " + "will have to re-verify signature later"); } } - - crypto::hash valid_input_verification_id = crypto::null_hash; - if (!dereferenced_mix_ring.empty()) + else if (rct::is_rct_fcmp(tx.rct_signatures.type)) { - valid_input_verification_id = make_input_verification_id(get_transaction_hash(tx), dereferenced_mix_ring); + crypto::ec_point ref_tree_root{}; + if (get_fcmp_tx_tree_root(m_db, tx, ref_tree_root)) + { + valid_input_verification_id = make_input_verification_id(get_transaction_hash(tx), ref_tree_root); + } + else + { + MWARNING("Failed to fetch FCMP tree root input data for popped transaction, " + "will have to re-verify FCMP later"); + } } else { - MWARNING("Failed to fetch ring signature input data for popped transaction, " - "will have to re-verify signature later"); + MWARNING("Unknown referenced chain data type for popped non-coinbase tx " << get_transaction_hash(tx)); } // We assume that if they were in a block, the transactions are already known to the network @@ -1372,11 +1434,29 @@ bool Blockchain::prevalidate_miner_transaction(const block& b, uint64_t height, CHECK_AND_ASSERT_MES(check_output_types(b.miner_tx, hf_version), false, "miner transaction has invalid output type(s) in block " << get_block_hash(b)); + CHECK_AND_ASSERT_MES(check_transaction_output_pubkeys_order(b.miner_tx, hf_version), + false, "FCMP++ miner transaction has unsorted outputs in block " << get_block_hash(b)); + + // from v17, require tx.extra size be within limit + if (hf_version >= HF_VERSION_REJECT_LARGE_EXTRA) + { + // Scale extra limit by number of outputs since Carrot requires 1 32-byte ephemeral pubkey per output (for Janus). + const std::size_t max_extra_size = MAX_TX_EXTRA_SIZE + b.miner_tx.vout.size() * 32; + CHECK_AND_ASSERT_MES(b.miner_tx.extra.size() < max_extra_size, false, "miner transaction extra too big"); + } + + // from v17, require number of tx outputs to be within limit + if (hf_version >= HF_VERSION_REJECT_MANY_MINER_OUTPUTS) + { + CHECK_AND_ASSERT_MES(b.miner_tx.vout.size() <= FCMP_PLUS_PLUS_MAX_MINER_OUTPUTS, + false, "too many miner transaction outputs"); + } + return true; } //------------------------------------------------------------------ // This function validates the miner transaction reward -bool Blockchain::validate_miner_transaction(const block& b, size_t cumulative_block_weight, uint64_t fee, uint64_t& base_reward, uint64_t already_generated_coins, bool &partial_block_reward, uint8_t version) +bool Blockchain::validate_miner_transaction(const block& b, size_t cumulative_block_weight, uint64_t fee, uint64_t& base_reward, uint64_t already_generated_coins, bool &partial_block_reward, uint8_t version, const std::unordered_map &transparent_amount_commitments) { LOG_PRINT_L3("Blockchain::" << __func__); //validate reward @@ -1434,6 +1514,26 @@ bool Blockchain::validate_miner_transaction(const block& b, size_t cumulative_bl partial_block_reward = true; base_reward = money_in_use - fee; } + + if (version >= HF_VERSION_FCMP_PLUS_PLUS) + { + // Collect pubkeys and commitments for torsion check + std::vector pubkeys_and_commitments; + pubkeys_and_commitments.reserve(b.miner_tx.vout.size() * 2); + + if (!collect_points_for_torsion_check(b.miner_tx, transparent_amount_commitments, pubkeys_and_commitments)) + { + MERROR_VER("failed to collect pubkeys and commitments from miner tx"); + return false; + } + + if (!rct::verPointsForTorsion(pubkeys_and_commitments)) + { + MERROR_VER("miner tx outs have torsion"); + return false; + } + } + return true; } //------------------------------------------------------------------ @@ -1567,6 +1667,7 @@ bool Blockchain::create_block_template(block& b, const crypto::hash *from_block, invalidate_block_template_cache(); } + const uint64_t cur_n_blocks = m_db->height(); if (from_block) { //build alternative subchain, front -> mainchain, back -> alternative head @@ -1627,6 +1728,12 @@ bool Blockchain::create_block_template(block& b, const crypto::hash *from_block, b.minor_version = m_hardfork->get_ideal_version(); b.prev_id = *from_block; + if (b.major_version >= HF_VERSION_FCMP_PLUS_PLUS && alt_chain.size()) + { + MERROR("The daemon is not structured to build the FCMP++ tree for alt chains and does not know the correct root for the alt block header"); + return false; + } + // cheat and use the weight of the block we start from, virtually certain to be acceptable // and use 1.9 times rather than 2 times so we're even more sure if (parent_in_main) @@ -1649,7 +1756,7 @@ bool Blockchain::create_block_template(block& b, const crypto::hash *from_block, } else { - height = m_db->height(); + height = cur_n_blocks; b.major_version = m_hardfork->get_current_version(); b.minor_version = m_hardfork->get_ideal_version(); b.prev_id = get_tail_id(); @@ -1673,6 +1780,11 @@ bool Blockchain::create_block_template(block& b, const crypto::hash *from_block, CHECK_AND_ASSERT_MES(diffic, false, "difficulty overhead."); + if (b.major_version >= HF_VERSION_FCMP_PLUS_PLUS) + { + b.fcmp_pp_n_tree_layers = m_db->get_tree_root_at_blk_idx(cryptonote::get_default_last_locked_block_index(height - 1), b.fcmp_pp_tree_root); + } + size_t txs_weight; uint64_t fee; if (!m_tx_pool.fill_block_template(b, median_weight, already_generated_coins, txs_weight, fee, expected_reward, b.major_version)) @@ -1806,15 +1918,21 @@ bool Blockchain::create_block_template(block& b, const account_public_address& m return create_block_template(b, NULL, miner_address, diffic, height, expected_reward, cumulative_weight, ex_nonce, seed_height, seed_hash); } //------------------------------------------------------------------ -bool Blockchain::get_miner_data(uint8_t& major_version, uint64_t& height, crypto::hash& prev_id, crypto::hash& seed_hash, difficulty_type& difficulty, uint64_t& median_weight, uint64_t& already_generated_coins, std::vector& tx_backlog) +bool Blockchain::get_miner_data(uint8_t& major_version, uint64_t& height, crypto::hash& prev_id, uint8_t& fcmp_pp_n_tree_layers, crypto::ec_point& fcmp_pp_tree_root, crypto::hash& seed_hash, difficulty_type& difficulty, uint64_t& median_weight, uint64_t& already_generated_coins, std::vector& tx_backlog) { prev_id = m_db->top_block_hash(&height); + uint64_t top_block_idx = height; ++height; major_version = m_hardfork->get_ideal_version(height); + fcmp_pp_n_tree_layers = 0; + fcmp_pp_tree_root = crypto::ec_point{}; + if (major_version >= HF_VERSION_FCMP_PLUS_PLUS) + fcmp_pp_n_tree_layers = m_db->get_tree_root_at_blk_idx(cryptonote::get_default_last_locked_block_index(top_block_idx), fcmp_pp_tree_root); + seed_hash = crypto::null_hash; - if (m_hardfork->get_current_version() >= RX_BLOCK_VERSION) + if (major_version >= RX_BLOCK_VERSION) { uint64_t seed_height, next_height; crypto::rx_seedheights(height, &seed_height, &next_height); @@ -1823,7 +1941,7 @@ bool Blockchain::get_miner_data(uint8_t& major_version, uint64_t& height, crypto difficulty = get_difficulty_for_next_block(); median_weight = m_current_block_cumul_weight_median; - already_generated_coins = m_db->get_block_already_generated_coins(height - 1); + already_generated_coins = m_db->get_block_already_generated_coins(top_block_idx); m_tx_pool.get_block_template_backlog(tx_backlog); @@ -2046,9 +2164,13 @@ bool Blockchain::handle_alternative_block(const block& b, const crypto::hash& id } bei.cumulative_difficulty += current_diff; + // Collect transparent amount commitments + std::unordered_map transparent_amount_commitments; + collect_transparent_amount_commitments(extra_block_txs.txs_by_txid, transparent_amount_commitments); + // Now that we have the PoW verification out of the way, verify all pool supplement txs tx_verification_context tvc{}; - if (!ver_non_input_consensus(extra_block_txs, tvc, hf_version)) + if (!ver_non_input_consensus(extra_block_txs, transparent_amount_commitments, tvc, hf_version)) { MERROR_VER("Transaction pool supplement verification failure for alt block " << id); bvc.m_verifivation_failed = true; @@ -2329,8 +2451,7 @@ uint64_t Blockchain::get_num_mature_outputs(uint64_t amount) const crypto::public_key Blockchain::get_output_key(uint64_t amount, uint64_t global_index) const { - output_data_t data = m_db->get_output_key(amount, global_index); - return data.pubkey; + return m_db->get_output_key(amount, global_index).data.pubkey; } //------------------------------------------------------------------ @@ -2381,7 +2502,7 @@ bool Blockchain::get_outs(const COMMAND_RPC_GET_OUTPUTS_BIN::request& req, COMMA //------------------------------------------------------------------ void Blockchain::get_output_key_mask_unlocked(const uint64_t& amount, const uint64_t& index, crypto::public_key& key, rct::key& mask, bool& unlocked) const { - const auto o_data = m_db->get_output_key(amount, index); + const auto o_data = m_db->get_output_key(amount, index).data; key = o_data.pubkey; mask = o_data.commitment; tx_out_index toi = m_db->get_output_tx_and_index(amount, index); @@ -2431,6 +2552,74 @@ bool Blockchain::get_output_distribution(uint64_t amount, uint64_t from_height, } } //------------------------------------------------------------------ +fcmp_pp::OutsByLastLockedBlock Blockchain::get_recent_locked_outputs(uint64_t end_block_idx) const +{ + LOG_PRINT_L3("Blockchain::" << __func__); + CRITICAL_REGION_LOCAL(m_blockchain_lock); + + db_rtxn_guard rtxn_guard(m_db); + + fcmp_pp::OutsByLastLockedBlock outs; + + const uint64_t height = m_db->height(); + if (height == 0) + return outs; + + const uint64_t coinbase_start_idx = CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW > end_block_idx + ? 0 + : end_block_idx - CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW; + + const uint64_t normal_start_idx = CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE > end_block_idx + ? 0 + : end_block_idx - CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE; + + const uint64_t end_blk_idx = std::min(height - 1, end_block_idx); + + const auto get_outs_from_block = [this, &outs, normal_start_idx](uint64_t b_idx, const crypto::hash, const block &b) -> bool + { + auto get_outs_from_tx = [this, &outs, b_idx](const crypto::hash &tx_hash, const bool is_coinbase) + { + // Get tx outputs' last locked block and data necessary to rebuild curve tree + cryptonote::transaction tx; + const auto out_data = m_db->get_tx_output_data(tx_hash, tx); + if (out_data.empty()) + return; + const uint64_t last_locked_block = cryptonote::get_last_locked_block_index(out_data.front().data.unlock_time, b_idx); + + // Ignore custom timelocked outputs + if (cryptonote::is_custom_timelocked(is_coinbase, last_locked_block, b_idx)) + return; + + for (std::size_t i = 0; i < out_data.size(); ++i) + { + const auto &out = out_data[i]; + const auto &tx_out = tx.vout.at(i).target; + + auto output_pair = cryptonote::to_output_pair(tx_out, out.data.commitment); + const fcmp_pp::UnifiedOutput unified_output{ out.unified_id, std::move(output_pair) }; + + outs[last_locked_block].emplace_back(unified_output); + } + }; + + // Add coinbase outputs + get_outs_from_tx(cryptonote::get_transaction_hash(b.miner_tx), true); + + if (normal_start_idx > b_idx) + return true; + + // Add normal outputs + for (const auto &tx_hash : b.tx_hashes) + get_outs_from_tx(tx_hash, false); + + return true; + }; + + m_db->for_blocks_range(coinbase_start_idx, end_blk_idx, get_outs_from_block); + + return outs; +} +//------------------------------------------------------------------ // This function takes a list of block hashes from another node // on the network to find where the split point is between us and them. // This is used to see what to send another node that needs to sync. @@ -2595,6 +2784,104 @@ static bool fill(BlockchainDB *db, const crypto::hash &tx_hash, tx_blob_entry &t return true; } //------------------------------------------------------------------ +static bool set_fcmp_tx_tree_root(const BlockchainDB *db, + const cryptonote::transaction &tx, + std::unordered_map> &tree_root_by_block_idx_inout) +{ + if (!rct::is_rct_fcmp(tx.rct_signatures.type)) + return true; + CHECK_AND_ASSERT_MES(!tx.pruned, false, "can't set root for pruned FCMP txs"); + + const uint64_t ref_block_index = tx.rct_signatures.p.reference_block; + + // See if we already have this block's tree root + auto tree_root_it = tree_root_by_block_idx_inout.find(ref_block_index); + if (tree_root_it != tree_root_by_block_idx_inout.end()) + { + // cache hit + if (tree_root_it->second.second == tx.rct_signatures.p.n_tree_layers) + return true; + + MERROR_VER("Tx included incorrect n tree layers"); + return false; + } + + // Get ref block's tree root from the db + crypto::ec_point tree_root; + if (!get_fcmp_tx_tree_root(db, tx, tree_root)) + { + MERROR_VER("Failed to get referenced tree root"); + return false; + } + + tree_root_by_block_idx_inout[ref_block_index] = {std::move(tree_root), tx.rct_signatures.p.n_tree_layers}; + return true; +} +//------------------------------------------------------------------ +static bool batch_verify_fcmp_pp_txs(const BlockchainDB *db, + pool_supplement &extra_block_txs, + std::unordered_map &valid_input_verification_id_by_txid_out) +{ + valid_input_verification_id_by_txid_out.clear(); + + // 1. Collect referenced tree roots + std::unordered_map> tree_root_by_block_idx; + for (const auto &extra_tx : extra_block_txs.txs_by_txid) + { + const cryptonote::transaction &tx = extra_tx.second.first; + if (!set_fcmp_tx_tree_root(db, tx, tree_root_by_block_idx)) + { + MERROR_VER("Failed to set FCMP tx tree root"); + return false; + } + } + + // 2. Batch verify FCMP++'s and collect verIDs + if (!batch_ver_fcmp_pp_consensus(extra_block_txs, tree_root_by_block_idx, valid_input_verification_id_by_txid_out)) + { + MERROR_VER("Failed to batch verify FCMP++ txs"); + return false; + } + + return true; +} +//------------------------------------------------------------------ +void cryptonote::handle_fcmp_tree(BlockchainDB *db, const uint64_t block_idx, const uint64_t first_unified_id, const std::vector> &tx_refs, const std::unordered_map &transparent_amount_commitments) +{ + // Collect outs by last locked block to add to the db + OutsByLastLockedBlockMeta new_locked_outs = cryptonote::get_outs_by_last_locked_block(tx_refs, transparent_amount_commitments, first_unified_id, block_idx); + + // Get the outputs with default last locked block + const uint64_t default_last_locked_block = cryptonote::get_default_last_locked_block_index(block_idx); + auto new_default_locked_outs_it = new_locked_outs.outs_by_last_locked_block.find(default_last_locked_block); + const auto new_default_locked_outs = new_default_locked_outs_it != new_locked_outs.outs_by_last_locked_block.end() + ? std::move(new_default_locked_outs_it->second) + : std::vector{}; + + // Insert the new locked outputs into the db, excluding outputs created in + // this block with default last locked block. Outputs with default last locked + // block will be added to the tree immediately below. Outputs with last + // locked block higher than the default will be added to the locked outputs + // tables, staged for insertion to the tree later. + new_locked_outs.outs_by_last_locked_block.erase(default_last_locked_block); + db->add_locked_outs(new_locked_outs.outs_by_last_locked_block, new_locked_outs.timelocked_outputs); + + // Assume we just added block n. The soonest that outputs from block n can be + // included in the chain is in block n + CRYPTNOTE_DEFAULT_SPENDABLE_AGE. So + // we grow the tree with these outputs (and any others with last locked block + // n + CRYPTNOTE_DEFAULT_SPENDABLE_AGE - 1). We then expect this tree root + // be included in block header n+1. This way miners will build on top of the + // tree root usable in FCMP++'s in a future block. After block + // n + (CRYPTNOTE_DEFAULT_SPENDABLE_AGE - 1) is added to the chain, SPV + // clients syncing just block headers will have a solid assurance that the + // root usable to construct FCMP++ proofs is the correct root, since it will + // have 9 blocks of PoW on top of it. + // To be clear, block header n+1 includes the tree root usable to spend + // outputs with last locked block n + CRYPTNOTE_DEFAULT_SPENDABLE_AGE - 1. + static_assert(CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE > 0, "Expect a non-0 spendable age"); + db->advance_tree(block_idx, new_default_locked_outs); +} +//------------------------------------------------------------------ //TODO: return type should be void, throw on exception // alternatively, return true only if no transactions missed bool Blockchain::get_transactions_blobs(const std::vector& txs_ids, std::vector& txs, std::vector& missed_txs, bool pruned) const @@ -2786,17 +3073,18 @@ bool Blockchain::find_blockchain_supplement(const std::list& qbloc // find split point between ours and foreign blockchain (or start at // blockchain height ), and return up to max_count FULL // blocks by reference. -bool Blockchain::find_blockchain_supplement(const uint64_t req_start_block, const std::list& qblock_ids, std::vector, std::vector > > >& blocks, uint64_t& total_height, crypto::hash& top_hash, uint64_t& start_height, bool pruned, bool get_miner_tx_hash, size_t max_block_count, size_t max_tx_count) const +bool Blockchain::find_blockchain_supplement(const uint64_t req_start_block, const std::list& qblock_ids, std::vector, std::vector > > >& blocks, uint64_t& total_height, crypto::hash& top_hash, uint64_t& start_height, bool pruned, bool get_miner_tx_hash, size_t max_block_count, size_t max_tx_count, bool qblock_ids_skip_common_block) const { LOG_PRINT_L3("Blockchain::" << __func__); CRITICAL_REGION_LOCAL(m_blockchain_lock); + top_hash = m_db->top_block_hash(&total_height); + ++total_height; + // if a specific start height has been requested if(req_start_block > 0) { // if requested height is higher than our chain, return false -- we can't help - top_hash = m_db->top_block_hash(&total_height); - ++total_height; if (req_start_block >= total_height) { return false; @@ -2805,15 +3093,30 @@ bool Blockchain::find_blockchain_supplement(const uint64_t req_start_block, cons } else { + // find_blockchain_supplement's start_height is the highest block idx included in qblock_ids that's *also* in the main chain if(!find_blockchain_supplement(qblock_ids, start_height)) { return false; } + if (qblock_ids_skip_common_block) + { + // start from 1 block higher than the first common block (i.e. from the first block the client might not know about) + ++start_height; + + // if start_height is now the chain tip, we can return a truthy empty resp + if (start_height == total_height) + { + LOG_PRINT_L3("Returning empty find_blockchain_supplement, start_height: " << start_height); + blocks.clear(); + return true; + } + } } db_rtxn_guard rtxn_guard(m_db); top_hash = m_db->top_block_hash(&total_height); ++total_height; + CHECK_AND_ASSERT_MES(total_height >= start_height, false, "chain height expected to be higher than start block"); blocks.reserve(std::min(std::min(max_block_count, (size_t)10000), (size_t)(total_height - start_height))); CHECK_AND_ASSERT_MES(m_db->get_blocks_from(start_height, 3, max_block_count, max_tx_count, FIND_BLOCKCHAIN_SUPPLEMENT_MAX_SIZE, blocks, pruned, get_miner_tx_hash), false, "Error getting blocks"); @@ -3185,6 +3488,30 @@ bool Blockchain::check_tx_outputs(const transaction& tx, tx_verification_context } } + // from v17, allow FCMP++ + if (hf_version < HF_VERSION_FCMP_PLUS_PLUS) { + if (tx.version >= 2) { + const bool is_fcmp_pp = tx.rct_signatures.type == rct::RCTTypeFcmpPlusPlus; + if (is_fcmp_pp || !tx.rct_signatures.p.fcmp_pp.empty() || tx.rct_signatures.p.reference_block != 0 || tx.rct_signatures.p.n_tree_layers != 0) + { + MERROR("FCMP++ not allowed before v" << std::to_string(HF_VERSION_FCMP_PLUS_PLUS)); + tvc.m_invalid_output = true; + return false; + } + } + } + + // from v18, allow only FCMP++ + if (hf_version > HF_VERSION_FCMP_PLUS_PLUS) { + const bool is_fcmp_pp = tx.rct_signatures.type == rct::RCTTypeFcmpPlusPlus; + if (!is_fcmp_pp) + { + MERROR("FCMP++ required after v" << std::to_string(HF_VERSION_FCMP_PLUS_PLUS)); + tvc.m_invalid_output = true; + return false; + } + } + // from v15, require view tags on outputs if (!check_output_types(tx, hf_version)) { @@ -3217,7 +3544,7 @@ std::vector Blockchain::have_tx_keyimges_as_spent(const epee::spanhas_key_images(key_imgs); } //------------------------------------------------------------------ -bool Blockchain::expand_transaction_2(transaction &tx, const crypto::hash &tx_prefix_hash, const std::vector> &pubkeys) +bool Blockchain::expand_transaction_2(transaction &tx, const crypto::hash &tx_prefix_hash, const std::vector> &pubkeys, const fcmp_pp::TreeRootShared &tree_root) { PERF_TIMER(expand_transaction_2); CHECK_AND_ASSERT_MES(tx.version == 2, false, "Transaction version is not 2"); @@ -3256,6 +3583,11 @@ bool Blockchain::expand_transaction_2(transaction &tx, const crypto::hash &tx_pr } } } + else if (rv.type == rct::RCTTypeFcmpPlusPlus) + { + CHECK_AND_ASSERT_MES(pubkeys.empty(), false, "non-empty pubkeys"); + CHECK_AND_ASSERT_MES(rv.mixRing.empty(), false, "non-empty mixRing"); + } else { CHECK_AND_ASSERT_MES(false, false, "Unsupported rct tx type: " + boost::lexical_cast(rv.type)); @@ -3295,6 +3627,21 @@ bool Blockchain::expand_transaction_2(transaction &tx, const crypto::hash &tx_pr } } } + else if (rv.type == rct::RCTTypeFcmpPlusPlus) + { + if (!tx.pruned) + { + CHECK_AND_ASSERT_MES(tree_root != nullptr, false, "tree_root is null"); + rv.p.fcmp_ver_helper_data.tree_root = tree_root; + rv.p.fcmp_ver_helper_data.key_images.resize(tx.vin.size()); + for (size_t n = 0; n < tx.vin.size(); ++n) + { + rv.p.fcmp_ver_helper_data.key_images[n] = boost::get(tx.vin[n]).k_image; + } + } + } + // WARNING to any future devs adding to this function: this function can be called with an already expanded tx. + // Make sure this function can handle that properly. else { CHECK_AND_ASSERT_MES(false, false, "Unsupported rct tx type: " + boost::lexical_cast(rv.type)); @@ -3338,11 +3685,33 @@ bool Blockchain::check_tx_inputs(transaction& tx, } } - // from hard fork 2, we require mixin at least 2 unless one output cannot mix with 2 others - // if one output cannot mix with 2 others, we accept at most 1 output that can mix - if (hf_version >= 2) + size_t n_unmixable = 0; + + // after FCMP++ hard fork, require all inputs have 0 mixin + if (hf_version > HF_VERSION_FCMP_PLUS_PLUS) + { + for (const auto& txin : tx.vin) + { + if (txin.type() == typeid(txin_to_key)) + { + const txin_to_key& in_to_key = boost::get(txin); + if (!in_to_key.key_offsets.empty()) + { + MERROR_VER("Tx " << get_transaction_hash(tx) << " has non-empty ring after FCMP++ fork"); + tvc.m_invalid_input = true; + return false; + } + } + } + } + else if (hf_version >= 2) { - size_t n_unmixable = 0, n_mixable = 0; + // At HF_VERSION_FCMP_PLUS_PLUS, temporarily allow either 0 ring size or + // 16 to allow the transition to FCMP++. + // from hard fork 2 to HF_VERSION_FCMP_PLUS_PLUS, we require mixin at least + // 2 unless one output cannot mix with 2 others if one output cannot mix + // with 2 others, we accept at most 1 output that can mix + size_t n_mixable = 0; size_t min_actual_mixin = std::numeric_limits::max(); size_t max_actual_mixin = 0; const size_t min_mixin = hf_version >= HF_VERSION_MIN_MIXIN_15 ? 15 : hf_version >= HF_VERSION_MIN_MIXIN_10 ? 10 : hf_version >= HF_VERSION_MIN_MIXIN_6 ? 6 : hf_version >= HF_VERSION_MIN_MIXIN_4 ? 4 : 2; @@ -3352,6 +3721,12 @@ bool Blockchain::check_tx_inputs(transaction& tx, if (txin.type() == typeid(txin_to_key)) { const txin_to_key& in_to_key = boost::get(txin); + if (in_to_key.key_offsets.empty()) + { + min_actual_mixin = 0; + continue; + } + if (in_to_key.amount == 0) { // always consider rct inputs mixable. Even if there's not enough rct @@ -3388,11 +3763,16 @@ bool Blockchain::check_tx_inputs(transaction& tx, } } - // The only circumstance where ring sizes less than expected are - // allowed is when spending unmixable non-RCT outputs in the chain. - // Caveat: at HF_VERSION_MIN_MIXIN_15, temporarily allow ring sizes + // Before HF_VERSION_FCMP_PLUS_PLUS, the only circumstance where ring sizes + // less than expected are allowed is when spending unmixable non-RCT outputs + // in the chain. + // At HF_VERSION_MIN_MIXIN_15, temporarily allow ring sizes // of 11 to allow a grace period in the transition to larger ring size. - if (min_actual_mixin < min_mixin && !(hf_version == HF_VERSION_MIN_MIXIN_15 && min_actual_mixin == 10)) + if (hf_version >= HF_VERSION_FCMP_PLUS_PLUS && min_actual_mixin == 0 && max_actual_mixin == 0) + { + // 0 ring size is allowed at HF_VERSION_FCMP_PLUS_PLUS + } + else if (min_actual_mixin < min_mixin && !(hf_version == HF_VERSION_MIN_MIXIN_15 && min_actual_mixin == 10)) { if (n_unmixable == 0) { @@ -3416,22 +3796,22 @@ bool Blockchain::check_tx_inputs(transaction& tx, tvc.m_low_mixin = true; return false; } + } - // min/max tx version based on HF, and we accept v1 txes if having a non mixable - const size_t max_tx_version = (hf_version <= 3) ? 1 : 2; - if (tx.version > max_tx_version) - { - MERROR_VER("transaction version " << (unsigned)tx.version << " is higher than max accepted version " << max_tx_version); - tvc.m_verifivation_failed = true; - return false; - } - const size_t min_tx_version = (n_unmixable > 0 ? 1 : (hf_version >= HF_VERSION_ENFORCE_RCT) ? 2 : 1); - if (tx.version < min_tx_version) - { - MERROR_VER("transaction version " << (unsigned)tx.version << " is lower than min accepted version " << min_tx_version); - tvc.m_verifivation_failed = true; - return false; - } + // min/max tx version based on HF, and we accept v1 txes if having a non mixable + const size_t max_tx_version = get_maximum_transaction_version(hf_version); + if (tx.version > max_tx_version) + { + MERROR_VER("transaction version " << (unsigned)tx.version << " is higher than max accepted version " << max_tx_version); + tvc.m_verifivation_failed = true; + return false; + } + const size_t min_tx_version = get_minimum_transaction_version(hf_version, n_unmixable > 0); + if (tx.version < min_tx_version) + { + MERROR_VER("transaction version " << (unsigned)tx.version << " is lower than min accepted version " << min_tx_version); + tvc.m_verifivation_failed = true; + return false; } // from v7, sorted ins @@ -3454,7 +3834,12 @@ bool Blockchain::check_tx_inputs(transaction& tx, } } - std::vector> pubkeys(tx.vin.size()); + const bool uses_ring_signatures = tx.version == 1 + || (tx.version == 2 && tx.rct_signatures.type <= rct::RCTTypeBulletproofPlus); + + std::vector> pubkeys; + if (uses_ring_signatures) + pubkeys.reserve(tx.vin.size()); uint64_t max_used_block_height = 0; if (!pmax_used_block_height) @@ -3466,9 +3851,6 @@ bool Blockchain::check_tx_inputs(transaction& tx, CHECK_AND_ASSERT_MES(txin.type() == typeid(txin_to_key), false, "wrong type id in tx input at Blockchain::check_tx_inputs"); const txin_to_key& in_to_key = boost::get(txin); - // make sure tx output has key offset(s) (is signed to be used) - CHECK_AND_ASSERT_MES(in_to_key.key_offsets.size(), false, "empty in_to_key.key_offsets in transaction with id " << get_transaction_hash(tx)); - if(have_tx_keyimg_as_spent(in_to_key.k_image)) { MERROR_VER("Key image already spent in blockchain: " << epee::string_tools::pod_to_hex(in_to_key.k_image)); @@ -3476,15 +3858,32 @@ bool Blockchain::check_tx_inputs(transaction& tx, return false; } + if (rct::is_rct_fcmp(tx.rct_signatures.type)) + { + // All FCMP tx inputs should have 0 amount + CHECK_AND_ASSERT_MES(in_to_key.amount == 0, false, "non-0 amount on FCMP tx input in transaction with id " << get_transaction_hash(tx)); + + // No need to check ring signature members for FCMP txs + CHECK_AND_ASSERT_MES(in_to_key.key_offsets.empty(), false, "non-empty in_to_key.key_offsets in transaction with id " << get_transaction_hash(tx)); + // IMPORTANT: continue so that key image spend check still executes for all key images + continue; + } + + // The rest of this function concerns ring signature validation if (tx.version == 1) { // basically, make sure number of inputs == number of signatures CHECK_AND_ASSERT_MES(sig_index < tx.signatures.size(), false, "wrong transaction: not signature entry for input with index= " << sig_index); } + // make sure tx output has key offset(s) (is signed to be used) + CHECK_AND_ASSERT_MES(in_to_key.key_offsets.size(), false, "empty in_to_key.key_offsets in transaction with id " << get_transaction_hash(tx)); + // make sure that output being spent matches up correctly with the // signature spending it. - if (!check_tx_input(tx.version, in_to_key, tx_prefix_hash, tx.version == 1 ? tx.signatures[sig_index] : std::vector(), tx.rct_signatures, pubkeys[sig_index], pmax_used_block_height, hf_version)) + if (!check_tx_input(tx.version, in_to_key, tx_prefix_hash, + tx.version == 1 ? tx.signatures[sig_index] : std::vector(), tx.rct_signatures, + pubkeys.emplace_back(), pmax_used_block_height, hf_version)) { MERROR_VER("Failed to check ring signature for tx " << get_transaction_hash(tx) << " vin key with k_image: " << in_to_key.k_image << " sig_index: " << sig_index); if (pmax_used_block_height) // a default value of NULL is used when called from Blockchain::handle_block_to_main_chain() @@ -3505,13 +3904,23 @@ bool Blockchain::check_tx_inputs(transaction& tx, false, "Transaction spends at least one output which is too young"); } + // Read the db for the tree root for FCMP txs + crypto::ec_point ref_tree_root{}; + if (rct::is_rct_fcmp(tx.rct_signatures.type) && !get_fcmp_tx_tree_root(m_db, tx, ref_tree_root)) + { + // We might not be synced yet and an honest synced peer may have sent us the tx, so we make this a no-drop-offense + tvc.m_no_drop_offense = true; + return false; + } + const crypto::hash txid = get_transaction_hash(tx); // Try skipping verification if input verification ID matches a previously valid ID crypto::hash calculated_input_verification_id = crypto::null_hash; if (valid_input_verification_id_inout != crypto::null_hash) { - calculated_input_verification_id = make_input_verification_id(get_transaction_hash(tx), pubkeys); + calculated_input_verification_id = make_input_verification_id(tx, pubkeys, ref_tree_root); + if (calculated_input_verification_id == valid_input_verification_id_inout) { MDEBUG("Valid verID hit for tx " << txid << ", skipping input verification..."); @@ -3522,47 +3931,77 @@ bool Blockchain::check_tx_inputs(transaction& tx, MDEBUG("Previously valid verID for tx " << txid << " does not match current. Perhaps there was a reorg? " "Continuing to input verification even though this is not likely to succeed..."); } + valid_input_verification_id_inout = crypto::null_hash; } else { MDEBUG("No previously valid verID provided for tx " << txid << ", continuing to input verification as normal..."); } - // Verify ring signature input proofs - valid_input_verification_id_inout = crypto::null_hash; - if (!ver_input_proofs_rings(tx, pubkeys)) + // Verify input proofs + if (uses_ring_signatures) + { + // Ring signatures + if (!ver_input_proofs_rings(tx, pubkeys)) + { + MERROR_VER("Failed to verify input ring signatures for tx " << txid); + return false; + } + } + else if (rct::is_rct_fcmp(tx.rct_signatures.type)) + { + // FCMPs + if (!ver_input_proofs_fcmps(tx, ref_tree_root)) + { + MERROR_VER("Failed to verify input FCMP++ signatures for tx " << txid); + return false; + } + } + else { - MERROR_VER("Failed to verify input ring signatures for tx " << txid); + MERROR_VER("Unrecognized input lookup type for tx " << txid); return false; } // At this point, we've succeeded at input verification, so set `valid_input_verification_id_inout` valid_input_verification_id_inout = (calculated_input_verification_id == crypto::null_hash) - ? make_input_verification_id(get_transaction_hash(tx), pubkeys) + ? make_input_verification_id(tx, pubkeys, ref_tree_root) : calculated_input_verification_id; MDEBUG("Input verification for tx " << txid << " succeeded. Setting verID to " << valid_input_verification_id_inout); return true; } - //------------------------------------------------------------------ -uint64_t Blockchain::get_dynamic_base_fee(uint64_t block_reward, size_t median_block_weight) +uint64_t Blockchain::get_reference_tx_weight(uint8_t hf_version) { - constexpr uint64_t min_block_weight = CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5; + if (hf_version >= HF_VERSION_2026_SCALING) + return DYNAMIC_FEE_REFERENCE_TRANSACTION_WEIGHT_V17; + else + return DYNAMIC_FEE_REFERENCE_TRANSACTION_WEIGHT_V8; +} +//------------------------------------------------------------------ +uint64_t Blockchain::get_dynamic_base_fee(const uint64_t block_reward, + size_t median_block_weight, + const uint8_t hf_version) +{ + const uint64_t min_block_weight = (hf_version < HF_VERSION_2026_SCALING) + ? CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5 : get_min_block_weight(hf_version); if (median_block_weight < min_block_weight) median_block_weight = min_block_weight; uint64_t hi, lo; { - lo = mul128(block_reward, DYNAMIC_FEE_REFERENCE_TRANSACTION_WEIGHT, &hi); + const uint64_t reference_tx_weight = get_reference_tx_weight(hf_version); + lo = mul128(block_reward, reference_tx_weight, &hi); div128_64(hi, lo, median_block_weight, &hi, &lo, NULL, NULL); { - // min_fee_per_byte = round_up( 0.95 * block_reward * ref_weight / (fee_median^2) ) + // min_fee_per_byte = round_up( block_reward * ref_weight / (fee_median^2) ) [REDUCED BY 5% before v17] // note: since hardfork HF_VERSION_2021_SCALING, fee_median (a.k.a. median_block_weight) equals effective long term median div128_64(hi, lo, median_block_weight, &hi, &lo, NULL, NULL); assert(hi == 0); - lo -= lo / 20; + if (hf_version < HF_VERSION_2026_SCALING) + lo -= lo / 20; return lo == 0 ? 1 : lo; } } @@ -3587,7 +4026,7 @@ bool Blockchain::check_fee(size_t tx_weight, uint64_t fee) const uint64_t needed_fee; { const uint64_t fee_per_byte = get_dynamic_base_fee(base_reward, - std::min(median, m_long_term_effective_median_block_weight)); + std::min(median, m_long_term_effective_median_block_weight), version); MDEBUG("Using " << print_money(fee_per_byte) << "/byte fee"); needed_fee = tx_weight * fee_per_byte; // quantize fee up to 8 decimals @@ -3612,26 +4051,26 @@ void Blockchain::get_dynamic_base_fee_estimate_2021_scaling(uint64_t base_reward const uint64_t Mfw = std::min(Mnw, Mlw); // 3 kB divided by something ? It's going to be either 0 or *very* quantized, so fold it into integer steps below - //const uint64_t Brlw = DYNAMIC_FEE_REFERENCE_TRANSACTION_WEIGHT / Mfw; + //const uint64_t Brlw = DYNAMIC_FEE_REFERENCE_TRANSACTION_WEIGHT_V10 / Mfw; // constant.... equal to 0, unless floating point, so fold it into integer steps below - //const uint64_t Br = DYNAMIC_FEE_REFERENCE_TRANSACTION_WEIGHT / CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5 + //const uint64_t Br = DYNAMIC_FEE_REFERENCE_TRANSACTION_WEIGHT_V10 / CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5 //const uint64_t Fl = base_reward * Brlw / Mfw; fold Brlw from above - const uint64_t Fl = base_reward * /*Brlw*/ DYNAMIC_FEE_REFERENCE_TRANSACTION_WEIGHT / (Mfw * Mfw); + const uint64_t Fl = base_reward * /*Brlw*/ DYNAMIC_FEE_REFERENCE_TRANSACTION_WEIGHT_V8 / (Mfw * Mfw); // fold Fl into this for better precision (and to match the test cases in the PDF) // const uint64_t Fn = 4 * Fl; - const uint64_t Fn = 4 * base_reward * /*Brlw*/ DYNAMIC_FEE_REFERENCE_TRANSACTION_WEIGHT / (Mfw * Mfw); + const uint64_t Fn = 4 * base_reward * /*Brlw*/ DYNAMIC_FEE_REFERENCE_TRANSACTION_WEIGHT_V8 / (Mfw * Mfw); // const uint64_t Fm = 16 * base_reward * Br / Mfw; fold Br from above - const uint64_t Fm = 16 * base_reward * DYNAMIC_FEE_REFERENCE_TRANSACTION_WEIGHT / (CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5 * Mfw); + const uint64_t Fm = 16 * base_reward * DYNAMIC_FEE_REFERENCE_TRANSACTION_WEIGHT_V8 / (CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5 * Mfw); // const uint64_t Fp = 2 * base_reward / Mnw; // fold Br from above, move 4Fm in the max to decrease quantization effect - //const uint64_t Fh = 4 * Fm * std::max(1, Mfw / (32 * DYNAMIC_FEE_REFERENCE_TRANSACTION_WEIGHT * Mnw / CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5)); - const uint64_t Fh = std::max(4 * Fm, 4 * Fm * Mfw / (32 * DYNAMIC_FEE_REFERENCE_TRANSACTION_WEIGHT * Mnw / CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5)); + //const uint64_t Fh = 4 * Fm * std::max(1, Mfw / (32 * DYNAMIC_FEE_REFERENCE_TRANSACTION_WEIGHT_V8 * Mnw / CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5)); + const uint64_t Fh = std::max(4 * Fm, 4 * Fm * Mfw / (32 * DYNAMIC_FEE_REFERENCE_TRANSACTION_WEIGHT_V8 * Mnw / CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5)); fees.resize(4); fees[0] = cryptonote::round_money_up(Fl, CRYPTONOTE_SCALING_2021_FEE_ROUNDING_PLACES); @@ -3640,24 +4079,128 @@ void Blockchain::get_dynamic_base_fee_estimate_2021_scaling(uint64_t base_reward fees[3] = cryptonote::round_money_up(Fh, CRYPTONOTE_SCALING_2021_FEE_ROUNDING_PLACES); } -void Blockchain::get_dynamic_base_fee_estimate_2021_scaling(uint64_t grace_blocks, std::vector &fees) const +void Blockchain::get_dynamic_base_fee_estimate_2026_scaling(const uint64_t base_reward, const uint64_t Mlw, + std::vector &fees) +{ + fees.clear(); + + const uint64_t Mfw = std::max(Mlw, CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V17); + + // f_X + std::uint64_t max_marginal_penalty_rate{}; + { + // Maximum penalty per byte for a tx paying for itself at the edge of B=1 + // (AKA M_B = 2*M_N). Equals slope of penalty at B=(1-B_RLW) + // = 2 R_Base / M_FW = 2 (M_FW / T_R) f_L + boost::multiprecision::uint128_t fee = base_reward; + fee *= 2; + fee /= Mfw; + max_marginal_penalty_rate = boost::numeric_cast(fee); + } + + std::uint64_t f_l{}; + { + // f_L = R_Base * T_R / M_FW^2 + boost::multiprecision::uint128_t fee = DYNAMIC_FEE_REFERENCE_TRANSACTION_WEIGHT_V17; + fee *= base_reward; + fee /= Mfw; + fee /= Mfw; + f_l = boost::numeric_cast(fee); + } + + static constexpr std::size_t n_fee_levels = 5; + static constexpr std::uint64_t fee_multiplier_ratio = 4; + std::uint64_t fee_multipler = 1; + + // for each fee level... + fees.reserve(n_fee_levels); + for (std::size_t i = 0; i < n_fee_levels; ++i) + { + // fee/byte = fee_multipler * f_L + // OR max_marginal_penalty_rate iff last fee level, whichever is greater + boost::multiprecision::uint128_t fee = f_l; + fee *= fee_multipler; + + const bool is_last_level = (i + 1) == n_fee_levels; + const std::uint64_t fee64 = is_last_level ? max_marginal_penalty_rate : boost::numeric_cast(fee); + assert(fee64 >= get_dynamic_base_fee(base_reward, Mfw, HF_VERSION_2026_SCALING)); + fees.push_back(cryptonote::round_money_up(fee64, CRYPTONOTE_SCALING_2021_FEE_ROUNDING_PLACES)); + + fee_multipler *= fee_multiplier_ratio; + } + + // debug log fee levels + if (el::Loggers::allowed(el::Level::Debug, MONERO_DEFAULT_LOG_CATEGORY)) + { + // skip debug log and return if parameters were the same last print + static boost::mutex dpm; + { + const boost::lock_guard dplg(dpm); + static std::uint64_t last_base_reward = 0; + static std::uint64_t last_Mfw = 0; + if (Mfw == last_Mfw && base_reward == last_base_reward) + return; + last_base_reward = base_reward; + last_Mfw = Mfw; + } + + std::stringstream ss; + ss << "Fee/byte estimates, type 2026, R_Base = " << cryptonote::print_money(base_reward); + ss << ", M_FW = " << Mfw << ": ["; + bool first = true; + for (const std::uint64_t fee : fees) + { + if (!first) + ss << ", "; + ss << cryptonote::print_money(fee); + first = false; + } + ss << "]"; + MDEBUG(ss.str()); + } +} + +void Blockchain::get_dynamic_base_fee_estimate(uint64_t grace_blocks, std::vector &fees) const { const uint8_t version = get_current_hard_fork_version(); const uint64_t db_height = m_db->height(); - CHECK_AND_ASSERT_THROW_MES(grace_blocks <= CRYPTONOTE_REWARD_BLOCKS_WINDOW, "Grace blocks invalid In 2021 fee scaling estimate."); + if (version < HF_VERSION_2026_SCALING) + { + CHECK_AND_ASSERT_THROW_MES(grace_blocks <= CRYPTONOTE_REWARD_BLOCKS_WINDOW, + "Grace blocks invalid In 2021 fee scaling estimate."); + } + else if (grace_blocks != 1000 /*FEE_ESTIMATE_GRACE_BLOCKS_2026*/) + { + MWARNING("Grace blocks not equal to expected 1000 in dynamic base fee estimate, possible wallet fingerprint."); + } + + const uint64_t already_generated_coins = db_height ? m_db->get_block_already_generated_coins(db_height - 1) : 0; + uint64_t base_reward; + if (!get_block_reward(m_current_block_cumul_weight_limit / 2, 1, already_generated_coins, base_reward, version)) + { + MERROR("Failed to determine block reward, using placeholder " << print_money(BLOCK_REWARD_OVERESTIMATE) << " as a high bound"); + base_reward = BLOCK_REWARD_OVERESTIMATE; + } + + epee::misc_utils::rolling_median_t rm = m_long_term_block_weights_cache_rolling_median; + for (size_t i = 0; i < grace_blocks; ++i) + rm.insert(0); + const uint64_t min_block_weight = version < HF_VERSION_2026_SCALING + ? CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5 : get_min_block_weight(version); + const uint64_t Mlw_penalty_free_zone_for_wallet = std::max(rm.size() == 0 ? 0 : rm.median(), min_block_weight); + + if (version >= HF_VERSION_2026_SCALING) + { + return get_dynamic_base_fee_estimate_2026_scaling(base_reward, Mlw_penalty_free_zone_for_wallet, fees); + } // we want Mlw = median of max((min(Mbw, 1.7 * Ml), Zm), Ml / 1.7) // Mbw: block weight for the last 99990 blocks, 0 for the next 10 // Ml: penalty free zone (dynamic), aka long_term_median, aka median of max((min(Mb, 1.7 * Ml), Zm), Ml / 1.7) // Zm: 300000 (minimum penalty free zone) // - // So we copy the current rolling median state, add 10 (grace_blocks) zeroes to it, and get back Mlw - - epee::misc_utils::rolling_median_t rm = m_long_term_block_weights_cache_rolling_median; - for (size_t i = 0; i < grace_blocks; ++i) - rm.insert(0); - const uint64_t Mlw_penalty_free_zone_for_wallet = std::max(rm.size() == 0 ? 0 : rm.median(), CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5); + // So we copy the current rolling median state, add `grace_blocks` zeroes to it, and get back Mlw // Msw: median over [100 - grace blocks] past + [grace blocks] future blocks std::vector weights; @@ -3669,14 +4212,6 @@ void Blockchain::get_dynamic_base_fee_estimate_2021_scaling(uint64_t grace_block const uint64_t Mnw = std::min(Msw_effective_short_term_median, 50 * Mlw_penalty_free_zone_for_wallet); - uint64_t already_generated_coins = db_height ? m_db->get_block_already_generated_coins(db_height - 1) : 0; - uint64_t base_reward; - if (!get_block_reward(m_current_block_cumul_weight_limit / 2, 1, already_generated_coins, base_reward, version)) - { - MERROR("Failed to determine block reward, using placeholder " << print_money(BLOCK_REWARD_OVERESTIMATE) << " as a high bound"); - base_reward = BLOCK_REWARD_OVERESTIMATE; - } - get_dynamic_base_fee_estimate_2021_scaling(base_reward, Mnw, Mlw_penalty_free_zone_for_wallet, fees); } //------------------------------------------------------------------ @@ -4027,6 +4562,20 @@ bool Blockchain::handle_block_to_main_chain(const block& bl, const crypto::hash& TIME_MEASURE_START(t3); + // Make sure the block uses the correct FCMP++ tree root and n tree layers + if (hf_version >= HF_VERSION_FCMP_PLUS_PLUS) + { + crypto::ec_point fcmp_pp_tree_root; + const uint64_t expected_tree_tip_block_idx = cryptonote::get_default_last_locked_block_index(blockchain_height - 1); + const uint8_t n_tree_layers = m_db->get_tree_root_at_blk_idx(expected_tree_tip_block_idx, fcmp_pp_tree_root); + if (bl.fcmp_pp_n_tree_layers != n_tree_layers || bl.fcmp_pp_tree_root != fcmp_pp_tree_root) + { + MERROR_VER("Block with id: " << id << " used incorrect FCMP++ n tree layers or tree root"); + bvc.m_verifivation_failed = true; + goto leave; + } + } + // sanity check basic miner tx properties; if(!prevalidate_miner_transaction(bl, blockchain_height, hf_version)) { @@ -4035,14 +4584,22 @@ bool Blockchain::handle_block_to_main_chain(const block& bl, const crypto::hash& goto leave; } + // Start collecting transparent amount commitments. We'll need them for the following: + // 1. To verify the commitments don't have torsion (is this even possible for a transparent amount?) + // 2. To add each v2 coinbase output to the db + // 3. To add the output to the curve tree + std::unordered_map transparent_amount_commitments; + // verify all non-input consensus rules for txs inside the pool supplement (if not inside checkpoint zone) #if defined(PER_BLOCK_CHECKPOINT) if (!fast_check) #endif { + collect_transparent_amount_commitments(extra_block_txs.txs_by_txid, transparent_amount_commitments); + tx_verification_context tvc{}; // If fail non-input consensus rule checking... - if (!ver_non_input_consensus(extra_block_txs, tvc, hf_version)) + if (!ver_non_input_consensus(extra_block_txs, transparent_amount_commitments, tvc, hf_version)) { MERROR_VER("Pool supplement provided for block with id: " << id << " failed to pass validation"); bvc.m_verifivation_failed = true; @@ -4050,6 +4607,20 @@ bool Blockchain::handle_block_to_main_chain(const block& bl, const crypto::hash& } } + // Batch verify FCMP++'s, they'll be cached + std::unordered_map batched_fcmp_valid_input_verification_id_by_txid; +#if defined(PER_BLOCK_CHECKPOINT) + if (!fast_check) +#endif + { + if (!batch_verify_fcmp_pp_txs(m_db, extra_block_txs, batched_fcmp_valid_input_verification_id_by_txid)) + { + MERROR_VER("Failed to batch verify FCMP++ txs"); + bvc.m_verifivation_failed = true; + goto leave; + } + } + size_t coinbase_weight = get_transaction_weight(bl.miner_tx); size_t cumulative_block_weight = coinbase_weight; @@ -4171,6 +4742,9 @@ bool Blockchain::handle_block_to_main_chain(const block& bl, const crypto::hash& extra_block_txs.txs_by_txid.erase(extra_txs_it); txpool_events.emplace_back(txpool_event{tx, tx_id, txblob.size(), tx_weight, true}); find_tx_failure = false; + const auto ver_id_it = batched_fcmp_valid_input_verification_id_by_txid.find(tx_id); + if (ver_id_it != batched_fcmp_valid_input_verification_id_by_txid.cend()) + valid_input_verification_id = ver_id_it->second; } } @@ -4256,10 +4830,18 @@ bool Blockchain::handle_block_to_main_chain(const block& bl, const crypto::hash& cumulative_block_weight = m_blocks_hash_check[blockchain_height].second; } + + TIME_MEASURE_START(tac); + + // Collect all remaining transparent amount commitments + const auto tx_refs = collect_transparent_amount_commitments(bl.miner_tx, txs, transparent_amount_commitments); + + TIME_MEASURE_FINISH(tac); + TIME_MEASURE_START(vmt); uint64_t base_reward = 0; uint64_t already_generated_coins = blockchain_height ? m_db->get_block_already_generated_coins(blockchain_height - 1) : 0; - if(!validate_miner_transaction(bl, cumulative_block_weight, fee_summary, base_reward, already_generated_coins, bvc.m_partial_block_reward, m_hardfork->get_current_version())) + if(!validate_miner_transaction(bl, cumulative_block_weight, fee_summary, base_reward, already_generated_coins, bvc.m_partial_block_reward, m_hardfork->get_current_version(), transparent_amount_commitments)) { MERROR_VER("Block with id: " << id << " has incorrect miner transaction"); bvc.m_verifivation_failed = true; @@ -4286,6 +4868,8 @@ bool Blockchain::handle_block_to_main_chain(const block& bl, const crypto::hash& if(precomputed) block_processing_time += m_fake_pow_calc_time; + const uint64_t first_unified_id = m_db->num_outputs(); + rtxn_guard.stop(); TIME_MEASURE_START(addblock); uint64_t new_height = 0; @@ -4295,7 +4879,7 @@ bool Blockchain::handle_block_to_main_chain(const block& bl, const crypto::hash& { uint64_t long_term_block_weight = get_next_long_term_block_weight(block_weight); cryptonote::blobdata bd = cryptonote::block_to_blob(bl); - new_height = m_db->add_block(std::make_pair(std::move(bl), std::move(bd)), block_weight, long_term_block_weight, cumulative_difficulty, already_generated_coins, txs); + new_height = m_db->add_block(std::make_pair(std::move(bl), std::move(bd)), block_weight, long_term_block_weight, cumulative_difficulty, already_generated_coins, txs, transparent_amount_commitments); } catch (const KEY_IMAGE_EXISTS& e) { @@ -4320,6 +4904,13 @@ bool Blockchain::handle_block_to_main_chain(const block& bl, const crypto::hash& LOG_ERROR("Blocks that failed verification should not reach here"); } + if (new_height == 0) + { + LOG_ERROR("handle_block_to_main_chain: unexpected new_height == 0"); + bvc.m_verifivation_failed = true; + return false; + } + TIME_MEASURE_FINISH(addblock); // do this after updating the hard fork state since the weight limit may change due to fork @@ -4330,6 +4921,18 @@ bool Blockchain::handle_block_to_main_chain(const block& bl, const crypto::hash& return false; } + TIME_MEASURE_START(advance_tree); + + try { handle_fcmp_tree(m_db, new_height-1, first_unified_id, tx_refs, transparent_amount_commitments); } + catch (const std::exception& e) + { + LOG_ERROR("Failed to advance tree at block with hash: " << id << ", what = " << e.what()); + bvc.m_verifivation_failed = true; + return false; + } + + TIME_MEASURE_FINISH(advance_tree); + MINFO("+++++ BLOCK SUCCESSFULLY ADDED" << std::endl << "id:\t" << id << std::endl << "PoW:\t" << proof_of_work << std::endl << "HEIGHT " << new_height-1 << ", difficulty:\t" << current_diffic << std::endl << "block reward: " << print_money(fee_summary + base_reward) << "(" << print_money(base_reward) << " + " << print_money(fee_summary) << "), coinbase_weight: " << coinbase_weight << ", cumulative weight: " << cumulative_block_weight << ", " << block_processing_time << "(" << target_calculating_time << "/" << longhash_calculating_time << ")ms"); if(m_show_time_stats) { @@ -4337,7 +4940,8 @@ bool Blockchain::handle_block_to_main_chain(const block& bl, const crypto::hash& << cumulative_block_weight << " p/t: " << block_processing_time << " (" << target_calculating_time << "/" << longhash_calculating_time << "/" << t1 << "/" << t2 << "/" << t3 << "/" << t_exists << "/" << t_pool - << "/" << t_checktx << "/" << t_dblspnd << "/" << vmt << "/" << addblock << ")ms"); + << "/" << t_checktx << "/" << t_dblspnd << "/" << tac << "/" << vmt + << "/" << addblock << "/" << advance_tree << ")ms"); } bvc.m_added_to_main_chain = true; @@ -4406,7 +5010,6 @@ bool Blockchain::check_blockchain_pruning() return m_db->check_pruning(); } //------------------------------------------------------------------ -// returns min(Mb, 1.7*Ml) as per https://github.com/ArticMine/Monero-Documents/blob/master/MoneroScaling2021-02.pdf from HF_VERSION_LONG_TERM_BLOCK_WEIGHT uint64_t Blockchain::get_next_long_term_block_weight(uint64_t block_weight) const { PERF_TIMER(get_next_long_term_block_weight); @@ -4418,11 +5021,17 @@ uint64_t Blockchain::get_next_long_term_block_weight(uint64_t block_weight) cons if (hf_version < HF_VERSION_LONG_TERM_BLOCK_WEIGHT) return block_weight; - uint64_t long_term_median = get_long_term_block_weight_median(db_height - nblocks, nblocks); - uint64_t long_term_effective_median_block_weight = std::max(CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5, long_term_median); + const uint64_t long_term_median = get_long_term_block_weight_median(db_height - nblocks, nblocks); + const uint64_t long_term_effective_median_block_weight = std::max(get_min_block_weight(hf_version), long_term_median); - uint64_t short_term_constraint; - if (hf_version >= HF_VERSION_2021_SCALING) + uint64_t short_term_constraint{}; + if (hf_version >= HF_VERSION_2026_SCALING) + { + // long_term_block_weight = block_weight bounded to range [long-term-median/1.2, long-term-median*1.2] + block_weight = std::max(block_weight, long_term_effective_median_block_weight * 5 / 6); + short_term_constraint = long_term_effective_median_block_weight * 6 / 5; + } + else if (hf_version >= HF_VERSION_2021_SCALING) { // long_term_block_weight = block_weight bounded to range [long-term-median/1.7, long-term-median*1.7] block_weight = std::max(block_weight, long_term_effective_median_block_weight * 10 / 17); @@ -4433,7 +5042,7 @@ uint64_t Blockchain::get_next_long_term_block_weight(uint64_t block_weight) cons // long_term_block_weight = block_weight bounded to range [0, long-term-median*1.4] short_term_constraint = long_term_effective_median_block_weight + long_term_effective_median_block_weight * 2 / 5; } - uint64_t long_term_block_weight = std::min(block_weight, short_term_constraint); + const uint64_t long_term_block_weight = std::min(block_weight, short_term_constraint); return long_term_block_weight; } @@ -4447,7 +5056,7 @@ bool Blockchain::update_next_cumulative_weight_limit(uint64_t *long_term_effecti // when we reach this, the last hf version is not yet written to the db const uint64_t db_height = m_db->height(); const uint8_t hf_version = get_current_hard_fork_version(); - uint64_t full_reward_zone = get_min_block_weight(hf_version); + const uint64_t full_reward_zone = get_min_block_weight(hf_version); if (hf_version < HF_VERSION_LONG_TERM_BLOCK_WEIGHT) { @@ -4460,24 +5069,30 @@ bool Blockchain::update_next_cumulative_weight_limit(uint64_t *long_term_effecti const uint64_t nblocks = std::min(m_long_term_block_weights_window, db_height); const uint64_t long_term_median = get_long_term_block_weight_median(db_height - nblocks, nblocks); - m_long_term_effective_median_block_weight = std::max(CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5, long_term_median); + m_long_term_effective_median_block_weight = std::max(full_reward_zone, long_term_median); std::vector weights; get_last_n_blocks_weights(weights, CRYPTONOTE_REWARD_BLOCKS_WINDOW); uint64_t short_term_median = epee::misc_utils::median(weights); uint64_t effective_median_block_weight; - if (hf_version >= HF_VERSION_2021_SCALING) + if (hf_version >= HF_VERSION_2026_SCALING) + { + // effective median = short_term_median bounded to range [long_term_median, 8*long_term_median], but it can't be smaller than the + // minimum penalty free zone (a.k.a. 'full reward zone') + effective_median_block_weight = std::min(std::max(m_long_term_effective_median_block_weight, short_term_median), CRYPTONOTE_SHORT_TERM_BLOCK_SURGE_FACTOR_V17 * m_long_term_effective_median_block_weight); + } + else if (hf_version >= HF_VERSION_2021_SCALING) { // effective median = short_term_median bounded to range [long_term_median, 50*long_term_median], but it can't be smaller than the // minimum penalty free zone (a.k.a. 'full reward zone') - effective_median_block_weight = std::min(std::max(m_long_term_effective_median_block_weight, short_term_median), CRYPTONOTE_SHORT_TERM_BLOCK_WEIGHT_SURGE_FACTOR * m_long_term_effective_median_block_weight); + effective_median_block_weight = std::min(std::max(m_long_term_effective_median_block_weight, short_term_median), CRYPTONOTE_SHORT_TERM_BLOCK_SURGE_FACTOR_V10 * m_long_term_effective_median_block_weight); } else { // effective median = short_term_median bounded to range [0, 50*long_term_median], but it can't be smaller than the // minimum penalty free zone (a.k.a. 'full reward zone') - effective_median_block_weight = std::min(std::max(CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5, short_term_median), CRYPTONOTE_SHORT_TERM_BLOCK_WEIGHT_SURGE_FACTOR * m_long_term_effective_median_block_weight); + effective_median_block_weight = std::min(std::max(CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5, short_term_median), CRYPTONOTE_SHORT_TERM_BLOCK_SURGE_FACTOR_V10 * m_long_term_effective_median_block_weight); } m_current_block_cumul_weight_median = effective_median_block_weight; @@ -5587,6 +6202,7 @@ void Blockchain::send_miner_notifications(uint64_t height, const crypto::hash &s { if (m_miner_notifiers.empty()) return; + CHECK_AND_ASSERT_THROW_MES(height > 0, "Unexpected height == 0"); const uint8_t major_version = m_hardfork->get_ideal_version(height); const difficulty_type diff = get_difficulty_for_next_block(); @@ -5595,9 +6211,14 @@ void Blockchain::send_miner_notifications(uint64_t height, const crypto::hash &s std::vector tx_backlog; m_tx_pool.get_block_template_backlog(tx_backlog); + uint8_t fcmp_pp_n_tree_layers = 0; + crypto::ec_point fcmp_pp_tree_root{}; + if (major_version >= HF_VERSION_FCMP_PLUS_PLUS) + fcmp_pp_n_tree_layers = m_db->get_tree_root_at_blk_idx(cryptonote::get_default_last_locked_block_index(height - 1), fcmp_pp_tree_root); + for (const auto& notifier : m_miner_notifiers) { - notifier(major_version, height, prev_id, seed_hash, diff, median_weight, already_generated_coins, tx_backlog); + notifier(major_version, height, prev_id, fcmp_pp_n_tree_layers, fcmp_pp_tree_root, seed_hash, diff, median_weight, already_generated_coins, tx_backlog); } } diff --git a/src/cryptonote_core/blockchain.h b/src/cryptonote_core/blockchain.h index 663ff98361a..d1ceb09551f 100644 --- a/src/cryptonote_core/blockchain.h +++ b/src/cryptonote_core/blockchain.h @@ -72,6 +72,12 @@ namespace cryptonote class tx_memory_pool; struct test_options; + void handle_fcmp_tree(BlockchainDB *db, + uint64_t block_idx, + uint64_t first_unified_id, + const std::vector> &tx_refs, + const std::unordered_map &transparent_amount_commitments); + /** Declares ways in which the BlockchainDB backend should be told to sync * */ @@ -94,7 +100,7 @@ namespace cryptonote typedef boost::function)> TxpoolNotifyCallback; typedef boost::function /* blocks */)> BlockNotifyCallback; - typedef boost::function& /* tx_backlog */)> MinerNotifyCallback; + typedef boost::function& /* tx_backlog */)> MinerNotifyCallback; /************************************************************************/ /* */ @@ -405,6 +411,8 @@ namespace cryptonote * @param major_version current hardfork version * @param height current blockchain height * @param prev_id hash of the top block + * @param fcmp_pp_n_tree_layers number of layers in the FCMP++ curve tree + * @param fcmp_pp_tree_root FCMP++ root as of when this next block enters the chain * @param seed_hash seed hash used for RandomX initialization * @param difficulty current mining difficulty * @param median_weight current median block weight @@ -413,7 +421,7 @@ namespace cryptonote * * @return true if block template filled in successfully, else false */ - bool get_miner_data(uint8_t& major_version, uint64_t& height, crypto::hash& prev_id, crypto::hash& seed_hash, difficulty_type& difficulty, uint64_t& median_weight, uint64_t& already_generated_coins, std::vector& tx_backlog); + bool get_miner_data(uint8_t& major_version, uint64_t& height, crypto::hash& prev_id, uint8_t& fcmp_pp_n_tree_layers, crypto::ec_point& fcmp_pp_tree_root, crypto::hash& seed_hash, difficulty_type& difficulty, uint64_t& median_weight, uint64_t& already_generated_coins, std::vector& tx_backlog); /** * @brief checks if a block is known about with a given hash @@ -516,10 +524,11 @@ namespace cryptonote * @param pruned whether to return full or pruned tx blobs * @param max_block_count the max number of blocks to get * @param max_tx_count the max number of txes to get (it can get overshot by the last block's number of txes minus 1) + * @param qblock_ids_skip_common_block when using qblock_ids, indicates whether or not to include common block in response * * @return true if a block found in common or req_start_block specified, else false */ - bool find_blockchain_supplement(const uint64_t req_start_block, const std::list& qblock_ids, std::vector, std::vector > > >& blocks, uint64_t& total_height, crypto::hash& top_hash, uint64_t& start_height, bool pruned, bool get_miner_tx_hash, size_t max_block_count, size_t max_tx_count) const; + bool find_blockchain_supplement(const uint64_t req_start_block, const std::list& qblock_ids, std::vector, std::vector > > >& blocks, uint64_t& total_height, crypto::hash& top_hash, uint64_t& start_height, bool pruned, bool get_miner_tx_hash, size_t max_block_count, size_t max_tx_count, bool qblock_ids_skip_common_block = false) const; /** * @brief retrieves a set of blocks and their transactions, and possibly other transactions @@ -607,6 +616,20 @@ namespace cryptonote bool get_tx_outputs_gindexs(const crypto::hash& tx_id, std::vector& indexs) const; bool get_tx_outputs_gindexs(const crypto::hash& tx_id, size_t n_txes, std::vector>& indexs) const; + /** + * @brief returns recently created locked outputs, excluding custom timelocked outputs + * + * Returns: + * - coinbase outputs created between [end_block_idx - CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW] + * - normal outputs created between [end_block_idx - CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE] + * - the outputs are grouped by last locked block idx + * + * @param end_block_idx the terminating block (recently locked outputs created before this block) + * + * @return outputs grouped by last locked block idx + */ + fcmp_pp::OutsByLastLockedBlock get_recent_locked_outputs(uint64_t end_block_idx) const; + /** * @brief stores the blockchain * @@ -623,8 +646,11 @@ namespace cryptonote * RingCT transactions do not transmit some of their data if it * can be reconstituted by the receiver. This function expands * that implicit data. + * + * This function can potentially be called with an already + * expanded tx. */ - static bool expand_transaction_2(transaction &tx, const crypto::hash &tx_prefix_hash, const std::vector> &pubkeys); + static bool expand_transaction_2(transaction &tx, const crypto::hash &tx_prefix_hash, const std::vector> &pubkeys, const fcmp_pp::TreeRootShared &tree_root); /** * @brief validates a transaction's inputs @@ -670,6 +696,15 @@ namespace cryptonote return tools::PowerOf<10, CRYPTONOTE_DISPLAY_DECIMAL_POINT - PER_KB_FEE_QUANTIZATION_DECIMALS>::Value; } + /** + * @brief get reference tx weight, used for fee calculations + * + * @param hf_version hard fork version + * + * @return reference tx weight for block with given hard fork version + */ + static uint64_t get_reference_tx_weight(uint8_t hf_version); + /** * @brief get dynamic per kB or byte fee for a given block weight * @@ -679,10 +714,11 @@ namespace cryptonote * * @param block_reward the current block reward * @param median_block_weight the median block weight in the past window + * @param hf_version hard fork version * * @return the fee */ - static uint64_t get_dynamic_base_fee(uint64_t block_reward, size_t median_block_weight); + static uint64_t get_dynamic_base_fee(uint64_t block_reward, size_t median_block_weight, uint8_t hf_version); /** * @brief get four levels of dynamic per byte fee estimate for the next few blocks @@ -699,18 +735,32 @@ namespace cryptonote uint64_t Mlw, std::vector &fees); /** - * @brief get four levels of dynamic per byte fee estimate for the next few blocks + * @brief get five levels of dynamic per byte fee estimate for the next few blocks * * The dynamic fee is based on the block weight in a past window, and * the current block reward. It is expressed per byte, and is based on - * https://github.com/ArticMine/Monero-Documents/blob/master/MoneroScaling2021-02.pdf + * https://github.com/ArticMine/Monero-Documents/blob/master/MoneroScaling2026-02.pdf + * + * @param Mlw The median over the last 99000 and future 10 blocks of max(min(Mbw, 1.2*Ml), Zm, Ml/1.2) + * @param[out] fees fee estimate levels [Fl, Fn, Fm, Fh64, Fh256] + */ + static void get_dynamic_base_fee_estimate_2026_scaling(uint64_t base_reward, uint64_t Mlw, + std::vector &fees); + + /** + * @brief get four levels of dynamic per byte fee estimate for the next few blocks + * + * The dynamic fee is based on the block weight in a past window, and + * the current block reward. It is expressed per byte, and is based on: + * - https://github.com/ArticMine/Monero-Documents/blob/master/MoneroScaling2021-02.pdf + * - https://github.com/ArticMine/Monero-Documents/blob/master/MoneroScaling2026-02.pdf * This function calculates an estimate for a dynamic fee which will be * valid for the next grace_blocks * * @param grace_blocks number of blocks we want the fee to be valid for * @param[out] fees fee estimate levels [Fl, Fn, Fm, Fh] */ - void get_dynamic_base_fee_estimate_2021_scaling(uint64_t grace_blocks, std::vector &fees) const; + void get_dynamic_base_fee_estimate(uint64_t grace_blocks, std::vector &fees) const; /** * @brief validate a transaction's fee @@ -753,7 +803,21 @@ namespace cryptonote /** * @brief gets the long term block weight for a new block * - * @return the long term block weight + * v01-v09: M_L = M_B + * v10-v14: M_L = min(M_B, 1.4 * M^prev_L) + * v15-v16: M_L = M_B clamped to [M^prev_L / 1.7, M^prev_L * 1.7] + * v17-now: M_L = M_B clamped to [M^prev_L / 1.2, M^prev_L * 1.2] + * where: + * M_B is `block_weight`, + * M^prev_L is the median of the previous CRYPTONOTE_LONG_TERM_BLOCK_WEIGHT_WINDOW_SIZE blocks' values of M_L, + * M^prev_L is forced to always be at least Z_M, the minimum penalty free zone for the next block, + * and the fork version is determined using the ideal fork version of the next block + * + * Sources for scaling docs: + * - https://github.com/ArticMine/Monero-Documents/blob/master/MoneroScaling2021-02.pdf + * - https://github.com/ArticMine/Monero-Documents/blob/master/MoneroScaling2026-02.pdf + * + * @return the long term block weight M_L */ uint64_t get_next_long_term_block_weight(uint64_t block_weight) const; @@ -1213,7 +1277,7 @@ namespace cryptonote std::vector m_difficulties; uint64_t m_timestamps_and_difficulties_height; bool m_reset_timestamps_and_difficulties_height; - uint64_t m_long_term_block_weights_window; + uint64_t m_long_term_block_weights_window; // non-0 iff using "custom" long-term window uint64_t m_long_term_effective_median_block_weight; mutable crypto::hash m_long_term_block_weights_cache_tip_hash; mutable epee::misc_utils::rolling_median_t m_long_term_block_weights_cache_rolling_median; @@ -1477,10 +1541,11 @@ namespace cryptonote * @param already_generated_coins the amount of currency generated prior to this block * @param partial_block_reward return-by-reference true if miner accepted only partial reward * @param version hard fork version for that transaction + * @param transparent_amount_commitments pre-calculated transparent amount commitments * * @return false if anything is found wrong with the miner transaction, otherwise true */ - bool validate_miner_transaction(const block& b, size_t cumulative_block_weight, uint64_t fee, uint64_t& base_reward, uint64_t already_generated_coins, bool &partial_block_reward, uint8_t version); + bool validate_miner_transaction(const block& b, size_t cumulative_block_weight, uint64_t fee, uint64_t& base_reward, uint64_t already_generated_coins, bool &partial_block_reward, uint8_t version, const std::unordered_map &transparent_amount_commitments); /** * @brief reverts the blockchain to its previous state following a failed switch diff --git a/src/cryptonote_core/cryptonote_core.cpp b/src/cryptonote_core/cryptonote_core.cpp index 29648754b6a..2b31d85dd9b 100644 --- a/src/cryptonote_core/cryptonote_core.cpp +++ b/src/cryptonote_core/cryptonote_core.cpp @@ -135,7 +135,6 @@ namespace cryptonote "sync-pruned-blocks" , "Allow syncing from nodes with only pruned blocks" }; - static const command_line::arg_descriptor arg_test_drop_download = { "test-drop-download" , "For net tests: in download, discard ALL blocks instead checking/saving them (very fast)" @@ -461,6 +460,9 @@ namespace cryptonote { start_time = std::time(nullptr); + // Necessary for FCMP++ sync on Linux platforms, especially with limited memory + rct::limitMaxMemArenas(); + const bool regtest = command_line::get_arg(vm, arg_regtest_on); if (test_options != NULL || regtest) { @@ -808,7 +810,7 @@ namespace cryptonote return false; } //----------------------------------------------------------------------------------------------- - bool core::handle_incoming_tx(const blobdata& tx_blob, tx_verification_context& tvc, relay_method tx_relay, bool relayed) + bool core::handle_incoming_tx(const blobdata& tx_blob, tx_verification_context& tvc, relay_method tx_relay, bool relayed, crypto::hash& txid) { tvc = {}; @@ -825,7 +827,6 @@ namespace cryptonote } transaction tx; - crypto::hash txid; if (!parse_and_validate_tx_from_blob(tx_blob, tx, txid)) { LOG_PRINT_L1("Incoming transactions failed to parse, rejected"); @@ -1160,6 +1161,9 @@ namespace cryptonote NOTIFY_NEW_TRANSACTIONS::request public_req{}; NOTIFY_NEW_TRANSACTIONS::request private_req{}; NOTIFY_NEW_TRANSACTIONS::request stem_req{}; + std::vector public_tx_hashes{}; + std::vector private_tx_hashes{}; + std::vector stem_tx_hashes{}; for (auto& tx : txs) { switch (std::get<2>(tx)) @@ -1169,14 +1173,17 @@ namespace cryptonote break; case relay_method::local: private_req.txs.push_back(std::move(std::get<1>(tx))); + private_tx_hashes.push_back(std::move(std::get<0>(tx))); break; case relay_method::forward: stem_req.txs.push_back(std::move(std::get<1>(tx))); + stem_tx_hashes.push_back(std::move(std::get<0>(tx))); break; case relay_method::block: case relay_method::fluff: case relay_method::stem: public_req.txs.push_back(std::move(std::get<1>(tx))); + public_tx_hashes.push_back(std::move(std::get<0>(tx))); break; } } @@ -1187,11 +1194,11 @@ namespace cryptonote re-relaying public and private _should_ be acceptable here. */ const boost::uuids::uuid source = boost::uuids::nil_uuid(); if (!public_req.txs.empty()) - get_protocol()->relay_transactions(public_req, source, epee::net_utils::zone::public_, relay_method::fluff); + get_protocol()->relay_transactions(public_req, std::move(public_tx_hashes), source, epee::net_utils::zone::public_, relay_method::fluff); if (!private_req.txs.empty()) - get_protocol()->relay_transactions(private_req, source, epee::net_utils::zone::invalid, relay_method::local); + get_protocol()->relay_transactions(private_req, std::move(private_tx_hashes), source, epee::net_utils::zone::invalid, relay_method::local); if (!stem_req.txs.empty()) - get_protocol()->relay_transactions(stem_req, source, epee::net_utils::zone::public_, relay_method::stem); + get_protocol()->relay_transactions(stem_req, std::move(stem_tx_hashes), source, epee::net_utils::zone::public_, relay_method::stem); } return true; } @@ -1265,9 +1272,9 @@ namespace cryptonote return m_blockchain_storage.create_block_template(b, prev_block, adr, diffic, height, expected_reward, cumulative_weight, ex_nonce, seed_height, seed_hash); } //----------------------------------------------------------------------------------------------- - bool core::get_miner_data(uint8_t& major_version, uint64_t& height, crypto::hash& prev_id, crypto::hash& seed_hash, difficulty_type& difficulty, uint64_t& median_weight, uint64_t& already_generated_coins, std::vector& tx_backlog) + bool core::get_miner_data(uint8_t& major_version, uint64_t& height, crypto::hash& prev_id, uint8_t& fcmp_pp_n_tree_layers, crypto::ec_point& fcmp_pp_tree_root, crypto::hash& seed_hash, difficulty_type& difficulty, uint64_t& median_weight, uint64_t& already_generated_coins, std::vector& tx_backlog) { - return m_blockchain_storage.get_miner_data(major_version, height, prev_id, seed_hash, difficulty, median_weight, already_generated_coins, tx_backlog); + return m_blockchain_storage.get_miner_data(major_version, height, prev_id, fcmp_pp_n_tree_layers, fcmp_pp_tree_root, seed_hash, difficulty, median_weight, already_generated_coins, tx_backlog); } //----------------------------------------------------------------------------------------------- bool core::find_blockchain_supplement(const std::list& qblock_ids, bool clip_pruned, NOTIFY_RESPONSE_CHAIN_ENTRY::request& resp) const @@ -1275,9 +1282,9 @@ namespace cryptonote return m_blockchain_storage.find_blockchain_supplement(qblock_ids, clip_pruned, resp); } //----------------------------------------------------------------------------------------------- - bool core::find_blockchain_supplement(const uint64_t req_start_block, const std::list& qblock_ids, std::vector, std::vector > > >& blocks, uint64_t& total_height, crypto::hash& top_hash, uint64_t& start_height, bool pruned, bool get_miner_tx_hash, size_t max_block_count, size_t max_tx_count) const + bool core::find_blockchain_supplement(const uint64_t req_start_block, const std::list& qblock_ids, std::vector, std::vector > > >& blocks, uint64_t& total_height, crypto::hash& top_hash, uint64_t& start_height, bool pruned, bool get_miner_tx_hash, size_t max_block_count, size_t max_tx_count, bool qblock_ids_skip_common_block) const { - return m_blockchain_storage.find_blockchain_supplement(req_start_block, qblock_ids, blocks, total_height, top_hash, start_height, pruned, get_miner_tx_hash, max_block_count, max_tx_count); + return m_blockchain_storage.find_blockchain_supplement(req_start_block, qblock_ids, blocks, total_height, top_hash, start_height, pruned, get_miner_tx_hash, max_block_count, max_tx_count, qblock_ids_skip_common_block); } //----------------------------------------------------------------------------------------------- bool core::get_outs(const COMMAND_RPC_GET_OUTPUTS_BIN::request& req, COMMAND_RPC_GET_OUTPUTS_BIN::response& res) const @@ -1905,9 +1912,9 @@ namespace cryptonote m_blockchain_storage.flush_invalid_blocks(); } //----------------------------------------------------------------------------------------------- - bool core::get_txpool_complement(std::vector hashes, std::vector &txes) + bool core::get_txpool_complement(std::vector hashes, std::vector &inv_txes) { - return m_mempool.get_complement(std::move(hashes), txes); + return m_mempool.get_complement(std::move(hashes), inv_txes); } //----------------------------------------------------------------------------------------------- bool core::update_blockchain_pruning() diff --git a/src/cryptonote_core/cryptonote_core.h b/src/cryptonote_core/cryptonote_core.h index d57c6cc16d5..33745702ac7 100644 --- a/src/cryptonote_core/cryptonote_core.h +++ b/src/cryptonote_core/cryptonote_core.h @@ -123,10 +123,11 @@ namespace cryptonote * @param tvc metadata about the transaction's validity * @param tx_relay how the transaction was received * @param relayed whether or not the transaction was relayed to us + * @param txid return by reference * * @return true if the transaction was accepted, false otherwise */ - bool handle_incoming_tx(const blobdata& tx_blob, tx_verification_context& tvc, relay_method tx_relay, bool relayed); + bool handle_incoming_tx(const blobdata& tx_blob, tx_verification_context& tvc, relay_method tx_relay, bool relayed, crypto::hash& txid); /** * @brief handles a single incoming block @@ -243,7 +244,7 @@ namespace cryptonote * * @note see Blockchain::get_miner_data */ - bool get_miner_data(uint8_t& major_version, uint64_t& height, crypto::hash& prev_id, crypto::hash& seed_hash, difficulty_type& difficulty, uint64_t& median_weight, uint64_t& already_generated_coins, std::vector& tx_backlog); + bool get_miner_data(uint8_t& major_version, uint64_t& height, crypto::hash& prev_id, uint8_t& fcmp_pp_n_tree_layers, crypto::ec_point& fcmp_pp_tree_root, crypto::hash& seed_hash, difficulty_type& difficulty, uint64_t& median_weight, uint64_t& already_generated_coins, std::vector& tx_backlog); /** * @brief called when a transaction is relayed. @@ -590,11 +591,11 @@ namespace cryptonote bool find_blockchain_supplement(const std::list& qblock_ids, bool clip_pruned, NOTIFY_RESPONSE_CHAIN_ENTRY::request& resp) const; /** - * @copydoc Blockchain::find_blockchain_supplement(const uint64_t, const std::list&, std::vector > >&, uint64_t&, uint64_t&, size_t) const + * @copydoc Blockchain::find_blockchain_supplement(const uint64_t, const std::list&, std::vector > >&, uint64_t&, uint64_t&, size_t, bool) const * - * @note see Blockchain::find_blockchain_supplement(const uint64_t, const std::list&, std::vector > >&, uint64_t&, uint64_t&, size_t) const + * @note see Blockchain::find_blockchain_supplement(const uint64_t, const std::list&, std::vector > >&, uint64_t&, uint64_t&, size_t, bool) const */ - bool find_blockchain_supplement(const uint64_t req_start_block, const std::list& qblock_ids, std::vector, std::vector > > >& blocks, uint64_t& total_height, crypto::hash& top_hash, uint64_t& start_height, bool pruned, bool get_miner_tx_hash, size_t max_block_count, size_t max_tx_count) const; + bool find_blockchain_supplement(const uint64_t req_start_block, const std::list& qblock_ids, std::vector, std::vector > > >& blocks, uint64_t& total_height, crypto::hash& top_hash, uint64_t& start_height, bool pruned, bool get_miner_tx_hash, size_t max_block_count, size_t max_tx_count, bool qblock_ids_skip_common_block = false) const; /** * @copydoc Blockchain::get_tx_outputs_gindexs @@ -896,13 +897,13 @@ namespace cryptonote void flush_invalid_blocks(); /** - * @brief returns the set of transactions in the txpool which are not in the argument + * @brief returns the set of transaction hashes in the txpool which are not in the argument * * @param hashes hashes of transactions to exclude from the result * * @return true iff success, false otherwise */ - bool get_txpool_complement(std::vector hashes, std::vector &txes); + bool get_txpool_complement(std::vector hashes, std::vector &inv_txes); /** * @brief validates some simple properties of a transaction diff --git a/src/cryptonote_core/cryptonote_tx_utils.cpp b/src/cryptonote_core/cryptonote_tx_utils.cpp index e9a66ac5076..66beb991b0e 100644 --- a/src/cryptonote_core/cryptonote_tx_utils.cpp +++ b/src/cryptonote_core/cryptonote_tx_utils.cpp @@ -34,6 +34,7 @@ #include "string_tools.h" using namespace epee; +#include "carrot_impl/format_utils.h" #include "common/apply_permutation.h" #include "cryptonote_tx_utils.h" #include "cryptonote_config.h" @@ -80,17 +81,6 @@ namespace cryptonote tx.vout.clear(); tx.extra.clear(); - keypair txkey = keypair::generate(hw::get_device("default")); - add_tx_pub_key_to_extra(tx, txkey.pub); - if(!extra_nonce.empty()) - if(!add_extra_nonce_to_tx_extra(tx.extra, extra_nonce)) - return false; - if (!sort_tx_extra(tx.extra, tx.extra)) - return false; - - txin_gen in; - in.height = height; - uint64_t block_reward; if(!get_block_reward(median_weight, current_block_weight, already_generated_coins, block_reward, hard_fork_version)) { @@ -104,6 +94,39 @@ namespace cryptonote #endif block_reward += fee; + const bool do_carrot = hard_fork_version >= HF_VERSION_CARROT; + if (do_carrot) + { + try + { + carrot::CarrotDestinationV1 destination; + carrot::make_carrot_main_address_v1(miner_address.m_spend_public_key, + miner_address.m_view_public_key, + destination); + + tx = carrot::make_single_enote_carrot_coinbase_transaction_v1(destination, block_reward, height, extra_nonce); + tx.invalidate_hashes(); + } + catch (const std::exception &e) + { + MERROR("Failed to construct Carrot coinbase transaction: " << e.what()); + return false; + } + + return true; + } + + keypair txkey = keypair::generate(hw::get_device("default")); + add_tx_pub_key_to_extra(tx, txkey.pub); + if(!extra_nonce.empty()) + if(!add_extra_nonce_to_tx_extra(tx.extra, extra_nonce)) + return false; + if (!sort_tx_extra(tx.extra, tx.extra)) + return false; + + txin_gen in; + in.height = height; + // from hard fork 2, we cut out the low significant digits. This makes the tx smaller, and // keeps the paid amount almost the same. The unpaid remainder gets pushed back to the // emission schedule @@ -223,6 +246,9 @@ namespace cryptonote tx.extra = extra; crypto::public_key txkey_pub; + const bool is_fcmp_pp = rct && rct_config.bp_version >= 5; + CHECK_AND_ASSERT_THROW_MES(!is_fcmp_pp, "Cannot construct FCMP++ tx with construct_tx_with_tx_key, use Carrot tx builders instead"); + // if we have a stealth payment id, find it and encrypt it with the tx key now std::vector tx_extra_fields; if (parse_tx_extra(tx.extra, tx_extra_fields)) diff --git a/src/cryptonote_core/cryptonote_tx_utils.h b/src/cryptonote_core/cryptonote_tx_utils.h index 582b023df48..b5e666fdae3 100644 --- a/src/cryptonote_core/cryptonote_tx_utils.h +++ b/src/cryptonote_core/cryptonote_tx_utils.h @@ -31,6 +31,7 @@ #pragma once #include "cryptonote_basic/cryptonote_format_utils.h" +#include "fcmp_pp/curve_trees.h" #include "ringct/rctOps.h" namespace cryptonote @@ -52,7 +53,9 @@ namespace cryptonote rct::key mask; //ringct amount mask rct::multisig_kLRki multisig_kLRki; //multisig info - void push_output(uint64_t idx, const crypto::public_key &k, uint64_t amount) { outputs.push_back(std::make_pair(idx, rct::ctkey({rct::pk2rct(k), rct::zeroCommit(amount)}))); } + FcmpRerandomizedOutputCompressed rerandomized_output; + + void push_output(uint64_t idx, const crypto::public_key &k, uint64_t amount) { outputs.push_back(std::make_pair(idx, rct::ctkey({rct::pk2rct(k), rct::zeroCommitVartime(amount)}))); } BEGIN_SERIALIZE_OBJECT() FIELD(outputs) diff --git a/src/cryptonote_core/tx_pool.cpp b/src/cryptonote_core/tx_pool.cpp index 1890ef903f6..825a6883d60 100644 --- a/src/cryptonote_core/tx_pool.cpp +++ b/src/cryptonote_core/tx_pool.cpp @@ -157,21 +157,12 @@ namespace cryptonote return false; } - if (version != nic_verified_hf_version && !cryptonote::ver_non_input_consensus(tx, tvc, version)) - { - LOG_PRINT_L1("transaction " << id << " failed non-input consensus rule checks"); - tvc.m_verifivation_failed = true; // should already be set, but just in case - return false; - } - - uint64_t fee; + const uint64_t fee = get_tx_fee(tx); bool fee_good = false; try { - // get_tx_fee() can throw. It shouldn't throw because we check preconditions in - // ver_non_input_consensus(), but let's put it in a try block just in case. - fee = get_tx_fee(tx); - fee_good = kept_by_block || m_blockchain.check_fee(tx_weight, fee); + fee_good = kept_by_block || + (check_pool_capacity(id, tx_weight, fee) && m_blockchain.check_fee(tx_weight, fee)); } catch(...) {} if (!fee_good) // if fee calculation failed or fee in relayed tx is too low... @@ -201,6 +192,13 @@ namespace cryptonote return false; } + if (version != nic_verified_hf_version && !cryptonote::ver_non_input_consensus(tx, tvc, version)) + { + LOG_PRINT_L1("transaction " << id << " failed non-input consensus rule checks"); + tvc.m_verifivation_failed = true; // should already be set, but just in case + return false; + } + // if the transaction came from a block popped from the chain, // don't check if we have its key images as spent. // TODO: Investigate why not? @@ -351,7 +349,7 @@ namespace cryptonote ++m_cookie; - MINFO("Transaction added to pool: txid " << id << " weight: " << tx_weight << " fee/byte: " << (fee / (double)(tx_weight ? tx_weight : 1)) << ", count: " << m_added_txs_by_id.size()); + MINFO("Transaction added to pool: txid " << id << " weight: " << tx_weight << " fee/byte: " << (fee / (double)(tx_weight ? tx_weight : 1)) << ", count: " << m_added_txs_by_id.size() << ", pool total weight: " << m_txpool_weight); prune(m_txpool_max_weight); @@ -383,6 +381,35 @@ namespace cryptonote m_txpool_max_weight = bytes; } //--------------------------------------------------------------------------------- + bool tx_memory_pool::check_pool_capacity(const crypto::hash &id, const size_t weight, const uint64_t fee) const + { + if (weight == 0) + return true; + + CRITICAL_REGION_LOCAL(m_transactions_lock); + + // If the tx doesn't push the pool over the capacity limit, it fits! We can immediately return true + if ((weight + m_txpool_weight) < m_txpool_max_weight) + return true; + + // If it does, then see if it pays a higher fee than any txs already in the pool + if (m_txs_by_fee_and_receive_time.size() <= 1) + return true; + const auto it = --m_txs_by_fee_and_receive_time.end(); + if (it == m_txs_by_fee_and_receive_time.begin()) + return true; + + const double fee_per_byte = (double) fee / weight; + MDEBUG("Check pool capacity for tx " << id << ", fee/byte: " << fee_per_byte << ", pool total weight: " << m_txpool_weight); + + const double lowest_fee_per_byte = it->get_left().first; + if (fee_per_byte > lowest_fee_per_byte) + return true; + + LOG_PRINT_L1("Pool is at capacity, and tx " << id << " does not pay a high enough fee to enter"); + return false; + } + //--------------------------------------------------------------------------------- void tx_memory_pool::reduce_txpool_weight(size_t weight) { if (weight > m_txpool_weight) @@ -684,7 +711,7 @@ namespace cryptonote return true; } //--------------------------------------------------------------------------------- - bool tx_memory_pool::get_complement(std::vector hashes, std::vector &txes) const + bool tx_memory_pool::get_complement(std::vector hashes, std::vector &inv_txes) const { CRITICAL_REGION_LOCAL(m_transactions_lock); CRITICAL_REGION_LOCAL1(m_blockchain); @@ -692,35 +719,15 @@ namespace cryptonote // Sort so we can do binary search later std::sort(hashes.begin(), hashes.end()); - m_blockchain.for_all_txpool_txes([this, &hashes, &txes](const crypto::hash &txid, const txpool_tx_meta_t &meta, const cryptonote::blobdata_ref*) { - const auto tx_relay_method = meta.get_relay_method(); - if (tx_relay_method != relay_method::block && tx_relay_method != relay_method::fluff) - return true; - + m_blockchain.for_all_txpool_txes([this, &hashes, &inv_txes](const crypto::hash &txid, const txpool_tx_meta_t &meta, const cryptonote::blobdata_ref*) { // Do binary search for our pool TXID in given list, skip to next if already present const auto hash_it = std::lower_bound(hashes.cbegin(), hashes.cend(), txid); if (hash_it != hashes.cend() && *hash_it == txid) return true; - { - cryptonote::blobdata bd; - try - { - if (!m_blockchain.get_txpool_tx_blob(txid, bd, cryptonote::relay_category::broadcasted)) - { - MERROR("Failed to get blob for txpool transaction " << txid); - return true; - } - txes.emplace_back(std::move(bd)); - } - catch (const std::exception &e) - { - MERROR("Failed to get blob for txpool transaction " << txid << ": " << e.what()); - return true; - } - } + inv_txes.push_back(txid); return true; - }, false); + }, false, cryptonote::relay_category::broadcasted); return true; } //--------------------------------------------------------------------------------- @@ -1411,6 +1418,7 @@ namespace cryptonote const std::unordered_map>::const_iterator i = m_input_cache.find(txid); if (i != m_input_cache.end()) { + MDEBUG("Input cache hit in check_tx_inputs: " << txid); max_used_block_height = std::get<2>(i->second); max_used_block_id = std::get<3>(i->second); tvc = std::get<1>(i->second); diff --git a/src/cryptonote_core/tx_pool.h b/src/cryptonote_core/tx_pool.h index 49bebce19f5..88ce294f76c 100644 --- a/src/cryptonote_core/tx_pool.h +++ b/src/cryptonote_core/tx_pool.h @@ -504,7 +504,7 @@ namespace cryptonote /** * @brief get transactions not in the passed set */ - bool get_complement(std::vector hashes, std::vector &txes) const; + bool get_complement(std::vector hashes, std::vector &inv_txes) const; /** * @brief get info necessary for update of pool-related info in a wallet, preferably incremental @@ -617,6 +617,21 @@ namespace cryptonote */ void mark_double_spend(const transaction &tx); + /** + * @brief check if pool has capacity for the given tx + * + * If the tx would push the pool above its max weight limit, then the tx + * must pay a fee higher than txs in the pool already in order to enter the + * pool. Otherwise, the pool does not have capacity for the tx. + * + * @param txid the txid of the transaction to check, strictly used for logging + * @param weight the transaction weight + * @param fee the tx fee + * + * @return true if the pool has capacity for the tx + */ + bool check_pool_capacity(const crypto::hash &id, const size_t weight, const uint64_t fee) const; + /** * @brief prune lowest fee/byte txes till we're not above bytes * diff --git a/src/cryptonote_core/tx_verification_utils.cpp b/src/cryptonote_core/tx_verification_utils.cpp index 406d018c0da..2fbb2a18577 100644 --- a/src/cryptonote_core/tx_verification_utils.cpp +++ b/src/cryptonote_core/tx_verification_utils.cpp @@ -28,34 +28,70 @@ #include +#include "carrot_impl/format_utils.h" #include "common/threadpool.h" +#include "cryptonote_basic/cryptonote_format_utils.h" #include "cryptonote_core/blockchain.h" #include "cryptonote_core/cryptonote_core.h" #include "cryptonote_core/tx_verification_utils.h" #include "hardforks/hardforks.h" +#include "fcmp_pp/curve_trees.h" +#include "fcmp_pp/proof_len.h" +#include "fcmp_pp/prove.h" #include "ringct/rctSigs.h" #undef MONERO_DEFAULT_LOG_CATEGORY #define MONERO_DEFAULT_LOG_CATEGORY "verify" #define VER_ASSERT(cond, msgexpr) CHECK_AND_ASSERT_MES(cond, false, msgexpr) +#define VER_ASSERT_EQ(a, b, msgexpr) VER_ASSERT(a == b, msgexpr << " (" << a << " != " << b << ")"); using namespace cryptonote; -// Do RCT expansion, then do post-expansion sanity checks, then do full non-semantics verification. -static bool expand_tx_and_ver_rct_non_sem(transaction& tx, const rct::ctkeyM& mix_ring) +// Sanity checks on expanded FCMP++ tx +static bool check_fcmp_pp_expanded_tx(const transaction& tx) { // Pruned transactions can not be expanded and verified because they are missing RCT data VER_ASSERT(!tx.pruned, "Pruned transaction will not pass verRctNonSemanticsSimple"); - // Calculate prefix hash - const crypto::hash tx_prefix_hash = get_transaction_prefix_hash(tx); + const rct::rctSig& rv = tx.rct_signatures; + VER_ASSERT(rct::is_rct_fcmp(rv.type), "Unexpected RCT type in post-FCMP tx expansion"); + + VER_ASSERT(rv.mixRing.empty(), "Non-empty mixRing after expanding FCMP tx"); + VER_ASSERT(rv.p.CLSAGs.empty(), "Non-empty CLSAGs after expanding FCMP tx"); + VER_ASSERT(rv.p.MGs.empty(), "Non-empty MGs after expanding FCMP tx"); + VER_ASSERT(rv.p.rangeSigs.empty(), "Non-empty range sigs after expanding FCMP tx"); + VER_ASSERT(rv.p.bulletproofs.empty(), "Non-empty bulletproofs after expanding FCMP tx"); + VER_ASSERT(rv.pseudoOuts.empty(), "Non-empty old pseudo outs after expanding FCMP tx"); + + // Make sure the tree root is set + VER_ASSERT(rv.p.fcmp_ver_helper_data.tree_root != nullptr, "tree_root is not set"); + + // Check pseudoOuts size against transaction inputs + const size_t n_inputs = rv.p.pseudoOuts.size(); + VER_ASSERT_EQ(n_inputs, tx.vin.size(), "Mismatched pseudo outs to inputs after expanding FCMP tx"); + VER_ASSERT_EQ(n_inputs, rv.p.fcmp_ver_helper_data.key_images.size(), "Mismatched key images to inputs after expanding FCMP tx"); + + // For each input, check that the key images were copied into the expanded RCT sig correctly + for (size_t n = 0; n < n_inputs; ++n) + { + const crypto::key_image& nth_vin_image = boost::get(tx.vin[n]).k_image; + const bool ki_match = 0 == memcmp(&nth_vin_image, &rv.p.fcmp_ver_helper_data.key_images[n], 32); + VER_ASSERT(ki_match, "Failed to check ringct signatures: mismatched FCMP key image"); + } - // Expand mixring, tx inputs, tx key images, prefix hash message, etc into the RCT sig - const bool exp_res = Blockchain::expand_transaction_2(tx, tx_prefix_hash, mix_ring); - VER_ASSERT(exp_res, "Failed to expand rct signatures!"); + return true; +} + +// Sanity checks on expanded pre-FCMP, simple tx +static bool check_simple_pre_fcmp_expanded_tx(const transaction& tx, const rct::ctkeyM& mix_ring) +{ + // Pruned transactions can not be expanded and verified because they are missing RCT data + VER_ASSERT(!tx.pruned, "Pruned transaction will not pass verRctNonSemanticsSimple"); const rct::rctSig& rv = tx.rct_signatures; + VER_ASSERT(rct::is_rct_simple(rv.type) && !rct::is_rct_fcmp(rv.type), + "Unexpected RCT type in pre-FCMP simple tx expansion"); // Check that expanded RCT mixring == input mixring VER_ASSERT(rv.mixRing == mix_ring, "Failed to check ringct signatures: mismatched pubkeys/mixRing"); @@ -84,24 +120,15 @@ static bool expand_tx_and_ver_rct_non_sem(transaction& tx, const rct::ctkeyM& mi } // Mix ring data is now known to be correctly incorporated into the RCT sig inside tx. - VER_ASSERT(rct::verRctNonSemanticsSimple(rv), "Failed to verify simple RingCT signatures"); return true; } -// Same as expand_tx_and_ver_rct_non_sem(), but for RingCT sigs of type RCTTypeFull only -static bool expand_tx_and_ver_full_rct_non_sem(transaction& tx, const rct::ctkeyM& mix_ring) +// Sanity checks on expanded pre-FCMP, full tx +static bool check_full_pre_fcmp_expanded_tx(transaction& tx, const rct::ctkeyM& mix_ring) { // Pruned transactions can not be expanded and verified because they are missing RCT data VER_ASSERT(!tx.pruned, "Pruned transaction will not pass verRctNonSemanticsSimple"); - VER_ASSERT(tx.rct_signatures.type == rct::RCTTypeFull, - "Non-full (simple) RingCT transaction will not pass rct::verRct"); - - // Calculate prefix hash - const crypto::hash tx_prefix_hash = get_transaction_prefix_hash(tx); - - // Expand mixring, tx inputs, tx key images, prefix hash message, etc into the RCT sig - const bool exp_res = Blockchain::expand_transaction_2(tx, tx_prefix_hash, mix_ring); - VER_ASSERT(exp_res, "Failed to expand rct signatures!"); + VER_ASSERT(tx.rct_signatures.type == rct::RCTTypeFull, "Unexpected RCT type in pre-FCMP full tx expansion"); const rct::rctSig& rv = tx.rct_signatures; @@ -153,12 +180,22 @@ static bool expand_tx_and_ver_full_rct_non_sem(transaction& tx, const rct::ctkey } } - if (!rct::verRct(rv, false)) + return true; +} + +static bool expand_fcmp_pp_tx(transaction &tx, const fcmp_pp::TreeRootShared &decompressed_tree_root) +{ + const crypto::hash tx_prefix_hash = get_transaction_prefix_hash(tx); + if (!Blockchain::expand_transaction_2(tx, tx_prefix_hash, /*pubkeys=*/{}, decompressed_tree_root)) { - MERROR("Failed to check ringct signatures!"); + MERROR("Failed to expand FCMP++ tx"); + return false; + } + else if (!check_fcmp_pp_expanded_tx(tx)) + { + MERROR("Failed post-expansion FCMP++ checks"); return false; } - return true; } @@ -225,7 +262,6 @@ static bool tx_ver_legacy_ring_sigs(transaction& tx, const rct::ctkeyM& mix_ring return !fail_occurred.test_and_set(); // test_and_set() returns previously held value } - static bool is_canonical_bulletproof_layout(const std::vector &proofs) { if (proofs.size() != 1) @@ -246,16 +282,47 @@ static bool is_canonical_bulletproof_plus_layout(const std::vector FCMP_PLUS_PLUS_MAX_LAYERS) + return false; + if (n_inputs == 0 || n_inputs > FCMP_PLUS_PLUS_MAX_INPUTS) + return false; + if (n_outputs == 0 || n_outputs > FCMP_PLUS_PLUS_MAX_OUTPUTS) + return false; + if (proof.empty()) + return false; + const std::size_t act_sz = proof.size(); + if (act_sz == 0) + return false; + const std::size_t exp_sz = fcmp_pp::fcmp_pp_proof_len(n_inputs, n_tree_layers); + if (act_sz != exp_sz) + return false; + return true; +} + template -static bool ver_non_input_consensus_templated(TxForwardIt tx_begin, TxForwardIt tx_end, - tx_verification_context& tvc, std::uint8_t hf_version) +static bool ver_non_input_consensus_templated(TxForwardIt tx_begin, + TxForwardIt tx_end, + const std::unordered_map& transparent_amount_commitments, + tx_verification_context& tvc, + std::uint8_t hf_version) { std::vector rvv; + std::vector pubkeys_and_commitments; rvv.reserve(static_cast(std::distance(tx_begin, tx_end))); + pubkeys_and_commitments.reserve(static_cast(std::distance(tx_begin, tx_end)) * 2); - const size_t max_tx_version = hf_version < HF_VERSION_DYNAMIC_FEE ? 1 : 2; + // We assume transactions have an unmixable ring since it's more permissive. The version is + // checked again in Blockchain::check_tx_inputs() with `has_unmixable_ring` actually resolved. + const size_t min_tx_version = get_minimum_transaction_version(hf_version, /*has_unmixable_ring=*/true); + const size_t max_tx_version = get_maximum_transaction_version(hf_version); - const size_t tx_weight_limit = get_transaction_weight_limit(hf_version); + const size_t tx_weight_limit = get_non_coinbase_tx_weight_limit(hf_version); for (; tx_begin != tx_end; ++tx_begin) { @@ -270,10 +337,26 @@ static bool ver_non_input_consensus_templated(TxForwardIt tx_begin, TxForwardIt return false; } - // Rule 2 & 3 - if (tx.version == 0 || tx.version > max_tx_version) + // Rule 2 and Rule 3 + if (tx.version < min_tx_version || tx.version > max_tx_version) + { + tvc.m_verifivation_failed = true; + return false; + } + + // Rule 8 + if (hf_version >= HF_VERSION_REJECT_UNLOCK_TIME && tx.unlock_time != 0) + { + tvc.m_verifivation_failed = true; + tvc.m_nonzero_unlock_time = true; + return false; + } + + // Rule 9 + if (hf_version >= HF_VERSION_REJECT_LARGE_EXTRA && tx.extra.size() > MAX_TX_EXTRA_SIZE) { tvc.m_verifivation_failed = true; + tvc.m_tx_extra_too_big = true; return false; } @@ -286,6 +369,14 @@ static bool ver_non_input_consensus_templated(TxForwardIt tx_begin, TxForwardIt return false; } + // Rule 10 + if (!check_transaction_output_pubkeys_order(tx, hf_version)) + { + tvc.m_verifivation_failed = true; + tvc.m_invalid_output = true; + return false; + } + // Rule 5 if (!core::check_tx_semantic(tx, tvc, hf_version)) return false; @@ -297,6 +388,13 @@ static bool ver_non_input_consensus_templated(TxForwardIt tx_begin, TxForwardIt // We only want to check RingCT semantics if this is actually a RingCT transaction if (tx.version >= 2) rvv.push_back(&tx.rct_signatures); + + // Collect pubkeys and commitments for torsion check + if (!collect_points_for_torsion_check(tx, transparent_amount_commitments, pubkeys_and_commitments)) + { + tvc.m_verifivation_failed = true; + return false; + } } // Rule 7 @@ -307,6 +405,69 @@ static bool ver_non_input_consensus_templated(TxForwardIt tx_begin, TxForwardIt return false; } + // Rule 8 + // Note: technically this could be threaded with ver_mixed_rct_semantics + if (!rct::verPointsForTorsion(pubkeys_and_commitments)) + { + tvc.m_verifivation_failed = true; + tvc.m_invalid_output = true; + return false; + } + + return true; +} + +static bool collect_fcmp_pp_tx_verify_input(cryptonote::transaction &tx, + const std::pair &tree_root_for_block_index, + fcmp_pp::TreeRootShared &decompressed_tree_root_for_block_index_inout, + fcmp_pp::FcmpPpVerifyInput &fcmp_pp_verify_input_out) +{ + VER_ASSERT(tx.rct_signatures.type == rct::RCTTypeFcmpPlusPlus, "expected FCMP++ RCT Type"); + VER_ASSERT(!tx.pruned, "expected unpruned FCMP++ tx"); + + // Make sure tree metadata is correct + const uint64_t ref_block_index = tx.rct_signatures.p.reference_block; + const uint8_t n_tree_layers = tx.rct_signatures.p.n_tree_layers; + + VER_ASSERT(ref_block_index > 0, "tx reference_block must be > 0"); + VER_ASSERT(n_tree_layers > 0, "tx n_tree_layers must be > 0"); + + VER_ASSERT(n_tree_layers == tree_root_for_block_index.second, "Unexpected tx n tree layers"); + + const crypto::ec_point &tree_root = tree_root_for_block_index.first; + + // De-compress the tree root + if (!decompressed_tree_root_for_block_index_inout) + { + const auto curve_trees = fcmp_pp::curve_trees::curve_trees_v1(); + decompressed_tree_root_for_block_index_inout = curve_trees->get_tree_root_from_bytes(n_tree_layers, tree_root); + VER_ASSERT(decompressed_tree_root_for_block_index_inout != nullptr, "Failed to decompress root"); + } + + // Expand transaction + if (!expand_fcmp_pp_tx(tx, decompressed_tree_root_for_block_index_inout)) + return false; + + // Now instantiate the FCMP++ verify input + const auto &rv = tx.rct_signatures; + const rct::key signable_tx_hash = rct::get_pre_mlsag_hash(rv, hw::get_device("default")); + + // Type conversion on pseudo outs + const auto &pseudoOuts = rv.p.pseudoOuts; + std::vector pseudo_outs; + pseudo_outs.reserve(pseudoOuts.size()); + for (const auto &po : pseudoOuts) + pseudo_outs.emplace_back(rct::rct2pt(po)); + + fcmp_pp_verify_input_out = fcmp_pp::fcmp_pp_verify_input_new( + rct::rct2hash(signable_tx_hash), + rv.p.fcmp_pp, + n_tree_layers, + rv.p.fcmp_ver_helper_data.tree_root, + pseudo_outs, + rv.p.fcmp_ver_helper_data.key_images + ); + return true; } @@ -315,15 +476,157 @@ static bool ver_non_input_consensus_templated(TxForwardIt tx_begin, TxForwardIt namespace cryptonote { -uint64_t get_transaction_weight_limit(const uint8_t hf_version) +bool collect_points_for_torsion_check(const transaction& tx, + const std::unordered_map &transparent_amount_commitments, + std::vector &pubkeys_and_commitments_inout) +{ + for (std::size_t i = 0; i < tx.vout.size(); ++i) + { + // Don't need to collect points if we're not checking the tx outs for torsion + if (!cryptonote::output_checked_for_torsion(tx.vout[i].target)) + continue; + + crypto::public_key output_pubkey; + if (!cryptonote::get_output_public_key(tx.vout[i], output_pubkey)) + return false; + rct::key pubkey = rct::pk2rct(output_pubkey); + + rct::key commitment; + if (!cryptonote::get_commitment(tx, i, transparent_amount_commitments, commitment)) + return false; + + pubkeys_and_commitments_inout.emplace_back(pubkey); + pubkeys_and_commitments_inout.emplace_back(commitment); + } + + return true; +} + +void collect_transparent_amount_commitments( + const std::vector> &txs, + std::unordered_map &transparent_amount_commitments_inout) +{ + // Note: we do not clear transparent_amount_commitments_inout because it may be a rolling cache + + for (const auto &tx_ref : txs) + { + const auto &tx = tx_ref.get(); + + // We only need commitments for transparent amounts, which are tx version 1 || coinbase txs + if (tx.version > 1 && !cryptonote::is_coinbase(tx)) + continue; + for (const auto &tx_out : tx.vout) + { + const uint64_t amount = tx_out.amount; + if (transparent_amount_commitments_inout.find(amount) == transparent_amount_commitments_inout.end()) + transparent_amount_commitments_inout[amount] = rct::zeroCommitVartime(amount); + } + } +} + +std::vector> collect_transparent_amount_commitments( + const transaction &miner_tx, + const std::vector> &tx_pairs, + std::unordered_map &transparent_amount_commitments_inout) +{ + std::vector> tx_refs; + tx_refs.reserve(1 + tx_pairs.size()); + tx_refs.push_back(std::cref(miner_tx)); + for (const auto &tx : tx_pairs) + tx_refs.push_back(std::cref(tx.first)); + collect_transparent_amount_commitments(tx_refs, transparent_amount_commitments_inout); + return tx_refs; +} + +std::vector> collect_transparent_amount_commitments( + const transaction &miner_tx, + const std::vector &txs, + std::unordered_map &transparent_amount_commitments_inout) +{ + std::vector> tx_refs; + tx_refs.reserve(1 + txs.size()); + tx_refs.push_back(std::cref(miner_tx)); + for (const auto &tx : txs) + tx_refs.push_back(std::cref(tx)); + collect_transparent_amount_commitments(tx_refs, transparent_amount_commitments_inout); + return tx_refs; +} + +void collect_transparent_amount_commitments( + const std::unordered_map> &txs_by_txid, + std::unordered_map &transparent_amount_commitments_inout) +{ + std::vector> tx_refs; + tx_refs.reserve(txs_by_txid.size()); + for (const auto &tx_pair : txs_by_txid) + tx_refs.push_back(std::cref(tx_pair.second.first)); + collect_transparent_amount_commitments(tx_refs, transparent_amount_commitments_inout); +} + +uint64_t get_non_coinbase_tx_weight_limit(const uint8_t hf_version) { + static_assert(MAX_HF_VERSION == 18, "Max fork version bumped, check the max non coinbase tx weight limit"); + if (hf_version >= HF_VERSION_FCMP_PLUS_PLUS) + return FCMP_PLUS_PLUS_MAX_TX_WEIGHT; // from v8, limit a tx to 50% of the minimum block weight - if (hf_version >= HF_VERSION_PER_BYTE_FEE) + else if (hf_version >= HF_VERSION_PER_BYTE_FEE) return get_min_block_weight(hf_version) / 2 - CRYPTONOTE_COINBASE_BLOB_RESERVED_SIZE; else return get_min_block_weight(hf_version) - CRYPTONOTE_COINBASE_BLOB_RESERVED_SIZE; } +bool are_transaction_output_pubkeys_sorted(const std::vector &vout) +{ + crypto::public_key last_output_pubkey = crypto::null_pkey; + for (const tx_out &o : vout) { + crypto::public_key output_pubkey; + if (!get_output_public_key(o, output_pubkey)) { + return false; + } + else if (!(output_pubkey > last_output_pubkey)) { + return false; + } + last_output_pubkey = output_pubkey; + } + + return true; +} + +bool check_transaction_output_pubkeys_order(const transaction_prefix &tx_prefix, const std::uint8_t hf_version) +{ + if (hf_version > HF_VERSION_FCMP_PLUS_PLUS || carrot::is_carrot_transaction_v1(tx_prefix)) + return are_transaction_output_pubkeys_sorted(tx_prefix.vout); + return true; +} + +size_t get_minimum_transaction_version(uint8_t hf_version, bool has_unmixable_ring) +{ + if (hf_version >= HF_VERSION_REJECT_UNMIXABLE_V1) + { + return 2; + } + else if (hf_version < HF_VERSION_ENFORCE_RCT) + { + return 1; + } + else // HF_VERSION_ENFORCE_RCT <= hf_version < HF_VERSION_REJECT_UNMIXABLE_V1 + { + return has_unmixable_ring ? 1 : 2; + } +} + +size_t get_maximum_transaction_version(uint8_t hf_version) +{ + if (hf_version >= HF_VERSION_DYNAMIC_FEE) + { + return 2; + } + else // hf_version < HF_VERSION_DYNAMIC_FEE + { + return 1; + } +} + bool ver_input_proofs_rings(transaction& tx, const rct::ctkeyM &dereferenced_mix_ring) { // Hello future Monero dev! If you got this assert, read the following carefully: @@ -346,19 +649,27 @@ bool ver_input_proofs_rings(transaction& tx, const rct::ctkeyM &dereferenced_mix } else if (tx.version == 2) { + const crypto::hash tx_prefix_hash = get_transaction_prefix_hash(tx); + const bool exp_res = Blockchain::expand_transaction_2(tx, tx_prefix_hash, dereferenced_mix_ring, nullptr/*tree_root*/); + VER_ASSERT(exp_res, "Failed to expand pre-FCMP++ RingCT signatures!"); + switch (tx.rct_signatures.type) { case rct::RCTTypeNull: MERROR("Null RingCT does not have input proofs to verify"); return false; case rct::RCTTypeFull: - return expand_tx_and_ver_full_rct_non_sem(tx, dereferenced_mix_ring); + VER_ASSERT(check_full_pre_fcmp_expanded_tx(tx, dereferenced_mix_ring), + "Failed post-expansion checks on full RingCT tx"); + return rct::verRct(tx.rct_signatures, /*semantics=*/false); case rct::RCTTypeSimple: case rct::RCTTypeBulletproof: case rct::RCTTypeBulletproof2: case rct::RCTTypeCLSAG: case rct::RCTTypeBulletproofPlus: - return expand_tx_and_ver_rct_non_sem(tx, dereferenced_mix_ring); + VER_ASSERT(check_simple_pre_fcmp_expanded_tx(tx, dereferenced_mix_ring), + "Failed post-expansion checks on simple, pre-FCMP++ RingCT tx"); + return rct::verRctNonSemanticsSimple(tx.rct_signatures); default: MERROR("Unrecognized RingCT type: " << tx.rct_signatures.type); return false; @@ -371,6 +682,35 @@ bool ver_input_proofs_rings(transaction& tx, const rct::ctkeyM &dereferenced_mix } } +bool ver_input_proofs_fcmps(transaction& tx, const crypto::ec_point &dereferenced_fcmp_root) +{ + // Hello future Monero dev! If you got this assert, read the following carefully: + // + // For this version of RCT, the way we guaranteed that verification caches do not generate false + // positives (and thus possibly enabling double spends) is we take a hash of two things. One, + // we use get_transaction_hash() which gives us a (cryptographically secure) unique + // representation of all "knobs" controlled by the possibly malicious constructor of the + // transaction. Two, we take a hash of all *previously validated* blockchain data referenced by + // this transaction which is required to validate the ring signature. In our case, this is the + // FCMP tree root. Future versions of the protocol may differ in this regard, but if this + // assumption holds true in the future, enable the verification hash by modifying the + // `untested_tx` condition below. + const bool untested_tx = !(tx.version == 2 && tx.rct_signatures.type == rct::RCTTypeFcmpPlusPlus); + VER_ASSERT(!untested_tx, + "Unknown TX type. Make sure FCMP cache works correctly with this type and then enable it in the code here."); + VER_ASSERT(!tx.pruned, "Expected unpruned transaction"); + + fcmp_pp::TreeRootShared decompressed_root; + std::vector fcmp_pp_verify_inputs(1); // @TODO: make non-vector verify() overload + if (!collect_fcmp_pp_tx_verify_input(tx, {dereferenced_fcmp_root, tx.rct_signatures.p.n_tree_layers}, + decompressed_root, fcmp_pp_verify_inputs.back())) + { + return false; + } + + return fcmp_pp::verify(fcmp_pp_verify_inputs); +} + crypto::hash make_input_verification_id(const crypto::hash &tx_hash, const rct::ctkeyM &dereferenced_mix_ring) { std::stringstream ss; @@ -391,6 +731,35 @@ crypto::hash make_input_verification_id(const crypto::hash &tx_hash, const rct:: return input_verification_id; } +crypto::hash make_input_verification_id(const crypto::hash &tx_hash, const crypto::ec_point &dereferenced_fcmp_root) +{ + std::stringstream ss; + + // Start with domain seperation + ss << config::HASH_KEY_TXHASH_AND_TREE_ROOT; + + // Then add TX hash + ss.write(tx_hash.data, sizeof(crypto::hash)); + + // Then serialize FCMP tree root + ss.write(dereferenced_fcmp_root.data, sizeof(dereferenced_fcmp_root)); + + // Calculate hash of TX hash and FCMP tree root blob + crypto::hash input_verification_id; + get_blob_hash(ss.str(), input_verification_id); + return input_verification_id; +} + +crypto::hash make_input_verification_id(const transaction &tx, + const rct::ctkeyM &dereferenced_mix_ring, + const crypto::ec_point &dereferenced_fcmp_root) +{ + if (rct::is_rct_fcmp(tx.rct_signatures.type)) + return make_input_verification_id(get_transaction_hash(tx), dereferenced_fcmp_root); + else + return make_input_verification_id(get_transaction_hash(tx), dereferenced_mix_ring); +} + bool ver_mixed_rct_semantics(std::vector rvv) { size_t batch_rv_size = 0; // this acts as an "end" iterator to the last simple batchable sig ptr @@ -439,6 +808,19 @@ bool ver_mixed_rct_semantics(std::vector rvv) } is_batchable_rv = true; break; + case rct::RCTTypeFcmpPlusPlus: + if (!is_canonical_bulletproof_plus_layout(rv.p.bulletproofs_plus) || + !is_canonical_fcmp_plus_plus_layout(rv.p.reference_block, + rv.p.n_tree_layers, + rv.p.pseudoOuts.size(), // number of tx inputs + rv.outPk.size(), // number of tx outputs + rv.p.fcmp_pp)) + { + MERROR("fcmp_plus_plus does not have canonical form"); + return false; + } + is_batchable_rv = true; + break; default: MERROR("Unknown rct type: " << rv.type); return false; @@ -463,13 +845,87 @@ bool ver_mixed_rct_semantics(std::vector rvv) return true; } +bool batch_ver_fcmp_pp_consensus +( + pool_supplement& ps, + const std::unordered_map>& tree_root_by_block_index, + std::unordered_map &valid_input_verification_id_by_txid_out +) +{ + valid_input_verification_id_by_txid_out.clear(); + if (ps.txs_by_txid.empty()) + { + return true; + } + + // Collect unverified FCMP++ txs for batch verification + std::unordered_map decompressed_tree_roots_by_block_index; + std::vector fcmp_pp_verify_inputs; + fcmp_pp_verify_inputs.reserve(ps.txs_by_txid.size()); + + // Prepare input verification ID's for FCMP++'s we are verifying + std::unordered_map input_verification_id_by_txid; + input_verification_id_by_txid.reserve(ps.txs_by_txid.size()); + + for (auto &tx_entry : ps.txs_by_txid) + { + const crypto::hash &txid = tx_entry.first; + cryptonote::transaction &tx = tx_entry.second.first; + if (tx.pruned || tx.version != 2 || tx.rct_signatures.type != rct::RCTTypeFcmpPlusPlus) + { + continue; + } + + MDEBUG("Preparing FCMP++ tx " << txid << " for batch verification " << "(" << tx.vin.size() << " inputs)"); + + const uint64_t reference_block = tx.rct_signatures.p.reference_block; + const bool r = collect_fcmp_pp_tx_verify_input(tx, + tree_root_by_block_index.at(reference_block), + decompressed_tree_roots_by_block_index[reference_block], + fcmp_pp_verify_inputs.emplace_back()); + + if (!r) + { + return false; + } + + input_verification_id_by_txid[txid] = make_input_verification_id(txid, + tree_root_by_block_index.at(reference_block).first); + } + + if (fcmp_pp_verify_inputs.empty()) + { + return true; + } + + // Ok, we're ready to batch verify all FCMP++ txs now + const std::size_t n_proofs = fcmp_pp_verify_inputs.size(); + MDEBUG("Batch verifying " << n_proofs << " FCMP++ txs"); + if (!rct::batchVerifyFcmpPpProofs(std::move(fcmp_pp_verify_inputs))) + { + return false; + } + MDEBUG("Successfully batch verified " << n_proofs << " FCMP++ txs"); + + // All FCMP++'s have been verified, so set the valid input verification ID's + valid_input_verification_id_by_txid_out = std::move(input_verification_id_by_txid); + + return true; +} + bool ver_non_input_consensus(const transaction& tx, tx_verification_context& tvc, std::uint8_t hf_version) { - return ver_non_input_consensus_templated(&tx, &tx + 1, tvc, hf_version); + // Get tx's transparent amount commitments + std::unordered_map transparent_amount_commitments; + collect_transparent_amount_commitments({std::cref(tx)}, transparent_amount_commitments); + + return ver_non_input_consensus_templated(&tx, &tx + 1, transparent_amount_commitments, tvc, hf_version); } -bool ver_non_input_consensus(const pool_supplement& ps, tx_verification_context& tvc, +bool ver_non_input_consensus(const pool_supplement& ps, + const std::unordered_map& transparent_amount_commitments, + tx_verification_context& tvc, const std::uint8_t hf_version) { // We already verified the pool supplement for this hard fork version! Yippee! @@ -482,7 +938,11 @@ bool ver_non_input_consensus(const pool_supplement& ps, tx_verification_context& const auto tx_end = boost::make_transform_iterator(ps.txs_by_txid.cend(), it_transform); // Perform the checks... - const bool verified = ver_non_input_consensus_templated(tx_begin, tx_end, tvc, hf_version); + const bool verified = ver_non_input_consensus_templated(tx_begin, + tx_end, + transparent_amount_commitments, + tvc, + hf_version); // Cache the hard fork version on success if (verified) diff --git a/src/cryptonote_core/tx_verification_utils.h b/src/cryptonote_core/tx_verification_utils.h index e27b21bf933..76e14bf58c6 100644 --- a/src/cryptonote_core/tx_verification_utils.h +++ b/src/cryptonote_core/tx_verification_utils.h @@ -28,12 +28,43 @@ #pragma once +#include + #include "cryptonote_basic/blobdatatype.h" #include "cryptonote_basic/cryptonote_basic.h" #include "cryptonote_basic/verification_context.h" namespace cryptonote { +/** + * @brief Add the tx's output pub keys and commitments to the collection + * + * @param tx + * @param transparent_amount_commitments pre-calculated transparent amount commitments + * @param pubkeys_and_commitments_inout the collection by ref + * @return successful insert + */ +bool collect_points_for_torsion_check(const transaction& tx, + const std::unordered_map &transparent_amount_commitments, + std::vector &pubkeys_and_commitments_inout); + +void collect_transparent_amount_commitments( + const std::vector> &txs, + std::unordered_map &transparent_amount_commitments_inout); + +std::vector> collect_transparent_amount_commitments( + const transaction &miner_tx, + const std::vector> &tx_pairs, + std::unordered_map &transparent_amount_commitments_inout); + +std::vector> collect_transparent_amount_commitments( + const transaction &miner_tx, + const std::vector &txs, + std::unordered_map &transparent_amount_commitments_inout); + +void collect_transparent_amount_commitments( + const std::unordered_map> &txs_by_txid, + std::unordered_map &transparent_amount_commitments_inout); /** * @brief Get the maximum transaction weight for a given hardfork @@ -41,7 +72,43 @@ namespace cryptonote * @param hf_version hard fork version * @return the maximum unconditional transaction weight */ -uint64_t get_transaction_weight_limit(uint8_t hf_version); +uint64_t get_non_coinbase_tx_weight_limit(uint8_t hf_version); + +/** + * @brief Check whether transaction's output pubkeys are sorted in strictly increasing lexicographical order + */ +bool are_transaction_output_pubkeys_sorted(const std::vector &vout); + +/** + * @brief Check whether transaction's output pubkeys are sorted, iff required by fork rule + * @param tx_prefix + * @param hf_version hard fork version + * @see are_transaction_output_pubkeys_sorted() + * + * Output pubkeys must be sorted after FCMP++ grace period or if a Carrot tx during FCMP++ grace period. + */ +bool check_transaction_output_pubkeys_order(const transaction_prefix &tx_prefix, std::uint8_t hf_version); + +/** + * @brief Get the minimum allowed transaction version + * + * An "unmixable" ring is a ring appearing in block index BI spending a pre-RingCT enote + * (i.e. referencible amount != 0) where the minimum required ring size is greater than the total + * number of pre-RingCT enotes on-chain at block indices < BI with that same amount. + * + * @param hf_version hard fork version + * @param has_unmixable_ring true iff at least one of the rings in the transaction is "unmixable" + * @return the minimum allowed transaction version + */ +size_t get_minimum_transaction_version(uint8_t hf_version, bool has_unmixable_ring); + +/** + * @brief Get the maximum allowed transaction version + * + * @param hf_version hard fork version + * @return the maximum allowed transaction version + */ +size_t get_maximum_transaction_version(uint8_t hf_version); /** * @brief Tx-safe version of crypto::check_ring_signature() / rct::verRct(_, false) / rct::verRctNonSemanticsSimple() @@ -62,6 +129,25 @@ uint64_t get_transaction_weight_limit(uint8_t hf_version); */ bool ver_input_proofs_rings(transaction& tx, const rct::ctkeyM &dereferenced_mix_ring); +/** + * @brief Tx-safe version of rct::verRctNonSemanticsSimple() for FCMP++ txs + * + * This function will not affect how the transaction is serialized and it will never modify the + * transaction prefix. + * + * The reference to tx is mutable since the transaction's ring signatures will be expanded by + * Blockchain::expand_transaction_2. This means that the caller does not need to call + * expand_transaction_2 on this transaction before passing it; the transaction will not successfully + * verify with "old" FCMP tree / misc RCT data if the transaction has been otherwise modified since + * the last verification. + * + * @param tx transaction which contains RCT signature to verify + * @param dereferenced_fcmp_root FCMP tree root referenced by this tx. THIS DATA MUST BE PREVIOUSLY VALIDATED + * @return true when verRctNonSemanticsSimple() w/ expanded tx.rct_signatures would return true + * @return false when verRctNonSemanticsSimple() w/ expanded tx.rct_signatures would return false + */ +bool ver_input_proofs_fcmps(transaction& tx, const crypto::ec_point &dereferenced_fcmp_root); + /** * @brief Make an ID for the parameters to ver_input_proofs_rings() for a transaction and its dereferenced chain data * @@ -73,6 +159,10 @@ bool ver_input_proofs_rings(transaction& tx, const rct::ctkeyM &dereferenced_mix * was fetched without properly invalidating the hashes. */ crypto::hash make_input_verification_id(const crypto::hash &tx_hash, const rct::ctkeyM &dereferenced_mix_ring); +crypto::hash make_input_verification_id(const crypto::hash &tx_hash, const crypto::ec_point &dereferenced_fcmp_root); +crypto::hash make_input_verification_id(const transaction &tx, + const rct::ctkeyM &dereferenced_mix_ring, + const crypto::ec_point &dereferenced_fcmp_root); /** * @brief Verify the semantics of a group of RingCT signatures as a batch (if applicable) @@ -97,17 +187,28 @@ struct pool_supplement mutable std::uint8_t nic_verified_hf_version = 0; }; +// TODO: document the function +bool batch_ver_fcmp_pp_consensus +( + pool_supplement& ps, + const std::unordered_map>& tree_root_by_block_index, + std::unordered_map &valid_input_verification_id_by_txid_out +); + /** * @brief Verify every non-input consensus rule for a group of non-coinbase transactions * * List of checks that we do for each transaction: * 1. Check tx blob size < get_max_tx_size() - * 2. Check tx version != 0 - * 3. Check tx version is less than maximum for given hard fork version - * 4. Check tx weight < get_transaction_weight_limit() + * 2. Check tx version >= get_minimum_transaction_version() + * 3. Check tx version <= get_maximum_transaction_version() + * 4. Check tx weight < get_non_coinbase_tx_weight_limit() * 5. Passes core::check_tx_semantic() * 6. Passes Blockchain::check_tx_outputs() * 7. Passes ver_mixed_rct_semantics() [Uses batch RingCT verification when applicable] + * 8. Check unlock time is 0 from hardfork v17 + * 9. Check extra size <= MAX_TX_EXTRA_SIZE from hardfork v17 + * 10. Passes check_transaction_output_pubkeys_order() * * For pool_supplement input: * We assume the structure of the pool supplement is already correct: for each value entry, the @@ -124,7 +225,9 @@ struct pool_supplement bool ver_non_input_consensus(const transaction& tx, tx_verification_context& tvc, std::uint8_t hf_version); -bool ver_non_input_consensus(const pool_supplement& ps, tx_verification_context& tvc, +bool ver_non_input_consensus(const pool_supplement& ps, + const std::unordered_map& transparent_amount_commitments, + tx_verification_context& tvc, std::uint8_t hf_version); } // namespace cryptonote diff --git a/src/cryptonote_protocol/cryptonote_protocol_defs.h b/src/cryptonote_protocol/cryptonote_protocol_defs.h index b34276f487a..4907f0476cf 100644 --- a/src/cryptonote_protocol/cryptonote_protocol_defs.h +++ b/src/cryptonote_protocol/cryptonote_protocol_defs.h @@ -34,6 +34,8 @@ #include "serialization/keyvalue_serialization.h" #include "cryptonote_basic/cryptonote_basic.h" #include "cryptonote_basic/blobdatatype.h" +#include "byte_slice.h" +#include "storages/portable_storage_template_helper.h" namespace cryptonote { @@ -207,6 +209,39 @@ namespace cryptonote }; typedef epee::misc_utils::struct_init request; }; + + struct tx_request_padding + { + void operator()(NOTIFY_NEW_TRANSACTIONS::request& request) const + { + size_t bytes = 9 /* header */ + 4 /* 1 + 'txs' */ + tools::get_varint_data(request.txs.size()).size(); + for(auto tx_blob_it = request.txs.begin(); tx_blob_it!=request.txs.end(); ++tx_blob_it) + bytes += tools::get_varint_data(tx_blob_it->size()).size() + tx_blob_it->size(); + + // stuff some dummy bytes in to stay safe from traffic volume analysis + static constexpr const size_t granularity = 1024; + size_t padding = granularity - bytes % granularity; + const size_t overhead = 2 /* 1 + '_' */ + tools::get_varint_data(padding).size(); + if (overhead > padding) + padding = 0; + else + padding -= overhead; + request._ = std::string(padding, ' '); + + epee::byte_slice arg_buff; + epee::serialization::store_t_to_binary(request, arg_buff); + + // we probably lowballed the payload size a bit, so added a but too much. Fix this now. + size_t remove = arg_buff.size() % granularity; + if (remove > request._.size()) + request._.clear(); + else + request._.resize(request._.size() - remove); + // if the size of _ moved enough, we might lose byte in size encoding, we don't care + } + }; + inline constexpr tx_request_padding pad_tx_request{}; + /************************************************************************/ /* */ /************************************************************************/ @@ -375,5 +410,49 @@ namespace cryptonote }; typedef epee::misc_utils::struct_init request; }; + + /************************************************************************ + * Announces new transaction hashes that * + * the sender believes the receiver may not have. * + * The receiver can pull which hashes are missing locally * + * and optionally request the actual serialized transactions for them. * + *************************************************************************/ + struct NOTIFY_TX_POOL_HASH + { + const static int ID = BC_COMMANDS_POOL_BASE + 11; + + struct request_t + { + std::vector t; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE_CONTAINER_POD_AS_BLOB(t) + END_KV_SERIALIZE_MAP() + }; + + typedef epee::misc_utils::struct_init request; + }; + + /************************************************************************ + * Requests the actual transaction data corresponding * + * to a set of transaction hashes. * + * The receiver should look up each transaction in its own pool * + * or memory and respond with the serialized version, if known. * + *************************************************************************/ + struct NOTIFY_REQUEST_TX_POOL_TXS + { + const static int ID = BC_COMMANDS_POOL_BASE + 12; + + struct request_t + { + std::vector t; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE_CONTAINER_POD_AS_BLOB(t) + END_KV_SERIALIZE_MAP() + }; + + typedef epee::misc_utils::struct_init request; + }; } diff --git a/src/cryptonote_protocol/cryptonote_protocol_handler.h b/src/cryptonote_protocol/cryptonote_protocol_handler.h index a7dc77c2756..4f6ccfd4bdd 100644 --- a/src/cryptonote_protocol/cryptonote_protocol_handler.h +++ b/src/cryptonote_protocol/cryptonote_protocol_handler.h @@ -49,8 +49,10 @@ #include "cryptonote_basic/connection_context.h" #include "net/levin_base.h" #include "p2p/net_node_common.h" +#include "request_manager.h" #include #include +#include PUSH_WARNINGS DISABLE_VS_WARNINGS(4355) @@ -95,8 +97,10 @@ namespace cryptonote HANDLE_NOTIFY_T2(NOTIFY_REQUEST_CHAIN, &cryptonote_protocol_handler::handle_request_chain) HANDLE_NOTIFY_T2(NOTIFY_RESPONSE_CHAIN_ENTRY, &cryptonote_protocol_handler::handle_response_chain_entry) HANDLE_NOTIFY_T2(NOTIFY_NEW_FLUFFY_BLOCK, &cryptonote_protocol_handler::handle_notify_new_fluffy_block) - HANDLE_NOTIFY_T2(NOTIFY_REQUEST_FLUFFY_MISSING_TX, &cryptonote_protocol_handler::handle_request_fluffy_missing_tx) + HANDLE_NOTIFY_T2(NOTIFY_REQUEST_FLUFFY_MISSING_TX, &cryptonote_protocol_handler::handle_request_fluffy_missing_tx) HANDLE_NOTIFY_T2(NOTIFY_GET_TXPOOL_COMPLEMENT, &cryptonote_protocol_handler::handle_notify_get_txpool_complement) + HANDLE_NOTIFY_T2(NOTIFY_TX_POOL_HASH, &cryptonote_protocol_handler::handle_notify_tx_pool_hash) + HANDLE_NOTIFY_T2(NOTIFY_REQUEST_TX_POOL_TXS, &cryptonote_protocol_handler::handle_request_tx_pool_txs) END_INVOKE_MAP2() bool on_idle(); @@ -156,10 +160,12 @@ namespace cryptonote int handle_notify_new_fluffy_block(int command, NOTIFY_NEW_FLUFFY_BLOCK::request& arg, cryptonote_connection_context& context); int handle_request_fluffy_missing_tx(int command, NOTIFY_REQUEST_FLUFFY_MISSING_TX::request& arg, cryptonote_connection_context& context); int handle_notify_get_txpool_complement(int command, NOTIFY_GET_TXPOOL_COMPLEMENT::request& arg, cryptonote_connection_context& context); + int handle_notify_tx_pool_hash(int command, NOTIFY_TX_POOL_HASH::request& arg, cryptonote_connection_context& context); + int handle_request_tx_pool_txs(int command, NOTIFY_REQUEST_TX_POOL_TXS::request& arg, cryptonote_connection_context& context); //----------------- i_bc_protocol_layout --------------------------------------- virtual bool relay_block(NOTIFY_NEW_FLUFFY_BLOCK::request& arg, cryptonote_connection_context& exclude_context); - virtual bool relay_transactions(NOTIFY_NEW_TRANSACTIONS::request& arg, const boost::uuids::uuid& source, epee::net_utils::zone zone, relay_method tx_relay); + virtual bool relay_transactions(NOTIFY_NEW_TRANSACTIONS::request& arg, std::vector &&tx_hashes, const boost::uuids::uuid& source, epee::net_utils::zone zone, relay_method tx_relay); //---------------------------------------------------------------------------------- //bool get_payload_sync_data(HANDSHAKE_DATA::request& hshd, cryptonote_connection_context& context); bool should_drop_connection(cryptonote_connection_context& context, uint32_t next_stripe); @@ -175,6 +181,9 @@ namespace cryptonote bool kick_idle_peers(); bool check_standby_peers(); bool update_sync_search(); + void send_txs_request(cryptonote_connection_context &context, std::vector &&tx_hashes); + std::mutex m_check_tx_request_queue_mutex; + bool check_tx_request_queue(); int try_add_next_blocks(cryptonote_connection_context &context); void notify_new_stripe(cryptonote_connection_context &context, uint32_t stripe); size_t skip_unneeded_hashes(cryptonote_connection_context& context, bool check_block_queue) const; @@ -197,6 +206,7 @@ namespace cryptonote epee::math_helper::once_a_time_milliseconds<100> m_standby_checker; epee::math_helper::once_a_time_seconds<101> m_sync_search_checker; epee::math_helper::once_a_time_seconds<43> m_bad_peer_checker; + epee::math_helper::once_a_time_seconds<5> m_peer_tx_request_checker; std::unordered_map m_max_out_peers; mutable epee::critical_section m_max_out_peers_lock; tools::PerformanceTimer m_sync_timer, m_add_timer; @@ -209,6 +219,8 @@ namespace cryptonote std::atomic m_span_limit; std::atomic m_bss; + request_manager m_request_manager; + // Values for sync time estimates boost::posix_time::ptime m_sync_start_time; boost::posix_time::ptime m_period_start_time; diff --git a/src/cryptonote_protocol/cryptonote_protocol_handler.inl b/src/cryptonote_protocol/cryptonote_protocol_handler.inl index bcf44711329..4e58107409b 100644 --- a/src/cryptonote_protocol/cryptonote_protocol_handler.inl +++ b/src/cryptonote_protocol/cryptonote_protocol_handler.inl @@ -38,12 +38,15 @@ #include #include #include +#include +#include #include #include #include #include "cryptonote_protocol/cryptonote_protocol_handler.h" #include "cryptonote_basic/cryptonote_format_utils.h" +#include "misc_log_ex.h" #include "profile_tools.h" #include "net/network_throttle-detail.hpp" #include "common/pruning.h" @@ -179,7 +182,34 @@ namespace cryptonote return make_pool_supplement_from_block_entry(blk_entry.txs, blk_tx_hashes, blk_entry.pruned, pool_supplement); } + //----------------------------------------------------------------------------------------------------------------------- + static std::size_t max_n_txs_per_packet() + { + static const std::size_t MAX_N_TXS = get_command_max_bytes(NOTIFY_NEW_TRANSACTIONS::ID) / get_max_tx_size(); + CHECK_AND_ASSERT_MES(MAX_N_TXS > 0, 100/*sane default*/, "MAX_N_TXS is expected >0, something is wrong."); + return MAX_N_TXS; + } + //----------------------------------------------------------------------------------------------------------------------- + static std::size_t max_n_tx_hashes_per_packet() + { + static const std::size_t MAX_N_TX_HASHES = get_command_max_bytes(NOTIFY_TX_POOL_HASH::ID) / sizeof(crypto::hash) * 95 / 100; // 95% for overhead + CHECK_AND_ASSERT_MES(MAX_N_TX_HASHES > 0, 100/*sane default*/, "MAX_N_TX_HASHES is expected >0, something is wrong."); + return MAX_N_TX_HASHES; + } + //----------------------------------------------------------------------------------------------------------------------- + static void shrink_to_fit_hashes_container(std::vector &hashes_inout) + { + static const std::size_t max_n_hashes = max_n_tx_hashes_per_packet(); + if (max_n_hashes >= hashes_inout.size()) + return; + // If this hits, it means we should look into updating the protocol for filling a node's empty txpool. + // The pool complement protocol is not designed to handle refilling huge pools. We truncate to avoid the worst case + // of connection drops, since missing pool txs is more desirable than dropped peers. But it would be better to have + // a protocol capable of refilling an empty pool when the pool is large. + MWARNING("Truncating " << (hashes_inout.size() - max_n_hashes) << " tx hashes for the pool complement"); + hashes_inout.resize(max_n_hashes); + } //----------------------------------------------------------------------------------------------------------------------- template t_cryptonote_protocol_handler::t_cryptonote_protocol_handler(t_core& rcore, nodetool::i_p2p_endpoint* p_net_layout, bool offline):m_core(rcore), @@ -191,8 +221,8 @@ namespace cryptonote m_no_sync(false), m_span_limit(BLOCK_QUEUE_NSPANS_MINIMUM), m_span_time(0), - m_bss(0) - + m_bss(0), + m_request_manager(max_n_txs_per_packet()) { if(!m_p2p) m_p2p = &m_p2p_stub; @@ -215,7 +245,6 @@ namespace cryptonote m_block_download_max_size = command_line::get_arg(vm, cryptonote::arg_block_download_max_size); m_sync_pruned_blocks = command_line::get_arg(vm, cryptonote::arg_sync_pruned_blocks); m_span_time = command_line::get_arg(vm, cryptonote::arg_span_limit); - return true; } //------------------------------------------------------------------------------------------------------------------------ @@ -748,6 +777,8 @@ namespace cryptonote // Relay an empty block arg.b.txs.clear(); relay_block(arg, context); + for (const auto &tx_hash : new_block.tx_hashes) + m_request_manager.remove_request(tx_hash); } else if( bvc.m_marked_as_orphaned ) { @@ -874,28 +905,106 @@ namespace cryptonote { MLOG_P2P_MESSAGE("Received NOTIFY_GET_TXPOOL_COMPLEMENT (" << arg.hashes.size() << " txes)"); if(context.m_state != cryptonote_connection_context::state_normal) + { + MLOG_PEER_STATE("Not sending txpool complement, connection is not in normal state yet"); return 1; + } - std::vector> local_blocks; - std::vector local_txs; - - std::vector txes; - if (!m_core.get_txpool_complement(std::move(arg.hashes), txes)) + NOTIFY_TX_POOL_HASH::request inv_txes_req; + if (!m_core.get_txpool_complement(std::move(arg.hashes), inv_txes_req.t)) { LOG_ERROR_CCONTEXT("failed to get txpool complement"); return 1; } - NOTIFY_NEW_TRANSACTIONS::request new_txes; - new_txes.txs = std::move(txes); + shrink_to_fit_hashes_container(inv_txes_req.t); - MLOG_P2P_MESSAGE - ( - "-->>NOTIFY_NEW_TRANSACTIONS: " - << ", txs.size()=" << new_txes.txs.size() - ); + MLOG_P2P_MESSAGE("-->>NOTIFY_TX_POOL_HASH:" << " txs.size()=" << inv_txes_req.t.size()); + post_notify(inv_txes_req, context); + return 1; + } + //------------------------------------------------------------------------------------------------------------------------ + template + int t_cryptonote_protocol_handler::handle_notify_tx_pool_hash(int command, NOTIFY_TX_POOL_HASH::request& arg, cryptonote_connection_context& context) + { + MLOG_P2P_MESSAGE("Received NOTIFY_TX_POOL_HASH (" << arg.t.size() << " txes)"); + + if (!is_synchronized()) + { + LOG_DEBUG_CC(context, "Received new tx hash while syncing, ignored"); + return 1; + } + + // If the peer's chain is too far ahead of us, then we ignore its reported pool txs. + // We don't need pool txs until we're synced. Uses a 2 block window in case we're in the process of adding blocks, + // and we should be able to capture the pool txs once synced. + if (context.m_remote_blockchain_height > (m_core.get_current_blockchain_height() + 2)) + { + LOG_DEBUG_CC(context, "Received new tx hash from peer that is too far ahead of us, ignored"); + return 1; + } + + // Create a list to hold transaction hashes missing in our local tx pool. + std::vector missing_tx_hashes; + missing_tx_hashes.reserve(arg.t.size()); + + // Iterate over each advertised transaction hash and check our pool and our requested tracker. + for (const auto &tx_hash : arg.t) + { + // If we have the tx already, we don't need to request it. + // Warning: the db read can potentially be expensive if someone is sending us many txs already in the db. + // We can mitigate by keeping tack of how many chain txs someone sends us, and dropping after exceeding a threshold. + if (m_core.pool_has_tx(tx_hash) || m_core.get_blockchain_storage().have_tx(tx_hash)) + { + // Remove it if it's in the request queue already since we already have it (it really shouldn't be here anyway) + m_request_manager.remove_request(tx_hash); + continue; + } + const bool send_request = m_request_manager.add_request(tx_hash, context.m_connection_id); + if (send_request) + missing_tx_hashes.push_back(tx_hash); + } + + this->send_txs_request(context, std::move(missing_tx_hashes)); + + return 1; + } + //------------------------------------------------------------------------------------------------------------------------ + template + int t_cryptonote_protocol_handler::handle_request_tx_pool_txs(int command, NOTIFY_REQUEST_TX_POOL_TXS::request& arg, cryptonote_connection_context& context) + { + MLOG_P2P_MESSAGE("Received NOTIFY_REQUEST_TX_POOL_TXS (" << arg.t.size() << " txes)"); + + std::vector txs; + + if (arg.t.size() > max_n_txs_per_packet()) + { + LOG_ERROR_CCONTEXT("Too many txs, cannot accept request to send " << arg.t.size() << " txs to " << context.m_connection_id); + return 1; + } + + // Iterate over requested txin hashes + for (const auto &tx_hash : arg.t) + { + // Attempt to get the transaction blob from the mempool; + cryptonote::blobdata tx_blob; + if (m_core.get_pool_transaction(tx_hash, tx_blob, cryptonote::relay_category::broadcasted)) + { + txs.push_back(std::move(tx_blob)); + } + // If tx is not in the pool, then ignore it (do not penalize peer) + } + + // Send response if any txs found + if (!txs.empty()) + { + NOTIFY_NEW_TRANSACTIONS::request request = {}; + request.txs = std::move(txs); + request.dandelionpp_fluff = true; + pad_tx_request(request); + post_notify(request, context); + } - post_notify(new_txes, context); return 1; } //------------------------------------------------------------------------------------------------------------------------ @@ -903,6 +1012,7 @@ namespace cryptonote int t_cryptonote_protocol_handler::handle_notify_new_transactions(int command, NOTIFY_NEW_TRANSACTIONS::request& arg, cryptonote_connection_context& context) { MLOG_P2P_MESSAGE("Received NOTIFY_NEW_TRANSACTIONS (" << arg.txs.size() << " txes)"); + std::lock_guard m_check_lock(m_check_tx_request_queue_mutex); std::unordered_set seen; for (const auto &blob: arg.txs) { @@ -943,6 +1053,8 @@ namespace cryptonote std::vector stem_txs{}; std::vector fluff_txs{}; + std::vector stem_hashes{}; + std::vector fluff_hashes{}; if (arg.dandelionpp_fluff) { tx_relay = relay_method::fluff; @@ -951,25 +1063,31 @@ namespace cryptonote else stem_txs.reserve(arg.txs.size()); - for (auto& tx : arg.txs) + for (auto& tx_blob : arg.txs) { tx_verification_context tvc{}; - if (!m_core.handle_incoming_tx(tx, tvc, tx_relay, true) && !tvc.m_no_drop_offense) + crypto::hash tx_hash{}; + if (!m_core.handle_incoming_tx(tx_blob, tvc, tx_relay, true, tx_hash) && !tvc.m_no_drop_offense) { LOG_PRINT_CCONTEXT_L1("Tx verification failed, dropping connection"); drop_connection(context, false, false); return 1; } + if (tx_hash != crypto::hash{}) + m_request_manager.remove_request(tx_hash); + switch (tvc.m_relay) { case relay_method::local: case relay_method::stem: - stem_txs.push_back(std::move(tx)); + stem_txs.push_back(std::move(tx_blob)); + stem_hashes.push_back(std::move(tx_hash)); break; case relay_method::block: case relay_method::fluff: - fluff_txs.push_back(std::move(tx)); + fluff_txs.push_back(std::move(tx_blob)); + fluff_hashes.push_back(std::move(tx_hash)); break; default: case relay_method::forward: // not supposed to happen here @@ -983,14 +1101,14 @@ namespace cryptonote //TODO: add announce usage here arg.dandelionpp_fluff = false; arg.txs = std::move(stem_txs); - relay_transactions(arg, context.m_connection_id, context.m_remote_address.get_zone(), relay_method::stem); + relay_transactions(arg, std::move(stem_hashes), context.m_connection_id, context.m_remote_address.get_zone(), relay_method::stem); } if (!fluff_txs.empty()) { //TODO: add announce usage here arg.dandelionpp_fluff = true; arg.txs = std::move(fluff_txs); - relay_transactions(arg, context.m_connection_id, context.m_remote_address.get_zone(), relay_method::fluff); + relay_transactions(arg, std::move(fluff_hashes), context.m_connection_id, context.m_remote_address.get_zone(), relay_method::fluff); } return 1; } @@ -1582,6 +1700,26 @@ namespace cryptonote m_block_queue.remove_spans(span_connection_id, start_height); return 1; } + else if (bvc.m_added_to_main_chain) + { + block tmp; + const std::vector* tx_hashes_ptr = nullptr; + if (!pblocks.empty()) + { + tx_hashes_ptr = &pblocks[blockidx].tx_hashes; + } + else + { + crypto::hash ignore; + if (parse_and_validate_block_from_blob(block_entry.block, tmp, &ignore)) + tx_hashes_ptr = &tmp.tx_hashes; + } + if (tx_hashes_ptr) + { + for (const auto &h : *tx_hashes_ptr) + m_request_manager.remove_request(h); + } + } TIME_MEASURE_FINISH(block_process_time); block_process_time_full += block_process_time; @@ -1698,6 +1836,7 @@ skip: m_idle_peer_kicker.do_call(boost::bind(&t_cryptonote_protocol_handler::kick_idle_peers, this)); m_standby_checker.do_call(boost::bind(&t_cryptonote_protocol_handler::check_standby_peers, this)); m_sync_search_checker.do_call(boost::bind(&t_cryptonote_protocol_handler::update_sync_search, this)); + m_peer_tx_request_checker.do_call(boost::bind(&t_cryptonote_protocol_handler::check_tx_request_queue, this)); return m_core.on_idle(); } //------------------------------------------------------------------------------------------------------------------------ @@ -1785,6 +1924,80 @@ skip: } //------------------------------------------------------------------------------------------------------------------------ template + void t_cryptonote_protocol_handler::send_txs_request(cryptonote_connection_context& context, std::vector &&tx_hashes) + { + if (tx_hashes.empty()) + { + MLOG_P2P_MESSAGE("Not sending a request for txs"); + return; + } + + // We *never* expect to call this with more txs than can be sent in a packet + CHECK_AND_ASSERT_MES(tx_hashes.size() <= max_n_txs_per_packet(),, "Too many txs in NOTIFY_REQUEST_TX_POOL_TXS"); + + NOTIFY_REQUEST_TX_POOL_TXS::request req; + req.t = std::move(tx_hashes); + MLOG_P2P_MESSAGE("Requesting " << req.t.size() << " transactions via NOTIFY_REQUEST_TX_POOL_TXS"); + post_notify(req, context); + } + //----------------------------------------------------------------------------------------------------------------------- + template + bool t_cryptonote_protocol_handler::check_tx_request_queue() + { + // We want to check this frequently, so we keep making sure tx requests that aren't in flight get placed in flight. + // At time of writing, I set this to run every 5s, and the default timeout for stale requests is 30s. + + // If we're not synchronized, we shouldn't be requesting any txs. Syncing might end up removing many tx requests + // because the txs enter the chain. + if (!is_synchronized()) + { + MDEBUG("Skipping tx request queue check, not yet synced"); + return true; + } + + MCTRACE("net.p2p.msg", "on_idle :: check_tx_request_queue, starting ..."); + + // Synchronize with handling incoming txs, because that function can take a long time to execute and may be in + // the process of verifying large txs that we requested. We don't want to count request misses that are actually + // good and just take a long time to verify. + std::lock_guard m_check_lock(m_check_tx_request_queue_mutex); + + // We drop connections that exceed the threshold for allowed missed txs + const auto drop_peers = m_request_manager.remove_stale_requests(); + for (const auto &peer_id : drop_peers) + { + MCINFO("net.p2p.msg", "Missed tx request more than threshold of the time, dropping peer : " << epee::string_tools::pod_to_hex(peer_id)); + drop_connection(peer_id); + } + + // Let fly any queued tx requests that our connections can handle. This is the section that benefits from calling + // check_tx_request_queue more frequently than the timeout. Since connections can become able to handle new + // requests frequently as we process incoming txs. + m_p2p->for_each_connection([&](cryptonote_connection_context& context, nodetool::peerid_type _unused, uint32_t _unused2)->bool + { + if (drop_peers.count(context.m_connection_id)) + { + MDEBUG(context << "connection is set to be dropped, not sending more tx requests"); + return true; + } + + // If the connection isn't synced we don't need to send it requests + if (context.m_state != cryptonote_connection_context::state_normal) + { + MDEBUG(context << "not ready for tx request"); + return true; + } + + std::vector new_requests = m_request_manager.fly_available_requests(context.m_connection_id); + this->send_txs_request(context, std::move(new_requests)); + + return true; + }); + + return true; + } + //------------------------------------------------------------------------------------------------------------------------ + template bool t_cryptonote_protocol_handler::check_standby_peers() { m_p2p->for_each_connection([&](cryptonote_connection_context& context, nodetool::peerid_type peer_id, uint32_t support_flags)->bool @@ -2696,14 +2909,14 @@ skip: } //------------------------------------------------------------------------------------------------------------------------ template - bool t_cryptonote_protocol_handler::relay_transactions(NOTIFY_NEW_TRANSACTIONS::request& arg, const boost::uuids::uuid& source, epee::net_utils::zone zone, relay_method tx_relay) + bool t_cryptonote_protocol_handler::relay_transactions(NOTIFY_NEW_TRANSACTIONS::request& arg, std::vector &&tx_hashes, const boost::uuids::uuid& source, epee::net_utils::zone zone, relay_method tx_relay) { /* Push all outgoing transactions to this function. The behavior needs to identify how the transaction is going to be relayed, and then update the local mempool before doing the relay. The code was already updating the DB twice on received transactions - it is difficult to workaround this due to the internal design. */ - return m_p2p->send_txs(std::move(arg.txs), zone, source, tx_relay) != epee::net_utils::zone::invalid; + return m_p2p->send_txs(std::move(arg.txs), std::move(tx_hashes), zone, source, tx_relay) != epee::net_utils::zone::invalid; } //------------------------------------------------------------------------------------------------------------------------ template @@ -2715,6 +2928,9 @@ skip: MERROR("Failed to get txpool hashes"); return false; } + + shrink_to_fit_hashes_container(r.hashes); + MLOG_P2P_MESSAGE("-->>NOTIFY_GET_TXPOOL_COMPLEMENT: hashes.size()=" << r.hashes.size() ); post_notify(r, context); MLOG_PEER_STATE("requesting txpool complement"); @@ -2894,6 +3110,7 @@ skip: } m_block_queue.flush_spans(context.m_connection_id, false); + m_request_manager.remove_peer(context.m_connection_id); MLOG_PEER_STATE("closed"); } diff --git a/src/cryptonote_protocol/cryptonote_protocol_handler_common.h b/src/cryptonote_protocol/cryptonote_protocol_handler_common.h index df9249ea32d..cf26bee56a4 100644 --- a/src/cryptonote_protocol/cryptonote_protocol_handler_common.h +++ b/src/cryptonote_protocol/cryptonote_protocol_handler_common.h @@ -42,7 +42,7 @@ namespace cryptonote { virtual bool is_synchronized() const = 0; virtual bool relay_block(NOTIFY_NEW_FLUFFY_BLOCK::request& arg, cryptonote_connection_context& exclude_context)=0; - virtual bool relay_transactions(NOTIFY_NEW_TRANSACTIONS::request& arg, const boost::uuids::uuid& source, epee::net_utils::zone zone, relay_method tx_relay)=0; + virtual bool relay_transactions(NOTIFY_NEW_TRANSACTIONS::request& arg, std::vector &&tx_hashes, const boost::uuids::uuid& source, epee::net_utils::zone zone, relay_method tx_relay)=0; //virtual bool request_objects(NOTIFY_REQUEST_GET_OBJECTS::request& arg, cryptonote_connection_context& context)=0; }; @@ -59,7 +59,7 @@ namespace cryptonote { return false; } - virtual bool relay_transactions(NOTIFY_NEW_TRANSACTIONS::request& arg, const boost::uuids::uuid& source, epee::net_utils::zone zone, relay_method tx_relay) + virtual bool relay_transactions(NOTIFY_NEW_TRANSACTIONS::request& arg, std::vector &&tx_hashes, const boost::uuids::uuid& source, epee::net_utils::zone zone, relay_method tx_relay) { return false; } diff --git a/src/cryptonote_protocol/levin_notify.cpp b/src/cryptonote_protocol/levin_notify.cpp index f87cf9cfd5f..6294f450812 100644 --- a/src/cryptonote_protocol/levin_notify.cpp +++ b/src/cryptonote_protocol/levin_notify.cpp @@ -49,6 +49,7 @@ #include "cryptonote_protocol/cryptonote_protocol_defs.h" #include "net/dandelionpp.h" #include "p2p/net_node.h" +#include "cryptonote_basic/cryptonote_format_utils.h" #undef MONERO_DEFAULT_LOG_CATEGORY #define MONERO_DEFAULT_LOG_CATEGORY "net.p2p.tx" @@ -169,30 +170,7 @@ namespace levin if (pad) { - size_t bytes = 9 /* header */ + 4 /* 1 + 'txs' */ + tools::get_varint_data(request.txs.size()).size(); - for(auto tx_blob_it = request.txs.begin(); tx_blob_it!=request.txs.end(); ++tx_blob_it) - bytes += tools::get_varint_data(tx_blob_it->size()).size() + tx_blob_it->size(); - - // stuff some dummy bytes in to stay safe from traffic volume analysis - static constexpr const size_t granularity = 1024; - size_t padding = granularity - bytes % granularity; - const size_t overhead = 2 /* 1 + '_' */ + tools::get_varint_data(padding).size(); - if (overhead > padding) - padding = 0; - else - padding -= overhead; - request._ = std::string(padding, ' '); - - epee::byte_slice arg_buff; - epee::serialization::store_t_to_binary(request, arg_buff); - - // we probably lowballed the payload size a bit, so added a but too much. Fix this now. - size_t remove = arg_buff.size() % granularity; - if (remove > request._.size()) - request._.clear(); - else - request._.resize(request._.size() - remove); - // if the size of _ moved enough, we might lose byte in size encoding, we don't care + pad_tx_request(request); } epee::levin::message_writer out; @@ -202,12 +180,31 @@ namespace levin return out; } + epee::levin::message_writer make_tx_hash_message(std::vector&& txs) + { + NOTIFY_TX_POOL_HASH::request request{}; + request.t = std::move(txs); + + epee::levin::message_writer out; + if (!epee::serialization::store_t_to_binary(request, out.buffer)) + throw std::runtime_error{"Failed to serialize to epee binary format"}; + + return out; + } + bool make_payload_send_txs(connections& p2p, std::vector&& txs, const boost::uuids::uuid& destination, const bool pad, const bool fluff) { epee::byte_slice blob = make_tx_message(std::move(txs), pad, fluff).finalize_notify(NOTIFY_NEW_TRANSACTIONS::ID); return p2p.send(std::move(blob), destination); } + // TODO: do we want to pad to avoid revealing definitive n txs in the req? + bool make_payload_send_txs_relay_v2(connections& p2p, std::vector&& txs, const boost::uuids::uuid& destination) + { + epee::byte_slice blob = make_tx_hash_message(std::move(txs)).finalize_notify(NOTIFY_TX_POOL_HASH::ID); + return p2p.send(std::move(blob), destination); + } + /* The current design uses `asio::strand`s. The documentation isn't as clear as it should be - a `strand` has an internal `mutex` and `bool`. The `mutex` synchronizes thread access and the `bool` is set when a thread is @@ -291,8 +288,10 @@ namespace levin boost::asio::io_context::strand strand; struct context_t { std::vector fluff_txs; + std::vector fluff_txs_v2; std::chrono::steady_clock::time_point flush_time; bool m_is_income; + bool tx_relay_v2; }; boost::unordered_map contexts; net::dandelionpp::connection_map map;//!< Tracks outgoing uuid's for noise channels or Dandelion++ stems @@ -371,17 +370,24 @@ namespace levin const auto now = std::chrono::steady_clock::now(); auto next_flush = std::chrono::steady_clock::time_point::max(); std::vector, boost::uuids::uuid>> connections{}; + std::vector, boost::uuids::uuid>> connections_relay_v2{}; for (auto &e: zone_->contexts) { auto &id = e.first; auto &context = e.second; - if (!context.fluff_txs.empty()) + if (!context.fluff_txs.empty() + || !context.fluff_txs_v2.empty()) { if (context.flush_time <= now || timer_error) // flush on canceled timer { context.flush_time = std::chrono::steady_clock::time_point::max(); - connections.emplace_back(std::move(context.fluff_txs), id); - context.fluff_txs.clear(); + if (context.tx_relay_v2) { + connections_relay_v2.emplace_back(std::move(context.fluff_txs_v2), id); + context.fluff_txs_v2.clear(); + } else { + connections.emplace_back(std::move(context.fluff_txs), id); + context.fluff_txs.clear(); + } } else // not flushing yet next_flush = std::min(next_flush, context.flush_time); @@ -395,6 +401,16 @@ namespace levin network is therefore replacing the sybil protection of Dandelion++. Dandelion++ stem phase over i2p/tor is also worth investigating (with/without "noise"?). */ + for (auto& connection : connections_relay_v2) + { + sort(connection.first.begin(), connection.first.end(), + [](const crypto::hash &lhs, const crypto::hash &rhs) { + return std::memcmp(&lhs, &rhs, sizeof(lhs)) < 0; + }); + connection.first.erase(unique(connection.first.begin(), connection.first.end()), connection.first.end()); + make_payload_send_txs_relay_v2(*zone_->p2p, std::move(connection.first), connection.second); + } + for (auto& connection : connections) { std::sort(connection.first.begin(), connection.first.end()); // don't leak receive order @@ -416,14 +432,15 @@ namespace levin { std::shared_ptr zone_; std::vector txs_; + std::vector tx_hashes_; boost::uuids::uuid source_; void operator()() { - run(std::move(zone_), epee::to_span(txs_), source_); + run(std::move(zone_), epee::to_span(txs_), epee::to_span(tx_hashes_), source_); } - static void run(std::shared_ptr zone, epee::span txs, const boost::uuids::uuid& source) + static void run(std::shared_ptr zone, epee::span txs, epee::span tx_hashes, const boost::uuids::uuid& source) { if (!zone || !zone->p2p || txs.empty()) return; @@ -449,8 +466,13 @@ namespace levin context.flush_time = now + (context.m_is_income ? in_duration() : out_duration()); next_flush = std::min(next_flush, context.flush_time); - context.fluff_txs.reserve(context.fluff_txs.size() + txs.size()); - context.fluff_txs.insert(context.fluff_txs.end(), txs.begin(), txs.end()); + if (context.tx_relay_v2) { + context.fluff_txs_v2.reserve(context.fluff_txs_v2.size() + tx_hashes.size()); + context.fluff_txs_v2.insert(context.fluff_txs_v2.end(), tx_hashes.begin(), tx_hashes.end()); + } else { + context.fluff_txs.reserve(context.fluff_txs.size() + txs.size()); + context.fluff_txs.insert(context.fluff_txs.end(), txs.begin(), txs.end()); + } } } @@ -545,6 +567,7 @@ namespace levin std::shared_ptr zone_; i_core_events* core_; std::vector txs_; + std::vector tx_hashes_; boost::uuids::uuid source_; relay_method tx_relay; @@ -576,7 +599,7 @@ namespace levin } core_->on_transactions_relayed(epee::to_span(txs_), relay_method::fluff); - fluff_notify::run(std::move(zone_), epee::to_span(txs_), source_); + fluff_notify::run(std::move(zone_), epee::to_span(txs_), epee::to_span(tx_hashes_), source_); } }; @@ -767,17 +790,19 @@ namespace levin ); } - void notify::on_handshake_complete(const boost::uuids::uuid &id, bool is_income) + void notify::on_handshake_complete(const boost::uuids::uuid &id, bool is_income, bool tx_relay_v2) { if (!zone_) return; auto& zone = zone_; - boost::asio::dispatch(zone_->strand, [zone, id, is_income] { + boost::asio::dispatch(zone_->strand, [zone, id, is_income, tx_relay_v2] { zone->contexts[id] = { .fluff_txs = {}, + .fluff_txs_v2 = {}, .flush_time = std::chrono::steady_clock::time_point::max(), .m_is_income = is_income, + .tx_relay_v2 = tx_relay_v2, }; }); } @@ -816,7 +841,7 @@ namespace levin zone_->flush_txs.cancel(); } - bool notify::send_txs(std::vector txs, const boost::uuids::uuid& source, relay_method tx_relay) + bool notify::send_txs(std::vector txs, std::vector &&tx_hashes, const boost::uuids::uuid& source, relay_method tx_relay) { if (txs.empty()) return true; @@ -824,6 +849,8 @@ namespace levin if (!zone_) return false; + CHECK_AND_ASSERT_MES(txs.size() == tx_hashes.size(), false, "Mismatch size of txs <> tx_hashes in send_txs"); + /* If noise is enabled in a zone, it always takes precedence. The technique provides good protection against ISP adversaries, but not sybil adversaries. Noise is currently only enabled over I2P/Tor - those @@ -885,7 +912,7 @@ namespace levin // this will change a local/forward tx to stem or fluff ... boost::asio::dispatch( zone_->strand, - dandelionpp_notify{zone_, core_, std::move(txs), source, tx_relay} + dandelionpp_notify{zone_, core_, std::move(txs), std::move(tx_hashes), source, tx_relay} ); break; } @@ -897,7 +924,7 @@ namespace levin ipv4/6. Marking it as "fluff" here will make the tx immediately visible externally from this node, which is not desired. */ core_->on_transactions_relayed(epee::to_span(txs), tx_relay); - boost::asio::dispatch(zone_->strand, fluff_notify{zone_, std::move(txs), source}); + boost::asio::dispatch(zone_->strand, fluff_notify{zone_, std::move(txs), std::move(tx_hashes), source}); break; } } diff --git a/src/cryptonote_protocol/levin_notify.h b/src/cryptonote_protocol/levin_notify.h index 4b07924a69f..f9e09e35bb9 100644 --- a/src/cryptonote_protocol/levin_notify.h +++ b/src/cryptonote_protocol/levin_notify.h @@ -39,6 +39,7 @@ #include "cryptonote_protocol/fwd.h" #include "net/enums.h" #include "span.h" +#include "crypto/hash.h" namespace epee { @@ -102,7 +103,7 @@ namespace levin //! Probe for new outbound connection - skips if not needed. void new_out_connection(); - void on_handshake_complete(const boost::uuids::uuid &id, bool is_income); + void on_handshake_complete(const boost::uuids::uuid &id, bool is_income, bool tx_relay_v2); void on_connection_close(const boost::uuids::uuid &id); //! Run the logic for the next epoch immediately. Only use in testing. @@ -124,12 +125,13 @@ namespace levin enabled. \param txs The transactions that need to be serialized and relayed. + \param tx_hashes The hashes of all txs. \param source The source of the notification. `is_nil()` indicates this node is the source. Dandelion++ will use this to map a source to a particular stem. \return True iff the notification is queued for sending. */ - bool send_txs(std::vector txs, const boost::uuids::uuid& source, relay_method tx_relay); + bool send_txs(std::vector txs, std::vector &&tx_hashes, const boost::uuids::uuid& source, relay_method tx_relay); }; } // levin } // net diff --git a/src/cryptonote_protocol/request_manager.cpp b/src/cryptonote_protocol/request_manager.cpp new file mode 100644 index 00000000000..f264d7ff304 --- /dev/null +++ b/src/cryptonote_protocol/request_manager.cpp @@ -0,0 +1,186 @@ +// Copyright (c) 2014-2025, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list +// of conditions and the following disclaimer in the documentation and/or +// other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors +// may be +// used to endorse or promote products derived from this software without +// specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + + +#include "cryptonote_protocol/request_manager.h" +#include "cryptonote_protocol/txrequestqueue.h" +#include "misc_log_ex.h" +#include +#include + +void request_manager::remove_peer(const boost::uuids::uuid &peer_id) { + MINFO("Removing all requests for disconnected peer: " << epee::string_tools::pod_to_hex(peer_id)); + std::lock_guard lock(m_mutex); + auto& by_peer = get_requests_by_peer_id(m_requested_txs); + auto peer_range = by_peer.equal_range(peer_id); + if (peer_range.first != peer_range.second) { + size_t removed = std::distance(peer_range.first, peer_range.second); + by_peer.erase(peer_range.first, peer_range.second); + MINFO("Removed " << removed << " requests for peer " << epee::string_tools::pod_to_hex(peer_id)); + } + m_connection_stats.erase(peer_id); +} + +std::unordered_set request_manager::remove_stale_requests() { + std::lock_guard lock(m_mutex); + std::unordered_set drop_peers_out; + size_t removed = 0; + auto now = std::chrono::steady_clock::now(); + for (auto it = m_requested_txs.begin(); it != m_requested_txs.end();) { + // Stale means it's been in flight for longer than the allowed timeout + const auto elapsed = std::chrono::duration_cast(now - it->last_action_timestamp); + if (!it->in_flight || elapsed.count() < m_request_timeout) { + ++it; + continue; + } + + MINFO("Removing stale request for tx " << it->tx_hash + << " from peer " << epee::string_tools::pod_to_hex(it->peer_id) + << ", age: " << elapsed.count() << "ms"); + + // If this peer has missed too many requests, we want to drop it + if (this->missed_request(it->peer_id, 1)) { + drop_peers_out.insert(it->peer_id); + } + + it = m_requested_txs.erase(it); + ++removed; + } + + MINFO("Cleaned up " << removed << " stale requests"); + return drop_peers_out; +} + +bool request_manager::add_request(const crypto::hash &tx_hash, const boost::uuids::uuid &peer_id) { + MINFO("Requesting from peer: " << epee::string_tools::pod_to_hex(peer_id) << " the transaction: " << tx_hash); + std::lock_guard lock(m_mutex); + + auto& by_peer_and_tx = get_requests_by_peer_and_tx(m_requested_txs); + auto it = by_peer_and_tx.find(boost::make_tuple(peer_id, tx_hash)); + + const bool in_queue = it != by_peer_and_tx.end(); + const bool let_it_fly = m_connection_stats[peer_id].in_flight_requests < m_max_in_flight + && !this->request_is_in_flight(tx_hash); + + if (in_queue && !let_it_fly) { + // already have this peer for this tx, we can't process additional reqs at this time + MDEBUG("Peer " << epee::string_tools::pod_to_hex(peer_id) + << " already in request queue for tx " << tx_hash); + return false; + } + + if (!in_queue) { + // Add the new request! + m_requested_txs.insert(tx_request(peer_id, tx_hash, let_it_fly)); + } else if (let_it_fly) { + // let_it_fly should always be true here + it->fly(); + } + + if (let_it_fly) { + ++m_connection_stats[peer_id].n_total_requests; + ++m_connection_stats[peer_id].in_flight_requests; + } + + return let_it_fly; +} + +std::vector request_manager::fly_available_requests(const boost::uuids::uuid &peer_id) { + std::lock_guard lock(m_mutex); + + std::vector tx_hashes; + auto& by_peer = get_requests_by_peer_id(m_requested_txs); + auto range = by_peer.equal_range(peer_id); + for (auto it = range.first; it != range.second; ++it) + { + if (m_connection_stats[peer_id].in_flight_requests >= m_max_in_flight) + break; + if (it->in_flight || this->request_is_in_flight(it->tx_hash)) + continue; + + it->fly(); + + ++m_connection_stats[peer_id].n_total_requests; + ++m_connection_stats[peer_id].in_flight_requests; + + tx_hashes.push_back(it->tx_hash); + } + return tx_hashes; +} + +bool request_manager::remove_request(const crypto::hash &tx_hash) { + std::lock_guard lock(m_mutex); + auto& by_tx_hash = get_requests_by_tx_hash(m_requested_txs); + auto range = by_tx_hash.equal_range(tx_hash); + if (range.first == range.second) { + MDEBUG("No requests for tx " << tx_hash); + return false; + } + for (auto it = range.first; it != range.second;) { + const boost::uuids::uuid &peer_id = it->peer_id; + MDEBUG("Removing tx request " << it->tx_hash << " for peer " << epee::string_tools::pod_to_hex(peer_id)); + if (it->in_flight && m_connection_stats[peer_id].in_flight_requests > 0) + { + --m_connection_stats[peer_id].in_flight_requests; + MINFO("Decremented in_flight_requests count for peer: " << epee::string_tools::pod_to_hex(peer_id) << ", current in_flight_requests: " << m_connection_stats[peer_id].in_flight_requests); + } + it = by_tx_hash.erase(it); + } + return true; +} + +bool request_manager::missed_request(const boost::uuids::uuid &peer_id, const std::size_t n_missed_reqs) { + std::lock_guard lock(m_mutex); + if ((m_connection_stats[peer_id].missed + n_missed_reqs) > m_connection_stats[peer_id].missed) + m_connection_stats[peer_id].missed += n_missed_reqs; + const size_t n_total_reqs = m_connection_stats[peer_id].n_total_requests; + + // Return false if we don't have enough samples + if (n_total_reqs < P2P_MIN_SAMPLE_SIZE_FOR_DROPPING) return false; + + const size_t percent = (m_connection_stats[peer_id].missed * 100) / n_total_reqs; + MINFO("Peer " << epee::string_tools::pod_to_hex(peer_id) + << " has missed " << m_connection_stats[peer_id].missed << " out of " + << n_total_reqs << " total requests (" << percent << "%)"); + return percent > P2P_REQUEST_FAILURE_THRESHOLD_PERCENTAGE; +} + +bool request_manager::request_is_in_flight(const crypto::hash &tx_hash) const { + std::lock_guard lock(m_mutex); + auto& by_tx_hash = get_requests_by_tx_hash(m_requested_txs); + auto range = by_tx_hash.equal_range(tx_hash); + for (auto it = range.first; it != range.second; ++it) + if (it->in_flight) + return true; + return false; +} diff --git a/src/cryptonote_protocol/request_manager.h b/src/cryptonote_protocol/request_manager.h new file mode 100644 index 00000000000..5c92b0d34aa --- /dev/null +++ b/src/cryptonote_protocol/request_manager.h @@ -0,0 +1,107 @@ +// Copyright (c) 2014-2025, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list +// of conditions and the following disclaimer in the documentation and/or +// other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors +// may be +// used to endorse or promote products derived from this software without +// specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +#ifndef CRYPTONOTE_PROTOCOL_REQUEST_MANAGER_H +#define CRYPTONOTE_PROTOCOL_REQUEST_MANAGER_H + +#include "crypto/hash.h" +#include "string_tools.h" +#include "txrequestqueue.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class request_manager { + +private: + request_container m_requested_txs; + mutable std::recursive_mutex m_mutex; + + const std::size_t m_max_in_flight; + const int64_t m_request_timeout; + + request_manager(const request_manager &) = delete; + request_manager &operator=(const request_manager &) = delete; + request_manager(request_manager &&) noexcept = delete; + request_manager &operator=(request_manager &&) = delete; + +private: + struct connection_statistics + { + std::atomic n_total_requests = 0; + std::atomic missed = 0; + std::atomic in_flight_requests = 0; + }; + std::unordered_map m_connection_stats; + +public: + request_manager(const std::size_t max_in_flight, + const int64_t request_timeout = P2P_DEFAULT_REQUEST_TIMEOUT) + : m_requested_txs(), + m_mutex(), + m_connection_stats(), + m_max_in_flight(max_in_flight), + m_request_timeout(request_timeout) + {}; + + void remove_peer(const boost::uuids::uuid &peer_id); + + // Returns the set of peers to drop because they've missed too many requests + std::unordered_set remove_stale_requests(); + + // Return true if the request should be sent over the connection + bool add_request(const crypto::hash &tx_hash, const boost::uuids::uuid &id); + + // Remove current in-flight request for a transaction, if present + // true: found, false: not found or none in-flight + bool remove_request(const crypto::hash &tx_hash); + + // Returns the vector of tx hashes to request + std::vector fly_available_requests(const boost::uuids::uuid &peer_id); + +private: + // Return true if we should drop the peer because it exceeded threshold for allowed missed reqs + bool missed_request(const boost::uuids::uuid &peer_id, const std::size_t n_missed_reqs = 1); + + // Return true if *any* peer has the provided tx hash request in flight + bool request_is_in_flight(const crypto::hash &tx_hash) const; +}; + +#endif // CRYPTONOTE_PROTOCOL_REQUEST_MANAGER_H diff --git a/src/cryptonote_protocol/txrequestqueue.h b/src/cryptonote_protocol/txrequestqueue.h new file mode 100644 index 00000000000..bd70352ad14 --- /dev/null +++ b/src/cryptonote_protocol/txrequestqueue.h @@ -0,0 +1,111 @@ +// Copyright (c) 2014-2025, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list +// of conditions and the following disclaimer in the documentation and/or +// other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors +// may be +// used to endorse or promote products derived from this software without +// specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +#ifndef CRYPTONOTE_PROTOCOL_TXREQUESTQUEUE_H +#define CRYPTONOTE_PROTOCOL_TXREQUESTQUEUE_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "misc_log_ex.h" +#include "string_tools.h" +#include "syncobj.h" +#include "common/util.h" + + +struct tx_request +{ + boost::uuids::uuid peer_id; + crypto::hash tx_hash; + mutable std::chrono::steady_clock::time_point last_action_timestamp; + mutable bool in_flight = false; + + tx_request(const boost::uuids::uuid& _peer_id, + const crypto::hash& _tx_hash, + const bool _in_flight): + peer_id(_peer_id), + tx_hash(_tx_hash), + last_action_timestamp(std::chrono::steady_clock::now()), + in_flight(_in_flight) + {} + +public: + void fly() const { in_flight = true; last_action_timestamp = std::chrono::steady_clock::now(); }; +}; + +using boost::multi_index::hashed_non_unique; +using boost::multi_index::hashed_unique; +using boost::multi_index::indexed_by; +using boost::multi_index::member; +using boost::multi_index::multi_index_container; +using boost::multi_index::composite_key; + +typedef multi_index_container< + tx_request, + indexed_by< + // Index 0: by peer_id - all requests for a peer + hashed_non_unique>, + // Index 1: by tx_hash - all requests for a tx + hashed_non_unique>, + // Index 2: by (peer_id, tx_hash) - unique requests + hashed_unique, + member + >> + > +> request_container; + +template +decltype(auto) get_requests_by_peer_id(container_t&& container) { + return std::forward(container).template get<0>(); +} + +template +decltype(auto) get_requests_by_tx_hash(container_t&& container) { + return std::forward(container).template get<1>(); +} + +template +decltype(auto) get_requests_by_peer_and_tx(container_t&& container) { + return std::forward(container).template get<2>(); +} + +#endif // CRYPTONOTE_PROTOCOL_TXREQUESTQUEUE_H diff --git a/src/device/device_cold.hpp b/src/device/device_cold.hpp index f6a55f8100c..c113c526f26 100644 --- a/src/device/device_cold.hpp +++ b/src/device/device_cold.hpp @@ -30,15 +30,32 @@ #ifndef MONERO_DEVICE_COLD_H #define MONERO_DEVICE_COLD_H -#include "wallet/wallet2.h" +#include "cryptonote_basic/cryptonote_basic_impl.h" + #include #include +// fwd +namespace wallet2_basic +{ +struct transfer_details; +} //namespace wallet2_basic +namespace tools +{ +namespace wallet +{ +namespace cold +{ +struct UnsignedPreCarrotTransactionSet; +struct SignedFullTransactionSet; +} //namespace cold +} //namespace wallet +} //namespace tools namespace hw { typedef struct wallet_shim { - boost::function get_tx_pub_key_from_received_outs; + boost::function get_tx_pub_key_from_received_outs; } wallet_shim; class tx_aux_data { @@ -106,15 +123,15 @@ namespace hw { * Key image sync with the cold protocol. */ virtual void ki_sync(wallet_shim * wallet, - const std::vector<::tools::wallet2::transfer_details> & transfers, + const std::vector<::wallet2_basic::transfer_details> & transfers, exported_key_image & ski) =0; /** * Signs unsigned transaction with the cold protocol. */ virtual void tx_sign(wallet_shim * wallet, - const ::tools::wallet2::unsigned_tx_set & unsigned_tx, - ::tools::wallet2::signed_tx_set & signed_tx, + const ::tools::wallet::cold::UnsignedPreCarrotTransactionSet & unsigned_tx, + ::tools::wallet::cold::SignedFullTransactionSet& signed_tx, tx_aux_data & aux_data) =0; /** diff --git a/src/device_trezor/device_trezor.cpp b/src/device_trezor/device_trezor.cpp index 7feacc0e869..fe40d56efe5 100644 --- a/src/device_trezor/device_trezor.cpp +++ b/src/device_trezor/device_trezor.cpp @@ -310,7 +310,7 @@ namespace trezor { } void device_trezor::ki_sync(wallet_shim * wallet, - const std::vector & transfers, + const std::vector & transfers, hw::device_cold::exported_key_image & ski) { #define EVENT_PROGRESS(P) do { if (m_callback) {(m_callback)->on_progress(device_cold::op_progress(P)); } }while(0) @@ -507,8 +507,8 @@ namespace trezor { } void device_trezor::tx_sign(wallet_shim * wallet, - const tools::wallet2::unsigned_tx_set & unsigned_tx, - tools::wallet2::signed_tx_set & signed_tx, + const tools::wallet::cold::UnsignedPreCarrotTransactionSet & unsigned_tx, + tools::wallet::cold::SignedFullTransactionSet & signed_tx, hw::tx_aux_data & aux_data) { CHECK_AND_ASSERT_THROW_MES(std::get<0>(unsigned_tx.transfers) == 0, "Unsupported non zero offset"); @@ -540,7 +540,6 @@ namespace trezor { cpend.fee = cpend.tx.rct_signatures.txnFee; cpend.dust_added_to_fee = false; cpend.change_dts = cdata.tx_data.change_dts; - cpend.selected_transfers = cdata.tx_data.selected_transfers; cpend.key_images = ""; cpend.dests = cdata.tx_data.dests; cpend.construction_data = cdata.tx_data; @@ -595,7 +594,7 @@ namespace trezor { } void device_trezor::tx_sign(wallet_shim * wallet, - const tools::wallet2::unsigned_tx_set & unsigned_tx, + const tools::wallet::cold::UnsignedPreCarrotTransactionSet & unsigned_tx, size_t idx, hw::tx_aux_data & aux_data, std::shared_ptr & signer) @@ -613,7 +612,7 @@ namespace trezor { CHECK_AND_ASSERT_THROW_MES(idx < unsigned_tx.txes.size(), "Invalid transaction index"); signer = std::make_shared(wallet, &unsigned_tx, idx, &aux_data); - const tools::wallet2::tx_construction_data & cur_tx = unsigned_tx.txes[idx]; + const tools::wallet::PreCarrotTransactionProposal & cur_tx = unsigned_tx.txes[idx]; unsigned long num_sources = cur_tx.sources.size(); unsigned long num_outputs = cur_tx.splitted_dsts.size(); @@ -712,7 +711,7 @@ namespace trezor { return client_version; } - void device_trezor::transaction_versions_check(const ::tools::wallet2::unsigned_tx_set & unsigned_tx, hw::tx_aux_data & aux_data) + void device_trezor::transaction_versions_check(const ::tools::wallet::cold::UnsignedPreCarrotTransactionSet & unsigned_tx, hw::tx_aux_data & aux_data) { unsigned cversion = client_version(); diff --git a/src/device_trezor/device_trezor.hpp b/src/device_trezor/device_trezor.hpp index aef7e52598b..67b5526fa79 100644 --- a/src/device_trezor/device_trezor.hpp +++ b/src/device_trezor/device_trezor.hpp @@ -68,7 +68,7 @@ namespace trezor { size_t m_num_transations_to_sign; unsigned client_version(); - void transaction_versions_check(const ::tools::wallet2::unsigned_tx_set & unsigned_tx, hw::tx_aux_data & aux_data); + void transaction_versions_check(const ::tools::wallet::cold::UnsignedPreCarrotTransactionSet & unsigned_tx, hw::tx_aux_data & aux_data); void transaction_pre_check(std::shared_ptr init_msg); void transaction_check(const protocol::tx::TData & tdata, const hw::tx_aux_data & aux_data); void device_state_initialize_unsafe() override; @@ -80,7 +80,7 @@ namespace trezor { * Signs particular transaction idx in the unsigned set, keeps state in the signer */ virtual void tx_sign(wallet_shim * wallet, - const ::tools::wallet2::unsigned_tx_set & unsigned_tx, + const ::tools::wallet::cold::UnsignedPreCarrotTransactionSet & unsigned_tx, size_t idx, hw::tx_aux_data & aux_data, std::shared_ptr & signer); @@ -156,7 +156,7 @@ namespace trezor { * Key image sync with the Trezor. */ void ki_sync(wallet_shim * wallet, - const std::vector<::tools::wallet2::transfer_details> & transfers, + const std::vector<::wallet2_basic::transfer_details> & transfers, hw::device_cold::exported_key_image & ski) override; bool is_live_refresh_supported() const override; @@ -202,8 +202,8 @@ namespace trezor { * Signs unsigned transaction with the Trezor. */ void tx_sign(wallet_shim * wallet, - const ::tools::wallet2::unsigned_tx_set & unsigned_tx, - ::tools::wallet2::signed_tx_set & signed_tx, + const ::tools::wallet::cold::UnsignedPreCarrotTransactionSet & unsigned_tx, + ::tools::wallet::cold::SignedFullTransactionSet & signed_tx, hw::tx_aux_data & aux_data) override; /** diff --git a/src/device_trezor/trezor/protocol.cpp b/src/device_trezor/trezor/protocol.cpp index e70122b799a..52f02be3b6d 100644 --- a/src/device_trezor/trezor/protocol.cpp +++ b/src/device_trezor/trezor/protocol.cpp @@ -145,7 +145,7 @@ namespace chacha { namespace ki { bool key_image_data(wallet_shim * wallet, - const std::vector & transfers, + const std::vector & transfers, std::vector & res) { for(auto & td : transfers){ @@ -189,7 +189,7 @@ namespace ki { } void generate_commitment(std::vector & mtds, - const std::vector & transfers, + const std::vector & transfers, std::shared_ptr & req) { req = std::make_shared(); @@ -441,7 +441,7 @@ namespace tx { void Signer::set_tx_input(MoneroTransactionSourceEntry * dst, size_t idx, bool need_ring_keys, bool need_ring_indices){ const cryptonote::tx_source_entry & src = cur_tx().sources[idx]; - const tools::wallet2::transfer_details & transfer = get_source_transfer(idx); + const wallet2_basic::transfer_details & transfer = get_source_transfer(idx); dst->set_real_output(src.real_output); for(size_t i = 0; i < src.outputs.size(); ++i){ diff --git a/src/device_trezor/trezor/protocol.hpp b/src/device_trezor/trezor/protocol.hpp index 4e97a67c0e6..73bd0b902c2 100644 --- a/src/device_trezor/trezor/protocol.hpp +++ b/src/device_trezor/trezor/protocol.hpp @@ -34,7 +34,7 @@ #include "device/device_cold.hpp" #include "messages_map.hpp" #include "transport.hpp" -#include "wallet/wallet2.h" +#include "wallet/hot_cold.h" namespace hw{ namespace trezor{ @@ -115,7 +115,7 @@ namespace ki { * Converts transfer details to the MoneroTransferDetails required for KI sync */ bool key_image_data(wallet_shim * wallet, - const std::vector & transfers, + const std::vector & transfers, std::vector & res); /** @@ -127,7 +127,7 @@ namespace ki { * Generates KI sync request with commitments computed. */ void generate_commitment(std::vector & mtds, - const std::vector & transfers, + const std::vector & transfers, std::shared_ptr & req); /** @@ -151,8 +151,8 @@ namespace tx { using MoneroRctKey = messages::monero::MoneroTransactionSourceEntry_MoneroOutputEntry_MoneroRctKeyPublic; using MoneroRsigData = messages::monero::MoneroTransactionRsigData; - using tx_construction_data = tools::wallet2::tx_construction_data; - using unsigned_tx_set = tools::wallet2::unsigned_tx_set; + using tx_construction_data = tools::wallet::PreCarrotTransactionProposal; + using unsigned_tx_set = tools::wallet::cold::UnsignedPreCarrotTransactionSet; void translate_address(MoneroAccountPublicAddress * dst, const cryptonote::account_public_address * src); void translate_dst_entry(MoneroTransactionDestinationEntry * dst, const cryptonote::tx_destination_entry * src); @@ -229,12 +229,12 @@ namespace tx { return m_ct.tx_data; } - const tools::wallet2::transfer_details & get_transfer(size_t idx) const { + const wallet2_basic::transfer_details & get_transfer(size_t idx) const { CHECK_AND_ASSERT_THROW_MES(idx < std::get<2>(m_unsigned_tx->transfers).size() + std::get<0>(m_unsigned_tx->transfers) && idx >= std::get<0>(m_unsigned_tx->transfers), "Invalid transfer index"); return std::get<2>(m_unsigned_tx->transfers)[idx - std::get<0>(m_unsigned_tx->transfers)]; } - const tools::wallet2::transfer_details & get_source_transfer(size_t idx) const { + const wallet2_basic::transfer_details & get_source_transfer(size_t idx) const { const auto & sel_transfers = cur_tx().selected_transfers; CHECK_AND_ASSERT_THROW_MES(idx < m_ct.source_permutation.size(), "Invalid source index - permutation"); CHECK_AND_ASSERT_THROW_MES(m_ct.source_permutation[idx] < sel_transfers.size(), "Invalid source index"); diff --git a/src/fcmp_pp/CMakeLists.txt b/src/fcmp_pp/CMakeLists.txt new file mode 100644 index 00000000000..46724c20750 --- /dev/null +++ b/src/fcmp_pp/CMakeLists.txt @@ -0,0 +1,76 @@ +# Copyright (c) 2024, The Monero Project +# +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, are +# permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this list of +# conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, this list +# of conditions and the following disclaimer in the documentation and/or other +# materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors may be +# used to endorse or promote products derived from this software without specific +# prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +# THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +set(fcmp_pp_sources + curve_trees.cpp + fcmp_pp_crypto.cpp + fcmp_pp_types.cpp + proof_len.cpp + prove.cpp + tower_cycle.cpp + tree_cache.cpp) + +monero_find_all_headers(fcmp_pp_headers "${CMAKE_CURRENT_SOURCE_DIR}") + +add_subdirectory(fcmp_pp_rust) + +monero_add_library_with_deps( + NAME fcmp_pp + DEPENDS fcmp_pp_rust + SOURCES + ${fcmp_pp_sources} + ${fcmp_pp_headers}) + +if(WIN32) + set(EXTRA_RUST_LIBRARIES ws2_32 ntdll userenv) +else() + set(EXTRA_RUST_LIBRARIES ) +endif() + +target_link_libraries(fcmp_pp + PUBLIC + cncrypto + common + epee + PRIVATE + ${Boost_SERIALIZATION_LIBRARY} + ${CMAKE_CURRENT_BINARY_DIR}/fcmp_pp_rust/libfcmp_pp_rust.a + ${EXTRA_LIBRARIES} + ${EXTRA_RUST_LIBRARIES}) + +# Try compiling C file ffi_api_c_compat.c which includes fcmp++.h, and throw an error if C compilation fails +try_compile(FFI_API_C_COMPAT_RES + "${CMAKE_CURRENT_BINARY_DIR}/fcmp_ffi_c" + "${CMAKE_CURRENT_SOURCE_DIR}/ffi_api_c_compat.c" + CMAKE_FLAGS -DCMAKE_C_STANDARD=11 + COMPILE_DEFINITIONS -I"${CMAKE_CURRENT_SOURCE_DIR}/fcmp_pp_rust" + OUTPUT_VARIABLE FFI_API_C_COMPAT_OUTPUT) +if(NOT FFI_API_C_COMPAT_RES) + message(SEND_ERROR "Error compiling ffi_api_c_compat.c: ${FFI_API_C_COMPAT_OUTPUT}") + message(FATAL_ERROR "The FCMP++ FFI API header 'fcmp++.h' has broken compatibility with C") +endif() diff --git a/src/fcmp_pp/curve_trees.cpp b/src/fcmp_pp/curve_trees.cpp new file mode 100644 index 00000000000..820c329fb10 --- /dev/null +++ b/src/fcmp_pp/curve_trees.cpp @@ -0,0 +1,1884 @@ +// Copyright (c) 2024, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "curve_trees.h" + +#include "common/threadpool.h" +#include "fcmp_pp_crypto.h" +#include "profile_tools.h" +#include "string_tools.h" + +#include + +namespace +{ + // Struct composed of ec elems needed to get a full-fledged leaf tuple + struct PreLeafTuple final + { + fcmp_pp::EdDerivatives O_derivatives; + fcmp_pp::EdDerivatives I_derivatives; + fcmp_pp::EdDerivatives C_derivatives; + }; +} + +namespace fcmp_pp +{ +namespace curve_trees +{ +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +// Instantiate the tower cycle types +template class CurveTrees; +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +// Public helper functions +//---------------------------------------------------------------------------------------------------------------------- +template +typename C::Point get_new_parent(const std::unique_ptr &curve, const typename C::Chunk &new_children) +{ + for (std::size_t i = 0; i < new_children.len; ++i) + MTRACE("Hashing " << curve->to_string(new_children.buf[i])); + + return curve->hash_grow( + curve->hash_init_point(), + 0,/*offset*/ + curve->zero_scalar(), + new_children + ); +}; +template Selene::Point get_new_parent(const std::unique_ptr &curve, + const typename Selene::Chunk &new_children); +template Helios::Point get_new_parent(const std::unique_ptr &curve, + const typename Helios::Chunk &new_children); +//---------------------------------------------------------------------------------------------------------------------- +OutputTuple output_to_tuple(const OutputPair &output_pair, bool use_fast_check) +{ + const crypto::public_key &output_pubkey = output_pubkey_cref(output_pair); + const crypto::ec_point &commitment = commitment_cref(output_pair); + + crypto::ec_point O = output_pubkey; + crypto::ec_point C = commitment; + + // If the output has already been checked for torsion, then we don't need to clear torsion here + if (!output_checked_for_torsion(output_pair)) + { + TIME_MEASURE_NS_START(clear_torsion_ns); + + // Clear torsion on output if it wasn't already checked for torsion + if (!use_fast_check) + { + if (!fcmp_pp::get_valid_torsion_cleared_point(output_pubkey, O)) + throw std::runtime_error("O is invalid for insertion to tree"); + if (!fcmp_pp::get_valid_torsion_cleared_point(commitment, C)) + throw std::runtime_error("C is invalid for insertion to tree"); + } + else + { + if (!fcmp_pp::get_valid_torsion_cleared_point_fast(output_pubkey, O)) + throw std::runtime_error("O is invalid for insertion to tree"); + if (!fcmp_pp::get_valid_torsion_cleared_point_fast(commitment, C)) + throw std::runtime_error("C is invalid for insertion to tree"); + } + + if (O != output_pubkey) + LOG_PRINT_L2("Output pubkey has torsion: " << output_pubkey); + if (C != commitment) + LOG_PRINT_L2("Commitment has torsion: " << commitment); + + TIME_MEASURE_NS_FINISH(clear_torsion_ns); + + LOG_PRINT_L3("clear_torsion_ns: " << clear_torsion_ns); + } + +#if !defined(NDEBUG) + { + // Debug build safety checks + crypto::ec_point O_debug; + crypto::ec_point C_debug; + assert(fcmp_pp::get_valid_torsion_cleared_point(output_pubkey, O_debug)); + assert(fcmp_pp::get_valid_torsion_cleared_point(commitment, C_debug)); + assert(O == O_debug); + assert(C == C_debug); + } +#endif + + // Redundant check for safety + if (O == crypto::EC_I) + throw std::runtime_error("O cannot equal identity"); + if (C == crypto::EC_I) + throw std::runtime_error("C cannot equal identity"); + + TIME_MEASURE_NS_START(derive_key_image_generator_ns); + + // Derive key image generator using original output pubkey + crypto::ec_point I; + crypto::derive_key_image_generator(output_pubkey, use_biased_hash_to_point(output_pair), I); + + TIME_MEASURE_NS_FINISH(derive_key_image_generator_ns); + + LOG_PRINT_L3("derive_key_image_generator_ns: " << derive_key_image_generator_ns); + + return output_tuple_from_bytes(O, I, C); +} +//---------------------------------------------------------------------------------------------------------------------- +std::shared_ptr curve_trees_v1(const std::size_t selene_chunk_width, const std::size_t helios_chunk_width) +{ + std::unique_ptr selene(new Selene()); + std::unique_ptr helios(new Helios()); + return std::shared_ptr( + new CurveTreesV1( + std::move(selene), + std::move(helios), + selene_chunk_width, + helios_chunk_width + ) + ); +}; +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +// Static functions +//---------------------------------------------------------------------------------------------------------------------- +// After hashing a layer of children points, convert those children x-coordinates into their respective cycle +// scalars, and prepare them to be hashed for the next layer +template +static std::vector next_child_scalars_from_children(const std::unique_ptr &c_child, + const typename C_CHILD::Point *last_root, + const LayerExtension &children) +{ + std::vector child_scalars_out; + child_scalars_out.reserve(1 + children.hashes.size()); + + // If we're creating a *new* root at the existing root layer, we may need to include the *existing* root when + // hashing the *existing* root layer + if (last_root != nullptr) + { + // If the children don't already include the existing root, then we need to include it to be hashed + // - the children would include the existing root already if the existing root was updated in the child + // layer (the start_idx would be 0) + if (children.start_idx > 0) + { + MTRACE("Updating root layer and including the existing root in next children"); + child_scalars_out.emplace_back(c_child->point_to_cycle_scalar(*last_root)); + } + } + + // Convert child points to scalars + tower_cycle::extend_scalars_from_cycle_points(c_child, children.hashes, child_scalars_out); + + return child_scalars_out; +}; +//---------------------------------------------------------------------------------------------------------------------- +template +static void hash_first_chunk(const std::unique_ptr &curve, + const typename C::Scalar *old_last_child, + const typename C::Point *old_last_parent, + const std::size_t start_offset, + const std::vector &new_child_scalars, + const std::size_t chunk_size, + typename C::Point &hash_out) +{ + // Prepare to hash + const auto &existing_hash = old_last_parent != nullptr + ? *old_last_parent + : curve->hash_init_point(); + + const auto &prior_child_after_offset = old_last_child != nullptr + ? *old_last_child + : curve->zero_scalar(); + + const auto chunk_start = new_child_scalars.data(); + const typename C::Chunk chunk{chunk_start, chunk_size}; + + MTRACE("First chunk existing_hash: " << curve->to_string(existing_hash) << " , start_offset: " << start_offset + << " , prior_child_after_offset: " << curve->to_string(prior_child_after_offset)); + + for (std::size_t i = 0; i < chunk_size; ++i) + MTRACE("Hashing child in first chunk " << curve->to_string(chunk_start[i])); + + // Do the hash + auto chunk_hash = curve->hash_grow( + existing_hash, + start_offset, + prior_child_after_offset, + chunk + ); + + MTRACE("First chunk result: " << curve->to_string(chunk_hash) << " , chunk_size: " << chunk_size); + + // We've got our hash + hash_out = std::move(chunk_hash); +} +//---------------------------------------------------------------------------------------------------------------------- +template +static void hash_next_chunk(const std::unique_ptr &curve, + const std::size_t chunk_start_idx, + const std::vector &new_child_scalars, + const std::size_t chunk_size, + typename C::Point &hash_out) +{ + const auto chunk_start = new_child_scalars.data() + chunk_start_idx; + const typename C::Chunk chunk{chunk_start, chunk_size}; + + for (std::size_t i = 0; i < chunk_size; ++i) + MTRACE("Child chunk_start_idx " << chunk_start_idx << " hashing child " << curve->to_string(chunk_start[i])); + + auto chunk_hash = get_new_parent(curve, chunk); + + MTRACE("Child chunk_start_idx " << chunk_start_idx << " result: " << curve->to_string(chunk_hash) + << " , chunk_size: " << chunk_size); + + // We've got our hash + hash_out = std::move(chunk_hash); +} +//---------------------------------------------------------------------------------------------------------------------- +// Hash chunks of a layer of new children, outputting the next layer's parents +template +static LayerExtension hash_children_chunks(const std::unique_ptr &curve, + const typename C::Scalar *old_last_child, + const typename C::Point *old_last_parent, + const std::size_t start_offset, + const uint64_t next_parent_start_index, + const std::vector &new_child_scalars, + const std::size_t chunk_width) +{ + LayerExtension parents_out; + parents_out.start_idx = next_parent_start_index; + parents_out.update_existing_last_hash = old_last_parent != nullptr; + + CHECK_AND_ASSERT_THROW_MES(!new_child_scalars.empty(), "empty child scalars"); + CHECK_AND_ASSERT_THROW_MES(chunk_width > start_offset, "start_offset must be smaller than chunk_width"); + + // See how many children we need to fill up the existing last chunk + const std::size_t first_chunk_size = std::min(new_child_scalars.size(), chunk_width - start_offset); + + CHECK_AND_ASSERT_THROW_MES(new_child_scalars.size() >= first_chunk_size, "unexpected first chunk size"); + + const std::size_t n_chunks = 1 // first chunk + + (new_child_scalars.size() - first_chunk_size) / chunk_width // middle chunks + + (((new_child_scalars.size() - first_chunk_size) % chunk_width > 0) ? 1 : 0); // final chunk + + parents_out.hashes.resize(n_chunks); + + MTRACE("First chunk_size: " << first_chunk_size + << " , num new child scalars: " << new_child_scalars.size() + << " , start_offset: " << start_offset + << " , parent layer start idx: " << parents_out.start_idx + << " , n chunks: " << n_chunks); + + // Hash batches of chunks in parallel + tools::threadpool& tpool = tools::threadpool::getInstanceForCompute(); + tools::threadpool::waiter waiter(tpool); + const std::size_t n_threads = std::max(1, tpool.get_max_concurrency()); + + const std::size_t HASH_BATCH_SIZE = std::max(1, (n_chunks / n_threads)); + for (std::size_t i = 0; i < n_chunks; i += HASH_BATCH_SIZE) + { + const std::size_t end = std::min(i + HASH_BATCH_SIZE, n_chunks); + tpool.submit(&waiter, + [ + &curve, + &old_last_child, + &old_last_parent, + &new_child_scalars, + &parents_out, + start_offset, + first_chunk_size, + chunk_width, + i, + end + ]() + { + for (std::size_t j = i; j < end; ++j) + { + auto &hash_out = parents_out.hashes[j]; + + // Hash the first chunk + if (j == 0) + { + hash_first_chunk(curve, + old_last_child, + old_last_parent, + start_offset, + new_child_scalars, + first_chunk_size, + hash_out); + continue; + } + + const std::size_t chunk_start = j * chunk_width; + + CHECK_AND_ASSERT_THROW_MES(chunk_start > start_offset, "unexpected small chunk_start"); + const std::size_t chunk_start_idx = chunk_start - start_offset; + + const std::size_t chunk_end_idx = std::min(chunk_start_idx + chunk_width, new_child_scalars.size()); + + CHECK_AND_ASSERT_THROW_MES(chunk_end_idx > chunk_start_idx, "unexpected large chunk_start_idx"); + const std::size_t chunk_size = chunk_end_idx - chunk_start_idx; + + hash_next_chunk(curve, chunk_start_idx, new_child_scalars, chunk_size, hash_out); + } + }, + true + ); + } + + CHECK_AND_ASSERT_THROW_MES(waiter.wait(), "failed to hash chunks"); + + return parents_out; +}; +//---------------------------------------------------------------------------------------------------------------------- +static GrowLayerInstructions get_grow_layer_instructions(const uint64_t old_total_children, + const uint64_t new_total_children, + const std::size_t parent_chunk_width, + const bool last_child_will_change) +{ + // 1. Check pre-conditions on total number of children + // - If there's only 1 old child, it must be the old root, and we must be setting a new parent layer after old root + const bool setting_next_layer_after_old_root = old_total_children == 1; + if (setting_next_layer_after_old_root) + { + CHECK_AND_ASSERT_THROW_MES(new_total_children > old_total_children, + "new_total_children must be > old_total_children when setting next layer after old root"); + } + else + { + CHECK_AND_ASSERT_THROW_MES(new_total_children >= old_total_children, + "new_total_children must be >= old_total_children"); + } + + // 2. Calculate old and new total number of parents using totals for children + // If there's only 1 child, then it must be the old root and thus it would have no old parents + const uint64_t old_total_parents = old_total_children > 1 + ? (1 + ((old_total_children - 1) / parent_chunk_width)) + : 0; + const uint64_t new_total_parents = 1 + ((new_total_children - 1) / parent_chunk_width); + + // 3. Check pre-conditions on total number of parents + CHECK_AND_ASSERT_THROW_MES(new_total_parents >= old_total_parents, + "new_total_parents must be >= old_total_parents"); + CHECK_AND_ASSERT_THROW_MES(new_total_parents < new_total_children, + "new_total_parents must be < new_total_children"); + + if (setting_next_layer_after_old_root) + { + CHECK_AND_ASSERT_THROW_MES(old_total_parents == 0, + "old_total_parents expected to be 0 when setting next layer after old root"); + } + + // 4. Set the current offset in the last chunk + // - Note: this value starts at the last child in the last chunk, but it might need to be decremented by 1 if we're + // changing that last child + std::size_t offset = old_total_parents > 0 + ? (old_total_children % parent_chunk_width) + : 0; + + // 5. Check if the last chunk is full (keep in mind it's also possible it's empty) + const bool last_chunk_is_full = offset == 0; + + // 6. When the last child changes, we'll need to use its old value to update the parent + // - We only care if the child has a parent, otherwise we won't need the child's old value to update the parent + // (since there is no parent to update) + const bool need_old_last_child = old_total_parents > 0 && last_child_will_change; + + // 7. If we're changing the last child, we need to subtract the offset by 1 to account for that child + if (need_old_last_child) + { + CHECK_AND_ASSERT_THROW_MES(old_total_children > 0, "no old children but last child is supposed to change"); + + // If the chunk is full, must subtract the chunk width by 1 + offset = offset == 0 ? (parent_chunk_width - 1) : (offset - 1); + } + + // 8. When the last parent changes, we'll need to use its old value to update itself + const bool adding_members_to_existing_last_chunk = old_total_parents > 0 && !last_chunk_is_full + && new_total_children > old_total_children; + const bool need_old_last_parent = need_old_last_child || adding_members_to_existing_last_chunk; + + // 9. Set the next parent's start index + uint64_t next_parent_start_index = old_total_parents; + if (need_old_last_parent) + { + // If we're updating the last parent, we need to bring the starting parent index back 1 + CHECK_AND_ASSERT_THROW_MES(old_total_parents > 0, "no old parents but last parent is supposed to change1"); + --next_parent_start_index; + } + + // Done + MTRACE("parent_chunk_width: " << parent_chunk_width + << " , old_total_children: " << old_total_children + << " , new_total_children: " << new_total_children + << " , old_total_parents: " << old_total_parents + << " , new_total_parents: " << new_total_parents + << " , setting_next_layer_after_old_root: " << setting_next_layer_after_old_root + << " , need_old_last_child: " << need_old_last_child + << " , need_old_last_parent: " << need_old_last_parent + << " , start_offset: " << offset + << " , next_parent_start_index: " << next_parent_start_index); + + return GrowLayerInstructions{ + .parent_chunk_width = parent_chunk_width, + .old_total_parents = old_total_parents, + .new_total_parents = new_total_parents, + .setting_next_layer_after_old_root = setting_next_layer_after_old_root, + .need_old_last_child = need_old_last_child, + .need_old_last_parent = need_old_last_parent, + .start_offset = offset, + .next_parent_start_index = next_parent_start_index, + }; + +}; +//---------------------------------------------------------------------------------------------------------------------- +static GrowLayerInstructions get_leaf_layer_grow_instructions(const uint64_t old_n_leaf_tuples, + const uint64_t new_n_leaf_tuples, + const std::size_t leaf_tuple_size, + const std::size_t leaf_layer_chunk_width) +{ + // The leaf layer can never be the root layer + const bool setting_next_layer_after_old_root = false; + + const uint64_t old_total_children = old_n_leaf_tuples * leaf_tuple_size; + const uint64_t new_total_children = (old_n_leaf_tuples + new_n_leaf_tuples) * leaf_tuple_size; + + const uint64_t old_total_parents = old_total_children > 0 + ? (1 + ((old_total_children - 1) / leaf_layer_chunk_width)) + : 0; + const uint64_t new_total_parents = 1 + ((new_total_children - 1) / leaf_layer_chunk_width); + + CHECK_AND_ASSERT_THROW_MES(new_total_children >= old_total_children, + "new_total_children must be >= old_total_children"); + CHECK_AND_ASSERT_THROW_MES(new_total_parents >= old_total_parents, + "new_total_parents must be >= old_total_parents"); + + // Since leaf layer is append-only, no leaf can ever change and we'll never need an old leaf + const bool need_old_last_child = false; + + const std::size_t offset = old_total_children % leaf_layer_chunk_width; + + const bool last_chunk_is_full = offset == 0; + const bool adding_members_to_existing_last_chunk = old_total_parents > 0 && !last_chunk_is_full + && new_total_children > old_total_children; + const bool need_old_last_parent = adding_members_to_existing_last_chunk; + + uint64_t next_parent_start_index = old_total_parents; + if (need_old_last_parent) + { + // If we're updating the last parent, we need to bring the starting parent index back 1 + CHECK_AND_ASSERT_THROW_MES(old_total_parents > 0, "no old parents but last parent is supposed to change2"); + --next_parent_start_index; + } + + MTRACE("parent_chunk_width: " << leaf_layer_chunk_width + << " , old_total_children: " << old_total_children + << " , new_total_children: " << new_total_children + << " , old_total_parents: " << old_total_parents + << " , new_total_parents: " << new_total_parents + << " , setting_next_layer_after_old_root: " << setting_next_layer_after_old_root + << " , need_old_last_child: " << need_old_last_child + << " , need_old_last_parent: " << need_old_last_parent + << " , start_offset: " << offset + << " , next_parent_start_index: " << next_parent_start_index); + + return GrowLayerInstructions{ + .parent_chunk_width = leaf_layer_chunk_width, + .old_total_parents = old_total_parents, + .new_total_parents = new_total_parents, + .setting_next_layer_after_old_root = setting_next_layer_after_old_root, + .need_old_last_child = need_old_last_child, + .need_old_last_parent = need_old_last_parent, + .start_offset = offset, + .next_parent_start_index = next_parent_start_index, + }; +}; +//---------------------------------------------------------------------------------------------------------------------- +// Helper function used to get the next layer extension used to grow the next layer in the tree +// - for example, if we just grew the parent layer after the leaf layer, the "next layer" would be the grandparent +// layer of the leaf layer +template +static LayerExtension get_next_layer_extension(const std::unique_ptr &c_child, + const std::unique_ptr &c_parent, + const GrowLayerInstructions &grow_layer_instructions, + const std::vector &child_last_hashes, + const std::vector &parent_last_hashes, + const std::vector> child_layer_extensions, + const std::size_t last_updated_child_idx, + const std::size_t last_updated_parent_idx) +{ + // TODO: comments + const auto *child_last_hash = (last_updated_child_idx >= child_last_hashes.size()) + ? nullptr + : &child_last_hashes[last_updated_child_idx]; + + const auto *parent_last_hash = (last_updated_parent_idx >= parent_last_hashes.size()) + ? nullptr + : &parent_last_hashes[last_updated_parent_idx]; + + // Pre-conditions + CHECK_AND_ASSERT_THROW_MES(last_updated_child_idx < child_layer_extensions.size(), "missing child layer"); + const auto &child_extension = child_layer_extensions[last_updated_child_idx]; + + if (grow_layer_instructions.setting_next_layer_after_old_root) + { + CHECK_AND_ASSERT_THROW_MES((last_updated_child_idx + 1) == child_last_hashes.size(), + "unexpected last updated child idx"); + CHECK_AND_ASSERT_THROW_MES(child_last_hash != nullptr, "missing last child when setting layer after old root"); + } + + const auto child_scalars = next_child_scalars_from_children(c_child, + grow_layer_instructions.setting_next_layer_after_old_root ? child_last_hash : nullptr, + child_extension); + + if (grow_layer_instructions.need_old_last_parent) + CHECK_AND_ASSERT_THROW_MES(parent_last_hash != nullptr, "missing last parent"); + + typename C_PARENT::Scalar last_child_scalar; + if (grow_layer_instructions.need_old_last_child) + { + CHECK_AND_ASSERT_THROW_MES(child_last_hash != nullptr, "missing last child"); + last_child_scalar = c_child->point_to_cycle_scalar(*child_last_hash); + } + + // Do the hashing + LayerExtension layer_extension = hash_children_chunks( + c_parent, + grow_layer_instructions.need_old_last_child ? &last_child_scalar : nullptr, + grow_layer_instructions.need_old_last_parent ? parent_last_hash : nullptr, + grow_layer_instructions.start_offset, + grow_layer_instructions.next_parent_start_index, + child_scalars, + grow_layer_instructions.parent_chunk_width + ); + + CHECK_AND_ASSERT_THROW_MES((layer_extension.start_idx + layer_extension.hashes.size()) == + grow_layer_instructions.new_total_parents, + "unexpected num parents extended"); + + return layer_extension; +} +//---------------------------------------------------------------------------------------------------------------------- +static PreLeafTuple output_tuple_to_pre_leaf_tuple(const OutputTuple &o) +{ + TIME_MEASURE_NS_START(point_to_ed_derivatives_ns); + + const crypto::ec_point &O = (crypto::ec_point&) o.O; + const crypto::ec_point &I = (crypto::ec_point&) o.I; + const crypto::ec_point &C = (crypto::ec_point&) o.C; + + // TODO: this relatively new point_to_ed_derivatives function introduced a point de-compression in order to get wei + // y coordinates from the ed x coordinate. It's worth re-investigating the tree building perf hit as a result. The + // daemon is de-compressing these points twice (once when checking for torsion, and again here). + PreLeafTuple plt; + if (!fcmp_pp::point_to_ed_derivatives(O, plt.O_derivatives)) + throw std::runtime_error("failed to get ed derivatives from O"); + if (!fcmp_pp::point_to_ed_derivatives(I, plt.I_derivatives)) + throw std::runtime_error("failed to get ed derivatives from I"); + if (!fcmp_pp::point_to_ed_derivatives(C, plt.C_derivatives)) + throw std::runtime_error("failed to get ed derivatives from C"); + + TIME_MEASURE_NS_FINISH(point_to_ed_derivatives_ns); + + LOG_PRINT_L3("point_to_ed_derivatives_ns: " << point_to_ed_derivatives_ns); + + return plt; +} +//---------------------------------------------------------------------------------------------------------------------- +static PreLeafTuple output_to_pre_leaf_tuple(const OutputPair &output_pair, bool use_fast_check = false) +{ + const auto o = output_to_tuple(output_pair, use_fast_check); + return output_tuple_to_pre_leaf_tuple(o); +} +//---------------------------------------------------------------------------------------------------------------------- +static CurveTrees::LeafTuple pre_leaf_tuple_to_leaf_tuple(const PreLeafTuple &plt) +{ + crypto::ec_coord O_x, O_y, I_x, I_y, C_x, C_y; + if (!fcmp_pp::ed_derivatives_to_wei_x_y(plt.O_derivatives, O_x, O_y)) + throw std::runtime_error("failed to get wei x y from O derivatives"); + if (!fcmp_pp::ed_derivatives_to_wei_x_y(plt.I_derivatives, I_x, I_y)) + throw std::runtime_error("failed to get wei x y from I derivatives"); + if (!fcmp_pp::ed_derivatives_to_wei_x_y(plt.C_derivatives, C_x, C_y)) + throw std::runtime_error("failed to get wei x y from C derivatives"); + + return CurveTrees::LeafTuple{ + .O_x = tower_cycle::selene_scalar_from_bytes(O_x), + .O_y = tower_cycle::selene_scalar_from_bytes(O_y), + + .I_x = tower_cycle::selene_scalar_from_bytes(I_x), + .I_y = tower_cycle::selene_scalar_from_bytes(I_y), + + .C_x = tower_cycle::selene_scalar_from_bytes(C_x), + .C_y = tower_cycle::selene_scalar_from_bytes(C_y) + }; +} +//---------------------------------------------------------------------------------------------------------------------- +static CurveTrees::LeafTuple output_tuple_to_leaf_tuple(const OutputTuple &output_tuple) +{ + const auto plt = output_tuple_to_pre_leaf_tuple(output_tuple); + return pre_leaf_tuple_to_leaf_tuple(plt); +} +//---------------------------------------------------------------------------------------------------------------------- +template +static typename C_PARENT::Point get_chunk_hash(const std::unique_ptr &c_child, + const std::unique_ptr &c_parent, + const std::vector> &child_layers, + const bool use_new_last_hash, + const typename C_CHILD::Point &new_last_hash, + std::size_t &c_idx_inout) +{ + CHECK_AND_ASSERT_THROW_MES(child_layers.size() > c_idx_inout, "high c_idx"); + const auto &layer = child_layers[c_idx_inout]; + + // Collect child scalars so we can hash them + std::vector scalars; + scalars.reserve(layer.size()); + + CHECK_AND_ASSERT_THROW_MES(!layer.empty(), "empty layer"); + for (std::size_t i = 0; i < (layer.size() - 1); ++i) + scalars.emplace_back(c_child->point_to_cycle_scalar(layer[i])); + + // Use the newly calculated hash from the preceding layer + const auto &last_hash = use_new_last_hash ? new_last_hash : layer.back(); + scalars.emplace_back(c_child->point_to_cycle_scalar(last_hash)); + + // Hash scalars + const typename C_PARENT::Chunk chunk{scalars.data(), scalars.size()}; + const typename C_PARENT::Point hash = get_new_parent(c_parent, chunk); + + MTRACE("Hash result: " << c_parent->to_string(hash)); + + ++c_idx_inout; + return hash; +} +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +// CurveTrees public member functions +//---------------------------------------------------------------------------------------------------------------------- +template<> +CurveTrees::LeafTuple CurveTrees::leaf_tuple(const OutputPair &output_pair) const +{ + const auto plt = output_to_pre_leaf_tuple(output_pair); + return pre_leaf_tuple_to_leaf_tuple(plt); +}; +//---------------------------------------------------------------------------------------------------------------------- +template +std::vector CurveTrees::flatten_leaves(std::vector &&leaves) const +{ + std::vector flattened_leaves; + flattened_leaves.reserve(leaves.size() * LEAF_TUPLE_SIZE); + + for (auto &l : leaves) + { + flattened_leaves.emplace_back(std::move(l.O_x)); + flattened_leaves.emplace_back(std::move(l.O_y)); + + flattened_leaves.emplace_back(std::move(l.I_x)); + flattened_leaves.emplace_back(std::move(l.I_y)); + + flattened_leaves.emplace_back(std::move(l.C_x)); + flattened_leaves.emplace_back(std::move(l.C_y)); + } + + return flattened_leaves; +}; + +// Explicit instantiation +template std::vector CurveTrees::flatten_leaves( + std::vector &&leaves) const; +//---------------------------------------------------------------------------------------------------------------------- +template +typename CurveTrees::TreeExtension CurveTrees::get_tree_extension( + const uint64_t old_n_leaf_tuples, + const LastHashes &existing_last_hashes, + std::vector> &&new_outputs, + const bool use_fast_torsion_check) const +{ + TreeExtension tree_extension; + tree_extension.leaves.start_leaf_tuple_idx = old_n_leaf_tuples; + + if (new_outputs.empty()) + return tree_extension; + + TIME_MEASURE_START(sorting_outputs); + + // Sort the outputs by order they appear in the chain + // Note: the outputs are expected to be grouped by last locked block + std::vector flat_sorted_outputs; + for (auto &unsorted_outputs : new_outputs) + { + const auto sort_fn = [](const UnifiedOutput &a, const UnifiedOutput &b){return a.unified_id < b.unified_id;}; + std::sort(unsorted_outputs.begin(), unsorted_outputs.end(), sort_fn); + + // No duplicates allowed + const auto dup_check = [](const UnifiedOutput &a, const UnifiedOutput &b){return a.unified_id == b.unified_id;}; + CHECK_AND_ASSERT_THROW_MES(std::adjacent_find(unsorted_outputs.begin(), unsorted_outputs.end(), dup_check) + == unsorted_outputs.end(), "get_tree_extension: duplicate output id's"); + + flat_sorted_outputs.insert(flat_sorted_outputs.end(), + std::make_move_iterator(unsorted_outputs.begin()), + std::make_move_iterator(unsorted_outputs.end())); + } + + TIME_MEASURE_FINISH(sorting_outputs); + + // Convert sorted outputs into leaf tuples, place each element of each leaf tuple in a flat vector to be hashed, + // and place the outputs in a tree extension struct for insertion into the db. We ignore invalid outputs, since + // they cannot be inserted to the tree. + std::vector flattened_leaves; + this->outputs_to_leaves(std::move(flat_sorted_outputs), + flattened_leaves, + tree_extension.leaves.tuples, + use_fast_torsion_check); + + if (flattened_leaves.empty()) + return tree_extension; + + TIME_MEASURE_START(hashing_leaves); + + MTRACE("Getting extension for layer 0"); + auto grow_layer_instructions = get_leaf_layer_grow_instructions( + old_n_leaf_tuples, + tree_extension.leaves.tuples.size(), + LEAF_TUPLE_SIZE, + m_leaf_layer_chunk_width); + + if (grow_layer_instructions.need_old_last_parent) + CHECK_AND_ASSERT_THROW_MES(!existing_last_hashes.c1_last_hashes.empty(), "missing last c1 parent"); + + // Hash the leaf layer + auto leaf_parents = hash_children_chunks(m_c1, + nullptr, // We never need the old last child from leaf layer because the leaf layer is always append-only + grow_layer_instructions.need_old_last_parent ? &existing_last_hashes.c1_last_hashes[0] : nullptr, + grow_layer_instructions.start_offset, + grow_layer_instructions.next_parent_start_index, + flattened_leaves, + m_leaf_layer_chunk_width + ); + TIME_MEASURE_FINISH(hashing_leaves); + + CHECK_AND_ASSERT_THROW_MES( + (leaf_parents.start_idx + leaf_parents.hashes.size()) == grow_layer_instructions.new_total_parents, + "unexpected num leaf parents extended"); + + tree_extension.c1_layer_extensions.emplace_back(std::move(leaf_parents)); + + // Alternate between hashing c1 children, c2 children, c1, c2, ... + bool parent_is_c2 = true; + + std::size_t c1_last_idx = 0; + std::size_t c2_last_idx = 0; + TIME_MEASURE_START(hashing_layers); + while (grow_layer_instructions.new_total_parents > 1) + { + MTRACE("Getting extension for layer " << (c1_last_idx + c2_last_idx + 1)); + + const uint64_t new_total_children = grow_layer_instructions.new_total_parents; + + grow_layer_instructions = this->set_next_layer_extension( + grow_layer_instructions, + parent_is_c2, + existing_last_hashes, + c1_last_idx, + c2_last_idx, + tree_extension + ); + + // Sanity check to make sure we're making progress to exit the while loop + CHECK_AND_ASSERT_THROW_MES(grow_layer_instructions.new_total_parents < new_total_children, + "expect fewer parents than children in every layer"); + + parent_is_c2 = !parent_is_c2; + } + TIME_MEASURE_FINISH(hashing_layers); + + m_sorting_outputs_ms += sorting_outputs; + m_hash_leaves_ms += hashing_leaves; + m_hash_layers_ms += hashing_layers; + + LOG_PRINT_L2("Total time spent hashing leaves: " << m_hash_leaves_ms / 1000 + << " , hashing layers: " << m_hash_layers_ms / 1000 + << " , sorting outputs: " << m_sorting_outputs_ms / 1000); + + return tree_extension; +}; + +// Explicit instantiation +template CurveTrees::TreeExtension CurveTrees::get_tree_extension( + const uint64_t old_n_leaf_tuples, + const LastHashes &existing_last_hashes, + std::vector> &&new_outputs, + const bool use_fast_torsion_check) const; +//---------------------------------------------------------------------------------------------------------------------- +template +CompressedTreeExtension CurveTrees::compress_tree_extension( + typename CurveTrees::TreeExtension &&tree_extension) const +{ + std::vector layer_extensions; + const std::size_t n_layers = tree_extension.c1_layer_extensions.size() + tree_extension.c2_layer_extensions.size(); + layer_extensions.reserve(n_layers); + + bool parent_is_c1 = true; + std::size_t c1_idx = 0, c2_idx = 0; + for (std::size_t i = 0; i < n_layers; ++i) + { + if (parent_is_c1) + { + const auto &c1_layer_ext = tree_extension.c1_layer_extensions.at(c1_idx); + + std::vector hashes; + hashes.reserve(c1_layer_ext.hashes.size()); + for (const auto &h : c1_layer_ext.hashes) + hashes.emplace_back(m_c1->to_bytes(h)); + + layer_extensions.emplace_back(fcmp_pp::CompressedLayerExtension{ + .start_idx = c1_layer_ext.start_idx, + .update_existing_last_hash = c1_layer_ext.update_existing_last_hash, + .hashes = std::move(hashes) + }); + + ++c1_idx; + } + else + { + const auto &c2_layer_ext = tree_extension.c2_layer_extensions.at(c2_idx); + + std::vector hashes; + hashes.reserve(c2_layer_ext.hashes.size()); + for (const auto &h : c2_layer_ext.hashes) + hashes.emplace_back(m_c2->to_bytes(h)); + + layer_extensions.emplace_back(fcmp_pp::CompressedLayerExtension{ + .start_idx = c2_layer_ext.start_idx, + .update_existing_last_hash = c2_layer_ext.update_existing_last_hash, + .hashes = std::move(hashes) + }); + + ++c2_idx; + } + + parent_is_c1 = !parent_is_c1; + } + + return CompressedTreeExtension{ + .leaves = std::move(tree_extension.leaves), + .layer_extensions = std::move(layer_extensions) + }; +} + +// Explicit instantiation +template CompressedTreeExtension CurveTrees::compress_tree_extension( + typename CurveTrees::TreeExtension &&tree_ext) const; +//---------------------------------------------------------------------------------------------------------------------- +template +std::vector CurveTrees::n_elems_per_layer(const uint64_t n_leaf_tuples) const +{ + std::vector n_elems_per_layer; + if (n_leaf_tuples == 0) + return n_elems_per_layer; + + uint64_t n_children = n_leaf_tuples; + bool parent_is_c1 = true; + do + { + const std::size_t parent_chunk_width = parent_is_c1 ? m_c1_width : m_c2_width; + const uint64_t n_parents = ((n_children - 1) / parent_chunk_width) + 1; + n_elems_per_layer.push_back(n_parents); + n_children = n_parents; + parent_is_c1 = !parent_is_c1; + } + while (n_children > 1); + + return n_elems_per_layer; +} + +// Explicit instantiation +template std::vector CurveTrees::n_elems_per_layer(const uint64_t n_leaf_tuples) const; +//---------------------------------------------------------------------------------------------------------------------- +template +std::size_t CurveTrees::n_layers(const uint64_t n_leaf_tuples) const +{ + return this->n_elems_per_layer(n_leaf_tuples).size(); +} + +// Explicit instantiation +template std::size_t CurveTrees::n_layers(const uint64_t n_leaf_tuples) const; +//---------------------------------------------------------------------------------------------------------------------- +template +PathIndexes CurveTrees::get_path_indexes(const uint64_t n_leaf_tuples, const uint64_t leaf_tuple_idx) const +{ + PathIndexes path_indexes_out; + + MTRACE("Getting path indexes for leaf_tuple_idx: " << leaf_tuple_idx << " , n_leaf_tuples: " << n_leaf_tuples); + + const auto child_chunk_indexes = this->get_child_chunk_indexes(n_leaf_tuples, leaf_tuple_idx); + if (child_chunk_indexes.empty()) + return path_indexes_out; + + const auto n_elems_per_layer = this->n_elems_per_layer(n_leaf_tuples); + CHECK_AND_ASSERT_THROW_MES(child_chunk_indexes.size() == (n_elems_per_layer.size() + 1), + "size mismatch n elems per layer <> child_chunk_indexes"); + + // Set the leaf range + { + const std::size_t parent_chunk_width = m_c1_width; + + const uint64_t n_children = n_leaf_tuples; + const uint64_t start_range = child_chunk_indexes.front() * parent_chunk_width; + const uint64_t end_range = std::min(n_children, start_range + parent_chunk_width); + + path_indexes_out.leaf_range = { start_range, end_range }; + } + + // Set ranges on layers above + bool parent_is_c2 = true; + for (std::size_t i = 0; i < n_elems_per_layer.size(); ++i) + { + const std::size_t parent_chunk_width = parent_is_c2 ? m_c2_width : m_c1_width; + + const uint64_t child_chunk_idx = child_chunk_indexes[i + 1]; + const uint64_t n_children = n_elems_per_layer[i]; + + const uint64_t start_range = child_chunk_idx * parent_chunk_width; + const uint64_t end_range = std::min(n_children, start_range + parent_chunk_width); + + MTRACE("start_range: " << start_range + << " , end_range: " << end_range + << " , parent_chunk_width: " << parent_chunk_width + << " , n_children: " << n_children); + + path_indexes_out.layers.emplace_back(PathIndexes::Range{start_range, end_range}); + + parent_is_c2 = !parent_is_c2; + } + + return path_indexes_out; +} + +// Explicit instantiation +template PathIndexes CurveTrees::get_path_indexes(const uint64_t n_leaf_tuples, + const uint64_t leaf_tuple_idx) const; +//---------------------------------------------------------------------------------------------------------------------- +template +std::vector CurveTrees::get_child_chunk_indexes(const uint64_t n_leaf_tuples, + const uint64_t leaf_tuple_idx) const +{ + std::vector child_chunk_indexes_out; + if (n_leaf_tuples <= leaf_tuple_idx) + return child_chunk_indexes_out; + + const std::size_t n_layers = this->n_layers(n_leaf_tuples); + + bool parent_is_c1 = true; + uint64_t child_idx = leaf_tuple_idx; + for (std::size_t i = 0; i < n_layers; ++i) + { + const std::size_t parent_chunk_width = parent_is_c1 ? m_c1_width : m_c2_width; + const uint64_t child_chunk_idx = child_idx / parent_chunk_width; + + child_chunk_indexes_out.push_back(child_chunk_idx); + + child_idx = child_chunk_idx; + parent_is_c1 = !parent_is_c1; + } + + // Add a 0 for the root (it's its layer above's 0-index child) + child_chunk_indexes_out.push_back(0); + + return child_chunk_indexes_out; +} + +// Explicit instantiation +template std::vector CurveTrees::get_child_chunk_indexes(const uint64_t n_leaf_tuples, + const uint64_t leaf_tuple_idx) const; +//---------------------------------------------------------------------------------------------------------------------- +template +typename CurveTrees::LastHashes CurveTrees::tree_edge_to_last_hashes( + const std::vector &tree_edge) const +{ + typename CurveTrees::LastHashes last_hashes; + + bool parent_is_c1 = true; + for (const auto &last_hash : tree_edge) + { + if (parent_is_c1) + last_hashes.c1_last_hashes.push_back(m_c1->from_bytes(last_hash)); + else + last_hashes.c2_last_hashes.push_back(m_c2->from_bytes(last_hash)); + parent_is_c1 = !parent_is_c1; + } + + return last_hashes; +} + +// Explicit instantiation +template CurveTrees::LastHashes CurveTrees::tree_edge_to_last_hashes( + const std::vector &tree_edge) const; +//---------------------------------------------------------------------------------------------------------------------- +template<> +bool CurveTrees::audit_path(const CurveTrees::Path &path, + const OutputPair &output, + const uint64_t n_leaf_tuples_in_tree) const +{ + // TODO: use the leaf idx to know exactly which parent indexes we expect and how many parent hashes we expect at each layer + + // Cleaner refs + const auto &leaves = path.leaves; + const auto &c1_layers = path.c1_layers; + const auto &c2_layers = path.c2_layers; + + const std::size_t n_layers = c1_layers.size() + c2_layers.size(); + CHECK_AND_ASSERT_MES(n_layers == this->n_layers(n_leaf_tuples_in_tree), false, "unexpected n_layers"); + + // Make sure output tuple is present in leaves + const auto output_tuple = output_to_tuple(output); + bool found = false; + for (std::size_t i = 0; !found && i < leaves.size(); ++i) + found = output_tuple == leaves[i]; + CHECK_AND_ASSERT_MES(found, false, "did not find output in chunk of leaves"); + + // Get all hashes + const auto hashes = this->calc_hashes_from_path(path); + CHECK_AND_ASSERT_MES(hashes.size(), false, "empty hashes from path"); + CHECK_AND_ASSERT_MES(n_layers == hashes.size(), false, "hashes <> n_layers mismatch"); + + // Make sure each hash is present in each layer above + bool parent_is_c1 = true; + std::size_t c1_idx = 0, c2_idx = 0; + for (std::size_t i = 0; i < hashes.size(); ++i) + { + MDEBUG("Auditing layer " << i); + const auto hash_str = epee::string_tools::pod_to_hex(hashes[i]); + + // TODO: template + if (parent_is_c1) + { + // Make sure hash is present in c1 layer + CHECK_AND_ASSERT_MES(c1_layers.size() > c1_idx, false, "high c1_idx"); + const auto &c1_layer = c1_layers[c1_idx]; + + MDEBUG("Looking for c1 hash: " << hash_str << " among " << c1_layer.size() << " hashes"); + found = false; + for (std::size_t j = 0; !found && j < c1_layer.size(); ++j) + { + MDEBUG("Reading: " << m_c1->to_string(c1_layer[j])); + found = (hash_str == m_c1->to_string(c1_layer[j])); + } + CHECK_AND_ASSERT_MES(found, false, "did not find c1 hash"); + + ++c1_idx; + } + else + { + // Make sure hash is present in c2 layer + CHECK_AND_ASSERT_MES(c2_layers.size() > c2_idx, false, "high c2_idx"); + const auto &c2_layer = c2_layers[c2_idx]; + + MDEBUG("Looking for c2 hash: " << hash_str << " among " << c2_layer.size() << " hashes"); + found = false; + for (std::size_t j = 0; !found && j < c2_layer.size(); ++j) + { + MDEBUG("Reading: " << m_c2->to_string(c2_layer[j])); + found = (hash_str == m_c2->to_string(c2_layer[j])); + } + CHECK_AND_ASSERT_MES(found, false, "did not find c2 hash"); + + ++c2_idx; + } + + parent_is_c1 = !parent_is_c1; + } + + return true; +} +//---------------------------------------------------------------------------------------------------------------------- +template<> +CurveTrees::Path CurveTrees::path_bytes_to_path(const CompressedPath &path_bytes) const +{ + typename CurveTrees::Path path; + + // Leaves + path.leaves.reserve(path_bytes.leaves.size()); + for (const auto &leaf : path_bytes.leaves) + path.leaves.emplace_back(output_to_tuple(leaf.output_pair)); + + // Layers + bool parent_is_c1 = true; + for (const auto &layer : path_bytes.layer_chunks) + { + if (parent_is_c1) + { + path.c1_layers.emplace_back(); + path.c1_layers.back().reserve(layer.elems.size()); + for (const auto &elem : layer.elems) + path.c1_layers.back().emplace_back(m_c1->from_bytes(elem)); + } + else + { + path.c2_layers.emplace_back(); + path.c2_layers.back().reserve(layer.elems.size()); + for (const auto &elem : layer.elems) + path.c2_layers.back().emplace_back(m_c2->from_bytes(elem)); + } + + parent_is_c1 = !parent_is_c1; + } + + return path; +} +//---------------------------------------------------------------------------------------------------------------------- +template<> +CompressedPath CurveTrees::compress_path(const CurveTrees::Path &path, + const std::vector &outputs) const +{ + CHECK_AND_ASSERT_THROW_MES(path.leaves.size() == outputs.size(), "path.leaves.size() != outputs.size()"); + + CompressedPath compressed_path; + + // Leaves (leaf tuples don't have enough context, need to know the output's type) + compressed_path.leaves = outputs; + + // Layers + bool parent_is_c1 = true; + std::size_t c1_idx = 0, c2_idx = 0; + for (std::size_t i = 0; i < (path.c1_layers.size() + path.c2_layers.size()); ++i) + { + auto &layer_bytes = compressed_path.layer_chunks.emplace_back(); + + if (parent_is_c1) + { + const auto &layer = path.c1_layers.at(c1_idx); + layer_bytes.elems.reserve(layer.size()); + for (const auto &elem : layer) + layer_bytes.elems.emplace_back(m_c1->to_bytes(elem)); + ++c1_idx; + } + else + { + const auto &layer = path.c2_layers.at(c2_idx); + layer_bytes.elems.reserve(layer.size()); + for (const auto &elem : layer) + layer_bytes.elems.emplace_back(m_c2->to_bytes(elem)); + ++c2_idx; + } + + parent_is_c1 = !parent_is_c1; + } + + return compressed_path; +} +//---------------------------------------------------------------------------------------------------------------------- +template +std::vector CurveTrees::calc_hashes_from_path( + const typename CurveTrees::Path &path, + const bool replace_last_hash) const +{ + std::vector c1_hashes; + std::vector c2_hashes; + + // Cleaner refs + const auto &leaves = path.leaves; + const auto &c1_layers = path.c1_layers; + const auto &c2_layers = path.c2_layers; + + const std::size_t n_layers = c1_layers.size() + c2_layers.size(); + c1_hashes.reserve(1 + c1_layers.size()); + c2_hashes.reserve(c2_layers.size()); + + // Initial checks + CHECK_AND_ASSERT_THROW_MES(!leaves.empty(), "empty leaves"); + CHECK_AND_ASSERT_THROW_MES(!c1_layers.empty(), "empty c1 layers"); + CHECK_AND_ASSERT_THROW_MES(leaves.size() <= m_c1_width, "too many leaves"); + + { + MTRACE("Hashing leaves"); + + // Collect leaves so we can hash them + std::vector leaf_scalars; + leaf_scalars.reserve(leaves.size() * LEAF_TUPLE_SIZE); + for (auto &l : leaves) + { + auto leaf_tuple = output_tuple_to_leaf_tuple(l); + leaf_scalars.emplace_back(std::move(leaf_tuple.O_x)); + leaf_scalars.emplace_back(std::move(leaf_tuple.O_y)); + + leaf_scalars.emplace_back(std::move(leaf_tuple.I_x)); + leaf_scalars.emplace_back(std::move(leaf_tuple.I_y)); + + leaf_scalars.emplace_back(std::move(leaf_tuple.C_x)); + leaf_scalars.emplace_back(std::move(leaf_tuple.C_y)); + } + + // Hash the leaf chunk + const typename C1::Chunk leaf_chunk{leaf_scalars.data(), leaf_scalars.size()}; + const typename C1::Point leaf_parent_hash = get_new_parent(m_c1, leaf_chunk); + + c1_hashes.push_back(leaf_parent_hash); + MTRACE("c1 hash result: " << m_c1->to_string(c1_hashes.back())); + } + + // Continue hashing every layer chunk until there are no more layers + std::size_t c1_idx = 0, c2_idx = 0; + for (std::size_t i = 1; i < n_layers; ++i) + { + MTRACE("Hashing layer " << i); + if (c1_idx == c2_idx /*c2 parent*/) + { + auto hash = get_chunk_hash(m_c1, m_c2, c1_layers, replace_last_hash, c1_hashes.back(), c1_idx); + c2_hashes.emplace_back(std::move(hash)); + } + else + { + auto hash = get_chunk_hash(m_c2, m_c1, c2_layers, replace_last_hash, c2_hashes.back(), c2_idx); + c1_hashes.emplace_back(std::move(hash)); + } + } + + // Collect hashes + std::vector hashes; + hashes.reserve(n_layers); + c1_idx = 0, c2_idx = 0; + for (std::size_t i = 0; i < n_layers; ++i) + { + if (c1_idx == c2_idx /*c1 parent*/) + hashes.emplace_back(m_c1->to_bytes(c1_hashes[c1_idx++])); + else + hashes.emplace_back(m_c2->to_bytes(c2_hashes[c2_idx++])); + } + + return hashes; +} + +// Explicit instantiation +template std::vector CurveTrees::calc_hashes_from_path( + const typename CurveTrees::Path &path, + const bool replace_last_hash) const; +//---------------------------------------------------------------------------------------------------------------------- +template<> +CurveTreesV1::ConsolidatedPaths CurveTrees::get_dummy_paths( + const std::vector &outputs, + uint8_t n_layers) const +{ + CHECK_AND_ASSERT_THROW_MES(this->n_layers(outputs.size()) <= n_layers, "n_layers is too low"); + + // Require outputs fit into whole chunks to simplify last chunk handling + CHECK_AND_ASSERT_THROW_MES((outputs.size() % m_c1_width) == 0, "expect outputs to fit into whole chunks"); + + CurveTreesV1::ConsolidatedPaths paths; + if (outputs.empty()) + return paths; + + // Leaves + for (uint64_t i = 0; i < outputs.size(); ++i) + { + auto leaf_tuple = fcmp_pp::curve_trees::output_to_tuple(outputs[i].output_pair); + + const uint64_t leaf_chunk_idx = i / m_c1_width; + paths.leaves_by_chunk_idx[leaf_chunk_idx].emplace_back(std::move(leaf_tuple)); + } + + // First c1 layer + { + paths.c1_layers.push_back({}); + auto &c1_layer = paths.c1_layers.back(); + + std::size_t leaf_chunk_idx = 0; + std::size_t chunk_n = 0; + + auto leaves_by_chunk_it = paths.leaves_by_chunk_idx.find(leaf_chunk_idx); + while (leaves_by_chunk_it != paths.leaves_by_chunk_idx.end()) + { + // Flatten leaves + std::vector leaf_tuples; + for (const auto &leaf : leaves_by_chunk_it->second) + leaf_tuples.push_back(output_tuple_to_leaf_tuple(leaf)); + const auto flat_leaves = this->flatten_leaves(std::move(leaf_tuples)); + + // Hash the leaves + auto &c1_layer_chunk = c1_layer[chunk_n]; + c1_layer_chunk.push_back({}); + hash_first_chunk(m_c1, nullptr, nullptr, 0, flat_leaves, flat_leaves.size(), c1_layer_chunk.back()); + + // Get ready to hash next chunk of leaves + if (c1_layer_chunk.size() == m_c2_width) + ++chunk_n; + leaves_by_chunk_it = paths.leaves_by_chunk_idx.find(++leaf_chunk_idx); + } + + // Fill out the last chunk with 0's as needed + auto last_chunk_it = paths.c1_layers.back().find(chunk_n); + if (last_chunk_it == paths.c1_layers.back().end()) + last_chunk_it = paths.c1_layers.back().find(chunk_n - 1); + CHECK_AND_ASSERT_THROW_MES(last_chunk_it != paths.c1_layers.back().end(), "missing last c1 layer"); + while (n_layers > 1 && last_chunk_it->second.size() < m_c2_width) + last_chunk_it->second.push_back(m_c1->hash_init_point()); + } + + // Rest of the tree + bool parent_is_c2 = true; + for (uint8_t l = 1; l < n_layers; ++l) + { + std::size_t prev_layer_i = 0; + std::size_t chunk_n = 0; + + if (parent_is_c2) + { + paths.c2_layers.push_back({}); + auto &c2_layer = paths.c2_layers.back(); + + auto prev_layer_it = paths.c1_layers.back().find(prev_layer_i); + auto prev_layer_child_it = prev_layer_it->second.begin(); + + while (prev_layer_it != paths.c1_layers.back().end()) + { + std::vector c1_points; + for (std::size_t j = 0; j < m_c2_width; ++j) + { + c1_points.push_back(*prev_layer_child_it); + ++prev_layer_child_it; + if (prev_layer_child_it != prev_layer_it->second.end()) + continue; + ++prev_layer_i; + prev_layer_it = paths.c1_layers.back().find(prev_layer_i); + if (prev_layer_it == paths.c1_layers.back().end()) + break; + prev_layer_child_it = prev_layer_it->second.begin(); + } + + // Convert Selene points to Helios scalars + std::vector c2_scalars; + fcmp_pp::tower_cycle::extend_scalars_from_cycle_points(m_c1, c1_points, c2_scalars); + + // Get hash of prior layer chunk + auto &c2_layer_chunk = c2_layer[chunk_n]; + c2_layer_chunk.push_back({}); + hash_first_chunk(m_c2, nullptr, nullptr, 0, c2_scalars, c2_scalars.size(), c2_layer_chunk.back()); + + if (c2_layer_chunk.size() == m_c1_width) + ++chunk_n; + } + + // Fill out the last chunk with 0's for non-root + auto last_chunk_it = paths.c2_layers.back().find(chunk_n); + if (last_chunk_it == paths.c2_layers.back().end()) + last_chunk_it = paths.c2_layers.back().find(chunk_n - 1); + CHECK_AND_ASSERT_THROW_MES(last_chunk_it != paths.c2_layers.back().end(), "missing last c2 layer"); + while ((l + 1) < n_layers && last_chunk_it->second.size() < m_c1_width) + last_chunk_it->second.push_back(m_c2->hash_init_point()); + } + else + { + paths.c1_layers.push_back({}); + auto &c1_layer = paths.c1_layers.back(); + + auto prev_layer_it = paths.c2_layers.back().find(prev_layer_i); + auto prev_layer_child_it = prev_layer_it->second.begin(); + + while (prev_layer_it != paths.c2_layers.back().end()) + { + std::vector c2_points; + for (std::size_t j = 0; j < m_c1_width; ++j) + { + c2_points.push_back(*prev_layer_child_it); + ++prev_layer_child_it; + if (prev_layer_child_it != prev_layer_it->second.end()) + continue; + ++prev_layer_i; + prev_layer_it = paths.c2_layers.back().find(prev_layer_i); + if (prev_layer_it == paths.c2_layers.back().end()) + break; + prev_layer_child_it = prev_layer_it->second.begin(); + } + + // Convert Helios points to Selene scalars + std::vector c1_scalars; + fcmp_pp::tower_cycle::extend_scalars_from_cycle_points(m_c2, c2_points, c1_scalars); + + // Get hash of prior layer chunk + auto &c1_layer_chunk = c1_layer[chunk_n]; + c1_layer_chunk.push_back({}); + hash_first_chunk(m_c1, nullptr, nullptr, 0, c1_scalars, c1_scalars.size(), c1_layer_chunk.back()); + + if (c1_layer_chunk.size() == m_c2_width) + ++chunk_n; + } + + // Fill out the last chunk with 0's for non-root layer + auto last_chunk_it = paths.c1_layers.back().find(chunk_n); + if (last_chunk_it == paths.c1_layers.back().end()) + last_chunk_it = paths.c1_layers.back().find(chunk_n - 1); + CHECK_AND_ASSERT_THROW_MES(last_chunk_it != paths.c1_layers.back().end(), "missing last c1 layer"); + while ((l + 1) < n_layers && last_chunk_it->second.size() < m_c2_width) + last_chunk_it->second.push_back(m_c1->hash_init_point()); + } + + parent_is_c2 = !parent_is_c2; + } + + return paths; +}; +//---------------------------------------------------------------------------------------------------------------------- +template<> +CurveTreesV1::Path CurveTrees::get_single_dummy_path( + const CurveTreesV1::ConsolidatedPaths &dummy_paths, + const uint64_t n_leaf_tuples, + const uint64_t leaf_tuple_idx) const +{ + CurveTreesV1::Path path; + const auto path_idxs = this->get_path_indexes(n_leaf_tuples, leaf_tuple_idx); + + // Leaves + const uint64_t leaf_child_idx = path_idxs.leaf_range.first / m_c1_width; + const auto leaf_it = dummy_paths.leaves_by_chunk_idx.find(leaf_child_idx); + CHECK_AND_ASSERT_THROW_MES(leaf_it != dummy_paths.leaves_by_chunk_idx.end(), "dummy missing leaf chunk"); + CHECK_AND_ASSERT_THROW_MES((path_idxs.leaf_range.first + leaf_it->second.size()) == path_idxs.leaf_range.second, + "dummy leaves size mismatch"); + path.leaves = leaf_it->second; + + // All other layers + bool parent_is_c1 = true; + std::size_t c1_idx = 0, c2_idx = 0; + for (uint8_t l = 0; l < path_idxs.layers.size(); ++l) + { + const auto start = path_idxs.layers[l].first; + const auto end = path_idxs.layers[l].second; + + const uint64_t child_idx = start / (parent_is_c1 ? m_c2_width : m_c1_width); + if (parent_is_c1) + { + CHECK_AND_ASSERT_THROW_MES(dummy_paths.c1_layers.size() > c1_idx, "dummy c1 layers too small"); + const auto chunk_it = dummy_paths.c1_layers[c1_idx].find(child_idx); + CHECK_AND_ASSERT_THROW_MES((start + chunk_it->second.size()) == end, "dummy c1 layer size mismatch"); + path.c1_layers.push_back(chunk_it->second); + + ++c1_idx; + } + else + { + CHECK_AND_ASSERT_THROW_MES(dummy_paths.c2_layers.size() > c2_idx, "dummy c2 layers too small"); + const auto chunk_it = dummy_paths.c2_layers[c2_idx].find(child_idx); + CHECK_AND_ASSERT_THROW_MES((start + chunk_it->second.size()) == end, "dummy c2 layer size mismatch"); + path.c2_layers.push_back(chunk_it->second); + + ++c2_idx; + } + + parent_is_c1 = !parent_is_c1; + } + + return path; +}; +//---------------------------------------------------------------------------------------------------------------------- +template +typename CurveTrees::TreeExtension CurveTrees::path_to_tree_extension(const CompressedPath &path_bytes, + const PathIndexes &path_idxs) const +{ + typename CurveTrees::TreeExtension tree_extension; + tree_extension.leaves = ContiguousLeaves{ + .start_leaf_tuple_idx = path_idxs.leaf_range.first, + .tuples = path_bytes.leaves + }; + + uint8_t layer_idx = 0; + bool parent_is_c1 = true; + for (const auto &chunk : path_bytes.layer_chunks) + { + // Get the start indexes and expected size of the last chunk + const auto &idx_range = path_idxs.layers.at(layer_idx); + const uint64_t start_idx = idx_range.first; + const uint64_t end_idx = idx_range.second; + + CHECK_AND_ASSERT_THROW_MES(end_idx > start_idx, + "path_to_tree_extension: unexpected end_idx <= start_idx"); + CHECK_AND_ASSERT_THROW_MES(chunk.elems.size() == (end_idx - start_idx), + "path_to_tree_extension: size mismatch last chunk"); + + if (parent_is_c1) + { + LayerExtension layer_ext; + layer_ext.start_idx = start_idx; + layer_ext.update_existing_last_hash = false; + for (const auto &child : chunk.elems) + layer_ext.hashes.emplace_back(m_c1->from_bytes(child)); + tree_extension.c1_layer_extensions.emplace_back(std::move(layer_ext)); + } + else + { + LayerExtension layer_ext; + layer_ext.start_idx = start_idx; + layer_ext.update_existing_last_hash = false; + for (const auto &child : chunk.elems) + layer_ext.hashes.emplace_back(m_c2->from_bytes(child)); + tree_extension.c2_layer_extensions.emplace_back(std::move(layer_ext)); + } + + ++layer_idx; + parent_is_c1 = !parent_is_c1; + } + + return tree_extension; +} + +// Explicit instantiation +template CurveTrees::TreeExtension CurveTrees::path_to_tree_extension( + const CompressedPath &path_bytes, + const PathIndexes &path_idxs) const; +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +// CurveTrees private member functions +//---------------------------------------------------------------------------------------------------------------------- +template +void CurveTrees::outputs_to_leaves(std::vector &&new_outputs, + std::vector &flattened_leaves_out, + std::vector &valid_outputs_out, + const bool use_fast_torsion_check) const +{ + flattened_leaves_out.clear(); + valid_outputs_out.clear(); + + TIME_MEASURE_START(outputs_to_leaves); + + // Keep track of valid outputs to make sure we only use leaves from valid outputs. Can't use std::vector + // because std::vector concurrent access is not thread safe. + enum Boolean : uint8_t { + False = 0, + True = 1, + }; + std::vector valid_outputs(new_outputs.size(), False); + + tools::threadpool& tpool = tools::threadpool::getInstanceForCompute(); + tools::threadpool::waiter waiter(tpool); + const std::size_t n_threads = std::max(1, tpool.get_max_concurrency()); + + TIME_MEASURE_START(convert_valid_leaves); + // Step 1. Multithreaded convert valid outputs into Edwards derivatives needed to get Wei coordinates + std::vector pre_leaves; + pre_leaves.resize(new_outputs.size()); + const std::size_t LEAF_CONVERT_BATCH_SIZE = std::max(1, (new_outputs.size() / n_threads)); + for (std::size_t i = 0; i < new_outputs.size(); i += LEAF_CONVERT_BATCH_SIZE) + { + const std::size_t end = std::min(i + LEAF_CONVERT_BATCH_SIZE, new_outputs.size()); + tpool.submit(&waiter, + [ + &new_outputs, + &valid_outputs, + &pre_leaves, + i, + end, + use_fast_torsion_check + ]() + { + for (std::size_t j = i; j < end; ++j) + { + CHECK_AND_ASSERT_THROW_MES(valid_outputs.size() > j, "unexpected valid outputs size"); + CHECK_AND_ASSERT_THROW_MES(!valid_outputs[j], "unexpected valid output"); + CHECK_AND_ASSERT_THROW_MES(pre_leaves.size() > j, "unexpected pre_leaves size"); + + const auto &output_pair = new_outputs[j].output_pair; + try + { + pre_leaves[j] = output_to_pre_leaf_tuple(output_pair, use_fast_torsion_check); + } + catch(...) + { + /* Invalid outputs can't be added to the tree */ + LOG_PRINT_L2("Output " << new_outputs[j].unified_id << " is invalid (out pubkey " + << output_pubkey_cref(output_pair) + << " , commitment " << commitment_cref(output_pair) << ")"); + continue; + } + + valid_outputs[j] = True; + } + }, + true + ); + } + + CHECK_AND_ASSERT_THROW_MES(waiter.wait(), "failed to convert outputs to ed derivatives"); + TIME_MEASURE_FINISH(convert_valid_leaves); + + TIME_MEASURE_START(collect_derivatives); + // Step 2. Collect valid Edwards y derivatives + const std::size_t n_valid_outputs = std::count(valid_outputs.begin(), valid_outputs.end(), True); + const std::size_t n_valid_leaf_points = n_valid_outputs * LEAF_TUPLE_POINTS; + + // Collecting [(1+y),(1-y),((1-y)*x)] for batch inversion + std::unique_ptr one_plus_y_vec = std::make_unique(n_valid_leaf_points); + std::unique_ptr fe_batch = std::make_unique(n_valid_leaf_points * 2); + std::unique_ptr batch_inv_res = std::make_unique(n_valid_leaf_points * 2); + + std::size_t valid_i = 0, batch_i = 0; + for (std::size_t i = 0; i < valid_outputs.size(); ++i) + { + if (!valid_outputs[i]) + continue; + + CHECK_AND_ASSERT_THROW_MES(pre_leaves.size() > i, "unexpected size of pre_leaves"); + CHECK_AND_ASSERT_THROW_MES(n_valid_leaf_points > valid_i, "unexpected valid_i"); + + auto &pl = pre_leaves[i]; + + auto &O_derivatives = pl.O_derivatives; + auto &I_derivatives = pl.I_derivatives; + auto &C_derivatives = pl.C_derivatives; + + static_assert(LEAF_TUPLE_POINTS == 3, "unexpected n leaf tuple points"); + + // TODO: avoid copying underlying (tried using pointer to pointers, but wasn't clean) + memcpy(&one_plus_y_vec[valid_i++], &O_derivatives.one_plus_y, sizeof(fe)); + memcpy(&one_plus_y_vec[valid_i++], &I_derivatives.one_plus_y, sizeof(fe)); + memcpy(&one_plus_y_vec[valid_i++], &C_derivatives.one_plus_y, sizeof(fe)); + + memcpy(&fe_batch[batch_i++], &O_derivatives.one_minus_y, sizeof(fe)); + memcpy(&fe_batch[batch_i++], &O_derivatives.one_minus_y_mul_x, sizeof(fe)); + + memcpy(&fe_batch[batch_i++], &I_derivatives.one_minus_y, sizeof(fe)); + memcpy(&fe_batch[batch_i++], &I_derivatives.one_minus_y_mul_x, sizeof(fe)); + + memcpy(&fe_batch[batch_i++], &C_derivatives.one_minus_y, sizeof(fe)); + memcpy(&fe_batch[batch_i++], &C_derivatives.one_minus_y_mul_x, sizeof(fe)); + } + + CHECK_AND_ASSERT_THROW_MES(n_valid_leaf_points == valid_i, "unexpected end valid_i"); + CHECK_AND_ASSERT_THROW_MES((n_valid_leaf_points * 2) == batch_i, "unexpected end batch_i"); + TIME_MEASURE_FINISH(collect_derivatives); + + TIME_MEASURE_START(batch_invert); + // Step 3. Get batch inverse of all valid (1-y)'s and ((1-y)*x)'s + // - Batch inversion is significantly faster than inverting 1 at a time + fe_batch_invert(batch_inv_res.get(), fe_batch.get(), n_valid_leaf_points * 2); + TIME_MEASURE_FINISH(batch_invert); + + TIME_MEASURE_START(get_selene_scalars); + // Step 4. Multithreaded get Wei coordinates and convert to Selene scalars + const std::size_t n_valid_leaf_elems = n_valid_outputs * LEAF_TUPLE_SIZE; + flattened_leaves_out.resize(n_valid_leaf_elems); + CHECK_AND_ASSERT_THROW_MES(flattened_leaves_out.size() == (2 * n_valid_leaf_points), + "unexpected size of flattened leaves"); + + const std::size_t DERIVATION_BATCH_SIZE = std::max(1, (n_valid_leaf_points / n_threads)); + for (std::size_t i = 0; i < n_valid_leaf_points; i += DERIVATION_BATCH_SIZE) + { + const std::size_t end = std::min(n_valid_leaf_points, i + DERIVATION_BATCH_SIZE); + tpool.submit(&waiter, + [ + &batch_inv_res, + &one_plus_y_vec, + &flattened_leaves_out, + i, + end + ]() + { + std::size_t point_idx = i * 2; + for (std::size_t j = i; j < end; ++j) + { + crypto::ec_coord wei_x; + crypto::ec_coord wei_y; + fe_ed_derivatives_to_wei_x_y( + to_bytes(wei_x), + to_bytes(wei_y), + batch_inv_res[point_idx]/*inv_one_minus_y*/, + one_plus_y_vec[j], + batch_inv_res[point_idx+1]/*inv_one_minus_y_mul_x*/ + ); + + flattened_leaves_out[point_idx++] = tower_cycle::selene_scalar_from_bytes(wei_x); + flattened_leaves_out[point_idx++] = tower_cycle::selene_scalar_from_bytes(wei_y); + } + }, + true + ); + } + + CHECK_AND_ASSERT_THROW_MES(waiter.wait(), "failed to convert outputs to wei coords"); + TIME_MEASURE_FINISH(get_selene_scalars); + + // Step 5. Set valid tuples to be stored in the db + valid_outputs_out.reserve(n_valid_outputs); + for (std::size_t i = 0; i < valid_outputs.size(); ++i) + { + if (!valid_outputs[i]) + continue; + + CHECK_AND_ASSERT_THROW_MES(new_outputs.size() > i, "unexpected size of valid outputs"); + + // We can derive leaf tuples from output pairs, so we store just the unified output in the db to save 32 bytes + valid_outputs_out.emplace_back(std::move(new_outputs[i])); + } + + TIME_MEASURE_FINISH(outputs_to_leaves); + + m_convert_valid_leaves_ms += convert_valid_leaves; + m_collect_derivatives_ms += collect_derivatives; + m_batch_invert_ms += batch_invert; + m_get_selene_scalars_ms += get_selene_scalars; + + m_outputs_to_leaves_ms += outputs_to_leaves; + + LOG_PRINT_L2("Total time spent setting leaves: " << m_outputs_to_leaves_ms / 1000 + << " , converting valid leaves: " << m_convert_valid_leaves_ms / 1000 + << " , collecting derivatives: " << m_collect_derivatives_ms / 1000 + << " , batch invert: " << m_batch_invert_ms / 1000 + << " , get selene scalars: " << m_get_selene_scalars_ms / 1000); +} +//---------------------------------------------------------------------------------------------------------------------- +template +GrowLayerInstructions CurveTrees::set_next_layer_extension( + const GrowLayerInstructions &prev_layer_instructions, + const bool parent_is_c2, + const LastHashes &last_hashes, + std::size_t &c1_last_idx_inout, + std::size_t &c2_last_idx_inout, + TreeExtension &tree_extension_inout) const +{ + const auto &c1_last_hashes = last_hashes.c1_last_hashes; + const auto &c2_last_hashes = last_hashes.c2_last_hashes; + + auto &c1_layer_extensions_out = tree_extension_inout.c1_layer_extensions; + auto &c2_layer_extensions_out = tree_extension_inout.c2_layer_extensions; + + const std::size_t parent_chunk_width = parent_is_c2 ? m_c2_width : m_c1_width; + + const auto grow_layer_instructions = get_grow_layer_instructions( + prev_layer_instructions.old_total_parents, + prev_layer_instructions.new_total_parents, + parent_chunk_width, + prev_layer_instructions.need_old_last_parent + ); + + if (parent_is_c2) + { + auto c2_layer_extension = get_next_layer_extension( + m_c1, + m_c2, + grow_layer_instructions, + c1_last_hashes, + c2_last_hashes, + c1_layer_extensions_out, + c1_last_idx_inout, + c2_last_idx_inout + ); + + c2_layer_extensions_out.emplace_back(std::move(c2_layer_extension)); + ++c1_last_idx_inout; + } + else + { + auto c1_layer_extension = get_next_layer_extension( + m_c2, + m_c1, + grow_layer_instructions, + c2_last_hashes, + c1_last_hashes, + c2_layer_extensions_out, + c2_last_idx_inout, + c1_last_idx_inout + ); + + c1_layer_extensions_out.emplace_back(std::move(c1_layer_extension)); + ++c2_last_idx_inout; + } + + return grow_layer_instructions; +}; +//---------------------------------------------------------------------------------------------------------------------- +template<> +TreeRootShared CurveTrees::get_tree_root_from_bytes(const std::size_t n_layers, + const crypto::ec_point &tree_root) const +{ + if (n_layers == 0) + return nullptr; + + if ((n_layers % 2) == 0) + return fcmp_pp::helios_tree_root(m_c2->from_bytes(tree_root)); + else + return fcmp_pp::selene_tree_root(m_c1->from_bytes(tree_root)); +} +//---------------------------------------------------------------------------------------------------------------------- +template<> +CurveTrees::PathForProof CurveTrees::path_for_proof( + const CurveTrees::Path &path, + const OutputTuple &output_tuple) const +{ + // Get output's index in the path + std::size_t output_idx_in_path = 0; + { + bool found = false; + for (const auto &leaf : path.leaves) + { + found = output_tuple == leaf; + if (found) + break; + ++output_idx_in_path; + } + CHECK_AND_ASSERT_THROW_MES(found, "failed to find output in path"); + } + + const size_t n_tree_layers = path.c1_layers.size() + path.c2_layers.size(); + const bool root_is_c1 = n_tree_layers % 2 == 1; + const size_t n_c2_scalar_chunk_layers = path.c1_layers.size() - size_t(root_is_c1); + const size_t n_c1_scalar_chunk_layers = path.c2_layers.size() - size_t(!root_is_c1); + + // c2 helios scalars from c1 selene points + std::vector> c2_scalar_chunks; + c2_scalar_chunks.reserve(n_c2_scalar_chunk_layers); + for (std::size_t i = 0; i < n_c2_scalar_chunk_layers; ++i) + { + const auto &c1_points = path.c1_layers.at(i); + auto &c2_scalar_layer = c2_scalar_chunks.emplace_back(); + c2_scalar_layer.reserve(m_c2_width); + for (const auto &c1_point : c1_points) + c2_scalar_layer.emplace_back(m_c1->point_to_cycle_scalar(c1_point)); + // Padding with 0's + for (std::size_t j = c1_points.size(); j < m_c2_width; ++j) + c2_scalar_layer.emplace_back(m_c2->zero_scalar()); + } + + // c1 selene scalars from c2 helios points + std::vector> c1_scalar_chunks; + c1_scalar_chunks.reserve(n_c1_scalar_chunk_layers); + for (std::size_t i = 0; i < n_c1_scalar_chunk_layers; ++i) + { + const auto &c2_points = path.c2_layers.at(i); + auto &c1_scalar_layer = c1_scalar_chunks.emplace_back(); + c1_scalar_layer.reserve(m_c1_width); + for (const auto &c2_point : c2_points) + c1_scalar_layer.emplace_back(m_c2->point_to_cycle_scalar(c2_point)); + // Padding with 0's + for (std::size_t j = c2_points.size(); j < m_c1_width; ++j) + c1_scalar_layer.emplace_back(m_c1->zero_scalar()); + } + + return PathForProof { + .leaves = path.leaves, + .output_idx = output_idx_in_path, + .c2_scalar_chunks = std::move(c2_scalar_chunks), + .c1_scalar_chunks = std::move(c1_scalar_chunks), + }; +} +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +} //namespace curve_trees +} //namespace fcmp_pp diff --git a/src/fcmp_pp/curve_trees.h b/src/fcmp_pp/curve_trees.h new file mode 100644 index 00000000000..b3f0bab8372 --- /dev/null +++ b/src/fcmp_pp/curve_trees.h @@ -0,0 +1,309 @@ +// Copyright (c) 2024, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "crypto/crypto.h" +#include "fcmp_pp_types.h" +#include "misc_log_ex.h" +#include "tower_cycle.h" + +#include +#include +#include + + +namespace fcmp_pp +{ +namespace curve_trees +{ +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +// Curve Trees type defs +//---------------------------------------------------------------------------------------------------------------------- +// A layer of contiguous hashes starting from a specific start_idx in the tree +template +struct LayerExtension final +{ + uint64_t start_idx{0}; + bool update_existing_last_hash; + std::vector hashes; +}; + +// Useful metadata for growing a layer +struct GrowLayerInstructions final +{ + // The max chunk width of children used to hash into a parent + std::size_t parent_chunk_width; + + // Total parents refers to the total number of hashes of chunks of children + uint64_t old_total_parents; + uint64_t new_total_parents; + + // When updating the tree, we use this boolean to know when we'll need to use the tree's existing old root in order + // to set a new layer after that root + // - We'll need to be sure the old root gets hashed when setting the next layer + bool setting_next_layer_after_old_root; + // When the last child in the child layer changes, we'll need to use its old value to update its parent hash + bool need_old_last_child; + // When the last parent in the layer changes, we'll need to use its old value to update itself + bool need_old_last_parent; + + // The first chunk that needs to be updated's first child's offset within that chunk + std::size_t start_offset; + // The parent's starting index in the layer + uint64_t next_parent_start_index; +}; +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +// Hash a chunk of new children +template +typename C::Point get_new_parent(const std::unique_ptr &curve, const typename C::Chunk &new_children); +//---------------------------------------------------------------------------------------------------------------------- +OutputTuple output_to_tuple(const OutputPair &output_pair, bool use_fast_check = false); +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +// This class is useful to help update the curve trees merkle tree without needing to keep the entire tree in memory +// - It requires instantiation with the C1 and C2 curve classes and widths, hardening the tree structure +// - It ties the C1 curve in the tree to the leaf layer (the leaf layer is composed of C1 scalars) +template +class CurveTrees +{ +public: + CurveTrees(std::unique_ptr &&c1, + std::unique_ptr &&c2, + const std::size_t c1_width, + const std::size_t c2_width): + m_c1{std::move(c1)}, + m_c2{std::move(c2)}, + m_c1_width{c1_width}, + m_c2_width{c2_width}, + m_leaf_layer_chunk_width{LEAF_TUPLE_SIZE * c1_width} + { + assert(c1_width > 0); + assert(c2_width > 0); + }; + +//member structs +public: + // Tuple that composes a single leaf in the tree + struct LeafTuple final + { + // Output ed25519 point wei x and y coordinates + typename C1::Scalar O_x; + typename C1::Scalar O_y; + // Key image generator wei x and y coordinates + typename C1::Scalar I_x; + typename C1::Scalar I_y; + // Commitment wei x and y coordinates + typename C1::Scalar C_x; + typename C1::Scalar C_y; + }; + + static const std::size_t LEAF_TUPLE_POINTS = 3; + static constexpr std::size_t LEAF_TUPLE_SIZE = LEAF_TUPLE_POINTS * 2; + + static_assert(sizeof(LeafTuple) == (sizeof(typename C1::Scalar) * LEAF_TUPLE_SIZE), "unexpected LeafTuple size"); + + // A struct useful to extend an existing tree + // - layers alternate between C1 and C2 + // - c1_layer_extensions[0] is first layer after leaves, then c2_layer_extensions[0], c1_layer_extensions[1], etc + struct TreeExtension final + { + ContiguousLeaves leaves; + std::vector> c1_layer_extensions; + std::vector> c2_layer_extensions; + }; + + // Last hashes from each layer in the tree + // - layers alternate between C1 and C2 + // - c1_last_hashes[0] refers to the layer after leaves, then c2_last_hashes[0], then c1_last_hashes[1], etc + struct LastHashes final + { + std::vector c1_last_hashes; + std::vector c2_last_hashes; + }; + + // A path in the tree containing whole chunks at each layer + // - leaves contain a complete chunk of leaves, encoded as compressed ed25519 points + // - c1_layers[0] refers to the chunk of elems in the tree in the layer after leaves. The hash of the chunk of + // leaves is 1 member of the c1_layers[0] chunk. The rest of c1_layers[0] is the chunk of elems that hash is in. + // - layers alternate between C1 and C2 + // - c2_layers[0] refers to the chunk of elems in the tree in the layer after c1_layers[0]. The hash of the chunk + // of c1_layers[0] is 1 member of the c2_layers[0] chunk. The rest of c2_layers[0] is the chunk of elems that hash + // is in. + // - c1_layers[1] refers to the chunk of elems in the tree in the layer after c2_layers[0] etc. + struct Path final + { + std::vector leaves; + std::vector> c1_layers; + std::vector> c2_layers; + + void clear() + { + leaves.clear(); + c1_layers.clear(); + c2_layers.clear(); + } + + bool empty() const { return leaves.empty() && c1_layers.empty() && c2_layers.empty(); } + }; + + // Contains minimum path elems necessary for multiple paths (e.g. only contains the root once) + struct ConsolidatedPaths final + { + std::unordered_map> leaves_by_chunk_idx; + std::vector>> c1_layers; + std::vector>> c2_layers; + }; + + // A path ready to be used to construct an FCMP++ proof + struct PathForProof final + { + std::vector leaves; + std::size_t output_idx; + std::vector> c2_scalar_chunks; + std::vector> c1_scalar_chunks; + }; + +//member functions +public: + // Convert output pairs into leaf tuples, from {output pubkey,commitment} -> {O,C} -> {O.x,O.y,I.x,I.y,C.x,C.y} + LeafTuple leaf_tuple(const OutputPair &output_pair) const; + + // Flatten leaves + // From: [{O.x,O.y,I.x,I.y,C.x,C.y},{O.x,O.y,I.x,I.y,C.x,C.y},...] + // To: [O.x,O.y,I.x,I.y,C.x,C.y,O.x,O.y,I.x,I.y,C.x,C.y...] + std::vector flatten_leaves(std::vector &&leaves) const; + + // Take in the existing number of leaf tuples and the existing last hash in each layer in the tree, as well as new + // outputs to add to the tree, and return a tree extension struct that can be used to extend a tree + TreeExtension get_tree_extension(const uint64_t old_n_leaf_tuples, + const LastHashes &existing_last_hashes, + std::vector> &&new_outputs, + const bool use_fast_torsion_check = false) const; + + // Compress all the points in the tree extension + CompressedTreeExtension compress_tree_extension(TreeExtension &&tree_extension) const; + + // Calculate the number of elems in each layer of the tree based on the number of leaf tuples + std::vector n_elems_per_layer(const uint64_t n_leaf_tuples) const; + + // Calculate how many layers in the tree there are based on the number of leaf tuples + std::size_t n_layers(const uint64_t n_leaf_tuples) const; + + // Get path indexes for the provided leaf tuple + // - Returns empty path indexes if leaf is not in the tree (if n_leaf_tuples <= leaf_tuple_idx) + PathIndexes get_path_indexes(const uint64_t n_leaf_tuples, const uint64_t leaf_tuple_idx) const; + + // Get child chunk indexes for the provided leaf tuple + // - Returns empty if leaf is not in the tree (if n_leaf_tuples <= leaf_tuple_idx) + std::vector get_child_chunk_indexes(const uint64_t n_leaf_tuples, const uint64_t leaf_tuple_idx) const; + + LastHashes tree_edge_to_last_hashes(const std::vector &tree_edge_to_last_hashes) const; + + // Audit the provided path + bool audit_path(const Path &path, const OutputPair &output, const uint64_t n_leaf_tuples_in_tree) const; + + TreeRootShared get_tree_root_from_bytes(const std::size_t n_layers, const crypto::ec_point &tree_root) const; + + Path path_bytes_to_path(const CompressedPath &path_bytes) const; + + CompressedPath compress_path(const Path &path, const std::vector &outputs) const; + + PathForProof path_for_proof(const Path &path, const OutputTuple &output_tuple) const; + + std::vector calc_hashes_from_path(const Path &path, const bool replace_last_hash = false) const; + + TreeExtension path_to_tree_extension(const CompressedPath &path_bytes, const PathIndexes &path_idxs) const; + + ConsolidatedPaths get_dummy_paths(const std::vector &outputs, uint8_t n_layers) const; + + Path get_single_dummy_path(const ConsolidatedPaths &dummy_paths, + const uint64_t n_leaf_tuples, + const uint64_t leaf_tuple_idx) const; +private: + // Multithreaded helper function to convert valid outputs to leaf tuples ready for insertion to the tree & db + void outputs_to_leaves(std::vector &&new_outputs, + std::vector &flattened_leaves_out, + std::vector &valid_outputs_out, + const bool use_fast_torsion_check = false) const; + + // Helper function used to set the next layer extension used to grow the next layer in the tree + // - for example, if we just grew the parent layer after the leaf layer, the "next layer" would be the grandparent + // layer of the leaf layer + GrowLayerInstructions set_next_layer_extension( + const GrowLayerInstructions &prev_layer_instructions, + const bool parent_is_c1, + const LastHashes &last_hashes, + std::size_t &c1_last_idx_inout, + std::size_t &c2_last_idx_inout, + TreeExtension &tree_extension_inout) const; + +//private state +private: + mutable uint64_t m_outputs_to_leaves_ms{0}; + mutable uint64_t m_get_selene_scalars_ms{0}; + mutable uint64_t m_batch_invert_ms{0}; + mutable uint64_t m_collect_derivatives_ms{0}; + mutable uint64_t m_convert_valid_leaves_ms{0}; + + mutable uint64_t m_sorting_outputs_ms{0}; + mutable uint64_t m_hash_leaves_ms{0}; + mutable uint64_t m_hash_layers_ms{0}; + +//public member variables +public: + // The curve interfaces + const std::unique_ptr m_c1; + const std::unique_ptr m_c2; + + // The leaf layer has a distinct chunk width than the other layers + const std::size_t m_leaf_layer_chunk_width; + + // The chunk widths of the layers in the tree tied to each curve + const std::size_t m_c1_width; + const std::size_t m_c2_width; +}; +//---------------------------------------------------------------------------------------------------------------------- +using Selene = tower_cycle::Selene; +using Helios = tower_cycle::Helios; +using CurveTreesV1 = CurveTrees; + +// https://github.com/kayabaNerve/fcmp-plus-plus/blob +// /b2742e86f3d18155fd34dd1ed69cb8f79b900fce/crypto/fcmps/src/tests.rs#L81-L82 +const std::size_t SELENE_CHUNK_WIDTH = 38; +const std::size_t HELIOS_CHUNK_WIDTH = 18; + +std::shared_ptr curve_trees_v1( + const std::size_t selene_chunk_width = SELENE_CHUNK_WIDTH, + const std::size_t helios_chunk_width = HELIOS_CHUNK_WIDTH); +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +} //namespace curve_trees +} //namespace fcmp_pp diff --git a/src/fcmp_pp/fcmp_pp_crypto.cpp b/src/fcmp_pp/fcmp_pp_crypto.cpp new file mode 100644 index 00000000000..2df466c05ed --- /dev/null +++ b/src/fcmp_pp/fcmp_pp_crypto.cpp @@ -0,0 +1,413 @@ +// Copyright (c) 2024, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "fcmp_pp_crypto.h" + +#include "misc_log_ex.h" + +#include + +// static void print_bytes(const fe f) +// { +// unsigned char bytes[32]; +// fe_tobytes(bytes, f); +// for (int i = 0; i < 32; ++i) +// { +// printf("%d, ", bytes[i]); +// } +// printf("\n"); +// } + +// static void print_fe(const fe f) +// { +// for (int i = 0; i < 10; ++i) +// { +// printf("%d, ", f[i]); +// } +// printf("\n"); +// } + +static bool fe_compare(const fe a, const fe b) +{ + unsigned char a_bytes[32]; + unsigned char b_bytes[32]; + + fe_tobytes(a_bytes, a); + fe_tobytes(b_bytes, b); + + return memcmp(a_bytes, b_bytes, sizeof(a_bytes)) == 0; +} + +static bool sqrt_ext(fe y, const fe x) +{ + fe y_res; + + fe x2; + fe_dbl(x2, x); + + fe b; + fe_pow22523(b, x2); + + fe b_sq; + fe_sq(b_sq, b); + + fe c; + fe_mul(c, x2, b_sq); + + if (fe_compare(c, fe_one) || fe_compare(c, fe_m1)) + { + fe_0(c); + c[0] = 3; + } + + fe c_sub_1; + fe_sub(c_sub_1, c, fe_one); + + fe_mul(y_res, x, b); + fe_mul(y_res, y_res, c_sub_1); + + if (fe_isnegative(y_res)) { + fe_neg(y_res, y_res); + } + + fe y_sq; + fe_sq(y_sq, y_res); + bool r = fe_compare(x, y_sq); + + fe_copy(y, y_res); + return r; +}; + +namespace fcmp_pp +{ + // TODO: impl faster sqrt + bool sqrt(fe y, const fe x) + { + return sqrt_ext(y, x); + }; +}//namespace fcmp_pp + +static void inv_iso(fe u_out, fe w_out, const fe u, const fe w) +{ + // 4u + fe_dbl(u_out, u); + fe_dbl(u_out, u_out); + // 2w + fe_dbl(w_out, w); +}; + +static void inv_psi1(fe e_out, fe u_out, fe w_out, const fe e, const fe u, const fe w) +{ + fe e_res, u_res, w_res; + + fe tt; + bool cc = sqrt_ext(tt, u); + fe_copy(w_res, tt); + fe w_; + fe_copy(w_, w); + fe_copy(e_res, e); + + if (!cc) + { + fe tt_sq; + fe_sq(tt_sq, tt); + fe neg_u_dbl; + fe_dbl(neg_u_dbl, u); + fe_neg(neg_u_dbl, neg_u_dbl); + if (fe_compare(tt_sq, neg_u_dbl)) { + fe_mul(tt, tt, fe_sqrtm1); + } + + fe_mul(w_, w, tt); + + fe e_sq; + fe_sq(e_sq, e); + fe_mul(w_res, fe_msqrt2b, e_sq); + + fe_mul(e_res, e_res, tt); + } + + fe w_res_sq; + fe_sq(w_res_sq, w_res); + + fe e_res_sq; + fe_sq(e_res_sq, e_res); + + fe A_e_sq; + fe_mul(A_e_sq, fe_a0, e_res_sq); + + fe w_res_w; + fe_mul(w_res_w, w_res, w_); + + fe_sub(u_res, w_res_sq, A_e_sq); + fe_reduce(u_res, u_res); + fe_sub(u_res, u_res, w_res_w); + fe_mul(u_res, u_res, fe_inv2); + + fe_copy(e_out, e_res); + fe_copy(u_out, u_res); + fe_copy(w_out, w_res); +}; + +static bool inv_psi2(fe u_out, fe w_out, const fe e, const fe u, const fe w) +{ + fe u_res, w_res; + + if (!fcmp_pp::sqrt(w_res, u)) + return false; + fe e_sq; + fe_sq(e_sq, e); + fe Ap_e_sq; + fe_mul(Ap_e_sq, fe_ap, e_sq); + + fe w_res_w; + fe_mul(w_res_w, w_res, w); + + fe_sub(u_res, u, Ap_e_sq); + fe_reduce(u_res, u_res); + fe_sub(u_res, u_res, w_res_w); + fe_mul(u_res, u_res, fe_inv2); + + fe_copy(u_out, u_res); + fe_copy(w_out, w_res); + + return true; +}; + +#if !defined(NDEBUG) +static bool check_e_u_w(const fe e, const fe u, const fe w) +{ + fe a; + fe_1(a); + fe_neg(a, a); + fe A; + fe_add(A, a, fe_d); + fe_dbl(A, A); + fe B; + fe_sq(B, fe_a_sub_d); + + fe w_sq, u_w_sq; + fe_sq(w_sq, w); + fe_mul(u_w_sq, u, w_sq); + + fe u_sq, e_sq, e_sq_sq; + fe_sq(u_sq, u); + fe_sq(e_sq, e); + fe_sq(e_sq_sq, e_sq); + + fe A_u_mul_e_sq, B_mul_e_sq_sq; + fe_mul(A_u_mul_e_sq, A, u); + fe_mul(A_u_mul_e_sq, A_u_mul_e_sq, e_sq); + fe_mul(B_mul_e_sq_sq, B, e_sq_sq); + + fe_reduce(A_u_mul_e_sq, A_u_mul_e_sq); + fe_reduce(B_mul_e_sq_sq, B_mul_e_sq_sq); + + fe sum; + fe_add(sum, u_sq, A_u_mul_e_sq); + fe_reduce(sum, sum); + fe_add(sum, sum, B_mul_e_sq_sq); + fe_reduce(sum, sum); + + if (!fe_compare(u_w_sq, sum)) { + return false; + } + + return true; +} +#endif + +namespace fcmp_pp +{ +//---------------------------------------------------------------------------------------------------------------------- +bool mul8_is_identity(const ge_p3 &point) { + ge_p2 point_ge_p2; + ge_p3_to_p2(&point_ge_p2, &point); + ge_p1p1 point_mul8; + ge_mul8(&point_mul8, &point_ge_p2); + ge_p3 point_mul8_p3; + ge_p1p1_to_p3(&point_mul8_p3, &point_mul8); + return ge_p3_is_point_at_infinity_vartime(&point_mul8_p3); +} +//---------------------------------------------------------------------------------------------------------------------- +// https://github.com/kayabaNerve/fcmp-plus-plus/blob/94744c5324e869a9483bbbd93a864e108304bf76/crypto/divisors/src/tests/torsion_check.rs +// Returns true if point is torsion free +// Pre-condition: point is a valid point and point*8 not equal to identity +// WARNING1: this approach needs to be carefully vetted academically and audited +// before it can be used in production. +// WARNING2: since fe_add and fe_sub expect the input fe's to be within a +// smaller domain than the output fe, we sometimes need to "reduce" a field elem +// to chain calls to fe_add and fe_sub. Notice all calls to fe_reduce. +bool torsion_check_vartime(const ge_p3 &point) { + assert(!mul8_is_identity(point)); + + // ed to wei + fe e, u, w; + { + fe z_plus_ed_y, z_minus_ed_y; + fe_add(z_plus_ed_y, fe_one, point.Y); + fe_sub(z_minus_ed_y, fe_one, point.Y); + + // e + fe_mul(e, z_minus_ed_y, point.X); + // u + fe_mul(u, fe_a_sub_d, z_plus_ed_y); + fe_mul(u, u, point.X); + fe_mul(u, u, e); + // w + fe_dbl(w, z_minus_ed_y); + } + + assert(check_e_u_w(e, u, w)); + + // Torsion check + for (int i = 0; i < 2; ++i) { + inv_iso(u, w, u, w); + if (!inv_psi2(u, w, e, u, w)) { + return false; + } + inv_psi1(e, u, w, e, u, w); + assert(check_e_u_w(e, u, w)); + } + + fe _; + inv_iso(u, _, u, w); + + if (!sqrt(u, u)) { + return false; + } + + return true; +} +//---------------------------------------------------------------------------------------------------------------------- +crypto::ec_point clear_torsion(const ge_p3 &point) { + // mul by inv 8, then mul by 8 + ge_p2 point_inv_8; + ge_scalarmult(&point_inv_8, to_bytes(crypto::EC_INV_EIGHT), &point); + ge_p1p1 point_inv_8_mul_8; + ge_mul8(&point_inv_8_mul_8, &point_inv_8); + ge_p3 torsion_cleared_point; + ge_p1p1_to_p3(&torsion_cleared_point, &point_inv_8_mul_8); + crypto::ec_point k_out; + ge_p3_tobytes(to_bytes(k_out), &torsion_cleared_point); + return k_out; +} +//---------------------------------------------------------------------------------------------------------------------- +bool get_valid_torsion_cleared_point(const crypto::ec_point &point, crypto::ec_point &torsion_cleared_out) { + ge_p3 p3; + if (ge_frombytes_vartime(&p3, to_bytes(point)) != 0) + return false; + if (mul8_is_identity(p3)) + return false; + torsion_cleared_out = fcmp_pp::clear_torsion(p3); + if (torsion_cleared_out == crypto::EC_I) + return false; + return true; +} +//---------------------------------------------------------------------------------------------------------------------- +// torsion_check_vartime is a risky optimization to avoid needing to clear torsion when not necessary. We label this +// function "fast" because it uses the optimization. This function should only be used in contexts where the risk is +// acceptable. +bool get_valid_torsion_cleared_point_fast(const crypto::ec_point &point, crypto::ec_point &torsion_cleared_out) { + ge_p3 p3; + if (ge_frombytes_vartime(&p3, to_bytes(point)) != 0) + return false; + if (mul8_is_identity(p3)) + return false; + torsion_cleared_out = point; + if (!torsion_check_vartime(p3)) + torsion_cleared_out = fcmp_pp::clear_torsion(p3); + if (torsion_cleared_out == crypto::EC_I) + return false; + return true; +} +//---------------------------------------------------------------------------------------------------------------------- +bool point_to_ed_derivatives(const crypto::ec_point &pub, EdDerivatives &ed_derivatives) { + if (pub == crypto::EC_I) + return false; + // fe y; + ge_p3 p3; + if (ge_frombytes_vartime(&p3, to_bytes(pub)) != 0) + return false; + fe one; + fe_1(one); + // (1+y),(1-y) + fe_add(ed_derivatives.one_plus_y, one, p3.Y); + fe_sub(ed_derivatives.one_minus_y, one, p3.Y); + // (1-y) * x + fe_mul(ed_derivatives.one_minus_y_mul_x, ed_derivatives.one_minus_y, p3.X); + return true; +} +//---------------------------------------------------------------------------------------------------------------------- +bool ed_derivatives_to_wei_x_y(const EdDerivatives &ed_derivatives, crypto::ec_coord &wei_x, crypto::ec_coord &wei_y) { + static const int N_ELEMS = 2; + + // Get inverse of (1-y) and ((1-y)*x) + std::unique_ptr fe_batch = std::make_unique(N_ELEMS); + std::unique_ptr inv_res = std::make_unique(N_ELEMS); + + memcpy(&fe_batch[0], &ed_derivatives.one_minus_y, sizeof(fe)); + memcpy(&fe_batch[1], &ed_derivatives.one_minus_y_mul_x, sizeof(fe)); + + fe_batch_invert(inv_res.get(), fe_batch.get(), N_ELEMS); + + fe_ed_derivatives_to_wei_x_y( + to_bytes(wei_x), + to_bytes(wei_y), + inv_res[0]/*(1/(1-y))*/, + ed_derivatives.one_plus_y, + inv_res[1]/*(1/((1-y)*x))*/); + + return true; +} +//---------------------------------------------------------------------------------------------------------------------- +bool point_to_wei_x(const crypto::ec_point &pub, crypto::ec_coord &wei_x) { + EdDerivatives ed_derivatives; + if (!point_to_ed_derivatives(pub, ed_derivatives)) + return false; + crypto::ec_coord _; + return ed_derivatives_to_wei_x_y(ed_derivatives, wei_x, _); +} +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +void scalarmult_and_add(unsigned char *Q, const ge_p3 &P, const unsigned char *a, const ge_p3 &A) +{ + ge_p2 p2; + ge_p3 p3; + ge_p1p1 p1p1; + ge_cached c; + + ge_p3_to_cached(&c, &P); + ge_scalarmult_p3(&p3, a, &A); + ge_add(&p1p1, &p3, &c); + ge_p1p1_to_p2(&p2, &p1p1); + ge_tobytes(Q, &p2); +} +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +}//namespace fcmp_pp diff --git a/src/fcmp_pp/fcmp_pp_crypto.h b/src/fcmp_pp/fcmp_pp_crypto.h new file mode 100644 index 00000000000..9a06c7d4483 --- /dev/null +++ b/src/fcmp_pp/fcmp_pp_crypto.h @@ -0,0 +1,64 @@ +// 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 + +extern "C" +{ +#include "crypto/crypto-ops.h" +} +#include "crypto/crypto.h" + +namespace fcmp_pp +{ +//---------------------------------------------------------------------------------------------------------------------- +// Field elems needed to get wei x and y coords +struct EdDerivatives final +{ + fe one_plus_y; + fe one_minus_y; + fe one_minus_y_mul_x; +}; +//---------------------------------------------------------------------------------------------------------------------- +// TODO: tests for these functions +bool sqrt(fe y, const fe x); +bool mul8_is_identity(const ge_p3 &point); +bool torsion_check_vartime(const ge_p3 &point); +crypto::ec_point clear_torsion(const ge_p3 &point); +bool get_valid_torsion_cleared_point(const crypto::ec_point &point, crypto::ec_point &torsion_cleared_out); +bool get_valid_torsion_cleared_point_fast(const crypto::ec_point &point, crypto::ec_point &torsion_cleared_out); +bool point_to_ed_derivatives(const crypto::ec_point &pub, EdDerivatives &ed_derivatives); +bool ed_derivatives_to_wei_x_y(const EdDerivatives &ed_derivatives, crypto::ec_coord &wei_x, crypto::ec_coord &wei_y); +bool point_to_wei_x(const crypto::ec_point &pub, crypto::ec_coord &wei_x); +/** + * brief - scalarmult_and_add - Q = P + a * A + */ +void scalarmult_and_add(unsigned char *Q, const ge_p3 &P, const unsigned char *a, const ge_p3 &A); +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +}//namespace fcmp_pp diff --git a/src/fcmp_pp/fcmp_pp_rust/.gitignore b/src/fcmp_pp/fcmp_pp_rust/.gitignore new file mode 100644 index 00000000000..5a07b8927f8 --- /dev/null +++ b/src/fcmp_pp/fcmp_pp_rust/.gitignore @@ -0,0 +1,4 @@ +# If a developer runs cargo build inside this sub-directory to only work with +# the Rust side of things, they'll create this target directory which shouldn't +# be committed +target diff --git a/src/fcmp_pp/fcmp_pp_rust/CMakeLists.txt b/src/fcmp_pp/fcmp_pp_rust/CMakeLists.txt new file mode 100644 index 00000000000..818a7dc2d2d --- /dev/null +++ b/src/fcmp_pp/fcmp_pp_rust/CMakeLists.txt @@ -0,0 +1,125 @@ +# Copyright (c) 2016-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. + +if(ARCH_ID MATCHES "x86-64" OR ARCH_ID MATCHES "amd64") + set(RUST_ARCH "x86_64") +elseif(ARCH_ID MATCHES "(arm64|armv8a|armv8-a)") + set(RUST_ARCH "aarch64") +elseif(ARCH_ID MATCHES "armv7-a") + set(RUST_ARCH "armv7") +elseif(ARCH_ID MATCHES "i386") + set(RUST_ARCH "i686") +elseif(ARCH_ID MATCHES "riscv64") + set(RUST_ARCH "riscv64gc") +else() + set(RUST_ARCH "${ARCH_ID}") +endif() + +if(MINGW) + set(RUST_PLATFORM "pc-windows") + set(RUST_TOOLCHAIN "-gnu") +elseif(MSVC) + set(RUST_PLATFORM "pc-windows") + set(RUST_TOOLCHAIN "-msvc") +elseif(APPLE) + set(RUST_PLATFORM "apple-darwin") + set(RUST_TOOLCHAIN "") +elseif(FREEBSD) + set(RUST_PLATFORM "unknown-freebsd") + set(RUST_TOOLCHAIN "") +elseif(OPENBSD) + set(RUST_PLATFORM "unknown-openbsd") + set(RUST_TOOLCHAIN "") +elseif(ANDROID) + set(RUST_PLATFORM "linux-android") + if(RUST_ARCH MATCHES "(arm|armv7)") + set(RUST_TOOLCHAIN "eabi") + else() + set(RUST_TOOLCHAIN "") + endif() +elseif(DRAGONFLY) + set(RUST_PLATFORM "unknown-dragonfly") + set(RUST_TOOLCHAIN "") +elseif(CMAKE_SYSTEM_NAME MATCHES "(SunOS|Solaris)") + if(RUST_ARCH MATCHES "x86_64") + set(RUST_PLATFORM "pc-solaris") + set(RUST_TOOLCHAIN "") + elseif(RUST_ARCH MATCHES "sparcv9") + set(RUST_PLATFORM "sun-solaris") + set(RUST_TOOLCHAIN "") + endif() +else() + set(RUST_PLATFORM "unknown-linux") + if(RUST_ARCH MATCHES "armv7") + # Rust does support non-HF, yet Monero assumes HF for armv7 + set(RUST_TOOLCHAIN "-gnueabihf") + else() + set(RUST_TOOLCHAIN "-gnu") + endif() +endif() + +set(RUST_TARGET "${RUST_ARCH}-${RUST_PLATFORM}${RUST_TOOLCHAIN}") +message(STATUS "Using Rust target ${RUST_TARGET}") + +if(CMAKE_BUILD_TYPE STREQUAL "Debug") + if(SANITIZE) + message(STATUS "Using Rust ASAN") + # Rust ASAN is an experimental feature included in nightly at time of writing + set(CARGO_CMD RUSTFLAGS=-Zsanitizer=address cargo +nightly build --target "${RUST_TARGET}") + else() + set(CARGO_CMD cargo build --target "${RUST_TARGET}") + endif() + set(TARGET_DIR "debug") +else () + set(CARGO_CMD cargo build --target "${RUST_TARGET}" --release) + set(TARGET_DIR "release") +endif () + +set(FCMP_PP_RUST_HEADER_DIR "${MONERO_GENERATED_HEADERS_DIR}/fcmp_pp_rust") +set(FCMP_PP_RUST_HEADER "${FCMP_PP_RUST_HEADER_DIR}/fcmp++.h") +set(FCMP_PP_RUST_LIB "${CMAKE_CURRENT_BINARY_DIR}/libfcmp_pp_rust.a") + +# Removing OUTPUT files makes sure custom command runs every time +file(REMOVE_RECURSE "${FCMP_PP_RUST_HEADER_DIR}") +file(MAKE_DIRECTORY "${FCMP_PP_RUST_HEADER_DIR}") + +file(REMOVE "${FCMP_PP_RUST_LIB}") + +add_custom_command( + COMMENT "Building fcmp++ rust lib" + OUTPUT ${FCMP_PP_RUST_HEADER} + OUTPUT ${FCMP_PP_RUST_LIB} + COMMAND ${CMAKE_COMMAND} -E env CARGO_TARGET_DIR=${CMAKE_CURRENT_BINARY_DIR} ${CARGO_CMD} + COMMAND cp ${CMAKE_CURRENT_SOURCE_DIR}/fcmp++.h ${FCMP_PP_RUST_HEADER} + COMMAND cp ${CMAKE_CURRENT_BINARY_DIR}/${RUST_TARGET}/${TARGET_DIR}/libfcmp_pp_rust.a ${FCMP_PP_RUST_LIB} + COMMAND echo "Finished copying fcmp++ rust targets" + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + VERBATIM +) + +add_custom_target(fcmp_pp_rust DEPENDS ${FCMP_PP_RUST_LIB}) diff --git a/src/fcmp_pp/fcmp_pp_rust/Cargo.lock b/src/fcmp_pp/fcmp_pp_rust/Cargo.lock new file mode 100644 index 00000000000..0c6d7f1fe21 --- /dev/null +++ b/src/fcmp_pp/fcmp_pp_rust/Cargo.lock @@ -0,0 +1,855 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array 0.14.9", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "ciphersuite" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ae44ce6224d75ced726e5597265b8f334b9bf4767b8b42e058f2304005d8475" +dependencies = [ + "dalek-ff-group 0.4.6", + "digest", + "elliptic-curve", + "ff", + "flexible-transcript", + "group", + "k256", + "minimal-ed448", + "p256", + "rand_core", + "sha2", + "sha3", + "std-shims", + "subtle", + "zeroize", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "git+https://github.com/kayabaNerve/crypto-bigint?branch=c-repr#78352771313f1e9b8e48abe5ce30d50d6bdd291d" +dependencies = [ + "generic-array 0.14.9", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array 0.14.9", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "ff", + "fiat-crypto", + "group", + "rand_core", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dalek-ff-group" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "231684731518402967286b76bedadedfd6b0867f814e2cd41d4a82bedea2902a" +dependencies = [ + "crypto-bigint", + "curve25519-dalek", + "digest", + "ff", + "group", + "rand_core", + "rustversion", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "dalek-ff-group" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26103554e8f7857b6015a4a9ef551c6ce0669f6caa040785d4394bf52041ccaf" +dependencies = [ + "ciphersuite", + "crypto-bigint", + "curve25519-dalek", + "digest", + "ff", + "group", + "rand_core", + "rustversion", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "ec-divisors" +version = "0.1.0" +source = "git+https://github.com/monero-oxide/monero-oxide?rev=0d6f5e840ad1f955e4e4dec00c5165f134815b15#0d6f5e840ad1f955e4e4dec00c5165f134815b15" +dependencies = [ + "crypto-bigint", + "dalek-ff-group 0.5.0", + "ff", + "group", + "rand_core", + "std-shims", + "subtle", + "zeroize", +] + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "elliptic-curve", + "signature", + "spki", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array 0.14.9", + "group", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "tap", + "zeroize", +] + +[[package]] +name = "fcmp_pp_rust" +version = "0.0.0" +dependencies = [ + "ciphersuite", + "dalek-ff-group 0.5.0", + "ec-divisors", + "full-chain-membership-proofs", + "generalized-bulletproofs", + "helioselene", + "monero-ed25519", + "monero-fcmp-plus-plus", + "monero-fcmp-plus-plus-generators", + "multiexp", + "rand_core", + "std-shims", +] + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "bitvec", + "rand_core", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "flexible-transcript" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "329fdf3a39d36921c94b665764caa152fdf08f0ed631085ba4ee7e51899d93b4" +dependencies = [ + "blake2", + "digest", + "merlin", + "std-shims", + "subtle", + "zeroize", +] + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "full-chain-membership-proofs" +version = "0.1.0" +source = "git+https://github.com/monero-oxide/monero-oxide?rev=0d6f5e840ad1f955e4e4dec00c5165f134815b15#0d6f5e840ad1f955e4e4dec00c5165f134815b15" +dependencies = [ + "blake2", + "ciphersuite", + "ec-divisors", + "generalized-bulletproofs", + "generalized-bulletproofs-circuit-abstraction", + "generalized-bulletproofs-ec-gadgets", + "generic-array 1.3.5", + "multiexp", + "rand_core", + "std-shims", + "zeroize", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "generalized-bulletproofs" +version = "0.1.0" +source = "git+https://github.com/monero-oxide/monero-oxide?rev=0d6f5e840ad1f955e4e4dec00c5165f134815b15#0d6f5e840ad1f955e4e4dec00c5165f134815b15" +dependencies = [ + "blake2", + "ciphersuite", + "ff", + "flexible-transcript", + "multiexp", + "rand_core", + "std-shims", + "subtle", + "zeroize", +] + +[[package]] +name = "generalized-bulletproofs-circuit-abstraction" +version = "0.1.0" +source = "git+https://github.com/monero-oxide/monero-oxide?rev=0d6f5e840ad1f955e4e4dec00c5165f134815b15#0d6f5e840ad1f955e4e4dec00c5165f134815b15" +dependencies = [ + "ciphersuite", + "generalized-bulletproofs", + "std-shims", + "zeroize", +] + +[[package]] +name = "generalized-bulletproofs-ec-gadgets" +version = "0.1.0" +source = "git+https://github.com/monero-oxide/monero-oxide?rev=0d6f5e840ad1f955e4e4dec00c5165f134815b15#0d6f5e840ad1f955e4e4dec00c5165f134815b15" +dependencies = [ + "ciphersuite", + "generalized-bulletproofs-circuit-abstraction", + "generic-array 1.3.5", + "std-shims", +] + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "generic-array" +version = "1.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaf57c49a95fd1fe24b90b3033bee6dc7e8f1288d51494cb44e627c295e38542" +dependencies = [ + "rustversion", + "typenum", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash", +] + +[[package]] +name = "helioselene" +version = "0.1.0" +source = "git+https://github.com/monero-oxide/monero-oxide?rev=0d6f5e840ad1f955e4e4dec00c5165f134815b15#0d6f5e840ad1f955e4e4dec00c5165f134815b15" +dependencies = [ + "blake2", + "ciphersuite", + "crypto-bigint", + "dalek-ff-group 0.5.0", + "ec-divisors", + "ff", + "group", + "rand_core", + "std-shims", + "subtle", + "zeroize", +] + +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "libc" +version = "0.2.184" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" + +[[package]] +name = "merlin" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" +dependencies = [ + "byteorder", + "keccak", + "rand_core", + "zeroize", +] + +[[package]] +name = "minimal-ed448" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0209ec7c2b5573660a3c5d0f64c90f8ff286171b15ad2234e7a3fbf8f26180be" +dependencies = [ + "crypto-bigint", + "ff", + "generic-array 1.3.5", + "group", + "rand_core", + "rustversion", + "subtle", + "zeroize", +] + +[[package]] +name = "monero-ed25519" +version = "0.1.0" +source = "git+https://github.com/monero-oxide/monero-oxide?rev=0d6f5e840ad1f955e4e4dec00c5165f134815b15#0d6f5e840ad1f955e4e4dec00c5165f134815b15" +dependencies = [ + "blake2", + "crypto-bigint", + "curve25519-dalek", + "monero-io", + "rand_core", + "sha3", + "std-shims", + "subtle", + "zeroize", +] + +[[package]] +name = "monero-fcmp-plus-plus" +version = "0.1.0" +source = "git+https://github.com/monero-oxide/monero-oxide?rev=0d6f5e840ad1f955e4e4dec00c5165f134815b15#0d6f5e840ad1f955e4e4dec00c5165f134815b15" +dependencies = [ + "blake2", + "ciphersuite", + "dalek-ff-group 0.5.0", + "ec-divisors", + "flexible-transcript", + "full-chain-membership-proofs", + "generalized-bulletproofs", + "generalized-bulletproofs-ec-gadgets", + "generic-array 1.3.5", + "helioselene", + "monero-ed25519", + "monero-fcmp-plus-plus-generators", + "monero-io", + "multiexp", + "rand_chacha", + "rand_core", + "std-shims", + "zeroize", +] + +[[package]] +name = "monero-fcmp-plus-plus-generators" +version = "0.1.0" +source = "git+https://github.com/monero-oxide/monero-oxide?rev=0d6f5e840ad1f955e4e4dec00c5165f134815b15#0d6f5e840ad1f955e4e4dec00c5165f134815b15" +dependencies = [ + "ciphersuite", + "full-chain-membership-proofs", + "generalized-bulletproofs", + "group", + "helioselene", + "monero-ed25519", + "monero-primitives", + "std-shims", +] + +[[package]] +name = "monero-io" +version = "0.1.0" +source = "git+https://github.com/monero-oxide/monero-oxide?rev=0d6f5e840ad1f955e4e4dec00c5165f134815b15#0d6f5e840ad1f955e4e4dec00c5165f134815b15" +dependencies = [ + "std-shims", +] + +[[package]] +name = "monero-primitives" +version = "0.1.0" +source = "git+https://github.com/monero-oxide/monero-oxide?rev=0d6f5e840ad1f955e4e4dec00c5165f134815b15#0d6f5e840ad1f955e4e4dec00c5165f134815b15" +dependencies = [ + "sha3", +] + +[[package]] +name = "multiexp" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ec2ce93a6f06ac6cae04c1da3f2a6a24fcfc1f0eb0b4e0f3d302f0df45326cb" +dependencies = [ + "ff", + "group", + "rand_core", + "rustversion", + "std-shims", + "zeroize", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array 0.14.9", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest", + "keccak", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core", +] + +[[package]] +name = "spin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "std-shims" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "227c4f8561598188d0df96dbe749824576174bba278b5b6bb2eacff1066067d0" +dependencies = [ + "hashbrown", + "rustversion", + "spin", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/src/fcmp_pp/fcmp_pp_rust/Cargo.toml b/src/fcmp_pp/fcmp_pp_rust/Cargo.toml new file mode 100644 index 00000000000..fb3c48a91ea --- /dev/null +++ b/src/fcmp_pp/fcmp_pp_rust/Cargo.toml @@ -0,0 +1,60 @@ +[package] +name = "fcmp_pp_rust" +version = "0.0.0" +edition = "2021" + +[lib] +name = "fcmp_pp_rust" +crate-type = ["staticlib"] + +[dependencies] +rand_core = { version = "0.6", default-features = false } + +multiexp = "0.4" +ciphersuite = { version = "0.4.2", features = ["ed25519"] } +dalek-ff-group = "0.5.0" +helioselene = { git = "https://github.com/monero-oxide/monero-oxide", rev = "0d6f5e840ad1f955e4e4dec00c5165f134815b15" } + +generalized-bulletproofs = { git = "https://github.com/monero-oxide/monero-oxide", rev = "0d6f5e840ad1f955e4e4dec00c5165f134815b15" } +ec-divisors = { git = "https://github.com/monero-oxide/monero-oxide", rev = "0d6f5e840ad1f955e4e4dec00c5165f134815b15", features = ["ed25519"] } +full-chain-membership-proofs = { git = "https://github.com/monero-oxide/monero-oxide", rev = "0d6f5e840ad1f955e4e4dec00c5165f134815b15" } + +monero-fcmp-plus-plus-generators = { git = "https://github.com/monero-oxide/monero-oxide", rev = "0d6f5e840ad1f955e4e4dec00c5165f134815b15" } +monero-fcmp-plus-plus = { git = "https://github.com/monero-oxide/monero-oxide", rev = "0d6f5e840ad1f955e4e4dec00c5165f134815b15" } + +monero-ed25519 = { git = "https://github.com/monero-oxide/monero-oxide", rev = "0d6f5e840ad1f955e4e4dec00c5165f134815b15" } +std-shims = { version = "0.1.5", default-features = false } + +[patch.crates-io] +crypto-bigint = { git = "https://github.com/kayabaNerve/crypto-bigint", branch = "c-repr" } + +[profile.dev] +lto = "off" +panic = "abort" +overflow-checks = true + +[profile.release] +lto = "off" +panic = "abort" +overflow-checks = true + +[profile.dev.package] +subtle = { opt-level = 3 } +curve25519-dalek = { opt-level = 3 } + +ff = { opt-level = 3 } +group = { opt-level = 3 } + +crypto-bigint = { opt-level = 3 } +dalek-ff-group = { opt-level = 3 } +helioselene = { opt-level = 3 } + +multiexp = { opt-level = 3 } + +ec-divisors = { opt-level = 3 } +generalized-bulletproofs = { opt-level = 3 } +generalized-bulletproofs-circuit-abstraction = { opt-level = 3 } +generalized-bulletproofs-ec-gadgets = { opt-level = 3 } +full-chain-membership-proofs = { opt-level = 3 } + +monero-fcmp-plus-plus = { opt-level = 3 } diff --git a/src/fcmp_pp/fcmp_pp_rust/fcmp++.h b/src/fcmp_pp/fcmp_pp_rust/fcmp++.h new file mode 100644 index 00000000000..c3f5420b5f1 --- /dev/null +++ b/src/fcmp_pp/fcmp_pp_rust/fcmp++.h @@ -0,0 +1,368 @@ +// Copyright (c) 2025, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +// static assertions +#if defined(__cplusplus) || (__STDC_VERSION__ >= 202311L) +#define FCMP_STATIC_ASSERT static_assert +#else +#include +#define FCMP_STATIC_ASSERT _Static_assert +#endif + +#include +#include + +// https://github.com/kayabaNerve/fcmp-plus-plus/blob/78754718faa21f0a5751fbd30c9495d7f7f5c2b1/networks/monero/ringct/fcmp%2B%2B/src/lib.rs#L273-L274 +#define FCMP_PP_SAL_PROOF_SIZE_V1 (12*32) +#define FCMP_PP_INPUT_TUPLE_SIZE_V1 (3*32) + + +// ----- deps C bindings ----- + +/// A constant-time implementation of the Ed25519 field. +struct SeleneScalar { + uintptr_t _0[32 / sizeof(uintptr_t)]; +}; + +/// The field novel to Helios/Selene. +struct HeliosScalar { + uintptr_t _0[32 / sizeof(uintptr_t)]; +}; + +struct HeliosPoint { + struct SeleneScalar x; + struct SeleneScalar y; + struct SeleneScalar z; +}; + +struct SelenePoint { + struct HeliosScalar x; + struct HeliosScalar y; + struct HeliosScalar z; +}; + +// ----- End deps C bindings ----- + +struct OutputTuple +{ + uint8_t O[32]; + uint8_t I[32]; + uint8_t C[32]; +}; + +struct FcmpInputCompressed +{ + uint8_t O_tilde[32]; + uint8_t I_tilde[32]; + uint8_t R[32]; + uint8_t C_tilde[32]; +}; +FCMP_STATIC_ASSERT(sizeof(struct FcmpInputCompressed) == 4 * 32, + "FcmpInputCompressed has padding and thus cannot be treated as a byte buffer"); + +struct FcmpRerandomizedOutputCompressed +{ + struct FcmpInputCompressed input; + + uint8_t r_o[32]; + uint8_t r_i[32]; + uint8_t r_r_i[32]; + uint8_t r_c[32]; +}; +FCMP_STATIC_ASSERT(sizeof(struct FcmpRerandomizedOutputCompressed) == 8 * 32, + "RerandomizedOutputCompressed has padding and thus cannot be treated as a byte buffer"); + +struct HeliosScalarSlice +{ + const struct HeliosScalar *buf; + uintptr_t len; +}; + +struct SeleneScalarSlice +{ + const struct SeleneScalar *buf; + uintptr_t len; +}; + +struct OutputSlice +{ + const struct OutputTuple *buf; + uintptr_t len; +}; + +struct InputSlice +{ + const struct FcmpInputCompressed *buf; + uintptr_t len; +}; + +struct HeliosScalarChunks +{ + const struct HeliosScalarSlice *buf; + uintptr_t len; +}; + +struct SeleneScalarChunks +{ + const struct SeleneScalarSlice *buf; + uintptr_t len; +}; + +struct ObjectSlice +{ + const uint8_t * const *buf; + uintptr_t len; +}; + +struct TreeRootUnsafe; + +struct PathUnsafe; + +struct HeliosBranchBlindUnsafe; +struct SeleneBranchBlindUnsafe; + +struct BlindedOBlindUnsafe; +struct BlindedIBlindUnsafe; +struct BlindedIBlindBlindUnsafe; +struct BlindedCBlindUnsafe; + +struct OutputBlindsUnsafe; + +struct FcmpPpProveMembershipInputUnsafe; +struct FcmpPpVerifyInputUnsafe; + +struct HeliosBranchBlindSliceUnsafe +{ + const struct HeliosBranchBlindUnsafe * const *buf; + uintptr_t len; +}; + +struct SeleneBranchBlindSliceUnsafe +{ + const struct SeleneBranchBlindUnsafe * const *buf; + uintptr_t len; +}; + +struct FcmpPpProveMembershipInputSliceUnsafe +{ + const struct FcmpPpProveMembershipInputUnsafe * const *buf; + uintptr_t len; +}; + +struct FcmpPpVerifyInputSliceUnsafe +{ + const struct FcmpPpVerifyInputUnsafe * const *buf; + uintptr_t len; +}; + +#ifdef __cplusplus +extern "C" { +#endif + +struct HeliosPoint helios_hash_init_point(void); + +struct SelenePoint selene_hash_init_point(void); + +void helios_scalar_to_bytes(const struct HeliosScalar *helios_scalar, uint8_t bytes_out[32]); + +void selene_scalar_to_bytes(const struct SeleneScalar *selene_scalar, uint8_t bytes_out[32]); + +void helios_point_to_bytes(const struct HeliosPoint *helios_point, uint8_t bytes_out[32]); + +void selene_point_to_bytes(const struct SelenePoint *selene_point, uint8_t bytes_out[32]); + +int helios_point_from_bytes(const uint8_t *helios_point_bytes, struct HeliosPoint *helios_point_out); + +int selene_point_from_bytes(const uint8_t *selene_point_bytes, struct SelenePoint *selene_point_out); + +int selene_scalar_from_bytes(const uint8_t *selene_scalar_bytes, struct SeleneScalar *selene_scalar_out); + +int selene_point_to_helios_scalar(struct SelenePoint selene_point, struct HeliosScalar *helios_scalar_out); + +int helios_point_to_selene_scalar(struct HeliosPoint helios_point, struct SeleneScalar *selene_scalar_out); + +struct HeliosScalar helios_zero_scalar(void); + +struct SeleneScalar selene_zero_scalar(void); + +int selene_tree_root(struct SelenePoint selene_point, struct TreeRootUnsafe **tree_root_out); +int helios_tree_root(struct HeliosPoint helios_point, struct TreeRootUnsafe **tree_root_out); + +void destroy_tree_root(struct TreeRootUnsafe *tree_root); + +int hash_grow_helios(struct HeliosPoint existing_hash, + uintptr_t offset, + struct HeliosScalar existing_child_at_offset, + struct HeliosScalarSlice new_children, + struct HeliosPoint *hash_out); + +int hash_grow_selene(struct SelenePoint existing_hash, + uintptr_t offset, + struct SeleneScalar existing_child_at_offset, + struct SeleneScalarSlice new_children, + struct SelenePoint *hash_out); + +int path_new(struct OutputSlice leaves, + uintptr_t output_idx, + struct HeliosScalarChunks helios_layer_chunks, + struct SeleneScalarChunks selene_layer_chunks, + struct PathUnsafe **path_out); + +void destroy_path(struct PathUnsafe *path); + +int rerandomize_output(struct OutputTuple output, + struct FcmpRerandomizedOutputCompressed *rerandomized_output_out); + +int o_blind(const struct FcmpRerandomizedOutputCompressed *rerandomized_output, + struct SeleneScalar *o_blind_out); +int i_blind(const struct FcmpRerandomizedOutputCompressed *rerandomized_output, + struct SeleneScalar *i_blind_out); +int i_blind_blind(const struct FcmpRerandomizedOutputCompressed *rerandomized_output, + struct SeleneScalar *i_blind_blind_out); +int c_blind(const struct FcmpRerandomizedOutputCompressed *rerandomized_output, + struct SeleneScalar *c_blind_out); + +int blind_o_blind(const struct SeleneScalar *o_blind, struct BlindedOBlindUnsafe **blinded_o_blind_out); +int blind_i_blind(const struct SeleneScalar *i_blind, struct BlindedIBlindUnsafe **blinded_i_blind_out); +int blind_i_blind_blind(const struct SeleneScalar *i_blind_blind, struct BlindedIBlindBlindUnsafe **blinded_i_blind_blind_out); +int blind_c_blind(const struct SeleneScalar *c_blind, struct BlindedCBlindUnsafe **blinded_c_blind_out); + +void destroy_blinded_o_blind(struct BlindedOBlindUnsafe *blinded_o_blind); +void destroy_blinded_i_blind(struct BlindedIBlindUnsafe *blinded_i_blind); +void destroy_blinded_i_blind_blind(struct BlindedIBlindBlindUnsafe *blinded_i_blind_blind); +void destroy_blinded_c_blind(struct BlindedCBlindUnsafe *blinded_c_blind); + +int output_blinds_new(const struct BlindedOBlindUnsafe *blinded_o_blind, + const struct BlindedIBlindUnsafe *blinded_i_blind, + const struct BlindedIBlindBlindUnsafe *blinded_i_blind_blind, + const struct BlindedCBlindUnsafe *blidned_c_blind, + struct OutputBlindsUnsafe **output_blinds_out); + +void destroy_output_blinds(struct OutputBlindsUnsafe *output_blinds); + +int generate_helios_branch_blind(struct HeliosBranchBlindUnsafe **branch_blind_out); +int generate_selene_branch_blind(struct SeleneBranchBlindUnsafe **branch_blind_out); + +void destroy_helios_branch_blind(struct HeliosBranchBlindUnsafe *helios_branch_blind); +void destroy_selene_branch_blind(struct SeleneBranchBlindUnsafe *selene_branch_blind); + +int fcmp_pp_prove_input_new(const struct PathUnsafe *path, + const struct OutputBlindsUnsafe *output_blinds, + struct SeleneBranchBlindSliceUnsafe selene_branch_blinds, + struct HeliosBranchBlindSliceUnsafe helios_branch_blinds, + struct FcmpPpProveMembershipInputUnsafe **fcmp_pp_prove_input_out); + +void destroy_fcmp_pp_prove_input(struct FcmpPpProveMembershipInputUnsafe *fcmp_pp_prove_input); + +/** + * brief: fcmp_pp_prove_sal - Make a FCMP++ spend auth & linkability proof + * param: signable_tx_hash - message to sign + * param: x - ed25519 scalar s.t. O~ = x G + y T + * param: y - ed25519 scalar s.t. O~ = x G + y T + * param: rerandomized_output - used for input tuple, r_i, and r_r_i + * outparam: sal_proof_out - a buffer of size FCMP_PP_SAL_PROOF_SIZE_V1 where resultant SAL proof is stored + * return: 0 on success, a negative value on failure + * + * note: This call can technically be stripped down even more because `rerandomized_output` contains + * more information than we need: we can discard r_o and r_c. However, in practice, these + * values will always be known before a call to this function since O~ and C~ are added to the + * challenge transcript, so passing `rerandomized_output` is more ergonomic. + */ +int fcmp_pp_prove_sal(const uint8_t signable_tx_hash[32], + const uint8_t x[32], + const uint8_t y[32], + const struct FcmpRerandomizedOutputCompressed *rerandomized_output, + uint8_t sal_proof_out[FCMP_PP_SAL_PROOF_SIZE_V1], + uint8_t key_image_out[32]); + +/** + * brief: fcmp_pp_prove_membership - Make a FCMP++ membership proof for N inputs + * param: inputs - a slice of FCMP provable inputs returned from fcmp_pp_prove_input_new() + * param: n_tree_layers - + * param: proof_len - + * outparam: fcmp_proof_out - a buffer where the FCMP proof will be written to + * return: an error on failure, nothing otherwise + */ +int fcmp_pp_prove_membership(const struct FcmpPpProveMembershipInputSliceUnsafe fcmp_pp_prove_inputs, + uintptr_t n_tree_layers, + uintptr_t proof_len, + uint8_t fcmp_proof_out[]); + +// The following proof_size functions are tabled through proof_len.h. Use +// those functions instead. +uintptr_t _slow_membership_proof_size(uintptr_t n_inputs, uintptr_t n_tree_layers); + +uintptr_t _slow_fcmp_pp_proof_size(uintptr_t n_inputs, uintptr_t n_tree_layers); + +int fcmp_pp_verify_input_new(const uint8_t *signable_tx_hash, + const uint8_t *fcmp_pp_proof, + uintptr_t fcmp_pp_proof_len, + uintptr_t n_tree_layers, + const struct TreeRootUnsafe *tree_root, + struct ObjectSlice pseudo_outs, + struct ObjectSlice key_images, + struct FcmpPpVerifyInputUnsafe **fcmp_pp_verify_input_out); + +void destroy_fcmp_pp_verify_input(struct FcmpPpVerifyInputUnsafe *fcmp_pp_verify_input); + +/** + * brief: fcmp_pp_verify_sal - Verify a FCMP++ spend auth & linkability proof + * param: signable_tx_hash - message to verify + * param: input - (O~, I~, C~, R) tuple + * param: L - L = x Hp(O), AKA key image + * param: sal_proof - SAL proof to verify + * return: true on verification success, false otherwise + */ +bool fcmp_pp_verify_sal(const uint8_t signable_tx_hash[32], + const struct FcmpInputCompressed *input, + const uint8_t L[32], + const uint8_t sal_proof[FCMP_PP_SAL_PROOF_SIZE_V1]); +/** + * brief: fcmp_pp_verify_membership - Verify a FCMP++ membership proof + * param: inputs - a slice of fcmp_input_ref pointers + * param: tree_root - + * param: n_tree_layers - + * param: fcmp_proof - FCMP proof bytes to verify + * param: fcmp_proof_len - length of fcmp_proof buffer + * return: true on verification success, false otherwise + */ +bool fcmp_pp_verify_membership(struct InputSlice inputs, + const struct TreeRootUnsafe *tree_root, + const uintptr_t n_tree_layers, + const uint8_t fcmp_proof[], + const uintptr_t fcmp_proof_len); + +uintptr_t fcmp_pp_n_inputs(const struct FcmpPpVerifyInputUnsafe *fcmp_pp_verify_input); + +bool fcmp_pp_verify(const struct FcmpPpVerifyInputSliceUnsafe fcmp_pp_verify_inputs); + +#ifdef __cplusplus +} //extern "C" +#endif diff --git a/src/fcmp_pp/fcmp_pp_rust/src/lib.rs b/src/fcmp_pp/fcmp_pp_rust/src/lib.rs new file mode 100644 index 00000000000..fa1c39c07af --- /dev/null +++ b/src/fcmp_pp/fcmp_pp_rust/src/lib.rs @@ -0,0 +1,1182 @@ +use rand_core::OsRng; + +use ciphersuite::{ + group::{ + ff::{Field, PrimeField}, + Group, GroupEncoding, + }, + Ciphersuite, +}; +use dalek_ff_group::{Ed25519, EdwardsPoint, Scalar}; +use helioselene::{ + Field25519 as SeleneScalar, Helios, HeliosPoint, HelioseleneField as HeliosScalar, Selene, + SelenePoint, +}; + +use ec_divisors::{DivisorCurve, ScalarDecomposition}; +use full_chain_membership_proofs::tree::hash_grow; + +use monero_fcmp_plus_plus::{ + fcmps, + fcmps::{ + BranchBlind, Branches, CBlind, Fcmp, IBlind, IBlindBlind, OBlind, OutputBlinds, Path, + TreeRoot, + }, + sal::{OpenedInputTuple, RerandomizedOutput, SpendAuthAndLinkability}, + Curves, FcmpPlusPlus, Input, Output, FCMP_PARAMS, HELIOS_FCMP_GENERATORS, + SELENE_FCMP_GENERATORS, +}; +use monero_fcmp_plus_plus_generators::{ + FCMP_PLUS_PLUS_U, FCMP_PLUS_PLUS_V, HELIOS_HASH_INIT, SELENE_HASH_INIT, +}; +use monero_ed25519::CompressedPoint; +use std_shims::sync::LazyLock; + +use std::os::raw::c_int; + +//-------------------------------------------------------------------------------------- Generators + +// This T generator may be exposed by one of the crates in the future, for now use a static +static T: LazyLock = LazyLock::new(|| { + Ed25519::read_G(&mut CompressedPoint::T.to_bytes().as_slice()) + .expect("couldn't decompress `CompressedPoint::T`") +}); + +//-------------------------------------------------------------------------------------- Curve points + +#[no_mangle] +pub extern "C" fn helios_hash_init_point() -> HeliosPoint { + *HELIOS_HASH_INIT +} + +#[no_mangle] +pub extern "C" fn selene_hash_init_point() -> SelenePoint { + *SELENE_HASH_INIT +} + +fn new_box_raw(obj: T) -> *mut T { + Box::into_raw(Box::new(obj)) +} + +// https://doc.rust-lang.org/std/boxed/struct.Box.html#method.from_raw +fn destroy_box(ptr: *mut T) { + let _ = unsafe { Box::from_raw(ptr) }; +} + +macro_rules! destroy_fn { + ($fn_name:ident, $type:ty) => { + /// # Safety + /// + /// This function assumes that the obj was allocated on the heap via + /// Box::into_raw(Box::new()) + #[no_mangle] + pub unsafe extern "C" fn $fn_name(obj: *mut $type) { + destroy_box(obj); + } + }; +} + +macro_rules! ec_elem_to_bytes { + ($fn_name:ident, $Type:ty, $to_bytes:ident) => { + /// # Safety + /// + /// This function assumes a raw pointer to expected obj type, and to have + /// 32 bytes already allocated for bytes_out. + #[no_mangle] + pub unsafe extern "C" fn $fn_name(obj: *const $Type, bytes_out: *mut u8) { + let bytes_out = core::slice::from_raw_parts_mut(bytes_out, 32); + bytes_out.clone_from_slice(&(*obj).$to_bytes()); + } + }; +} + +macro_rules! ec_elem_from_bytes { + ($fn_name:ident, $Type:ty, $Curve:ty, $from_bytes:ident) => { + /// # Safety + /// + /// This function assumes 32 bytes allocated, and to be passed a raw pointer to + /// the expected type. + #[allow(clippy::not_unsafe_ptr_arg_deref)] + #[no_mangle] + pub unsafe extern "C" fn $fn_name(bytes: *const u8, ec_elem_out: *mut $Type) -> c_int { + if ec_elem_out.is_null() { + return -1; + } + let mut bytes = unsafe { core::slice::from_raw_parts(bytes, 32) }; + match <$Curve>::$from_bytes(&mut bytes) { + Ok(ec_elem) => { + *ec_elem_out = ec_elem; + 0 + } + Err(_) => -2, + } + } + }; +} + +ec_elem_to_bytes!(helios_scalar_to_bytes, HeliosScalar, to_repr); +ec_elem_to_bytes!(selene_scalar_to_bytes, SeleneScalar, to_repr); +ec_elem_to_bytes!(helios_point_to_bytes, HeliosPoint, to_bytes); +ec_elem_to_bytes!(selene_point_to_bytes, SelenePoint, to_bytes); + +ec_elem_from_bytes!(helios_scalar_from_bytes, HeliosScalar, Helios, read_F); +ec_elem_from_bytes!(selene_scalar_from_bytes, SeleneScalar, Selene, read_F); +ec_elem_from_bytes!(helios_point_from_bytes, HeliosPoint, Helios, read_G); +ec_elem_from_bytes!(selene_point_from_bytes, SelenePoint, Selene, read_G); + +// Undefined behavior occurs when the data pointer passed to core::slice::from_raw_parts is null, +// even when len is 0. slice_from_raw_parts_0able() lets you pass p as null, as long as len is 0 +const unsafe fn slice_from_raw_parts_0able<'a, T>(p: *const T, len: usize) -> &'a [T] { + if len == 0 { + &[] + } else { + core::slice::from_raw_parts(p, len) + } +} + +fn ed25519_point_from_bytes(ed25519_point: *const u8) -> std::io::Result { + let mut ed25519_point = unsafe { core::slice::from_raw_parts(ed25519_point, 32) }; + ::read_G(&mut ed25519_point) +} + +fn ed25519_scalar_from_bytes(ed25519_scalar: *const u8) -> std::io::Result { + let mut ed25519_scalar = unsafe { core::slice::from_raw_parts(ed25519_scalar, 32) }; + ::read_F(&mut ed25519_scalar) +} + +fn hash_array_from_bytes( + h: *const u8, +) -> std::result::Result<[u8; 32], std::array::TryFromSliceError> { + unsafe { core::slice::from_raw_parts(h, 32) }.try_into() +} + +// @TODO: this is horrible :(, expose direct read/write for Input in the fcmp-plus-plus crate +fn input_from_bytes(input_bytes: &[u8]) -> std::io::Result { + let mut rerandomized_output_bytes = [0u8; 8 * 32]; + if input_bytes.len() != 4 * 32 { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "passed input slice wrong size", + )); + } + rerandomized_output_bytes[0..4 * 32].copy_from_slice(input_bytes); + let rerandomized_output = RerandomizedOutput::read(&mut rerandomized_output_bytes.as_slice())?; + Ok(rerandomized_output.input()) +} + +macro_rules! point_to_cycle_scalar { + ($fn_name:ident, $Point:ty, $Scalar:ty) => { + /// # Safety + /// + /// This function assumes scalar_out is a non-null pointer to the expected type. + #[no_mangle] + pub unsafe extern "C" fn $fn_name(point: $Point, scalar_out: *mut $Scalar) -> c_int { + if scalar_out.is_null() { + return -1; + } + let Some(xy_coords) = <$Point>::to_xy(point) else { + return -2; + }; + *scalar_out = xy_coords.0; + 0 + } + }; +} + +point_to_cycle_scalar!(selene_point_to_helios_scalar, SelenePoint, HeliosScalar); +point_to_cycle_scalar!(helios_point_to_selene_scalar, HeliosPoint, SeleneScalar); + +#[no_mangle] +pub extern "C" fn helios_zero_scalar() -> HeliosScalar { + HeliosScalar::ZERO +} + +#[no_mangle] +pub extern "C" fn selene_zero_scalar() -> SeleneScalar { + SeleneScalar::ZERO +} + +/// # Safety +/// +/// This function expects a non-null pointer to TreeRootUnsafe in tree_root_out. +#[no_mangle] +pub unsafe extern "C" fn selene_tree_root( + selene_point: SelenePoint, + tree_root_out: *mut *mut TreeRoot, +) -> c_int { + if tree_root_out.is_null() { + return -1; + } + + let tree_root = TreeRoot::::C1(selene_point); + *tree_root_out = new_box_raw(tree_root); + 0 +} + +/// # Safety +/// +/// This function expects a non-null pointer to TreeRootUnsafe in tree_root_out. +#[no_mangle] +pub unsafe extern "C" fn helios_tree_root( + helios_point: HeliosPoint, + tree_root_out: *mut *mut TreeRoot, +) -> c_int { + if tree_root_out.is_null() { + return -1; + } + + let tree_root = TreeRoot::::C2(helios_point); + *tree_root_out = new_box_raw(tree_root); + 0 +} + +destroy_fn!(destroy_tree_root, TreeRoot::); + +#[allow(non_snake_case)] +#[repr(C)] +pub struct OutputTuple { + O: [u8; 32], + I: [u8; 32], + C: [u8; 32], +} + +#[repr(C)] +pub struct Slice { + buf: *const T, + len: usize, +} +pub type HeliosScalarSlice = Slice; +pub type SeleneScalarSlice = Slice; +pub type OutputSlice = Slice; +pub type HeliosScalarChunks = Slice; +pub type SeleneScalarChunks = Slice; +pub type HeliosBranchBlindSlice = Slice<*const BranchBlind<::G>>; +pub type SeleneBranchBlindSlice = Slice<*const BranchBlind<::G>>; +impl From> for &[T] { + fn from(slice: Slice) -> Self { + unsafe { slice_from_raw_parts_0able(slice.buf, slice.len) } + } +} +impl From<&Slice> for &[T] { + fn from(slice: &Slice) -> Self { + unsafe { slice_from_raw_parts_0able(slice.buf, slice.len) } + } +} + +/// # Safety +/// +/// This function expects a valid pointer to a HeliosPoint passed in hash_out. +#[no_mangle] +pub unsafe extern "C" fn hash_grow_helios( + existing_hash: HeliosPoint, + offset: usize, + existing_child_at_offset: HeliosScalar, + new_children: HeliosScalarSlice, + hash_out: *mut HeliosPoint, +) -> c_int { + if hash_out.is_null() { + return -1; + } + + let hash = hash_grow( + &HELIOS_FCMP_GENERATORS.generators, + existing_hash, + offset, + existing_child_at_offset, + new_children.into(), + ); + + let Some(hash) = hash else { + return -2; + }; + + *hash_out = hash; + 0 +} + +/// # Safety +/// +/// This function expects a valid pointer to a SelenePoint passed in hash_out. +#[no_mangle] +pub unsafe extern "C" fn hash_grow_selene( + existing_hash: SelenePoint, + offset: usize, + existing_child_at_offset: SeleneScalar, + new_children: SeleneScalarSlice, + hash_out: *mut SelenePoint, +) -> c_int { + if hash_out.is_null() { + return -1; + } + + let hash = hash_grow( + &SELENE_FCMP_GENERATORS.generators, + existing_hash, + offset, + existing_child_at_offset, + new_children.into(), + ); + + let Some(hash) = hash else { + return -2; + }; + + *hash_out = hash; + 0 +} + +//-------------------------------------------------------------------------------------- Path + +/// # Safety +/// +/// This function assumes that the leaves passed in are composed of 3 * 32 byte slices, that +/// the output_idx is < n leaves, and that the helios and selene layer chunks are composed +/// of scalars of the expected type. +#[no_mangle] +pub unsafe extern "C" fn path_new( + leaves: OutputSlice, + output_idx: usize, + helios_layer_chunks: HeliosScalarChunks, + selene_layer_chunks: SeleneScalarChunks, + path_out: *mut *mut Path, +) -> c_int { + if path_out.is_null() { + return -1; + } + if output_idx >= leaves.len { + return -2; + } + + // Collect decompressed leaves + let leaves_slice: &[OutputTuple] = leaves.into(); + let mut leaves: Vec = Vec::with_capacity(leaves_slice.len()); + #[allow(non_snake_case)] + for leaf in leaves_slice { + let O = if let Some(O) = EdwardsPoint::from_bytes(&leaf.O).into() { + O + } else { + return -3; + }; + let I = if let Some(I) = EdwardsPoint::from_bytes(&leaf.I).into() { + I + } else { + return -4; + }; + let C = if let Some(C) = EdwardsPoint::from_bytes(&leaf.C).into() { + C + } else { + return -5; + }; + + let Ok(new_output) = Output::new(O, I, C) else { + return -6; + }; + leaves.push(new_output); + } + + // Output + let output = leaves[output_idx]; + + // Collect helios layer chunks + let helios_layers: &[HeliosScalarSlice] = helios_layer_chunks.into(); + let helios_layers: Vec::F>> = helios_layers + .iter() + .map(|x| { + let inner_slice: &[::F] = x.into(); + inner_slice.iter().map(|y| y.to_owned()).collect() + }) + .collect(); + + // Collect selene layer chunks + let selene_layers: &[SeleneScalarSlice] = selene_layer_chunks.into(); + let selene_layers: Vec::F>> = selene_layers + .iter() + .map(|x| { + let inner_slice: &[::F] = x.into(); + inner_slice.iter().map(|y| y.to_owned()).collect() + }) + .collect(); + + let curve_2_layers = helios_layers; + let curve_1_layers = selene_layers; + + let path: Path = Path { + output, + leaves, + curve_2_layers, + curve_1_layers, + }; + *path_out = new_box_raw(path); + 0 +} + +destroy_fn!(destroy_path, Path); + +//-------------------------------------------------------------------------------------- Blindings + +//---------------------------------------------- RerandomizedOutput + +/// # Safety +/// +/// This function expects a non-null pointer to FcmpRerandomizedOutputCompressed in rerandomized_output_bytes. +#[no_mangle] +#[allow(non_snake_case)] +pub unsafe extern "C" fn rerandomize_output( + output: OutputTuple, + rerandomized_output_bytes: *mut u8, +) -> c_int { + let O = if let Some(O) = EdwardsPoint::from_bytes(&output.O).into() { + O + } else { + return -1; + }; + let I = if let Some(I) = EdwardsPoint::from_bytes(&output.I).into() { + I + } else { + return -2; + }; + let C = if let Some(C) = EdwardsPoint::from_bytes(&output.C).into() { + C + } else { + return -3; + }; + + let Ok(output) = Output::new(O, I, C) else { + return -4; + }; + + let mut rerandomized_output_bytes = + core::slice::from_raw_parts_mut(rerandomized_output_bytes, 8 * 32); + + let rerandomized_output = RerandomizedOutput::new(&mut OsRng, output); + if rerandomized_output + .write(&mut rerandomized_output_bytes) + .is_err() + { + -5 + } else { + 0 + } +} + +//---------------------------------------------- OBlind + +/// # Safety +/// +/// This function assumes that the rerandomized output byte buffer being passed +/// in is 8*32 bytes. +#[no_mangle] +pub unsafe extern "C" fn o_blind( + rerandomized_output_bytes: *const u8, + o_blind: *mut Scalar, +) -> c_int { + let mut rerandomized_output_bytes = + core::slice::from_raw_parts(rerandomized_output_bytes, 8 * 32); + match RerandomizedOutput::read(&mut rerandomized_output_bytes) { + Ok(rerandomized_output) => { + o_blind.write(rerandomized_output.o_blind()); + 0 + } + Err(_) => -1, + } +} + +/// # Safety +/// +/// This function assumes that the scalar being passed in input was +/// allocated on the heap and returned through a CResult instance. +#[no_mangle] +pub unsafe extern "C" fn blind_o_blind( + o_blind: *const Scalar, + blinded_o_blind_out: *mut *mut OBlind, +) -> c_int { + if blinded_o_blind_out.is_null() { + return -1; + } + + let scalar_decomp = ScalarDecomposition::new(*o_blind); + let Some(scalar_decomp) = scalar_decomp else { + return -2; + }; + + let blinded_o_blind = OBlind::new(EdwardsPoint(**T), scalar_decomp); + *blinded_o_blind_out = new_box_raw(blinded_o_blind); + 0 +} + +destroy_fn!(destroy_blinded_o_blind, OBlind); + +//---------------------------------------------- CBlind + +/// # Safety +/// +/// This function assumes that the rerandomized output byte buffer being passed +/// in is 8*32 bytes. +#[no_mangle] +pub unsafe extern "C" fn c_blind( + rerandomized_output_bytes: *const u8, + c_blind: *mut Scalar, +) -> c_int { + let mut rerandomized_output_bytes = + core::slice::from_raw_parts(rerandomized_output_bytes, 8 * 32); + match RerandomizedOutput::read(&mut rerandomized_output_bytes) { + Ok(rerandomized_output) => { + c_blind.write(rerandomized_output.c_blind()); + 0 + } + Err(_) => -1, + } +} + +/// # Safety +/// +/// This function assumes that the scalar being passed in input was +/// allocated on the heap and returned through a CResult instance. +#[no_mangle] +pub unsafe extern "C" fn blind_c_blind( + c_blind: *const Scalar, + blinded_c_blind_out: *mut *mut CBlind, +) -> c_int { + if blinded_c_blind_out.is_null() { + return -1; + } + + let scalar_decomp = ScalarDecomposition::new(*c_blind); + let Some(scalar_decomp) = scalar_decomp else { + return -2; + }; + + let blinded_c_blind = CBlind::new(EdwardsPoint::generator(), scalar_decomp); + *blinded_c_blind_out = new_box_raw(blinded_c_blind); + 0 +} + +destroy_fn!(destroy_blinded_c_blind, CBlind); + +//---------------------------------------------- IBlind + +/// # Safety +/// +/// This function assumes that the rerandomized output byte buffer being passed +/// in is 8*32 bytes. +#[no_mangle] +pub unsafe extern "C" fn i_blind( + rerandomized_output_bytes: *const u8, + i_blind: *mut Scalar, +) -> c_int { + let mut rerandomized_output_bytes = + core::slice::from_raw_parts(rerandomized_output_bytes, 8 * 32); + match RerandomizedOutput::read(&mut rerandomized_output_bytes) { + Ok(rerandomized_output) => { + i_blind.write(rerandomized_output.i_blind()); + 0 + } + Err(_) => -1, + } +} + +/// # Safety +/// +/// This function assumes that the scalar being passed in input was +/// allocated on the heap and returned through a CResult instance. +#[no_mangle] +pub unsafe extern "C" fn blind_i_blind( + i_blind: *const Scalar, + blinded_i_blind_out: *mut *mut IBlind, +) -> c_int { + if blinded_i_blind_out.is_null() { + return -1; + } + + let scalar_decomp = ScalarDecomposition::new(*i_blind); + let Some(scalar_decomp) = scalar_decomp else { + return -2; + }; + + let blinded_i_blind = IBlind::new( + EdwardsPoint((*FCMP_PLUS_PLUS_U).into()), + EdwardsPoint((*FCMP_PLUS_PLUS_V).into()), + scalar_decomp, + ); + *blinded_i_blind_out = new_box_raw(blinded_i_blind); + 0 +} + +destroy_fn!(destroy_blinded_i_blind, IBlind); + +//---------------------------------------------- IBlindBlind + +/// # Safety +/// +/// This function assumes that the rerandomized output byte buffer being passed +/// in is 8*32 bytes. +#[no_mangle] +pub unsafe extern "C" fn i_blind_blind( + rerandomized_output_bytes: *const u8, + i_blind_blind: *mut Scalar, +) -> c_int { + let mut rerandomized_output_bytes = + core::slice::from_raw_parts(rerandomized_output_bytes, 8 * 32); + match RerandomizedOutput::read(&mut rerandomized_output_bytes) { + Ok(rerandomized_output) => { + i_blind_blind.write(rerandomized_output.i_blind_blind()); + 0 + } + Err(_) => -1, + } +} + +/// # Safety +/// +/// This function assumes that the scalar being passed in input was +/// allocated on the heap and returned through a CResult instance. +#[no_mangle] +pub unsafe extern "C" fn blind_i_blind_blind( + i_blind_blind: *const Scalar, + blinded_i_blind_blind_out: *mut *mut IBlindBlind, +) -> c_int { + if blinded_i_blind_blind_out.is_null() { + return -1; + } + + let scalar_decomp = ScalarDecomposition::new(*i_blind_blind); + let Some(scalar_decomp) = scalar_decomp else { + return -2; + }; + + let blinded_i_blind_blind = IBlindBlind::new(EdwardsPoint(**T), scalar_decomp); + *blinded_i_blind_blind_out = new_box_raw(blinded_i_blind_blind); + 0 +} + +destroy_fn!(destroy_blinded_i_blind_blind, IBlindBlind); + +//---------------------------------------------- OutputBlinds + +/// # Safety +/// +/// This function assumes that the blinds passed in input were allocated +/// on the heap and returned through a CResult instance. +#[no_mangle] +pub unsafe extern "C" fn output_blinds_new( + o_blind: *const OBlind, + i_blind: *const IBlind, + i_blind_blind: *const IBlindBlind, + c_blind: *const CBlind, + output_blinds_out: *mut *mut OutputBlinds, +) -> c_int { + if output_blinds_out.is_null() { + return -1; + } + + // Clone so that even if the underlying blinds get dropped, the object remains valid + let output_blinds = OutputBlinds::new( + (*o_blind).clone(), + (*i_blind).clone(), + (*i_blind_blind).clone(), + (*c_blind).clone(), + ); + *output_blinds_out = new_box_raw(output_blinds); + 0 +} + +destroy_fn!(destroy_output_blinds, OutputBlinds); + +//---------------------------------------------- BranchBlind + +/// # Safety +/// +/// This function allocates a branch blind on the heap via Box::new, then +/// gets its raw pointer via Box::into_raw, then sets the input pointer's +/// address to point to the raw box pointer. The input parameter must not +/// be null. Also be sure to clean up the branch blind with +/// destroy_helios_branch_blind below. +#[no_mangle] +pub unsafe extern "C" fn generate_helios_branch_blind( + branch_blind_out: *mut *mut BranchBlind<::G>, +) -> c_int { + if branch_blind_out.is_null() { + return -1; + } + + let scalar_decomp = ScalarDecomposition::new(::F::random(&mut OsRng)); + let Some(scalar_decomp) = scalar_decomp else { + return -2; + }; + + let branch_blind = BranchBlind::<::G>::new( + HELIOS_FCMP_GENERATORS.generators.h(), + scalar_decomp, + ); + *branch_blind_out = new_box_raw(branch_blind); + 0 +} + +/// # Safety +/// +/// This function allocates a branch blind on the heap via Box::new, then +/// gets its raw pointer via Box::into_raw, then sets the input pointer's +/// address to point to the raw box pointer. The input parameter must not +/// be null. Also be sure to clean up the branch blind with +/// destroy_selene_branch_blind below. +#[no_mangle] +pub unsafe extern "C" fn generate_selene_branch_blind( + branch_blind_out: *mut *mut BranchBlind<::G>, +) -> c_int { + if branch_blind_out.is_null() { + return -1; + } + + let scalar_decomp = ScalarDecomposition::new(::F::random(&mut OsRng)); + let Some(scalar_decomp) = scalar_decomp else { + return -2; + }; + + let branch_blind = BranchBlind::<::G>::new( + SELENE_FCMP_GENERATORS.generators.h(), + scalar_decomp, + ); + *branch_blind_out = new_box_raw(branch_blind); + 0 +} + +destroy_fn!( + destroy_helios_branch_blind, + BranchBlind<::G> +); +destroy_fn!( + destroy_selene_branch_blind, + BranchBlind<::G> +); + +//-------------------------------------------------------------------------------------- Fcmp + +#[derive(Clone)] +pub struct FcmpPpProveMembershipInput { + path: Path, + output_blinds: OutputBlinds, + c1_branch_blinds: Vec::G>>, + c2_branch_blinds: Vec::G>>, +} + +pub type FcmpPpProveMembershipInputSlice = Slice<*const FcmpPpProveMembershipInput>; + +unsafe fn prove_membership_native( + inputs: &[*const FcmpPpProveMembershipInput], + n_tree_layers: usize, +) -> Option> { + let paths = inputs.iter().cloned().map(|x| (*x).path.clone()).collect(); + let output_blinds = inputs + .iter() + .cloned() + .map(|x| (*x).output_blinds.clone()) + .collect(); + + let c1_branch_blinds: Vec<_> = inputs + .iter() + .cloned() + .map(|x| (*x).c1_branch_blinds.clone()) + .collect::>() + .into_iter() + .flatten() + .collect(); + let c2_branch_blinds: Vec<_> = inputs + .iter() + .cloned() + .map(|x| (*x).c2_branch_blinds.clone()) + .collect::>() + .into_iter() + .flatten() + .collect(); + + let branches = Branches::new(paths)?; + + if branches.necessary_c1_blinds() != c1_branch_blinds.len() { + return None; + } + if branches.necessary_c2_blinds() != c2_branch_blinds.len() { + return None; + } + + let n_branch_blinds = (c1_branch_blinds.len() + c2_branch_blinds.len()) / inputs.len(); + if n_tree_layers != n_branch_blinds + 1 { + return None; + } + + let blinded_branches = branches + .blind(output_blinds, c1_branch_blinds, c2_branch_blinds) + .ok()?; + + Fcmp::prove(&mut OsRng, &*FCMP_PARAMS, blinded_branches).ok() +} + +/// # Safety +/// +/// This function assumes that Path and OutputBlinds were +/// allocated on the heap (via Box::into_raw(Box::new())), and the branch +/// blinds are slices of BranchBlind allocated on the heap (via CResult). +#[no_mangle] +pub unsafe extern "C" fn fcmp_pp_prove_input_new( + path: *const Path, + output_blinds: *const OutputBlinds, + selene_branch_blinds: SeleneBranchBlindSlice, + helios_branch_blinds: HeliosBranchBlindSlice, + fcmp_pp_prove_input_out: *mut *mut FcmpPpProveMembershipInput, +) -> c_int { + if fcmp_pp_prove_input_out.is_null() { + return -1; + } + + // Path and output blinds + let path = (*path).clone(); + let output_blinds = (*output_blinds).clone(); + + // Collect branch blinds + let c1_branch_blinds: &[*const BranchBlind<::G>] = + selene_branch_blinds.into(); + let c1_branch_blinds: Vec::G>> = c1_branch_blinds + .iter() + .map(|x| (*x.to_owned()).clone()) + .collect(); + + let c2_branch_blinds: &[*const BranchBlind<::G>] = + helios_branch_blinds.into(); + let c2_branch_blinds: Vec::G>> = c2_branch_blinds + .iter() + .map(|x| (*x.to_owned()).clone()) + .collect(); + + let fcmp_pp_prove_input = FcmpPpProveMembershipInput { + path, + output_blinds, + c1_branch_blinds, + c2_branch_blinds, + }; + *fcmp_pp_prove_input_out = new_box_raw(fcmp_pp_prove_input); + 0 +} + +destroy_fn!(destroy_fcmp_pp_prove_input, FcmpPpProveMembershipInput); + +/// # Safety +/// +/// This function assumes that the signable_tx_hash, x, and y are 32 bytes, sal_proof_out is +/// FCMP_PP_SAL_PROOF_SIZE_V1 bytes, and that the rerandomized output is returned from rerandomize_output(). +#[no_mangle] +pub unsafe extern "C" fn fcmp_pp_prove_sal( + signable_tx_hash: *const u8, + x: *const u8, + y: *const u8, + rerandomized_output_bytes: *const u8, + sal_proof_out: *mut u8, + key_image_out: *mut u8, +) -> c_int { + let Ok(signable_tx_hash) = hash_array_from_bytes(signable_tx_hash) else { + return -1; + }; + + let mut rerandomized_output_bytes = + core::slice::from_raw_parts(rerandomized_output_bytes, 8 * 32); + let Ok(rerandomized_output) = RerandomizedOutput::read(&mut rerandomized_output_bytes) else { + return -2; + }; + + let Ok(x) = ed25519_scalar_from_bytes(x) else { + return -3; + }; + let Ok(y) = ed25519_scalar_from_bytes(y) else { + return -4; + }; + + let Some(opening) = OpenedInputTuple::open(&rerandomized_output, &x, &y) else { + return -5; + }; + + let (key_image, proof) = SpendAuthAndLinkability::prove(&mut OsRng, signable_tx_hash, &opening); + + let sal_proof_out = &mut core::slice::from_raw_parts_mut(sal_proof_out, 12 * 32); // @TODO: remove magic number + let key_image_out = &mut core::slice::from_raw_parts_mut(key_image_out, 32); + + if proof.write(sal_proof_out).is_err() { + return -6; + } + + key_image_out.copy_from_slice(&key_image.to_bytes()); + + 0 +} + +/// # Safety +/// +/// This function assumes inputs are a slice of FcmpPpProveMembershipInputSliceUnsafe, allocated via the FFI. +/// It also assumes fcmp_proof_out was allocated with size proof_len. proof_len should be the size of a +/// membership proof for the given number if inputs and tree layers. +#[no_mangle] +pub unsafe extern "C" fn fcmp_pp_prove_membership( + inputs: FcmpPpProveMembershipInputSlice, + n_tree_layers: usize, + proof_len: usize, + fcmp_proof_out: *mut u8, +) -> c_int { + let inputs: &[*const FcmpPpProveMembershipInput] = inputs.into(); + debug_assert_eq!( + proof_len, + _slow_membership_proof_size(inputs.len(), n_tree_layers) + ); + let mut buf_out = core::slice::from_raw_parts_mut(fcmp_proof_out, proof_len); + + match prove_membership_native(inputs, n_tree_layers) { + Some(fcmp) => match fcmp.write(&mut buf_out) { + Ok(_) => 0, + Err(_) => -2, + }, + None => -1, + } +} + +// These functions are slow as implemented. We use a table in fcmp_pp/proof_len.h +#[no_mangle] +pub extern "C" fn _slow_membership_proof_size(n_inputs: usize, n_tree_layers: usize) -> usize { + Fcmp::::proof_size(n_inputs, n_tree_layers) +} + +#[no_mangle] +pub extern "C" fn _slow_fcmp_pp_proof_size(n_inputs: usize, n_tree_layers: usize) -> usize { + FcmpPlusPlus::proof_size(n_inputs, n_tree_layers) +} + +pub struct FcmpPpVerifyInput { + fcmp_pp: FcmpPlusPlus, + tree_root: TreeRoot, + n_tree_layers: usize, + signable_tx_hash: [u8; 32], + key_images: Vec, +} + +/// # Safety +/// +/// This function assumes that the signable tx hash is 32 bytes, the tree root is heap +/// allocated via a CResult, and pseudo outs and key images are 32 bytes each +#[no_mangle] +pub unsafe extern "C" fn fcmp_pp_verify_input_new( + signable_tx_hash: *const u8, + proof: *const u8, + proof_len: usize, + n_tree_layers: usize, + tree_root: *const TreeRoot, + pseudo_outs: Slice<*const u8>, + key_images: Slice<*const u8>, + fcmp_pp_verify_input_out: *mut *mut FcmpPpVerifyInput, +) -> c_int { + if fcmp_pp_verify_input_out.is_null() { + return -1; + } + + // Early checks + let n_inputs = pseudo_outs.len; + if n_inputs == 0 { + return -2; + } + if n_inputs != key_images.len { + return -3; + } + debug_assert_eq!(proof_len, _slow_fcmp_pp_proof_size(n_inputs, n_tree_layers)); + + let Ok(signable_tx_hash) = hash_array_from_bytes(signable_tx_hash) else { + return -4; + }; + + let mut proof: &[u8] = unsafe { core::slice::from_raw_parts(proof, proof_len) }; + + // 32 byte pseudo outs + let pseudo_outs: &[*const u8] = pseudo_outs.into(); + let pseudo_outs: Vec<[u8; 32]> = pseudo_outs + .iter() + .map(|&x| { + let x = unsafe { core::slice::from_raw_parts(x, 32) }; + let mut pseudo_out = [0u8; 32]; + pseudo_out.copy_from_slice(x); + pseudo_out + }) + .collect(); + + // Read the FCMP++ proof + let Ok(fcmp_plus_plus) = FcmpPlusPlus::read(&pseudo_outs, n_tree_layers, &mut proof) else { + return -5; + }; + + let tree_root: TreeRoot = unsafe { *tree_root }; + + // Collect de-compressed key images into a Vec + let key_images_slice: &[*const u8] = key_images.into(); + let mut key_images = Vec::with_capacity(key_images_slice.len()); + for compressed_ki in key_images_slice { + let Ok(key_image) = ed25519_point_from_bytes(*compressed_ki) else { + return -6; + }; + key_images.push(key_image); + } + + let fcmp_pp_verify_input = FcmpPpVerifyInput { + fcmp_pp: fcmp_plus_plus, + tree_root, + n_tree_layers, + signable_tx_hash, + key_images, + }; + + *fcmp_pp_verify_input_out = new_box_raw(fcmp_pp_verify_input); + 0 +} + +destroy_fn!(destroy_fcmp_pp_verify_input, FcmpPpVerifyInput); + +/// # Safety +/// +/// This function assumes that the signable tx hash is 32 bytes, the input is heap +/// allocated via a CResult, key_image is 32 bytes, and sal_proof is FCMP_PP_SAL_PROOF_SIZE_V1 bytes +#[no_mangle] +pub unsafe extern "C" fn fcmp_pp_verify_sal( + signable_tx_hash: *const u8, + input_bytes: *const u8, + key_image: *const u8, + sal_proof: *const u8, +) -> bool { + let Ok(signable_tx_hash) = hash_array_from_bytes(signable_tx_hash) else { + return false; + }; + let input_bytes = core::slice::from_raw_parts(input_bytes, 4 * 32); + let Ok(input) = input_from_bytes(input_bytes) else { + return false; + }; + let Ok(key_image) = ed25519_point_from_bytes(key_image) else { + return false; + }; + let Ok(sal_proof) = + SpendAuthAndLinkability::read(&mut core::slice::from_raw_parts(sal_proof, 12 * 32)) + else { + // @TODO: remove magic number + return false; + }; + + let mut ed_verifier = multiexp::BatchVerifier::new(/*capacity: */ 1); + + let Ok(_) = sal_proof.verify( + &mut OsRng, + &mut ed_verifier, + signable_tx_hash, + &input, + key_image, + ) else { + return false; + }; + + ed_verifier.verify_vartime() +} + +/// # Safety +/// +/// This function assumes that each element of inputs is heap allocated via a +/// CResult, [fcmp_proof, fcmp_proof+fcmp_proof_len) is a valid readable range +#[no_mangle] +#[allow(non_snake_case)] +pub unsafe extern "C" fn fcmp_pp_verify_membership( + inputs: Slice<[u8; 4 * 32]>, + tree_root: *const TreeRoot, + n_tree_layers: usize, + fcmp_proof: *const u8, + fcmp_proof_len: usize, +) -> bool { + let inputs: &[[u8; 4 * 32]] = inputs.into(); + let Ok(inputs) = inputs + .iter() + .map(|i| { + let i = input_from_bytes(i.as_slice())?; + + let O_tilde = Ed25519::read_G(&mut i.O_tilde().as_slice())?; + let I_tilde = Ed25519::read_G(&mut i.I_tilde().as_slice())?; + let R = Ed25519::read_G(&mut i.R().as_slice())?; + let C_tilde = Ed25519::read_G(&mut i.C_tilde().as_slice())?; + + fcmps::Input::new(O_tilde, I_tilde, R, C_tilde) + }) + .collect::, _>>() + else { + return false; + }; + + let mut fcmp_proof_buf = core::slice::from_raw_parts(fcmp_proof, fcmp_proof_len); + let fcmp = match Fcmp::read(&mut fcmp_proof_buf, inputs.len(), n_tree_layers) { + Ok(p) => p, + Err(_) => return false, + }; + + let mut c1_verifier = generalized_bulletproofs::Generators::batch_verifier(); + let mut c2_verifier = generalized_bulletproofs::Generators::batch_verifier(); + + match fcmp.verify( + &mut OsRng, + &mut c1_verifier, + &mut c2_verifier, + &*FCMP_PARAMS, + *tree_root, + n_tree_layers, + &inputs, + ) { + Ok(()) => { + SELENE_FCMP_GENERATORS.generators.verify(c1_verifier) + && HELIOS_FCMP_GENERATORS.generators.verify(c2_verifier) + } + Err(_) => false, + } +} + +/// # Safety +/// +/// This function assumes that the inputs are from fcmp_pp_verify_input_new +#[no_mangle] +pub unsafe extern "C" fn fcmp_pp_n_inputs(input: *const FcmpPpVerifyInput) -> usize { + let fcmp_pp_verify_input = &*input; + fcmp_pp_verify_input.key_images.len() +} + +/// # Safety +/// +/// This function assumes that the inputs are from fcmp_pp_verify_input_new +#[no_mangle] +pub unsafe extern "C" fn fcmp_pp_verify(inputs: Slice<*const FcmpPpVerifyInput>) -> bool { + let inputs: &[*const FcmpPpVerifyInput] = inputs.into(); + + let mut ed_verifier = multiexp::BatchVerifier::new(inputs.len()); + let mut c1_verifier = generalized_bulletproofs::Generators::batch_verifier(); + let mut c2_verifier = generalized_bulletproofs::Generators::batch_verifier(); + + // TODO: consider multithreading verify individual proofs, needs internal re-work + for &input in inputs { + // Use ref so the input doesn't get consumed (the caller handles de-allocating) + let fcmp_pp_verify_input = &*input; + + let Ok(_) = fcmp_pp_verify_input.fcmp_pp.verify( + &mut OsRng, + &mut ed_verifier, + &mut c1_verifier, + &mut c2_verifier, + fcmp_pp_verify_input.tree_root, + fcmp_pp_verify_input.n_tree_layers, + fcmp_pp_verify_input.signable_tx_hash, + fcmp_pp_verify_input.key_images.clone(), + ) else { + return false; + }; + } + + // TODO: consider multithreading + ed_verifier.verify_vartime() + && SELENE_FCMP_GENERATORS.generators.verify(c1_verifier) + && HELIOS_FCMP_GENERATORS.generators.verify(c2_verifier) +} + +// https://github.com/rust-lang/rust/issues/79609 +#[cfg(all(target_os = "windows", target_arch = "x86"))] +#[no_mangle] +pub extern "C" fn _Unwind_Resume() {} diff --git a/src/fcmp_pp/fcmp_pp_serialization.h b/src/fcmp_pp/fcmp_pp_serialization.h new file mode 100644 index 00000000000..f441fd5478c --- /dev/null +++ b/src/fcmp_pp/fcmp_pp_serialization.h @@ -0,0 +1,61 @@ +// Copyright (c) 2025, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "fcmp_pp_types.h" +#include "serialization/serialization.h" +#include "serialization/binary_archive.h" +#include "serialization/debug_archive.h" +#include "serialization/json_archive.h" +#include "serialization/variant.h" + + +BEGIN_SERIALIZE_OBJECT_FN(fcmp_pp::LegacyOutputPair) + FIELD_F(output_pubkey) + FIELD_F(commitment) +END_SERIALIZE() + +BEGIN_SERIALIZE_OBJECT_FN(fcmp_pp::CarrotOutputPairV1) + FIELD_F(output_pubkey) + FIELD_F(commitment) +END_SERIALIZE() + +VARIANT_TAG(binary_archive, fcmp_pp::LegacyOutputPair, 0x70); +VARIANT_TAG(binary_archive, fcmp_pp::CarrotOutputPairV1, 0x71); + +VARIANT_TAG(json_archive, fcmp_pp::LegacyOutputPair, "legacy_out_pair"); +VARIANT_TAG(json_archive, fcmp_pp::CarrotOutputPairV1, "carrot_out_pair_v1"); + +VARIANT_TAG(debug_archive, fcmp_pp::LegacyOutputPair, "legacy_out_pair"); +VARIANT_TAG(debug_archive, fcmp_pp::CarrotOutputPairV1, "carrot_out_pair_v1"); + +BEGIN_SERIALIZE_OBJECT_FN(fcmp_pp::UnifiedOutput) + FIELD_F(unified_id) + FIELD_F(output_pair) +END_SERIALIZE() diff --git a/src/fcmp_pp/fcmp_pp_types.cpp b/src/fcmp_pp/fcmp_pp_types.cpp new file mode 100644 index 00000000000..def487ab561 --- /dev/null +++ b/src/fcmp_pp/fcmp_pp_types.cpp @@ -0,0 +1,362 @@ +// Copyright (c) 2024, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "fcmp_pp_types.h" + +#include "misc_log_ex.h" +#include "proof_len.h" + +namespace fcmp_pp +{ +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +// Helpers +//---------------------------------------------------------------------------------------------------------------------- +OutputTuple output_tuple_from_bytes(const crypto::ec_point &O, const crypto::ec_point &I, const crypto::ec_point &C) +{ + OutputTuple output_tuple; + + static_assert(sizeof(output_tuple.O) == sizeof(O), "unexpected sizeof O"); + static_assert(sizeof(output_tuple.I) == sizeof(I), "unexpected sizeof I"); + static_assert(sizeof(output_tuple.C) == sizeof(C), "unexpected sizeof C"); + + memcpy(output_tuple.O, &O, sizeof(O)); + memcpy(output_tuple.I, &I, sizeof(I)); + memcpy(output_tuple.C, &C, sizeof(C)); + + return output_tuple; +} +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +// FFI types +//---------------------------------------------------------------------------------------------------------------------- +#define CHECK_FFI_RES \ + CHECK_AND_ASSERT_THROW_MES(r == 0, __func__ << " failed with error code " << r); + +#define IMPLEMENT_FCMP_FFI_TYPE(raw_t, cpp_fn, rust_fn, destroy_fn) \ + void raw_t##Deleter::operator()(raw_t##Unsafe *p) const noexcept { ::destroy_fn(p); }; \ + \ + raw_t cpp_fn \ + { \ + ::raw_t##Unsafe* raw_ptr; \ + int r = ::rust_fn; \ + CHECK_FFI_RES; \ + return raw_t(raw_ptr); \ + }; + +#define IMPLEMENT_FCMP_FFI_SHARED_TYPE(raw_t, cpp_fn1, rust_fn1, cpp_fn2, rust_fn2, destroy_fn) \ + raw_t##Shared cpp_fn1 \ + { \ + ::raw_t##Unsafe* raw_ptr; \ + int r = ::rust_fn1; \ + CHECK_FFI_RES; \ + return raw_t##Shared(raw_ptr, ::destroy_fn); \ + }; \ + \ + raw_t##Shared cpp_fn2 \ + { \ + ::raw_t##Unsafe* raw_ptr; \ + int r = ::rust_fn2; \ + CHECK_FFI_RES; \ + return raw_t##Shared(raw_ptr, ::destroy_fn); \ + }; +//---------------------------------------------------------------------------------------------------------------------- +// Branch blinds +IMPLEMENT_FCMP_FFI_TYPE(HeliosBranchBlind, + gen_helios_branch_blind(), + generate_helios_branch_blind(&raw_ptr), + destroy_helios_branch_blind); +IMPLEMENT_FCMP_FFI_TYPE(SeleneBranchBlind, + gen_selene_branch_blind(), + generate_selene_branch_blind(&raw_ptr), + destroy_selene_branch_blind); +//---------------------------------------------------------------------------------------------------------------------- +// Blinded blinds +IMPLEMENT_FCMP_FFI_TYPE(BlindedOBlind, + blind_o_blind(const SeleneScalar &o_blind), + blind_o_blind(&o_blind, &raw_ptr), + destroy_blinded_o_blind); +IMPLEMENT_FCMP_FFI_TYPE(BlindedIBlind, + blind_i_blind(const SeleneScalar &i_blind), + blind_i_blind(&i_blind, &raw_ptr), + destroy_blinded_i_blind); +IMPLEMENT_FCMP_FFI_TYPE(BlindedIBlindBlind, + blind_i_blind_blind(const SeleneScalar &i_blind_blind), + blind_i_blind_blind(&i_blind_blind, &raw_ptr), + destroy_blinded_i_blind_blind); +IMPLEMENT_FCMP_FFI_TYPE(BlindedCBlind, + blind_c_blind(const SeleneScalar &c_blind), + blind_c_blind(&c_blind, &raw_ptr), + destroy_blinded_c_blind); +//---------------------------------------------------------------------------------------------------------------------- +// Output blinds +IMPLEMENT_FCMP_FFI_TYPE(OutputBlinds, + output_blinds_new(const BlindedOBlind &blinded_o_blind, + const BlindedIBlind &blinded_i_blind, + const BlindedIBlindBlind &blinded_i_blind_blind, + const BlindedCBlind &blinded_c_blind), + output_blinds_new(blinded_o_blind.get(), + blinded_i_blind.get(), + blinded_i_blind_blind.get(), + blinded_c_blind.get(), + &raw_ptr), + destroy_output_blinds); +//---------------------------------------------------------------------------------------------------------------------- +// Tree root +IMPLEMENT_FCMP_FFI_SHARED_TYPE(TreeRoot, + helios_tree_root(const HeliosPoint &helios_point), + helios_tree_root(helios_point, &raw_ptr), + selene_tree_root(const SelenePoint &selene_point), + selene_tree_root(selene_point, &raw_ptr), + destroy_tree_root); +//---------------------------------------------------------------------------------------------------------------------- +// Path +IMPLEMENT_FCMP_FFI_TYPE(Path, + path_new(const OutputChunk &output_chunk, + std::size_t output_idx, + const HeliosT::ScalarChunks &helios_layer_chunks, + const SeleneT::ScalarChunks &selene_layer_chunks), + path_new(output_chunk, output_idx, helios_layer_chunks, selene_layer_chunks, &raw_ptr), + destroy_path); +//---------------------------------------------------------------------------------------------------------------------- +// Implement FCMP++ Prove Input manually because will need temp slices +void FcmpPpProveMembershipInputDeleter::operator()(FcmpPpProveMembershipInputUnsafe *p) const noexcept +{ + ::destroy_fcmp_pp_prove_input(p); +} + +FcmpPpProveMembershipInput fcmp_pp_prove_input_new(const Path &path, + const OutputBlinds &output_blinds, + const std::vector &selene_branch_blinds, + const std::vector &helios_branch_blinds) +{ + MAKE_TEMP_FFI_SLICE(SeleneBranchBlind, selene_branch_blinds, selene_branch_blind_slice); + MAKE_TEMP_FFI_SLICE(HeliosBranchBlind, helios_branch_blinds, helios_branch_blind_slice); + + FcmpPpProveMembershipInputUnsafe *raw_ptr; + int r = ::fcmp_pp_prove_input_new(path.get(), + output_blinds.get(), + selene_branch_blind_slice, + helios_branch_blind_slice, + &raw_ptr); + CHECK_FFI_RES; + + return FcmpPpProveMembershipInput(raw_ptr); +} +//---------------------------------------------------------------------------------------------------------------------- +// Implement FCMP++ Verify Input manually because need to type cast +void FcmpPpVerifyInputDeleter::operator()(FcmpPpVerifyInputUnsafe *p) const noexcept +{ + ::destroy_fcmp_pp_verify_input(p); +} + +FcmpPpVerifyInput fcmp_pp_verify_input_new(const crypto::hash &signable_tx_hash, + const fcmp_pp::FcmpPpProof &fcmp_pp_proof, + const std::size_t n_tree_layers, + const fcmp_pp::TreeRootShared &tree_root, + const std::vector &pseudo_outs, + const std::vector &key_images) +{ + // Cast pseudo outs to vector of const uint8_t* + std::vector pseudo_outs_ptrs; + pseudo_outs_ptrs.reserve(pseudo_outs.size()); + for (const auto &po : pseudo_outs) + pseudo_outs_ptrs.emplace_back((const uint8_t *)&po); + + // Cast key images to vector const uint8_t* + std::vector key_images_ptrs; + key_images_ptrs.reserve(key_images.size()); + for (const auto &ki : key_images) + key_images_ptrs.emplace_back((const uint8_t *)&ki.data); + + FcmpPpVerifyInputUnsafe *raw_ptr; + int r = ::fcmp_pp_verify_input_new( + reinterpret_cast(&signable_tx_hash), + fcmp_pp_proof.data(), + fcmp_pp_proof.size(), + n_tree_layers, + tree_root.get(), + {pseudo_outs_ptrs.data(), pseudo_outs_ptrs.size()}, + {key_images_ptrs.data(), key_images_ptrs.size()}, + &raw_ptr + ); + CHECK_FFI_RES; + + return FcmpPpVerifyInput(raw_ptr); +} +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +fcmp_pp::FcmpPpProof fcmp_pp_proof_from_parts_v1( + const std::vector &rerandomized_outputs, + const std::vector &sal_proofs, + const fcmp_pp::FcmpMembershipProof &membership_proof, + const std::uint8_t n_tree_layers) +{ + const size_t n_inputs = rerandomized_outputs.size(); + CHECK_AND_ASSERT_THROW_MES(sal_proofs.size() == n_inputs, + "fcmp_pp_proof_from_parts_v1: wrong number of sal_proofs"); + for (const fcmp_pp::FcmpPpSalProof &sal_proof : sal_proofs) + CHECK_AND_ASSERT_THROW_MES(sal_proof.size() == FCMP_PP_SAL_PROOF_SIZE_V1, + "fcmp_pp_proof_from_parts_v1: sal proof is incorrect size"); + CHECK_AND_ASSERT_THROW_MES(membership_proof.size() == fcmp_pp::membership_proof_len(n_inputs, n_tree_layers), + "fcmp_pp_proof_from_parts_v1: membership proof is incorrect size"); + const size_t actual_proof_size = membership_proof.size() + + (FCMP_PP_INPUT_TUPLE_SIZE_V1 + FCMP_PP_SAL_PROOF_SIZE_V1) * n_inputs; + CHECK_AND_ASSERT_THROW_MES(actual_proof_size == fcmp_pp::fcmp_pp_proof_len(n_inputs, n_tree_layers), + "fcmp_pp_proof_from_parts_v1: bug: bad length calculation"); + + // build FCMP++ from FCMP and SA/L parts + fcmp_pp::FcmpPpProof proof_bytes; + proof_bytes.reserve(actual_proof_size); + for (size_t i = 0; i < n_inputs; ++i) + { + const FcmpInputCompressed &input = rerandomized_outputs.at(i).input; + const fcmp_pp::FcmpPpSalProof &sal_proof = sal_proofs.at(i); + + // append O~, I~, R (C_tilde not included) + proof_bytes.insert(proof_bytes.end(), input.O_tilde, input.C_tilde); + // append SAL proof + proof_bytes.insert(proof_bytes.end(), sal_proof.cbegin(), sal_proof.cend()); + } + // append membership proof + proof_bytes.insert(proof_bytes.end(), membership_proof.cbegin(), membership_proof.cend()); + + CHECK_AND_ASSERT_THROW_MES(proof_bytes.size() == actual_proof_size, + "fcmp_pp_proof_from_parts_v1: bug: bad proof building"); + + return proof_bytes; +} +//---------------------------------------------------------------------------------------------------------------------- +void fcmp_pp_parts_from_proof_v1( + const fcmp_pp::FcmpPpProof &proof_bytes, + const std::vector &pseudo_outs, + const std::uint8_t n_tree_layers, + fcmp_pp::FcmpMembershipProof &membership_proof_out, + std::vector &sal_proofs_out, + std::vector &fcmp_raw_inputs_out) +{ + const size_t n_inputs = pseudo_outs.size(); + CHECK_AND_ASSERT_THROW_MES(proof_bytes.size() == fcmp_pp::fcmp_pp_proof_len(n_inputs, n_tree_layers), + "fcmp_pp_proof_from_parts_v1: bug: bad length calculation"); + const std::size_t membership_proof_len = fcmp_pp::membership_proof_len(n_inputs, n_tree_layers); + + // Copy the membership proof + CHECK_AND_ASSERT_THROW_MES(proof_bytes.size() > membership_proof_len, + "fcmp_pp_parts_from_proof_v1: unexpected proof_bytes.size() <= membership_proof_len"); + size_t proof_bytes_idx = proof_bytes.size() - membership_proof_len; + membership_proof_out = fcmp_pp::FcmpMembershipProof(proof_bytes.begin() + proof_bytes_idx, proof_bytes.end()); + CHECK_AND_ASSERT_THROW_MES(membership_proof_out.size() == membership_proof_len, + "fcmp_pp_parts_from_proof_v1: unexpeted membership proof len"); + + sal_proofs_out.resize(n_inputs); + fcmp_raw_inputs_out.resize(n_inputs); + for (size_t i = 0; i < n_inputs; ++i) + { + const size_t input_idx = n_inputs - 1 - i; + + // Copy SAL proof + CHECK_AND_ASSERT_THROW_MES(proof_bytes_idx > FCMP_PP_SAL_PROOF_SIZE_V1, + "fcmp_pp_parts_from_proof_v1: proof bytes not larger than SAL proof size"); + proof_bytes_idx -= FCMP_PP_SAL_PROOF_SIZE_V1; + auto proof_it = proof_bytes.begin() + proof_bytes_idx; + sal_proofs_out.at(input_idx) = fcmp_pp::FcmpPpSalProof(proof_it, proof_it + FCMP_PP_SAL_PROOF_SIZE_V1); + CHECK_AND_ASSERT_THROW_MES(sal_proofs_out.at(input_idx).size() == FCMP_PP_SAL_PROOF_SIZE_V1, + "fcmp_pp_parts_from_proof_v1: unexpeted SAL proof len"); + + // Copy O~, I~, R + CHECK_AND_ASSERT_THROW_MES(proof_bytes_idx >= (32*3), + "fcmp_pp_parts_from_proof_v1: proof bytes not larger than 32*3"); + proof_bytes_idx -= 32*3; + memcpy(&fcmp_raw_inputs_out.at(input_idx), proof_bytes.data() + proof_bytes_idx, 32*3); + + // Copy pseudo out + memcpy(&fcmp_raw_inputs_out.at(input_idx).C_tilde, &pseudo_outs.at(input_idx), 32); + } + + CHECK_AND_ASSERT_THROW_MES(proof_bytes_idx == 0, "fcmp_pp_parts_from_proof_v1: expected to get to 0"); +} +//---------------------------------------------------------------------------------------------------------------------- +std::size_t n_inputs_in_fcmp_pp(const FcmpPpVerifyInput &fcmp_pp_verify_input) +{ + return ::fcmp_pp_n_inputs(fcmp_pp_verify_input.get()); +} +//---------------------------------------------------------------------------------------------------------------------- +const crypto::public_key &output_pubkey_cref(const OutputPair &output_pair) +{ + struct output_pair_visitor + { + const crypto::public_key &operator()(const LegacyOutputPair &o) const + { return o.output_pubkey; } + const crypto::public_key &operator()(const CarrotOutputPairV1 &o) const + { return o.output_pubkey; } + }; + + return std::visit(output_pair_visitor{}, output_pair); +} +//---------------------------------------------------------------------------------------------------------------------- +const crypto::ec_point &commitment_cref(const OutputPair &output_pair) +{ + struct output_pair_visitor + { + const crypto::ec_point &operator()(const LegacyOutputPair &o) const + { return o.commitment; } + const crypto::ec_point &operator()(const CarrotOutputPairV1 &o) const + { return o.commitment; } + }; + + return std::visit(output_pair_visitor{}, output_pair); +} +//---------------------------------------------------------------------------------------------------------------------- +bool output_checked_for_torsion(const OutputPair &output_pair) +{ + struct output_pair_visitor + { + bool operator()(const CarrotOutputPairV1&) const + { return true; } + bool operator()(const LegacyOutputPair&) const + { return false; } + }; + + return std::visit(output_pair_visitor{}, output_pair); +} +//---------------------------------------------------------------------------------------------------------------------- +bool use_biased_hash_to_point(const OutputPair &output_pair) +{ + struct output_pair_visitor + { + bool operator()(const CarrotOutputPairV1&) const + { return false; } + bool operator()(const LegacyOutputPair&) const + { return true; } + }; + + return std::visit(output_pair_visitor{}, output_pair); +} +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +}//namespace fcmp_pp diff --git a/src/fcmp_pp/fcmp_pp_types.h b/src/fcmp_pp/fcmp_pp_types.h new file mode 100644 index 00000000000..39e49861870 --- /dev/null +++ b/src/fcmp_pp/fcmp_pp_types.h @@ -0,0 +1,329 @@ +// Copyright (c) 2024, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "crypto/crypto.h" +#include "crypto/hash.h" +#include "fcmp_pp_crypto.h" +#include "fcmp_pp_rust/fcmp++.h" +#include "serialization/keyvalue_serialization.h" + +namespace fcmp_pp +{ +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +// Rust types +//---------------------------------------------------------------------------------------------------------------------- +using SeleneScalar = ::SeleneScalar; +static_assert(sizeof(SeleneScalar) == 32, "unexpected size of selene scalar"); +using HeliosScalar = ::HeliosScalar; +static_assert(sizeof(HeliosScalar) == 32, "unexpected size of helios scalar"); +//---------------------------------------------------------------------------------------------------------------------- +struct SeleneT final +{ + using Scalar = SeleneScalar; + using Point = ::SelenePoint; + using Chunk = ::SeleneScalarSlice; + using CycleScalar = HeliosScalar; + using ScalarChunks = ::SeleneScalarChunks; +}; +//---------------------------------------------------------------------------------------------------------------------- +struct HeliosT final +{ + using Scalar = HeliosScalar; + using Point = ::HeliosPoint; + using Chunk = ::HeliosScalarSlice; + using CycleScalar = SeleneScalar; + using ScalarChunks = ::HeliosScalarChunks; +}; +//---------------------------------------------------------------------------------------------------------------------- +using OutputTuple = ::OutputTuple; +using OutputChunk = ::OutputSlice; +//---------------------------------------------------------------------------------------------------------------------- +OutputTuple output_tuple_from_bytes(const crypto::ec_point &O, const crypto::ec_point &I, const crypto::ec_point &C); +//---------------------------------------------------------------------------------------------------------------------- +// Define FCMP++ prove/verify C++ type here so it can be used in FFI types +using FcmpPpProof = std::vector; +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +// FFI types +//---------------------------------------------------------------------------------------------------------------------- +// FFI types instantiated on the Rust side must be destroyed back on the Rust side. We wrap them in a unique ptr with a +// custom deleter that calls the respective Rust destroy fn. +#define DEFINE_FCMP_FFI_TYPE(raw_t, cpp_fn) \ + struct raw_t##Deleter { void operator()(raw_t##Unsafe *p) const noexcept; }; \ + using raw_t = std::unique_ptr; \ + raw_t cpp_fn; + +// Macro to instantiate an FFI-compatible slice from a vector of FCMP FFI type. Instantiates a vector in local scope +// so it remains in scope while the slice points to it, making sure memory addresses remain contiguous. The slice is +// only usable within local scope, hence "TEMP". +#define MAKE_TEMP_FFI_SLICE(raw_t, vec, slice_name) \ + std::vector raw_t##Vector; \ + raw_t##Vector.reserve(vec.size()); \ + for (const raw_t &elem : vec) \ + raw_t##Vector.push_back(elem.get()); \ + ::raw_t##SliceUnsafe slice_name{raw_t##Vector.data(), raw_t##Vector.size()}; + +DEFINE_FCMP_FFI_TYPE(HeliosBranchBlind, gen_helios_branch_blind()); +DEFINE_FCMP_FFI_TYPE(SeleneBranchBlind, gen_selene_branch_blind()); + +DEFINE_FCMP_FFI_TYPE(BlindedOBlind, blind_o_blind(const SeleneScalar &)); +DEFINE_FCMP_FFI_TYPE(BlindedIBlind, blind_i_blind(const SeleneScalar &)); +DEFINE_FCMP_FFI_TYPE(BlindedIBlindBlind, blind_i_blind_blind(const SeleneScalar &)); +DEFINE_FCMP_FFI_TYPE(BlindedCBlind, blind_c_blind(const SeleneScalar &)); + +DEFINE_FCMP_FFI_TYPE(OutputBlinds, + output_blinds_new(const BlindedOBlind &, const BlindedIBlind &, const BlindedIBlindBlind &, const BlindedCBlind &)); + +// Use a shared pointer so we can reference the same underlying tree root in multiple places +using TreeRootShared = std::shared_ptr; +TreeRootShared helios_tree_root(const HeliosPoint &); +TreeRootShared selene_tree_root(const SelenePoint &); + +DEFINE_FCMP_FFI_TYPE(Path, + path_new(const OutputChunk &, std::size_t, const HeliosT::ScalarChunks &, const SeleneT::ScalarChunks &)); + +DEFINE_FCMP_FFI_TYPE(FcmpPpProveMembershipInput, + fcmp_pp_prove_input_new(const Path &, + const OutputBlinds &, + const std::vector &, + const std::vector &)); + +DEFINE_FCMP_FFI_TYPE(FcmpPpVerifyInput, + fcmp_pp_verify_input_new(const crypto::hash &signable_tx_hash, + const fcmp_pp::FcmpPpProof &fcmp_pp_proof, + const std::size_t n_tree_layers, + const fcmp_pp::TreeRootShared &tree_root, + const std::vector &pseudo_outs, + const std::vector &key_images)); +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +// C++ types +//---------------------------------------------------------------------------------------------------------------------- +// Curve trees types +//---------------------------------------------------------------------------------------------------------------------- +// Output pub key and commitment, ready to be converted to a leaf tuple +// - From {output_pubkey,commitment} -> {O,C} -> {O.x,O.y,I.x,I.y,C.x,C.y} +// - Output pairs do NOT necessarily have torsion cleared. We need the output pubkey as it exists in the chain in order +// to derive the correct I (when deriving {O.x,O.y,I.x,I.y,C.x,C.y}). Torsion clearing O before deriving I from O +// would enable spending a torsioned output once before FCMP++ fork and again with a different key image via FCMP++. +template +struct OutputPairTemplate +{ + crypto::public_key output_pubkey; + // Uses the ec_point type to avoid a circular dep to ringct/rctTypes.h, and to differentiate from output_pubkey + crypto::ec_point commitment; + + OutputPairTemplate(const crypto::public_key &_output_pubkey, const crypto::ec_point &_commitment): + output_pubkey(_output_pubkey), + commitment(_commitment) + {}; + + OutputPairTemplate(): + output_pubkey{}, + commitment{} + {}; + + bool operator==(const OutputPairTemplate &other) const + { + return output_pubkey == other.output_pubkey + && commitment == other.commitment; + } +}; + +// May have torsion, use biased key image generator for I +struct LegacyOutputPair : public OutputPairTemplate{}; +// No torsion, use unbiased key image generator for I +struct CarrotOutputPairV1 : public OutputPairTemplate{}; + +static_assert(sizeof(LegacyOutputPair) == (32+32), "sizeof LegacyOutputPair unexpected"); +static_assert(sizeof(CarrotOutputPairV1) == (32+32), "sizeof CarrotOutputPairV1 unexpected"); + +static_assert(std::has_unique_object_representations_v); +static_assert(std::has_unique_object_representations_v); + +using OutputPair = std::variant; + +const crypto::public_key &output_pubkey_cref(const OutputPair &output_pair); +const crypto::ec_point &commitment_cref(const OutputPair &output_pair); + +bool output_checked_for_torsion(const OutputPair &output_pair); +bool use_biased_hash_to_point(const OutputPair &output_pair); + +// Wrapper for outputs with context to insert the output into the FCMP++ curve tree +struct UnifiedOutput final +{ + // Output's unique id in the chain, used to insert the output in the tree in the order it entered the chain + uint64_t unified_id{0}; + OutputPair output_pair; + + bool operator==(const UnifiedOutput &other) const + { + return unified_id == other.unified_id && output_pair == other.output_pair; + } + + // TODO: move to fcmp_pp_serialization.h + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(unified_id) + KV_SERIALIZE(output_pair) + END_KV_SERIALIZE_MAP() +}; + +#define SIZEOF_SERIALIZED_UNIFIED_OUTPUT 73 // 8+1+32+32 + +using OutsByLastLockedBlock = std::unordered_map>; + +// Contiguous leaves in the tree, starting at a specified start_idx in the leaf layer +struct ContiguousLeaves final +{ + // Starting leaf tuple index in the leaf layer + uint64_t start_leaf_tuple_idx{0}; + // Contiguous leaves in a tree that start at the start_idx + std::vector tuples; +}; + +/* The "Compressed" prefix means all points contained in the struct are compressed points */ + +// A layer of contiguous hashes starting from a specific start_idx in the tree +struct CompressedLayerExtension final +{ + uint64_t start_idx{0}; + bool update_existing_last_hash; + std::vector hashes; +}; + +// A struct useful to extend an existing tree +// - layers alternate between C1 and C2 +// - layer_extensions[0] is C1 first layer after leaves, then layer_extensions[1] is C2, layer_extensions[2] is C1, etc. +struct CompressedTreeExtension final +{ + ContiguousLeaves leaves; + std::vector layer_extensions; +}; + +// A chunk in the tree +struct CompressedChunk final +{ + std::vector elems; + + bool operator==(const CompressedChunk &other) const { return elems == other.elems; } + + // TODO: move to fcmp_pp_serialization.h + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE_CONTAINER_POD_AS_BLOB(elems) + END_KV_SERIALIZE_MAP() +}; + +// A path in the tree +struct CompressedPath final +{ + std::vector leaves; + std::vector layer_chunks; + + bool operator==(const CompressedPath &other) const + {return leaves == other.leaves && layer_chunks == other.layer_chunks;} + + // TODO: move to fcmp_pp_serialization.h + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE_CONTAINER_POD_AS_BLOB(leaves) + KV_SERIALIZE(layer_chunks) + END_KV_SERIALIZE_MAP() +}; + +// The indexes in the tree of a leaf's path elems containing whole chunks at each layer +// - leaf_range refers to a complete chunk of leaves +struct PathIndexes final +{ + using StartIdx = uint64_t; + using EndIdxExclusive = uint64_t; + using Range = std::pair; + + Range leaf_range; + std::vector layers; +}; +//---------------------------------------------------------------------------------------------------------------------- +// FCMP++ prove/verify types +//---------------------------------------------------------------------------------------------------------------------- +// Byte buffer containing the fcmp++ proof +using FcmpPpSalProof = std::vector; +using FcmpMembershipProof = std::vector; + +struct ProofInput final +{ + Path path; + OutputBlinds output_blinds; + std::vector selene_branch_blinds; + std::vector helios_branch_blinds; +}; + +struct FcmpVerifyHelperData final +{ + TreeRootShared tree_root; + std::vector key_images; +}; + +// Serialize types into a single byte buffer +FcmpPpProof fcmp_pp_proof_from_parts_v1( + const std::vector &rerandomized_outputs, + const std::vector &sal_proofs, + const FcmpMembershipProof &membership_proof, + const std::uint8_t n_tree_layers); + +// De-serialize types from a single byte buffer +void fcmp_pp_parts_from_proof_v1( + const fcmp_pp::FcmpPpProof &proof_bytes, + const std::vector &pseudo_outs, + const std::uint8_t n_tree_layers, + fcmp_pp::FcmpMembershipProof &membership_proof_out, + std::vector &sal_proofs_out, + std::vector &fcmp_raw_inputs_out); + +// Get the number of inputs included in the FCMP++ verify input +std::size_t n_inputs_in_fcmp_pp(const FcmpPpVerifyInput &fcmp_pp_verify_input); +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +}//namespace fcmp_pp + +inline bool operator==(const fcmp_pp::OutputTuple &a, const fcmp_pp::OutputTuple &b) +{ + static_assert(sizeof(fcmp_pp::OutputTuple) == (sizeof(a.O) + sizeof(a.I) + sizeof(a.C)), + "unexpected sizeof OutputTuple for == implementation"); + return + (memcmp(a.O, b.O, sizeof(a.O)) == 0) && + (memcmp(a.I, b.I, sizeof(a.I)) == 0) && + (memcmp(a.C, b.C, sizeof(a.C)) == 0); +} diff --git a/src/fcmp_pp/ffi_api_c_compat.c b/src/fcmp_pp/ffi_api_c_compat.c new file mode 100644 index 00000000000..c92510ebd9f --- /dev/null +++ b/src/fcmp_pp/ffi_api_c_compat.c @@ -0,0 +1,3 @@ +#include "fcmp++.h" + +int main() { return 0; } diff --git a/src/fcmp_pp/proof_len.cpp b/src/fcmp_pp/proof_len.cpp new file mode 100644 index 00000000000..ce1e97decd0 --- /dev/null +++ b/src/fcmp_pp/proof_len.cpp @@ -0,0 +1,49 @@ +// Copyright (c) 2024, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "proof_len.h" + +#include "fcmp_pp_rust/fcmp++.h" +#include "misc_log_ex.h" + +namespace fcmp_pp +{ +//---------------------------------------------------------------------------------------------------------------------- +std::size_t membership_proof_len(const std::size_t n_inputs, const uint8_t n_layers) +{ + // TODO: just remove this proof_len table altogether now that the calculation is fast + return ::_slow_membership_proof_size(n_inputs, n_layers); +}; + +std::size_t fcmp_pp_proof_len(const std::size_t n_inputs, const uint8_t n_layers) +{ + return membership_proof_len(n_inputs, n_layers) + + (n_inputs * (FCMP_PP_INPUT_TUPLE_SIZE_V1 + FCMP_PP_SAL_PROOF_SIZE_V1)); +}; +//---------------------------------------------------------------------------------------------------------------------- +}//namespace fcmp_pp diff --git a/src/fcmp_pp/proof_len.h b/src/fcmp_pp/proof_len.h new file mode 100644 index 00000000000..2f4a551dede --- /dev/null +++ b/src/fcmp_pp/proof_len.h @@ -0,0 +1,180 @@ +// Copyright (c) 2024, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "cryptonote_config.h" + +namespace fcmp_pp +{ +//---------------------------------------------------------------------------------------------------------------------- + +static_assert(FCMP_PLUS_PLUS_MAX_INPUTS == 128, "FCMP++ proof len table expects max 128 inputs"); + +// Constructed using ::_slow_membership_proof_size +static const uint32_t PROOF_LEN_TABLE[FCMP_PLUS_PLUS_MAX_INPUTS][FCMP_PLUS_PLUS_MAX_LAYERS] +{ + {2880, 3296, 3520, 4064, 4416, 4832, 5056, 5600, 5248, 4768, 4960, 5312, }, + {3680, 4640, 4288, 5120, 5632, 5376, 5888, 6336, 6144, 5696, 6048, 6432, }, + {3360, 4736, 5312, 5728, 5408, 5472, 6016, 6592, 6976, 7552, 7424, 7136, }, + {3744, 5664, 5504, 5824, 6528, 6464, 7168, 7808, 7808, 7552, 8096, 8672, }, + {4288, 5728, 5408, 6016, 6784, 7744, 7616, 7680, 8256, 9024, 9600, 10208, }, + {3424, 5216, 6144, 6656, 6688, 7104, 7744, 8672, 9408, 10208, 10304, 10496, }, + {3616, 5632, 5856, 6656, 7456, 7904, 8832, 9792, 9888, 10144, 10912, 11712, }, + {3808, 6176, 6400, 7104, 8192, 8512, 9600, 10624, 11008, 11136, 12064, 13024, }, + {4160, 6016, 6112, 6976, 8096, 9440, 9728, 10176, 11136, 12288, 13248, 14368, }, + {4352, 6272, 6496, 7520, 8832, 10208, 10496, 11104, 12224, 13408, 14400, 15680, }, + {3456, 5696, 7040, 8096, 8544, 9536, 10720, 11904, 13056, 14400, 15712, 16352, }, + {3488, 5920, 7424, 8512, 9120, 10112, 11328, 12832, 14144, 15520, 16192, 16960, }, + {3648, 6272, 7808, 9056, 9696, 10720, 12096, 13632, 14976, 15968, 16768, 18112, }, + {3680, 6496, 7328, 8736, 10272, 11456, 12864, 14560, 15392, 16384, 17760, 19296, }, + {3840, 6848, 7712, 9152, 10848, 12064, 13632, 15360, 16192, 17216, 18752, 20448, }, + {3872, 7072, 8064, 9536, 11392, 12480, 14336, 16128, 17280, 18176, 19872, 21600, }, + {4064, 6752, 7744, 9376, 11136, 13216, 14400, 15648, 17408, 19296, 21024, 22784, }, + {4224, 6944, 7968, 9760, 11680, 13824, 15168, 16416, 18176, 20256, 22144, 23936, }, + {4256, 7168, 8352, 10304, 12256, 14560, 15776, 17184, 19264, 21216, 23136, 25248, }, + {4416, 7360, 8544, 10528, 12800, 15136, 16384, 17952, 20032, 22176, 24128, 26368, }, + {4448, 7712, 8928, 10944, 12512, 15008, 17120, 18720, 20864, 23168, 25280, 27552, }, + {3520, 6816, 9152, 11456, 12896, 14880, 17184, 19488, 21632, 24096, 26400, 28160, }, + {3520, 7008, 9504, 11840, 13472, 15456, 17760, 20256, 22720, 25216, 26848, 28480, }, + {3552, 7200, 9856, 12096, 13856, 16000, 18368, 21024, 23488, 26016, 27840, 29760, }, + {3712, 7552, 10240, 12640, 14432, 16608, 19104, 21792, 24320, 27008, 28960, 30912, }, + {3712, 7584, 10304, 12992, 14816, 17024, 19840, 22560, 25088, 27392, 29248, 31904, }, + {3744, 7808, 10688, 13408, 15360, 17728, 20448, 23328, 25504, 27680, 30368, 33088, }, + {3744, 7968, 10144, 12896, 15776, 18304, 21056, 24096, 26272, 28608, 31328, 34208, }, + {3904, 8320, 10496, 13280, 16192, 18720, 21760, 24864, 27072, 29440, 32320, 35200, }, + {3904, 8352, 10720, 13664, 16736, 19328, 22528, 25632, 27840, 30368, 33408, 36352, }, + {3936, 8576, 10944, 14048, 17280, 19872, 22976, 26400, 28928, 31328, 34400, 37632, }, + {3936, 8736, 11264, 14272, 17664, 20288, 23680, 27008, 29696, 32128, 35360, 38624, }, + {4096, 8384, 10944, 14112, 17408, 21024, 23744, 26528, 29792, 33216, 36480, 39776, }, + {4128, 8576, 11008, 14464, 17792, 21568, 24192, 27264, 30592, 34176, 37472, 40928, }, + {4128, 8608, 11360, 14720, 18336, 22176, 24960, 27904, 31360, 34976, 38432, 42080, }, + {4288, 8800, 11552, 15072, 18720, 22592, 25664, 28640, 32128, 35936, 39552, 43072, }, + {4288, 8992, 11776, 15456, 19296, 23168, 26112, 29408, 33056, 36736, 40384, 44192, }, + {4320, 9184, 12128, 15840, 19680, 23872, 26848, 30016, 33984, 37696, 41504, 45504, }, + {4320, 9216, 12320, 16192, 20064, 24288, 27584, 30912, 34752, 38784, 42624, 46496, }, + {4480, 9408, 12512, 16416, 20608, 24864, 28032, 31520, 35520, 39584, 43456, 47616, }, + {4512, 9760, 12896, 16832, 20320, 24736, 28768, 32288, 36320, 40512, 44576, 48800, }, + {4512, 9920, 13088, 17184, 20704, 25152, 29344, 33024, 37120, 41504, 45696, 49792, }, + {3584, 9024, 13280, 17408, 21088, 25152, 29408, 33664, 37888, 42304, 46528, 50240, }, + {3584, 9056, 13504, 17920, 21472, 25568, 29984, 34400, 38656, 43232, 47648, 51520, }, + {3584, 9248, 13856, 18304, 22048, 26144, 30560, 35168, 39584, 44192, 48768, 52512, }, + {3584, 9280, 14048, 18528, 22432, 26688, 31136, 35776, 40512, 45152, 49056, 52960, }, + {3616, 9472, 14272, 18912, 22816, 27104, 31872, 36672, 41280, 46080, 50016, 53952, }, + {3616, 9632, 14592, 19136, 23200, 27648, 32320, 37280, 42048, 46880, 51008, 55232, }, + {3616, 9824, 14816, 19520, 23616, 28096, 33056, 38048, 42880, 47872, 52128, 56384, }, + {3776, 10016, 15040, 20032, 24160, 28672, 33632, 38784, 43648, 48800, 53088, 57376, }, + {3776, 10176, 15360, 20256, 24544, 29216, 34208, 39392, 44416, 49600, 54048, 58496, }, + {3776, 10208, 15424, 20608, 24928, 29632, 34944, 40160, 45184, 49984, 54336, 59488, }, + {3776, 10400, 15808, 21024, 25472, 30176, 35392, 40928, 46272, 50944, 55424, 60608, }, + {3808, 10592, 16128, 21248, 25856, 30752, 36128, 41536, 46368, 51200, 56416, 61792, }, + {3808, 10624, 15456, 20864, 26272, 31328, 36704, 42272, 47136, 52000, 57376, 62784, }, + {3808, 10784, 15648, 21088, 26656, 31872, 37312, 43040, 47904, 52928, 58336, 63904, }, + {3968, 11136, 16000, 21472, 27072, 32288, 38016, 43808, 48704, 53728, 59296, 64896, }, + {3968, 11168, 16064, 21824, 27456, 32832, 38464, 44544, 49472, 54688, 60416, 66016, }, + {3968, 11328, 16416, 22080, 27840, 33280, 39200, 45152, 50240, 55488, 61248, 67168, }, + {3968, 11360, 16608, 22432, 28384, 33856, 39936, 45920, 51008, 56416, 62336, 68160, }, + {4000, 11584, 16832, 22816, 28928, 34400, 40384, 46688, 52096, 57376, 63328, 69440, }, + {4000, 11744, 17152, 23040, 29312, 34816, 41088, 47296, 52864, 58176, 64288, 70432, }, + {4000, 11776, 17216, 23392, 29696, 35360, 41536, 48032, 53632, 59104, 65248, 71552, }, + {4000, 11936, 17536, 23616, 30080, 35776, 42240, 48640, 54400, 59904, 66208, 72544, }, + {4160, 11584, 17216, 23456, 29824, 36512, 42304, 48160, 54496, 60992, 67328, 73696, }, + {4160, 11616, 17280, 23808, 30208, 37056, 42752, 48896, 55264, 61792, 68160, 74816, }, + {4160, 11776, 17600, 24032, 30592, 37472, 43456, 49504, 56032, 62720, 69248, 75808, }, + {4192, 11968, 17664, 24384, 30976, 38016, 43904, 50240, 56832, 63680, 70240, 76960, }, + {4192, 12000, 17984, 24608, 31520, 38592, 44640, 50880, 57600, 64480, 71200, 77952, }, + {4192, 12160, 18208, 24992, 31904, 39040, 45376, 51616, 58368, 65408, 72160, 79104, }, + {4192, 12192, 18400, 25216, 32288, 39584, 45824, 52224, 59136, 66240, 73152, 80224, }, + {4352, 12384, 18592, 25568, 32672, 40000, 46528, 52960, 59904, 67168, 74240, 81216, }, + {4352, 12576, 18816, 25952, 33216, 40544, 46976, 53728, 60832, 67968, 75072, 82336, }, + {4352, 12608, 19136, 26176, 33632, 41120, 47680, 54336, 61600, 68896, 76160, 83328, }, + {4384, 12800, 19200, 26528, 34016, 41696, 48288, 55072, 62368, 69696, 77152, 84640, }, + {4384, 12960, 19552, 26912, 34400, 42240, 48864, 55680, 63296, 70656, 78112, 85760, }, + {4384, 12992, 19744, 27264, 34784, 42656, 49600, 56576, 64064, 71744, 79232, 86752, }, + {4384, 13152, 19936, 27488, 35168, 43200, 50048, 57184, 64832, 72544, 80064, 87872, }, + {4544, 13344, 20128, 27840, 35712, 43776, 50752, 57920, 65600, 73472, 81152, 88864, }, + {4544, 13376, 20320, 28064, 36096, 44192, 51200, 58528, 66368, 74272, 81984, 89984, }, + {4544, 13696, 20672, 28448, 36512, 44768, 51936, 59296, 67168, 75200, 83104, 91168, }, + {4576, 13888, 20896, 28832, 36192, 44480, 52512, 60032, 67936, 76000, 84064, 92160, }, + {4576, 13920, 21088, 29056, 36576, 45024, 53088, 60640, 68736, 76960, 85024, 93280, }, + {4576, 14080, 21280, 29408, 36960, 45440, 53664, 61376, 69504, 77920, 86144, 94272, }, + {3488, 13024, 21472, 29632, 37344, 45984, 54240, 61984, 70272, 78720, 86976, 95392, }, + {3648, 13216, 21664, 29984, 37728, 45856, 54304, 62752, 71040, 79648, 88064, 95840, }, + {3648, 13376, 21856, 30208, 38112, 46400, 54880, 63360, 71808, 80448, 88896, 96832, }, + {3648, 13408, 22080, 30720, 38496, 46816, 55456, 64096, 72576, 81376, 90016, 98112, }, + {3648, 13600, 22432, 31104, 39072, 47392, 56032, 64864, 73504, 82176, 90976, 99104, }, + {3648, 13632, 22624, 31328, 39456, 47936, 56608, 65472, 74272, 83264, 91968, 100224, }, + {3648, 13792, 22816, 31680, 39840, 48352, 57184, 66208, 75200, 84224, 93056, 101216, }, + {3648, 13824, 23008, 31904, 40224, 48896, 57760, 66816, 75968, 85024, 93344, 101664, }, + {3680, 14016, 23200, 32256, 40608, 49312, 58336, 67552, 76736, 85952, 94304, 102656, }, + {3680, 14176, 23552, 32512, 40992, 49856, 58944, 68320, 77504, 86752, 95264, 103776, }, + {3680, 14208, 23616, 32864, 41376, 50272, 59648, 69056, 78272, 87680, 96384, 104928, }, + {3680, 14368, 23936, 33088, 41760, 50816, 60096, 69664, 79040, 88480, 97216, 106048, }, + {3680, 14560, 24160, 33472, 42176, 51264, 60832, 70432, 79840, 89440, 98336, 107200, }, + {3680, 14720, 24352, 33824, 42560, 51808, 61408, 71168, 80640, 90400, 99296, 108192, }, + {3680, 14752, 24704, 34208, 43104, 52384, 61984, 71776, 81408, 91200, 100256, 109312, }, + {3840, 14944, 24768, 34560, 43488, 52800, 62560, 72512, 82176, 92128, 101216, 110304, }, + {3840, 15104, 25088, 34784, 43872, 53344, 63136, 73120, 82944, 92928, 102176, 111424, }, + {3840, 15136, 25152, 35136, 44256, 53760, 63712, 73856, 83712, 93856, 103136, 112416, }, + {3840, 15296, 25472, 35360, 44640, 54304, 64320, 74496, 84480, 94112, 103552, 113536, }, + {3840, 15328, 25536, 35712, 45024, 54720, 65024, 75232, 85248, 95040, 104384, 114528, }, + {3840, 15520, 25920, 36128, 45568, 55264, 65472, 76000, 86176, 95840, 105472, 115648, }, + {3840, 15552, 26240, 36352, 45952, 55680, 66176, 76608, 87104, 96800, 106304, 116640, }, + {3872, 15744, 26304, 36704, 46336, 56384, 66784, 77344, 87872, 97728, 107392, 117792, }, + {3872, 15904, 26624, 36928, 46720, 56800, 67360, 77952, 87968, 97984, 108384, 118944, }, + {3872, 15936, 26688, 37280, 47136, 57376, 67936, 78688, 88736, 98784, 109344, 119936, }, + {3872, 16096, 26144, 36768, 47520, 57920, 68512, 79296, 89504, 99712, 110304, 121056, }, + {3872, 16128, 26336, 37120, 47904, 58336, 69248, 80192, 90272, 100512, 111264, 122048, }, + {3872, 16288, 26528, 37344, 48288, 58880, 69696, 80800, 91040, 101440, 112224, 123168, }, + {3872, 16480, 26880, 37728, 48704, 59296, 70400, 81568, 91840, 102240, 113184, 124160, }, + {4032, 16672, 26944, 38080, 49088, 59840, 70848, 82304, 92608, 103168, 114144, 125280, }, + {4032, 16832, 27264, 38304, 49472, 60256, 71552, 82912, 93376, 103968, 115104, 126272, }, + {4032, 16864, 27328, 38656, 49856, 60800, 72000, 83648, 94144, 104928, 116224, 127392, }, + {4032, 17024, 27680, 38912, 50240, 61216, 72704, 84256, 94912, 105728, 117056, 128384, }, + {4032, 17056, 27872, 39264, 50624, 61792, 73312, 84992, 95680, 106656, 118144, 129536, }, + {4032, 17216, 28064, 39488, 51168, 62368, 73888, 85600, 96448, 107456, 118976, 130656, }, + {4032, 17248, 28256, 39840, 51552, 62784, 74624, 86368, 97216, 108384, 120064, 131648, }, + {4064, 17472, 28480, 40224, 52096, 63328, 75072, 87136, 98304, 109344, 120896, 132768, }, + {4064, 17632, 28800, 40448, 52480, 63744, 75776, 87744, 99072, 110144, 122016, 133920, }, + {4064, 17664, 28864, 40800, 52864, 64288, 76224, 88480, 99840, 111072, 122976, 135040, }, + {4064, 17824, 29184, 41024, 53248, 64704, 76928, 89088, 100608, 111872, 123936, 136032, }, + {4064, 17856, 29248, 41376, 53632, 65248, 77376, 89824, 101376, 112800, 124896, 137152, }, + {4064, 18016, 29568, 41600, 54016, 65664, 78080, 90432, 102144, 113600, 125856, 138144, }, + {4064, 18048, 29632, 41952, 54400, 66208, 78528, 91168, 102912, 114528, 126816, 139264, }, + {4064, 18208, 29952, 42176, 54784, 66624, 79232, 91776, 103680, 115328, 127776, 140256, }, +}; + +// Size of the membership proof alone +std::size_t membership_proof_len(const std::size_t n_inputs, const uint8_t n_layers); + +// Size of the FCMP++ proof (membership proof + spend-auth + linkability proofs & input tuples) +// https://github.com/kayabaNerve/fcmp-plus-plus/blob/78754718faa21f0a5751fbd30c9495d7f7f5c2b1/networks/monero/ringct/fcmp%2B%2B/src/lib.rs#L273-L274 +std::size_t fcmp_pp_proof_len(const std::size_t n_inputs, const uint8_t n_layers); + +//---------------------------------------------------------------------------------------------------------------------- +}//namespace fcmp_pp diff --git a/src/fcmp_pp/prove.cpp b/src/fcmp_pp/prove.cpp new file mode 100644 index 00000000000..2cfaecdb51c --- /dev/null +++ b/src/fcmp_pp/prove.cpp @@ -0,0 +1,301 @@ +// Copyright (c) 2024, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "prove.h" + +#include "crypto/generators.h" +#include "fcmp_pp_crypto.h" +#include "misc_log_ex.h" +#include "proof_len.h" + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "fcmp_pp" + +#define HANDLE_RES_CODE(res_t, api_func, ...) do { \ + res_t res; \ + if (api_func(__VA_ARGS__, &res) < 0) \ + throw std::runtime_error("failed to " #api_func); \ + return res; \ + } while (0); \ + +namespace fcmp_pp +{ +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +FcmpRerandomizedOutputCompressed rerandomize_output(const OutputTuple output) +{ + HANDLE_RES_CODE(FcmpRerandomizedOutputCompressed, ::rerandomize_output, output); +} +//---------------------------------------------------------------------------------------------------------------------- +FcmpRerandomizedOutputCompressed rerandomize_output(const crypto::public_key &onetime_address, + const crypto::ec_point &amount_commitment, + const bool use_biased_hash_to_point) +{ + crypto::ec_point I; + crypto::derive_key_image_generator(onetime_address, use_biased_hash_to_point, I); + + const OutputTuple output_tuple = output_tuple_from_bytes(onetime_address, I, amount_commitment); + return rerandomize_output(output_tuple); +} +//---------------------------------------------------------------------------------------------------------------------- +FcmpInputCompressed calculate_fcmp_input_for_rerandomizations(const crypto::public_key &onetime_address, + const crypto::ec_point &amount_commitment, + const bool use_biased_hash_to_point, + const crypto::secret_key &r_o, + const crypto::secret_key &r_i, + const crypto::secret_key &r_r_i, + const crypto::secret_key &r_c) +{ + FcmpInputCompressed res; + + ge_p2 p2_1; + ge_p3 p3_1, p3_2; + ge_p1p1 p1p1_1; + ge_cached c_1; + int r; + + // O~ = O + r_o T + r = ge_frombytes_vartime(&p3_1, to_bytes(onetime_address)); + CHECK_AND_ASSERT_THROW_MES(!r, "calculate fcmp input for rerandomizations: bad O"); + scalarmult_and_add(res.O_tilde, p3_1, to_bytes(r_o), crypto::get_T_p3()); + + // I = Hp(O) + crypto::ec_point I; + crypto::derive_key_image_generator(onetime_address, use_biased_hash_to_point, I); + + // I~ = I + r_i U + r = ge_frombytes_vartime(&p3_1, to_bytes(I)); + CHECK_AND_ASSERT_THROW_MES(!r, "calculate fcmp input for rerandomizations: bad I"); + scalarmult_and_add(res.I_tilde, p3_1, to_bytes(r_i), crypto::get_U_p3()); + + // R = r_i V + r_r_i T + p3_1 = crypto::get_V_p3(); + p3_2 = crypto::get_T_p3(); + ge_scalarmult_p3(&p3_1, to_bytes(r_i), &p3_1); + ge_scalarmult_p3(&p3_2, to_bytes(r_r_i), &p3_2); + ge_p3_to_cached(&c_1, &p3_1); + ge_add(&p1p1_1, &p3_2, &c_1); + ge_p1p1_to_p2(&p2_1, &p1p1_1); + ge_tobytes(res.R, &p2_1); + + // C~ = C + r_c G + r = ge_frombytes_vartime(&p3_1, to_bytes(amount_commitment)); + CHECK_AND_ASSERT_THROW_MES(!r, "calculate fcmp input for rerandomizations: bad C"); + scalarmult_and_add(res.C_tilde, p3_1, to_bytes(r_c), crypto::get_G_p3()); // @TODO: could be faster + + return res; +} +//---------------------------------------------------------------------------------------------------------------------- +void make_balanced_rerandomized_output_set( + const std::vector &input_onetime_addresses, + const std::vector &input_amount_commitments, + const std::vector &input_uses_biased_hash_to_point, + const std::vector &input_amount_blinding_factors, + const std::vector &r_o, + const crypto::secret_key &output_amount_blinding_factor_sum, + std::vector &rerandomized_outputs_out) +{ + rerandomized_outputs_out.clear(); + + const size_t nins = input_onetime_addresses.size(); + CHECK_AND_ASSERT_THROW_MES(nins, "make balanced rerandomized output set: no inputs provided"); + CHECK_AND_ASSERT_THROW_MES(input_amount_commitments.size() == nins, + "make balanced rerandomized output set: wrong input amount commitments size"); + CHECK_AND_ASSERT_THROW_MES(input_uses_biased_hash_to_point.size() == nins, + "make balanced rerandomized output set: wrong input biased hash-to-point config size"); + CHECK_AND_ASSERT_THROW_MES(input_amount_blinding_factors.size() == nins, + "make balanced rerandomized output set: wrong input amount blinding factors size"); + CHECK_AND_ASSERT_THROW_MES(r_o.size() == nins, + "make balanced rerandomized output set: wrong r_o size"); + + // set blinding_factor_imbalance to sum(output amount blinding factors) - sum(input amount blinding factors) + crypto::secret_key blinding_factor_imbalance = output_amount_blinding_factor_sum; + for (const crypto::secret_key &ibf : input_amount_blinding_factors) + { + sc_sub(to_bytes(blinding_factor_imbalance), + to_bytes(blinding_factor_imbalance), + to_bytes(ibf)); + } + + rerandomized_outputs_out.reserve(nins); + for (size_t i = 0; i < nins; ++i) + { + FcmpRerandomizedOutputCompressed &rerandomized_output = rerandomized_outputs_out.emplace_back(); + + // O + const crypto::public_key &onetime_address = input_onetime_addresses.at(i); + // C + const crypto::ec_point &amount_commitment = input_amount_commitments.at(i); + + // sample r_i, r_r_i randomly + crypto::secret_key r_i, r_r_i; + crypto::random32_unbiased(to_bytes(r_i)); + crypto::random32_unbiased(to_bytes(r_r_i)); + + // sample r_c for all inputs except for the last one, set that one such that the tx balances + crypto::secret_key r_c; + if (i == nins - 1) + r_c = blinding_factor_imbalance; + else // not last input + crypto::random32_unbiased(to_bytes(r_c)); + + // update blinding_factor_imbalance with new rerandomization + sc_sub(to_bytes(blinding_factor_imbalance), to_bytes(blinding_factor_imbalance), to_bytes(r_c)); + + // calculate FCMP input + const bool use_biased_hash_to_point = input_uses_biased_hash_to_point.at(i); + rerandomized_output.input = calculate_fcmp_input_for_rerandomizations(onetime_address, + amount_commitment, use_biased_hash_to_point, r_o.at(i), r_i, r_r_i, r_c); + + // copy rerandomizations + memcpy(rerandomized_output.r_o, to_bytes(r_o.at(i)), 32); + memcpy(rerandomized_output.r_i, to_bytes(r_i), 32); + memcpy(rerandomized_output.r_r_i, to_bytes(r_r_i), 32); + memcpy(rerandomized_output.r_c, to_bytes(r_c), 32); + } +} +//---------------------------------------------------------------------------------------------------------------------- +SeleneScalar o_blind(const FcmpRerandomizedOutputCompressed &rerandomized_output) +{ + HANDLE_RES_CODE(SeleneScalar, ::o_blind, &rerandomized_output); +} +//---------------------------------------------------------------------------------------------------------------------- +SeleneScalar i_blind(const FcmpRerandomizedOutputCompressed &rerandomized_output) +{ + HANDLE_RES_CODE(SeleneScalar, ::i_blind, &rerandomized_output); +} +//---------------------------------------------------------------------------------------------------------------------- +SeleneScalar i_blind_blind(const FcmpRerandomizedOutputCompressed &rerandomized_output) +{ + HANDLE_RES_CODE(SeleneScalar, ::i_blind_blind, &rerandomized_output); +} +//---------------------------------------------------------------------------------------------------------------------- +SeleneScalar c_blind(const FcmpRerandomizedOutputCompressed &rerandomized_output) +{ + HANDLE_RES_CODE(SeleneScalar, ::c_blind, &rerandomized_output); +} +//---------------------------------------------------------------------------------------------------------------------- +std::pair prove_sal(const crypto::hash &signable_tx_hash, + const crypto::secret_key &x, + const crypto::secret_key &y, + const FcmpRerandomizedOutputCompressed &rerandomized_output) +{ + FcmpPpSalProof p; + p.resize(FCMP_PP_SAL_PROOF_SIZE_V1); + + crypto::key_image L; + + const int r = ::fcmp_pp_prove_sal(to_bytes(signable_tx_hash), + to_bytes(x), + to_bytes(y), + &rerandomized_output, + &p[0], + to_bytes(L)); + + if (r < 0) + throw std::runtime_error("fcmp_pp_prove_sal failed with code: " + std::to_string(r)); + + return {std::move(p), L}; +} +//---------------------------------------------------------------------------------------------------------------------- +FcmpMembershipProof prove_membership(const std::vector &fcmp_pp_prove_inputs, + const std::size_t n_tree_layers) +{ + MAKE_TEMP_FFI_SLICE(FcmpPpProveMembershipInput, fcmp_pp_prove_inputs, fcmp_pp_prove_inputs_slice); + + FcmpPpSalProof p; + const std::size_t proof_len = membership_proof_len(fcmp_pp_prove_inputs.size(), n_tree_layers); + p.resize(proof_len); + + const int r = ::fcmp_pp_prove_membership(fcmp_pp_prove_inputs_slice, + n_tree_layers, + proof_len, + &p[0]); + + if (r < 0) + throw std::runtime_error("prove_membership failed with code: " + std::to_string(r)); + + // No `free()` since result type `()` is zero-sized + + return p; +} +//---------------------------------------------------------------------------------------------------------------------- +bool verify_sal(const crypto::hash &signable_tx_hash, + const FcmpInputCompressed &input, + const crypto::key_image &key_image, + const FcmpPpSalProof &sal_proof) +{ + if (sal_proof.size() != FCMP_PP_SAL_PROOF_SIZE_V1) + return false; + + return ::fcmp_pp_verify_sal(to_bytes(signable_tx_hash), + &input, + to_bytes(key_image), + sal_proof.data()); +} +//---------------------------------------------------------------------------------------------------------------------- +bool verify_membership(const FcmpMembershipProof &fcmp_proof, + const std::size_t n_tree_layers, + const fcmp_pp::TreeRootShared &tree_root, + const std::vector &inputs) +{ + return ::fcmp_pp_verify_membership( + {inputs.data(), inputs.size()}, + tree_root.get(), + n_tree_layers, + fcmp_proof.data(), + fcmp_proof.size()); +} +//---------------------------------------------------------------------------------------------------------------------- +bool verify(const std::vector &fcmp_pp_verify_inputs) +{ + MAKE_TEMP_FFI_SLICE(FcmpPpVerifyInput, fcmp_pp_verify_inputs, fcmp_pp_verify_inputs_slice); + return ::fcmp_pp_verify(fcmp_pp_verify_inputs_slice); +} +//---------------------------------------------------------------------------------------------------------------------- +bool verify(const crypto::hash &signable_tx_hash, + const fcmp_pp::FcmpPpProof &fcmp_pp_proof, + const std::size_t n_tree_layers, + const fcmp_pp::TreeRootShared &tree_root, + const std::vector &pseudo_outs, + const std::vector &key_images) +{ + auto fcmp_pp_verify_input = fcmp_pp::fcmp_pp_verify_input_new( + signable_tx_hash, + fcmp_pp_proof, + n_tree_layers, + tree_root, + pseudo_outs, + key_images + ); + std::vector fcmp_pp_verify_inputs; + fcmp_pp_verify_inputs.emplace_back(std::move(fcmp_pp_verify_input)); + return verify(fcmp_pp_verify_inputs); +} +//---------------------------------------------------------------------------------------------------------------------- +}//namespace fcmp_pp diff --git a/src/fcmp_pp/prove.h b/src/fcmp_pp/prove.h new file mode 100644 index 00000000000..aae77be43ee --- /dev/null +++ b/src/fcmp_pp/prove.h @@ -0,0 +1,96 @@ +// Copyright (c) 2024, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include +#include + +#include "crypto/crypto.h" +#include "crypto/hash.h" +#include "fcmp_pp_rust/fcmp++.h" +#include "fcmp_pp_types.h" + +namespace fcmp_pp +{ +//---------------------------------------------------------------------------------------------------------------------- + +FcmpRerandomizedOutputCompressed rerandomize_output(const OutputTuple output); +FcmpRerandomizedOutputCompressed rerandomize_output(const crypto::public_key &onetime_address, + const crypto::ec_point &amount_commitment, + const bool use_biased_hash_to_point); + +FcmpInputCompressed calculate_fcmp_input_for_rerandomizations(const crypto::public_key &onetime_address, + const crypto::ec_point &amount_commitment, + const bool use_biased_hash_to_point, + const crypto::secret_key &r_o, + const crypto::secret_key &r_i, + const crypto::secret_key &r_r_i, + const crypto::secret_key &r_c); + +void make_balanced_rerandomized_output_set( + const std::vector &input_onetime_addresses, + const std::vector &input_amount_commitments, + const std::vector &input_uses_biased_hash_to_point, + const std::vector &input_amount_blinding_factors, + const std::vector &r_o, + const crypto::secret_key &output_amount_blinding_factor_sum, + std::vector &rerandomized_outputs_out); + +SeleneScalar o_blind(const FcmpRerandomizedOutputCompressed &rerandomized_output); +SeleneScalar i_blind(const FcmpRerandomizedOutputCompressed &rerandomized_output); +SeleneScalar i_blind_blind(const FcmpRerandomizedOutputCompressed &rerandomized_output); +SeleneScalar c_blind(const FcmpRerandomizedOutputCompressed &rerandomized_output); + +std::pair prove_sal(const crypto::hash &signable_tx_hash, + const crypto::secret_key &x, + const crypto::secret_key &y, + const FcmpRerandomizedOutputCompressed &rerandomized_output); + +FcmpMembershipProof prove_membership(const std::vector &fcmp_pp_prove_inputs, + const std::size_t n_tree_layers); + +bool verify_sal(const crypto::hash &signable_tx_hash, + const FcmpInputCompressed &input, + const crypto::key_image &key_image, + const FcmpPpSalProof &sal_proof); + +bool verify_membership(const FcmpMembershipProof &fcmp_proof, + const std::size_t n_tree_layers, + const fcmp_pp::TreeRootShared &tree_root, + const std::vector &inputs); + +bool verify(const std::vector &fcmp_pp_verify_inputs); + +bool verify(const crypto::hash &signable_tx_hash, + const fcmp_pp::FcmpPpProof &fcmp_pp_proof, + const std::size_t n_tree_layers, + const fcmp_pp::TreeRootShared &tree_root, + const std::vector &pseudo_outs, + const std::vector &key_images); +}//namespace fcmp_pp diff --git a/src/fcmp_pp/tower_cycle.cpp b/src/fcmp_pp/tower_cycle.cpp new file mode 100644 index 00000000000..8950b43a142 --- /dev/null +++ b/src/fcmp_pp/tower_cycle.cpp @@ -0,0 +1,251 @@ +// Copyright (c) 2024, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "misc_log_ex.h" +#include "string_tools.h" +#include "tower_cycle.h" + +namespace fcmp_pp +{ +namespace tower_cycle +{ +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +#define CHECK_FFI_RES \ + CHECK_AND_ASSERT_THROW_MES(r == 0, __func__ << " failed with error code " << r); +//---------------------------------------------------------------------------------------------------------------------- +Selene::Point Selene::hash_init_point() const +{ + return ::selene_hash_init_point(); +} +//---------------------------------------------------------------------------------------------------------------------- +Helios::Point Helios::hash_init_point() const +{ + return ::helios_hash_init_point(); +} +//---------------------------------------------------------------------------------------------------------------------- +Selene::CycleScalar Selene::point_to_cycle_scalar(const Selene::Point &point) const +{ + Selene::CycleScalar helios_scalar; + int r = ::selene_point_to_helios_scalar(point, &helios_scalar); + CHECK_FFI_RES; + return helios_scalar; +} +//---------------------------------------------------------------------------------------------------------------------- +Helios::CycleScalar Helios::point_to_cycle_scalar(const Helios::Point &point) const +{ + Helios::CycleScalar selene_scalar; + int r = ::helios_point_to_selene_scalar(point, &selene_scalar); + CHECK_FFI_RES; + return selene_scalar; +} +//---------------------------------------------------------------------------------------------------------------------- +Selene::Point Selene::hash_grow( + const Selene::Point &existing_hash, + const std::size_t offset, + const Selene::Scalar &existing_child_at_offset, + const Selene::Chunk &new_children) const +{ + Selene::Point hash; + int r = ::hash_grow_selene( + existing_hash, + offset, + existing_child_at_offset, + new_children, + &hash); + CHECK_FFI_RES; + return hash; +} +//---------------------------------------------------------------------------------------------------------------------- +Helios::Point Helios::hash_grow( + const Helios::Point &existing_hash, + const std::size_t offset, + const Helios::Scalar &existing_child_at_offset, + const Helios::Chunk &new_children) const +{ + Helios::Point hash; + int r = ::hash_grow_helios( + existing_hash, + offset, + existing_child_at_offset, + new_children, + &hash); + CHECK_FFI_RES; + return hash; +} +//---------------------------------------------------------------------------------------------------------------------- +Selene::Scalar Selene::zero_scalar() const +{ + return ::selene_zero_scalar(); +} +//---------------------------------------------------------------------------------------------------------------------- +Helios::Scalar Helios::zero_scalar() const +{ + return ::helios_zero_scalar(); +} +//---------------------------------------------------------------------------------------------------------------------- +crypto::ec_scalar Selene::to_bytes(const Selene::Scalar &scalar) const +{ + crypto::ec_scalar res; + ::selene_scalar_to_bytes(&scalar, ::to_bytes(res)); + return res; +} +//---------------------------------------------------------------------------------------------------------------------- +crypto::ec_scalar Helios::to_bytes(const Helios::Scalar &scalar) const +{ + crypto::ec_scalar res; + ::helios_scalar_to_bytes(&scalar, ::to_bytes(res)); + return res; +} +//---------------------------------------------------------------------------------------------------------------------- +crypto::ec_point Selene::to_bytes(const Selene::Point &point) const +{ + crypto::ec_point res; + ::selene_point_to_bytes(&point, ::to_bytes(res)); + return res; +} +//---------------------------------------------------------------------------------------------------------------------- +crypto::ec_point Helios::to_bytes(const Helios::Point &point) const +{ + crypto::ec_point res; + ::helios_point_to_bytes(&point, ::to_bytes(res)); + return res; +} +//---------------------------------------------------------------------------------------------------------------------- +Selene::Point Selene::from_bytes(const crypto::ec_point &bytes) const +{ + Selene::Point selene_point; + int r = ::selene_point_from_bytes(reinterpret_cast(&bytes), &selene_point); + CHECK_FFI_RES; + return selene_point; +} +//---------------------------------------------------------------------------------------------------------------------- +Helios::Point Helios::from_bytes(const crypto::ec_point &bytes) const +{ + Helios::Point helios_point; + int r = ::helios_point_from_bytes(reinterpret_cast(&bytes), &helios_point); + CHECK_FFI_RES; + return helios_point; +} +//---------------------------------------------------------------------------------------------------------------------- +std::string Selene::to_string(const typename Selene::Scalar &scalar) const +{ + return epee::string_tools::pod_to_hex(this->to_bytes(scalar)); +} +//---------------------------------------------------------------------------------------------------------------------- +std::string Helios::to_string(const typename Helios::Scalar &scalar) const +{ + return epee::string_tools::pod_to_hex(this->to_bytes(scalar)); +} +//---------------------------------------------------------------------------------------------------------------------- +std::string Selene::to_string(const typename Selene::Point &point) const +{ + return epee::string_tools::pod_to_hex(this->to_bytes(point)); +} +//---------------------------------------------------------------------------------------------------------------------- +std::string Helios::to_string(const typename Helios::Point &point) const +{ + return epee::string_tools::pod_to_hex(this->to_bytes(point)); +} +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +// Exposed helper functions +//---------------------------------------------------------------------------------------------------------------------- +SeleneScalar selene_scalar_from_bytes(const crypto::ec_coord &bytes) +{ + SeleneScalar selene_scalar; + int r = ::selene_scalar_from_bytes(to_bytes(bytes), &selene_scalar); + CHECK_FFI_RES; + return selene_scalar; +} +//---------------------------------------------------------------------------------------------------------------------- +template +void extend_zeroes(const std::unique_ptr &curve, + const std::size_t num_zeroes, + std::vector &zeroes_inout) +{ + zeroes_inout.reserve(zeroes_inout.size() + num_zeroes); + + for (std::size_t i = 0; i < num_zeroes; ++i) + zeroes_inout.emplace_back(curve->zero_scalar()); +} + +// Explicit instantiations +template void extend_zeroes(const std::unique_ptr &curve, + const std::size_t num_zeroes, + std::vector &zeroes_inout); + +template void extend_zeroes(const std::unique_ptr &curve, + const std::size_t num_zeroes, + std::vector &zeroes_inout); +//---------------------------------------------------------------------------------------------------------------------- +template +void extend_scalars_from_cycle_points(const std::unique_ptr &curve, + const std::vector &points, + std::vector &scalars_out) +{ + scalars_out.reserve(scalars_out.size() + points.size()); + + for (const auto &point : points) + { + typename C_SCALARS::Scalar scalar = curve->point_to_cycle_scalar(point); + scalars_out.push_back(std::move(scalar)); + } +} + +// Explicit instantiations +template void extend_scalars_from_cycle_points(const std::unique_ptr &curve, + const std::vector &points, + std::vector &scalars_out); + +template void extend_scalars_from_cycle_points(const std::unique_ptr &curve, + const std::vector &points, + std::vector &scalars_out); +//---------------------------------------------------------------------------------------------------------------------- +template +std::vector scalar_chunks_to_chunk_vector( + const std::vector> &scalar_chunks) +{ + std::vector scalar_chunk_vector; + scalar_chunk_vector.reserve(scalar_chunks.size()); + for (const auto &scalar_chunk : scalar_chunks) + { + scalar_chunk_vector.push_back({ scalar_chunk.data(), scalar_chunk.size() }); + } + return scalar_chunk_vector; +} + +// Explicit instantiations +template std::vector scalar_chunks_to_chunk_vector( + const std::vector> &scalar_chunks); + +template std::vector scalar_chunks_to_chunk_vector( + const std::vector> &scalar_chunks); +//---------------------------------------------------------------------------------------------------------------------- +} //namespace tower_cycle +} //namespace fcmp_pp diff --git a/src/fcmp_pp/tower_cycle.h b/src/fcmp_pp/tower_cycle.h new file mode 100644 index 00000000000..8ff704fb1c5 --- /dev/null +++ b/src/fcmp_pp/tower_cycle.h @@ -0,0 +1,156 @@ +// Copyright (c) 2024, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "crypto/crypto.h" +#include "fcmp_pp_rust/fcmp++.h" +#include "fcmp_pp_types.h" + +#include + +namespace fcmp_pp +{ +namespace tower_cycle +{ +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +// Abstract parent curve class that curves in a cycle must implement +template +class Curve +{ +//member functions +public: + virtual typename C::Point hash_init_point() const = 0; + + // Read the x-coordinate from this curve's point to get this curve's cycle scalar + virtual typename C::CycleScalar point_to_cycle_scalar(const typename C::Point &point) const = 0; + + virtual typename C::Point hash_grow( + const typename C::Point &existing_hash, + const std::size_t offset, + const typename C::Scalar &existing_child_at_offset, + const typename C::Chunk &new_children) const = 0; + + virtual typename C::Scalar zero_scalar() const = 0; + + virtual crypto::ec_scalar to_bytes(const typename C::Scalar &scalar) const = 0; + virtual crypto::ec_point to_bytes(const typename C::Point &point) const = 0; + + virtual typename C::Point from_bytes(const crypto::ec_point &bytes) const = 0; + + virtual std::string to_string(const typename C::Scalar &scalar) const = 0; + virtual std::string to_string(const typename C::Point &point) const = 0; +}; +//---------------------------------------------------------------------------------------------------------------------- +class Selene final : public Curve +{ +//typedefs +public: + using Scalar = SeleneT::Scalar; + using Point = SeleneT::Point; + using Chunk = SeleneT::Chunk; + using CycleScalar = SeleneT::CycleScalar; + using ScalarChunks = SeleneT::ScalarChunks; + +//member functions +public: + Point hash_init_point() const override; + + CycleScalar point_to_cycle_scalar(const Point &point) const override; + + Point hash_grow( + const Point &existing_hash, + const std::size_t offset, + const Scalar &existing_child_at_offset, + const Chunk &new_children) const override; + + Scalar zero_scalar() const override; + + crypto::ec_scalar to_bytes(const Scalar &scalar) const override; + crypto::ec_point to_bytes(const Point &point) const override; + + Point from_bytes(const crypto::ec_point &bytes) const override; + + std::string to_string(const Scalar &scalar) const override; + std::string to_string(const Point &point) const override; +}; +//---------------------------------------------------------------------------------------------------------------------- +class Helios final : public Curve +{ +//typedefs +public: + using Scalar = HeliosT::Scalar; + using Point = HeliosT::Point; + using Chunk = HeliosT::Chunk; + using CycleScalar = HeliosT::CycleScalar; + using ScalarChunks = HeliosT::ScalarChunks; + +//member functions +public: + Point hash_init_point() const override; + + CycleScalar point_to_cycle_scalar(const Point &point) const override; + + Point hash_grow( + const Point &existing_hash, + const std::size_t offset, + const Scalar &existing_child_at_offset, + const Chunk &new_children) const override; + + Scalar zero_scalar() const override; + + crypto::ec_scalar to_bytes(const Scalar &scalar) const override; + crypto::ec_point to_bytes(const Point &point) const override; + + Point from_bytes(const crypto::ec_point &bytes) const override; + + std::string to_string(const Scalar &scalar) const override; + std::string to_string(const Point &point) const override; +}; +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +SeleneScalar selene_scalar_from_bytes(const crypto::ec_coord &bytes); +//---------------------------------------------------------------------------------------------------------------------- +template +void extend_zeroes(const std::unique_ptr &curve, + const std::size_t num_zeroes, + std::vector &zeroes_inout); +//---------------------------------------------------------------------------------------------------------------------- +template +void extend_scalars_from_cycle_points(const std::unique_ptr &curve, + const std::vector &points, + std::vector &scalars_out); +//---------------------------------------------------------------------------------------------------------------------- +template +std::vector scalar_chunks_to_chunk_vector( + const std::vector> &scalar_chunks); +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +}//namespace tower_cycle +}//namespace fcmp_pp diff --git a/src/fcmp_pp/tree_cache.cpp b/src/fcmp_pp/tree_cache.cpp new file mode 100644 index 00000000000..125316e3c5e --- /dev/null +++ b/src/fcmp_pp/tree_cache.cpp @@ -0,0 +1,1525 @@ +// Copyright (c) 2024, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "tree_cache.h" + +#include "common/merge_sorted_vectors.h" +#include "misc_log_ex.h" +#include "profile_tools.h" +#include "string_tools.h" + +#include + + +namespace fcmp_pp +{ +namespace curve_trees +{ +//---------------------------------------------------------------------------------------------------------------------- +static OutputRefHash get_output_ref_hash(const OutputPair &o_variant) +{ + const crypto::public_key &output_pubkey = output_pubkey_cref(o_variant); + const crypto::ec_point &commitment = commitment_cref(o_variant); + + static_assert(sizeof(output_pubkey) == sizeof(commitment), "unexpected size of output pubkey & commitment"); + + // Hash the type info as well + crypto::public_key type = crypto::null_pkey; + const std::size_t variant_index = o_variant.index(); + static_assert(sizeof(type) >= sizeof(variant_index), "variant index type is too large"); + memcpy(&type, &variant_index, sizeof(variant_index)); + + static constexpr std::size_t N_HASH_ELEMS = 3; + const crypto::public_key data[N_HASH_ELEMS] = { + output_pubkey, + (crypto::public_key&)commitment, + type + }; + + crypto::hash h; + crypto::cn_fast_hash(data, N_HASH_ELEMS * sizeof(crypto::public_key), h); + return h; +}; +//---------------------------------------------------------------------------------------------------------------------- +static void assign_new_output(const OutputPair &output_pair, + const LeafIdx leaf_idx, + RegisteredOutputs ®istered_outputs_inout) +{ + const auto output_ref_hash = get_output_ref_hash(output_pair); + + auto registered_output_it = registered_outputs_inout.find(output_ref_hash); + if (registered_output_it == registered_outputs_inout.end()) + return; + + // If it's already assigned a leaf idx, then it must be a duplicate and we only care about the earliest one + // TODO: test this circumstance + if (registered_output_it->second.assigned_leaf_idx) + return; + + LOG_PRINT_L1("Found output " << output_pubkey_cref(output_pair) << " at leaf idx " << leaf_idx); + + registered_output_it->second.assign_leaf(leaf_idx); + + return; +} +//---------------------------------------------------------------------------------------------------------------------- +static uint64_t add_to_locked_outputs_cache(const OutsByLastLockedBlock &outs_by_last_locked_block, + const CreatedBlockIdx created_block_idx, + LockedOutsByLastLockedBlock &locked_outputs_inout, + LockedOutputsByCreated &locked_outputs_refs_inout) +{ + uint64_t n_outputs_added = 0; + + LockedOutputRefHashes locked_output_ref_hashes; + for (const auto &last_locked_block : outs_by_last_locked_block) + { + const LastLockedBlockIdx last_locked_block_idx = last_locked_block.first; + CHECK_AND_ASSERT_THROW_MES(last_locked_block_idx > created_block_idx, "last locked block idx should be > created block"); + const auto &new_locked_outputs = last_locked_block.second; + + // We keep track of the number outputs we're adding to the cache at a specific last locked block, so that we can + // quickly remove those outputs from the cache upon popping a block. + const auto n_new_outputs = new_locked_outputs.size(); + locked_output_ref_hashes[last_locked_block_idx] = n_new_outputs; + + n_outputs_added += n_new_outputs; + + // Add to locked outputs cache by last locked block, so we can use them to grow the tree upon unlock. + auto locked_outputs_it = locked_outputs_inout.find(last_locked_block_idx); + if (locked_outputs_it == locked_outputs_inout.end()) + { + locked_outputs_inout[last_locked_block_idx] = new_locked_outputs; + continue; + } + + // Merge existing sorted locked outputs with new sorted locked outputs + const auto &locked_outputs = locked_outputs_it->second; + std::vector all_locked_outputs; + const auto is_less = [](const UnifiedOutput &a, const UnifiedOutput &b) { return a.unified_id < b.unified_id; }; + bool r = tools::merge_sorted_vectors(locked_outputs, new_locked_outputs, is_less, all_locked_outputs); + CHECK_AND_ASSERT_THROW_MES(r, "failed to merge sorted locked outputs"); + + locked_outputs_inout[last_locked_block_idx] = std::move(all_locked_outputs); + } + + // This is keeping track of locked output refs in the locked outputs cache by their created block. We use this to + // quickly remove locked outputs form the cache cache upon popping the block from the chain. + CHECK_AND_ASSERT_THROW_MES(locked_outputs_refs_inout.find(created_block_idx) == locked_outputs_refs_inout.end(), + "unexpected locked output refs found"); + locked_outputs_refs_inout[created_block_idx] = std::move(locked_output_ref_hashes); + + return n_outputs_added; +} +//---------------------------------------------------------------------------------------------------------------------- +static uint64_t remove_outputs_created_at_block(const CreatedBlockIdx &created_block_idx, + LockedOutsByLastLockedBlock &locked_outputs_inout, + LockedOutputsByCreated &locked_outputs_refs_inout) +{ + uint64_t n_outputs_removed = 0; + + // Get the outputs created at the provided creation block + auto locked_output_ref_hashes_it = locked_outputs_refs_inout.find(created_block_idx); + CHECK_AND_ASSERT_THROW_MES(locked_output_ref_hashes_it != locked_outputs_refs_inout.end(), "missing locked output refs"); + + for (const auto &locked_output_ref_hashes : locked_output_ref_hashes_it->second) + { + // The outputs are grouped by last locked block + const LastLockedBlockIdx last_locked_block_idx = locked_output_ref_hashes.first; + const NumOutputs n_outputs_to_remove = locked_output_ref_hashes.second; + + // Find the locked outputs using the last locked block + const auto locked_outputs_it = locked_outputs_inout.find(last_locked_block_idx); + CHECK_AND_ASSERT_THROW_MES(locked_outputs_it != locked_outputs_inout.end(), "missing locked outputs"); + + const NumOutputs n_cur_outputs = locked_outputs_it->second.size(); + CHECK_AND_ASSERT_THROW_MES(n_cur_outputs >= n_outputs_to_remove, "unexpected n locked outputs"); + + // We're removing the number of outputs we originally added upon creation in add_to_locked_outputs_cache + n_outputs_removed += n_outputs_to_remove; + + // Now remove those outputs from the locked outputs cache + if (n_cur_outputs == n_outputs_to_remove) + { + locked_outputs_inout.erase(locked_outputs_it); + continue; + } + + const uint64_t n_new_outputs = n_cur_outputs - n_outputs_to_remove; + locked_outputs_it->second.erase( + locked_outputs_it->second.begin() + n_new_outputs, + locked_outputs_it->second.end() + ); + } + + // Don't need the refs anymore, we're done with the outputs created at the given block + locked_outputs_refs_inout.erase(locked_output_ref_hashes_it); + + return n_outputs_removed; +} +//---------------------------------------------------------------------------------------------------------------------- +template +static void cache_leaf_chunk(const ChildChunkIdx chunk_idx, + const std::size_t leaf_parent_chunk_width, + const ContiguousLeaves &leaves, + const LeafIdx start_leaf_tuple_idx, + const uint64_t n_leaf_tuples, + const bool bump_ref_count, + LeafCache &leaf_cache_inout) +{ + if (n_leaf_tuples == 0) + return; + + const LeafIdx start_leaf_idx = chunk_idx * leaf_parent_chunk_width; + const LeafIdx end_leaf_idx = std::min(start_leaf_idx + leaf_parent_chunk_width, n_leaf_tuples); + + CHECK_AND_ASSERT_THROW_MES(end_leaf_idx > start_leaf_idx, "start_leaf_idx is too high"); + + MTRACE("Caching leaves at chunk_idx: " << chunk_idx + << " , start_leaf_idx: " << start_leaf_idx + << " , end_leaf_idx: " << end_leaf_idx + << " , bump_ref_count: " << bump_ref_count + << " , start_leaf_tuple_idx: " << start_leaf_tuple_idx); + + // If the leaf's chunk isn't present in this leaf extension, there are no new leaves we need to cache + if (start_leaf_tuple_idx >= end_leaf_idx) + return; + + // Check if the leaf's chunk is already cached + uint64_t cached_chunk_size = 0; + auto leaf_chunk_it = leaf_cache_inout.find(chunk_idx); + const bool cache_hit = leaf_chunk_it != leaf_cache_inout.end(); + if (cache_hit) + { + if (bump_ref_count) + leaf_chunk_it->second.ref_count += 1; + + cached_chunk_size = (uint64_t) leaf_chunk_it->second.leaves.size(); + } + + // Add the *new* elems in the chunk to the cache + const ChildChunkIdx start_leaf_idx_offset = start_leaf_idx + cached_chunk_size; + + // If we already have all the latest leaves, we're done, we've already bumped the ref count if needed + if (start_leaf_idx_offset == end_leaf_idx) + return; + CHECK_AND_ASSERT_THROW_MES(end_leaf_idx > start_leaf_idx_offset, "high start_leaf_idx_offset comp to end_leaf_idx"); + + CHECK_AND_ASSERT_THROW_MES(start_leaf_idx_offset >= leaves.start_leaf_tuple_idx, "high start_leaf_idx_offset"); + const ChildChunkIdx start_i = start_leaf_idx_offset - leaves.start_leaf_tuple_idx; + const ChildChunkIdx end_i = end_leaf_idx - leaves.start_leaf_tuple_idx; + CHECK_AND_ASSERT_THROW_MES(leaves.tuples.size() >= end_i, "high end_i"); + + std::vector new_leaves; + for (LeafIdx i = start_i; i < end_i; ++i) + { + const auto &output_pair = leaves.tuples[i].output_pair; + if (cache_hit) + leaf_chunk_it->second.leaves.push_back(output_pair); + else + new_leaves.push_back(output_pair); + } + + // Add to the cache + if (!cache_hit) + leaf_cache_inout[chunk_idx] = CachedLeafChunk { .leaves = std::move(new_leaves), .ref_count = 1 }; +} +//---------------------------------------------------------------------------------------------------------------------- +// TODO: fewer params here? +template +static void cache_path_chunk(const std::unique_ptr &curve, + const std::size_t parent_width, + const LayerExtension &layer_ext, + const LayerIdx layer_idx, + const bool bump_ref_count, + const ChildChunkIdx parent_idx, + const uint64_t n_layer_elems, + TreeElemCache &cached_tree_elems_inout) +{ + CHECK_AND_ASSERT_THROW_MES(!layer_ext.hashes.empty(), "empty layer ext"); + + const ChildChunkIdx start_chunk_idx = parent_idx * parent_width; + const ChildChunkIdx end_chunk_idx = std::min(start_chunk_idx + parent_width, n_layer_elems); + CHECK_AND_ASSERT_THROW_MES(end_chunk_idx > start_chunk_idx, "end_chunk_idx is too low"); + + MTRACE("Caching path elems at layer_idx: " << layer_idx + << " , parent_idx: " << parent_idx + << " , start_chunk_idx: " << start_chunk_idx + << " , end_chunk_idx: " << end_chunk_idx + << " , bump_ref_count: " << bump_ref_count + << " , n_layer_elems: " << n_layer_elems + << " , layer_ext.start_idx: " << layer_ext.start_idx); + + // Check if the layer is already cached + auto cached_layer_it = cached_tree_elems_inout.find(layer_idx); + const bool layer_cache_hit = cached_layer_it != cached_tree_elems_inout.end(); + + // Check if the path chunk is already cached + bool cache_hit = false; + uint64_t cached_chunk_size = 0; + ChildChunkCache::iterator cached_chunk_it; + if (layer_cache_hit) + { + cached_chunk_it = cached_layer_it->second.find(parent_idx); + cache_hit = cached_chunk_it != cached_layer_it->second.end(); + + if (cache_hit) + { + if (bump_ref_count) + cached_chunk_it->second.ref_count += 1; + + cached_chunk_size = (uint64_t) cached_chunk_it->second.tree_elems.size(); + } + } + + MTRACE("layer_cache_hit: " << layer_cache_hit + << " , cache_hit: " << cache_hit + << " , cached_chunk_size: " << cached_chunk_size); + + // Add the *new* elems in the chunk to the cache + const ChildChunkIdx start_idx_offset = start_chunk_idx + cached_chunk_size; + + // If we already have all the latest elems, we're done, we've already bumped the ref count if needed + if (start_idx_offset == end_chunk_idx) + return; + CHECK_AND_ASSERT_THROW_MES(end_chunk_idx > start_idx_offset, "high start_idx_offset comp to end_chunk_idx"); + + CHECK_AND_ASSERT_THROW_MES(start_idx_offset >= layer_ext.start_idx, "high start_idx_offset"); + const ChildChunkIdx start_i = start_idx_offset - layer_ext.start_idx; + const ChildChunkIdx end_i = end_chunk_idx - layer_ext.start_idx; + CHECK_AND_ASSERT_THROW_MES(layer_ext.hashes.size() >= end_i, "high end_i"); + + // Collect the new elems into cache + std::vector new_elems; + for (ChildChunkIdx i = start_i; i < end_i; ++i) + { + const auto tree_elem_bytes = curve->to_bytes(layer_ext.hashes[i]); + if (cache_hit) + cached_chunk_it->second.tree_elems.push_back(tree_elem_bytes); + else + new_elems.push_back(tree_elem_bytes); + } + + // If no cache hit, add collected chunk to the cache + if (!layer_cache_hit) + { + cached_tree_elems_inout[layer_idx] = {{ parent_idx, CachedTreeElemChunk{ + .tree_elems = std::move(new_elems), + .ref_count = 1, + }}}; + } + else if (!cache_hit) + { + cached_tree_elems_inout[layer_idx][parent_idx] = CachedTreeElemChunk{ + .tree_elems = std::move(new_elems), + .ref_count = 1, + }; + } +} +//---------------------------------------------------------------------------------------------------------------------- +static ChildChunkCache::const_iterator read_child_chunk(const std::size_t layer_idx, + const ChildChunkIdx child_chunk_idx, + const TreeElemCache &tree_elem_cache) +{ + MTRACE("Reading cached layer " << layer_idx << " and child chunk idx " << child_chunk_idx); + + const auto layer_it = tree_elem_cache.find(layer_idx); + CHECK_AND_ASSERT_THROW_MES(layer_it != tree_elem_cache.end(), "missing cached layer"); + + const auto child_chunk_it = layer_it->second.find(child_chunk_idx); + CHECK_AND_ASSERT_THROW_MES(child_chunk_it != layer_it->second.end(), "missing cached child chunk"); + + CHECK_AND_ASSERT_THROW_MES(!child_chunk_it->second.tree_elems.empty(), "empty child chunk cache"); + return child_chunk_it; +} +//---------------------------------------------------------------------------------------------------------------------- +static ChildChunkCache::iterator get_child_chunk_it(const std::size_t layer_idx, + const ChildChunkIdx child_chunk_idx, + TreeElemCache &tree_elem_cache) +{ + MTRACE("Reading cached layer " << layer_idx << " and child chunk idx " << child_chunk_idx); + + auto layer_it = tree_elem_cache.find(layer_idx); + CHECK_AND_ASSERT_THROW_MES(layer_it != tree_elem_cache.end(), "missing cached layer"); + + auto child_chunk_it = layer_it->second.find(child_chunk_idx); + CHECK_AND_ASSERT_THROW_MES(child_chunk_it != layer_it->second.end(), "missing cached child chunk"); + + CHECK_AND_ASSERT_THROW_MES(!child_chunk_it->second.tree_elems.empty(), "empty child chunk cache"); + return child_chunk_it; +} +//---------------------------------------------------------------------------------------------------------------------- +template +static void update_last_hash(const std::unique_ptr &curve, + const std::vector> &layer_exts, + const std::size_t layer_ext_idx, + const LayerIdx layer_idx, + const ChildChunkIdx last_parent_idx, + TreeElemCache &cached_tree_elems_inout) +{ + CHECK_AND_ASSERT_THROW_MES(layer_exts.size() > layer_ext_idx, "high layer_ext_idx"); + auto &layer_ext = layer_exts[layer_ext_idx]; + + if (!layer_ext.update_existing_last_hash) + return; + CHECK_AND_ASSERT_THROW_MES(!layer_ext.hashes.empty(), "empty layer ext"); + + auto cached_chunk_it = get_child_chunk_it(layer_idx, last_parent_idx, cached_tree_elems_inout); + cached_chunk_it->second.tree_elems.back() = curve->to_bytes(layer_ext.hashes.front()); +} +//---------------------------------------------------------------------------------------------------------------------- +template +static void cache_path_chunks(const LeafIdx leaf_idx, + const std::shared_ptr> &curve_trees, + const std::vector> &c1_layer_exts, + const std::vector> &c2_layer_exts, + const uint64_t start_leaf_tuple_idx, + const uint64_t n_leaf_tuples, + const bool bump_ref_count, + TreeElemCache &tree_elem_cache_inout) +{ + if (n_leaf_tuples == 0) + return; + if (n_leaf_tuples == start_leaf_tuple_idx) + return; + + CHECK_AND_ASSERT_THROW_MES(n_leaf_tuples > leaf_idx, "high leaf_idx"); + + // Get the child chunk indexes of the leaf for each layer + const auto child_chunk_idxs = curve_trees->get_child_chunk_indexes(n_leaf_tuples, leaf_idx); + const auto n_elems_per_layer = curve_trees->n_elems_per_layer(n_leaf_tuples); + const std::size_t n_layers = n_elems_per_layer.size(); + CHECK_AND_ASSERT_THROW_MES(child_chunk_idxs.size() == (n_layers + 1), "unexpected n child chunk idxs"); + + // start_leaf_tuple_idx should be the same as old_n_leaf_tuples + const std::size_t old_n_layers = curve_trees->n_layers(start_leaf_tuple_idx); + + std::size_t c1_idx = 0, c2_idx = 0; + for (LayerIdx layer_idx = 0; layer_idx < n_layers; ++layer_idx) + { + const ChildChunkIdx parent_idx = child_chunk_idxs[layer_idx + 1]; + MTRACE("Caching tree elems from layer_idx " << layer_idx << " parent_idx " << parent_idx); + + // We need to keep track of newly added chunks always. E.g. assume the tree grows and a new root is added. + // We would then need to add a ref to the new root for every registered and assigned leaf. + const bool is_new_chunk = layer_idx >= old_n_layers; + const bool bump_chunk_ref_count = bump_ref_count || is_new_chunk; + + if (c1_idx == c2_idx /*c2 parent*/) + { + cache_path_chunk(curve_trees->m_c1, + curve_trees->m_c2_width, + c1_layer_exts.at(c1_idx), + layer_idx, + bump_chunk_ref_count, + parent_idx, + n_elems_per_layer[layer_idx], + tree_elem_cache_inout + ); + + ++c1_idx; + } + else + { + cache_path_chunk(curve_trees->m_c2, + curve_trees->m_c1_width, + c2_layer_exts.at(c2_idx), + layer_idx, + bump_chunk_ref_count, + parent_idx, + n_elems_per_layer[layer_idx], + tree_elem_cache_inout + ); + + ++c2_idx; + } + } +} +//---------------------------------------------------------------------------------------------------------------------- +template +static void update_existing_last_hashes(const std::shared_ptr> &curve_trees, + const typename CurveTrees::TreeExtension &tree_extension, + TreeElemCache &tree_elem_cache_inout) +{ + const uint64_t old_n_leaf_tuples = tree_extension.leaves.start_leaf_tuple_idx; + if (old_n_leaf_tuples == 0) + return; + + const auto &c1_layer_exts = tree_extension.c1_layer_extensions; + const auto &c2_layer_exts = tree_extension.c2_layer_extensions; + + // Get the child chunk indexes of the last leaf for each layer + const uint64_t last_leaf_idx = old_n_leaf_tuples - 1; + const auto child_chunk_idxs = curve_trees->get_child_chunk_indexes(old_n_leaf_tuples, last_leaf_idx); + const std::size_t n_layers = curve_trees->n_layers(old_n_leaf_tuples); + CHECK_AND_ASSERT_THROW_MES(child_chunk_idxs.size() == (n_layers + 1), "unexpected n child chunk idxs"); + + std::size_t c1_idx = 0, c2_idx = 0; + for (LayerIdx layer_idx = 0; layer_idx < n_layers; ++layer_idx) + { + const ChildChunkIdx last_parent_idx = child_chunk_idxs[layer_idx + 1]; + MTRACE("Updating existing last hash from layer_idx " << layer_idx << " last_parent_idx " << last_parent_idx); + + if (c1_idx == c2_idx /*c2 parent*/) + { + update_last_hash(curve_trees->m_c1, + c1_layer_exts, + c1_idx, + layer_idx, + last_parent_idx, + tree_elem_cache_inout + ); + + ++c1_idx; + } + else + { + update_last_hash(curve_trees->m_c2, + c2_layer_exts, + c2_idx, + layer_idx, + last_parent_idx, + tree_elem_cache_inout + ); + + ++c2_idx; + } + } +} +//---------------------------------------------------------------------------------------------------------------------- +static void remove_leaf_chunk_ref(const ChildChunkIdx chunk_idx, LeafCache &leaf_cache_inout) +{ + auto leaf_chunk_it = leaf_cache_inout.find(chunk_idx); + CHECK_AND_ASSERT_THROW_MES(leaf_chunk_it != leaf_cache_inout.end(), "cache is missing leaf chunk"); + CHECK_AND_ASSERT_THROW_MES(leaf_chunk_it->second.ref_count != 0, "leaf chunk has 0 ref count"); + + leaf_chunk_it->second.ref_count -= 1; + MTRACE("Removing leaf chunk " << chunk_idx << " , updated ref count: " << leaf_chunk_it->second.ref_count); + + // If the ref count is 0, garbage collect it + if (leaf_chunk_it->second.ref_count == 0) + leaf_cache_inout.erase(leaf_chunk_it); +} +//---------------------------------------------------------------------------------------------------------------------- +static void remove_path_chunk_ref(const LayerIdx layer_idx, + const ChildChunkIdx chunk_idx, + TreeElemCache &tree_elem_cache_inout) +{ + // Get the layer + auto cache_layer_it = tree_elem_cache_inout.find(layer_idx); + CHECK_AND_ASSERT_THROW_MES(cache_layer_it != tree_elem_cache_inout.end(), "layer " << layer_idx << " is missing"); + + // Get the chunk + auto cache_chunk_it = cache_layer_it->second.find(chunk_idx); + CHECK_AND_ASSERT_THROW_MES(cache_chunk_it != cache_layer_it->second.end(), + "chunk " << chunk_idx << " is missing from layer " << layer_idx); + CHECK_AND_ASSERT_THROW_MES(cache_chunk_it->second.ref_count != 0, + "chunk " << chunk_idx << " from layer " << layer_idx << " has 0 ref count"); + + cache_chunk_it->second.ref_count -= 1; + MTRACE("Removing ref to chunk " << chunk_idx << " in layer " << layer_idx + << " , updated ref count: " << cache_chunk_it->second.ref_count); + + // If the chunk's ref count is 0, garbage collect it + if (cache_chunk_it->second.ref_count == 0) + { + MDEBUG("Removing ref to chunk " << chunk_idx << " from layer " << layer_idx); + cache_layer_it->second.erase(cache_chunk_it); + } + + // If the layer is empty, garbage collect it + if (cache_layer_it->second.empty()) + { + MDEBUG("Removing layer " << layer_idx); + tree_elem_cache_inout.erase(cache_layer_it); + } +} +//---------------------------------------------------------------------------------------------------------------------- +template +static void remove_path_chunks_refs(const LeafIdx leaf_idx, + const std::shared_ptr> &curve_trees, + const uint64_t n_leaf_tuples, + TreeElemCache &tree_elem_cache_inout) +{ + if (n_leaf_tuples == 0) + return; + + const auto child_chunk_idxs = curve_trees->get_child_chunk_indexes(n_leaf_tuples, leaf_idx); + const std::size_t n_layers = curve_trees->n_layers(n_leaf_tuples); + CHECK_AND_ASSERT_THROW_MES(child_chunk_idxs.size() == (n_layers + 1), "unexpected n child chunk idxs"); + + for (LayerIdx layer_idx = 0; layer_idx < n_layers; ++layer_idx) + { + const ChildChunkIdx parent_idx = child_chunk_idxs[layer_idx + 1]; + remove_path_chunk_ref(layer_idx, parent_idx, tree_elem_cache_inout); + } +} +//---------------------------------------------------------------------------------------------------------------------- +static void shrink_cached_last_leaf_chunk(const uint64_t new_n_leaf_tuples, + const std::size_t leaf_parent_chunk_width, + LeafCache &leaf_cache_inout) +{ + // If the offset is 0, the last chunk is full and we're supposed to keep all elems in it + const std::size_t offset = new_n_leaf_tuples % leaf_parent_chunk_width; + if (offset == 0) + return; + + const LeafIdx last_leaf_idx = new_n_leaf_tuples - 1; + const ChildChunkIdx chunk_idx = last_leaf_idx / leaf_parent_chunk_width; + + auto leaf_chunk_it = leaf_cache_inout.find(chunk_idx); + CHECK_AND_ASSERT_THROW_MES(leaf_chunk_it != leaf_cache_inout.end(), "cache is missing leaf chunk to shrink"); + + // The last chunk should have at least offset leaves + const std::size_t n_leaves_last_chunk = leaf_chunk_it->second.leaves.size(); + CHECK_AND_ASSERT_THROW_MES(n_leaves_last_chunk >= offset, "unexpected n leaves in cached last chunk"); + + leaf_chunk_it->second.leaves.erase( + leaf_chunk_it->second.leaves.begin() + offset, + leaf_chunk_it->second.leaves.end() + ); +} +//---------------------------------------------------------------------------------------------------------------------- +template +static void reduce_cached_last_chunks(const uint64_t new_n_leaf_tuples, + const std::vector &new_tree_edge, + const std::shared_ptr> &curve_trees, + TreeElemCache &tree_elem_cache_inout) +{ + if (new_n_leaf_tuples == 0) + return; + + // Get child chunk indexes and layer meta + const LeafIdx last_leaf_idx = new_n_leaf_tuples - 1; + const auto child_chunk_idxs = curve_trees->get_child_chunk_indexes(new_n_leaf_tuples, last_leaf_idx); + const auto n_elems_per_layer = curve_trees->n_elems_per_layer(new_n_leaf_tuples); + const std::size_t n_layers = n_elems_per_layer.size(); + CHECK_AND_ASSERT_THROW_MES(child_chunk_idxs.size() == (n_layers + 1), "unexpected n child chunk idxs"); + CHECK_AND_ASSERT_THROW_MES(new_tree_edge.size() == n_layers, "unexpected tree edge size"); + + bool parent_is_c2 = true; + for (LayerIdx layer_idx = 0; layer_idx < n_layers; ++layer_idx) + { + const ChildChunkIdx parent_idx = child_chunk_idxs[layer_idx + 1]; + auto cached_chunk_it = get_child_chunk_it(layer_idx, parent_idx, tree_elem_cache_inout); + + // Shrink the chunk to the expected size + const uint64_t n_layer_elems = n_elems_per_layer[layer_idx]; + const std::size_t parent_width = parent_is_c2 ? curve_trees->m_c2_width : curve_trees->m_c1_width; + const std::size_t chunk_offset = n_layer_elems % parent_width; + const std::size_t new_chunk_size = chunk_offset == 0 ? parent_width : chunk_offset; + CHECK_AND_ASSERT_THROW_MES(new_chunk_size > 0, "new_chunk_size is too small"); + + MTRACE("Reducing cached last chunk in layer_idx: " << layer_idx + << " , parent_idx: " << parent_idx + << " , n_layer_elems: " << n_layer_elems + << " , new_chunk_size: " << new_chunk_size); + + CHECK_AND_ASSERT_THROW_MES(cached_chunk_it->second.tree_elems.size() >= new_chunk_size, "chunk is too small"); + cached_chunk_it->second.tree_elems.resize(new_chunk_size); + + // Update the last hash in the chunk + cached_chunk_it->second.tree_elems.back() = new_tree_edge[layer_idx]; + + parent_is_c2 = !parent_is_c2; + } +} +//---------------------------------------------------------------------------------------------------------------------- +template +static void update_registered_path(const std::shared_ptr> &curve_trees, + const LeafIdx leaf_idx, + const typename CurveTrees::TreeExtension &tree_extension, + const LeafIdx start_leaf_tuple_idx, + const uint64_t n_leaf_tuples, + LeafCache &leaf_cache_inout, + TreeElemCache &tree_elem_cach_inout) +{ + if (n_leaf_tuples == 0) + return; + + // We only need to bump the ref count on this registered output's leaf chunk if it was just included in the tree + const bool bump_ref_count = leaf_idx >= start_leaf_tuple_idx && leaf_idx < n_leaf_tuples; + + // Cache registered leaf's chunk + cache_leaf_chunk(leaf_idx / curve_trees->m_c1_width, + curve_trees->m_c1_width, + tree_extension.leaves, + start_leaf_tuple_idx, + n_leaf_tuples, + bump_ref_count, + leaf_cache_inout); + + // Now cache the rest of the path elems for each registered output + cache_path_chunks(leaf_idx, + curve_trees, + tree_extension.c1_layer_extensions, + tree_extension.c2_layer_extensions, + start_leaf_tuple_idx, + n_leaf_tuples, + bump_ref_count, + tree_elem_cach_inout); +} +//---------------------------------------------------------------------------------------------------------------------- +template +static void cache_last_chunk_leaves(const std::shared_ptr> &curve_trees, + const ContiguousLeaves &leaves, + const LeafIdx start_leaf_tuple_idx, + const uint64_t n_leaf_tuples, + LeafCache &leaf_cache_inout) +{ + if (n_leaf_tuples == 0) + return; + + const LeafIdx last_leaf_idx = n_leaf_tuples - 1; + const ChildChunkIdx chunk_idx = last_leaf_idx / curve_trees->m_c1_width; + + // Always bump the ref count for last chunk of leaves so that it sticks around until pruned + const bool bump_ref_count = true; + + cache_leaf_chunk(chunk_idx, + curve_trees->m_c1_width, + leaves, + start_leaf_tuple_idx, + n_leaf_tuples, + bump_ref_count, + leaf_cache_inout); +} +//---------------------------------------------------------------------------------------------------------------------- +template +static void cache_last_chunks(const std::shared_ptr> &curve_trees, + const typename CurveTrees::TreeExtension &tree_extension, + const LeafIdx start_leaf_tuple_idx, + const uint64_t n_leaf_tuples, + TreeElemCache &tree_elem_cache_inout) +{ + if (n_leaf_tuples == 0) + return; + + const LeafIdx last_leaf_idx = n_leaf_tuples - 1; + + // Always bump the ref count for last chunk of hashes so that it sticks around until pruned + const bool bump_ref_count = true; + + cache_path_chunks(last_leaf_idx, + curve_trees, + tree_extension.c1_layer_extensions, + tree_extension.c2_layer_extensions, + start_leaf_tuple_idx, + n_leaf_tuples, + bump_ref_count, + tree_elem_cache_inout); +} +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +template +bool TreeCache::register_output(const OutputPair &output) +{ + auto output_ref_hash = get_output_ref_hash(output); + CHECK_AND_ASSERT_MES(m_registered_outputs.find(output_ref_hash) == m_registered_outputs.end(), false, + "output is already registered"); + + // Add to registered outputs container + m_registered_outputs.insert({ output_ref_hash, AssignedLeafIdx{} }); + + MDEBUG("Registered output " << fcmp_pp::output_pubkey_cref(output) + << " , commitment " << fcmp_pp::commitment_cref(output) + << " , type: " << output.index() + << " , output ref: " << output_ref_hash); + + return true; +} + +// Explicit instantiation +template bool TreeCache::register_output(const OutputPair &output); +//---------------------------------------------------------------------------------------------------------------------- +template +void TreeCache::sync_block(const uint64_t block_idx, + const crypto::hash &block_hash, + const crypto::hash &prev_block_hash, + const fcmp_pp::OutsByLastLockedBlock &outs_by_last_locked_block) +{ + const std::vector new_block_hashes{block_hash}; + const std::vector outs{outs_by_last_locked_block}; + + CacheStateChange cache_state_change; + + this->prepare_to_grow_cache(block_idx, + prev_block_hash, + new_block_hashes, + outs, + cache_state_change); + + this->grow_cache(block_idx, new_block_hashes, std::move(cache_state_change)); +} + +// Explicit instantiation +template void TreeCache::sync_block(const uint64_t block_idx, + const crypto::hash &block_hash, + const crypto::hash &prev_block_hash, + const fcmp_pp::OutsByLastLockedBlock &outs_by_last_locked_block); +//---------------------------------------------------------------------------------------------------------------------- +template +void TreeCache::prepare_to_grow_cache(const uint64_t start_block_idx, + const crypto::hash &prev_block_hash, + const std::vector &new_block_hashes, + const std::vector &outs_by_last_locked_blocks, + CacheStateChange &cache_state_change_out) const +{ + CHECK_AND_ASSERT_THROW_MES(new_block_hashes.size() == outs_by_last_locked_blocks.size(), "size mismatch sync_blocks"); + + cache_state_change_out = {}; + + const uint64_t n_new_blocks = (uint64_t) new_block_hashes.size(); + if (n_new_blocks == 0) + return; + + // Pre-checks + if (m_cached_blocks.empty()) + { + CHECK_AND_ASSERT_THROW_MES(start_block_idx == 0, "must init before sync_blocks"); + CHECK_AND_ASSERT_THROW_MES(prev_block_hash == crypto::null_hash, "expected null prev last hash"); + + // Make sure all blockchain containers are empty + CHECK_AND_ASSERT_THROW_MES(m_cached_blocks.empty(), "expected empty cached blocks"); + CHECK_AND_ASSERT_THROW_MES(m_leaf_cache.empty(), "expected empty cached leaves"); + CHECK_AND_ASSERT_THROW_MES(m_tree_elem_cache.empty(), "expected empty cached tree elems"); + } + else + { + // Make sure provided block is contiguous to prior synced block + const auto &prev_block = m_cached_blocks.back(); + CHECK_AND_ASSERT_THROW_MES((prev_block.blk_idx + 1) == start_block_idx, "failed contiguity idx check"); + CHECK_AND_ASSERT_THROW_MES(prev_block.blk_hash == prev_block_hash, "failed contiguity hash check"); + } + + // Copy the cache's locked outputs and locked output refs so that this function makes no modifications to + // the existing cache, and its results can be used to update the cache + // TODO: return state diff instead of copying the whole thing + TIME_MEASURE_START(getting_unlocked_outputs); + cache_state_change_out.locked_outputs = m_locked_outputs; + cache_state_change_out.locked_output_ref_hashes = m_locked_output_ref_hashes; + + // Update the locked outputs cache with all outputs set to unlock, and collect unlocked outputs and output id's + std::vector> unlocked_outputs; + std::vector> unlocked_unified_ids_by_block; + unlocked_outputs.reserve(n_new_blocks); + unlocked_unified_ids_by_block.reserve(n_new_blocks); + uint64_t n_unlocked_outputs = 0; + for (uint64_t i = 0; i < n_new_blocks; ++i) + { + const BlockIdx blk_idx = start_block_idx + i; + + cache_state_change_out.n_outputs_observed += add_to_locked_outputs_cache(outs_by_last_locked_blocks[i], + blk_idx, + cache_state_change_out.locked_outputs, + cache_state_change_out.locked_output_ref_hashes + ); + + // Copy the unlocked outputs in the block. The reason we copy here is to make sure we handle reorgs correctly. + // We don't need to re-add locked outputs back to the cache upon popping a block this way. + auto unlocked_outputs_in_blk = cache_state_change_out.locked_outputs[blk_idx]; + const std::size_t n_new_unlocked_outputs = unlocked_outputs_in_blk.size(); + + n_unlocked_outputs += n_new_unlocked_outputs; + + // Collect unlock output id's by block + std::vector new_unlocked_unified_ids; + new_unlocked_unified_ids.reserve(n_new_unlocked_outputs); + for (const auto &unlocked_output : unlocked_outputs_in_blk) + new_unlocked_unified_ids.push_back(unlocked_output.unified_id); + + unlocked_outputs.emplace_back(std::move(unlocked_outputs_in_blk)); + unlocked_unified_ids_by_block.emplace_back(std::move(new_unlocked_unified_ids)); + } + TIME_MEASURE_FINISH(getting_unlocked_outputs); + + TIME_MEASURE_START(getting_tree_extension); + // Get the tree extension using existing tree data. We'll use the tree extension to update registered output paths + // in the tree and cache the data necessary to either build the next block's tree extension or pop the block. + cache_state_change_out.tree_extension = TreeSync::m_curve_trees->get_tree_extension( + this->get_n_leaf_tuples(), + this->get_last_hashes(), + std::move(unlocked_outputs), + true/*use_fast_torsion_check*/); + + CHECK_AND_ASSERT_THROW_MES(n_unlocked_outputs >= cache_state_change_out.tree_extension.leaves.tuples.size(), + "unexpected new n tuples"); + + TIME_MEASURE_FINISH(getting_tree_extension); + + // Read the tree extension and determine n leaf tuples added per block + cache_state_change_out.n_new_leaf_tuples_per_block.reserve(n_new_blocks); + auto new_leaf_tuple_it = cache_state_change_out.tree_extension.leaves.tuples.begin(); + for (uint64_t i = 0; i < n_new_blocks; ++i) + { + uint64_t n_leaf_tuples_in_block = 0; + + const auto &unlocked_unified_ids = unlocked_unified_ids_by_block[i]; + for (const uint64_t unified_id : unlocked_unified_ids) + { + // This expects the unlocked outputs in a block to be inserted to the tree in sorted order + if (unified_id == new_leaf_tuple_it->unified_id) + { + ++n_leaf_tuples_in_block; + ++new_leaf_tuple_it; + } + } + + cache_state_change_out.n_new_leaf_tuples_per_block.push_back(n_leaf_tuples_in_block); + } + + CHECK_AND_ASSERT_THROW_MES(new_leaf_tuple_it == cache_state_change_out.tree_extension.leaves.tuples.end(), + "did not reach all leaf tuples"); + + m_getting_unlocked_outs_ms += getting_unlocked_outputs; + m_getting_tree_extension_ms += getting_tree_extension; + + LOG_PRINT_L1("Total time getting unlocked outs: " << m_getting_unlocked_outs_ms / 1000 + << " , getting tree extension: " << m_getting_tree_extension_ms / 1000); +} + +// Explicit instantiation +template void TreeCache::prepare_to_grow_cache(const uint64_t start_block_idx, + const crypto::hash &prev_block_hash, + const std::vector &new_block_hashes, + const std::vector &outs_by_last_locked_blocks, + CacheStateChange &cache_state_change_out) const; +//---------------------------------------------------------------------------------------------------------------------- +template +void TreeCache::grow_cache(const uint64_t start_block_idx, + const std::vector &new_block_hashes, + CacheStateChange &&cache_state_change, + const bool skip_shrink_to_reorg_depth) +{ + // Pre-checks + CHECK_AND_ASSERT_THROW_MES(new_block_hashes.size() == cache_state_change.n_new_leaf_tuples_per_block.size(), + "size mismatch new block hashes <> n new leaf tuples"); + + uint64_t n_leaf_tuples = 0; + if (m_cached_blocks.empty()) + { + CHECK_AND_ASSERT_THROW_MES(start_block_idx == 0, "must init first"); + + // Make sure all blockchain containers are empty + CHECK_AND_ASSERT_THROW_MES(m_cached_blocks.empty(), "expected empty cached blocks"); + CHECK_AND_ASSERT_THROW_MES(m_leaf_cache.empty(), "expected empty cached leaves"); + CHECK_AND_ASSERT_THROW_MES(m_tree_elem_cache.empty(), "expected empty cached tree elems"); + } + else + { + CHECK_AND_ASSERT_THROW_MES(start_block_idx > 0, "expected start_block_idx > 0"); + + // Make sure provided block is contiguous to prior synced block + const auto &prev_block = m_cached_blocks.back(); + CHECK_AND_ASSERT_THROW_MES((prev_block.blk_idx + 1) == start_block_idx, + "failed contiguity idx check processing synced blocks"); + + n_leaf_tuples = prev_block.n_leaf_tuples; + } + + // Update the output count + m_output_count += cache_state_change.n_outputs_observed; + + // Set the next locked outputs and refs + m_locked_outputs = std::move(cache_state_change.locked_outputs); + m_locked_output_ref_hashes = std::move(cache_state_change.locked_output_ref_hashes); + + // Update the existing last hashes in the cache using the tree extension + const auto &tree_extension = cache_state_change.tree_extension; + update_existing_last_hashes(TreeSync::m_curve_trees, tree_extension, m_tree_elem_cache); + + // Go block-by-block using slices of the tree extension to update values in the cache + uint64_t tuple_idx_start_slice = 0; + for (std::size_t i = 0; i < new_block_hashes.size(); ++i) + { + const uint64_t n_new_leaf_tuples = cache_state_change.n_new_leaf_tuples_per_block[i]; + n_leaf_tuples += n_new_leaf_tuples; + + const LeafIdx start_leaf_tuple_idx = tree_extension.leaves.start_leaf_tuple_idx + tuple_idx_start_slice; + + MDEBUG("Processing synced block " << new_block_hashes[i] + << " , block idx: " << start_block_idx + i + << " , n_leaf_tuples: " << n_leaf_tuples + << " , start_leaf_tuple_idx: " << start_leaf_tuple_idx); + + // Check if any registered outputs are present in the tree extension. If so, we assign the output its leaf idx + // and start keeping track of the output's path elems + for (uint64_t i = 0; i < n_new_leaf_tuples; ++i) + { + const LeafIdx tuple_idx = tuple_idx_start_slice + i; + CHECK_AND_ASSERT_THROW_MES(tree_extension.leaves.tuples.size() > tuple_idx, "unexpected tuple_idx"); + + const auto &output_pair = tree_extension.leaves.tuples[tuple_idx].output_pair; + const LeafIdx leaf_idx = start_leaf_tuple_idx + i; + assign_new_output(output_pair, leaf_idx, m_registered_outputs); + } + tuple_idx_start_slice += n_new_leaf_tuples; + + // Cache tree elems from the tree extension needed in order to keep track of registered output paths in the tree + for (const auto ®istered_o : m_registered_outputs) + { + // Skip all registered outputs which have not been included in the tree yet + if (!registered_o.second.assigned_leaf_idx) + continue; + + update_registered_path(TreeSync::m_curve_trees, + registered_o.second.leaf_idx, + cache_state_change.tree_extension, + start_leaf_tuple_idx, + n_leaf_tuples, + m_leaf_cache, + m_tree_elem_cache); + } + + // Cache the last chunk of leaves, so if a registered output appears in the first chunk next block, we'll have + // all prior leaves from that output's chunk already saved + cache_last_chunk_leaves(TreeSync::m_curve_trees, + tree_extension.leaves, + start_leaf_tuple_idx, + n_leaf_tuples, + m_leaf_cache); + + // Cache the last chunk of hashes from every layer. We do this to handle: + // 1) So we can use the tree's last hashes to grow the tree from here next block. + // 2) In case a registered output appears in the first chunk next block, we'll have all its path elems cached. + cache_last_chunks(TreeSync::m_curve_trees, + tree_extension, + start_leaf_tuple_idx, + n_leaf_tuples, + m_tree_elem_cache); + + // Enqueue block meta + const BlockIdx blk_idx = start_block_idx + i; + const auto &blk_hash = new_block_hashes[i]; + auto blk_meta = BlockMeta { + .blk_idx = blk_idx, + .blk_hash = blk_hash, + .n_leaf_tuples = n_leaf_tuples, + }; + m_cached_blocks.push_back(std::move(blk_meta)); + } + CHECK_AND_ASSERT_THROW_MES(tuple_idx_start_slice == tree_extension.leaves.tuples.size(), + "did not account for all new leaf tuples"); + + if (!skip_shrink_to_reorg_depth) + this->shrink_to_reorg_depth(); +} + +template void TreeCache::grow_cache(const uint64_t start_block_idx, + const std::vector &new_block_hashes, + CacheStateChange &&cache_state_change, + const bool skip_shrink_to_reorg_depth); +//---------------------------------------------------------------------------------------------------------------------- +template +void TreeCache::shrink_to_reorg_depth() +{ + // Deque the oldest cached block upon reaching the max reorg depth + while ((uint64_t)m_cached_blocks.size() > TreeSync::m_max_reorg_depth) + { + const auto &oldest_block = m_cached_blocks.front(); + + // All locked outputs that unlocked in the oldest block idx should already be in the tree. We keep them cached + // to handle reorgs (in case an output trimmed from the tree is supposed to re-enter the cache). We don't need + // to keep them past the reorg depth. + m_locked_outputs.erase(/*LastLockedBlockIdx*/oldest_block.blk_idx); + + // We keep locked output refs around for outputs *created* in the oldest block, so we can quickly remove them + // from the locked outputs cache upon popping the block. Once the reorg depth is exceeded, we can't remove those + // outputs anyway, so remove from the cache. + m_locked_output_ref_hashes.erase(/*CreatedBlockIdx*/oldest_block.blk_idx); + + this->deque_block(oldest_block.n_leaf_tuples); + m_cached_blocks.pop_front(); + } +} + +// Explicit instantiation +template void TreeCache::shrink_to_reorg_depth(); +//---------------------------------------------------------------------------------------------------------------------- +template +bool TreeCache::pop_block() +{ + CHECK_AND_ASSERT_MES(m_cached_blocks.size(), false, "pop_block: empty cache"); + CHECK_AND_ASSERT_MES(m_cached_blocks.size() > 1, false, "pop_block: cache must have at least 1 block after pop"); + auto cache_it = m_cached_blocks.rbegin(); + MDEBUG("Popping block " << cache_it->blk_idx << " , " << cache_it->blk_hash << " from tree cache"); + ++cache_it; + return this->pop_to_block(cache_it->blk_idx, cache_it->blk_hash); +} + +// Explicit instantiation +template bool TreeCache::pop_block(); +//---------------------------------------------------------------------------------------------------------------------- +template +bool TreeCache::pop_to_block(const uint64_t new_top_blk_idx, const crypto::hash &new_top_hash) +{ + // Pre-checks comparing passed new top block to cur top block + BlockMeta cur_top_block; + CHECK_AND_ASSERT_MES(this->get_top_block(cur_top_block), false, "pop_to_block: failed to get top block"); + CHECK_AND_ASSERT_MES(new_top_blk_idx <= cur_top_block.blk_idx, false, "pop_to_block: new_top_blk_idx too high"); + if (new_top_blk_idx == cur_top_block.blk_idx) + { + CHECK_AND_ASSERT_MES(new_top_hash == cur_top_block.blk_hash, false, "pop_to_block: unexpected top hash"); + MDEBUG("No blocks to pop"); + return true; + } + + // Pre-checks comparing passed new top block to first block + const BlockMeta &first_block = m_cached_blocks.front(); + CHECK_AND_ASSERT_MES(new_top_blk_idx >= first_block.blk_idx, false, "pop_to_block: new_top_blk_idx too low"); + + // We expect the new top block to already be present in the cached blocks + const uint64_t cached_blocks_idx = new_top_blk_idx - first_block.blk_idx; + CHECK_AND_ASSERT_MES(m_cached_blocks.size() > cached_blocks_idx, false, "pop_to_block: cached_blocks_idx too high"); + const BlockMeta &new_top_block = m_cached_blocks.at(cached_blocks_idx); + CHECK_AND_ASSERT_MES(new_top_block.blk_idx == new_top_blk_idx && new_top_block.blk_hash == new_top_hash, false, + "pop_to_block: the new top block must already be present in the cached blocks"); + + // Trim the tree down to the new top block, removing refs to last chunks + const uint64_t old_n_leaf_tuples = m_cached_blocks.back().n_leaf_tuples; + const uint64_t new_n_leaf_tuples = new_top_block.n_leaf_tuples; + CHECK_AND_ASSERT_MES(old_n_leaf_tuples >= new_n_leaf_tuples, false, + "pop_to_block: expected old_n_leaf_tuples >= new_n_leaf_tuples"); + + const uint64_t popping_n_blocks = cur_top_block.blk_idx - new_top_blk_idx; + MDEBUG("Popping " << popping_n_blocks << " blocks from tree cache"); + + // Remove all refs to leaves and layers for each block, back to the new top block idx + for (std::size_t pop_block_idx = cur_top_block.blk_idx; pop_block_idx > new_top_blk_idx; --pop_block_idx) + { + MDEBUG("Popping block " << m_cached_blocks.back().blk_idx << " from tree cache"); + this->deque_block(m_cached_blocks.back().n_leaf_tuples); + m_cached_blocks.pop_back(); + + // Remove locked outputs from the cache that were created in this block + const uint64_t n_outputs_removed = remove_outputs_created_at_block( + pop_block_idx, + m_locked_outputs, + m_locked_output_ref_hashes); + CHECK_AND_ASSERT_MES(m_output_count >= n_outputs_removed, false, "pop_to_block: output count too low"); + m_output_count -= n_outputs_removed; + } + + // No leaves to trim, safe return + if (old_n_leaf_tuples == new_n_leaf_tuples) + return true; + + // Shrink the new last chunk if some of the leaves in it got cut off + shrink_cached_last_leaf_chunk(new_n_leaf_tuples, TreeSync::m_curve_trees->m_c1_width, m_leaf_cache); + + // Get the tree edge when the new top block was the top block + const auto new_tree_edge = this->get_tree_edge(new_n_leaf_tuples); + + // Update ref'd last hashes and shrink current last chunks as necessary + reduce_cached_last_chunks(new_n_leaf_tuples, + new_tree_edge, + TreeSync::m_curve_trees, + m_tree_elem_cache); + + // Update registered output path refs + for (auto ®istered_o : m_registered_outputs) + { + // If the output isn't in the tree, it has no path elems we need to change in the cache + if (!registered_o.second.assigned_leaf_idx) + continue; + + // If the output remains in the tree, its chunk refs remain unchanged + const LeafIdx leaf_idx = registered_o.second.leaf_idx; + if (new_n_leaf_tuples > leaf_idx) + continue; + + MDEBUG("Un-assigning leaf idx " << leaf_idx); + + // The output was just removed from the tree, so remove its refs + const ChildChunkIdx leaf_chunk_idx = leaf_idx / TreeSync::m_curve_trees->m_c1_width; + remove_leaf_chunk_ref(leaf_chunk_idx, m_leaf_cache); + remove_path_chunks_refs(leaf_idx, TreeSync::m_curve_trees, old_n_leaf_tuples, m_tree_elem_cache); + + registered_o.second.unassign_leaf(); + } + + // Check if there are any remaining layers that need to be removed + // NOTE: this should only be useful for removing excess layers from registered outputs + LayerIdx layer_idx = new_tree_edge.size(); + while (1) + { + auto cache_layer_it = m_tree_elem_cache.find(layer_idx); + if (cache_layer_it == m_tree_elem_cache.end()) + break; + + MDEBUG("Removing cached layer " << layer_idx); + m_tree_elem_cache.erase(cache_layer_it); + ++layer_idx; + } + + return true; +} + +// Explicit instantiation +template bool TreeCache::pop_to_block(const uint64_t new_top_blk_idx, const crypto::hash &new_top_hash); +//---------------------------------------------------------------------------------------------------------------------- +template +bool TreeCache::get_output_path(const OutputPair &output, + typename CurveTrees::Path &path_out) const +{ + path_out.clear(); + + // Return false if the output isn't registered + auto registered_output_it = m_registered_outputs.find(get_output_ref_hash(output)); + if (registered_output_it == m_registered_outputs.end()) + return false; + + // Return empty path if the output is registered but isn't in the tree + if (!registered_output_it->second.assigned_leaf_idx) + return true; + + const uint64_t n_leaf_tuples = this->get_n_leaf_tuples(); + CHECK_AND_ASSERT_THROW_MES(n_leaf_tuples > 0, "n_leaf_tuples must be >0 if leaf is already assigned"); + + const LeafIdx leaf_idx = registered_output_it->second.leaf_idx; + CHECK_AND_ASSERT_THROW_MES(n_leaf_tuples > leaf_idx, "leaf_idx too high"); + + return this->get_leaf_path(n_leaf_tuples, leaf_idx, path_out); +} + +// Explicit instantiation +template bool TreeCache::get_output_path(const OutputPair &output, + CurveTrees::Path &path_out) const; +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +template +void TreeCache::init(const uint64_t start_block_idx, + const crypto::hash &start_block_hash, + const uint64_t n_leaf_tuples, + const fcmp_pp::CompressedPath &last_path, + const OutsByLastLockedBlock &timelocked_outputs) +{ + CHECK_AND_ASSERT_THROW_MES(m_cached_blocks.empty(), "expected empty tree cache"); + CHECK_AND_ASSERT_THROW_MES(n_leaf_tuples >= last_path.leaves.size(), "n_leaf_tuples too small"); + + BlockMeta init_block{ + .blk_idx = start_block_idx, + .blk_hash = start_block_hash, + .n_leaf_tuples = n_leaf_tuples, + }; + + m_cached_blocks.push_back(std::move(init_block)); + + const uint64_t last_leaf_idx = n_leaf_tuples > 0 ? n_leaf_tuples - 1 : 0; + const auto last_path_indexes = TreeSync::m_curve_trees->get_path_indexes(n_leaf_tuples, last_leaf_idx); + CHECK_AND_ASSERT_THROW_MES(last_path_indexes.layers.size() == last_path.layer_chunks.size(), + "unexpected size of layer chunks"); + + // Construct a mock tree extension from last path + const uint64_t start_leaf_tuple_idx = n_leaf_tuples - last_path.leaves.size(); + CHECK_AND_ASSERT_THROW_MES(last_path_indexes.leaf_range.first == start_leaf_tuple_idx, + "unexpected start leaf tuple idx"); + const auto tree_extension = TreeSync::m_curve_trees->path_to_tree_extension(last_path, last_path_indexes); + + // Cache the last chunk of leaves, so if a registered output appears in the first chunk next block, we'll have + // all prior leaves from that output's chunk already saved + cache_last_chunk_leaves(TreeSync::m_curve_trees, + tree_extension.leaves, + start_leaf_tuple_idx, + n_leaf_tuples, + m_leaf_cache); + + // Cache the last chunk of hashes from every layer. We need to do this to handle: + // 1) So we can use the tree's last hashes to grow the tree from here next block. + // 2) In case a registered output appears in the first chunk next block, we'll have all its path elems cached. + // 3) To trim the tree on reorg by re-growing with the children in each last chunk. + cache_last_chunks(TreeSync::m_curve_trees, + tree_extension, + start_leaf_tuple_idx, + n_leaf_tuples, + m_tree_elem_cache); + + // Add all timelocked outputs created before start_block_idx with last locked block >= start_block_idx so that we + // grow the tree with those outputs correctly upon unlock. + // - Assume the created block idx is the genesis block so the outputs won't get pruned. + const CreatedBlockIdx created_block_idx{0}; + add_to_locked_outputs_cache(timelocked_outputs, created_block_idx, m_locked_outputs, m_locked_output_ref_hashes); + + // Set the output count to the max output id + 1 + // WARNING: this is a little hacky because if there are no timelocked outputs provided (which should never be the + // case), then the output count would be 0 even if initializing at a block index > 0 + for (const auto &bl: timelocked_outputs) + for (const auto &o : bl.second) + if (o.unified_id >= m_output_count) + m_output_count = o.unified_id + 1; +} + +// Explicit instantiation +template void TreeCache::init(const uint64_t start_block_idx, + const crypto::hash &start_block_hash, + const uint64_t n_leaf_tuples, + const fcmp_pp::CompressedPath &last_hashes, + const OutsByLastLockedBlock &timelocked_outputs); +//---------------------------------------------------------------------------------------------------------------------- +template +uint64_t TreeCache::get_n_leaf_tuples() const noexcept +{ + return m_cached_blocks.empty() ? 0 : m_cached_blocks.back().n_leaf_tuples; +} + +// Explicit instantiation +template uint64_t TreeCache::get_n_leaf_tuples() const noexcept; +//---------------------------------------------------------------------------------------------------------------------- +template +void TreeCache::clear() +{ + m_locked_outputs.clear(); + m_locked_output_ref_hashes.clear(); + m_output_count = 0; + m_registered_outputs.clear(); + m_leaf_cache.clear(); + m_tree_elem_cache.clear(); + m_cached_blocks.clear(); +} +template void TreeCache::clear(); +//---------------------------------------------------------------------------------------------------------------------- +template +void TreeCache::force_add_output_path(const OutputPair &output, + const LeafIdx leaf_idx, + const CompressedPath &path_bytes, + const uint64_t n_leaf_tuples) +{ + MDEBUG("Force adding output " << fcmp_pp::output_pubkey_cref(output) + << " , commitment " << fcmp_pp::commitment_cref(output)); + + // If an empty path is passed in here, this function is not sufficiently capable of handling it. The output must + // be added to the locked outputs cache in this case, which would require the output's last locked block. + CHECK_AND_ASSERT_THROW_MES(path_bytes.leaves.size() && path_bytes.layer_chunks.size(), + "force_add_output_path: unexpected empty path"); + + // This function expects the output to be registered already, but not yet assigned. + const auto registered_output_it = m_registered_outputs.find(get_output_ref_hash(output)); + CHECK_AND_ASSERT_THROW_MES(registered_output_it != m_registered_outputs.end(), + "force_add_output_path: output is not already registered"); + CHECK_AND_ASSERT_THROW_MES(!registered_output_it->second.assigned_leaf_idx, + "force_add_output_path: output is already assigned"); + + // Assign the output's leaf tuple + assign_new_output(output, leaf_idx, m_registered_outputs); + + // Get a mock tree extension we'll use to add the output's path to the cache + const auto path_idxs = TreeSync::m_curve_trees->get_path_indexes(n_leaf_tuples, leaf_idx); + const auto tree_extension = TreeSync::m_curve_trees->path_to_tree_extension(path_bytes, path_idxs); + + cache_leaf_chunk(leaf_idx / TreeSync::m_curve_trees->m_c1_width, + TreeSync::m_curve_trees->m_c1_width, + tree_extension.leaves, + tree_extension.leaves.start_leaf_tuple_idx, + n_leaf_tuples, + true/*bump_ref_count*/, + m_leaf_cache); + + cache_path_chunks(leaf_idx, + TreeSync::m_curve_trees, + tree_extension.c1_layer_extensions, + tree_extension.c2_layer_extensions, + tree_extension.leaves.start_leaf_tuple_idx, + n_leaf_tuples, + true/*bump_ref_count*/, + m_tree_elem_cache); +} + +// Explicit instantiation +template void TreeCache::force_add_output_path(const OutputPair &output, + const LeafIdx leaf_idx, + const CompressedPath &path_bytes, + const uint64_t n_leaf_tuples); +//---------------------------------------------------------------------------------------------------------------------- +template +void TreeCache::force_set_top_block_unsafe(const BlockMeta &top_block) +{ + m_cached_blocks.push_back(top_block); +} + +// Explicit instantiation +template void TreeCache::force_set_top_block_unsafe(const BlockMeta &top_block); +//---------------------------------------------------------------------------------------------------------------------- +template +uint8_t TreeCache::get_tree_root(crypto::ec_point &tree_root_out) const +{ + tree_root_out = crypto::ec_point{}; + const uint64_t n_leaf_tuples = this->get_n_leaf_tuples(); + if (n_leaf_tuples == 0) + return 0; + + const LeafIdx last_leaf_idx = n_leaf_tuples - 1; + const auto child_chunk_idxs = TreeSync::m_curve_trees->get_child_chunk_indexes(n_leaf_tuples, + last_leaf_idx); + CHECK_AND_ASSERT_THROW_MES(child_chunk_idxs.size() >= 2, "unexpected empty child chunk indexes"); + const LayerIdx last_layer_idx = child_chunk_idxs.size() - 2; + const auto child_chunk_it = read_child_chunk(last_layer_idx, child_chunk_idxs.back(), m_tree_elem_cache); + + CHECK_AND_ASSERT_THROW_MES(child_chunk_it->second.tree_elems.size() == 1, "unexpected root layer size"); + tree_root_out = child_chunk_it->second.tree_elems.back(); + return TreeSync::m_curve_trees->n_layers(n_leaf_tuples); +} + +// Explicit instantiation +template uint8_t TreeCache::get_tree_root(crypto::ec_point &tree_root_out) const; +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +template +typename CurveTrees::LastHashes TreeCache::get_last_hashes() const +{ + const uint64_t n_leaf_tuples = this->get_n_leaf_tuples(); + MTRACE("Getting last hashes on tree with " << n_leaf_tuples << " leaf tuples"); + + typename CurveTrees::LastHashes last_hashes; + if (n_leaf_tuples == 0) + return last_hashes; + + // Get the child chunk indexes of the last leaf for each layer + const uint64_t last_leaf_idx = n_leaf_tuples - 1; + const auto child_chunk_idxs = TreeSync::m_curve_trees->get_child_chunk_indexes(n_leaf_tuples, + last_leaf_idx); + const std::size_t n_layers = TreeSync::m_curve_trees->n_layers(n_leaf_tuples); + CHECK_AND_ASSERT_THROW_MES(child_chunk_idxs.size() == (n_layers + 1), "unexpected n child chunk idxs"); + + // Read the last elem of each layer, starting at layer above leaf layer + std::vector tree_edge; + tree_edge.reserve(n_layers); + for (LayerIdx i = 0; i < n_layers; ++i) + { + const auto child_chunk_it = read_child_chunk(i, child_chunk_idxs[i + 1], m_tree_elem_cache); + tree_edge.push_back(child_chunk_it->second.tree_elems.back()); + } + + return TreeSync::m_curve_trees->tree_edge_to_last_hashes(tree_edge); +} + +// Explicit instantiation +template CurveTrees::LastHashes TreeCache::get_last_hashes() const; +//---------------------------------------------------------------------------------------------------------------------- +template +void TreeCache::deque_block(const uint64_t old_n_leaf_tuples) +{ + if (old_n_leaf_tuples == 0) + return; + + // Remove ref to last chunk leaves from the cache + const LeafIdx old_last_leaf_idx = old_n_leaf_tuples - 1; + const ChildChunkIdx leaf_chunk_idx = old_last_leaf_idx / TreeSync::m_curve_trees->m_c1_width; + remove_leaf_chunk_ref(leaf_chunk_idx, m_leaf_cache); + + // Remove refs to last chunk in every layer + remove_path_chunks_refs(old_last_leaf_idx, TreeSync::m_curve_trees, old_n_leaf_tuples, m_tree_elem_cache); +} +//---------------------------------------------------------------------------------------------------------------------- +template +bool TreeCache::get_leaf_path(const uint64_t n_leaf_tuples, + const LeafIdx leaf_idx, + typename CurveTrees::Path &path_out) const +{ + path_out.clear(); + if (n_leaf_tuples == 0) + return true; + + CHECK_AND_ASSERT_THROW_MES(n_leaf_tuples <= this->get_n_leaf_tuples(), "n_leaf_tuples is too high"); + CHECK_AND_ASSERT_THROW_MES(n_leaf_tuples > leaf_idx, "leaf_idx too high"); + + MTRACE("Getting path at leaf_idx: " << leaf_idx << " , tree has " << n_leaf_tuples << " leaf tuples"); + + const auto path_indexes = TreeSync::m_curve_trees->get_path_indexes(n_leaf_tuples, leaf_idx); + const auto child_chunk_idxs = TreeSync::m_curve_trees->get_child_chunk_indexes(n_leaf_tuples, leaf_idx); + CHECK_AND_ASSERT_THROW_MES(!child_chunk_idxs.empty(), "empty child chunk indexes"); + CHECK_AND_ASSERT_THROW_MES(child_chunk_idxs.size() == (path_indexes.layers.size() + 1), + "size mismatch path indexes <> child chunk indexes"); + + // Collect cached leaves from the leaf chunk the leaf is in + { + const auto leaf_chunk_it = m_leaf_cache.find(child_chunk_idxs.front()); + CHECK_AND_ASSERT_THROW_MES(leaf_chunk_it != m_leaf_cache.end(), "missing cached leaf chunk"); + + CHECK_AND_ASSERT_THROW_MES(path_indexes.leaf_range.second > path_indexes.leaf_range.first, "bad leaf range"); + const uint64_t n_leaves_in_chunk = path_indexes.leaf_range.second - path_indexes.leaf_range.first; + CHECK_AND_ASSERT_THROW_MES(leaf_chunk_it->second.leaves.size() >= n_leaves_in_chunk, "leaf chunk is too small"); + + for (std::size_t i = 0; i < n_leaves_in_chunk; ++i) + path_out.leaves.push_back(output_to_tuple(leaf_chunk_it->second.leaves[i])); + } + + // Read all members of each chunk + bool parent_is_c2 = true; + for (LayerIdx i = 0; i < path_indexes.layers.size(); ++i) + { + const auto child_chunk_it = read_child_chunk(i, child_chunk_idxs[i + 1], m_tree_elem_cache); + const auto &tree_elems = child_chunk_it->second.tree_elems; + + const auto &layer_range = path_indexes.layers[i]; + CHECK_AND_ASSERT_THROW_MES(layer_range.second > layer_range.first, "bad layer range"); + const uint64_t n_chunk_elems = layer_range.second - layer_range.first; + CHECK_AND_ASSERT_THROW_MES(tree_elems.size() >= n_chunk_elems, "layer chunk is too small"); + + if (parent_is_c2) + path_out.c1_layers.emplace_back(); + else + path_out.c2_layers.emplace_back(); + + for (std::size_t i = 0; i < n_chunk_elems; ++i) + { + const auto &tree_elem = tree_elems[i]; + if (parent_is_c2) + path_out.c1_layers.back().push_back(TreeSync::m_curve_trees->m_c1->from_bytes(tree_elem)); + else + path_out.c2_layers.back().push_back(TreeSync::m_curve_trees->m_c2->from_bytes(tree_elem)); + } + + parent_is_c2 = !parent_is_c2; + } + + return true; +} + +// Explicit instantiation +template bool TreeCache::get_leaf_path(const uint64_t n_leaf_tuples, + const LeafIdx leaf_idx, + CurveTrees::Path &path_out) const; +//---------------------------------------------------------------------------------------------------------------------- +template +std::vector TreeCache::get_tree_edge(const uint64_t n_leaf_tuples) const +{ + std::vector tree_edge_out; + if (n_leaf_tuples == 0) + return tree_edge_out; + + typename CurveTrees::Path last_path; + CHECK_AND_ASSERT_THROW_MES(this->get_leaf_path(n_leaf_tuples, n_leaf_tuples - 1, last_path), + "failed to get last leaf path"); + + // Re-hash every layer in this last path, starting from leaves to get the + // tree edge as it was when there were n_leaf_tuples in the chain + return TreeSync::m_curve_trees->calc_hashes_from_path(last_path, true/*replace_last_hash*/); +} +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +}//namespace curve_trees +}//namespace fcmp_pp diff --git a/src/fcmp_pp/tree_cache.h b/src/fcmp_pp/tree_cache.h new file mode 100644 index 00000000000..9ed3c6c010f --- /dev/null +++ b/src/fcmp_pp/tree_cache.h @@ -0,0 +1,298 @@ +// Copyright (c) 2024, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "crypto/crypto.h" +#include "crypto/hash.h" +#include "curve_trees.h" +#include "fcmp_pp_serialization.h" +#include "fcmp_pp_types.h" +#include "serialization/containers.h" +#include "serialization/crypto.h" +#include "serialization/pair.h" +#include "serialization/serialization.h" +#include "tree_sync.h" + +#include +#include +#include + + +namespace fcmp_pp +{ +namespace curve_trees +{ +//---------------------------------------------------------------------------------------------------------------------- +static const int TREE_CACHE_VERSION = 0; + +using BlockIdx = uint64_t; +using BlockHash = crypto::hash; + +using LeafIdx = uint64_t; +using LayerIdx = std::size_t; +using ChildChunkIdx = uint64_t; + +using LastLockedBlockIdx = BlockIdx; +using CreatedBlockIdx = BlockIdx; +using NumOutputs = std::size_t; + +using OutputRefHash = crypto::hash; + +struct BlockMeta final +{ + BlockIdx blk_idx; + BlockHash blk_hash; + uint64_t n_leaf_tuples; + + BEGIN_SERIALIZE_OBJECT() + FIELD(blk_idx) + FIELD(blk_hash) + FIELD(n_leaf_tuples) + END_SERIALIZE() +}; + +// We need to use a ref count on all individual elems in the cache because it's possible for: +// a) multiple blocks to share path elems that need to remain after pruning a block past the max reorg depth. +// b) multiple registered outputs to share the same path elems. +// We can't remove a cached elem unless we know it's ref'd 0 times. +struct CachedLeafChunk final +{ + std::vector leaves; + uint64_t ref_count; + + BEGIN_SERIALIZE_OBJECT() + FIELD(leaves) + FIELD(ref_count) + END_SERIALIZE() +}; + +struct CachedTreeElemChunk final +{ + std::vector tree_elems; + uint64_t ref_count; + + BEGIN_SERIALIZE_OBJECT() + FIELD(tree_elems) + FIELD(ref_count) + END_SERIALIZE() +}; + +struct AssignedLeafIdx final +{ + bool assigned_leaf_idx{false}; + LeafIdx leaf_idx{0}; + + void assign_leaf(const LeafIdx idx) { leaf_idx = idx; assigned_leaf_idx = true; } + void unassign_leaf() { leaf_idx = 0; assigned_leaf_idx = false; } + + BEGIN_SERIALIZE_OBJECT() + FIELD(assigned_leaf_idx) + FIELD(leaf_idx) + END_SERIALIZE() +}; + +using LockedOutsByLastLockedBlock = std::unordered_map>; +using LockedOutputRefHashes = std::unordered_map; +using LockedOutputsByCreated = std::unordered_map; + +using RegisteredOutputs = std::unordered_map; +using LeafCache = std::unordered_map; +using ChildChunkCache = std::unordered_map; + +// TODO: technically this can be a vector. There should *always* be at least 1 entry for every layer +using TreeElemCache = std::unordered_map; +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +// Syncs the tree and keeps a user's known received outputs up to date, all saved in memory. +// - The object does not store the entire tree locally. The object only stores what it needs in order to update paths +// of known received outputs as it syncs. +// - The memory footprint of the object is roughly ALL locked outputs in the chain, all known output paths, and the last +// chunk of tree elems at every layer of the tree the last N blocks. The latter is required to handle reorgs up to +// N blocks deep. +// - WARNING: the implementation is not thread safe, it expects synchronous calls. +// TODO: use a mutex to enforce thread safety. +template +class TreeCache final : public TreeSync +{ +public: + // A helpful struct useful to prepare a state change to the cache, and then use it to change the cache + struct CacheStateChange final + { + uint64_t n_outputs_observed{0}; + LockedOutsByLastLockedBlock locked_outputs; + LockedOutputsByCreated locked_output_ref_hashes; + typename fcmp_pp::curve_trees::CurveTrees::TreeExtension tree_extension; + std::vector n_new_leaf_tuples_per_block; + }; + +public: + TreeCache(std::shared_ptr> curve_trees, + const uint64_t max_reorg_depth = ORPHANED_BLOCKS_MAX_COUNT): + TreeSync(curve_trees, max_reorg_depth) + {}; + + bool register_output(const OutputPair &output) override; + + // TODO: bool cancel_output_registration + + void sync_block(const uint64_t block_idx, + const crypto::hash &block_hash, + const crypto::hash &prev_block_hash, + const OutsByLastLockedBlock &outs_by_last_locked_block) override; + + bool pop_block() override; + + bool get_output_path(const OutputPair &output, typename CurveTrees::Path &path_out) const override; + +// Public functions not part of TreeSync interface +public: + // Note: it's possible that the cache already contains force added paths from force_add_output_path. Calling init + // does not get rid of those already added paths. + void init(const uint64_t start_block_idx, + const crypto::hash &start_block_hash, + const uint64_t n_leaf_tuples, + const fcmp_pp::CompressedPath &last_path, + const OutsByLastLockedBlock &timelocked_outputs); + + // TODO: make this part of the TreeSync interface + uint64_t get_n_leaf_tuples() const noexcept; + bool get_top_block(BlockMeta &top_block_out) const + { + CHECK_AND_ASSERT_MES(!m_cached_blocks.empty(), false, "get_top_block: empty cached blocks"); + BlockMeta top_block = m_cached_blocks.back(); + top_block_out = std::move(top_block); + return true; + }; + + bool get_front_block(BlockMeta &front_block_out) const + { + CHECK_AND_ASSERT_MES(!m_cached_blocks.empty(), false, "get_front_block: empty cached blocks"); + BlockMeta front_block = m_cached_blocks.front(); + front_block_out = std::move(front_block); + return true; + }; + + uint64_t n_synced_blocks() const { return m_cached_blocks.empty() ? 0 : (m_cached_blocks.back().blk_idx + 1); } + + uint64_t get_output_count() const { return m_output_count; } + + // Gets the tree root and n_tree_layers for the tree currently cached in the TreeCache. If the TreeCache's tip is + // block index n, then this will return the tree root and n_tree_layers for the tree composed of all valid spendable + // outputs in the chain when the chain tip is block index n. + uint8_t get_tree_root(crypto::ec_point &tree_root_out) const; + + // Build the tree extension and all other types needed to grow the cache, returning the state change by ref + void prepare_to_grow_cache(const uint64_t start_block_idx, + const crypto::hash &prev_block_hash, + const std::vector &new_block_hashes, + const std::vector &outs_by_last_locked_blocks, + CacheStateChange &cache_state_change) const; + + // Advance the cache state, processing the state change prepared above + void grow_cache(const uint64_t start_block_idx, + const std::vector &new_block_hashes, + CacheStateChange &&cache_state_change, + const bool skip_shrink_to_reorg_depth = false); + + // The pruning feature of the cache gets rid of all refs we don't need anymore + void shrink_to_reorg_depth(); + + // Enable popping back to a specific block efficiently + bool pop_to_block(const uint64_t new_top_blk_idx, const crypto::hash &new_top_hash); + + // Clear all state + void clear(); + + // Force add a path to the cache without re-constructing it via sync + void force_add_output_path(const OutputPair &output, + const LeafIdx leaf_idx, + const CompressedPath &path_bytes, + const uint64_t n_leaf_tuples); + + // Useful for testing + void force_set_top_block_unsafe(const BlockMeta &top_block); + +// Internal helper functions +private: + typename CurveTrees::LastHashes get_last_hashes() const; + + bool get_leaf_path(const uint64_t n_leaf_tuples, + const LeafIdx leaf_idx, + typename CurveTrees::Path &path_out) const; + + void deque_block(const uint64_t old_n_leaf_tuples); + + std::vector get_tree_edge(const uint64_t n_leaf_tuples) const; + +// State held in memory +private: + // Locked outputs in the chain that we use to grow the tree with internally upon unlock + LockedOutsByLastLockedBlock m_locked_outputs; + LockedOutputsByCreated m_locked_output_ref_hashes; + + // Keep a global output counter so the caller knows how output id's should be set + uint64_t m_output_count{0}; + + // The outputs that TreeCache should keep track of while syncing + RegisteredOutputs m_registered_outputs; + + // Cached leaves and tree elems + LeafCache m_leaf_cache; + TreeElemCache m_tree_elem_cache; + + // Used for getting tree extensions when growing and for trimming + // - These are unspecific to the wallet's registered outputs. These are strictly necessary to ensure we can rebuild + // the tree extensions (and trim backwards) for each block correctly locally when syncing. + // - It's possible for m_cached_blocks.size() > m_max_reorg_depth if the max reorg depth changes across runs. + // This is ok as implemented. m_cached_blocks.size() will stay constant while syncing in this case. + std::deque m_cached_blocks; + + mutable uint64_t m_getting_unlocked_outs_ms{0}; + mutable uint64_t m_getting_tree_extension_ms{0}; + mutable uint64_t m_updating_cache_values_ms{0}; + +// Serialization +public: + BEGIN_SERIALIZE_OBJECT() + VERSION_FIELD(TREE_CACHE_VERSION) + FIELD(m_locked_outputs) + FIELD(m_locked_output_ref_hashes) + FIELD(m_output_count) + FIELD(m_registered_outputs) + FIELD(m_leaf_cache) + FIELD(m_tree_elem_cache) + FIELD(m_cached_blocks) + END_SERIALIZE() +}; + +using TreeCacheV1 = TreeCache; +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +}//namespace curve_trees +}//namespace fcmp_pp diff --git a/src/fcmp_pp/tree_sync.h b/src/fcmp_pp/tree_sync.h new file mode 100644 index 00000000000..31d34207c67 --- /dev/null +++ b/src/fcmp_pp/tree_sync.h @@ -0,0 +1,97 @@ +// Copyright (c) 2024, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "crypto/hash.h" +#include "cryptonote_config.h" +#include "curve_trees.h" + +#include + + +namespace fcmp_pp +{ +namespace curve_trees +{ +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +// Interface to sync the tree, keeping track of known output paths +// - Wallets can use this interface to sync the tree locally, making sure they can construct fcmp++'s for received +// outputs using the outputs' latest paths in the tree, without revealing which output is being spent to the daemon. +// - The caller first calls register_output for any known received outputs. +// - The caller then calls sync_block, which identifies and updates known output paths in the tree. +// - The caller can get an output's latest path in the tree via get_output_path. +// - If there's a reorg, the caller can use pop_block, which trims the locally synced tree and updates paths as needed. +template +class TreeSync +{ +public: + TreeSync(std::shared_ptr> curve_trees, + const uint64_t max_reorg_depth = ORPHANED_BLOCKS_MAX_COUNT): + m_curve_trees{curve_trees}, + m_max_reorg_depth{max_reorg_depth} + {}; + + // Registers an output with the TreeSync object so that syncing will keep track of the output's path in the tree + // - Returns true on successful new insertion + // - Returns false if the output is already registered. + virtual bool register_output(const OutputPair &output) = 0; + + // TODO: bool cancel_output_registration + + // Sync the outputs created in the provided block and grow the tree with outputs with last locked block block_idx + // - The block must be contiguous to the most recently synced block + // - If any registered outputs are present in the new leaf tuples, keeps track of their paths in the tree + // - Uses the new leaf tuples to update any existing known output paths in the tree + virtual void sync_block(const uint64_t block_idx, + const crypto::hash &block_hash, + const crypto::hash &prev_block_hash, + const fcmp_pp::OutsByLastLockedBlock &outs_by_last_locked_block) = 0; + + // Trim from the locally synced tree and update any paths as necesary + // - Returns false if we cannot pop any more blocks (if the max reorg depth is reached, or no more blocks to pop) + virtual bool pop_block() = 0; + + // Get a registered output's path in the tree + // - Returns false if the output is not registered + // - Returns true with empty path_out if the output is registered but not yet included in the tree + virtual bool get_output_path(const OutputPair &output, typename CurveTrees::Path &path_out) const = 0; + + // Overwrite the max reorg depth + void set_max_reorg_depth(const uint64_t max_reorg_depth) { m_max_reorg_depth = max_reorg_depth; }; + +// Internal member variables accessible by derived class +protected: + std::shared_ptr> m_curve_trees; + uint64_t m_max_reorg_depth; +}; +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +}//namespace curve_trees +}//namespace fcmp_pp diff --git a/src/hardforks/hardforks.cpp b/src/hardforks/hardforks.cpp index 571ba381890..5acda6e9a35 100644 --- a/src/hardforks/hardforks.cpp +++ b/src/hardforks/hardforks.cpp @@ -73,6 +73,10 @@ const hardfork_t mainnet_hard_forks[] = { { 15, 2688888, 0, 1656629117 }, { 16, 2689608, 0, 1656629118 }, + + // Mock values for tests + { 17, 2689609, 0, 1656629119 }, + { 18, 2689610, 0, 1656629120 }, }; const size_t num_mainnet_hard_forks = sizeof(mainnet_hard_forks) / sizeof(mainnet_hard_forks[0]); const uint64_t mainnet_hard_fork_version_1_till = 1009826; diff --git a/src/p2p/net_node.h b/src/p2p/net_node.h index 3052cca29c9..98dcacecf74 100644 --- a/src/p2p/net_node.h +++ b/src/p2p/net_node.h @@ -344,7 +344,7 @@ namespace nodetool virtual void callback(p2p_connection_context& context); //----------------- i_p2p_endpoint ------------------------------------------------------------- virtual bool relay_notify_to_list(int command, epee::levin::message_writer message, std::vector> connections) final; - virtual epee::net_utils::zone send_txs(std::vector txs, const epee::net_utils::zone origin, const boost::uuids::uuid& source, cryptonote::relay_method tx_relay); + virtual epee::net_utils::zone send_txs(std::vector txs, std::vector &&tx_hashes, const epee::net_utils::zone origin, const boost::uuids::uuid& source, cryptonote::relay_method tx_relay); virtual bool invoke_notify_to_peer(int command, epee::levin::message_writer message, const epee::net_utils::connection_context_base& context) final; virtual bool drop_connection(const epee::net_utils::connection_context_base& context); virtual void request_callback(const epee::net_utils::connection_context_base& context); diff --git a/src/p2p/net_node.inl b/src/p2p/net_node.inl index ce2d5346a9b..94f4ed52675 100644 --- a/src/p2p/net_node.inl +++ b/src/p2p/net_node.inl @@ -1478,7 +1478,7 @@ namespace nodetool ape.first_seen = first_seen_stamp ? first_seen_stamp : time(nullptr); zone.m_peerlist.append_with_peer_anchor(ape); - zone.m_notifier.on_handshake_complete(con->m_connection_id, con->m_is_income); + zone.m_notifier.on_handshake_complete(con->m_connection_id, con->m_is_income, (con->support_flags & P2P_SUPPORT_FLAG_TX_RELAY_V2)); zone.m_notifier.new_out_connection(); LOG_DEBUG_CC(*con, "CONNECTION HANDSHAKED OK."); @@ -2372,13 +2372,13 @@ namespace nodetool } //----------------------------------------------------------------------------------- template - epee::net_utils::zone node_server::send_txs(std::vector txs, const epee::net_utils::zone origin, const boost::uuids::uuid& source, const cryptonote::relay_method tx_relay) + epee::net_utils::zone node_server::send_txs(std::vector txs, std::vector &&tx_hashes, const epee::net_utils::zone origin, const boost::uuids::uuid& source, const cryptonote::relay_method tx_relay) { namespace enet = epee::net_utils; - const auto send = [&txs, &source, tx_relay] (std::pair& network) + const auto send = [&txs, &tx_hashes, &source, tx_relay] (std::pair& network) { - if (network.second.m_notifier.send_txs(std::move(txs), source, tx_relay)) + if (network.second.m_notifier.send_txs(std::move(txs), std::move(tx_hashes), source, tx_relay)) return network.first; return enet::zone::invalid; }; @@ -2676,7 +2676,7 @@ namespace nodetool return 1; } - zone.m_notifier.on_handshake_complete(context.m_connection_id, context.m_is_income); + zone.m_notifier.on_handshake_complete(context.m_connection_id, context.m_is_income, (arg.node_data.support_flags & P2P_SUPPORT_FLAG_TX_RELAY_V2)); //associate peer_id with this connection context.peer_id = arg.node_data.peer_id; diff --git a/src/p2p/net_node_common.h b/src/p2p/net_node_common.h index 625259eb197..94941c1b9be 100644 --- a/src/p2p/net_node_common.h +++ b/src/p2p/net_node_common.h @@ -53,7 +53,7 @@ namespace nodetool struct i_p2p_endpoint { virtual bool relay_notify_to_list(int command, epee::levin::message_writer message, std::vector> connections)=0; - virtual epee::net_utils::zone send_txs(std::vector txs, const epee::net_utils::zone origin, const boost::uuids::uuid& source, cryptonote::relay_method tx_relay)=0; + virtual epee::net_utils::zone send_txs(std::vector txs, std::vector &&tx_hashes, const epee::net_utils::zone origin, const boost::uuids::uuid& source, cryptonote::relay_method tx_relay)=0; virtual bool invoke_notify_to_peer(int command, epee::levin::message_writer message, const epee::net_utils::connection_context_base& context)=0; virtual bool drop_connection(const epee::net_utils::connection_context_base& context)=0; virtual void request_callback(const epee::net_utils::connection_context_base& context)=0; @@ -77,7 +77,7 @@ namespace nodetool { return false; } - virtual epee::net_utils::zone send_txs(std::vector txs, const epee::net_utils::zone origin, const boost::uuids::uuid& source, cryptonote::relay_method tx_relay) + virtual epee::net_utils::zone send_txs(std::vector txs, std::vector &&tx_hashes, const epee::net_utils::zone origin, const boost::uuids::uuid& source, cryptonote::relay_method tx_relay) { return epee::net_utils::zone::invalid; } diff --git a/src/ringct/CMakeLists.txt b/src/ringct/CMakeLists.txt index 8a6ed9f8fbc..7603584fd8e 100644 --- a/src/ringct/CMakeLists.txt +++ b/src/ringct/CMakeLists.txt @@ -45,6 +45,7 @@ target_link_libraries(ringct_basic PUBLIC common cncrypto + fcmp_pp # for fcmp_pp::fcmp_pp_proof_len PRIVATE ${OPENSSL_LIBRARIES} ${EXTRA_LIBRARIES}) @@ -70,6 +71,7 @@ target_link_libraries(ringct common cncrypto device + fcmp_pp PRIVATE ${OPENSSL_LIBRARIES} ${EXTRA_LIBRARIES}) diff --git a/src/ringct/rctOps.cpp b/src/ringct/rctOps.cpp index 3860a7e1fff..3c11e6b94c4 100644 --- a/src/ringct/rctOps.cpp +++ b/src/ringct/rctOps.cpp @@ -29,6 +29,7 @@ // THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include +#include "crypto/generators.h" #include "misc_log_ex.h" #include "rctOps.h" using namespace crypto; @@ -216,6 +217,33 @@ static const zero_commitment zero_commitments[] = { { (uint64_t)10000000000000000000ull, {{0x65, 0x8d, 0x1, 0x37, 0x6d, 0x18, 0x63, 0xe7, 0x7b, 0x9, 0x6f, 0x98, 0xe6, 0xe5, 0x13, 0xc2, 0x4, 0x10, 0xf5, 0xc7, 0xfb, 0x18, 0xa6, 0xe5, 0x9a, 0x52, 0x66, 0x84, 0x5c, 0xd9, 0xb1, 0xe3}} }, }; +static constexpr std::size_t H_TABLE_SIZE = 64; +const std::vector& H_TABLE() +{ + struct static_h_table + { + std::vector h_table; + static_h_table() + : h_table() + { + h_table.resize(H_TABLE_SIZE); + + ge_p3_to_cached(&h_table.at(0), &ge_p3_H); + ge_p3 H_bit_p3 = ge_p3_H; + + for (std::size_t i = 1; i < H_TABLE_SIZE; ++i) + { + ge_p1p1 H_bit_p1p1; + ge_p3_dbl(&H_bit_p1p1, &H_bit_p3); + ge_p1p1_to_p3(&H_bit_p3, &H_bit_p1p1); + ge_p3_to_cached(&h_table.at(i), &H_bit_p3); + } + } + }; + static const static_h_table out; + return out.h_table; +} + namespace rct { //Various key initialization functions @@ -318,7 +346,7 @@ namespace rct { return make_tuple(sk, pk); } - key zeroCommit(xmr_amount amount) { + key zeroCommitVartime(xmr_amount amount) { const zero_commitment *begin = zero_commitments; const zero_commitment *end = zero_commitments + sizeof(zero_commitments) / sizeof(zero_commitments[0]); const zero_commitment value{amount, rct::zero()}; @@ -327,9 +355,20 @@ namespace rct { { return it->commitment; } - key am = d2h(amount); - key bH = scalarmultH(am); - return addKeys(G, bH); + ge_p3 res_ge_p3 = get_G_p3(); + static_assert(sizeof(xmr_amount) * 8 == H_TABLE_SIZE, "unexpected size of h table"); + for (size_t i = 0; i < H_TABLE_SIZE; ++i) + { + if (amount & (xmr_amount(1) << i)) + { + ge_p1p1 p1p1; + ge_add(&p1p1, &res_ge_p3, &H_TABLE()[i]); + ge_p1p1_to_p3(&res_ge_p3, &p1p1); + } + } + rct::key res; + ge_p3_tobytes(res.bytes, &res_ge_p3); + return res; } key commit(xmr_amount amount, const key &mask) { diff --git a/src/ringct/rctOps.h b/src/ringct/rctOps.h index 0edd0308c46..e4110c6266a 100644 --- a/src/ringct/rctOps.h +++ b/src/ringct/rctOps.h @@ -108,7 +108,7 @@ namespace rct { // make a pedersen commitment with given key key commit(xmr_amount amount, const key &mask); // make a pedersen commitment with zero key - key zeroCommit(xmr_amount amount); + key zeroCommitVartime(xmr_amount amount); //generates a random uint long long xmr_amount randXmrAmount(xmr_amount upperlimit); diff --git a/src/ringct/rctSigs.cpp b/src/ringct/rctSigs.cpp index 206a9a51d8f..fe9fcdde32f 100644 --- a/src/ringct/rctSigs.cpp +++ b/src/ringct/rctSigs.cpp @@ -30,17 +30,26 @@ #include "rctSigs.h" +#include + #include "misc_log_ex.h" #include "misc_language.h" #include "common/perf_timer.h" #include "common/threadpool.h" #include "common/util.h" +#include "fcmp_pp/fcmp_pp_crypto.h" +#include "fcmp_pp/proof_len.h" +#include "fcmp_pp/prove.h" #include "bulletproofs.h" #include "bulletproofs_plus.h" #include "cryptonote_config.h" #include "device/device.hpp" #include "serialization/crypto.h" +#if defined(__GLIBC__) +#include +#endif + using namespace crypto; using namespace std; @@ -49,8 +58,7 @@ using namespace std; #define CHECK_AND_ASSERT_MES_L1(expr, ret, message) {if(!(expr)) {MCERROR("verify", message); return ret;}} -namespace -{ +namespace rct { rct::Bulletproof make_dummy_bulletproof(const std::vector &outamounts, rct::keyV &C, rct::keyV &masks) { const size_t n_outs = outamounts.size(); @@ -119,9 +127,7 @@ namespace const size_t n_scalars = ring_size; return rct::clsag{rct::keyV(n_scalars, I), I, I, I}; } -} -namespace rct { Bulletproof proveRangeBulletproof(keyV &C, keyV &masks, const std::vector &amounts, epee::span sk, hw::device &hwdev) { CHECK_AND_ASSERT_THROW_MES(amounts.size() == sk.size(), "Invalid amounts/sk sizes"); @@ -608,8 +614,10 @@ namespace rct { std::stringstream ss; binary_archive ba(ss); - CHECK_AND_ASSERT_THROW_MES(!rv.mixRing.empty(), "Empty mixRing"); - const size_t inputs = is_rct_simple(rv.type) ? rv.mixRing.size() : rv.mixRing[0].size(); + const size_t inputs = !rct::is_rct_simple(rv.type) ? rv.mixRing.at(0).size() + : rct::is_rct_fcmp(rv.type) ? 0 + : rv.mixRing.size(); + CHECK_AND_ASSERT_THROW_MES(rct::is_rct_fcmp(rv.type) || inputs > 0, "Empty pseudoOuts"); const size_t outputs = rv.ecdhInfo.size(); key prehash; CHECK_AND_ASSERT_THROW_MES(const_cast(rv).serialize_rctsig_base(ba, inputs, outputs), @@ -619,6 +627,12 @@ namespace rct { hashes.push_back(hash2rct(h)); keyV kv; + if (rv.type == RCTTypeFcmpPlusPlus) + { + // Don't hash range proof data to enable cleaner separation of SAL signature <> membership proof <> range proof + goto done; + } + if (rv.type == RCTTypeBulletproof || rv.type == RCTTypeBulletproof2 || rv.type == RCTTypeCLSAG) { kv.reserve((6*2+9) * rv.p.bulletproofs.size()); @@ -675,6 +689,7 @@ namespace rct { } } hashes.push_back(cn_fast_hash(kv)); +done: hwdev.mlsag_prehash(ss.str(), inputs, outputs, hashes, rv.outPk, prehash); return prehash; } @@ -1076,7 +1091,7 @@ namespace rct { //mask amount and mask rv.ecdhInfo[i].mask = copy(outSk[i].mask); rv.ecdhInfo[i].amount = d2h(amounts[i]); - hwdev.ecdhEncode(rv.ecdhInfo[i], amount_keys[i], rv.type == RCTTypeBulletproof2 || rv.type == RCTTypeCLSAG || rv.type == RCTTypeBulletproofPlus); + hwdev.ecdhEncode(rv.ecdhInfo[i], amount_keys[i], rct::is_rct_short_amount(rv.type)); } //set txn fee @@ -1107,6 +1122,8 @@ namespace rct { //for post-rct only rctSig genRctSimple(const key &message, const ctkeyV & inSk, const keyV & destinations, const vector &inamounts, const vector &outamounts, xmr_amount txnFee, const ctkeyM & mixRing, const keyV &amount_keys, const std::vector & index, ctkeyV &outSk, const RCTConfig &rct_config, hw::device &hwdev) { const bool bulletproof_or_plus = rct_config.range_proof_type > RangeProofBorromean; + const bool is_fcmp_pp = rct_config.bp_version >= 5; + CHECK_AND_ASSERT_THROW_MES(!is_fcmp_pp, "cannot use genRctSimple for FCMP++ tx"); CHECK_AND_ASSERT_THROW_MES(inamounts.size() > 0, "Empty inamounts"); CHECK_AND_ASSERT_THROW_MES(inamounts.size() == inSk.size(), "Different number of inamounts/inSk"); CHECK_AND_ASSERT_THROW_MES(outamounts.size() == destinations.size(), "Different number of amounts/destinations"); @@ -1211,7 +1228,7 @@ namespace rct { //mask amount and mask rv.ecdhInfo[i].mask = copy(outSk[i].mask); rv.ecdhInfo[i].amount = d2h(outamounts[i]); - hwdev.ecdhEncode(rv.ecdhInfo[i], amount_keys[i], rv.type == RCTTypeBulletproof2 || rv.type == RCTTypeCLSAG || rv.type == RCTTypeBulletproofPlus); + hwdev.ecdhEncode(rv.ecdhInfo[i], amount_keys[i], rct::is_rct_short_amount(rv.type)); } //set txn fee @@ -1357,8 +1374,7 @@ namespace rct { { CHECK_AND_ASSERT_MES(rvp, false, "rctSig pointer is NULL"); const rctSig &rv = *rvp; - CHECK_AND_ASSERT_MES(rv.type == RCTTypeSimple || rv.type == RCTTypeBulletproof || rv.type == RCTTypeBulletproof2 || rv.type == RCTTypeCLSAG || rv.type == RCTTypeBulletproofPlus, - false, "verRctSemanticsSimple called on non simple rctSig"); + CHECK_AND_ASSERT_MES(is_rct_simple(rv.type), false, "verRctSemanticsSimple called on non simple rctSig"); const bool bulletproof = is_rct_bulletproof(rv.type); const bool bulletproof_plus = is_rct_bulletproof_plus(rv.type); if (bulletproof || bulletproof_plus) @@ -1367,7 +1383,17 @@ namespace rct { CHECK_AND_ASSERT_MES(rv.outPk.size() == n_bulletproof_plus_amounts(rv.p.bulletproofs_plus), false, "Mismatched sizes of outPk and bulletproofs_plus"); else CHECK_AND_ASSERT_MES(rv.outPk.size() == n_bulletproof_amounts(rv.p.bulletproofs), false, "Mismatched sizes of outPk and bulletproofs"); - if (is_rct_clsag(rv.type)) + if (rv.type == RCTTypeFcmpPlusPlus) + { + CHECK_AND_ASSERT_MES(rv.p.MGs.empty(), false, "MGs are not empty for FCMP++"); + CHECK_AND_ASSERT_MES(rv.p.CLSAGs.empty(), false, "CLSAGs are not empty for FCMP++"); + CHECK_AND_ASSERT_MES(rv.p.pseudoOuts.size(), false, "Empty pseudo outs"); + CHECK_AND_ASSERT_MES(rv.p.pseudoOuts.size() <= FCMP_PLUS_PLUS_MAX_INPUTS, false, "Too many pseudo outs"); + CHECK_AND_ASSERT_MES(rv.p.n_tree_layers > 0, false, "0 tree layers"); + CHECK_AND_ASSERT_MES(rv.p.n_tree_layers <= FCMP_PLUS_PLUS_MAX_LAYERS, false, "Too many layers"); + CHECK_AND_ASSERT_MES(rv.p.fcmp_pp.size() == fcmp_pp::fcmp_pp_proof_len(rv.p.pseudoOuts.size(), rv.p.n_tree_layers), false, "Unexpected FCMP++ proof size"); + } + else if (is_rct_clsag(rv.type)) { CHECK_AND_ASSERT_MES(rv.p.MGs.empty(), false, "MGs are not empty for CLSAG"); CHECK_AND_ASSERT_MES(rv.p.pseudoOuts.size() == rv.p.CLSAGs.size(), false, "Mismatched sizes of rv.p.pseudoOuts and rv.p.CLSAGs"); @@ -1486,10 +1512,34 @@ namespace rct { { PERF_TIMER(verRctNonSemanticsSimple); - CHECK_AND_ASSERT_MES(rv.type == RCTTypeSimple || rv.type == RCTTypeBulletproof || rv.type == RCTTypeBulletproof2 || rv.type == RCTTypeCLSAG || rv.type == RCTTypeBulletproofPlus, - false, "verRctNonSemanticsSimple called on non simple rctSig"); + CHECK_AND_ASSERT_MES(is_rct_simple(rv.type), false, "verRctNonSemanticsSimple called on non simple rctSig"); const bool bulletproof = is_rct_bulletproof(rv.type); const bool bulletproof_plus = is_rct_bulletproof_plus(rv.type); + const keyV &pseudoOuts = (bulletproof || bulletproof_plus) ? rv.p.pseudoOuts : rv.pseudoOuts; + + const key message = get_pre_mlsag_hash(rv, hw::get_device("default")); + + if (is_rct_fcmp(rv.type)) + { + CHECK_AND_ASSERT_MES(rv.type == rct::RCTTypeFcmpPlusPlus, false, "verRctNonSemanticsSimple called on unsupported FCMP type"); + + // Type conversion on pseudo outs + std::vector pseudo_outs; + pseudo_outs.reserve(pseudoOuts.size()); + for (const auto &po : pseudoOuts) + pseudo_outs.emplace_back(rct::rct2pt(po)); + + bool r = fcmp_pp::verify(rct::rct2hash(message), + rv.p.fcmp_pp, + rv.p.n_tree_layers, + rv.p.fcmp_ver_helper_data.tree_root, + pseudo_outs, + rv.p.fcmp_ver_helper_data.key_images); + + CHECK_AND_ASSERT_MES(r, false, "Failed to verify FCMP++ proof"); + return true; + } + // semantics check is early, and mixRing/MGs aren't resolved yet if (bulletproof || bulletproof_plus) CHECK_AND_ASSERT_MES(rv.p.pseudoOuts.size() == rv.mixRing.size(), false, "Mismatched sizes of rv.p.pseudoOuts and mixRing"); @@ -1502,10 +1552,6 @@ namespace rct { tools::threadpool& tpool = tools::threadpool::getInstanceForCompute(); tools::threadpool::waiter waiter(tpool); - const keyV &pseudoOuts = bulletproof || bulletproof_plus ? rv.p.pseudoOuts : rv.pseudoOuts; - - const key message = get_pre_mlsag_hash(rv, hw::get_device("default")); - results.clear(); results.resize(rv.mixRing.size()); for (size_t i = 0 ; i < rv.mixRing.size() ; i++) { @@ -1558,7 +1604,7 @@ namespace rct { //mask amount and mask ecdhTuple ecdh_info = rv.ecdhInfo[i]; - hwdev.ecdhDecode(ecdh_info, sk, rv.type == RCTTypeBulletproof2 || rv.type == RCTTypeCLSAG || rv.type == RCTTypeBulletproofPlus); + hwdev.ecdhDecode(ecdh_info, sk, rct::is_rct_short_amount(rv.type)); mask = ecdh_info.mask; key amount = ecdh_info.amount; key C = rv.outPk[i].mask; @@ -1582,14 +1628,13 @@ namespace rct { } xmr_amount decodeRctSimple(const rctSig & rv, const key & sk, unsigned int i, key &mask, hw::device &hwdev) { - CHECK_AND_ASSERT_MES(rv.type == RCTTypeSimple || rv.type == RCTTypeBulletproof || rv.type == RCTTypeBulletproof2 || rv.type == RCTTypeCLSAG || rv.type == RCTTypeBulletproofPlus, - false, "decodeRct called on non simple rctSig"); + CHECK_AND_ASSERT_MES(rct::is_rct_simple(rv.type), false, "decodeRct called on non simple rctSig"); CHECK_AND_ASSERT_THROW_MES(i < rv.ecdhInfo.size(), "Bad index"); CHECK_AND_ASSERT_THROW_MES(rv.outPk.size() == rv.ecdhInfo.size(), "Mismatched sizes of rv.outPk and rv.ecdhInfo"); //mask amount and mask ecdhTuple ecdh_info = rv.ecdhInfo[i]; - hwdev.ecdhDecode(ecdh_info, sk, rv.type == RCTTypeBulletproof2 || rv.type == RCTTypeCLSAG || rv.type == RCTTypeBulletproofPlus); + hwdev.ecdhDecode(ecdh_info, sk, rct::is_rct_short_amount(rv.type)); mask = ecdh_info.mask; key amount = ecdh_info.amount; key C = rv.outPk[i].mask; @@ -1611,4 +1656,170 @@ namespace rct { key mask; return decodeRctSimple(rv, sk, i, mask, hwdev); } + + bool verPointsForTorsion(const std::vector & pts) { + if (pts.empty()) + return true; + + tools::threadpool& tpool = tools::threadpool::getInstanceForCompute(); + tools::threadpool::waiter waiter(tpool); + + std::deque torsion_free; + + torsion_free.resize(pts.size()); + for (size_t i = 0; i < pts.size(); i++) { + tpool.submit(&waiter, [&pts, &torsion_free, i] + { + const crypto::ec_point &point = rct::rct2pt(pts[i]); + crypto::ec_point torsion_cleared_point; + if (!fcmp_pp::get_valid_torsion_cleared_point(point, torsion_cleared_point)) + { + torsion_free[i] = false; + return; + } + // Point is torsion free if after clearing torsion, it's equal to itself + torsion_free[i] = point == torsion_cleared_point; + }); + } + + if (!waiter.wait()) + return false; + for (size_t i = 0; i < torsion_free.size(); ++i) { + if (!torsion_free[i]) { + LOG_PRINT_L1("Torsion check failed for point " << i); + return false; + } + } + + return true; + } + + bool batchVerifyFcmpPpProofs(std::vector &&fcmp_pp_verify_inputs) { + const std::size_t n_proofs = fcmp_pp_verify_inputs.size(); + if (n_proofs == 0) + return true; + + // Sort the inputs in descending order based on input count. We will use a bin packing algo to spread the load. + std::sort(fcmp_pp_verify_inputs.begin(), fcmp_pp_verify_inputs.end(), [](auto &a, auto &b) + { return fcmp_pp::n_inputs_in_fcmp_pp(a) > fcmp_pp::n_inputs_in_fcmp_pp(b); }); + + tools::threadpool &tpool = tools::threadpool::getInstanceForCompute(); + tools::threadpool::waiter waiter(tpool); + const std::size_t n_threads = std::max(1, tpool.get_max_concurrency()); + const bool multithreaded = n_threads > 1; + + // 1 batch per thread + const std::size_t n_batches = std::min(n_threads, n_proofs); + + struct ProofBatch + { + std::vector batch; + std::size_t total_inputs{0}; + }; + + std::vector batches; + batches.reserve(n_batches); + + // Spread the load based on n inputs in each proof, to make it more even. + for (std::size_t i = 0; i < n_proofs; ++i) + { + // Here's the proof we're adding to a batch + fcmp_pp::FcmpPpVerifyInput &fcmp_pp_verify_input = fcmp_pp_verify_inputs.at(i); + const std::size_t n_inputs = fcmp_pp::n_inputs_in_fcmp_pp(fcmp_pp_verify_input); + + // Find the batch with the lowest weight + std::size_t min_weight_batch_idx = 0; + std::size_t min_weight = 0; + for (std::size_t j = 0; j < n_batches; ++j) + { + if (batches.size() <= j) + { + // We found an empty batch, use it + batches.emplace_back(); + batches.back().batch.reserve(n_proofs - i); + min_weight_batch_idx = j; + break; + } + + if (min_weight > 0 && batches.at(j).total_inputs >= min_weight) + continue; + + // We found a batch with a lower weight + min_weight = batches.at(j).total_inputs; + min_weight_batch_idx = j; + } + + // Add the proof to the min weight batch + auto &min_weight_batch = batches.at(min_weight_batch_idx); + assert(min_weight_batch.batch.size() < min_weight_batch.batch.capacity()); // if tripped, we pre-reserved wrong + min_weight_batch.batch.emplace_back(std::move(fcmp_pp_verify_input)); + min_weight_batch.total_inputs += n_inputs; + + MDEBUG("Placed FCMP++ tx in batch " << (min_weight_batch_idx+1) + << ", batch now has " << min_weight_batch.total_inputs << " total inputs"); + } + CHECK_AND_ASSERT_MES(batches.size() <= n_batches, false, "Too many batches"); + + std::deque results; + results.resize(batches.size()); + for (std::size_t i = 0; i < batches.size(); ++i) + { + CHECK_AND_ASSERT_MES(batches[i].batch.size(), false, "Empty batch in batchVerifyFcmpPpProofs"); + + MDEBUG("Verifying FCMP++ batch " << (i+1) << " (" + << batches[i].total_inputs << " total inputs across " << batches[i].batch.size() << " txs)"); + + if (!multithreaded) + { + results[i] = fcmp_pp::verify(batches[i].batch); + continue; + } + + tpool.submit(&waiter, + [&batches, &results, i]() + { + results[i] = fcmp_pp::verify(batches[i].batch); + + MDEBUG("Finished verifying FCMP++ batch " << (i+1) << " / " << batches.size()); + }, + true + ); + } + + if (multithreaded) + CHECK_AND_ASSERT_THROW_MES(waiter.wait(), "Failed to batch verify FCMP++ proofs"); + + for (bool r : results) { + if (!r) + return false; + } + return true; + } + + void limitMaxMemArenas() + { +#ifdef M_ARENA_MAX + tools::threadpool &tpool = tools::threadpool::getInstanceForCompute(); + const std::size_t n_threads = std::max(1, tpool.get_max_concurrency()); + + // Use at least 2 arenas always to match glibc's default minimum + // https://github.com/bminor/glibc/blob/40a751b0044114488e841f0223e630596c527c53/malloc/arena.c#L824-L834 + // https://github.com/bminor/glibc/blob/40a751b0044114488e841f0223e630596c527c53/malloc/malloc.c#L1974 + const std::size_t max_arenas = std::max(2, n_threads); + + // See mallopt and M_ARENA_MAX at: https://man7.org/linux/man-pages/man3/mallopt.3.html + int r = mallopt(M_ARENA_MAX, max_arenas); + if (r == 1) + { + MDEBUG("Set max arenas to " << max_arenas); + return; + } + + MWARNING("Failed to set max arenas, the system may use more memory than expected during sync."); +#else + MDEBUG("System does not have mallopt and M_ARENA_MAX setting. This setting is crucial for some Linux platforms to" + << " avoid OOM's when batch verifying FCMP++ txs. If we see OOM's in the future when batch verifying on a non-" + << "Linux platform, then check the system allocator behavior and see if it has a setting similar to mallopt"); +#endif + } } diff --git a/src/ringct/rctSigs.h b/src/ringct/rctSigs.h index 035d866d696..e121f3ebc70 100644 --- a/src/ringct/rctSigs.h +++ b/src/ringct/rctSigs.h @@ -47,7 +47,6 @@ extern "C" { } #include "crypto/crypto.h" - #include "rctTypes.h" #include "rctOps.h" @@ -64,6 +63,10 @@ namespace hw { namespace rct { + // helpers for mock txs + Bulletproof make_dummy_bulletproof(const std::vector &outamounts, keyV &C, keyV &masks); + BulletproofPlus make_dummy_bulletproof_plus(const std::vector &outamounts, keyV &C, keyV &masks); + clsag make_dummy_clsag(size_t ring_size); boroSig genBorromean(const key64 x, const key64 P1, const key64 P2, const bits indices); bool verifyBorromean(const boroSig &bb, const key64 P1, const key64 P2); @@ -138,6 +141,23 @@ namespace rct { xmr_amount decodeRctSimple(const rctSig & rv, const key & sk, unsigned int i, key & mask, hw::device &hwdev); xmr_amount decodeRctSimple(const rctSig & rv, const key & sk, unsigned int i, hw::device &hwdev); key get_pre_mlsag_hash(const rctSig &rv, hw::device &hwdev); + + // Make sure points are valid points, don't have torsion, and are not equal to identity + bool verPointsForTorsion(const std::vector & pts); + + // Split into batches and verify each batch in parallel + bool batchVerifyFcmpPpProofs(std::vector &&fcmp_pp_verify_inputs); + + // The default libc allocator on most Linux systems may cache allocated memory for reuse. + // As a result, verifying many large batches of FCMP++ proofs in multithreaded contexts + // can end up using a lot of memory that does not get released back to the OS, even + // though memory is already freed. + // This function uses the mallopt syscall to limit the max number of "arenas" the system + // may use, setting it to the number of threads the system has. This way there won't + // be more memory allocated and kept around than expected in a potentially unbounded + // number of arenas. + // More on this here: https://gotplt.org/posts/malloc-per-thread-arenas-in-glibc.html + void limitMaxMemArenas(); } #endif /* RCTSIGS_H */ diff --git a/src/ringct/rctTypes.cpp b/src/ringct/rctTypes.cpp index 8b0345e976d..095517b9572 100644 --- a/src/ringct/rctTypes.cpp +++ b/src/ringct/rctTypes.cpp @@ -197,6 +197,7 @@ namespace rct { case RCTTypeBulletproof2: case RCTTypeCLSAG: case RCTTypeBulletproofPlus: + case RCTTypeFcmpPlusPlus: return true; default: return false; @@ -221,6 +222,7 @@ namespace rct { switch (type) { case RCTTypeBulletproofPlus: + case RCTTypeFcmpPlusPlus: return true; default: return false; @@ -251,6 +253,44 @@ namespace rct { } } + bool is_rct_short_amount(int type) + { + switch (type) + { + case RCTTypeBulletproof2: + case RCTTypeCLSAG: + case RCTTypeBulletproofPlus: + case RCTTypeFcmpPlusPlus: + return true; + case RCTTypeNull: + case RCTTypeFull: + case RCTTypeSimple: + case RCTTypeBulletproof: + return false; + default: + CHECK_AND_ASSERT_THROW_MES(false, "unknown rct type = " << type); + } + } + + bool is_rct_fcmp(int type) + { + switch (type) + { + case RCTTypeFcmpPlusPlus: + return true; + case RCTTypeNull: + case RCTTypeFull: + case RCTTypeSimple: + case RCTTypeBulletproof: + case RCTTypeBulletproof2: + case RCTTypeCLSAG: + case RCTTypeBulletproofPlus: + return false; + default: + CHECK_AND_ASSERT_THROW_MES(false, "unknown rct type = " << type); + } + } + static size_t n_bulletproof_amounts_base(const size_t L_size, const size_t R_size, const size_t V_size, const size_t max_outputs) { CHECK_AND_ASSERT_MES(L_size >= 6, 0, "Invalid bulletproof L size"); diff --git a/src/ringct/rctTypes.h b/src/ringct/rctTypes.h index ee896f11e1b..e1e41e620b3 100644 --- a/src/ringct/rctTypes.h +++ b/src/ringct/rctTypes.h @@ -45,7 +45,8 @@ extern "C" { } #include "crypto/generic-ops.h" #include "crypto/crypto.h" - +#include "fcmp_pp/fcmp_pp_types.h" +#include "fcmp_pp/proof_len.h" #include "hex.h" #include "span.h" #include "memwipe.h" @@ -304,6 +305,7 @@ namespace rct { RCTTypeBulletproof2 = 4, RCTTypeCLSAG = 5, RCTTypeBulletproofPlus = 6, + RCTTypeFcmpPlusPlus = 7, }; enum RangeProofType { RangeProofBorromean, RangeProofPaddedBulletproof }; struct RCTConfig { @@ -336,7 +338,7 @@ namespace rct { FIELD(type) if (type == RCTTypeNull) return ar.good(); - if (type != RCTTypeFull && type != RCTTypeSimple && type != RCTTypeBulletproof && type != RCTTypeBulletproof2 && type != RCTTypeCLSAG && type != RCTTypeBulletproofPlus) + if (type != RCTTypeFull && type != RCTTypeSimple && type != RCTTypeBulletproof && type != RCTTypeBulletproof2 && type != RCTTypeCLSAG && type != RCTTypeBulletproofPlus && type != RCTTypeFcmpPlusPlus) return false; VARINT_FIELD(txnFee) // inputs/outputs not saved, only here for serialization help @@ -365,7 +367,7 @@ namespace rct { return false; for (size_t i = 0; i < outputs; ++i) { - if (type == RCTTypeBulletproof2 || type == RCTTypeCLSAG || type == RCTTypeBulletproofPlus) + if (type == RCTTypeBulletproof2 || type == RCTTypeCLSAG || type == RCTTypeBulletproofPlus || type == RCTTypeFcmpPlusPlus) { // Since RCTTypeBulletproof2 enote types, we don't serialize the blinding factor, and only serialize the // first 8 bytes of ecdhInfo[i].amount @@ -421,6 +423,11 @@ namespace rct { std::vector MGs; // simple rct has N, full has 1 std::vector CLSAGs; keyV pseudoOuts; //C - for simple rct + // FCMP data + uint64_t reference_block{0}; // used to get the tree root as of when this reference block index enters the chain + uint8_t n_tree_layers{0}; // number of layers in the tree as of the block when the reference block index enters the chain + fcmp_pp::FcmpPpProof fcmp_pp; // FCMP++ SAL and membership proof + fcmp_pp::FcmpVerifyHelperData fcmp_ver_helper_data; // used to verify FCMP proofs (not serialized, reconstructed) // when changing this function, update cryptonote::get_pruned_transaction_weight template class Archive> @@ -434,9 +441,9 @@ namespace rct { return false; if (type == RCTTypeNull) return ar.good(); - if (type != RCTTypeFull && type != RCTTypeSimple && type != RCTTypeBulletproof && type != RCTTypeBulletproof2 && type != RCTTypeCLSAG && type != RCTTypeBulletproofPlus) + if (type != RCTTypeFull && type != RCTTypeSimple && type != RCTTypeBulletproof && type != RCTTypeBulletproof2 && type != RCTTypeCLSAG && type != RCTTypeBulletproofPlus && type != RCTTypeFcmpPlusPlus) return false; - if (type == RCTTypeBulletproofPlus) + if (type == RCTTypeBulletproofPlus || type == RCTTypeFcmpPlusPlus) { uint32_t nbp = bulletproofs_plus.size(); VARINT_FIELD(nbp) @@ -493,7 +500,31 @@ namespace rct { ar.end_array(); } - if (type == RCTTypeCLSAG || type == RCTTypeBulletproofPlus) + if (type == RCTTypeFcmpPlusPlus) + { + VARINT_FIELD(reference_block) + // n_tree_layers can be inferred from the reference_block, however, if we didn't save n_tree_layers on the + // tx, we would need a db read (for n_tree_layers as of the block) in order to de-serialize the FCMP++ proof + FIELD(n_tree_layers) + ar.tag("fcmp_pp"); + if (inputs == 0) + return false; + if (inputs > FCMP_PLUS_PLUS_MAX_INPUTS) + return false; + if (n_tree_layers == 0) + return false; + if (n_tree_layers > FCMP_PLUS_PLUS_MAX_LAYERS) + return false; + const std::size_t proof_len = fcmp_pp::fcmp_pp_proof_len(inputs, n_tree_layers); + if (!typename Archive::is_saving()) + fcmp_pp.resize(proof_len); + if (fcmp_pp.size() != proof_len) + return false; + ar.serialize_blob(fcmp_pp.data(), proof_len); + if (!ar.good()) + return false; + } + else if (type == RCTTypeCLSAG || type == RCTTypeBulletproofPlus) { ar.tag("CLSAGs"); ar.begin_array(); @@ -584,7 +615,7 @@ namespace rct { } ar.end_array(); } - if (type == RCTTypeBulletproof || type == RCTTypeBulletproof2 || type == RCTTypeCLSAG || type == RCTTypeBulletproofPlus) + if (type == RCTTypeBulletproof || type == RCTTypeBulletproof2 || type == RCTTypeCLSAG || type == RCTTypeBulletproofPlus || type == RCTTypeFcmpPlusPlus) { ar.tag("pseudoOuts"); ar.begin_array(); @@ -608,6 +639,9 @@ namespace rct { FIELD(bulletproofs_plus) FIELD(MGs) FIELD(CLSAGs) + VARINT_FIELD(reference_block) + FIELD(n_tree_layers) + FIELD(fcmp_pp) FIELD(pseudoOuts) END_SERIALIZE() }; @@ -616,12 +650,12 @@ namespace rct { keyV& get_pseudo_outs() { - return type == RCTTypeBulletproof || type == RCTTypeBulletproof2 || type == RCTTypeCLSAG || type == RCTTypeBulletproofPlus ? p.pseudoOuts : pseudoOuts; + return type == RCTTypeBulletproof || type == RCTTypeBulletproof2 || type == RCTTypeCLSAG || type == RCTTypeBulletproofPlus || type == RCTTypeFcmpPlusPlus ? p.pseudoOuts : pseudoOuts; } keyV const& get_pseudo_outs() const { - return type == RCTTypeBulletproof || type == RCTTypeBulletproof2 || type == RCTTypeCLSAG || type == RCTTypeBulletproofPlus ? p.pseudoOuts : pseudoOuts; + return type == RCTTypeBulletproof || type == RCTTypeBulletproof2 || type == RCTTypeCLSAG || type == RCTTypeBulletproofPlus || type == RCTTypeFcmpPlusPlus ? p.pseudoOuts : pseudoOuts; } BEGIN_SERIALIZE_OBJECT() @@ -736,15 +770,19 @@ namespace rct { bool is_rct_bulletproof_plus(int type); bool is_rct_borromean(int type); bool is_rct_clsag(int type); + bool is_rct_short_amount(int type); + bool is_rct_fcmp(int type); static inline const rct::key &pk2rct(const crypto::public_key &pk) { return (const rct::key&)pk; } static inline const rct::key &sk2rct(const crypto::secret_key &sk) { return (const rct::key&)sk; } static inline const rct::key &ki2rct(const crypto::key_image &ki) { return (const rct::key&)ki; } static inline const rct::key &hash2rct(const crypto::hash &h) { return (const rct::key&)h; } + static inline const rct::key &pt2rct(const crypto::ec_point &pt) { return (const rct::key&)pt; } static inline const crypto::public_key &rct2pk(const rct::key &k) { return (const crypto::public_key&)k; } static inline const crypto::secret_key &rct2sk(const rct::key &k) { return (const crypto::secret_key&)k; } static inline const crypto::key_image &rct2ki(const rct::key &k) { return (const crypto::key_image&)k; } static inline const crypto::hash &rct2hash(const rct::key &k) { return (const crypto::hash&)k; } + static inline const crypto::ec_point &rct2pt(const rct::key &k) { return (const crypto::ec_point&)k; } static inline bool operator==(const rct::key &k0, const crypto::public_key &k1) { return !crypto_verify_32(k0.bytes, (const unsigned char*)&k1); } static inline bool operator!=(const rct::key &k0, const crypto::public_key &k1) { return crypto_verify_32(k0.bytes, (const unsigned char*)&k1); } } diff --git a/src/rpc/core_rpc_server.cpp b/src/rpc/core_rpc_server.cpp index ed8b536d1b8..618ca8e4a73 100644 --- a/src/rpc/core_rpc_server.cpp +++ b/src/rpc/core_rpc_server.cpp @@ -40,6 +40,7 @@ using namespace epee; #include "common/updates.h" #include "common/download.h" #include "common/util.h" +#include "common/merge_sorted_vectors.h" #include "common/perf_timer.h" #include "int-util.h" #include "cryptonote_basic/cryptonote_format_utils.h" @@ -47,6 +48,7 @@ using namespace epee; #include "cryptonote_basic/cryptonote_basic_impl.h" #include "cryptonote_basic/merge_mining.h" #include "cryptonote_core/tx_sanity_check.h" +#include "fcmp_pp/curve_trees.h" #include "misc_language.h" #include "net/local_ip.h" #include "net/parse.h" @@ -65,6 +67,7 @@ using namespace epee; #define MAX_RESTRICTED_FAKE_OUTS_COUNT 40 #define MAX_RESTRICTED_GLOBAL_FAKE_OUTS_COUNT 5000 +#define MAX_RESTRICTED_PATHS_COUNT 50 #define OUTPUT_HISTOGRAM_RECENT_CUTOFF_RESTRICTION (3 * 86400) // 3 days max, the wallet requests 1.8 days @@ -629,6 +632,85 @@ namespace cryptonote END_SERIALIZE() }; //------------------------------------------------------------------------------------------------------------------------------ + static bool set_init_tree_sync_data(const uint64_t init_block_idx, const crypto::hash &init_hash, const core &m_core, COMMAND_RPC_GET_BLOCKS_FAST::init_tree_sync_data_t &init_tree_sync_data) + { + db_rtxn_guard txn_guard(&m_core.get_blockchain_storage().get_db()); + + CHECK_AND_ASSERT_MES(m_core.get_blockchain_storage().get_db().height() > init_block_idx, false, + "set_init_tree_sync_data: init_block_idx expected less than current chain height"); + CHECK_AND_ASSERT_MES(m_core.get_blockchain_storage().get_db().get_block_hash_from_height(init_block_idx) == init_hash, false, + "set_init_tree_sync_data: mismatched init_hash to init_block_idx"); + + init_tree_sync_data = COMMAND_RPC_GET_BLOCKS_FAST::init_tree_sync_data_t{}; + init_tree_sync_data.init_block_idx = init_block_idx; + init_tree_sync_data.init_block_hash = init_hash; + + // 1. Custom timelocked outputs created before sync_start_idx with last locked block >= sync_start_idx + const uint64_t sync_start_idx = init_block_idx + 1; + auto custom_outs_by_last_locked_block = m_core.get_blockchain_storage().get_db().get_custom_timelocked_outputs(sync_start_idx); + + // 2a. Coinbase unified outputs created between blocks [init_block_idx - 60, init_block_idx] inclusive + // 2b. Normal unified outputs created between blocks [init_block_idx - 10, init_block_idx] inclusive + auto outs_by_last_locked_block = m_core.get_blockchain_storage().get_recent_locked_outputs(init_block_idx); + + // 3. Combine all locked outputs into vec + auto &locked_outputs = init_tree_sync_data.locked_outputs; + locked_outputs.reserve(custom_outs_by_last_locked_block.size() + outs_by_last_locked_block.size()); + + // 3a. Iterate over all custom locked outs and check if last locked block is present in other outs. If so, combine. + for (auto &o : custom_outs_by_last_locked_block) + { + const uint64_t last_locked_block = o.first; + + auto outs_it = outs_by_last_locked_block.find(last_locked_block); + if (outs_it == outs_by_last_locked_block.end()) + { + locked_outputs.push_back({ last_locked_block, std::move(o.second) }); + continue; + } + + // Merge custom locked with other outs + const auto is_less = [](const fcmp_pp::UnifiedOutput &a, const fcmp_pp::UnifiedOutput &b) + { return a.unified_id < b.unified_id; }; + std::vector sorted_outs; + if (!tools::merge_sorted_vectors(o.second, outs_it->second, is_less, sorted_outs)) + { + LOG_ERROR("Failed to merge locked outs"); + return false; + } + + locked_outputs.push_back({ last_locked_block, std::move(sorted_outs) }); + } + + // 3b. Get the remaining locked outs + for (auto &o : outs_by_last_locked_block) + { + const uint64_t last_locked_block = o.first; + + auto custom_outs_it = custom_outs_by_last_locked_block.find(last_locked_block); + if (custom_outs_it != custom_outs_by_last_locked_block.end()) + { + // We've already added it in 3a above + continue; + } + + locked_outputs.push_back({ last_locked_block, std::move(o.second) }); + } + + // 3c. Sort locked outputs by last locked block + std::sort(locked_outputs.begin(), locked_outputs.end(), + [](const COMMAND_RPC_GET_BLOCKS_FAST::locked_outputs_t &a, const COMMAND_RPC_GET_BLOCKS_FAST::locked_outputs_t &b) + { return a.last_locked_block < b.last_locked_block; }); + + // 4. N leaf tuples and last chunk at each layer of the tree when init_block_idx was the last block in the chain + auto last_path = m_core.get_blockchain_storage().get_db().get_last_path(init_block_idx); + init_tree_sync_data.n_leaf_tuples = last_path.first; + init_tree_sync_data.last_path = std::move(last_path.second); + + MDEBUG("Set init tree sync data, blk " << init_tree_sync_data.init_block_idx << " , hash " << init_tree_sync_data.init_block_hash); + return true; + } + //------------------------------------------------------------------------------------------------------------------------------ bool core_rpc_server::on_get_blocks(const COMMAND_RPC_GET_BLOCKS_FAST::request& req, COMMAND_RPC_GET_BLOCKS_FAST::response& res, const connection_context *ctx) { RPC_TRACKER(get_blocks); @@ -671,6 +753,12 @@ namespace cryptonote return true; } + if (req.init_tree_sync && !get_blocks) + { + res.status = "Failed, must get blocks when requesting init tree sync data"; + return true; + } + res.pool_info_extent = COMMAND_RPC_GET_BLOCKS_FAST::NONE; if (get_pool) @@ -735,6 +823,14 @@ namespace cryptonote res.start_height = 0; res.current_height = last_block_height + 1; res.top_block_hash = last_block_hash; + + // Get the data necessary to start syncing the tree from the current tip + if (req.init_tree_sync && !set_init_tree_sync_data(last_block_height, last_block_hash, m_core, res.init_tree_sync_data)) + { + res.status = "Failed"; + return true; + } + res.status = CORE_RPC_STATUS_OK; return true; } @@ -755,7 +851,7 @@ namespace cryptonote } std::vector, std::vector > > > bs; - if(!m_core.find_blockchain_supplement(req.start_height, req.block_ids, bs, res.current_height, res.top_block_hash, res.start_height, req.prune, !req.no_miner_tx, max_blocks, COMMAND_RPC_GET_BLOCKS_FAST_MAX_TX_COUNT)) + if(!m_core.find_blockchain_supplement(req.start_height, req.block_ids, bs, res.current_height, res.top_block_hash, res.start_height, req.prune, !req.no_miner_tx, max_blocks, COMMAND_RPC_GET_BLOCKS_FAST_MAX_TX_COUNT, req.block_ids_skip_common_block)) { res.status = "Failed"; add_host_fail(ctx); @@ -809,6 +905,32 @@ namespace cryptonote MDEBUG("on_get_blocks: " << bs.size() << " blocks, " << ntxes << " txes, size " << size); } + if (req.init_tree_sync) + { + // Get the first hash in the result + if (res.blocks.empty()) + { + res.status = "Failed"; + return true; + } + + block b; + if(!parse_and_validate_block_from_blob(res.blocks.front().block, b)) + { + res.status = "Failed"; + return true; + } + + // Get the data necessary to start syncing the tree from the first returned block + const uint64_t init_block_idx = res.start_height == 0 ? 0 : (res.start_height - 1); + const crypto::hash init_block_hash = res.start_height == 0 ? get_block_hash(b) : b.prev_id; + if (!set_init_tree_sync_data(init_block_idx, init_block_hash, m_core, res.init_tree_sync_data)) + { + res.status = "Failed"; + return true; + } + } + res.status = CORE_RPC_STATUS_OK; return true; } @@ -1001,6 +1123,46 @@ namespace cryptonote return true; } //------------------------------------------------------------------------------------------------------------------------------ + bool core_rpc_server::on_get_path_by_unified_id_bin(const COMMAND_RPC_GET_PATH_BY_UNIFIED_ID_BIN::request& req, COMMAND_RPC_GET_PATH_BY_UNIFIED_ID_BIN::response& res, const connection_context *ctx) + { + RPC_TRACKER(get_outs); + bool r; + if (use_bootstrap_daemon_if_necessary(invoke_http_mode::BIN, "/get_path_by_unified_id.bin", req, res, r)) + return r; + + CHECK_PAYMENT_MIN1(req, res, req.unified_ids.size() * COST_PER_OUT, false); + + res.status = "Failed"; + + const bool restricted = m_restricted && ctx; + if (restricted) + { + if (req.unified_ids.size() > MAX_RESTRICTED_PATHS_COUNT) + { + res.status = "Too many paths requested"; + return true; + } + } + + try + { + std::vector leaf_idxs; + std::vector paths; + res.n_leaf_tuples = m_core.get_blockchain_storage().get_db().get_path_by_unified_id(req.unified_ids, req.as_of_n_blocks, leaf_idxs, paths); + res.paths.reserve(leaf_idxs.size()); + for (std::size_t i = 0; i < leaf_idxs.size(); ++i) + res.paths.emplace_back(COMMAND_RPC_GET_PATH_BY_UNIFIED_ID_BIN::response::path_entry{ leaf_idxs.at(i), std::move(paths.at(i)) }); + } + catch (...) + { + res.status = "Failed"; + return true; + } + + res.status = CORE_RPC_STATUS_OK; + return true; + } + //------------------------------------------------------------------------------------------------------------------------------ bool core_rpc_server::on_get_transactions(const COMMAND_RPC_GET_TRANSACTIONS::request& req, COMMAND_RPC_GET_TRANSACTIONS::response& res, const connection_context *ctx) { RPC_TRACKER(get_transactions); @@ -1232,12 +1394,26 @@ namespace cryptonote // output indices too if not in pool if (pool_tx_hashes.find(tx_hash) == pool_tx_hashes.end()) { - bool r = m_core.get_tx_outputs_gindexs(tx_hash, e.output_indices); - if (!r) + std::vector tx_outs_data; + try { + cryptonote::transaction _; + tx_outs_data = m_core.get_blockchain_storage().get_db().get_tx_output_data(tx_hash, _); + } + catch (...) + { + MERROR("Failed to get tx output data for " << tx_hash); res.status = "Failed"; return true; } + + e.output_indices.reserve(tx_outs_data.size()); + e.unified_ids.reserve(tx_outs_data.size()); + for (const auto &tx_out_data : tx_outs_data) + { + e.output_indices.push_back(tx_out_data.amount_index); + e.unified_ids.push_back(tx_out_data.unified_id); + } } } @@ -1390,10 +1566,11 @@ namespace cryptonote } res.sanity_check_failed = false; + crypto::hash txid{}; if (!skip_validation) { tx_verification_context tvc{}; - if(!m_core.handle_incoming_tx(tx_blob, tvc, (req.do_not_relay ? relay_method::none : relay_method::local), false) || tvc.m_verifivation_failed) + if(!m_core.handle_incoming_tx(tx_blob, tvc, (req.do_not_relay ? relay_method::none : relay_method::local), false, txid) || tvc.m_verifivation_failed) { res.status = "Failed"; std::string reason = ""; @@ -1438,10 +1615,20 @@ namespace cryptonote return true; } } + else + { + transaction tx; + if (!parse_and_validate_tx_from_blob(tx_blob, tx, txid)) + { + res.status = "Failed"; + res.reason = "Failed to parse tx"; + return true; + } + } NOTIFY_NEW_TRANSACTIONS::request r; r.txs.push_back(std::move(tx_blob)); - m_core.get_protocol()->relay_transactions(r, boost::uuids::nil_uuid(), epee::net_utils::zone::invalid, relay_method::local); + m_core.get_protocol()->relay_transactions(r, {txid}, boost::uuids::nil_uuid(), epee::net_utils::zone::invalid, relay_method::local); //TODO: make sure that tx has reached other nodes here, probably wait to receive reflections from other nodes res.status = CORE_RPC_STATUS_OK; return true; @@ -2046,7 +2233,9 @@ namespace cryptonote difficulty_type difficulty; std::vector tx_backlog; - if (!m_core.get_miner_data(res.major_version, res.height, prev_id, seed_hash, difficulty, res.median_weight, res.already_generated_coins, tx_backlog)) + uint8_t fcmp_pp_n_tree_layers; + crypto::ec_point fcmp_pp_tree_root{}; + if (!m_core.get_miner_data(res.major_version, res.height, prev_id, fcmp_pp_n_tree_layers, fcmp_pp_tree_root, seed_hash, difficulty, res.median_weight, res.already_generated_coins, tx_backlog)) { error_resp.code = CORE_RPC_ERROR_CODE_INTERNAL_ERROR; error_resp.message = "Internal error: failed to get miner data"; @@ -2065,6 +2254,8 @@ namespace cryptonote res.prev_id = string_tools::pod_to_hex(prev_id); res.seed_hash = string_tools::pod_to_hex(seed_hash); res.difficulty = cryptonote::hex(difficulty); + res.fcmp_pp_n_tree_layers = fcmp_pp_n_tree_layers; + res.fcmp_pp_tree_root = string_tools::pod_to_hex(fcmp_pp_tree_root); res.status = CORE_RPC_STATUS_OK; return true; @@ -3099,7 +3290,7 @@ namespace cryptonote CHECK_PAYMENT(req, res, COST_PER_FEE_ESTIMATE); { - m_core.get_blockchain_storage().get_dynamic_base_fee_estimate_2021_scaling(req.grace_blocks, res.fees); + m_core.get_blockchain_storage().get_dynamic_base_fee_estimate(req.grace_blocks, res.fees); res.fee = res.fees[0]; } res.quantization_mask = Blockchain::get_fee_quantization_mask(); @@ -3357,7 +3548,7 @@ namespace cryptonote NOTIFY_NEW_TRANSACTIONS::request r; r.txs.push_back(std::move(txblob)); const auto tx_relay = broadcasted ? relay_method::fluff : relay_method::local; - m_core.get_protocol()->relay_transactions(r, boost::uuids::nil_uuid(), epee::net_utils::zone::invalid, tx_relay); + m_core.get_protocol()->relay_transactions(r, {txid}, boost::uuids::nil_uuid(), epee::net_utils::zone::invalid, tx_relay); //TODO: make sure that tx has reached other nodes here, probably wait to receive reflections from other nodes } else diff --git a/src/rpc/core_rpc_server.h b/src/rpc/core_rpc_server.h index ce87730052d..56e2a8c0cce 100644 --- a/src/rpc/core_rpc_server.h +++ b/src/rpc/core_rpc_server.h @@ -108,6 +108,7 @@ namespace cryptonote MAP_URI_AUTO_BIN2("/gethashes.bin", on_get_hashes, COMMAND_RPC_GET_HASHES_FAST) MAP_URI_AUTO_BIN2("/get_o_indexes.bin", on_get_indexes, COMMAND_RPC_GET_TX_GLOBAL_OUTPUTS_INDEXES) MAP_URI_AUTO_BIN2("/get_outs.bin", on_get_outs_bin, COMMAND_RPC_GET_OUTPUTS_BIN) + MAP_URI_AUTO_BIN2("/get_path_by_unified_id.bin", on_get_path_by_unified_id_bin, COMMAND_RPC_GET_PATH_BY_UNIFIED_ID_BIN) MAP_URI_AUTO_JON2("/get_transactions", on_get_transactions, COMMAND_RPC_GET_TRANSACTIONS) MAP_URI_AUTO_JON2("/gettransactions", on_get_transactions, COMMAND_RPC_GET_TRANSACTIONS) MAP_URI_AUTO_JON2("/get_alt_blocks_hashes", on_get_alt_blocks_hashes, COMMAND_RPC_GET_ALT_BLOCKS_HASHES) @@ -205,6 +206,7 @@ namespace cryptonote bool on_mining_status(const COMMAND_RPC_MINING_STATUS::request& req, COMMAND_RPC_MINING_STATUS::response& res, const connection_context *ctx = NULL); bool on_get_outs_bin(const COMMAND_RPC_GET_OUTPUTS_BIN::request& req, COMMAND_RPC_GET_OUTPUTS_BIN::response& res, const connection_context *ctx = NULL); bool on_get_outs(const COMMAND_RPC_GET_OUTPUTS::request& req, COMMAND_RPC_GET_OUTPUTS::response& res, const connection_context *ctx = NULL); + bool on_get_path_by_unified_id_bin(const COMMAND_RPC_GET_PATH_BY_UNIFIED_ID_BIN::request& req, COMMAND_RPC_GET_PATH_BY_UNIFIED_ID_BIN::response& res, const connection_context *ctx = NULL); bool on_get_info(const COMMAND_RPC_GET_INFO::request& req, COMMAND_RPC_GET_INFO::response& res, const connection_context *ctx = NULL); bool on_get_net_stats(const COMMAND_RPC_GET_NET_STATS::request& req, COMMAND_RPC_GET_NET_STATS::response& res, const connection_context *ctx = NULL); bool on_save_bc(const COMMAND_RPC_SAVE_BC::request& req, COMMAND_RPC_SAVE_BC::response& res, const connection_context *ctx = NULL); diff --git a/src/rpc/core_rpc_server_commands_defs.h b/src/rpc/core_rpc_server_commands_defs.h index 60b48622f52..5a027ae940e 100644 --- a/src/rpc/core_rpc_server_commands_defs.h +++ b/src/rpc/core_rpc_server_commands_defs.h @@ -36,6 +36,7 @@ #include "cryptonote_basic/cryptonote_basic.h" #include "cryptonote_basic/difficulty.h" #include "crypto/hash.h" +#include "fcmp_pp/curve_trees.h" #include "rpc/rpc_handler.h" #include "common/varint.h" #include "common/perf_timer.h" @@ -100,8 +101,9 @@ inline const std::string get_rpc_status(const bool trusted_daemon, const std::st // whether they can talk to a given daemon without having to know in // advance which version they will stop working with // Don't go over 32767 for any of these +// TODO: make sure this matches RPC version expected in wallet2::check_version when FCMP++ is ready #define CORE_RPC_VERSION_MAJOR 3 -#define CORE_RPC_VERSION_MINOR 16 +#define CORE_RPC_VERSION_MINOR 17 #define MAKE_CORE_RPC_VERSION(major,minor) (((major)<<16)|(minor)) #define CORE_RPC_VERSION MAKE_CORE_RPC_VERSION(CORE_RPC_VERSION_MAJOR, CORE_RPC_VERSION_MINOR) @@ -191,7 +193,8 @@ inline const std::string get_rpc_status(const bool trusted_daemon, const std::st bool no_miner_tx; uint64_t pool_info_since; uint64_t max_block_count; - + bool init_tree_sync; + bool block_ids_skip_common_block; BEGIN_KV_SERIALIZE_MAP() KV_SERIALIZE_PARENT(rpc_access_request_base) KV_SERIALIZE_OPT(requested_info, (uint8_t)0) @@ -201,6 +204,8 @@ inline const std::string get_rpc_status(const bool trusted_daemon, const std::st KV_SERIALIZE_OPT(no_miner_tx, false) KV_SERIALIZE_OPT(pool_info_since, (uint64_t)0) KV_SERIALIZE_OPT(max_block_count, (uint64_t)0) + KV_SERIALIZE_OPT(init_tree_sync, false) + KV_SERIALIZE_OPT(block_ids_skip_common_block, false) END_KV_SERIALIZE_MAP() }; typedef epee::misc_utils::struct_init request; @@ -243,6 +248,51 @@ inline const std::string get_rpc_status(const bool trusted_daemon, const std::st FULL = 2 }; + struct locked_outputs_t + { + uint64_t last_locked_block; + std::vector outputs; + + bool operator==(const locked_outputs_t& other) const + { + return last_locked_block == other.last_locked_block && outputs == other.outputs; + } + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(last_locked_block) + KV_SERIALIZE_CONTAINER_POD_AS_BLOB(outputs) + END_KV_SERIALIZE_MAP() + }; + + struct init_tree_sync_data_t + { + uint64_t init_block_idx{0}; + crypto::hash init_block_hash{crypto::null_hash}; + /// n leaf tuples in the chain when the chain tip was block init_block_idx + uint64_t n_leaf_tuples{0}; + /// the last full path in the tree when the chain tip was block init_block_idx + fcmp_pp::CompressedPath last_path; + /// outputs created before init_block_idx with last locked block >= init_block_idx + std::vector locked_outputs; + + bool operator==(const init_tree_sync_data_t& other) const + { + return init_block_idx == other.init_block_idx + && init_block_hash == other.init_block_hash + && n_leaf_tuples == other.n_leaf_tuples + && last_path == other.last_path + && locked_outputs == other.locked_outputs; + } + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(init_block_idx) + KV_SERIALIZE_VAL_POD_AS_BLOB(init_block_hash) + KV_SERIALIZE(n_leaf_tuples) + KV_SERIALIZE(last_path) + KV_SERIALIZE(locked_outputs) + END_KV_SERIALIZE_MAP() + }; + struct response_t: public rpc_access_response_base { std::vector blocks; @@ -255,6 +305,7 @@ inline const std::string get_rpc_status(const bool trusted_daemon, const std::st std::vector added_pool_txs; std::vector remaining_added_pool_txids; std::vector removed_pool_txids; + init_tree_sync_data_t init_tree_sync_data; BEGIN_KV_SERIALIZE_MAP() KV_SERIALIZE_PARENT(rpc_access_response_base) @@ -274,6 +325,7 @@ inline const std::string get_rpc_status(const bool trusted_daemon, const std::st { KV_SERIALIZE_CONTAINER_POD_AS_BLOB(removed_pool_txids) } + KV_SERIALIZE_OPT(init_tree_sync_data, init_tree_sync_data_t{}) END_KV_SERIALIZE_MAP() }; typedef epee::misc_utils::struct_init response; @@ -389,6 +441,7 @@ inline const std::string get_rpc_status(const bool trusted_daemon, const std::st uint64_t block_timestamp; uint64_t received_timestamp; std::vector output_indices; + std::vector unified_ids; bool relayed; BEGIN_KV_SERIALIZE_MAP() @@ -406,6 +459,7 @@ inline const std::string get_rpc_status(const bool trusted_daemon, const std::st KV_SERIALIZE(confirmations) KV_SERIALIZE(block_timestamp) KV_SERIALIZE(output_indices) + KV_SERIALIZE(unified_ids) } else { @@ -596,6 +650,47 @@ inline const std::string get_rpc_status(const bool trusted_daemon, const std::st typedef epee::misc_utils::struct_init response; }; //----------------------------------------------- + struct COMMAND_RPC_GET_PATH_BY_UNIFIED_ID_BIN + { + struct request_t: public rpc_access_request_base + { + uint64_t as_of_n_blocks; + std::vector unified_ids; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE_PARENT(rpc_access_request_base) + KV_SERIALIZE_OPT(as_of_n_blocks, (uint64_t)0) + KV_SERIALIZE(unified_ids) + END_KV_SERIALIZE_MAP() + }; + typedef epee::misc_utils::struct_init request; + + struct response_t: public rpc_access_response_base + { + uint64_t n_leaf_tuples; + + struct path_entry + { + uint64_t leaf_idx{0}; + fcmp_pp::CompressedPath path; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(leaf_idx) + KV_SERIALIZE(path) + END_KV_SERIALIZE_MAP() + }; + + std::vector paths; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE_PARENT(rpc_access_response_base) + KV_SERIALIZE(n_leaf_tuples) + KV_SERIALIZE(paths) + END_KV_SERIALIZE_MAP() + }; + typedef epee::misc_utils::struct_init response; + }; + //----------------------------------------------- struct COMMAND_RPC_SEND_RAW_TX { struct request_t: public rpc_access_request_base @@ -998,6 +1093,8 @@ inline const std::string get_rpc_status(const bool trusted_daemon, const std::st uint8_t major_version; uint64_t height; std::string prev_id; + uint8_t fcmp_pp_n_tree_layers; + std::string fcmp_pp_tree_root; std::string seed_hash; std::string difficulty; uint64_t median_weight; @@ -1023,6 +1120,11 @@ inline const std::string get_rpc_status(const bool trusted_daemon, const std::st KV_SERIALIZE(major_version) KV_SERIALIZE(height) KV_SERIALIZE(prev_id) + if (major_version >= HF_VERSION_FCMP_PLUS_PLUS) + { + KV_SERIALIZE(fcmp_pp_n_tree_layers) + KV_SERIALIZE(fcmp_pp_tree_root) + } KV_SERIALIZE(seed_hash) KV_SERIALIZE(difficulty) KV_SERIALIZE(median_weight) diff --git a/src/rpc/daemon_handler.cpp b/src/rpc/daemon_handler.cpp index 8a1f5cb47b9..b6ff71daf70 100644 --- a/src/rpc/daemon_handler.cpp +++ b/src/rpc/daemon_handler.cpp @@ -373,7 +373,8 @@ namespace rpc tx_verification_context tvc = AUTO_VAL_INIT(tvc); - if(!m_core.handle_incoming_tx(tx_blob, tvc, (relay ? relay_method::local : relay_method::none), false) || tvc.m_verifivation_failed) + crypto::hash txid; + if(!m_core.handle_incoming_tx(tx_blob, tvc, (relay ? relay_method::local : relay_method::none), false, txid) || tvc.m_verifivation_failed) { if (tvc.m_verifivation_failed) { @@ -455,7 +456,7 @@ namespace rpc NOTIFY_NEW_TRANSACTIONS::request r; r.txs.push_back(std::move(tx_blob)); - m_core.get_protocol()->relay_transactions(r, boost::uuids::nil_uuid(), epee::net_utils::zone::invalid, relay_method::local); + m_core.get_protocol()->relay_transactions(r, {txid}, boost::uuids::nil_uuid(), epee::net_utils::zone::invalid, relay_method::local); //TODO: make sure that tx has reached other nodes here, probably wait to receive reflections from other nodes res.status = Message::STATUS_OK; @@ -847,7 +848,7 @@ namespace rpc { res.hard_fork_version = m_core.get_blockchain_storage().get_current_hard_fork_version(); - m_core.get_blockchain_storage().get_dynamic_base_fee_estimate_2021_scaling(req.num_grace_blocks, res.fees); + m_core.get_blockchain_storage().get_dynamic_base_fee_estimate(req.num_grace_blocks, res.fees); res.estimated_base_fee = res.fees.at(0); { diff --git a/src/rpc/zmq_pub.cpp b/src/rpc/zmq_pub.cpp index cc9705d35eb..70a30e42662 100644 --- a/src/rpc/zmq_pub.cpp +++ b/src/rpc/zmq_pub.cpp @@ -60,7 +60,7 @@ namespace constexpr const char txpool_signal[] = "tx_signal"; using chain_writer = void(epee::byte_stream&, std::uint64_t, epee::span); - using miner_writer = void(epee::byte_stream&, uint8_t, uint64_t, const crypto::hash&, const crypto::hash&, cryptonote::difficulty_type, uint64_t, uint64_t, const std::vector&); + using miner_writer = void(epee::byte_stream&, uint8_t, uint64_t, const crypto::hash&, const uint8_t, const crypto::ec_point&, const crypto::hash&, cryptonote::difficulty_type, uint64_t, uint64_t, const std::vector&); using txpool_writer = void(epee::byte_stream&, epee::span); template @@ -126,6 +126,8 @@ namespace uint8_t major_version; uint64_t height; const crypto::hash& prev_id; + const uint8_t fcmp_pp_n_tree_layers; + const crypto::ec_point& fcmp_pp_tree_root; const crypto::hash& seed_hash; cryptonote::difficulty_type diff; uint64_t median_weight; @@ -170,6 +172,11 @@ namespace INSERT_INTO_JSON_OBJECT(dest, major_version, self.major_version); INSERT_INTO_JSON_OBJECT(dest, height, self.height); INSERT_INTO_JSON_OBJECT(dest, prev_id, self.prev_id); + if (self.major_version >= HF_VERSION_FCMP_PLUS_PLUS) + { + INSERT_INTO_JSON_OBJECT(dest, fcmp_pp_n_tree_layers, self.fcmp_pp_n_tree_layers); + INSERT_INTO_JSON_OBJECT(dest, fcmp_pp_tree_root, self.fcmp_pp_tree_root); + } INSERT_INTO_JSON_OBJECT(dest, seed_hash, self.seed_hash); INSERT_INTO_JSON_OBJECT(dest, difficulty, cryptonote::hex(self.diff)); INSERT_INTO_JSON_OBJECT(dest, median_weight, self.median_weight); @@ -198,9 +205,9 @@ namespace json_pub(buf, minimal_chain{height, blocks}); } - void json_miner_data(epee::byte_stream& buf, uint8_t major_version, uint64_t height, const crypto::hash& prev_id, const crypto::hash& seed_hash, cryptonote::difficulty_type diff, uint64_t median_weight, uint64_t already_generated_coins, const std::vector& tx_backlog) + void json_miner_data(epee::byte_stream& buf, uint8_t major_version, uint64_t height, const crypto::hash& prev_id, const uint8_t fcmp_pp_n_tree_layers, const crypto::ec_point& fcmp_pp_tree_root, const crypto::hash& seed_hash, cryptonote::difficulty_type diff, uint64_t median_weight, uint64_t already_generated_coins, const std::vector& tx_backlog) { - json_pub(buf, miner_data{major_version, height, prev_id, seed_hash, diff, median_weight, already_generated_coins, tx_backlog}); + json_pub(buf, miner_data{major_version, height, prev_id, fcmp_pp_n_tree_layers, fcmp_pp_tree_root, seed_hash, diff, median_weight, already_generated_coins, tx_backlog}); } // boost::adaptors are in place "views" - no copy/move takes place @@ -480,7 +487,7 @@ std::size_t zmq_pub::send_chain_main(const std::uint64_t height, const epee::spa return 0; } -std::size_t zmq_pub::send_miner_data(uint8_t major_version, uint64_t height, const crypto::hash& prev_id, const crypto::hash& seed_hash, difficulty_type diff, uint64_t median_weight, uint64_t already_generated_coins, const std::vector& tx_backlog) +std::size_t zmq_pub::send_miner_data(uint8_t major_version, uint64_t height, const crypto::hash& prev_id, const uint8_t fcmp_pp_n_tree_layers, const crypto::ec_point& fcmp_pp_tree_root, const crypto::hash& seed_hash, difficulty_type diff, uint64_t median_weight, uint64_t already_generated_coins, const std::vector& tx_backlog) { boost::unique_lock guard{sync_}; @@ -491,7 +498,7 @@ std::size_t zmq_pub::send_miner_data(uint8_t major_version, uint64_t height, con { if (sub) { - auto messages = make_pubs(subs_copy, miner_contexts, major_version, height, prev_id, seed_hash, diff, median_weight, already_generated_coins, tx_backlog); + auto messages = make_pubs(subs_copy, miner_contexts, major_version, height, prev_id, fcmp_pp_n_tree_layers, fcmp_pp_tree_root, seed_hash, diff, median_weight, already_generated_coins, tx_backlog); guard.lock(); return send_messages(relay_.get(), messages); } @@ -529,11 +536,11 @@ void zmq_pub::chain_main::operator()(const std::uint64_t height, epee::span& tx_backlog) const +void zmq_pub::miner_data::operator()(uint8_t major_version, uint64_t height, const crypto::hash& prev_id, const uint8_t fcmp_pp_n_tree_layers, const crypto::ec_point& fcmp_pp_tree_root, const crypto::hash& seed_hash, difficulty_type diff, uint64_t median_weight, uint64_t already_generated_coins, const std::vector& tx_backlog) const { const std::shared_ptr self = self_.lock(); if (self) - self->send_miner_data(major_version, height, prev_id, seed_hash, diff, median_weight, already_generated_coins, tx_backlog); + self->send_miner_data(major_version, height, prev_id, fcmp_pp_n_tree_layers, fcmp_pp_tree_root, seed_hash, diff, median_weight, already_generated_coins, tx_backlog); else MERROR("Unable to send ZMQ/Pub - ZMQ server destroyed"); } diff --git a/src/rpc/zmq_pub.h b/src/rpc/zmq_pub.h index add9481c62b..154c5ad6c84 100644 --- a/src/rpc/zmq_pub.h +++ b/src/rpc/zmq_pub.h @@ -37,6 +37,7 @@ #include #include +#include "crypto/crypto.h" #include "cryptonote_basic/fwd.h" #include "net/zmq.h" #include "span.h" @@ -94,7 +95,7 @@ class zmq_pub /*! Send a `ZMQ_PUB` notification for a new miner data. Thread-safe. \return Number of ZMQ messages sent to relay. */ - std::size_t send_miner_data(uint8_t major_version, uint64_t height, const crypto::hash& prev_id, const crypto::hash& seed_hash, difficulty_type diff, uint64_t median_weight, uint64_t already_generated_coins, const std::vector& tx_backlog); + std::size_t send_miner_data(uint8_t major_version, uint64_t height, const crypto::hash& prev_id, const uint8_t fcmp_pp_n_tree_layers, const crypto::ec_point& fcmp_pp_tree_root, const crypto::hash& seed_hash, difficulty_type diff, uint64_t median_weight, uint64_t already_generated_coins, const std::vector& tx_backlog); /*! Send a `ZMQ_PUB` notification for new tx(es) being added to the local pool. Thread-safe. @@ -112,7 +113,7 @@ class zmq_pub struct miner_data { std::weak_ptr self_; - void operator()(uint8_t major_version, uint64_t height, const crypto::hash& prev_id, const crypto::hash& seed_hash, difficulty_type diff, uint64_t median_weight, uint64_t already_generated_coins, const std::vector& tx_backlog) const; + void operator()(uint8_t major_version, uint64_t height, const crypto::hash& prev_id, const uint8_t fcmp_pp_n_tree_layers, const crypto::ec_point& fcmp_pp_tree_root, const crypto::hash& seed_hash, difficulty_type diff, uint64_t median_weight, uint64_t already_generated_coins, const std::vector& tx_backlog) const; }; //! Callable for `send_txpool_add` with weak ownership to `zmq_pub` object. diff --git a/src/serialization/crypto.h b/src/serialization/crypto.h index 57f05490ea0..72052336702 100644 --- a/src/serialization/crypto.h +++ b/src/serialization/crypto.h @@ -37,6 +37,7 @@ #include "crypto/chacha.h" #include "crypto/crypto.h" #include "crypto/hash.h" +#include "mx25519.h" // read template