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); }