diff --git a/.gitmodules b/.gitmodules index 40fb2b826e8..66d6886bf4e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -21,3 +21,6 @@ path = external/mx25519 url = https://github.com/jeffro256/mx25519 branch = unclamped +[submodule "external/equix"] + path = external/equix + url = https://github.com/tevador/equix diff --git a/CMakeLists.txt b/CMakeLists.txt index e7abbbe1c06..63c0aa77f71 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -381,6 +381,7 @@ if(NOT MANUAL_SUBMODULES) check_submodule(external/randomx) check_submodule(external/supercop) check_submodule(external/mx25519) + check_submodule(external/equix) endif() endif() @@ -453,7 +454,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 external/mx25519/include) +include_directories(external/rapidjson/include external/easylogging++ src contrib/epee/include external external/supercop/include external/mx25519/include external/equix/include) if(MINGW) set(DEFAULT_STATIC true) diff --git a/docs/POWER.md b/docs/POWER.md new file mode 100644 index 00000000000..d916738fa9e --- /dev/null +++ b/docs/POWER.md @@ -0,0 +1,134 @@ +# PoWER + +Proof-of-Work-Enabled Relay (PoWER) is a [client puzzle protocol](https://en.wikipedia.org/wiki/Client_Puzzle_Protocol) meant to mitigate denial-of-service (DoS) attacks against Monero nodes caused by spam transactions. Monero nodes provide challenges that require clients/peers to provide a solution in order to relay high-input transactions. + +This document contains instructions on how to follow the protocol. + +- [Background](#background) +- [Definitions and notes](#definitions-and-notes) +- [Calculating PoWER challenges and solutions](#calculating-power-challenges-solutions) + - [Challenge](#challenge) + - [RPC](#rpc) + - [P2P](#p2p) + - [Solution](#solution) + - [Equi-X](#equi-x) + - [Difficulty](#difficulty) + +## Background + +Currently, verification of FCMP++ transactions with many inputs (e.g. 128-input transactions) can take several seconds on high-end hardware, while creation of invalid transactions is almost instantaneous. An attacker can exploit this asymmetry by spamming nodes with invalid transactions. + +PoWER adds a computational cost by requiring Proof-of-Work (PoW) to be performed to enable relaying of high-input transactions. + +## Definitions and notes + +| Parameter | Value | Description | +|--------------------------|----------------|-------------| +| `INPUT_THRESHOLD` | 8 | PoWER is required for transactions with input counts greater than this. Transaction with input counts less than or equal to this value can skip PoWER. +| `HEIGHT_WINDOW` | 2 | Amount of block hashes that are valid as input for RPC PoWER challenge construction. +| `DIFFICULTY` | 100 | Fixed value used for difficulty calculation. +| `PERSONALIZATION_STRING` | "Monero PoWER" | Personalization string used in PoWER related functions. + +- Concatenation of bytes is denoted by `||`. +- All operations converting between integers and bytes are in little endian encoding. + +## Calculating PoWER challenges and solutions + +[Equi-X](https://github.com/tevador/equix) is the PoW algorithm used for PoWER challenges and solutions. + +Equi-X is a CPU-friendly client-puzzle that takes in a ["challenge" (bytes)](https://github.com/tevador/equix/blob/c0b0d2bd210b870b3077f487a3705dfa7578208f/include/equix.h#L121) and outputs a [16-byte array "solution"](https://github.com/tevador/equix/blob/c0b0d2bd210b870b3077f487a3705dfa7578208f/include/equix.h#L28-L30). + +### Challenge + +Challenges are constructed differently depending on the interface. The below sections explain each interface. + +#### RPC + +For RPC (and ZMQ-RPC): + +``` +challenge = (PERSONALIZATION_STRING || tx_prefix_hash || recent_block_hash || nonce) +``` + +where: + +- `PERSONALIZATION_STRING` is the string "Monero PoWER" as bytes. +- `tx_prefix_hash` is the transaction prefix hash of the transaction being relayed. +- `recent_block_hash` is a hash of a block within the last `HEIGHT_WINDOW` blocks. +- `nonce` is a 32-bit unsigned integer. + +In the Monero codebase, this is the `create_challenge_rpc` function. + +RPC endpoints that relay transactions contain fields where this data must be passed alongside the transaction. + +Note that these fields are not required when any of the following are true: +- The transaction has less than or equal to `INPUT_THRESHOLD` inputs. +- The transaction orignates from a local/trusted source (unrestricted RPC, localhost, etc) + +#### P2P + +For P2P: + +``` +challenge = (PERSONALIZATION_STRING || seed || difficulty || nonce) +``` + +where: + +- `PERSONALIZATION_STRING` is the string "Monero PoWER" as bytes. +- `seed` is a random 128-bit unsigned integer generated for each connection. +- `difficulty` is the 32-bit unsigned integer difficulty parameter the node requires to be used. +- `nonce` is a 32-bit unsigned integer. + +In the Monero codebase, this is the `create_challenge_p2p` function. + +`seed` and `difficulty` are provided by nodes in the initial P2P handshake message. Note that in the Monero codebase, `seed` is split into two 64-bit unsigned integers representing the low and high bits. + +`nonce` should be adjusted until a valid Equi-X `solution` is produced that passes the difficulty formula with `difficulty`, then a `NOTIFY_POWER_SOLUTION` message should be sent containing the `solution` and `nonce`. This will enable high input transaction relay for that connection. + +## Solution + +A PoWER solution has 2 requirements: + +1. It must be a valid Equi-X solution. +2. It must pass a difficulty formula. + +The `nonce` in challenges should be adjusted until both 1 and 2 are satisfied. + +### Equi-X + +For 1, create an Equi-X `solution` for the `challenge` data created previously. + +Note that `equix_solve` does not always create valid solutions. The `challenge` for +all interfaces contain a `nonce` field that should be adjusted until `equix_solve` +produces valid solution(s). + +### Difficulty + +For 2, a difficulty scalar must be created with: + +``` +scalar = to_le_bytes(blake2b_32(PERSONALIZATION_STRING || challenge || solution)) +``` + +where: + +- `to_le_bytes` converts a 4-byte array into a 32-bit unsigned integer in little endian order. +- `blake2b_32` is a `blake2b` hash set to a 32-bit output. +- `PERSONALIZATION_STRING` is the string "Monero PoWER". +- `challenge` are the full challenge bytes. +- `solution` are the Equi-X solution bytes. + +In the Monero codebase, this is the `create_difficulty_scalar` function. + +`scalar` must now pass the following difficulty formula: + +``` +scalar * difficulty <= MAX_UINT32 +``` + +where: + +- `difficulty` is either a constant (`DIFFICULTY`) for RPC, or the `difficulty` received from a peer for P2P. + +In the Monero codebase, this is the `check_difficulty` function. \ No newline at end of file diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index b16723b63f0..429ce92ebbd 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -68,3 +68,4 @@ add_subdirectory(easylogging++) add_subdirectory(qrcodegen) add_subdirectory(randomx EXCLUDE_FROM_ALL) add_subdirectory(mx25519) +add_subdirectory(equix) diff --git a/external/equix b/external/equix new file mode 160000 index 00000000000..c0b0d2bd210 --- /dev/null +++ b/external/equix @@ -0,0 +1 @@ +Subproject commit c0b0d2bd210b870b3077f487a3705dfa7578208f diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index 0046823f5df..93f5be0efce 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -41,6 +41,7 @@ set(common_sources password.cpp perf_timer.cpp pruning.cpp + power.cpp spawn.cpp threadpool.cpp updates.cpp @@ -75,6 +76,7 @@ target_link_libraries(common ${Boost_REGEX_LIBRARY} ${Boost_CHRONO_LIBRARY} PRIVATE + equix_static ${OPENSSL_LIBRARIES} ${EXTRA_LIBRARIES}) target_include_directories(common diff --git a/src/common/power.cpp b/src/common/power.cpp new file mode 100644 index 00000000000..78c51b78f6c --- /dev/null +++ b/src/common/power.cpp @@ -0,0 +1,281 @@ +// PoWER uses Equi-X: +// . +// +// Equi-X is: +// Copyright (c) 2020 tevador +// +// and licensed under the terms of the LGPL version 3.0: +// + +// Copyright (c) 2019-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 "power.h" + +//local headers +#include "crypto/blake2b.h" +#include "int-util.h" + +//third party headers +#include +#include +#include +#include +#include + +//standard headers +#include +#include +#include + +//forward declarations + +namespace tools +{ + namespace power + { + using boost::multiprecision::uint128_t; + + namespace { + template + bool verify_equix_solution( + const std::array& challenge, + const solution_array solution + ) { + equix_ctx* ctx = equix_alloc(EQUIX_CTX_VERIFY); + + if (ctx == nullptr) + { + return false; + } + + equix_result result = equix_verify( + ctx, + challenge.data(), + challenge.size(), + reinterpret_cast(solution.data()) + ); + + equix_free(ctx); + + return result == EQUIX_OK; + } + + // Generic Equi-X + difficulty solving function. + template + solution_data solve( + std::array challenge, + const uint32_t difficulty, + const size_t nonce_index + ) { + equix_ctx* ctx = equix_alloc(EQUIX_CTX_SOLVE); + + if (ctx == nullptr) + { + throw std::runtime_error("equix_alloc returned nullptr"); + } + + equix_solution solutions[EQUIX_MAX_SOLS]; + solution_array solution; + + for (uint32_t nonce = 0;; ++nonce) { + const uint32_t n = swap32le(nonce); + memcpy(challenge.data() + nonce_index, &n, sizeof(n)); + + const int solution_count = equix_solve(ctx, challenge.data(), challenge.size(), solutions); + + if (solution_count <= 0) + { + continue; + } + + for (int i = 0; i < solution_count; ++i) { + memcpy(solution.data(), solutions[i].idx, sizeof(solution)); + uint32_t scalar = create_difficulty_scalar(challenge.data(), challenge.size(), solution); + + if (check_difficulty(scalar, difficulty)) + { + equix_free(ctx); + solution_data s; + s.challenge = std::vector(challenge.begin(), challenge.end()); + s.solution = solution; + s.nonce = nonce; + return s; + } + } + } + + equix_free(ctx); + throw std::runtime_error("practically unreachable for realistic difficulties"); + } + + // Generic Equi-X + difficulty verifying function. + template + bool verify( + const std::array challenge, + const uint32_t difficulty, + const solution_array solution + ) { + if (!verify_equix_solution(challenge, solution)) + { + return false; + } + + const uint32_t scalar = create_difficulty_scalar(challenge.data(), challenge.size(), solution); + return (check_difficulty(scalar, difficulty)); + } + } + + uint32_t create_difficulty_scalar( + const void* challenge, + const size_t challenge_size, + const solution_array solution + ) noexcept { + assert(challenge != nullptr); + assert(challenge_size != 0); + + blake2b_state state; + blake2b_init(&state, 4); + blake2b_update(&state, PERSONALIZATION_STRING.data(), PERSONALIZATION_STRING.size()); + blake2b_update(&state, challenge, challenge_size); + blake2b_update(&state, reinterpret_cast(solution.data()), sizeof(solution)); + + uint8_t out[4]; + blake2b_final(&state, out, 4); + + uint32_t scalar; + memcpy_swap32le(&scalar, out, sizeof(scalar)); + + return scalar; + } + + constexpr bool check_difficulty(uint32_t scalar, uint32_t difficulty) noexcept + { + const uint64_t product = uint64_t(scalar) * uint64_t(difficulty); + return product <= std::numeric_limits::max(); + } + + challenge_rpc create_challenge_rpc( + const crypto::hash tx_prefix_hash, + const crypto::hash recent_block_hash, + const uint32_t nonce + ) noexcept { + challenge_rpc out {}; + + memcpy(out.data(), PERSONALIZATION_STRING.data(), PERSONALIZATION_STRING.size()); + memcpy(out.data() + 12, reinterpret_cast(&tx_prefix_hash), 32); + memcpy(out.data() + 44, reinterpret_cast(&recent_block_hash), 32); + + const uint32_t n = swap32le(nonce); + memcpy(out.data() + 76, &n, sizeof(n)); + + return out; + } + + challenge_p2p create_challenge_p2p( + const uint64_t seed, + const uint64_t seed_top64, + const uint32_t difficulty, + const uint32_t nonce + ) noexcept { + challenge_p2p out {}; + + memcpy(out.data(), PERSONALIZATION_STRING.data(), PERSONALIZATION_STRING.size()); + + const uint64_t low = swap64le(seed); + const uint64_t high = swap64le(seed_top64); + const uint128_t nonce_128 = (uint128_t(high) << 64) | low; + memcpy(out.data() + 12, &nonce_128, sizeof(nonce_128)); + + const uint32_t d = swap32le(difficulty); + memcpy(out.data() + 28, &d, sizeof(d)); + + const uint32_t n = swap32le(nonce); + memcpy(out.data() + 32, &n, sizeof(n)); + + return out; + } + + solution_data solve_rpc( + const crypto::hash& tx_prefix_hash, + const crypto::hash& recent_block_hash, + const uint32_t difficulty + ) { + challenge_rpc challenge = + create_challenge_rpc(tx_prefix_hash, recent_block_hash, 0); + + return solve(challenge, difficulty, 76); + } + + solution_data solve_p2p( + uint64_t seed, + uint64_t seed_top64, + uint32_t difficulty + ) { + challenge_p2p challenge = + create_challenge_p2p(seed, seed_top64, difficulty, 0); + + return solve(challenge, difficulty, 28); + } + + bool verify_rpc( + const crypto::hash& tx_prefix_hash, + const crypto::hash& recent_block_hash, + const uint32_t nonce, + const uint32_t difficulty, + const solution_array solution + ) { + challenge_rpc challenge = create_challenge_rpc( + tx_prefix_hash, + recent_block_hash, + nonce + ); + + return verify(challenge, difficulty, solution); + } + + bool verify_p2p( + const uint64_t seed, + const uint64_t seed_top64, + const uint32_t difficulty, + const uint32_t nonce, + const solution_array solution + ) { + challenge_p2p challenge = create_challenge_p2p( + seed, + seed_top64, + difficulty, + nonce + ); + + return verify(challenge, difficulty, solution); + } + + } // namespace power +} // namespace tools diff --git a/src/common/power.h b/src/common/power.h new file mode 100644 index 00000000000..693bab3cdc6 --- /dev/null +++ b/src/common/power.h @@ -0,0 +1,267 @@ +// PoWER uses Equi-X: +// . +// +// Equi-X is: +// Copyright (c) 2020 tevador +// +// and licensed under the terms of the LGPL version 3.0: +// + +// Copyright (c) 2019-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/hash.h" + +//third party headers +#include + +//standard headers +#include +#include +#include +#include +#include + +//forward declarations + +namespace tools +{ + // RPC related code apply to both the RPC and ZMQ-RPC interfaces. + namespace power + { + // Ban score for peers that either: + // - attempt to send high-input transactions without PoWER. + // - send an invalid or malformed PoWER solution. + inline constexpr size_t BAN_SCORE = 5; + + // Input counts greater than this require PoWER. + inline constexpr size_t INPUT_THRESHOLD = 8; + + // Number of recent block hashes viable for RPC. + inline constexpr size_t HEIGHT_WINDOW = 2; + + // Fixed difficulty for the difficulty formula. + // + // Target time = ~1s of single-threaded computation. + // The difficulty value and computation time have a quadratic relationship. + // Reference values; value of machines are measured in seconds: + // + // | Difficulty | Raspberry Pi 5 | Ryzen 5950x | Mac mini M4 | + // |------------|----------------|-------------|-------------| + // | 0 | 0.024 | 0.006 | 0.005 | + // | 25 | 0.307 | 0.076 | 0.067 | + // | 50 | 0.832 | 0.207 | 0.187 | + // | 75 | 1.654 | 0.395 | 0.373 | + // | 100 | 2.811 | 0.657 | 0.611 | + // | 125 | 4.135 | 0.995 | 0.918 | + // | 150 | 5.740 | 1.397 | 1.288 | + // | 175 | 7.740 | 1.868 | 1.682 | + // | 200 | 9.935 | 2.365 | 2.140 | + // | 225 | 12.279 | 2.892 | 2.645 | + // | 250 | 14.855 | 3.573 | 3.226 | + // | 275 | 17.736 | 4.378 | 3.768 | + // | 300 | 20.650 | 5.116 | 4.422 | + inline constexpr uint32_t DIFFICULTY = 100; + + // Max difficulty value. + // + // Technically, nodes can be modified to send lower/higher difficulties in P2P. + // A vanilla node will adjust accordingly; it can and will solve a lower/higher difficulty challenge. + // This is the max valid difficulty requested from a peer before the connection is dropped. + inline constexpr uint32_t MAX_DIFFICULTY = DIFFICULTY * 2; + + // Personalization string used in PoWER hashes. + inline constexpr std::string_view PERSONALIZATION_STRING = "Monero PoWER"; + + // (PERSONALIZATION_STRING || tx_prefix_hash || recent_block_hash || nonce) + inline constexpr size_t CHALLENGE_SIZE_RPC = + PERSONALIZATION_STRING.size() + + sizeof(crypto::hash) + + sizeof(crypto::hash) + + sizeof(uint32_t); + + // (PERSONALIZATION_STRING || seed || seed_top64 || difficulty || nonce) + inline constexpr size_t CHALLENGE_SIZE_P2P = + PERSONALIZATION_STRING.size() + + sizeof(uint64_t) + + sizeof(uint64_t) + + sizeof(uint32_t) + + sizeof(uint32_t); + + // Challenge byte array for RPC. + typedef std::array challenge_rpc; + + // Challenge byte array for P2P. + typedef std::array challenge_p2p; + + // Equi-X solution in byte array form. + typedef std::array solution_array; + + static_assert(PERSONALIZATION_STRING.size() == 12, "Implementation assumes 12 bytes"); + static_assert(sizeof(challenge_rpc) == 80, "Implementation assumes 80 bytes"); + static_assert(sizeof(challenge_p2p) == 36, "Implementation assumes 36 bytes"); + static_assert(sizeof(solution_array) == sizeof(equix_solution), "Implementation assumes 16 bytes"); + static_assert(sizeof(crypto::hash) == 32, "Implementation assumes 32 bytes"); + + // PoWER solution with context data. + struct solution_data + { + // Challenge bytes. + std::vector challenge; + // Equi-X solution bytes. + solution_array solution; + // Correct nonce required for the solution. + uint32_t nonce; + }; + + /** + * @brief Create the difficulty scalar used for `check_difficulty`. + * + * @param challenge Pointer to the challenge data. + * @param challenge_size Size of the challenge. + * @param solution An Equi-X solution. + */ + uint32_t create_difficulty_scalar( + const void* challenge, + const size_t challenge_size, + const solution_array solution + ) noexcept ; + + /** + * @brief Check if a PoWER solution satisfies a difficulty. + * + * @param scalar The PoWER solution as a scalar using `create_difficulty_scalar`. + * @param difficulty The difficulty parameter. + * + * @return - true if the difficulty check passes, false otherwise. + */ + constexpr bool check_difficulty(const uint32_t scalar, uint32_t difficulty) noexcept; + + /** + * @brief Create a PoWER challenge for RPC. + * + * @param tx_prefix_hash Hash of transaction prefix. + * @param recent_block_hash Block hash within the last POWER_HEIGHT_WINDOW blocks. + * @param nonce The nonce parameter. + * + * @return PoWER RPC challenge as bytes. + */ + std::array create_challenge_rpc( + const crypto::hash tx_prefix_hash, + const crypto::hash recent_block_hash, + const uint32_t nonce + ) noexcept; + + /** + * @brief Create a PoWER challenge for P2P. + * + * @param seed Low bytes of challenge seed. + * @param seed_top64 High bytes of challenge seed. + * @param difficulty The difficulty parameter. + * @param nonce The nonce parameter. + * + * @return PoWER P2P challenge as bytes. + */ + std::array create_challenge_p2p( + const uint64_t seed, + const uint64_t seed_top64, + const uint32_t difficulty, + const uint32_t nonce + ) noexcept; + + /** + * @brief Generate and solve a PoWER challenge for RPC for a given difficulty. + * + * @param tx_prefix_hash Hash of transaction prefix. + * @param recent_block_hash Block hash within the last POWER_HEIGHT_WINDOW blocks. + * @param difficulty The difficulty parameter. + */ + solution_data solve_rpc( + const crypto::hash& tx_prefix_hash, + const crypto::hash& recent_block_hash, + const uint32_t difficulty + ); + + /** + * @brief Generate and solve a PoWER challenge for P2P for a given difficulty. + * + * @param seed Low bytes of challenge seed. + * @param seed_top64 High bytes of challenge seed. + * @param difficulty The difficulty parameter. + */ + solution_data solve_p2p( + const uint64_t seed, + const uint64_t seed_top64, + const uint32_t difficulty + ); + + /** + * @brief Verify a PoWER solution for RPC. + * + * @param tx_prefix_hash Hash of transaction prefix. + * @param recent_block_hash Block hash within the last POWER_HEIGHT_WINDOW blocks. + * @param nonce A valid nonce. + * @param difficulty The difficulty parameter. + * @param solution The Equi-X solution. + * + * @return true – if verification succeeded + * @return false – if verification failed (invalid input, allocation error, difficulty too low). + */ + bool verify_rpc( + const crypto::hash& tx_prefix_hash, + const crypto::hash& recent_block_hash, + const uint32_t nonce, + const uint32_t difficulty, + const solution_array solution + ); + + /** + * @brief Verify a PoWER solution for P2P. + * + * @param seed Low bytes of challenge seed. + * @param seed_top64 High bytes of challenge seed. + * @param nonce A valid nonce. + * @param difficulty The difficulty parameter. + * @param solution The Equi-X solution. + * + * @return true – if verification succeeded + * @return false – if verification failed (invalid input, allocation error, difficulty too low). + */ + bool verify_p2p( + const uint64_t seed, + const uint64_t seed_top64, + const uint32_t difficulty, + const uint32_t nonce, + const solution_array solution + ); + + } // namespace power +} // namespace tools \ No newline at end of file diff --git a/src/cryptonote_basic/connection_context.h b/src/cryptonote_basic/connection_context.h index aca32341b5f..4f18db4f7ca 100644 --- a/src/cryptonote_basic/connection_context.h +++ b/src/cryptonote_basic/connection_context.h @@ -79,6 +79,8 @@ namespace cryptonote return 1024 * 1024 * 2; // 2 MB case cryptonote::NOTIFY_REQUEST_TX_POOL_TXS::ID: return 1024 * 1024 * 2; // 2 MB + case cryptonote::NOTIFY_POWER_SOLUTION::ID: + return 4096; default: break; }; diff --git a/src/cryptonote_protocol/cryptonote_protocol_defs.h b/src/cryptonote_protocol/cryptonote_protocol_defs.h index 4907f0476cf..bdd97bbac02 100644 --- a/src/cryptonote_protocol/cryptonote_protocol_defs.h +++ b/src/cryptonote_protocol/cryptonote_protocol_defs.h @@ -88,6 +88,8 @@ namespace cryptonote uint8_t address_type; + bool power_enabled; + BEGIN_KV_SERIALIZE_MAP() KV_SERIALIZE(incoming) KV_SERIALIZE(localhost) @@ -114,6 +116,7 @@ namespace cryptonote KV_SERIALIZE(height) KV_SERIALIZE(pruning_seed) KV_SERIALIZE(address_type) + KV_SERIALIZE(power_enabled) END_KV_SERIALIZE_MAP() }; @@ -455,4 +458,23 @@ namespace cryptonote typedef epee::misc_utils::struct_init request; }; + /************************************************************************/ + /* */ + /************************************************************************/ + struct NOTIFY_POWER_SOLUTION + { + const static int ID = BC_COMMANDS_POOL_BASE + 13; + + struct request_t + { + std::vector solution; + uint32_t nonce; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE_CONTAINER_POD_AS_BLOB(solution) + KV_SERIALIZE(nonce) + 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 f818e2f6f4f..650fb2d007b 100644 --- a/src/cryptonote_protocol/cryptonote_protocol_handler.h +++ b/src/cryptonote_protocol/cryptonote_protocol_handler.h @@ -102,6 +102,7 @@ namespace cryptonote 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) + HANDLE_NOTIFY_T2(NOTIFY_POWER_SOLUTION, &cryptonote_protocol_handler::handle_notify_power_solution) END_INVOKE_MAP2() bool on_idle(); @@ -163,6 +164,7 @@ namespace cryptonote 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); + int handle_notify_power_solution(int command, NOTIFY_POWER_SOLUTION::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); diff --git a/src/cryptonote_protocol/cryptonote_protocol_handler.inl b/src/cryptonote_protocol/cryptonote_protocol_handler.inl index 6c03e52ac41..a36c4760cb0 100644 --- a/src/cryptonote_protocol/cryptonote_protocol_handler.inl +++ b/src/cryptonote_protocol/cryptonote_protocol_handler.inl @@ -44,9 +44,11 @@ #include #include +#include "common/power.h" #include "cryptonote_protocol/cryptonote_protocol_handler.h" #include "cryptonote_basic/cryptonote_format_utils.h" #include "misc_log_ex.h" +#include "p2p/p2p_protocol_defs.h" #include "profile_tools.h" #include "net/network_throttle-detail.hpp" #include "common/pruning.h" @@ -475,6 +477,8 @@ namespace cryptonote cnx.pruning_seed = cntxt.m_pruning_seed; cnx.address_type = (uint8_t)cntxt.m_remote_address.get_type_id(); + cnx.power_enabled = m_p2p->get_power_enabled(); + connections.push_back(cnx); return true; @@ -1009,6 +1013,51 @@ namespace cryptonote } //------------------------------------------------------------------------------------------------------------------------ template + int t_cryptonote_protocol_handler::handle_notify_power_solution(int command, NOTIFY_POWER_SOLUTION::request& arg, cryptonote_connection_context& context) + { + nodetool::power_challenge_data c = m_p2p->get_power_challenge(); + + MLOG_P2P_MESSAGE( + "Received NOTIFY_POWER_SOLUTION (nonce=" + << arg.nonce + << ", seed=" + << c.seed + << ", seed_top64=" + << c.seed_top64 + << ", difficulty=" + << c.difficulty + << ")" + ); + + constexpr size_t size = tools::power::solution_array {}.size(); + + if (arg.solution.size() != size) + { + LOG_PRINT_CCONTEXT_L1("PoWER solution wrong size, dropping connection"); + drop_connection_with_score(context, tools::power::BAN_SCORE, false); + return 0; + } + + tools::power::solution_array s {}; + std::copy(arg.solution.begin(), arg.solution.end(), s.begin()); + + if (!tools::power::verify_p2p( + c.seed, + c.seed_top64, + arg.nonce, + c.difficulty, + s + )) { + LOG_PRINT_CCONTEXT_L1("PoWER verification failed, dropping connection"); + drop_connection_with_score(context, tools::power::BAN_SCORE, false); + return 0; + } + + m_p2p->set_power_enabled(true); + return 1; + } + //------------------------------------------------------------------------------------------------------------------------ + template 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)"); @@ -1063,8 +1112,28 @@ namespace cryptonote else stem_txs.reserve(arg.txs.size()); + bool power_enabled = m_p2p->get_power_enabled(); + for (auto& tx_blob : arg.txs) { + if (!power_enabled) + { + transaction_prefix tx_prefix; + if (!parse_and_validate_tx_prefix_from_blob(tx_blob, tx_prefix)) + { + LOG_PRINT_L1("Incoming transactions failed to parse, rejected"); + drop_connection(context, false, false); + return 1; + } + + if (tx_prefix.vin.size() >= tools::power::INPUT_THRESHOLD) + { + LOG_PRINT_L1("Incoming transactions failed PoWER, rejected"); + drop_connection_with_score(context, tools::power::BAN_SCORE, false); + return 1; + } + } + tx_verification_context tvc{}; crypto::hash tx_hash{}; if (!m_core.handle_incoming_tx(tx_blob, tvc, tx_relay, true, tx_hash) && !tvc.m_no_drop_offense) diff --git a/src/p2p/net_node.h b/src/p2p/net_node.h index 5acf5c30cda..924c4fbe08b 100644 --- a/src/p2p/net_node.h +++ b/src/p2p/net_node.h @@ -300,6 +300,11 @@ namespace nodetool virtual void remove_used_stripe_peer(const typename t_payload_net_handler::connection_context &context); virtual void clear_used_stripe_peers(); + virtual nodetool::power_challenge_data get_power_challenge(); + virtual void set_power_challenge(const nodetool::power_challenge_data challenge); + virtual bool get_power_enabled(); + virtual void set_power_enabled(bool enabled); + private: const std::vector m_seed_nodes_list = { "seeds.moneroseeds.se" @@ -513,6 +518,10 @@ namespace nodetool boost::mutex m_used_stripe_peers_mutex; std::array, 1 << CRYPTONOTE_PRUNING_LOG_STRIPES> m_used_stripe_peers; + boost::mutex m_power_challenge_lock; + nodetool::power_challenge_data m_power_challenge; // Our challenge for an incoming peer. + bool m_power_enabled; + boost::uuids::uuid m_network_id; cryptonote::network_type m_nettype; diff --git a/src/p2p/net_node.inl b/src/p2p/net_node.inl index 5b7ad1b4af5..bd7f60b0c3a 100644 --- a/src/p2p/net_node.inl +++ b/src/p2p/net_node.inl @@ -45,6 +45,8 @@ #include #include +#include "common/power.h" +#include "cryptonote_protocol/cryptonote_protocol_defs.h" #include "version.h" #include "string_tools.h" #include "common/util.h" @@ -1231,6 +1233,38 @@ namespace nodetool hsh_result = false; return; } + + // Solve PoWER challenge and send to peer. + if (rsp.power_challenge.difficulty >= tools::power::MAX_DIFFICULTY) + { + LOG_WARNING_CC( + context, + "COMMAND_HANDSHAKE invoked but PoWER difficulty from peer is too high: " + << rsp.power_challenge.difficulty + << ", dropping connection." + ); + hsh_result = false; + return; + } + tools::power::solution_data s = tools::power::solve_p2p( + rsp.power_challenge.seed, + rsp.power_challenge.seed_top64, + rsp.power_challenge.difficulty + ); + epee::levin::message_writer out{4096}; + cryptonote::NOTIFY_POWER_SOLUTION::request_t r = { std::vector(s.solution.begin(), s.solution.end()), s.nonce }; + epee::serialization::store_t_to_binary(r, out.buffer); + if (zone.m_net_server + .get_config_object() + .send(out.finalize_notify(cryptonote::NOTIFY_POWER_SOLUTION::ID), context.m_connection_id) + ) { + // The peer we are handshaking to does not + // need to do PoWER, enable it for them. + set_power_enabled(true); + } else { + LOG_WARNING_CC(context, "COMMAND_HANDSHAKE invoked but NOTIFY_POWER_SOLUTION failed, continuing with degraded tx relay."); + } + LOG_INFO_CC(context, "New connection handshaked, pruning seed " << epee::string_tools::to_string_hex(context.m_pruning_seed)); LOG_DEBUG_CC(context, " COMMAND_HANDSHAKE INVOKED OK"); }else @@ -2716,12 +2750,15 @@ namespace nodetool flags_context.support_flags = support_flags; }); + set_power_challenge({ crypto::rand(), crypto::rand(), tools::power::DIFFICULTY }); + //fill response zone.m_peerlist.get_peerlist_head(rsp.local_peerlist_new, true); for (const auto &e: rsp.local_peerlist_new) context.sent_addresses.insert(e.adr); get_local_node_data(rsp.node_data, zone); m_payload_handler.get_payload_sync_data(rsp.payload_data); + rsp.power_challenge = get_power_challenge(); LOG_DEBUG_CC(context, "COMMAND_HANDSHAKE"); return 1; } @@ -3073,6 +3110,32 @@ namespace nodetool e.clear(); } + template + nodetool::power_challenge_data node_server::get_power_challenge() + { + CRITICAL_REGION_LOCAL(m_power_challenge_lock); + return m_power_challenge; + } + + template + void node_server::set_power_challenge(const nodetool::power_challenge_data challenge) + { + CRITICAL_REGION_LOCAL(m_power_challenge_lock); + m_power_challenge = challenge; + } + + template + bool node_server::get_power_enabled() + { + return m_power_enabled; + } + + template + void node_server::set_power_enabled(bool enabled) + { + m_power_enabled = enabled; + } + template void node_server::add_upnp_port_mapping_impl(uint32_t port, bool ipv6) // if ipv6 false, do ipv4 { diff --git a/src/p2p/net_node_common.h b/src/p2p/net_node_common.h index 94941c1b9be..2031d604236 100644 --- a/src/p2p/net_node_common.h +++ b/src/p2p/net_node_common.h @@ -68,6 +68,10 @@ namespace nodetool virtual void add_used_stripe_peer(const t_connection_context &context)=0; virtual void remove_used_stripe_peer(const t_connection_context &context)=0; virtual void clear_used_stripe_peers()=0; + virtual nodetool::power_challenge_data get_power_challenge()=0; + virtual void set_power_challenge(const nodetool::power_challenge_data challenge)=0; + virtual bool get_power_enabled()=0; + virtual void set_power_enabled(bool enabled)=0; }; template @@ -135,5 +139,19 @@ namespace nodetool virtual void clear_used_stripe_peers() { } + virtual nodetool::power_challenge_data get_power_challenge() + { + return nodetool::power_challenge_data { 0, 0, 0 }; + } + virtual void set_power_challenge(const nodetool::power_challenge_data challenge) + { + } + virtual bool get_power_enabled() + { + return false; + } + virtual void set_power_enabled(bool enabled) + { + } }; } diff --git a/src/p2p/p2p_protocol_defs.h b/src/p2p/p2p_protocol_defs.h index 5f0fb1bde06..66be5529bec 100644 --- a/src/p2p/p2p_protocol_defs.h +++ b/src/p2p/p2p_protocol_defs.h @@ -168,6 +168,19 @@ namespace nodetool KV_SERIALIZE_OPT(support_flags, (uint32_t)0) END_KV_SERIALIZE_MAP() }; + + struct power_challenge_data + { + uint64_t seed; + uint64_t seed_top64; + uint32_t difficulty; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(seed) + KV_SERIALIZE(seed_top64) + KV_SERIALIZE(difficulty) + END_KV_SERIALIZE_MAP() + }; #define P2P_COMMANDS_POOL_BASE 1000 @@ -196,11 +209,13 @@ namespace nodetool { basic_node_data node_data; t_playload_type payload_data; + power_challenge_data power_challenge; std::vector local_peerlist_new; BEGIN_KV_SERIALIZE_MAP() KV_SERIALIZE(node_data) KV_SERIALIZE(payload_data) + KV_SERIALIZE(power_challenge) KV_SERIALIZE(local_peerlist_new) END_KV_SERIALIZE_MAP() }; diff --git a/src/rpc/core_rpc_server.cpp b/src/rpc/core_rpc_server.cpp index 14212a44118..56087adea24 100644 --- a/src/rpc/core_rpc_server.cpp +++ b/src/rpc/core_rpc_server.cpp @@ -39,6 +39,7 @@ using namespace epee; #include "common/command_line.h" #include "common/updates.h" #include "common/download.h" +#include "common/power.h" #include "common/util.h" #include "common/merge_sorted_vectors.h" #include "common/perf_timer.h" @@ -506,6 +507,89 @@ namespace cryptonote } #define CHECK_CORE_READY() do { if(!check_core_ready()){res.status = CORE_RPC_STATUS_BUSY;return true;} } while(0) + //------------------------------------------------------------------------------------------------------------------------------ + bool core_rpc_server::validate_power( + const cryptonote::blobdata& txblob, + const std::string& power_block_hash, + const std::string& power_solution, + uint32_t power_nonce, + bool restricted, + std::string& status + ) { + if (!restricted) + return true; + + cryptonote::transaction_prefix tx_prefix; + if (!cryptonote::parse_and_validate_tx_prefix_from_blob(txblob, tx_prefix)) + { + status = "Failed to parse and validate tx prefix from blob"; + return false; + } + + if (tx_prefix.vin.size() <= tools::power::INPUT_THRESHOLD) + return true; + + crypto::hash block_hash; + if (power_block_hash.size() != 64 || !string_tools::hex_to_pod(power_block_hash, block_hash)) + { + status = "Failed to decode power_block_hash"; + return false; + } + + tools::power::solution_array solution; + if (power_solution.size() != 32 || !string_tools::hex_to_pod(power_solution, solution)) + { + status = "Failed to decode power_solution"; + return false; + } + + const uint64_t height = m_core.get_current_blockchain_height() - 1; + + bool hash_is_recent = false; + for (size_t i = 0; i < tools::power::HEIGHT_WINDOW; ++i) + { + const uint64_t h = height >= i ? height - i : 0; + + if (h == 0) + { + break; + } + + const crypto::hash id = m_core.get_block_id_by_height(h); + + if (id == crypto::null_hash) + { + status = "power_block_hash was not found"; + return false; + } + + if (id == block_hash) + { + hash_is_recent = true; + break; + } + } + + if (!hash_is_recent) + { + status = "power_block_hash is not within the allowed window"; + return false; + } + + if (!tools::power::verify_rpc( + get_transaction_prefix_hash(tx_prefix), + block_hash, + power_nonce, + tools::power::DIFFICULTY, + solution + )) { + status = "Invalid PoW solution"; + return false; + } + + return true; + } + //------------------------------------------------------------------------------------------------------------------------------ bool core_rpc_server::on_get_height(const COMMAND_RPC_GET_HEIGHT::request& req, COMMAND_RPC_GET_HEIGHT::response& res, const connection_context *ctx) { @@ -1566,6 +1650,18 @@ namespace cryptonote } res.sanity_check_failed = false; + if (!validate_power( + tx_blob, + req.power_block_hash, + req.power_solution, + req.power_nonce, + restricted, + res.reason + )) { + res.status = "Failed"; + return false; + } + crypto::hash txid{}; if (!skip_validation) { diff --git a/src/rpc/core_rpc_server.h b/src/rpc/core_rpc_server.h index 1b4c3f13f4d..f436d5b0f5f 100644 --- a/src/rpc/core_rpc_server.h +++ b/src/rpc/core_rpc_server.h @@ -292,6 +292,29 @@ namespace cryptonote bool use_bootstrap_daemon_if_necessary(const invoke_http_mode &mode, const std::string &command_name, const typename COMMAND_TYPE::request& req, typename COMMAND_TYPE::response& res, bool &r); bool get_block_template(const account_public_address &address, const crypto::hash *prev_block, const cryptonote::blobdata &extra_nonce, size_t &reserved_offset, cryptonote::difficulty_type &difficulty, uint64_t &height, uint64_t &expected_reward, uint64_t& cumulative_weight, block &b, uint64_t &seed_height, crypto::hash &seed_hash, crypto::hash &next_seed_hash, epee::json_rpc::error &error_resp); bool check_payment(const std::string &client, uint64_t payment, const std::string &rpc, bool same_ts, std::string &message, uint64_t &credits, std::string &top_hash); + + /** + * @brief Validate PoWER for RPC requests. + * + * @param txblob Raw transaction blob from request. + * @param block_hash Hex encoded recent block hash from request. + * @param solution Hex encoded Equi-X solution from request. + * @param nonce Nonce value from request. + * + * @param restricted `true` if the endpoint is restricted, otherwise `false`. + * @param status `status` field from request type. + * + * @return true – PoW is either not required or the solution is valid. + * @return false – an error was detected; `status`, `code`, and `message` should propagate to the RPC response. + */ + bool validate_power( + const cryptonote::blobdata& txblob, + const std::string& block_hash, + const std::string& solution, + uint32_t nonce, + bool restricted, + std::string& status + ); core& m_core; nodetool::node_server >& m_p2p; diff --git a/src/rpc/core_rpc_server_commands_defs.h b/src/rpc/core_rpc_server_commands_defs.h index 84f2429b885..54220b093b7 100644 --- a/src/rpc/core_rpc_server_commands_defs.h +++ b/src/rpc/core_rpc_server_commands_defs.h @@ -726,12 +726,18 @@ inline const std::string get_rpc_status(const bool trusted_daemon, const std::st struct request_t: public rpc_access_request_base { std::string tx_as_hex; + std::string power_block_hash; + std::string power_solution; + uint32_t power_nonce; bool do_not_relay; bool do_sanity_checks; BEGIN_KV_SERIALIZE_MAP() KV_SERIALIZE_PARENT(rpc_access_request_base) KV_SERIALIZE(tx_as_hex) + KV_SERIALIZE(power_block_hash) + KV_SERIALIZE(power_solution) + KV_SERIALIZE(power_nonce) KV_SERIALIZE_OPT(do_not_relay, false) KV_SERIALIZE_OPT(do_sanity_checks, true) END_KV_SERIALIZE_MAP() diff --git a/src/rpc/daemon_handler.cpp b/src/rpc/daemon_handler.cpp index e644ed6da56..394608dde5e 100644 --- a/src/rpc/daemon_handler.cpp +++ b/src/rpc/daemon_handler.cpp @@ -34,6 +34,7 @@ #include #include +#include "common/power.h" // likely included by daemon_handler.h's includes, // but including here for clarity #include "cryptonote_core/cryptonote_core.h" @@ -343,7 +344,7 @@ namespace rpc void DaemonHandler::handle(const SendRawTx::Request& req, SendRawTx::Response& res) { - handleTxBlob(cryptonote::tx_to_blob(req.tx), req.relay, res); + handleTxBlob(cryptonote::tx_to_blob(req.tx), req.power_block_hash, req.power_solution, req.power_nonce, req.relay, res); } void DaemonHandler::handle(const SendRawTxHex::Request& req, SendRawTxHex::Response& res) @@ -356,11 +357,81 @@ namespace rpc res.error_details = "Invalid hex"; return; } - handleTxBlob(std::move(tx_blob), req.relay, res); + handleTxBlob(std::move(tx_blob), req.power_block_hash, req.power_solution, req.power_nonce, req.relay, res); } - void DaemonHandler::handleTxBlob(std::string&& tx_blob, bool relay, SendRawTx::Response& res) - { + bool DaemonHandler::validatePower( + const cryptonote::blobdata& tx_blob, + const crypto::hash& power_block_hash, + const tools::power::solution_array& power_solution, + uint32_t power_nonce, + std::string& error_details + ) { + cryptonote::transaction_prefix tx_prefix; + if (!cryptonote::parse_and_validate_tx_prefix_from_blob(tx_blob, tx_prefix)) + { + error_details = "Failed to parse and validate tx prefix from blob"; + return false; + } + + if (tx_prefix.vin.size() <= tools::power::INPUT_THRESHOLD) + return true; + + const uint64_t height = m_core.get_current_blockchain_height() - 1; + + bool hash_is_recent = false; + for (size_t i = 0; i < tools::power::HEIGHT_WINDOW; ++i) + { + const uint64_t h = height >= i ? height - i : 0; + + if (h == 0) + { + break; + } + + const crypto::hash id = m_core.get_block_id_by_height(h); + + if (id == crypto::null_hash) + { + error_details = "recent_block_hash was not found"; + return false; + } + + if (id == power_block_hash) + { + hash_is_recent = true; + break; + } + } + + if (!hash_is_recent) + { + error_details = "recent_block_hash is not within the allowed window"; + return false; + } + + if (!tools::power::verify_rpc( + get_transaction_prefix_hash(tx_prefix), + power_block_hash, + power_nonce, + tools::power::DIFFICULTY, + power_solution + )) { + error_details = "Invalid PoW solution"; + return false; + } + + return true; + } + + void DaemonHandler::handleTxBlob( + std::string&& tx_blob, + const crypto::hash& recent_block_hash, + const tools::power::solution_array& power_solution, + const uint32_t nonce, + bool relay, + SendRawTx::Response& res + ) { if (!m_p2p.get_payload_object().is_synchronized()) { res.status = Message::STATUS_FAILED; @@ -441,6 +512,14 @@ namespace rpc return; } + if (!validatePower(tx_blob, recent_block_hash, power_solution, nonce, res.error_details)) + { + res.relayed = false; + res.status = Message::STATUS_OK; + + return; + } + if(tvc.m_relay == relay_method::none || !relay) { MERROR("[SendRawTx]: tx accepted, but not relayed"); diff --git a/src/rpc/daemon_handler.h b/src/rpc/daemon_handler.h index 1da5419d2a5..824ea4625cd 100644 --- a/src/rpc/daemon_handler.h +++ b/src/rpc/daemon_handler.h @@ -139,7 +139,22 @@ class DaemonHandler : public RpcHandler bool getBlockHeaderByHash(const crypto::hash& hash_in, cryptonote::rpc::BlockHeaderResponse& response); - void handleTxBlob(std::string&& tx_blob, bool relay, SendRawTx::Response& res); + bool validatePower( + const cryptonote::blobdata& tx_blob, + const crypto::hash& power_block_hash, + const tools::power::solution_array& power_solution, + uint32_t power_nonce, + std::string& error_details + ); + + void handleTxBlob( + std::string&& tx_blob, + const crypto::hash& recent_block_hash, + const tools::power::solution_array& power_solution, + const uint32_t power_nonce, + bool relay, + SendRawTx::Response& res + ); cryptonote::core& m_core; t_p2p& m_p2p; diff --git a/src/rpc/daemon_messages.h b/src/rpc/daemon_messages.h index 539afd6c80a..58717c61829 100644 --- a/src/rpc/daemon_messages.h +++ b/src/rpc/daemon_messages.h @@ -33,6 +33,7 @@ #include #include "byte_stream.h" +#include "common/power.h" #include "message.h" #include "cryptonote_protocol/cryptonote_protocol_defs.h" #include "rpc/message_data_structs.h" @@ -167,6 +168,9 @@ END_RPC_MESSAGE_CLASS; BEGIN_RPC_MESSAGE_CLASS(SendRawTx); BEGIN_RPC_MESSAGE_REQUEST; RPC_MESSAGE_MEMBER(cryptonote::transaction, tx); + RPC_MESSAGE_MEMBER(crypto::hash, power_block_hash); + RPC_MESSAGE_MEMBER(tools::power::solution_array, power_solution); + RPC_MESSAGE_MEMBER(uint32_t, power_nonce); RPC_MESSAGE_MEMBER(bool, relay); END_RPC_MESSAGE_REQUEST; BEGIN_RPC_MESSAGE_RESPONSE; @@ -177,6 +181,9 @@ END_RPC_MESSAGE_CLASS; BEGIN_RPC_MESSAGE_CLASS(SendRawTxHex); BEGIN_RPC_MESSAGE_REQUEST; RPC_MESSAGE_MEMBER(std::string, tx_as_hex); + RPC_MESSAGE_MEMBER(crypto::hash, power_block_hash); + RPC_MESSAGE_MEMBER(tools::power::solution_array, power_solution); + RPC_MESSAGE_MEMBER(uint32_t, power_nonce); RPC_MESSAGE_MEMBER(bool, relay); END_RPC_MESSAGE_REQUEST; using Response = SendRawTx::Response; diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp index 3f968e07137..f975165d2fe 100644 --- a/src/wallet/wallet2.cpp +++ b/src/wallet/wallet2.cpp @@ -91,6 +91,7 @@ using namespace epee; #include "common/dns_utils.h" #include "common/notify.h" #include "common/perf_timer.h" +#include "common/power.h" #include "ringct/rctSigs.h" #include "ringdb.h" #include "device/device_cold.hpp" @@ -7771,6 +7772,32 @@ void wallet2::commit_tx(pending_tx& ptx) req.tx_as_hex = epee::string_tools::buff_to_hex_nodelimer(tx_to_blob(ptx.tx)); req.do_not_relay = false; req.do_sanity_checks = true; + + // Find PoWER solution if necessary. + if (!tools::is_local_address(m_daemon_address) && ptx.tx.vin.size() > tools::power::INPUT_THRESHOLD) + { + MDEBUG("Finding PoWER solution..."); + + THROW_WALLET_EXCEPTION_IF( + m_blockchain.size() < 1, + error::wallet_internal_error, + "Wallet does not have block hashes for PoWER input data" + ); + + const crypto::hash power_block_hash = m_blockchain[m_blockchain.size() - 1]; + const crypto::hash tx_prefix_hash = cryptonote::get_transaction_prefix_hash(ptx.tx); + + tools::power::solution_data s = tools::power::solve_rpc( + tx_prefix_hash, + power_block_hash, + tools::power::DIFFICULTY + ); + + req.power_block_hash = epee::string_tools::pod_to_hex(power_block_hash); + req.power_solution = epee::string_tools::pod_to_hex(s.solution); + req.power_nonce = s.nonce; + } + COMMAND_RPC_SEND_RAW_TX::response daemon_send_resp; { diff --git a/tests/unit_tests/CMakeLists.txt b/tests/unit_tests/CMakeLists.txt index 83578d42a20..bcaaf26e630 100644 --- a/tests/unit_tests/CMakeLists.txt +++ b/tests/unit_tests/CMakeLists.txt @@ -86,6 +86,7 @@ set(unit_tests_sources output_distribution.cpp parse_amount.cpp pruning.cpp + power.cpp random.cpp request_manager.cpp rolling_median.cpp @@ -148,6 +149,7 @@ target_link_libraries(unit_tests wallet p2p version + equix_static ${Boost_CHRONO_LIBRARY} ${Boost_THREAD_LIBRARY} ${GTEST_LIBRARIES} diff --git a/tests/unit_tests/power.cpp b/tests/unit_tests/power.cpp new file mode 100644 index 00000000000..9e4f61a354a --- /dev/null +++ b/tests/unit_tests/power.cpp @@ -0,0 +1,273 @@ +// 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. + +//test header +#include "gtest/gtest.h" + +//local headers +#include "common/power.cpp" +#include "common/power.h" +#include "hex.h" +#include "string_tools.h" + +//third party headers +#include + +//standard headers +#include +#include +#include + +// Test difficulty, real difficulty value is too high for debug builds. +constexpr uint32_t DIFF = 15; + +struct test_data_equix { + std::string_view challenge; + std::string_view expected_solution; + int expected_solution_count; + uint32_t expected_scalar; +}; + +struct test_data_rpc { + std::string_view tx_prefix_hash; + std::string_view recent_block_hash; + uint32_t expected_nonce; + /// Final challenge bytes, not the initial bytes. + std::string_view expected_challenge; + std::string_view expected_solution; + uint32_t expected_scalar; +}; + +struct test_data_p2p { + uint64_t seed; + uint64_t seed_top64; + uint32_t expected_nonce; + /// Final challenge bytes, not the initial bytes. + std::string_view expected_challenge; + std::string_view expected_solution; + uint32_t expected_scalar; +}; + +constexpr std::array TEST_DATA_EQUIX {{ + // test UTF8 + { + "よ、ひさしぶりだね。", + "546658a95f6466ecc41b24dca5a5e8f5", + 3, + 609012647 + }, + { + "👋,🕒👉🕘.", + "7854ba6c1c9bf7cc9354aed876ce64f4", + 3, + 1651207227 + }, + { + "Privacy is necessary for an open society in the electronic age.", + "7d1467364825e586ae44b9e95ff388f3", + 4, + 2074493700 + }, + { + "We must defend our own privacy if we expect to have any.", + "a330e6561142a57be57513c1095d46ff", + 3, + 1892198895 + }, + { + "We must come together and create systems which allow anonymous transactions to take place.", + "ca1e0362d9252bbb85c62fcdf4ac68f6", + 2, + 283799637 + }, +}}; + +constexpr std::array TEST_DATA_RPC {{ + test_data_rpc { + "c01d4920b75c0cad3a75aa71d6aa73e3d90d0be3ac8da5f562b3fc101e74b57c", + "77ff034133bdd86914c6e177563ee8b08af896dd2603b882e280762deab609c0", + 5, + "4d6f6e65726f20506f574552c01d4920b75c0cad3a75aa71d6aa73e3d90d0be3ac8da5f562b3fc101e74b57c77ff034133bdd86914c6e177563ee8b08af896dd2603b882e280762deab609c005000000", + "6c81ba867f822ea88b14fe2ed027e1ee", + 259977672, + }, + { + "17bac54d909964de0ed46eda755904b33fb42eead7ce015fbdde17fa6f0ec95f", + "6d4c090582ed8cecfc8f8d90ddd8e6b7c8b39dd86c7e882078b670a7ba29b03f", + 24, + "4d6f6e65726f20506f57455217bac54d909964de0ed46eda755904b33fb42eead7ce015fbdde17fa6f0ec95f6d4c090582ed8cecfc8f8d90ddd8e6b7c8b39dd86c7e882078b670a7ba29b03f18000000", + "6992d7cb29ae95dbc92f6b8d50e820ef", + 252939049, + }, + { + "6dd6a8df16e052f53d51f5f76372ab0c14c60d748908c4589a90327bdc6498a1", + "bc322459b35f5c58082d4193c8d6bf4f057aedd0823121f2ecbcb117276d13a2", + 1, + "4d6f6e65726f20506f5745526dd6a8df16e052f53d51f5f76372ab0c14c60d748908c4589a90327bdc6498a1bc322459b35f5c58082d4193c8d6bf4f057aedd0823121f2ecbcb117276d13a201000000", + "19018e8d20beaeda149816cd74f33bfd", + 187745649, + }, +}}; + +constexpr std::array TEST_DATA_P2P {{ + { + 0, 0, 10, + "4d6f6e65726f20506f574552000000000000000000000000000000000f0000000a000000", + "ad025bac4c7bb2dfcb4bb666cf2643e8", + 252557470, + }, + { + 1589356, 6700, 0, + "4d6f6e65726f20506f5745526c401800000000002c1a0000000000000f00000000000000", + "0d25ad67fb065baae91a0d29a31db9d8", + 50548387, + }, + { + std::numeric_limits::max(), std::numeric_limits::max(), 4, + "4d6f6e65726f20506f574552ffffffffffffffffffffffffffffffff0f00000004000000", + "3357a279712c70e3e26442d864282ef8", + 170469575, + }, +}}; + +namespace tools +{ + namespace power + { + + // Sanity test Equi-X functions. + TEST(power, equix_functions) + { + equix_ctx* ctx = equix_alloc(EQUIX_CTX_SOLVE); + + for (const auto& t : TEST_DATA_EQUIX) + { + const void* challenge = t.challenge.data(); + const size_t size = t.challenge.size(); + + equix_solution solutions[EQUIX_MAX_SOLS]; + const int count = equix_solve(ctx, challenge, size, solutions); + ASSERT_EQ(count, t.expected_solution_count); + const equix_solution s = solutions[0]; + + const std::string h = epee::string_tools::pod_to_hex(s); + ASSERT_EQ(h, t.expected_solution); + + tools::power::solution_array s2; + memcpy(s2.data(), s.idx, sizeof(s2)); + + const uint32_t d = create_difficulty_scalar(challenge, size, s2); + ASSERT_EQ(d, t.expected_scalar); + + const uint32_t last_difficulty_that_passes = + std::numeric_limits::max() / d; + + ASSERT_EQ(true, check_difficulty(d, last_difficulty_that_passes)); + ASSERT_EQ(false, check_difficulty(d, last_difficulty_that_passes + 1)); + } + } + + TEST(power, rpc) + { + for (const auto& t : TEST_DATA_RPC) + { + crypto::hash tx_prefix_hash {}; + crypto::hash recent_block_hash {}; + epee::string_tools::hex_to_pod(t.tx_prefix_hash.data(), tx_prefix_hash); + epee::string_tools::hex_to_pod(t.recent_block_hash.data(), recent_block_hash); + + const solution_data s = solve_rpc(tx_prefix_hash, recent_block_hash, DIFF); + + ASSERT_EQ(s.nonce, t.expected_nonce); + + const std::array c = + create_challenge_rpc(tx_prefix_hash, recent_block_hash, t.expected_nonce); + + const std::string c_hex = epee::string_tools::pod_to_hex(c); + ASSERT_EQ(c_hex, t.expected_challenge); + + const std::string c2_hex = epee::to_hex::string({s.challenge.data(), s.challenge.size()}); + ASSERT_EQ(c2_hex, t.expected_challenge); + + const uint32_t d = create_difficulty_scalar(s.challenge.data(), s.challenge.size(), s.solution); + ASSERT_EQ(d, t.expected_scalar); + + const uint32_t last_difficulty_that_passes = + std::numeric_limits::max() / d; + + ASSERT_EQ(true, check_difficulty(d, last_difficulty_that_passes)); + ASSERT_EQ(false, check_difficulty(d, last_difficulty_that_passes + 1)); + + ASSERT_EQ(true, verify_rpc( + tx_prefix_hash, + recent_block_hash, + t.expected_nonce, + DIFF, + s.solution + )); + } + } + + TEST(power, p2p) + { + for (const auto& t : TEST_DATA_P2P) + { + const solution_data s = solve_p2p(t.seed, t.seed_top64, DIFF); + + ASSERT_EQ(s.nonce, t.expected_nonce); + + const std::array c = + create_challenge_p2p(t.seed, t.seed_top64, DIFF, t.expected_nonce); + + const std::string c_hex = epee::string_tools::pod_to_hex(c); + ASSERT_EQ(c_hex, t.expected_challenge); + + const std::string c2_hex = epee::to_hex::string({s.challenge.data(), s.challenge.size()}); + ASSERT_EQ(c2_hex, t.expected_challenge); + + const uint32_t d = create_difficulty_scalar(s.challenge.data(), s.challenge.size(), s.solution); + ASSERT_EQ(d, t.expected_scalar); + + const uint32_t last_difficulty_that_passes = + std::numeric_limits::max() / d; + + ASSERT_EQ(true, check_difficulty(d, last_difficulty_that_passes)); + ASSERT_EQ(false, check_difficulty(d, last_difficulty_that_passes + 1)); + + ASSERT_EQ(true, verify_p2p( + t.seed, + t.seed_top64, + t.expected_nonce, + DIFF, + s.solution + )); + } + } + + } //namespace power +} // namespace tools