Defer block template construction until getblocktemplate RPC completes so treasury outputs are always present, treat RPC failures as fatal (no template built, retry on next ZMQ), fix txout_to_key parser missing unlock_time field

This commit is contained in:
Matt Hess
2026-02-18 17:44:24 +00:00
parent 336d15b8a5
commit d99e139f3a
7 changed files with 592 additions and 210 deletions
+92 -45
View File
@@ -668,22 +668,31 @@ void BlockTemplate::update(const MinerData& data, const Mempool& mempool, const
LOGINFO(5, "Stored miner TX hash at position 0: " << miner_tx_hash);
// Write empty protocol tx to blob (for sidechain consensus)
// The real protocol_tx from daemon is spliced in at submit_block() time
// Write protocol tx to blob (for sidechain consensus)
m_protocolTxOffsetInTemplate = m_blockTemplateBlob.size();
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);
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();
}
// Add protocol tx hash after miner tx
m_transactionHashes.insert(m_transactionHashes.end(), m_protocolTxHash.h, m_protocolTxHash.h + HASH_SIZE);
@@ -990,11 +999,9 @@ void BlockTemplate::select_mempool_transactions(const Mempool& mempool)
PoolBlock* b = m_poolBlockTemplate;
b->m_transactions.clear();
b->m_transactions.resize(1);
// For Carrot v1 blocks, protocol TX takes a slot
// For Carrot v1 blocks, protocol TX takes a slot (use hash already set during template build)
if (b->m_majorVersion >= 10) {
hash protocol_tx_hash;
calculate_protocol_tx_hash(b->m_txinGenHeight, protocol_tx_hash);
b->m_transactions.push_back(static_cast<indexed_hash>(protocol_tx_hash));
b->m_transactions.push_back(static_cast<indexed_hash>(m_protocolTxHash));
}
b->m_ephPublicKeys.clear();
b->m_outputAmounts.clear();
@@ -1039,8 +1046,10 @@ int BlockTemplate::create_miner_tx(const MinerData& data, const std::vector<Mine
{
m_minerTx.clear();
const size_t num_outputs = shares.size();
m_minerTx.reserve(num_outputs * 39 + 55);
const size_t num_miner_outputs = shares.size();
const size_t num_protocol_outputs = data.protocol_outputs.size();
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
@@ -1057,8 +1066,9 @@ int BlockTemplate::create_miner_tx(const MinerData& data, const std::vector<Mine
m_poolBlockTemplate->m_outputAmounts.clear();
m_poolBlockTemplate->m_viewTags.clear();
m_poolBlockTemplate->m_encryptedAnchors.clear();
m_poolBlockTemplate->m_ephPublicKeys.reserve(num_outputs);
m_poolBlockTemplate->m_outputAmounts.reserve(num_outputs);
m_poolBlockTemplate->m_protocolOutputs.clear();
m_poolBlockTemplate->m_ephPublicKeys.reserve(num_miner_outputs);
m_poolBlockTemplate->m_outputAmounts.reserve(num_miner_outputs);
uint64_t reward_amounts_weight = 0;
@@ -1071,7 +1081,7 @@ int BlockTemplate::create_miner_tx(const MinerData& data, const std::vector<Mine
// Structure for Carrot output with pre-computed K_o
struct CarrotOutput {
size_t share_index; // Original index into shares/m_rewards
size_t share_index; // Original index into shares/m_rewards; SIZE_MAX = protocol output
uint64_t amount;
hash onetime_address; // K_o
hash eph_pubkey; // D_e (per-output ephemeral pubkey)
@@ -1081,8 +1091,8 @@ int BlockTemplate::create_miner_tx(const MinerData& data, const std::vector<Mine
std::vector<CarrotOutput> outputs;
outputs.reserve(num_outputs);
// Generate all outputs using single D_e, position-independent K_o
for (size_t i = 0; i < num_outputs; ++i) {
// Generate miner outputs
for (size_t i = 0; i < num_miner_outputs; ++i) {
CarrotOutput out;
out.share_index = i;
out.amount = m_rewards[i];
@@ -1145,19 +1155,38 @@ int BlockTemplate::create_miner_tx(const MinerData& data, const std::vector<Mine
outputs.push_back(out);
}
// Sort outputs by K_o (required by Salvium daemon)
// Add protocol outputs (treasury, etc.) from daemon's miner_tx
for (const auto& po : data.protocol_outputs) {
CarrotOutput out;
out.share_index = SIZE_MAX; // sentinel: not a miner share
out.amount = po.amount;
out.onetime_address = po.onetime_address;
out.eph_pubkey = po.eph_pubkey;
memcpy(out.view_tag, po.view_tag, 3);
memcpy(out.encrypted_anchor, po.anchor_enc, 16);
outputs.push_back(out);
}
// Sort all outputs by K_o (required by Salvium daemon)
std::sort(outputs.begin(), outputs.end(),
[](const CarrotOutput& a, const CarrotOutput& b) {
return memcmp(a.onetime_address.h, b.onetime_address.h, HASH_SIZE) < 0;
});
// Write sorted outputs to miner tx (single D_e used for all)
// Write sorted outputs to miner tx
for (const auto& out : outputs) {
// Amount
writeVarint(out.amount, [this, &reward_amounts_weight](uint8_t b) {
m_minerTx.push_back(b);
++reward_amounts_weight;
});
const bool is_protocol_output = (out.share_index == SIZE_MAX);
// Amount: only count miner output amount bytes in reward_amounts_weight
if (is_protocol_output) {
// Protocol output: amount is fixed; write without contributing to reward_amounts_weight
writeVarint(out.amount, m_minerTx);
} else {
writeVarint(out.amount, [this, &reward_amounts_weight](uint8_t b) {
m_minerTx.push_back(b);
++reward_amounts_weight;
});
}
// txout_to_carrot_v1
m_minerTx.push_back(TXOUT_TO_CARROT_V1);
@@ -1181,13 +1210,24 @@ int BlockTemplate::create_miner_tx(const MinerData& data, const std::vector<Mine
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);
// Save for pool block template
m_poolBlockTemplate->m_ephPublicKeys.emplace_back(out.onetime_address);
m_poolBlockTemplate->m_outputAmounts.emplace_back(out.amount, out.view_tag[0]);
std::vector<uint8_t> vt(out.view_tag, out.view_tag + 3);
std::vector<uint8_t> ea(out.encrypted_anchor, out.encrypted_anchor + 16);
m_poolBlockTemplate->m_viewTags.push_back(vt);
m_poolBlockTemplate->m_encryptedAnchors.push_back(ea);
if (is_protocol_output) {
// Save protocol output to pool block template
ProtocolOutput po;
po.onetime_address = out.onetime_address;
po.eph_pubkey = out.eph_pubkey;
po.amount = out.amount;
memcpy(po.view_tag, out.view_tag, 3);
memcpy(po.anchor_enc, out.encrypted_anchor, 16);
m_poolBlockTemplate->m_protocolOutputs.push_back(po);
} else {
// Save miner output to pool block template
m_poolBlockTemplate->m_ephPublicKeys.emplace_back(out.onetime_address);
m_poolBlockTemplate->m_outputAmounts.emplace_back(out.amount, out.view_tag[0]);
std::vector<uint8_t> vt(out.view_tag, out.view_tag + 3);
std::vector<uint8_t> ea(out.encrypted_anchor, out.encrypted_anchor + 16);
m_poolBlockTemplate->m_viewTags.push_back(vt);
m_poolBlockTemplate->m_encryptedAnchors.push_back(ea);
}
}
}
@@ -1218,20 +1258,27 @@ int BlockTemplate::create_miner_tx(const MinerData& data, const std::vector<Mine
outputs[0].eph_pubkey.h + HASH_SIZE);
}
} else {
// Multiple outputs: use TX_EXTRA_TAG_ADDITIONAL_PUBKEYS with ALL D_e values
// Multiple outputs: use TX_EXTRA_TAG_ADDITIONAL_PUBKEYS with ALL D_e values in K_o sorted order
m_minerTxExtra.push_back(TX_EXTRA_TAG_ADDITIONAL_PUBKEYS);
writeVarint(num_outputs, m_minerTxExtra);
if (dry_run) {
m_minerTxExtra.insert(m_minerTxExtra.end(), num_outputs * HASH_SIZE, 0);
} else {
// Store first D_e in m_txkeyPub for compatibility
m_poolBlockTemplate->m_txkeyPub = outputs[0].eph_pubkey;
// Write ALL D_e in sorted K_o order (miner + protocol interleaved)
// Also store ONLY miner D_e in m_txkeyPub + m_additionalPubKeys (for serialize_mainchain_data)
bool first_miner = true;
for (size_t i = 0; i < num_outputs; ++i) {
m_minerTxExtra.insert(m_minerTxExtra.end(),
outputs[i].eph_pubkey.h,
outputs[i].eph_pubkey.h + HASH_SIZE);
if (i > 0) {
m_poolBlockTemplate->m_additionalPubKeys.push_back(outputs[i].eph_pubkey);
if (outputs[i].share_index != SIZE_MAX) {
// Miner output
if (first_miner) {
m_poolBlockTemplate->m_txkeyPub = outputs[i].eph_pubkey;
first_miner = false;
} else {
m_poolBlockTemplate->m_additionalPubKeys.push_back(outputs[i].eph_pubkey);
}
}
}
}
+18
View File
@@ -527,6 +527,21 @@ struct AuxChainData
difficulty_type difficulty;
};
struct ProtocolOutput
{
hash onetime_address; // K_o (onetime address)
hash eph_pubkey; // D_e (ephemeral public key)
uint64_t amount;
uint8_t view_tag[3];
uint8_t anchor_enc[16];
FORCEINLINE ProtocolOutput() : onetime_address{}, eph_pubkey{}, amount(0), view_tag{}, anchor_enc{} {}
FORCEINLINE bool operator==(const ProtocolOutput& rhs) const {
return onetime_address == rhs.onetime_address;
}
};
struct MinerData
{
FORCEINLINE MinerData()
@@ -541,6 +556,7 @@ struct MinerData
, aux_nonce(0)
, protocol_tx_hash()
, protocol_tx_loaded(false)
, expected_reward(0)
{}
uint8_t major_version;
@@ -559,6 +575,8 @@ struct MinerData
std::vector<uint8_t> protocol_tx_blob;
hash protocol_tx_hash;
bool protocol_tx_loaded;
uint64_t expected_reward;
std::vector<ProtocolOutput> protocol_outputs;
std::chrono::high_resolution_clock::time_point time_received;
};
+201 -26
View File
@@ -1130,15 +1130,13 @@ void p2pool::submit_block() const
size_t nonce_offset = 0;
size_t extra_nonce_offset = 0;
size_t merkle_root_offset = 0;
size_t ptx_offset = 0;
size_t ptx_size = 0;
hash merge_mining_root;
const BlockTemplate* block_tpl = nullptr;
bool is_external = false;
if (submit_data.blob.empty()) {
submit_data.blob = m_blockTemplate->get_block_template_blob(submit_data.template_id, submit_data.extra_nonce, nonce_offset, extra_nonce_offset, merkle_root_offset, merge_mining_root, &block_tpl, &ptx_offset, &ptx_size);
submit_data.blob = m_blockTemplate->get_block_template_blob(submit_data.template_id, submit_data.extra_nonce, nonce_offset, extra_nonce_offset, merkle_root_offset, merge_mining_root, &block_tpl);
LOGINFO(0, log::LightGreen() << "submit_block: height = " << height
<< ", template id = " << submit_data.template_id
<< ", nonce = " << submit_data.nonce
@@ -1150,13 +1148,6 @@ void p2pool::submit_block() const
return;
}
// Splice real protocol_tx from daemon if available
MinerData data = miner_data();
if (!data.protocol_tx_blob.empty() && data.protocol_tx_loaded && ptx_offset > 0 && ptx_size > 0) {
LOGINFO(1, "submit_block: splicing real protocol_tx (" << data.protocol_tx_blob.size() << " bytes) at offset " << ptx_offset);
submit_data.blob.erase(submit_data.blob.begin() + static_cast<ptrdiff_t>(ptx_offset), submit_data.blob.begin() + static_cast<ptrdiff_t>(ptx_offset + ptx_size));
submit_data.blob.insert(submit_data.blob.begin() + static_cast<ptrdiff_t>(ptx_offset), data.protocol_tx_blob.begin(), data.protocol_tx_blob.end());
}
}
else {
LOGINFO(0, log::LightGreen() << "submit_block: height = " << height << ", external blob");
@@ -1313,12 +1304,16 @@ void p2pool::update_block_template()
m_hasher->set_seed_async(data.seed_hash);
}
m_blockTemplate->update(data, *m_mempool, &m_params, in_donation_mode(data.height));
// Fetch real protocol_tx from daemon in parallel (used at submit_block time)
// Wait for getblocktemplate RPC before building the block template.
// ZMQ gives us basic miner data; the RPC gives us treasury/protocol outputs.
// parse_block_template_rpc() will call update_block_template_async() when ready.
if (!data.protocol_tx_loaded) {
fetch_block_template();
return;
}
m_blockTemplate->update(data, *m_mempool, &m_params, in_donation_mode(data.height));
stratum_on_block();
api_update_pool_stats();
@@ -1949,6 +1944,165 @@ 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
// 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)
{
const uint8_t* const blob_end = data + size;
auto read_varint_fn = [&data, blob_end](uint64_t& val) -> bool {
data = readVarint(data, blob_end, val);
return data != nullptr;
};
auto skip_bytes_fn = [&data, blob_end](size_t n) -> bool {
if (static_cast<size_t>(blob_end - data) < n) return false;
data += n;
return true;
};
// Skip block header: major_version, minor_version, timestamp (varints) + prev_id (32) + nonce (4)
uint64_t dummy;
if (!read_varint_fn(dummy) || !read_varint_fn(dummy) || !read_varint_fn(dummy)) return false;
if (!skip_bytes_fn(32 + 4)) return false;
// Parse miner_tx
// version, unlock_time
if (!read_varint_fn(dummy) || !read_varint_fn(dummy)) return false;
// vin
uint64_t vin_count;
if (!read_varint_fn(vin_count)) return false;
for (uint64_t i = 0; i < vin_count; ++i) {
if (data >= blob_end) return false;
uint8_t tag = *(data++);
if (tag == 0xff) {
if (!read_varint_fn(dummy)) return false;
} else {
return false;
}
}
// vout
uint64_t vout_count;
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;
// Read all outputs, store them temporarily
struct TmpOutput {
uint64_t amount;
hash K_o;
uint8_t view_tag[3];
uint8_t anchor_enc[16];
};
std::vector<TmpOutput> tmp_outputs;
tmp_outputs.reserve(static_cast<size_t>(vout_count));
for (uint64_t i = 0; i < vout_count; ++i) {
uint64_t amount;
if (!read_varint_fn(amount)) return false;
if (data >= blob_end) return false;
uint8_t type_tag = *(data++);
if (type_tag != 4) return false; // must be txout_to_carrot_v1
TmpOutput t;
t.amount = amount;
if (static_cast<size_t>(blob_end - data) < HASH_SIZE) return false;
memcpy(t.K_o.h, data, HASH_SIZE);
data += HASH_SIZE;
// asset_type: length byte + "SAL1"
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"
// view_tag (3 bytes)
if (static_cast<size_t>(blob_end - data) < 3) return false;
memcpy(t.view_tag, data, 3);
data += 3;
// encrypted_anchor (16 bytes)
if (static_cast<size_t>(blob_end - data) < 16) return false;
memcpy(t.anchor_enc, data, 16);
data += 16;
tmp_outputs.push_back(t);
}
// Parse tx_extra to get D_e values (one per output in vout order)
uint64_t extra_size;
if (!read_varint_fn(extra_size)) return false;
if (static_cast<size_t>(blob_end - data) < extra_size) return false;
const uint8_t* extra_begin = data;
const uint8_t* extra_end = data + extra_size;
// Parse D_e values from tx_extra
std::vector<hash> de_values;
while (data < extra_end) {
uint8_t tag = *(data++);
if (tag == TX_EXTRA_TAG_PUBKEY) {
// Single D_e
if (data + HASH_SIZE > extra_end) break;
hash de;
memcpy(de.h, data, HASH_SIZE);
data += HASH_SIZE;
de_values.push_back(de);
} else if (tag == TX_EXTRA_TAG_ADDITIONAL_PUBKEYS) {
// Multiple D_e: varint count + count * 32 bytes
uint64_t n_keys;
data = readVarint(data, extra_end, n_keys);
if (!data) break;
for (uint64_t k = 0; k < n_keys && data + HASH_SIZE <= extra_end; ++k) {
hash de;
memcpy(de.h, data, HASH_SIZE);
data += HASH_SIZE;
de_values.push_back(de);
}
} else if (tag == TX_EXTRA_NONCE) {
uint64_t nonce_size;
data = readVarint(data, extra_end, nonce_size);
if (!data) break;
if (data + nonce_size > extra_end) break;
data += nonce_size;
} else if (tag == TX_EXTRA_MERGE_MINING_TAG) {
if (data >= extra_end) break;
uint8_t mm_size = *(data++);
if (data + mm_size > extra_end) break;
data += mm_size;
} else {
// Unknown tag: skip rest of extra
break;
}
}
// Advance past the full extra section
data = extra_begin + extra_size;
// Extract protocol outputs: those whose amount != miner_amount (or all if expected_reward == 0)
// D_e at position k corresponds to tmp_outputs[k] (vout is in K_o sorted order)
for (size_t i = 0; i < tmp_outputs.size(); ++i) {
const TmpOutput& t = tmp_outputs[i];
if (expected_reward == 0 || t.amount != miner_amount) {
// This is a protocol output
ProtocolOutput po;
po.onetime_address = t.K_o;
po.amount = t.amount;
memcpy(po.view_tag, t.view_tag, 3);
memcpy(po.anchor_enc, t.anchor_enc, 16);
// Get D_e from tx_extra at matching index
if (i < de_values.size()) {
po.eph_pubkey = de_values[i];
}
protocol_outputs_out.push_back(po);
LOGINFO(2, "Protocol output found: amount=" << t.amount << " K_o=" << t.K_o);
}
}
return true;
}
// 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).
@@ -1988,9 +2142,10 @@ static bool extract_protocol_tx_from_blob(const uint8_t* data, size_t size, std:
if (data >= blob_end) return false;
uint8_t tag = *(data++);
switch (tag) {
case 2: // txout_to_key: key(32) + asset_type(string)
case 2: // txout_to_key: key(32) + asset_type(string) + unlock_time(varint)
if (!skip_bytes(32)) return false;
{ uint64_t len; if (!skip_string(len)) return false; }
if (!skip_varint()) return false; // unlock_time
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;
@@ -2124,16 +2279,12 @@ void p2pool::fetch_block_template()
{
parse_block_template_rpc(data, size, height);
},
[this](const char* data, size_t size, double)
[](const char* data, size_t size, double)
{
if (size > 0) {
LOGWARN(1, "getblocktemplate RPC request failed: " << log::const_buf(data, size));
}
// RPC failed - empty protocol_tx will be used at submit time
{
WriteLock lock(m_minerDataLock);
m_minerData.protocol_tx_loaded = true;
}
// RPC failed - leave protocol_tx_loaded=false; next ZMQ notification will retry
});
}
@@ -2141,11 +2292,8 @@ void p2pool::parse_block_template_rpc(const char* data, size_t size, uint64_t ex
{
if (m_stopped) return;
// On any failure, just mark as loaded (empty protocol_tx will be used at submit time)
auto fallback = [this]() {
WriteLock lock(m_minerDataLock);
m_minerData.protocol_tx_loaded = true;
};
// On any failure, leave protocol_tx_loaded=false; next ZMQ notification will retry the RPC
auto fallback = []() {};
rapidjson::Document doc;
doc.Parse(data, size);
@@ -2190,6 +2338,13 @@ void p2pool::parse_block_template_rpc(const char* data, size_t size, uint64_t ex
return;
}
// Parse expected_reward (used to identify miner's output vs protocol outputs)
uint64_t rpc_expected_reward = 0;
auto it_expected_reward = result.FindMember("expected_reward");
if (it_expected_reward != result.MemberEnd() && it_expected_reward->value.IsUint64()) {
rpc_expected_reward = it_expected_reward->value.GetUint64();
}
// Hex-decode the blob
const char* hex_str = it_blob->value.GetString();
const size_t hex_len = it_blob->value.GetStringLength();
@@ -2244,7 +2399,19 @@ void p2pool::parse_block_template_rpc(const char* data, size_t size, uint64_t ex
LOGINFO(2, "Protocol TX for height " << height << " has " << vout_count << " outputs");
}
// Store protocol_tx for use at submit_block() time
// Extract protocol outputs from daemon's miner_tx (treasury, etc.)
std::vector<ProtocolOutput> protocol_outputs;
if (rpc_expected_reward > 0) {
if (!extract_protocol_outputs_from_blob(blob.data(), blob.size(), rpc_expected_reward, protocol_outputs)) {
LOGWARN(3, "getblocktemplate: failed to extract protocol outputs from blob, will use empty list");
protocol_outputs.clear();
}
if (!protocol_outputs.empty()) {
LOGINFO(2, "getblocktemplate: found " << protocol_outputs.size() << " protocol output(s) at height " << height);
}
}
// Store protocol_tx and protocol outputs for use at submit_block() / block template time
{
WriteLock lock(m_minerDataLock);
if (m_minerData.height != height) {
@@ -2254,7 +2421,15 @@ void p2pool::parse_block_template_rpc(const char* data, size_t size, uint64_t ex
m_minerData.protocol_tx_blob = std::move(protocol_tx_blob);
m_minerData.protocol_tx_hash = protocol_tx_hash;
m_minerData.protocol_tx_loaded = true;
m_minerData.expected_reward = rpc_expected_reward;
m_minerData.protocol_outputs = std::move(protocol_outputs);
}
// Rebuild block template now that protocol outputs are known.
// The initial template was created before this RPC returned (protocol_tx_loaded was false),
// so the miner_tx lacked treasury/protocol outputs. This triggers one additional update
// which won't loop because protocol_tx_loaded is now true.
update_block_template_async();
}
bool p2pool::parse_block_header(const char* data, size_t size, ChainMain& c)
+97 -68
View File
@@ -141,13 +141,16 @@ PoolBlock& PoolBlock::operator=(const PoolBlock& b)
m_powHash = b.m_powHash;
m_seed = b.m_seed;
m_protocolTxBlob = b.m_protocolTxBlob;
m_protocolOutputs = b.m_protocolOutputs;
return *this;
}
std::vector<uint8_t> PoolBlock::serialize_mainchain_data(size_t* header_size, size_t* miner_tx_size, int* outputs_offset, int* outputs_blob_size, const uint32_t* nonce, const uint32_t* extra_nonce, bool include_tx_hashes) const
{
std::vector<uint8_t> data;
data.reserve(std::min<size_t>(128 + m_outputAmounts.size() * 39 + m_transactions.size() * HASH_SIZE, 131072));
data.reserve(std::min<size_t>(128 + (m_outputAmounts.size() + m_protocolOutputs.size()) * 60 + m_transactions.size() * HASH_SIZE, 131072));
// Header
data.push_back(m_majorVersion);
@@ -177,69 +180,86 @@ std::vector<uint8_t> PoolBlock::serialize_mainchain_data(size_t* header_size, si
*outputs_offset = outputs_offset0;
}
writeVarint(m_outputAmounts.size(), data);
LOGINFO(6, "DEBUG serialize: numOutputs=" << m_outputAmounts.size() << " numEphKeys=" << m_ephPublicKeys.size() << " numViewTags=" << m_viewTags.size() << " numEncAnchors=" << m_encryptedAnchors.size() << " sidechainHeight=" << m_sidechainHeight);
if (!m_ephPublicKeys.empty()) {
LOGINFO(6, "DEBUG serialize K_o[0]=" << m_ephPublicKeys[0]);
}
if (!m_viewTags.empty() && m_viewTags[0].size() >= 3) {
char buf[16]; snprintf(buf, sizeof(buf), "%02x%02x%02x", m_viewTags[0][0], m_viewTags[0][1], m_viewTags[0][2]);
LOGINFO(6, "DEBUG serialize viewTag[0]=" << static_cast<const char*>(buf));
}
if (!m_encryptedAnchors.empty() && m_encryptedAnchors[0].size() >= 16) {
std::string hex;
for (int i = 0; i < 16; ++i) { char buf[4]; snprintf(buf, sizeof(buf), "%02x", m_encryptedAnchors[0][i]); hex += buf; }
LOGINFO(6, "DEBUG serialize encAnchor[0]=" << hex);
}
LOGINFO(6, "DEBUG serialize D_e=" << m_txkeyPub);
LOGINFO(6, "DEBUG serialize merkleRoot=" << static_cast<const hash&>(m_merkleRoot));
// Build merged output list (miner outputs + protocol outputs), sorted by K_o
struct MergedOutput {
hash K_o;
hash D_e;
uint64_t amount;
uint8_t view_tag[3];
uint8_t anchor[16];
};
std::vector<MergedOutput> merged;
merged.reserve(m_outputAmounts.size() + m_protocolOutputs.size());
// Miner outputs: K_o in m_ephPublicKeys (sorted order), D_e in m_txkeyPub + m_additionalPubKeys
for (size_t i = 0, n = m_outputAmounts.size(); i < n; ++i) {
const TxOutput& output = m_outputAmounts[i];
MergedOutput mo;
mo.K_o = m_ephPublicKeys[i];
mo.D_e = (i == 0) ? m_txkeyPub : m_additionalPubKeys[i - 1];
mo.amount = m_outputAmounts[i].m_reward;
if (i < m_viewTags.size() && m_viewTags[i].size() >= 3)
memcpy(mo.view_tag, m_viewTags[i].data(), 3);
else
memset(mo.view_tag, 0, 3);
if (i < m_encryptedAnchors.size() && m_encryptedAnchors[i].size() >= 16)
memcpy(mo.anchor, m_encryptedAnchors[i].data(), 16);
else
memset(mo.anchor, 0, 16);
merged.push_back(mo);
}
writeVarint(output.m_reward, data);
// 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);
}
// Sort all outputs by K_o
std::sort(merged.begin(), merged.end(), [](const MergedOutput& a, const MergedOutput& b) {
return memcmp(a.K_o.h, b.K_o.h, HASH_SIZE) < 0;
});
writeVarint(merged.size(), data);
LOGINFO(6, "DEBUG serialize: numOutputs=" << merged.size() << " (miner=" << m_outputAmounts.size() << " proto=" << m_protocolOutputs.size() << ") sidechainHeight=" << m_sidechainHeight);
for (const auto& mo : merged) {
writeVarint(mo.amount, data);
data.push_back(TXOUT_TO_CARROT_V1);
const hash h = m_ephPublicKeys[i];
data.insert(data.end(), h.h, h.h + HASH_SIZE);
// Carrot v1: asset_len + "SAL1" + 3-byte view_tag + 16-byte encrypted_anchor
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');
data.insert(data.end(), m_viewTags[i].begin(), m_viewTags[i].end());
data.insert(data.end(), m_encryptedAnchors[i].begin(), m_encryptedAnchors[i].end());
data.insert(data.end(), mo.view_tag, mo.view_tag + 3);
data.insert(data.end(), mo.anchor, mo.anchor + 16);
}
if (outputs_blob_size) {
*outputs_blob_size = static_cast<int>(data.size()) - outputs_offset0;
}
std::vector<uint8_t> tx_extra(128 + (1 + m_additionalPubKeys.size()) * HASH_SIZE);
const size_t total_outputs = merged.size();
std::vector<uint8_t> tx_extra(128 + total_outputs * HASH_SIZE);
uint8_t* p = tx_extra.data();
if (m_additionalPubKeys.empty()) {
if (total_outputs == 1) {
// Single output: use TX_EXTRA_TAG_PUBKEY
*(p++) = TX_EXTRA_TAG_PUBKEY;
memcpy(p, m_txkeyPub.h, HASH_SIZE);
memcpy(p, merged[0].D_e.h, HASH_SIZE);
p += HASH_SIZE;
} else {
// Multiple outputs: TX_EXTRA_TAG_ADDITIONAL_PUBKEYS with ALL D_e values
} else if (total_outputs > 1) {
// Multiple outputs: TX_EXTRA_TAG_ADDITIONAL_PUBKEYS with ALL D_e values in K_o sorted order
*(p++) = TX_EXTRA_TAG_ADDITIONAL_PUBKEYS;
size_t total_pubkeys = 1 + m_additionalPubKeys.size();
writeVarint(total_pubkeys, [&p](uint8_t b) { *(p++) = b; });
// First D_e (stored in m_txkeyPub)
memcpy(p, m_txkeyPub.h, HASH_SIZE);
p += HASH_SIZE;
// Remaining D_e values
for (const auto& pk : m_additionalPubKeys) {
memcpy(p, pk.h, HASH_SIZE);
writeVarint(total_outputs, [&p](uint8_t b) { *(p++) = b; });
for (const auto& mo : merged) {
memcpy(p, mo.D_e.h, HASH_SIZE);
p += HASH_SIZE;
}
}
@@ -294,30 +314,23 @@ std::vector<uint8_t> PoolBlock::serialize_mainchain_data(size_t* header_size, si
// Protocol tx (Salvium Carrot v1+)
if (m_majorVersion >= 10) {
writeVarint(4, data); // version = TRANSACTION_VERSION_CARROT
writeVarint(60, data); // unlock_time = 60
// vin (1 txin_gen)
writeVarint(1, data); // vin.size() = 1
data.push_back(TXIN_GEN);
writeVarint(m_txinGenHeight, data);
// vout (empty)
writeVarint(0, data); // vout.size() = 0
// extra (2 bytes: 0x02 0x00)
writeVarint(2, data); // extra.size() = 2
data.push_back(0x02);
data.push_back(0x00);
// type = PROTOCOL
writeVarint(2, data); // transaction_type::PROTOCOL = 2
data.push_back(0); // RCT
// Calculate protocol tx hash using salviumd-compatible serialization
hash protocol_tx_hash;
calculate_protocol_tx_hash(m_txinGenHeight, protocol_tx_hash);
LOGINFO(3, "Sidechain protocol TX hash: " << protocol_tx_hash);
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 (include_tx_hashes) {
writeVarint(m_transactions.size() - 1, data);
@@ -395,6 +408,18 @@ 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));
// Append protocol outputs (new format, appended after sidechainExtraBuf)
if (!m_protocolOutputs.empty()) {
writeVarint(m_protocolOutputs.size(), data);
for (const auto& po : m_protocolOutputs) {
data.insert(data.end(), po.onetime_address.h, po.onetime_address.h + HASH_SIZE);
data.insert(data.end(), po.eph_pubkey.h, po.eph_pubkey.h + HASH_SIZE);
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 POOL_BLOCK_DEBUG
if (!m_sideChainDataDebug.empty() && (data != m_sideChainDataDebug)) {
LOGERR(1, "serialize_sidechain_data() has a bug, fix it!");
@@ -465,7 +490,11 @@ bool PoolBlock::get_pow_hash(RandomX_Hasher_Base* hasher, uint64_t height, const
m_transactions.resize(2);
}
hash protocol_tx_hash;
calculate_protocol_tx_hash(m_txinGenHeight, 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;
+9
View File
@@ -69,6 +69,9 @@ 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
static constexpr uint64_t SIDECHAIN_PROTOCOL_TX_HARDFORK_HEIGHT = 1100ULL;
// Limited by the format of the Merkle tree parameters in tx_extra
static constexpr uint64_t MERGE_MINING_MAX_CHAINS = 256;
static constexpr uint64_t LOG2_MERGE_MINING_MAX_CHAINS = 8;
@@ -195,6 +198,12 @@ struct PoolBlock
hash m_powHash;
hash m_seed;
// Real protocol_tx bytes from the daemon (populated for height >= SIDECHAIN_PROTOCOL_TX_HARDFORK_HEIGHT)
std::vector<uint8_t> m_protocolTxBlob;
// Protocol outputs from daemon's miner_tx (treasury, etc.) - boomeranged into p2pool's miner_tx
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;
+113 -25
View File
@@ -284,65 +284,102 @@ int PoolBlock::deserialize(const uint8_t* data, size_t size, const SideChain& si
if (m_majorVersion >= 10) {
// Save position in case we need to backtrack
const uint8_t* saved_pos = data;
// Helper to advance data by n bytes with bounds check
auto ptx_skip = [&data, data_end](size_t n) -> bool {
if (static_cast<size_t>(data_end - data) < n) return false;
data += n;
return true;
};
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) {
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
if (peek2 && second_val == 60) {
// This is a protocol_tx, parse it
data = peek1; // Consume version varint
data = peek2; // Consume unlock_time varint
uint64_t protocol_vin_size;
data = readVarint(data, data_end, protocol_vin_size);
if (!data || protocol_vin_size != 1) { data = saved_pos; goto skip_protocol_tx; }
uint8_t txin_type;
if (!read_byte(txin_type)) { data = saved_pos; goto skip_protocol_tx; }
if (txin_type != TXIN_GEN) { data = saved_pos; goto skip_protocol_tx; }
uint64_t protocol_height;
data = readVarint(data, data_end, protocol_height);
if (!data) { data = saved_pos; goto skip_protocol_tx; }
uint64_t protocol_vout_size;
data = readVarint(data, data_end, protocol_vout_size);
if (!data) { data = saved_pos; goto skip_protocol_tx; }
// Skip vout if any
// Skip vout entries with correct per-type sizes
for (uint64_t i = 0; i < protocol_vout_size; ++i) {
uint64_t amount;
data = readVarint(data, data_end, amount);
// amount varint
uint64_t ptx_amount;
data = readVarint(data, data_end, ptx_amount);
if (!data) { data = saved_pos; goto skip_protocol_tx; }
uint8_t type;
if (!read_byte(type)) { data = saved_pos; goto skip_protocol_tx; }
// Skip output data (32 bytes for key)
if (!read_buf(nullptr, 32)) { data = saved_pos; goto skip_protocol_tx; }
// output type tag
uint8_t ptx_out_type;
if (!read_byte(ptx_out_type)) { data = saved_pos; goto skip_protocol_tx; }
// All types: 32-byte key (K_o)
if (!ptx_skip(32)) { data = saved_pos; goto skip_protocol_tx; }
// All types: asset_type string (varint length + bytes)
uint64_t asset_type_len;
data = readVarint(data, data_end, asset_type_len);
if (!data) { data = saved_pos; goto skip_protocol_tx; }
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) {
// txout_to_tagged_key: unlock_time(varint) + view_tag(1)
uint64_t ptx_unlock;
data = readVarint(data, data_end, ptx_unlock);
if (!data) { data = saved_pos; goto skip_protocol_tx; }
if (!ptx_skip(1)) { data = saved_pos; goto skip_protocol_tx; }
} 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) {
// Unknown output type
data = saved_pos; goto skip_protocol_tx;
}
// type 2 (txout_to_key): nothing more to skip
}
// extra (vector<uint8_t>)
uint64_t protocol_extra_size;
data = readVarint(data, data_end, protocol_extra_size);
if (!data) { data = saved_pos; goto skip_protocol_tx; }
for (uint64_t i = 0; i < protocol_extra_size; ++i) {
uint8_t tmp;
if (!read_byte(tmp)) { data = saved_pos; goto skip_protocol_tx; }
}
if (!ptx_skip(static_cast<size_t>(protocol_extra_size))) { data = saved_pos; goto skip_protocol_tx; }
// transaction type varint
uint64_t protocol_type;
data = readVarint(data, data_end, protocol_type);
if (!data) { data = saved_pos; goto skip_protocol_tx; }
// rct_type byte (must be 0)
uint8_t rct;
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);
}
}
}
}
@@ -552,6 +589,24 @@ skip_protocol_tx:
READ_BUF(m_sidechainExtraBuf, sizeof(m_sidechainExtraBuf));
// Read protocol outputs if present (optional, appended after sidechainExtraBuf)
m_protocolOutputs.clear();
if (data < data_end) {
uint64_t proto_count;
READ_VARINT(proto_count);
if (proto_count > 256) return __LINE__;
m_protocolOutputs.reserve(static_cast<size_t>(proto_count));
for (uint64_t k = 0; k < proto_count; ++k) {
ProtocolOutput po;
READ_BUF(po.onetime_address.h, HASH_SIZE);
READ_BUF(po.eph_pubkey.h, HASH_SIZE);
READ_VARINT(po.amount);
READ_BUF(po.view_tag, 3);
READ_BUF(po.anchor_enc, 16);
m_protocolOutputs.push_back(po);
}
}
#undef READ_BYTE
#undef EXPECT_BYTE
#undef READ_VARINT
@@ -664,16 +719,49 @@ skip_protocol_tx:
return __LINE__;
}
// For Carrot v1 blocks, ensure protocol TX is populated
// For Carrot v1 blocks, ensure protocol TX hash is populated
if (m_majorVersion >= 10) {
if (m_transactions.size() < 2) {
m_transactions.resize(2);
}
hash protocol_tx_hash;
calculate_protocol_tx_hash(m_txinGenHeight, 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.
// Protocol outputs are identified by matching K_o (onetime_address) against m_protocolOutputs.
// We also update m_txkeyPub/m_additionalPubKeys to contain only miner D_e values.
for (const auto& po : m_protocolOutputs) {
for (size_t idx = 0; idx < m_ephPublicKeys.size(); ++idx) {
if (m_ephPublicKeys[idx] == po.onetime_address) {
// Remove from output arrays
m_ephPublicKeys.erase(m_ephPublicKeys.begin() + idx);
m_outputAmounts.erase(m_outputAmounts.begin() + idx);
if (idx < m_viewTags.size())
m_viewTags.erase(m_viewTags.begin() + idx);
if (idx < m_encryptedAnchors.size())
m_encryptedAnchors.erase(m_encryptedAnchors.begin() + idx);
// Remove corresponding D_e from m_txkeyPub/m_additionalPubKeys
if (idx == 0) {
if (!m_additionalPubKeys.empty()) {
m_txkeyPub = m_additionalPubKeys[0];
m_additionalPubKeys.erase(m_additionalPubKeys.begin());
} else {
m_txkeyPub = {};
}
} else {
m_additionalPubKeys.erase(m_additionalPubKeys.begin() + idx - 1);
}
break;
}
}
}
reset_offchain_data();
return 0;
}
+62 -46
View File
@@ -940,63 +940,76 @@ bool SideChain::get_outputs_blob(PoolBlock* block, uint64_t total_reward, std::v
{
ReadLock lock(m_sidechainLock);
auto it = block->m_sidechainId.empty() ? m_blocksById.end() : m_blocksById.find(block->m_sidechainId);
// Helper: build outputs blob from miner outputs + protocol outputs, sorted by K_o
auto build_merged_blob = [](const PoolBlock* b, std::vector<uint8_t>& out_blob) {
struct MergedEntry {
hash K_o;
uint64_t amount;
uint8_t view_tag[3];
uint8_t anchor[16];
};
std::vector<MergedEntry> merged;
merged.reserve(b->m_outputAmounts.size() + b->m_protocolOutputs.size());
for (size_t i = 0, n = b->m_outputAmounts.size(); i < n; ++i) {
MergedEntry e;
e.K_o = b->m_ephPublicKeys[i];
e.amount = b->m_outputAmounts[i].m_reward;
if (i < b->m_viewTags.size() && b->m_viewTags[i].size() >= 3)
memcpy(e.view_tag, b->m_viewTags[i].data(), 3);
else
memset(e.view_tag, 0, 3);
if (i < b->m_encryptedAnchors.size() && b->m_encryptedAnchors[i].size() >= 16)
memcpy(e.anchor, b->m_encryptedAnchors[i].data(), 16);
else
memset(e.anchor, 0, 16);
merged.push_back(e);
}
for (const auto& po : b->m_protocolOutputs) {
MergedEntry e;
e.K_o = po.onetime_address;
e.amount = po.amount;
memcpy(e.view_tag, po.view_tag, 3);
memcpy(e.anchor, po.anchor_enc, 16);
merged.push_back(e);
}
std::sort(merged.begin(), merged.end(), [](const MergedEntry& a, const MergedEntry& b) {
return memcmp(a.K_o.h, b.K_o.h, HASH_SIZE) < 0;
});
out_blob.reserve(merged.size() * 60 + 4);
writeVarint(merged.size(), out_blob);
for (const auto& e : merged) {
writeVarint(e.amount, out_blob);
out_blob.push_back(TXOUT_TO_CARROT_V1);
out_blob.insert(out_blob.end(), e.K_o.h, e.K_o.h + HASH_SIZE);
out_blob.push_back(4);
out_blob.push_back('S');
out_blob.push_back('A');
out_blob.push_back('L');
out_blob.push_back('1');
out_blob.insert(out_blob.end(), e.view_tag, e.view_tag + 3);
out_blob.insert(out_blob.end(), e.anchor, e.anchor + 16);
}
};
auto it = block->m_sidechainId.empty() ? m_blocksById.end() : m_blocksById.find(block->m_sidechainId);
if (it != m_blocksById.end()) {
const PoolBlock* b = it->second;
const size_t n = b->m_outputAmounts.size();
blob.reserve(n * 58 + 64);
writeVarint(n, blob);
for (size_t i = 0; i < n; ++i) {
const PoolBlock::TxOutput& output = b->m_outputAmounts[i];
writeVarint(output.m_reward, blob);
blob.emplace_back(TXOUT_TO_CARROT_V1);
const hash h = b->m_ephPublicKeys[i];
blob.insert(blob.end(), h.h, h.h + HASH_SIZE);
if (b->m_majorVersion >= 10) {
// Carrot v1 format
blob.push_back(4);
blob.push_back('S');
blob.push_back('A');
blob.push_back('L');
blob.push_back('1');
blob.insert(blob.end(), b->m_viewTags[i].begin(), b->m_viewTags[i].end());
blob.insert(blob.end(), b->m_encryptedAnchors[i].begin(), b->m_encryptedAnchors[i].end());
} else {
blob.emplace_back(static_cast<uint8_t>(output.m_viewTag));
}
}
build_merged_blob(b, blob);
block->m_ephPublicKeys = b->m_ephPublicKeys;
block->m_outputAmounts = b->m_outputAmounts;
block->m_viewTags = b->m_viewTags;
block->m_encryptedAnchors = b->m_encryptedAnchors;
block->m_protocolOutputs = b->m_protocolOutputs;
return true;
}
// For Carrot v1+ external blocks, K_o values are NOT derivable from m_txkeySec
// They must be preserved from parsing (computed via Carrot crypto)
if (block->m_majorVersion >= 10 && !block->m_ephPublicKeys.empty()) {
const size_t n = block->m_ephPublicKeys.size();
blob.reserve(n * 58 + 64);
writeVarint(n, blob);
for (size_t i = 0; i < n; ++i) {
const PoolBlock::TxOutput& output = block->m_outputAmounts[i];
writeVarint(output.m_reward, blob);
blob.emplace_back(TXOUT_TO_CARROT_V1);
const hash eph_key = block->m_ephPublicKeys[i];
blob.insert(blob.end(), eph_key.h, eph_key.h + HASH_SIZE);
// Carrot v1 format
blob.push_back(4);
blob.push_back('S');
blob.push_back('A');
blob.push_back('L');
blob.push_back('1');
blob.insert(blob.end(), block->m_viewTags[i].begin(), block->m_viewTags[i].end());
blob.insert(blob.end(), block->m_encryptedAnchors[i].begin(), block->m_encryptedAnchors[i].end());
}
build_merged_blob(block, blob);
return true;
}
@@ -2003,15 +2016,18 @@ void SideChain::verify(PoolBlock* block)
shares.emplace_back(total_weight, m_devWallet);
}
// After parser post-processing, m_outputAmounts contains only miner outputs (protocol outputs removed)
if (shares.size() != block->m_outputAmounts.size()) {
LOGWARN(3, "block at height = " << block->m_sidechainHeight <<
", id = " << block->m_sidechainId <<
", mainchain height = " << block->m_txinGenHeight
<< " has invalid number of outputs: got " << block->m_outputAmounts.size() << ", expected " << shares.size());
<< " has invalid number of outputs: got " << block->m_outputAmounts.size() << " miner + "
<< block->m_protocolOutputs.size() << " protocol, expected " << shares.size() << " miner");
block->m_invalid = true;
return;
}
// total_reward is sum of miner outputs only (protocol outputs are separate)
uint64_t total_reward = std::accumulate(block->m_outputAmounts.begin(), block->m_outputAmounts.end(), 0ULL,
[](uint64_t a, const PoolBlock::TxOutput& b)
{