From 00fb07800420484c15bc70976dc5b46159ae8a86 Mon Sep 17 00:00:00 2001 From: Matt Hess Date: Fri, 5 Dec 2025 23:44:53 +0000 Subject: [PATCH] Implement genesis reconciliation protocol for sidechain stability Problem: P2Pool nodes starting at different times or experiencing network issues would create independent genesis blocks, resulting in incompatible chains. Nodes would ban each other for invalid blocks that were actually valid on a different chain. Cache resume after restart frequently failed due to genesis mismatch between nodes. Solution: Oldest compatible genesis wins protocol that coordinates genesis selection across peers before mining begins. New P2P message GENESIS_INFO exchanges: - Genesis block hash - Genesis timestamp - Genesis mainchain height - Protocol version Startup behavior: - Wait up to 90 seconds for peer genesis info (with progress logging) - Adopt oldest genesis from compatible peers - Only create own genesis if no peers respond Late joiner reconciliation: - Running nodes that receive older genesis from new peer will purge their sidechain and re-sync to the older chain - Cache files deleted on purge to prevent reload of stale blocks Protocol versioning: - PROTOCOL_VERSION constant at top of side_chain.h - Increment only on consensus-breaking changes - Version mismatch logs warning, prevents genesis adoption Tiebreaker: When timestamps match, lexicographically lower hash wins. --- sidechain_config.json | 2 +- src/block_cache.cpp | 35 +++- src/block_template.cpp | 239 ++++++++++++++--------- src/carrot_crypto.cpp | 35 ++++ src/carrot_crypto.h | 17 ++ src/p2p_server.cpp | 149 +++++++++++++-- src/p2p_server.h | 6 +- src/p2pool.cpp | 18 +- src/p2pool.h | 2 +- src/params.cpp | 25 ++- src/params.h | 5 + src/pool_block.cpp | 176 ++++++++++++----- src/pool_block.h | 4 +- src/pool_block_parser.inl | 120 +++++++++--- src/side_chain.cpp | 388 ++++++++++++++++++++++++++++++-------- src/side_chain.h | 25 ++- src/wallet.cpp | 43 +++++ src/wallet.h | 1 + 18 files changed, 1011 insertions(+), 279 deletions(-) diff --git a/sidechain_config.json b/sidechain_config.json index 5c49a7d..cca55d3 100644 --- a/sidechain_config.json +++ b/sidechain_config.json @@ -8,7 +8,7 @@ "name": "salvium_main", "password": "", "block_time": 10, - "min_diff": 100000, + "min_diff": 10000, "pplns_window": 2160, "uncle_penalty": 20 } diff --git a/src/block_cache.cpp b/src/block_cache.cpp index d4bc1a4..84868ec 100644 --- a/src/block_cache.cpp +++ b/src/block_cache.cpp @@ -20,6 +20,7 @@ #include "pool_block.h" #include "p2p_server.h" #include "side_chain.h" +#include LOG_CATEGORY(BlockCache) @@ -27,6 +28,7 @@ static constexpr uint32_t BLOCK_SIZE = 96 * 1024; static constexpr uint32_t NUM_BLOCKS = 4608; static constexpr uint32_t CACHE_SIZE = BLOCK_SIZE * NUM_BLOCKS; static constexpr char cache_name[] = "p2pool.cache"; +static const uint64_t CACHE_VERSION = std::hash{}(__DATE__ __TIME__); namespace p2pool { @@ -191,14 +193,33 @@ void BlockCache::store(const PoolBlock& block) void BlockCache::load_all(const SideChain& side_chain, P2PServer& server) { - if (!m_impl->m_data) { - return; - } + // Check cache version - invalidates on recompile + const std::string version_path = DATA_DIR + "p2pool.cache.version"; + bool version_ok = false; + { + std::ifstream f(version_path); + uint64_t v = 0; + if ((f >> v) && (v == CACHE_VERSION)) { + version_ok = true; + } + } + if (!version_ok) { + LOGINFO(1, "cache version mismatch (recompiled binary), clearing cache"); + if (m_impl->m_data) { + memset(m_impl->m_data, 0, CACHE_SIZE); + } + std::ofstream f(version_path); + f << CACHE_VERSION; + } - // Can be only called once - if (m_loadingStarted.exchange(1)) { - return; - } + if (!m_impl->m_data) { + return; + } + + // Can be only called once + if (m_loadingStarted.exchange(1)) { + return; + } LOGINFO(1, "loading cached blocks"); diff --git a/src/block_template.cpp b/src/block_template.cpp index 9f33703..090148f 100644 --- a/src/block_template.cpp +++ b/src/block_template.cpp @@ -383,6 +383,11 @@ void BlockTemplate::update(const MinerData& data, const Mempool& mempool, const return; } + // DEBUG: Show share/reward assignment + for (size_t i = 0; i < m_shares.size(); ++i) { + LOGINFO(1, "BlockTemplate share[" << i << "]: spend_key=" << m_shares[i].m_wallet->spend_public_key() << " weight=" << m_shares[i].m_weight << " reward=" << m_rewards[i]); + } + auto get_reward_amounts_weight = [this]() { return std::accumulate(m_rewards.begin(), m_rewards.end(), 0ULL, [](uint64_t a, uint64_t b) @@ -949,6 +954,8 @@ void BlockTemplate::select_mempool_transactions(const Mempool& mempool) b->m_transactions.resize(1); b->m_ephPublicKeys.clear(); b->m_outputAmounts.clear(); + b->m_viewTags.clear(); + b->m_encryptedAnchors.clear(); // Block template size without coinbase outputs and transactions (minus 2 bytes for output and tx count dummy varints) size_t k = b->serialize_mainchain_data().size() + b->serialize_sidechain_data().size() - 2; @@ -974,6 +981,7 @@ void BlockTemplate::select_mempool_transactions(const Mempool& mempool) if (max_transactions == 0) { m_mempoolTxs.clear(); + } else if (m_mempoolTxs.size() > max_transactions) { std::nth_element(m_mempoolTxs.begin(), m_mempoolTxs.begin() + max_transactions, m_mempoolTxs.end()); @@ -1003,6 +1011,8 @@ int BlockTemplate::create_miner_tx(const MinerData& data, const std::vectorm_ephPublicKeys.clear(); 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); @@ -1012,96 +1022,129 @@ int BlockTemplate::create_miner_tx(const MinerData& data, const std::vector 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) { - // Amount (not encrypted for coinbase) - writeVarint(m_rewards[i], [this, &reward_amounts_weight](uint8_t b) { - m_minerTx.push_back(b); - ++reward_amounts_weight; - }); + CarrotOutput out; + out.share_index = i; + out.amount = m_rewards[i]; + memset(out.onetime_address.h, 0, HASH_SIZE); + memset(out.eph_pubkey.h, 0, HASH_SIZE); + memset(out.view_tag, 0, 3); + memset(out.encrypted_anchor, 0, 16); - // txout_to_carrot_v1 structure - m_minerTx.push_back(TXOUT_TO_CARROT_V1); // variant tag = 4 + if (!dry_run) { + // Derive anchor from wallet's spend public key (position-independent) + uint8_t anchor[16]; + carrot::derive_deterministic_anchor_from_pubkey( + m_poolBlockTemplate->m_txkeySecSeed, + shares[i].m_wallet->spend_public_key(), + anchor); - // K_o - onetime address (32 bytes) - hash onetime_address; - hash ephemeral_pubkey; - uint8_t view_tag[3] = {0}; - uint8_t encrypted_anchor[16] = {0}; - - if (!dry_run) { - // Generate janus anchor (randomness for this output) - uint8_t anchor[16]; - carrot::generate_janus_anchor(anchor); - - // Generate ephemeral private key - hash ephemeral_privkey; - carrot::make_ephemeral_privkey( - anchor, - input_context, - shares[i].m_wallet->spend_public_key(), - null_payment_id, - ephemeral_privkey); - - // Generate ephemeral public key D_e - carrot::make_ephemeral_pubkey_mainaddress(ephemeral_privkey, ephemeral_pubkey); - - // Generate shared secret (sender-side ECDH) - hash shared_secret_unctx; - if (!carrot::make_shared_secret_sender( - ephemeral_privkey, - shares[i].m_wallet->view_public_key(), - shared_secret_unctx)) { - LOGERR(1, "Failed to generate shared secret for output " << i); - return -4; - } - - // Generate contextualized sender-receiver secret - hash sender_receiver_secret; - carrot::make_sender_receiver_secret( - shared_secret_unctx, - ephemeral_pubkey, - input_context, - sender_receiver_secret); - - // Generate onetime address K_o - carrot::make_onetime_address_coinbase( - shares[i].m_wallet->spend_public_key(), - sender_receiver_secret, - m_rewards[i], - onetime_address); - - // Generate 3-byte view tag - carrot::make_view_tag(shared_secret_unctx, input_context, onetime_address, view_tag); - - // Encrypt janus anchor - carrot::encrypt_anchor(anchor, sender_receiver_secret, onetime_address, encrypted_anchor); - - // Save for pool block template - m_poolBlockTemplate->m_ephPublicKeys.emplace_back(ephemeral_pubkey); - m_poolBlockTemplate->m_outputAmounts.emplace_back(m_rewards[i], view_tag[0]); + // Derive per-output ephemeral private key d_e from anchor + hash eph_privkey; + carrot::make_ephemeral_privkey( + anchor, + input_context, + shares[i].m_wallet->spend_public_key(), + null_payment_id, + eph_privkey); - // Save view_tag and encrypted_anchor for later - std::vector vt(view_tag, view_tag + 3); - std::vector ea(encrypted_anchor, encrypted_anchor + 16); - m_poolBlockTemplate->m_viewTags.push_back(vt); - m_poolBlockTemplate->m_encryptedAnchors.push_back(ea); + // Derive per-output ephemeral public key D_e = d_e * B + carrot::make_ephemeral_pubkey_mainaddress(eph_privkey, out.eph_pubkey); + + // Generate shared secret using per-output d_e + hash shared_secret_unctx; + if (!carrot::make_shared_secret_sender( + eph_privkey, + shares[i].m_wallet->view_public_key(), + shared_secret_unctx)) { + LOGERR(1, "Failed to generate shared secret for output " << i); + return -4; } - // Write output data (zeros for dry_run, real values otherwise) - m_minerTx.insert(m_minerTx.end(), onetime_address.h, onetime_address.h + HASH_SIZE); + // Generate sender-receiver secret using per-output D_e + hash sender_receiver_secret; + carrot::make_sender_receiver_secret( + shared_secret_unctx, + out.eph_pubkey, + input_context, + sender_receiver_secret); - // asset_type - string "SAL1" - m_minerTx.push_back(4); // string length + // Generate onetime address K_o + carrot::make_onetime_address_coinbase( + shares[i].m_wallet->spend_public_key(), + sender_receiver_secret, + out.amount, + out.onetime_address); + + // Generate 3-byte view tag + carrot::make_view_tag(shared_secret_unctx, input_context, out.onetime_address, out.view_tag); + + // Encrypt anchor + carrot::encrypt_anchor(anchor, sender_receiver_secret, out.onetime_address, out.encrypted_anchor); + } + outputs.push_back(out); + } + + // Sort 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) + for (const auto& out : outputs) { + // Amount + 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); + + if (dry_run) { + m_minerTx.insert(m_minerTx.end(), HASH_SIZE, 0); + m_minerTx.push_back(4); m_minerTx.push_back('S'); m_minerTx.push_back('A'); m_minerTx.push_back('L'); m_minerTx.push_back('1'); + m_minerTx.insert(m_minerTx.end(), 3, 0); + m_minerTx.insert(m_minerTx.end(), 16, 0); + } else { + m_minerTx.insert(m_minerTx.end(), out.onetime_address.h, out.onetime_address.h + HASH_SIZE); + m_minerTx.push_back(4); + m_minerTx.push_back('S'); + m_minerTx.push_back('A'); + m_minerTx.push_back('L'); + m_minerTx.push_back('1'); + 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); - // view_tag and encrypted_anchor - m_minerTx.insert(m_minerTx.end(), view_tag, view_tag + 3); - m_minerTx.insert(m_minerTx.end(), encrypted_anchor, 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 vt(out.view_tag, out.view_tag + 3); + std::vector ea(out.encrypted_anchor, out.encrypted_anchor + 16); + m_poolBlockTemplate->m_viewTags.push_back(vt); + m_poolBlockTemplate->m_encryptedAnchors.push_back(ea); + } } if (dry_run) { @@ -1114,19 +1157,40 @@ int BlockTemplate::create_miner_tx(const MinerData& data, const std::vector(dry_run)=" << static_cast(dry_run) << ", num_eph_keys=" << m_poolBlockTemplate->m_ephPublicKeys.size()); + // TX_EXTRA - per-output D_e for Janus protection + LOGINFO(3, "DEBUG: Carrot extra - major_version=" << data.major_version << ", dry_run=" << static_cast(dry_run) << ", num_outputs=" << num_outputs); m_minerTxExtra.clear(); + m_poolBlockTemplate->m_additionalPubKeys.clear(); - // Carrot v1: TX_EXTRA contains ephemeral pubkey, extra_nonce, and merge mining tag - // Ephemeral pubkey - m_minerTxExtra.push_back(TX_EXTRA_TAG_PUBKEY); - if (dry_run) { - m_minerTxExtra.insert(m_minerTxExtra.end(), HASH_SIZE, 0); + if (num_outputs == 1) { + // Single output: use TX_EXTRA_TAG_PUBKEY + m_minerTxExtra.push_back(TX_EXTRA_TAG_PUBKEY); + if (dry_run) { + m_minerTxExtra.insert(m_minerTxExtra.end(), HASH_SIZE, 0); + } else { + m_poolBlockTemplate->m_txkeyPub = outputs[0].eph_pubkey; + m_minerTxExtra.insert(m_minerTxExtra.end(), + outputs[0].eph_pubkey.h, + outputs[0].eph_pubkey.h + HASH_SIZE); + } } else { - m_minerTxExtra.insert(m_minerTxExtra.end(), - m_poolBlockTemplate->m_ephPublicKeys[0].h, - m_poolBlockTemplate->m_ephPublicKeys[0].h + HASH_SIZE); + // Multiple outputs: use TX_EXTRA_TAG_ADDITIONAL_PUBKEYS with ALL D_e values + 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; + 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); + } + } + } } // Extra nonce @@ -1165,6 +1229,7 @@ int BlockTemplate::create_miner_tx(const MinerData& data, const std::vectorm_amountBurnt = stake_amount; // Save prefix size - everything up to here is the transaction prefix m_minerTxPrefixSize = static_cast(m_minerTx.size()); diff --git a/src/carrot_crypto.cpp b/src/carrot_crypto.cpp index 633141e..babfe08 100644 --- a/src/carrot_crypto.cpp +++ b/src/carrot_crypto.cpp @@ -418,6 +418,41 @@ void encrypt_anchor( debug_hex("anchor encrypted", encrypted_anchor, 16); } +void derive_deterministic_anchor_from_pubkey( + const hash& tx_key_seed, + const hash& spend_public_key, + uint8_t (&anchor)[16]) +{ + // anchor = H_16("p2pool_anchor_v2" || tx_key_seed || spend_public_key) + const char domain[] = "p2pool_anchor_v2"; + uint8_t data[sizeof(domain) + 32 + 32]; + memcpy(data, domain, sizeof(domain)); + memcpy(data + sizeof(domain), tx_key_seed.h, 32); + memcpy(data + sizeof(domain) + 32, spend_public_key.h, 32); + + hash h; + blake2b_hash(h.h, 16, data, sizeof(data), nullptr, 0); + memcpy(anchor, h.h, 16); +} + +void derive_transaction_ephemeral_privkey( + const hash& tx_key_seed, + const uint8_t (&input_context)[33], + hash& ephemeral_privkey_out) +{ + // d_e = H_n("p2pool_tx_eph_v1" || tx_key_seed || input_context) + const char domain[] = "p2pool_tx_eph_v1"; + uint8_t data[sizeof(domain) + 32 + 33]; + memcpy(data, domain, sizeof(domain)); + memcpy(data + sizeof(domain), tx_key_seed.h, 32); + memcpy(data + sizeof(domain) + 32, input_context, 33); + + // Derive scalar (reduces to valid ed25519 scalar) + derive_scalar(data, sizeof(data), nullptr, ephemeral_privkey_out.h); + + debug_hex("tx_eph d_e", ephemeral_privkey_out.h, 32); +} + } // namespace carrot } // namespace p2pool diff --git a/src/carrot_crypto.h b/src/carrot_crypto.h index fc53374..e3d4167 100644 --- a/src/carrot_crypto.h +++ b/src/carrot_crypto.h @@ -61,6 +61,23 @@ void encrypt_anchor( const hash& onetime_address, uint8_t (&encrypted_anchor)[16]); +// Derive deterministic anchor for P2Pool validation +void derive_deterministic_anchor( + const hash& tx_key_seed, + uint32_t output_index, + uint8_t (&anchor)[16]); + +// Derive deterministic anchor from wallet spend public key (position-independent) +void derive_deterministic_anchor_from_pubkey( + const hash& tx_key_seed, + const hash& spend_public_key, + uint8_t (&anchor)[16]); + +void derive_transaction_ephemeral_privkey( + const hash& tx_key_seed, + const uint8_t (&input_context)[33], + hash& ephemeral_privkey_out); + } // namespace carrot } // namespace p2pool diff --git a/src/p2p_server.cpp b/src/p2p_server.cpp index 8123fba..4e02eac 100644 --- a/src/p2p_server.cpp +++ b/src/p2p_server.cpp @@ -161,6 +161,31 @@ P2PServer::P2PServer(p2pool* pool) WriteLock lock(m_cachedBlocksLock); m_cache->load_all(m_pool->side_chain(), *this); m_cacheLoaded = true; + + // Bootstrap sidechain from cache - add blocks in height order + if (m_cachedBlocks && !m_cachedBlocks->empty()) { + LOGINFO(1, "bootstrapping sidechain from " << m_cachedBlocks->size() << " cached blocks"); + + // Collect and sort by height ascending + std::vector sorted_blocks; + sorted_blocks.reserve(m_cachedBlocks->size()); + for (const auto& it : *m_cachedBlocks) { + sorted_blocks.push_back(it.second); + } + std::sort(sorted_blocks.begin(), sorted_blocks.end(), + [](const PoolBlock* a, const PoolBlock* b) { + return a->m_sidechainHeight < b->m_sidechainHeight; + }); + + // Add in height order so parents exist before children + std::vector missing; + for (PoolBlock* block : sorted_blocks) { + (void)m_pool->side_chain().add_external_block(*block, missing); + } + + LOGINFO(1, "cache bootstrap complete, chain tip height = " << + (m_pool->side_chain().chainTip() ? m_pool->side_chain().chainTip()->m_sidechainHeight : 0)); + } } m_timer.data = this; @@ -1004,6 +1029,11 @@ P2PServer::Broadcast::Broadcast(const PoolBlock& block, const PoolBlock* parent) data->pruned_blob.insert(data->pruned_blob.end(), sidechain_data.begin(), sidechain_data.end()); + // Carrot v1 blocks have encrypted anchors that can't be deterministically reconstructed + // Force full blob broadcasts by clearing pruned/compact blobs + data->pruned_blob.clear(); + data->compact_blob.clear(); + data->ancestor_hashes.reserve(block.m_uncles.size() + 1); data->ancestor_hashes = block.m_uncles; data->ancestor_hashes.push_back(block.m_parent); @@ -1122,7 +1152,7 @@ void P2PServer::on_broadcast() return p - buf; } - bool send_pruned = true; + bool send_pruned = !data->pruned_blob.empty(); bool send_compact = (client->m_protocolVersion >= PROTOCOL_VERSION_1_1) && !data->compact_blob.empty() && (data->compact_blob.size() < data->pruned_blob.size()); for (const hash& id : data->ancestor_hashes) { @@ -2221,6 +2251,17 @@ bool P2PServer::P2PClient::on_read(const char* data, uint32_t size) } } break; + + case MessageId::GENESIS_INFO: + LOGINFO(5, "peer " << log::Gray() << static_cast(m_addrString) << log::NoColor() << " sent GENESIS_INFO"); + if (bytes_left >= 1 + HASH_SIZE + sizeof(uint64_t) + sizeof(uint64_t) + sizeof(uint32_t)) { + bytes_read = 1 + HASH_SIZE + sizeof(uint64_t) + sizeof(uint64_t) + sizeof(uint32_t); + if (!on_genesis_info(buf + 1)) { + return false; + } + } + break; + } if (bytes_read) { @@ -2579,6 +2620,23 @@ void P2PServer::P2PClient::on_after_handshake(uint8_t* &p) m_blockPendingRequests.push_back(0); m_lastBroadcastTimestamp = seconds_since_epoch(); + + // Send our genesis info for chain reconciliation + hash genesis_id; + uint64_t genesis_timestamp, genesis_height; + if (static_cast(m_owner)->m_pool->side_chain().get_genesis_info(genesis_id, genesis_timestamp, genesis_height)) { + LOGINFO(5, "sending GENESIS_INFO to " << static_cast(m_addrString) << " (v" << PROTOCOL_VERSION << ")"); + *(p++) = static_cast(MessageId::GENESIS_INFO); + memcpy(p, genesis_id.h, HASH_SIZE); + p += HASH_SIZE; + memcpy(p, &genesis_timestamp, sizeof(uint64_t)); + p += sizeof(uint64_t); + memcpy(p, &genesis_height, sizeof(uint64_t)); + p += sizeof(uint64_t); + const uint32_t version = PROTOCOL_VERSION; + memcpy(p, &version, sizeof(uint32_t)); + p += sizeof(uint32_t); + } } bool P2PServer::P2PClient::on_listen_port(const uint8_t* buf) @@ -3018,6 +3076,63 @@ void P2PServer::P2PClient::on_block_notify(const uint8_t* buf) } } +bool P2PServer::P2PClient::on_genesis_info(const uint8_t* buf) +{ + hash peer_genesis_id; + memcpy(peer_genesis_id.h, buf, HASH_SIZE); + + const uint64_t peer_timestamp = read_unaligned(reinterpret_cast(buf + HASH_SIZE)); + const uint64_t peer_height = read_unaligned(reinterpret_cast(buf + HASH_SIZE + sizeof(uint64_t))); + const uint32_t peer_version = read_unaligned(reinterpret_cast(buf + HASH_SIZE + sizeof(uint64_t) + sizeof(uint64_t))); + + P2PServer* server = static_cast(m_owner); + SideChain& side_chain = server->m_pool->side_chain(); + + LOGINFO(4, "peer " << log::Gray() << static_cast(m_addrString) << log::NoColor() + << " genesis: " << peer_genesis_id << " timestamp=" << peer_timestamp + << " height=" << peer_height << " version=" << peer_version); + + // Check protocol version compatibility + if (peer_version != PROTOCOL_VERSION) { + if (peer_version > PROTOCOL_VERSION) { + LOGWARN(3, "Peer " << static_cast(m_addrString) + << " running newer protocol v" << peer_version + << " (we have v" << PROTOCOL_VERSION << "). Upgrade recommended."); + } else { + LOGWARN(3, "Peer " << static_cast(m_addrString) + << " running older protocol v" << peer_version + << " (we have v" << PROTOCOL_VERSION << "). Ignoring their genesis."); + } + return true; // Don't fail connection, just don't adopt + } + + if (!side_chain.consider_peer_genesis(peer_genesis_id, peer_timestamp, peer_height)) { + return false; + } + + // If we don't have this block, request it + if (!server->find_block(peer_genesis_id)) { + LOGINFO(5, "Requesting genesis block " << peer_genesis_id << " from peer"); + const bool result = server->send(this, + [&peer_genesis_id, this](uint8_t* buf, size_t buf_size) -> size_t + { + if (buf_size < 1 + HASH_SIZE) { + return 0; + } + uint8_t* p = buf; + *(p++) = static_cast(MessageId::BLOCK_REQUEST); + memcpy(p, peer_genesis_id.h, HASH_SIZE); + p += HASH_SIZE; + return p - buf; + }); + if (result) { + m_blockPendingRequests.push_back(*peer_genesis_id.u64()); + } + } + + return true; +} + bool P2PServer::P2PClient::on_aux_job_donation(const uint8_t* buf, uint32_t size) { P2PServer* server = static_cast(m_owner); @@ -3491,20 +3606,26 @@ void P2PServer::P2PClient::post_handle_incoming_block(p2pool* pool, const PoolBl { const uint32_t new_reset_counter = m_resetCounter.load(); - if (!result) { - // Client sent bad data, disconnect and ban it - if (reset_counter == new_reset_counter) { - close(); - LOGWARN(3, "peer " << static_cast(m_addrString) << " banned for " << DEFAULT_BAN_TIME << " seconds"); - } - else { - LOGWARN(3, addr << " banned for " << DEFAULT_BAN_TIME << " seconds"); - } + if (!result) { + // Only ban if block was actually invalid, not just missing parents + if (missing_blocks.empty()) { + // Client sent bad data, disconnect and ban it + if (reset_counter == new_reset_counter) { + close(); + LOGWARN(3, "peer " << static_cast(m_addrString) << " banned for " << DEFAULT_BAN_TIME << " seconds"); + } + else { + LOGWARN(3, addr << " banned for " << DEFAULT_BAN_TIME << " seconds"); + } - P2PServer* server = pool->p2p_server(); - server->ban(is_v6, addr, DEFAULT_BAN_TIME); - server->remove_peer_from_list(addr); - } + P2PServer* server = pool->p2p_server(); + server->ban(is_v6, addr, DEFAULT_BAN_TIME); + server->remove_peer_from_list(addr); + return; + } + // Otherwise, missing_blocks has parents we need - fall through to request them + LOGINFO(5, "block verification pending, need " << missing_blocks.size() << " parent blocks"); + } // We might have been disconnected while side_chain was adding the block // In this case we can't send BLOCK_REQUEST messages on this connection anymore diff --git a/src/p2p_server.h b/src/p2p_server.h index 88dc24f..618b3f7 100644 --- a/src/p2p_server.h +++ b/src/p2p_server.h @@ -64,7 +64,10 @@ public: AUX_JOB_DONATION, // Broadcast 3rd-party Monero blocks to make the whole Monero network faster MONERO_BLOCK_BROADCAST, - LAST = MONERO_BLOCK_BROADCAST, + // Genesis reconciliation for chain compatibility + GENESIS_INFO, + LAST = GENESIS_INFO, + }; explicit P2PServer(p2pool *pool); @@ -122,6 +125,7 @@ public: [[nodiscard]] bool on_peer_list_request(const uint8_t* buf); void on_peer_list_response(const uint8_t* buf); void on_block_notify(const uint8_t* buf); + [[nodiscard]] bool on_genesis_info(const uint8_t* buf); [[nodiscard]] bool on_aux_job_donation(const uint8_t* buf, uint32_t size); [[nodiscard]] bool on_monero_block_broadcast(const uint8_t* buf, uint32_t size); diff --git a/src/p2pool.cpp b/src/p2pool.cpp index 4764de4..6863cde 100644 --- a/src/p2pool.cpp +++ b/src/p2pool.cpp @@ -351,20 +351,10 @@ void p2pool::print_hosts() const } } -bool p2pool::in_donation_mode() +bool p2pool::in_donation_mode(uint64_t height) { - const PoolBlock* tip = m_sideChain->chainTip(); - if (!tip) { - return false; - } - - // Check the NEXT block height (the one being created), not current tip - const uint64_t next_height = tip->m_sidechainHeight + 1; - - // Donate every 100th block (blocks 100, 200, 300, etc.) - const uint64_t cycle_length = 100; - - return (next_height % cycle_length) == 0; + const uint64_t cycle_length = (SideChain::network_type() == NetworkType::Mainnet) ? DONATION_CYCLE_MAINNET : DONATION_CYCLE_TESTNET; + return (height % cycle_length) == 0; } bool p2pool::calculate_hash(const void* data, size_t size, uint64_t height, const hash& seed, hash& result, bool force_light_mode) @@ -1290,7 +1280,7 @@ void p2pool::update_block_template() if (m_updateSeed.exchange(false)) { m_hasher->set_seed_async(data.seed_hash); } - m_blockTemplate->update(data, *m_mempool, m_params, in_donation_mode()); + m_blockTemplate->update(data, *m_mempool, m_params, in_donation_mode(data.height)); stratum_on_block(); api_update_pool_stats(); diff --git a/src/p2pool.h b/src/p2pool.h index d83c923..e69a615 100644 --- a/src/p2pool.h +++ b/src/p2pool.h @@ -49,7 +49,7 @@ public: void stop(); const Params& params() const { return *m_params; } - bool in_donation_mode(); + bool in_donation_mode(uint64_t height); BlockTemplate& block_template() { return *m_blockTemplate; } SideChain& side_chain() { return *m_sideChain; } diff --git a/src/params.cpp b/src/params.cpp index 4b5b154..7a34bc4 100644 --- a/src/params.cpp +++ b/src/params.cpp @@ -361,11 +361,26 @@ Params::Params(int argc, char* const argv[]) m_mainWallet.encode(display_wallet_buf); } - // Initialize dev wallet - const char* dev_wallet_str = "SC11VXXJyJTZcFJikJrgQKE2HmfXCt2DnRoM7tLB2vm3H2urbN1bUvaVGHY1osS4pmKrQ558cXmAf4nRYDayAmER6PYG6QRoNX"; - if (!m_devWallet.decode(dev_wallet_str)) { - LOGERR(1, "Failed to decode dev wallet address"); - } + // Initialize dev wallet based on network type + const char* dev_wallet_str = nullptr; + if (m_mainWallet.valid()) { + switch (m_mainWallet.type()) { + case NetworkType::Mainnet: + dev_wallet_str = "SC11VXXJyJTZcFJikJrgQKE2HmfXCt2DnRoM7tLB2vm3H2urbN1bUvaVGHY1osS4pmKrQ558cXmAf4nRYDayAmER6PYG6QRoNX"; + break; + case NetworkType::Testnet: + dev_wallet_str = "SC1ToqmproHYLhyPtehEMhE6dYRT7YziKRhaTtRCX2tVFDjqPJDXKtHPfLDvD6pn5b8pm1ZLrDQ2FfcHUEtapFDnbxk5RBBYmqT"; + break; + case NetworkType::Stagenet: + dev_wallet_str = ""; + break; + default: + break; + } + } + if (dev_wallet_str && !m_devWallet.decode(dev_wallet_str)) { + LOGERR(1, "Failed to decode dev wallet address"); + } m_displayWallet.assign(display_wallet_buf, Wallet::ADDRESS_LENGTH); } diff --git a/src/params.h b/src/params.h index d812d48..efd6152 100644 --- a/src/params.h +++ b/src/params.h @@ -22,6 +22,11 @@ namespace p2pool { static constexpr uint64_t DEFAULT_STRATUM_BAN_TIME = 600; +// Donation mode cycle lengths (in mainchain blocks) +// Mainnet: every 720 blocks (~1 day at 2 min block time) +// Testnet/Stagenet: every 100 blocks (~3.3 hours) +constexpr uint64_t DONATION_CYCLE_MAINNET = 720; +constexpr uint64_t DONATION_CYCLE_TESTNET = 100; struct Params { diff --git a/src/pool_block.cpp b/src/pool_block.cpp index 36efc82..76f545f 100644 --- a/src/pool_block.cpp +++ b/src/pool_block.cpp @@ -23,6 +23,7 @@ #include "protocol_tx_hash.h" #include "crypto.h" #include "merkle.h" +#include LOG_CATEGORY(PoolBlock) @@ -93,7 +94,11 @@ PoolBlock& PoolBlock::operator=(const PoolBlock& b) m_txinGenHeight = b.m_txinGenHeight; m_ephPublicKeys = b.m_ephPublicKeys; m_outputAmounts = b.m_outputAmounts; + m_viewTags = b.m_viewTags; + m_encryptedAnchors = b.m_encryptedAnchors; + m_amountBurnt = b.m_amountBurnt; m_txkeyPub = b.m_txkeyPub; + m_additionalPubKeys = b.m_additionalPubKeys; m_extraNonceSize = b.m_extraNonceSize; m_extraNonce = b.m_extraNonce; m_merkleTreeDataSize = b.m_merkleTreeDataSize; @@ -138,7 +143,7 @@ PoolBlock& PoolBlock::operator=(const PoolBlock& b) return *this; } -std::vector 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) const +std::vector 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 data; data.reserve(std::min(128 + m_outputAmounts.size() * 39 + m_transactions.size() * HASH_SIZE, 131072)); @@ -173,26 +178,52 @@ std::vector PoolBlock::serialize_mainchain_data(size_t* header_size, si writeVarint(m_outputAmounts.size(), data); - for (size_t i = 0, n = m_outputAmounts.size(); i < n; ++i) { - const TxOutput& output = m_outputAmounts[i]; + LOGINFO(0, "DEBUG serialize: numOutputs=" << m_outputAmounts.size() << " numEphKeys=" << m_ephPublicKeys.size() << " numViewTags=" << m_viewTags.size() << " numEncAnchors=" << m_encryptedAnchors.size() << " sidechainHeight=" << m_sidechainHeight); - writeVarint(output.m_reward, data); - data.push_back(TXOUT_TO_TAGGED_KEY); - const hash h = m_ephPublicKeys[i]; - data.insert(data.end(), h.h, h.h + HASH_SIZE); - data.push_back(static_cast(output.m_viewTag)); - } + for (size_t i = 0, n = m_outputAmounts.size(); i < n; ++i) { + const TxOutput& output = m_outputAmounts[i]; + + writeVarint(output.m_reward, 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.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()); + } if (outputs_blob_size) { *outputs_blob_size = static_cast(data.size()) - outputs_offset0; } - uint8_t tx_extra[128]; - uint8_t* p = tx_extra; + std::vector tx_extra(128 + (1 + m_additionalPubKeys.size()) * HASH_SIZE); + uint8_t* p = tx_extra.data(); - *(p++) = TX_EXTRA_TAG_PUBKEY; - memcpy(p, m_txkeyPub.h, HASH_SIZE); - p += HASH_SIZE; + if (m_additionalPubKeys.empty()) { + // Single output: use TX_EXTRA_TAG_PUBKEY + *(p++) = TX_EXTRA_TAG_PUBKEY; + memcpy(p, m_txkeyPub.h, HASH_SIZE); + p += HASH_SIZE; + } else { + // Multiple outputs: TX_EXTRA_TAG_ADDITIONAL_PUBKEYS with ALL D_e values + *(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); + p += HASH_SIZE; + } + } uint64_t extra_nonce_size = m_extraNonceSize; if (extra_nonce_size > EXTRA_NONCE_MAX_SIZE) { @@ -220,21 +251,15 @@ std::vector PoolBlock::serialize_mainchain_data(size_t* header_size, si memcpy(p, m_merkleRoot.h, HASH_SIZE); p += HASH_SIZE; - writeVarint(static_cast(p - tx_extra), data); - data.insert(data.end(), tx_extra, p); + writeVarint(static_cast(p - tx_extra.data()), data); + data.insert(data.end(), tx_extra.data(), p); // For Carrot v1+ (major_version >= 10), add type and amount_burnt instead of vin_rct_type if (m_majorVersion >= 10) { // type = MINER writeVarint(1, data); - // amount_burnt = 20% of total reward - uint64_t miner_total = 0; - for (const TxOutput& output : m_outputAmounts) { - miner_total += output.m_reward; - } - uint64_t stake_amount = miner_total / 4; - writeVarint(stake_amount, data); + writeVarint(m_amountBurnt, data); data.push_back(0); @@ -275,17 +300,18 @@ std::vector PoolBlock::serialize_mainchain_data(size_t* header_size, si calculate_protocol_tx_hash(m_txinGenHeight, protocol_tx_hash); LOGINFO(3, "Sidechain protocol TX hash: " << protocol_tx_hash); } - writeVarint(m_transactions.size() - 1, data); - + if (include_tx_hashes) { + writeVarint(m_transactions.size() - 1, data); #ifdef WITH_INDEXED_HASHES - for (size_t i = 1, n = m_transactions.size(); i < n; ++i) { - const hash h = m_transactions[i]; - data.insert(data.end(), h.h, h.h + HASH_SIZE); - } + for (size_t i = 1, n = m_transactions.size(); i < n; ++i) { + const hash h = m_transactions[i]; + data.insert(data.end(), h.h, h.h + HASH_SIZE); + } #else - const uint8_t* t = reinterpret_cast(m_transactions.data()); - data.insert(data.end(), t + HASH_SIZE, t + m_transactions.size() * HASH_SIZE); + const uint8_t* t = reinterpret_cast(m_transactions.data()); + data.insert(data.end(), t + HASH_SIZE, t + m_transactions.size() * HASH_SIZE); #endif + } #if POOL_BLOCK_DEBUG if ((nonce == &m_nonce) && (extra_nonce == &m_extraNonce) && !m_mainChainDataDebug.empty() && (data != m_mainChainDataDebug)) { @@ -421,6 +447,19 @@ bool PoolBlock::get_pow_hash(RandomX_Hasher_Base* hasher, uint64_t height, const memcpy(blob, mainchain_data.data(), blob_size); const uint8_t* miner_tx = mainchain_data.data() + header_size; + + // DEBUG: dump miner TX for comparison + LOGINFO(0, "get_pow_hash: header_size=" << header_size << " miner_tx_size=" << miner_tx_size); + { + std::string hex_dump; + for (size_t dbg_i = 0; dbg_i < std::min(miner_tx_size, size_t(160)); ++dbg_i) { + char dbg_buf[4]; + snprintf(dbg_buf, sizeof(dbg_buf), "%02x", miner_tx[dbg_i]); + hex_dump += dbg_buf; + } + LOGINFO(0, "miner_tx bytes: " << hex_dump); + } + hash tmp; // "miner_tx_size - 1" because the last byte is 0x00 (base rct data), it goes into the second hash @@ -431,10 +470,30 @@ bool PoolBlock::get_pow_hash(RandomX_Hasher_Base* hasher, uint64_t height, const keccak(reinterpret_cast(hashes), HASH_SIZE * 3, tmp.h); - // Save the coinbase tx hash into the first element of m_transactions - m_transactions[0] = static_cast(tmp); + // Save the coinbase tx hash into the first element of m_transactions + m_transactions[0] = static_cast(tmp); - root_hash tmp_root; + // 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) { + const hash& dbg_h = m_transactions[dbg_i]; + std::string hex; + for (size_t j = 0; j < HASH_SIZE; ++j) { + char buf[4]; + snprintf(buf, sizeof(buf), "%02x", dbg_h.h[j]); + hex += buf; + } + LOGINFO(0, "get_pow_hash: m_transactions[" << dbg_i << "]=" << hex); + } + + root_hash tmp_root; #ifdef WITH_INDEXED_HASHES std::vector transactions; @@ -463,19 +522,29 @@ bool PoolBlock::get_pow_hash(RandomX_Hasher_Base* hasher, uint64_t height, const uint64_t PoolBlock::get_payout(const Wallet& w) const { - for (size_t i = 0, n = m_outputAmounts.size(); i < n; ++i) { - const TxOutput& out = m_outputAmounts[i]; - - hash eph_public_key; - - uint8_t view_tag; - const uint8_t expected_view_tag = out.m_viewTag; - if (w.get_eph_public_key(m_txkeySec, i, eph_public_key, view_tag, &expected_view_tag) && (m_ephPublicKeys[i] == eph_public_key)) { - return out.m_reward; - } - } - - return 0; + const hash tx_key_seed = m_txkeySecSeed; + + LOGINFO(3, "get_payout: checking " << m_outputAmounts.size() << " outputs, tx_key_seed=" << tx_key_seed); + + for (size_t i = 0, n = m_outputAmounts.size(); i < n; ++i) { + const TxOutput& out = m_outputAmounts[i]; + hash eph_public_key; + uint8_t view_tag; + + const bool derived = w.get_eph_public_key_carrot(tx_key_seed, m_txinGenHeight, i, out.m_reward, eph_public_key, view_tag); + const bool match = (m_ephPublicKeys[i] == eph_public_key); + + LOGINFO(3, "get_payout: output " << i << " reward=" << out.m_reward + << " derived=" << (derived ? 1 : 0) + << " expected=" << m_ephPublicKeys[i] + << " got=" << eph_public_key + << " match=" << (match ? 1 : 0)); + + if (derived && match) { + return out.m_reward; + } + } + return 0; } hash PoolBlock::calculate_tx_key_seed() const @@ -483,9 +552,20 @@ hash PoolBlock::calculate_tx_key_seed() const const char domain[] = "tx_key_seed"; const uint32_t zero = 0; - const std::vector mainchain_data = serialize_mainchain_data(nullptr, nullptr, nullptr, nullptr, &zero, &zero); + // For Carrot v1 (v10+), exclude transaction hashes for canonical serialization + // The tx list differs between local creation and P2P reception + const bool include_tx_hashes = (m_majorVersion < 10); + const std::vector mainchain_data = serialize_mainchain_data(nullptr, nullptr, nullptr, nullptr, &zero, &zero, include_tx_hashes); const std::vector sidechain_data = serialize_sidechain_data(); + // DEBUG: Log sizes for tx_key_seed comparison + LOGINFO(3, "calculate_tx_key_seed: height=" << m_sidechainHeight + << " mainchain_size=" << mainchain_data.size() + << " sidechain_size=" << sidechain_data.size() + << " m_transactions.size=" << m_transactions.size() + << " m_viewTags.size=" << m_viewTags.size() + << " m_encryptedAnchors.size=" << m_encryptedAnchors.size()); + hash result; keccak_custom([&domain, &mainchain_data, &sidechain_data](int offset) -> uint8_t { size_t k = offset; diff --git a/src/pool_block.h b/src/pool_block.h index 5e6c255..76b0a1a 100644 --- a/src/pool_block.h +++ b/src/pool_block.h @@ -119,7 +119,9 @@ struct PoolBlock std::vector> m_viewTags; std::vector> m_encryptedAnchors; + uint64_t m_amountBurnt; hash m_txkeyPub; + std::vector m_additionalPubKeys; uint64_t m_extraNonceSize; uint32_t m_extraNonce; @@ -193,7 +195,7 @@ struct PoolBlock hash m_powHash; hash m_seed; - std::vector 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) const; + std::vector 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 serialize_sidechain_data() const; [[nodiscard]] int deserialize(const uint8_t* data, size_t size, const SideChain& sidechain, uv_loop_t* loop, bool compact); diff --git a/src/pool_block_parser.inl b/src/pool_block_parser.inl index 4a2cfc0..90ede00 100644 --- a/src/pool_block_parser.inl +++ b/src/pool_block_parser.inl @@ -100,10 +100,10 @@ int PoolBlock::deserialize(const uint8_t* data, size_t size, const SideChain& si int outputs_blob_size; if (num_outputs > 0) { - // Outputs are in the buffer, just read them - // Each output is at least 34 bytes, exit early if there's not enough data left - // 1 byte for reward, 1 byte for tx_type, 32 bytes for eph_pub_key - constexpr uint64_t MIN_OUTPUT_SIZE = 34; + // Carrot v1 outputs: each output is at least 89 bytes + // 1 byte amount, 1 byte tx_type(0x04), 32 bytes K_o, 4 bytes SAL1, + // 3 bytes view_tag, 16 bytes anchor, 32 bytes D_e + constexpr uint64_t MIN_OUTPUT_SIZE = 58; if (num_outputs > std::numeric_limits::max() / MIN_OUTPUT_SIZE) return __LINE__; if (static_cast(data_end - data) < num_outputs * MIN_OUTPUT_SIZE) return __LINE__; @@ -111,6 +111,9 @@ int PoolBlock::deserialize(const uint8_t* data, size_t size, const SideChain& si m_ephPublicKeys.resize(num_outputs); m_outputAmounts.resize(num_outputs); + m_viewTags.clear(); + m_encryptedAnchors.clear(); + m_ephPublicKeys.shrink_to_fit(); m_outputAmounts.shrink_to_fit(); @@ -120,7 +123,6 @@ int PoolBlock::deserialize(const uint8_t* data, size_t size, const SideChain& si uint64_t reward; READ_VARINT(reward); - // TxOutput max value check if (reward >= (1ULL << 56)) { return __LINE__; } @@ -128,15 +130,38 @@ int PoolBlock::deserialize(const uint8_t* data, size_t size, const SideChain& si t.m_reward = reward; total_reward += reward; - EXPECT_BYTE(TXOUT_TO_TAGGED_KEY); + EXPECT_BYTE(TXOUT_TO_CARROT_V1); - hash ephPublicKey; - READ_BUF(ephPublicKey.h, HASH_SIZE); - m_ephPublicKeys[i] = ephPublicKey; + // K_o onetime address (32 bytes) + hash onetime_address; + READ_BUF(onetime_address.h, HASH_SIZE); + + // asset_type string: length byte + "SAL1" + uint8_t asset_len; + READ_BYTE(asset_len); + if (asset_len != 4) { + return __LINE__; + } + uint8_t sal1_marker[4]; + READ_BUF(sal1_marker, 4); + if (memcmp(sal1_marker, "SAL1", 4) != 0) { + return __LINE__; + } - uint8_t view_tag; - READ_BYTE(view_tag); - t.m_viewTag = view_tag; + // 3-byte view tag + uint8_t view_tag_bytes[3]; + READ_BUF(view_tag_bytes, 3); + t.m_viewTag = view_tag_bytes[0]; + m_viewTags.push_back(std::vector(view_tag_bytes, view_tag_bytes + 3)); + + // 16-byte encrypted anchor + uint8_t encrypted_anchor[16]; + READ_BUF(encrypted_anchor, 16); + m_encryptedAnchors.push_back(std::vector(encrypted_anchor, encrypted_anchor + 16)); + + // Note: D_e ephemeral pubkey is in tx_extra, not per-output + // Store empty for now - will be populated from tx_extra + m_ephPublicKeys[i] = onetime_address; } outputs_blob_size = static_cast(data - data_begin) - outputs_offset; @@ -181,8 +206,31 @@ int PoolBlock::deserialize(const uint8_t* data, size_t size, const SideChain& si const uint8_t* tx_extra_begin = data; - EXPECT_BYTE(TX_EXTRA_TAG_PUBKEY); - READ_BUF(m_txkeyPub.h, HASH_SIZE); + // Handle Carrot tx_extra: either TAG_PUBKEY (single) or TAG_ADDITIONAL_PUBKEYS (multi) + m_additionalPubKeys.clear(); + if (*data == TX_EXTRA_TAG_PUBKEY) { + // Single output format + ++data; + READ_BUF(m_txkeyPub.h, HASH_SIZE); + } else if (*data == TX_EXTRA_TAG_ADDITIONAL_PUBKEYS) { + // Multiple output format: all D_e in additional pubkeys + ++data; + uint64_t num_pubkeys; + READ_VARINT(num_pubkeys); + if (num_pubkeys < 1) return __LINE__; + if (data + num_pubkeys * HASH_SIZE > data_end) return __LINE__; + // First D_e goes in m_txkeyPub + READ_BUF(m_txkeyPub.h, HASH_SIZE); + // Rest go in m_additionalPubKeys + if (num_pubkeys > 1) { + m_additionalPubKeys.resize(num_pubkeys - 1); + for (uint64_t i = 0; i < num_pubkeys - 1; ++i) { + READ_BUF(m_additionalPubKeys[i].h, HASH_SIZE); + } + } + } else { + return __LINE__; // Unknown tx_extra format + } EXPECT_BYTE(TX_EXTRA_NONCE); READ_VARINT(m_extraNonceSize); @@ -221,7 +269,16 @@ int PoolBlock::deserialize(const uint8_t* data, size_t size, const SideChain& si if (static_cast(data - tx_extra_begin) != tx_extra_size) return __LINE__; - EXPECT_BYTE(0); + // Carrot v1: type (MINER=1) and amount_burnt before RCT type + uint64_t tx_type; + READ_VARINT(tx_type); + // tx_type should be 1 (MINER) for coinbase + + uint64_t amount_burnt; + READ_VARINT(amount_burnt); + m_amountBurnt = amount_burnt; + + EXPECT_BYTE(0); // RCT type // Protocol TX (Salvium Carrot v1+) - parse if present if (m_majorVersion >= 10) { @@ -361,20 +418,25 @@ skip_protocol_tx: READ_BUF(m_txkeySecSeed.h, HASH_SIZE); - hash pub; - get_tx_keys(pub, m_txkeySec, m_txkeySecSeed, m_prevId); - if (pub != m_txkeyPub) { - return __LINE__; - } + // Only verify tx pubkey derivation for pre-Carrot blocks + // Carrot v1 uses different key derivation (D_e is not derivable from m_txkeySecSeed) + if (m_majorVersion < 10) { + hash pub; + get_tx_keys(pub, m_txkeySec, m_txkeySecSeed, m_prevId); + if (pub != m_txkeyPub) { + return __LINE__; + } - if (!check_keys(m_txkeyPub, m_txkeySec)) { - return __LINE__; + if (!check_keys(m_txkeyPub, m_txkeySec)) { + return __LINE__; + } } READ_BUF(m_parent.h, HASH_SIZE); m_transactions.clear(); - m_transactions.reserve(transactions.size()); + m_transactions.reserve(transactions.size() + 1); + m_transactions.emplace_back(hash{}); if (compact) { const PoolBlock* parent = sidechain.find_block(m_parent); @@ -577,6 +639,18 @@ skip_protocol_tx: const uint32_t mm_aux_slot = get_aux_slot(sidechain.consensus_hash(), mm_nonce, mm_n_aux_chains); + { + std::string hex; + for (size_t i = 0; i < std::min(64, outputs_blob.size()); ++i) { + char buf[4]; + snprintf(buf, sizeof(buf), "%02x", outputs_blob[i]); + hex += buf; + } + LOGINFO(0, "DEBUG parser outputs_blob (first 64): " << hex << " size=" << outputs_blob.size()); + } + + LOGINFO(0, "DEBUG merkle verify: sidechain_height=" << m_sidechainHeight << " mm_aux_slot=" << mm_aux_slot << " n_chains=" << mm_n_aux_chains << " proof_size=" << m_merkleProof.size() << " check=" << check << " merkleRoot=" << static_cast(m_merkleRoot)); + if (!verify_merkle_proof(check, m_merkleProof, mm_aux_slot, mm_n_aux_chains, m_merkleRoot)) { return __LINE__; } diff --git a/src/side_chain.cpp b/src/side_chain.cpp index 60eab1e..ec61e5e 100644 --- a/src/side_chain.cpp +++ b/src/side_chain.cpp @@ -72,6 +72,7 @@ SideChain::SideChain(p2pool* pool, NetworkType type, const char* pool_name, cons , m_chainWindowSize(2160) , m_unclePenalty(20) , m_precalcFinished(false) + , m_externalBlockFailures(0) #ifdef DEV_TEST_SYNC , m_firstPruneTime(0) #endif @@ -245,9 +246,47 @@ bool SideChain::fill_sidechain_data(PoolBlock& block, std::vector& s const PoolBlock* tip = m_chainTip; - if (!tip) { + if (!tip) { + // If we've learned of a peer's genesis, wait for their chain instead of creating our own + if (!m_adoptedGenesisId.empty()) { + const uint64_t elapsed = seconds_since_epoch() - m_adoptedGenesisTime; + const uint64_t timeout = 90; + + if (elapsed < timeout) { + // Log every 15 seconds so user knows we're working + if ((elapsed % 15) < 2) { + LOGINFO(3, "Waiting for peer's genesis block " << m_adoptedGenesisId + << " (" << elapsed << "s / " << timeout << "s)"); + } + return false; + } + + // Timeout - give up on peer's genesis and create our own + LOGWARN(3, "Timeout waiting for peer's genesis block after " << elapsed + << "s, creating own genesis"); + m_adoptedGenesisId = {}; + m_adoptedGenesisTimestamp = 0; + m_adoptedGenesisHeight = 0; + m_adoptedGenesisTime = 0; + } + + // Don't create genesis block until initial peer sync has been attempted + // This prevents nodes from creating independent chains when starting simultaneously + if (!m_precalcFinished.load()) { + P2PServer* p2p = m_pool->p2p_server(); + if (p2p && (p2p->peer_list_size() > 0 || p2p->num_connections() > 0)) { + LOGINFO(5, "Waiting for initial peer sync before creating genesis block"); + return false; + } + } + + // No peers with existing chain - create our own genesis + LOGINFO(3, "Creating new genesis block (no peer chain found)"); + m_genesisDecisionMade = true; + block.m_parent = {}; block.m_sidechainHeight = 0; + block.m_difficulty = m_minDifficulty; block.m_cumulativeDifficulty = m_minDifficulty; block.m_txkeySecSeed = m_consensusHash; @@ -374,7 +413,7 @@ bool SideChain::get_shares(const PoolBlock* tip, std::vector& shares ; if (m_pool && !tip->m_parent.empty()) { - const uint64_t h = p2pool::get_seed_height(tip->m_txinGenHeight); + const uint64_t h = tip->m_txinGenHeight; if (!m_pool->get_difficulty_at_height(h, mainchain_diff)) { LOGWARN(L, "get_shares: couldn't get mainchain difficulty for height = " << h); return false; @@ -433,6 +472,8 @@ bool SideChain::get_shares(const PoolBlock* tip, std::vector& shares } pplns_weight += cur_weight; + LOGINFO(0, "get_shares: height=" << cur->m_sidechainHeight << " wallet=" << cur->m_minerWallet << " pplns_weight=" << pplns_weight << " max=" << max_pplns_weight << " mainchain_h=" << tip->m_txinGenHeight << " depth=" << block_depth); + // One non-uncle share can go above the limit, but it will also guarantee that "shares" is never empty if (pplns_weight > max_pplns_weight) { break; @@ -601,7 +642,7 @@ bool SideChain::add_external_block(PoolBlock& block, std::vector& missing_ if (!m_pool->get_difficulty_at_height(block.m_txinGenHeight, diff)) { LOGWARN(3, "add_external_block: couldn't get mainchain difficulty for height = " << block.m_txinGenHeight); } - else if (diff.check_pow(block.m_powHash)) { + else if (diff.check_pow(block.m_powHash) && (block.m_txinGenHeight + 2 >= miner_data.height)) { LOGINFO(0, log::LightGreen() << "add_external_block: block " << block.m_sidechainId << " has enough PoW for Salvium height " << block.m_txinGenHeight << ", submitting it"); m_pool->submit_block_async(block.serialize_mainchain_data()); } @@ -679,7 +720,30 @@ bool SideChain::add_external_block(PoolBlock& block, std::vector& missing_ m_pool->api_update_block_found(&data, &block); } - return add_block(block); + const bool added = add_block(block); + if (added && block.m_verified) { + if (block.m_invalid) { + ++m_externalBlockFailures; + LOGWARN(3, "external block validation failed, failure count: " << m_externalBlockFailures << "/" << DIVERGENCE_THRESHOLD); + if (m_externalBlockFailures >= DIVERGENCE_THRESHOLD) { + LOGERR(0, log::LightRed() << "DIVERGENCE DETECTED: " << m_externalBlockFailures << " consecutive external block failures. Purging cache and restarting..."); + // Delete cache file + const std::string cache_path = "p2pool.cache"; + remove(cache_path.c_str()); + // Signal shutdown - systemd will restart + if (m_pool) { + m_pool->stop(); + } + } + } else { + // Valid external block - reset counter + if (m_externalBlockFailures > 0) { + LOGINFO(3, "external block validated successfully, resetting failure counter from " << m_externalBlockFailures); + m_externalBlockFailures = 0; + } + } + } + return added; } bool SideChain::add_block(const PoolBlock& block) @@ -809,6 +873,16 @@ const PoolBlock* SideChain::get_block_blob(const hash& id, std::vector& } blob = block->serialize_mainchain_data(); + { + std::string hex; + size_t start = 43; // approximate outputs offset + for (size_t i = start; i < std::min(start + 64, blob.size()); ++i) { + char buf[4]; + snprintf(buf, sizeof(buf), "%02x", blob[i]); + hex += buf; + } + LOGINFO(0, "DEBUG get_block_blob outputs area (64 bytes from offset 43): " << hex); + } const std::vector sidechain_data = block->serialize_sidechain_data(); blob.insert(blob.end(), sidechain_data.begin(), sidechain_data.end()); @@ -837,28 +911,75 @@ 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); - if (it != m_blocksById.end()) { - const PoolBlock* b = it->second; - const size_t n = b->m_outputAmounts.size(); + 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(output.m_viewTag)); + } + } + + block->m_ephPublicKeys = b->m_ephPublicKeys; + block->m_outputAmounts = b->m_outputAmounts; + block->m_viewTags = b->m_viewTags; + block->m_encryptedAnchors = b->m_encryptedAnchors; + return true; + } - blob.reserve(n * 39 + 64); - writeVarint(n, blob); + // 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); + blob.insert(blob.end(), block->m_ephPublicKeys[i].h, block->m_ephPublicKeys[i].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()); + } + return true; + } - 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_TAGGED_KEY); - const hash h = b->m_ephPublicKeys[i]; - blob.insert(blob.end(), h.h, h.h + HASH_SIZE); - blob.emplace_back(static_cast(output.m_viewTag)); - } + // Can't compute outputs from PPLNS if we don't have this block's parent chain + if (!block->m_parent.empty()) { + auto parent_it = m_blocksById.find(block->m_parent); + if (parent_it == m_blocksById.end()) { + LOGINFO(5, "get_outputs_blob: can't compute outputs, parent " << block->m_parent << " not found"); + return false; + } + } - block->m_ephPublicKeys = b->m_ephPublicKeys; - block->m_outputAmounts = b->m_outputAmounts; - return true; - } + data = std::make_shared(); - data = std::make_shared(); data->blockMinerWallet = block->m_minerWallet; data->txkeySec = block->m_txkeySec; @@ -866,8 +987,8 @@ bool SideChain::get_outputs_blob(PoolBlock* block, uint64_t total_reward, std::v return false; } // Handle donation mode during validation - match block creation logic - const uint64_t block_height = block->m_sidechainHeight; - if ((block_height % 100) == 0 && m_devWallet) { + const uint64_t donation_cycle = (s_networkType == NetworkType::Mainnet) ? DONATION_CYCLE_MAINNET : DONATION_CYCLE_TESTNET; + if ((block->m_txinGenHeight % donation_cycle) == 0 && m_devWallet) { // This is a donation block - replace all shares with single dev wallet output difficulty_type total_weight; for (const auto& share : data->tmpShares) { @@ -919,7 +1040,8 @@ bool SideChain::get_outputs_blob(PoolBlock* block, uint64_t total_reward, std::v block->m_ephPublicKeys.clear(); block->m_outputAmounts.clear(); - + block->m_viewTags.clear(); + block->m_encryptedAnchors.clear(); block->m_ephPublicKeys.reserve(n); block->m_outputAmounts.reserve(n); @@ -934,7 +1056,7 @@ bool SideChain::get_outputs_blob(PoolBlock* block, uint64_t total_reward, std::v writeVarint(tmpRewards[i], blob); - blob.emplace_back(TXOUT_TO_TAGGED_KEY); + blob.emplace_back(TXOUT_TO_CARROT_V1); uint8_t view_tag; if (!data->tmpShares[i].m_wallet->get_eph_public_key(data->txkeySec, i, eph_public_key, view_tag)) { @@ -1531,12 +1653,17 @@ void SideChain::verify(PoolBlock* block) !block->m_uncles.empty() || (block->m_difficulty != m_minDifficulty) || (block->m_cumulativeDifficulty != m_minDifficulty) || - (block->m_txkeySecSeed != m_consensusHash)) + (block->m_txkeySecSeed != m_consensusHash)) { + LOGWARN(3, "genesis block validation failed: height=" << block->m_sidechainHeight + << " parent_empty=" << (block->m_parent.empty() ? 1 : 0) + << " diff=" << block->m_difficulty << " expected=" << m_minDifficulty); block->m_invalid = true; } - block->m_verified = true; + if (!block->m_invalid) { + LOGINFO(3, "Genesis block verified: " << block->m_sidechainId); + } return; } @@ -1556,7 +1683,8 @@ void SideChain::verify(PoolBlock* block) // Regular block // Must have a parent - if (block->m_parent.empty()) { + if (block->m_parent.empty()) { + LOGWARN(3, "block at height = " << block->m_sidechainHeight << ", id = " << block->m_sidechainId << ", mainchain height = " << block->m_txinGenHeight << " has empty parent (non-genesis)"); block->m_verified = true; block->m_invalid = true; return; @@ -1571,7 +1699,8 @@ void SideChain::verify(PoolBlock* block) // If it's invalid then this block is also invalid const PoolBlock* parent = it->second; - if (parent->m_invalid) { + if (parent->m_invalid) { + LOGWARN(3, "block at height = " << block->m_sidechainHeight << ", id = " << block->m_sidechainId << ", mainchain height = " << block->m_txinGenHeight << ": get_shares failed"); block->m_verified = true; block->m_invalid = true; return; @@ -1580,7 +1709,7 @@ void SideChain::verify(PoolBlock* block) // Check m_txkeySecSeed const hash h = (block->m_prevId == parent->m_prevId) ? parent->m_txkeySecSeed : parent->calculate_tx_key_seed(); if (block->m_txkeySecSeed != h) { - LOGWARN(3, "block " << block->m_sidechainId << " has invalid tx key seed: expected " << h << ", got " << block->m_txkeySecSeed); + LOGWARN(3, "block at height = " << block->m_sidechainHeight << ", id = " << block->m_sidechainId << ", mainchain height = " << block->m_txinGenHeight << " has invalid parent " << block->m_parent); block->m_verified = true; block->m_invalid = true; return; @@ -1746,15 +1875,13 @@ void SideChain::verify(PoolBlock* block) diff = difficulty(); } else if (!get_difficulty(parent, m_difficultyData, diff)) { + LOGWARN(3, "block at height = " << block->m_sidechainHeight << ", id = " << block->m_sidechainId << ", mainchain height = " << block->m_txinGenHeight << ": get_difficulty failed for parent " << block->m_parent); block->m_invalid = true; return; } if (diff != block->m_difficulty) { - LOGWARN(3, "block at height = " << block->m_sidechainHeight << - ", id = " << block->m_sidechainId << - ", mainchain height = " << block->m_txinGenHeight << - " has wrong difficulty: got " << block->m_difficulty << ", expected " << diff); + LOGWARN(3, "block at height = " << block->m_sidechainHeight << ", id = " << block->m_sidechainId << ", mainchain height = " << block->m_txinGenHeight << " has wrong difficulty: got " << block->m_difficulty << ", expected " << diff); block->m_invalid = true; return; } @@ -1766,13 +1893,15 @@ void SideChain::verify(PoolBlock* block) shares = std::move(block->m_precalculatedShares); } - if (shares.empty() && !get_shares(block, shares)) { + if (shares.empty() && !get_shares(block, shares)) { + LOGWARN(3, "block at height = " << block->m_sidechainHeight << ", id = " << block->m_sidechainId << ", mainchain height = " << block->m_txinGenHeight << ": get_shares failed"); block->m_invalid = true; return; } // Handle donation mode during verification - match block creation logic - if ((block->m_sidechainHeight % 100) == 0 && m_devWallet) { + const uint64_t donation_cycle = (s_networkType == NetworkType::Mainnet) ? DONATION_CYCLE_MAINNET : DONATION_CYCLE_TESTNET; + if ((block->m_txinGenHeight % donation_cycle) == 0 && m_devWallet) { difficulty_type total_weight; for (const auto& share : shares) { total_weight += share.m_weight; @@ -1814,50 +1943,58 @@ void SideChain::verify(PoolBlock* block) return; } - for (size_t i = 0, n = rewards.size(); i < n; ++i) { - const PoolBlock::TxOutput& out = block->m_outputAmounts[i]; + // Carrot v1 validation - generate and sort expected outputs + struct ValidationOutput { + size_t share_index; + uint64_t reward; + hash ko; + uint8_t view_tag; + }; + std::vector expected; + expected.reserve(rewards.size()); - if (rewards[i] != out.m_reward) { - LOGWARN(3, "block at height = " << block->m_sidechainHeight << - ", id = " << block->m_sidechainId << - ", mainchain height = " << block->m_txinGenHeight << - " has invalid reward at index " << i << ": got " << out.m_reward << ", expected " << rewards[i]); - block->m_invalid = true; - return; - } + for (size_t i = 0; i < rewards.size(); ++i) { + ValidationOutput out; + out.share_index = i; + out.reward = rewards[i]; + if (!shares[i].m_wallet->get_eph_public_key_carrot(block->m_txkeySecSeed, block->m_txinGenHeight, i, rewards[i], out.ko, out.view_tag)) { + LOGWARN(3, "block at height " << block->m_sidechainHeight << " failed to generate K_o at index " << i); + block->m_invalid = true; + return; + } + expected.push_back(out); + } - hash eph_public_key; - uint8_t view_tag; - if (!shares[i].m_wallet->get_eph_public_key(block->m_txkeySec, i, eph_public_key, view_tag)) { - LOGWARN(3, "block at height = " << block->m_sidechainHeight << - ", id = " << block->m_sidechainId << - ", mainchain height = " << block->m_txinGenHeight << - " failed to eph_public_key at index " << i); - block->m_invalid = true; - return; - } + // Sort by K_o + std::sort(expected.begin(), expected.end(), + [](const ValidationOutput& a, const ValidationOutput& b) { + return memcmp(a.ko.h, b.ko.h, HASH_SIZE) < 0; + }); - if (out.m_viewTag != view_tag) { - LOGWARN(3, "block at height = " << block->m_sidechainHeight << - ", id = " << block->m_sidechainId << - ", mainchain height = " << block->m_txinGenHeight << - " has an incorrect view tag at index " << i); - block->m_invalid = true; - return; - } + // Validate sorted outputs + for (size_t i = 0; i < expected.size(); ++i) { + const ValidationOutput& exp = expected[i]; + const PoolBlock::TxOutput& actual = block->m_outputAmounts[i]; - if (eph_public_key != block->m_ephPublicKeys[i]) { - LOGWARN(3, "block at height = " << block->m_sidechainHeight << - ", id = " << block->m_sidechainId << - ", mainchain height = " << block->m_txinGenHeight << - " pays out to a wrong wallet at index " << i); - block->m_invalid = true; - return; - } - } + if (exp.reward != actual.m_reward) { + LOGWARN(3, "block at height " << block->m_sidechainHeight << " has invalid reward at position " << i); + block->m_invalid = true; + return; + } + if (exp.view_tag != actual.m_viewTag) { + LOGWARN(3, "block at height " << block->m_sidechainHeight << " has invalid view_tag at position " << i); + block->m_invalid = true; + return; + } + if (exp.ko != block->m_ephPublicKeys[i]) { + LOGWARN(3, "block at height " << block->m_sidechainHeight << " has invalid K_o at position " << i); + block->m_invalid = true; + return; + } + } - // All checks passed - block->m_invalid = false; + // All checks passed + block->m_invalid = false; } void SideChain::update_chain_tip(PoolBlock* block) @@ -2340,6 +2477,105 @@ void SideChain::get_missing_blocks(unordered_set& missing_blocks) const } } +bool SideChain::consider_peer_genesis(const hash& genesis_id, uint64_t timestamp, uint64_t height) +{ + // Get our current genesis info for comparison + uint64_t our_genesis_timestamp = 0; + hash our_genesis_id; + { + ReadLock lock(m_sidechainLock); + auto it = m_blocksByHeight.find(0); + if (it != m_blocksByHeight.end() && !it->second.empty()) { + our_genesis_timestamp = it->second.front()->m_timestamp; + our_genesis_id = it->second.front()->m_sidechainId; + } + } + + // 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 + } else { + // Our genesis is older or same, keep it + return true; + } + } + + if (m_adoptedGenesisTimestamp == 0 || timestamp < m_adoptedGenesisTimestamp || + (timestamp == m_adoptedGenesisTimestamp && genesis_id < m_adoptedGenesisId)) { + LOGINFO(3, "Adopting older genesis from peer: " << genesis_id + << " timestamp=" << timestamp << " height=" << height); + m_adoptedGenesisId = genesis_id; + m_adoptedGenesisTimestamp = timestamp; + m_adoptedGenesisHeight = height; + m_adoptedGenesisTime = seconds_since_epoch(); + } + + return true; +} + +void SideChain::purge_sidechain() +{ + LOGWARN(3, "Purging sidechain to adopt older genesis from peer"); + + WriteLock lock(m_sidechainLock); + + // Free all block memory + for (auto& it : m_blocksById) { + delete it.second; + } + + // Clear all block tracking + m_blocksById.clear(); + m_blocksByHeight.clear(); + m_blocksByMerkleRoot.clear(); + m_chainTip = nullptr; + + // Clear difficulty data + m_difficultyData.clear(); + + // Reset genesis state to allow new genesis adoption + m_genesisDecisionMade = false; + m_adoptedGenesisId = {}; + m_adoptedGenesisTimestamp = 0; + m_adoptedGenesisHeight = 0; + m_adoptedGenesisTime = 0; + + // Delete cache files to prevent reload of old blocks on restart + const std::string cache_path = DATA_DIR + "p2pool.cache"; + const std::string version_path = DATA_DIR + "p2pool.cache.version"; + + if (remove(cache_path.c_str()) == 0) { + LOGINFO(3, "Deleted cache file: " << cache_path); + } + if (remove(version_path.c_str()) == 0) { + LOGINFO(3, "Deleted cache version file: " << version_path); + } + LOGINFO(3, "Sidechain purged, ready to sync from peer"); +} + +bool SideChain::get_genesis_info(hash& id, uint64_t& timestamp, uint64_t& height) const +{ + ReadLock lock(m_sidechainLock); + + auto it = m_blocksByHeight.find(0); + if (it == m_blocksByHeight.end() || it->second.empty()) { + return false; + } + + const PoolBlock* genesis = it->second.front(); + id = genesis->m_sidechainId; + timestamp = genesis->m_timestamp; + height = genesis->m_txinGenHeight; + return true; +} + bool SideChain::load_config(const std::string& filename) { if (filename.empty()) { diff --git a/src/side_chain.h b/src/side_chain.h index 8b9fb61..bd4ed03 100644 --- a/src/side_chain.h +++ b/src/side_chain.h @@ -24,6 +24,13 @@ namespace p2pool { +// ============================================================ +// PROTOCOL_VERSION - INCREMENT THIS ON CONSENSUS-BREAKING CHANGES +// Examples: serialization format, validation rules, payout calculation +// Do NOT increment for: bug fixes, logging, performance changes +// ============================================================ +static constexpr uint32_t PROTOCOL_VERSION = 1; + class p2pool; class P2PServer; @@ -57,6 +64,10 @@ public: [[nodiscard]] const PoolBlock* find_block_by_merkle_root(const root_hash& merkle_root) const; void watch_mainchain_block(const ChainMain& data, const hash& possible_merkle_root); + bool consider_peer_genesis(const hash& genesis_id, uint64_t timestamp, uint64_t height); + bool get_genesis_info(hash& id, uint64_t& timestamp, uint64_t& height) const; + void purge_sidechain(); + [[nodiscard]] const PoolBlock* get_block_blob(const hash& id, std::vector& blob) const; [[nodiscard]] bool get_outputs_blob(PoolBlock* block, uint64_t total_reward, std::vector& blob, uv_loop_t* loop) const; @@ -121,7 +132,15 @@ private: void prune_seen_data(); mutable uv_rwlock_t m_sidechainLock; - std::atomic m_chainTip; + std::atomic m_chainTip; + + // Genesis reconciliation + mutable std::atomic m_genesisDecisionMade{ false }; + mutable hash m_adoptedGenesisId; + mutable uint64_t m_adoptedGenesisTimestamp{ 0 }; + mutable uint64_t m_adoptedGenesisHeight{ 0 }; + mutable uint64_t m_adoptedGenesisTime{ 0 }; + std::map> m_blocksByHeight; unordered_map m_blocksById; unordered_map m_blocksByMerkleRoot; @@ -168,6 +187,10 @@ private: uint64_t m_firstPruneTime; #endif + // Divergence detection + static constexpr uint32_t DIVERGENCE_THRESHOLD = 20; + uint32_t m_externalBlockFailures; + hash m_consensusHash; void launch_precalc(const PoolBlock* block); diff --git a/src/wallet.cpp b/src/wallet.cpp index e76dc03..d43bfba 100644 --- a/src/wallet.cpp +++ b/src/wallet.cpp @@ -21,6 +21,7 @@ #include "common.h" #include "wallet.h" #include "keccak.h" +#include "carrot_crypto.h" #include "crypto.h" #include @@ -377,4 +378,46 @@ bool Wallet::torsion_check() const fcmp_pp::torsion_check_vartime(p2); } +bool Wallet::get_eph_public_key_carrot(const hash& tx_key_seed, uint64_t height, size_t output_index, uint64_t amount, hash& eph_public_key, uint8_t& view_tag) const +{ + (void)output_index; // Not used - anchor derived from spend pubkey, not position + + // 1. Build input_context from height + uint8_t input_context[33]; + carrot::make_input_context_coinbase(height, input_context); + + // 2. Derive anchor from wallet's spend public key (position-independent) + uint8_t anchor[16]; + carrot::derive_deterministic_anchor_from_pubkey(tx_key_seed, m_spendPublicKey, anchor); + + // 3. Derive per-output ephemeral private key d_e from anchor + static const uint8_t null_payment_id[8] = {0}; + hash ephemeral_privkey; + carrot::make_ephemeral_privkey(anchor, input_context, m_spendPublicKey, null_payment_id, ephemeral_privkey); + + // 4. Derive ephemeral public key D_e = d_e * B + hash ephemeral_pubkey; + carrot::make_ephemeral_pubkey_mainaddress(ephemeral_privkey, ephemeral_pubkey); + + // 5. Derive shared secret using this wallet's view pubkey + hash shared_secret; + if (!carrot::make_shared_secret_sender(ephemeral_privkey, m_viewPublicKey, shared_secret)) { + return false; + } + + // 6. Derive sender-receiver secret + hash sender_receiver_secret; + carrot::make_sender_receiver_secret(shared_secret, ephemeral_pubkey, input_context, sender_receiver_secret); + + // 7. Derive onetime address K_o + carrot::make_onetime_address_coinbase(m_spendPublicKey, sender_receiver_secret, amount, eph_public_key); + + // 8. Derive view tag + uint8_t view_tag_full[3]; + carrot::make_view_tag(shared_secret, input_context, eph_public_key, view_tag_full); + view_tag = view_tag_full[0]; + + return true; +} + } // namespace p2pool diff --git a/src/wallet.h b/src/wallet.h index 99e7f36..43c06fa 100644 --- a/src/wallet.h +++ b/src/wallet.h @@ -46,6 +46,7 @@ public: } [[nodiscard]] bool get_eph_public_key(const hash& txkey_sec, size_t output_index, hash& eph_public_key, uint8_t& view_tag, const uint8_t* expected_view_tag = nullptr) const; + [[nodiscard]] bool get_eph_public_key_carrot(const hash& tx_key_seed, uint64_t height, size_t output_index, uint64_t amount, hash& eph_public_key, uint8_t& view_tag) const; FORCEINLINE bool operator<(const Wallet& w) const { return (m_spendPublicKey < w.m_spendPublicKey) || ((m_spendPublicKey == w.m_spendPublicKey) && (m_viewPublicKey < w.m_viewPublicKey)); } FORCEINLINE bool operator==(const Wallet& w) const { return (m_spendPublicKey == w.m_spendPublicKey) && (m_viewPublicKey == w.m_viewPublicKey); }