From 02bab424c6fb2ca00cea7692509c33b41e691521 Mon Sep 17 00:00:00 2001 From: Ben Evanoff Date: Mon, 2 Mar 2026 19:00:01 -0600 Subject: [PATCH 01/15] initial SECOR proposal [WIP] --- docs/SECOR.md | 108 ++++++++++++++++ src/blockchain_db/blockchain_db.cpp | 3 +- src/blockchain_db/blockchain_db.h | 2 + src/blockchain_db/lmdb/db_lmdb.cpp | 8 +- src/blockchain_db/lmdb/db_lmdb.h | 18 ++- src/cryptonote_basic/cryptonote_basic.h | 2 + .../cryptonote_basic_impl.cpp | 35 ++++-- .../cryptonote_boost_serialization.h | 1 + src/cryptonote_config.h | 9 +- src/cryptonote_core/blockchain.cpp | 119 ++++++++++++++++-- src/cryptonote_core/blockchain.h | 4 +- src/cryptonote_core/cryptonote_core.cpp | 17 ++- src/cryptonote_core/cryptonote_tx_utils.cpp | 18 +++ src/cryptonote_core/cryptonote_tx_utils.h | 2 + .../cryptonote_protocol_handler.inl | 23 +++- src/wallet/wallet2.cpp | 50 ++++++-- 16 files changed, 379 insertions(+), 40 deletions(-) create mode 100644 docs/SECOR.md diff --git a/docs/SECOR.md b/docs/SECOR.md new file mode 100644 index 00000000..fb8c5136 --- /dev/null +++ b/docs/SECOR.md @@ -0,0 +1,108 @@ +# SECOR + +https://github.com/masari-project/research-corner/blob/master/secor/secor.pdf + +## Proposal Summary + +I would like to increase the speed of Nerva transactions by integrating the "Simple Extendend Consensus Resolution" (SECOR) protocol into Nerva. + +The goal is to better define how the network should respond to temporary chain forks when an orphan block is found and reduce deep chain reorganizations. + +### Consensus + +When two blocks are found for the top block height, all network participants should reorganize so that the block with Proof-of-Work which clears the highest difficulty should be on top. + +Miners should include the hash of the losing block in their next block via the `uncle_hash` block field. + +A miner will be rewarded an additional bonus for including that uncle block hash in their block, and the difficulty for the uncle block will be included in the chain cumulative difficulty, giving a miner who includes a valid uncle hash an advantage over miners who may choose not to. + +### Miner Transaction Format + +Miner transactions should include only 1 reward output if the uncle_hash is null. + +If an uncle block is included in the block, then the miner transaction must include a secondary transaction output. + +In order to generate a transaction on behalf of another miner in a non-interactive way without compromising privacy, we re-use the keys associated with the original uncle block miner transaction in the nephew block. + +The uncle block transaction public key from the uncle block transaction extra field should be included in nephew miner transaction at index 1. The output key from the uncle block should be re-used at nephew miner transaction output index 1. Since output index is a part of the stealth address generation algorithm, +``` +Hs(aR|i)*G + B = Hs(rA|i) + B +``` +(see cryptonote whitepaper section 4.3), +wallets should use index 0 to compute the shared secret for both outputs in a nephew block miner reward transaction. + +### Block Reward Bonuses + +I use the reward constants proposed in the original SECOR paper: +5% of the base reward amount to the nephew miner and 20% of the base reward amount to the uncle miner. The impact of this is detailed in the "Block Reward Rationale" section. + +The optimal reward for Nerva's implementation is still an active research question. + +### Block Time Target and Transaction Maturity + +Nerva currently has a target block time of 1 minute and requires a maturity of 20 blocks to spend non-miner transaction outputs. I propose to use a 15 second block time as proposed in the original SECOR paper. The first (and only known?) project to integrate SECOR with CryptoNote was Masari, which decreased from a 2 minute to a 1 minute block time. Therefore, real-world behavior with a 15 second block time is currently unknown. Further research should be done to select/confirm the optimal block time for Nerva. + +With a 20 block transaction maturity threshold and a 15 seconds block target, it should take about 5 minutes for a transaction to mature. If we succeed in preventing blockchain reorganizations > depth 1, then we may be able to decrease the threshold to only 5 blocks, or 75 seconds. + +## Block Reward Rationale +Maximum block reward emission per minute, R, given base reward X: +``` +R = X + (X/2) + (X/20) +R = X + (10X/20) + (X/20) +R = X + (11X/20) +``` + +If we keep the base emission reward the same, X=0.3, then... +``` +R = 0.3 + ((11*0.3)/20) = 0.465 +``` + +If we preserve the current emission target of R=0.3 then... +``` +0.3 = (20X/20) + (11X/20) = 31X/20 +0.3 * 20 = 31X +X = (0.3 * 20) / 31 = 0.19354838709 +``` + +If you have an estimate of how frequently uncle blocks are included in the blockchain, Z, then you can scale the reward to produce the same estimated average emission. +``` +R = X + (((X/2) + (X/20)) * Z) +R = X + ((11X/20) * Z) +``` + +If we think that uncle blocks will be included in half of the main chain blocks then... +``` +0.3 = X + ((11X/20) * 0.5) +0.3 = X + (11X/40) +0.3 = 51X / 40 +X = (0.3 * 40) / 51 = 0.23529411764 +``` + +## Status + +This proposal is a work in progress. I am seeking community feedback to see if I should continue working on this idea, and to get an idea of what people want to do about the block reward. + +### TODOs + +#### Coding + +* Address the TODO comments + +* graceful database migration - currently a sync from scratch is required after upgrading + +#### Research + +* How does changing the difficulty target affect the rest of the difficulty algo parameters ? + * CRYPTONOTE_LOCKED_TX_ALLOWED_DELTA_SECONDS_V1 references this. TX locking can be done with unix timestamps instead of blocks. This should be consistent. + * DIFFICULTY_BLOCKS_COUNT - should the DIFFICULTY_WINDOW be adjusted ? + +* How long does it currently take for a block to propogate throughout the network ? + +* Quantify frequency of orphans / chain re-organizations + +* Decide transaction maturity blocks # + +#### Testing + +* Unit test the whole thing + +* Deploy to testnet diff --git a/src/blockchain_db/blockchain_db.cpp b/src/blockchain_db/blockchain_db.cpp index 3eae66e9..d7a4b944 100644 --- a/src/blockchain_db/blockchain_db.cpp +++ b/src/blockchain_db/blockchain_db.cpp @@ -167,6 +167,7 @@ uint64_t BlockchainDB::add_block( const std::pair& blck , const difficulty_type_128& cumulative_difficulty , const uint64_t& coins_generated , const std::vector>& txs + , const crypto::hash& uncle_blk_hash ) { const block &blk = blck.first; @@ -206,7 +207,7 @@ uint64_t BlockchainDB::add_block( const std::pair& blck // call out to subclass implementation to add the block & metadata time1 = epee::misc_utils::get_tick_count(); - add_block(blk, block_weight, long_term_block_weight, cumulative_difficulty, coins_generated, num_rct_outs, blk_hash); + add_block(blk, block_weight, long_term_block_weight, cumulative_difficulty, coins_generated, num_rct_outs, blk_hash, uncle_blk_hash); TIME_MEASURE_FINISH(time1); time_add_block1 += time1; diff --git a/src/blockchain_db/blockchain_db.h b/src/blockchain_db/blockchain_db.h index b34d703f..3d3d9c57 100644 --- a/src/blockchain_db/blockchain_db.h +++ b/src/blockchain_db/blockchain_db.h @@ -384,6 +384,7 @@ class BlockchainDB , const uint64_t& coins_generated , uint64_t num_rct_outs , const crypto::hash& blk_hash + , const crypto::hash& uncle_blk_hash ) = 0; /** @@ -837,6 +838,7 @@ class BlockchainDB , const difficulty_type_128& cumulative_difficulty , const uint64_t& coins_generated , const std::vector>& txs + , const crypto::hash& uncle_blk_hash ); /** diff --git a/src/blockchain_db/lmdb/db_lmdb.cpp b/src/blockchain_db/lmdb/db_lmdb.cpp index 5c95b186..a1aac83f 100644 --- a/src/blockchain_db/lmdb/db_lmdb.cpp +++ b/src/blockchain_db/lmdb/db_lmdb.cpp @@ -679,7 +679,7 @@ uint64_t BlockchainLMDB::get_estimated_batch_size(uint64_t batch_num_blocks, uin } void BlockchainLMDB::add_block(const block& blk, size_t block_weight, uint64_t long_term_block_weight, const difficulty_type_128& cumulative_difficulty, const uint64_t& coins_generated, - uint64_t num_rct_outs, const crypto::hash& blk_hash) + uint64_t num_rct_outs, const crypto::hash& blk_hash, const crypto::hash& uncle_blk_hash) { LOG_PRINT_L3("BlockchainLMDB::" << __func__); check_open(); @@ -743,6 +743,8 @@ void BlockchainLMDB::add_block(const block& blk, size_t block_weight, uint64_t l } bi.bi_long_term_block_weight = long_term_block_weight; + bi.uncle_blk_hash = uncle_blk_hash; + MDB_val_set(val, bi); result = mdb_cursor_put(m_cur_block_info, (MDB_val *)&zerokval, &val, MDB_APPENDDUP); if (result) @@ -4079,7 +4081,7 @@ void BlockchainLMDB::block_rtxn_abort() const } uint64_t BlockchainLMDB::add_block(const std::pair& blk, size_t block_weight, uint64_t long_term_block_weight, const difficulty_type_128& cumulative_difficulty, const uint64_t& coins_generated, - const std::vector>& txs) + const std::vector>& txs, const crypto::hash& uncle_blk_hash) { LOG_PRINT_L3("BlockchainLMDB::" << __func__); check_open(); @@ -4097,7 +4099,7 @@ uint64_t BlockchainLMDB::add_block(const std::pair& blk, size_t try { - BlockchainDB::add_block(blk, block_weight, long_term_block_weight, cumulative_difficulty, coins_generated, txs); + BlockchainDB::add_block(blk, block_weight, long_term_block_weight, cumulative_difficulty, coins_generated, txs, uncle_blk_hash); } catch (const DB_ERROR_TXN_START& e) { diff --git a/src/blockchain_db/lmdb/db_lmdb.h b/src/blockchain_db/lmdb/db_lmdb.h index 82cdce40..9a0b17e2 100644 --- a/src/blockchain_db/lmdb/db_lmdb.h +++ b/src/blockchain_db/lmdb/db_lmdb.h @@ -66,7 +66,21 @@ typedef struct mdb_block_info_4 uint64_t bi_long_term_block_weight; } mdb_block_info_4; -typedef mdb_block_info_4 mdb_block_info; +typedef struct mdb_block_info_5 +{ + uint64_t bi_height; + uint64_t bi_timestamp; + uint64_t bi_coins; + uint64_t bi_weight; + uint64_t bi_diff_hi; + uint64_t bi_diff_lo; + crypto::hash bi_hash; + uint64_t bi_cum_rct; + uint64_t bi_long_term_block_weight; + crypto::hash uncle_blk_hash; +} mdb_block_info_5; + +typedef mdb_block_info_5 mdb_block_info; typedef struct txindex { crypto::hash key; @@ -348,6 +362,7 @@ class BlockchainLMDB : public BlockchainDB , const difficulty_type_128& cumulative_difficulty , const uint64_t& coins_generated , const std::vector>& txs + , const crypto::hash& uncle_blk_hash ); virtual void set_batch_transactions(bool batch_transactions); @@ -402,6 +417,7 @@ class BlockchainLMDB : public BlockchainDB , const uint64_t& coins_generated , uint64_t num_rct_outs , const crypto::hash& block_hash + , const crypto::hash& uncle_blk_hash ); virtual void remove_block(); diff --git a/src/cryptonote_basic/cryptonote_basic.h b/src/cryptonote_basic/cryptonote_basic.h index a7f0e91e..84f2189a 100644 --- a/src/cryptonote_basic/cryptonote_basic.h +++ b/src/cryptonote_basic/cryptonote_basic.h @@ -414,6 +414,7 @@ namespace cryptonote uint64_t timestamp; crypto::hash prev_id; uint32_t nonce; + crypto::hash uncle_hash; BEGIN_SERIALIZE() VARINT_FIELD(major_version) @@ -421,6 +422,7 @@ namespace cryptonote VARINT_FIELD(timestamp) FIELD(prev_id) FIELD(nonce) + if (major_version >= HF_VERSION_SECOR) FIELD(uncle_hash) END_SERIALIZE() }; diff --git a/src/cryptonote_basic/cryptonote_basic_impl.cpp b/src/cryptonote_basic/cryptonote_basic_impl.cpp index 98b0d1c1..e55ba09d 100644 --- a/src/cryptonote_basic/cryptonote_basic_impl.cpp +++ b/src/cryptonote_basic/cryptonote_basic_impl.cpp @@ -79,20 +79,29 @@ namespace cryptonote { } //----------------------------------------------------------------------------------------------- bool get_block_reward(size_t median_weight, size_t current_block_weight, uint64_t already_generated_coins, uint64_t &reward, uint8_t version) { - static_assert(DIFFICULTY_TARGET % 60 == 0,"difficulty targets must be a multiple of 60"); - const int target_minutes = DIFFICULTY_TARGET / 60; - const int emission_speed_factor = EMISSION_SPEED_FACTOR_PER_MINUTE - (target_minutes-1); - - const uint64_t premine = 180000000000000000U; - if (median_weight > 0 && already_generated_coins < premine) { - reward = premine; - return true; - } - - uint64_t base_reward = (MONEY_SUPPLY - already_generated_coins) >> emission_speed_factor; - if (base_reward < FINAL_SUBSIDY_PER_MINUTE*target_minutes) + uint64_t base_reward = 0; + if (version < HF_VERSION_SECOR) { - base_reward = FINAL_SUBSIDY_PER_MINUTE*target_minutes; + static_assert(DIFFICULTY_TARGET % 60 == 0,"difficulty targets must be a multiple of 60"); + const int target_minutes = DIFFICULTY_TARGET / 60; + const int emission_speed_factor = EMISSION_SPEED_FACTOR_PER_MINUTE - (target_minutes-1); + + const uint64_t premine = 180000000000000000U; + if (median_weight > 0 && already_generated_coins < premine) { + reward = premine; + return true; + } + + base_reward = (MONEY_SUPPLY - already_generated_coins) >> emission_speed_factor; + if (base_reward < FINAL_SUBSIDY_PER_MINUTE*target_minutes) + { + base_reward = FINAL_SUBSIDY_PER_MINUTE*target_minutes; + } + } + else + { + // assumes that tail emission was already reached before/by HF_VERSION_SECOR + base_reward = (FINAL_SUBSIDY_PER_MINUTE / 60) * DIFFICULTY_TARGET_SECOR; } uint64_t full_reward_zone = get_min_block_weight(version); diff --git a/src/cryptonote_basic/cryptonote_boost_serialization.h b/src/cryptonote_basic/cryptonote_boost_serialization.h index 44c3ebb5..5f1cbb3f 100644 --- a/src/cryptonote_basic/cryptonote_boost_serialization.h +++ b/src/cryptonote_basic/cryptonote_boost_serialization.h @@ -182,6 +182,7 @@ namespace boost a & b.prev_id; a & b.nonce; //------------------ + if (b.major_version >= HF_VERSION_SECOR) a & b.uncle_hash; a & b.miner_tx; a & b.tx_hashes; } diff --git a/src/cryptonote_config.h b/src/cryptonote_config.h index effeb3bd..16f72fe9 100644 --- a/src/cryptonote_config.h +++ b/src/cryptonote_config.h @@ -81,12 +81,12 @@ #define DYNAMIC_FEE_PER_KB_BASE_BLOCK_REWARD ((uint64_t)10000000000000) #define DIFFICULTY_TARGET 60 +#define DIFFICULTY_TARGET_SECOR 15 #define DIFFICULTY_WINDOW 720 #define DIFFICULTY_LAG 15 #define DIFFICULTY_CUT 60 #define DIFFICULTY_BLOCKS_COUNT DIFFICULTY_WINDOW + DIFFICULTY_LAG -#define DIFFICULTY_BLOCKS_ESTIMATE_TIMESPAN DIFFICULTY_TARGET #define DIFFICULTY_WINDOW_V2 17 #define DIFFICULTY_CUT_V2 6 #define DIFFICULTY_BLOCKS_COUNT_V2 DIFFICULTY_WINDOW_V2 + DIFFICULTY_CUT_V2 * 2 @@ -171,6 +171,10 @@ #define CRYPTONOTE_MAX_FRAGMENTS 20 +#define HF_VERSION_SECOR 13 +#define SECOR_UNCLE_REWARD_RATIO 2 +#define SECOR_NEPHEW_REWARD_RATIO 20 + #define DONATION_ADDR "NV1aMtARDQjK8j7XeoQ66S7XQe5ZS8CX92XqXmJxSZMpSDf2i11NQyqgHzghmRsDHR1LwYv3bEnE3VoqqbmyRdrR2MMBfdXvY" struct hard_fork @@ -258,7 +262,8 @@ namespace config { 9, 570}, {10, 580}, {11, 590}, - {12, 700} + {12, 700}, + {13, 800} }; } } diff --git a/src/cryptonote_core/blockchain.cpp b/src/cryptonote_core/blockchain.cpp index 6c412ecb..84625a3e 100644 --- a/src/cryptonote_core/blockchain.cpp +++ b/src/cryptonote_core/blockchain.cpp @@ -790,7 +790,7 @@ crypto::hash Blockchain::get_pending_block_id_by_height(uint64_t height) const return get_block_id_by_height(height); } //------------------------------------------------------------------ -bool Blockchain::get_block_by_hash(const crypto::hash &h, block &blk, bool *orphan) const +bool Blockchain::get_block_by_hash(const crypto::hash &h, block &blk, bool *orphan, bool *uncle) const { LOG_PRINT_L3("Blockchain::" << __func__); CRITICAL_REGION_LOCAL(m_blockchain_lock); @@ -801,6 +801,8 @@ bool Blockchain::get_block_by_hash(const crypto::hash &h, block &blk, bool *orph blk = m_db->get_block(h); if (orphan) *orphan = false; + if (uncle) + *uncle = false; return true; } // try to find block in alternative chain @@ -815,8 +817,22 @@ bool Blockchain::get_block_by_hash(const crypto::hash &h, block &blk, bool *orph MERROR("Found block " << h << " in alt chain, but failed to parse it"); throw std::runtime_error("Found block in alt chain, but failed to parse it"); } + + if (data.height >= m_db->height()-1) // if the block has a height equal to that of the top block, then it can not be an uncle + { + if (orphan) + *orphan = true; + if (uncle) + *uncle = false; + return true; + } + cryptonote::block nephew_candidate = m_db->get_block(m_db->get_block_hash_from_height(data.height+1)); + if (orphan) - *orphan = true; + *orphan = nephew_candidate.uncle_hash != h; + if (uncle) + *uncle = nephew_candidate.uncle_hash == h; + return true; } } @@ -918,7 +934,7 @@ uint64_t Blockchain::get_difficulty_for_next_block() m_timestamps = timestamps; m_difficulties = difficulties; } - size_t target = DIFFICULTY_TARGET; + size_t target = version >= HF_VERSION_SECOR ? DIFFICULTY_TARGET_SECOR : DIFFICULTY_TARGET; uint64_t diff; if (version == 1) { diff = next_difficulty(timestamps, difficulties, target); @@ -1172,7 +1188,7 @@ uint64_t Blockchain::get_next_difficulty_for_alternative_chain(const std::list= HF_VERSION_SECOR ? DIFFICULTY_TARGET_SECOR : DIFFICULTY_TARGET; // calculate the difficulty target for the block and return it if (version == 1) { @@ -1242,7 +1258,25 @@ bool Blockchain::validate_miner_transaction(const block& b, size_t cumulative_bl return false; } - const uint64_t total_reward = base_reward + fee; + uint64_t total_reward = base_reward + fee; + if (b.uncle_hash != crypto::null_hash) + { + total_reward += base_reward / SECOR_UNCLE_REWARD_RATIO; + // make sure that the uncle reward uses the output key from the uncle block miner tx and is for the correct amount - dont let the nephew miner steal the uncle reward + cryptonote::block uncle_block; + get_block_by_hash(b.uncle_hash, uncle_block); + // TODO: enforce nephew reward at index 0 and uncle reward at index 1 + if (boost::get(b.miner_tx.vout[1].target).key != boost::get(uncle_block.miner_tx.vout[0].target).key) + { + MERROR_VER("nephew block miner tx does not include proper uncle reward output key"); + return false; + } + if (get_tx_pub_key_from_extra(b.miner_tx, 1) != get_tx_pub_key_from_extra(uncle_block.miner_tx, 0)) + { + MERROR_VER("nephew block miner tx does not include proper uncle reward tx public key in tx extra"); + return false; + } + } if(total_reward != money_in_use && already_generated_coins > 0) { MERROR_VER("coinbase transaction doesn't use full amount of block reward: spent " << money_in_use @@ -1438,6 +1472,7 @@ bool Blockchain::create_block_template(block& b, const crypto::hash *from_block, already_generated_coins = m_db->get_block_already_generated_coins(height - 1); } b.timestamp = time(NULL); + b.uncle_hash = crypto::null_hash; uint64_t median_ts; if (!check_block_timestamp(b, median_ts)) @@ -1454,6 +1489,21 @@ bool Blockchain::create_block_template(block& b, const crypto::hash *from_block, return false; } pool_cookie = m_tx_pool.cookie(); + + const uint8_t hf_version = m_hardfork->get_current_version(); + if (hf_version >= HF_VERSION_SECOR) + { + // check alt chains for a block with the same height as the top block + for (auto it = m_uncle_candidates.end(); it != m_uncle_candidates.begin(); it--) + { + if (it->second == height - 1) + { + LOG_PRINT_L0("uncle block candidate found: " << it->first << " " << it->second); + b.uncle_hash = it->first; + } + } + } + #if defined(DEBUG_CREATE_BLOCK_TEMPLATE) size_t real_txs_weight = 0; uint64_t real_fee = 0; @@ -1500,8 +1550,15 @@ bool Blockchain::create_block_template(block& b, const crypto::hash *from_block, block weight, so first miner transaction generated with fake amount of money, and with phase we know think we know expected block weight */ //make blocks coin-base tx looks close to real coinbase tx to get truthful blob size - uint8_t hf_version = m_hardfork->get_current_version(); bool r = construct_miner_tx(height, median_weight, already_generated_coins, txs_weight, fee, miner_address, b.miner_tx, ex_nonce, 0, hf_version); + if (b.uncle_hash != crypto::null_hash) + { + cryptonote::block uncle_block; + get_block_by_hash(b.uncle_hash, uncle_block); + crypto::public_key uncle_out = boost::get(uncle_block.miner_tx.vout[0].target).key; + crypto::public_key uncle_tx_pubkey = get_tx_pub_key_from_extra(uncle_block.miner_tx); + construct_uncle_miner_tx(expected_reward, uncle_out, uncle_tx_pubkey, b.miner_tx); // should we be using the reward calculated from previous hieght / uncle chain ? + } CHECK_AND_ASSERT_MES(r, false, "Failed to construct miner tx, first chance"); size_t cumulative_weight = txs_weight + get_transaction_weight(b.miner_tx); #if defined(DEBUG_CREATE_BLOCK_TEMPLATE) @@ -1511,7 +1568,14 @@ bool Blockchain::create_block_template(block& b, const crypto::hash *from_block, for (size_t try_count = 0; try_count != 10; ++try_count) { r = construct_miner_tx(height, median_weight, already_generated_coins, cumulative_weight, fee, miner_address, b.miner_tx, ex_nonce, 0, hf_version); - + if (b.uncle_hash != crypto::null_hash) + { + cryptonote::block uncle_block; + get_block_by_hash(b.uncle_hash, uncle_block); + crypto::public_key uncle_out = boost::get(uncle_block.miner_tx.vout[0].target).key; + crypto::public_key uncle_tx_pubkey = get_tx_pub_key_from_extra(uncle_block.miner_tx); + construct_uncle_miner_tx(expected_reward, uncle_out, uncle_tx_pubkey, b.miner_tx); // should we be using the reward calculated from previous hieght / uncle chain ? + } CHECK_AND_ASSERT_MES(r, false, "Failed to construct miner tx, second chance"); size_t coinbase_weight = get_transaction_weight(b.miner_tx); if (coinbase_weight > cumulative_weight - txs_weight) @@ -1805,6 +1869,13 @@ bool Blockchain::handle_alternative_block(const block& b, const crypto::hash& id data.already_generated_coins = bei.already_generated_coins; m_db->add_alt_block(id, data, cryptonote::block_to_blob(bei.bl)); alt_chain.push_back(bei); + m_uncle_candidates.push_back(std::make_pair(id, data.height)); // TODO: free old data at some point + + uint64_t top_block_height = m_db->height()-1; + cryptonote::block top_block = m_db->get_top_block(); + crypto::hash pow_top_block; + memset(pow_top_block.data, 0xff, sizeof(pow_top_block.data)); + get_block_longhash(m_hash_context, this, top_block, pow_top_block, top_block_height); // FIXME: is it even possible for a checkpoint to show up not on the main chain? if(is_a_checkpoint) @@ -1831,6 +1902,17 @@ bool Blockchain::handle_alternative_block(const block& b, const crypto::hash& id bvc.m_verifivation_failed = true; return r; } + else if (block_height == top_block_height && reinterpret_cast(pow_top_block) < reinterpret_cast(proof_of_work)) // TODO: enforce this when processing nephew blocks in handle block to main chain + { + MGINFO_GREEN("###### REORGANIZE DUE TO UNCLE BLOCK on height: " << alt_chain.front().height << " of " << m_db->height() - 1 << " with cum_difficulty " << m_db->get_block_cumulative_difficulty(m_db->height() - 1) << std::endl << " alternative blockchain size: " << alt_chain.size() << " with cum_difficulty " << bei.cumulative_difficulty); + + bool r = switch_to_alternative_blockchain(alt_chain, false); + if (r) + bvc.m_added_to_main_chain = true; + else + bvc.m_verifivation_failed = true; + return r; + } else { MGINFO_BLUE("----- BLOCK ADDED AS ALTERNATIVE ON HEIGHT " << bei.height << std::endl << "id:\t" << id << std::endl << "PoW:\t" << proof_of_work << std::endl << "difficulty:\t" << current_diff); @@ -3693,6 +3775,25 @@ bool Blockchain::handle_block_to_main_chain(const block& bl, const crypto::hash& if(blockchain_height) cumulative_difficulty += m_db->get_block_cumulative_difficulty(blockchain_height - 1); + if (bl.uncle_hash != crypto::null_hash) + { + CHECK_AND_ASSERT_MES(blockchain_height, 0, "blockchain_height is NULL"); + cryptonote::block uncle_block; + get_block_by_hash(bl.uncle_hash, uncle_block); + + // validate uncle block PoW + difficulty_type_128 uncle_difficulty = m_db->get_block_cumulative_difficulty(blockchain_height-2) - m_db->get_block_cumulative_difficulty(blockchain_height-3); + crypto::hash uncle_pow; + memset(uncle_pow.data, 0xff, sizeof(uncle_pow.data)); + get_block_longhash(m_hash_context, this, uncle_block, uncle_pow, m_db->height()-2); + check_hash(uncle_pow, (uint64_t)uncle_difficulty); // is this safe? + + // add block difficulty for uncle block(s) to chain cumulative difficulty + cumulative_difficulty += uncle_difficulty; + + // TODO: support extended ancestry ? + } + TIME_MEASURE_FINISH(block_processing_time); rtxn_guard.stop(); @@ -3704,7 +3805,7 @@ bool Blockchain::handle_block_to_main_chain(const block& bl, const crypto::hash& { uint64_t long_term_block_weight = get_next_long_term_block_weight(block_weight); cryptonote::blobdata bd = cryptonote::block_to_blob(bl); - new_height = m_db->add_block(std::make_pair(std::move(bl), std::move(bd)), block_weight, long_term_block_weight, cumulative_difficulty, already_generated_coins, txs); + new_height = m_db->add_block(std::make_pair(std::move(bl), std::move(bd)), block_weight, long_term_block_weight, cumulative_difficulty, already_generated_coins, txs, bl.uncle_hash); } catch (const KEY_IMAGE_EXISTS& e) { @@ -4631,7 +4732,7 @@ bool Blockchain::get_hard_fork_voting_info(uint8_t version, uint32_t &window, ui uint64_t Blockchain::get_difficulty_target() const { - return DIFFICULTY_TARGET; + return get_current_hard_fork_version() >= HF_VERSION_SECOR ? DIFFICULTY_TARGET_SECOR : DIFFICULTY_TARGET; } std::map> Blockchain:: get_output_histogram(const std::vector &amounts, bool unlocked, uint64_t recent_cutoff, uint64_t min_count) const diff --git a/src/cryptonote_core/blockchain.h b/src/cryptonote_core/blockchain.h index eace9477..f7026069 100644 --- a/src/cryptonote_core/blockchain.h +++ b/src/cryptonote_core/blockchain.h @@ -236,7 +236,7 @@ namespace cryptonote * * @return true if the block was found, else false */ - bool get_block_by_hash(const crypto::hash &h, block &blk, bool *orphan = NULL) const; + bool get_block_by_hash(const crypto::hash &h, block &blk, bool *orphan = NULL, bool *uncle = NULL) const; /** * @brief performs some preprocessing on a group of incoming blocks to speed up verification @@ -1095,6 +1095,8 @@ namespace cryptonote uint64_t m_prepare_nblocks; std::vector *m_prepare_blocks; + std::list> m_uncle_candidates; // id, height + /** * @brief collects the keys for all outputs being "spent" as an input * diff --git a/src/cryptonote_core/cryptonote_core.cpp b/src/cryptonote_core/cryptonote_core.cpp index 7e4e034d..f0fd079e 100644 --- a/src/cryptonote_core/cryptonote_core.cpp +++ b/src/cryptonote_core/cryptonote_core.cpp @@ -702,8 +702,8 @@ namespace cryptonote r = m_miner.init(vm, m_nettype); CHECK_AND_ASSERT_MES(r, false, "Failed to initialize miner instance"); - if (!keep_alt_blocks && !m_blockchain_storage.get_db().is_read_only()) - m_blockchain_storage.get_db().drop_alt_blocks(); + // if (!keep_alt_blocks && !m_blockchain_storage.get_db().is_read_only()) + // m_blockchain_storage.get_db().drop_alt_blocks(); if (prune_blockchain) { @@ -1443,6 +1443,19 @@ namespace cryptonote CHECK_AND_ASSERT_MES(!bvc.m_verifivation_failed, false, "mined block failed verification"); if(bvc.m_added_to_main_chain) { + if (b.uncle_hash != crypto::null_hash) + { + // first relay uncle blocks if an uncle block hash is included in the block + cryptonote_connection_context exclude_context = {}; + NOTIFY_NEW_BLOCK::request arg = AUTO_VAL_INIT(arg); + arg.current_blockchain_height = m_blockchain_storage.get_current_blockchain_height()-1; // -1 per uncle block definition + cryptonote::block uncle_block; + get_block_by_hash(b.uncle_hash, uncle_block); + block_to_blob(uncle_block, arg.b.block); + m_pprotocol->relay_block(arg, exclude_context); + // TODO: this should be recursive - if the uncle's prev_id isnt in the main chain then continue relaying the alt chain history until a common ancestor is found + } + cryptonote_connection_context exclude_context = {}; NOTIFY_NEW_BLOCK::request arg = AUTO_VAL_INIT(arg); arg.current_blockchain_height = m_blockchain_storage.get_current_blockchain_height(); diff --git a/src/cryptonote_core/cryptonote_tx_utils.cpp b/src/cryptonote_core/cryptonote_tx_utils.cpp index 60723c33..359461b2 100644 --- a/src/cryptonote_core/cryptonote_tx_utils.cpp +++ b/src/cryptonote_core/cryptonote_tx_utils.cpp @@ -139,6 +139,24 @@ namespace cryptonote return true; } //--------------------------------------------------------------- + bool construct_uncle_miner_tx(uint64_t amount, crypto::public_key out_eph_public_key, crypto::public_key tx_pubkey, transaction& tx) + { + add_tx_pub_key_to_extra(tx, tx_pubkey); + + txout_to_key tk; + tk.key = out_eph_public_key; + + tx_out out; + out.amount = amount / SECOR_UNCLE_REWARD_RATIO; + out.target = tk; + tx.vout.push_back(out); + + //lock + tx.invalidate_hashes(); + + return true; + } + //--------------------------------------------------------------- bool construct_genesis_tx(transaction& tx, uint64_t amount) { tx.vin.clear(); tx.vout.clear(); diff --git a/src/cryptonote_core/cryptonote_tx_utils.h b/src/cryptonote_core/cryptonote_tx_utils.h index ee2d5661..77957ee2 100644 --- a/src/cryptonote_core/cryptonote_tx_utils.h +++ b/src/cryptonote_core/cryptonote_tx_utils.h @@ -48,6 +48,8 @@ namespace cryptonote //--------------------------------------------------------------- bool construct_miner_tx(size_t height, size_t median_weight, uint64_t already_generated_coins, size_t current_block_weight, uint64_t fee, const account_public_address &miner_address, transaction& tx, const blobdata& extra_nonce = blobdata(), size_t max_outs = 999, uint8_t hard_fork_version = 1); + bool construct_uncle_miner_tx(uint64_t amount, crypto::public_key out_eph_public_key, crypto::public_key tx_pubkey, transaction& tx); + bool construct_genesis_tx(transaction& tx, uint64_t amount); struct tx_source_entry diff --git a/src/cryptonote_protocol/cryptonote_protocol_handler.inl b/src/cryptonote_protocol/cryptonote_protocol_handler.inl index 2e0618aa..efbd1b54 100644 --- a/src/cryptonote_protocol/cryptonote_protocol_handler.inl +++ b/src/cryptonote_protocol/cryptonote_protocol_handler.inl @@ -432,7 +432,8 @@ namespace cryptonote template int t_cryptonote_protocol_handler::handle_notify_new_block(int command, NOTIFY_NEW_BLOCK::request& arg, cryptonote_connection_context& context) { - MLOGIF_P2P_MESSAGE(crypto::hash hash; cryptonote::block b; bool ret = cryptonote::parse_and_validate_block_from_blob(arg.b.block, b, &hash);, ret, "Received NOTIFY_NEW_BLOCK " << hash << " (height " << arg.current_blockchain_height << ", " << arg.b.txs.size() << " txes)"); + cryptonote::block b; + MLOGIF_P2P_MESSAGE(crypto::hash hash; b; bool ret = cryptonote::parse_and_validate_block_from_blob(arg.b.block, b, &hash);, ret, "Received NOTIFY_NEW_BLOCK " << hash << " (height " << arg.current_blockchain_height << ", " << arg.b.txs.size() << " txes)"); if(context.m_state != cryptonote_connection_context::state_normal) return 1; if(!is_synchronized() || m_no_sync) // can happen if a peer connection goes to normal but another thread still hasn't finished adding queued blocks @@ -483,6 +484,16 @@ namespace cryptonote if(bvc.m_added_to_main_chain) { //TODO: Add here announce protocol usage + if (b.uncle_hash != crypto::null_hash) + { + cryptonote_connection_context exclude_context = {}; + NOTIFY_NEW_BLOCK::request arg_uncle_request = AUTO_VAL_INIT(arg_uncle_request); + arg_uncle_request.current_blockchain_height = arg.current_blockchain_height-1; // -1 per uncle block definition + cryptonote::block uncle_block; + m_core.get_block_by_hash(b.uncle_hash, uncle_block); + block_to_blob(uncle_block, arg_uncle_request.b.block); + relay_block(arg_uncle_request, exclude_context); + } relay_block(arg, context); }else if(bvc.m_marked_as_orphaned) { @@ -761,6 +772,16 @@ namespace cryptonote if( bvc.m_added_to_main_chain ) { //TODO: Add here announce protocol usage + if (new_block.uncle_hash != crypto::null_hash) + { + cryptonote_connection_context exclude_context = {}; + NOTIFY_NEW_BLOCK::request arg_uncle_request = AUTO_VAL_INIT(arg_uncle_request); + arg_uncle_request.current_blockchain_height = arg.current_blockchain_height-1; // -1 per uncle block definition + cryptonote::block uncle_block; + m_core.get_block_by_hash(new_block.uncle_hash, uncle_block); + block_to_blob(uncle_block, arg_uncle_request.b.block); + relay_block(arg_uncle_request, exclude_context); + } NOTIFY_NEW_BLOCK::request reg_arg = AUTO_VAL_INIT(reg_arg); reg_arg.current_blockchain_height = arg.current_blockchain_height; reg_arg.b = b; diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp index 94c0c76e..e05673a9 100644 --- a/src/wallet/wallet2.cpp +++ b/src/wallet/wallet2.cpp @@ -1707,7 +1707,11 @@ void wallet2::scan_output(const cryptonote::transaction &tx, bool miner_tx, cons } else { - bool r = cryptonote::generate_key_image_helper_precomp(m_account.get_keys(), boost::get(tx.vout[i].target).key, tx_scan_info.received->derivation, i, tx_scan_info.received->index, tx_scan_info.in_ephemeral, tx_scan_info.ki, m_account.get_device()); + bool r; + if (miner_tx && i > 0) + r = cryptonote::generate_key_image_helper_precomp(m_account.get_keys(), boost::get(tx.vout[i].target).key, tx_scan_info.received->derivation, 0, {0, 0}, tx_scan_info.in_ephemeral, tx_scan_info.ki, m_account.get_device()); + else + r = cryptonote::generate_key_image_helper_precomp(m_account.get_keys(), boost::get(tx.vout[i].target).key, tx_scan_info.received->derivation, i, tx_scan_info.received->index, tx_scan_info.in_ephemeral, tx_scan_info.ki, m_account.get_device()); THROW_WALLET_EXCEPTION_IF(!r, error::wallet_internal_error, "Failed to generate key image"); THROW_WALLET_EXCEPTION_IF(tx_scan_info.in_ephemeral.pub != boost::get(tx.vout[i].target).key, error::wallet_internal_error, "key_image generated ephemeral public key not matched with output_key"); @@ -1931,15 +1935,47 @@ void wallet2::process_new_transaction(const crypto::hash &txid, const cryptonote { for (size_t i = 0; i < tx.vout.size(); ++i) { - check_acc_out_precomp_once(tx.vout[i], derivation, additional_derivations, i, is_out_data_ptr, tx_scan_info[i], output_found[i]); - THROW_WALLET_EXCEPTION_IF(tx_scan_info[i].error, error::acc_outs_lookup_error, tx, tx_pub_key, m_account.get_keys()); - if (tx_scan_info[i].received) + if (miner_tx && i > 0) { + crypto::key_derivation additional_key_derivation; hw::device &hwdev = m_account.get_device(); boost::unique_lock hwdev_lock (hwdev); - hwdev.set_mode(hw::device::NONE); - hwdev.conceal_derivation(tx_scan_info[i].received->derivation, tx_pub_key, additional_tx_pub_keys.data, derivation, additional_derivations); - scan_output(tx, miner_tx, tx_pub_key, i, tx_scan_info[i], num_vouts_received, tx_money_got_in_outs, outs, pool); + hwdev.set_mode(hw::device::TRANSACTION_PARSE); + crypto::public_key additional_tx_pubkey = get_tx_pub_key_from_extra(tx, i); + if (!hwdev.generate_key_derivation(additional_tx_pubkey, keys.m_view_secret_key, additional_key_derivation)) + { + std::cout << "Failed to generate key derivation from additional tx pubkey in " << txid << std::endl; + MWARNING("Failed to generate key derivation from additional tx pubkey in " << txid); + } + check_acc_out_precomp(tx.vout[i], additional_key_derivation, additional_derivations, 0, tx_scan_info[i]); + THROW_WALLET_EXCEPTION_IF(tx_scan_info[i].error, error::acc_outs_lookup_error, tx, additional_tx_pubkey, m_account.get_keys()); + if (tx_scan_info[i].received) + { + LOG_PRINT_L0("Scanning potential uncle reward"); + auto vouts_start = num_vouts_received; + hw::device &hwdev = m_account.get_device(); + boost::unique_lock hwdev_lock (hwdev); + hwdev.set_mode(hw::device::NONE); + hwdev.conceal_derivation(tx_scan_info[i].received->derivation, additional_tx_pubkey, additional_tx_pub_keys.data, additional_key_derivation, additional_derivations); + scan_output(tx, miner_tx, additional_tx_pubkey, i, tx_scan_info[i], num_vouts_received, tx_money_got_in_outs, outs, pool); + if (num_vouts_received > vouts_start) + { + LOG_PRINT_L0("Found uncle reward"); + } + } + } + else + { + check_acc_out_precomp(tx.vout[i], derivation, additional_derivations, i, is_out_data_ptr, tx_scan_info[i]); + THROW_WALLET_EXCEPTION_IF(tx_scan_info[i].error, error::acc_outs_lookup_error, tx, tx_pub_key, m_account.get_keys()); + if (tx_scan_info[i].received) + { + hw::device &hwdev = m_account.get_device(); + boost::unique_lock hwdev_lock (hwdev); + hwdev.set_mode(hw::device::NONE); + hwdev.conceal_derivation(tx_scan_info[i].received->derivation, tx_pub_key, additional_tx_pub_keys.data, derivation, additional_derivations); + scan_output(tx, miner_tx, tx_pub_key, i, tx_scan_info[i], num_vouts_received, tx_money_got_in_outs, outs, pool); + } } } } From 9601de3aaf5e3ec85c5490e0c5e2bd46b7a2f85d Mon Sep 17 00:00:00 2001 From: Ben Evanoff Date: Mon, 2 Mar 2026 20:38:24 -0600 Subject: [PATCH 02/15] SECOR: add nephew reward to nephew block txs --- src/cryptonote_core/blockchain.cpp | 5 +++-- src/cryptonote_core/cryptonote_tx_utils.cpp | 3 ++- src/cryptonote_core/cryptonote_tx_utils.h | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/cryptonote_core/blockchain.cpp b/src/cryptonote_core/blockchain.cpp index 84625a3e..4933f2d8 100644 --- a/src/cryptonote_core/blockchain.cpp +++ b/src/cryptonote_core/blockchain.cpp @@ -1261,6 +1261,7 @@ bool Blockchain::validate_miner_transaction(const block& b, size_t cumulative_bl uint64_t total_reward = base_reward + fee; if (b.uncle_hash != crypto::null_hash) { + total_reward += base_reward / SECOR_NEPHEW_REWARD_RATIO; total_reward += base_reward / SECOR_UNCLE_REWARD_RATIO; // make sure that the uncle reward uses the output key from the uncle block miner tx and is for the correct amount - dont let the nephew miner steal the uncle reward cryptonote::block uncle_block; @@ -1550,7 +1551,7 @@ bool Blockchain::create_block_template(block& b, const crypto::hash *from_block, block weight, so first miner transaction generated with fake amount of money, and with phase we know think we know expected block weight */ //make blocks coin-base tx looks close to real coinbase tx to get truthful blob size - bool r = construct_miner_tx(height, median_weight, already_generated_coins, txs_weight, fee, miner_address, b.miner_tx, ex_nonce, 0, hf_version); + bool r = construct_miner_tx(height, median_weight, already_generated_coins, txs_weight, fee, miner_address, b.miner_tx, ex_nonce, 0, hf_version, b.uncle_hash != crypto::null_hash); if (b.uncle_hash != crypto::null_hash) { cryptonote::block uncle_block; @@ -1567,7 +1568,7 @@ bool Blockchain::create_block_template(block& b, const crypto::hash *from_block, #endif for (size_t try_count = 0; try_count != 10; ++try_count) { - r = construct_miner_tx(height, median_weight, already_generated_coins, cumulative_weight, fee, miner_address, b.miner_tx, ex_nonce, 0, hf_version); + r = construct_miner_tx(height, median_weight, already_generated_coins, cumulative_weight, fee, miner_address, b.miner_tx, ex_nonce, 0, hf_version, b.uncle_hash != crypto::null_hash); if (b.uncle_hash != crypto::null_hash) { cryptonote::block uncle_block; diff --git a/src/cryptonote_core/cryptonote_tx_utils.cpp b/src/cryptonote_core/cryptonote_tx_utils.cpp index 359461b2..c5debfd7 100644 --- a/src/cryptonote_core/cryptonote_tx_utils.cpp +++ b/src/cryptonote_core/cryptonote_tx_utils.cpp @@ -81,7 +81,7 @@ namespace cryptonote } //--------------------------------------------------------------- bool construct_miner_tx(size_t height, size_t median_weight, uint64_t already_generated_coins, size_t current_block_weight, uint64_t fee, const account_public_address &miner_address, - transaction& tx, const blobdata& extra_nonce, size_t max_outs, uint8_t hard_fork_version) { + transaction& tx, const blobdata& extra_nonce, size_t max_outs, uint8_t hard_fork_version, bool uncle_reward) { tx.vin.clear(); tx.vout.clear(); tx.extra.clear(); @@ -108,6 +108,7 @@ namespace cryptonote LOG_PRINT_L1("Creating block template: reward " << block_reward << ", fee " << fee); #endif + if (uncle_reward) block_reward += block_reward / SECOR_NEPHEW_REWARD_RATIO; block_reward += fee; crypto::key_derivation derivation = AUTO_VAL_INIT(derivation); diff --git a/src/cryptonote_core/cryptonote_tx_utils.h b/src/cryptonote_core/cryptonote_tx_utils.h index 77957ee2..c6e2e6ec 100644 --- a/src/cryptonote_core/cryptonote_tx_utils.h +++ b/src/cryptonote_core/cryptonote_tx_utils.h @@ -46,7 +46,7 @@ namespace cryptonote { //--------------------------------------------------------------- - bool construct_miner_tx(size_t height, size_t median_weight, uint64_t already_generated_coins, size_t current_block_weight, uint64_t fee, const account_public_address &miner_address, transaction& tx, const blobdata& extra_nonce = blobdata(), size_t max_outs = 999, uint8_t hard_fork_version = 1); + bool construct_miner_tx(size_t height, size_t median_weight, uint64_t already_generated_coins, size_t current_block_weight, uint64_t fee, const account_public_address &miner_address, transaction& tx, const blobdata& extra_nonce = blobdata(), size_t max_outs = 999, uint8_t hard_fork_version = 1, bool uncle_reward = false); bool construct_uncle_miner_tx(uint64_t amount, crypto::public_key out_eph_public_key, crypto::public_key tx_pubkey, transaction& tx); From f326d63c7235da7a02e2e337c8e61594a400e078 Mon Sep 17 00:00:00 2001 From: Ben Evanoff Date: Thu, 12 Mar 2026 21:30:01 -0500 Subject: [PATCH 03/15] basic tx frequency analysis script --- scripts/analyze_tx_frequency.py | 42 +++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 scripts/analyze_tx_frequency.py diff --git a/scripts/analyze_tx_frequency.py b/scripts/analyze_tx_frequency.py new file mode 100644 index 00000000..83e353e8 --- /dev/null +++ b/scripts/analyze_tx_frequency.py @@ -0,0 +1,42 @@ +import os +import json +import requests + +DAEMON_HOST = 'http://127.0.0.1:17566/json_rpc' + +def get_top_block() -> (str, int): + r = requests.post(DAEMON_HOST, json={'method': 'get_info'}) + return r.json()['result']['top_block_hash'], r.json()['result']['height'] + +def get_block(block_hash:str) -> dict: + r = requests.post(DAEMON_HOST, json={'method': 'get_block', 'params': {'hash': block_hash}}) + return json.loads(r.json()['result']['json']) + +if __name__ == '__main__': + top_block_hash, top_block_height = get_top_block() + + + empty_block_counter = 0 + blocks_found = 0 + total_blocks_scanned = 0 + + non_miner_txs = [] + height = top_block_height + next_block_hash = top_block_hash + while height > 1: # speed could be improved by batching the network requests + block = get_block(next_block_hash) + non_miner_txs = block['tx_hashes'] + next_block_hash = block['prev_id'] + total_blocks_scanned += 1 + + if block['tx_hashes']: + blocks_found += 1 + # print(block) + height = block['miner_tx']['vin'][0]['gen']['height'] + print(f'{empty_block_counter} block searched since last block') + print(f'{blocks_found} / {total_blocks_scanned} are not empty ({(float(blocks_found)/float(total_blocks_scanned))*100.0}%)') + empty_block_counter = 0 # reset counter to get gap until next non-empty block + else: + empty_block_counter += 1 + + print(f'{blocks_found} / {total_blocks_scanned} are not empty ({(float(blocks_found)/float(total_blocks_scanned))*100.0}%)') \ No newline at end of file From 8d9346a280729ad3e3f012e68309c4685ae20baa Mon Sep 17 00:00:00 2001 From: Ben Evanoff Date: Thu, 12 Mar 2026 21:58:21 -0500 Subject: [PATCH 04/15] refactor analyze tx frequency script --- scripts/analyze_tx_frequency.py | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/scripts/analyze_tx_frequency.py b/scripts/analyze_tx_frequency.py index 83e353e8..7de08b9f 100644 --- a/scripts/analyze_tx_frequency.py +++ b/scripts/analyze_tx_frequency.py @@ -1,4 +1,3 @@ -import os import json import requests @@ -12,31 +11,39 @@ def get_block(block_hash:str) -> dict: r = requests.post(DAEMON_HOST, json={'method': 'get_block', 'params': {'hash': block_hash}}) return json.loads(r.json()['result']['json']) -if __name__ == '__main__': - top_block_hash, top_block_height = get_top_block() +class BlockchainRPCIterator: + ''' + Iterate from the top of the chain to the origin block via RPC + ''' + def __init__(self): # TODO allow caller to set a batch size to get blocks in batches + self.next_block_hash, self.height = get_top_block() + + def __iter__(self): + return self + def __next__(self): + block = get_block(self.next_block_hash) + self.next_block_hash = block['prev_id'] + self.height = block['miner_tx']['vin'][0]['gen']['height'] + return block + +if __name__ == '__main__': empty_block_counter = 0 blocks_found = 0 total_blocks_scanned = 0 - non_miner_txs = [] - height = top_block_height - next_block_hash = top_block_hash - while height > 1: # speed could be improved by batching the network requests - block = get_block(next_block_hash) + for block in BlockchainRPCIterator(): non_miner_txs = block['tx_hashes'] - next_block_hash = block['prev_id'] total_blocks_scanned += 1 - + if block['tx_hashes']: blocks_found += 1 # print(block) - height = block['miner_tx']['vin'][0]['gen']['height'] print(f'{empty_block_counter} block searched since last block') print(f'{blocks_found} / {total_blocks_scanned} are not empty ({(float(blocks_found)/float(total_blocks_scanned))*100.0}%)') empty_block_counter = 0 # reset counter to get gap until next non-empty block else: empty_block_counter += 1 - print(f'{blocks_found} / {total_blocks_scanned} are not empty ({(float(blocks_found)/float(total_blocks_scanned))*100.0}%)') \ No newline at end of file + print(f'{100.0 - ((float(blocks_found)/float(total_blocks_scanned))*100.0)}% blocks are empty') \ No newline at end of file From 1602dd4fe07e01c6a019217dfa9af764e04558ac Mon Sep 17 00:00:00 2001 From: Ben Evanoff Date: Thu, 12 Mar 2026 22:29:25 -0500 Subject: [PATCH 05/15] begin analyze alt chains script --- scripts/analyze_alt_chains.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 scripts/analyze_alt_chains.py diff --git a/scripts/analyze_alt_chains.py b/scripts/analyze_alt_chains.py new file mode 100644 index 00000000..a192b4ae --- /dev/null +++ b/scripts/analyze_alt_chains.py @@ -0,0 +1,23 @@ +import json +import requests + +DAEMON_HOST = 'http://127.0.0.1:17566/json_rpc' + +def get_block(block_hash:str) -> dict: + r = requests.post(DAEMON_HOST, json={'method': 'get_block', 'params': {'hash': block_hash}}) + return json.loads(r.json()['result']['json']) + +def get_block_by_height(height:int) -> dict: + r = requests.post(DAEMON_HOST, json={'method': 'get_block', 'params': {'height': height}}) + return json.loads(r.json()['result']['json']) + +def get_alt_chains(): + r = requests.post(DAEMON_HOST, json={'method': 'get_alternate_chains'}) + return r.json()['result']['chains'] + +if __name__ == '__main__': + for alt_chain in get_alt_chains(): + alt_block_timestamp = get_block(alt_chain['block_hashes'][-1])['timestamp'] + main_chain_sibling_timestamp = get_block_by_height(alt_chain['height'])['timestamp'] + print(f'Alt chain found at height {alt_chain["height"]} with head {alt_chain["block_hashes"][-1]}') + print(f' Timestamp Difference: {abs(alt_block_timestamp - main_chain_sibling_timestamp)}s') \ No newline at end of file From 8421d7a08ada13cbe8c99d6e5efcda5570921fb3 Mon Sep 17 00:00:00 2001 From: Ben Evanoff Date: Fri, 13 Mar 2026 19:44:36 -0500 Subject: [PATCH 06/15] scripts: re use connection session in BlockchainRPCIterator --- scripts/analyze_tx_frequency.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/scripts/analyze_tx_frequency.py b/scripts/analyze_tx_frequency.py index 7de08b9f..ca3b07d1 100644 --- a/scripts/analyze_tx_frequency.py +++ b/scripts/analyze_tx_frequency.py @@ -3,30 +3,31 @@ DAEMON_HOST = 'http://127.0.0.1:17566/json_rpc' -def get_top_block() -> (str, int): - r = requests.post(DAEMON_HOST, json={'method': 'get_info'}) - return r.json()['result']['top_block_hash'], r.json()['result']['height'] - -def get_block(block_hash:str) -> dict: - r = requests.post(DAEMON_HOST, json={'method': 'get_block', 'params': {'hash': block_hash}}) - return json.loads(r.json()['result']['json']) - class BlockchainRPCIterator: ''' Iterate from the top of the chain to the origin block via RPC ''' - def __init__(self): # TODO allow caller to set a batch size to get blocks in batches - self.next_block_hash, self.height = get_top_block() + def __init__(self): + self.session = requests.Session() + self.next_block_hash, self.height = self.get_top_block() def __iter__(self): return self def __next__(self): - block = get_block(self.next_block_hash) + block = self.get_block(self.next_block_hash) self.next_block_hash = block['prev_id'] self.height = block['miner_tx']['vin'][0]['gen']['height'] return block + def get_top_block(self) -> (str, int): + r = self.session.post(DAEMON_HOST, json={'method': 'get_info'}) + return r.json()['result']['top_block_hash'], r.json()['result']['height'] + + def get_block(self, block_hash:str) -> dict: + r = self.session.post(DAEMON_HOST, json={'method': 'get_block', 'params': {'hash': block_hash}}) + return json.loads(r.json()['result']['json']) + if __name__ == '__main__': empty_block_counter = 0 From 066e967c47ff0fded6ec8b5e2718dac57bb2335b Mon Sep 17 00:00:00 2001 From: Ben Evanoff Date: Sat, 14 Mar 2026 09:18:13 -0500 Subject: [PATCH 07/15] scripts: add StopIteration condition to BlockchainRPCIterator --- scripts/analyze_tx_frequency.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/analyze_tx_frequency.py b/scripts/analyze_tx_frequency.py index ca3b07d1..e9518ee3 100644 --- a/scripts/analyze_tx_frequency.py +++ b/scripts/analyze_tx_frequency.py @@ -16,6 +16,8 @@ def __iter__(self): def __next__(self): block = self.get_block(self.next_block_hash) + if not block: + raise StopIteration self.next_block_hash = block['prev_id'] self.height = block['miner_tx']['vin'][0]['gen']['height'] return block @@ -25,8 +27,11 @@ def get_top_block(self) -> (str, int): return r.json()['result']['top_block_hash'], r.json()['result']['height'] def get_block(self, block_hash:str) -> dict: - r = self.session.post(DAEMON_HOST, json={'method': 'get_block', 'params': {'hash': block_hash}}) - return json.loads(r.json()['result']['json']) + try: + r = self.session.post(DAEMON_HOST, json={'method': 'get_block', 'params': {'hash': block_hash}}) + return json.loads(r.json()['result']['json']) + except KeyError: + return if __name__ == '__main__': From edba9905f1424a506d6557d215ba809ad16d1c91 Mon Sep 17 00:00:00 2001 From: Ben Evanoff Date: Sat, 14 Mar 2026 11:10:43 -0500 Subject: [PATCH 08/15] SECOR: restrict miner tx outs to 1 for normal block and 2 for nephew block --- src/cryptonote_core/blockchain.cpp | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/cryptonote_core/blockchain.cpp b/src/cryptonote_core/blockchain.cpp index 4933f2d8..83ed8dd2 100644 --- a/src/cryptonote_core/blockchain.cpp +++ b/src/cryptonote_core/blockchain.cpp @@ -1259,14 +1259,20 @@ bool Blockchain::validate_miner_transaction(const block& b, size_t cumulative_bl } uint64_t total_reward = base_reward + fee; - if (b.uncle_hash != crypto::null_hash) + if (version >= HF_VERSION_SECOR && b.uncle_hash != crypto::null_hash) { + if (b.miner_tx.vout.size() != 2) + { + MERROR("miner tx from a block which includes an uncle hash must contain two separate outputs with separate recipients"); + return false; + } + total_reward += base_reward / SECOR_NEPHEW_REWARD_RATIO; total_reward += base_reward / SECOR_UNCLE_REWARD_RATIO; // make sure that the uncle reward uses the output key from the uncle block miner tx and is for the correct amount - dont let the nephew miner steal the uncle reward cryptonote::block uncle_block; get_block_by_hash(b.uncle_hash, uncle_block); - // TODO: enforce nephew reward at index 0 and uncle reward at index 1 + // nephew reward at index 0 and uncle reward at index 1 is enforced if (boost::get(b.miner_tx.vout[1].target).key != boost::get(uncle_block.miner_tx.vout[0].target).key) { MERROR_VER("nephew block miner tx does not include proper uncle reward output key"); @@ -1277,7 +1283,23 @@ bool Blockchain::validate_miner_transaction(const block& b, size_t cumulative_bl MERROR_VER("nephew block miner tx does not include proper uncle reward tx public key in tx extra"); return false; } + + if (b.miner_tx.vout[1].amount != base_reward / SECOR_UNCLE_REWARD_RATIO) + { + MERROR_VER("miner tx contains uncle reward but the uncle reward amount is incorrect. Amount found " << b.miner_tx.vout[1].amount << " vs amount expected " << base_reward / SECOR_UNCLE_REWARD_RATIO); + return false; + } + + } + else if (version >= HF_VERSION_SECOR && b.uncle_hash == crypto::null_hash) + { + if (b.miner_tx.vout.size() > 1) + { + MERROR("miner tx after v" << HF_VERSION_SECOR << " cannot contain more than 1 output without an uncle hash in the mined block"); + return false; + } } + if(total_reward != money_in_use && already_generated_coins > 0) { MERROR_VER("coinbase transaction doesn't use full amount of block reward: spent " << money_in_use From 8177cec534b058e048e933fd8e814d81fe51af77 Mon Sep 17 00:00:00 2001 From: Ben Evanoff Date: Sun, 15 Mar 2026 01:13:48 -0500 Subject: [PATCH 09/15] scripts: add more depth since last alt chain to alt chains analysis script --- scripts/analyze_alt_chains.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/scripts/analyze_alt_chains.py b/scripts/analyze_alt_chains.py index a192b4ae..380403a0 100644 --- a/scripts/analyze_alt_chains.py +++ b/scripts/analyze_alt_chains.py @@ -1,4 +1,5 @@ import json +import datetime import requests DAEMON_HOST = 'http://127.0.0.1:17566/json_rpc' @@ -13,11 +14,17 @@ def get_block_by_height(height:int) -> dict: def get_alt_chains(): r = requests.post(DAEMON_HOST, json={'method': 'get_alternate_chains'}) - return r.json()['result']['chains'] + chains = r.json()['result']['chains'] + sorted_chains = sorted(chains, key=lambda alt_chain: alt_chain['height']) + return sorted_chains if __name__ == '__main__': + prev_alt_chain_height = None for alt_chain in get_alt_chains(): alt_block_timestamp = get_block(alt_chain['block_hashes'][-1])['timestamp'] main_chain_sibling_timestamp = get_block_by_height(alt_chain['height'])['timestamp'] print(f'Alt chain found at height {alt_chain["height"]} with head {alt_chain["block_hashes"][-1]}') - print(f' Timestamp Difference: {abs(alt_block_timestamp - main_chain_sibling_timestamp)}s') \ No newline at end of file + print(f' Alt chain length: {alt_chain["length"]}') + print(f' Timestamp: {datetime.datetime.fromtimestamp(alt_block_timestamp)}; Difference from mainchain: {abs(alt_block_timestamp - main_chain_sibling_timestamp)}s') + print(f' Height difference from previous alt chain head: {None if not prev_alt_chain_height else alt_chain["height"] - prev_alt_chain_height}') + prev_alt_chain_height = alt_chain["height"] \ No newline at end of file From 66bcba1b358bbaf582adb916aef5bf3f1b841870 Mon Sep 17 00:00:00 2001 From: Ben Evanoff Date: Sun, 15 Mar 2026 01:16:43 -0500 Subject: [PATCH 10/15] SECOR: begin orphan/uncle probability proofs --- docs/SECOR.md | 89 +++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 83 insertions(+), 6 deletions(-) diff --git a/docs/SECOR.md b/docs/SECOR.md index fb8c5136..63aeb2d8 100644 --- a/docs/SECOR.md +++ b/docs/SECOR.md @@ -24,16 +24,12 @@ If an uncle block is included in the block, then the miner transaction must incl In order to generate a transaction on behalf of another miner in a non-interactive way without compromising privacy, we re-use the keys associated with the original uncle block miner transaction in the nephew block. -The uncle block transaction public key from the uncle block transaction extra field should be included in nephew miner transaction at index 1. The output key from the uncle block should be re-used at nephew miner transaction output index 1. Since output index is a part of the stealth address generation algorithm, -``` -Hs(aR|i)*G + B = Hs(rA|i) + B -``` -(see cryptonote whitepaper section 4.3), +The uncle block transaction public key from the uncle block transaction extra field should be included in nephew miner transaction at index 1. The output key from the uncle block should be re-used at nephew miner transaction output index 1. Since output index is a part of the stealth address generation algorithm, `Hs(aR|i)*G + B = Hs(rA|i)G + B` (see cryptonote whitepaper section 4.3), wallets should use index 0 to compute the shared secret for both outputs in a nephew block miner reward transaction. ### Block Reward Bonuses -I use the reward constants proposed in the original SECOR paper: +5% of the base reward amount to the nephew miner and 20% of the base reward amount to the uncle miner. The impact of this is detailed in the "Block Reward Rationale" section. +I use the reward constants proposed in the original SECOR paper: +5% of the base reward amount to the nephew miner and 50% of the base reward amount to the uncle miner. The impact of this is detailed in the "Block Reward Rationale" section. The optimal reward for Nerva's implementation is still an active research question. @@ -77,6 +73,87 @@ If we think that uncle blocks will be included in half of the main chain blocks X = (0.3 * 40) / 51 = 0.23529411764 ``` +## Modeling Orphan/Uncle Block Frequencies + +Mining is essentially an [IID process](https://en.wikipedia.org/wiki/Independent_and_identically_distributed_random_variables) as the output of the hashing function used for mining should be an IID random variable. Each time the miner chooses a nonce and produces a hash, the miner has either found a valid block for the given difficulty, or they have not and must attempt again with a successive independent trial. The probabilities for this kind of binary problem follow a [bernoulli distribution](https://en.wikipedia.org/wiki/Bernoulli_distribution). Therefore, a [binomial distribution](https://en.wikipedia.org/wiki/Binomial_distribution) where k=1, aka a [geometric distribution](https://en.wikipedia.org/wiki/Geometric_distribution), can be used to model the cumulative probability of finding a block after mining `n` hashes. + +A block difficulty target is used to control the rate at which blockchain can be written to. Nerva currently has a block time target of 1 block per minute. If there are 10 miners on the network, each mining at a rate of 5 hashes/second, then the target difficulty should eventualy fit to about `10*5*60`. + +#### Scenario A - Few miners, low difficulty. +``` +Suppose: +There is a target block time of 60 seconds. +There are 2 miners, miner A and miner B, on the network, each mining at a rate of 1 H/s. + +Then: +The network difficulty should be 1*2*60=120 +The probability that miner A finds a block after 1 second of hashing is 1/120. +The probability that miner B has found a block after 1 second of hashing is also 1/120. + +The probability that miner A finds a block after 2 seconds of hashing is 2/120. +The probability that miner B has found a block after 2 seconds of hashing is also 1/120. + +The probability that miner A and miner B both find a block after 1 second of mining (at the same time) is (1/120)*(1/120) per IID joint probability. + +The cumulative probability that miner A finds a block after 2 seconds of mining is 1 - (1-(1/120))^2 per geometric CDF +The cumulative probability that miner B finds a block after 2 seconds of mining is 1 - (1-(1/120))^2 per geometric CDF + +The probability that miner A and miner B have both found a block after 2 seconds of mining is (1 - (1-(1/120))^2)^2 + +The probability that miner A and miner B have both found a block after 10 seconds of mining is (1 - (1-(1/120))^10)^2 + +The probability that a given miner finds a block after mining for 1 minute is 1 - ((1-(1/120))^60), roughly 40%. + +The probability that both miners have each found a block after 1 minute of mining is (1 - ((1-(1/120))^60))^2, which is roughly 15%. +``` + +#### Scenario B - Few miners, higher difficulty. +``` +Suppose: +There is a target block time of 60 seconds. +There are 2 miners, miner A and miner B, on the network, each mining at a rate of 1,000 H/s. + +Then: +The network difficulty should be 1000*2*60=120000 +The probability that miner A finds a block after mining a single hash is 1/120000. +The probability that miner B has found a block after mining a single is also 1/120000. + +The probability that miner A and miner B both find a block at the same time after each mining a single hash is (1/120000)^2 per IID joint probability. + +The probability that a given miner finds a block after mining for 1 second is 1 - ((1-(1/120000))^1000) per geometric CDF. + +The probability that miner A and miner B have both found a block after mining 10 hashes is (1 - (1-(1/120000))^10)^2 + +The probability that miner A and miner B have both found after 1 second of mining is (1 - (1-(1/120000))^1000)^2 + +The probability that a given miner finds a block after mining for 1 minute is 1 - ((1-(1/120000))^60000), which is roughly 40%. This is the same as when the network hashrate is only 2 H/s. + +The probability that both miners have each found a block after mining for 1 minute is (1 - ((1-(1/120000))^60000))^2, which is roughly 15%. +``` + +#### Scenario C - Many miners +``` +Suppose: +There is a target block time of 60 seconds. +There are 100 miners on the network, each mining at a rate of 1,000 H/s. + +Then: +The network difficulty should be 1000*100*60=6000000 +The probability that a given miner finds a block after mining a single hash is 1/6000000. + +The probability that 2 miners both find a block at the same time after each mining a single hash is (1/6000000)^2 per IID joint probability. + +The probability that a given miner finds a block after mining for 1 second is 1 - ((1-(1/6000000))^1000) per geometric CDF. + +The probability that two miners have both found a block after mining 10 hashes is (1 - (1-(1/6000000))^10)^2 + +The probability that two miners have both found after 1 second of mining is (1 - (1-(1/6000000))^1000)^2 + +The cumulative probability that a given miner finds a block after mining for 1 minute is 1 - ((1-(1/6000000))^60000), which is just less than 1%. + +The probability that a two miners have each find a block after mining for 1 minute is (1 - ((1-(1/6000000))^60000))^2, which is less than 0.01%. +``` + ## Status This proposal is a work in progress. I am seeking community feedback to see if I should continue working on this idea, and to get an idea of what people want to do about the block reward. From 9e92409416ef130471725f1ad70eb9e24d5ad77a Mon Sep 17 00:00:00 2001 From: Ben Evanoff Date: Sun, 15 Mar 2026 15:30:09 -0500 Subject: [PATCH 11/15] SECOR: enforce uncle block PoW less than main chain PoW (smaller hash = greater PoW) --- src/cryptonote_core/blockchain.cpp | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/cryptonote_core/blockchain.cpp b/src/cryptonote_core/blockchain.cpp index 83ed8dd2..10528b27 100644 --- a/src/cryptonote_core/blockchain.cpp +++ b/src/cryptonote_core/blockchain.cpp @@ -1289,7 +1289,6 @@ bool Blockchain::validate_miner_transaction(const block& b, size_t cumulative_bl MERROR_VER("miner tx contains uncle reward but the uncle reward amount is incorrect. Amount found " << b.miner_tx.vout[1].amount << " vs amount expected " << base_reward / SECOR_UNCLE_REWARD_RATIO); return false; } - } else if (version >= HF_VERSION_SECOR && b.uncle_hash == crypto::null_hash) { @@ -1925,7 +1924,7 @@ bool Blockchain::handle_alternative_block(const block& b, const crypto::hash& id bvc.m_verifivation_failed = true; return r; } - else if (block_height == top_block_height && reinterpret_cast(pow_top_block) < reinterpret_cast(proof_of_work)) // TODO: enforce this when processing nephew blocks in handle block to main chain + else if (block_height == top_block_height && reinterpret_cast(proof_of_work) < reinterpret_cast(pow_top_block)) { MGINFO_GREEN("###### REORGANIZE DUE TO UNCLE BLOCK on height: " << alt_chain.front().height << " of " << m_db->height() - 1 << " with cum_difficulty " << m_db->get_block_cumulative_difficulty(m_db->height() - 1) << std::endl << " alternative blockchain size: " << alt_chain.size() << " with cum_difficulty " << bei.cumulative_difficulty); @@ -3809,7 +3808,27 @@ bool Blockchain::handle_block_to_main_chain(const block& bl, const crypto::hash& crypto::hash uncle_pow; memset(uncle_pow.data, 0xff, sizeof(uncle_pow.data)); get_block_longhash(m_hash_context, this, uncle_block, uncle_pow, m_db->height()-2); - check_hash(uncle_pow, (uint64_t)uncle_difficulty); // is this safe? + if(!check_hash(uncle_pow, (uint64_t)uncle_difficulty)) // is this safe? + { + MERROR_VER("proof-of-work for uncle block failed verification"); + bvc.m_verifivation_failed = true; + return_tx_to_pool(txs); + goto leave; + } + + // the main chain block at the uncle block height should have a smaller PoW hash than the uncle block + cryptonote::block uncle_sibling_block; + get_block_by_hash(get_block_id_by_height(m_db->height()-2), uncle_sibling_block); + crypto::hash pow_uncle_sibling; + memset(pow_uncle_sibling.data, 0xff, sizeof(pow_uncle_sibling.data)); + get_block_longhash(m_hash_context, this, uncle_sibling_block, pow_uncle_sibling, m_db->height()-2); + if (reinterpret_cast(uncle_sibling_block) >= reinterpret_cast(uncle_pow)) + { + MERROR_VER("the proof-of-work hash for an uncle block must be greater than that of its main chain sibling"); + bvc.m_verifivation_failed = true; + return_tx_to_pool(txs); + goto leave; + } // add block difficulty for uncle block(s) to chain cumulative difficulty cumulative_difficulty += uncle_difficulty; From 4e7cbc5d7cdf17191479271ba472b3936b327ed0 Mon Sep 17 00:00:00 2001 From: Ben Evanoff Date: Wed, 18 Mar 2026 20:43:38 -0500 Subject: [PATCH 12/15] SECOR: fix scenario C chain fork model --- docs/SECOR.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/SECOR.md b/docs/SECOR.md index 63aeb2d8..13585efb 100644 --- a/docs/SECOR.md +++ b/docs/SECOR.md @@ -27,6 +27,8 @@ In order to generate a transaction on behalf of another miner in a non-interacti The uncle block transaction public key from the uncle block transaction extra field should be included in nephew miner transaction at index 1. The output key from the uncle block should be re-used at nephew miner transaction output index 1. Since output index is a part of the stealth address generation algorithm, `Hs(aR|i)*G + B = Hs(rA|i)G + B` (see cryptonote whitepaper section 4.3), wallets should use index 0 to compute the shared secret for both outputs in a nephew block miner reward transaction. +TODO: This should all be easier if we instead enforce that the uncle reward is placed at index 0 and place the nephew reward at index 1 + ### Block Reward Bonuses I use the reward constants proposed in the original SECOR paper: +5% of the base reward amount to the nephew miner and 50% of the base reward amount to the uncle miner. The impact of this is detailed in the "Block Reward Rationale" section. @@ -151,9 +153,11 @@ The probability that two miners have both found after 1 second of mining is (1 - The cumulative probability that a given miner finds a block after mining for 1 minute is 1 - ((1-(1/6000000))^60000), which is just less than 1%. -The probability that a two miners have each find a block after mining for 1 minute is (1 - ((1-(1/6000000))^60000))^2, which is less than 0.01%. +The probability that a two miners have each find a block after mining for 1 minute is (100*99/2)*((1-(1-(1/6000000))^(60000))^2)*(((1-(1/6000000))^(60000))^98), per binomial distribution (100, 2) which is about 18% ``` +Overall, the network difficulty adjusts in a way that adding miners to the network doesnt affect the probability of chain splits significantly assuming that mining power is distributed evenly among miners. This model does not account for the fact that adding miners to the network increases the amount of work required for block propogation across all miners on the network. + ## Status This proposal is a work in progress. I am seeking community feedback to see if I should continue working on this idea, and to get an idea of what people want to do about the block reward. @@ -164,6 +168,8 @@ This proposal is a work in progress. I am seeking community feedback to see if I * Address the TODO comments +* revisit miner reward tx format + * graceful database migration - currently a sync from scratch is required after upgrading #### Research From 1233b4c7d2f6f179f4b420a697ef75598dc175eb Mon Sep 17 00:00:00 2001 From: Ben Evanoff Date: Thu, 19 Mar 2026 23:03:50 -0500 Subject: [PATCH 13/15] SECOR: re order enforced reward indices --- docs/SECOR.md | 14 ++-- .../cryptonote_format_utils.cpp | 12 +-- src/cryptonote_core/blockchain.cpp | 44 ++++++---- src/cryptonote_core/cryptonote_tx_utils.cpp | 83 ++++++++++++++----- src/cryptonote_core/cryptonote_tx_utils.h | 7 +- src/wallet/wallet2.cpp | 76 ++++------------- 6 files changed, 122 insertions(+), 114 deletions(-) diff --git a/docs/SECOR.md b/docs/SECOR.md index 13585efb..cd882095 100644 --- a/docs/SECOR.md +++ b/docs/SECOR.md @@ -22,12 +22,13 @@ Miner transactions should include only 1 reward output if the uncle_hash is null If an uncle block is included in the block, then the miner transaction must include a secondary transaction output. -In order to generate a transaction on behalf of another miner in a non-interactive way without compromising privacy, we re-use the keys associated with the original uncle block miner transaction in the nephew block. +In order to generate a transaction on behalf of another miner in a non-interactive way without compromising privacy, we re-use the keys associated with the original uncle block miner transaction from the alt chain into the nephew block. -The uncle block transaction public key from the uncle block transaction extra field should be included in nephew miner transaction at index 1. The output key from the uncle block should be re-used at nephew miner transaction output index 1. Since output index is a part of the stealth address generation algorithm, `Hs(aR|i)*G + B = Hs(rA|i)G + B` (see cryptonote whitepaper section 4.3), -wallets should use index 0 to compute the shared secret for both outputs in a nephew block miner reward transaction. +Since output index is a part of the stealth address generation algorithm, `Hs(aR|i)*G + B = Hs(rA|i)G + B` (see cryptonote whitepaper section 4.3), the ordering of the output destinations in the transaction is important. -TODO: This should all be easier if we instead enforce that the uncle reward is placed at index 0 and place the nephew reward at index 1 +If a block doesn't reference an uncle block, then there should be only one output in the reward transaction, which should be used to reward the person who mined the block. If that block is then included into a nephew block as an uncle block, then the nephew block must place the reward for the uncle miner, using the output key from the uncle block miner tx, into the nephew block miner transaction at index 0. The reward for the person who found the nephew block should be placed at index 1 in the reward transaction. + +If an uncle block references an uncle block itself, then the reward to the true uncle miner should be at index 1 in the uncle block. To accomodate this, the reward to the nephew miner in the main chain should be addressed to output index 0 so that the uncle reward from index 1 may be recycled. ### Block Reward Bonuses @@ -153,7 +154,7 @@ The probability that two miners have both found after 1 second of mining is (1 - The cumulative probability that a given miner finds a block after mining for 1 minute is 1 - ((1-(1/6000000))^60000), which is just less than 1%. -The probability that a two miners have each find a block after mining for 1 minute is (100*99/2)*((1-(1-(1/6000000))^(60000))^2)*(((1-(1/6000000))^(60000))^98), per binomial distribution (100, 2) which is about 18% +The probability that any two miners have each found a block after mining for 1 minute is (100*99/2)*((1-(1-(1/6000000))^(60000))^2)*(((1-(1/6000000))^(60000))^98), per binomial distribution (100, 2) which is about 18% ``` Overall, the network difficulty adjusts in a way that adding miners to the network doesnt affect the probability of chain splits significantly assuming that mining power is distributed evenly among miners. This model does not account for the fact that adding miners to the network increases the amount of work required for block propogation across all miners on the network. @@ -168,8 +169,6 @@ This proposal is a work in progress. I am seeking community feedback to see if I * Address the TODO comments -* revisit miner reward tx format - * graceful database migration - currently a sync from scratch is required after upgrading #### Research @@ -177,6 +176,7 @@ This proposal is a work in progress. I am seeking community feedback to see if I * How does changing the difficulty target affect the rest of the difficulty algo parameters ? * CRYPTONOTE_LOCKED_TX_ALLOWED_DELTA_SECONDS_V1 references this. TX locking can be done with unix timestamps instead of blocks. This should be consistent. * DIFFICULTY_BLOCKS_COUNT - should the DIFFICULTY_WINDOW be adjusted ? + * CRYPTONOTE_BLOCK_FUTURE_TIME_LIMIT_V6 should probably be adjusted * How long does it currently take for a block to propogate throughout the network ? diff --git a/src/cryptonote_basic/cryptonote_format_utils.cpp b/src/cryptonote_basic/cryptonote_format_utils.cpp index e487fd1d..a2e269e2 100644 --- a/src/cryptonote_basic/cryptonote_format_utils.cpp +++ b/src/cryptonote_basic/cryptonote_format_utils.cpp @@ -909,11 +909,13 @@ namespace cryptonote // try additional tx pubkeys if available if (!additional_derivations.empty()) { - CHECK_AND_ASSERT_MES(output_index < additional_derivations.size(), boost::none, "wrong number of additional derivations"); - hwdev.derive_subaddress_public_key(out_key, additional_derivations[output_index], output_index, subaddress_spendkey); - found = subaddresses.find(subaddress_spendkey); - if (found != subaddresses.end()) - return subaddress_receive_info{ found->second, additional_derivations[output_index] }; + for (auto additional_derivation : additional_derivations) + { + hwdev.derive_subaddress_public_key(out_key, additional_derivation, output_index, subaddress_spendkey); + found = subaddresses.find(subaddress_spendkey); + if (found != subaddresses.end()) + return subaddress_receive_info{ found->second, additional_derivation }; + } } return boost::none; } diff --git a/src/cryptonote_core/blockchain.cpp b/src/cryptonote_core/blockchain.cpp index 10528b27..c0ae0f83 100644 --- a/src/cryptonote_core/blockchain.cpp +++ b/src/cryptonote_core/blockchain.cpp @@ -1272,19 +1272,25 @@ bool Blockchain::validate_miner_transaction(const block& b, size_t cumulative_bl // make sure that the uncle reward uses the output key from the uncle block miner tx and is for the correct amount - dont let the nephew miner steal the uncle reward cryptonote::block uncle_block; get_block_by_hash(b.uncle_hash, uncle_block); - // nephew reward at index 0 and uncle reward at index 1 is enforced - if (boost::get(b.miner_tx.vout[1].target).key != boost::get(uncle_block.miner_tx.vout[0].target).key) + // nephew reward at index 1 and uncle reward at index 0 is enforced + crypto::public_key uncle_out_key; + crypto::public_key uncle_tx_key; + get_uncle_block_original_miner_details(uncle_block, uncle_out_key, uncle_tx_key); + crypto::public_key mainchain_out_key; + crypto::public_key mainchain_tx_key; + get_mainchain_block_original_miner_details(b, mainchain_out_key, mainchain_tx_key); + if (boost::get(b.miner_tx.vout[0].target).key != uncle_out_key) { MERROR_VER("nephew block miner tx does not include proper uncle reward output key"); return false; } - if (get_tx_pub_key_from_extra(b.miner_tx, 1) != get_tx_pub_key_from_extra(uncle_block.miner_tx, 0)) + if (get_tx_pub_key_from_extra(b.miner_tx, 0) != uncle_tx_key) { MERROR_VER("nephew block miner tx does not include proper uncle reward tx public key in tx extra"); return false; } - if (b.miner_tx.vout[1].amount != base_reward / SECOR_UNCLE_REWARD_RATIO) + if (b.miner_tx.vout[0].amount != base_reward / SECOR_UNCLE_REWARD_RATIO) { MERROR_VER("miner tx contains uncle reward but the uncle reward amount is incorrect. Amount found " << b.miner_tx.vout[1].amount << " vs amount expected " << base_reward / SECOR_UNCLE_REWARD_RATIO); return false; @@ -1572,15 +1578,20 @@ bool Blockchain::create_block_template(block& b, const crypto::hash *from_block, block weight, so first miner transaction generated with fake amount of money, and with phase we know think we know expected block weight */ //make blocks coin-base tx looks close to real coinbase tx to get truthful blob size - bool r = construct_miner_tx(height, median_weight, already_generated_coins, txs_weight, fee, miner_address, b.miner_tx, ex_nonce, 0, hf_version, b.uncle_hash != crypto::null_hash); + bool r; + int uncle_out_idx; + crypto::public_key uncle_out; + crypto::public_key uncle_tx_pubkey; if (b.uncle_hash != crypto::null_hash) { cryptonote::block uncle_block; get_block_by_hash(b.uncle_hash, uncle_block); - crypto::public_key uncle_out = boost::get(uncle_block.miner_tx.vout[0].target).key; - crypto::public_key uncle_tx_pubkey = get_tx_pub_key_from_extra(uncle_block.miner_tx); - construct_uncle_miner_tx(expected_reward, uncle_out, uncle_tx_pubkey, b.miner_tx); // should we be using the reward calculated from previous hieght / uncle chain ? + uncle_out_idx = get_uncle_block_original_miner_details(uncle_block, uncle_out, uncle_tx_pubkey); + r = construct_miner_tx(height, median_weight, already_generated_coins, txs_weight, fee, miner_address, b.miner_tx, ex_nonce, 0, hf_version, &uncle_out, &uncle_tx_pubkey, &uncle_out_idx); } + else + r = construct_miner_tx(height, median_weight, already_generated_coins, txs_weight, fee, miner_address, b.miner_tx, ex_nonce, 0, hf_version); + CHECK_AND_ASSERT_MES(r, false, "Failed to construct miner tx, first chance"); size_t cumulative_weight = txs_weight + get_transaction_weight(b.miner_tx); #if defined(DEBUG_CREATE_BLOCK_TEMPLATE) @@ -1589,15 +1600,11 @@ bool Blockchain::create_block_template(block& b, const crypto::hash *from_block, #endif for (size_t try_count = 0; try_count != 10; ++try_count) { - r = construct_miner_tx(height, median_weight, already_generated_coins, cumulative_weight, fee, miner_address, b.miner_tx, ex_nonce, 0, hf_version, b.uncle_hash != crypto::null_hash); if (b.uncle_hash != crypto::null_hash) - { - cryptonote::block uncle_block; - get_block_by_hash(b.uncle_hash, uncle_block); - crypto::public_key uncle_out = boost::get(uncle_block.miner_tx.vout[0].target).key; - crypto::public_key uncle_tx_pubkey = get_tx_pub_key_from_extra(uncle_block.miner_tx); - construct_uncle_miner_tx(expected_reward, uncle_out, uncle_tx_pubkey, b.miner_tx); // should we be using the reward calculated from previous hieght / uncle chain ? - } + r = construct_miner_tx(height, median_weight, already_generated_coins, cumulative_weight, fee, miner_address, b.miner_tx, ex_nonce, 0, hf_version, &uncle_out, &uncle_tx_pubkey, &uncle_out_idx); + else + r = construct_miner_tx(height, median_weight, already_generated_coins, cumulative_weight, fee, miner_address, b.miner_tx, ex_nonce, 0, hf_version); + CHECK_AND_ASSERT_MES(r, false, "Failed to construct miner tx, second chance"); size_t coinbase_weight = get_transaction_weight(b.miner_tx); if (coinbase_weight > cumulative_weight - txs_weight) @@ -1891,6 +1898,7 @@ bool Blockchain::handle_alternative_block(const block& b, const crypto::hash& id data.already_generated_coins = bei.already_generated_coins; m_db->add_alt_block(id, data, cryptonote::block_to_blob(bei.bl)); alt_chain.push_back(bei); + LOG_PRINT_L0("pushing uncle block candidate: " << id << " " << data.height); m_uncle_candidates.push_back(std::make_pair(id, data.height)); // TODO: free old data at some point uint64_t top_block_height = m_db->height()-1; @@ -3807,8 +3815,8 @@ bool Blockchain::handle_block_to_main_chain(const block& bl, const crypto::hash& difficulty_type_128 uncle_difficulty = m_db->get_block_cumulative_difficulty(blockchain_height-2) - m_db->get_block_cumulative_difficulty(blockchain_height-3); crypto::hash uncle_pow; memset(uncle_pow.data, 0xff, sizeof(uncle_pow.data)); - get_block_longhash(m_hash_context, this, uncle_block, uncle_pow, m_db->height()-2); - if(!check_hash(uncle_pow, (uint64_t)uncle_difficulty)) // is this safe? + get_block_longhash(m_hash_context, this, uncle_block, uncle_pow, m_db->height()-1); + if(!check_hash(uncle_pow, uncle_difficulty.convert_to())) { MERROR_VER("proof-of-work for uncle block failed verification"); bvc.m_verifivation_failed = true; diff --git a/src/cryptonote_core/cryptonote_tx_utils.cpp b/src/cryptonote_core/cryptonote_tx_utils.cpp index c5debfd7..57d6f05c 100644 --- a/src/cryptonote_core/cryptonote_tx_utils.cpp +++ b/src/cryptonote_core/cryptonote_tx_utils.cpp @@ -81,13 +81,28 @@ namespace cryptonote } //--------------------------------------------------------------- bool construct_miner_tx(size_t height, size_t median_weight, uint64_t already_generated_coins, size_t current_block_weight, uint64_t fee, const account_public_address &miner_address, - transaction& tx, const blobdata& extra_nonce, size_t max_outs, uint8_t hard_fork_version, bool uncle_reward) { + transaction& tx, const blobdata& extra_nonce, size_t max_outs, uint8_t hard_fork_version, crypto::public_key* uncle_reward_out_key, crypto::public_key* uncle_tx_pubkey, int* uncle_reward_idx) { tx.vin.clear(); tx.vout.clear(); tx.extra.clear(); keypair txkey = keypair::generate(hw::get_device("default")); - add_tx_pub_key_to_extra(tx, txkey.pub); + + size_t miner_index; + if (uncle_reward_out_key && uncle_tx_pubkey && uncle_reward_idx && *uncle_reward_idx == 0) { + miner_index = 1; + add_tx_pub_key_to_extra(tx, *uncle_tx_pubkey); + add_additional_tx_pub_keys_to_extra(tx.extra, { txkey.pub }); + } + else if (uncle_reward_out_key && uncle_tx_pubkey && uncle_reward_idx && *uncle_reward_idx == 1) { + miner_index = 0; + add_tx_pub_key_to_extra(tx, txkey.pub); + add_additional_tx_pub_keys_to_extra(tx.extra, { *uncle_tx_pubkey }); + } else { + miner_index = 0; + add_tx_pub_key_to_extra(tx, txkey.pub); + } + if(!extra_nonce.empty()) if(!add_extra_nonce_to_tx_extra(tx.extra, extra_nonce)) return false; @@ -108,7 +123,19 @@ namespace cryptonote LOG_PRINT_L1("Creating block template: reward " << block_reward << ", fee " << fee); #endif - if (uncle_reward) block_reward += block_reward / SECOR_NEPHEW_REWARD_RATIO; + if (uncle_reward_out_key && uncle_tx_pubkey) + { + txout_to_key tk; + tk.key = *uncle_reward_out_key; + + tx_out out; + out.amount = block_reward / SECOR_UNCLE_REWARD_RATIO; + out.target = tk; + tx.vout.push_back(out); + + block_reward += block_reward / SECOR_NEPHEW_REWARD_RATIO; + } + block_reward += fee; crypto::key_derivation derivation = AUTO_VAL_INIT(derivation); @@ -116,7 +143,6 @@ namespace cryptonote bool r = crypto::generate_key_derivation(miner_address.m_view_public_key, txkey.sec, derivation); CHECK_AND_ASSERT_MES(r, false, "while creating outs: failed to generate_key_derivation(" << miner_address.m_view_public_key << ", " << rct::sk2rct(txkey.sec) << ")"); - size_t miner_index = 0; r = crypto::derive_public_key(derivation, miner_index, miner_address.m_spend_public_key, out_eph_public_key); CHECK_AND_ASSERT_MES(r, false, "while creating outs: failed to derive_public_key(" << derivation << ", " << miner_address.m_spend_public_key << ")"); @@ -140,24 +166,6 @@ namespace cryptonote return true; } //--------------------------------------------------------------- - bool construct_uncle_miner_tx(uint64_t amount, crypto::public_key out_eph_public_key, crypto::public_key tx_pubkey, transaction& tx) - { - add_tx_pub_key_to_extra(tx, tx_pubkey); - - txout_to_key tk; - tk.key = out_eph_public_key; - - tx_out out; - out.amount = amount / SECOR_UNCLE_REWARD_RATIO; - out.target = tk; - tx.vout.push_back(out); - - //lock - tx.invalidate_hashes(); - - return true; - } - //--------------------------------------------------------------- bool construct_genesis_tx(transaction& tx, uint64_t amount) { tx.vin.clear(); tx.vout.clear(); @@ -820,4 +828,35 @@ namespace cryptonote return true; } + int get_uncle_block_original_miner_details(const cryptonote::block &block, crypto::public_key &output_key, crypto::public_key &tx_pkey) + { + if (block.uncle_hash == crypto::null_hash) + { + output_key = boost::get(block.miner_tx.vout[0].target).key; + tx_pkey = get_tx_pub_key_from_extra(block.miner_tx, 0); + return 0; + } + else + { + output_key = boost::get(block.miner_tx.vout[1].target).key; + tx_pkey = get_additional_tx_pub_keys_from_extra(block.miner_tx).at(0); + return 1; + } + } + + int get_mainchain_block_original_miner_details(const cryptonote::block &block, crypto::public_key &output_key, crypto::public_key &tx_pkey) + { + if (block.uncle_hash == crypto::null_hash) + { + output_key = boost::get(block.miner_tx.vout[1].target).key; + tx_pkey = get_tx_pub_key_from_extra(block.miner_tx, 1); + return 1; + } + else + { + output_key = boost::get(block.miner_tx.vout[0].target).key; + tx_pkey = get_additional_tx_pub_keys_from_extra(block.miner_tx).at(0); + return 0; + } + } } diff --git a/src/cryptonote_core/cryptonote_tx_utils.h b/src/cryptonote_core/cryptonote_tx_utils.h index c6e2e6ec..1c63bd0e 100644 --- a/src/cryptonote_core/cryptonote_tx_utils.h +++ b/src/cryptonote_core/cryptonote_tx_utils.h @@ -46,9 +46,7 @@ namespace cryptonote { //--------------------------------------------------------------- - bool construct_miner_tx(size_t height, size_t median_weight, uint64_t already_generated_coins, size_t current_block_weight, uint64_t fee, const account_public_address &miner_address, transaction& tx, const blobdata& extra_nonce = blobdata(), size_t max_outs = 999, uint8_t hard_fork_version = 1, bool uncle_reward = false); - - bool construct_uncle_miner_tx(uint64_t amount, crypto::public_key out_eph_public_key, crypto::public_key tx_pubkey, transaction& tx); + bool construct_miner_tx(size_t height, size_t median_weight, uint64_t already_generated_coins, size_t current_block_weight, uint64_t fee, const account_public_address &miner_address, transaction& tx, const blobdata& extra_nonce = blobdata(), size_t max_outs = 999, uint8_t hard_fork_version = 1, crypto::public_key* uncle_reward_out_key = nullptr, crypto::public_key* uncle_tx_pubkey = nullptr, int* uncle_reward_idx = nullptr); bool construct_genesis_tx(transaction& tx, uint64_t amount); @@ -156,6 +154,9 @@ namespace cryptonote bool get_block_longhash_v10(crypto::cn_hash_context_t *context, cryptonote::BlockchainDB &db, const blobdata &blob, crypto::hash &res, uint64_t height); bool get_block_longhash_v9(crypto::cn_hash_context_t *context, cryptonote::BlockchainDB &db, const blobdata &blob, crypto::hash &res, uint64_t height); bool get_block_longhash_v7_8(crypto::cn_hash_context_t *context, cryptonote::BlockchainDB &db, const blobdata &blob, crypto::hash &res, uint64_t height, uint64_t data_offset); + + int get_mainchain_block_original_miner_details(const cryptonote::block &block, crypto::public_key &output_key, crypto::public_key &tx_pkey); + int get_uncle_block_original_miner_details(const cryptonote::block &block, crypto::public_key &output_key, crypto::public_key &tx_pkey); } namespace boost diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp index e05673a9..4d9b2f21 100644 --- a/src/wallet/wallet2.cpp +++ b/src/wallet/wallet2.cpp @@ -1707,11 +1707,7 @@ void wallet2::scan_output(const cryptonote::transaction &tx, bool miner_tx, cons } else { - bool r; - if (miner_tx && i > 0) - r = cryptonote::generate_key_image_helper_precomp(m_account.get_keys(), boost::get(tx.vout[i].target).key, tx_scan_info.received->derivation, 0, {0, 0}, tx_scan_info.in_ephemeral, tx_scan_info.ki, m_account.get_device()); - else - r = cryptonote::generate_key_image_helper_precomp(m_account.get_keys(), boost::get(tx.vout[i].target).key, tx_scan_info.received->derivation, i, tx_scan_info.received->index, tx_scan_info.in_ephemeral, tx_scan_info.ki, m_account.get_device()); + bool r = cryptonote::generate_key_image_helper_precomp(m_account.get_keys(), boost::get(tx.vout[i].target).key, tx_scan_info.received->derivation, i, tx_scan_info.received->index, tx_scan_info.in_ephemeral, tx_scan_info.ki, m_account.get_device()); THROW_WALLET_EXCEPTION_IF(!r, error::wallet_internal_error, "Failed to generate key image"); THROW_WALLET_EXCEPTION_IF(tx_scan_info.in_ephemeral.pub != boost::get(tx.vout[i].target).key, error::wallet_internal_error, "key_image generated ephemeral public key not matched with output_key"); @@ -1843,19 +1839,16 @@ void wallet2::process_new_transaction(const crypto::hash &txid, const cryptonote memcpy(&derivation, rct::identity().bytes, sizeof(derivation)); } - // additional tx pubkeys and derivations for multi-destination transfers involving one or more subaddresses - if (pk_index == 1) + // additional tx pubkeys and derivations for multi-destination transfers involving one or more subaddresses + if (find_tx_extra_field_by_type(tx_extra_fields, additional_tx_pub_keys)) { - if (find_tx_extra_field_by_type(tx_extra_fields, additional_tx_pub_keys)) + for (size_t i = 0; i < additional_tx_pub_keys.data.size(); ++i) { - for (size_t i = 0; i < additional_tx_pub_keys.data.size(); ++i) + additional_derivations.push_back({}); + if (!hwdev.generate_key_derivation(additional_tx_pub_keys.data[i], keys.m_view_secret_key, additional_derivations.back())) { - additional_derivations.push_back({}); - if (!hwdev.generate_key_derivation(additional_tx_pub_keys.data[i], keys.m_view_secret_key, additional_derivations.back())) - { - MWARNING("Failed to generate key derivation from additional tx pubkey in " << txid << ", skipping"); - memcpy(&additional_derivations.back(), rct::identity().bytes, sizeof(crypto::key_derivation)); - } + MWARNING("Failed to generate key derivation from additional tx pubkey in " << txid << ", skipping"); + memcpy(&additional_derivations.back(), rct::identity().bytes, sizeof(crypto::key_derivation)); } } } @@ -1866,13 +1859,10 @@ void wallet2::process_new_transaction(const crypto::hash &txid, const cryptonote error::wallet_internal_error, "pk_index out of range of tx_cache_data"); is_out_data_ptr = &tx_cache_data.primary[pk_index - 1]; derivation = tx_cache_data.primary[pk_index - 1].derivation; - if (pk_index == 1) + for (size_t n = 0; n < tx_cache_data.additional.size(); ++n) { - for (size_t n = 0; n < tx_cache_data.additional.size(); ++n) - { - additional_tx_pub_keys.data.push_back(tx_cache_data.additional[n].pkey); - additional_derivations.push_back(tx_cache_data.additional[n].derivation); - } + additional_tx_pub_keys.data.push_back(tx_cache_data.additional[n].pkey); + additional_derivations.push_back(tx_cache_data.additional[n].derivation); } } @@ -1935,47 +1925,15 @@ void wallet2::process_new_transaction(const crypto::hash &txid, const cryptonote { for (size_t i = 0; i < tx.vout.size(); ++i) { - if (miner_tx && i > 0) + check_acc_out_precomp_once(tx.vout[i], derivation, additional_derivations, i, is_out_data_ptr, tx_scan_info[i], output_found[i]); + THROW_WALLET_EXCEPTION_IF(tx_scan_info[i].error, error::acc_outs_lookup_error, tx, tx_pub_key, m_account.get_keys()); + if (tx_scan_info[i].received) { - crypto::key_derivation additional_key_derivation; hw::device &hwdev = m_account.get_device(); boost::unique_lock hwdev_lock (hwdev); - hwdev.set_mode(hw::device::TRANSACTION_PARSE); - crypto::public_key additional_tx_pubkey = get_tx_pub_key_from_extra(tx, i); - if (!hwdev.generate_key_derivation(additional_tx_pubkey, keys.m_view_secret_key, additional_key_derivation)) - { - std::cout << "Failed to generate key derivation from additional tx pubkey in " << txid << std::endl; - MWARNING("Failed to generate key derivation from additional tx pubkey in " << txid); - } - check_acc_out_precomp(tx.vout[i], additional_key_derivation, additional_derivations, 0, tx_scan_info[i]); - THROW_WALLET_EXCEPTION_IF(tx_scan_info[i].error, error::acc_outs_lookup_error, tx, additional_tx_pubkey, m_account.get_keys()); - if (tx_scan_info[i].received) - { - LOG_PRINT_L0("Scanning potential uncle reward"); - auto vouts_start = num_vouts_received; - hw::device &hwdev = m_account.get_device(); - boost::unique_lock hwdev_lock (hwdev); - hwdev.set_mode(hw::device::NONE); - hwdev.conceal_derivation(tx_scan_info[i].received->derivation, additional_tx_pubkey, additional_tx_pub_keys.data, additional_key_derivation, additional_derivations); - scan_output(tx, miner_tx, additional_tx_pubkey, i, tx_scan_info[i], num_vouts_received, tx_money_got_in_outs, outs, pool); - if (num_vouts_received > vouts_start) - { - LOG_PRINT_L0("Found uncle reward"); - } - } - } - else - { - check_acc_out_precomp(tx.vout[i], derivation, additional_derivations, i, is_out_data_ptr, tx_scan_info[i]); - THROW_WALLET_EXCEPTION_IF(tx_scan_info[i].error, error::acc_outs_lookup_error, tx, tx_pub_key, m_account.get_keys()); - if (tx_scan_info[i].received) - { - hw::device &hwdev = m_account.get_device(); - boost::unique_lock hwdev_lock (hwdev); - hwdev.set_mode(hw::device::NONE); - hwdev.conceal_derivation(tx_scan_info[i].received->derivation, tx_pub_key, additional_tx_pub_keys.data, derivation, additional_derivations); - scan_output(tx, miner_tx, tx_pub_key, i, tx_scan_info[i], num_vouts_received, tx_money_got_in_outs, outs, pool); - } + hwdev.set_mode(hw::device::NONE); + hwdev.conceal_derivation(tx_scan_info[i].received->derivation, tx_pub_key, additional_tx_pub_keys.data, derivation, additional_derivations); + scan_output(tx, miner_tx, tx_pub_key, i, tx_scan_info[i], num_vouts_received, tx_money_got_in_outs, outs, pool); } } } From 7f5f90935f691c6af9d3655a1650410606540559 Mon Sep 17 00:00:00 2001 From: Ben Evanoff Date: Sat, 21 Mar 2026 19:12:25 -0500 Subject: [PATCH 14/15] SECOR: recursively relay uncle blocks | set CRYPTONOTE_LOCKED_TX_ALLOWED_DELTA_SECONDS_SECOR --- docs/SECOR.md | 13 +++++--- src/cryptonote_config.h | 1 + src/cryptonote_core/blockchain.cpp | 2 +- src/cryptonote_core/cryptonote_core.cpp | 30 +++++++++++-------- src/cryptonote_core/cryptonote_core.h | 2 ++ .../cryptonote_protocol_handler.inl | 22 ++------------ src/simplewallet/simplewallet.cpp | 4 +-- src/wallet/wallet2.cpp | 7 +++-- 8 files changed, 39 insertions(+), 42 deletions(-) diff --git a/docs/SECOR.md b/docs/SECOR.md index cd882095..d7a1af04 100644 --- a/docs/SECOR.md +++ b/docs/SECOR.md @@ -159,6 +159,14 @@ The probability that any two miners have each found a block after mining for 1 m Overall, the network difficulty adjusts in a way that adding miners to the network doesnt affect the probability of chain splits significantly assuming that mining power is distributed evenly among miners. This model does not account for the fact that adding miners to the network increases the amount of work required for block propogation across all miners on the network. +### Mining Difficulty Considerations + +The current difficulty algorithm adjusts based on the observed difference between block timestamps of recently found blocks compared against the target solve time. We will be lowering that target solve time from 60 seconds to 15 seconds. The algorithm should adjust quickly, but it will be a bit wonky for the first ~60 blocks after hard forking. + +Currently the LWMA algorithm uses the parameter N=60 so we are using the most recent 60 blocks when calculating the weighted average. Since we are lowering the target block time, that means we will only be using 15 minutes worth of block histories instead of the last 1 hour worth of blocks. Is this OK? + +The "future time limit" used in the difficulty algorithm is intended to be 500 seconds according to the comments in the code. Currently this is, in fact, hard coded to 300 seconds (`CRYPTONOTE_BLOCK_FUTURE_TIME_LIMIT_V6`). Since we are lowering the target solve time, the (FTL / T) ratio increases. Should this be adjusted? + ## Status This proposal is a work in progress. I am seeking community feedback to see if I should continue working on this idea, and to get an idea of what people want to do about the block reward. @@ -173,10 +181,7 @@ This proposal is a work in progress. I am seeking community feedback to see if I #### Research -* How does changing the difficulty target affect the rest of the difficulty algo parameters ? - * CRYPTONOTE_LOCKED_TX_ALLOWED_DELTA_SECONDS_V1 references this. TX locking can be done with unix timestamps instead of blocks. This should be consistent. - * DIFFICULTY_BLOCKS_COUNT - should the DIFFICULTY_WINDOW be adjusted ? - * CRYPTONOTE_BLOCK_FUTURE_TIME_LIMIT_V6 should probably be adjusted +* Confirm that the difficulty algorithm doesnt require further adjustment * How long does it currently take for a block to propogate throughout the network ? diff --git a/src/cryptonote_config.h b/src/cryptonote_config.h index 16f72fe9..13593955 100644 --- a/src/cryptonote_config.h +++ b/src/cryptonote_config.h @@ -98,6 +98,7 @@ #define DIFFICULTY_BLOCKS_COUNT_V6 DIFFICULTY_WINDOW_V6 + 1 #define CRYPTONOTE_LOCKED_TX_ALLOWED_DELTA_SECONDS_V1 DIFFICULTY_TARGET *CRYPTONOTE_LOCKED_TX_ALLOWED_DELTA_BLOCKS +#define CRYPTONOTE_LOCKED_TX_ALLOWED_DELTA_SECONDS_SECOR DIFFICULTY_TARGET_SECOR *CRYPTONOTE_LOCKED_TX_ALLOWED_DELTA_BLOCKS #define CRYPTONOTE_LOCKED_TX_ALLOWED_DELTA_BLOCKS 1 #define BLOCKS_IDS_SYNCHRONIZING_DEFAULT_COUNT 10000 diff --git a/src/cryptonote_core/blockchain.cpp b/src/cryptonote_core/blockchain.cpp index c0ae0f83..4ba9fed7 100644 --- a/src/cryptonote_core/blockchain.cpp +++ b/src/cryptonote_core/blockchain.cpp @@ -3350,7 +3350,7 @@ bool Blockchain::is_tx_spendtime_unlocked(uint64_t unlock_time) const { //interpret as time uint64_t current_time = static_cast(time(NULL)); - if(current_time + CRYPTONOTE_LOCKED_TX_ALLOWED_DELTA_SECONDS_V1 >= unlock_time) + if(current_time + (get_current_hard_fork_version() >= HF_VERSION_SECOR ? CRYPTONOTE_LOCKED_TX_ALLOWED_DELTA_SECONDS_SECOR : CRYPTONOTE_LOCKED_TX_ALLOWED_DELTA_SECONDS_V1) >= unlock_time) return true; else return false; diff --git a/src/cryptonote_core/cryptonote_core.cpp b/src/cryptonote_core/cryptonote_core.cpp index f0fd079e..6a75d130 100644 --- a/src/cryptonote_core/cryptonote_core.cpp +++ b/src/cryptonote_core/cryptonote_core.cpp @@ -1443,19 +1443,7 @@ namespace cryptonote CHECK_AND_ASSERT_MES(!bvc.m_verifivation_failed, false, "mined block failed verification"); if(bvc.m_added_to_main_chain) { - if (b.uncle_hash != crypto::null_hash) - { - // first relay uncle blocks if an uncle block hash is included in the block - cryptonote_connection_context exclude_context = {}; - NOTIFY_NEW_BLOCK::request arg = AUTO_VAL_INIT(arg); - arg.current_blockchain_height = m_blockchain_storage.get_current_blockchain_height()-1; // -1 per uncle block definition - cryptonote::block uncle_block; - get_block_by_hash(b.uncle_hash, uncle_block); - block_to_blob(uncle_block, arg.b.block); - m_pprotocol->relay_block(arg, exclude_context); - // TODO: this should be recursive - if the uncle's prev_id isnt in the main chain then continue relaying the alt chain history until a common ancestor is found - } - + relay_uncle_blocks(b, m_blockchain_storage.get_current_blockchain_height()); cryptonote_connection_context exclude_context = {}; NOTIFY_NEW_BLOCK::request arg = AUTO_VAL_INIT(arg); arg.current_blockchain_height = m_blockchain_storage.get_current_blockchain_height(); @@ -1852,6 +1840,22 @@ namespace cryptonote return true; } //----------------------------------------------------------------------------------------------- + void core::relay_uncle_blocks(const cryptonote::block &b, uint64_t height) + { + // first relay uncle blocks if an uncle block hash is included in the block + if (b.uncle_hash != crypto::null_hash) + { + cryptonote::block uncle_block; + get_block_by_hash(b.uncle_hash, uncle_block); + relay_uncle_blocks(uncle_block, height-2); // -2 for uncle of an uncle + cryptonote_connection_context exclude_context = {}; + NOTIFY_NEW_BLOCK::request arg = AUTO_VAL_INIT(arg); + arg.current_blockchain_height = height-1; // -1 per uncle block definition + block_to_blob(uncle_block, arg.b.block); + m_pprotocol->relay_block(arg, exclude_context); + } + } + //----------------------------------------------------------------------------------------------- bool core::contact_server() { if (analytics::is_enabled() && (m_nettype == MAINNET || m_nettype == TESTNET)) diff --git a/src/cryptonote_core/cryptonote_core.h b/src/cryptonote_core/cryptonote_core.h index 428f84df..394335e5 100644 --- a/src/cryptonote_core/cryptonote_core.h +++ b/src/cryptonote_core/cryptonote_core.h @@ -851,6 +851,8 @@ namespace cryptonote */ void flush_bad_txs_cache(); + void relay_uncle_blocks(const cryptonote::block &b, uint64_t height); + private: /** diff --git a/src/cryptonote_protocol/cryptonote_protocol_handler.inl b/src/cryptonote_protocol/cryptonote_protocol_handler.inl index efbd1b54..29e5741e 100644 --- a/src/cryptonote_protocol/cryptonote_protocol_handler.inl +++ b/src/cryptonote_protocol/cryptonote_protocol_handler.inl @@ -484,16 +484,7 @@ namespace cryptonote if(bvc.m_added_to_main_chain) { //TODO: Add here announce protocol usage - if (b.uncle_hash != crypto::null_hash) - { - cryptonote_connection_context exclude_context = {}; - NOTIFY_NEW_BLOCK::request arg_uncle_request = AUTO_VAL_INIT(arg_uncle_request); - arg_uncle_request.current_blockchain_height = arg.current_blockchain_height-1; // -1 per uncle block definition - cryptonote::block uncle_block; - m_core.get_block_by_hash(b.uncle_hash, uncle_block); - block_to_blob(uncle_block, arg_uncle_request.b.block); - relay_block(arg_uncle_request, exclude_context); - } + m_core.relay_uncle_blocks(b, arg.current_blockchain_height); // relay uncle blocks BEFORE main block relay_block(arg, context); }else if(bvc.m_marked_as_orphaned) { @@ -772,16 +763,7 @@ namespace cryptonote if( bvc.m_added_to_main_chain ) { //TODO: Add here announce protocol usage - if (new_block.uncle_hash != crypto::null_hash) - { - cryptonote_connection_context exclude_context = {}; - NOTIFY_NEW_BLOCK::request arg_uncle_request = AUTO_VAL_INIT(arg_uncle_request); - arg_uncle_request.current_blockchain_height = arg.current_blockchain_height-1; // -1 per uncle block definition - cryptonote::block uncle_block; - m_core.get_block_by_hash(new_block.uncle_hash, uncle_block); - block_to_blob(uncle_block, arg_uncle_request.b.block); - relay_block(arg_uncle_request, exclude_context); - } + m_core.relay_uncle_blocks(new_block, arg.current_blockchain_height); // relay uncle blocks BEFORE main block NOTIFY_NEW_BLOCK::request reg_arg = AUTO_VAL_INIT(reg_arg); reg_arg.current_blockchain_height = arg.current_blockchain_height; reg_arg.b = b; diff --git a/src/simplewallet/simplewallet.cpp b/src/simplewallet/simplewallet.cpp index 625706e5..38c39029 100644 --- a/src/simplewallet/simplewallet.cpp +++ b/src/simplewallet/simplewallet.cpp @@ -7810,7 +7810,7 @@ bool simple_wallet::get_transfers(std::vector& local_args, std::vec else { uint64_t current_time = static_cast(time(NULL)); - uint64_t threshold = current_time + CRYPTONOTE_LOCKED_TX_ALLOWED_DELTA_SECONDS_V1; + uint64_t threshold = current_time + CRYPTONOTE_LOCKED_TX_ALLOWED_DELTA_SECONDS_V1; // TODO: the correct threshold depends on hard fork version if (threshold < pd.m_unlock_time) locked_msg = get_human_readable_timespan(std::chrono::seconds(pd.m_unlock_time - threshold)); } @@ -9410,7 +9410,7 @@ bool simple_wallet::show_transfer(const std::vector &args) else { uint64_t current_time = static_cast(time(NULL)); - uint64_t threshold = current_time + CRYPTONOTE_LOCKED_TX_ALLOWED_DELTA_SECONDS_V1; + uint64_t threshold = current_time + CRYPTONOTE_LOCKED_TX_ALLOWED_DELTA_SECONDS_V1; // TODO: the correct threshold depends on hard fork version if (threshold >= pd.m_unlock_time) success_msg_writer() << "unlocked for " << get_human_readable_timespan(std::chrono::seconds(threshold - pd.m_unlock_time)); else diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp index 4d9b2f21..ee783f33 100644 --- a/src/wallet/wallet2.cpp +++ b/src/wallet/wallet2.cpp @@ -5857,7 +5857,7 @@ bool wallet2::is_tx_spendtime_unlocked(uint64_t unlock_time, uint64_t block_heig uint64_t current_time = static_cast(time(NULL)); // XXX: this needs to be fast, so we'd need to get the starting heights // from the daemon to be correct once voting kicks in - uint64_t leeway = CRYPTONOTE_LOCKED_TX_ALLOWED_DELTA_SECONDS_V1; + uint64_t leeway = CRYPTONOTE_LOCKED_TX_ALLOWED_DELTA_SECONDS_V1; // TODO: the correct threshold depends on hard fork version if(current_time + leeway >= unlock_time) return true; else @@ -9461,7 +9461,10 @@ bool wallet2::sanity_check(const std::vector &ptx_vector, s std::string proof = get_tx_proof(ptx.tx, ptx.tx_key, ptx.additional_tx_keys, address, r.second.second, "automatic-sanity-check"); check_tx_proof(ptx.tx, address, r.second.second, "automatic-sanity-check", proof, received); } - catch (const std::exception &e) { received = 0; } + catch (const std::exception &e) { + MDEBUG("tx proof failed sanity check"); + received = 0; + } total_received += received; } From 63cf8dcf70c7c1fc672f7309fde4ef06dc3c40e2 Mon Sep 17 00:00:00 2001 From: Ben Evanoff Date: Thu, 26 Mar 2026 18:42:11 -0500 Subject: [PATCH 15/15] SECOR: fix uncle vs parent PoW check | better uncle reward validation | better error handling --- src/cryptonote_core/blockchain.cpp | 58 +++++++++++++++++++++++------- 1 file changed, 45 insertions(+), 13 deletions(-) diff --git a/src/cryptonote_core/blockchain.cpp b/src/cryptonote_core/blockchain.cpp index 4ba9fed7..c7939525 100644 --- a/src/cryptonote_core/blockchain.cpp +++ b/src/cryptonote_core/blockchain.cpp @@ -1261,6 +1261,7 @@ bool Blockchain::validate_miner_transaction(const block& b, size_t cumulative_bl uint64_t total_reward = base_reward + fee; if (version >= HF_VERSION_SECOR && b.uncle_hash != crypto::null_hash) { + MDEBUG("beginning validation of miner tx for a block which references an uncle block"); if (b.miner_tx.vout.size() != 2) { MERROR("miner tx from a block which includes an uncle hash must contain two separate outputs with separate recipients"); @@ -1271,26 +1272,31 @@ bool Blockchain::validate_miner_transaction(const block& b, size_t cumulative_bl total_reward += base_reward / SECOR_UNCLE_REWARD_RATIO; // make sure that the uncle reward uses the output key from the uncle block miner tx and is for the correct amount - dont let the nephew miner steal the uncle reward cryptonote::block uncle_block; - get_block_by_hash(b.uncle_hash, uncle_block); + if (!get_block_by_hash(b.uncle_hash, uncle_block)) + { + MERROR("couldn't find uncle block " << b.uncle_hash << " in the database"); + return false; + } // nephew reward at index 1 and uncle reward at index 0 is enforced crypto::public_key uncle_out_key; crypto::public_key uncle_tx_key; get_uncle_block_original_miner_details(uncle_block, uncle_out_key, uncle_tx_key); crypto::public_key mainchain_out_key; crypto::public_key mainchain_tx_key; - get_mainchain_block_original_miner_details(b, mainchain_out_key, mainchain_tx_key); - if (boost::get(b.miner_tx.vout[0].target).key != uncle_out_key) + int mainchain_miner_idx = get_mainchain_block_original_miner_details(b, mainchain_out_key, mainchain_tx_key); + int mainchain_uncle_idx = mainchain_miner_idx ? 0 : 1; + if (boost::get(b.miner_tx.vout[mainchain_uncle_idx].target).key != uncle_out_key) { MERROR_VER("nephew block miner tx does not include proper uncle reward output key"); return false; } - if (get_tx_pub_key_from_extra(b.miner_tx, 0) != uncle_tx_key) + if (get_tx_pub_key_from_extra(b.miner_tx, mainchain_uncle_idx) != uncle_tx_key) { MERROR_VER("nephew block miner tx does not include proper uncle reward tx public key in tx extra"); return false; } - if (b.miner_tx.vout[0].amount != base_reward / SECOR_UNCLE_REWARD_RATIO) + if (b.miner_tx.vout[mainchain_uncle_idx].amount != base_reward / SECOR_UNCLE_REWARD_RATIO) { MERROR_VER("miner tx contains uncle reward but the uncle reward amount is incorrect. Amount found " << b.miner_tx.vout[1].amount << " vs amount expected " << base_reward / SECOR_UNCLE_REWARD_RATIO); return false; @@ -3807,15 +3813,29 @@ bool Blockchain::handle_block_to_main_chain(const block& bl, const crypto::hash& if (bl.uncle_hash != crypto::null_hash) { + MINFO("beginning validation of uncle block " << bl.uncle_hash); CHECK_AND_ASSERT_MES(blockchain_height, 0, "blockchain_height is NULL"); + cryptonote::block uncle_block; - get_block_by_hash(bl.uncle_hash, uncle_block); + if (!get_block_by_hash(bl.uncle_hash, uncle_block)) + { + MERROR_VER("uncle block with hash " << bl.uncle_hash << " could not be found in the blockchain database"); + bvc.m_verifivation_failed = true; + return_tx_to_pool(txs); + goto leave; + } // validate uncle block PoW difficulty_type_128 uncle_difficulty = m_db->get_block_cumulative_difficulty(blockchain_height-2) - m_db->get_block_cumulative_difficulty(blockchain_height-3); crypto::hash uncle_pow; memset(uncle_pow.data, 0xff, sizeof(uncle_pow.data)); - get_block_longhash(m_hash_context, this, uncle_block, uncle_pow, m_db->height()-1); + if (!get_block_longhash(m_hash_context, this, uncle_block, uncle_pow, m_db->height()-1)) + { + MERROR_VER("could not generate PoW hash for uncle block " << bl.uncle_hash); + bvc.m_verifivation_failed = true; + return_tx_to_pool(txs); + goto leave; + } if(!check_hash(uncle_pow, uncle_difficulty.convert_to())) { MERROR_VER("proof-of-work for uncle block failed verification"); @@ -3826,13 +3846,27 @@ bool Blockchain::handle_block_to_main_chain(const block& bl, const crypto::hash& // the main chain block at the uncle block height should have a smaller PoW hash than the uncle block cryptonote::block uncle_sibling_block; - get_block_by_hash(get_block_id_by_height(m_db->height()-2), uncle_sibling_block); + crypto::hash uncle_sibling_hash = get_block_id_by_height(m_db->height()-1); + MDEBUG("verifying that uncle block " << bl.uncle_hash << " PoW is less than its sibling block " << uncle_sibling_hash); + if (!get_block_by_hash(uncle_sibling_hash, uncle_sibling_block)) + { + MERROR_VER("could not find uncle block sibling " << uncle_sibling_hash << " by hash in the database"); + bvc.m_verifivation_failed = true; + return_tx_to_pool(txs); + goto leave; + } crypto::hash pow_uncle_sibling; memset(pow_uncle_sibling.data, 0xff, sizeof(pow_uncle_sibling.data)); - get_block_longhash(m_hash_context, this, uncle_sibling_block, pow_uncle_sibling, m_db->height()-2); - if (reinterpret_cast(uncle_sibling_block) >= reinterpret_cast(uncle_pow)) + if(!get_block_longhash(m_hash_context, this, uncle_sibling_block, pow_uncle_sibling, m_db->height()-1)) { - MERROR_VER("the proof-of-work hash for an uncle block must be greater than that of its main chain sibling"); + MERROR_VER("failed to calculate block long hash for uncle block sibling"); + bvc.m_verifivation_failed = true; + return_tx_to_pool(txs); + goto leave; + } + if (reinterpret_cast(uncle_pow) >= reinterpret_cast(pow_uncle_sibling)) + { + MERROR_VER("the proof-of-work hash for an uncle block must be smaller than that of its main chain sibling"); bvc.m_verifivation_failed = true; return_tx_to_pool(txs); goto leave; @@ -3840,8 +3874,6 @@ bool Blockchain::handle_block_to_main_chain(const block& bl, const crypto::hash& // add block difficulty for uncle block(s) to chain cumulative difficulty cumulative_difficulty += uncle_difficulty; - - // TODO: support extended ancestry ? } TIME_MEASURE_FINISH(block_processing_time);