diff --git a/src/block_template.cpp b/src/block_template.cpp index ab26d45..e928444 100644 --- a/src/block_template.cpp +++ b/src/block_template.cpp @@ -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); diff --git a/src/common.h b/src/common.h index e795fdd..7e2b06f 100644 --- a/src/common.h +++ b/src/common.h @@ -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 aux_chains; uint32_t aux_nonce; std::vector protocol_tx_identifier; + std::vector protocol_tx_blob; + hash protocol_tx_hash; + bool protocol_tx_loaded; std::chrono::high_resolution_clock::time_point time_received; }; diff --git a/src/p2pool.cpp b/src/p2pool.cpp index ea68908..4c67091 100644 --- a/src/p2pool.cpp +++ b/src/p2pool.cpp @@ -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& 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(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(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) + 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) + uint64_t extra_size; + if (!read_varint(extra_size)) return false; + if (!skip_bytes(static_cast(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 blob; + if (!from_hex(hex_str, hex_len, blob)) { + LOGWARN(1, "getblocktemplate: failed to decode blocktemplate_blob hex"); + return; + } + + // Extract protocol_tx + std::vector 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; diff --git a/src/p2pool.h b/src/p2pool.h index 6bf8904..5a8f577 100644 --- a/src/p2pool.h +++ b/src/p2pool.h @@ -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); diff --git a/src/protocol_tx_hash.cpp b/src/protocol_tx_hash.cpp index 6a82749..2a98b42 100644 --- a/src/protocol_tx_hash.cpp +++ b/src/protocol_tx_hash.cpp @@ -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& 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(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 diff --git a/src/protocol_tx_hash.h b/src/protocol_tx_hash.h index 9a29ff8..cb6f461 100644 --- a/src/protocol_tx_hash.h +++ b/src/protocol_tx_hash.h @@ -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& blob, hash& result); } // namespace p2pool