mirror of
https://github.com/monero-project/monero.git
synced 2026-01-12 17:17:18 +09:00
Merge pull request #10157
022fb8e unit_tests: @j-berman unit tests for #10157 (jeffro256) 40eb828 cryptonote_core: cache input verification results directly in mempool (jeffro256)
This commit is contained in:
commit
4286fbe6cd
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -629,11 +629,25 @@ namespace cryptonote
|
||||
* @param pmax_used_block_height return-by-reference block height of most recent input
|
||||
* @param max_used_block_id return-by-reference block hash of most recent input
|
||||
* @param tvc returned information about tx verification
|
||||
* @param valid_input_verification_id_inout a previously valid verID if non-null, set on input verification
|
||||
* @param kept_by_block whether or not the transaction is from a previously-verified block
|
||||
*
|
||||
* @return false if any input is invalid, otherwise true
|
||||
*
|
||||
* If `valid_input_verification_id_inout` is passed as null, then input verification proceeds as normal.
|
||||
* If `valid_input_verification_id_inout` is non-null and the verification ID from the current chain
|
||||
* state matches, then input verification is skipped, assumed to be successful, and
|
||||
* `valid_input_verification_id_inout` is not modified. If the current verification ID does not match,
|
||||
* then input verification is attempted anyways. If input verification is attempted and fails,
|
||||
* then `valid_input_verification_id_inout` is set to null. If input verification is attempted and succeeds,
|
||||
* then `valid_input_verification_id_inout` is set to the current used verification ID.
|
||||
*/
|
||||
bool check_tx_inputs(transaction& tx, uint64_t& pmax_used_block_height, crypto::hash& max_used_block_id, tx_verification_context &tvc, bool kept_by_block = false) const;
|
||||
bool check_tx_inputs(transaction& tx,
|
||||
uint64_t& pmax_used_block_height,
|
||||
crypto::hash& max_used_block_id,
|
||||
tx_verification_context &tvc,
|
||||
crypto::hash &valid_input_verification_id_inout,
|
||||
bool kept_by_block = false) const;
|
||||
|
||||
/**
|
||||
* @brief get fee quantization mask
|
||||
@ -1250,9 +1264,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
|
||||
*
|
||||
@ -1321,11 +1332,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
|
||||
@ -1590,18 +1613,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
|
||||
*
|
||||
|
||||
@ -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
|
||||
@ -356,7 +359,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;
|
||||
@ -364,7 +368,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
|
||||
@ -530,8 +534,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);
|
||||
|
||||
@ -569,6 +585,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
|
||||
@ -1374,7 +1391,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)
|
||||
{
|
||||
@ -1387,7 +1410,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;
|
||||
@ -1424,6 +1447,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))
|
||||
@ -1726,12 +1750,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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
return tx_and_mixring_hash;
|
||||
for (size_t n = 0; n < mix_ring.size(); ++n)
|
||||
{
|
||||
for (size_t m = 0; m < mix_ring[n].size(); ++m)
|
||||
{
|
||||
if (mix_ring[n][m].dest != rct::rct2pk(rv.mixRing[m][n].dest))
|
||||
{
|
||||
MERROR("Failed to check ringct signatures: mismatched pubkey at vin " << n << ", index " << m);
|
||||
return false;
|
||||
}
|
||||
if (mix_ring[n][m].mask != rct::rct2pk(rv.mixRing[m][n].mask))
|
||||
{
|
||||
MERROR("Failed to check ringct signatures: mismatched commitment at vin " << n << ", index " << m);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (rv.p.MGs.size() != 1)
|
||||
{
|
||||
MERROR("Failed to check ringct signatures: Bad MGs size");
|
||||
return false;
|
||||
}
|
||||
if (rv.p.MGs.empty() || rv.p.MGs[0].II.size() != tx.vin.size())
|
||||
{
|
||||
MERROR("Failed to check ringct signatures: mismatched II/vin sizes");
|
||||
return false;
|
||||
}
|
||||
for (size_t n = 0; n < tx.vin.size(); ++n)
|
||||
{
|
||||
if (memcmp(&boost::get<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;
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -87,12 +87,13 @@ set(unit_tests_sources
|
||||
test_protocol_pack.cpp
|
||||
threadpool.cpp
|
||||
tx_proof.cpp
|
||||
tx_verification_utils.cpp
|
||||
hardfork.cpp
|
||||
unbound.cpp
|
||||
uri.cpp
|
||||
util.cpp
|
||||
varint.cpp
|
||||
ver_rct_non_semantics_simple_cached.cpp
|
||||
verRctNonSemanticsSimple.cpp
|
||||
ringct.cpp
|
||||
output_selection.cpp
|
||||
vercmp.cpp
|
||||
|
||||
@ -145,3 +145,35 @@ TEST(threadpool, leaf_reentrancy)
|
||||
waiter.wait();
|
||||
ASSERT_EQ(counter, 500000);
|
||||
}
|
||||
|
||||
static bool check_test_and_set(const std::size_t n, const std::function<bool(std::size_t)> &fail_condition)
|
||||
{
|
||||
std::shared_ptr<tools::threadpool> tpool(tools::threadpool::getNewForUnitTests(std::max<std::size_t>(n, 1)));
|
||||
tools::threadpool::waiter waiter(*tpool);
|
||||
|
||||
std::atomic_flag fail_occurred{};
|
||||
for (std::size_t i = 0; i < n; ++i)
|
||||
{
|
||||
tpool->submit(&waiter, [&, i](){ if (fail_condition(i)) { fail_occurred.test_and_set(); }}, true);
|
||||
}
|
||||
|
||||
return waiter.wait() && !fail_occurred.test_and_set();
|
||||
}
|
||||
|
||||
TEST(threadpool, test_and_set)
|
||||
{
|
||||
// No failure
|
||||
ASSERT_TRUE(check_test_and_set(0, [](std::size_t i) -> bool { return false; }));
|
||||
static const std::size_t N = 4;
|
||||
ASSERT_TRUE(check_test_and_set(N, [](std::size_t i) -> bool { return false; }));
|
||||
|
||||
// 1 failure
|
||||
ASSERT_FALSE(check_test_and_set(N, [](std::size_t i) -> bool { return i == 0; }));
|
||||
ASSERT_FALSE(check_test_and_set(N, [](std::size_t i) -> bool { return i == 1; }));
|
||||
ASSERT_FALSE(check_test_and_set(N, [](std::size_t i) -> bool { return i == 2; }));
|
||||
ASSERT_FALSE(check_test_and_set(N, [](std::size_t i) -> bool { return i == 3; }));
|
||||
|
||||
// Multiple failures
|
||||
ASSERT_FALSE(check_test_and_set(N, [](std::size_t i) -> bool { return i > 0; }));
|
||||
ASSERT_FALSE(check_test_and_set(N, [](std::size_t i) -> bool { return true; }));
|
||||
}
|
||||
|
||||
269
tests/unit_tests/tx_verification_utils.cpp
Normal file
269
tests/unit_tests/tx_verification_utils.cpp
Normal file
@ -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));
|
||||
}
|
||||
}
|
||||
@ -243,8 +243,6 @@ TEST(verRctNonSemanticsSimple, tx1_preconditions)
|
||||
// If this unit test fails, something changed about transaction deserialization / expansion or
|
||||
// something changed about RingCT signature verification.
|
||||
|
||||
cryptonote::rct_ver_cache_t rct_ver_cache;
|
||||
|
||||
cryptonote::transaction tx = expand_transaction_from_bin_file_and_pubkeys
|
||||
(tx1_file_name, tx1_input_pubkeys);
|
||||
const rct::rctSig& rs = tx.rct_signatures;
|
||||
@ -274,8 +272,8 @@ TEST(verRctNonSemanticsSimple, tx1_preconditions)
|
||||
EXPECT_TRUE(rct::verRctSemanticsSimple(rs));
|
||||
EXPECT_TRUE(rct::verRctNonSemanticsSimple(rs));
|
||||
EXPECT_TRUE(rct::verRctSimple(rs));
|
||||
EXPECT_TRUE(cryptonote::ver_rct_non_semantics_simple_cached(tx, tx1_input_pubkeys, rct_ver_cache, rct::RCTTypeBulletproofPlus));
|
||||
EXPECT_TRUE(cryptonote::ver_rct_non_semantics_simple_cached(tx, tx1_input_pubkeys, rct_ver_cache, rct::RCTTypeBulletproofPlus));
|
||||
EXPECT_TRUE(cryptonote::ver_input_proofs_rings(tx, tx1_input_pubkeys));
|
||||
EXPECT_TRUE(cryptonote::ver_input_proofs_rings(tx, tx1_input_pubkeys));
|
||||
}
|
||||
|
||||
#define SERIALIZABLE_SIG_CHANGES_SUBTEST(fieldmodifyclause) \
|
||||
@ -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 = ""):
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user