blockchain_db/rpc: faster is_key_image_spent
What changed, and why it matters
This commit is a performance optimization for a Monero daemon RPC endpoint called /is_key_image_spent. It speeds up wallet rescan operations by reading on-chain key images in a single database transaction and by querying the memory pool more efficiently. The change is framed as a fix for severe daemon slowdowns during stress testing, not as a security vulnerability. There is no direct evidence in the commit of a security flaw, but the prior behavior could be abused to cause denial of service by sending many expensive queries.
Treat as a performance hardening patch rather than a critical security fix. Operators running public RPC nodes should deploy it to mitigate potential denial-of-service from expensive is_key_image_spent requests, especially when the mempool is large. No immediate incident response is warranted absent additional disclosure.
Security signals we found
Performance degradation / denial-of-service vector in RPC endpoint
Reduction in per-key-image LMDB transaction overhead
Reduction in mempool data exposure per RPC call
No memory safety, cryptographic, or authorization changes observed
Evidence from the diff
The patch refactors is_key_image_spent handling: adds a batch has_key_images() method to BlockchainDB and the LMDB backend, exposes a span-based have_tx_keyimges_as_spent() in Blockchain, switches core::are_key_images_spent() to use the batched call, and updates the RPC handler to filter already-spent key images before checking the mempool. It also removes unused Boost serialization code from tx_pool.h. The prior implementation performed one LMDB transaction per on-chain key-image check and pulled the entire mempool’s spent-key-image list for every request, which caused major daemon slowdowns under load.
Changed components
src/blockchain_db/blockchain_db.cpp/hsrc/blockchain_db/lmdb/db_lmdb.cpp/hsrc/cryptonote_core/blockchain.cpp/hsrc/cryptonote_core/cryptonote_core.cppsrc/cryptonote_core/tx_pool.hsrc/rpc/core_rpc_server.cppInspect captured patch +95 / −65
diff --git a/src/blockchain_db/blockchain_db.cpp b/src/blockchain_db/blockchain_db.cpp
index b290f81..72492f9 100644
--- a/src/blockchain_db/blockchain_db.cpp
+++ b/src/blockchain_db/blockchain_db.cpp
@@ -407,6 +407,17 @@ transaction BlockchainDB::get_pruned_tx(const crypto::hash& h) const
return tx;
}
+std::vector<bool> BlockchainDB::has_key_images(const epee::span<const crypto::key_image> img) const
+{
+ std::vector<bool> spent(img.size(), true);
+ for (std::size_t i = 0; i < img.size(); ++i)
+ {
+ const crypto::key_image &ki = img[i];
+ spent[i] = this->has_key_image(ki);
+ }
+ return spent;
+}
+
void BlockchainDB::reset_stats()
{
num_calls = 0;
diff --git a/src/blockchain_db/blockchain_db.h b/src/blockchain_db/blockchain_db.h
index 42591dc..2a82fc2 100644
--- a/src/blockchain_db/blockchain_db.h
+++ b/src/blockchain_db/blockchain_db.h
@@ -1510,6 +1510,15 @@ public:
*/
virtual bool has_key_image(const crypto::key_image& img) const = 0;
+ /**
+ * @brief check if key images are stored as spent
+ *
+ * @param img the key images to check for
+ *
+ * @return true at element `i` if the `img[i]` is present, otherwise false
+ */
+ virtual std::vector<bool> has_key_images(const epee::span<const crypto::key_image> img) const;
+
/**
* @brief add a txpool transaction
*
diff --git a/src/blockchain_db/lmdb/db_lmdb.cpp b/src/blockchain_db/lmdb/db_lmdb.cpp
index d5422e9..3acc8f5 100644
--- a/src/blockchain_db/lmdb/db_lmdb.cpp
+++ b/src/blockchain_db/lmdb/db_lmdb.cpp
@@ -3612,6 +3612,27 @@ bool BlockchainLMDB::has_key_image(const crypto::key_image& img) const
return ret;
}
+std::vector<bool> BlockchainLMDB::has_key_images(const epee::span<const crypto::key_image> img) const
+{
+ LOG_PRINT_L3("BlockchainLMDB::" << __func__);
+ check_open();
+
+ std::vector<bool> ret(img.size(), true);
+
+ TXN_PREFIX_RDONLY();
+ RCURSOR(spent_keys);
+
+ for (std::size_t i = 0; i < img.size(); ++i)
+ {
+ crypto::key_image ki = img[i];
+ MDB_val k = {sizeof(ki), reinterpret_cast<void*>(&ki)};
+ ret[i] = (mdb_cursor_get(m_cur_spent_keys, const_cast<MDB_val *>(&zerokval), &k, MDB_GET_BOTH) == 0);
+ }
+
+ TXN_POSTFIX_RDONLY();
+ return ret;
+}
+
bool BlockchainLMDB::for_all_key_images(std::function<bool(const crypto::key_image&)> f) const
{
LOG_PRINT_L3("BlockchainLMDB::" << __func__);
diff --git a/src/blockchain_db/lmdb/db_lmdb.h b/src/blockchain_db/lmdb/db_lmdb.h
index 45b3f37..1fad9f4 100644
--- a/src/blockchain_db/lmdb/db_lmdb.h
+++ b/src/blockchain_db/lmdb/db_lmdb.h
@@ -282,6 +282,8 @@ public:
bool has_key_image(const crypto::key_image& img) const override;
+ std::vector<bool> has_key_images(const epee::span<const crypto::key_image> img) const override;
+
void add_txpool_tx(const crypto::hash &txid, const cryptonote::blobdata_ref &blob, const txpool_tx_meta_t& meta) override;
void update_txpool_tx(const crypto::hash &txid, const txpool_tx_meta_t& meta) override;
uint64_t get_txpool_tx_count(relay_category category = relay_category::broadcasted) const override;
diff --git a/src/cryptonote_core/blockchain.cpp b/src/cryptonote_core/blockchain.cpp
index 44a427e..98ca572 100644
--- a/src/cryptonote_core/blockchain.cpp
+++ b/src/cryptonote_core/blockchain.cpp
@@ -3206,6 +3206,17 @@ bool Blockchain::have_tx_keyimges_as_spent(const transaction &tx) const
}
return false;
}
+//------------------------------------------------------------------
+std::vector<bool> Blockchain::have_tx_keyimges_as_spent(const epee::span<const crypto::key_image> key_imgs) const
+{
+ LOG_PRINT_L3("Blockchain::" << __func__);
+ // WARNING: this function does not take m_blockchain_lock, and thus should only call read only
+ // m_db functions which do not depend on one another (ie, no getheight + gethash(height-1), as
+ // well as not accessing class members, even read only (ie, m_invalid_blocks). The caller must
+ // lock if it is otherwise needed.
+ return m_db->has_key_images(key_imgs);
+}
+//------------------------------------------------------------------
bool Blockchain::expand_transaction_2(transaction &tx, const crypto::hash &tx_prefix_hash, const std::vector<std::vector<rct::ctkey>> &pubkeys)
{
PERF_TIMER(expand_transaction_2);
diff --git a/src/cryptonote_core/blockchain.h b/src/cryptonote_core/blockchain.h
index 78f47b6..25ae3e9 100644
--- a/src/cryptonote_core/blockchain.h
+++ b/src/cryptonote_core/blockchain.h
@@ -286,6 +286,15 @@ namespace cryptonote
*/
bool have_tx_keyimges_as_spent(const transaction &tx) const;
+ /**
+ * @brief check if key images are already spent on the blockchain
+ *
+ * @param key_imgs the key images to search for
+ *
+ * @return true at element `i` iff `key_imgs[i]` is already spent in the blockchain, else false
+ */
+ std::vector<bool> have_tx_keyimges_as_spent(const epee::span<const crypto::key_image> key_imgs) const;
+
/**
* @brief check if a key image is already spent on the blockchain
*
diff --git a/src/cryptonote_core/cryptonote_core.cpp b/src/cryptonote_core/cryptonote_core.cpp
index a537d8b..c7d3017 100644
--- a/src/cryptonote_core/cryptonote_core.cpp
+++ b/src/cryptonote_core/cryptonote_core.cpp
@@ -933,11 +933,7 @@ namespace cryptonote
//-----------------------------------------------------------------------------------------------
bool core::are_key_images_spent(const std::vector<crypto::key_image>& key_im, std::vector<bool> &spent) const
{
- spent.clear();
- for(auto& ki: key_im)
- {
- spent.push_back(m_blockchain_storage.have_tx_keyimg_as_spent(ki));
- }
+ spent = m_blockchain_storage.have_tx_keyimges_as_spent(epee::to_span(key_im));
return true;
}
//-----------------------------------------------------------------------------------------------
diff --git a/src/cryptonote_core/tx_pool.h b/src/cryptonote_core/tx_pool.h
index 88afc32..8528e63 100644
--- a/src/cryptonote_core/tx_pool.h
+++ b/src/cryptonote_core/tx_pool.h
@@ -37,7 +37,6 @@
#include <unordered_map>
#include <unordered_set>
#include <queue>
-#include <boost/serialization/version.hpp>
#include <boost/utility.hpp>
#include <boost/bimap.hpp>
#include <boost/bimap/set_of.hpp>
@@ -450,9 +449,6 @@ namespace cryptonote
*/
void reduce_txpool_weight(size_t weight);
-#define CURRENT_MEMPOOL_ARCHIVE_VER 11
-#define CURRENT_MEMPOOL_TX_DETAILS_ARCHIVE_VER 13
-
/**
* @brief information about a single transaction
*/
@@ -724,38 +720,3 @@ private:
friend struct BlockchainAndPool;
};
}
-
-namespace boost
-{
- namespace serialization
- {
- template<class archive_t>
- void serialize(archive_t & ar, cryptonote::tx_memory_pool::tx_details& td, const unsigned int version)
- {
- ar & td.blob_size;
- ar & td.fee;
- ar & td.tx;
- ar & td.max_used_block_height;
- ar & td.max_used_block_id;
- ar & td.last_failed_height;
- ar & td.last_failed_id;
- ar & td.receive_time;
- ar & td.last_relayed_time;
- ar & td.relayed;
- if (version < 11)
- return;
- ar & td.kept_by_block;
- if (version < 12)
- return;
- ar & td.do_not_relay;
- if (version < 13)
- return;
- ar & td.weight;
- }
- }
-}
-BOOST_CLASS_VERSION(cryptonote::tx_memory_pool, CURRENT_MEMPOOL_ARCHIVE_VER)
-BOOST_CLASS_VERSION(cryptonote::tx_memory_pool::tx_details, CURRENT_MEMPOOL_TX_DETAILS_ARCHIVE_VER)
-
-
-
diff --git a/src/rpc/core_rpc_server.cpp b/src/rpc/core_rpc_server.cpp
index f1964e3..c1b5199 100644
--- a/src/rpc/core_rpc_server.cpp
+++ b/src/rpc/core_rpc_server.cpp
@@ -1269,6 +1269,7 @@ namespace cryptonote
CHECK_PAYMENT_MIN1(req, res, req.key_images.size() * COST_PER_KEY_IMAGE, false);
+ // parse key images from request
std::vector<crypto::key_image> key_images;
for(const auto& ki_hex_str: req.key_images)
{
@@ -1286,44 +1287,53 @@ namespace cryptonote
crypto::key_image &ki = key_images.emplace_back();
memcpy(&ki, b.data(), sizeof(crypto::key_image));
}
+
+ // check key images in blockchain
std::vector<bool> spent_status;
bool r = m_core.are_key_images_spent(key_images, spent_status);
- if(!r)
+ if (!r || spent_status.size() != key_images.size())
{
res.status = "Failed";
return true;
}
res.spent_status.clear();
+ res.spent_status.reserve(spent_status.size());
for (size_t n = 0; n < spent_status.size(); ++n)
res.spent_status.push_back(spent_status[n] ? COMMAND_RPC_IS_KEY_IMAGE_SPENT::SPENT_IN_BLOCKCHAIN : COMMAND_RPC_IS_KEY_IMAGE_SPENT::UNSPENT);
+ // filter out known spent key images
+ std::vector<crypto::key_image> filtered_key_images;
+ std::vector<std::size_t> filtered_key_image_idxs;
+ filtered_key_images.reserve(key_images.size());
+ filtered_key_image_idxs.reserve(key_images.size());
+ for (std::size_t i = 0; i < key_images.size(); ++i)
+ {
+ if (!spent_status.at(i))
+ {
+ filtered_key_images.push_back(key_images.at(i));
+ filtered_key_image_idxs.push_back(i);
+ }
+ }
+ if (filtered_key_images.size() != filtered_key_image_idxs.size())
+ {
+ res.status = "Failed";
+ return true;
+ }
+
// check the pool too
- std::vector<cryptonote::tx_info> txs;
- std::vector<cryptonote::spent_key_image_info> ki;
- r = m_core.get_pool_transactions_and_spent_keys_info(txs, ki, !request_has_rpc_origin || !restricted);
- if(!r)
+ spent_status.clear();
+ r = m_core.are_key_images_spent_in_pool(filtered_key_images, spent_status);
+ if (!r || spent_status.size() != filtered_key_images.size())
{
res.status = "Failed";
return true;
}
- for (std::vector<cryptonote::spent_key_image_info>::const_iterator i = ki.begin(); i != ki.end(); ++i)
+ for (std::size_t i = 0; i < spent_status.size(); ++i)
{
- crypto::hash hash;
- crypto::key_image spent_key_image;
- if (parse_hash256(i->id_hash, hash))
+ if (spent_status.at(i))
{
- memcpy(&spent_key_image, &hash, sizeof(hash)); // a bit dodgy, should be other parse functions somewhere
- for (size_t n = 0; n < res.spent_status.size(); ++n)
- {
- if (res.spent_status[n] == COMMAND_RPC_IS_KEY_IMAGE_SPENT::UNSPENT)
- {
- if (key_images[n] == spent_key_image)
- {
- res.spent_status[n] = COMMAND_RPC_IS_KEY_IMAGE_SPENT::SPENT_IN_POOL;
- break;
- }
- }
- }
+ const std::size_t res_idx = filtered_key_image_idxs.at(i);
+ res.spent_status.at(res_idx) = COMMAND_RPC_IS_KEY_IMAGE_SPENT::SPENT_IN_POOL;
}
}
Why this scored 18/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.