Skip to content

Conversation

0xFFFC0000
Copy link
Collaborator

@0xFFFC0000 0xFFFC0000 commented May 22, 2025

Tx Relay v2: Enhanced Transaction Propagation Protocol #9334

This was made possible thanks to @Boog900’s support in designing and implementing each step.

Key Improvements & Technical Implementation

1. Inventory-Based Transaction Announcements

Protocol Messages:

NOTIFY_TX_POOL_INV (ID 2011):
- Broadcasts transaction hashes instead of full TXs
- Contains vector of crypto::hash entries

NOTIFY_REQUEST_TX_POOL_TXS (ID 2012):
- Requests specific TXs by hash
- Enables selective fetching of missing transactions

Workflow:

  • Node advertises new TX hashes via NOTIFY_TX_POOL_INV

  • Receivers check local mempool & request missing TXs with NOTIFY_REQUEST_TX_POOL_TXS

  • Only requested transactions are transmitted

Bandwidth Savings:

  • Reduces initial broadcast size by ~99% (hash vs full TX)

Peer Reliability System

Peer Scoring Metrics (peerinfo_manager):

  • Tracked per connection:

    • Announcements received
    • Successful TX deliveries
    • Missed requests
    • Request/response ratios
    • Timestamps for temporal analysis
  • Threshold-based eviction:
    if (missed_requests / total_announcements) > 70%: disconnect_peer()

Failure Detection:

  • Tracks 3 consecutive failures for any TX request

  • Uses sliding window (last 30 requests) for fairness

  • Penalizes peers for:

    • Unresponsive behavior

    • Invalid transaction responses

Components:

[Request Manager] <-handles-> [TX Request Queue]
[TX Request Queue] <-tracks-> [Peer UUIDs]
[TX Handler] <-manages-> [Timeouts]

Configuration

 // cryptonote_config.h
#define P2P_DEFAULT_REQUEST_TIMEOUT 5      // Seconds
#define P2P_REQUEST_FAILURE_THRESHOLD 70   // Percentage

@0xFFFC0000 0xFFFC0000 force-pushed the dev/0xfffc/tx-relay-v2 branch 2 times, most recently from 3022add to e13b7f0 Compare May 22, 2025 10:49
case cryptonote::NOTIFY_TX_POOL_INV::ID:
return 1024 * 1024 * 16; // 16 MB
case cryptonote::NOTIFY_REQUEST_TX_POOL_TXS::ID:
return 1024 * 1024 * 16; // 16 MB
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

16 MB is 524288 32-byte hashes. Do we really need to request 500k transactions at a time? I think a few thousand will be max, so 1 MB will be more than enough.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a note: the default txpool config is something like 648mb, or about max 432k txs

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this limit makes sense then. But it doesn't have to be this big anyway - peers can send multiple messages if there are too many transactions.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These messages should contain no where near the number of txs as there are in the pool. I would say 1 MB would be good, if we really wanted to push it we could go with the amount of txs needed to reach the 100 MB packet limit in NOTIFY_NEW_TRANSACTIONS: 100_000_000 / 1_500 = 66_666, 66_666 * 32 = 2_133_333. So 2 MB.

NOTIFY_GET_TXPOOL_COMPLEMENT which contains the ID of every tx in our pool itself has a 4 MB limit.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks to everyone participating in this discussion. I used 16MB as placeholders, since the tx-relay-v2 parameters will, of course, be a discussion topic when reviewing. I agree that @Boog900’s suggestion is a reasonable approach. Other limitations will kick in even before approaching 16MB.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the padding still there?

@Boog900 thoughts on padding the hashes?
My thought was that its unnecessary. Padding txs makes sense (privacy. Different txs have different sizes) but not the hashes, which should be(?) uniform in size

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the padding still there?

@Boog900 thoughts on padding the hashes? My thought was that its unnecessary. Padding txs makes sense (privacy. Different txs have different sizes) but not the hashes, which should be(?) uniform in size

Yes, we do still have padding in v2. It is quite easy to disable it too, if necessary. The granularity of padding is 128 bytes.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to: #9933 (comment)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


#define P2P_FAILED_ADDR_FORGET_SECONDS (60*60) //1 hour
#define P2P_IP_BLOCKTIME (60*60*24) //24 hour
#define P2P_IP_FAILS_BEFORE_BLOCK 10
#define P2P_IDLE_CONNECTION_KILL_INTERVAL (5*60) //5 minutes

