Fix Carrot v1 protocol TX handling for peer synchronization

Root cause: Merkle verification failures (error 659) and peer bans occurred
because protocol TX was not consistently present in m_transactions[1].

Parser fix (pool_block_parser.inl):
- Skip dummy transactions[0] entry when populating m_transactions
- Add protocol TX computation after parsing for Carrot v1 blocks

Block template fix (block_template.cpp):
- Insert protocol TX at position [1] during block creation in update()
- Insert protocol TX during select_mempool_transactions()

get_pow_hash fix (pool_block.cpp):
- Ensure protocol TX is populated before serialize_mainchain_data()

The protocol TX must be at m_transactions[1] before ANY serialization
occurs, otherwise sender and receiver compute different merkle roots.

Tested: Multiple restart cycles with two nodes, no bans, chains stay synced.
This commit is contained in:
Matt Hess
2025-12-07 23:57:46 +00:00
parent 00fb078004
commit 728d6c2cbf
5 changed files with 121 additions and 28 deletions
+15
View File
@@ -682,6 +682,15 @@ void BlockTemplate::update(const MinerData& data, const Mempool& mempool, const
m_poolBlockTemplate->m_transactions.push_back(m_mempoolTxs[m_mempoolTxsOrder[i]].id);
}
// For Carrot v1 blocks, insert protocol TX at position 1
if (m_poolBlockTemplate->m_majorVersion >= 10) {
hash protocol_tx_hash;
calculate_protocol_tx_hash(m_poolBlockTemplate->m_txinGenHeight, protocol_tx_hash);
m_poolBlockTemplate->m_transactions.insert(
m_poolBlockTemplate->m_transactions.begin() + 1,
static_cast<indexed_hash>(protocol_tx_hash));
}
m_poolBlockTemplate->m_minerWallet = params->m_miningWallet;
// Layout: [software id, version, random number, sidechain extra_nonce]
@@ -952,6 +961,12 @@ 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
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_ephPublicKeys.clear();
b->m_outputAmounts.clear();
b->m_viewTags.clear();
+45 -10
View File
@@ -180,6 +180,24 @@ std::vector<uint8_t> PoolBlock::serialize_mainchain_data(size_t* header_size, si
LOGINFO(0, "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(0, "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(0, "DEBUG serialize viewTag[0]=" << (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(0, "DEBUG serialize encAnchor[0]=" << hex);
}
LOGINFO(0, "DEBUG serialize D_e=" << m_txkeyPub);
LOGINFO(0, "DEBUG serialize merkleRoot=" << static_cast<const hash&>(m_merkleRoot));
for (size_t i = 0, n = m_outputAmounts.size(); i < n; ++i) {
const TxOutput& output = m_outputAmounts[i];
@@ -302,6 +320,11 @@ std::vector<uint8_t> PoolBlock::serialize_mainchain_data(size_t* header_size, si
}
if (include_tx_hashes) {
writeVarint(m_transactions.size() - 1, data);
if (m_transactions.size() > 1) {
LOGINFO(0, "DEBUG serialize tx: m_transactions[1]=" << m_transactions[1] << " sidechainHeight=" << m_sidechainHeight);
}
#ifdef WITH_INDEXED_HASHES
for (size_t i = 1, n = m_transactions.size(); i < n; ++i) {
const hash h = m_transactions[i];
@@ -434,9 +457,18 @@ bool PoolBlock::get_pow_hash(RandomX_Hasher_Base* hasher, uint64_t height, const
uint8_t blob[128];
size_t blob_size = 0;
{
size_t header_size, miner_tx_size;
const std::vector<uint8_t> mainchain_data = serialize_mainchain_data(&header_size, &miner_tx_size, nullptr, nullptr, nullptr, nullptr);
{
// For Carrot v1 blocks, ensure m_transactions has space for protocol TX before serializing
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);
m_transactions[1] = static_cast<indexed_hash>(protocol_tx_hash);
}
size_t header_size, miner_tx_size;
const std::vector<uint8_t> mainchain_data = serialize_mainchain_data(&header_size, &miner_tx_size, nullptr, nullptr, nullptr, nullptr);
if (!header_size || !miner_tx_size || (mainchain_data.size() < header_size + miner_tx_size)) {
LOGERR(1, "tried to calculate PoW of uninitialized block");
@@ -473,13 +505,6 @@ bool PoolBlock::get_pow_hash(RandomX_Hasher_Base* hasher, uint64_t height, const
// Save the coinbase tx hash into the first element of m_transactions
m_transactions[0] = static_cast<indexed_hash>(tmp);
// For Carrot v1 blocks, compute and store protocol TX hash at position 1
if ((m_majorVersion >= 10) && (m_transactions.size() >= 2)) {
hash protocol_tx_hash;
calculate_protocol_tx_hash(m_txinGenHeight, protocol_tx_hash);
m_transactions[1] = static_cast<indexed_hash>(protocol_tx_hash);
}
// DEBUG: dump m_transactions for comparison
LOGINFO(0, "get_pow_hash: m_transactions.size()=" << m_transactions.size());
for (size_t dbg_i = 0; dbg_i < m_transactions.size(); ++dbg_i) {
@@ -517,6 +542,16 @@ bool PoolBlock::get_pow_hash(RandomX_Hasher_Base* hasher, uint64_t height, const
// cppcheck-suppress danglingLifetime
m_hashingBlob.assign(blob, blob + blob_size);
{
std::string hex;
for (size_t i = 0; i < blob_size; ++i) {
char buf[4];
snprintf(buf, sizeof(buf), "%02x", blob[i]);
hex += buf;
}
LOGINFO(0, "DEBUG hashing blob (" << blob_size << " bytes): " << hex);
}
return hasher->calculate(blob, blob_size, height, seed_hash, pow_hash, force_light_mode);
}
+20 -6
View File
@@ -446,10 +446,6 @@ skip_protocol_tx:
const uint64_t n = transactions.size();
if (n > 0) {
m_transactions.emplace_back(transactions[0]);
}
for (uint64_t i = 1; i < n; ++i) {
const uint64_t parent_index = parent_indices[i];
if (parent_index) {
@@ -465,8 +461,8 @@ skip_protocol_tx:
}
}
else {
for (const hash& h : transactions) {
m_transactions.emplace_back(h);
for (size_t i = 1; i < transactions.size(); ++i) {
m_transactions.emplace_back(transactions[i]);
}
}
@@ -575,6 +571,12 @@ skip_protocol_tx:
const uint8_t* transactions_blob = reinterpret_cast<uint8_t*>(transactions.data() + 1);
if (transactions_blob_size > 0) {
hash tx_h;
memcpy(tx_h.h, transactions_blob, HASH_SIZE);
LOGINFO(0, "DEBUG transactions_blob first hash: " << tx_h);
}
#if POOL_BLOCK_DEBUG
memcpy(m_mainChainDataDebug.data() + outputs_offset, outputs_blob.data(), outputs_blob_size);
memcpy(m_mainChainDataDebug.data() + transactions_offset + outputs_blob_size_diff, transactions_blob, transactions_blob_size);
@@ -584,6 +586,8 @@ skip_protocol_tx:
const std::vector<uint8_t>& consensus_id = sidechain.consensus_id();
const int data_size = static_cast<int>((data_end - data_begin) + outputs_blob_size_diff + transactions_blob_size_diff);
LOGINFO(0, "DEBUG hash params: data_size=" << data_size << " outputs_offset=" << outputs_offset << " outputs_blob_size=" << outputs_blob_size << " outputs_blob_size_diff=" << outputs_blob_size_diff << " transactions_offset=" << transactions_offset << " transactions_blob_size=" << transactions_blob_size << " transactions_blob_size_diff=" << transactions_blob_size_diff << " nonce_offset=" << nonce_offset << " extra_nonce_offset=" << extra_nonce_offset << " mm_root_hash_offset=" << mm_root_hash_offset << " consensus_id_size=" << consensus_id.size());
if (data_size > static_cast<int>(MAX_BLOCK_SIZE)) {
return __LINE__;
}
@@ -660,6 +664,16 @@ skip_protocol_tx:
return __LINE__;
}
// For Carrot v1 blocks, ensure protocol TX 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);
m_transactions[1] = static_cast<indexed_hash>(protocol_tx_hash);
}
reset_offchain_data();
return 0;
}
+28 -12
View File
@@ -625,6 +625,8 @@ bool SideChain::add_external_block(PoolBlock& block, std::vector<hash>& missing_
return false;
}
LOGINFO(0, "DEBUG get_pow_hash: seed=" << block.m_seed << " txinGenHeight=" << block.m_txinGenHeight);
if (!block.get_pow_hash(m_pool->hasher(), block.m_txinGenHeight, block.m_seed, block.m_powHash)) {
LOGWARN(3, "add_external_block: couldn't get PoW hash for height = " << block.m_sidechainHeight << ", mainchain height " << block.m_txinGenHeight << ". Ignoring it.");
forget_incoming_block(block);
@@ -648,6 +650,8 @@ bool SideChain::add_external_block(PoolBlock& block, std::vector<hash>& missing_
}
}
LOGINFO(0, "DEBUG PoW check: sidechainHeight=" << block.m_sidechainHeight << " m_difficulty=" << block.m_difficulty << " m_powHash=" << block.m_powHash);
if (!block.m_difficulty.check_pow(block.m_powHash)) {
LOGWARN(3,
"add_external_block mined by " << block.m_minerWallet <<
@@ -2491,19 +2495,26 @@ bool SideChain::consider_peer_genesis(const hash& genesis_id, uint64_t timestamp
}
}
// If we have a genesis and peer's is older (or same timestamp but lower hash), we need to yield
if (m_genesisDecisionMade && our_genesis_timestamp > 0) {
const bool peer_wins = (timestamp < our_genesis_timestamp) ||
(timestamp == our_genesis_timestamp && genesis_id < our_genesis_id);
if (peer_wins) {
LOGWARN(3, "Peer has older genesis (theirs=" << timestamp
<< " ours=" << our_genesis_timestamp << "), purging to re-sync");
purge_sidechain();
// Fall through to adopt peer's genesis
// If we have a genesis and peer's is older (or same timestamp but lower hash), we need to yield
if (m_genesisDecisionMade) {
if (our_genesis_timestamp > 0) {
// We have an actual genesis block - check if peer's is older
const bool peer_wins = (timestamp < our_genesis_timestamp) ||
(timestamp == our_genesis_timestamp && genesis_id < our_genesis_id);
if (peer_wins) {
LOGWARN(3, "Peer has older genesis (theirs=" << timestamp
<< " ours=" << our_genesis_timestamp << "), purging to re-sync");
purge_sidechain();
// Fall through to adopt peer's genesis
} else {
// Our genesis is older or same, keep it
return true;
}
} else {
// Our genesis is older or same, keep it
return true;
// We decided to create our own genesis but haven't mined it yet
// Reset and adopt peer's genesis instead
LOGINFO(3, "Canceling own genesis creation, will adopt peer's genesis");
m_genesisDecisionMade = false;
}
}
@@ -2540,6 +2551,11 @@ void SideChain::purge_sidechain()
// Clear difficulty data
m_difficultyData.clear();
{
WriteLock diff_lock(m_curDifficultyLock);
m_curDifficulty = m_minDifficulty;
}
// Reset genesis state to allow new genesis adoption
m_genesisDecisionMade = false;
m_adoptedGenesisId = {};
+13
View File
@@ -983,6 +983,19 @@ void StratumServer::on_share_found(uv_work_t* req)
}
hash pow_hash;
LOGINFO(0, "DEBUG stratum hash: height=" << height << " seed=" << seed_hash << " blob_size=" << blob_size);
{
std::string hex;
for (size_t i = 0; i < blob_size; ++i) {
char buf[4];
snprintf(buf, sizeof(buf), "%02x", blob[i]);
hex += buf;
}
LOGINFO(0, "DEBUG stratum blob: " << hex);
}
if (!pool->calculate_hash(blob, blob_size, height, seed_hash, pow_hash, false)) {
LOGWARN(3, "client " << static_cast<char*>(share->m_clientAddrString) << " couldn't check share PoW");
share->m_result = SubmittedShare::Result::COULDNT_CHECK_POW;