mirror of
https://github.com/hl-archive-node/nanoreth.git
synced 2025-12-06 10:59:55 +00:00
Implement ETH P2P (#81)
* refactor: move things to types
* feat(ethwire): bring in message type from ethp2p
30c11138d5/src/message.rs
* feat(ethwire): add eth-stream with Stream/Sink impls
* feat(ethwire): make Sink error an EthStreamError
* feat(ecies): expose util module
* add more deps
* feat: add broadcast newblockhashes
* fix: rlp serialize with message-id first
* chore: cleanup doc
* wip: test eth connection
* bump cargo lock
* feat: add rlp codec and get stream tests working
* fix: convert RlpCodec to PassthroughCodec
we were rlp encoding twice
* Revert "fix: convert RlpCodec to PassthroughCodec"
This reverts commit 6e6e0a58112c14d7ffba62dc83f9747ddc280641.
This does not handle framing, which would fail decoding if a partial
write happened to the socket.
* add tracing
* refactor: add handshake error
* feat(ethwire): add status handshake
* test(ethwire): handshake works
* refactor: expose EthStream::new
* chore: clippy lints
* fix: get rid of rlp codec
we can instead use LengthLimitedCodec from Tokio IO, which we re-export
as PassthroughCodec
* fix(eth): add handshake + msg error checks
1. 10MB message lengths
2. Same Genesis, Version, Chain on Status Handshake
* chore: ignore result_large_err
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
This commit is contained in:
committed by
GitHub
parent
5dfe5ac29b
commit
8009d997c0
9
Cargo.lock
generated
9
Cargo.lock
generated
@ -2199,12 +2199,21 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"crc",
|
"crc",
|
||||||
"ethers-core",
|
"ethers-core",
|
||||||
|
"futures",
|
||||||
"hex",
|
"hex",
|
||||||
"hex-literal",
|
"hex-literal",
|
||||||
"maplit",
|
"maplit",
|
||||||
|
"pin-project",
|
||||||
|
"rand",
|
||||||
|
"reth-ecies",
|
||||||
"reth-primitives",
|
"reth-primitives",
|
||||||
"reth-rlp",
|
"reth-rlp",
|
||||||
|
"secp256k1",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
|
"tokio-stream",
|
||||||
|
"tokio-util",
|
||||||
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@ -10,6 +10,7 @@ fn zeroless_view(v: &impl AsRef<[u8]>) -> &[u8] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Header {
|
impl Header {
|
||||||
|
/// Encodes the header into the `out` buffer.
|
||||||
pub fn encode(&self, out: &mut dyn BufMut) {
|
pub fn encode(&self, out: &mut dyn BufMut) {
|
||||||
if self.payload_length < 56 {
|
if self.payload_length < 56 {
|
||||||
let code = if self.list { EMPTY_LIST_CODE } else { EMPTY_STRING_CODE };
|
let code = if self.list { EMPTY_LIST_CODE } else { EMPTY_STRING_CODE };
|
||||||
@ -22,6 +23,13 @@ impl Header {
|
|||||||
out.put_slice(len_be);
|
out.put_slice(len_be);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the length of the encoded header
|
||||||
|
pub fn length(&self) -> usize {
|
||||||
|
let mut out = BytesMut::new();
|
||||||
|
self.encode(&mut out);
|
||||||
|
out.len()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn length_of_length(payload_length: usize) -> usize {
|
pub const fn length_of_length(payload_length: usize) -> usize {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
#[derive(Clone, Default, PartialEq)]
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||||
pub struct Header {
|
pub struct Header {
|
||||||
pub list: bool,
|
pub list: bool,
|
||||||
pub payload_length: usize,
|
pub payload_length: usize,
|
||||||
|
|||||||
@ -2,7 +2,7 @@ use crate::{
|
|||||||
algorithm::{ECIES, MAX_BODY_SIZE},
|
algorithm::{ECIES, MAX_BODY_SIZE},
|
||||||
ECIESError, EgressECIESValue, IngressECIESValue,
|
ECIESError, EgressECIESValue, IngressECIESValue,
|
||||||
};
|
};
|
||||||
use bytes::{Bytes, BytesMut};
|
use bytes::BytesMut;
|
||||||
use reth_primitives::H512 as PeerId;
|
use reth_primitives::H512 as PeerId;
|
||||||
use secp256k1::SecretKey;
|
use secp256k1::SecretKey;
|
||||||
use std::{fmt::Debug, io};
|
use std::{fmt::Debug, io};
|
||||||
@ -105,7 +105,8 @@ impl Decoder for ECIESCodec {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut data = buf.split_to(self.ecies.body_len());
|
let mut data = buf.split_to(self.ecies.body_len());
|
||||||
let ret = Bytes::copy_from_slice(self.ecies.read_body(&mut data)?);
|
let mut ret = BytesMut::new();
|
||||||
|
ret.extend_from_slice(self.ecies.read_body(&mut data)?);
|
||||||
|
|
||||||
self.state = ECIESState::Header;
|
self.state = ECIESState::Header;
|
||||||
return Ok(Some(IngressECIESValue::Message(ret)))
|
return Ok(Some(IngressECIESValue::Message(ret)))
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
use crate::IngressECIESValue;
|
use crate::IngressECIESValue;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
/// An error that occurs while reading or writing to an ECIES stream.
|
/// An error that occurs while reading or writing to an ECIES stream.
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
#![allow(clippy::result_large_err)]
|
||||||
#![warn(missing_docs, unreachable_pub)]
|
#![warn(missing_docs, unreachable_pub)]
|
||||||
#![deny(unused_must_use, rust_2018_idioms)]
|
#![deny(unused_must_use, rust_2018_idioms)]
|
||||||
#![doc(test(
|
#![doc(test(
|
||||||
@ -10,7 +11,7 @@
|
|||||||
pub mod algorithm;
|
pub mod algorithm;
|
||||||
pub mod mac;
|
pub mod mac;
|
||||||
pub mod stream;
|
pub mod stream;
|
||||||
mod util;
|
pub mod util;
|
||||||
|
|
||||||
mod error;
|
mod error;
|
||||||
pub use error::ECIESError;
|
pub use error::ECIESError;
|
||||||
@ -38,5 +39,5 @@ pub enum IngressECIESValue {
|
|||||||
/// Receiving an ACK message
|
/// Receiving an ACK message
|
||||||
Ack,
|
Ack,
|
||||||
/// Receiving a message
|
/// Receiving a message
|
||||||
Message(bytes::Bytes),
|
Message(bytes::BytesMut),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
//! The ECIES Stream implementation which wraps over [`AsyncRead`] and [`AsyncWrite`].
|
//! The ECIES Stream implementation which wraps over [`AsyncRead`] and [`AsyncWrite`].
|
||||||
use crate::{ECIESError, EgressECIESValue, IngressECIESValue};
|
use crate::{codec::ECIESCodec, ECIESError, EgressECIESValue, IngressECIESValue};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use futures::{ready, Sink, SinkExt};
|
use futures::{ready, Sink, SinkExt};
|
||||||
use reth_primitives::H512 as PeerId;
|
use reth_primitives::H512 as PeerId;
|
||||||
@ -19,8 +19,6 @@ use tokio_stream::{Stream, StreamExt};
|
|||||||
use tokio_util::codec::{Decoder, Framed};
|
use tokio_util::codec::{Decoder, Framed};
|
||||||
use tracing::{debug, instrument, trace};
|
use tracing::{debug, instrument, trace};
|
||||||
|
|
||||||
use crate::codec::ECIESCodec;
|
|
||||||
|
|
||||||
/// `ECIES` stream over TCP exchanging raw bytes
|
/// `ECIES` stream over TCP exchanging raw bytes
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ECIESStream<Io> {
|
pub struct ECIESStream<Io> {
|
||||||
@ -106,7 +104,7 @@ impl<Io> Stream for ECIESStream<Io>
|
|||||||
where
|
where
|
||||||
Io: AsyncRead + Unpin,
|
Io: AsyncRead + Unpin,
|
||||||
{
|
{
|
||||||
type Item = Result<Bytes, io::Error>;
|
type Item = Result<bytes::BytesMut, io::Error>;
|
||||||
|
|
||||||
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||||
match ready!(Pin::new(&mut self.get_mut().stream).poll_next(cx)) {
|
match ready!(Pin::new(&mut self.get_mut().stream).poll_next(cx)) {
|
||||||
@ -156,8 +154,6 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
// TODO: implement test for the proposed
|
|
||||||
// API: https://github.com/foundry-rs/reth/issues/64#issue-1408708420
|
|
||||||
async fn can_write_and_read() {
|
async fn can_write_and_read() {
|
||||||
let listener = TcpListener::bind("127.0.0.1:8080").await.unwrap();
|
let listener = TcpListener::bind("127.0.0.1:8080").await.unwrap();
|
||||||
let server_key = SecretKey::new(&mut rand::thread_rng());
|
let server_key = SecretKey::new(&mut rand::thread_rng());
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
//! Utility functions for hashing and encoding.
|
||||||
|
|
||||||
use hmac::{Hmac, Mac};
|
use hmac::{Hmac, Mac};
|
||||||
use reth_primitives::{H256, H512 as PeerId};
|
use reth_primitives::{H256, H512 as PeerId};
|
||||||
use secp256k1::PublicKey;
|
use secp256k1::PublicKey;
|
||||||
@ -22,7 +24,7 @@ pub(crate) fn hmac_sha256(key: &[u8], input: &[&[u8]], auth_data: &[u8]) -> H256
|
|||||||
|
|
||||||
/// Converts a [secp256k1::PublicKey] to a [PeerId] by stripping the
|
/// Converts a [secp256k1::PublicKey] to a [PeerId] by stripping the
|
||||||
/// SECP256K1_TAG_PUBKEY_UNCOMPRESSED tag and storing the rest of the slice in the [PeerId].
|
/// SECP256K1_TAG_PUBKEY_UNCOMPRESSED tag and storing the rest of the slice in the [PeerId].
|
||||||
pub(crate) fn pk2id(pk: &PublicKey) -> PeerId {
|
pub fn pk2id(pk: &PublicKey) -> PeerId {
|
||||||
PeerId::from_slice(&pk.serialize_uncompressed()[1..])
|
PeerId::from_slice(&pk.serialize_uncompressed()[1..])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "reth-eth-wire"
|
name = "reth-eth-wire"
|
||||||
|
description = "Impements the eth/64 and eth/65 P2P protocols"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
@ -12,13 +13,22 @@ hex = "0.4"
|
|||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
|
|
||||||
# reth
|
# reth
|
||||||
|
reth-ecies = { path = "../ecies" }
|
||||||
reth-primitives = { path = "../../primitives" }
|
reth-primitives = { path = "../../primitives" }
|
||||||
reth-rlp = { path = "../../common/rlp", features = ["alloc", "derive", "std", "ethereum-types"] }
|
reth-rlp = { path = "../../common/rlp", features = ["alloc", "derive", "std", "ethereum-types"] }
|
||||||
|
|
||||||
#used for forkid
|
#used for forkid
|
||||||
crc = "1"
|
crc = "1"
|
||||||
maplit = "1"
|
maplit = "1"
|
||||||
|
tokio = { version = "1.21.2", features = ["full"] }
|
||||||
|
futures = "0.3.24"
|
||||||
|
tokio-stream = "0.1.11"
|
||||||
|
secp256k1 = { version = "0.24.0", features = ["global-context", "rand-std", "recovery"] }
|
||||||
|
tokio-util = { version = "0.7.4", features = ["io"] }
|
||||||
|
pin-project = "1.0"
|
||||||
|
tracing = "0.1.37"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
hex-literal = "0.3"
|
hex-literal = "0.3"
|
||||||
ethers-core = { git = "https://github.com/gakonst/ethers-rs", default-features = false }
|
ethers-core = { git = "https://github.com/gakonst/ethers-rs", default-features = false }
|
||||||
|
rand = "0.8"
|
||||||
|
|||||||
39
crates/net/eth-wire/src/error.rs
Normal file
39
crates/net/eth-wire/src/error.rs
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
//! Error cases when handling a [`crate::EthStream`]
|
||||||
|
use std::io;
|
||||||
|
|
||||||
|
use reth_primitives::{Chain, H256};
|
||||||
|
|
||||||
|
use crate::types::forkid::ValidationError;
|
||||||
|
|
||||||
|
/// Errors when sending/receiving messages
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
pub enum EthStreamError {
|
||||||
|
#[error(transparent)]
|
||||||
|
Io(#[from] io::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
Rlp(#[from] reth_rlp::DecodeError),
|
||||||
|
#[error(transparent)]
|
||||||
|
HandshakeError(#[from] HandshakeError),
|
||||||
|
#[error("message size ({0}) exceeds max length (10MB)")]
|
||||||
|
MessageTooBig(usize),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
pub enum HandshakeError {
|
||||||
|
#[error("status message can only be recv/sent in handshake")]
|
||||||
|
StatusNotInHandshake,
|
||||||
|
#[error("received non-status message when trying to handshake")]
|
||||||
|
NonStatusMessageInHandshake,
|
||||||
|
#[error("no response received when sending out handshake")]
|
||||||
|
NoResponse,
|
||||||
|
#[error(transparent)]
|
||||||
|
InvalidFork(#[from] ValidationError),
|
||||||
|
#[error("mismatched genesis in Status message. expected: {expected:?}, got: {got:?}")]
|
||||||
|
MismatchedGenesis { expected: H256, got: H256 },
|
||||||
|
#[error("mismatched protocol version in Status message. expected: {expected:?}, got: {got:?}")]
|
||||||
|
MismatchedProtocolVersion { expected: u8, got: u8 },
|
||||||
|
#[error("mismatched chain in Status message. expected: {expected:?}, got: {got:?}")]
|
||||||
|
MismatchedChain { expected: Chain, got: Chain },
|
||||||
|
}
|
||||||
@ -4,13 +4,13 @@
|
|||||||
no_crate_inject,
|
no_crate_inject,
|
||||||
attr(deny(warnings, rust_2018_idioms), allow(dead_code, unused_variables))
|
attr(deny(warnings, rust_2018_idioms), allow(dead_code, unused_variables))
|
||||||
))]
|
))]
|
||||||
|
//! Implementation of the `eth` wire protocol.
|
||||||
|
|
||||||
//! Types for the eth wire protocol.
|
pub use tokio_util::codec::{
|
||||||
|
LengthDelimitedCodec as PassthroughCodec, LengthDelimitedCodecError as PassthroughCodecError,
|
||||||
|
};
|
||||||
|
pub mod error;
|
||||||
|
mod stream;
|
||||||
|
pub mod types;
|
||||||
|
|
||||||
mod status;
|
pub use stream::EthStream;
|
||||||
pub use status::Status;
|
|
||||||
|
|
||||||
mod version;
|
|
||||||
pub use version::EthVersion;
|
|
||||||
|
|
||||||
pub mod forkid;
|
|
||||||
|
|||||||
301
crates/net/eth-wire/src/stream.rs
Normal file
301
crates/net/eth-wire/src/stream.rs
Normal file
@ -0,0 +1,301 @@
|
|||||||
|
use crate::{
|
||||||
|
error::{EthStreamError, HandshakeError},
|
||||||
|
types::{forkid::ForkFilter, EthMessage, ProtocolMessage, Status},
|
||||||
|
};
|
||||||
|
use bytes::BytesMut;
|
||||||
|
use futures::{ready, Sink, SinkExt, StreamExt};
|
||||||
|
use pin_project::pin_project;
|
||||||
|
use reth_rlp::{Decodable, Encodable};
|
||||||
|
use std::{
|
||||||
|
io,
|
||||||
|
pin::Pin,
|
||||||
|
task::{Context, Poll},
|
||||||
|
};
|
||||||
|
use tokio_stream::Stream;
|
||||||
|
|
||||||
|
/// [`MAX_MESSAGE_SIZE`] is the maximum cap on the size of a protocol message.
|
||||||
|
// https://github.com/ethereum/go-ethereum/blob/30602163d5d8321fbc68afdcbbaf2362b2641bde/eth/protocols/eth/protocol.go#L50
|
||||||
|
const MAX_MESSAGE_SIZE: usize = 10 * 1024 * 1024;
|
||||||
|
|
||||||
|
/// An `EthStream` wraps over any `Stream` that yields bytes and makes it
|
||||||
|
/// compatible with eth-networking protocol messages, which get RLP encoded/decoded.
|
||||||
|
#[pin_project]
|
||||||
|
pub struct EthStream<S> {
|
||||||
|
#[pin]
|
||||||
|
inner: S,
|
||||||
|
/// Whether the `Status` handshake has been completed
|
||||||
|
authed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> EthStream<S> {
|
||||||
|
/// Creates a new unauthed [`EthStream`] from a provided stream. You will need
|
||||||
|
/// to manually handshake a peer.
|
||||||
|
pub fn new(inner: S) -> Self {
|
||||||
|
Self { inner, authed: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> EthStream<S>
|
||||||
|
where
|
||||||
|
S: Stream<Item = Result<bytes::BytesMut, io::Error>>
|
||||||
|
+ Sink<bytes::Bytes, Error = io::Error>
|
||||||
|
+ Unpin,
|
||||||
|
{
|
||||||
|
/// Given an instantiated transport layer, it proceeds to return an [`EthStream`]
|
||||||
|
/// after performing a [`Status`] message handshake as specified in
|
||||||
|
pub async fn connect(
|
||||||
|
inner: S,
|
||||||
|
status: Status,
|
||||||
|
fork_filter: ForkFilter,
|
||||||
|
) -> Result<Self, EthStreamError> {
|
||||||
|
let mut this = Self::new(inner);
|
||||||
|
this.handshake(status, fork_filter).await?;
|
||||||
|
Ok(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Performs a handshake with the connected peer over the transport stream.
|
||||||
|
pub async fn handshake(
|
||||||
|
&mut self,
|
||||||
|
status: Status,
|
||||||
|
fork_filter: ForkFilter,
|
||||||
|
) -> Result<(), EthStreamError> {
|
||||||
|
tracing::trace!("sending eth status ...");
|
||||||
|
self.send(EthMessage::Status(status)).await?;
|
||||||
|
|
||||||
|
tracing::trace!("waiting for eth status from peer ...");
|
||||||
|
let msg = self
|
||||||
|
.next()
|
||||||
|
.await
|
||||||
|
.ok_or_else(|| EthStreamError::HandshakeError(HandshakeError::NoResponse))??;
|
||||||
|
|
||||||
|
// TODO: Add any missing checks
|
||||||
|
// https://github.com/ethereum/go-ethereum/blob/9244d5cd61f3ea5a7645fdf2a1a96d53421e412f/eth/protocols/eth/handshake.go#L87-L89
|
||||||
|
match msg {
|
||||||
|
EthMessage::Status(resp) => {
|
||||||
|
self.authed = true;
|
||||||
|
|
||||||
|
if status.genesis != resp.genesis {
|
||||||
|
return Err(HandshakeError::MismatchedGenesis {
|
||||||
|
expected: status.genesis,
|
||||||
|
got: resp.genesis,
|
||||||
|
}
|
||||||
|
.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
if status.version != resp.version {
|
||||||
|
return Err(HandshakeError::MismatchedProtocolVersion {
|
||||||
|
expected: status.version,
|
||||||
|
got: resp.version,
|
||||||
|
}
|
||||||
|
.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
if status.chain != resp.chain {
|
||||||
|
return Err(HandshakeError::MismatchedChain {
|
||||||
|
expected: status.chain,
|
||||||
|
got: resp.chain,
|
||||||
|
}
|
||||||
|
.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(fork_filter.validate(resp.forkid).map_err(HandshakeError::InvalidFork)?)
|
||||||
|
}
|
||||||
|
_ => Err(EthStreamError::HandshakeError(HandshakeError::NonStatusMessageInHandshake)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> Stream for EthStream<S>
|
||||||
|
where
|
||||||
|
S: Stream<Item = Result<bytes::BytesMut, io::Error>> + Unpin,
|
||||||
|
{
|
||||||
|
type Item = Result<EthMessage, EthStreamError>;
|
||||||
|
|
||||||
|
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||||
|
let this = self.project();
|
||||||
|
let res = ready!(this.inner.poll_next(cx));
|
||||||
|
let bytes = match res {
|
||||||
|
Some(Ok(bytes)) => bytes,
|
||||||
|
Some(Err(err)) => return Poll::Ready(Some(Err(err.into()))),
|
||||||
|
None => return Poll::Ready(None),
|
||||||
|
};
|
||||||
|
|
||||||
|
if bytes.len() > MAX_MESSAGE_SIZE {
|
||||||
|
return Poll::Ready(Some(Err(EthStreamError::MessageTooBig(bytes.len()))))
|
||||||
|
}
|
||||||
|
|
||||||
|
let msg = match ProtocolMessage::decode(&mut bytes.as_ref()) {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(err) => return Poll::Ready(Some(Err(err.into()))),
|
||||||
|
};
|
||||||
|
|
||||||
|
if *this.authed && matches!(msg.message, EthMessage::Status(_)) {
|
||||||
|
return Poll::Ready(Some(Err(EthStreamError::HandshakeError(
|
||||||
|
HandshakeError::StatusNotInHandshake,
|
||||||
|
))))
|
||||||
|
}
|
||||||
|
|
||||||
|
Poll::Ready(Some(Ok(msg.message)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> Sink<EthMessage> for EthStream<S>
|
||||||
|
where
|
||||||
|
S: Sink<bytes::Bytes, Error = io::Error> + Unpin,
|
||||||
|
{
|
||||||
|
type Error = EthStreamError;
|
||||||
|
|
||||||
|
fn poll_ready(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||||
|
self.project().inner.poll_ready(cx).map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_send(self: Pin<&mut Self>, item: EthMessage) -> Result<(), Self::Error> {
|
||||||
|
if self.authed && matches!(item, EthMessage::Status(_)) {
|
||||||
|
return Err(EthStreamError::HandshakeError(HandshakeError::StatusNotInHandshake))
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut bytes = BytesMut::new();
|
||||||
|
ProtocolMessage::from(item).encode(&mut bytes);
|
||||||
|
let bytes = bytes.freeze();
|
||||||
|
|
||||||
|
self.project().inner.start_send(bytes)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||||
|
self.project().inner.poll_flush(cx).map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||||
|
self.project().inner.poll_close(cx).map_err(Into::into)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::{
|
||||||
|
types::{broadcast::BlockHashNumber, forkid::ForkFilter, EthMessage, Status},
|
||||||
|
EthStream, PassthroughCodec,
|
||||||
|
};
|
||||||
|
use futures::{SinkExt, StreamExt};
|
||||||
|
use reth_ecies::{stream::ECIESStream, util::pk2id};
|
||||||
|
use secp256k1::{SecretKey, SECP256K1};
|
||||||
|
use tokio::net::{TcpListener, TcpStream};
|
||||||
|
use tokio_util::codec::Decoder;
|
||||||
|
|
||||||
|
use crate::types::EthVersion;
|
||||||
|
use ethers_core::types::Chain;
|
||||||
|
use reth_primitives::{H256, U256};
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn can_handshake() {
|
||||||
|
let genesis = H256::random();
|
||||||
|
let fork_filter = ForkFilter::new(0, genesis, vec![]);
|
||||||
|
|
||||||
|
let status = Status {
|
||||||
|
version: EthVersion::Eth67 as u8,
|
||||||
|
chain: Chain::Mainnet.into(),
|
||||||
|
total_difficulty: U256::from(0),
|
||||||
|
blockhash: H256::random(),
|
||||||
|
genesis,
|
||||||
|
// Pass the current fork id.
|
||||||
|
forkid: fork_filter.current(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||||
|
let local_addr = listener.local_addr().unwrap();
|
||||||
|
|
||||||
|
let status_clone = status;
|
||||||
|
let fork_filter_clone = fork_filter.clone();
|
||||||
|
let handle = tokio::spawn(async move {
|
||||||
|
// roughly based off of the design of tokio::net::TcpListener
|
||||||
|
let (incoming, _) = listener.accept().await.unwrap();
|
||||||
|
let stream = crate::PassthroughCodec::default().framed(incoming);
|
||||||
|
let _ = EthStream::connect(stream, status_clone, fork_filter_clone).await.unwrap();
|
||||||
|
});
|
||||||
|
|
||||||
|
let outgoing = TcpStream::connect(local_addr).await.unwrap();
|
||||||
|
let sink = crate::PassthroughCodec::default().framed(outgoing);
|
||||||
|
|
||||||
|
// try to connect
|
||||||
|
let _ = EthStream::connect(sink, status, fork_filter).await.unwrap();
|
||||||
|
|
||||||
|
// wait for it to finish
|
||||||
|
handle.await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn can_write_and_read_cleartext() {
|
||||||
|
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||||
|
let local_addr = listener.local_addr().unwrap();
|
||||||
|
let test_msg = EthMessage::NewBlockHashes(
|
||||||
|
vec![
|
||||||
|
BlockHashNumber { hash: reth_primitives::H256::random(), number: 5 },
|
||||||
|
BlockHashNumber { hash: reth_primitives::H256::random(), number: 6 },
|
||||||
|
]
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let test_msg_clone = test_msg.clone();
|
||||||
|
let handle = tokio::spawn(async move {
|
||||||
|
// roughly based off of the design of tokio::net::TcpListener
|
||||||
|
let (incoming, _) = listener.accept().await.unwrap();
|
||||||
|
let stream = PassthroughCodec::default().framed(incoming);
|
||||||
|
let mut stream = EthStream::new(stream);
|
||||||
|
|
||||||
|
// use the stream to get the next message
|
||||||
|
let message = stream.next().await.unwrap().unwrap();
|
||||||
|
assert_eq!(message, test_msg_clone);
|
||||||
|
});
|
||||||
|
|
||||||
|
let outgoing = TcpStream::connect(local_addr).await.unwrap();
|
||||||
|
let sink = PassthroughCodec::default().framed(outgoing);
|
||||||
|
let mut client_stream = EthStream::new(sink);
|
||||||
|
|
||||||
|
client_stream.send(test_msg).await.unwrap();
|
||||||
|
|
||||||
|
// make sure the server receives the message and asserts before ending the test
|
||||||
|
handle.await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn can_write_and_read_ecies() {
|
||||||
|
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||||
|
let local_addr = listener.local_addr().unwrap();
|
||||||
|
let server_key = SecretKey::new(&mut rand::thread_rng());
|
||||||
|
let test_msg = EthMessage::NewBlockHashes(
|
||||||
|
vec![
|
||||||
|
BlockHashNumber { hash: reth_primitives::H256::random(), number: 5 },
|
||||||
|
BlockHashNumber { hash: reth_primitives::H256::random(), number: 6 },
|
||||||
|
]
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let test_msg_clone = test_msg.clone();
|
||||||
|
let handle = tokio::spawn(async move {
|
||||||
|
// roughly based off of the design of tokio::net::TcpListener
|
||||||
|
let (incoming, _) = listener.accept().await.unwrap();
|
||||||
|
let stream = ECIESStream::incoming(incoming, server_key).await.unwrap();
|
||||||
|
let mut stream = EthStream::new(stream);
|
||||||
|
|
||||||
|
// use the stream to get the next message
|
||||||
|
let message = stream.next().await.unwrap().unwrap();
|
||||||
|
assert_eq!(message, test_msg_clone);
|
||||||
|
});
|
||||||
|
|
||||||
|
// create the server pubkey
|
||||||
|
let server_id = pk2id(&server_key.public_key(SECP256K1));
|
||||||
|
|
||||||
|
let client_key = SecretKey::new(&mut rand::thread_rng());
|
||||||
|
|
||||||
|
let outgoing = TcpStream::connect(local_addr).await.unwrap();
|
||||||
|
let outgoing = ECIESStream::connect(outgoing, client_key, server_id).await.unwrap();
|
||||||
|
let mut client_stream = EthStream::new(outgoing);
|
||||||
|
|
||||||
|
client_stream.send(test_msg).await.unwrap();
|
||||||
|
|
||||||
|
// make sure the server receives the message and asserts before ending the test
|
||||||
|
handle.await.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
31
crates/net/eth-wire/src/types/broadcast.rs
Normal file
31
crates/net/eth-wire/src/types/broadcast.rs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
//! Types for broadcasting new data.
|
||||||
|
use reth_rlp::{RlpDecodable, RlpDecodableWrapper, RlpEncodable, RlpEncodableWrapper};
|
||||||
|
|
||||||
|
/// This informs peers of new blocks that have appeared on the network.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, RlpEncodableWrapper, RlpDecodableWrapper)]
|
||||||
|
pub struct NewBlockHashes(
|
||||||
|
/// New block hashes and the block number for each blockhash.
|
||||||
|
/// Clients should request blocks using a [`GetBlockBodies`](crate::GetBlockBodies) message.
|
||||||
|
pub Vec<BlockHashNumber>,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// A block hash _and_ a block number.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, RlpEncodable, RlpDecodable)]
|
||||||
|
pub struct BlockHashNumber {
|
||||||
|
/// The block hash
|
||||||
|
pub hash: reth_primitives::H256,
|
||||||
|
/// The block number
|
||||||
|
pub number: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Vec<BlockHashNumber>> for NewBlockHashes {
|
||||||
|
fn from(v: Vec<BlockHashNumber>) -> Self {
|
||||||
|
NewBlockHashes(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<NewBlockHashes> for Vec<BlockHashNumber> {
|
||||||
|
fn from(v: NewBlockHashes) -> Self {
|
||||||
|
v.0
|
||||||
|
}
|
||||||
|
}
|
||||||
367
crates/net/eth-wire/src/types/message.rs
Normal file
367
crates/net/eth-wire/src/types/message.rs
Normal file
@ -0,0 +1,367 @@
|
|||||||
|
#![allow(missing_docs)]
|
||||||
|
use super::{broadcast::NewBlockHashes, Status};
|
||||||
|
use bytes::Buf;
|
||||||
|
use reth_rlp::{length_of_length, Decodable, Encodable, Header};
|
||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
/// An `eth` protocol message, containing a message ID and payload.
|
||||||
|
pub struct ProtocolMessage {
|
||||||
|
pub message_type: EthMessageID,
|
||||||
|
pub message: EthMessage,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProtocolMessage {
|
||||||
|
/// Create a new ProtocolMessage from a message type and message rlp bytes.
|
||||||
|
pub fn decode_message(
|
||||||
|
message_type: EthMessageID,
|
||||||
|
buf: &mut &[u8],
|
||||||
|
) -> Result<Self, reth_rlp::DecodeError> {
|
||||||
|
let message = match message_type {
|
||||||
|
EthMessageID::Status => EthMessage::Status(Status::decode(buf)?),
|
||||||
|
EthMessageID::NewBlockHashes => {
|
||||||
|
EthMessage::NewBlockHashes(NewBlockHashes::decode(buf)?)
|
||||||
|
}
|
||||||
|
_ => unimplemented!(),
|
||||||
|
// EthMessageID::NewBlock => EthMessage::NewBlock(Box::new(NewBlock::decode(buf)?)),
|
||||||
|
// EthMessageID::Transactions => EthMessage::Transactions(Transactions::decode(buf)?),
|
||||||
|
// EthMessageID::NewPooledTransactionHashes => {
|
||||||
|
// EthMessage::NewPooledTransactionHashes(NewPooledTransactionHashes::decode(buf)?)
|
||||||
|
// }
|
||||||
|
// EthMessageID::GetBlockHeaders => {
|
||||||
|
// let request_pair = RequestPair::<GetBlockHeaders>::decode(buf)?;
|
||||||
|
// EthMessage::GetBlockHeaders(request_pair)
|
||||||
|
// }
|
||||||
|
// EthMessageID::BlockHeaders => {
|
||||||
|
// let request_pair = RequestPair::<BlockHeaders>::decode(buf)?;
|
||||||
|
// EthMessage::BlockHeaders(request_pair)
|
||||||
|
// }
|
||||||
|
// EthMessageID::GetBlockBodies => {
|
||||||
|
// let request_pair = RequestPair::<GetBlockBodies>::decode(buf)?;
|
||||||
|
// EthMessage::GetBlockBodies(request_pair)
|
||||||
|
// }
|
||||||
|
// EthMessageID::BlockBodies => {
|
||||||
|
// let request_pair = RequestPair::<BlockBodies>::decode(buf)?;
|
||||||
|
// EthMessage::BlockBodies(request_pair)
|
||||||
|
// }
|
||||||
|
// EthMessageID::GetPooledTransactions => {
|
||||||
|
// let request_pair = RequestPair::<GetPooledTransactions>::decode(buf)?;
|
||||||
|
// EthMessage::GetPooledTransactions(request_pair)
|
||||||
|
// }
|
||||||
|
// EthMessageID::PooledTransactions => {
|
||||||
|
// let request_pair = RequestPair::<PooledTransactions>::decode(buf)?;
|
||||||
|
// EthMessage::PooledTransactions(request_pair)
|
||||||
|
// }
|
||||||
|
// EthMessageID::GetNodeData => {
|
||||||
|
// let request_pair = RequestPair::<GetNodeData>::decode(buf)?;
|
||||||
|
// EthMessage::GetNodeData(request_pair)
|
||||||
|
// }
|
||||||
|
// EthMessageID::NodeData => {
|
||||||
|
// let request_pair = RequestPair::<NodeData>::decode(buf)?;
|
||||||
|
// EthMessage::NodeData(request_pair)
|
||||||
|
// }
|
||||||
|
// EthMessageID::GetReceipts => {
|
||||||
|
// let request_pair = RequestPair::<GetReceipts>::decode(buf)?;
|
||||||
|
// EthMessage::GetReceipts(request_pair)
|
||||||
|
// }
|
||||||
|
// EthMessageID::Receipts => {
|
||||||
|
// let request_pair = RequestPair::<Receipts>::decode(buf)?;
|
||||||
|
// EthMessage::Receipts(request_pair)
|
||||||
|
// }
|
||||||
|
};
|
||||||
|
Ok(ProtocolMessage { message_type, message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encodes the protocol message into bytes.
|
||||||
|
/// The message type is encoded as a single byte and prepended to the message.
|
||||||
|
impl Encodable for ProtocolMessage {
|
||||||
|
fn length(&self) -> usize {
|
||||||
|
self.message_type.length() + self.message.length()
|
||||||
|
}
|
||||||
|
fn encode(&self, out: &mut dyn bytes::BufMut) {
|
||||||
|
self.message_type.encode(out);
|
||||||
|
self.message.encode(out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decodes a protocol message from bytes, using the first byte to determine the message type.
|
||||||
|
/// This decodes `eth/66` request ids for each message type.
|
||||||
|
impl Decodable for ProtocolMessage {
|
||||||
|
fn decode(buf: &mut &[u8]) -> Result<Self, reth_rlp::DecodeError> {
|
||||||
|
let message_type = EthMessageID::decode(buf)?;
|
||||||
|
Self::decode_message(message_type, buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<EthMessage> for ProtocolMessage {
|
||||||
|
fn from(message: EthMessage) -> Self {
|
||||||
|
ProtocolMessage { message_type: message.message_id(), message }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: determine whats up with this enum variant size warning
|
||||||
|
|
||||||
|
/// Represents a message in the eth wire protocol, versions 66 and 67.
|
||||||
|
///
|
||||||
|
/// The ethereum wire protocol is a set of messages that are broadcasted to the network in two
|
||||||
|
/// styles:
|
||||||
|
/// * A request message sent by a peer (such as [`GetPooledTransactions`]), and an associated
|
||||||
|
/// response message (such as [`PooledTransactions`]).
|
||||||
|
/// * A message that is broadcast to the network, without a corresponding request.
|
||||||
|
///
|
||||||
|
/// The newer `eth/66` is an efficiency upgrade on top of `eth/65`, introducing a request id to
|
||||||
|
/// correlate request-response message pairs. This allows for request multiplexing.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum EthMessage {
|
||||||
|
// Status is required for the protocol handshake
|
||||||
|
Status(Status),
|
||||||
|
// // The following messages are broadcast to the network
|
||||||
|
NewBlockHashes(NewBlockHashes),
|
||||||
|
// NewBlock(Box<NewBlock>),
|
||||||
|
// Transactions(Transactions),
|
||||||
|
// NewPooledTransactionHashes(NewPooledTransactionHashes),
|
||||||
|
|
||||||
|
// // The following messages are request-response message pairs
|
||||||
|
// GetBlockHeaders(RequestPair<GetBlockHeaders>),
|
||||||
|
// BlockHeaders(RequestPair<BlockHeaders>),
|
||||||
|
// GetBlockBodies(RequestPair<GetBlockBodies>),
|
||||||
|
// BlockBodies(RequestPair<BlockBodies>),
|
||||||
|
// GetPooledTransactions(RequestPair<GetPooledTransactions>),
|
||||||
|
// PooledTransactions(RequestPair<PooledTransactions>),
|
||||||
|
// GetNodeData(RequestPair<GetNodeData>),
|
||||||
|
// NodeData(RequestPair<NodeData>),
|
||||||
|
// GetReceipts(RequestPair<GetReceipts>),
|
||||||
|
// Receipts(RequestPair<Receipts>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EthMessage {
|
||||||
|
/// Returns the message's ID.
|
||||||
|
pub fn message_id(&self) -> EthMessageID {
|
||||||
|
match self {
|
||||||
|
EthMessage::Status(_) => EthMessageID::Status,
|
||||||
|
EthMessage::NewBlockHashes(_) => EthMessageID::NewBlockHashes,
|
||||||
|
// EthMessage::NewBlock(_) => EthMessageID::NewBlock,
|
||||||
|
// EthMessage::Transactions(_) => EthMessageID::Transactions,
|
||||||
|
// EthMessage::NewPooledTransactionHashes(_) =>
|
||||||
|
// EthMessageID::NewPooledTransactionHashes, EthMessage::GetBlockHeaders(_)
|
||||||
|
// => EthMessageID::GetBlockHeaders, EthMessage::BlockHeaders(_) =>
|
||||||
|
// EthMessageID::BlockHeaders, EthMessage::GetBlockBodies(_) =>
|
||||||
|
// EthMessageID::GetBlockBodies, EthMessage::BlockBodies(_) =>
|
||||||
|
// EthMessageID::BlockBodies, EthMessage::GetPooledTransactions(_) =>
|
||||||
|
// EthMessageID::GetPooledTransactions, EthMessage::PooledTransactions(_) =>
|
||||||
|
// EthMessageID::PooledTransactions, EthMessage::GetNodeData(_) =>
|
||||||
|
// EthMessageID::GetNodeData, EthMessage::NodeData(_) =>
|
||||||
|
// EthMessageID::NodeData, EthMessage::GetReceipts(_) =>
|
||||||
|
// EthMessageID::GetReceipts, EthMessage::Receipts(_) =>
|
||||||
|
// EthMessageID::Receipts,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Encodable for EthMessage {
|
||||||
|
fn length(&self) -> usize {
|
||||||
|
match self {
|
||||||
|
EthMessage::Status(status) => status.length(),
|
||||||
|
EthMessage::NewBlockHashes(new_block_hashes) => new_block_hashes.length(),
|
||||||
|
// EthMessage::NewBlock(new_block) => new_block.length(),
|
||||||
|
// EthMessage::Transactions(transactions) => transactions.length(),
|
||||||
|
// EthMessage::NewPooledTransactionHashes(hashes) => hashes.length(),
|
||||||
|
// EthMessage::GetBlockHeaders(request) => request.length(),
|
||||||
|
// EthMessage::BlockHeaders(headers) => headers.length(),
|
||||||
|
// EthMessage::GetBlockBodies(request) => request.length(),
|
||||||
|
// EthMessage::BlockBodies(bodies) => bodies.length(),
|
||||||
|
// EthMessage::GetPooledTransactions(request) => request.length(),
|
||||||
|
// EthMessage::PooledTransactions(transactions) => transactions.length(),
|
||||||
|
// EthMessage::GetNodeData(request) => request.length(),
|
||||||
|
// EthMessage::NodeData(data) => data.length(),
|
||||||
|
// EthMessage::GetReceipts(request) => request.length(),
|
||||||
|
// EthMessage::Receipts(receipts) => receipts.length(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn encode(&self, out: &mut dyn bytes::BufMut) {
|
||||||
|
match self {
|
||||||
|
EthMessage::Status(status) => status.encode(out),
|
||||||
|
EthMessage::NewBlockHashes(new_block_hashes) => new_block_hashes.encode(out),
|
||||||
|
// EthMessage::NewBlock(new_block) => new_block.encode(out),
|
||||||
|
// EthMessage::Transactions(transactions) => transactions.encode(out),
|
||||||
|
// EthMessage::NewPooledTransactionHashes(hashes) => hashes.encode(out),
|
||||||
|
// EthMessage::GetBlockHeaders(request) => request.encode(out),
|
||||||
|
// EthMessage::BlockHeaders(headers) => headers.encode(out),
|
||||||
|
// EthMessage::GetBlockBodies(request) => request.encode(out),
|
||||||
|
// EthMessage::BlockBodies(bodies) => bodies.encode(out),
|
||||||
|
// EthMessage::GetPooledTransactions(request) => request.encode(out),
|
||||||
|
// EthMessage::PooledTransactions(transactions) => transactions.encode(out),
|
||||||
|
// EthMessage::GetNodeData(request) => request.encode(out),
|
||||||
|
// EthMessage::NodeData(data) => data.encode(out),
|
||||||
|
// EthMessage::GetReceipts(request) => request.encode(out),
|
||||||
|
// EthMessage::Receipts(receipts) => receipts.encode(out),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents message IDs for eth protocol messages.
|
||||||
|
#[repr(u8)]
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum EthMessageID {
|
||||||
|
Status = 0x00,
|
||||||
|
NewBlockHashes = 0x01,
|
||||||
|
Transactions = 0x02,
|
||||||
|
GetBlockHeaders = 0x03,
|
||||||
|
BlockHeaders = 0x04,
|
||||||
|
GetBlockBodies = 0x05,
|
||||||
|
BlockBodies = 0x06,
|
||||||
|
NewBlock = 0x07,
|
||||||
|
NewPooledTransactionHashes = 0x08,
|
||||||
|
GetPooledTransactions = 0x09,
|
||||||
|
PooledTransactions = 0x0a,
|
||||||
|
GetNodeData = 0x0d,
|
||||||
|
NodeData = 0x0e,
|
||||||
|
GetReceipts = 0x0f,
|
||||||
|
Receipts = 0x10,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Encodable for EthMessageID {
|
||||||
|
fn length(&self) -> usize {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
fn encode(&self, out: &mut dyn bytes::BufMut) {
|
||||||
|
out.put_u8(*self as u8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Decodable for EthMessageID {
|
||||||
|
fn decode(buf: &mut &[u8]) -> Result<Self, reth_rlp::DecodeError> {
|
||||||
|
let id = buf.first().ok_or(reth_rlp::DecodeError::InputTooShort)?;
|
||||||
|
let id = match id {
|
||||||
|
0x00 => EthMessageID::Status,
|
||||||
|
0x01 => EthMessageID::NewBlockHashes,
|
||||||
|
0x02 => EthMessageID::Transactions,
|
||||||
|
0x03 => EthMessageID::GetBlockHeaders,
|
||||||
|
0x04 => EthMessageID::BlockHeaders,
|
||||||
|
0x05 => EthMessageID::GetBlockBodies,
|
||||||
|
0x06 => EthMessageID::BlockBodies,
|
||||||
|
0x07 => EthMessageID::NewBlock,
|
||||||
|
0x08 => EthMessageID::NewPooledTransactionHashes,
|
||||||
|
0x09 => EthMessageID::GetPooledTransactions,
|
||||||
|
0x0a => EthMessageID::PooledTransactions,
|
||||||
|
0x0d => EthMessageID::GetNodeData,
|
||||||
|
0x0e => EthMessageID::NodeData,
|
||||||
|
0x0f => EthMessageID::GetReceipts,
|
||||||
|
0x10 => EthMessageID::Receipts,
|
||||||
|
_ => return Err(reth_rlp::DecodeError::Custom("Invalid message ID")),
|
||||||
|
};
|
||||||
|
buf.advance(1);
|
||||||
|
Ok(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<usize> for EthMessageID {
|
||||||
|
type Error = &'static str;
|
||||||
|
|
||||||
|
fn try_from(value: usize) -> Result<Self, Self::Error> {
|
||||||
|
match value {
|
||||||
|
0x00 => Ok(EthMessageID::Status),
|
||||||
|
0x01 => Ok(EthMessageID::NewBlockHashes),
|
||||||
|
0x02 => Ok(EthMessageID::Transactions),
|
||||||
|
0x03 => Ok(EthMessageID::GetBlockHeaders),
|
||||||
|
0x04 => Ok(EthMessageID::BlockHeaders),
|
||||||
|
0x05 => Ok(EthMessageID::GetBlockBodies),
|
||||||
|
0x06 => Ok(EthMessageID::BlockBodies),
|
||||||
|
0x07 => Ok(EthMessageID::NewBlock),
|
||||||
|
0x08 => Ok(EthMessageID::NewPooledTransactionHashes),
|
||||||
|
0x09 => Ok(EthMessageID::GetPooledTransactions),
|
||||||
|
0x0a => Ok(EthMessageID::PooledTransactions),
|
||||||
|
0x0d => Ok(EthMessageID::GetNodeData),
|
||||||
|
0x0e => Ok(EthMessageID::NodeData),
|
||||||
|
0x0f => Ok(EthMessageID::GetReceipts),
|
||||||
|
0x10 => Ok(EthMessageID::Receipts),
|
||||||
|
_ => Err("Invalid message ID"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This is used for all request-response style `eth` protocol messages.
|
||||||
|
/// This can represent either a request or a response, since both include a message payload and
|
||||||
|
/// request id.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct RequestPair<T> {
|
||||||
|
/// id for the contained request or response message
|
||||||
|
pub request_id: u64,
|
||||||
|
|
||||||
|
/// the request or response message payload
|
||||||
|
pub message: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Allows messages with request ids to be serialized into RLP bytes.
|
||||||
|
impl<T> Encodable for RequestPair<T>
|
||||||
|
where
|
||||||
|
T: Encodable,
|
||||||
|
{
|
||||||
|
fn length(&self) -> usize {
|
||||||
|
let mut length = 0;
|
||||||
|
length += self.request_id.length();
|
||||||
|
length += self.message.length();
|
||||||
|
length += length_of_length(length);
|
||||||
|
length
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode(&self, out: &mut dyn reth_rlp::BufMut) {
|
||||||
|
let header =
|
||||||
|
Header { list: true, payload_length: self.request_id.length() + self.message.length() };
|
||||||
|
|
||||||
|
header.encode(out);
|
||||||
|
self.request_id.encode(out);
|
||||||
|
self.message.encode(out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Allows messages with request ids to be deserialized into RLP bytes.
|
||||||
|
impl<T> Decodable for RequestPair<T>
|
||||||
|
where
|
||||||
|
T: Decodable,
|
||||||
|
{
|
||||||
|
fn decode(buf: &mut &[u8]) -> Result<Self, reth_rlp::DecodeError> {
|
||||||
|
let _header = Header::decode(buf)?;
|
||||||
|
Ok(Self { request_id: u64::decode(buf)?, message: T::decode(buf)? })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use crate::types::message::RequestPair;
|
||||||
|
use hex_literal::hex;
|
||||||
|
use reth_rlp::{Decodable, Encodable};
|
||||||
|
|
||||||
|
fn encode<T: Encodable>(value: T) -> Vec<u8> {
|
||||||
|
let mut buf = vec![];
|
||||||
|
value.encode(&mut buf);
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_pair_encode() {
|
||||||
|
let request_pair = RequestPair { request_id: 1337, message: vec![5u8] };
|
||||||
|
|
||||||
|
// c5: start of list (c0) + len(full_list) (length is <55 bytes)
|
||||||
|
// 82: 0x80 + len(1337)
|
||||||
|
// 05 39: 1337 (request_id)
|
||||||
|
// === full_list ===
|
||||||
|
// c1: start of list (c0) + len(list) (length is <55 bytes)
|
||||||
|
// 05: 5 (message)
|
||||||
|
let expected = hex!("c5820539c105");
|
||||||
|
let got = encode(request_pair);
|
||||||
|
assert_eq!(expected[..], got, "expected: {:X?}, got: {:X?}", expected, got,);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_pair_decode() {
|
||||||
|
let raw_pair = &hex!("c5820539c105")[..];
|
||||||
|
|
||||||
|
let expected = RequestPair { request_id: 1337, message: vec![5u8] };
|
||||||
|
|
||||||
|
let got = RequestPair::<Vec<u8>>::decode(&mut &*raw_pair).unwrap();
|
||||||
|
assert_eq!(expected.length(), raw_pair.len());
|
||||||
|
assert_eq!(expected, got);
|
||||||
|
}
|
||||||
|
}
|
||||||
14
crates/net/eth-wire/src/types/mod.rs
Normal file
14
crates/net/eth-wire/src/types/mod.rs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
//! Types for the eth wire protocol.
|
||||||
|
|
||||||
|
mod status;
|
||||||
|
pub use status::Status;
|
||||||
|
|
||||||
|
mod version;
|
||||||
|
pub use version::EthVersion;
|
||||||
|
|
||||||
|
pub mod forkid;
|
||||||
|
|
||||||
|
pub mod message;
|
||||||
|
pub use message::{EthMessage, EthMessageID, ProtocolMessage};
|
||||||
|
|
||||||
|
pub mod broadcast;
|
||||||
@ -1,4 +1,4 @@
|
|||||||
use crate::forkid::ForkId;
|
use super::forkid::ForkId;
|
||||||
use reth_primitives::{Chain, H256, U256};
|
use reth_primitives::{Chain, H256, U256};
|
||||||
use reth_rlp::{RlpDecodable, RlpEncodable};
|
use reth_rlp::{RlpDecodable, RlpEncodable};
|
||||||
use std::fmt::{Debug, Display};
|
use std::fmt::{Debug, Display};
|
||||||
@ -92,7 +92,7 @@ mod tests {
|
|||||||
use reth_rlp::{Decodable, Encodable};
|
use reth_rlp::{Decodable, Encodable};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use crate::{
|
use crate::types::{
|
||||||
forkid::{ForkHash, ForkId},
|
forkid::{ForkHash, ForkId},
|
||||||
EthVersion, Status,
|
EthVersion, Status,
|
||||||
};
|
};
|
||||||
@ -20,7 +20,7 @@ pub enum EthVersion {
|
|||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
/// ```
|
/// ```
|
||||||
/// use reth_eth_wire::EthVersion;
|
/// use reth_eth_wire::types::EthVersion;
|
||||||
///
|
///
|
||||||
/// let version = EthVersion::try_from("67").unwrap();
|
/// let version = EthVersion::try_from("67").unwrap();
|
||||||
/// assert_eq!(version, EthVersion::Eth67);
|
/// assert_eq!(version, EthVersion::Eth67);
|
||||||
@ -42,7 +42,7 @@ impl TryFrom<&str> for EthVersion {
|
|||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
/// ```
|
/// ```
|
||||||
/// use reth_eth_wire::EthVersion;
|
/// use reth_eth_wire::types::EthVersion;
|
||||||
///
|
///
|
||||||
/// let version = EthVersion::try_from(67).unwrap();
|
/// let version = EthVersion::try_from(67).unwrap();
|
||||||
/// assert_eq!(version, EthVersion::Eth67);
|
/// assert_eq!(version, EthVersion::Eth67);
|
||||||
Reference in New Issue
Block a user