zmq: apply restricted-mode privacy filtering to get_transaction_pool
What changed, and why it matters
This commit fixes a privacy gap in Monero's restricted ZeroMQ RPC mode. Before the patch, someone using the --restricted-zmq-rpc option could still see exact times when transactions arrived and were last relayed through the get_transaction_pool ZMQ call. The HTTP restricted mode already hid those timing details, but the ZMQ path did not. The patch adds the same on/off privacy filter to the ZMQ path so restricted callers now get zeroed-out timestamps, matching restricted HTTP behavior. Timing metadata can help an observer deanonymize or trace transactions, so this is a privacy hardening fix.
Apply the patch and ensure nodes running --restricted-zmq-rpc are restarted. Operators exposing ZMQ RPC publicly should verify restricted mode is enabled. No immediate incident response is indicated beyond normal patching.
Security signals we found
Privacy filter inconsistency between HTTP and ZMQ restricted RPC paths
Exposure of transaction receive_time and last_relayed_time to restricted ZMQ callers
Timing metadata can aid transaction-source deanonymization / network-level traffic analysis
Patch closes the gap by reusing existing HTTP masking logic in ZMQ path
Evidence from the diff
The change threads an include_sensitive boolean through tx_memory_pool::get_pool_for_rpc and its core passthrough, mirroring include_sensitive_data in get_transactions_and_spent_keys_info. When include_sensitive is false, receive_time and last_relayed_time are set to 0. The ZMQ daemon_handler.cpp now passes !m_restricted, so restricted ZMQ callers receive filtered pool data. Stem-phase transactions are still excluded via the existing relay_category::broadcasted filter. A test call is updated to pass true to preserve prior behavior.
Changed components
src/cryptonote_core/tx_pool.cppsrc/cryptonote_core/tx_pool.hsrc/cryptonote_core/cryptonote_core.cppsrc/cryptonote_core/cryptonote_core.hsrc/rpc/daemon_handler.cpptests/core_tests/tx_pool.cppInspect captured patch +14 / −10
diff --git a/src/cryptonote_core/cryptonote_core.cpp b/src/cryptonote_core/cryptonote_core.cpp
index 8580017..8e326e7 100644
--- a/src/cryptonote_core/cryptonote_core.cpp
+++ b/src/cryptonote_core/cryptonote_core.cpp
@@ -1606,9 +1606,9 @@ namespace cryptonote
return m_mempool.get_transactions_and_spent_keys_info(tx_infos, key_image_infos, include_sensitive_data);
}
//-----------------------------------------------------------------------------------------------
- bool core::get_pool_for_rpc(std::vector<cryptonote::rpc::tx_in_pool>& tx_infos, cryptonote::rpc::key_images_with_tx_hashes& key_image_infos) const
+ bool core::get_pool_for_rpc(std::vector<cryptonote::rpc::tx_in_pool>& tx_infos, cryptonote::rpc::key_images_with_tx_hashes& key_image_infos, bool include_sensitive) const
{
- return m_mempool.get_pool_for_rpc(tx_infos, key_image_infos);
+ return m_mempool.get_pool_for_rpc(tx_infos, key_image_infos, include_sensitive);
}
//-----------------------------------------------------------------------------------------------
bool core::get_short_chain_history(std::list<crypto::hash>& ids, uint64_t& current_height) const
diff --git a/src/cryptonote_core/cryptonote_core.h b/src/cryptonote_core/cryptonote_core.h
index 2cea119..588ae4a 100644
--- a/src/cryptonote_core/cryptonote_core.h
+++ b/src/cryptonote_core/cryptonote_core.h
@@ -556,10 +556,11 @@ namespace cryptonote
/**
* @copydoc tx_memory_pool::get_pool_for_rpc
+ * @param include_sensitive include node-private fields (timing)
*
* @note see tx_memory_pool::get_pool_for_rpc
*/
- bool get_pool_for_rpc(std::vector<cryptonote::rpc::tx_in_pool>& tx_infos, cryptonote::rpc::key_images_with_tx_hashes& key_image_infos) const;
+ bool get_pool_for_rpc(std::vector<cryptonote::rpc::tx_in_pool>& tx_infos, cryptonote::rpc::key_images_with_tx_hashes& key_image_infos, bool include_sensitive) const;
/**
* @copydoc tx_memory_pool::get_transactions_count
diff --git a/src/cryptonote_core/tx_pool.cpp b/src/cryptonote_core/tx_pool.cpp
index 94ce764..e51fa67 100644
--- a/src/cryptonote_core/tx_pool.cpp
+++ b/src/cryptonote_core/tx_pool.cpp
@@ -1253,13 +1253,13 @@ namespace cryptonote
return true;
}
//---------------------------------------------------------------------------------
- bool tx_memory_pool::get_pool_for_rpc(std::vector<cryptonote::rpc::tx_in_pool>& tx_infos, cryptonote::rpc::key_images_with_tx_hashes& key_image_infos) const
+ bool tx_memory_pool::get_pool_for_rpc(std::vector<cryptonote::rpc::tx_in_pool>& tx_infos, cryptonote::rpc::key_images_with_tx_hashes& key_image_infos, bool include_sensitive) const
{
CRITICAL_REGION_LOCAL(m_transactions_lock);
CRITICAL_REGION_LOCAL1(m_blockchain);
tx_infos.reserve(m_blockchain.get_txpool_tx_count());
key_image_infos.reserve(m_blockchain.get_txpool_tx_count());
- m_blockchain.for_all_txpool_txes([&tx_infos, key_image_infos](const crypto::hash &txid, const txpool_tx_meta_t &meta, const cryptonote::blobdata_ref *bd){
+ m_blockchain.for_all_txpool_txes([&tx_infos, key_image_infos, include_sensitive](const crypto::hash &txid, const txpool_tx_meta_t &meta, const cryptonote::blobdata_ref *bd){
cryptonote::rpc::tx_in_pool txi;
txi.tx_hash = txid;
if (!(meta.pruned ? parse_and_validate_tx_base_from_blob(*bd, txi.tx) : parse_and_validate_tx_from_blob(*bd, txi.tx)))
@@ -1277,9 +1277,11 @@ namespace cryptonote
txi.max_used_block_hash = meta.max_used_block_id;
txi.last_failed_block_height = meta.last_failed_height;
txi.last_failed_block_hash = meta.last_failed_id;
- txi.receive_time = meta.receive_time;
+ // In restricted mode we do not include this data:
+ txi.receive_time = include_sensitive ? meta.receive_time : 0;
txi.relayed = meta.relayed;
- txi.last_relayed_time = meta.dandelionpp_stem ? 0 : meta.last_relayed_time;
+ // In restricted mode we do not include this data:
+ txi.last_relayed_time = (include_sensitive && !meta.dandelionpp_stem) ? meta.last_relayed_time : 0;
txi.do_not_relay = meta.do_not_relay;
txi.double_spend_seen = meta.double_spend_seen;
tx_infos.push_back(txi);
diff --git a/src/cryptonote_core/tx_pool.h b/src/cryptonote_core/tx_pool.h
index ca2a79c..4c812c1 100644
--- a/src/cryptonote_core/tx_pool.h
+++ b/src/cryptonote_core/tx_pool.h
@@ -341,10 +341,11 @@ namespace cryptonote
*
* @param tx_infos [out] the transactions' information
* @param key_image_infos [out] the spent key images' information
+ * @param include_sensitive include fields that are sensitive to node privacy
*
* @return true
*/
- bool get_pool_for_rpc(std::vector<cryptonote::rpc::tx_in_pool>& tx_infos, cryptonote::rpc::key_images_with_tx_hashes& key_image_infos) const;
+ bool get_pool_for_rpc(std::vector<cryptonote::rpc::tx_in_pool>& tx_infos, cryptonote::rpc::key_images_with_tx_hashes& key_image_infos, bool include_sensitive) const;
/**
* @brief check for presence of key images in the pool
diff --git a/src/rpc/daemon_handler.cpp b/src/rpc/daemon_handler.cpp
index ef7403c..087b4c4 100644
--- a/src/rpc/daemon_handler.cpp
+++ b/src/rpc/daemon_handler.cpp
@@ -756,7 +756,7 @@ namespace rpc
void DaemonHandler::handle(const GetTransactionPool::Request& req, GetTransactionPool::Response& res)
{
- bool r = m_core.get_pool_for_rpc(res.transactions, res.key_images);
+ bool r = m_core.get_pool_for_rpc(res.transactions, res.key_images, !m_restricted);
if (!r) res.status = Message::STATUS_FAILED;
else res.status = Message::STATUS_OK;
diff --git a/tests/core_tests/tx_pool.cpp b/tests/core_tests/tx_pool.cpp
index 1bee8ef..87b3fc5 100644
--- a/tests/core_tests/tx_pool.cpp
+++ b/tests/core_tests/tx_pool.cpp
@@ -455,7 +455,7 @@ bool txpool_double_spend_base::check_changed(cryptonote::core& c, const size_t e
{
std::vector<cryptonote::rpc::tx_in_pool> infos{};
cryptonote::rpc::key_images_with_tx_hashes key_images{};
- if (!c.get_pool_for_rpc(infos, key_images) || infos.size() != m_broadcasted_hashes.size() || key_images.size() != m_broadcasted_hashes.size())
+ if (!c.get_pool_for_rpc(infos, key_images, true) || infos.size() != m_broadcasted_hashes.size() || key_images.size() != m_broadcasted_hashes.size())
{
MERROR("Expected broadcasted rpc data to return " << m_broadcasted_hashes.size() << " but got " << infos.size() << " infos and " << key_images.size() << "key images");
return false;
Why this scored 51/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.