Import current p2pool-salvium snapshot
C/C++ CI / build-alpine-static (map[arch:aarch64 branch:latest-stable flags:-ffunction-sections -mfix-cortex-a53-835769 -mfix-cortex-a53-843419]) (push) Has been cancelled
C/C++ CI / build-alpine-static (map[arch:riscv64 branch:latest-stable flags:-ffunction-sections]) (push) Has been cancelled
C/C++ CI / build-alpine-static (map[arch:x86_64 branch:latest-stable flags:-ffunction-sections]) (push) Has been cancelled
C/C++ CI / build-ubuntu (map[c:gcc-10 cpp:g++-10 flags: os:ubuntu-22.04]) (push) Has been cancelled
C/C++ CI / build-ubuntu (map[c:gcc-11 cpp:g++-11 flags: os:ubuntu-22.04]) (push) Has been cancelled
C/C++ CI / build-ubuntu (map[c:gcc-12 cpp:g++-12 flags: os:ubuntu-22.04]) (push) Has been cancelled
C/C++ CI / build-ubuntu (map[c:gcc-13 cpp:g++-13 flags: os:ubuntu-24.04]) (push) Has been cancelled
C/C++ CI / build-ubuntu (map[c:gcc-14 cpp:g++-14 flags: os:ubuntu-24.04]) (push) Has been cancelled
C/C++ CI / build-ubuntu (map[c:gcc-9 cpp:g++-9 flags: os:ubuntu-22.04]) (push) Has been cancelled
C/C++ CI / build-ubuntu-static-libs (map[flags:-fuse-linker-plugin -ffunction-sections]) (push) Has been cancelled
C/C++ CI / build-ubuntu-aarch64 (map[flags:-fuse-linker-plugin -ffunction-sections -mfix-cortex-a53-835769 -mfix-cortex-a53-843419 os:ubuntu-22.04-arm]) (push) Has been cancelled
C/C++ CI / build-ubuntu-riscv64 (map[flags:-fuse-linker-plugin -ffunction-sections os:ubuntu-24.04]) (push) Has been cancelled
C/C++ CI / build-windows-msys2 (map[c:gcc cxx:g++ flags:-ffunction-sections -Wno-error=maybe-uninitialized -Wno-error=attributes -Wno-attributes]) (push) Has been cancelled
C/C++ CI / build-windows-msbuild (map[grpc:OFF os:2022 rx:ON tls:ON upnp:ON vs:Visual Studio 17 2022 vspath:C:\Program Files\Microsoft Visual Studio\2022\Enterprise]) (push) Has been cancelled
C/C++ CI / build-windows-msbuild (map[grpc:ON os:2022 rx:ON tls:ON upnp:ON vs:Visual Studio 17 2022 vspath:C:\Program Files\Microsoft Visual Studio\2022\Enterprise]) (push) Has been cancelled
C/C++ CI / build-macos (push) Has been cancelled
C/C++ CI / build-macos-aarch64 (push) Has been cancelled
C/C++ CI / build-freebsd (map[architecture:x86-64 host:ubuntu-latest name:freebsd version:13.3]) (push) Has been cancelled
C/C++ CI / build-openbsd (map[architecture:x86-64 host:ubuntu-latest name:openbsd version:7.4]) (push) Has been cancelled
C/C++ CI / build-windows-msys2 (map[c:clang cxx:clang++ flags:-fuse-ld=lld -Wno-unused-command-line-argument -Wno-nan-infinity-disabled -Wno-attributes]) (push) Has been cancelled
clang-tidy / clang-tidy (push) Has been cancelled
CodeQL / Analyze (cpp) (push) Has been cancelled
Code coverage / coverage (push) Has been cancelled
cppcheck / cppcheck-ubuntu (push) Has been cancelled
cppcheck / cppcheck-windows (push) Has been cancelled
Microsoft C++ Code Analysis / Analyze (push) Has been cancelled
source-snapshot / source-snapshot (push) Has been cancelled
Sync test (macOS) / sync-test-macos-intel (push) Has been cancelled
Sync test (macOS) / sync-test-macos-arm64 (push) Has been cancelled
Sync test (Ubuntu) / sync-test-ubuntu-tsan (push) Has been cancelled
Sync test (Ubuntu) / sync-test-ubuntu-msan (push) Has been cancelled
Sync test (Ubuntu) / sync-test-ubuntu-ubsan (push) Has been cancelled
Sync test (Ubuntu) / sync-test-ubuntu-asan (push) Has been cancelled
Sync test (Windows) / sync-test-windows-debug-asan (push) Has been cancelled
Sync test (Windows) / sync-test-windows-leaks (push) Has been cancelled

