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:
+22
-15
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user