#define P2P_SUPPORT_FLAG_FLUFFY_BLOCKS 0x01
#define P2P_SUPPORT_FLAGS P2P_SUPPORT_FLAG_FLUFFY_BLOCKS
#define P2P_SUPPORT_FLAG_V2 0x02
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If v2 adds only notify_tx messages, I would call this flag P2P_SUPPORT_FLAG_NOTIFY_TX

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say P2P_SUPPORT_FLAG_TX_RELAY_V2, the "old" message is called NOTIFY_NEW_TRANSACTIONS.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2P_SUPPORT_FLAG_TX_RELAY_V2 seems more reasonable. I have nothing against P2P_SUPPORT_FLAG_NOTIFY_TX, but P2P_SUPPORT_FLAG_NOTIFY_TX name does not show that we are using totally separate method of tx relay.

I will update it in next push.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Feel free to resolve it.

Copy link
Contributor

@Boog900 Boog900 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First pass, thanks for working on this 🙏

struct request_t
{
std::vector<crypto::hash> txs;
std::string _; // padding
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO these messages do not need padding, the old method sent the txs whole meaning any size differences could be used to tell which tx(s) are being sent. The only thing you will be able to tell with the new method is the amount of txs being sent. Also this message is only used to fluff txs not to stem.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also if we are padding this one we also should pad NOTIFY_REQUEST_TX_POOL_TXS as that will leak data too.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

std::string _; // padding

BEGIN_KV_SERIALIZE_MAP()
KV_SERIALIZE_CONTAINER_POD_AS_BLOB(txs)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

naming this field t instead of txs could save a couple bytes per broadcast, free savings that we might as well make use of.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

std::vector<crypto::hash> txs;

BEGIN_KV_SERIALIZE_MAP()
KV_SERIALIZE_CONTAINER_POD_AS_BLOB(txs)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here with the field name.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines 988 to 1011
// Create a response (for new transactions)
NOTIFY_NEW_TRANSACTIONS::request response = {};

// Iterate over requested txin hashes
for (const auto &tx_hash : arg.txs)
{
m_peer_info_manager.add_requested_from_me(context.m_connection_id);
// Attempt to get the transaction blob from the mempool;
cryptonote::blobdata tx_blob;
if (m_core.get_pool_transaction(tx_hash, tx_blob, cryptonote::relay_category::all))
{
m_peer_info_manager.add_sent(context.m_connection_id);
response.txs.push_back(std::move(tx_blob));
}
// If tx is not in the pool, then ignore it (do not penalize peer)
}

// Send response if any txs found
if (!response.txs.empty())
{
post_notify<NOTIFY_NEW_TRANSACTIONS>(response, context);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because this is handling part of the fluff protocol we might need to include padding here? The old code in levin_notify seems to be adding padding for certain network zones for fluff txs.

@vtnerd thoughts, if padding is needed here?

Copy link
Contributor

@vtnerd vtnerd May 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was duplicated from the thread above, so removing.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GitHub was too slick for me. Editing one modified both. Here's what I original said:

  • Padding mostly helps with Tor/I2P when noise is OFF. In this PR, only number of hashes is potentially leaked, so I don't see a massive need for including it.
  • Padding this way should be avoided in favor of what is done in my SSL patch. See levin_base.h and levin_base.cpp. The inner epee format is self-describing so you can safely write random bytes after the payload, and they will get dropped/ignored. This is quicker, more efficient, and less error prone than doing it with the _ field.
    • The SSL will deprecate this padding code if merged first, so might as well just import it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree padding is not necessary for the new messages. Although here is where we are sending the full txs in response to a request for them.

The old protocol looked like this:

node --- txs (maybe padded) --> node

the new protocol:

node --- inv (not padded) --> node
node <-- request (not padded) --- node
node --- txs (not padded) --> node

my question was about the last step. IMO we should pad to match the old protocol. Although if we never fluff over Tor/I2p this isn't really necessary.

Copy link
Contributor

@nahuhh nahuhh May 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although if we never fluff over Tor/I2p this isn't really necessary.

We do fluff (immediately) to tor/i2p AIUI (when disable_noise is on)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm yeah it looks like we do, I guess we need to add padding here then.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines 213 to 215
size_t bytes = 9 /* header */ + 4 /* 1 + 'txs' */ + tools::get_varint_data(request.txs.size()).size();
for(auto tx_hash = request.txs.begin(); tx_hash!=request.txs.end(); ++tx_hash)
bytes += tools::get_varint_data(sizeof(*tx_hash)).size() + sizeof(*tx_hash);
Copy link
Contributor

@Boog900 Boog900 May 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The serialized message stores all hashes in a single string, so this is incorrect.

Suggested change
size_t bytes = 9 /* header */ + 4 /* 1 + 'txs' */ + tools::get_varint_data(request.txs.size()).size();
for(auto tx_hash = request.txs.begin(); tx_hash!=request.txs.end(); ++tx_hash)
bytes += tools::get_varint_data(sizeof(*tx_hash)).size() + sizeof(*tx_hash);
size_t bytes = 9 /* header */ + 4 /* 1 + 'txs' */ + tools::get_varint_data(request.txs.size() * 32).size() + request.txs.size() * 32;

Although even that is incorrect, it uses the consensus (COCOA BEANS) varint, not the epee varint and misses the field tag's size. This is the same as the make_tx_message padding calculation.

@vtnerd does this slight difference in calculation of the size vs the real size matter for the padding?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is why I promoted the usage of the technique the SSL PR used - it's more accurate, quicker, and easier to just pad the end of the levin message as the extra trailing data gets dropped. Arguably this feature should be dropped entirely (see below).

Assuming this implementation is kept, it's possibly leaking data, but it's unlikely to matter. This feature is only useful if the connection is encrypted, which currently occurs only over Tor/I2P, both of which are already padded to disrupt traffic shaping analysis. In other words, Tor/I2P already do what we want here.

When/if SSL gets merged, this should be re-thought as SSL provides no automatic padding, which is why I added randomized padding (at the levin "layer") in that patch. It pads a randomized amount up to 8KiB, but perhaps it should just "round up" like Tor/I2P do (if it works for those networks, surely it works for us).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to resolve this, we can ignore this comment since we have removed the padding:

#9933 (comment)

}
else
{
m_peer_info_manager.add_received(context.m_connection_id);
Copy link
Contributor

@Boog900 Boog900 May 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO this should take into account if we actually requested the tx from the peer. Currently the peer can send us an inv, we request it, they send a different tx and to us it looks like they have sent as many as we requested, when in reality they haven't.

Ignore, we count the number of missed txs directly instead of doing requested vs responded.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Feel free to resolve this.

Comment on lines 941 to 981
int t_cryptonote_protocol_handler<t_core>::handle_notify_tx_pool_inv(int command, NOTIFY_TX_POOL_INV::request& arg, cryptonote_connection_context& context)
{
MLOG_P2P_MESSAGE("Received NOTIFY_TX_POOL_INV (" << arg.txs.size() << " txes)");

// Create a list to hold transaction hashes missing in our local tx pool.
std::vector<crypto::hash> missing_tx_hashes;
std::time_t now = std::time(nullptr);

// Iterate over each advertised transaction hash and check our pool and our requested tracker.
for (const auto &tx_hash : arg.txs)
{
// If we have the tx in our pool, also remove it from request manager and skip.
if (m_core.pool_has_tx(tx_hash))
{
m_request_manager.remove_transaction(tx_hash);
continue;
}

bool need_request = m_request_manager.add_transaction(tx_hash, context.m_connection_id, now);
m_peer_info_manager.add_announcement(context.m_connection_id, tx_hash);
if (need_request) {
m_peer_info_manager.add_requested_from_peer(context.m_connection_id);
missing_tx_hashes.push_back(tx_hash);
}
}

// If there are missing transactions, send a RequestTxPoolTxs message.
if (!missing_tx_hashes.empty())
{
NOTIFY_REQUEST_TX_POOL_TXS::request req;
req.txs = std::move(missing_tx_hashes);
post_notify<NOTIFY_REQUEST_TX_POOL_TXS>(req, context);
MLOG_P2P_MESSAGE("Requested " << req.txs.size() << " missing transactions via RequestTxPoolTxs");
}
else
{
MLOG_P2P_MESSAGE("All advertised transactions are already in the pool or were requested recently");
}

return 1;
}
Copy link
Contributor

@Boog900 Boog900 May 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently from what I can see a peer has 30 seconds to bomb our node with as many tx invs as possible and we have to track each one. I think we should add a limit on the amount of inflight requests we make to a single peer.

So if we have more than like 50 requests to a single peer we should ignore all future invs from that peer until that number drops.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in the new push. Feel free to resolve this issue if you consider the implementation adequate.

tx_request_handler &operator=(tx_request_handler &&other) = delete;
tx_request_handler &operator=(const tx_request_handler &other) = delete;
tx_request_handler *operator&() = delete;
bool operator>(const tx_request_handler &other) const = delete;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

C++ doesn't define defaults for these, just remove them and the compiler will generate errors.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I know C++ doesn’t define them, it is left over that I didn’t want to touch. Since don’t want anyone to abuse this class in future.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to resolve this; fixed in updated push.


private:
// Thread for checking the transaction requests
std::thread m_handler_thread;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There has to be a way to implement this without some arbitrary thread tucked away. Admittedly I'm being lazy and not going through everything, but this just has to be the result of poor design choices.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need to do something periodically, use the "idle" callback. It gets called once a second.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This just has to be the result of poor design choices.

That is inaccurate statement. I don’t want to block existing operations. I know already idle gets called once a second. But doing stuff there means blocking it.

Copy link
Contributor

@vtnerd vtnerd May 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At a glance it looks like this std::thread "blocks" the ASIO threads with locks. Is that any better? The std::thread could be blocked for a while by the ASIO thread (or vice-versa)?

Just use the idle thread timer, it's there specifically for this purpose. As long as the locks are released within a reasonable time-frame it'll be fine. You might be able to create a strand implementation to prevent all blocking, but that'd be a bit more work I think.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might be able to create a strand implementation to prevent all blocking, but that'd be a bit more work I think.

This is a better approach IMHO. Let me look up what I can do.

All of your comments will be addressed in the next update.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to resolve this; fixed in updated push, and addressed all the criteria you mentioned.

I implemented this feature using on_idle. I did a few experiments and found on_idle robust.

About the concurrency mechanism, I kept the locking, since it is read/write (lightweight) locking. The issue with the strand is that I had to create another io_context ( and a thread to run it ). The cryptonote_protocol_handler does not have any io_context by itself.

Comment on lines 988 to 1011
// Create a response (for new transactions)
NOTIFY_NEW_TRANSACTIONS::request response = {};

// Iterate over requested txin hashes
for (const auto &tx_hash : arg.txs)
{
m_peer_info_manager.add_requested_from_me(context.m_connection_id);
// Attempt to get the transaction blob from the mempool;
cryptonote::blobdata tx_blob;
if (m_core.get_pool_transaction(tx_hash, tx_blob, cryptonote::relay_category::all))
{
m_peer_info_manager.add_sent(context.m_connection_id);
response.txs.push_back(std::move(tx_blob));
}
// If tx is not in the pool, then ignore it (do not penalize peer)
}

// Send response if any txs found
if (!response.txs.empty())
{
post_notify<NOTIFY_NEW_TRANSACTIONS>(response, context);
}
Copy link
Contributor

@vtnerd vtnerd May 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was duplicated from the thread above, so removing.

#include "string_tools.h"
#include "syncobj.h"

class peer_info_manager {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class shouldn't exist - you can fetch by uuid/connection_id already. Instead, the information should be tucked into the connection_context object.

This implementation creates several problems:

  • You have to separately prune the map when connections close
  • Everything EXCEPT "tx inventory" is stored in the connection_context object

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree. Connection context is mostly networking / connection related information. I am trying to have peer abstraction here without the networking information.

Eventually I want to have well defined statistical scoring system that would score the peer based on its performance. If this was more networking related information, your proposal would’ve made more sense. But at this abstraction layer I disagree with your proposal.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is already a scoring system in the connection_context. Are you looking to replace it?

And I fail to see how keeping a separate map of connections will be an overall improvement. This new map is tracking data foreach connection, thats why the map key is a connection uuid. Just use the map that is already there. This isn't integrated well.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your criticism about using connection_context is reasonable.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of your comments will be addressed in the next update.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to resolve it. I moved everything inside connection_context, and got rid of this class.

struct peer_info_state {
boost::uuids::uuid connection_id;
std::unordered_set<crypto::hash> announcements;
size_t received = 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need to track all of this information? Is it useful for debugging somehow?

Copy link
Collaborator Author

@0xFFFC0000 0xFFFC0000 May 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. That is the problem with our existing code. Used haphazard, temporary methods. And trying to fix later. No industry grade professional engineer is thinking about keeping few bytes, that can be used later, as problem.

Instead I am keeping all the info in central, well defined place. That can be used later. Possibly with a better way to statistically score peers and remove them if the score drops.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. That is the problem with our existing code. Used haphazard, temporary methods. And trying to fix later.

This is somewhat vague criticism. Do other systems of similar size have permanent (never needing maintenance) code? Zero bugs or something?

No industry grade professional engineer is thinking about keeping few bytes, that can be used later, as problem.

I never said it would be a memory issue; we sometimes track information of limited use in debugging. These field dumps can just clutter the log further in the worse case. I'm on the fence whether these fields help with debugging. If they helped you at some point, then perhaps they are useful.

I find that that tracing when particular events occur is more useful. Because you don't really know ordering or timing with these kinds of dumps, just that event X occurred Y times.

return m_data->missed;
}

std::string get_info() const {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just make this std::ostream operator<<(...). Its more efficient.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I got a bit lazy and didn't see if it was being used outside of std::ostream cases. So safely ignore if not always in use.

void add_peer(const boost::uuids::uuid &id) { m_peer_info.emplace(id); }

void remove_peer(const boost::uuids::uuid &id) {
auto it = std::find_if(m_peer_info.begin(), m_peer_info.end(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should be using an unordered_map<id, data> instead of an unordered_set and then all of these find_if calls become efficient hash lookups instead of log(n) iteration. Another reason to use the built-in connection tracking, which does this (relatively) efficiently already.

Copy link
Collaborator Author

@0xFFFC0000 0xFFFC0000 May 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is inaccurate statement IMHO. unordered_set and unordered_map are using same underlying data structure. For example, IIRC libc++ is using redblack tree to implement both. I think you are mistaken std::unordered_set with std::set

Both std::unordered_set and std::unordered_map offer average-case O(1) time for insert, find, and erase (hash-based) operations

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I grant you that use of std::find_if is wrong here. Which is leftover from first version I wrote. But std::unordered_set is okay.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I know they use the same underlying code. This has nothing to do with my comment. You have a natural case for map<uuid, struct> but instead are using set with shared_ptr. Use the uuid as a key, and use the struct (with uuid removed) as the value without shared_ptr.

And none of this matters, as all of this should be shoved into the connection_context.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And to clarify a bit, the design makes more sense if the shared_ptr was being used outside of this class. But the way its being used here doesn't seem helpful.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your analysis about not using shared_ptr is incorrect. This data is being used from multiple threads (potentially), and should support cases where the container ( whether set or map ) moves or reorganizes the data. So you need a shared_ptr here. Otherwise you will get a concurrency bug.

You have a natural case for map<uuid, struct> but instead are using set with shared_ptr

I think my explanation was a little bit confusing. I don't want to expose any internal approach to outside and want to abstract as much as possible. Yes, of course you can unordered_map<uuid, struct> here, but I don't want expose any complexity outside since I have to handle concurrency too ( plus abstracting away the details make the code much more readable / maintainable, contrary to have what we have in few places ).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of your comments will be addressed in the next update.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A shared_ptr was not useful originally, but may be useful after the ongoing re-design.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vtnerd thanks, will take that into account.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should be using an unordered_map<id, data> instead of an unordered_set and then all of these find_if calls become efficient hash lookups instead of log(n) iteration. Another reason to use the built-in connection tracking, which does this (relatively) efficiently already.

Addressed in updated push. Everything moved to connection_context, so no need to discuss this further. Thanks

Co-authored-by: Boog900 <boog900@tutanota.com>
@0xFFFC0000 0xFFFC0000 force-pushed the dev/0xfffc/tx-relay-v2 branch from 3fcd5c7 to f345aea Compare July 26, 2025 15:08
that compiler generates implicitly.
* Removed extra classes.
* Used on_idle as callback mechanism.
@0xFFFC0000 0xFFFC0000 force-pushed the dev/0xfffc/tx-relay-v2 branch from 9acac8d to 2ac323d Compare September 10, 2025 07:31
@0xFFFC0000
Copy link
Collaborator Author

This is anecdotal data—consider it a single data point rather than a controlled, scientific experiment.

Test Setup

  • 20 monerod nodes
  • 20 monero-wallet-rpc instances
  • Probabilistic mining to simulate real-world block production without the cost of RandomX computation
  • After synchronizing a few blocks, each node accumulates unlocked balance and then sends 10 % of that balance at random to another wallet on the same private testnet

Baseline (No Transactions)

With no transactions in flight, each new block transfers roughly 5 MBit/s. This establishes our baseline bandwidth usage:

image

Master Branch (With Transactions)

When transactions are enabled and the tx pool is active, the master configuration averages about 20 MBit/s:

image

PR #9933 (With Transactions)

Applying PR #9933 consistently cuts bandwidth by at least 50 %, bringing the average down to around 12 MBit/s under similar transaction load:

image

Although this isn’t a formal experiment, the results are stable across multiple runs and varied configurations.

Benchmarking Tool

All measurements were collected using a simple benchmarking tool I wrote for monerod [1].

  1. https://github.com/0xFFFC0000/benchmark-project

@0xFFFC0000 0xFFFC0000 force-pushed the dev/0xfffc/tx-relay-v2 branch from 2ac323d to 5c46f6f Compare September 10, 2025 09:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants