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