-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Multiple improvements to discovery ping handling #8771
Conversation
It looks like @jimpo signed our Contributor License Agreement. 👍 Many thanks, Parity Technologies CLA Bot |
1230ef0
to
bd208f3
Compare
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! The changes look good to me.
util/network-devp2p/src/discovery.rs
Outdated
send_queue: VecDeque<Datagramm>, | ||
check_timestamps: bool, | ||
adding_nodes: Vec<NodeEntry>, | ||
ip_filter: IpFilter, | ||
request_backoff: Vec<Duration>, |
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.
What is the motivation for NodeBucket::request_backoff
? Why not declare REQUEST_BACKOFF
as a [Duration; 4]
?
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.
It's on Discovery, not NodeBucket. And the motivation is just injecting dependencies explicitly. It's nice because the backoff can be tweaked in tests and stuff (which I 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.
It's on Discovery, not NodeBucket.
Of course, sorry. Typo.
And the motivation is just injecting dependencies explicitly.
I was just thinking that since Discovery::new
refers to const REQUEST_BACKOFF
anyway only to map its integers to Duration
s, why not declare REQUEST_BACKOFF
itself as a &'static [Duration]
and only assign the reference to Duration::request_backoff
. After all, client code can not change the backoff setting and in test modules one can still define other backoff sequences and overwrite the default. But it was just a quick thought and certainly not important.
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 364c8e1. Let me know if you prefer that or want me to revert. Shouldn't make any difference performance-wise because Discovery is only instantiated once.
util/network-devp2p/src/discovery.rs
Outdated
self.adding_nodes = nodes; | ||
self.update_new_nodes(); | ||
pub fn add_node_list(&mut self, mut nodes: Vec<NodeEntry>) { | ||
for node in nodes.drain(..) { |
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.
No need to drain
because nodes
is moved here. for node in nodes { .. }
is sufficient.
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.
Will do. I assume the drain can be removed from init_node_list
as well? (which is why I did it here).
util/network-devp2p/src/discovery.rs
Outdated
}; | ||
packet[32..(32 + 65)].clone_from_slice(&signature[..]); | ||
let signed_hash = keccak(&packet[32..]); | ||
packet[0..32].clone_from_slice(&signed_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.
I think you can use copy_from_slice
.
util/network-devp2p/src/discovery.rs
Outdated
} | ||
|
||
/// Add a list of known nodes to the table. | ||
pub fn init_node_list(&mut self, mut nodes: Vec<NodeEntry>) { | ||
for n in nodes.drain(..) { | ||
for n in nodes { |
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 nodes
parameter here and in add_node_list
does not need to be declared as mut
.
d9a4ad9
to
b8581b3
Compare
rlp.begin_list(c.len()); | ||
for n in 0 .. c.len() { | ||
rlp.begin_list(4); | ||
c[n].endpoint.to_rlp(&mut rlp); | ||
rlp.append(&c[n].id); | ||
} | ||
append_expiration(&mut rlp); |
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 may be a stupid question but isn't this a modification of the protocol?
(because if so, what about compatibility?)
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.
No, this should not affect behavior. The change is that the expiry timestamp used to be appended in send_packet
and now it is appended to the RLP payload before calling that method.
util/network-devp2p/src/discovery.rs
Outdated
fn assemble_packet(packet_id: u8, bytes: &[u8], secret: &Secret) -> Result<Bytes, Error> { | ||
let mut packet = Bytes::with_capacity(bytes.len() + 32 + 65 + 1); | ||
packet.extend_from_slice(&[0; 32 + 65][..]); | ||
packet.extend_from_slice(&[packet_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.
No need to create temporaries:
packet.resize(32 + 65, 0);
packet.put_u8(packet_id);
util/network-devp2p/src/discovery.rs
Outdated
} else { false }; | ||
|
||
if remove_from_bucket { | ||
let id_hash = keccak(&node_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.
Why not moving this block directly after entry.remove();
?
if !is_expected { | ||
debug!(target: "discovery", "Got unexpected Neighbors from {:?}", &from); | ||
return Ok(None); | ||
} |
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 quite understand response_count
. If we receive two responses with 10 results each, this would only consider the first one and ignore the second response? I am probably thick, but I would be grateful if you could explain the idea a bit more. Also, would datagram duplication not interfere with response_count
s? (Not sure how much of a problem this would be in practice.)
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.
Yeah, this is not really ideal, but I don't have a better solution. Since the NEIGHBORS packet does not have any ID or hash that can be used to associate it with a particular request, the best we can do is ensure there is at most one FIND_NODE in flight to any node at all times and associate the response based on sender. The sender is not supposed to send more than k=16 results to any query, so if they do, this code will ignore the second 10 result packet in your example. As far as packet duplication, if it is a concern, we could dedup the results based on node ID and track unique results in the response count.
I'm open to suggestions here.
This function is useful inside unit tests.
Previously the discovery algorithm would add nodes to the routing table before confirming that the endpoint is participating in the protocol. This now tracks in-flight pings and adds to the routing table only after receiving a response.
Now that we may ping nodes before adding to a k-bucket, the timeout tracking must be separate from BucketEntry.
Stores packet hash with in-flight requests and matches with pong response.
UDP packets may get dropped, so instead of immediately booting nodes that fail to respond to a ping, retry 4 times with exponential backoff.
} | ||
}; | ||
if entry.get().response_count == BUCKET_SIZE { | ||
entry.remove(); |
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 a node reports less than BUCKET_SIZE
responses it will not have it's entry removed here and instead timeout. This will eventually cause the node from being removed altogether, no?
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.
Yeah, ideally the packet would have an indication that it's the end of a series of NEIGHBORS packets, but since it doesn't I think this is the best strategy. From my reading, Geth has the same behavior (https://github.com/ethereum/go-ethereum/blob/master/p2p/discover/udp.go#L329) of triggering a timeout if it receives fewer than k results (and more than k would be incorrect).
I'm assuming that this won't be too much of a problem in practice because 1) you only return <16 if there's <16 nodes in the whole routing table (which I suppose is possible on a small network) 2) there are 4 retries and if any of them are a PING, it will clear the failure count. In a future PR, it would probably be good to put something in to ensure we PING nodes that have failed a few FIND_NODE requests.
Another alternative would be to keep the PendingRequest alive but not treat it as a timeout if it clears with <16 responses. On the other hand, I think it's more likely in that case that we stay connected to a high latency node that continues to give incomplete NEIGHBORS responses without ever booting them.
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.
Another alternative would be to keep the PendingRequest alive but not treat it as a timeout if it clears with <16 responses.
This is what I had in mind, but I agree that the approach chosen here is probably better in practice.
} | ||
}; | ||
if entry.get().response_count == BUCKET_SIZE { | ||
entry.remove(); |
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.
Another alternative would be to keep the PendingRequest alive but not treat it as a timeout if it clears with <16 responses.
This is what I had in mind, but I agree that the approach chosen here is probably better in practice.
* discovery: Only add nodes to routing table after receiving pong. Previously the discovery algorithm would add nodes to the routing table before confirming that the endpoint is participating in the protocol. This now tracks in-flight pings and adds to the routing table only after receiving a response. * discovery: Refactor packet creation into its own function. This function is useful inside unit tests. * discovery: Additional testing for new add_node behavior. * discovery: Track expiration of pings to non-yet-in-bucket nodes. Now that we may ping nodes before adding to a k-bucket, the timeout tracking must be separate from BucketEntry. * discovery: Verify echo hash on pong packets. Stores packet hash with in-flight requests and matches with pong response. * discovery: Track timeouts on FIND_NODE requests. * discovery: Retry failed pings with exponential backoff. UDP packets may get dropped, so instead of immediately booting nodes that fail to respond to a ping, retry 4 times with exponential backoff. * !fixup Use slice instead of Vec for request_backoff.
* parity-version: betalize 2.0 * Multiple improvements to discovery ping handling (#8771) * discovery: Only add nodes to routing table after receiving pong. Previously the discovery algorithm would add nodes to the routing table before confirming that the endpoint is participating in the protocol. This now tracks in-flight pings and adds to the routing table only after receiving a response. * discovery: Refactor packet creation into its own function. This function is useful inside unit tests. * discovery: Additional testing for new add_node behavior. * discovery: Track expiration of pings to non-yet-in-bucket nodes. Now that we may ping nodes before adding to a k-bucket, the timeout tracking must be separate from BucketEntry. * discovery: Verify echo hash on pong packets. Stores packet hash with in-flight requests and matches with pong response. * discovery: Track timeouts on FIND_NODE requests. * discovery: Retry failed pings with exponential backoff. UDP packets may get dropped, so instead of immediately booting nodes that fail to respond to a ping, retry 4 times with exponential backoff. * !fixup Use slice instead of Vec for request_backoff. * Add separate database directory for light client (#8927) (#9064) * Add seperate default DB path for light client (#8927) * Improve readability * Revert "Replace `std::env::home_dir` with `dirs::home_dir` (#9077)" (#9097) * Revert "Replace `std::env::home_dir` with `dirs::home_dir` (#9077)" This reverts commit 7e77932. * Restore some of the changes * Update parity-common * Offload cull to IoWorker. (#9099) * Fix work-notify. (#9104) * Update hidapi, fixes #7542 (#9108) * docker: add cmake dependency (#9111) * Update light client hardcoded headers (#9098) * Insert Kovan hardcoded headers until #7690241 * Insert Kovan hardcoded headers until block 7690241 * Insert Ropsten hardcoded headers until #3612673 * Insert Mainnet hardcoded headers until block 5941249 * Make sure to produce full blocks. (#9115) * Insert ETC (classic) hardcoded headers until block #6170625 (#9121) * fix verification in ethcore-sync collect_blocks (#9135) * Completely remove all dapps struct from rpc (#9107) * Completely remove all dapps struct from rpc * Remove unused pub use * `evm bench` fix broken dependencies (#9134) * `evm bench` use valid dependencies Benchmarks of the `evm` used stale versions of a couple a crates that this commit fixes! * fix warnings * Update snapcraft.yaml (#9132)
This addresses some of the problems listed in #8757. The PR is rather large, so it is best reviewed commit by commit and could be split into multiple PRs if reviewers would like.
There are a few behavior changes:
There are more improvements to come (the k-buckets are still not limited to k entries), but I think this is a step in the right direction.