When Salvium yield/audit payouts mature, the daemon expects a non-empty

protocol_tx in submitted blocks. P2pool previously hardcoded an empty
   protocol_tx (0 outputs), causing block rejections with "Invalid number
   of outputs in protocol_tx".

   Now fetches the daemon's blocktemplate_blob via getblocktemplate RPC,
   parses out the real protocol_tx bytes, and uses them in the block
   template. Falls back to the empty protocol_tx when the RPC hasn't
   returned yet (correct for blocks with no payouts due).
This commit is contained in:
Matt Hess
2026-02-02 19:10:38 +00:00
parent f0b58de64a
commit 5ab43ff6d9
6 changed files with 360 additions and 17 deletions
+22 -15
View File
@@ -667,21 +667,28 @@ void BlockTemplate::update(const MinerData& data, const Mempool& mempool, const
LOGINFO(5, "Stored miner TX hash at position 0: " << miner_tx_hash);
// Write protocol tx bytes to blob
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
// Calculate protocol tx hash and store in member variable
calculate_protocol_tx_hash(data.height, m_protocolTxHash);
LOGINFO(5, "Protocol TX hash: " << m_protocolTxHash);
if (!data.protocol_tx_blob.empty() && data.protocol_tx_loaded) {
// Use real protocol_tx from daemon's getblocktemplate
m_blockTemplateBlob.insert(m_blockTemplateBlob.end(),
data.protocol_tx_blob.begin(), data.protocol_tx_blob.end());
m_protocolTxHash = data.protocol_tx_hash;
LOGINFO(5, "Using real protocol TX from daemon, hash: " << m_protocolTxHash);
} else {
// Fallback: empty protocol_tx (works when no payouts are due)
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
calculate_protocol_tx_hash(data.height, m_protocolTxHash);
LOGINFO(5, "Using fallback empty protocol TX, hash: " << m_protocolTxHash);
}
// Add protocol tx hash after miner tx
m_transactionHashes.insert(m_transactionHashes.end(), m_protocolTxHash.h, m_protocolTxHash.h + HASH_SIZE);
+5
View File
@@ -539,6 +539,8 @@ struct MinerData
, already_generated_coins(0)
, median_timestamp(0)
, aux_nonce(0)
, protocol_tx_hash()
, protocol_tx_loaded(false)
{}
uint8_t major_version;
@@ -554,6 +556,9 @@ struct MinerData
std::vector<AuxChainData> aux_chains;
uint32_t aux_nonce;
std::vector<uint8_t> protocol_tx_identifier;
std::vector<uint8_t> protocol_tx_blob;
hash protocol_tx_hash;
bool protocol_tx_loaded;
std::chrono::high_resolution_clock::time_point time_received;
};
+301
View File
@@ -28,6 +28,8 @@
#include "pow_hash.h"
#include "block_template.h"
#include "side_chain.h"
#include "protocol_tx_hash.h"
#include "wallet.h"
#include "stratum_server.h"
#include "p2p_server.h"
#if defined(WITH_RANDOMX) && !defined(P2POOL_UNIT_TESTS)
@@ -1348,6 +1350,11 @@ void p2pool::update_block_template()
stratum_on_block();
api_update_pool_stats();
// Fetch real protocol_tx from daemon if we don't have it yet for this height
if (!data.protocol_tx_loaded) {
fetch_block_template();
}
#if defined(WITH_RANDOMX) && !defined(P2POOL_UNIT_TESTS)
if (m_isAlternativeBlock.exchange(false)) {
MutexLock lock(m_minerLock);
@@ -1972,6 +1979,300 @@ void p2pool::parse_get_miner_data_rpc(const char* data, size_t size)
}
}
// Extract the protocol_tx bytes from a daemon blocktemplate_blob.
// The blob layout is: block_header | miner_tx | protocol_tx | varint(tx_count) | tx_hashes
// At current HF (10), block_header has no pricing_record (HF_VERSION_ENABLE_ORACLE = 255).
static bool extract_protocol_tx_from_blob(const uint8_t* data, size_t size, std::vector<uint8_t>& protocol_tx_out)
{
const uint8_t* const end = data + size;
// Helper: skip a varint, return false on failure
auto skip_varint = [&data, end]() -> bool {
uint64_t dummy;
data = readVarint(data, end, dummy);
return data != nullptr;
};
// Helper: read a varint value
auto read_varint = [&data, end](uint64_t& val) -> bool {
data = readVarint(data, end, val);
return data != nullptr;
};
// Helper: skip N bytes
auto skip_bytes = [&data, end](size_t n) -> bool {
if (static_cast<size_t>(end - data) < n) return false;
data += n;
return true;
};
// Helper: skip a serialized string (varint length + bytes)
auto skip_string = [&](uint64_t& len) -> bool {
if (!read_varint(len)) return false;
return skip_bytes(static_cast<size_t>(len));
};
// Helper: skip a single tx output (amount varint + type tag + type-specific data)
auto skip_tx_output = [&]() -> bool {
if (!skip_varint()) return false; // amount
if (data >= end) return false;
uint8_t tag = *(data++);
switch (tag) {
case 2: // txout_to_key: key(32) + asset_type(string)
if (!skip_bytes(32)) return false;
{ uint64_t len; if (!skip_string(len)) return false; }
return true;
case 3: // txout_to_tagged_key: key(32) + asset_type(string) + unlock_time(varint) + view_tag(1)
if (!skip_bytes(32)) return false;
{ uint64_t len; if (!skip_string(len)) return false; }
if (!skip_varint()) return false; // unlock_time
if (!skip_bytes(1)) return false; // view_tag
return true;
case 4: // txout_to_carrot_v1: key(32) + asset_type(string) + view_tag(3) + anchor_enc(16)
if (!skip_bytes(32)) return false;
{ uint64_t len; if (!skip_string(len)) return false; }
if (!skip_bytes(3)) return false; // view_tag
if (!skip_bytes(16)) return false; // encrypted_janus_anchor
return true;
default:
return false; // Unknown output type
}
};
// Helper: skip a complete transaction (prefix + rct_type byte)
auto skip_transaction = [&]() -> bool {
// version
if (!skip_varint()) return false;
// unlock_time
if (!skip_varint()) return false;
// vin (vector of txin_v)
uint64_t vin_count;
if (!read_varint(vin_count)) return false;
for (uint64_t i = 0; i < vin_count; ++i) {
if (data >= end) return false;
uint8_t vin_tag = *(data++);
if (vin_tag == 0xff) {
// txin_gen: height varint
if (!skip_varint()) return false;
} else if (vin_tag == 2) {
// txin_to_key: amount(varint) + asset_type(string) + key_offsets(vector<varint>) + k_image(32)
if (!skip_varint()) return false; // amount
{ uint64_t len; if (!skip_string(len)) return false; } // asset_type
uint64_t offsets_count;
if (!read_varint(offsets_count)) return false;
for (uint64_t j = 0; j < offsets_count; ++j) {
if (!skip_varint()) return false;
}
if (!skip_bytes(32)) return false; // k_image
} else {
return false; // Unknown input type
}
}
// vout (vector of tx_out)
uint64_t vout_count;
if (!read_varint(vout_count)) return false;
for (uint64_t i = 0; i < vout_count; ++i) {
if (!skip_tx_output()) return false;
}
// extra (vector<uint8_t>)
uint64_t extra_size;
if (!read_varint(extra_size)) return false;
if (!skip_bytes(static_cast<size_t>(extra_size))) return false;
// type (varint) - Salvium transaction type
uint64_t tx_type;
if (!read_varint(tx_type)) return false;
// Conditional fields based on type
// type 0 = UNSET, 1 = MINER, 2 = PROTOCOL
if (tx_type != 0 && tx_type != 2) {
// amount_burnt
if (!skip_varint()) return false;
if (tx_type != 1) {
// 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)
// so we should never reach here
return false;
}
}
// rct_type byte (0 = RCTTypeNull for coinbase)
if (!skip_bytes(1)) return false;
return true;
};
// --- Parse block header ---
// major_version (varint)
if (!skip_varint()) return false;
// minor_version (varint)
if (!skip_varint()) return false;
// timestamp (varint)
if (!skip_varint()) return false;
// prev_id (32 bytes)
if (!skip_bytes(32)) return false;
// nonce (4 bytes)
if (!skip_bytes(4)) return false;
// NOTE: pricing_record only if major_version >= HF_VERSION_ENABLE_ORACLE (255)
// At current HF (10), this is not present
// --- Skip miner_tx ---
if (!skip_transaction()) return false;
// --- Extract protocol_tx ---
const uint8_t* protocol_tx_start = data;
if (!skip_transaction()) return false;
const uint8_t* protocol_tx_end = data;
protocol_tx_out.assign(protocol_tx_start, protocol_tx_end);
return true;
}
void p2pool::fetch_block_template()
{
if (m_stopped) return;
const Params::Host& host = current_host();
char wallet_buf[Wallet::ADDRESS_LENGTH];
m_params->m_miningWallet.encode(wallet_buf);
const std::string wallet_addr(wallet_buf);
uint64_t height;
{
ReadLock lock(m_minerDataLock);
height = m_minerData.height;
}
std::string request = "{\"jsonrpc\":\"2.0\",\"id\":\"0\",\"method\":\"getblocktemplate\",\"params\":{\"wallet_address\":\"" + wallet_addr + "\",\"reserve_size\":1}}";
JSONRPCRequest::call(host.m_address, host.m_rpcPort, request, host.m_rpcLogin, m_params->m_socks5Proxy, host.m_rpcSSL, host.m_rpcSSL_Fingerprint,
[this, height](const char* data, size_t size, double)
{
parse_block_template_rpc(data, size, height);
},
[](const char* data, size_t size, double)
{
if (size > 0) {
LOGWARN(1, "getblocktemplate RPC request failed: " << log::const_buf(data, size));
}
});
}
void p2pool::parse_block_template_rpc(const char* data, size_t size, uint64_t expected_height)
{
if (m_stopped) return;
rapidjson::Document doc;
doc.Parse(data, size);
if (doc.HasParseError() || !doc.IsObject()) {
LOGWARN(1, "getblocktemplate RPC response is not valid JSON");
return;
}
if (!doc.HasMember("result")) {
if (doc.HasMember("error") && doc["error"].IsObject() && doc["error"].HasMember("message") && doc["error"]["message"].IsString()) {
const char* msg = doc["error"]["message"].GetString();
LOGWARN(1, "getblocktemplate RPC error: " << msg);
} else {
LOGWARN(1, "getblocktemplate RPC response has no result");
}
return;
}
const auto& result = doc["result"];
auto it_blob = result.FindMember("blocktemplate_blob");
if (it_blob == result.MemberEnd() || !it_blob->value.IsString()) {
LOGWARN(1, "getblocktemplate RPC response missing blocktemplate_blob");
return;
}
auto it_height = result.FindMember("height");
if (it_height == result.MemberEnd() || !it_height->value.IsUint64()) {
LOGWARN(1, "getblocktemplate RPC response missing height");
return;
}
const uint64_t height = it_height->value.GetUint64();
if (height != expected_height) {
LOGINFO(5, "getblocktemplate height " << height << " doesn't match expected " << expected_height << ", ignoring");
return;
}
// Hex-decode the blob
const char* hex_str = it_blob->value.GetString();
const size_t hex_len = it_blob->value.GetStringLength();
std::vector<uint8_t> blob;
if (!from_hex(hex_str, hex_len, blob)) {
LOGWARN(1, "getblocktemplate: failed to decode blocktemplate_blob hex");
return;
}
// Extract protocol_tx
std::vector<uint8_t> protocol_tx_blob;
if (!extract_protocol_tx_from_blob(blob.data(), blob.size(), protocol_tx_blob)) {
LOGWARN(1, "getblocktemplate: failed to extract protocol_tx from blob");
return;
}
// Count vout in the protocol_tx to determine if it has outputs
// The protocol_tx starts with: version(varint) + unlock_time(varint) + vin_count(varint) + vin_data + vout_count(varint)
// We need to read vout_count to know if there are outputs
const uint8_t* p = protocol_tx_blob.data();
const uint8_t* p_end = p + protocol_tx_blob.size();
uint64_t dummy;
p = readVarint(p, p_end, dummy); // version
if (!p) return;
p = readVarint(p, p_end, dummy); // unlock_time
if (!p) return;
uint64_t vin_count;
p = readVarint(p, p_end, vin_count); // vin count
if (!p) return;
// Skip vin entries
for (uint64_t i = 0; i < vin_count; ++i) {
if (p >= p_end) return;
uint8_t tag = *(p++);
if (tag == 0xff) {
p = readVarint(p, p_end, dummy); // txin_gen height
if (!p) return;
} else {
return; // Unexpected vin type in protocol_tx
}
}
uint64_t vout_count;
p = readVarint(p, p_end, vout_count);
if (!p) return;
// Compute protocol_tx hash
hash protocol_tx_hash;
calculate_protocol_tx_hash_from_blob(protocol_tx_blob, protocol_tx_hash);
LOGINFO(4, "getblocktemplate: protocol_tx for height " << height << " has " << vout_count << " outputs, hash " << protocol_tx_hash);
// Check if current miner data is still for this height
{
WriteLock lock(m_minerDataLock);
if (m_minerData.height != height) {
LOGINFO(5, "getblocktemplate: height changed since request, ignoring");
return;
}
m_minerData.protocol_tx_blob = std::move(protocol_tx_blob);
m_minerData.protocol_tx_hash = protocol_tx_hash;
m_minerData.protocol_tx_loaded = true;
}
// If protocol_tx has outputs, trigger a template update so miners get the correct block
if (vout_count > 0) {
LOGINFO(2, "Protocol TX for height " << height << " has " << vout_count << " outputs, updating block template");
update_block_template_async();
}
}
bool p2pool::parse_block_header(const char* data, size_t size, ChainMain& c)
{
rapidjson::Document doc;
+2 -2
View File
@@ -195,8 +195,8 @@ private:
void get_miner_data(bool retry = true);
void parse_get_miner_data_rpc(const char* data, size_t size);
void fetch_block_template(MinerData& data);
void parse_block_template_rpc(const char* data, size_t size, MinerData& miner_data);
void fetch_block_template();
void parse_block_template_rpc(const char* data, size_t size, uint64_t expected_height);
bool parse_block_header(const char* data, size_t size, ChainMain& c);
uint32_t parse_block_headers_range(const char* data, size_t size);
+29
View File
@@ -46,5 +46,34 @@ void calculate_protocol_tx_hash(uint64_t height, hash& result) {
keccak(combined, sizeof(combined), result.h);
}
void calculate_protocol_tx_hash_from_blob(const std::vector<uint8_t>& blob, hash& result) {
// The blob is the full serialized transaction: prefix + rct_type(1 byte)
// For coinbase/protocol transactions, rct_type is always 0 (RCTTypeNull)
// The hash is computed as: keccak(prefix_hash + base_rct_hash + null_hash)
// where prefix = everything except the last byte (rct_type)
if (blob.size() < 2) {
// Fallback: shouldn't happen
memset(result.h, 0, HASH_SIZE);
return;
}
// Prefix is everything except the last byte (rct_type)
const size_t prefix_size = blob.size() - 1;
const uint8_t rct_type = blob[blob.size() - 1];
hash prefix_hash, base_rct_hash;
keccak(blob.data(), static_cast<int>(prefix_size), prefix_hash.h);
keccak(&rct_type, 1, base_rct_hash.h);
// Combine: prefix_hash + base_rct_hash + null_hash (32 zeros for RCTTypeNull)
uint8_t combined[HASH_SIZE * 3];
memcpy(combined, prefix_hash.h, HASH_SIZE);
memcpy(combined + HASH_SIZE, base_rct_hash.h, HASH_SIZE);
memset(combined + HASH_SIZE * 2, 0, HASH_SIZE);
keccak(combined, sizeof(combined), result.h);
}
} // namespace p2pool
+1
View File
@@ -6,5 +6,6 @@
namespace p2pool {
void calculate_protocol_tx_hash(uint64_t height, hash& result);
void calculate_protocol_tx_hash_from_blob(const std::vector<uint8_t>& blob, hash& result);
} // namespace p2pool