-
-
Notifications
You must be signed in to change notification settings - Fork 3.3k
Tx Relay v2: New Relay Logic with threshold-based peer dropping #9933
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Tx Relay v2: New Relay Logic with threshold-based peer dropping #9933
Conversation
3022add
to
e13b7f0
Compare
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
src/cryptonote_config.h
Outdated
|
||
#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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this 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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed the padding here. Fixed. Feel free to resolve it.
std::string _; // padding | ||
|
||
BEGIN_KV_SERIALIZE_MAP() | ||
KV_SERIALIZE_CONTAINER_POD_AS_BLOB(txs) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// 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); | ||
} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
andlevin_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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added padding here. Fixed. Feel free to resolve it.
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); |
There was a problem hiding this comment.
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.
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?
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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:
} | ||
else | ||
{ | ||
m_peer_info_manager.add_received(context.m_connection_id); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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; | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
// 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); | ||
} |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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(), |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 ).
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 anunordered_set
and then all of thesefind_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
e13b7f0
to
f2061bf
Compare
d024d36
to
3fcd5c7
Compare
Co-authored-by: Boog900 <boog900@tutanota.com>
3fcd5c7
to
f345aea
Compare
that compiler generates implicitly.
* Removed extra classes. * Used on_idle as callback mechanism.
9acac8d
to
2ac323d
Compare
This is anecdotal data—consider it a single data point rather than a controlled, scientific experiment. Test Setup
Baseline (No Transactions)With no transactions in flight, each new block transfers roughly 5 MBit/s. This establishes our baseline bandwidth usage: ![]() Master Branch (With Transactions)When transactions are enabled and the tx pool is active, the master configuration averages about 20 MBit/s: ![]() 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: ![]() Although this isn’t a formal experiment, the results are stable across multiple runs and varied configurations. Benchmarking ToolAll measurements were collected using a simple benchmarking tool I wrote for |
2ac323d
to
5c46f6f
Compare
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:
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:
Peer Reliability System
Peer Scoring Metrics (
peerinfo_manager
):Tracked per connection:
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:
Configuration