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.
This commit is contained in:
Matt Hess
2025-12-05 23:44:53 +00:00
parent a5ee896215
commit 00fb078004
18 changed files with 1011 additions and 279 deletions
+1 -1
View File
@@ -8,7 +8,7 @@
"name": "salvium_main",
"password": "",
"block_time": 10,
"min_diff": 100000,
"min_diff": 10000,
"pplns_window": 2160,
"uncle_penalty": 20
}
+28 -7
View File
@@ -20,6 +20,7 @@
#include "pool_block.h"
#include "p2p_server.h"
#include "side_chain.h"
#include <fstream>
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<std::string>{}(__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");
+152 -87
View File
@@ -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::vector<Mine
m_poolBlockTemplate->m_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<Mine
uint8_t input_context[33];
carrot::make_input_context_coinbase(data.height, input_context);
uint8_t null_payment_id[8] = {0};
// Null payment ID for main addresses
static const uint8_t null_payment_id[8] = {0};
// Structure for Carrot output with pre-computed K_o
struct CarrotOutput {
size_t share_index; // Original index into shares/m_rewards
uint64_t amount;
hash onetime_address; // K_o
hash eph_pubkey; // D_e (per-output ephemeral pubkey)
uint8_t view_tag[3];
uint8_t encrypted_anchor[16];
};
std::vector<CarrotOutput> outputs;
outputs.reserve(num_outputs);
// Generate all outputs using single D_e, position-independent K_o
for (size_t i = 0; i < num_outputs; ++i) {
// 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<uint8_t> vt(view_tag, view_tag + 3);
std::vector<uint8_t> 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<uint8_t> vt(out.view_tag, out.view_tag + 3);
std::vector<uint8_t> ea(out.encrypted_anchor, out.encrypted_anchor + 16);
m_poolBlockTemplate->m_viewTags.push_back(vt);
m_poolBlockTemplate->m_encryptedAnchors.push_back(ea);
}
}
if (dry_run) {
@@ -1114,19 +1157,40 @@ int BlockTemplate::create_miner_tx(const MinerData& data, const std::vector<Mine
return -2;
}
// TX_EXTRA
LOGINFO(3, "DEBUG: Carrot extra - major_version=" << data.major_version << ", static_cast<int>(dry_run)=" << static_cast<int>(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<int>(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::vector<Mine
}
uint64_t stake_amount = full_block_reward / 5;
writeVarint(stake_amount, m_minerTx);
m_poolBlockTemplate->m_amountBurnt = stake_amount;
// Save prefix size - everything up to here is the transaction prefix
m_minerTxPrefixSize = static_cast<uint32_t>(m_minerTx.size());
+35
View File
@@ -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
+17
View File
@@ -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
+135 -14
View File
@@ -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<PoolBlock*> 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<hash> 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<char*>(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<P2PServer*>(m_owner)->m_pool->side_chain().get_genesis_info(genesis_id, genesis_timestamp, genesis_height)) {
LOGINFO(5, "sending GENESIS_INFO to " << static_cast<char*>(m_addrString) << " (v" << PROTOCOL_VERSION << ")");
*(p++) = static_cast<uint8_t>(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<const uint64_t*>(buf + HASH_SIZE));
const uint64_t peer_height = read_unaligned(reinterpret_cast<const uint64_t*>(buf + HASH_SIZE + sizeof(uint64_t)));
const uint32_t peer_version = read_unaligned(reinterpret_cast<const uint32_t*>(buf + HASH_SIZE + sizeof(uint64_t) + sizeof(uint64_t)));
P2PServer* server = static_cast<P2PServer*>(m_owner);
SideChain& side_chain = server->m_pool->side_chain();
LOGINFO(4, "peer " << log::Gray() << static_cast<char*>(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<char*>(m_addrString)
<< " running newer protocol v" << peer_version
<< " (we have v" << PROTOCOL_VERSION << "). Upgrade recommended.");
} else {
LOGWARN(3, "Peer " << static_cast<char*>(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<uint8_t>(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<P2PServer*>(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<char*>(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<char*>(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
+5 -1
View File
@@ -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);
+4 -14
View File
@@ -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();
+1 -1
View File
@@ -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; }
+20 -5
View File
@@ -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);
}
+5
View File
@@ -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
{
+128 -48
View File
@@ -23,6 +23,7 @@
#include "protocol_tx_hash.h"
#include "crypto.h"
#include "merkle.h"
#include <sstream>
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<uint8_t> PoolBlock::serialize_mainchain_data(size_t* header_size, size_t* miner_tx_size, int* outputs_offset, int* outputs_blob_size, const uint32_t* nonce, const uint32_t* extra_nonce) const
std::vector<uint8_t> PoolBlock::serialize_mainchain_data(size_t* header_size, size_t* miner_tx_size, int* outputs_offset, int* outputs_blob_size, const uint32_t* nonce, const uint32_t* extra_nonce, bool include_tx_hashes) const
{
std::vector<uint8_t> data;
data.reserve(std::min<size_t>(128 + m_outputAmounts.size() * 39 + m_transactions.size() * HASH_SIZE, 131072));
@@ -173,26 +178,52 @@ std::vector<uint8_t> 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<uint8_t>(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<int>(data.size()) - outputs_offset0;
}
uint8_t tx_extra[128];
uint8_t* p = tx_extra;
std::vector<uint8_t> 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<uint8_t> PoolBlock::serialize_mainchain_data(size_t* header_size, si
memcpy(p, m_merkleRoot.h, HASH_SIZE);
p += HASH_SIZE;
writeVarint(static_cast<size_t>(p - tx_extra), data);
data.insert(data.end(), tx_extra, p);
writeVarint(static_cast<size_t>(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<uint8_t> 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<const uint8_t*>(m_transactions.data());
data.insert(data.end(), t + HASH_SIZE, t + m_transactions.size() * HASH_SIZE);
const uint8_t* t = reinterpret_cast<const uint8_t*>(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<uint8_t*>(hashes), HASH_SIZE * 3, tmp.h);
// Save the coinbase tx hash into the first element of m_transactions
m_transactions[0] = static_cast<indexed_hash>(tmp);
// Save the coinbase tx hash into the first element of m_transactions
m_transactions[0] = static_cast<indexed_hash>(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<indexed_hash>(protocol_tx_hash);
}
// DEBUG: dump m_transactions for comparison
LOGINFO(0, "get_pow_hash: m_transactions.size()=" << m_transactions.size());
for (size_t dbg_i = 0; dbg_i < m_transactions.size(); ++dbg_i) {
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<hash> 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<uint8_t> 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<uint8_t> mainchain_data = serialize_mainchain_data(nullptr, nullptr, nullptr, nullptr, &zero, &zero, include_tx_hashes);
const std::vector<uint8_t> 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;
+3 -1
View File
@@ -119,7 +119,9 @@ struct PoolBlock
std::vector<std::vector<uint8_t>> m_viewTags;
std::vector<std::vector<uint8_t>> m_encryptedAnchors;
uint64_t m_amountBurnt;
hash m_txkeyPub;
std::vector<hash> m_additionalPubKeys;
uint64_t m_extraNonceSize;
uint32_t m_extraNonce;
@@ -193,7 +195,7 @@ struct PoolBlock
hash m_powHash;
hash m_seed;
std::vector<uint8_t> serialize_mainchain_data(size_t* header_size = nullptr, size_t* miner_tx_size = nullptr, int* outputs_offset = nullptr, int* outputs_blob_size = nullptr, const uint32_t* nonce = nullptr, const uint32_t* extra_nonce = nullptr) const;
std::vector<uint8_t> serialize_mainchain_data(size_t* header_size = nullptr, size_t* miner_tx_size = nullptr, int* outputs_offset = nullptr, int* outputs_blob_size = nullptr, const uint32_t* nonce = nullptr, const uint32_t* extra_nonce = nullptr, bool include_tx_hashes = true) const;
std::vector<uint8_t> serialize_sidechain_data() const;
[[nodiscard]] int deserialize(const uint8_t* data, size_t size, const SideChain& sidechain, uv_loop_t* loop, bool compact);
+97 -23
View File
@@ -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<uint64_t>::max() / MIN_OUTPUT_SIZE) return __LINE__;
if (static_cast<uint64_t>(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<uint8_t>(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<uint8_t>(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<int>(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<uint64_t>(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<size_t>(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<const hash&>(m_merkleRoot));
if (!verify_merkle_proof(check, m_merkleProof, mm_aux_slot, mm_n_aux_chains, m_merkleRoot)) {
return __LINE__;
}
+312 -76
View File
@@ -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<MinerShare>& 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<MinerShare>& 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<MinerShare>& 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<hash>& 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<hash>& 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<uint8_t>&
}
blob = block->serialize_mainchain_data();
{
std::string hex;
size_t start = 43; // approximate outputs offset
for (size_t i = start; i < std::min<size_t>(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<uint8_t> 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<uint8_t>(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<uint8_t>(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>();
data = std::make_shared<Data>();
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<ValidationOutput> 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<hash>& 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()) {
+24 -1
View File
@@ -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<uint8_t>& blob) const;
[[nodiscard]] bool get_outputs_blob(PoolBlock* block, uint64_t total_reward, std::vector<uint8_t>& blob, uv_loop_t* loop) const;
@@ -121,7 +132,15 @@ private:
void prune_seen_data();
mutable uv_rwlock_t m_sidechainLock;
std::atomic<PoolBlock*> m_chainTip;
std::atomic<PoolBlock*> m_chainTip;
// Genesis reconciliation
mutable std::atomic<bool> 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<uint64_t, std::vector<PoolBlock*>> m_blocksByHeight;
unordered_map<hash, PoolBlock*> m_blocksById;
unordered_map<root_hash, PoolBlock*> 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);
+43
View File
@@ -21,6 +21,7 @@
#include "common.h"
#include "wallet.h"
#include "keccak.h"
#include "carrot_crypto.h"
#include "crypto.h"
#include <ios>
@@ -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
+1
View File
@@ -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); }