This commit is contained in:
Codex Bot
2026-04-25 11:36:07 +02:00
parent e432577257
commit 6a0421816a
23 changed files with 2190 additions and 520 deletions
+1002
View File
File diff suppressed because it is too large Load Diff
+15 -8
View File
@@ -4,14 +4,21 @@ Decentralized pool for Salvium mining. Originally forked from [SChernykh's P2Poo
### Build Status
![C/C++ CI](https://github.com/mxhess/p2pool-salvium/actions/workflows/c-cpp.yml/badge.svg)
![test-sync-ubuntu](https://github.com/mxhess/p2pool-salvium/actions/workflows/test-sync-ubuntu.yml/badge.svg)
![test-sync-macos](https://github.com/mxhess/p2pool-salvium/actions/workflows/test-sync-macos.yml/badge.svg)
![test-sync-windows](https://github.com/mxhess/p2pool-salvium/actions/workflows/test-sync-windows.yml/badge.svg)
![CodeQL](https://github.com/mxhess/p2pool-salvium/actions/workflows/codeql-analysis.yml/badge.svg)
![msvc-analysis](https://github.com/mxhess/p2pool-salvium/actions/workflows/msvc-analysis.yml/badge.svg)
![cppcheck](https://github.com/mxhess/p2pool-salvium/actions/workflows/cppcheck.yml/badge.svg)
![clang-tidy](https://github.com/mxhess/p2pool-salvium/actions/workflows/clang-tidy.yml/badge.svg)
[![pipeline](https://gitlab.com/whiskyrelaxing-group/p2pool-salvium/badges/main/pipeline.svg)](https://gitlab.com/whiskyrelaxing-group/p2pool-salvium/-/pipelines)
[![coverage](https://gitlab.com/whiskyrelaxing-group/p2pool-salvium/badges/main/coverage.svg)](https://gitlab.com/whiskyrelaxing-group/p2pool-salvium/-/pipelines)
[![clang-tidy](https://gitlab.com/whiskyrelaxing-group/p2pool-salvium/-/jobs/artifacts/main/raw/badges/clang-tidy.svg?job=generate-badges)](https://gitlab.com/whiskyrelaxing-group/p2pool-salvium/-/pipelines)
[![cppcheck-linux](https://gitlab.com/whiskyrelaxing-group/p2pool-salvium/-/jobs/artifacts/main/raw/badges/cppcheck-linux.svg?job=generate-badges)](https://gitlab.com/whiskyrelaxing-group/p2pool-salvium/-/pipelines)
[![cppcheck-windows](https://gitlab.com/whiskyrelaxing-group/p2pool-salvium/-/jobs/artifacts/main/raw/badges/cppcheck-windows.svg?job=generate-badges)](https://gitlab.com/whiskyrelaxing-group/p2pool-salvium/-/pipelines)
[![build-x64](https://gitlab.com/whiskyrelaxing-group/p2pool-salvium/-/jobs/artifacts/main/raw/badges/build-alpine-static-x64.svg?job=generate-badges)](https://gitlab.com/whiskyrelaxing-group/p2pool-salvium/-/pipelines)
[![build-aarch64](https://gitlab.com/whiskyrelaxing-group/p2pool-salvium/-/jobs/artifacts/main/raw/badges/build-alpine-static-aarch64.svg?job=generate-badges)](https://gitlab.com/whiskyrelaxing-group/p2pool-salvium/-/pipelines)
[![build-riscv64](https://gitlab.com/whiskyrelaxing-group/p2pool-salvium/-/jobs/artifacts/main/raw/badges/build-alpine-static-riscv64.svg?job=generate-badges)](https://gitlab.com/whiskyrelaxing-group/p2pool-salvium/-/pipelines)
[![build-debian](https://gitlab.com/whiskyrelaxing-group/p2pool-salvium/-/jobs/artifacts/main/raw/badges/build-debian-12.svg?job=generate-badges)](https://gitlab.com/whiskyrelaxing-group/p2pool-salvium/-/pipelines)
[![build-windows](https://gitlab.com/whiskyrelaxing-group/p2pool-salvium/-/jobs/artifacts/main/raw/badges/build-windows-x64.svg?job=generate-badges)](https://gitlab.com/whiskyrelaxing-group/p2pool-salvium/-/pipelines)
[![TSAN](https://gitlab.com/whiskyrelaxing-group/p2pool-salvium/-/jobs/artifacts/main/raw/badges/sync-test-tsan.svg?job=generate-badges)](https://gitlab.com/whiskyrelaxing-group/p2pool-salvium/-/pipelines)
[![ASAN](https://gitlab.com/whiskyrelaxing-group/p2pool-salvium/-/jobs/artifacts/main/raw/badges/sync-test-asan.svg?job=generate-badges)](https://gitlab.com/whiskyrelaxing-group/p2pool-salvium/-/pipelines)
[![UBSAN](https://gitlab.com/whiskyrelaxing-group/p2pool-salvium/-/jobs/artifacts/main/raw/badges/sync-test-ubsan.svg?job=generate-badges)](https://gitlab.com/whiskyrelaxing-group/p2pool-salvium/-/pipelines)
[![MSAN](https://gitlab.com/whiskyrelaxing-group/p2pool-salvium/-/jobs/artifacts/main/raw/badges/sync-test-msan.svg?job=generate-badges)](https://gitlab.com/whiskyrelaxing-group/p2pool-salvium/-/pipelines)
# Contents
- [Pool mining vs Solo mining vs P2Pool mining](#pool-mining-vs-solo-mining-vs-p2pool-mining)
+8
View File
@@ -0,0 +1,8 @@
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_CROSSCOMPILING TRUE)
set(CMAKE_SYSTEM_PROCESSOR aarch64)
set(CMAKE_C_COMPILER /usr/bin/aarch64-linux-gnu-gcc)
set(CMAKE_CXX_COMPILER /usr/bin/aarch64-linux-gnu-g++)
set(CMAKE_STRIP /usr/bin/aarch64-linux-gnu-strip)
+2 -2
View File
@@ -38,7 +38,7 @@ if (CMAKE_CXX_COMPILER_ID MATCHES GNU)
else()
set(OPTIMIZATION_FLAGS "-Og")
endif()
set(OPTIMIZATION_FLAGS "${OPTIMIZATION_FLAGS} -g3 -ftrapv")
set(OPTIMIZATION_FLAGS "${OPTIMIZATION_FLAGS} -g3")
else()
set(OPTIMIZATION_FLAGS "-O3 -ffast-math -s")
endif()
@@ -130,7 +130,7 @@ elseif (CMAKE_CXX_COMPILER_ID MATCHES Clang)
else()
set(OPTIMIZATION_FLAGS "-Og")
endif()
set(OPTIMIZATION_FLAGS "${OPTIMIZATION_FLAGS} -g3 -ftrapv")
set(OPTIMIZATION_FLAGS "${OPTIMIZATION_FLAGS} -g3")
else()
set(OPTIMIZATION_FLAGS "-O3 -ffast-math -funroll-loops -fmerge-all-constants")
endif()
+15
View File
@@ -0,0 +1,15 @@
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR riscv64)
set(CMAKE_CROSSCOMPILING TRUE)
set(triple riscv64-linux-musl)
set(CMAKE_C_COMPILER ${triple}-gcc)
set(CMAKE_CXX_COMPILER ${triple}-g++)
set(CMAKE_AR ${triple}-ar)
set(CMAKE_RANLIB ${triple}-ranlib)
set(CMAKE_STRIP ${triple}-strip)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
+19
View File
@@ -0,0 +1,19 @@
@echo off
"C:\Cppcheck\cppcheck.exe" -j 4 ../src/*.cpp -D__cppcheck__ -DSIZE_MAX=UINT64_MAX -DRAPIDJSON_ENDIAN=RAPIDJSON_LITTLEENDIAN -D_WIN32=1 -D_WIN64=1 -DWIN32=1 -D_WINDOWS=1 -DNDEBUG=1 -DWITH_GRPC=1 -DPROTOBUF_ENABLE_DEBUG_LOGGING_MAY_LEAK_PII=0 -DWITH_RANDOMX=1 -DWITH_UPNP=1 -DWITH_TLS=1 -DWITH_MERGE_MINING_DONATION=1 -DCURL_STATICLIB=1 -DWIN32_LEAN_AND_MEAN=1 -D_WIN32_WINNT=0x0600 -D_DISABLE_VECTOR_ANNOTATION=1 -D_DISABLE_STRING_ANNOTATION=1 -DZMQ_STATIC=1 -DZMQ_VERSION=40306 -DHAVE_BITSCANREVERSE64=1 -DRAPIDJSON_PARSE_DEFAULT_FLAGS=kParseTrailingCommasFlag -DMINIUPNP_STATICLIB=1 -DCARES_STATICLIB=1 -DCMAKE_INTDIR="Release" -D__SSE2__=1 -D_MSC_VER=1929 --platform=win64 --std=c++17 --enable=all --inconclusive --inline-suppr --template="{file}:{line}:{id}{inconclusive: INCONCLUSIVE} {message}" --includes-file=includes.txt --suppressions-list=suppressions.txt --output-file=errors_full.txt --max-ctu-depth=3 --check-level=exhaustive --checkers-report=checkers_report.txt
findstr /V /C:"external\src" errors_full.txt > errors_filtered0.txt
findstr /V /C:":checkersReport" errors_filtered0.txt > errors_filtered.txt
findstr /C:"There were critical errors" checkers_report.txt > checkers_report_filtered.txt
for /f %%i in ("errors_filtered.txt") do set size=%%~zi
if %size% gtr 0 (
type errors_filtered.txt
exit 1
)
for /f %%i in ("checkers_report_filtered.txt") do set size2=%%~zi
if %size2% gtr 0 (
type checkers_report_filtered.txt
exit 1
)
+6
View File
@@ -61,6 +61,9 @@ const hardfork_t mainnet_hard_forks[] = {
// version 10 Carrot - including treasury mint - starts from block 334750, which is on or around the 13th of October, 2025. Fork time finalised on 2025-09-29. No fork voting occurs for the v10 fork.
{10, 334750, 0, 1759142500 },
// version 11 Two Milestone 1 - starts from block 465000, which is on or around the 13th of April, 2026. Fork time finalised on 2026-03-20. No fork voting occurs for the v11 fork.
{11, 465000, 0, 1774000000 },
};
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 = ((uint64_t)-1);
@@ -95,6 +98,9 @@ const hardfork_t testnet_hard_forks[] = {
// version 10 Carrot - including treasury mint - starts from block 1100
{10, 1100, 0, 1739780005 },
// version 11
{11, 1200, 0, 1739880005 },
};
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 = ((uint64_t)-1);
+31
View File
@@ -0,0 +1,31 @@
#!/bin/sh
# Generate a shields.io-style SVG badge as a CI artifact.
# Usage: gen-badge.sh <label> <status> <color> <output_file>
# Example: gen-badge.sh "clang-tidy" "passing" "brightgreen" "clang-tidy.svg"
LABEL="$1"
STATUS="$2"
COLOR="$3"
OUTPUT="$4"
LABEL_WIDTH=$(( ${#LABEL} * 7 + 10 ))
STATUS_WIDTH=$(( ${#STATUS} * 7 + 10 ))
TOTAL_WIDTH=$(( LABEL_WIDTH + STATUS_WIDTH ))
cat > "$OUTPUT" << SVGEOF
<svg xmlns="http://www.w3.org/2000/svg" width="${TOTAL_WIDTH}" height="20">
<linearGradient id="a" x2="0" y2="100%">
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
<stop offset="1" stop-opacity=".1"/>
</linearGradient>
<rect rx="3" width="${TOTAL_WIDTH}" height="20" fill="#555"/>
<rect rx="3" x="${LABEL_WIDTH}" width="${STATUS_WIDTH}" height="20" fill="${COLOR}"/>
<rect rx="3" width="${TOTAL_WIDTH}" height="20" fill="url(#a)"/>
<g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
<text x="$(( LABEL_WIDTH / 2 ))" y="15" fill="#010101" fill-opacity=".3">${LABEL}</text>
<text x="$(( LABEL_WIDTH / 2 ))" y="14">${LABEL}</text>
<text x="$(( LABEL_WIDTH + STATUS_WIDTH / 2 ))" y="15" fill="#010101" fill-opacity=".3">${STATUS}</text>
<text x="$(( LABEL_WIDTH + STATUS_WIDTH / 2 ))" y="14">${STATUS}</text>
</g>
</svg>
SVGEOF
+65 -53
View File
@@ -28,7 +28,6 @@
#include "p2pool.h"
#include "side_chain.h"
#include "pool_block.h"
#include "protocol_tx_hash.h"
#include "merkle.h"
#include <zmq.hpp>
#include <ctime>
@@ -67,6 +66,7 @@ BlockTemplate::BlockTemplate(SideChain* sidechain, RandomX_Hasher_Base* hasher)
, m_minerTxKeccakStateInputLength(0)
, m_sidechainHashKeccakState{}
, m_sidechainHashInputLength(0)
, m_sidechainExtraNonceOffset(0)
, m_rng(RandomDeviceSeed::instance)
{
// Diffuse the initial state in case it has low quality
@@ -115,15 +115,8 @@ BlockTemplate::BlockTemplate(const BlockTemplate& b)
*this = b;
}
// cppcheck-suppress operatorEqVarError
BlockTemplate& BlockTemplate::operator=(const BlockTemplate& b)
void BlockTemplate::copy_from_nolock(const BlockTemplate& b)
{
if (this == &b) {
return *this;
}
WriteLock lock(m_lock);
m_sidechain = b.m_sidechain;
m_hasher = b.m_hasher;
m_templateId = b.m_templateId;
@@ -158,11 +151,13 @@ BlockTemplate& BlockTemplate::operator=(const BlockTemplate& b)
m_sidechainHashKeccakState = b.m_sidechainHashKeccakState;
m_sidechainHashInputLength = b.m_sidechainHashInputLength;
m_sidechainExtraNonceOffset = b.m_sidechainExtraNonceOffset;
m_minerTx.clear();
m_blockHeader.clear();
m_minerTxExtra.clear();
m_transactionHashes.clear();
m_transactionHashesSet.clear();
m_rewards.clear();
m_mempoolTxs.clear();
m_mempoolTxsOrder.clear();
@@ -174,7 +169,17 @@ BlockTemplate& BlockTemplate::operator=(const BlockTemplate& b)
#if TEST_MEMPOOL_PICKING_ALGORITHM
m_knapsack.clear();
#endif
}
// cppcheck-suppress operatorEqVarError ; m_lock and m_oldTemplates are per-instance, must not be copied
BlockTemplate& BlockTemplate::operator=(const BlockTemplate& b)
{
if (this == &b) {
return *this;
}
WriteLock lock(m_lock);
copy_from_nolock(b);
return *this;
}
@@ -252,7 +257,7 @@ void BlockTemplate::update(const MinerData& data, const Mempool& mempool, const
auto use_old_template = [this]() {
const uint32_t id = m_templateId - 1;
LOGWARN(4, "using old block template with ID = " << id);
*this = *m_oldTemplates[id % array_size(&BlockTemplate::m_oldTemplates)];
copy_from_nolock(*m_oldTemplates[id % array_size(&BlockTemplate::m_oldTemplates)]);
};
m_height = data.height;
@@ -296,6 +301,15 @@ void BlockTemplate::update(const MinerData& data, const Mempool& mempool, const
m_poolBlockTemplate->m_minerWallet = params->m_miningWallet;
// Protocol TX blob is always required for Carrot v1+ blocks.
// update_block_template() ensures it's loaded from the daemon before calling update().
m_poolBlockTemplate->m_protocolTxBlob = data.protocol_tx_blob;
m_protocolTxHash = data.protocol_tx_hash;
// Detect and handle hardfork transitions before building the sidechain data.
// This purges the stale sidechain and enables genesis creation on the new chain.
m_sidechain->handle_hardfork_transition(data.major_version);
if (!m_sidechain->fill_sidechain_data(*m_poolBlockTemplate, m_shares)) {
use_old_template();
return;
@@ -562,9 +576,17 @@ void BlockTemplate::update(const MinerData& data, const Mempool& mempool, const
#endif
}
// Salvium: Calculate full reward, then apply 80/20 split
// Salvium: Calculate full reward, then apply reward split
full_block_reward = final_reward; // Full reward including fees
final_reward = final_reward - (final_reward / 5); // 80% to miners
if (data.major_version >= 11) {
// HF11: 60% miner + 25% treasury + 15% staker
const uint64_t treasury_reward = full_block_reward * 25 / 100;
const uint64_t staker_reward = (full_block_reward - treasury_reward) * 20 / 100;
final_reward = full_block_reward - treasury_reward - staker_reward;
} else {
// HF10: 80% miner + 20% staker
final_reward = final_reward - (final_reward / 5);
}
if (!SideChain::split_reward(final_reward, m_shares, m_rewards)) {
use_old_template();
return;
@@ -655,31 +677,10 @@ void BlockTemplate::update(const MinerData& data, const Mempool& mempool, const
m_transactionHashes.reserve(HASH_SIZE * 2 + mempool_txs.size());
m_transactionHashes.insert(m_transactionHashes.end(), miner_tx_hash.h, miner_tx_hash.h + HASH_SIZE);
// Write protocol tx to blob (for sidechain consensus)
// Write protocol tx to block template blob (already set on pool block template at top of update())
m_protocolTxOffsetInTemplate = m_blockTemplateBlob.size();
if (m_poolBlockTemplate->m_sidechainHeight >= SIDECHAIN_PROTOCOL_TX_HARDFORK_HEIGHT && data.protocol_tx_loaded && !data.protocol_tx_blob.empty()) {
// Real protocol_tx from daemon - use it directly
m_blockTemplateBlob.insert(m_blockTemplateBlob.end(), data.protocol_tx_blob.begin(), data.protocol_tx_blob.end());
m_protocolTxSize = m_blockTemplateBlob.size() - m_protocolTxOffsetInTemplate;
m_protocolTxHash = data.protocol_tx_hash;
m_poolBlockTemplate->m_protocolTxBlob = data.protocol_tx_blob;
} else {
// Stub protocol_tx (pre-hardfork or blob not yet available)
writeVarint(4, m_blockTemplateBlob); // version
writeVarint(60, m_blockTemplateBlob); // unlock_time
writeVarint(1, m_blockTemplateBlob); // vin count
m_blockTemplateBlob.push_back(0xff); // TXIN_GEN
writeVarint(data.height, m_blockTemplateBlob); // height
writeVarint(0, m_blockTemplateBlob); // vout count
writeVarint(2, m_blockTemplateBlob); // extra size
m_blockTemplateBlob.push_back(0x02); // extra[0]
m_blockTemplateBlob.push_back(0x00); // extra[1]
writeVarint(2, m_blockTemplateBlob); // type PROTOCOL
m_blockTemplateBlob.push_back(0); // RCT type
m_protocolTxSize = m_blockTemplateBlob.size() - m_protocolTxOffsetInTemplate;
calculate_protocol_tx_hash(data.height, m_protocolTxHash);
m_poolBlockTemplate->m_protocolTxBlob.clear();
}
m_blockTemplateBlob.insert(m_blockTemplateBlob.end(), m_poolBlockTemplate->m_protocolTxBlob.begin(), m_poolBlockTemplate->m_protocolTxBlob.end());
m_protocolTxSize = m_blockTemplateBlob.size() - m_protocolTxOffsetInTemplate;
// Add protocol tx hash after miner tx
m_transactionHashes.insert(m_transactionHashes.end(), m_protocolTxHash.h, m_protocolTxHash.h + HASH_SIZE);
@@ -759,17 +760,21 @@ void BlockTemplate::update(const MinerData& data, const Mempool& mempool, const
init_merge_mining_merkle_proof();
const std::vector<uint8_t> sidechain_data = m_poolBlockTemplate->serialize_sidechain_data();
size_t sidechain_extra_nonce_offset_in_sc;
const std::vector<uint8_t> sidechain_data = m_poolBlockTemplate->serialize_sidechain_data(&sidechain_extra_nonce_offset_in_sc);
const std::vector<uint8_t>& consensus_id = m_sidechain->consensus_id();
m_sidechainHashBlob = m_poolBlockTemplate->serialize_mainchain_data();
const size_t mainchain_size = m_sidechainHashBlob.size();
m_sidechainHashBlob.insert(m_sidechainHashBlob.end(), sidechain_data.begin(), sidechain_data.end());
m_sidechainHashBlob.insert(m_sidechainHashBlob.end(), consensus_id.begin(), consensus_id.end());
m_sidechainExtraNonceOffset = mainchain_size + sidechain_extra_nonce_offset_in_sc;
{
m_sidechainHashKeccakState = {};
const size_t extra_nonce_offset = m_sidechainHashBlob.size() - HASH_SIZE - EXTRA_NONCE_SIZE;
const size_t extra_nonce_offset = m_sidechainExtraNonceOffset;
if (extra_nonce_offset >= KeccakParams::HASH_DATA_AREA) {
// Sidechain data is big enough to cache keccak state up to extra_nonce
m_sidechainHashInputLength = (extra_nonce_offset / KeccakParams::HASH_DATA_AREA) * KeccakParams::HASH_DATA_AREA;
@@ -1022,8 +1027,8 @@ int BlockTemplate::create_miner_tx(const MinerData& data, const std::vector<Mine
const size_t num_outputs = num_miner_outputs + num_protocol_outputs;
m_minerTx.reserve(num_outputs * 60 + 55);
// For Carrot v1 (HF10+), use version 4
m_minerTx.push_back(4); // TRANSACTION_VERSION_CARROT
// For Carrot v1 (HF10+), use version 4; HF11+ uses version 5
m_minerTx.push_back(data.major_version >= 11 ? TX_VERSION_ENABLE_TOKENS : TX_VERSION);
writeVarint(MINER_REWARD_UNLOCK_TIME, m_minerTx);
m_minerTx.push_back(1); // Number of inputs
@@ -1058,6 +1063,7 @@ int BlockTemplate::create_miner_tx(const MinerData& data, const std::vector<Mine
hash eph_pubkey; // D_e (per-output ephemeral pubkey)
uint8_t view_tag[3];
uint8_t encrypted_anchor[16];
std::string asset_type; // "SAL1" for miner outputs, variable for protocol outputs
};
std::vector<CarrotOutput> outputs;
outputs.reserve(num_outputs);
@@ -1072,6 +1078,8 @@ int BlockTemplate::create_miner_tx(const MinerData& data, const std::vector<Mine
memset(out.view_tag, 0, 3);
memset(out.encrypted_anchor, 0, 16);
out.asset_type = "SAL1";
if (!dry_run) {
// Derive anchor from wallet's spend public key (position-independent)
uint8_t anchor[16];
@@ -1135,6 +1143,7 @@ int BlockTemplate::create_miner_tx(const MinerData& data, const std::vector<Mine
out.eph_pubkey = po.eph_pubkey;
memcpy(out.view_tag, po.view_tag, 3);
memcpy(out.encrypted_anchor, po.anchor_enc, 16);
out.asset_type = po.asset_type;
outputs.push_back(out);
}
@@ -1164,20 +1173,14 @@ int BlockTemplate::create_miner_tx(const MinerData& data, const std::vector<Mine
if (dry_run) {
m_minerTx.insert(m_minerTx.end(), HASH_SIZE, 0);
m_minerTx.push_back(4);
m_minerTx.push_back('S');
m_minerTx.push_back('A');
m_minerTx.push_back('L');
m_minerTx.push_back('1');
writeVarint(out.asset_type.size(), m_minerTx);
m_minerTx.insert(m_minerTx.end(), out.asset_type.begin(), out.asset_type.end());
m_minerTx.insert(m_minerTx.end(), 3, 0);
m_minerTx.insert(m_minerTx.end(), 16, 0);
} else {
m_minerTx.insert(m_minerTx.end(), out.onetime_address.h, out.onetime_address.h + HASH_SIZE);
m_minerTx.push_back(4);
m_minerTx.push_back('S');
m_minerTx.push_back('A');
m_minerTx.push_back('L');
m_minerTx.push_back('1');
writeVarint(out.asset_type.size(), m_minerTx);
m_minerTx.insert(m_minerTx.end(), out.asset_type.begin(), out.asset_type.end());
m_minerTx.insert(m_minerTx.end(), out.view_tag, out.view_tag + 3);
m_minerTx.insert(m_minerTx.end(), out.encrypted_anchor, out.encrypted_anchor + 16);
@@ -1189,6 +1192,7 @@ int BlockTemplate::create_miner_tx(const MinerData& data, const std::vector<Mine
po.amount = out.amount;
memcpy(po.view_tag, out.view_tag, 3);
memcpy(po.anchor_enc, out.encrypted_anchor, 16);
po.asset_type = out.asset_type;
m_poolBlockTemplate->m_protocolOutputs.push_back(po);
} else {
// Save miner output to pool block template
@@ -1283,8 +1287,16 @@ int BlockTemplate::create_miner_tx(const MinerData& data, const std::vector<Mine
// type = MINER (1)
writeVarint(1, m_minerTx);
// amount_burnt = 20% of total block reward
uint64_t amount_burnt = full_block_reward / 5;
// amount_burnt = staker share of total block reward
uint64_t amount_burnt;
if (data.major_version >= 11) {
// HF11: staker gets 20% of (block_reward - treasury) = 15% of total
const uint64_t treasury_reward = full_block_reward * 25 / 100;
amount_burnt = (full_block_reward - treasury_reward) * 20 / 100;
} else {
// HF10: staker gets 20% of total
amount_burnt = full_block_reward / 5;
}
writeVarint(amount_burnt, m_minerTx);
m_poolBlockTemplate->m_amountBurnt = amount_burnt;
@@ -1301,7 +1313,7 @@ hash BlockTemplate::calc_sidechain_hash(uint32_t sidechain_extra_nonce) const
const size_t size = m_sidechainHashBlob.size();
const size_t N = m_sidechainHashInputLength;
const size_t sidechain_extra_nonce_offset = size - HASH_SIZE - EXTRA_NONCE_SIZE;
const size_t sidechain_extra_nonce_offset = m_sidechainExtraNonceOffset;
const uint8_t sidechain_extra_nonce_buf[EXTRA_NONCE_SIZE] = {
static_cast<uint8_t>(sidechain_extra_nonce >> 0),
static_cast<uint8_t>(sidechain_extra_nonce >> 8),
+2
View File
@@ -74,6 +74,7 @@ private:
RandomX_Hasher_Base* m_hasher;
private:
void copy_from_nolock(const BlockTemplate& b);
void select_mempool_transactions(const Mempool& mempool);
int create_miner_tx(const MinerData& data, const std::vector<MinerShare>& shares, uint64_t max_reward_amounts_weight, bool dry_run, uint64_t full_block_reward);
hash calc_sidechain_hash(uint32_t sidechain_extra_nonce) const;
@@ -132,6 +133,7 @@ private:
std::vector<uint8_t> m_sidechainHashBlob;
std::array<uint64_t, 25> m_sidechainHashKeccakState;
size_t m_sidechainHashInputLength;
size_t m_sidechainExtraNonceOffset;
std::vector<uint8_t> m_blockHeader;
std::vector<uint8_t> m_minerTxExtra;
+17 -2
View File
@@ -117,12 +117,13 @@ extern "C" void __nss_module_disable_loading();
namespace p2pool {
constexpr size_t HASH_SIZE = 32;
constexpr uint8_t HARDFORK_SUPPORTED_VERSION = 10;
constexpr uint8_t HARDFORK_SUPPORTED_VERSION = 11;
constexpr uint8_t MINER_REWARD_UNLOCK_TIME = 60;
constexpr uint8_t NONCE_SIZE = 4;
constexpr uint8_t EXTRA_NONCE_SIZE = 4;
constexpr uint8_t EXTRA_NONCE_MAX_SIZE = EXTRA_NONCE_SIZE + 10;
constexpr uint8_t TX_VERSION = 4;
constexpr uint8_t TX_VERSION_ENABLE_TOKENS = 5;
constexpr uint8_t TXIN_GEN = 0xFF;
constexpr uint8_t TXOUT_TO_TAGGED_KEY = 4; // Changed from 3 to 4 for Carrot v1
constexpr uint8_t TXOUT_TO_CARROT_V1 = 4;
@@ -131,6 +132,19 @@ constexpr uint8_t TX_EXTRA_TAG_ADDITIONAL_PUBKEYS = 4;
constexpr uint8_t TX_EXTRA_NONCE = 2;
constexpr uint8_t TX_EXTRA_MERGE_MINING_TAG = 3;
// Salvium transaction types
constexpr uint8_t TX_TYPE_UNSET = 0;
constexpr uint8_t TX_TYPE_MINER = 1;
constexpr uint8_t TX_TYPE_PROTOCOL = 2;
constexpr uint8_t TX_TYPE_TRANSFER = 3;
constexpr uint8_t TX_TYPE_CONVERT = 4;
constexpr uint8_t TX_TYPE_BURN = 5;
constexpr uint8_t TX_TYPE_STAKE = 6;
constexpr uint8_t TX_TYPE_RETURN = 7;
constexpr uint8_t TX_TYPE_AUDIT = 8;
constexpr uint8_t TX_TYPE_CREATE_TOKEN = 9;
constexpr uint8_t TX_TYPE_ROLLUP = 10;
#ifdef _MSC_VER
#define umul128 _umul128
#define udiv128 _udiv128
@@ -534,8 +548,9 @@ struct ProtocolOutput
uint64_t amount;
uint8_t view_tag[3];
uint8_t anchor_enc[16];
std::string asset_type; // "SAL1" by default, variable at HF11+
FORCEINLINE ProtocolOutput() : onetime_address{}, eph_pubkey{}, amount(0), view_tag{}, anchor_enc{} {}
FORCEINLINE ProtocolOutput() : onetime_address{}, eph_pubkey{}, amount(0), view_tag{}, anchor_enc{}, asset_type("SAL1") {}
FORCEINLINE bool operator==(const ProtocolOutput& rhs) const {
return onetime_address == rhs.onetime_address;
+1 -1
View File
@@ -273,7 +273,7 @@ static void do_status(p2pool *m_pool, const char * /* args */)
if (tip && (data.height < tip->m_txinGenHeight)) {
node_health -= 5;
comments.push_back("Your Salvium node is lagging");
comments.push_back("Mainchain data is stale (miner_data height behind sidechain tip)");
}
if (stratum && (stratum->num_connections() == 0)) {
+181 -97
View File
@@ -85,6 +85,7 @@ P2PServer::P2PServer(p2pool* pool)
, m_numOnionConnections(0)
, m_lookForMissingBlocks(true)
, m_fastestPeer(nullptr)
, m_maxPeerTipHeight(0)
, m_newP2PoolVersionDetected(false)
, m_auxJobLastMessageTimestamp(0)
{
@@ -166,29 +167,12 @@ P2PServer::P2PServer(p2pool* pool)
m_cache->load_all(m_pool->side_chain(), *this);
m_cacheLoaded = true;
// Bootstrap sidechain from cache - add blocks in height order
if (m_cachedBlocks && !m_cachedBlocks->empty()) {
LOGINFO(1, "bootstrapping sidechain from " << m_cachedBlocks->size() << " cached blocks");
// Collect and sort by height ascending
std::vector<PoolBlock*> sorted_blocks;
sorted_blocks.reserve(m_cachedBlocks->size());
for (const auto& it : *m_cachedBlocks) {
sorted_blocks.push_back(it.second);
}
std::sort(sorted_blocks.begin(), sorted_blocks.end(),
[](const PoolBlock* a, const PoolBlock* b) {
return a->m_sidechainHeight < b->m_sidechainHeight;
});
// Add in height order so parents exist before children
std::vector<hash> missing;
for (PoolBlock* block : sorted_blocks) {
(void)m_pool->side_chain().add_external_block(*block, missing);
}
LOGINFO(1, "cache bootstrap complete, chain tip height = " <<
(m_pool->side_chain().chainTip() ? m_pool->side_chain().chainTip()->m_sidechainHeight : 0));
// Cache loaded — blocks are available via m_cachedBlocks for on-demand
// lookup in download_missing_blocks. We don't bootstrap eagerly here
// because mainchain data isn't available yet (salviumd not connected),
// so add_external_block would defer and forget every block.
if (m_cachedBlocks) {
LOGINFO(1, "loaded " << m_cachedBlocks->size() << " cached blocks (available for on-demand sync)");
}
}
@@ -357,6 +341,7 @@ void P2PServer::update_peer_connections()
unordered_set<raw_ip> connected_clients;
unordered_set<std::string_view> connected_clients_domain;
std::vector<uint64_t> peer_tip_heights;
connected_clients.reserve(m_numConnections);
@@ -405,6 +390,9 @@ void P2PServer::update_peer_connections()
if (client->is_good()) {
has_good_peers = true;
if (client->m_broadcastMaxHeight > 0) {
peer_tip_heights.push_back(client->m_broadcastMaxHeight);
}
if ((client->m_pingTime >= 0) && (!m_fastestPeer || (m_fastestPeer->m_pingTime > client->m_pingTime))) {
m_fastestPeer = client;
}
@@ -419,6 +407,21 @@ void P2PServer::update_peer_connections()
m_newP2PoolVersionDetected = newer_version_p2pool * 5 > total_outgoing_p2pool;
// Update max peer tip height (read safely from any thread via atomic)
{
uint64_t max_h = 0;
for (uint64_t h : peer_tip_heights) {
if (h > max_h) max_h = h;
}
m_maxPeerTipHeight.store(max_h);
}
// Check if we've diverged from peer consensus (e.g., after isolation)
// Run every 30 seconds to avoid unnecessary overhead
if ((m_timerCounter % 30) == 15) {
m_pool->side_chain().check_peer_consensus(peer_tip_heights);
}
std::vector<Peer> peer_list;
{
MutexLock lock(m_peerListLock);
@@ -1469,6 +1472,19 @@ void P2PServer::on_timer()
}
flush_cache();
// Periodically re-enable missing block search as a safety net
// Prevents permanent stalls if the flag was incorrectly cleared
if ((t % 15) == 0) {
m_lookForMissingBlocks = true;
}
// Periodically retry verifying unverified blocks (every 10 seconds)
// Blocks added without mainchain data sit unverified until this runs
if ((t % 5) == 3) {
m_pool->side_chain().retry_unverified_blocks();
}
download_missing_blocks();
update_peer_list();
save_peer_list_async();
@@ -1526,6 +1542,17 @@ void P2PServer::download_missing_blocks()
unordered_set<hash> missing_blocks;
m_pool->side_chain().get_missing_blocks(missing_blocks);
// Merge checkpoint-driven sync targets into missing blocks.
// Remove targets we've already received, add the rest to the request set.
for (auto it = m_syncTargetBlocks.begin(); it != m_syncTargetBlocks.end();) {
if (m_pool->side_chain().find_block(*it)) {
it = m_syncTargetBlocks.erase(it);
} else {
missing_blocks.insert(*it);
++it;
}
}
if (missing_blocks.empty()) {
m_lookForMissingBlocks = false;
m_missingBlockRequests.clear();
@@ -1533,6 +1560,18 @@ void P2PServer::download_missing_blocks()
return;
}
// Expire stale request entries older than 10 seconds so we can retry with different peers
{
const uint64_t now = seconds_since_epoch();
for (auto it = m_missingBlockRequests.begin(); it != m_missingBlockRequests.end();) {
if (now - it->second >= 10) {
it = m_missingBlockRequests.erase(it);
} else {
++it;
}
}
}
if (m_numConnections == 0) {
return;
}
@@ -1560,14 +1599,19 @@ void P2PServer::download_missing_blocks()
// Group blocks for batch requests to 1.6+ clients
std::map<P2PClient*, std::vector<hash>> batch_requests;
// Try to download each block from a random client
// Shuffle clients so we distribute requests across peers
for (size_t i = clients.size(); i > 1; --i) {
std::swap(clients[i - 1], clients[get_random64() % i]);
}
// Try to download each missing block, cycling through all peers until one accepts
for (const hash& id : missing_blocks) {
// Check cache first
if (m_cachedBlocks) {
auto it = m_cachedBlocks->find(id);
if (it != m_cachedBlocks->end()) {
LOGINFO(5, "using cached block for id = " << id);
P2PClient* client = clients[get_random64() % clients.size()];
P2PClient* client = clients[0];
if (!client->handle_incoming_block_async(it->second)) {
LOGERR(5, "download_missing_blocks: handle_incoming_block_async failed");
}
@@ -1575,84 +1619,76 @@ void P2PServer::download_missing_blocks()
}
}
// Prefer 1.6+ clients for batch requests
P2PClient* client;
if (!clients_v16.empty()) {
client = clients_v16[get_random64() % clients_v16.size()];
} else {
client = clients[get_random64() % clients.size()];
}
// Try each peer until one accepts the request
for (P2PClient* client : clients) {
if (client->m_blockPendingRequests.size() >= 25) {
continue;
}
if (client->m_blockPendingRequests.size() >= 25) {
// Too many pending requests to this peer
continue;
}
if (!m_missingBlockRequests.emplace(std::make_pair(client->m_peerId, *id.u64()), seconds_since_epoch()).second) {
// Already asked this peer for this block (and request hasn't expired yet)
continue;
}
if (!m_missingBlockRequests.emplace(client->m_peerId, *id.u64()).second) {
// We already asked this peer about this block
continue;
}
// Use batch requests for 1.6+ peers
if (client->m_protocolVersion >= PROTOCOL_VERSION_1_6) {
batch_requests[client].push_back(id);
if (batch_requests[client].size() >= BLOCK_BATCH_MAX_COUNT) {
auto& batch = batch_requests[client];
const bool result = send(client,
[&batch, client](uint8_t* buf, size_t buf_size) -> size_t
{
LOGINFO(5, "[download_missing_blocks] sending BLOCK_BATCH_REQUEST for " << batch.size() << " blocks to " << static_cast<char*>(client->m_addrString));
// Use batch requests for 1.6+ peers
if (client->m_protocolVersion >= PROTOCOL_VERSION_1_6) {
batch_requests[client].push_back(id);
// Limit batch size
if (batch_requests[client].size() >= BLOCK_BATCH_MAX_COUNT) {
// Send this batch now
auto& batch = batch_requests[client];
const size_t needed = 2 + batch.size() * HASH_SIZE;
if (buf_size < needed) {
return 0;
}
uint8_t* p = buf;
*(p++) = static_cast<uint8_t>(MessageId::BLOCK_BATCH_REQUEST);
*(p++) = static_cast<uint8_t>(batch.size());
for (const hash& h : batch) {
memcpy(p, h.h, HASH_SIZE);
p += HASH_SIZE;
}
return p - buf;
});
if (result) {
for (const hash& h : batch) {
client->m_blockPendingRequests.push_back(*h.u64());
}
}
batch.clear();
}
} else {
const bool result = send(client,
[&batch, client](uint8_t* buf, size_t buf_size) -> size_t
[&id, client](uint8_t* buf, size_t buf_size) -> size_t
{
LOGINFO(5, "[download_missing_blocks] sending BLOCK_BATCH_REQUEST for " << batch.size() << " blocks to " << static_cast<char*>(client->m_addrString));
LOGINFO(5, "[download_missing_blocks] sending BLOCK_REQUEST for id = " << id << " to " << static_cast<char*>(client->m_addrString));
const size_t needed = 2 + batch.size() * HASH_SIZE;
if (buf_size < needed) {
if (buf_size < 1 + HASH_SIZE) {
return 0;
}
uint8_t* p = buf;
*(p++) = static_cast<uint8_t>(MessageId::BLOCK_BATCH_REQUEST);
*(p++) = static_cast<uint8_t>(batch.size());
for (const hash& h : batch) {
memcpy(p, h.h, HASH_SIZE);
p += HASH_SIZE;
}
*(p++) = static_cast<uint8_t>(MessageId::BLOCK_REQUEST);
memcpy(p, id.h, HASH_SIZE);
p += HASH_SIZE;
return p - buf;
});
if (result) {
for (const hash& h : batch) {
client->m_blockPendingRequests.push_back(*h.u64());
}
client->m_blockPendingRequests.push_back(*id.u64());
}
batch.clear();
}
} else {
// Legacy single block request for older peers
const bool result = send(client,
[&id, client](uint8_t* buf, size_t buf_size) -> size_t
{
LOGINFO(5, "[download_missing_blocks] sending BLOCK_REQUEST for id = " << id << " to " << static_cast<char*>(client->m_addrString));
if (buf_size < 1 + HASH_SIZE) {
return 0;
}
uint8_t* p = buf;
*(p++) = static_cast<uint8_t>(MessageId::BLOCK_REQUEST);
memcpy(p, id.h, HASH_SIZE);
p += HASH_SIZE;
return p - buf;
});
if (result) {
client->m_blockPendingRequests.push_back(*id.u64());
}
break; // Found a peer for this block, move to the next block
}
}
@@ -3076,10 +3112,8 @@ bool P2PServer::P2PClient::on_block_response(const uint8_t* buf, uint32_t size,
return false;
}
const SideChain& side_chain = server->m_pool->side_chain();
const uint64_t max_time_delta = side_chain.precalcFinished() ? (side_chain.block_time() * side_chain.chain_window_size() * 4) : 0;
return handle_incoming_block_async(block, max_time_delta);
// No timestamp check on requested blocks — we asked for these
return handle_incoming_block_async(block, 0);
}
bool P2PServer::P2PClient::on_block_batch_request(const uint8_t* buf, uint32_t size)
@@ -3253,10 +3287,8 @@ bool P2PServer::P2PClient::on_block_batch_response(const uint8_t* buf, uint32_t
m_blockPendingRequests.pop_front();
}
const SideChain& side_chain = server->m_pool->side_chain();
const uint64_t max_time_delta = side_chain.precalcFinished() ? (side_chain.block_time() * side_chain.chain_window_size() * 4) : 0;
if (!handle_incoming_block_async(block, max_time_delta)) {
// No timestamp check on requested blocks — we asked for these
if (!handle_incoming_block_async(block, 0)) {
LOGWARN(4, "failed to handle block from batch response");
}
@@ -3313,7 +3345,7 @@ bool P2PServer::P2PClient::on_block_broadcast(const uint8_t* buf, uint32_t size,
}
else if (peer_height > our_height) {
if (peer_height >= our_height + 2) {
LOGWARN(3, "peer " << static_cast<char*>(m_addrString) << " is ahead on mainchain (height " << peer_height << ", your height " << our_height << "). Is your salviumd stuck or lagging?");
LOGWARN(3, "peer " << static_cast<char*>(m_addrString) << " is ahead on mainchain (height " << peer_height << ", your height " << our_height << "). Mainchain data may be stale — check ZMQ connection");
}
}
else {
@@ -3483,6 +3515,31 @@ void P2PServer::P2PClient::on_peer_list_response(const uint8_t* buf)
);
}
// Reject peers running protocol versions that produce incompatible block serialization
if (m_protocolVersion < MIN_SUPPORTED_PROTOCOL_VERSION) {
LOGWARN(3, "peer " << static_cast<char*>(m_addrString)
<< " protocol version " << (m_protocolVersion >> 16) << '.' << (m_protocolVersion & 0xFFFF)
<< " is below minimum required " << (MIN_SUPPORTED_PROTOCOL_VERSION >> 16) << '.' << (MIN_SUPPORTED_PROTOCOL_VERSION & 0xFFFF)
<< " — disconnecting to prevent sidechain corruption");
ban(DEFAULT_BAN_TIME);
server->remove_peer_from_list(this);
return;
}
// At HF11+, reject peers running software older than v4.18
// (they lack variable asset_type support and would corrupt sidechain state)
if (m_SoftwareVersion && m_SoftwareVersion < MIN_VERSION_HF11) {
if (server->m_pool->miner_data().major_version >= 11) {
LOGWARN(3, "peer " << static_cast<char*>(m_addrString)
<< " software version "
<< (m_SoftwareVersion >> 16) << '.' << ((m_SoftwareVersion >> 8) & 0xFF) << '.' << (m_SoftwareVersion & 0xFF)
<< " is too old for HF11 — disconnecting");
ban(DEFAULT_BAN_TIME);
server->remove_peer_from_list(this);
return;
}
}
// We know this peer's protocol version now. Send protocol-specific messages here.
if ((m_protocolVersion >= PROTOCOL_VERSION_1_3) && !server->m_auxJobLastMessage.empty()) {
if (static_cast<uint64_t>(time(nullptr)) < server->m_auxJobLastMessageTimestamp + AUX_JOB_TIMEOUT) {
@@ -3554,7 +3611,7 @@ void P2PServer::P2PClient::on_block_notify(const uint8_t* buf)
return;
}
if (!server->m_blockNotifyRequests.insert(*id.u64()).second || !server->m_missingBlockRequests.emplace(m_peerId, *id.u64()).second) {
if (!server->m_blockNotifyRequests.insert(*id.u64()).second || !server->m_missingBlockRequests.emplace(std::make_pair(m_peerId, *id.u64()), seconds_since_epoch()).second) {
LOGINFO(6, "BLOCK_REQUEST for id = " << id << " was already sent");
return;
}
@@ -3801,6 +3858,25 @@ bool P2PServer::P2PClient::on_checkpoint_response(const uint8_t* buf, uint32_t c
<< static_cast<char*>(m_addrString));
}
// Use checkpoint data for sync bootstrapping.
// Store checkpoint block hashes we don't have yet — they serve as entry points
// for the backward-walking sync, giving us multiple starting points instead of
// depending solely on the chain tip request.
if (!side_chain.is_ready_to_mine()) {
uint32_t new_targets = 0;
for (const Checkpoint& cp : peer_checkpoints) {
if (!side_chain.find_block(cp.id)) {
if (server->m_syncTargetBlocks.insert(cp.id).second) {
++new_targets;
}
}
}
if (new_targets > 0) {
server->m_lookForMissingBlocks = true;
LOGINFO(3, "Checkpoint sync: added " << new_targets << " checkpoint blocks as sync targets (total " << server->m_syncTargetBlocks.size() << ")");
}
}
return true;
}
@@ -3970,7 +4046,7 @@ bool P2PServer::P2PClient::on_monero_block_broadcast(const uint8_t* buf, uint32_
return false;
}
if (buf[data.header_size] != TX_VERSION) {
if (buf[data.header_size] != TX_VERSION && buf[data.header_size] != TX_VERSION_ENABLE_TOKENS) {
LOGWARN(3, "Invalid MONERO_BLOCK_BROADCAST: TX_VERSION byte not found");
return false;
}
@@ -4311,6 +4387,14 @@ bool P2PServer::P2PClient::handle_incoming_block_async(const PoolBlock* block, u
void P2PServer::P2PClient::handle_incoming_block(p2pool* pool, PoolBlock& block, std::vector<hash>& missing_blocks, bool& result)
{
result = pool->side_chain().add_external_block(block, missing_blocks);
// If block wasn't actually added to the sidechain (PoW failure, too_low_diff, etc.),
// clear incoming tracking so re-download attempts aren't silently dropped.
// Without this, a block that fails processing stays "seen" for 10 minutes,
// and all BLOCK_REQUEST responses for it are silently discarded.
if (!pool->side_chain().find_block(block.m_sidechainId)) {
pool->side_chain().forget_incoming_block(block);
}
}
void P2PServer::P2PClient::post_handle_incoming_block(p2pool* pool, const PoolBlock& block, const uint32_t reset_counter, bool is_v6, const raw_ip& addr, std::vector<hash>& missing_blocks, const bool result)
@@ -4376,7 +4460,7 @@ void P2PServer::P2PClient::post_handle_incoming_block(p2pool* pool, const PoolBl
}
}
if (!server->m_missingBlockRequests.emplace(m_peerId, *id.u64()).second) {
if (!server->m_missingBlockRequests.emplace(std::make_pair(m_peerId, *id.u64()), seconds_since_epoch()).second) {
continue;
}
+16 -2
View File
@@ -48,6 +48,13 @@ static constexpr uint32_t PROTOCOL_VERSION_1_6 = 0x00010006UL;
static constexpr uint32_t SUPPORTED_PROTOCOL_VERSION = PROTOCOL_VERSION_1_6;
// Minimum protocol version required to connect. Peers below this are disconnected
// during handshake. v1.6 is required for correct Carrot v1 block serialization.
static constexpr uint32_t MIN_SUPPORTED_PROTOCOL_VERSION = PROTOCOL_VERSION_1_6;
// Minimum software version required at HF11+ (v4.18 adds variable asset_type support)
static constexpr uint32_t MIN_VERSION_HF11 = (4 << 16) | (18 << 8);
// Batch block request limits
static constexpr uint8_t BLOCK_BATCH_MAX_COUNT = 10;
@@ -209,6 +216,8 @@ public:
};
void broadcast(const PoolBlock& block, const PoolBlock* parent);
void set_look_for_missing_blocks() { m_lookForMissingBlocks = true; }
[[nodiscard]] uint64_t max_peer_tip_height() const { return m_maxPeerTipHeight.load(); }
[[nodiscard]] uint64_t get_random64();
[[nodiscard]] uint64_t get_peerId(bool is_tor) const { return is_tor ? m_peerId_TOR : m_peerId; }
@@ -315,11 +324,16 @@ private:
uv_async_t m_broadcastAsync;
std::vector<Broadcast*> m_broadcastQueue;
bool m_lookForMissingBlocks;
unordered_set<std::pair<uint64_t, uint64_t>> m_missingBlockRequests;
std::atomic<bool> m_lookForMissingBlocks;
unordered_map<std::pair<uint64_t, uint64_t>, uint64_t> m_missingBlockRequests;
unordered_set<uint64_t> m_blockNotifyRequests;
// Checkpoint-driven sync: block hashes from peer checkpoint responses
// Used as additional sync entry points alongside get_missing_blocks results
unordered_set<hash> m_syncTargetBlocks;
P2PClient* m_fastestPeer;
std::atomic<uint64_t> m_maxPeerTipHeight;
std::atomic<bool> m_newP2PoolVersionDetected;
static void on_broadcast(uv_async_t* handle) { reinterpret_cast<P2PServer*>(handle->data)->on_broadcast(); }
+72 -51
View File
@@ -68,7 +68,7 @@ constexpr char FOUND_BLOCKS_FILE[] = "p2pool.blocks";
namespace p2pool {
static uint64_t BLOCK_HEADERS_REQUIRED = 720;
static uint64_t BLOCK_HEADERS_REQUIRED = 2000;
p2pool::p2pool(const Params& params)
: m_stopped(false)
@@ -91,9 +91,9 @@ p2pool::p2pool(const Params& params)
LOGINFO(1, "Connected to Redis at " << m_params.m_redisHost << ":" << m_params.m_redisPort << " (db " << m_params.m_redisDb << ")");
#endif
// P2Pool-nano requires more Monero blocks for the initial sync
// P2Pool-nano requires more blocks for the initial sync
if (m_params.m_nano) {
BLOCK_HEADERS_REQUIRED = 1440;
BLOCK_HEADERS_REQUIRED = 4000;
}
bkg_jobs_tracker = new BackgroundJobTracker();
@@ -564,20 +564,20 @@ void p2pool::get_missing_heights()
{
ChainMain block;
if (!parse_block_header(data, size, block)) {
LOGERR(1, "couldn't download block header for height " << h);
MutexLock lock(m_missingHeightsLock);
m_missingHeights.push_back(h);
LOGWARN(3, "couldn't download block header for height " << h);
// Don't re-queue: retry_unverified_blocks re-detects missing heights
// every 5s from the P2P timer. Re-queuing here caused an infinite
// tight loop that overwhelmed the daemon with requests.
}
else {
m_sideChain->retry_unverified_blocks();
}
get_missing_heights();
},
[this, h](const char* data, size_t size, double)
{
if (size > 0) {
LOGERR(1, "couldn't download block header for height " << h << ", error " << log::const_buf(data, size));
MutexLock lock(m_missingHeightsLock);
m_missingHeights.push_back(h);
LOGWARN(3, "couldn't download block header for height " << h << ", error " << log::const_buf(data, size));
}
get_missing_heights();
});
@@ -1401,7 +1401,7 @@ void p2pool::download_block_headers2(uint64_t current_height)
void p2pool::download_block_headers3(uint64_t start_height, uint64_t current_height)
{
// Workaround the restricted RPC limit
constexpr uint64_t RESTRICTED_BLOCK_HEADER_RANGE = 1000;
constexpr uint64_t RESTRICTED_BLOCK_HEADER_RANGE = 500;
if (current_height - start_height > RESTRICTED_BLOCK_HEADER_RANGE + 1) {
char buf[log::Stream::BUF_SIZE + 1] = {};
@@ -1459,35 +1459,11 @@ void p2pool::download_block_headers4(uint64_t start_height, uint64_t current_hei
LOGINFO(0, log::LightCyan() << "########################################################");
m_p2pServer.store(new P2PServer(this));
m_sideChain->set_p2p_start_time(seconds_since_epoch());
// Wait for sidechain to be ready before starting stratum/mining
int genesis_wait_count = 0;
bool genesis_escape = false;
while (!m_sideChain->is_ready_to_mine()) {
std::this_thread::sleep_for(std::chrono::seconds(5));
genesis_wait_count++;
// Genesis node escape: if we created genesis and have no peers, proceed
const P2PServer* server = m_p2pServer.load();
if (server && server->num_connections() == 0 && genesis_wait_count >= 24) {
LOGINFO(0, log::LightGreen() << "########################################");
LOGINFO(0, log::LightGreen() << "GENESIS NODE - MINING IS NOW ENABLED");
LOGINFO(0, log::LightGreen() << "########################################");
// Clear any stale checkpoints from a previous run so they
// don't block mining with an unsatisfiable validation loop
m_sideChain->clear_checkpoints();
m_sideChain->set_ready_to_mine(true);
genesis_escape = true;
break;
}
}
if (!genesis_escape) {
LOGINFO(0, log::LightGreen() << "########################################");
LOGINFO(0, log::LightGreen() << "SIDECHAIN LOADED - MINING IS NOW ENABLED");
LOGINFO(0, log::LightGreen() << "########################################");
}
// Start all services immediately — mining is gated by
// m_readyToMine/preflight_check(), which runs on every
// chain tip update and prune cycle. No spin loop needed.
m_stratumServer = new StratumServer(this);
#if defined(WITH_RANDOMX) && !defined(P2POOL_UNIT_TESTS)
if (m_params.m_minerThreads) {
@@ -1736,7 +1712,9 @@ void p2pool::parse_get_info_rpc(const char* data, size_t size)
if (doc.HasParseError() || !doc.IsObject() || !doc.HasMember("result")) {
LOGWARN(1, "get_info RPC response is invalid (\"result\" not found), trying again in 1 second");
if (m_stopped) return;
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
if (m_stopped) return;
switch_host();
get_info();
return;
@@ -1754,7 +1732,9 @@ void p2pool::parse_get_info_rpc(const char* data, size_t size)
!PARSE(result, info, testnet) ||
!PARSE(result, info, stagenet)) {
LOGWARN(1, "get_info RPC response is invalid, trying again in 1 second");
if (m_stopped) return;
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
if (m_stopped) return;
switch_host();
get_info();
return;
@@ -1762,7 +1742,9 @@ void p2pool::parse_get_info_rpc(const char* data, size_t size)
if (info.busy_syncing || !info.synchronized) {
LOGINFO(1, "salviumd is " << (info.busy_syncing ? "busy syncing" : "not synchronized") << ", trying again in 1 second");
if (m_stopped) return;
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
if (m_stopped) return;
switch_host();
get_info();
return;
@@ -1816,7 +1798,9 @@ void p2pool::parse_get_version_rpc(const char* data, size_t size)
if (doc.HasParseError() || !doc.IsObject() || !doc.HasMember("result")) {
LOGWARN(1, "get_version RPC response is invalid (\"result\" not found), trying again in 1 second");
if (m_stopped) return;
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
if (m_stopped) return;
get_version();
return;
}
@@ -1828,14 +1812,18 @@ void p2pool::parse_get_version_rpc(const char* data, size_t size)
if (!parseValue(result, "status", status) || !parseValue(result, "version", version)) {
LOGWARN(1, "get_version RPC response is invalid, trying again in 1 second");
if (m_stopped) return;
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
if (m_stopped) return;
get_version();
return;
}
if (status != "OK") {
LOGWARN(1, "get_version RPC failed, trying again in 1 second");
if (m_stopped) return;
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
if (m_stopped) return;
get_version();
return;
}
@@ -1955,7 +1943,7 @@ void p2pool::parse_get_miner_data_rpc(const char* data, size_t size)
}
// Extract protocol outputs (treasury, etc.) from the daemon's miner_tx in a blocktemplate_blob.
// The miner's own reward output is identified by: amount == expected_reward - expected_reward/5
// The miner's own reward output is identified by amount matching the expected miner share (80% at HF10, 60% at HF11+)
// All other outputs are "protocol outputs" (treasury, etc.) to be boomeranged into p2pool's miner_tx.
static bool extract_protocol_outputs_from_blob(const uint8_t* data, size_t size, uint64_t expected_reward, std::vector<ProtocolOutput>& protocol_outputs_out)
{
@@ -1977,7 +1965,8 @@ static bool extract_protocol_outputs_from_blob(const uint8_t* data, size_t size,
// Parse miner_tx
// version, unlock_time
if (!read_varint_fn(dummy) || !read_varint_fn(dummy)) return false;
uint64_t tx_version;
if (!read_varint_fn(tx_version) || !read_varint_fn(dummy)) return false;
// vin
uint64_t vin_count;
if (!read_varint_fn(vin_count)) return false;
@@ -1996,9 +1985,19 @@ static bool extract_protocol_outputs_from_blob(const uint8_t* data, size_t size,
if (!read_varint_fn(vout_count)) return false;
if (vout_count == 0) return true; // no outputs at all, nothing to extract
// Compute what the miner's output amount should be:
// miner_amount = expected_reward - expected_reward/5 (80% of full reward)
const uint64_t miner_amount = (expected_reward > 0) ? (expected_reward - expected_reward / 5) : 0;
// Compute what the miner's output amount should be
uint64_t miner_amount = 0;
if (expected_reward > 0) {
if (tx_version >= TX_VERSION_ENABLE_TOKENS) {
// HF11: 60% miner + 25% treasury + 15% staker
const uint64_t treasury_reward = expected_reward * 25 / 100;
const uint64_t staker_reward = (expected_reward - treasury_reward) * 20 / 100;
miner_amount = expected_reward - treasury_reward - staker_reward;
} else {
// HF10: 80% miner + 20% staker
miner_amount = expected_reward - expected_reward / 5;
}
}
// Read all outputs, store them temporarily
struct TmpOutput {
@@ -2006,6 +2005,7 @@ static bool extract_protocol_outputs_from_blob(const uint8_t* data, size_t size,
hash K_o;
uint8_t view_tag[3];
uint8_t anchor_enc[16];
std::string asset_type;
};
std::vector<TmpOutput> tmp_outputs;
tmp_outputs.reserve(static_cast<size_t>(vout_count));
@@ -2023,11 +2023,13 @@ static bool extract_protocol_outputs_from_blob(const uint8_t* data, size_t size,
memcpy(t.K_o.h, data, HASH_SIZE);
data += HASH_SIZE;
// asset_type: length byte + "SAL1"
// asset_type: varint length + asset string
uint64_t asset_len;
if (!read_varint_fn(asset_len)) return false;
if (asset_len != 4) return false;
if (!skip_bytes_fn(4)) return false; // "SAL1"
if (asset_len == 0 || asset_len > 32) return false;
if (static_cast<size_t>(blob_end - data) < asset_len) return false;
t.asset_type.assign(reinterpret_cast<const char*>(data), static_cast<size_t>(asset_len));
data += asset_len;
// view_tag (3 bytes)
if (static_cast<size_t>(blob_end - data) < 3) return false;
@@ -2101,6 +2103,7 @@ static bool extract_protocol_outputs_from_blob(const uint8_t* data, size_t size,
po.amount = t.amount;
memcpy(po.view_tag, t.view_tag, 3);
memcpy(po.anchor_enc, t.anchor_enc, 16);
po.asset_type = t.asset_type;
// Get D_e from tx_extra at matching index
if (i < de_values.size()) {
po.eph_pubkey = de_values[i];
@@ -2223,13 +2226,13 @@ static bool extract_protocol_tx_from_blob(const uint8_t* data, size_t size, std:
// Conditional fields based on type
// type 0 = UNSET, 1 = MINER, 2 = PROTOCOL
if (tx_type != 0 && tx_type != 2) {
if (tx_type != TX_TYPE_UNSET && tx_type != TX_TYPE_PROTOCOL) {
// amount_burnt
if (!skip_varint()) return false;
if (tx_type != 1) {
if (tx_type != TX_TYPE_MINER) {
// For TRANSFER, STAKE, etc: additional fields
// return_address/return_address_list, return_pubkey, source_asset, dest_asset, slippage
// This is complex - but for getblocktemplate, miner_tx is always type MINER(1)
// This is complex - but for getblocktemplate, miner_tx is always type MINER
// so we should never reach here
return false;
}
@@ -2624,6 +2627,24 @@ void p2pool::fetch_mainchain_block(uint64_t height)
);
}
void p2pool::queue_missing_mainchain_heights(std::vector<uint64_t>&& heights)
{
MutexLock lock(m_missingHeightsLock);
std::vector<uint64_t> moved_heights = std::move(heights);
for (const uint64_t h : moved_heights) {
// Deduplicate
if (std::find(m_missingHeights.begin(), m_missingHeights.end(), h) == m_missingHeights.end()) {
m_missingHeights.push_back(h);
}
}
if (!m_missingHeights.empty()) {
const int err = uv_async_send(&m_getMissingHeightsAsync);
if (err) {
LOGERR(1, "uv_async_send failed, error " << uv_err_name(err));
}
}
}
void p2pool::api_update_network_stats()
{
if (!m_api || m_stopped) {
+1
View File
@@ -121,6 +121,7 @@ public:
void download_block_headers4(uint64_t start_height, uint64_t current_height);
void prefetch_mainchain_blocks(uint64_t current_height);
void fetch_mainchain_block(uint64_t height);
void queue_missing_mainchain_heights(std::vector<uint64_t>&& heights);
bool chainmain_get_by_hash(const hash& id, ChainMain& data) const;
+58 -42
View File
@@ -169,7 +169,7 @@ std::vector<uint8_t> PoolBlock::serialize_mainchain_data(size_t* header_size, si
}
// Miner tx
data.push_back(TX_VERSION);
data.push_back(m_majorVersion >= 11 ? TX_VERSION_ENABLE_TOKENS : TX_VERSION);
writeVarint(MINER_REWARD_UNLOCK_TIME, data);
data.push_back(1);
data.push_back(TXIN_GEN);
@@ -187,6 +187,7 @@ std::vector<uint8_t> PoolBlock::serialize_mainchain_data(size_t* header_size, si
uint64_t amount;
uint8_t view_tag[3];
uint8_t anchor[16];
std::string asset_type;
};
std::vector<MergedOutput> merged;
merged.reserve(m_outputAmounts.size() + m_protocolOutputs.size());
@@ -205,18 +206,37 @@ std::vector<uint8_t> PoolBlock::serialize_mainchain_data(size_t* header_size, si
memcpy(mo.anchor, m_encryptedAnchors[i].data(), 16);
else
memset(mo.anchor, 0, 16);
mo.asset_type = "SAL1";
merged.push_back(mo);
}
// Protocol outputs
for (const auto& po : m_protocolOutputs) {
MergedOutput mo;
mo.K_o = po.onetime_address;
mo.D_e = po.eph_pubkey;
mo.amount = po.amount;
memcpy(mo.view_tag, po.view_tag, 3);
memcpy(mo.anchor, po.anchor_enc, 16);
merged.push_back(mo);
// Protocol outputs — only add if not already included in m_outputAmounts.
// After parsing from the wire, m_outputAmounts contains ALL outputs (miner + protocol
// interleaved), while m_protocolOutputs is populated separately from sidechain data.
// Blindly merging both would double the protocol outputs and corrupt the blob.
// Detect this by checking if the first protocol output's K_o already appears in m_ephPublicKeys.
bool protocol_already_in_outputs = false;
if (!m_protocolOutputs.empty()) {
const hash& first_po_Ko = m_protocolOutputs[0].onetime_address;
for (size_t i = 0, n = m_ephPublicKeys.size(); i < n; ++i) {
if (m_ephPublicKeys[i] == first_po_Ko) {
protocol_already_in_outputs = true;
break;
}
}
}
if (!protocol_already_in_outputs) {
for (const auto& po : m_protocolOutputs) {
MergedOutput mo;
mo.K_o = po.onetime_address;
mo.D_e = po.eph_pubkey;
mo.amount = po.amount;
memcpy(mo.view_tag, po.view_tag, 3);
memcpy(mo.anchor, po.anchor_enc, 16);
mo.asset_type = po.asset_type;
merged.push_back(mo);
}
}
// Sort all outputs by K_o
@@ -232,11 +252,8 @@ std::vector<uint8_t> PoolBlock::serialize_mainchain_data(size_t* header_size, si
writeVarint(mo.amount, data);
data.push_back(TXOUT_TO_CARROT_V1);
data.insert(data.end(), mo.K_o.h, mo.K_o.h + HASH_SIZE);
data.push_back(4);
data.push_back('S');
data.push_back('A');
data.push_back('L');
data.push_back('1');
writeVarint(mo.asset_type.size(), data);
data.insert(data.end(), mo.asset_type.begin(), mo.asset_type.end());
data.insert(data.end(), mo.view_tag, mo.view_tag + 3);
data.insert(data.end(), mo.anchor, mo.anchor + 16);
}
@@ -296,7 +313,7 @@ std::vector<uint8_t> PoolBlock::serialize_mainchain_data(size_t* header_size, si
// For Carrot v1+ (major_version >= 10), add type and amount_burnt instead of vin_rct_type
if (m_majorVersion >= 10) {
// type = MINER
writeVarint(1, data);
writeVarint(TX_TYPE_MINER, data);
writeVarint(m_amountBurnt, data);
@@ -312,25 +329,12 @@ std::vector<uint8_t> PoolBlock::serialize_mainchain_data(size_t* header_size, si
*miner_tx_size = data.size() - header_size0;
}
// Protocol tx (Salvium Carrot v1+)
// Protocol tx (Salvium Carrot v1+) — must always have the real blob
if (m_majorVersion >= 10) {
if (m_sidechainHeight >= SIDECHAIN_PROTOCOL_TX_HARDFORK_HEIGHT && !m_protocolTxBlob.empty()) {
// Real protocol_tx from daemon
data.insert(data.end(), m_protocolTxBlob.begin(), m_protocolTxBlob.end());
} else {
// Stub protocol_tx (pre-hardfork or blob not yet available)
writeVarint(4, data); // version = TRANSACTION_VERSION_CARROT
writeVarint(60, data); // unlock_time = 60
writeVarint(1, data); // vin.size() = 1
data.push_back(TXIN_GEN);
writeVarint(m_txinGenHeight, data);
writeVarint(0, data); // vout.size() = 0
writeVarint(2, data); // extra.size() = 2
data.push_back(0x02);
data.push_back(0x00);
writeVarint(2, data); // transaction_type::PROTOCOL = 2
data.push_back(0); // RCT
if (m_protocolTxBlob.empty()) {
LOGERR(1, "serialize_mainchain_data: m_protocolTxBlob is empty for Carrot v1 block at height " << m_sidechainHeight << " — this will break merkle proof verification");
}
data.insert(data.end(), m_protocolTxBlob.begin(), m_protocolTxBlob.end());
}
if (include_tx_hashes) {
writeVarint(m_transactions.size() - 1, data);
@@ -360,7 +364,7 @@ std::vector<uint8_t> PoolBlock::serialize_mainchain_data(size_t* header_size, si
return data;
}
std::vector<uint8_t> PoolBlock::serialize_sidechain_data() const
std::vector<uint8_t> PoolBlock::serialize_sidechain_data(size_t* extra_nonce_offset) const
{
std::vector<uint8_t> data;
@@ -408,6 +412,11 @@ std::vector<uint8_t> PoolBlock::serialize_sidechain_data() const
const uint8_t* p = reinterpret_cast<const uint8_t*>(m_sidechainExtraBuf);
data.insert(data.end(), p, p + sizeof(m_sidechainExtraBuf));
// The sidechain extra nonce is m_sidechainExtraBuf[3], the last 4 bytes of the 16-byte buffer
if (extra_nonce_offset) {
*extra_nonce_offset = data.size() - EXTRA_NONCE_SIZE;
}
// Append protocol outputs (new format, appended after sidechainExtraBuf)
if (!m_protocolOutputs.empty()) {
writeVarint(m_protocolOutputs.size(), data);
@@ -417,6 +426,10 @@ std::vector<uint8_t> PoolBlock::serialize_sidechain_data() const
writeVarint(po.amount, data);
data.insert(data.end(), po.view_tag, po.view_tag + 3);
data.insert(data.end(), po.anchor_enc, po.anchor_enc + 16);
if (m_majorVersion >= 11) {
writeVarint(po.asset_type.size(), data);
data.insert(data.end(), po.asset_type.begin(), po.asset_type.end());
}
}
}
@@ -484,18 +497,21 @@ bool PoolBlock::get_pow_hash(RandomX_Hasher_Base* hasher, uint64_t height, const
size_t blob_size = 0;
{
// For Carrot v1 blocks, ensure m_transactions has space for protocol TX before serializing
// For Carrot v1 blocks, ensure m_transactions has space for protocol TX.
// IMPORTANT: Don't override m_transactions[1] if it was already parsed from block data.
// The original hash must be preserved for PoW verification — the block creator may
// have used a different protocol TX hash (stub vs real blob).
if (m_majorVersion >= 10) {
if (m_transactions.size() < 2) {
m_transactions.resize(2);
hash protocol_tx_hash;
if (!m_protocolTxBlob.empty()) {
calculate_protocol_tx_hash_from_blob(m_protocolTxBlob, protocol_tx_hash);
} else {
calculate_protocol_tx_hash(m_txinGenHeight, protocol_tx_hash);
}
m_transactions[1] = static_cast<indexed_hash>(protocol_tx_hash);
}
hash protocol_tx_hash;
if (m_sidechainHeight >= SIDECHAIN_PROTOCOL_TX_HARDFORK_HEIGHT && !m_protocolTxBlob.empty()) {
calculate_protocol_tx_hash_from_blob(m_protocolTxBlob, protocol_tx_hash);
} else {
calculate_protocol_tx_hash(m_txinGenHeight, protocol_tx_hash);
}
m_transactions[1] = static_cast<indexed_hash>(protocol_tx_hash);
}
size_t header_size, miner_tx_size;
const std::vector<uint8_t> mainchain_data = serialize_mainchain_data(&header_size, &miner_tx_size, nullptr, nullptr, nullptr, nullptr);
+3 -2
View File
@@ -69,7 +69,8 @@ static constexpr difficulty_type MAX_CUMULATIVE_DIFFICULTY{ 13019633956666736640
// 1000 years at 1 block/second. It should be enough for any normal use.
static constexpr uint64_t MAX_SIDECHAIN_HEIGHT = 31556952000ULL;
// Sidechain height at which real protocol_tx data replaces the stub in mainchain data
// Legacy: sidechain height at which real protocol_tx data replaced the stub.
// No longer used as a gate — protocol TX is always required from the daemon.
static constexpr uint64_t SIDECHAIN_PROTOCOL_TX_HARDFORK_HEIGHT = 800001ULL;
// Limited by the format of the Merkle tree parameters in tx_extra
@@ -205,7 +206,7 @@ struct PoolBlock
std::vector<ProtocolOutput> m_protocolOutputs;
std::vector<uint8_t> serialize_mainchain_data(size_t* header_size = nullptr, size_t* miner_tx_size = nullptr, int* outputs_offset = nullptr, int* outputs_blob_size = nullptr, const uint32_t* nonce = nullptr, const uint32_t* extra_nonce = nullptr, bool include_tx_hashes = true) const;
std::vector<uint8_t> serialize_sidechain_data() const;
std::vector<uint8_t> serialize_sidechain_data(size_t* extra_nonce_offset = nullptr) const;
[[nodiscard]] int deserialize(const uint8_t* data, size_t size, const SideChain& sidechain, uv_loop_t* loop, bool compact);
void reset_offchain_data();
+48 -31
View File
@@ -75,10 +75,10 @@ int PoolBlock::deserialize(const uint8_t* data, size_t size, const SideChain& si
const int nonce_offset = static_cast<int>(data - data_begin);
READ_BUF(&m_nonce, NONCE_SIZE);
// Verify TX_VERSION (currently 4)
// Verify TX_VERSION (4 for HF10, 5 for HF11+)
uint8_t tx_version;
READ_BYTE(tx_version);
if (tx_version != TX_VERSION) return __LINE__;
if (tx_version != TX_VERSION && tx_version != TX_VERSION_ENABLE_TOKENS) return __LINE__;
uint64_t unlock_height;
READ_VARINT(unlock_height);
@@ -100,9 +100,9 @@ int PoolBlock::deserialize(const uint8_t* data, size_t size, const SideChain& si
int outputs_blob_size;
if (num_outputs > 0) {
// Carrot v1 outputs: each output is at least 89 bytes
// 1 byte amount, 1 byte tx_type(0x04), 32 bytes K_o, 4 bytes SAL1,
// 3 bytes view_tag, 16 bytes anchor, 32 bytes D_e
// Carrot v1 outputs: each output is at least 58 bytes
// 1 byte amount, 1 byte tx_type(0x04), 32 bytes K_o, 1+ bytes asset_type (varint len + string),
// 3 bytes view_tag, 16 bytes anchor (D_e is in tx_extra, not per-output)
constexpr uint64_t MIN_OUTPUT_SIZE = 58;
if (num_outputs > std::numeric_limits<uint64_t>::max() / MIN_OUTPUT_SIZE) return __LINE__;
@@ -136,17 +136,16 @@ int PoolBlock::deserialize(const uint8_t* data, size_t size, const SideChain& si
hash onetime_address;
READ_BUF(onetime_address.h, HASH_SIZE);
// asset_type string: length byte + "SAL1"
uint8_t asset_len;
READ_BYTE(asset_len);
if (asset_len != 4) {
// asset_type string: varint length + asset bytes
uint64_t asset_len;
READ_VARINT(asset_len);
if (asset_len == 0 || asset_len > 32) {
return __LINE__;
}
uint8_t sal1_marker[4];
READ_BUF(sal1_marker, 4);
if (memcmp(sal1_marker, "SAL1", 4) != 0) {
if (static_cast<uint64_t>(data_end - data) < asset_len) {
return __LINE__;
}
data += asset_len;
// 3-byte view tag
uint8_t view_tag_bytes[3];
@@ -295,13 +294,13 @@ int PoolBlock::deserialize(const uint8_t* data, size_t size, const SideChain& si
uint64_t first_val;
const uint8_t* peek1 = readVarint(data, data_end, first_val);
// Check if this is protocol_tx by looking for signature: version(4) + unlock_time(60)
// This avoids false positive when num_transactions = 4
if (peek1 && first_val == 4) {
// Check if this is protocol_tx by looking for signature: version(4 or 5) + unlock_time(60)
// This avoids false positive when num_transactions = 4 or 5
if (peek1 && (first_val == 4 || first_val == 5)) {
uint64_t second_val;
const uint8_t* peek2 = readVarint(peek1, data_end, second_val);
// Only parse as protocol_tx if we see version=4 AND unlock_time=60
// Only parse as protocol_tx if we see version=4 or 5 AND unlock_time=60
if (peek2 && second_val == 60) {
// This is a protocol_tx, parse it
data = peek1; // Consume version varint
@@ -344,7 +343,12 @@ int PoolBlock::deserialize(const uint8_t* data, size_t size, const SideChain& si
if (!ptx_skip(static_cast<size_t>(asset_type_len))) { data = saved_pos; goto skip_protocol_tx; }
// Type-specific additional fields
if (ptx_out_type == 3) {
if (ptx_out_type == 2) {
// txout_to_key: unlock_time(varint)
uint64_t ptx_unlock;
data = readVarint(data, data_end, ptx_unlock);
if (!data) { data = saved_pos; goto skip_protocol_tx; }
} else if (ptx_out_type == 3) {
// txout_to_tagged_key: unlock_time(varint) + view_tag(1)
uint64_t ptx_unlock;
data = readVarint(data, data_end, ptx_unlock);
@@ -353,11 +357,10 @@ int PoolBlock::deserialize(const uint8_t* data, size_t size, const SideChain& si
} else if (ptx_out_type == 4) {
// txout_to_carrot_v1: view_tag(3) + encrypted_janus_anchor(16)
if (!ptx_skip(19)) { data = saved_pos; goto skip_protocol_tx; }
} else if (ptx_out_type != 2) {
} else {
// Unknown output type
data = saved_pos; goto skip_protocol_tx;
}
// type 2 (txout_to_key): nothing more to skip
}
// extra (vector<uint8_t>)
@@ -376,10 +379,10 @@ int PoolBlock::deserialize(const uint8_t* data, size_t size, const SideChain& si
if (!read_byte(rct)) { data = saved_pos; goto skip_protocol_tx; }
if (rct != 0) { data = saved_pos; goto skip_protocol_tx; }
// Capture the real protocol_tx blob for height >= hardfork
if (m_sidechainHeight >= SIDECHAIN_PROTOCOL_TX_HARDFORK_HEIGHT) {
m_protocolTxBlob.assign(saved_pos, data);
}
// Always capture the real protocol_tx blob for round-trip fidelity.
// Note: m_sidechainHeight is not yet parsed at this point (it's in sidechain data),
// so we cannot gate on it here. The serializer will use this blob if available.
m_protocolTxBlob.assign(saved_pos, data);
}
}
}
@@ -603,6 +606,16 @@ skip_protocol_tx:
READ_VARINT(po.amount);
READ_BUF(po.view_tag, 3);
READ_BUF(po.anchor_enc, 16);
if (m_majorVersion >= 11) {
uint64_t asset_len;
READ_VARINT(asset_len);
if (asset_len == 0 || asset_len > 32) return __LINE__;
if (static_cast<uint64_t>(data_end - data) < asset_len) return __LINE__;
po.asset_type.assign(reinterpret_cast<const char*>(data), static_cast<size_t>(asset_len));
data += asset_len;
} else {
po.asset_type = "SAL1";
}
m_protocolOutputs.push_back(po);
}
}
@@ -719,18 +732,22 @@ skip_protocol_tx:
return __LINE__;
}
// For Carrot v1 blocks, ensure protocol TX hash is populated
// For Carrot v1 blocks, ensure protocol TX hash is populated.
// IMPORTANT: Only fill in the hash if it wasn't already parsed from the block data.
// Overriding the original hash breaks PoW verification when the block creator used
// a different hash method (e.g., stub hash vs real blob hash).
if (m_majorVersion >= 10) {
if (m_transactions.size() < 2) {
m_transactions.resize(2);
// Only compute if the slot was just created (no hash from block data)
hash protocol_tx_hash;
if (!m_protocolTxBlob.empty()) {
calculate_protocol_tx_hash_from_blob(m_protocolTxBlob, protocol_tx_hash);
} else {
calculate_protocol_tx_hash(m_txinGenHeight, protocol_tx_hash);
}
m_transactions[1] = static_cast<indexed_hash>(protocol_tx_hash);
}
hash protocol_tx_hash;
if (m_sidechainHeight >= SIDECHAIN_PROTOCOL_TX_HARDFORK_HEIGHT && !m_protocolTxBlob.empty()) {
calculate_protocol_tx_hash_from_blob(m_protocolTxBlob, protocol_tx_hash);
} else {
calculate_protocol_tx_hash(m_txinGenHeight, protocol_tx_hash);
}
m_transactions[1] = static_cast<indexed_hash>(protocol_tx_hash);
}
// Post-process: remove protocol outputs from m_outputAmounts and related arrays.
+588 -221
View File
File diff suppressed because it is too large Load Diff
+9
View File
@@ -62,6 +62,7 @@ public:
SideChain(p2pool* pool, NetworkType type, const char* pool_name = nullptr, const Wallet* dev_wallet = nullptr);
~SideChain();
void handle_hardfork_transition(uint8_t new_major_version);
[[nodiscard]] bool fill_sidechain_data(PoolBlock& block, std::vector<MinerShare>& shares) const;
[[nodiscard]] bool incoming_block_seen(const PoolBlock& block);
@@ -114,6 +115,9 @@ public:
void check_and_run_deferred_recovery();
bool is_in_recovery() const { return m_recoveryMode.load(); }
// Peer consensus check: detect if we're ahead of the network and recover
void check_peer_consensus(const std::vector<uint64_t>& peer_tips);
// Mainchain reorg handling
void on_mainchain_reorg(uint64_t split_height, const hash& old_block_hash);
bool is_in_reorg_grace_period() const;
@@ -127,6 +131,8 @@ public:
[[nodiscard]] uint64_t last_updated() const;
[[nodiscard]] bool is_ready_to_mine() const { return m_readyToMine.load(); }
void set_ready_to_mine(bool ready) { m_readyToMine.store(ready); }
void set_p2p_start_time(uint64_t t) { m_p2pStartTime.store(t); }
bool preflight_check();
[[nodiscard]] bool is_default() const;
[[nodiscard]] bool is_mini() const;
[[nodiscard]] bool is_nano() const;
@@ -192,6 +198,9 @@ private:
// Sync state - prevents mining/bans during startup
std::atomic<bool> m_readyToMine{ false };
std::atomic<bool> m_preflightPassed{ false };
std::atomic<uint64_t> m_p2pStartTime{ 0 };
std::map<uint64_t, std::vector<PoolBlock*>> m_blocksByHeight;
unordered_map<hash, PoolBlock*> m_blocksById;
+1 -1
View File
@@ -35,7 +35,7 @@
namespace p2pool {
#define P2POOL_VERSION_MAJOR 4
#define P2POOL_VERSION_MINOR 16
#define P2POOL_VERSION_MINOR 19
#define P2POOL_VERSION_PATCH 0
constexpr uint32_t P2POOL_VERSION = (P2POOL_VERSION_MAJOR << 16) | (P2POOL_VERSION_MINOR << 8) | P2POOL_VERSION_PATCH;
+30 -7
View File
@@ -23,6 +23,7 @@
#include "wallet.h"
#include "keccak.h"
#include "params.h"
#include "protocol_tx_hash.h"
#include "gtest/gtest.h"
namespace p2pool {
@@ -34,6 +35,24 @@ static hash H(const char* s)
return result;
};
// Build a protocol TX blob matching what the daemon would provide for a given height
static void build_protocol_tx(uint64_t height, std::vector<uint8_t>& blob, hash& tx_hash)
{
blob.clear();
writeVarint(4, blob); // version
writeVarint(60, blob); // unlock_time
writeVarint(1, blob); // vin count
blob.push_back(0xff); // TXIN_GEN
writeVarint(height, blob); // height
writeVarint(0, blob); // vout count
writeVarint(2, blob); // extra size
blob.push_back(0x02); // extra[0]
blob.push_back(0x00); // extra[1]
writeVarint(2, blob); // type PROTOCOL
blob.push_back(0); // RCT type
calculate_protocol_tx_hash_from_blob(blob, tx_hash);
}
TEST(block_template, update)
{
init_crypto_cache();
@@ -51,6 +70,8 @@ TEST(block_template, update)
data.median_weight = 300000;
data.already_generated_coins = 6887387843126525ULL; // Current Salvium supply
data.median_timestamp = (1ULL << 35) - 2;
build_protocol_tx(data.height, data.protocol_tx_blob, data.protocol_tx_hash);
data.protocol_tx_loaded = true;
Mempool mempool;
Params params;
@@ -61,7 +82,7 @@ TEST(block_template, update)
ASSERT_EQ(tpl.get_reward(), 8813943601ULL);
const PoolBlock* b = tpl.pool_block_template();
ASSERT_EQ(b->m_sidechainId, H("559b3c4ce0f85fb74862b21872c0e7652cd238cda0868bb5e109e45bba824987"));
ASSERT_EQ(b->m_sidechainId, H("061da16d9a2d3a31ebc1075f5d4e84fd7be7f55f6c4c1fc62a7e15ef344baf76"));
std::vector<uint8_t> blobs;
uint64_t height;
@@ -80,7 +101,7 @@ TEST(block_template, update)
hash blobs_hash;
keccak(blobs.data(), static_cast<int>(blobs.size()), blobs_hash.h);
ASSERT_EQ(blobs_hash, H("a0aa8de98e412b38856d3c3a60f4b24159c94bb7d0f847186c729538b2ed3b29"));
ASSERT_EQ(blobs_hash, H("d5c2782a239b702cd08bb2843a53e45996a924ae370cbacdf4be951b9dad4166"));
// Test 2: mempool with high fee and low fee transactions, it must choose high fee transactions
for (uint64_t i = 0; i < 513; ++i) {
@@ -115,7 +136,7 @@ TEST(block_template, update)
tpl.update(data, mempool, &params);
ASSERT_EQ(tpl.get_reward(), 20408427350ULL);
ASSERT_EQ(b->m_sidechainId, H("b141fb9d1ac6a0a657b342ca7febf8c3bd4fa26e5fcfe314698e5ed8a189b8b0"));
ASSERT_EQ(b->m_sidechainId, H("2508573073e47476fbcc6b78c0a9c07f880bb04d4e791c2312d5d620f5e0c2d6"));
ASSERT_EQ(b->m_transactions.size(), 258);
// Transaction selection algorithm differs with Salvium parameters
@@ -134,7 +155,7 @@ TEST(block_template, update)
ASSERT_EQ(template_id, 2U);
keccak(blobs.data(), static_cast<int>(blobs.size()), blobs_hash.h);
ASSERT_EQ(blobs_hash, H("dd2d21ae05f434fb776d39a85e6a517315e70054681a10086343eae620ce301c"));
ASSERT_EQ(blobs_hash, H("89d72d7d990c38356920a5e4225e9c1d6da1cdaae838365509cdde24359d0924"));
// Test 3: small but not empty mempool, and aux chains
/*
@@ -200,7 +221,7 @@ TEST(block_template, update)
tpl.update(data, mempool, &params);
ASSERT_EQ(tpl.get_reward(), 29290189890ULL);
ASSERT_EQ(b->m_sidechainId, H("785a5525982ac554fcfeaa61efa71c0b479d418422ed2028021bf7e8e6b79412"));
ASSERT_EQ(b->m_sidechainId, H("81de1eb4007bd91e8f7a63bd2da9391b10cd3a725ef10e1beaf502d530c3adc4"));
ASSERT_EQ(b->m_transactions.size(), 277);
tpl.get_hashing_blobs(0, 1000, blobs, height, diff, aux_diff, sidechain_diff, seed_hash, nonce_offset, template_id);
@@ -213,7 +234,7 @@ TEST(block_template, update)
ASSERT_EQ(template_id, 3U);
keccak(blobs.data(), static_cast<int>(blobs.size()), blobs_hash.h);
ASSERT_EQ(blobs_hash, H("ca4057ffed2822185c0f8ab6c30ec124c8eb024db56132d47c504b86607d45d7"));
ASSERT_EQ(blobs_hash, H("8e2e3a9119c1b37771f37b6cc7f8637fe92a7e37fcc2b6affaa61c98f9154754"));
}
destroy_crypto_cache();
@@ -248,6 +269,8 @@ TEST(block_template, submit_sidechain_block)
data.median_weight = 300000;
data.already_generated_coins = 6887387843126525ULL; // Current Salvium supply
data.median_timestamp = (1ULL << 35) - (sidechain.chain_window_size() * 2 + 10) * sidechain.block_time() - 3600;
build_protocol_tx(data.height, data.protocol_tx_blob, data.protocol_tx_hash);
data.protocol_tx_loaded = true;
Mempool mempool;
Params params;
@@ -289,7 +312,7 @@ TEST(block_template, submit_sidechain_block)
ASSERT_EQ(tip->m_txinGenHeight, data.height);
ASSERT_EQ(tip->m_sidechainHeight, sidechain.chain_window_size() * 3 - 1);
ASSERT_EQ(tip->m_sidechainId, H("1f1264edf6756741774ed89af60a8d7edafb397eda07dc05aabc2ced929e4d06"));
ASSERT_EQ(tip->m_sidechainId, H("64e7b577ef4c10e12c32c81a1148a1e31a728133d189602533c10ffacc751461"));
}
destroy_crypto_cache();