-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
feat(eth-wire): Implement p2p
stream
#114
Conversation
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.
let's allow dead_code for now, otherwise, this is a bit hard to review in the browser.
crates/net/eth-wire/src/p2pstream.rs
Outdated
// snappy::encode(ping): [0x01, 0x00, 0x80] | ||
// snappy::encode(pong): [0x01, 0x00, 0x80] | ||
// snappy::encode(reason): [0x01, 0x00, reason as u8] | ||
// the reason for doing this is so we don't need to run it through the encoder or decoder. |
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.
Can probably implement it as a constant and add a test that checks they are correct
crates/net/eth-wire/src/p2pstream.rs
Outdated
// complete overhaul of the p2p protocol? | ||
// should we even accept protocol v4? | ||
|
||
// how should we accept arbitrary subprotocol messages? |
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 do we need arbitrary subprotocols and how would we use 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.
nice progress.
I think the main loop is halfway there, but since Ping/Pong are part of the message protocol, we can simplify how they are tracked
@@ -31,7 +34,8 @@ const MAX_P2P_MESSAGE_ID: u8 = P2PMessageID::Pong as u8; | |||
const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(10); | |||
const PING_TIMEOUT: Duration = Duration::from_secs(15); | |||
const PING_INTERVAL: Duration = Duration::from_secs(60); | |||
const MAX_FAILED_PINGS: usize = 3; | |||
const GRACE_PERIOD: Duration = Duration::from_secs(2); | |||
const MAX_FAILED_PINGS: u8 = 3; | |||
|
|||
/// A P2PStream wraps over any `Stream` that yields bytes and makes it compatible with `p2p` |
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 should mention what (part of) the protocol this represents.
crates/net/eth-wire/src/p2pstream.rs
Outdated
/// | ||
/// This task must be created only after the [`Hello`] handshake has been completed, so this | ||
/// method will return an error if the [`P2PStream`] is not yet authed. | ||
pub fn ping_task(&mut self) -> Result<impl Future<Output = Result<(), P2PStreamError>> + '_, P2PStreamError> { |
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 looks like the actual session loop?
There seems to big a big emphasis on ping/pongs here, but I don't think there are that important? aren't they just regular requests that expect a pong response?
crates/net/eth-wire/src/p2pstream.rs
Outdated
// when we are not waiting for a pong, we will only be selecting on the ping interval. | ||
// otherwise, we will be retrying the ping until we either get a pong or we have reached | ||
// the maximum number of retries. | ||
let task = async move { |
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.
we can simplify this by moving everything in the poll function and looping over messages
fn poll() {
// here we need to poll the POLL_INTERVAL or `pinger` type to decide if a pong did not arrive, or whether to send a new ping
while let Some(msg) = this.inner.poll_next(cx) {
// this is where we handle all messages including pings
}
}
The pinger looks good, we can add a poll(cx)
function that returns Poll<Result<Ping, Err>>
where Ok(Ping)
should trigger a new ping message and Err(Err) that a pong did not arrive as expected.
aed5093
to
78e41ab
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.
love the direction
crates/net/eth-wire/src/p2pstream.rs
Outdated
/// [`HANDSHAKE_TIMEOUT`] determines the amount of time to wait before determining that a `p2p` | ||
/// handshake has timed out. | ||
const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(10); | ||
const PING_TIMEOUT: Duration = Duration::from_secs(15); | ||
const PING_INTERVAL: Duration = Duration::from_secs(60); | ||
const GRACE_PERIOD: Duration = Duration::from_secs(2); | ||
const MAX_FAILED_PINGS: u8 = 3; |
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.
Are these protocol-specific? Or should they be made configurable?
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 are not protocol specific, they could definitely be made configurable (with these values as defaults)
crates/net/eth-wire/src/p2pstream.rs
Outdated
/// Whether the `Hello` handshake has been completed | ||
authed: bool, |
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 we should refactor the stream to UnauthenticatedStream
and P2PStream
and remove the auth: bool
variable. The UnauthenticatedStream
would only contain the handshake
method and it'd return a P2PStream
. This lets us remove the if self.authed
error code path from the start_send
function below: https://github.com/foundry-rs/reth/pull/114/files#diff-669cc8ac18463cf275fc4d66d3d4dff60e2ae6d4ff6e634098db97fc83dfdb47R427-R429.
If you think you can do apply this pattern in this PR, and do a follow-up refactor on the EthStream that'd be great.
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 that makes sense, we can enforce that a stream is authenticated at the type level
#[pin_project] | ||
pub struct P2PStream<S> { |
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.
We may want to add examples on how to use these, and their various message types. For a follow-up.
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.
great progress.
some comments re Stream+Sink and codec.
crates/net/eth-wire/src/p2pstream.rs
Outdated
/// An un-authenticated `P2PStream`. This is consumed and returns a [`P2PStream`] after the `Hello` | ||
/// handshake is completed. | ||
#[pin_project] | ||
pub struct UnauthedP2PStream<S> { | ||
#[pin] | ||
stream: P2PStream<S>, | ||
} |
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 great.
This is essentially a future type that does the handshake.
{ | ||
/// Consumes the `UnauthedP2PStream` and returns a `P2PStream` after the `Hello` handshake is | ||
/// completed. | ||
pub async fn handshake(mut self, hello: HelloMessage) -> Result<P2PStream<S>, P2PStreamError> { |
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 also needs to return the contents of the received hello message.
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.
tracking in #157
crates/net/eth-wire/src/p2pstream.rs
Outdated
|
||
let send_res = Pin::new(&mut this.inner).send(ping_bytes.into()).poll_unpin(cx)?; | ||
ready!(send_res) |
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 invalid usage of the Sink API here since this SinkExt::send
creates a future that resolves when the item has been sent.
the underlying stream should be Framed, right? so we should use a codec for the stream I think.
But I think this could be tricky for the sending part since we need to handle the offset?
crates/net/eth-wire/src/p2pstream.rs
Outdated
// we should loop here to ensure we don't return Poll::Pending if we have a message to | ||
// return behind any pings we need to respond to | ||
while let Poll::Ready(res) = this.inner.as_mut().poll_next(cx) { | ||
let mut bytes = match res { |
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 this guaranteed to be a framed message?
let send_res = Pin::new(&mut this.inner).send(pong_bytes.into()).poll_unpin(cx)?; | ||
ready!(send_res) |
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.
we need to use the actual sink api here which is always:
if poll_ready().is_ready() {
start_send(msg)
} else {
// need to buffer until sink ready.
}
see Sink trait docs.
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.
tracking in #156
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 looks very good now, some parts are not quite complete yet, but I'd suggest merging this now and do smaller iterations moving forward? Because I think the stream-type interfaces are somewhat stable now.
we only really need Stream+Sinkand handshake function I believe.
wdyt @Rjected
933bd73
to
31a6d22
Compare
@mattsse sure, let me get tests passing with the handshake and we can merge. I'll make some issues for things left to do |
* TODO: make it compile * TODO: test ping/pong/disconnect state machine * TODO: send subprotocol messages to stream * TODO: encode non-hello p2p messages as snappy encoding without using an encoder * TODO: create test comparing encoder to hand-written snappy encoding for ping, pong, disconnect messages
* restricts S to be Stream+Sink for P2PStream to implement Stream * start of a poll-based refactor
* add tests * TODO: make stream/sink types compatible * TODO: handshake message ids * TODO: inner poll fn * TODO: pinger interval * TODO: ethstream test * TODO: passthrough test
* it should be much easier to poll for pings and detect timeouts now
* change item produced by stream so it's compatible with EthStream * add note on pros/cons * shorten message sends in stream
* switch to snappy formatting for non-hello p2p messages
* remove check for `Hello` messages outside of the handshake because `P2PStream`s should assume messages sent in the sink are subprotocol messages, not `p2p` messages.
* disallow protocol versions other than v5
* todo: test shared capabilities * todo: determine how / when / why to support multiple capabilities * removes obsolete authed and offset fields
* should add protocols when necessary rather than name unsupported protocols
* add test for p2pstream over a passthrough codec which tests that peers agree on a single shared capability
9a06fc6
to
2f2b250
Compare
OK, I got tests to pass - we can probably merge this now, the TODOs / smaller suggestions can be converted to issues if that sounds good |
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.
Really clean
use bytes::{Buf, Bytes, BytesMut}; | ||
use futures::{ready, FutureExt, Sink, SinkExt, StreamExt}; | ||
use pin_project::pin_project; | ||
use reth_primitives::H512 as PeerId; |
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.
Should we add PeerId as a re export in primitives and note why it's that size? It seems common 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.
Yeah that's a good idea
WIP meant to implement #64, specifically the
p2p
connection partTODO:
p2p
handshakeStream
andSink
implementationsECIESStream
and another stream typeEthStream
test on top ofP2PStream
p2pstream.rs