cryptonote_core: cache input verification results directly in mempool
What changed, and why it matters
This commit is a performance refactor of how Monero nodes cache expensive ring-signature verification results for transactions sitting in the memory pool (mempool). It replaces an in-memory cache limited to 8,192 entries with a persistent verification ID stored in each mempool transaction's metadata. The change is described by the author as improving block propagation and reorg handling, not as a security fix. The main risk is that if the verification ID is ever set incorrectly or reused with the wrong mix-ring data, a node could skip cryptographic checks on a bad transaction, which would be a consensus bug. The diff includes careful checks and unit tests, but it is a large, complex change touching core consensus code.
Treat this as a high-risk consensus refactor rather than an active vulnerability. Reviewers should verify that `make_input_verification_id()` uniquely identifies the exact mix-ring data used in verification, that the ID is never copied from an untrusted source, that `check_tx_inputs()` cannot return true with a stale or mismatched ID, and that the padding/alignment changes in `txpool_tx_meta_t` do not break existing database records. Run the new unit tests and, if possible, the disabled heavy-block benchmark on a testnet.
Security signals we found
Replaces in-memory verification cache with persistent mempool metadata field
Introduces `valid_input_verification_id` in `txpool_tx_meta_t` using previously reserved padding
Adds `make_input_verification_id()` hashing tx hash + dereferenced mix ring with domain separation
Adds `ver_input_proofs_rings()` covering v1 ring signatures, RCTTypeFull, and simple/RCTTypeBulletproof/CLSAG/BulletproofPlus
Skips cryptographic verification when stored ID matches recomputed ID
Sets ID only after successful verification; resets to null on failure
Removes threaded v1 ring-signature checking from `blockchain.cpp` and moves it into `tx_verification_utils.cpp`
Adds unit tests for ID uniqueness and tamper detection
Adds disabled benchmark for heavy block propagation under large mempool
Evidence from the diff
The patch removes ver_rct_non_semantics_simple_cached() and its 8,192-entry m_rct_ver_cache from cryptonote::Blockchain. In its place it introduces ver_input_proofs_rings() and make_input_verification_id(). The verification ID is a hash of (tx_hash, dereferenced_mix_ring) and is stored in txpool_tx_meta_t::valid_input_verification_id. When a transaction is added to the mempool or returned during a reorg, the ID can be passed in. Later, Blockchain::check_tx_inputs() recomputes the ID from the current chain state; if it matches the stored ID, the CPU-heavy ring-signature/RCT verification is skipped. The ID is only set after a successful verification, and is reset on failure. The metadata struct size is kept at 192 bytes by shrinking padding, avoiding a DB migration. Extensive unit tests verify ID uniqueness and that tampering with mix rings or signatures causes verification failure.
Changed components
src/cryptonote_core/blockchain.cppsrc/cryptonote_core/blockchain.hsrc/cryptonote_core/tx_pool.cppsrc/cryptonote_core/tx_pool.hsrc/cryptonote_core/tx_verification_utils.cppsrc/cryptonote_core/tx_verification_utils.hsrc/blockchain_db/blockchain_db.htests/unit_tests/tx_verification_utils.cpptests/unit_tests/verRctNonSemanticsSimple.cpptests/functional_tests/p2p.pyutils/python-rpc/framework/daemon.pyInspect captured patch +1245 / −729
diff --git a/src/blockchain_db/blockchain_db.h b/src/blockchain_db/blockchain_db.h
index 5bcc63d..42591dc 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 171087d..65fdd7b 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_to_key>(&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<u
// This function overloads its sister function with
// an extra value (hash of highest block that holds an output used as input)
// as a return-by-reference.
-bool Blockchain::check_tx_inputs(transaction& tx, uint64_t& max_used_block_height, crypto::hash& max_used_block_id, tx_verification_context &tvc, bool kept_by_block) const
+bool Blockchain::check_tx_inputs(transaction& tx,
+ uint64_t& max_used_block_height,
+ crypto::hash& max_used_block_id,
+ tx_verification_context &tvc,
+ crypto::hash &valid_input_verification_id_inout,
+ bool kept_by_block) const
{
LOG_PRINT_L3("Blockchain::" << __func__);
CRITICAL_REGION_LOCAL(m_blockchain_lock);
@@ -2974,7 +3029,7 @@ bool Blockchain::check_tx_inputs(transaction& tx, uint64_t& max_used_block_heigh
#endif
TIME_MEASURE_START(a);
- bool res = check_tx_inputs(tx, tvc, &max_used_block_height);
+ bool res = check_tx_inputs(tx, tvc, valid_input_verification_id_inout, &max_used_block_height);
TIME_MEASURE_FINISH(a);
if(m_show_time_stats)
{
@@ -3244,7 +3299,10 @@ bool Blockchain::expand_transaction_2(transaction &tx, const crypto::hash &tx_pr
// check_tx_input() rather than here, and use this function simply
// to iterate the inputs as necessary (splitting the task
// using threads, etc.)
-bool Blockchain::check_tx_inputs(transaction& tx, tx_verification_context &tvc, uint64_t* pmax_used_block_height) const
+bool Blockchain::check_tx_inputs(transaction& tx,
+ tx_verification_context &tvc,
+ crypto::hash &valid_input_verification_id_inout,
+ uint64_t* pmax_used_block_height) const
{
PERF_TIMER(check_tx_inputs);
LOG_PRINT_L3("Blockchain::" << __func__);
@@ -3386,12 +3444,6 @@ bool Blockchain::check_tx_inputs(transaction& tx, tx_verification_context &tvc,
}
std::vector<std::vector<rct::ctkey>> 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<txin_to_key>(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<rct::ctkey> &pubkeys, const std::vector<crypto::signature>& sig, uint64_t &result) const
-{
- std::vector<const crypto::public_key *> 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<crypto::hash> &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<std::pair<transaction, blobdata>> txs;
- // txid weight mempool?
- std::vector<std::tuple<crypto::hash, size_t, bool>> txs_meta;
+ // txid weight mempool? verID
+ std::vector<std::tuple<crypto::hash, size_t, bool, crypto::hash>> txs_meta;
// This will be the data sent to the ZMQ pool listeners for txs which skipped the mempool
std::vector<txpool_event> 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<transaction> 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<crypto::hash> 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 03a0d21..ee6e888 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
- */
- 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;
+ *
+ * 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,
+ 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<block> *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<rct::ctkey> &pubkeys, const std::vector<crypto::signature> &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 c63465e..8b3117b 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<cryptonote::transaction&(void)> &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<cryptonote::transaction&(void)> &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 1dbfd84..88afc32 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<cryptonote::transaction&(void)> &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<cryptonote::transaction&(void)> &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 38d74f0..406d018 100644
--- a/src/cryptonote_core/tx_verification_utils.cpp
+++ b/src/cryptonote_core/tx_verification_utils.cpp
@@ -28,6 +28,7 @@
#include <boost/iterator/transform_iterator.hpp>
+#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<true> ar(ss);
- ::do_serialize(ar, const_cast<rct::ctkeyM&>(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;
+ }
+
+ 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<txin_to_key>(tx.vin[n]).k_image, &rv.p.MGs[0].II[n], 32))
+ {
+ MERROR("Failed to check ringct signatures: mismatched II/vin sizes");
+ return false;
+ }
+ }
- return tx_and_mixring_hash;
+ 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<txin_to_key>(&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<const crypto::public_key *> 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<const crypto::public_key*>(&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<tools::threadpool::waiter> 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<rct::Bulletproof> &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<true> ar(ss);
+ ::do_serialize(ar, const_cast<rct::ctkeyM&>(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<const rct::rctSig*> rvv)
diff --git a/src/cryptonote_core/tx_verification_utils.h b/src/cryptonote_core/tx_verification_utils.h
index 5e377dd..e27b21b 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 e453427..c662f04 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 a25ea07..44b487e 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 0000000..f7a7ae1
--- /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<crypto::public_key, cryptonote::subaddress_index> 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<cryptonote::tx_source_entry> sources;
+ sources.reserve(N_INPUTS);
+ for (size_t i = 0; i < N_INPUTS; ++i)
+ {
+ const rct::xmr_amount in_amount = crypto::rand_range<rct::xmr_amount>(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<crypto::public_key> in_additional_tx_public_keys;
+ std::vector<rct::key> 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<cryptonote::tx_destination_entry> 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<crypto::secret_key> 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<cryptonote::txin_to_key>(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/verRctNonSemanticsSimple.cpp b/tests/unit_tests/verRctNonSemanticsSimple.cpp
new file mode 100644
index 0000000..c20b6dd
--- /dev/null
+++ b/tests/unit_tests/verRctNonSemanticsSimple.cpp
@@ -0,0 +1,424 @@
+// Copyright (c) 2023-2024, 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 <sstream>
+
+#define IN_UNIT_TESTS // To access Blockchain::{expand_transaction_2, verRctNonSemanticsSimpleCached}
+
+#include "gtest/gtest.h"
+#include "unit_tests_utils.h"
+
+#include "cryptonote_basic/cryptonote_format_utils.h"
+#include "cryptonote_core/blockchain.h"
+#include "file_io_utils.h"
+#include "misc_log_ex.h"
+#include "ringct/rctSigs.h"
+
+namespace cryptonote
+{
+// declaration not provided in cryptonote_format_utils.h, but definition is not static ;)
+bool expand_transaction_1(transaction &tx, bool base_only);
+}
+
+namespace
+{
+/**
+ * @brief Make rct::ctkey from hex string representation of destionation and mask
+ *
+ * @param dest_hex
+ * @param mask_hex
+ * @return rct::ctkey
+ */
+static rct::ctkey make_ctkey(const char* dest_hex, const char* mask_hex)
+{
+ rct::key dest;
+ rct::key mask;
+ CHECK_AND_ASSERT_THROW_MES(epee::from_hex::to_buffer(epee::as_mut_byte_span(dest), dest_hex), "dest bad hex: " << dest_hex);
+ CHECK_AND_ASSERT_THROW_MES(epee::from_hex::to_buffer(epee::as_mut_byte_span(mask), mask_hex), "mask bad hex: " << mask_hex);
+ return {dest, mask};
+}
+
+template <typename T>
+static std::string stringify_with_do_serialize(const T& t)
+{
+ std::stringstream ss;
+ binary_archive<true> ar(ss);
+ CHECK_AND_ASSERT_THROW_MES(ar.good(), "Archiver is not in a good state. This shouldn't happen!");
+ ::do_serialize(ar, const_cast<T&>(t));
+ return ss.str();
+}
+
+static bool check_tx_is_expanded(const cryptonote::transaction& tx, const rct::ctkeyM& pubkeys)
+{
+ // Ripped from cryptonote_core/blockchain.cpp
+
+ const rct::rctSig& rv = tx.rct_signatures;
+
+ if (pubkeys.size() != rv.mixRing.size())
+ {
+ MERROR("Failed to check ringct signatures: mismatched pubkeys/mixRing size");
+ return false;
+ }
+ for (size_t i = 0; i < pubkeys.size(); ++i)
+ {
+ if (pubkeys[i].size() != rv.mixRing[i].size())
+ {
+ MERROR("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[n][m].dest))
+ {
+ MERROR("Failed to check ringct signatures: mismatched pubkey at vin " << n << ", index " << m);
+ return false;
+ }
+ if (pubkeys[n][m].mask != rct::rct2pk(rv.mixRing[n][m].mask))
+ {
+ MERROR("Failed to check ringct signatures: mismatched commitment at vin " << n << ", index " << m);
+ return false;
+ }
+ }
+ }
+
+ const size_t n_sigs = rct::is_rct_clsag(rv.type) ? rv.p.CLSAGs.size() : rv.p.MGs.size();
+ if (n_sigs != tx.vin.size())
+ {
+ MERROR("Failed to check ringct signatures: mismatched MGs/vin sizes");
+ return false;
+ }
+ for (size_t n = 0; n < tx.vin.size(); ++n)
+ {
+ bool error;
+ if (rct::is_rct_clsag(rv.type))
+ error = memcmp(&boost::get<cryptonote::txin_to_key>(tx.vin[n]).k_image, &rv.p.CLSAGs[n].I, 32);
+ else
+ error = rv.p.MGs[n].II.empty() || memcmp(&boost::get<cryptonote::txin_to_key>(tx.vin[n]).k_image, &rv.p.MGs[n].II[0], 32);
+ if (error)
+ {
+ MERROR("Failed to check ringct signatures: mismatched key image");
+ return false;
+ }
+ }
+
+ return true;
+}
+
+/**
+ * @brief Perform expand_transaction_1 and Blockchain::expand_transaction_2 on a certain transaction
+ */
+static void expand_transaction_fully(cryptonote::transaction& tx, const rct::ctkeyM& input_pubkeys)
+{
+ const crypto::hash tx_prefix_hash = cryptonote::get_transaction_prefix_hash(tx);
+ CHECK_AND_ASSERT_THROW_MES(cryptonote::expand_transaction_1(tx, false), "expand 1 failed");
+ CHECK_AND_ASSERT_THROW_MES
+ (
+ cryptonote::Blockchain::expand_transaction_2(tx, tx_prefix_hash, input_pubkeys),
+ "expand 2 failed"
+ );
+ CHECK_AND_ASSERT_THROW_MES(!memcmp(&tx_prefix_hash, &tx.rct_signatures.message, 32), "message check failed");
+ CHECK_AND_ASSERT_THROW_MES(input_pubkeys == tx.rct_signatures.mixRing, "mixring check failed");
+ CHECK_AND_ASSERT_THROW_MES(check_tx_is_expanded(tx, input_pubkeys), "tx expansion check 2 failed");
+}
+
+/**
+ * @brief Mostly construct transaction from binary file and provided mix ring pubkeys
+ *
+ * Most important to us, this should populate the .rct_signatures.message and
+ * .rct_signatures.mixRings fields of the transaction.
+ *
+ * @param file_name relative file path in unit test data directory
+ * @param input_pubkeys manually retrived input pubkey destination / masks for each ring
+ * @return cryptonote::transaction the expanded transaction
+ */
+static cryptonote::transaction expand_transaction_from_bin_file_and_pubkeys
+(
+ const char* file_name,
+ const rct::ctkeyM& input_pubkeys
+)
+{
+ cryptonote::transaction transaction;
+
+ const boost::filesystem::path tx_json_path = unit_test::data_dir / file_name;
+ std::string tx_blob;
+ CHECK_AND_ASSERT_THROW_MES
+ (
+ epee::file_io_utils::load_file_to_string(tx_json_path.string(), tx_blob),
+ "loading file to string failed"
+ );
+
+ CHECK_AND_ASSERT_THROW_MES
+ (
+ cryptonote::parse_and_validate_tx_from_blob(tx_blob, transaction),
+ "TX blob could not be parsed"
+ );
+
+ expand_transaction_fully(transaction, input_pubkeys);
+
+ return transaction;
+}
+
+/**
+ * @brief Return whether a modification changes blob resulting from do_serialize()
+ */
+template <typename T, class TModifier>
+static bool modification_changes_do_serialize
+(
+ const T& og_obj,
+ TModifier& obj_modifier_func,
+ bool expected_change
+)
+{
+ T modded_obj = og_obj;
+ obj_modifier_func(modded_obj);
+ const std::string og_blob = stringify_with_do_serialize(og_obj);
+ const std::string modded_blob = stringify_with_do_serialize(modded_obj);
+ const bool did_change = modded_blob != og_blob;
+ if (did_change != expected_change)
+ {
+ const std::string og_hex = epee::to_hex::string(epee::strspan<uint8_t>(og_blob));
+ const std::string modded_hex = epee::to_hex::string(epee::strspan<uint8_t>(modded_blob));
+ MERROR("unexpected: modded_blob '" << modded_hex << "' vs og_blob ' << " << og_hex << "'");
+ }
+ return did_change;
+}
+
+// Contains binary representation of mainnet transaction (height 2777777):
+// e89415b95564aa7e3587c91422756ba5303e727996e19c677630309a0d52a7ca
+static constexpr const char* tx1_file_name = "txs/bpp_tx_e89415.bin";
+
+// This contains destination key / mask pairs for each output in the input ring of the above tx
+static const rct::ctkeyM tx1_input_pubkeys =
+{{
+ make_ctkey("e50f476129d40af31e0938743f7f2d60e867aab31294f7acaf6e38f0976f0228", "51e788ddf5c95c124a7314d45a91b52d60db25a0572de9c2b4ec515aca3d4481"),
+ make_ctkey("804245d067fcfe6cd66376db0571869989bc68b3e22a0f902109c7530df47a59", "c3cc65d3b3a05defaa05213dc3b0496f9b86dbeeefbff28db34b134b6ee3230b"),
+ make_ctkey("527563a03b498e47732b815f5f0c5875a70e0fb71a37c88123f0f8686349fae4", "04417c03b397cd11e403275ec89cb0ab5b8476bb88470e9ae7208ea63dacf073"),
+ make_ctkey("bffca8b5c7fe4235ba7136d6b5325f63df343dc147940b677f50217f8953bca6", "5cd8c5e54e07275422c9c5a9f4a7268d26c494ffba419e878b7e873a02ae2e76"),
+ make_ctkey("1f73385ea74308aa78b5abf585faac14a5e78a6e23f0f68c9c14681108b28ef0", "5c02b3156daaa8ec476d3244439d90efa266f3e51cb9c8eb384d8b9a8efaa024"),
+ make_ctkey("a2421eae8bb256644b34feeab48c6086c2c9feb40d2643436dc45e303eee8ab2", "787823abffa988b56d4a7b4a834630f71520220fd82fad035955e616ec095788"),
+ make_ctkey("17d8d8dc1e1c25b7295f2eab44c4ccc08a629b8e8d781bbb6f9a51a9561bcd4c", "db1ea24be6947e03176a297160dba16d65f37751bb0ef2ba71a4590d12b61dfc"),
+ make_ctkey("2c39348a9ab04dbabe3b5249819b7845ed8aaebd0d8eddd98bda0bf40753a398", "4e6cd25fbd10e2e040be84e3bf8043c612daeef625e66a5e5bcff88c9c46e82c"),
+ make_ctkey("c4c97157f23b45c7084526aaa9958fe858bebe446a7efa22c491c439b74271b1", "e251db2c86193a11a5bffefffe48c20e3d92a8dc98cb3a2f41704e565bcd860a"),
+ make_ctkey("d342045525139a8551bcdfa7aa0117d2ac2327cb6cf449ca59420c300e4471a5", "789c11f72060ad80f4cda5d89b24d49f9435bf765598dea7a91776e99f05f87c"),
+ make_ctkey("9a972ccf2c74f648070b0be839749c98eca87166de401a6c1f59e64b938a46c1", "5444cbed5cec31fb6ed1612f815d292f2bf3d2ff584bbcd8e5201ec59670d414"),
+ make_ctkey("49ccb806ccf5cbd74bae8d9fb2da8918ab61d0774ee6a6c3a6ccd237db22a088", "0c5db942fb44f29f6ef956e24db91f98a6de6e7288b0b04d01b8f260453d1431"),
+ make_ctkey("74417e8d1483df2df6fe68c88fc9a72639c35d765b38351b838521addf45dadc", "a1a606d6c4762ef51c1759bcb8b5c88be1d323025400c41fe6885431064b64dc"),
+ make_ctkey("48c4c349adaf7b3be27656ea70d1c83b93e1511bb0aac987861a4da9689b0e95", "ad14ffd5edac199ea7c5437d558089b0f2f03aa74bde43611322d769968b5a1c"),
+ make_ctkey("2d2ffade0f85ddd83a036469e49542e93cad94f9bea535f0ea2eb2f56304517e", "bcc48d00bd06dc5439200e749d0caf8a062b072d0c0eb1f78f6a4d8f2373e5f4"),
+ make_ctkey("4ee857d0ce17f66eca9c81eb326e404ceb50c8198248f2f827c440ee7aa0c0d7", "a8a9d61d4abbfb123630ffd214c834cc45113eaa51dd2f904cc6ae0c3c5d70e3")
+}};
+} // anonymous namespace
+
+TEST(verRctNonSemanticsSimple, tx1_preconditions)
+{
+ // If this unit test fails, something changed about transaction deserialization / expansion or
+ // something changed about RingCT signature verification.
+
+ cryptonote::transaction tx = expand_transaction_from_bin_file_and_pubkeys
+ (tx1_file_name, tx1_input_pubkeys);
+ const rct::rctSig& rs = tx.rct_signatures;
+
+ const crypto::hash tx_prefix_hash = cryptonote::get_transaction_prefix_hash(tx);
+
+ EXPECT_EQ(1, tx.vin.size());
+ EXPECT_EQ(2, tx.vout.size());
+ const rct::key expected_sig_msg = rct::hash2rct(tx_prefix_hash);
+ EXPECT_EQ(expected_sig_msg, rs.message);
+ EXPECT_EQ(1, rs.mixRing.size());
+ EXPECT_EQ(16, rs.mixRing[0].size());
+ EXPECT_EQ(0, rs.pseudoOuts.size());
+ EXPECT_EQ(0, rs.p.rangeSigs.size());
+ EXPECT_EQ(0, rs.p.bulletproofs.size());
+ EXPECT_EQ(1, rs.p.bulletproofs_plus.size());
+ EXPECT_EQ(2, rs.p.bulletproofs_plus[0].V.size());
+ EXPECT_EQ(7, rs.p.bulletproofs_plus[0].L.size());
+ EXPECT_EQ(7, rs.p.bulletproofs_plus[0].R.size());
+ EXPECT_EQ(0, rs.p.MGs.size());
+ EXPECT_EQ(1, rs.p.CLSAGs.size());
+ EXPECT_EQ(16, rs.p.CLSAGs[0].s.size());
+ EXPECT_EQ(1, rs.p.pseudoOuts.size());
+ EXPECT_EQ(tx1_input_pubkeys, rs.mixRing);
+ EXPECT_EQ(2, rs.outPk.size());
+
+ EXPECT_TRUE(rct::verRctSemanticsSimple(rs));
+ EXPECT_TRUE(rct::verRctNonSemanticsSimple(rs));
+ EXPECT_TRUE(rct::verRctSimple(rs));
+ 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) \
+ do { \
+ const auto sig_modifier_func = [](rct::rctSig& rs) { rs.fieldmodifyclause; }; \
+ EXPECT_TRUE(modification_changes_do_serialize(original_sig, sig_modifier_func, true)); \
+ } while (0); \
+
+TEST(verRctNonSemanticsSimple, serializable_sig_changes)
+{
+ // Hello, future visitors. If this unit test fails, then fields of rctSig have been dropped from
+ // serialization.
+
+ const cryptonote::transaction tx = expand_transaction_from_bin_file_and_pubkeys
+ (tx1_file_name, tx1_input_pubkeys);
+ const rct::rctSig& original_sig = tx.rct_signatures;
+
+ // These are the subtests most likely to fail. Fields 'message' and 'mixRing' are not serialized
+ // when sent over the wire, since they can be reconstructed from transaction data. However, they
+ // are serialized by ::do_serialize(rctSig).
+ // How signatures are serialized for the blockchain can be found in the methods
+ // rct::rctSigBase::serialize_rctsig_base and rct::rctSigPrunable::serialize_rctsig_prunable.
+ SERIALIZABLE_SIG_CHANGES_SUBTEST(message.bytes[31]++)
+ SERIALIZABLE_SIG_CHANGES_SUBTEST(mixRing.push_back({}))
+ SERIALIZABLE_SIG_CHANGES_SUBTEST(mixRing[0].push_back({}))
+ SERIALIZABLE_SIG_CHANGES_SUBTEST(mixRing[0][8].dest[10]--)
+ SERIALIZABLE_SIG_CHANGES_SUBTEST(mixRing[0][15].mask[3]--)
+
+ // rctSigBase changes. These subtests are less likely to break
+ SERIALIZABLE_SIG_CHANGES_SUBTEST(type ^= 23)
+ SERIALIZABLE_SIG_CHANGES_SUBTEST(pseudoOuts.push_back({}))
+ SERIALIZABLE_SIG_CHANGES_SUBTEST(ecdhInfo.push_back({}))
+ SERIALIZABLE_SIG_CHANGES_SUBTEST(outPk.push_back({}))
+ SERIALIZABLE_SIG_CHANGES_SUBTEST(outPk[0].dest[14]--)
+ SERIALIZABLE_SIG_CHANGES_SUBTEST(outPk[1].dest[14]--)
+ SERIALIZABLE_SIG_CHANGES_SUBTEST(outPk[0].mask[14]--)
+ SERIALIZABLE_SIG_CHANGES_SUBTEST(outPk[1].mask[14]--)
+ SERIALIZABLE_SIG_CHANGES_SUBTEST(txnFee *= 2023)
+
+ // rctSigPrunable changes
+ SERIALIZABLE_SIG_CHANGES_SUBTEST(p.rangeSigs.push_back({}))
+ SERIALIZABLE_SIG_CHANGES_SUBTEST(p.bulletproofs.push_back({}))
+ SERIALIZABLE_SIG_CHANGES_SUBTEST(p.bulletproofs_plus.push_back({}))
+ SERIALIZABLE_SIG_CHANGES_SUBTEST(p.bulletproofs_plus[0].A[13] -= 7)
+ SERIALIZABLE_SIG_CHANGES_SUBTEST(p.bulletproofs_plus[0].A1[13] -= 7)
+ SERIALIZABLE_SIG_CHANGES_SUBTEST(p.bulletproofs_plus[0].B[13] -= 7)
+ SERIALIZABLE_SIG_CHANGES_SUBTEST(p.bulletproofs_plus[0].r1[13] -= 7)
+ SERIALIZABLE_SIG_CHANGES_SUBTEST(p.bulletproofs_plus[0].s1[13] -= 7)
+ SERIALIZABLE_SIG_CHANGES_SUBTEST(p.bulletproofs_plus[0].d1[13] -= 7)
+ SERIALIZABLE_SIG_CHANGES_SUBTEST(p.bulletproofs_plus[0].L.push_back({}))
+ SERIALIZABLE_SIG_CHANGES_SUBTEST(p.bulletproofs_plus[0].L[2][13] -= 7)
+ SERIALIZABLE_SIG_CHANGES_SUBTEST(p.bulletproofs_plus[0].R.push_back({}))
+ SERIALIZABLE_SIG_CHANGES_SUBTEST(p.bulletproofs_plus[0].R[2][13] -= 7)
+ SERIALIZABLE_SIG_CHANGES_SUBTEST(p.MGs.push_back({}))
+ SERIALIZABLE_SIG_CHANGES_SUBTEST(p.CLSAGs.push_back({}))
+ SERIALIZABLE_SIG_CHANGES_SUBTEST(p.CLSAGs[0].s.push_back({}))
+ SERIALIZABLE_SIG_CHANGES_SUBTEST(p.CLSAGs[0].s[15][31] ^= 69)
+ SERIALIZABLE_SIG_CHANGES_SUBTEST(p.CLSAGs[0].c1[0] /= 3)
+ SERIALIZABLE_SIG_CHANGES_SUBTEST(p.CLSAGs[0].D[0] /= 3)
+ SERIALIZABLE_SIG_CHANGES_SUBTEST(p.pseudoOuts.push_back({}))
+
+ // Uncomment line below to sanity check SERIALIZABLE_SIG_CHANGES_SUBTEST
+ // SERIALIZABLE_SIG_CHANGES_SUBTEST(message) // should fail
+}
+
+#define UNSERIALIZABLE_SIG_CHANGES_SUBTEST(fieldmodifyclause) \
+ do { \
+ const auto sig_modifier_func = [](rct::rctSig& rs) { rs.fieldmodifyclause; }; \
+ EXPECT_FALSE(modification_changes_do_serialize(original_sig, sig_modifier_func, false)); \
+ } while (0); \
+
+TEST(verRctNonSemanticsSimple, unserializable_sig_changes)
+{
+ // Hello, future visitors. If this unit test fails, then congrats! ::do_serialize(rctSig) became
+ // better at uniquely representing rctSig.
+ const cryptonote::transaction tx = expand_transaction_from_bin_file_and_pubkeys
+ (tx1_file_name, tx1_input_pubkeys);
+ const rct::rctSig& original_sig = tx.rct_signatures;
+
+ UNSERIALIZABLE_SIG_CHANGES_SUBTEST(p.CLSAGs[0].I[14]++)
+ UNSERIALIZABLE_SIG_CHANGES_SUBTEST(p.bulletproofs_plus[0].V.push_back({}))
+ UNSERIALIZABLE_SIG_CHANGES_SUBTEST(p.bulletproofs_plus[0].V[1][31]--)
+
+ // Uncomment line below to sanity check UNSERIALIZABLE_SIG_CHANGES_SUBTEST_SHORTCUT
+ // UNSERIALIZABLE_SIG_CHANGES_SUBTEST_SHORTCUT(message[2]++) // should fail
+}
+
+#define SERIALIZABLE_MIXRING_CHANGES_SUBTEST(fieldmodifyclause) \
+ do { \
+ using mr_mod_func_t = std::function<void(rct::ctkeyM&)>; \
+ const mr_mod_func_t mr_modifier_func = [&](rct::ctkeyM& mr) { mr fieldmodifyclause; }; \
+ EXPECT_TRUE(modification_changes_do_serialize(original_mixring, mr_modifier_func, true)); \
+ } while (0); \
+
+TEST(verRctNonSemanticsSimple, serializable_mixring_changes)
+{
+ // Hello, future Monero devs! If this unit test fails, a huge concensus-related assumption has
+ // been broken and verRctNonSemanticsSimpleCached needs to be reevalulated for validity. If it
+ // is not, there may be an exploit which allows for double-spending. See the implementation for
+ // more comments on the uniqueness of the internal cache hash.
+
+ const rct::ctkeyM original_mixring = tx1_input_pubkeys;
+
+ const size_t mlen = tx1_input_pubkeys.size();
+ ASSERT_EQ(1, mlen);
+ const size_t nlen = tx1_input_pubkeys[0].size();
+ ASSERT_EQ(16, nlen);
+
+ SERIALIZABLE_MIXRING_CHANGES_SUBTEST(.clear())
+ SERIALIZABLE_MIXRING_CHANGES_SUBTEST(.push_back({}))
+ SERIALIZABLE_MIXRING_CHANGES_SUBTEST([0].clear())
+ SERIALIZABLE_MIXRING_CHANGES_SUBTEST([0].push_back({}))
+ SERIALIZABLE_MIXRING_CHANGES_SUBTEST([0][0].dest[4]--)
+ SERIALIZABLE_MIXRING_CHANGES_SUBTEST([0][15].mask[31]--)
+
+ // Loop through all bytes of the mixRing and check for serialiable changes
+ for (size_t i = 0; i < mlen; ++i)
+ {
+ for (size_t j = 0; j < nlen; ++j)
+ {
+ static_assert(sizeof(rct::key) == 32, "rct::key size wrong");
+ for (size_t k = 0; k < sizeof(rct::key); ++k)
+ {
+ SERIALIZABLE_MIXRING_CHANGES_SUBTEST([i][j].dest[k]++)
+ SERIALIZABLE_MIXRING_CHANGES_SUBTEST([i][j].mask[k]++)
+ }
+ }
+ }
+}
+
+#define EXPAND_TRANSACTION_2_FAILURES_SUBTEST(fieldmodifyclause) \
+ do { \
+ cryptonote::transaction test_tx = original_tx; \
+ test_tx.fieldmodifyclause; \
+ test_tx.invalidate_hashes(); \
+ EXPECT_FALSE(check_tx_is_expanded(test_tx, original_mixring)); \
+ } while (0); \
+
+TEST(verRctNonSemanticsSimple, expand_transaction_2_failures)
+{
+ cryptonote::transaction original_tx = expand_transaction_from_bin_file_and_pubkeys
+ (tx1_file_name, tx1_input_pubkeys);
+ rct::ctkeyM original_mixring = tx1_input_pubkeys;
+
+ EXPAND_TRANSACTION_2_FAILURES_SUBTEST(rct_signatures.p.CLSAGs[0].I[0]++)
+ EXPAND_TRANSACTION_2_FAILURES_SUBTEST(rct_signatures.mixRing[0][15].dest[31]++)
+ EXPAND_TRANSACTION_2_FAILURES_SUBTEST(rct_signatures.mixRing[0][15].mask[31]++)
+}
diff --git a/tests/unit_tests/ver_rct_non_semantics_simple_cached.cpp b/tests/unit_tests/ver_rct_non_semantics_simple_cached.cpp
deleted file mode 100644
index df542bd..0000000
--- a/tests/unit_tests/ver_rct_non_semantics_simple_cached.cpp
+++ /dev/null
@@ -1,426 +0,0 @@
-// Copyright (c) 2023-2024, 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 <sstream>
-
-#define IN_UNIT_TESTS // To access Blockchain::{expand_transaction_2, verRctNonSemanticsSimpleCached}
-
-#include "gtest/gtest.h"
-#include "unit_tests_utils.h"
-
-#include "cryptonote_basic/cryptonote_format_utils.h"
-#include "cryptonote_core/blockchain.h"
-#include "file_io_utils.h"
-#include "misc_log_ex.h"
-#include "ringct/rctSigs.h"
-
-namespace cryptonote
-{
-// declaration not provided in cryptonote_format_utils.h, but definition is not static ;)
-bool expand_transaction_1(transaction &tx, bool base_only);
-}
-
-namespace
-{
-/**
- * @brief Make rct::ctkey from hex string representation of destionation and mask
- *
- * @param dest_hex
- * @param mask_hex
- * @return rct::ctkey
- */
-static rct::ctkey make_ctkey(const char* dest_hex, const char* mask_hex)
-{
- rct::key dest;
- rct::key mask;
- CHECK_AND_ASSERT_THROW_MES(epee::from_hex::to_buffer(epee::as_mut_byte_span(dest), dest_hex), "dest bad hex: " << dest_hex);
- CHECK_AND_ASSERT_THROW_MES(epee::from_hex::to_buffer(epee::as_mut_byte_span(mask), mask_hex), "mask bad hex: " << mask_hex);
- return {dest, mask};
-}
-
-template <typename T>
-static std::string stringify_with_do_serialize(const T& t)
-{
- std::stringstream ss;
- binary_archive<true> ar(ss);
- CHECK_AND_ASSERT_THROW_MES(ar.good(), "Archiver is not in a good state. This shouldn't happen!");
- ::do_serialize(ar, const_cast<T&>(t));
- return ss.str();
-}
-
-static bool check_tx_is_expanded(const cryptonote::transaction& tx, const rct::ctkeyM& pubkeys)
-{
- // Ripped from cryptonote_core/blockchain.cpp
-
- const rct::rctSig& rv = tx.rct_signatures;
-
- if (pubkeys.size() != rv.mixRing.size())
- {
- MERROR("Failed to check ringct signatures: mismatched pubkeys/mixRing size");
- return false;
- }
- for (size_t i = 0; i < pubkeys.size(); ++i)
- {
- if (pubkeys[i].size() != rv.mixRing[i].size())
- {
- MERROR("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[n][m].dest))
- {
- MERROR("Failed to check ringct signatures: mismatched pubkey at vin " << n << ", index " << m);
- return false;
- }
- if (pubkeys[n][m].mask != rct::rct2pk(rv.mixRing[n][m].mask))
- {
- MERROR("Failed to check ringct signatures: mismatched commitment at vin " << n << ", index " << m);
- return false;
- }
- }
- }
-
- const size_t n_sigs = rct::is_rct_clsag(rv.type) ? rv.p.CLSAGs.size() : rv.p.MGs.size();
- if (n_sigs != tx.vin.size())
- {
- MERROR("Failed to check ringct signatures: mismatched MGs/vin sizes");
- return false;
- }
- for (size_t n = 0; n < tx.vin.size(); ++n)
- {
- bool error;
- if (rct::is_rct_clsag(rv.type))
- error = memcmp(&boost::get<cryptonote::txin_to_key>(tx.vin[n]).k_image, &rv.p.CLSAGs[n].I, 32);
- else
- error = rv.p.MGs[n].II.empty() || memcmp(&boost::get<cryptonote::txin_to_key>(tx.vin[n]).k_image, &rv.p.MGs[n].II[0], 32);
- if (error)
- {
- MERROR("Failed to check ringct signatures: mismatched key image");
- return false;
- }
- }
-
- return true;
-}
-
-/**
- * @brief Perform expand_transaction_1 and Blockchain::expand_transaction_2 on a certain transaction
- */
-static void expand_transaction_fully(cryptonote::transaction& tx, const rct::ctkeyM& input_pubkeys)
-{
- const crypto::hash tx_prefix_hash = cryptonote::get_transaction_prefix_hash(tx);
- CHECK_AND_ASSERT_THROW_MES(cryptonote::expand_transaction_1(tx, false), "expand 1 failed");
- CHECK_AND_ASSERT_THROW_MES
- (
- cryptonote::Blockchain::expand_transaction_2(tx, tx_prefix_hash, input_pubkeys),
- "expand 2 failed"
- );
- CHECK_AND_ASSERT_THROW_MES(!memcmp(&tx_prefix_hash, &tx.rct_signatures.message, 32), "message check failed");
- CHECK_AND_ASSERT_THROW_MES(input_pubkeys == tx.rct_signatures.mixRing, "mixring check failed");
- CHECK_AND_ASSERT_THROW_MES(check_tx_is_expanded(tx, input_pubkeys), "tx expansion check 2 failed");
-}
-
-/**
- * @brief Mostly construct transaction from binary file and provided mix ring pubkeys
- *
- * Most important to us, this should populate the .rct_signatures.message and
- * .rct_signatures.mixRings fields of the transaction.
- *
- * @param file_name relative file path in unit test data directory
- * @param input_pubkeys manually retrived input pubkey destination / masks for each ring
- * @return cryptonote::transaction the expanded transaction
- */
-static cryptonote::transaction expand_transaction_from_bin_file_and_pubkeys
-(
- const char* file_name,
- const rct::ctkeyM& input_pubkeys
-)
-{
- cryptonote::transaction transaction;
-
- const boost::filesystem::path tx_json_path = unit_test::data_dir / file_name;
- std::string tx_blob;
- CHECK_AND_ASSERT_THROW_MES
- (
- epee::file_io_utils::load_file_to_string(tx_json_path.string(), tx_blob),
- "loading file to string failed"
- );
-
- CHECK_AND_ASSERT_THROW_MES
- (
- cryptonote::parse_and_validate_tx_from_blob(tx_blob, transaction),
- "TX blob could not be parsed"
- );
-
- expand_transaction_fully(transaction, input_pubkeys);
-
- return transaction;
-}
-
-/**
- * @brief Return whether a modification changes blob resulting from do_serialize()
- */
-template <typename T, class TModifier>
-static bool modification_changes_do_serialize
-(
- const T& og_obj,
- TModifier& obj_modifier_func,
- bool expected_change
-)
-{
- T modded_obj = og_obj;
- obj_modifier_func(modded_obj);
- const std::string og_blob = stringify_with_do_serialize(og_obj);
- const std::string modded_blob = stringify_with_do_serialize(modded_obj);
- const bool did_change = modded_blob != og_blob;
- if (did_change != expected_change)
- {
- const std::string og_hex = epee::to_hex::string(epee::strspan<uint8_t>(og_blob));
- const std::string modded_hex = epee::to_hex::string(epee::strspan<uint8_t>(modded_blob));
- MERROR("unexpected: modded_blob '" << modded_hex << "' vs og_blob ' << " << og_hex << "'");
- }
- return did_change;
-}
-
-// Contains binary representation of mainnet transaction (height 2777777):
-// e89415b95564aa7e3587c91422756ba5303e727996e19c677630309a0d52a7ca
-static constexpr const char* tx1_file_name = "txs/bpp_tx_e89415.bin";
-
-// This contains destination key / mask pairs for each output in the input ring of the above tx
-static const rct::ctkeyM tx1_input_pubkeys =
-{{
- make_ctkey("e50f476129d40af31e0938743f7f2d60e867aab31294f7acaf6e38f0976f0228", "51e788ddf5c95c124a7314d45a91b52d60db25a0572de9c2b4ec515aca3d4481"),
- make_ctkey("804245d067fcfe6cd66376db0571869989bc68b3e22a0f902109c7530df47a59", "c3cc65d3b3a05defaa05213dc3b0496f9b86dbeeefbff28db34b134b6ee3230b"),
- make_ctkey("527563a03b498e47732b815f5f0c5875a70e0fb71a37c88123f0f8686349fae4", "04417c03b397cd11e403275ec89cb0ab5b8476bb88470e9ae7208ea63dacf073"),
- make_ctkey("bffca8b5c7fe4235ba7136d6b5325f63df343dc147940b677f50217f8953bca6", "5cd8c5e54e07275422c9c5a9f4a7268d26c494ffba419e878b7e873a02ae2e76"),
- make_ctkey("1f73385ea74308aa78b5abf585faac14a5e78a6e23f0f68c9c14681108b28ef0", "5c02b3156daaa8ec476d3244439d90efa266f3e51cb9c8eb384d8b9a8efaa024"),
- make_ctkey("a2421eae8bb256644b34feeab48c6086c2c9feb40d2643436dc45e303eee8ab2", "787823abffa988b56d4a7b4a834630f71520220fd82fad035955e616ec095788"),
- make_ctkey("17d8d8dc1e1c25b7295f2eab44c4ccc08a629b8e8d781bbb6f9a51a9561bcd4c", "db1ea24be6947e03176a297160dba16d65f37751bb0ef2ba71a4590d12b61dfc"),
- make_ctkey("2c39348a9ab04dbabe3b5249819b7845ed8aaebd0d8eddd98bda0bf40753a398", "4e6cd25fbd10e2e040be84e3bf8043c612daeef625e66a5e5bcff88c9c46e82c"),
- make_ctkey("c4c97157f23b45c7084526aaa9958fe858bebe446a7efa22c491c439b74271b1", "e251db2c86193a11a5bffefffe48c20e3d92a8dc98cb3a2f41704e565bcd860a"),
- make_ctkey("d342045525139a8551bcdfa7aa0117d2ac2327cb6cf449ca59420c300e4471a5", "789c11f72060ad80f4cda5d89b24d49f9435bf765598dea7a91776e99f05f87c"),
- make_ctkey("9a972ccf2c74f648070b0be839749c98eca87166de401a6c1f59e64b938a46c1", "5444cbed5cec31fb6ed1612f815d292f2bf3d2ff584bbcd8e5201ec59670d414"),
- make_ctkey("49ccb806ccf5cbd74bae8d9fb2da8918ab61d0774ee6a6c3a6ccd237db22a088", "0c5db942fb44f29f6ef956e24db91f98a6de6e7288b0b04d01b8f260453d1431"),
- make_ctkey("74417e8d1483df2df6fe68c88fc9a72639c35d765b38351b838521addf45dadc", "a1a606d6c4762ef51c1759bcb8b5c88be1d323025400c41fe6885431064b64dc"),
- make_ctkey("48c4c349adaf7b3be27656ea70d1c83b93e1511bb0aac987861a4da9689b0e95", "ad14ffd5edac199ea7c5437d558089b0f2f03aa74bde43611322d769968b5a1c"),
- make_ctkey("2d2ffade0f85ddd83a036469e49542e93cad94f9bea535f0ea2eb2f56304517e", "bcc48d00bd06dc5439200e749d0caf8a062b072d0c0eb1f78f6a4d8f2373e5f4"),
- make_ctkey("4ee857d0ce17f66eca9c81eb326e404ceb50c8198248f2f827c440ee7aa0c0d7", "a8a9d61d4abbfb123630ffd214c834cc45113eaa51dd2f904cc6ae0c3c5d70e3")
-}};
-} // anonymous namespace
-
-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;
-
- const crypto::hash tx_prefix_hash = cryptonote::get_transaction_prefix_hash(tx);
-
- EXPECT_EQ(1, tx.vin.size());
- EXPECT_EQ(2, tx.vout.size());
- const rct::key expected_sig_msg = rct::hash2rct(tx_prefix_hash);
- EXPECT_EQ(expected_sig_msg, rs.message);
- EXPECT_EQ(1, rs.mixRing.size());
- EXPECT_EQ(16, rs.mixRing[0].size());
- EXPECT_EQ(0, rs.pseudoOuts.size());
- EXPECT_EQ(0, rs.p.rangeSigs.size());
- EXPECT_EQ(0, rs.p.bulletproofs.size());
- EXPECT_EQ(1, rs.p.bulletproofs_plus.size());
- EXPECT_EQ(2, rs.p.bulletproofs_plus[0].V.size());
- EXPECT_EQ(7, rs.p.bulletproofs_plus[0].L.size());
- EXPECT_EQ(7, rs.p.bulletproofs_plus[0].R.size());
- EXPECT_EQ(0, rs.p.MGs.size());
- EXPECT_EQ(1, rs.p.CLSAGs.size());
- EXPECT_EQ(16, rs.p.CLSAGs[0].s.size());
- EXPECT_EQ(1, rs.p.pseudoOuts.size());
- EXPECT_EQ(tx1_input_pubkeys, rs.mixRing);
- EXPECT_EQ(2, rs.outPk.size());
-
- 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));
-}
-
-#define SERIALIZABLE_SIG_CHANGES_SUBTEST(fieldmodifyclause) \
- do { \
- const auto sig_modifier_func = [](rct::rctSig& rs) { rs.fieldmodifyclause; }; \
- EXPECT_TRUE(modification_changes_do_serialize(original_sig, sig_modifier_func, true)); \
- } while (0); \
-
-TEST(verRctNonSemanticsSimple, serializable_sig_changes)
-{
- // Hello, future visitors. If this unit test fails, then fields of rctSig have been dropped from
- // serialization.
-
- const cryptonote::transaction tx = expand_transaction_from_bin_file_and_pubkeys
- (tx1_file_name, tx1_input_pubkeys);
- const rct::rctSig& original_sig = tx.rct_signatures;
-
- // These are the subtests most likely to fail. Fields 'message' and 'mixRing' are not serialized
- // when sent over the wire, since they can be reconstructed from transaction data. However, they
- // are serialized by ::do_serialize(rctSig).
- // How signatures are serialized for the blockchain can be found in the methods
- // rct::rctSigBase::serialize_rctsig_base and rct::rctSigPrunable::serialize_rctsig_prunable.
- SERIALIZABLE_SIG_CHANGES_SUBTEST(message.bytes[31]++)
- SERIALIZABLE_SIG_CHANGES_SUBTEST(mixRing.push_back({}))
- SERIALIZABLE_SIG_CHANGES_SUBTEST(mixRing[0].push_back({}))
- SERIALIZABLE_SIG_CHANGES_SUBTEST(mixRing[0][8].dest[10]--)
- SERIALIZABLE_SIG_CHANGES_SUBTEST(mixRing[0][15].mask[3]--)
-
- // rctSigBase changes. These subtests are less likely to break
- SERIALIZABLE_SIG_CHANGES_SUBTEST(type ^= 23)
- SERIALIZABLE_SIG_CHANGES_SUBTEST(pseudoOuts.push_back({}))
- SERIALIZABLE_SIG_CHANGES_SUBTEST(ecdhInfo.push_back({}))
- SERIALIZABLE_SIG_CHANGES_SUBTEST(outPk.push_back({}))
- SERIALIZABLE_SIG_CHANGES_SUBTEST(outPk[0].dest[14]--)
- SERIALIZABLE_SIG_CHANGES_SUBTEST(outPk[1].dest[14]--)
- SERIALIZABLE_SIG_CHANGES_SUBTEST(outPk[0].mask[14]--)
- SERIALIZABLE_SIG_CHANGES_SUBTEST(outPk[1].mask[14]--)
- SERIALIZABLE_SIG_CHANGES_SUBTEST(txnFee *= 2023)
-
- // rctSigPrunable changes
- SERIALIZABLE_SIG_CHANGES_SUBTEST(p.rangeSigs.push_back({}))
- SERIALIZABLE_SIG_CHANGES_SUBTEST(p.bulletproofs.push_back({}))
- SERIALIZABLE_SIG_CHANGES_SUBTEST(p.bulletproofs_plus.push_back({}))
- SERIALIZABLE_SIG_CHANGES_SUBTEST(p.bulletproofs_plus[0].A[13] -= 7)
- SERIALIZABLE_SIG_CHANGES_SUBTEST(p.bulletproofs_plus[0].A1[13] -= 7)
- SERIALIZABLE_SIG_CHANGES_SUBTEST(p.bulletproofs_plus[0].B[13] -= 7)
- SERIALIZABLE_SIG_CHANGES_SUBTEST(p.bulletproofs_plus[0].r1[13] -= 7)
- SERIALIZABLE_SIG_CHANGES_SUBTEST(p.bulletproofs_plus[0].s1[13] -= 7)
- SERIALIZABLE_SIG_CHANGES_SUBTEST(p.bulletproofs_plus[0].d1[13] -= 7)
- SERIALIZABLE_SIG_CHANGES_SUBTEST(p.bulletproofs_plus[0].L.push_back({}))
- SERIALIZABLE_SIG_CHANGES_SUBTEST(p.bulletproofs_plus[0].L[2][13] -= 7)
- SERIALIZABLE_SIG_CHANGES_SUBTEST(p.bulletproofs_plus[0].R.push_back({}))
- SERIALIZABLE_SIG_CHANGES_SUBTEST(p.bulletproofs_plus[0].R[2][13] -= 7)
- SERIALIZABLE_SIG_CHANGES_SUBTEST(p.MGs.push_back({}))
- SERIALIZABLE_SIG_CHANGES_SUBTEST(p.CLSAGs.push_back({}))
- SERIALIZABLE_SIG_CHANGES_SUBTEST(p.CLSAGs[0].s.push_back({}))
- SERIALIZABLE_SIG_CHANGES_SUBTEST(p.CLSAGs[0].s[15][31] ^= 69)
- SERIALIZABLE_SIG_CHANGES_SUBTEST(p.CLSAGs[0].c1[0] /= 3)
- SERIALIZABLE_SIG_CHANGES_SUBTEST(p.CLSAGs[0].D[0] /= 3)
- SERIALIZABLE_SIG_CHANGES_SUBTEST(p.pseudoOuts.push_back({}))
-
- // Uncomment line below to sanity check SERIALIZABLE_SIG_CHANGES_SUBTEST
- // SERIALIZABLE_SIG_CHANGES_SUBTEST(message) // should fail
-}
-
-#define UNSERIALIZABLE_SIG_CHANGES_SUBTEST(fieldmodifyclause) \
- do { \
- const auto sig_modifier_func = [](rct::rctSig& rs) { rs.fieldmodifyclause; }; \
- EXPECT_FALSE(modification_changes_do_serialize(original_sig, sig_modifier_func, false)); \
- } while (0); \
-
-TEST(verRctNonSemanticsSimple, unserializable_sig_changes)
-{
- // Hello, future visitors. If this unit test fails, then congrats! ::do_serialize(rctSig) became
- // better at uniquely representing rctSig.
- const cryptonote::transaction tx = expand_transaction_from_bin_file_and_pubkeys
- (tx1_file_name, tx1_input_pubkeys);
- const rct::rctSig& original_sig = tx.rct_signatures;
-
- UNSERIALIZABLE_SIG_CHANGES_SUBTEST(p.CLSAGs[0].I[14]++)
- UNSERIALIZABLE_SIG_CHANGES_SUBTEST(p.bulletproofs_plus[0].V.push_back({}))
- UNSERIALIZABLE_SIG_CHANGES_SUBTEST(p.bulletproofs_plus[0].V[1][31]--)
-
- // Uncomment line below to sanity check UNSERIALIZABLE_SIG_CHANGES_SUBTEST_SHORTCUT
- // UNSERIALIZABLE_SIG_CHANGES_SUBTEST_SHORTCUT(message[2]++) // should fail
-}
-
-#define SERIALIZABLE_MIXRING_CHANGES_SUBTEST(fieldmodifyclause) \
- do { \
- using mr_mod_func_t = std::function<void(rct::ctkeyM&)>; \
- const mr_mod_func_t mr_modifier_func = [&](rct::ctkeyM& mr) { mr fieldmodifyclause; }; \
- EXPECT_TRUE(modification_changes_do_serialize(original_mixring, mr_modifier_func, true)); \
- } while (0); \
-
-TEST(verRctNonSemanticsSimple, serializable_mixring_changes)
-{
- // Hello, future Monero devs! If this unit test fails, a huge concensus-related assumption has
- // been broken and verRctNonSemanticsSimpleCached needs to be reevalulated for validity. If it
- // is not, there may be an exploit which allows for double-spending. See the implementation for
- // more comments on the uniqueness of the internal cache hash.
-
- const rct::ctkeyM original_mixring = tx1_input_pubkeys;
-
- const size_t mlen = tx1_input_pubkeys.size();
- ASSERT_EQ(1, mlen);
- const size_t nlen = tx1_input_pubkeys[0].size();
- ASSERT_EQ(16, nlen);
-
- SERIALIZABLE_MIXRING_CHANGES_SUBTEST(.clear())
- SERIALIZABLE_MIXRING_CHANGES_SUBTEST(.push_back({}))
- SERIALIZABLE_MIXRING_CHANGES_SUBTEST([0].clear())
- SERIALIZABLE_MIXRING_CHANGES_SUBTEST([0].push_back({}))
- SERIALIZABLE_MIXRING_CHANGES_SUBTEST([0][0].dest[4]--)
- SERIALIZABLE_MIXRING_CHANGES_SUBTEST([0][15].mask[31]--)
-
- // Loop through all bytes of the mixRing and check for serialiable changes
- for (size_t i = 0; i < mlen; ++i)
- {
- for (size_t j = 0; j < nlen; ++j)
- {
- static_assert(sizeof(rct::key) == 32, "rct::key size wrong");
- for (size_t k = 0; k < sizeof(rct::key); ++k)
- {
- SERIALIZABLE_MIXRING_CHANGES_SUBTEST([i][j].dest[k]++)
- SERIALIZABLE_MIXRING_CHANGES_SUBTEST([i][j].mask[k]++)
- }
- }
- }
-}
-
-#define EXPAND_TRANSACTION_2_FAILURES_SUBTEST(fieldmodifyclause) \
- do { \
- cryptonote::transaction test_tx = original_tx; \
- test_tx.fieldmodifyclause; \
- test_tx.invalidate_hashes(); \
- EXPECT_FALSE(check_tx_is_expanded(test_tx, original_mixring)); \
- } while (0); \
-
-TEST(verRctNonSemanticsSimple, expand_transaction_2_failures)
-{
- cryptonote::transaction original_tx = expand_transaction_from_bin_file_and_pubkeys
- (tx1_file_name, tx1_input_pubkeys);
- rct::ctkeyM original_mixring = tx1_input_pubkeys;
-
- EXPAND_TRANSACTION_2_FAILURES_SUBTEST(rct_signatures.p.CLSAGs[0].I[0]++)
- EXPAND_TRANSACTION_2_FAILURES_SUBTEST(rct_signatures.mixRing[0][15].dest[31]++)
- EXPAND_TRANSACTION_2_FAILURES_SUBTEST(rct_signatures.mixRing[0][15].mask[31]++)
-}
diff --git a/utils/python-rpc/framework/daemon.py b/utils/python-rpc/framework/daemon.py
index 917e68c..71aa0ae 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 = ""):
Why this scored 39/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.