From 728d6c2cbfff11a4e628ab5e99cc1ab817610640 Mon Sep 17 00:00:00 2001 From: Matt Hess Date: Sun, 7 Dec 2025 23:57:46 +0000 Subject: [PATCH] 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. --- src/block_template.cpp | 15 +++++++++++ src/pool_block.cpp | 55 ++++++++++++++++++++++++++++++++------- src/pool_block_parser.inl | 26 +++++++++++++----- src/side_chain.cpp | 40 +++++++++++++++++++--------- src/stratum_server.cpp | 13 +++++++++ 5 files changed, 121 insertions(+), 28 deletions(-) diff --git a/src/block_template.cpp b/src/block_template.cpp index 090148f..34370bb 100644 --- a/src/block_template.cpp +++ b/src/block_template.cpp @@ -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(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(protocol_tx_hash)); + } b->m_ephPublicKeys.clear(); b->m_outputAmounts.clear(); b->m_viewTags.clear(); diff --git a/src/pool_block.cpp b/src/pool_block.cpp index 76f545f..cab589d 100644 --- a/src/pool_block.cpp +++ b/src/pool_block.cpp @@ -180,6 +180,24 @@ std::vector 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(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 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 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(protocol_tx_hash); + } + size_t header_size, miner_tx_size; + const std::vector 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(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(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); } diff --git a/src/pool_block_parser.inl b/src/pool_block_parser.inl index 90ede00..ba07737 100644 --- a/src/pool_block_parser.inl +++ b/src/pool_block_parser.inl @@ -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(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& consensus_id = sidechain.consensus_id(); const int data_size = static_cast((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(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(protocol_tx_hash); + } + reset_offchain_data(); return 0; } diff --git a/src/side_chain.cpp b/src/side_chain.cpp index ec61e5e..4519d10 100644 --- a/src/side_chain.cpp +++ b/src/side_chain.cpp @@ -625,6 +625,8 @@ bool SideChain::add_external_block(PoolBlock& block, std::vector& 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& 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 = {}; diff --git a/src/stratum_server.cpp b/src/stratum_server.cpp index c3652b5..1036dda 100644 --- a/src/stratum_server.cpp +++ b/src/stratum_server.cpp @@ -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(share->m_clientAddrString) << " couldn't check share PoW"); share->m_result = SubmittedShare::Result::COULDNT_CHECK_POW;