feat(testnet): integrate FCMP++ and Carrot for Wownero

Add an isolated Wownero research network, Carrot-compatible miner authorization, release corrections, and local acceptance tests. Base the implementation on FCMP++ beta 3.0 with the beta 3.1 legacy ring-offset correction; both research build options remain disabled by default.
This commit is contained in:
2026-10-07 10:35:51 -04:00
parent d816367cb1
commit d70cfce21a
54 changed files with 2402 additions and 49 deletions
+3
View File
@@ -14,3 +14,6 @@
[submodule "external/mx25519"]
path = external/mx25519
url = https://github.com/tevador/mx25519
[submodule "external/randomwow"]
path = external/randomwow
url = https://github.com/wownero/RandomWOW.git
+14 -1
View File
@@ -381,6 +381,15 @@ else()
message(STATUS "Building without build tag")
endif()
option(WOWNERO_FCMP_TESTNET "Build the isolated Wownero FCMP++ research network." OFF)
if(WOWNERO_FCMP_TESTNET)
if(NOT UNIX)
message(FATAL_ERROR "The Wownero FCMP testnet profile currently supports POSIX platforms only")
endif()
add_compile_definitions(WOWNERO_FCMP_TESTNET)
set(PER_BLOCK_CHECKPOINT OFF CACHE BOOL "Upstream checkpoints do not apply to this network" FORCE)
endif()
option(MANUAL_SUBMODULES "Skip submodule up-to-date checks" OFF)
if(NOT MANUAL_SUBMODULES)
find_package(Git)
@@ -398,7 +407,11 @@ if(NOT MANUAL_SUBMODULES)
message(STATUS "Checking submodules")
check_submodule(external/rapidjson)
check_submodule(external/randomx)
if(WOWNERO_FCMP_TESTNET)
check_submodule(external/randomwow)
else()
check_submodule(external/randomx)
endif()
check_submodule(external/supercop)
check_submodule(external/mx25519)
endif()
+7 -1
View File
@@ -31,5 +31,11 @@
add_subdirectory(db_drivers)
add_subdirectory(easylogging++)
add_subdirectory(qrcodegen)
add_subdirectory(randomx EXCLUDE_FROM_ALL)
if(WOWNERO_FCMP_TESTNET)
set(RANDOMX_INCLUDE "${CMAKE_CURRENT_SOURCE_DIR}/randomwow/src" CACHE STRING "RandomWOW include path" FORCE)
add_subdirectory(randomwow EXCLUDE_FROM_ALL)
else()
set(RANDOMX_INCLUDE "${CMAKE_CURRENT_SOURCE_DIR}/randomx/src" CACHE STRING "RandomX include path" FORCE)
add_subdirectory(randomx EXCLUDE_FROM_ALL)
endif()
add_subdirectory(mx25519)
+4 -3
View File
@@ -16,6 +16,7 @@
#define EASYLOGGING_CC
#include "easylogging++.h"
#include <cstdint>
#include <atomic>
#include <unistd.h>
@@ -2502,11 +2503,11 @@ void DefaultLogDispatchCallback::handle(const LogDispatchData* data) {
template<typename Transform>
static inline void utf8canonical(std::string &s, Transform t = [](wint_t c)->wint_t { return c; })
static inline void utf8canonical(std::string &s, Transform t = [](std::uint32_t c)->std::uint32_t { return c; })
{
size_t avail = s.size();
const char *ptr = s.data();
wint_t cp = 0;
std::uint32_t cp = 0;
int rbytes = 1, bytes = -1;
char wbuf[8], *wptr;
size_t w_offset = 0;
@@ -2585,7 +2586,7 @@ static inline void utf8canonical(std::string &s, Transform t = [](wint_t c)->win
void sanitize(std::string &s)
{
utf8canonical(s, [](wint_t c)->wint_t {
utf8canonical(s, [](std::uint32_t c)->std::uint32_t {
if (c == 9 || c == 10 || c == 13)
return c;
if (c < 0x20)
+1
View File
@@ -2792,6 +2792,7 @@ class Storage : base::NoCopy, public base::threading::ThreadSafe {
}
};
extern ELPP_EXPORT base::type::StoragePointer elStorage;
ELPP_EXPORT void sanitize(std::string& s);
#define ELPP el::base::Storage::getELPP()
class DefaultLogDispatchCallback : public LogDispatchCallback {
protected:
Vendored Submodule
+1
Submodule external/randomwow added at ff47479662
+11 -6
View File
@@ -350,6 +350,7 @@ typedef struct mdb_tree_meta {
} mdb_tree_meta;
std::atomic<uint64_t> mdb_txn_safe::num_active_txns{0};
thread_local uint64_t mdb_txn_safe::num_active_txns_per_thread = 0;
std::atomic_flag mdb_txn_safe::creation_gate = ATOMIC_FLAG_INIT;
mdb_threadinfo::~mdb_threadinfo()
@@ -367,9 +368,12 @@ mdb_txn_safe::mdb_txn_safe(const bool check) : m_txn(NULL), m_tinfo(NULL), m_che
{
if (check)
{
while (creation_gate.test_and_set());
num_active_txns++;
creation_gate.clear();
const bool nested = num_active_txns_per_thread != 0;
if (!nested)
while (creation_gate.test_and_set());
increment_txns(1);
if (!nested)
creation_gate.clear();
}
}
@@ -400,12 +404,12 @@ mdb_txn_safe::~mdb_txn_safe()
}
mdb_txn_abort(m_txn);
}
num_active_txns--;
increment_txns(-1);
}
void mdb_txn_safe::uncheck()
{
num_active_txns--;
increment_txns(-1);
m_check = false;
}
@@ -460,7 +464,8 @@ void mdb_txn_safe::allow_new_txns()
void mdb_txn_safe::increment_txns(int i)
{
num_active_txns += i;
num_active_txns_per_thread += i;
num_active_txns += i;
}
#define TXN_PREFIX(flags); \
+1
View File
@@ -182,6 +182,7 @@ struct mdb_txn_safe
bool m_batch_txn = false;
bool m_check;
static std::atomic<uint64_t> num_active_txns;
static thread_local uint64_t num_active_txns_per_thread;
// could use a mutex here, but this should be sufficient.
static std::atomic_flag creation_gate;
+6
View File
@@ -195,6 +195,9 @@ namespace cryptonote
bool checkpoints::init_default_checkpoints(network_type nettype)
{
#ifdef WOWNERO_FCMP_TESTNET
return nettype == TESTNET;
#endif
if (nettype == TESTNET)
{
ADD_CHECKPOINT2(0, "48ca7cd3c8de5b6a4d53d2861fbdaedca141553559f9be9520068053cda8430b", "0x1");
@@ -313,6 +316,9 @@ namespace cryptonote
bool checkpoints::load_checkpoints_from_dns(network_type nettype)
{
#ifdef WOWNERO_FCMP_TESTNET
return nettype == TESTNET;
#endif
std::vector<std::string> records;
// All four MoneroPulse domains have DNSSEC on and valid
+6 -3
View File
@@ -95,17 +95,20 @@ public:
{
m_flush = false;
MCLOG_FILE(m_log_level, "msgwriter", m_oss.str());
std::string message = m_oss.str();
MCLOG_FILE(m_log_level, "msgwriter", message);
try { el::base::sanitize(message); }
catch (const std::exception&) { message = "<Invalid UTF-8 in message>"; }
PAUSE_READLINE();
if (epee::console_color_default == m_color)
{
std::cout << m_oss.str();
std::cout << message;
}
else
{
set_console_color(m_color, m_bright);
std::cout << m_oss.str();
std::cout << message;
epee::reset_console_color();
}
std::cout << std::endl;
+4
View File
@@ -59,6 +59,10 @@ endif()
include_directories(${RANDOMX_INCLUDE})
if(WOWNERO_FCMP_TESTNET)
list(APPEND crypto_sources wow_miner_proof.cpp)
endif()
set(crypto_headers)
monero_find_all_headers(crypto_private_headers "${CMAKE_CURRENT_SOURCE_DIR}")
+8
View File
@@ -120,12 +120,14 @@ static inline int disabled_flags(void) {
flags = 0;
}
#ifndef WOWNERO_FCMP_TESTNET
if (flags & RANDOMX_FLAG_V2)
{
mwarning(RX_LOGCAT,
"MONERO_RANDOMX_UMASK would mask RandomX V2, ignoring that bit...");
flags &= ~RANDOMX_FLAG_V2;
}
#endif
}
return flags;
@@ -148,9 +150,11 @@ static randomx_flags get_variant_flags(const int variant) {
{
case RX_VARIANT_1:
break;
#ifndef WOWNERO_FCMP_TESTNET
case RX_VARIANT_LATEST:
case RX_VARIANT_2:
return RANDOMX_FLAG_V2;
#endif
default:
local_abort("Unrecognized RandomX variant");
}
@@ -543,7 +547,11 @@ void rx_slow_hash(const char *seedhash, const int variant, const void *data, siz
}
void rx_commitment(const void *data, size_t length, const void *rx_hash, char *result_commitment) {
#ifdef WOWNERO_FCMP_TESTNET
local_abort("RandomX v2 commitments are unavailable in the RandomWOW testnet profile");
#else
randomx_calculate_commitment(data, length, rx_hash, result_commitment);
#endif
}
void rx_set_miner_thread(uint32_t value, size_t max_dataset_init_threads) {
+151
View File
@@ -0,0 +1,151 @@
// Copyright (c) 2026, The Wownero Project
// SPDX-License-Identifier: BSD-3-Clause
#include "wow_miner_proof.h"
#include <cstring>
#include "generators.h"
namespace wownero::research
{
namespace
{
constexpr char domain[] = "WOW-CARROT-MINER-PROOF-v1";
constexpr unsigned char subgroup_order[32] = {
0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58,
0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x10};
template<class T> const unsigned char *bytes(const T &value)
{
return reinterpret_cast<const unsigned char *>(value.data);
}
template<class T> unsigned char *bytes(T &value)
{
return reinterpret_cast<unsigned char *>(value.data);
}
bool decode_public_key(const crypto::public_key &key, ge_p3 &point)
{
if (ge_frombytes_vartime(&point, bytes(key)) != 0 ||
ge_p3_is_point_at_infinity_vartime(&point))
return false;
crypto::public_key canonical;
ge_p3_tobytes(bytes(canonical), &point);
if (std::memcmp(key.data, canonical.data, sizeof(key.data)) != 0)
return false;
// Multiplication uses the unreduced subgroup order to reject mixed-order points.
ge_p3 multiplied;
ge_scalarmult_p3(&multiplied, subgroup_order, &point);
return ge_p3_is_point_at_infinity_vartime(&multiplied);
}
void representation(const unsigned char *x, const unsigned char *y, ge_p3 &out)
{
ge_p3 xg, yt;
ge_scalarmult_base(&xg, x);
ge_scalarmult_p3(&yt, y, &crypto::get_T_p3());
ge_cached cached;
ge_p3_to_cached(&cached, &yt);
ge_p1p1 sum;
ge_add(&sum, &xg, &cached);
ge_p1p1_to_p3(&out, &sum);
}
crypto::ec_scalar challenge(const crypto::hash &network_genesis,
const crypto::hash &block_digest, const crypto::public_key &key, const ge_p3 &nonce)
{
std::array<unsigned char, sizeof(domain) - 1 + 4 * 32> transcript{};
auto *cursor = transcript.data();
std::memcpy(cursor, domain, sizeof(domain) - 1);
cursor += sizeof(domain) - 1;
for (const auto *field : {bytes(network_genesis), bytes(block_digest), bytes(key)})
{
std::memcpy(cursor, field, 32);
cursor += 32;
}
ge_p3_tobytes(cursor, &nonce);
crypto::ec_scalar result;
crypto::hash_to_scalar(transcript.data(), transcript.size(), result);
return result;
}
}
bool check_miner_public_key(const crypto::public_key &key)
{
ge_p3 point;
return decode_public_key(key, point);
}
bool generate_miner_proof(const crypto::hash &network_genesis,
const crypto::hash &block_digest, const crypto::public_key &key,
const crypto::secret_key &x, const crypto::secret_key &y, miner_proof &proof)
{
proof.fill(0);
if (sc_check(bytes(x)) != 0 || sc_check(bytes(y)) != 0 || !check_miner_public_key(key))
return false;
ge_p3 expected;
representation(bytes(x), bytes(y), expected);
crypto::public_key encoded;
ge_p3_tobytes(bytes(encoded), &expected);
if (std::memcmp(key.data, encoded.data, sizeof(key.data)) != 0)
return false;
// Secret scalars use the upstream locked and scrubbed key container.
crypto::secret_key a, b;
for (;;)
{
crypto::random32_unbiased(bytes(a));
crypto::random32_unbiased(bytes(b));
ge_p3 nonce;
representation(bytes(a), bytes(b), nonce);
if (ge_p3_is_point_at_infinity_vartime(&nonce))
continue;
const crypto::ec_scalar c = challenge(network_genesis, block_digest, key, nonce);
if (!sc_isnonzero(bytes(c)))
continue;
std::memcpy(proof.data(), c.data, 32);
sc_muladd(proof.data() + 32, bytes(c), bytes(x), bytes(a));
sc_muladd(proof.data() + 64, bytes(c), bytes(y), bytes(b));
return true;
}
}
bool verify_miner_proof(const crypto::hash &network_genesis,
const crypto::hash &block_digest, const crypto::public_key &key,
epee::span<const std::uint8_t> proof)
{
if (proof.size() != miner_proof{}.size())
return false;
const auto *c = proof.data();
const auto *sg = c + 32;
const auto *st = sg + 32;
if (sc_check(c) != 0 || sc_check(sg) != 0 || sc_check(st) != 0 || !sc_isnonzero(c))
return false;
ge_p3 public_point;
if (!decode_public_key(key, public_point))
return false;
ge_p3 response, ck;
representation(sg, st, response);
ge_scalarmult_p3(&ck, c, &public_point);
ge_cached cached;
ge_p3_to_cached(&cached, &ck);
ge_p1p1 difference;
ge_sub(&difference, &response, &cached);
ge_p3 nonce;
ge_p1p1_to_p3(&nonce, &difference);
if (ge_p3_is_point_at_infinity_vartime(&nonce))
return false;
const auto expected = challenge(network_genesis, block_digest, key, nonce);
return std::memcmp(expected.data, c, 32) == 0;
}
}
+30
View File
@@ -0,0 +1,30 @@
// Copyright (c) 2026, The Wownero Project
// SPDX-License-Identifier: BSD-3-Clause
#pragma once
#include <array>
#include <cstddef>
#include <cstdint>
#include "crypto.h"
#include "span.h"
namespace wownero::research
{
// Experimental Schnorr representation proof for K = xG + yT.
using miner_proof = std::array<std::uint8_t, 96>;
bool check_miner_public_key(const crypto::public_key &key);
// block_digest must cover the complete canonical block statement, excluding the proof itself.
// network_genesis is the network's genesis hash. Both parameters are transcript commitments.
// Returns false and clears proof on invalid scalars, public key, or witness mismatch.
bool generate_miner_proof(const crypto::hash &network_genesis,
const crypto::hash &block_digest, const crypto::public_key &key,
const crypto::secret_key &x, const crypto::secret_key &y, miner_proof &proof);
bool verify_miner_proof(const crypto::hash &network_genesis,
const crypto::hash &block_digest, const crypto::public_key &key,
epee::span<const std::uint8_t> proof);
}
+8
View File
@@ -54,6 +54,10 @@ set(cryptonote_basic_sources
merge_mining.cpp
miner.cpp)
if(WOWNERO_FCMP_TESTNET)
list(APPEND cryptonote_basic_sources wow_miner.cpp)
endif()
set(cryptonote_basic_headers)
monero_find_all_headers(cryptonote_basic_private_headers "${CMAKE_CURRENT_SOURCE_DIR}")
@@ -81,3 +85,7 @@ target_link_libraries(cryptonote_basic
${Boost_THREAD_LIBRARY}
PRIVATE
${EXTRA_LIBRARIES})
if(WOWNERO_FCMP_TESTNET)
target_link_libraries(cryptonote_basic PUBLIC carrot_core)
endif()
+15
View File
@@ -48,6 +48,9 @@
#include "cryptonote_config.h"
#include "crypto/crypto.h"
#include "crypto/hash.h"
#ifdef WOWNERO_FCMP_TESTNET
#include "crypto/wow_miner_proof.h"
#endif
#include "ringct/rctTypes.h"
#include "device/device.hpp"
#include "cryptonote_basic/fwd.h"
@@ -611,6 +614,10 @@ namespace cryptonote
uint64_t timestamp;
crypto::hash prev_id;
uint32_t nonce;
#ifdef WOWNERO_FCMP_TESTNET
wownero::research::miner_proof wow_miner_signature{};
uint16_t wow_vote = 0;
#endif
BEGIN_SERIALIZE()
VARINT_FIELD(major_version)
@@ -618,6 +625,14 @@ namespace cryptonote
VARINT_FIELD(timestamp)
FIELD(prev_id)
FIELD(nonce)
#ifdef WOWNERO_FCMP_TESTNET
if (major_version >= HF_VERSION_CARROT)
{
ar.tag("wow_miner_signature");
ar.serialize_blob(wow_miner_signature.data(), wow_miner_signature.size());
FIELD(wow_vote)
}
#endif
END_SERIALIZE()
};
@@ -270,12 +270,13 @@ namespace cryptonote
return true;
}
//---------------------------------------------------------------
bool parse_and_validate_tx_base_from_blob(const blobdata_ref& tx_blob, transaction& tx, const bool max_size_check)
bool parse_and_validate_tx_base_from_blob(const blobdata_ref& tx_blob, transaction& tx, const bool max_size_check, const bool require_eof)
{
CHECK_AND_ASSERT_MES(passes_max_size_check(max_size_check, tx_blob), false, "Tx blob too big");
binary_archive<false> ba{epee::strspan<std::uint8_t>(tx_blob)};
bool r = tx.serialize_base(ba);
CHECK_AND_ASSERT_MES(r, false, "Failed to parse transaction from blob");
CHECK_AND_ASSERT_MES(::serialization::check_stream_state(ba, !require_eof), false, "Unexpected trailing data after transaction base");
CHECK_AND_ASSERT_MES(expand_transaction_1(tx, true), false, "Failed to expand transaction data");
tx.invalidate_hashes();
return true;
@@ -61,7 +61,7 @@ namespace cryptonote
bool parse_and_validate_tx_from_blob(const blobdata_ref& tx_blob, transaction& tx, crypto::hash& tx_hash, crypto::hash& tx_prefix_hash, const bool max_size_check = false);
bool parse_and_validate_tx_from_blob(const blobdata_ref& tx_blob, transaction& tx, crypto::hash& tx_hash, const bool max_size_check = false);
bool parse_and_validate_tx_from_blob(const blobdata_ref& tx_blob, transaction& tx, const bool max_size_check = false);
bool parse_and_validate_tx_base_from_blob(const blobdata_ref& tx_blob, transaction& tx, const bool max_size_check = false);
bool parse_and_validate_tx_base_from_blob(const blobdata_ref& tx_blob, transaction& tx, const bool max_size_check = false, const bool require_eof = false);
/**
* @brief extract transaction version from transaction blob
* @param tx_blob transction blob
+50
View File
@@ -92,6 +92,10 @@ namespace cryptonote
namespace
{
#ifdef WOWNERO_FCMP_TESTNET
const command_line::arg_descriptor<int> arg_wow_miner_key_fd = {
"wow-miner-key-fd", "Inherited pipe containing the Carrot mining account frame", -1};
#endif
const command_line::arg_descriptor<std::string> arg_extra_messages = {"extra-messages-file", "Specify file for extra messages to include into coinbase transactions", "", true};
const command_line::arg_descriptor<std::string> arg_start_mining = {"start-mining", "Specify wallet address to mining for", "", true};
const command_line::arg_descriptor<uint32_t> arg_mining_threads = {"mining-threads", "Specify mining threads count", 0, true};
@@ -285,6 +289,9 @@ namespace cryptonote
//-----------------------------------------------------------------------------------------------------
void miner::init_options(boost::program_options::options_description& desc)
{
#ifdef WOWNERO_FCMP_TESTNET
command_line::add_arg(desc, arg_wow_miner_key_fd);
#endif
command_line::add_arg(desc, arg_extra_messages);
command_line::add_arg(desc, arg_start_mining);
command_line::add_arg(desc, arg_mining_threads);
@@ -297,6 +304,16 @@ namespace cryptonote
//-----------------------------------------------------------------------------------------------------
bool miner::init(const boost::program_options::variables_map& vm, network_type nettype)
{
#ifdef WOWNERO_FCMP_TESTNET
const int key_fd = command_line::get_arg(vm, arg_wow_miner_key_fd);
if (key_fd != -1)
{
wow_miner_account account;
CHECK_AND_ASSERT_MES(read_wow_miner_account(key_fd, account), false,
"Invalid mining account pipe: expected fd >= 3 and a complete canonical WOWMIN01 frame within five seconds");
m_wow_miner_account = account;
}
#endif
if(command_line::has_arg(vm, arg_extra_messages))
{
std::string buff;
@@ -370,6 +387,12 @@ namespace cryptonote
//-----------------------------------------------------------------------------------------------------
bool miner::start(const account_public_address& adr, size_t threads_count, bool do_background, bool ignore_battery)
{
#ifdef WOWNERO_FCMP_TESTNET
account_public_address signer_address;
CHECK_AND_ASSERT_MES(m_wow_miner_account && m_wow_miner_account->address(signer_address), false,
"Mining requires an account supplied through --wow-miner-key-fd at startup");
CHECK_AND_ASSERT_MES(adr == signer_address, false, "Mining address does not match the supplied account");
#endif
m_block_reward = 0;
m_mine_address = adr;
m_threads_total = static_cast<uint32_t>(threads_count);
@@ -393,7 +416,11 @@ namespace cryptonote
return false;
}
#ifdef WOWNERO_FCMP_TESTNET
CHECK_AND_ASSERT_MES(request_block_template(), false, "Unable to prepare Wownero mining template");
#else
request_block_template();//lets update block template
#endif
m_stop = false;
m_thread_index = 0;
@@ -475,6 +502,10 @@ namespace cryptonote
//-----------------------------------------------------------------------------------------------------
bool miner::find_nonce_for_given_block(const get_block_hash_t &gbh, block& bl, const difficulty_type& diffic, uint64_t height, const crypto::hash *seed_hash)
{
#ifdef WOWNERO_FCMP_TESTNET
CHECK_AND_ASSERT_MES(bl.major_version < HF_VERSION_CARROT, false,
"Synchronous mining cannot sign Carrot blocks; use the account-backed miner");
#endif
for(; bl.nonce != std::numeric_limits<uint32_t>::max(); bl.nonce++)
{
crypto::hash h;
@@ -533,6 +564,9 @@ namespace cryptonote
difficulty_type local_diff = 0;
uint32_t local_template_ver = 0;
block b;
#ifdef WOWNERO_FCMP_TESTNET
crypto::secret_key miner_x, miner_y;
#endif
slow_hash_allocate_state();
++m_threads_active;
while(!m_stop)
@@ -565,6 +599,14 @@ namespace cryptonote
CRITICAL_REGION_END();
local_template_ver = m_template_no;
nonce = m_starter_nonce + th_local_index;
#ifdef WOWNERO_FCMP_TESTNET
if (!prepare_wow_miner_output_keys(b, *m_wow_miner_account, miner_x, miner_y))
{
MERROR("Mining template does not pay the supplied Carrot account");
m_stop = true;
break;
}
#endif
}
if(!local_template_ver)//no any set_block_template call
@@ -575,6 +617,14 @@ namespace cryptonote
}
b.nonce = nonce;
#ifdef WOWNERO_FCMP_TESTNET
if (!sign_wow_miner_block(b, miner_x, miner_y))
{
MERROR("Failed to sign the Wownero mining candidate");
m_stop = true;
break;
}
#endif
crypto::hash h;
if ((b.major_version >= RX_BLOCK_VERSION) && !rx_set)
+7
View File
@@ -36,6 +36,10 @@
#include "cryptonote_basic.h"
#include "verification_context.h"
#include "difficulty.h"
#ifdef WOWNERO_FCMP_TESTNET
#include <optional>
#include "wow_miner.h"
#endif
#include "math_helper.h"
#include "syncobj.h"
#include "cryptonote_basic/blobdatatype.h"
@@ -138,6 +142,9 @@ namespace cryptonote
i_miner_handler* m_phandler;
get_block_hash_t m_gbh;
account_public_address m_mine_address;
#ifdef WOWNERO_FCMP_TESTNET
std::optional<wow_miner_account> m_wow_miner_account;
#endif
epee::math_helper::once_a_time_seconds<5> m_update_block_template_interval;
epee::math_helper::once_a_time_seconds<2> m_update_merge_hr_interval;
epee::math_helper::once_a_time_seconds<1> m_autodetect_interval;
+184
View File
@@ -0,0 +1,184 @@
// Copyright (c) 2026, The Wownero Project
// SPDX-License-Identifier: BSD-3-Clause
#include "wow_miner.h"
#include <chrono>
#include <cstring>
#include <cerrno>
#include <poll.h>
#include <sys/stat.h>
#include <unistd.h>
#include "carrot_core/account_secrets.h"
#include "carrot_core/device_ram_borrowed.h"
#include "carrot_core/scan.h"
#include "cryptonote_format_utils.h"
#include "string_tools.h"
#include "wownero/testnet_genesis.h"
namespace cryptonote
{
namespace
{
template<class T> unsigned char *bytes(T &value)
{
return reinterpret_cast<unsigned char *>(value.data);
}
template<class T> const unsigned char *bytes(const T &value)
{
return reinterpret_cast<const unsigned char *>(value.data);
}
const crypto::hash &network_genesis()
{
static const crypto::hash value = [] {
crypto::hash result;
if (!epee::string_tools::hex_to_pod(wownero::testnet::genesis_hash, result))
throw std::logic_error("Invalid compiled Wownero testnet genesis hash");
return result;
}();
return value;
}
const txout_to_carrot_v1 *miner_output(const block &b)
{
if (b.major_version < HF_VERSION_CARROT || b.wow_vote > 2 ||
b.miner_tx.vin.size() != 1 || b.miner_tx.vout.size() != 1 ||
!boost::strict_get<txin_gen>(&b.miner_tx.vin.front()))
return nullptr;
return boost::strict_get<txout_to_carrot_v1>(&b.miner_tx.vout.front().target);
}
}
bool wow_miner_account::address(account_public_address &out) const
{
if (sc_check(bytes(generate_image)) || sc_check(bytes(prove_spend)) ||
sc_check(bytes(view_incoming)) || !sc_isnonzero(bytes(view_incoming)))
return false;
carrot::make_carrot_spend_pubkey(generate_image, prove_spend, out.m_spend_public_key);
return wownero::research::check_miner_public_key(out.m_spend_public_key) &&
crypto::secret_key_to_public_key(view_incoming, out.m_view_public_key);
}
bool read_wow_miner_account(int fd, wow_miner_account &out)
{
out = {};
if (fd < 3)
return false;
struct descriptor_guard { int fd; ~descriptor_guard() { close(fd); } } guard{fd};
struct stat info{};
if (fstat(fd, &info) != 0 || !S_ISFIFO(info.st_mode))
return false;
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
auto read_chunk = [&](void *destination, std::size_t size) {
auto *cursor = static_cast<unsigned char *>(destination);
while (size)
{
const auto remaining = std::chrono::duration_cast<std::chrono::milliseconds>(
deadline - std::chrono::steady_clock::now()).count();
if (remaining <= 0)
return false;
pollfd ready{fd, POLLIN, 0};
const int status = poll(&ready, 1, static_cast<int>(remaining));
if (status < 0 && errno == EINTR)
continue;
if (status <= 0 || (ready.revents & (POLLERR | POLLNVAL)))
return false;
const ssize_t count = read(fd, cursor, size);
if (count < 0 && errno == EINTR)
continue;
if (count <= 0)
return false;
cursor += count;
size -= count;
}
return true;
};
char magic[8];
wow_miner_account candidate;
if (!read_chunk(magic, sizeof(magic)) || std::memcmp(magic, "WOWMIN01", 8) ||
!read_chunk(candidate.generate_image.data, 32) ||
!read_chunk(candidate.prove_spend.data, 32) ||
!read_chunk(candidate.view_incoming.data, 32))
return false;
// The fixed frame is sufficient. The input descriptor is closed immediately.
account_public_address address;
if (!candidate.address(address))
return false;
out = candidate;
return true;
}
crypto::hash get_wow_miner_statement_hash(const block &b)
{
block statement = b;
statement.wow_miner_signature.fill(0);
statement.invalidate_hashes();
statement.miner_tx.invalidate_hashes();
const auto blob = get_block_hashing_blob(statement);
return crypto::cn_fast_hash(blob.data(), blob.size());
}
bool prepare_wow_miner_output_keys(const block &b, const wow_miner_account &account,
crypto::secret_key &x, crypto::secret_key &y)
{
x = {}; y = {};
const auto *output = miner_output(b);
account_public_address address;
if (!output || !account.address(address))
return false;
std::vector<tx_extra_field> fields;
if (!parse_tx_extra(b.miner_tx.extra, fields))
return false;
const tx_extra_pub_key *ephemeral = nullptr;
for (const auto &field : fields)
{
if (const auto *key = boost::strict_get<tx_extra_pub_key>(&field))
{
if (ephemeral)
return false;
ephemeral = key;
}
if (boost::strict_get<tx_extra_additional_pub_keys>(&field))
return false;
}
if (!ephemeral)
return false;
carrot::CarrotCoinbaseEnoteV1 enote;
enote.block_index = boost::get<txin_gen>(b.miner_tx.vin.front()).height;
enote.amount = b.miner_tx.vout.front().amount;
enote.onetime_address = output->key;
enote.view_tag = output->view_tag;
enote.anchor_enc = output->encrypted_janus_anchor;
static_assert(sizeof(enote.enote_ephemeral_pubkey) == sizeof(ephemeral->pub_key));
std::memcpy(&enote.enote_ephemeral_pubkey, &ephemeral->pub_key, sizeof(ephemeral->pub_key));
carrot::view_incoming_key_ram_borrowed_device device(account.view_incoming);
mx25519_pubkey shared;
crypto::secret_key extension_g, extension_t;
if (!carrot::try_make_carrot_shared_key_receiver(device, enote.enote_ephemeral_pubkey, shared) ||
!carrot::try_scan_carrot_coinbase_enote_receiver(enote, shared, address.m_spend_public_key,
address.m_view_public_key, extension_g, extension_t))
return false;
sc_add(bytes(x), bytes(account.generate_image), bytes(extension_g));
sc_add(bytes(y), bytes(account.prove_spend), bytes(extension_t));
return true;
}
bool sign_wow_miner_block(block &b, const crypto::secret_key &x, const crypto::secret_key &y)
{
b.wow_miner_signature.fill(0);
b.invalidate_hashes();
const auto *output = miner_output(b);
return output && wownero::research::generate_miner_proof(network_genesis(),
get_wow_miner_statement_hash(b), output->key, x, y, b.wow_miner_signature);
}
bool verify_wow_miner_block(const block &b)
{
const auto *output = miner_output(b);
return output && wownero::research::verify_miner_proof(network_genesis(),
get_wow_miner_statement_hash(b), output->key,
{b.wow_miner_signature.data(), b.wow_miner_signature.size()});
}
}
+25
View File
@@ -0,0 +1,25 @@
// Copyright (c) 2026, The Wownero Project
// SPDX-License-Identifier: BSD-3-Clause
#pragma once
#include "cryptonote_basic.h"
namespace cryptonote
{
struct wow_miner_account
{
crypto::secret_key generate_image{}, prove_spend{}, view_incoming{};
bool address(account_public_address &out) const;
};
// Consumes a pipe descriptor and closes it on every outcome. The frame is
// "WOWMIN01" followed by three canonical scalars; no credential enters argv.
bool read_wow_miner_account(int fd, wow_miner_account &out);
crypto::hash get_wow_miner_statement_hash(const block &b);
bool prepare_wow_miner_output_keys(const block &b, const wow_miner_account &account,
crypto::secret_key &x, crypto::secret_key &y);
bool sign_wow_miner_block(block &b, const crypto::secret_key &x, const crypto::secret_key &y);
bool verify_wow_miner_block(const block &b);
}
+86
View File
@@ -34,39 +34,70 @@
#include <stdexcept>
#include <string>
#include <boost/uuid/uuid.hpp>
#ifdef WOWNERO_FCMP_TESTNET
#include "wownero/testnet_genesis.h"
#endif
#define CRYPTONOTE_DNS_TIMEOUT_MS 20000
#define CRYPTONOTE_MAX_BLOCK_NUMBER 500000000
#define CRYPTONOTE_MAX_TX_SIZE 1000000
#define CRYPTONOTE_MAX_TX_PER_BLOCK 0x10000000
#ifdef WOWNERO_FCMP_TESTNET
#define CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW 288
#else
#define CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW 60
#endif
#define CURRENT_TRANSACTION_VERSION 2
#define CURRENT_BLOCK_MAJOR_VERSION 1
#define CURRENT_BLOCK_MINOR_VERSION 0
#define CRYPTONOTE_BLOCK_FUTURE_TIME_LIMIT 60*60*2
#ifdef WOWNERO_FCMP_TESTNET
#define CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE 4
#else
#define CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE 10
#endif
#define BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW 60
// MONEY_SUPPLY - total number coins to be generated
#define MONEY_SUPPLY ((uint64_t)(-1))
#ifdef WOWNERO_FCMP_TESTNET
#define EMISSION_SPEED_FACTOR_PER_MINUTE (24)
#else
#define EMISSION_SPEED_FACTOR_PER_MINUTE (20)
#endif
#ifdef WOWNERO_FCMP_TESTNET
#define FINAL_SUBSIDY_PER_MINUTE ((uint64_t)0)
#else
#define FINAL_SUBSIDY_PER_MINUTE ((uint64_t)300000000000) // 3 * pow(10, 11)
#endif
#define CRYPTONOTE_REWARD_BLOCKS_WINDOW 100
#define CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V2 60000 //size of block (bytes) after which reward for block calculated using block size
#define CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V1 20000 //size of block (bytes) after which reward for block calculated using block size - before first fork
#define CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5 300000 //size of block (bytes) after which reward for block calculated using block size - second change, from v5
#ifdef WOWNERO_FCMP_TESTNET
#define CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V17 300000
#else
#define CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V17 10000000 //STRESSNET SPECIFIC size of block (bytes) after which reward for block calculated using block size - third change, from v17
#endif
#define CRYPTONOTE_LONG_TERM_BLOCK_WEIGHT_WINDOW_SIZE 100000 // size in blocks of the long term block weight median window
#define CRYPTONOTE_STRESSNET_BLOCK_WEIGHT_WINDOW_SIZE 10080 // size in blocks of the long term block weight median window [2 weeks] [STRESSNET ONLY!!!!]
#define CRYPTONOTE_SHORT_TERM_BLOCK_SURGE_FACTOR_V10 50
#define CRYPTONOTE_SHORT_TERM_BLOCK_SURGE_FACTOR_V17 8
#define CRYPTONOTE_COINBASE_BLOB_RESERVED_SIZE 600
#ifdef WOWNERO_FCMP_TESTNET
#define CRYPTONOTE_DISPLAY_DECIMAL_POINT 11
#else
#define CRYPTONOTE_DISPLAY_DECIMAL_POINT 12
#endif
// COIN - number of smallest units in one coin
#ifdef WOWNERO_FCMP_TESTNET
#define COIN ((uint64_t)100000000000)
#else
#define COIN ((uint64_t)1000000000000) // pow(10, 12)
#endif
#define FEE_PER_KB_OLD ((uint64_t)10000000000) // pow(10, 10)
#define FEE_PER_KB ((uint64_t)2000000000) // 2 * pow(10, 9)
@@ -80,11 +111,31 @@
#define ORPHANED_BLOCKS_MAX_COUNT 100
#ifdef WOWNERO_FCMP_TESTNET
#define DIFFICULTY_TARGET_V2 300
#else
#define DIFFICULTY_TARGET_V2 120 // seconds
#endif
#ifdef WOWNERO_FCMP_TESTNET
#define DIFFICULTY_TARGET_V1 300
#else
#define DIFFICULTY_TARGET_V1 60 // seconds - before first fork
#endif
#ifdef WOWNERO_FCMP_TESTNET
#define DIFFICULTY_WINDOW 144
#else
#define DIFFICULTY_WINDOW 720 // blocks
#endif
#ifdef WOWNERO_FCMP_TESTNET
#define DIFFICULTY_LAG 3
#else
#define DIFFICULTY_LAG 15 // !!!
#endif
#ifdef WOWNERO_FCMP_TESTNET
#define DIFFICULTY_CUT 12
#else
#define DIFFICULTY_CUT 60 // timestamps to cut after sorting
#endif
#define DIFFICULTY_BLOCKS_COUNT DIFFICULTY_WINDOW + DIFFICULTY_LAG
@@ -172,7 +223,11 @@
#define RPC_IP_FAILS_BEFORE_BLOCK 3
#ifdef WOWNERO_FCMP_TESTNET
#define CRYPTONOTE_NAME "wownero-fcmp-testnet"
#else
#define CRYPTONOTE_NAME "bitmonero"
#endif
#define CRYPTONOTE_BLOCKCHAINDATA_FILENAME "data.mdb"
#define CRYPTONOTE_BLOCKCHAINDATA_LOCK_FILENAME "lock.mdb"
#define P2P_NET_DATA_FILENAME "p2pstate.bin"
@@ -209,8 +264,16 @@
#define HF_VERSION_REJECT_UNMIXABLE_V1 17
#define HF_VERSION_REJECT_MANY_MINER_OUTPUTS 17
#define HF_VERSION_2026_SCALING 17
#ifdef WOWNERO_FCMP_TESTNET
#define HF_VERSION_POW_COMMITMENT 255
#else
#define HF_VERSION_POW_COMMITMENT 17
#endif
#ifdef WOWNERO_FCMP_TESTNET
#define HF_VERSION_RANDOMX_V2 255
#else
#define HF_VERSION_RANDOMX_V2 17
#endif
#define MAX_HF_VERSION 18
#define PER_KB_FEE_QUANTIZATION_DECIMALS 8
@@ -231,7 +294,11 @@
// https://libera.monerologs.net/no-wallet-left-behind/20250505#c523568-c523686
#define FCMP_PLUS_PLUS_MAX_INPUTS 128
#define FCMP_PLUS_PLUS_MAX_OUTPUTS 16
#ifdef WOWNERO_FCMP_TESTNET
#define FCMP_PLUS_PLUS_MAX_MINER_OUTPUTS 1
#else
#define FCMP_PLUS_PLUS_MAX_MINER_OUTPUTS 10000
#endif
#define FCMP_PLUS_PLUS_MAX_TX_WEIGHT 178063
// Limiting n layers ensures proof size is capped to a reasonable level when de-serializing a proof
@@ -300,6 +367,20 @@ namespace config
// Multisig
const uint32_t MULTISIG_MAX_SIGNERS{16};
#ifdef WOWNERO_FCMP_TESTNET
namespace testnet
{
uint64_t const CRYPTONOTE_PUBLIC_ADDRESS_BASE58_PREFIX = 35346;
uint64_t const CRYPTONOTE_PUBLIC_INTEGRATED_ADDRESS_BASE58_PREFIX = 35347;
uint64_t const CRYPTONOTE_PUBLIC_SUBADDRESS_BASE58_PREFIX = 35348;
uint16_t const P2P_DEFAULT_PORT = 48880;
uint16_t const RPC_DEFAULT_PORT = 48881;
uint16_t const ZMQ_RPC_DEFAULT_PORT = 48882;
boost::uuids::uuid const NETWORK_ID = {{0x45, 0x73, 0x4e, 0x6a, 0x7a, 0x13, 0x5c, 0x1e, 0x8b, 0x11, 0xe4, 0x0c, 0xee, 0xdd, 0x04, 0x03}};
std::string const GENESIS_TX = wownero::testnet::genesis_tx;
uint32_t const GENESIS_NONCE = wownero::testnet::genesis_nonce;
}
#else
namespace testnet
{
uint64_t const CRYPTONOTE_PUBLIC_ADDRESS_BASE58_PREFIX = 53;
@@ -314,6 +395,7 @@ namespace config
std::string const GENESIS_TX = "013c01ff0001ffffffffffff03029b2e4c0281c0b02e7c53291a94d1d0cbff8883f8024f5142ee494ffbbd08807121017767aafcde9be00dcfd098715ebcf7f410daebc582fda69d24a28e9d0bc890d1";
uint32_t const GENESIS_NONCE = 10001;
}
#endif
namespace stagenet
{
@@ -388,6 +470,10 @@ namespace cryptonote
::config::stagenet::GENESIS_TX,
::config::stagenet::GENESIS_NONCE
};
#ifdef WOWNERO_FCMP_TESTNET
if (nettype != TESTNET)
throw std::runtime_error("Wownero FCMP research binaries require the isolated testnet network");
#endif
switch (nettype)
{
case MAINNET: return mainnet;
+41 -11
View File
@@ -40,6 +40,9 @@
#include "cryptonote_basic/cryptonote_basic_impl.h"
#include "tx_pool.h"
#include "blockchain.h"
#ifdef WOWNERO_FCMP_TESTNET
#include "cryptonote_basic/wow_miner.h"
#endif
#include "blockchain_db/blockchain_db.h"
#include "cryptonote_basic/events.h"
#include "cryptonote_config.h"
@@ -132,7 +135,7 @@ Blockchain::Blockchain(tx_memory_pool& tx_pool) :
m_long_term_block_weights_cache_rolling_median(CRYPTONOTE_LONG_TERM_BLOCK_WEIGHT_WINDOW_SIZE),
m_difficulty_for_next_block_top_hash(crypto::null_hash),
m_difficulty_for_next_block(1),
m_btc_valid(false),
m_btc_include_sensitive(true), m_btc_valid(false),
m_batch_success(true),
m_prepare_height(0)
{
@@ -169,7 +172,7 @@ bool Blockchain::have_tx_keyimg_as_spent(const crypto::key_image &key_im) const
// and collects the public key for each from the transaction it was included in
// via the visitor passed to it.
template <class visitor_t>
bool Blockchain::scan_outputkeys_for_indexes(size_t tx_version, const txin_to_key& tx_in_to_key, visitor_t &vis, const crypto::hash &tx_prefix_hash, uint64_t* pmax_related_block_height) const
bool Blockchain::scan_outputkeys_for_indexes(const uint8_t hf_version, size_t tx_version, const txin_to_key& tx_in_to_key, visitor_t &vis, const crypto::hash &tx_prefix_hash, uint64_t* pmax_related_block_height) const
{
LOG_PRINT_L3("Blockchain::" << __func__);
@@ -248,6 +251,7 @@ bool Blockchain::scan_outputkeys_for_indexes(size_t tx_version, const txin_to_ke
}
size_t count = 0;
uint64_t last_height = 0;
for (const uint64_t& i : absolute_offsets)
{
try
@@ -261,6 +265,15 @@ bool Blockchain::scan_outputkeys_for_indexes(size_t tx_version, const txin_to_ke
else
output_index = m_db->get_output_key(tx_in_to_key.amount, i).data;
if (hf_version >= HF_VERSION_FCMP_PLUS_PLUS)
{
// The legacy spend guard uses the final ring member height to exclude
// Carrot outputs. It must be the greatest height from FCMP activation.
// Preserve historical offset behavior before activation.
CHECK_AND_ASSERT_MES(output_index.height >= last_height, false, "Unexpected decreasing ring member height");
last_height = output_index.height;
}
// call to the passed boost visitor to grab the public key for the output
if (!vis.handle_output(output_index.unlock_time, output_index.pubkey, output_index.commitment))
{
@@ -720,7 +733,7 @@ block Blockchain::pop_block_from_blockchain(bool keep_txs)
curr_ring.reserve(pin->key_offsets.size());
outputs_visitor vis{curr_ring};
if (!scan_outputkeys_for_indexes(tx.version, *pin, vis, tx_prefix_hash))
if (!scan_outputkeys_for_indexes(version, tx.version, *pin, vis, tx_prefix_hash))
{
dereferenced_mix_ring.clear();
break;
@@ -1035,6 +1048,10 @@ difficulty_type Blockchain::get_difficulty_for_next_block()
}
size_t target = get_difficulty_target();
difficulty_type diff = next_difficulty(timestamps, difficulties, target);
#ifdef WOWNERO_FCMP_TESTNET
if (height > 0 && height <= 720)
diff = 100;
#endif
CRITICAL_REGION_LOCAL1(m_difficulty_lock);
m_difficulty_for_next_block_top_hash = top_hash;
@@ -1395,6 +1412,10 @@ difficulty_type Blockchain::get_next_difficulty_for_alternative_chain(const std:
size_t target = get_ideal_hard_fork_version(bei.height) < 2 ? DIFFICULTY_TARGET_V1 : DIFFICULTY_TARGET_V2;
// calculate the difficulty target for the block and return it
#ifdef WOWNERO_FCMP_TESTNET
if (bei.height > 0 && bei.height <= 720)
return 100;
#endif
return next_difficulty(timestamps, cumulative_difficulties, target);
}
//------------------------------------------------------------------
@@ -1434,6 +1455,13 @@ bool Blockchain::prevalidate_miner_transaction(const block& b, uint64_t height,
CHECK_AND_ASSERT_MES(check_output_types(b.miner_tx, hf_version), false, "miner transaction has invalid output type(s) in block " << get_block_hash(b));
#ifdef WOWNERO_FCMP_TESTNET
if (hf_version >= HF_VERSION_CARROT)
{
CHECK_AND_ASSERT_MES(verify_wow_miner_block(b), false, "Invalid Carrot miner ownership proof or vote");
}
#endif
CHECK_AND_ASSERT_MES(check_transaction_output_pubkeys_order(b.miner_tx, hf_version),
false, "FCMP++ miner transaction has unsorted outputs in block " << get_block_hash(b));
@@ -1658,7 +1686,7 @@ uint64_t Blockchain::get_current_cumulative_block_weight_median() const
// in a lot of places. That flag is not referenced in any of the code
// nor any of the makefiles, however. Need to look into whether or not it's
// necessary at all.
bool Blockchain::create_block_template(block& b, const crypto::hash *from_block, const account_public_address& miner_address, difficulty_type& diffic, uint64_t& height, uint64_t& expected_reward, uint64_t& cumulative_weight, const blobdata& ex_nonce, uint64_t &seed_height, crypto::hash &seed_hash)
bool Blockchain::create_block_template(block& b, const crypto::hash *from_block, const account_public_address& miner_address, difficulty_type& diffic, uint64_t& height, uint64_t& expected_reward, uint64_t& cumulative_weight, const blobdata& ex_nonce, uint64_t &seed_height, crypto::hash &seed_hash, bool include_sensitive)
{
LOG_PRINT_L3("Blockchain::" << __func__);
size_t median_weight;
@@ -1676,7 +1704,8 @@ bool Blockchain::create_block_template(block& b, const crypto::hash *from_block,
// this would be the case anyway if we'd lock, and the change happened
// just after the block template was created
if (!memcmp(&miner_address, &m_btc_address, sizeof(cryptonote::account_public_address)) && m_btc_nonce == ex_nonce
&& m_btc_pool_cookie == m_tx_pool.cookie() && m_btc.prev_id == get_tail_id()) {
&& m_btc_pool_cookie == m_tx_pool.cookie() && m_btc.prev_id == get_tail_id()
&& m_btc_include_sensitive == include_sensitive) {
MDEBUG("Using cached template");
const uint64_t now = time(NULL);
if (m_btc.timestamp < now) // ensures it can't get below the median of the last few blocks
@@ -1814,7 +1843,7 @@ bool Blockchain::create_block_template(block& b, const crypto::hash *from_block,
size_t txs_weight;
uint64_t fee;
if (!m_tx_pool.fill_block_template(b, median_weight, already_generated_coins, txs_weight, fee, expected_reward, b.major_version))
if (!m_tx_pool.fill_block_template(b, median_weight, already_generated_coins, txs_weight, fee, expected_reward, b.major_version, include_sensitive))
{
return false;
}
@@ -1933,16 +1962,16 @@ bool Blockchain::create_block_template(block& b, const crypto::hash *from_block,
#endif
if (!from_block)
cache_block_template(b, miner_address, ex_nonce, diffic, height, expected_reward, cumulative_weight, seed_height, seed_hash, pool_cookie);
cache_block_template(b, miner_address, ex_nonce, diffic, height, expected_reward, cumulative_weight, seed_height, seed_hash, pool_cookie, include_sensitive);
return true;
}
LOG_ERROR("Failed to create_block_template with " << 10 << " tries");
return false;
}
//------------------------------------------------------------------
bool Blockchain::create_block_template(block& b, const account_public_address& miner_address, difficulty_type& diffic, uint64_t& height, uint64_t& expected_reward, uint64_t& cumulative_weight, const blobdata& ex_nonce, uint64_t &seed_height, crypto::hash &seed_hash)
bool Blockchain::create_block_template(block& b, const account_public_address& miner_address, difficulty_type& diffic, uint64_t& height, uint64_t& expected_reward, uint64_t& cumulative_weight, const blobdata& ex_nonce, uint64_t &seed_height, crypto::hash &seed_hash, bool include_sensitive)
{
return create_block_template(b, NULL, miner_address, diffic, height, expected_reward, cumulative_weight, ex_nonce, seed_height, seed_hash);
return create_block_template(b, NULL, miner_address, diffic, height, expected_reward, cumulative_weight, ex_nonce, seed_height, seed_hash, include_sensitive);
}
//------------------------------------------------------------------
bool Blockchain::get_miner_data(uint8_t& major_version, uint64_t& height, crypto::hash& prev_id, uint8_t& fcmp_pp_n_tree_layers, crypto::ec_point& fcmp_pp_tree_root, crypto::hash& seed_hash, difficulty_type& difficulty, uint64_t& median_weight, uint64_t& already_generated_coins, std::vector<tx_block_template_backlog_entry>& tx_backlog)
@@ -4328,7 +4357,7 @@ bool Blockchain::check_tx_input(size_t tx_version, const txin_to_key& txin, cons
// collect output keys
outputs_visitor vi(output_keys, *this, hf_version);
if (!scan_outputkeys_for_indexes(tx_version, txin, vi, tx_prefix_hash, pmax_related_block_height))
if (!scan_outputkeys_for_indexes(hf_version, tx_version, txin, vi, tx_prefix_hash, pmax_related_block_height))
{
MERROR_VER("Failed to get output keys for tx with amount = " << print_money(txin.amount) << " and count indexes " << txin.key_offsets.size());
return false;
@@ -6261,7 +6290,7 @@ void Blockchain::invalidate_block_template_cache()
m_btc_valid = false;
}
void Blockchain::cache_block_template(const block &b, const cryptonote::account_public_address &address, const blobdata &nonce, const difficulty_type &diff, uint64_t height, uint64_t expected_reward, uint64_t cumulative_weight, uint64_t seed_height, const crypto::hash &seed_hash, uint64_t pool_cookie)
void Blockchain::cache_block_template(const block &b, const cryptonote::account_public_address &address, const blobdata &nonce, const difficulty_type &diff, uint64_t height, uint64_t expected_reward, uint64_t cumulative_weight, uint64_t seed_height, const crypto::hash &seed_hash, uint64_t pool_cookie, bool include_sensitive)
{
MDEBUG("Setting block template cache");
m_btc = b;
@@ -6274,6 +6303,7 @@ void Blockchain::cache_block_template(const block &b, const cryptonote::account_
m_btc_seed_hash = seed_hash;
m_btc_seed_height = seed_height;
m_btc_pool_cookie = pool_cookie;
m_btc_include_sensitive = include_sensitive;
m_btc_valid = true;
}
+6 -4
View File
@@ -390,8 +390,8 @@ namespace cryptonote
*
* @return true if block template filled in successfully, else false
*/
bool create_block_template(block& b, const account_public_address& miner_address, difficulty_type& di, uint64_t& height, uint64_t& expected_reward, uint64_t& cumulative_weight, const blobdata& ex_nonce, uint64_t &seed_height, crypto::hash &seed_hash);
bool create_block_template(block& b, const crypto::hash *from_block, const account_public_address& miner_address, difficulty_type& di, uint64_t& height, uint64_t& expected_reward, uint64_t& cumulative_weight, const blobdata& ex_nonce, uint64_t &seed_height, crypto::hash &seed_hash);
bool create_block_template(block& b, const account_public_address& miner_address, difficulty_type& di, uint64_t& height, uint64_t& expected_reward, uint64_t& cumulative_weight, const blobdata& ex_nonce, uint64_t &seed_height, crypto::hash &seed_hash, bool include_sensitive = true);
bool create_block_template(block& b, const crypto::hash *from_block, const account_public_address& miner_address, difficulty_type& di, uint64_t& height, uint64_t& expected_reward, uint64_t& cumulative_weight, const blobdata& ex_nonce, uint64_t &seed_height, crypto::hash &seed_hash, bool include_sensitive = true);
/**
* @brief gets data required to create a block template and start mining on it
@@ -1303,6 +1303,7 @@ namespace cryptonote
uint64_t m_btc_cumulative_weight;
crypto::hash m_btc_seed_hash;
uint64_t m_btc_seed_height;
bool m_btc_include_sensitive;
bool m_btc_valid;
@@ -1347,11 +1348,12 @@ namespace cryptonote
* @param tx_prefix_hash the hash of the associated transaction_prefix
* @param pmax_related_block_height return-by-pointer the height of the most recent block in the input set
* @param tx_version version of the tx, if > 1 we also get commitments
* @param hf_version hard-fork version rules to use
*
* @return false if any keys are not found or any inputs are not unlocked, otherwise true
*/
template<class visitor_t>
inline bool scan_outputkeys_for_indexes(size_t tx_version, const txin_to_key& tx_in_to_key, visitor_t &vis, const crypto::hash &tx_prefix_hash, uint64_t* pmax_related_block_height = NULL) const;
inline bool scan_outputkeys_for_indexes(const uint8_t hf_version, size_t tx_version, const txin_to_key& tx_in_to_key, visitor_t &vis, const crypto::hash &tx_prefix_hash, uint64_t* pmax_related_block_height = NULL) const;
/**
* @brief collect output public keys of a transaction input set
@@ -1702,7 +1704,7 @@ namespace cryptonote
*
* At some point, may be used to push an update to miners
*/
void cache_block_template(const block &b, const cryptonote::account_public_address &address, const blobdata &nonce, const difficulty_type &diff, uint64_t height, uint64_t expected_reward, uint64_t cumulative_weight, uint64_t seed_height, const crypto::hash &seed_hash, uint64_t pool_cookie);
void cache_block_template(const block &b, const cryptonote::account_public_address &address, const blobdata &nonce, const difficulty_type &diff, uint64_t height, uint64_t expected_reward, uint64_t cumulative_weight, uint64_t seed_height, const crypto::hash &seed_hash, uint64_t pool_cookie, bool include_sensitive);
/**
* @brief sends new block notifications to ZMQ `miner_data` subscribers
+36 -2
View File
@@ -889,6 +889,14 @@ namespace cryptonote
return false;
}
if (!check_tx_inputs_ring_members_overflow(tx, hf_version))
{
MERROR_VER("tx ring members overflowed");
tvc.m_verifivation_failed = true;
tvc.m_invalid_input = true;
return false;
}
if (!check_tx_inputs_keyimages_domain(tx))
{
MERROR_VER("tx uses key image not in the valid domain");
@@ -1044,6 +1052,26 @@ namespace cryptonote
return true;
}
//-----------------------------------------------------------------------------------------------
bool core::check_tx_inputs_ring_members_overflow(const transaction& tx, const uint8_t hf_version)
{
if (hf_version >= HF_VERSION_FCMP_PLUS_PLUS)
{
for(const auto& in: tx.vin)
{
CHECKED_GET_SPECIFIC_VARIANT(in, const txin_to_key, tokey_in, false);
const std::vector<uint64_t> absolute_offsets = relative_output_offsets_to_absolute(tokey_in.key_offsets);
for (size_t i = 1; i < absolute_offsets.size(); ++i)
{
const uint64_t prev_offset = absolute_offsets[i-1];
const uint64_t next_offset = absolute_offsets[i];
if (prev_offset > next_offset)
return false;
}
}
}
return true;
}
//-----------------------------------------------------------------------------------------------
bool core::check_tx_inputs_keyimages_domain(const transaction& tx)
{
std::unordered_set<crypto::key_image> ki;
@@ -1218,9 +1246,9 @@ namespace cryptonote
return m_blockchain_storage.create_block_template(b, adr, diffic, height, expected_reward, cumulative_weight, ex_nonce, seed_height, seed_hash);
}
//-----------------------------------------------------------------------------------------------
bool core::get_block_template(block& b, const crypto::hash *prev_block, const account_public_address& adr, difficulty_type& diffic, uint64_t& height, uint64_t& expected_reward, uint64_t& cumulative_weight, const blobdata& ex_nonce, uint64_t &seed_height, crypto::hash &seed_hash)
bool core::get_block_template(block& b, const crypto::hash *prev_block, const account_public_address& adr, difficulty_type& diffic, uint64_t& height, uint64_t& expected_reward, uint64_t& cumulative_weight, const blobdata& ex_nonce, uint64_t &seed_height, crypto::hash &seed_hash, bool include_sensitive)
{
return m_blockchain_storage.create_block_template(b, prev_block, adr, diffic, height, expected_reward, cumulative_weight, ex_nonce, seed_height, seed_hash);
return m_blockchain_storage.create_block_template(b, prev_block, adr, diffic, height, expected_reward, cumulative_weight, ex_nonce, seed_height, seed_hash, include_sensitive);
}
//-----------------------------------------------------------------------------------------------
bool core::get_miner_data(uint8_t& major_version, uint64_t& height, crypto::hash& prev_id, uint8_t& fcmp_pp_n_tree_layers, crypto::ec_point& fcmp_pp_tree_root, crypto::hash& seed_hash, difficulty_type& difficulty, uint64_t& median_weight, uint64_t& already_generated_coins, std::vector<tx_block_template_backlog_entry>& tx_backlog)
@@ -1270,6 +1298,9 @@ namespace cryptonote
//-----------------------------------------------------------------------------------------------
block_complete_entry get_block_complete_entry(block& b, tx_memory_pool &pool)
{
const std::unordered_set<crypto::hash> tx_hashes(b.tx_hashes.cbegin(), b.tx_hashes.cend());
CHECK_AND_ASSERT_THROW_MES(tx_hashes.size() == b.tx_hashes.size(), "Duplicate transaction hashes in block");
block_complete_entry bce;
bce.block = cryptonote::block_to_blob(b);
bce.block_weight = 0; // we can leave it to 0, those txes aren't pruned
@@ -1623,6 +1654,9 @@ namespace cryptonote
//-----------------------------------------------------------------------------------------------
bool core::check_updates()
{
#ifdef WOWNERO_FCMP_TESTNET
return true;
#endif
static const char software[] = "monero";
#ifdef BUILD_TAG
static const char buildtag[] = BOOST_PP_STRINGIZE(BUILD_TAG);
+11 -1
View File
@@ -238,7 +238,7 @@ namespace cryptonote
* @note see Blockchain::create_block_template
*/
bool get_block_template(block& b, const account_public_address& adr, difficulty_type& diffic, uint64_t& height, uint64_t& expected_reward, uint64_t &cumulative_weight, const blobdata& ex_nonce, uint64_t &seed_height, crypto::hash &seed_hash) final;
bool get_block_template(block& b, const crypto::hash *prev_block, const account_public_address& adr, difficulty_type& diffic, uint64_t& height, uint64_t& expected_reward, uint64_t &cumulative_weight, const blobdata& ex_nonce, uint64_t &seed_height, crypto::hash &seed_hash);
bool get_block_template(block& b, const crypto::hash *prev_block, const account_public_address& adr, difficulty_type& diffic, uint64_t& height, uint64_t& expected_reward, uint64_t &cumulative_weight, const blobdata& ex_nonce, uint64_t &seed_height, crypto::hash &seed_hash, bool include_sensitive = true);
/**
* @copydoc Blockchain::get_miner_data
@@ -908,6 +908,16 @@ namespace cryptonote
*/
static bool check_tx_inputs_ring_members_diff(const transaction& tx, const uint8_t hf_version);
/**
* @brief verify that ring member absolute offsets don't overflow
*
* @param tx the transaction to check
* @param hf_version the hard fork version rules to use
*
* @return false if any absolute offsets overflowed, true otherwise
*/
static bool check_tx_inputs_ring_members_overflow(const transaction& tx, const uint8_t hf_version);
/**
* @brief verify that each input key image in a transaction is in
* the valid domain
+3 -2
View File
@@ -1673,7 +1673,7 @@ namespace cryptonote
}
//---------------------------------------------------------------------------------
//TODO: investigate whether boolean return is appropriate
bool tx_memory_pool::fill_block_template(block &bl, size_t median_weight, uint64_t already_generated_coins, size_t &total_weight, uint64_t &fee, uint64_t &expected_reward, uint8_t version)
bool tx_memory_pool::fill_block_template(block &bl, size_t median_weight, uint64_t already_generated_coins, size_t &total_weight, uint64_t &fee, uint64_t &expected_reward, uint8_t version, bool include_sensitive)
{
CRITICAL_REGION_LOCAL(m_transactions_lock);
CRITICAL_REGION_LOCAL1(m_blockchain);
@@ -1713,7 +1713,8 @@ namespace cryptonote
}
LOG_PRINT_L2("Considering " << sorted_it->get_right() << ", weight " << meta.weight << ", current block weight " << total_weight << "/" << max_total_weight << ", current coinbase " << print_money(best_coinbase) << ", relay method " << (unsigned)meta.get_relay_method());
if (!meta.matches(relay_category::legacy) && !(m_mine_stem_txes && meta.get_relay_method() == relay_method::stem))
const relay_category category = include_sensitive ? relay_category::legacy : relay_category::broadcasted;
if (!meta.matches(category) && !(include_sensitive && m_mine_stem_txes && meta.get_relay_method() == relay_method::stem))
{
LOG_PRINT_L2(" tx relay method is " << (unsigned)meta.get_relay_method());
continue;
+1 -1
View File
@@ -257,7 +257,7 @@ namespace cryptonote
*
* @return true
*/
bool fill_block_template(block &bl, size_t median_weight, uint64_t already_generated_coins, size_t &total_weight, uint64_t &fee, uint64_t &expected_reward, uint8_t version);
bool fill_block_template(block &bl, size_t median_weight, uint64_t already_generated_coins, size_t &total_weight, uint64_t &fee, uint64_t &expected_reward, uint8_t version, bool include_sensitive = true);
/**
* @brief get a list of all transactions in the pool
@@ -114,7 +114,7 @@ namespace cryptonote
const bool max_size_check = true;
if (is_pruned)
{
if ((parse_success = cryptonote::parse_and_validate_tx_base_from_blob(tx_entry.blob, tx, max_size_check)))
if ((parse_success = cryptonote::parse_and_validate_tx_base_from_blob(tx_entry.blob, tx, max_size_check, /*require_eof=*/true)))
parse_success = cryptonote::get_pruned_transaction_hash(tx, tx_entry.prunable_hash, tx_hash);
}
else
@@ -1650,6 +1650,7 @@ namespace cryptonote
if (!m_core.prepare_handle_incoming_blocks(blocks, pblocks))
{
LOG_ERROR_CCONTEXT("Failure in prepare_handle_incoming_blocks");
m_block_queue.flush_spans(span_connection_id, true);
drop_connections(span_origin);
return 1;
}
@@ -3103,6 +3104,7 @@ skip:
template<class t_core>
void t_cryptonote_protocol_handler<t_core>::drop_connection(const boost::uuids::uuid& id, bool add_fail)
{
m_block_queue.flush_spans(id, true);
m_p2p->for_connection(id, [this, add_fail](cryptonote_connection_context& context, nodetool::peerid_type peer_id, uint32_t f)->bool{
// This _could be_ outside of strand, so careful on actions
drop_connection(context, add_fail, false);
+4
View File
@@ -72,3 +72,7 @@ set_property(TARGET daemon
PROPERTY
OUTPUT_NAME "monerod")
install(TARGETS daemon DESTINATION bin)
if(WOWNERO_FCMP_TESTNET AND TARGET daemon)
set_property(TARGET daemon PROPERTY OUTPUT_NAME "wownero-fcmp-testnetd")
endif()
+21 -3
View File
@@ -46,6 +46,16 @@
#include "daemon/command_line_args.h"
#include "version.h"
namespace
{
#ifdef WOWNERO_FCMP_TESTNET
constexpr const char *project_name = "Wownero FCMP testnet";
#else
constexpr const char *project_name = "Monero";
#endif
}
#ifdef STACK_TRACE
#include "common/stack_trace.h"
#endif // STACK_TRACE
@@ -192,7 +202,7 @@ int main(int argc, char const * argv[])
if (command_line::get_arg(vm, command_line::arg_help))
{
std::cout << "Monero '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")" << ENDL << ENDL;
std::cout << project_name << " '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")" << ENDL << ENDL;
std::cout << "Usage: " + std::string{argv[0]} + " [options|settings] [daemon_command...]" << std::endl << std::endl;
std::cout << visible_options << std::endl;
return 0;
@@ -201,7 +211,7 @@ int main(int argc, char const * argv[])
// Monero Version
if (command_line::get_arg(vm, command_line::arg_version))
{
std::cout << "Monero '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")" << ENDL;
std::cout << project_name << " '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")" << ENDL;
return 0;
}
@@ -247,6 +257,14 @@ int main(int argc, char const * argv[])
return 1;
}
#ifdef WOWNERO_FCMP_TESTNET
if (cryptonote::core::get_network_type_from_args(vm) != cryptonote::TESTNET)
{
std::cerr << "Wownero FCMP research daemon requires --testnet; mainnet, stagenet and regtest are disabled." << std::endl;
return 1;
}
#endif
// data_dir
// default: e.g. ~/.bitmonero/ or ~/.bitmonero/testnet
// if data-dir argument given:
@@ -295,7 +313,7 @@ int main(int argc, char const * argv[])
tools::set_max_concurrency(command_line::get_arg(vm, daemon_args::arg_max_concurrency));
// logging is now set up
MGINFO("Monero '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")");
MGINFO(project_name << " '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")");
// If there are positional options, we're running a daemon command
{
+11
View File
@@ -81,6 +81,15 @@ const hardfork_t mainnet_hard_forks[] = {
const size_t num_mainnet_hard_forks = sizeof(mainnet_hard_forks) / sizeof(mainnet_hard_forks[0]);
const uint64_t mainnet_hard_fork_version_1_till = 1009826;
#ifdef WOWNERO_FCMP_TESTNET
const hardfork_t testnet_hard_forks[] = {
{ 1, 0, 0, 1 },
{ 17, 1, 0, 2 },
{ 18, 2, 0, 3 },
};
const size_t num_testnet_hard_forks = sizeof(testnet_hard_forks) / sizeof(testnet_hard_forks[0]);
const uint64_t testnet_hard_fork_version_1_till = 0;
#else
const hardfork_t testnet_hard_forks[] = {
// version 1 from the start of the blockchain
{ 1, 1, 0, 1341378000 },
@@ -112,6 +121,8 @@ const hardfork_t testnet_hard_forks[] = {
const size_t num_testnet_hard_forks = sizeof(testnet_hard_forks) / sizeof(testnet_hard_forks[0]);
const uint64_t testnet_hard_fork_version_1_till = 624633;
#endif
const hardfork_t stagenet_hard_forks[] = {
// version 1 from the start of the blockchain
{ 1, 1, 0, 1341378000 },
+12
View File
@@ -832,6 +832,9 @@ namespace nodetool
template<class t_payload_net_handler>
std::set<std::string> node_server<t_payload_net_handler>::get_ip_seed_nodes() const
{
#ifdef WOWNERO_FCMP_TESTNET
return {};
#endif
std::set<std::string> full_addrs;
if (m_nettype == cryptonote::TESTNET)
{
@@ -867,6 +870,9 @@ namespace nodetool
template<class t_payload_net_handler>
std::set<std::string> node_server<t_payload_net_handler>::get_dns_seed_nodes()
{
#ifdef WOWNERO_FCMP_TESTNET
return {};
#endif
if (!m_exclusive_peers.empty() || m_offline)
{
return {};
@@ -983,6 +989,9 @@ namespace nodetool
template<class t_payload_net_handler>
std::set<std::string> node_server<t_payload_net_handler>::get_seed_nodes(epee::net_utils::zone zone)
{
#ifdef WOWNERO_FCMP_TESTNET
return {};
#endif
switch (zone)
{
case epee::net_utils::zone::public_:
@@ -2202,6 +2211,9 @@ namespace nodetool
template<class t_payload_net_handler>
bool node_server<t_payload_net_handler>::update_dns_blocklist()
{
#ifdef WOWNERO_FCMP_TESTNET
return true;
#endif
if (!m_enable_dns_blocklist)
return true;
if (m_nettype != cryptonote::MAINNET)
+24 -3
View File
@@ -1467,6 +1467,7 @@ namespace cryptonote
COMMAND_RPC_GET_PEER_LIST::request peer_list_req;
COMMAND_RPC_GET_PEER_LIST::response peer_list_res;
peer_list_req.public_only = true;
peer_list_req.include_blocked = req.include_blocked;
const bool success = on_get_peer_list(peer_list_req, peer_list_res, ctx);
res.status = peer_list_res.status;
@@ -1665,10 +1666,10 @@ namespace cryptonote
return 0;
}
//------------------------------------------------------------------------------------------------------------------------------
bool core_rpc_server::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 core_rpc_server::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 include_sensitive)
{
b = boost::value_initialized<cryptonote::block>();
if(!m_core.get_block_template(b, prev_block, address, difficulty, height, expected_reward, cumulative_weight, extra_nonce, seed_height, seed_hash))
if(!m_core.get_block_template(b, prev_block, address, difficulty, height, expected_reward, cumulative_weight, extra_nonce, seed_height, seed_hash, include_sensitive))
{
error_resp.code = CORE_RPC_ERROR_CODE_INTERNAL_ERROR;
error_resp.message = "Internal error: failed to create block template";
@@ -1790,7 +1791,8 @@ namespace cryptonote
}
}
crypto::hash seed_hash, next_seed_hash;
if (!get_block_template(info.address, req.prev_block.empty() ? NULL : &prev_block, blob_reserve, reserved_offset, wdiff, res.height, res.expected_reward, res.cumulative_weight, b, res.seed_height, seed_hash, next_seed_hash, error_resp))
const bool include_sensitive = !m_restricted || !ctx;
if (!get_block_template(info.address, req.prev_block.empty() ? NULL : &prev_block, blob_reserve, reserved_offset, wdiff, res.height, res.expected_reward, res.cumulative_weight, b, res.seed_height, seed_hash, next_seed_hash, error_resp, include_sensitive))
return false;
if (b.major_version >= RX_BLOCK_VERSION)
{
@@ -2095,6 +2097,25 @@ namespace cryptonote
return false;
}
if (m_restricted && ctx)
{
block parent;
if (!m_core.get_block_by_hash(b.prev_id, parent))
{
error_resp.code = CORE_RPC_ERROR_CODE_BLOCK_NOT_ACCEPTED;
error_resp.message = "Unknown parent block";
return false;
}
const uint64_t block_height = get_block_height(parent) + 1;
const uint64_t chain_height = m_core.get_current_blockchain_height();
if (crypto::rx_seedheight(block_height) != crypto::rx_seedheight(chain_height))
{
error_resp.code = CORE_RPC_ERROR_CODE_BLOCK_NOT_ACCEPTED;
error_resp.message = "Block is too old for restricted RPC";
return false;
}
}
block_verification_context bvc;
if(!m_core.handle_block_found(b, bvc))
{
+1 -1
View File
@@ -254,7 +254,7 @@ private:
//utils
uint64_t get_block_reward(const block& blk);
bool fill_block_header_response(const block& blk, bool orphan_status, uint64_t height, const crypto::hash& hash, block_header_response& response, bool fill_pow_hash);
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 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 include_sensitive = true);
bool handle_get_blocks(const COMMAND_RPC_GET_BLOCKS_FAST::request& req, COMMAND_RPC_GET_BLOCKS_FAST::response& res, const connection_context *ctx);
core& m_core;
+1 -1
View File
@@ -59,10 +59,10 @@ namespace rpc
crypto::hash hash10 = get_hash(d.cached_to - 10);
if (hash10 == d.cached_m10_hash)
{
CHECK_AND_ASSERT_MES(d.cached_distribution.size() >= 10, boost::none, "Cached distribution size does not match cached bounds");
d.cached_to -= 10;
d.cached_top_hash = hash10;
d.cached_m10_hash = crypto::null_hash;
CHECK_AND_ASSERT_MES(d.cached_distribution.size() >= 10, boost::none, "Cached distribution size does not match cached bounds");
for (int p = 0; p < 10; ++p)
d.cached_distribution.pop_back();
can_extend = true;
+4
View File
@@ -63,3 +63,7 @@ set_property(TARGET simplewallet
PROPERTY
OUTPUT_NAME "monero-wallet-cli")
install(TARGETS simplewallet DESTINATION bin)
if(WOWNERO_FCMP_TESTNET AND TARGET simplewallet)
set_property(TARGET simplewallet PROPERTY OUTPUT_NAME "wownero-fcmp-testnet-wallet-cli")
endif()
+5
View File
@@ -1,6 +1,11 @@
#define DEF_MONERO_VERSION_TAG "@VERSIONTAG@"
#ifdef WOWNERO_FCMP_TESTNET
#define DEF_MONERO_VERSION "0.1.0-fcmp-testnet"
#define DEF_MONERO_RELEASE_NAME "Wownero FCMP Research"
#else
#define DEF_MONERO_VERSION "0.19.0.0-beta.3.0"
#define DEF_MONERO_RELEASE_NAME "Fluorine Fermi"
#endif
#define DEF_MONERO_VERSION_FULL DEF_MONERO_VERSION "-" DEF_MONERO_VERSION_TAG
#define DEF_MONERO_VERSION_IS_RELEASE @VERSION_IS_RELEASE@
+4
View File
@@ -111,3 +111,7 @@ endif()
add_subdirectory(api)
add_subdirectory(wallet2_basic)
if(WOWNERO_FCMP_TESTNET AND TARGET wallet_rpc_server)
set_property(TARGET wallet_rpc_server PROPERTY OUTPUT_NAME "wownero-fcmp-testnet-wallet-rpc")
endif()
+4
View File
@@ -181,7 +181,11 @@ namespace
boost::filesystem::path dir = tools::get_default_data_dir();
// remove .bitmonero, replace with .shared-ringdb
dir = dir.remove_filename();
#ifdef WOWNERO_FCMP_TESTNET
dir /= ".wownero-fcmp-testnet-ringdb";
#else
dir /= ".shared-ringdb";
#endif
return dir.string();
}
+22 -3
View File
@@ -36,6 +36,16 @@
#include "string_tools.h"
#include "version.h"
namespace
{
#ifdef WOWNERO_FCMP_TESTNET
constexpr const char *project_name = "Wownero FCMP testnet";
#else
constexpr const char *project_name = "Monero";
#endif
}
#if defined(WIN32)
#include <crtdbg.h>
#endif
@@ -142,7 +152,7 @@ namespace wallet_args
if (command_line::get_arg(vm, command_line::arg_help))
{
Print(print) << "Monero '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")" << ENDL;
Print(print) << project_name << " '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")" << ENDL;
Print(print) << wallet_args::tr("This is the command line monero wallet. It needs to connect to a monero\n"
"daemon to work correctly.") << ENDL;
Print(print) << wallet_args::tr("Usage:") << ENDL << " " << usage;
@@ -152,7 +162,7 @@ namespace wallet_args
}
else if (command_line::get_arg(vm, command_line::arg_version))
{
Print(print) << "Monero '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")";
Print(print) << project_name << " '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")";
should_terminate = true;
return true;
}
@@ -182,6 +192,15 @@ namespace wallet_args
if (should_terminate)
return {std::move(vm), should_terminate};
#ifdef WOWNERO_FCMP_TESTNET
if (!vm.count("testnet") || !vm["testnet"].as<bool>() ||
(vm.count("stagenet") && vm["stagenet"].as<bool>()))
{
Print(print) << "Wownero FCMP research wallet requires --testnet; other networks are disabled.";
return {boost::none, true};
}
#endif
std::string log_path;
if (!command_line::is_arg_defaulted(vm, arg_log_file))
log_path = command_line::get_arg(vm, arg_log_file);
@@ -203,7 +222,7 @@ namespace wallet_args
if (!command_line::is_arg_defaulted(vm, arg_max_concurrency))
tools::set_max_concurrency(command_line::get_arg(vm, arg_max_concurrency));
Print(print) << "Monero '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")";
Print(print) << project_name << " '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")";
if (!command_line::is_arg_defaulted(vm, arg_log_level))
MINFO("Setting log level = " << command_line::get_arg(vm, arg_log_level));
+12
View File
@@ -0,0 +1,12 @@
// Copyright (c) 2026, The Wownero Project
// SPDX-License-Identifier: BSD-3-Clause
// Do not edit. Regenerate with the wow_testnet_genesis executable.
#pragma once
namespace wownero::testnet
{
inline constexpr char genesis_tx[] = "01a00201ff0001ffffffffffff0302d85a51f64dfbe2d5343e2f5a398582b26c882d60a91fc5fb1040540082c184d6400162182889579a8d2f59afdd01d79e92c0b2270d9f58947c6a4f742832afbe949f021d574f572046434d502b2b20746573746e657420323032362d31302d3034";
inline constexpr char genesis_hash[] = "ec4899fa454479072232abe5924c1c5a2400c5a3f307f0ef6bd6f66bcbb9b685";
inline constexpr unsigned genesis_nonce = 20261004;
}
+5
View File
@@ -137,3 +137,8 @@ set(enabled_tests
add_custom_target(tests DEPENDS enabled_tests)
set_property(TARGET tests PROPERTY FOLDER "${folder}")
option(BUILD_WOW_MINER_PROOF_EXPERIMENT "Build the unaudited Carrot miner proof experiment." OFF)
if(BUILD_WOW_MINER_PROOF_EXPERIMENT)
add_subdirectory(wow_miner_proof)
endif()
+38
View File
@@ -0,0 +1,38 @@
# Copyright (c) 2026, The Wownero Project
# SPDX-License-Identifier: BSD-3-Clause
if(WOWNERO_FCMP_TESTNET)
add_library(wow_miner_proof_experiment INTERFACE)
target_link_libraries(wow_miner_proof_experiment INTERFACE cncrypto)
else()
monero_add_library(wow_miner_proof_experiment
../../src/crypto/wow_miner_proof.cpp)
target_link_libraries(wow_miner_proof_experiment PUBLIC cncrypto)
endif()
monero_add_minimal_executable(wow_miner_proof_tests tests.cpp)
target_link_libraries(wow_miner_proof_tests PRIVATE
wow_miner_proof_experiment carrot_core ${GTEST_LIBRARIES})
add_test(NAME wow_miner_proof COMMAND wow_miner_proof_tests)
monero_add_minimal_executable(wow_miner_proof_benchmark benchmark.cpp)
target_link_libraries(wow_miner_proof_benchmark PRIVATE
wow_miner_proof_experiment carrot_core)
monero_add_minimal_executable(wow_testnet_genesis genesis.cpp)
target_link_libraries(wow_testnet_genesis PRIVATE cryptonote_basic)
if(WOWNERO_FCMP_TESTNET)
monero_add_minimal_executable(wow_testnet_tests testnet.cpp)
target_link_libraries(wow_testnet_tests PRIVATE cryptonote_basic ${GTEST_LIBRARIES})
add_test(NAME wow_testnet COMMAND wow_testnet_tests)
monero_add_minimal_executable(wow_release_regressions release_regressions.cpp)
target_link_libraries(wow_release_regressions PRIVATE
cryptonote_core rpc_base ${GTEST_LIBRARIES})
add_test(NAME wow_release_regressions COMMAND wow_release_regressions)
monero_add_minimal_executable(wow_pow_vectors pow_vectors.cpp)
target_include_directories(wow_pow_vectors PRIVATE ../../external/randomwow/src)
target_link_libraries(wow_pow_vectors PRIVATE randomx epee)
monero_add_minimal_executable(wow_local_network local_network.cpp)
target_link_libraries(wow_local_network PRIVATE wallet cryptonote_basic)
endif()
+102
View File
@@ -0,0 +1,102 @@
# Carrot miner proof experiment
> Status: draft · Updated 2026-10-05 · Applies to: Wownero FCMP++ testnet research
This directory contains a standalone Carrot miner proof experiment and an optional, isolated Wownero FCMP++ network profile. Both build selectors default to `OFF`. The network profile is a research implementation, not a Wownero mainnet migration or a deployable release.
## Statement and encoding
Let `K = xG + yT` be the coinbase output public key. `G` and `T` are the exact Ed25519 subgroup generators supplied by the pinned FCMP++ implementation. They are not Ristretto encodings. Knowledge of a representation authorizes this experiment; the proof does not establish who performed the proof of work or prevent remote signing services.
The prover samples independent nonzero scalars `a, b`, computes `R = aG + bT`, then:
```text
c = Hs(domain || network_genesis || block_digest || encode(K) || encode(R))
s_g = a + c*x mod l
s_t = b + c*y mod l
proof = encode(c) || encode(s_g) || encode(s_t)
```
`domain` is the 25 ASCII bytes `WOW-CARROT-MINER-PROOF-v1`, without a terminating NUL. The four remaining transcript fields are 32 bytes each. `Hs` is upstream `crypto::hash_to_scalar`, using Keccak-256 followed by reduction modulo the Ed25519 subgroup order `l`. The proof contains three canonical little-endian scalars, totaling 96 bytes. Sampling repeats for identity `R` or zero `c`.
The verifier reconstructs `R = s_g G + s_t T - cK` and recomputes `c`. It rejects incorrect lengths, noncanonical scalars, zero challenges, identity reconstructed nonces, and invalid public keys. Public keys must use canonical Edwards encodings, be nonidentity, and satisfy `[l]K = identity`. This excludes small-order and mixed-order points without depending on a system libsodium point-validation version.
The signer rejects a witness that does not open the supplied output key. Nonces and witness scalars use upstream locked and scrubbed containers. This is not a completed side-channel audit of the scalar multiplication implementation or compiler output.
## Coinbase compatibility
Tests construct a Carrot coinbase with upstream `get_coinbase_enote_v1`, scan it through the receiver API, and add the recovered sender extensions to the account's two spending scalars. Both new Carrot addresses and legacy single-generator addresses are covered. A Carrot payment to a legacy address still acquires a `T` component through its sender extension.
## Isolated network profile
`WOWNERO_FCMP_TESTNET=ON` enables a separate testnet identity, genesis, address prefixes, data directory and binary names. Its daemon and wallets require `--testnet`; mainnet, stagenet and regtest are refused. Built-in upstream seeds, DNS blocklists, checkpoints and binary update queries are disabled. Explicit peers remain available.
The fresh chain uses feature version 1 at genesis, FCMP++/Carrot version 17 at height 1, and version 18 at height 2. These numbers follow the reference implementation and do not specify a migration from Wownero mainnet. The profile uses 11 decimal places, a five-minute target, Wownero emission parameters without tail subsidy, 288-block coinbase maturity, four-block ordinary output age and one coinbase output. RandomWOW is pinned as a separate submodule. Its difficulty window, lag and cut are 144, 3 and 12; bootstrap difficulty is 100 through height 720.
The genesis generator hashes independent public domains to curve points without a known spending witness. Its single reward is not assigned to an operator. Regenerate `src/wownero/testnet_genesis.h` with `wow_testnet_genesis`; do not edit the generated bytes.
## Block authorization
From feature version 17, the header contains a 96-byte miner proof followed by a two-byte vote. Votes are restricted to 0, 1 or 2. The statement hashes the canonical block hashing blob with the proof zeroed, including the nonce, timestamp, previous block, complete coinbase, transaction set, FCMP root and layer count. Height is bound through the coinbase input. Cached transaction and block hashes are invalidated when constructing the statement. The actual proof enters the normal proof-of-work input.
The built-in miner scans each template's Carrot coinbase and signs every nonce candidate. Consensus checks the proof during coinbase prevalidation. The synchronous mining helper refuses Carrot blocks because it lacks a signing account. The proof adds 32 bytes to Wownero's existing 64-byte signature and requires no trusted setup beyond the inherited curve generators. It remains unaudited.
Mining credentials enter through `--wow-miner-key-fd`, which accepts an inherited pipe descriptor of at least 3. The bounded input frame is the eight ASCII bytes `WOWMIN01`, followed by the canonical generate-image, prove-spend and incoming-view scalars, 32 bytes each. The reader closes the descriptor and rejects incomplete frames, invalid scalars and invalid account keys. A legacy account uses a zero prove-spend scalar. The payout address must match the account. Credential values must never enter arguments, logs or source files.
## Remaining qualification
Native tests cover block binding, serialization, proof-of-work binding, network isolation and credential input. Daemon regressions cover fork-gated legacy ring offset bounds, exact pruned-transaction boundaries, nested database admission, distribution-cache refusal and console sanitization. The local integration target exercises mining, synchronization, wallet scanning, FCMP payments and restricted-template privacy. It also rejects an unauthorized alternate block, replaces a two-block fork with a greater-work chain, restarts both nodes and confirms an FCMP spend from a wallet that scanned the losing fork.
Remaining qualification includes independent cryptographic review, complete release-fix reconciliation, deeper reorganizations and proof-of-work key transitions, sustained mining measurements and a reviewed deployment plan. The explicit PoW target compares four public inputs under the RandomWOW interpreter and secure JIT. Matching outputs across platforms establish agreement for those vectors; they do not exhaustively validate every generated program.
## Reproduction
Configure the pinned source and initialized submodules with the repository's normal platform dependencies:
```sh
cmake -S . -B "$WOW_BUILD_DIR" -DCMAKE_BUILD_TYPE=Release \
-DBUILD_TESTS=ON -DBUILD_WOW_MINER_PROOF_EXPERIMENT=ON
cmake --build "$WOW_BUILD_DIR" \
--target wow_miner_proof_tests wow_miner_proof_benchmark
ctest --test-dir "$WOW_BUILD_DIR" -R '^wow_miner_proof$' --output-on-failure
"$WOW_BUILD_DIR/tests/wow_miner_proof/wow_miner_proof_benchmark"
```
Set `WOW_BUILD_DIR` to a disposable build directory outside the source checkout. `BUILD_WOW_MINER_PROOF_EXPERIMENT` defaults to `OFF` and requires `BUILD_TESTS=ON`.
The benchmark reports 1,000 wall-clock samples after ten warmups, with a fixed synthetic statement and fresh nonces. Every resulting proof and legacy signature is checked. It includes public-key validation and witness matching in the experimental signing cost. Coinbase scanning, statement serialization, proof of work, network propagation and complete block validation are excluded. Recorded measurements belong with the exact source commit and host/compiler evidence.
Reference: [Carrot specification](https://github.com/jeffro256/carrot/blob/master/carrot.md).
## Local network reproduction
Use a separate build directory for the network profile:
```sh
cmake -S . -B "$WOW_BUILD_DIR" -DCMAKE_BUILD_TYPE=Release \
-DBUILD_TESTS=ON -DBUILD_WOW_MINER_PROOF_EXPERIMENT=ON \
-DWOWNERO_FCMP_TESTNET=ON
cmake --build "$WOW_BUILD_DIR" \
--target daemon simplewallet wallet_rpc_server wow_testnet_tests \
wow_miner_proof_tests wow_release_regressions wow_pow_vectors wow_local_network
ctest --test-dir "$WOW_BUILD_DIR" -R '^wow_(miner_proof|testnet|release_regressions)$' --output-on-failure
"$WOW_BUILD_DIR/tests/wow_miner_proof/wow_pow_vectors"
"$WOW_BUILD_DIR/tests/wow_miner_proof/wow_local_network" \
"$WOW_BUILD_DIR/bin/wownero-fcmp-testnetd" "$WOW_NEW_TEST_DATA_DIR"
```
The integration target first runs a loopback fixture that drops the initial `get_info` response, delays the next response for six seconds and requires recovery on a fresh connection. Run this cheap check independently with `wow_local_network --rpc-read-check`. The mining test then requires a new disposable data directory. It binds only loopback ports 49880–49882 and 49890–49892 for P2P, unrestricted RPC and restricted RPC. It disables TLS on its loopback RPC endpoints and rejects an unexpected RPC private-key file. Mining startup retries only an explicit busy response, with a bounded deadline and a specific refusal cause. The read-only `get_info` query uses a 30-second per-attempt timeout, reconnects once after a transport or decoding failure and requires a valid response; state-changing calls are not retried after transport failure. It generates in-memory wallets, passes mining credentials through a pipe and stops its child daemons on exit. It retains public chain data and logs, but no recoverable wallet. The target is explicit rather than part of ordinary CTest because real proof-of-work mining is resource-intensive.
## Verification checklist
- [x] Carrot coinbase creation, receiver scanning and representation proof round trip.
- [x] Legacy-address compatibility and rejection of missing witness components.
- [x] Network, digest, key and proof mutation rejection; proof length checks.
- [x] Noncanonical scalar and point rejection; identity, torsion and mixed-order rejection.
- [ ] Independent cryptographic review and cross-implementation vectors.
- [x] Block serialization, statement mutation rejection and miner integration.
- [x] Valid-PoW unauthorized block rejection and authorized control acceptance.
- [x] Local FCMP payment, observed maturity and observer restart.
- [x] Local alternate-chain rejection, two-block wallet recovery and subsequent FCMP spend.
- [ ] Broader fork recovery and proof-of-work key-transition qualification.
- [ ] Sustained mining measurements and deployment-platform qualification.
+81
View File
@@ -0,0 +1,81 @@
// Copyright (c) 2026, The Wownero Project
// SPDX-License-Identifier: BSD-3-Clause
#include "fixture.h"
#include <algorithm>
#include <chrono>
#include <iomanip>
#include <iostream>
#include <vector>
namespace wr = wownero::research;
template<class F> double measure(F &&operation)
{
const auto start = std::chrono::steady_clock::now();
if (!operation())
throw std::runtime_error("Benchmark operation failed validation");
const auto end = std::chrono::steady_clock::now();
return std::chrono::duration<double, std::micro>(end - start).count();
}
void report(const char *name, std::vector<double> &samples)
{
std::sort(samples.begin(), samples.end());
std::cout << " \"" << name << "\": {\"median_us\": " << samples[samples.size() / 2]
<< ", \"p95_us\": " << samples[(samples.size() * 95 + 99) / 100 - 1] << "}";
}
int main()
{
try
{
constexpr std::size_t count = 1000;
const wr::test::coinbase_fixture fixture;
crypto::secret_key legacy_secret;
crypto::public_key legacy_public;
crypto::generate_keys(legacy_public, legacy_secret);
std::vector<double> prove, verify, legacy_prove, legacy_verify;
for (std::size_t i = 0; i < count + 10; ++i)
{
wr::miner_proof proof;
const auto p = measure([&] { return wr::generate_miner_proof(fixture.network,
fixture.message, fixture.enote.onetime_address, fixture.x, fixture.y, proof); });
const auto v = measure([&] { return fixture.verify(proof); });
crypto::signature signature;
const auto lp = measure([&] {
crypto::generate_signature(fixture.message, legacy_public, legacy_secret, signature);
return true;
});
const auto lv = measure([&] {
return crypto::check_signature(fixture.message, legacy_public, signature);
});
if (i >= 10)
{
prove.push_back(p);
verify.push_back(v);
legacy_prove.push_back(lp);
legacy_verify.push_back(lv);
}
}
std::cout << std::fixed << std::setprecision(3)
<< "{\n \"samples\": " << count << ",\n \"warmups\": 10,\n"
<< " \"proof_bytes\": " << wr::miner_proof{}.size() << ",\n"
<< " \"scope\": \"native primitive; fixed statement; fresh nonces; no block validation or PoW\",\n";
report("prove", prove);
std::cout << ",\n";
report("verify", verify);
std::cout << ",\n";
report("legacy_prove", legacy_prove);
std::cout << ",\n";
report("legacy_verify", legacy_verify);
std::cout << "\n}\n";
return 0;
}
catch (const std::exception &error)
{
std::cerr << error.what() << '\n';
return 1;
}
}
+77
View File
@@ -0,0 +1,77 @@
// Copyright (c) 2026, The Wownero Project
// SPDX-License-Identifier: BSD-3-Clause
#pragma once
#include "carrot_core/account_secrets.h"
#include "carrot_core/device_ram_borrowed.h"
#include "carrot_core/payment_proposal.h"
#include "carrot_core/scan.h"
#include "crypto/generators.h"
#include "crypto/wow_miner_proof.h"
#include <stdexcept>
namespace wownero::research::test
{
template<class T> unsigned char *bytes(T &value)
{
return reinterpret_cast<unsigned char *>(value.data);
}
struct coinbase_fixture
{
crypto::secret_key account_g, account_t, x, y;
carrot::CarrotCoinbaseEnoteV1 enote;
crypto::hash network = crypto::cn_fast_hash("research-network", 16);
crypto::hash message = crypto::cn_fast_hash("research-block", 14);
explicit coinbase_fixture(bool legacy_address = false)
{
crypto::secret_key master, view_balance, image_preimage, view;
crypto::random32_unbiased(bytes(master));
crypto::public_key partial_spend, account_spend, view_public;
carrot::make_carrot_provespend_key(master, account_t);
carrot::make_carrot_partial_spend_pubkey(account_t, partial_spend);
carrot::make_carrot_viewbalance_secret(master, view_balance);
carrot::make_carrot_generateimage_preimage(view_balance, image_preimage);
carrot::make_carrot_generateimage_key(image_preimage, partial_spend, account_g);
carrot::make_carrot_viewincoming_key(view_balance, view);
if (legacy_address)
account_t = crypto::secret_key{};
carrot::make_carrot_spend_pubkey(account_g, account_t, account_spend);
if (!crypto::secret_key_to_public_key(view, view_public))
throw std::runtime_error("Unable to derive fixture view public key");
carrot::CarrotPaymentProposalV1 proposal{};
carrot::make_carrot_main_address_v1(account_spend, view_public, proposal.destination);
proposal.amount = 100000000000;
proposal.randomness = carrot::gen_janus_anchor();
carrot::get_coinbase_enote_v1(proposal, 101, enote);
carrot::view_incoming_key_ram_borrowed_device device(view);
mx25519_pubkey shared;
crypto::secret_key extension_g, extension_t;
if (!carrot::try_make_carrot_shared_key_receiver(device, enote.enote_ephemeral_pubkey, shared) ||
!carrot::try_scan_carrot_coinbase_enote_receiver(enote, shared, account_spend,
view_public, extension_g, extension_t))
throw std::runtime_error("Unable to scan fixture coinbase");
sc_add(bytes(x), bytes(account_g), bytes(extension_g));
sc_add(bytes(y), bytes(account_t), bytes(extension_t));
}
miner_proof sign() const
{
miner_proof proof;
if (!generate_miner_proof(network, message, enote.onetime_address, x, y, proof))
throw std::runtime_error("Unable to sign fixture coinbase");
return proof;
}
bool verify(const miner_proof &proof) const
{
return verify_miner_proof(network, message, enote.onetime_address,
{proof.data(), proof.size()});
}
};
}
+48
View File
@@ -0,0 +1,48 @@
// Copyright (c) 2026, The Wownero Project
// SPDX-License-Identifier: BSD-3-Clause
#include <iostream>
#include <string_view>
#include "crypto/crypto.h"
#include "cryptonote_basic/cryptonote_basic.h"
#include "cryptonote_basic/cryptonote_format_utils.h"
#include "string_tools.h"
namespace
{
crypto::public_key public_point(std::string_view domain)
{
crypto::public_key key;
crypto::unbiased_hash_to_ec(reinterpret_cast<const unsigned char *>(domain.data()), domain.size(), key);
return key;
}
}
int main()
{
cryptonote::block genesis{};
genesis.major_version = 1;
genesis.minor_version = 0;
genesis.timestamp = 0;
genesis.nonce = 20261004;
auto &tx = genesis.miner_tx;
tx.version = 1;
tx.unlock_time = 288;
tx.vin.emplace_back(cryptonote::txin_gen{0});
// Hash-to-point creates a genesis destination without a known spending witness.
tx.vout.push_back({UINT64_MAX >> 20,
cryptonote::txout_to_key{public_point("Wownero FCMP testnet genesis output 2026-10-04")}});
cryptonote::add_tx_pub_key_to_extra(tx,
public_point("Wownero FCMP testnet genesis transaction key 2026-10-04"));
cryptonote::add_extra_nonce_to_tx_extra(tx.extra, "WOW FCMP++ testnet 2026-10-04");
std::cout << "// Copyright (c) 2026, The Wownero Project\n"
<< "// SPDX-License-Identifier: BSD-3-Clause\n"
<< "// Do not edit. Regenerate with the wow_testnet_genesis executable.\n\n"
<< "#pragma once\n\nnamespace wownero::testnet\n{\n"
<< "inline constexpr char genesis_tx[] = \""
<< epee::string_tools::buff_to_hex_nodelimer(cryptonote::tx_to_blob(tx)) << "\";\n"
<< "inline constexpr char genesis_hash[] = \""
<< epee::string_tools::pod_to_hex(cryptonote::get_block_hash(genesis)) << "\";\n"
<< "inline constexpr unsigned genesis_nonce = 20261004;\n}\n";
}
+605
View File
@@ -0,0 +1,605 @@
// Copyright (c) 2026, The Wownero Project
// SPDX-License-Identifier: BSD-3-Clause
#include <algorithm>
#include <atomic>
#include <functional>
#include <memory>
#include <boost/asio.hpp>
#include <chrono>
#include <csignal>
#include <filesystem>
#include <iostream>
#include <thread>
#include <fcntl.h>
#include <sys/wait.h>
#include <unistd.h>
#include "cryptonote_basic/account.h"
#include "cryptonote_basic/wow_miner.h"
#include "cryptonote_basic/difficulty.h"
#include "cryptonote_core/cryptonote_tx_utils.h"
#include "rpc/core_rpc_server_error_codes.h"
#include "net/http_client.h"
#include "rpc/core_rpc_server_commands_defs.h"
#include "wallet/wallet2.h"
namespace
{
using namespace cryptonote;
using namespace std::chrono_literals;
void require(bool condition, const char *reason)
{
if (!condition)
throw std::runtime_error(reason);
}
struct local_node
{
pid_t pid = -1;
epee::net_utils::http::http_simple_client rpc;
~local_node() { stop(); }
void stop()
{
if (pid < 0)
return;
kill(pid, SIGTERM);
for (unsigned i = 0; i < 300; ++i)
{
if (waitpid(pid, nullptr, WNOHANG) == pid)
{
pid = -1;
return;
}
std::this_thread::sleep_for(100ms);
}
kill(pid, SIGKILL);
waitpid(pid, nullptr, 0);
pid = -1;
}
void start(const std::string &binary, const std::filesystem::path &data, unsigned port,
unsigned peer, const wow_miner_account *account = nullptr, bool offline = false)
{
std::filesystem::create_directories(data);
int keys[2] = {-1, -1};
if (account)
{
require(pipe(keys) == 0, "Unable to create the mining credential pipe");
require(write(keys[1], "WOWMIN01", 8) == 8, "Unable to write mining frame header");
for (const auto *key : {&account->generate_image, &account->prove_spend, &account->view_incoming})
require(write(keys[1], key->data, 32) == 32, "Unable to write mining frame");
close(keys[1]);
}
const int log = open((data / "console.log").c_str(), O_CREAT | O_WRONLY | O_APPEND, 0600);
require(log >= 0, "Unable to open the local daemon log");
std::vector<std::string> arguments{binary, "--testnet", "--non-interactive", "--no-zmq",
"--data-dir", data.string(), "--log-file", (data / "daemon.log").string(),
"--p2p-bind-ip", "127.0.0.1", "--p2p-bind-port", std::to_string(port),
"--rpc-bind-ip", "127.0.0.1", "--rpc-bind-port", std::to_string(port + 1),
"--rpc-restricted-bind-port", std::to_string(port + 2),
"--rpc-ssl", "disabled",
"--allow-local-ip", "--hide-my-port", "--no-igd", "--max-concurrency", "2",
"--check-updates", "disabled", "--disable-dns-checkpoints", "--log-level", "0"};
if (offline)
arguments.emplace_back("--offline");
if (peer)
{
arguments.emplace_back("--add-exclusive-node");
arguments.push_back("127.0.0.1:" + std::to_string(peer));
}
if (account)
{
arguments.emplace_back("--wow-miner-key-fd");
arguments.push_back(std::to_string(keys[0]));
}
std::vector<char *> argv;
for (auto &argument : arguments)
argv.push_back(argument.data());
argv.push_back(nullptr);
pid = fork();
require(pid >= 0, "Unable to fork the local daemon");
if (pid == 0)
{
dup2(log, STDOUT_FILENO);
dup2(log, STDERR_FILENO);
close(log);
execv(binary.c_str(), argv.data());
_exit(127);
}
close(log);
if (keys[0] >= 0)
close(keys[0]);
require(rpc.set_server("127.0.0.1:" + std::to_string(port + 1), boost::none), "Invalid local RPC endpoint");
const auto deadline = std::chrono::steady_clock::now() + 90s;
while (std::chrono::steady_clock::now() < deadline)
{
int status;
if (waitpid(pid, &status, WNOHANG) == pid)
{
pid = -1;
throw std::runtime_error("Local daemon exited before RPC startup; inspect its local log");
}
COMMAND_RPC_GET_INFO::response info{};
if (get_info(info))
{
require(!std::filesystem::exists(data / "testnet" / "rpc_ssl.key"),
"Loopback daemon created an RPC private-key file");
return;
}
std::this_thread::sleep_for(500ms);
}
throw std::runtime_error("Local daemon RPC startup timed out");
}
bool get_info(COMMAND_RPC_GET_INFO::response &info)
{
for (unsigned attempt = 0; attempt < 2; ++attempt)
{
info = {};
if (epee::net_utils::invoke_http_json("/get_info", COMMAND_RPC_GET_INFO::request{}, info, rpc, 30s))
{
if (info.status == CORE_RPC_STATUS_OK)
return true;
std::cerr << "get_info refused: " << info.status << std::endl;
return false;
}
if (attempt == 0)
{
std::cerr << "get_info transport or decoding failed; reconnecting once" << std::endl;
rpc.disconnect();
}
}
std::cerr << "get_info transport or decoding failed after reconnect" << std::endl;
return false;
}
void mining(const std::string &address, bool enable)
{
if (enable)
{
COMMAND_RPC_START_MINING::request req{};
COMMAND_RPC_START_MINING::response res{};
req.miner_address = address;
req.threads_count = 2;
req.ignore_battery = true;
const auto deadline = std::chrono::steady_clock::now() + 90s;
do
{
res = {};
require(epee::net_utils::invoke_http_json("/start_mining", req, res, rpc, 30s),
"Local mining start RPC transport failed");
if (res.status == CORE_RPC_STATUS_OK)
return;
if (res.status != CORE_RPC_STATUS_BUSY)
throw std::runtime_error("Local mining start refused: " + res.status);
std::this_thread::sleep_for(250ms);
} while (std::chrono::steady_clock::now() < deadline);
throw std::runtime_error("Local mining start remained busy after the readiness deadline");
}
else
{
COMMAND_RPC_STOP_MINING::response res{};
require(epee::net_utils::invoke_http_json("/stop_mining", COMMAND_RPC_STOP_MINING::request{}, res, rpc, 30s)
&& res.status == CORE_RPC_STATUS_OK, "Local mining stop failed");
}
}
};
void verify_read_reconnect()
{
using tcp = boost::asio::ip::tcp;
boost::asio::io_context io;
tcp::acceptor listener(io, tcp::endpoint(boost::asio::ip::address_v4::loopback(), 0));
std::atomic<bool> first_request_dropped{false};
std::shared_ptr<tcp::socket> responding_socket;
COMMAND_RPC_GET_INFO::response expected{};
expected.status = CORE_RPC_STATUS_OK;
expected.height = 42;
const std::string body = epee::serialization::store_t_to_json(expected);
const auto reply = std::make_shared<std::string>(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: "
+ std::to_string(body.size()) + "\r\nConnection: close\r\n\r\n" + body);
std::function<void()> accept;
accept = [&] {
const auto socket = std::make_shared<tcp::socket>(io);
listener.async_accept(*socket, [&, socket](const boost::system::error_code &error) {
if (error)
return;
const auto request = std::make_shared<boost::asio::streambuf>();
boost::asio::async_read_until(*socket, *request, "\r\n\r\n",
[&, socket, request](const boost::system::error_code &read_error, size_t) {
if (read_error)
return;
if (!first_request_dropped.exchange(true))
{
boost::system::error_code ignored;
socket->close(ignored);
accept();
return;
}
responding_socket = socket;
// Transaction validation can hold the daemon lock beyond a short read deadline.
const auto delay = std::make_shared<boost::asio::steady_timer>(io, 6s);
delay->async_wait([socket, reply, delay](const boost::system::error_code &wait_error) {
if (wait_error)
return;
boost::asio::async_write(*socket, boost::asio::buffer(*reply),
[socket, reply](const boost::system::error_code &, size_t) {
boost::system::error_code ignored;
socket->shutdown(tcp::socket::shutdown_send, ignored);
});
});
});
});
};
local_node client;
require(client.rpc.set_server("127.0.0.1:" + std::to_string(listener.local_endpoint().port()),
boost::none, epee::net_utils::ssl_support_t::e_ssl_support_disabled),
"Unable to configure the read-reconnection fixture");
accept();
std::thread server([&] { io.run(); });
COMMAND_RPC_GET_INFO::response observed{};
bool received = false;
try
{
received = client.get_info(observed);
}
catch (...)
{
io.stop();
server.join();
throw;
}
io.stop();
server.join();
require(first_request_dropped && received && observed.height == expected.height,
"Read-only RPC did not recover the response after a dropped connection");
std::cout << "read_rpc_reconnect_verified=true" << std::endl;
}
struct prepared_block
{
block candidate;
crypto::hash seed;
crypto::secret_key x, y;
uint64_t difficulty;
prepared_block(local_node &node, const std::string &address, const wow_miner_account &account)
{
COMMAND_RPC_GETBLOCKTEMPLATE::request req{};
COMMAND_RPC_GETBLOCKTEMPLATE::response res{};
req.wallet_address = address;
require(epee::net_utils::invoke_http_json_rpc("/json_rpc", "get_block_template", req, res, node.rpc)
&& res.status == CORE_RPC_STATUS_OK, "Unable to obtain a test block template");
require(res.difficulty_top64 == 0, "Unexpected local test difficulty width");
std::string blob;
require(epee::string_tools::parse_hexstr_to_binbuff(res.blocktemplate_blob, blob)
&& parse_and_validate_block_from_blob(blob, candidate)
&& epee::string_tools::hex_to_pod(res.seed_hash, seed), "Invalid local block template");
require(prepare_wow_miner_output_keys(candidate, account, x, y), "Unable to recover template witnesses");
difficulty = res.difficulty;
}
void solve(bool authorize)
{
const auto deadline = std::chrono::steady_clock::now() + 90s;
do
{
candidate.invalidate_hashes();
if (authorize)
require(sign_wow_miner_block(candidate, x, y), "Unable to authorize the test block");
else
candidate.wow_miner_signature.fill(0);
crypto::hash pow;
get_altblock_longhash(candidate, pow, seed);
if (check_hash(pow, difficulty))
return;
++candidate.nonce;
} while (std::chrono::steady_clock::now() < deadline);
throw std::runtime_error("Test block proof-of-work search timed out");
}
};
void submit_block(local_node &node, const block &candidate, bool expect_acceptance)
{
COMMAND_RPC_SUBMITBLOCK::request req{epee::string_tools::buff_to_hex_nodelimer(block_to_blob(candidate))};
COMMAND_RPC_SUBMITBLOCK::response res{};
epee::json_rpc::error error{};
const bool accepted = epee::net_utils::invoke_http_json_rpc("/json_rpc", "submit_block",
req, res, error, node.rpc, 30s);
if (expect_acceptance)
require(accepted && res.status == CORE_RPC_STATUS_OK, "Authorized test block was not accepted");
else
require(!accepted && error.code == CORE_RPC_ERROR_CODE_BLOCK_NOT_ACCEPTED,
"Invalid test block was not explicitly rejected");
}
void verify_miner_proof_enforcement(local_node &node, const std::string &address,
const wow_miner_account &account)
{
COMMAND_RPC_GET_INFO::response before{}, after{};
require(node.get_info(before), "Unable to read the chain before authorization tests");
prepared_block test(node, address, account);
test.solve(false);
submit_block(node, test.candidate, false);
require(node.get_info(after) && after.top_block_hash == before.top_block_hash,
"Unauthorized block changed the chain tip");
test.solve(true);
submit_block(node, test.candidate, true);
std::cout << "unauthorized_pow_block_rejected=true authorized_control_accepted=true" << std::endl;
}
void verify_restricted_templates(local_node &node, const std::string &address,
const std::vector<tools::wallet2::pending_tx> &transfers)
{
COMMAND_RPC_FLUSH_TRANSACTION_POOL::request flush{};
for (const auto &transfer : transfers)
{
COMMAND_RPC_SEND_RAW_TX::request req{};
COMMAND_RPC_SEND_RAW_TX::response res{};
req.tx_as_hex = epee::string_tools::buff_to_hex_nodelimer(tx_to_blob(transfer.tx));
req.do_not_relay = true;
req.do_sanity_checks = true;
require(epee::net_utils::invoke_http_json("/send_raw_transaction", req, res, node.rpc, 30s)
&& res.status == CORE_RPC_STATUS_OK && res.not_relayed, "Unable to admit a private test transaction");
flush.txids.push_back(epee::string_tools::pod_to_hex(get_transaction_hash(transfer.tx)));
}
epee::net_utils::http::http_simple_client restricted;
require(restricted.set_server("127.0.0.1:49882", boost::none), "Invalid restricted test endpoint");
// Alternate callers to exercise both directions of the shared template cache.
for (const bool public_rpc : {false, true, true, false, true})
{
COMMAND_RPC_GETBLOCKTEMPLATE::request req{};
COMMAND_RPC_GETBLOCKTEMPLATE::response res{};
req.wallet_address = address;
auto &client = public_rpc ? restricted : node.rpc;
require(epee::net_utils::invoke_http_json_rpc("/json_rpc", "get_block_template", req, res, client)
&& res.status == CORE_RPC_STATUS_OK, "Template policy test RPC failed");
std::string blob;
block b;
require(epee::string_tools::parse_hexstr_to_binbuff(res.blocktemplate_blob, blob)
&& parse_and_validate_block_from_blob(blob, b), "Template policy test returned an invalid block");
for (const auto &transfer : transfers)
{
const bool included = std::find(b.tx_hashes.begin(), b.tx_hashes.end(),
get_transaction_hash(transfer.tx)) != b.tx_hashes.end();
require(included == !public_rpc, "Template disclosed a private transaction or omitted the private control");
}
}
COMMAND_RPC_FLUSH_TRANSACTION_POOL::response flushed{};
require(epee::net_utils::invoke_http_json_rpc("/json_rpc", "flush_txpool", flush, flushed, node.rpc)
&& flushed.status == CORE_RPC_STATUS_OK, "Unable to clear the private test transactions");
std::cout << "restricted_template_privacy_verified=true" << std::endl;
}
void verify_reorganization(local_node &miner, local_node &observer, const std::string &binary,
const std::filesystem::path &data, const std::string &address, const wow_miner_account &account,
tools::wallet2 &recovering_wallet)
{
COMMAND_RPC_GET_INFO::response common{}, competing{}, tip{};
require(miner.get_info(common) && observer.get_info(tip) && common.top_block_hash == tip.top_block_hash,
"Reorganization test requires a common starting tip");
miner.stop();
observer.stop();
miner.start(binary, data / "miner", 49880, 0, &account, true);
observer.start(binary, data / "observer", 49890, 0, &account, true);
for (unsigned i = 0; i < 2; ++i)
{
prepared_block b(miner, address, account);
b.solve(true);
submit_block(miner, b.candidate, true);
}
require(miner.get_info(competing) && competing.height == common.height + 2,
"Short fork did not advance by two blocks");
require(recovering_wallet.init("http://127.0.0.1:49881"), "Unable to select the losing fork for wallet scanning");
recovering_wallet.refresh(true);
require(recovering_wallet.get_blockchain_current_height() == competing.height,
"The recovering wallet did not scan the losing fork");
prepared_block alternative(observer, address, account);
alternative.solve(false);
submit_block(miner, alternative.candidate, false);
require(miner.get_info(tip) && tip.top_block_hash == competing.top_block_hash,
"Unauthorized alternate block changed the chain tip");
std::vector<block> fork;
alternative.solve(true);
submit_block(observer, alternative.candidate, true);
fork.push_back(alternative.candidate);
for (unsigned i = 0; i < 3; ++i)
{
prepared_block b(observer, address, account);
b.solve(true);
submit_block(observer, b.candidate, true);
fork.push_back(b.candidate);
}
submit_block(miner, fork.front(), true);
require(miner.get_info(tip) && tip.top_block_hash == competing.top_block_hash,
"A shorter valid alternate chain displaced the main chain");
for (size_t i = 1; i < fork.size(); ++i)
submit_block(miner, fork[i], true);
require(observer.get_info(competing) && miner.get_info(tip)
&& tip.top_block_hash == competing.top_block_hash && tip.height == common.height + fork.size(),
"The node did not adopt the greater-work authorized fork");
miner.stop();
observer.stop();
miner.start(binary, data / "miner", 49880, 49890, &account);
observer.start(binary, data / "observer", 49890, 49880);
require(miner.get_info(tip) && tip.top_block_hash == competing.top_block_hash
&& observer.get_info(tip) && tip.top_block_hash == competing.top_block_hash,
"Restart did not preserve the reorganized chain");
require(recovering_wallet.init("http://127.0.0.1:49891"), "Unable to restore the observer wallet endpoint");
recovering_wallet.refresh(true);
require(recovering_wallet.get_blockchain_current_height() == competing.height,
"The recovering wallet did not adopt the winning fork");
std::cout << "alternate_unauthorized_pow_rejected=true reorganization_verified=true wallet_reorganization_verified=true detached_blocks=2" << std::endl;
}
void wait_height(local_node &node, uint64_t wanted)
{
const auto deadline = std::chrono::steady_clock::now() + 15min;
auto next_report = std::chrono::steady_clock::now();
while (std::chrono::steady_clock::now() < deadline)
{
COMMAND_RPC_GET_INFO::response info{};
require(node.get_info(info), "Local get_info did not return a valid response");
if (info.height >= wanted)
return;
if (std::chrono::steady_clock::now() >= next_report)
{
std::cout << "height=" << info.height << " target=" << wanted << std::endl;
next_report = std::chrono::steady_clock::now() + 20s;
}
std::this_thread::sleep_for(250ms);
}
throw std::runtime_error("Local mining or synchronization timed out");
}
}
int main(int argc, char **argv)
{
try
{
if (argc == 2 && std::string(argv[1]) == "--rpc-read-check")
{
verify_read_reconnect();
return 0;
}
require(argc == 3, "Usage: wow_local_network DAEMON NEW_DATA_DIRECTORY | --rpc-read-check");
verify_read_reconnect();
const std::filesystem::path data = std::filesystem::absolute(argv[2]);
require(!std::filesystem::exists(data), "The local test requires a new data directory");
account_base source, destination;
source.generate();
destination.generate();
const auto &keys = source.get_keys();
wow_miner_account miner_account;
miner_account.generate_image = keys.m_spend_secret_key;
miner_account.view_incoming = keys.m_view_secret_key;
const auto address = source.get_public_address_str(TESTNET);
local_node miner, observer;
miner.start(argv[1], data / "miner", 49880, 49890, &miner_account);
observer.start(argv[1], data / "observer", 49890, 49880);
miner.mining(address, true);
wait_height(miner, CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW + 12);
miner.mining(address, false);
COMMAND_RPC_GET_INFO::response mined{};
require(miner.get_info(mined), "Miner RPC failed after stopping");
wait_height(observer, mined.height);
COMMAND_RPC_GET_INFO::response observed{};
require(observer.get_info(observed) && observed.top_block_hash == mined.top_block_hash,
"The local nodes disagree on the mined chain");
std::cout << "synchronized_height=" << mined.height << std::endl;
tools::wallet2 sender(TESTNET, 1, true), receiver(TESTNET, 1, true);
sender.ask_password(tools::wallet2::AskPasswordNever);
receiver.ask_password(tools::wallet2::AskPasswordNever);
sender.generate("", "", keys.m_account_address, keys.m_spend_secret_key, keys.m_view_secret_key);
const auto &recipient_keys = destination.get_keys();
receiver.generate("", "", recipient_keys.m_account_address,
recipient_keys.m_spend_secret_key, recipient_keys.m_view_secret_key);
require(sender.init("http://127.0.0.1:49881") && receiver.init("http://127.0.0.1:49891"),
"Unable to initialize the in-memory test wallets");
sender.refresh(true);
require(sender.unlocked_balance(0, true) > COIN, "Mature coinbase funds were not scanned");
std::vector<tx_destination_entry> destinations{{COIN, recipient_keys.m_account_address, false}};
auto transfers = sender.create_transactions_2(destinations, 0, tools::fee_priority::Unimportant, {}, 0, {});
require(!transfers.empty(), "Wallet produced no FCMP transaction");
for (const auto &transfer : transfers)
{
require(rct::is_rct_fcmp(transfer.tx.rct_signatures.type), "Wallet did not construct an FCMP transaction");
std::cout << "fcmp_transaction_bytes=" << tx_to_blob(transfer.tx).size()
<< " fee_atomic=" << transfer.fee << std::endl;
}
verify_restricted_templates(miner, address, transfers);
sender.commit_tx(transfers);
std::cout << "fcmp_transaction_relayed=true" << std::endl;
COMMAND_RPC_GET_TRANSACTIONS::request lookup{};
lookup.prune = true;
for (const auto &transfer : transfers)
lookup.txs_hashes.push_back(epee::string_tools::pod_to_hex(get_transaction_hash(transfer.tx)));
miner.mining(address, true);
const auto payment_deadline = std::chrono::steady_clock::now() + 180s;
auto next_report = std::chrono::steady_clock::now();
for (;;)
{
require(std::chrono::steady_clock::now() < payment_deadline, "Payment inclusion or maturity timed out");
COMMAND_RPC_GET_TRANSACTIONS::response transactions{};
require(epee::net_utils::invoke_http_json("/get_transactions", lookup, transactions, miner.rpc, 10s)
&& transactions.status == CORE_RPC_STATUS_OK, "Unable to observe the relayed payment");
require(miner.get_info(mined), "Unable to read the payment confirmation height");
const bool mature = std::all_of(lookup.txs_hashes.begin(), lookup.txs_hashes.end(), [&](const std::string &hash) {
return std::any_of(transactions.txs.begin(), transactions.txs.end(), [&](const auto &tx) {
return tx.tx_hash == hash && !tx.in_pool &&
mined.height > tx.block_height + CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE;
});
});
if (mature)
break;
if (std::chrono::steady_clock::now() >= next_report)
{
std::cout << "payment_confirmation_pending height=" << mined.height << std::endl;
next_report = std::chrono::steady_clock::now() + 20s;
}
std::this_thread::sleep_for(500ms);
}
miner.mining(address, false);
require(miner.get_info(mined), "Miner RPC failed after payment confirmation");
wait_height(observer, mined.height);
receiver.refresh(true);
require(receiver.balance(0, true) == COIN, "Recipient did not receive the exact payment");
require(receiver.unlocked_balance(0, true) == COIN, "Recipient unlocked payment amount differs");
verify_miner_proof_enforcement(miner, address, miner_account);
require(miner.get_info(mined), "Unable to read height after the authorized control block");
wait_height(observer, mined.height);
observer.stop();
observer.start(argv[1], data / "observer", 49890, 49880);
require(observer.get_info(observed) && observed.top_block_hash == mined.top_block_hash,
"Observer restart did not recover the accepted chain");
std::cout << "fcmp_payment_verified=true restart_verified=true height=" << mined.height << std::endl;
verify_reorganization(miner, observer, argv[1], data, address, miner_account, receiver);
receiver.refresh(true);
require(receiver.unlocked_balance(0, true) == COIN, "Reorganization changed the settled recipient balance");
std::vector<tx_destination_entry> return_destinations{{COIN / 2, keys.m_account_address, false}};
auto returned = receiver.create_transactions_2(return_destinations, 0, tools::fee_priority::Unimportant, {}, 0, {});
require(!returned.empty(), "No FCMP payment was constructed after reorganization");
for (const auto &transfer : returned)
require(rct::is_rct_fcmp(transfer.tx.rct_signatures.type), "Post-reorganization spend did not use FCMP");
receiver.commit_tx(returned);
require(miner.get_info(mined), "Unable to read post-reorganization height");
miner.mining(address, true);
const auto recovery_deadline = std::chrono::steady_clock::now() + 180s;
for (;;)
{
require(std::chrono::steady_clock::now() < recovery_deadline, "Post-reorganization payment confirmation timed out");
COMMAND_RPC_GET_TRANSACTIONS::request req{};
COMMAND_RPC_GET_TRANSACTIONS::response res{};
req.prune = true;
for (const auto &transfer : returned)
req.txs_hashes.push_back(epee::string_tools::pod_to_hex(get_transaction_hash(transfer.tx)));
require(epee::net_utils::invoke_http_json("/get_transactions", req, res, miner.rpc, 10s)
&& res.status == CORE_RPC_STATUS_OK, "Unable to observe the post-reorganization payment");
const bool confirmed = std::all_of(req.txs_hashes.begin(), req.txs_hashes.end(), [&](const auto &hash) {
return std::any_of(res.txs.begin(), res.txs.end(), [&](const auto &tx) {
return tx.tx_hash == hash && !tx.in_pool;
});
});
if (confirmed)
break;
std::this_thread::sleep_for(500ms);
}
miner.mining(address, false);
require(miner.get_info(mined), "Unable to read final chain height");
wait_height(observer, mined.height);
std::cout << "post_reorganization_fcmp_spend_confirmed=true height=" << mined.height << std::endl;
return 0;
}
catch (const std::exception &error)
{
std::cerr << "Local test failed: " << error.what() << std::endl;
return 1;
}
}
+58
View File
@@ -0,0 +1,58 @@
// Copyright (c) 2026, The Wownero Project
// SPDX-License-Identifier: BSD-3-Clause
#include <array>
#include <iostream>
#include <memory>
#include <stdexcept>
#include <string>
#include <vector>
#include "randomx.h"
#include "string_tools.h"
int main()
{
try
{
const std::string seed = "Wownero FCMP testnet PoW qualification v1";
const std::vector<std::string> inputs{
"Wownero FCMP testnet genesis domain",
"Wownero FCMP testnet fork A",
"Wownero FCMP testnet fork B",
std::string(256, '\0')};
std::vector<std::array<char, RANDOMX_HASH_SIZE>> expected;
for (const auto flags : {RANDOMX_FLAG_DEFAULT, RANDOMX_FLAG_JIT})
{
std::unique_ptr<randomx_cache, decltype(&randomx_release_cache)> cache(
randomx_alloc_cache(flags), randomx_release_cache);
if (!cache)
throw std::runtime_error("Unable to allocate the RandomWOW qualification cache");
randomx_init_cache(cache.get(), seed.data(), seed.size());
std::unique_ptr<randomx_vm, decltype(&randomx_destroy_vm)> vm(
randomx_create_vm(flags | RANDOMX_FLAG_SECURE, cache.get(), nullptr), randomx_destroy_vm);
if (!vm)
throw std::runtime_error("Unable to create the requested RandomWOW VM");
for (size_t i = 0; i < inputs.size(); ++i)
{
std::array<char, RANDOMX_HASH_SIZE> hash{};
randomx_calculate_hash(vm.get(), inputs[i].data(), inputs[i].size(), hash.data());
if (flags == RANDOMX_FLAG_DEFAULT)
expected.push_back(hash);
else if (hash != expected.at(i))
throw std::runtime_error("RandomWOW interpreter and JIT disagree");
}
}
for (size_t i = 0; i < expected.size(); ++i)
std::cout << "vector=" << i << " hash="
<< epee::string_tools::buff_to_hex_nodelimer(std::string(expected[i].data(), expected[i].size()))
<< '\n';
std::cout << "interpreter_jit_agreement=true\n";
return 0;
}
catch (const std::exception &error)
{
std::cerr << "PoW qualification failed: " << error.what() << '\n';
return 1;
}
}
@@ -0,0 +1,167 @@
// Copyright (c) 2026, The Wownero Project
// SPDX-License-Identifier: BSD-3-Clause
#include <gtest/gtest.h>
#include <chrono>
#include <future>
#include <limits>
#include <thread>
#include "blockchain_db/lmdb/db_lmdb.h"
#include "cryptonote_basic/cryptonote_format_utils.h"
#include "cryptonote_core/cryptonote_core.h"
#include "easylogging++.h"
#include "rpc/rpc_handler.h"
#include "string_tools.h"
#include "wownero/testnet_genesis.h"
namespace
{
using namespace cryptonote;
using namespace std::chrono_literals;
TEST(WowReleaseRegression, LegacyRingOffsetsRespectForkBoundary)
{
transaction tx;
tx.version = 2;
txin_to_key input{};
input.key_offsets = {std::numeric_limits<uint64_t>::max(), 1};
tx.vin.push_back(input);
EXPECT_TRUE(core::check_tx_inputs_ring_members_overflow(tx, HF_VERSION_FCMP_PLUS_PLUS - 1));
EXPECT_FALSE(core::check_tx_inputs_ring_members_overflow(tx, HF_VERSION_FCMP_PLUS_PLUS));
EXPECT_FALSE(core::check_tx_inputs_ring_members_overflow(tx, HF_VERSION_FCMP_PLUS_PLUS + 1));
auto &ring = boost::get<txin_to_key>(tx.vin.front()).key_offsets;
ring = {std::numeric_limits<uint64_t>::max() - 1, 1};
EXPECT_TRUE(core::check_tx_inputs_ring_members_overflow(tx, HF_VERSION_FCMP_PLUS_PLUS));
ring = {1, std::numeric_limits<uint64_t>::max()};
EXPECT_FALSE(core::check_tx_inputs_ring_members_overflow(tx, HF_VERSION_FCMP_PLUS_PLUS));
}
TEST(WowReleaseRegression, LegacyRingOffsetsAreCheckedPerInput)
{
transaction tx;
tx.version = 2;
txin_to_key first{}, second{};
first.key_offsets = {std::numeric_limits<uint64_t>::max()};
second.key_offsets = {1, 2};
tx.vin = {first, second};
EXPECT_TRUE(core::check_tx_inputs_ring_members_overflow(tx, HF_VERSION_FCMP_PLUS_PLUS));
boost::get<txin_to_key>(tx.vin.back()).key_offsets = {std::numeric_limits<uint64_t>::max(), 2};
EXPECT_FALSE(core::check_tx_inputs_ring_members_overflow(tx, HF_VERSION_FCMP_PLUS_PLUS));
tx.vin.back() = txin_gen{};
EXPECT_FALSE(core::check_tx_inputs_ring_members_overflow(tx, HF_VERSION_FCMP_PLUS_PLUS));
}
TEST(WowReleaseRegression, PrunedBaseRequiresExactBoundary)
{
std::string blob;
ASSERT_TRUE(epee::string_tools::parse_hexstr_to_binbuff(wownero::testnet::genesis_tx, blob));
transaction tx;
ASSERT_TRUE(parse_and_validate_tx_base_from_blob(blob, tx, true, true));
blob.push_back('\0');
EXPECT_FALSE(parse_and_validate_tx_base_from_blob(blob, tx, true, true));
EXPECT_TRUE(parse_and_validate_tx_base_from_blob(blob, tx, true, false));
blob.pop_back();
blob.pop_back();
EXPECT_FALSE(parse_and_validate_tx_base_from_blob(blob, tx, true, true));
}
TEST(WowReleaseRegression, NestedDatabaseAdmissionSurvivesClosedGate)
{
const auto baseline = mdb_txn_safe::num_active_txns.load();
std::promise<void> outer_ready, start_nested, nested_done, release_outer;
auto ready = outer_ready.get_future();
auto start = start_nested.get_future();
auto done = nested_done.get_future();
auto release = release_outer.get_future();
std::thread worker([&] {
mdb_txn_safe outer;
outer_ready.set_value();
start.wait();
{
mdb_txn_safe nested;
nested_done.set_value();
release.wait();
}
});
ready.wait();
mdb_txn_safe::prevent_new_txns();
start_nested.set_value();
const bool nested_admitted = done.wait_for(2s) == std::future_status::ready;
// Reopen even on failure so the regression cannot strand its worker.
mdb_txn_safe::allow_new_txns();
release_outer.set_value();
worker.join();
EXPECT_TRUE(nested_admitted);
EXPECT_EQ(mdb_txn_safe::num_active_txns.load(), baseline);
}
TEST(WowReleaseRegression, FreshDatabaseAdmissionWaitsForOpenGate)
{
const auto baseline = mdb_txn_safe::num_active_txns.load();
std::promise<void> started, admitted;
auto running = started.get_future();
auto ready = admitted.get_future();
mdb_txn_safe::prevent_new_txns();
std::thread worker([&] {
started.set_value();
mdb_txn_safe txn;
admitted.set_value();
});
running.wait();
const bool remained_closed = ready.wait_for(100ms) == std::future_status::timeout;
mdb_txn_safe::allow_new_txns();
worker.join();
EXPECT_TRUE(remained_closed);
EXPECT_EQ(mdb_txn_safe::num_active_txns.load(), baseline);
}
TEST(WowReleaseRegression, DistributionRefusalPreservesCachedBounds)
{
bool reorganized = false;
auto hash = [&](uint64_t height) {
crypto::hash value{};
value.data[0] = static_cast<char>(height);
value.data[1] = reorganized && height > 10;
return value;
};
unsigned calls = 0;
auto distribution = [&](uint64_t, uint64_t, uint64_t,
uint64_t &start, std::vector<uint64_t> &values, uint64_t &base) {
++calls;
start = 16;
base = 0;
values = {1, 2, 3, 4, 5};
return true;
};
ASSERT_TRUE(rpc::RpcHandler::get_output_distribution(distribution, 0, 0, 20, hash, true, 40));
reorganized = true;
EXPECT_FALSE(rpc::RpcHandler::get_output_distribution(distribution, 0, 0, 21, hash, true, 40));
EXPECT_FALSE(rpc::RpcHandler::get_output_distribution(distribution, 0, 0, 21, hash, true, 40));
EXPECT_EQ(calls, 1u);
reorganized = false;
const auto original = rpc::RpcHandler::get_output_distribution(distribution, 0, 0, 20, hash, true, 40);
ASSERT_TRUE(original);
EXPECT_EQ(original->distribution, (std::vector<uint64_t>{1, 2, 3, 4, 5}));
}
TEST(WowReleaseRegression, ConsoleSanitizationRemovesEscapeControls)
{
std::string message = "payment \x1b[2J accepted";
el::base::sanitize(message);
EXPECT_EQ(message.find('\x1b'), std::string::npos);
EXPECT_NE(message.find("payment"), std::string::npos);
EXPECT_NE(message.find("accepted"), std::string::npos);
std::string plain = "valid UTF-8: \xc3\xa9\n";
const auto expected = plain;
el::base::sanitize(plain);
EXPECT_EQ(plain, expected);
}
}
int main(int argc, char **argv)
{
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
+185
View File
@@ -0,0 +1,185 @@
// Copyright (c) 2026, The Wownero Project
// SPDX-License-Identifier: BSD-3-Clause
#include <gtest/gtest.h>
#include <cstring>
#include <fcntl.h>
#include <unistd.h>
#include "carrot_core/payment_proposal.h"
#include "crypto/generators.h"
#include "cryptonote_basic/cryptonote_format_utils.h"
#include "cryptonote_basic/wow_miner.h"
#include "string_tools.h"
#include "wownero/testnet_genesis.h"
namespace
{
using namespace cryptonote;
struct block_fixture
{
wow_miner_account account;
crypto::secret_key x, y;
block b{};
explicit block_fixture(bool legacy = false)
{
crypto::random32_unbiased(reinterpret_cast<unsigned char *>(account.generate_image.data));
if (!legacy)
crypto::random32_unbiased(reinterpret_cast<unsigned char *>(account.prove_spend.data));
crypto::random32_unbiased(reinterpret_cast<unsigned char *>(account.view_incoming.data));
account_public_address address;
if (!account.address(address))
throw std::runtime_error("Invalid generated test account");
carrot::CarrotPaymentProposalV1 proposal{};
carrot::make_carrot_main_address_v1(address.m_spend_public_key, address.m_view_public_key, proposal.destination);
proposal.amount = COIN;
proposal.randomness = carrot::gen_janus_anchor();
carrot::CarrotCoinbaseEnoteV1 enote;
carrot::get_coinbase_enote_v1(proposal, 101, enote);
b.major_version = HF_VERSION_CARROT;
b.minor_version = HF_VERSION_CARROT;
b.timestamp = 1000;
b.nonce = 7;
b.fcmp_pp_n_tree_layers = 1;
b.fcmp_pp_tree_root = {};
auto &tx = b.miner_tx;
tx.version = 2;
tx.unlock_time = 101 + CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW;
tx.vin.emplace_back(txin_gen{101});
txout_to_carrot_v1 output;
output.key = enote.onetime_address;
output.view_tag = enote.view_tag;
output.encrypted_janus_anchor = enote.anchor_enc;
tx.vout.push_back({proposal.amount, output});
crypto::public_key ephemeral;
std::memcpy(&ephemeral, &enote.enote_ephemeral_pubkey, sizeof(ephemeral));
add_tx_pub_key_to_extra(tx, ephemeral);
if (!prepare_wow_miner_output_keys(b, account, x, y) || !sign_wow_miner_block(b, x, y))
throw std::runtime_error("Unable to prepare signed Carrot test block");
}
};
TEST(WowTestnet, NewAndLegacyCarrotAccounts)
{
for (bool legacy : {false, true})
{
block_fixture fixture(legacy);
EXPECT_TRUE(verify_wow_miner_block(fixture.b));
auto wrong = fixture.account;
crypto::random32_unbiased(reinterpret_cast<unsigned char *>(wrong.view_incoming.data));
EXPECT_FALSE(prepare_wow_miner_output_keys(fixture.b, wrong, fixture.x, fixture.y));
}
}
TEST(WowTestnet, CompleteBlockBinding)
{
block_fixture fixture;
const std::function<void(block &)> mutations[] = {
[](block &b) { ++b.nonce; },
[](block &b) { ++b.timestamp; },
[](block &b) { ++b.minor_version; },
[](block &b) { b.prev_id.data[0] ^= 1; },
[](block &b) { b.wow_vote = 1; },
[](block &b) { ++b.miner_tx.unlock_time; },
[](block &b) { ++b.miner_tx.vout.front().amount; },
[](block &b) { b.miner_tx.extra.push_back(0); },
[](block &b) { ++boost::get<txin_gen>(b.miner_tx.vin.front()).height; },
[](block &b) { b.tx_hashes.push_back(crypto::cn_fast_hash("tx", 2)); },
[](block &b) { ++b.fcmp_pp_n_tree_layers; },
[](block &b) { b.fcmp_pp_tree_root.data[0] ^= 1; },
};
// Populate caches first; verification must not trust stale cached hashes.
get_block_hash(fixture.b);
for (const auto &mutate : mutations)
{
block altered = fixture.b;
mutate(altered);
EXPECT_FALSE(verify_wow_miner_block(altered));
}
}
TEST(WowTestnet, SignatureSerializationAndPowBinding)
{
block_fixture fixture;
block decoded;
ASSERT_TRUE(parse_and_validate_block_from_blob(block_to_blob(fixture.b), decoded));
EXPECT_TRUE(verify_wow_miner_block(decoded));
const auto statement = get_wow_miner_statement_hash(fixture.b);
const auto pow_blob = get_block_hashing_blob(fixture.b);
fixture.b.wow_miner_signature[0] ^= 1;
EXPECT_EQ(statement, get_wow_miner_statement_hash(fixture.b));
EXPECT_NE(pow_blob, get_block_hashing_blob(fixture.b));
EXPECT_FALSE(verify_wow_miner_block(fixture.b));
}
TEST(WowTestnet, RejectsInvalidMinerShapes)
{
block_fixture fixture;
for (auto vote : {0, 1, 2})
{
fixture.b.wow_vote = vote;
ASSERT_TRUE(sign_wow_miner_block(fixture.b, fixture.x, fixture.y));
EXPECT_TRUE(verify_wow_miner_block(fixture.b));
}
fixture.b.wow_vote = 3;
EXPECT_FALSE(sign_wow_miner_block(fixture.b, fixture.x, fixture.y));
fixture.b.wow_vote = 0;
fixture.b.miner_tx.vout.push_back(fixture.b.miner_tx.vout.front());
EXPECT_FALSE(sign_wow_miner_block(fixture.b, fixture.x, fixture.y));
fixture.b.miner_tx.vout.resize(1);
fixture.b.miner_tx.vout.front().target = txout_to_key{};
EXPECT_FALSE(verify_wow_miner_block(fixture.b));
}
TEST(WowTestnet, IsolatedNetworkAndGenesis)
{
EXPECT_THROW(get_config(MAINNET), std::runtime_error);
EXPECT_THROW(get_config(STAGENET), std::runtime_error);
EXPECT_THROW(get_config(FAKECHAIN), std::runtime_error);
EXPECT_NO_THROW(get_config(TESTNET));
block genesis{};
genesis.major_version = 1;
genesis.nonce = wownero::testnet::genesis_nonce;
std::string blob;
ASSERT_TRUE(epee::string_tools::parse_hexstr_to_binbuff(wownero::testnet::genesis_tx, blob));
ASSERT_TRUE(parse_and_validate_tx_from_blob(blob, genesis.miner_tx));
EXPECT_EQ(epee::string_tools::pod_to_hex(get_block_hash(genesis)), wownero::testnet::genesis_hash);
EXPECT_EQ(genesis.miner_tx.unlock_time, CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW);
EXPECT_TRUE(wownero::research::check_miner_public_key(
boost::get<txout_to_key>(genesis.miner_tx.vout.front().target).key));
}
TEST(WowTestnet, MiningCredentialsUsePipeAndCloseDescriptor)
{
block_fixture fixture;
int descriptors[2];
ASSERT_EQ(pipe(descriptors), 0);
ASSERT_EQ(write(descriptors[1], "WOWMIN01", 8), 8);
for (const auto *key : {&fixture.account.generate_image, &fixture.account.prove_spend, &fixture.account.view_incoming})
ASSERT_EQ(write(descriptors[1], key->data, 32), 32);
close(descriptors[1]);
wow_miner_account decoded;
ASSERT_TRUE(read_wow_miner_account(descriptors[0], decoded));
EXPECT_EQ(fcntl(descriptors[0], F_GETFD), -1);
account_public_address expected, actual;
ASSERT_TRUE(fixture.account.address(expected));
ASSERT_TRUE(decoded.address(actual));
EXPECT_TRUE(expected == actual);
ASSERT_EQ(pipe(descriptors), 0);
ASSERT_EQ(write(descriptors[1], "WOWMIN01", 8), 8);
close(descriptors[1]);
EXPECT_FALSE(read_wow_miner_account(descriptors[0], decoded));
EXPECT_FALSE(decoded.address(actual));
EXPECT_EQ(fcntl(descriptors[0], F_GETFD), -1);
EXPECT_FALSE(read_wow_miner_account(STDIN_FILENO, decoded));
}
}
int main(int argc, char **argv)
{
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
+185
View File
@@ -0,0 +1,185 @@
// Copyright (c) 2026, The Wownero Project
// SPDX-License-Identifier: BSD-3-Clause
#include "fixture.h"
#include <algorithm>
#include <cstring>
#include <gtest/gtest.h>
namespace wr = wownero::research;
using wr::test::coinbase_fixture;
using wr::test::bytes;
TEST(WowMinerProof, CarrotCoinbaseReceiverCanSign)
{
const coinbase_fixture f;
EXPECT_TRUE(f.verify(f.sign()));
}
TEST(WowMinerProof, CarrotCoinbaseToLegacyAddressCanSign)
{
const coinbase_fixture f(true);
EXPECT_TRUE(f.verify(f.sign()));
EXPECT_TRUE(sc_isnonzero(reinterpret_cast<const unsigned char *>(f.y.data)));
}
TEST(WowMinerProof, FreshNoncesForRepeatedStatement)
{
const coinbase_fixture f;
const auto first = f.sign();
const auto second = f.sign();
EXPECT_NE(first, second);
EXPECT_TRUE(f.verify(first));
EXPECT_TRUE(f.verify(second));
}
TEST(WowMinerProof, BindsEveryByteOfBlockAndNetworkDigest)
{
coinbase_fixture f;
const auto proof = f.sign();
for (auto *digest : {&f.message, &f.network})
for (auto &byte : digest->data)
{
byte ^= 1;
EXPECT_FALSE(f.verify(proof));
byte ^= 1;
}
}
TEST(WowMinerProof, CannotTransferProofToAnotherCoinbase)
{
const coinbase_fixture first, second;
EXPECT_FALSE(second.verify(first.sign()));
}
TEST(WowMinerProof, RejectsMutationOfEveryProofByte)
{
const coinbase_fixture f;
auto proof = f.sign();
for (auto &byte : proof)
{
byte ^= 1;
EXPECT_FALSE(f.verify(proof));
byte ^= 1;
}
}
TEST(WowMinerProof, RejectsTruncatedAndExtendedProof)
{
const coinbase_fixture f;
const auto proof = f.sign();
for (std::size_t length = 0; length < proof.size(); ++length)
EXPECT_FALSE(wr::verify_miner_proof(f.network, f.message, f.enote.onetime_address,
{proof.data(), length}));
std::array<std::uint8_t, 97> extended{};
std::copy(proof.begin(), proof.end(), extended.begin());
EXPECT_FALSE(wr::verify_miner_proof(f.network, f.message, f.enote.onetime_address,
{extended.data(), extended.size()}));
}
TEST(WowMinerProof, RejectsNoncanonicalScalarsAndZeroChallenge)
{
const coinbase_fixture f;
const auto original = f.sign();
for (std::size_t offset : {0, 32, 64})
{
auto proof = original;
std::fill(proof.begin() + offset, proof.begin() + offset + 32, 0xff);
EXPECT_FALSE(f.verify(proof));
}
auto proof = original;
std::fill(proof.begin(), proof.begin() + 32, 0);
EXPECT_FALSE(f.verify(proof));
}
TEST(WowMinerProof, MissingEitherWitnessRefusesAndClearsOutput)
{
const coinbase_fixture f;
crypto::secret_key absent{};
for (const bool missing_g : {false, true})
{
auto proof = f.sign();
EXPECT_FALSE(wr::generate_miner_proof(f.network, f.message, f.enote.onetime_address,
missing_g ? absent : f.x, missing_g ? f.y : absent, proof));
EXPECT_TRUE(std::all_of(proof.begin(), proof.end(), [](auto byte) { return byte == 0; }));
}
}
TEST(WowMinerProof, AccountKeysCannotSignWithoutCoinbaseExtensions)
{
const coinbase_fixture f;
wr::miner_proof proof;
EXPECT_FALSE(wr::generate_miner_proof(f.network, f.message, f.enote.onetime_address,
f.account_g, f.account_t, proof));
}
TEST(WowMinerProof, RejectsNoncanonicalSecretScalar)
{
const coinbase_fixture f;
crypto::secret_key invalid;
std::memset(invalid.data, 0xff, sizeof(invalid.data));
wr::miner_proof proof;
EXPECT_FALSE(wr::generate_miner_proof(f.network, f.message, f.enote.onetime_address,
invalid, f.y, proof));
EXPECT_FALSE(wr::generate_miner_proof(f.network, f.message, f.enote.onetime_address,
f.x, invalid, proof));
}
TEST(WowMinerProof, AllowsZeroComponentForValidNonidentityRepresentation)
{
const coinbase_fixture f;
const crypto::secret_key zero{};
for (const bool zero_g : {false, true})
{
const auto &x = zero_g ? zero : f.x;
const auto &y = zero_g ? f.y : zero;
crypto::public_key key;
carrot::make_carrot_spend_pubkey(x, y, key);
wr::miner_proof proof;
ASSERT_TRUE(wr::generate_miner_proof(f.network, f.message, key, x, y, proof));
EXPECT_TRUE(wr::verify_miner_proof(f.network, f.message, key, {proof.data(), proof.size()}));
}
}
TEST(WowMinerProof, RejectsIdentityTorsionMixedOrderAndNoncanonicalKeys)
{
EXPECT_TRUE(wr::check_miner_public_key(crypto::get_G()));
EXPECT_TRUE(wr::check_miner_public_key(crypto::get_T()));
crypto::public_key identity{};
identity.data[0] = 1;
EXPECT_FALSE(wr::check_miner_public_key(identity));
crypto::public_key order_two;
std::memset(order_two.data, 0xff, 32);
order_two.data[0] = static_cast<char>(0xec);
order_two.data[31] = 0x7f;
EXPECT_FALSE(wr::check_miner_public_key(order_two));
ge_p3 torsion;
ASSERT_EQ(0, ge_frombytes_vartime(&torsion, bytes(order_two)));
ge_cached cached;
ge_p3_to_cached(&cached, &torsion);
ge_p1p1 mixed_sum;
ge_add(&mixed_sum, &crypto::get_G_p3(), &cached);
ge_p3 mixed_point;
ge_p1p1_to_p3(&mixed_point, &mixed_sum);
crypto::public_key mixed;
ge_p3_tobytes(bytes(mixed), &mixed_point);
EXPECT_FALSE(wr::check_miner_public_key(mixed));
crypto::public_key noncanonical;
std::memset(noncanonical.data, 0xff, 32);
noncanonical.data[0] = static_cast<char>(0xee);
noncanonical.data[31] = 0x7f;
EXPECT_FALSE(wr::check_miner_public_key(noncanonical));
identity.data[31] = static_cast<char>(0x80);
EXPECT_FALSE(wr::check_miner_public_key(identity));
coinbase_fixture f;
const auto proof = f.sign();
for (const auto &key : {identity, order_two, mixed, noncanonical})
{
f.enote.onetime_address = key;
EXPECT_FALSE(f.verify(proof));
}
}