From 40eb82873e76741b65b9ee275281a9c4869921df Mon Sep 17 00:00:00 2001 From: jeffro256 Date: Thu, 9 Oct 2025 15:15:59 -0500 Subject: [PATCH 1/2] cryptonote_core: cache input verification results directly in mempool This replaces `ver_rct_non_semantics_simple_cached()` with an API that offloads the responsibility of tracking input verification successes to the caller. The main caller of this function in the codebase, `cryptonote::Blockchain()` instead keeps track of the verification results for transaction in the mempool by storing a "verification ID" in the mempool metadata table (with `txpool_tx_meta_t`). This has several benefits, including: * When the mempool is large (>8192 txs), we no longer experience cache misses and unnecessarily re-verify ring signatures. This greatly improves block propagation time for FCMP++ blocks under load * For the same reason, reorg handling can be sped up by storing verification IDs of transactions popped from the chain * Speeds up re-validating every mempool transaction on fork change (monerod revalidates the whole tx-pool on HFs #10142) * Caches results for every single type of Monero transaction, not just latest RCT type * Cache persists over a node restart * Uses 512KiB less RAM (8192*2*32B) * No additional storage or DB migration required since `txpool_tx_meta_t` already had padding allocated * Moves more verification logic out of `cryptonote::Blockchain` Furthermore, this opens the door to future multi-threaded block verification speed-ups. Right now, transactions' input proof verification is limited to one transaction at a time. However, one can imagine a scenario with verification IDs where input proofs are optimistically multi-threaded in advance of block processing. Then, even though ring member fetching and verification is single-threaded inside of `cryptonote::Blockchain::check_tx_inputs()`, the single thread can skip the CPU-intensive cryptographic code if the verification ID allows it. Also changes the default log category in `tx_verification_utils.cpp` from "blockchain" to "verify". --- src/blockchain_db/blockchain_db.h | 10 +- src/cryptonote_core/blockchain.cpp | 290 +++++++----------- src/cryptonote_core/blockchain.h | 45 ++- src/cryptonote_core/tx_pool.cpp | 44 ++- src/cryptonote_core/tx_pool.h | 39 ++- src/cryptonote_core/tx_verification_utils.cpp | 222 +++++++++++--- src/cryptonote_core/tx_verification_utils.h | 51 ++- tests/functional_tests/p2p.py | 146 +++++++++ tests/unit_tests/CMakeLists.txt | 2 +- tests/unit_tests/tx_verification_utils.cpp | 269 ++++++++++++++++ ...ached.cpp => verRctNonSemanticsSimple.cpp} | 6 +- utils/python-rpc/framework/daemon.py | 4 +- 12 files changed, 822 insertions(+), 306 deletions(-) create mode 100644 tests/unit_tests/tx_verification_utils.cpp rename tests/unit_tests/{ver_rct_non_semantics_simple_cached.cpp => verRctNonSemanticsSimple.cpp} (98%) diff --git a/src/blockchain_db/blockchain_db.h b/src/blockchain_db/blockchain_db.h index 5bcc63d7f..42591dc99 100644 --- a/src/blockchain_db/blockchain_db.h +++ b/src/blockchain_db/blockchain_db.h @@ -172,7 +172,12 @@ struct txpool_tx_meta_t uint8_t is_forwarding: 1; uint8_t bf_padding: 3; - uint8_t padding[76]; // till 192 bytes + uint8_t padding[44]; // til 160 bytes + + // If non-null, this verification ID is set for this tx only when some mixring passed ver_input_proofs_rings() + crypto::hash valid_input_verification_id; + + // 192 bytes total void set_relay_method(relay_method method) noexcept; relay_method get_relay_method() const noexcept; @@ -186,7 +191,8 @@ struct txpool_tx_meta_t return matches_category(get_relay_method(), category); } }; - +static_assert(sizeof(txpool_tx_meta_t) == 192, "possible DB migration needed for changes to txpool_tx_meta_t"); +static_assert(offsetof(txpool_tx_meta_t, valid_input_verification_id) == 160, "verif ID wrong alignment"); #define DBF_SAFE 1 #define DBF_FAST 2 diff --git a/src/cryptonote_core/blockchain.cpp b/src/cryptonote_core/blockchain.cpp index 171087dae..65fdd7b54 100644 --- a/src/cryptonote_core/blockchain.cpp +++ b/src/cryptonote_core/blockchain.cpp @@ -99,8 +99,7 @@ Blockchain::Blockchain(tx_memory_pool& tx_pool) : m_difficulty_for_next_block(1), m_btc_valid(false), m_batch_success(true), - m_prepare_height(0), - m_rct_ver_cache() + m_prepare_height(0) { LOG_PRINT_L3("Blockchain::" << __func__); } @@ -643,6 +642,57 @@ block Blockchain::pop_block_from_blockchain() // in hf_versions. uint8_t version = get_ideal_hard_fork_version(m_db->height()); + // At time of popping, we know all of the referenced mix ring data for popped transactions, + // and since they are already in the chain, and not pruned, we assume that the ring signature + // input verification succeeded for these transactions. We can deference each each mix ring, + // calculate the verification ID for that (tx, ring) pair, then add to the mempool with that + // input verification ID. This speeds up re-org handling by allowing to skip verifying ring + // signatures which were previously verified. + const crypto::hash tx_prefix_hash = get_transaction_prefix_hash(tx); + + struct outputs_visitor + { + rct::ctkeyV ˚ + bool handle_output(uint64_t, const crypto::public_key &pubkey, const rct::key &commitment) + { + ring.push_back({rct::pk2rct(pubkey), commitment}); + return true; + } + }; + + rct::ctkeyM dereferenced_mix_ring; + dereferenced_mix_ring.reserve(tx.vin.size()); + for (const txin_v &txin : tx.vin) + { + const txin_to_key *pin = boost::get(&txin); + if (nullptr == pin || pin->key_offsets.empty()) + { + dereferenced_mix_ring.clear(); + break; + } + + rct::ctkeyV &curr_ring = dereferenced_mix_ring.emplace_back(); + curr_ring.reserve(pin->key_offsets.size()); + outputs_visitor vis{curr_ring}; + + if (!scan_outputkeys_for_indexes(tx.version, *pin, vis, tx_prefix_hash)) + { + dereferenced_mix_ring.clear(); + break; + } + } + + crypto::hash valid_input_verification_id = crypto::null_hash; + if (!dereferenced_mix_ring.empty()) + { + valid_input_verification_id = make_input_verification_id(get_transaction_hash(tx), dereferenced_mix_ring); + } + else + { + MWARNING("Failed to fetch ring signature input data for popped transaction, " + "will have to re-verify signature later"); + } + // We assume that if they were in a block, the transactions are already known to the network // as a whole. However, if we had mined that block, that might not be always true. Unlikely // though, and always relaying these again might cause a spike of traffic as many nodes @@ -650,7 +700,7 @@ block Blockchain::pop_block_from_blockchain() // we also set the "nic_verified_hf_version" paramater. Since we know we took this transaction // from the mempool earlier in this function call, when the mempool has the same current fork // version, we can return it without re-verifying the consensus rules on it. - const bool r = m_tx_pool.add_tx(tx, tvc, relay_method::block, true, version, version); + const bool r = m_tx_pool.add_tx(tx, tvc, relay_method::block, true, version, version, valid_input_verification_id); if (!r) { LOG_ERROR("Error returning transaction to tx_pool"); @@ -2958,7 +3008,12 @@ bool Blockchain::get_tx_outputs_gindexs(const crypto::hash& tx_id, std::vector> pubkeys(tx.vin.size()); - std::vector < uint64_t > results; - results.resize(tx.vin.size(), 0); - - tools::threadpool& tpool = tools::threadpool::getInstanceForCompute(); - tools::threadpool::waiter waiter(tpool); - int threads = tpool.get_max_concurrency(); uint64_t max_used_block_height = 0; if (!pmax_used_block_height) @@ -3432,36 +3484,8 @@ bool Blockchain::check_tx_inputs(transaction& tx, tx_verification_context &tvc, return false; } - if (tx.version == 1) - { - if (threads > 1) - { - // ND: Speedup - // 1. Thread ring signature verification if possible. - tpool.submit(&waiter, boost::bind(&Blockchain::check_ring_signature, this, std::cref(tx_prefix_hash), std::cref(in_to_key.k_image), std::cref(pubkeys[sig_index]), std::cref(tx.signatures[sig_index]), std::ref(results[sig_index])), true); - } - else - { - check_ring_signature(tx_prefix_hash, in_to_key.k_image, pubkeys[sig_index], tx.signatures[sig_index], results[sig_index]); - if (!results[sig_index]) - { - MERROR_VER("Failed to check ring signature for tx " << get_transaction_hash(tx) << " vin key with k_image: " << in_to_key.k_image << " sig_index: " << sig_index); - - if (pmax_used_block_height) // a default value of NULL is used when called from Blockchain::handle_block_to_main_chain() - { - MERROR_VER("*pmax_used_block_height: " << *pmax_used_block_height); - } - - return false; - } - } - } - sig_index++; } - if (tx.version == 1 && threads > 1) - if (!waiter.wait()) - return false; // enforce min output age if (hf_version >= HF_VERSION_ENFORCE_MIN_AGE) @@ -3470,143 +3494,45 @@ bool Blockchain::check_tx_inputs(transaction& tx, tx_verification_context &tvc, false, "Transaction spends at least one output which is too young"); } - // Warn that new RCT types are present, and thus the cache is not being used effectively - static constexpr const std::uint8_t RCT_CACHE_TYPE = rct::RCTTypeBulletproofPlus; - if (tx.rct_signatures.type > RCT_CACHE_TYPE) - { - MWARNING("RCT cache is not caching new verification results. Please update RCT_CACHE_TYPE!"); - } + const crypto::hash txid = get_transaction_hash(tx); - if (tx.version == 1) + // Try skipping verification if input verification ID matches a previously valid ID + crypto::hash calculated_input_verification_id = crypto::null_hash; + if (valid_input_verification_id_inout != crypto::null_hash) { - if (threads > 1) + calculated_input_verification_id = make_input_verification_id(get_transaction_hash(tx), pubkeys); + if (calculated_input_verification_id == valid_input_verification_id_inout) { - // save results to table, passed or otherwise - bool failed = false; - for (size_t i = 0; i < tx.vin.size(); i++) - { - if(!failed && !results[i]) - failed = true; - } - - if (failed) - { - MERROR_VER("Failed to check ring signatures!"); - return false; - } + MDEBUG("Valid verID hit for tx " << txid << ", skipping input verification..."); + return true; + } + else + { + MDEBUG("Previously valid verID for tx " << txid << " does not match current. Perhaps there was a reorg? " + "Continuing to input verification even though this is not likely to succeed..."); } } else { - // from version 2, check ringct signatures - // obviously, the original and simple rct APIs use a mixRing that's indexes - // in opposite orders, because it'd be too simple otherwise... - const rct::rctSig &rv = tx.rct_signatures; - switch (rv.type) - { - case rct::RCTTypeNull: { - // we only accept no signatures for coinbase txes - MERROR_VER("Null rct signature on non-coinbase tx"); - return false; - } - case rct::RCTTypeSimple: - case rct::RCTTypeBulletproof: - case rct::RCTTypeBulletproof2: - case rct::RCTTypeCLSAG: - case rct::RCTTypeBulletproofPlus: - { - if (!ver_rct_non_semantics_simple_cached(tx, pubkeys, m_rct_ver_cache, RCT_CACHE_TYPE)) - { - MERROR_VER("Failed to check ringct signatures!"); - return false; - } - break; - } - case rct::RCTTypeFull: - { - if (!expand_transaction_2(tx, tx_prefix_hash, pubkeys)) - { - MERROR_VER("Failed to expand rct signatures!"); - return false; - } - - // check all this, either reconstructed (so should really pass), or not - { - bool size_matches = true; - for (size_t i = 0; i < pubkeys.size(); ++i) - size_matches &= pubkeys[i].size() == rv.mixRing.size(); - for (size_t i = 0; i < rv.mixRing.size(); ++i) - size_matches &= pubkeys.size() == rv.mixRing[i].size(); - if (!size_matches) - { - MERROR_VER("Failed to check ringct signatures: mismatched pubkeys/mixRing size"); - return false; - } - - for (size_t n = 0; n < pubkeys.size(); ++n) - { - for (size_t m = 0; m < pubkeys[n].size(); ++m) - { - if (pubkeys[n][m].dest != rct::rct2pk(rv.mixRing[m][n].dest)) - { - MERROR_VER("Failed to check ringct signatures: mismatched pubkey at vin " << n << ", index " << m); - return false; - } - if (pubkeys[n][m].mask != rct::rct2pk(rv.mixRing[m][n].mask)) - { - MERROR_VER("Failed to check ringct signatures: mismatched commitment at vin " << n << ", index " << m); - return false; - } - } - } - } - - if (rv.p.MGs.size() != 1) - { - MERROR_VER("Failed to check ringct signatures: Bad MGs size"); - return false; - } - if (rv.p.MGs.empty() || rv.p.MGs[0].II.size() != tx.vin.size()) - { - MERROR_VER("Failed to check ringct signatures: mismatched II/vin sizes"); - return false; - } - for (size_t n = 0; n < tx.vin.size(); ++n) - { - if (memcmp(&boost::get(tx.vin[n]).k_image, &rv.p.MGs[0].II[n], 32)) - { - MERROR_VER("Failed to check ringct signatures: mismatched II/vin sizes"); - return false; - } - } - - if (!rct::verRct(rv, false)) - { - MERROR_VER("Failed to check ringct signatures!"); - return false; - } - break; - } - default: - MERROR_VER("Unsupported rct type: " << rv.type); - return false; - } + MDEBUG("No previously valid verID provided for tx " << txid << ", continuing to input verification as normal..."); } - return true; -} -//------------------------------------------------------------------ -void Blockchain::check_ring_signature(const crypto::hash &tx_prefix_hash, const crypto::key_image &key_image, const std::vector &pubkeys, const std::vector& sig, uint64_t &result) const -{ - std::vector p_output_keys; - p_output_keys.reserve(pubkeys.size()); - for (auto &key : pubkeys) + // Verify ring signature input proofs + valid_input_verification_id_inout = crypto::null_hash; + if (!ver_input_proofs_rings(tx, pubkeys)) { - // rct::key and crypto::public_key have the same structure, avoid object ctor/memcpy - p_output_keys.push_back(&(const crypto::public_key&)key.dest); + MERROR_VER("Failed to verify input ring signatures for tx " << txid); + return false; } - result = crypto::check_ring_signature(tx_prefix_hash, key_image, p_output_keys, sig.data()) ? 1 : 0; + // At this point, we've succeeded at input verification, so set `valid_input_verification_id_inout` + valid_input_verification_id_inout = (calculated_input_verification_id == crypto::null_hash) + ? make_input_verification_id(get_transaction_hash(tx), pubkeys) + : calculated_input_verification_id; + + MDEBUG("Input verification for tx " << txid << " succeeded. Setting verID to " << valid_input_verification_id_inout); + + return true; } //------------------------------------------------------------------ @@ -3929,9 +3855,11 @@ bool Blockchain::flush_txes_from_pool(const std::vector &txids) cryptonote::blobdata txblob; size_t tx_weight; uint64_t fee; + crypto::hash valid_input_verification_id; bool relayed, do_not_relay, double_spend_seen, pruned; MINFO("Removing txid " << txid << " from the pool"); - if(m_tx_pool.have_tx(txid, relay_category::all) && !m_tx_pool.take_tx(txid, tx, txblob, tx_weight, fee, relayed, do_not_relay, double_spend_seen, pruned)) + if (m_tx_pool.have_tx(txid, relay_category::all) && !m_tx_pool.take_tx(txid, tx, txblob, + tx_weight, fee, valid_input_verification_id, relayed, do_not_relay, double_spend_seen, pruned)) { MERROR("Failed to remove txid " << txid << " from the pool"); res = false; @@ -4115,8 +4043,8 @@ leave: size_t cumulative_block_weight = coinbase_weight; std::vector> txs; - // txid weight mempool? - std::vector> txs_meta; + // txid weight mempool? verID + std::vector> txs_meta; // This will be the data sent to the ZMQ pool listeners for txs which skipped the mempool std::vector txpool_events; @@ -4141,6 +4069,7 @@ leave: const crypto::hash &txid = std::get<0>(txs_meta[i]); const blobdata &tx_blob = txs[i].second; const size_t tx_weight = std::get<1>(txs_meta[i]); + const crypto::hash &valid_input_verification_id = std::get<3>(txs_meta[i]); // We assume that if they were in a block, the transactions are already known to the network // as a whole. However, if we had mined that block, that might not be always true. Unlikely @@ -4151,7 +4080,7 @@ leave: // version, we can return it without re-verifying the consensus rules on it. cryptonote::tx_verification_context tvc{}; if (!m_tx_pool.add_tx(tx, txid, tx_blob, tx_weight, tvc, relay_method::block, true, - hf_version, hf_version)) + hf_version, hf_version, valid_input_verification_id)) MERROR("Failed to return taken transaction with hash: " << txid << " to tx_pool"); } }; @@ -4203,6 +4132,7 @@ leave: blobdata &txblob = txs.back().second; size_t tx_weight{}; uint64_t fee{}; + crypto::hash valid_input_verification_id{}; bool pruned{}; /* @@ -4213,7 +4143,7 @@ leave: */ bool _unused1, _unused2, _unused3; const bool found_tx_in_pool{ - m_tx_pool.take_tx(tx_id, tx, txblob, tx_weight, fee, + m_tx_pool.take_tx(tx_id, tx, txblob, tx_weight, fee, valid_input_verification_id, _unused1, _unused2, _unused3, pruned, /*suppress_missing_msgs=*/true) }; bool find_tx_failure{!found_tx_in_pool}; @@ -4259,7 +4189,7 @@ leave: // add the transaction to the temp list of transactions, so we can either // store the list of transactions all at once or return the ones we've // taken from the tx_pool back to it if the block fails verification. - txs_meta.emplace_back(tx_id, tx_weight, found_tx_in_pool); + txs_meta.emplace_back(tx_id, tx_weight, found_tx_in_pool, valid_input_verification_id); TIME_MEASURE_START(dd); // FIXME: the storage should not be responsible for validation. @@ -4285,7 +4215,7 @@ leave: { // validate that transaction inputs and the keys spending them are correct. tx_verification_context tvc; - if(!check_tx_inputs(tx, tvc)) + if(!check_tx_inputs(tx, tvc, valid_input_verification_id)) { MERROR_VER("Block with id: " << id << " has at least one transaction (id: " << tx_id << ") with wrong inputs."); @@ -5570,19 +5500,9 @@ void Blockchain::load_compiled_in_block_hashes(const GetCheckpointsCallback& get // for tx hashes will fail in handle_block_to_main_chain(..) CRITICAL_REGION_LOCAL(m_tx_pool); - std::vector txs; - m_tx_pool.get_transactions(txs, true); - - size_t tx_weight; - uint64_t fee; - bool relayed, do_not_relay, double_spend_seen, pruned; - transaction pool_tx; - blobdata txblob; - for(const transaction &tx : txs) - { - crypto::hash tx_hash = get_transaction_hash(tx); - m_tx_pool.take_tx(tx_hash, pool_tx, txblob, tx_weight, fee, relayed, do_not_relay, double_spend_seen, pruned); - } + std::vector tx_hashes; + m_tx_pool.get_transaction_hashes(tx_hashes, true); + flush_txes_from_pool(tx_hashes); } } } diff --git a/src/cryptonote_core/blockchain.h b/src/cryptonote_core/blockchain.h index 03a0d2196..ee6e888c8 100644 --- a/src/cryptonote_core/blockchain.h +++ b/src/cryptonote_core/blockchain.h @@ -628,11 +628,25 @@ namespace cryptonote * @param pmax_used_block_height return-by-reference block height of most recent input * @param max_used_block_id return-by-reference block hash of most recent input * @param tvc returned information about tx verification + * @param valid_input_verification_id_inout a previously valid verID if non-null, set on input verification * @param kept_by_block whether or not the transaction is from a previously-verified block * * @return false if any input is invalid, otherwise true + * + * If `valid_input_verification_id_inout` is passed as null, then input verification proceeds as normal. + * If `valid_input_verification_id_inout` is non-null and the verification ID from the current chain + * state matches, then input verification is skipped, assumed to be successful, and + * `valid_input_verification_id_inout` is not modified. If the current verification ID does not match, + * then input verification is attempted anyways. If input verification is attempted and fails, + * then `valid_input_verification_id_inout` is set to null. If input verification is attempted and succeeds, + * then `valid_input_verification_id_inout` is set to the current used verification ID. */ - bool check_tx_inputs(transaction& tx, uint64_t& pmax_used_block_height, crypto::hash& max_used_block_id, tx_verification_context &tvc, bool kept_by_block = false) const; + bool check_tx_inputs(transaction& tx, + uint64_t& pmax_used_block_height, + crypto::hash& max_used_block_id, + tx_verification_context &tvc, + crypto::hash &valid_input_verification_id_inout, + bool kept_by_block = false) const; /** * @brief get fee quantization mask @@ -1249,9 +1263,6 @@ namespace cryptonote uint64_t m_prepare_nblocks; std::vector *m_prepare_blocks; - // cache for verifying transaction RCT non semantics - mutable rct_ver_cache_t m_rct_ver_cache; - /** * @brief Blockchain constructor * @@ -1320,11 +1331,23 @@ namespace cryptonote * * @param tx the transaction to validate * @param tvc returned information about tx verification + * @param valid_input_verification_id_inout a previously valid verID if non-null, set on input verification * @param pmax_related_block_height return-by-pointer the height of the most recent block in the input set * * @return false if any validation step fails, otherwise true + * + * If `valid_input_verification_id_inout` is passed as null, then input verification proceeds as normal. + * If `valid_input_verification_id_inout` is non-null and the verification ID from the current chain + * state matches, then input verification is skipped, assumed to be successful, and + * `valid_input_verification_id_inout` is not modified. If the current verification ID does not match, + * then input verification is attempted anyways. If input verification is attempted and fails, + * then `valid_input_verification_id_inout` is set to null. If input verification is attempted and succeeds, + * then `valid_input_verification_id_inout` is set to the current used verification ID. */ - bool check_tx_inputs(transaction& tx, tx_verification_context &tvc, uint64_t* pmax_used_block_height = NULL) const; + bool check_tx_inputs(transaction& tx, + tx_verification_context &tvc, + crypto::hash &valid_input_verification_id_inout, + uint64_t* pmax_used_block_height = NULL) const; /** * @brief performs a blockchain reorganization according to the longest chain rule @@ -1589,18 +1612,6 @@ namespace cryptonote */ bool check_for_double_spend(const transaction& tx, key_images_container& keys_this_block) const; - /** - * @brief validates a transaction input's ring signature - * - * @param tx_prefix_hash the transaction prefix' hash - * @param key_image the key image generated from the true input - * @param pubkeys the public keys for each input in the ring signature - * @param sig the signature generated for each input in the ring signature - * @param result false if the ring signature is invalid, otherwise true - */ - void check_ring_signature(const crypto::hash &tx_prefix_hash, const crypto::key_image &key_image, - const std::vector &pubkeys, const std::vector &sig, uint64_t &result) const; - /** * @brief loads block hashes from compiled-in data set * diff --git a/src/cryptonote_core/tx_pool.cpp b/src/cryptonote_core/tx_pool.cpp index c63465e4a..8b3117b22 100644 --- a/src/cryptonote_core/tx_pool.cpp +++ b/src/cryptonote_core/tx_pool.cpp @@ -137,7 +137,8 @@ namespace cryptonote bool tx_memory_pool::add_tx(transaction &tx, /*const crypto::hash& tx_prefix_hash,*/ const crypto::hash &id, const cryptonote::blobdata &blob, size_t tx_weight, tx_verification_context& tvc, relay_method tx_relay, bool relayed, - uint8_t version, uint8_t nic_verified_hf_version) + uint8_t version, uint8_t nic_verified_hf_version, + const crypto::hash &valid_input_verification_id) { const bool kept_by_block = (tx_relay == relay_method::block); @@ -224,7 +225,9 @@ namespace cryptonote crypto::hash max_used_block_id = null_hash; uint64_t max_used_block_height = 0; cryptonote::txpool_tx_meta_t meta{}; - bool ch_inp_res = check_tx_inputs([&tx]()->cryptonote::transaction&{ return tx; }, id, max_used_block_height, max_used_block_id, tvc, kept_by_block); + meta.valid_input_verification_id = valid_input_verification_id; + const bool ch_inp_res = check_tx_inputs([&tx]()->cryptonote::transaction&{ return tx; }, id, + meta.valid_input_verification_id, max_used_block_height, max_used_block_id, tvc, kept_by_block); if(!ch_inp_res) { // if the transaction was valid before (kept_by_block), then it @@ -355,7 +358,8 @@ namespace cryptonote } //--------------------------------------------------------------------------------- bool tx_memory_pool::add_tx(transaction &tx, tx_verification_context& tvc, relay_method tx_relay, - bool relayed, uint8_t version, uint8_t nic_verified_hf_version) + bool relayed, uint8_t version, uint8_t nic_verified_hf_version, + const crypto::hash &valid_input_verification_id) { crypto::hash h = null_hash; cryptonote::blobdata bl; @@ -363,7 +367,7 @@ namespace cryptonote if (bl.size() == 0 || !get_transaction_hash(tx, h)) return false; return add_tx(tx, h, bl, get_transaction_weight(tx, bl.size()), tvc, tx_relay, relayed, version, - nic_verified_hf_version); + nic_verified_hf_version, valid_input_verification_id); } //--------------------------------------------------------------------------------- size_t tx_memory_pool::get_txpool_weight() const @@ -529,8 +533,20 @@ namespace cryptonote return true; } //--------------------------------------------------------------------------------- - bool tx_memory_pool::take_tx(const crypto::hash &id, transaction &tx, cryptonote::blobdata &txblob, size_t& tx_weight, uint64_t& fee, bool &relayed, bool &do_not_relay, bool &double_spend_seen, bool &pruned, const bool suppress_missing_msgs) + bool tx_memory_pool::take_tx(const crypto::hash &id, + transaction &tx, + cryptonote::blobdata &txblob, + size_t& tx_weight, + uint64_t& fee, + crypto::hash &valid_input_verification_id, + bool &relayed, + bool &do_not_relay, + bool &double_spend_seen, + bool &pruned, + const bool suppress_missing_msgs) { + valid_input_verification_id = crypto::null_hash; + CRITICAL_REGION_LOCAL(m_transactions_lock); CRITICAL_REGION_LOCAL1(m_blockchain); @@ -568,6 +584,7 @@ namespace cryptonote do_not_relay = meta.do_not_relay; double_spend_seen = meta.double_spend_seen; pruned = meta.pruned; + valid_input_verification_id = meta.valid_input_verification_id; sensitive = !meta.matches(relay_category::broadcasted); // remove first, in case this throws, so key images aren't removed @@ -1373,7 +1390,13 @@ namespace cryptonote m_transactions_lock.unlock(); } //--------------------------------------------------------------------------------- - bool tx_memory_pool::check_tx_inputs(const std::function &get_tx, const crypto::hash &txid, uint64_t &max_used_block_height, crypto::hash &max_used_block_id, tx_verification_context &tvc, bool kept_by_block) const + bool tx_memory_pool::check_tx_inputs(const std::function &get_tx, + const crypto::hash &txid, + crypto::hash &valid_input_verification_id_inout, + uint64_t &max_used_block_height, + crypto::hash &max_used_block_id, + tx_verification_context &tvc, + bool kept_by_block) const { if (!kept_by_block) { @@ -1386,7 +1409,7 @@ namespace cryptonote return std::get<0>(i->second); } } - bool ret = m_blockchain.check_tx_inputs(get_tx(), max_used_block_height, max_used_block_id, tvc, kept_by_block); + bool ret = m_blockchain.check_tx_inputs(get_tx(), max_used_block_height, max_used_block_id, tvc, valid_input_verification_id_inout, kept_by_block); if (!kept_by_block) m_input_cache.insert(std::make_pair(txid, std::make_tuple(ret, tvc, max_used_block_height, max_used_block_id))); return ret; @@ -1423,6 +1446,7 @@ namespace cryptonote tx_verification_context tvc{}; if (!check_tx_inputs([&lazy_tx]()->cryptonote::transaction&{ return lazy_tx(); }, txid, + txd.valid_input_verification_id, txd.max_used_block_height, txd.max_used_block_id, tvc)) @@ -1725,12 +1749,14 @@ namespace cryptonote cryptonote::transaction tx; cryptonote::blobdata blob; bool relayed, do_not_relay, double_spend_seen, pruned; - if (!take_tx(e.txid, tx, blob, weight, fee, relayed, do_not_relay, double_spend_seen, pruned)) + crypto::hash valid_input_verification_id; + if (!take_tx(e.txid, tx, blob, weight, fee, valid_input_verification_id, relayed, do_not_relay, double_spend_seen, pruned)) MERROR("Failed to get tx " << e.txid << " from txpool for re-validation"); cryptonote::tx_verification_context tvc{}; relay_method tx_relay = e.meta.get_relay_method(); - if (!add_tx(tx, e.txid, blob, e.meta.weight, tvc, tx_relay, relayed, version)) + if (!add_tx(tx, e.txid, blob, e.meta.weight, tvc, tx_relay, relayed, version, + /*nic_verified_hf_version=*/0, valid_input_verification_id)) { MINFO("Failed to re-validate tx " << e.txid << " for v" << (unsigned)version << ", dropped"); continue; diff --git a/src/cryptonote_core/tx_pool.h b/src/cryptonote_core/tx_pool.h index 1dbfd8481..88afc3257 100644 --- a/src/cryptonote_core/tx_pool.h +++ b/src/cryptonote_core/tx_pool.h @@ -116,10 +116,12 @@ namespace cryptonote * @param id the transaction's hash * @tx_relay how the transaction was received * @param tx_weight the transaction's weight + * @param valid_input_verification_id a previously valid verID if non-null */ bool add_tx(transaction &tx, const crypto::hash &id, const cryptonote::blobdata &blob, size_t tx_weight, tx_verification_context& tvc, relay_method tx_relay, bool relayed, - uint8_t version, uint8_t nic_verified_hf_version = 0); + uint8_t version, uint8_t nic_verified_hf_version = 0, + const crypto::hash &valid_input_verification_id = crypto::null_hash); /** * @brief add a transaction to the transaction pool @@ -135,6 +137,7 @@ namespace cryptonote * @param relayed was this transaction from the network or a local client? * @param version the version used to create the transaction * @param nic_verified_hf_version hard fork which "tx" is known to pass non-input consensus test + * @param valid_input_verification_id a previously valid verID if non-null * * If "nic_verified_hf_version" parameter is equal to "version" parameter, then we skip the * asserting `ver_non_input_consensus(tx)`, which greatly speeds up block popping and returning @@ -145,7 +148,8 @@ namespace cryptonote * @return true if the transaction passes validations, otherwise false */ bool add_tx(transaction &tx, tx_verification_context& tvc, relay_method tx_relay, bool relayed, - uint8_t version, uint8_t nic_verified_hf_version = 0); + uint8_t version, uint8_t nic_verified_hf_version = 0, + const crypto::hash &valid_input_verification_id = crypto::null_hash); /** * @brief takes a transaction with the given hash from the pool @@ -155,15 +159,26 @@ namespace cryptonote * @param txblob return-by-reference the transaction as a blob * @param tx_weight return-by-reference the transaction's weight * @param fee the transaction fee - * @param relayed return-by-reference was transaction relayed to us by the network? - * @param do_not_relay return-by-reference is transaction not to be relayed to the network? - * @param double_spend_seen return-by-reference was a double spend seen for that transaction? - * @param pruned return-by-reference is the tx pruned - * @param suppress_missing_msgs suppress warning msgs when txid is missing (optional, defaults to `false`) + * @param[out] valid_input_verification_id return-by-reference was a previously valid verID if non-null + * @param[out] relayed return-by-reference was transaction relayed to us by the network? + * @param[out] do_not_relay return-by-reference is transaction not to be relayed to the network? + * @param[out] double_spend_seen return-by-reference was a double spend seen for that transaction? + * @param[out] pruned return-by-reference is the tx pruned + * @param[out] suppress_missing_msgs suppress warning msgs when txid is missing (optional, defaults to `false`) * * @return true unless the transaction cannot be found in the pool */ - bool take_tx(const crypto::hash &id, transaction &tx, cryptonote::blobdata &txblob, size_t& tx_weight, uint64_t& fee, bool &relayed, bool &do_not_relay, bool &double_spend_seen, bool &pruned, bool suppress_missing_msgs = false); + bool take_tx(const crypto::hash &id, + transaction &tx, + cryptonote::blobdata &txblob, + size_t& tx_weight, + uint64_t& fee, + crypto::hash &valid_input_verification_id, + bool &relayed, + bool &do_not_relay, + bool &double_spend_seen, + bool &pruned, + bool suppress_missing_msgs = false); /** * @brief checks if the pool has a transaction with the given hash @@ -679,7 +694,13 @@ private: sorted_tx_container::iterator find_tx_in_sorted_container(const crypto::hash& id); //! cache/call Blockchain::check_tx_inputs results - bool check_tx_inputs(const std::function &get_tx, const crypto::hash &txid, uint64_t &max_used_block_height, crypto::hash &max_used_block_id, tx_verification_context &tvc, bool kept_by_block = false) const; + bool check_tx_inputs(const std::function &get_tx, + const crypto::hash &txid, + crypto::hash &valid_input_verification_id_inout, + uint64_t &max_used_block_height, + crypto::hash &max_used_block_id, + tx_verification_context &tvc, + bool kept_by_block = false) const; //! transactions which are unlikely to be included in blocks /*! These transactions are kept in RAM in case they *are* included diff --git a/src/cryptonote_core/tx_verification_utils.cpp b/src/cryptonote_core/tx_verification_utils.cpp index 38d74f084..406d018c0 100644 --- a/src/cryptonote_core/tx_verification_utils.cpp +++ b/src/cryptonote_core/tx_verification_utils.cpp @@ -28,6 +28,7 @@ #include +#include "common/threadpool.h" #include "cryptonote_core/blockchain.h" #include "cryptonote_core/cryptonote_core.h" #include "cryptonote_core/tx_verification_utils.h" @@ -35,7 +36,7 @@ #include "ringct/rctSigs.h" #undef MONERO_DEFAULT_LOG_CATEGORY -#define MONERO_DEFAULT_LOG_CATEGORY "blockchain" +#define MONERO_DEFAULT_LOG_CATEGORY "verify" #define VER_ASSERT(cond, msgexpr) CHECK_AND_ASSERT_MES(cond, false, msgexpr) @@ -83,32 +84,148 @@ static bool expand_tx_and_ver_rct_non_sem(transaction& tx, const rct::ctkeyM& mi } // Mix ring data is now known to be correctly incorporated into the RCT sig inside tx. - return rct::verRctNonSemanticsSimple(rv); + VER_ASSERT(rct::verRctNonSemanticsSimple(rv), "Failed to verify simple RingCT signatures"); + return true; } -// Create a unique identifier for pair of tx blob + mix ring -static crypto::hash calc_tx_mixring_hash(const transaction& tx, const rct::ctkeyM& mix_ring) +// Same as expand_tx_and_ver_rct_non_sem(), but for RingCT sigs of type RCTTypeFull only +static bool expand_tx_and_ver_full_rct_non_sem(transaction& tx, const rct::ctkeyM& mix_ring) { - std::stringstream ss; + // Pruned transactions can not be expanded and verified because they are missing RCT data + VER_ASSERT(!tx.pruned, "Pruned transaction will not pass verRctNonSemanticsSimple"); + VER_ASSERT(tx.rct_signatures.type == rct::RCTTypeFull, + "Non-full (simple) RingCT transaction will not pass rct::verRct"); - // Start with domain seperation - ss << config::HASH_KEY_TXHASH_AND_MIXRING; + // Calculate prefix hash + const crypto::hash tx_prefix_hash = get_transaction_prefix_hash(tx); - // Then add TX hash - const crypto::hash tx_hash = get_transaction_hash(tx); - ss.write(tx_hash.data, sizeof(crypto::hash)); + // Expand mixring, tx inputs, tx key images, prefix hash message, etc into the RCT sig + const bool exp_res = Blockchain::expand_transaction_2(tx, tx_prefix_hash, mix_ring); + VER_ASSERT(exp_res, "Failed to expand rct signatures!"); - // Then serialize mix ring - binary_archive ar(ss); - ::do_serialize(ar, const_cast(mix_ring)); + const rct::rctSig& rv = tx.rct_signatures; - // Calculate hash of TX hash and mix ring blob - crypto::hash tx_and_mixring_hash; - get_blob_hash(ss.str(), tx_and_mixring_hash); + // check all this, either reconstructed (so should really pass), or not + bool size_matches = true; + for (size_t i = 0; i < mix_ring.size(); ++i) + size_matches &= mix_ring[i].size() == rv.mixRing.size(); + for (size_t i = 0; i < rv.mixRing.size(); ++i) + size_matches &= mix_ring.size() == rv.mixRing[i].size(); + if (!size_matches) + { + MERROR("Failed to check ringct signatures: mismatched pubkeys/mixRing size"); + return false; + } - return tx_and_mixring_hash; + for (size_t n = 0; n < mix_ring.size(); ++n) + { + for (size_t m = 0; m < mix_ring[n].size(); ++m) + { + if (mix_ring[n][m].dest != rct::rct2pk(rv.mixRing[m][n].dest)) + { + MERROR("Failed to check ringct signatures: mismatched pubkey at vin " << n << ", index " << m); + return false; + } + if (mix_ring[n][m].mask != rct::rct2pk(rv.mixRing[m][n].mask)) + { + MERROR("Failed to check ringct signatures: mismatched commitment at vin " << n << ", index " << m); + return false; + } + } + } + + if (rv.p.MGs.size() != 1) + { + MERROR("Failed to check ringct signatures: Bad MGs size"); + return false; + } + if (rv.p.MGs.empty() || rv.p.MGs[0].II.size() != tx.vin.size()) + { + MERROR("Failed to check ringct signatures: mismatched II/vin sizes"); + return false; + } + for (size_t n = 0; n < tx.vin.size(); ++n) + { + if (memcmp(&boost::get(tx.vin[n]).k_image, &rv.p.MGs[0].II[n], 32)) + { + MERROR("Failed to check ringct signatures: mismatched II/vin sizes"); + return false; + } + } + + if (!rct::verRct(rv, false)) + { + MERROR("Failed to check ringct signatures!"); + return false; + } + + return true; } +static bool tx_ver_legacy_ring_sigs(transaction& tx, const rct::ctkeyM& mix_ring) +{ + VER_ASSERT(!tx.pruned, "Pruned transaction will not pass crypto::check_ring_signature"); + VER_ASSERT(tx.version == 1, "RingCT transaction will not pass crypto::check_ring_signature"); + + VER_ASSERT(tx.signatures.size() == mix_ring.size(), "Wrong number of v1 mix rings"); + + // This shape checks should be implied as part of serialization, but we re-check them here anyways + VER_ASSERT(tx.signatures.size() == tx.vin.size(), "Wrong number of v1 ring signatures"); + + // Calculate prefix hash + const crypto::hash tx_prefix_hash = get_transaction_prefix_hash(tx); + + // Define job to run one call of crypto::check_ring_signature() + std::atomic_flag fail_occurred{}; + const auto check_ring_signature_job = [&fail_occurred, &tx, &mix_ring, &tx_prefix_hash](const std::size_t input_idx) + { + const txin_to_key *pin = boost::get(&tx.vin[input_idx]); + if (pin == nullptr || pin->key_offsets.size() != tx.signatures[input_idx].size()) + { + MERROR("Transaction input is wrong type or ring member count mismatch"); + fail_occurred.test_and_set(); + return; + } + + std::vector p_output_keys; + p_output_keys.reserve(mix_ring.at(input_idx).size()); + for (const rct::ctkey &key : mix_ring.at(input_idx)) + { + // rct::key and crypto::public_key have the same structure, avoid object ctor/memcpy + p_output_keys.push_back(reinterpret_cast(&key.dest)); + } + + const bool ver = crypto::check_ring_signature(tx_prefix_hash, + pin->k_image, + p_output_keys, + tx.signatures.at(input_idx).data()); + if (!ver) + { + MERROR("Failed to check ring signature for tx " << get_transaction_hash(tx) << " vin key with k_image: " + << pin->k_image << " sig_index: " << input_idx); + fail_occurred.test_and_set(); + } + }; + + // Multi-thread calls to check_ring_signature_job() for each input if available, else iterate on ths thread + tools::threadpool& tpool = tools::threadpool::getInstanceForCompute(); + const int threads = tpool.get_max_concurrency(); + const bool multi_threaded = threads > 1; + std::unique_ptr waiter(multi_threaded ? new tools::threadpool::waiter(tpool) : nullptr); + for (std::size_t input_idx = 0; input_idx < tx.signatures.size(); ++input_idx) + { + if (waiter) + tpool.submit(waiter.get(), [&, input_idx](){ check_ring_signature_job(input_idx); }, true); + else + check_ring_signature_job(input_idx); + } + if (waiter && !waiter->wait()) + return false; + + return !fail_occurred.test_and_set(); // test_and_set() returns previously held value +} + + static bool is_canonical_bulletproof_layout(const std::vector &proofs) { if (proofs.size() != 1) @@ -207,13 +324,7 @@ uint64_t get_transaction_weight_limit(const uint8_t hf_version) return get_min_block_weight(hf_version) - CRYPTONOTE_COINBASE_BLOB_RESERVED_SIZE; } -bool ver_rct_non_semantics_simple_cached -( - transaction& tx, - const rct::ctkeyM& mix_ring, - rct_ver_cache_t& cache, - const std::uint8_t rct_type_to_cache -) +bool ver_input_proofs_rings(transaction& tx, const rct::ctkeyM &dereferenced_mix_ring) { // Hello future Monero dev! If you got this assert, read the following carefully: // @@ -223,42 +334,61 @@ bool ver_rct_non_semantics_simple_cached // representation of all "knobs" controlled by the possibly malicious constructor of the // transaction. Two, we take a hash of all *previously validated* blockchain data referenced by // this transaction which is required to validate the ring signature. In our case, this is the - // mixring. Future versions of the protocol may differ in this regard, but if this assumptions + // mix_ring. Future versions of the protocol may differ in this regard, but if this assumptions // holds true in the future, enable the verification hash by modifying the `untested_tx` // condition below. const bool untested_tx = tx.version > 2 || tx.rct_signatures.type > rct::RCTTypeBulletproofPlus; VER_ASSERT(!untested_tx, "Unknown TX type. Make sure RCT cache works correctly with this type and then enable it in the code here."); - // Don't cache older (or newer) rctSig types - // This cache only makes sense when it caches data from mempool first, - // so only "current fork version-enabled" RCT types need to be cached - if (tx.rct_signatures.type != rct_type_to_cache) + if (tx.version == 1) { - MDEBUG("RCT cache: tx " << get_transaction_hash(tx) << " skipped"); - return expand_tx_and_ver_rct_non_sem(tx, mix_ring); + return tx_ver_legacy_ring_sigs(tx, dereferenced_mix_ring); } - - // Generate unique hash for tx+mix_ring pair - const crypto::hash tx_mixring_hash = calc_tx_mixring_hash(tx, mix_ring); - - // Search cache for successful verification of same TX + mix ring combination - if (cache.has(tx_mixring_hash)) + else if (tx.version == 2) { - MDEBUG("RCT cache: tx " << get_transaction_hash(tx) << " hit"); - return true; + switch (tx.rct_signatures.type) + { + case rct::RCTTypeNull: + MERROR("Null RingCT does not have input proofs to verify"); + return false; + case rct::RCTTypeFull: + return expand_tx_and_ver_full_rct_non_sem(tx, dereferenced_mix_ring); + case rct::RCTTypeSimple: + case rct::RCTTypeBulletproof: + case rct::RCTTypeBulletproof2: + case rct::RCTTypeCLSAG: + case rct::RCTTypeBulletproofPlus: + return expand_tx_and_ver_rct_non_sem(tx, dereferenced_mix_ring); + default: + MERROR("Unrecognized RingCT type: " << tx.rct_signatures.type); + return false; + } } - - // We had a cache miss, so now we must expand the mix ring and do full verification - MDEBUG("RCT cache: tx " << get_transaction_hash(tx) << " missed"); - if (!expand_tx_and_ver_rct_non_sem(tx, mix_ring)) + else { + MERROR("Unrecognized transaction version: " << tx.version); return false; } +} - // At this point, the TX RCT verified successfully, so add it to the cache and return true - cache.add(tx_mixring_hash); +crypto::hash make_input_verification_id(const crypto::hash &tx_hash, const rct::ctkeyM &dereferenced_mix_ring) +{ + std::stringstream ss; - return true; + // Start with domain seperation + ss << config::HASH_KEY_TXHASH_AND_MIXRING; + + // Then add TX hash + ss.write(tx_hash.data, sizeof(crypto::hash)); + + // Then serialize mix ring + binary_archive ar(ss); + ::do_serialize(ar, const_cast(dereferenced_mix_ring)); + + // Calculate hash of TX hash and mix ring blob + crypto::hash input_verification_id; + get_blob_hash(ss.str(), input_verification_id); + return input_verification_id; } bool ver_mixed_rct_semantics(std::vector rvv) diff --git a/src/cryptonote_core/tx_verification_utils.h b/src/cryptonote_core/tx_verification_utils.h index 5e377dd1a..e27b21bf9 100644 --- a/src/cryptonote_core/tx_verification_utils.h +++ b/src/cryptonote_core/tx_verification_utils.h @@ -29,7 +29,6 @@ #pragma once #include "cryptonote_basic/blobdatatype.h" -#include "common/data_cache.h" #include "cryptonote_basic/cryptonote_basic.h" #include "cryptonote_basic/verification_context.h" @@ -44,46 +43,36 @@ namespace cryptonote */ uint64_t get_transaction_weight_limit(uint8_t hf_version); -// Modifying this value should not affect consensus. You can adjust it for performance needs -static constexpr const size_t RCT_VER_CACHE_SIZE = 8192; - -using rct_ver_cache_t = ::tools::data_cache<::crypto::hash, RCT_VER_CACHE_SIZE>; - /** - * @brief Cached version of rct::verRctNonSemanticsSimple + * @brief Tx-safe version of crypto::check_ring_signature() / rct::verRct(_, false) / rct::verRctNonSemanticsSimple() * * This function will not affect how the transaction is serialized and it will never modify the * transaction prefix. * - * The reference to tx is mutable since the transaction's ring signatures may be expanded by - * Blockchain::expand_transaction_2. However, on cache hits, the transaction will not be - * expanded. This means that the caller does not need to call expand_transaction_2 on this - * transaction before passing it; the transaction will not successfully verify with "old" RCT data - * if the transaction has been otherwise modified since the last verification. - * - * But, if cryptonote::get_transaction_hash(tx) returns a "stale" hash, this function is not - * guaranteed to work. So make sure that the cryptonote::transaction passed has not had - * modifications to it since the last time its hash was fetched without properly invalidating the - * hashes. - * - * rct_type_to_cache can be any RCT version value as long as rct::verRctNonSemanticsSimple works for - * this RCT version, but for most applications, it doesn't make sense to not make this version - * the "current" RCT version (i.e. the version that transactions in the mempool are). + * The reference to tx is mutable since the transaction's ring signatures will be expanded by + * Blockchain::expand_transaction_2. This means that the caller does not need to call + * expand_transaction_2 on this transaction before passing it; the transaction will not successfully + * verify with "old" mixring / misc RCT data if the transaction has been otherwise modified since + * the last verification. * * @param tx transaction which contains RCT signature to verify - * @param mix_ring mixring referenced by this tx. THIS DATA MUST BE PREVIOUSLY VALIDATED - * @param cache saves tx+mixring hashes used to cache calls - * @param rct_type_to_cache Only RCT sigs with version (e.g. RCTTypeBulletproofPlus) will be cached + * @param dereferenced_mixring mixring referenced by this tx. THIS DATA MUST BE PREVIOUSLY VALIDATED * @return true when verRctNonSemanticsSimple() w/ expanded tx.rct_signatures would return true * @return false when verRctNonSemanticsSimple() w/ expanded tx.rct_signatures would return false */ -bool ver_rct_non_semantics_simple_cached -( - transaction& tx, - const rct::ctkeyM& mix_ring, - rct_ver_cache_t& cache, - std::uint8_t rct_type_to_cache -); +bool ver_input_proofs_rings(transaction& tx, const rct::ctkeyM &dereferenced_mix_ring); + +/** + * @brief Make an ID for the parameters to ver_input_proofs_rings() for a transaction and its dereferenced chain data + * + * For any two calls to ver_input_proofs_rings() in any order, if the input verification ID for the + * (transaction, mixring) pair match, the result of ver_input_proofs_rings() will also match. + * + * If this transaction hash is "stale", this function is not guaranteed to work. So make sure that + * the cryptonote::transaction passed has not had modifications to it since the last time its hash + * was fetched without properly invalidating the hashes. + */ +crypto::hash make_input_verification_id(const crypto::hash &tx_hash, const rct::ctkeyM &dereferenced_mix_ring); /** * @brief Verify the semantics of a group of RingCT signatures as a batch (if applicable) diff --git a/tests/functional_tests/p2p.py b/tests/functional_tests/p2p.py index e4534275e..c662f04d9 100755 --- a/tests/functional_tests/p2p.py +++ b/tests/functional_tests/p2p.py @@ -37,6 +37,17 @@ import time from framework.daemon import Daemon from framework.wallet import Wallet +def average(a): + return sum(a) / len(a) + +def median(a): + a = sorted(a) + i0 = len(a)//2 + if len(a) % 2 == 0: + return average(a[i0-1:i0+1]) + else: + return a[i0] + class P2PTest(): def run_test(self): self.reset() @@ -47,6 +58,7 @@ class P2PTest(): self.test_p2p_block_propagation_shared(txid) txid = self.test_p2p_tx_propagation() self.test_p2p_block_propagation_new(txid) + self.bench_p2p_heavy_block_propagation_speed() def reset(self): print('Resetting blockchain') @@ -307,6 +319,140 @@ class P2PTest(): assert ('in_pool' not in tx_details) or (not tx_details.in_pool) assert tx_details.block_height == block_height + def bench_p2p_heavy_block_propagation_speed(self): + ENABLED = False + if not ENABLED: + print('SKIPPING benchmark of P2P heavy block propagation') + return + + print('Benchmarking P2P heavy block propagation') + + daemon2 = Daemon(idx = 2) + daemon3 = Daemon(idx = 3) + start_height = daemon2.get_height().height + current_height = start_height + daemon2_address = '{}:{}'.format(daemon2.host, daemon2.port) + + print(' Setup: creating new wallet') + wallet = Wallet() + try: wallet.close_wallet() + except: pass + wallet.create_wallet() + wallet.auto_refresh(enable = False) + wallet.set_daemon(daemon2_address) + assert wallet.get_transfers() == {} + main_address = wallet.get_address().address + + CURRENT_RING_SIZE = 16 + min_height = CURRENT_RING_SIZE + 1 + 60 + if start_height < min_height: + print(' Setup: mining to mixable RingCT height: {}'.format(min_height)) + n_to_mine = min_height - start_height + daemon2.generateblocks(main_address, n_to_mine) + current_height += n_to_mine + + print(' Setup: spamming self-send transactions into mempool to increase size') + + update_unlocked_inputs = lambda: \ + [x for x in wallet.incoming_transfers().get('transfers', []) if not x.spent + and x.unlocked + and x.amount > 2 * last_fee] + + MAX_TX_OUTPUTS = 16 + MEMPOOL_TX_TARGET = 2 * 8192 # 2x previous ver_rct_non_semantics_simple_cached() cache size + n_mempool_txs = 0 + unlocked_inputs = update_unlocked_inputs() + last_fee = 10000000000 # 0.01 XMR to start off with is an over-estimation for a 1/16 in the penalty-free zone + + print_progress = lambda action: \ + print(' Progress: {}/{} ({:.1f}%) txs in mempool, {} usable inputs, {} blocks mined, just {} {}'.format( + n_mempool_txs, MEMPOOL_TX_TARGET, n_mempool_txs/MEMPOOL_TX_TARGET*100, len(unlocked_inputs), + current_height - start_height, action, ' ' * 10 + ), end='\r') + print_progress('started') + + while n_mempool_txs < MEMPOOL_TX_TARGET: + try: + if len(unlocked_inputs) == 0: + daemon2.generateblocks(main_address, 1) + wallet.refresh() + current_height += 1 + wallet_height = wallet.get_height().height + assert wallet_height == current_height, wallet_height + n_mempool_txs = len(daemon2.get_transaction_pool_hashes().get('tx_hashes', [])) + res = wallet.incoming_transfers() + unlocked_inputs = update_unlocked_inputs() + last_action = 'mined' + else: + inp = unlocked_inputs.pop() + res = wallet.sweep_single(main_address, outputs = MAX_TX_OUTPUTS - 1, key_image = inp.key_image) + assert res.spent_key_images.key_images == [inp.key_image] + last_fee = res.fee + n_mempool_txs += 1 + last_action = 'swept' + except AssertionError as ae: + print() # Clear carriage return + if 'Transaction sanity check failed' not in str(ae): + raise + # The RingCT output distribution gets so skewed in this test that the wallet + # thinks something is wrong with decoy selection. To recover, try mining a block on + # the next action. + print(' WARNING: caught transaction sanity check, stepping forward chain to try to fix') + unlocked_inputs = [] + last_action = 'caught sanity' + + print_progress(last_action) + + print() + print(' Setup: wait for daemons to reach equilibrium on mempool contents') + + assert n_mempool_txs > 0 + + sync_start = time.time() + sync_deadline = sync_start + 120 + while True: + mempool_hashes_2 = daemon2.get_transaction_pool_hashes().get('tx_hashes', []) + assert len(mempool_hashes_2) == n_mempool_txs # Txs were submitted to daemon 2 + mempool_hashes_3 = daemon3.get_transaction_pool_hashes().get('tx_hashes', []) + print(' {}/{} mempool txs propagated'.format(len(mempool_hashes_2), len(mempool_hashes_3)), end='\r') + if sorted(mempool_hashes_2) == sorted(mempool_hashes_3): + break + elif time.time() > sync_deadline: + raise RuntimeError('daemons did not sync mempools within deadline') + time.sleep(0.25) + + print() + print(' Bench: mine and propagate blocks until mempool is empty of profitable txs') + + timings = [] + while True: + time1 = time.time() + daemon2.generateblocks(main_address, 1) + current_height += 1 + time2 = time.time() + while daemon3.get_height().height != current_height: + time.sleep(0.01) + time3 = time.time() + new_n_mempool_txs = len(daemon2.get_transaction_pool_hashes().get('tx_hashes', [])) + assert new_n_mempool_txs <= n_mempool_txs + n_mined_txs = n_mempool_txs - new_n_mempool_txs + elapsed_mining = time2 - time1 + elapsed_prop = time3 - time2 + timings.append((n_mined_txs, elapsed_mining, elapsed_prop)) + n_mempool_txs = new_n_mempool_txs + print(' * Mined {} txs in {:.2f}s, propagated in {:.2f}s'.format(*(timings[-1]))) + if n_mempool_txs == 0 or n_mined_txs == 0: + break + + print(' Analysis of {}-tx mempool handling:'.format(MEMPOOL_TX_TARGET)) + avg_mining = average([x[1] for x in timings]) + median_mining = median([x[1] for x in timings]) + avg_prop = average([x[2] for x in timings]) + median_prop = median([x[2] for x in timings]) + print(' Average mining time: {:.2f}'.format(avg_mining)) + print(' Median mining time: {:.2f}'.format(median_mining)) + print(' Average block propagation time: {:.2f}'.format(avg_prop)) + print(' Median block propagation time: {:.2f}'.format(median_prop)) if __name__ == '__main__': P2PTest().run_test() diff --git a/tests/unit_tests/CMakeLists.txt b/tests/unit_tests/CMakeLists.txt index a25ea073c..44b487e4b 100644 --- a/tests/unit_tests/CMakeLists.txt +++ b/tests/unit_tests/CMakeLists.txt @@ -92,7 +92,7 @@ set(unit_tests_sources uri.cpp util.cpp varint.cpp - ver_rct_non_semantics_simple_cached.cpp + verRctNonSemanticsSimple.cpp ringct.cpp output_selection.cpp vercmp.cpp diff --git a/tests/unit_tests/tx_verification_utils.cpp b/tests/unit_tests/tx_verification_utils.cpp new file mode 100644 index 000000000..f7a7ae191 --- /dev/null +++ b/tests/unit_tests/tx_verification_utils.cpp @@ -0,0 +1,269 @@ +// Copyright (c) 2025, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "gtest/gtest.h" + +#include "cryptonote_core/cryptonote_tx_utils.h" +#include "cryptonote_core/tx_verification_utils.h" + +TEST(tx_verification_utils, make_input_verification_id) +{ + rct::key key1, key2, key3; + epee::from_hex::to_buffer(epee::as_mut_byte_span(key1), "e50f476129d40af31e0938743f7f2d60e867aab31294f7acaf6e38f0976f0228"); + epee::from_hex::to_buffer(epee::as_mut_byte_span(key2), "e50f476129d40af31e0938743f7f2d60e867aab31294f7acaf6e38f0976f0227"); + epee::from_hex::to_buffer(epee::as_mut_byte_span(key3), "d50f476129d40af31e0938743f7f2d60e867aab31294f7acaf6e38f0976f0228"); + + const crypto::hash hash1 = cryptonote::make_input_verification_id(rct::rct2hash(key1), {}); + const crypto::hash hash2 = cryptonote::make_input_verification_id(rct::rct2hash(key2), {}); + const crypto::hash hash3 = cryptonote::make_input_verification_id(rct::rct2hash(key3), {}); + ASSERT_NE(hash1, hash2); + ASSERT_NE(hash1, hash3); + ASSERT_NE(hash2, hash3); + + const crypto::hash hash4 = cryptonote::make_input_verification_id(rct::rct2hash(key1), {{{key1, key1}}}); + const crypto::hash hash5 = cryptonote::make_input_verification_id(rct::rct2hash(key1), {{{key1, key2}}}); + const crypto::hash hash6 = cryptonote::make_input_verification_id(rct::rct2hash(key1), {{{key1, key3}}}); + ASSERT_NE(hash4, hash5); + ASSERT_NE(hash4, hash6); + ASSERT_NE(hash5, hash6); + + const crypto::hash hash7 = cryptonote::make_input_verification_id(rct::rct2hash(key1), {{{key1, key1},{key1, key1}}}); + const crypto::hash hash8 = cryptonote::make_input_verification_id(rct::rct2hash(key1), {{{key1, key1}},{{key1, key1}}}); + ASSERT_NE(hash7, hash8); + + const crypto::hash hash1_eq = cryptonote::make_input_verification_id(rct::rct2hash(key1), {}); + const crypto::hash hash2_eq = cryptonote::make_input_verification_id(rct::rct2hash(key2), {}); + const crypto::hash hash3_eq = cryptonote::make_input_verification_id(rct::rct2hash(key3), {}); + const crypto::hash hash4_eq = cryptonote::make_input_verification_id(rct::rct2hash(key1), {{{key1, key1}}}); + const crypto::hash hash5_eq = cryptonote::make_input_verification_id(rct::rct2hash(key1), {{{key1, key2}}}); + const crypto::hash hash6_eq = cryptonote::make_input_verification_id(rct::rct2hash(key1), {{{key1, key3}}}); + const crypto::hash hash7_eq = cryptonote::make_input_verification_id(rct::rct2hash(key1), {{{key1, key1},{key1, key1}}}); + const crypto::hash hash8_eq = cryptonote::make_input_verification_id(rct::rct2hash(key1), {{{key1, key1}},{{key1, key1}}}); + + ASSERT_EQ(hash1, hash1_eq); + ASSERT_EQ(hash2, hash2_eq); + ASSERT_EQ(hash3, hash3_eq); + ASSERT_EQ(hash4, hash4_eq); + ASSERT_EQ(hash5, hash5_eq); + ASSERT_EQ(hash6, hash6_eq); + ASSERT_EQ(hash7, hash7_eq); + ASSERT_EQ(hash8, hash8_eq); +} + +TEST(tx_verification_utils, ver_input_proofs_rings) +{ + // constants + static constexpr size_t N_INPUTS = 2; + static constexpr size_t N_OUTPUTS = 10; + static constexpr size_t N_RING_MEMBERS = 16; + static constexpr bool USE_VIEW_TAGS = true; + static constexpr rct::RCTConfig RCT_CONFIG{ rct::RangeProofPaddedBulletproof, 4 }; // CLSAG, BP+ + static constexpr uint8_t HF_VERSION = HF_VERSION_VIEW_TAGS + 1; // CLSAG, BP+, after grace period + + // generate accounts + hw::device &hwdev = hw::get_device("default"); + + cryptonote::account_base alice; + alice.generate(); + const cryptonote::account_public_address &alice_main_addr = alice.get_keys().m_account_address; + const std::unordered_map alice_subaddresses{ + {alice_main_addr.m_spend_public_key, {}} + }; + + cryptonote::account_base bob; + bob.generate(); + const cryptonote::account_public_address &bob_main_addr = bob.get_keys().m_account_address; + + cryptonote::account_base aether; + aether.generate(); + + // populate inputs + rct::xmr_amount total_input_amounts = 0; + std::vector sources; + sources.reserve(N_INPUTS); + for (size_t i = 0; i < N_INPUTS; ++i) + { + const rct::xmr_amount in_amount = crypto::rand_range(0, COIN) + COIN; // [1, 2] XMR + const size_t real_in_ring_idx = crypto::rand_idx(N_RING_MEMBERS); + + // generate one-time address from derivation + crypto::secret_key in_main_tx_privkey; + crypto::public_key in_main_tx_pubkey; + crypto::generate_keys(in_main_tx_pubkey, in_main_tx_privkey); // (r, R) + crypto::secret_key_to_public_key(in_main_tx_privkey, in_main_tx_pubkey); + crypto::key_derivation ecdh; + ASSERT_TRUE(hwdev.generate_key_derivation(in_main_tx_pubkey, alice.get_keys().m_view_secret_key, ecdh)); + const size_t real_output_in_tx_index = crypto::rand_idx(N_OUTPUTS); + + crypto::public_key in_onetime_address; + crypto::view_tag in_view_tag; + std::vector in_additional_tx_public_keys; + std::vector in_amount_keys; + ASSERT_TRUE(hwdev.generate_output_ephemeral_keys(/*tx_version=*/2, + aether.get_keys(), in_main_tx_pubkey, in_main_tx_privkey, + {0, alice_main_addr, false}, /*change_addr=*/boost::none, real_output_in_tx_index, + /*need_additional_txkeys=*/false, /*additional_tx_keys=*/{}, + in_additional_tx_public_keys, + in_amount_keys, in_onetime_address, + USE_VIEW_TAGS, in_view_tag)); + ASSERT_EQ(1, in_amount_keys.size()); + + const rct::key in_amount_blinding_factor = rct::genCommitmentMask(in_amount_keys.at(0)); + const rct::key in_amount_commitment = rct::commit(in_amount, in_amount_blinding_factor); + + // randomly populate decoys and insert real spend + auto &tx_source = sources.emplace_back(); + tx_source.outputs.reserve(N_RING_MEMBERS); + for (size_t j = 0; j < N_RING_MEMBERS; ++j) + { + const size_t ring_member_global_output_idx = 20 * j; + if (j == real_in_ring_idx) + { + tx_source.outputs.emplace_back(ring_member_global_output_idx, + rct::ctkey{rct::pk2rct(in_onetime_address), in_amount_commitment}); + } + else // decoy + { + tx_source.outputs.emplace_back(ring_member_global_output_idx, + rct::ctkey{rct::pkGen(), rct::pkGen()}); + } + } + + tx_source.real_output = real_in_ring_idx; + tx_source.real_out_tx_key = in_main_tx_pubkey; + tx_source.real_out_additional_tx_keys = in_additional_tx_public_keys; + tx_source.real_output_in_tx_index = real_output_in_tx_index; + tx_source.amount = in_amount; + tx_source.rct = true; + tx_source.mask = in_amount_blinding_factor; + tx_source.multisig_kLRki = {}; + + total_input_amounts += in_amount; + } + + // populate destinations + const rct::xmr_amount approx_fee = 500000000000; // 0.5 XMR + const rct::xmr_amount dest_amount = (total_input_amounts - approx_fee) / N_OUTPUTS; + std::vector destinations; + destinations.reserve(N_OUTPUTS); + for (size_t i = 0; i < N_OUTPUTS - 1; ++i) + destinations.push_back(cryptonote::tx_destination_entry(dest_amount, bob_main_addr, false)); + destinations.push_back(cryptonote::tx_destination_entry(dest_amount, alice_main_addr, false)); + + // construct transaction + cryptonote::transaction tx; + crypto::secret_key main_tx_key; + std::vector additional_tx_keys; + ASSERT_TRUE(cryptonote::construct_tx_and_get_tx_key(alice.get_keys(), + alice_subaddresses, + sources, + destinations, + alice_main_addr, + /*extra=*/{}, + tx, + main_tx_key, + additional_tx_keys, + /*rct=*/true, + RCT_CONFIG, + USE_VIEW_TAGS)); + ASSERT_EQ(N_INPUTS, tx.vin.size()); + ASSERT_EQ(N_OUTPUTS, tx.vout.size()); + ASSERT_EQ(N_RING_MEMBERS, boost::get(tx.vin.at(0)).key_offsets.size()); + ASSERT_GE(tx.rct_signatures.txnFee, approx_fee); + ASSERT_LE(tx.rct_signatures.txnFee, approx_fee + N_OUTPUTS); + + // collect mix rings + rct::ctkeyM mixrings(N_INPUTS); + for (size_t i = 0; i < N_INPUTS; ++i) + { + mixrings.at(i).resize(N_RING_MEMBERS); + for (size_t j = 0; j < N_RING_MEMBERS; ++j) + { + mixrings.at(i).at(j) = sources.at(i).outputs.at(j).second; + } + } + + // serialize transaction to blob + const cryptonote::blobdata tx_blob = cryptonote::tx_to_blob(tx); + + // de-serialize transaction from blob + cryptonote::transaction deserialized_tx; + ASSERT_TRUE(cryptonote::parse_and_validate_tx_from_blob(tx_blob, deserialized_tx)); + + // test non-input consensus rules + cryptonote::tx_verification_context tvc{}; + ASSERT_TRUE(cryptonote::ver_non_input_consensus(deserialized_tx, tvc, HF_VERSION)); + ASSERT_FALSE(tvc.m_verifivation_failed); + + // test verify input rings [positive] + EXPECT_TRUE(cryptonote::ver_input_proofs_rings(deserialized_tx, mixrings)); + + // test verify input rings again (already expanded) [positive] + EXPECT_TRUE(cryptonote::ver_input_proofs_rings(deserialized_tx, mixrings)); + + // test verify input rings after modify to expansion [positive] + deserialized_tx.rct_signatures.mixRing.at(0).at(0) = {rct::pkGen(), rct::pkGen()}; + EXPECT_TRUE(cryptonote::ver_input_proofs_rings(deserialized_tx, mixrings)); + + // test verify input rings after modify to dereferenced mixring [negative] + rct::ctkeyM modified_mixrings = mixrings; + modified_mixrings.at(0).at(0) = {rct::pkGen(), rct::pkGen()}; + EXPECT_FALSE(cryptonote::ver_input_proofs_rings(deserialized_tx, modified_mixrings)); + modified_mixrings = mixrings; + modified_mixrings.at(0).at(1) = {rct::pkGen(), rct::pkGen()}; + EXPECT_FALSE(cryptonote::ver_input_proofs_rings(deserialized_tx, modified_mixrings)); + + // test verify input rings after add dereferenced mixring [negative] + modified_mixrings = mixrings; + modified_mixrings.emplace_back(); + EXPECT_FALSE(cryptonote::ver_input_proofs_rings(deserialized_tx, modified_mixrings)); + + // test verify input rings after remove dereferenced mixring [negative] + modified_mixrings = mixrings; + modified_mixrings.pop_back(); + EXPECT_FALSE(cryptonote::ver_input_proofs_rings(deserialized_tx, modified_mixrings)); + + // test verify input rings after add dereferenced decoy [negative] + modified_mixrings = mixrings; + modified_mixrings.at(0).push_back({rct::pkGen(), rct::pkGen()}); + EXPECT_FALSE(cryptonote::ver_input_proofs_rings(deserialized_tx, modified_mixrings)); + + // test verify input rings after remove dereferenced decoy [negative] + { + modified_mixrings = mixrings; + rct::ctkeyV &mixring0 = modified_mixrings.at(0); + mixring0.erase(mixring0.begin()); + EXPECT_FALSE(cryptonote::ver_input_proofs_rings(deserialized_tx, modified_mixrings)); + } + { + modified_mixrings = mixrings; + rct::ctkeyV &mixring0 = modified_mixrings.at(0); + mixring0.erase(mixring0.begin() + 1); + EXPECT_FALSE(cryptonote::ver_input_proofs_rings(deserialized_tx, modified_mixrings)); + } +} diff --git a/tests/unit_tests/ver_rct_non_semantics_simple_cached.cpp b/tests/unit_tests/verRctNonSemanticsSimple.cpp similarity index 98% rename from tests/unit_tests/ver_rct_non_semantics_simple_cached.cpp rename to tests/unit_tests/verRctNonSemanticsSimple.cpp index df542bdc0..c20b6ddf5 100644 --- a/tests/unit_tests/ver_rct_non_semantics_simple_cached.cpp +++ b/tests/unit_tests/verRctNonSemanticsSimple.cpp @@ -243,8 +243,6 @@ TEST(verRctNonSemanticsSimple, tx1_preconditions) // If this unit test fails, something changed about transaction deserialization / expansion or // something changed about RingCT signature verification. - cryptonote::rct_ver_cache_t rct_ver_cache; - cryptonote::transaction tx = expand_transaction_from_bin_file_and_pubkeys (tx1_file_name, tx1_input_pubkeys); const rct::rctSig& rs = tx.rct_signatures; @@ -274,8 +272,8 @@ TEST(verRctNonSemanticsSimple, tx1_preconditions) EXPECT_TRUE(rct::verRctSemanticsSimple(rs)); EXPECT_TRUE(rct::verRctNonSemanticsSimple(rs)); EXPECT_TRUE(rct::verRctSimple(rs)); - EXPECT_TRUE(cryptonote::ver_rct_non_semantics_simple_cached(tx, tx1_input_pubkeys, rct_ver_cache, rct::RCTTypeBulletproofPlus)); - EXPECT_TRUE(cryptonote::ver_rct_non_semantics_simple_cached(tx, tx1_input_pubkeys, rct_ver_cache, rct::RCTTypeBulletproofPlus)); + EXPECT_TRUE(cryptonote::ver_input_proofs_rings(tx, tx1_input_pubkeys)); + EXPECT_TRUE(cryptonote::ver_input_proofs_rings(tx, tx1_input_pubkeys)); } #define SERIALIZABLE_SIG_CHANGES_SUBTEST(fieldmodifyclause) \ diff --git a/utils/python-rpc/framework/daemon.py b/utils/python-rpc/framework/daemon.py index 917e68c8e..71aa0aeb5 100644 --- a/utils/python-rpc/framework/daemon.py +++ b/utils/python-rpc/framework/daemon.py @@ -36,8 +36,8 @@ class Daemon(object): def __init__(self, protocol='http', host='127.0.0.1', port=0, idx=0, restricted_rpc = False, username=None, password=None): base = 18480 if restricted_rpc else 18180 self.host = host - self.port = port - self.rpc = JSONRPC('{protocol}://{host}:{port}'.format(protocol=protocol, host=host, port=port if port else base+idx), + self.port = port if port else base+idx + self.rpc = JSONRPC('{protocol}://{host}:{port}'.format(protocol=protocol, host=host, port=self.port), username, password) def getblocktemplate(self, address, prev_block = "", client = ""): From 022fb8e3906abe54b53753e01054faeaaa85d5c5 Mon Sep 17 00:00:00 2001 From: jeffro256 Date: Thu, 16 Oct 2025 11:18:45 -0500 Subject: [PATCH 2/2] unit_tests: @j-berman unit tests for #10157 Co-authored-by: j-berman --- tests/unit_tests/CMakeLists.txt | 1 + tests/unit_tests/threadpool.cpp | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/tests/unit_tests/CMakeLists.txt b/tests/unit_tests/CMakeLists.txt index 44b487e4b..8298cc1ca 100644 --- a/tests/unit_tests/CMakeLists.txt +++ b/tests/unit_tests/CMakeLists.txt @@ -87,6 +87,7 @@ set(unit_tests_sources test_protocol_pack.cpp threadpool.cpp tx_proof.cpp + tx_verification_utils.cpp hardfork.cpp unbound.cpp uri.cpp diff --git a/tests/unit_tests/threadpool.cpp b/tests/unit_tests/threadpool.cpp index d89f16167..49bec616f 100644 --- a/tests/unit_tests/threadpool.cpp +++ b/tests/unit_tests/threadpool.cpp @@ -145,3 +145,35 @@ TEST(threadpool, leaf_reentrancy) waiter.wait(); ASSERT_EQ(counter, 500000); } + +static bool check_test_and_set(const std::size_t n, const std::function &fail_condition) +{ + std::shared_ptr tpool(tools::threadpool::getNewForUnitTests(std::max(n, 1))); + tools::threadpool::waiter waiter(*tpool); + + std::atomic_flag fail_occurred{}; + for (std::size_t i = 0; i < n; ++i) + { + tpool->submit(&waiter, [&, i](){ if (fail_condition(i)) { fail_occurred.test_and_set(); }}, true); + } + + return waiter.wait() && !fail_occurred.test_and_set(); +} + +TEST(threadpool, test_and_set) +{ + // No failure + ASSERT_TRUE(check_test_and_set(0, [](std::size_t i) -> bool { return false; })); + static const std::size_t N = 4; + ASSERT_TRUE(check_test_and_set(N, [](std::size_t i) -> bool { return false; })); + + // 1 failure + ASSERT_FALSE(check_test_and_set(N, [](std::size_t i) -> bool { return i == 0; })); + ASSERT_FALSE(check_test_and_set(N, [](std::size_t i) -> bool { return i == 1; })); + ASSERT_FALSE(check_test_and_set(N, [](std::size_t i) -> bool { return i == 2; })); + ASSERT_FALSE(check_test_and_set(N, [](std::size_t i) -> bool { return i == 3; })); + + // Multiple failures + ASSERT_FALSE(check_test_and_set(N, [](std::size_t i) -> bool { return i > 0; })); + ASSERT_FALSE(check_test_and_set(N, [](std::size_t i) -> bool { return true; })); +}