diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 932caaa90..c8d303b4a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,7 @@ env: RUSTFLAGS: -D warnings CARGO_TERM_COLOR: always GETH_BUILD: 1.10.26-e5eb32ac + AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES: 1 concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} @@ -70,11 +71,16 @@ jobs: command: install args: cargo-test-fuzz afl + - name: check for cargo afl + run: | + cargo install --force afl + cargo afl --version + - name: Run fuzz tests run: | ./.github/scripts/fuzz.sh reth-primitives ./.github/scripts/fuzz.sh reth-db - ./.github/scripts/fuzz.sh reth-interfaces + ./.github/scripts/fuzz.sh reth-eth-wire ./.github/scripts/fuzz.sh reth-codecs lint: diff --git a/Cargo.lock b/Cargo.lock index 7f7f199cd..c1000ace1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3342,6 +3342,7 @@ dependencies = [ "serde", "smol_str", "snap", + "test-fuzz", "thiserror", "tokio", "tokio-stream", @@ -3394,7 +3395,6 @@ dependencies = [ "reth-rpc-types", "secp256k1", "serde", - "test-fuzz", "thiserror", "tokio", "tokio-stream", diff --git a/crates/interfaces/Cargo.toml b/crates/interfaces/Cargo.toml index d23af22d0..0c62714c4 100644 --- a/crates/interfaces/Cargo.toml +++ b/crates/interfaces/Cargo.toml @@ -33,7 +33,6 @@ modular-bitfield = "0.11.2" [dev-dependencies] reth-db = { path = "../storage/db", features = ["test-utils"] } -test-fuzz = "3.0.4" tokio = { version = "1.21.2", features = ["full"] } tokio-stream = { version = "0.1.11", features = ["sync"] } arbitrary = { version = "1.1.7", features = ["derive"]} diff --git a/crates/net/eth-wire/Cargo.toml b/crates/net/eth-wire/Cargo.toml index 4aa8b81f2..507e68e13 100644 --- a/crates/net/eth-wire/Cargo.toml +++ b/crates/net/eth-wire/Cargo.toml @@ -29,6 +29,7 @@ snap = "1.0.5" smol_str = { version = "0.1", features = ["serde"] } [dev-dependencies] +test-fuzz = "3.0.4" reth-ecies = { path = "../ecies" } ethers-core = { git = "https://github.com/gakonst/ethers-rs", default-features = false } diff --git a/crates/net/eth-wire/src/builder.rs b/crates/net/eth-wire/src/builder.rs index f1c0dd6af..b1ff39ef1 100644 --- a/crates/net/eth-wire/src/builder.rs +++ b/crates/net/eth-wire/src/builder.rs @@ -2,9 +2,7 @@ //! messages. use crate::{ - capability::Capability, - p2pstream::{HelloMessage, ProtocolVersion}, - EthVersion, Status, + capability::Capability, hello::HelloMessage, p2pstream::ProtocolVersion, EthVersion, Status, }; use reth_primitives::{Chain, ForkId, PeerId, H256, U256}; diff --git a/crates/net/eth-wire/src/capability.rs b/crates/net/eth-wire/src/capability.rs index 847bf03fe..1b6dd50c0 100644 --- a/crates/net/eth-wire/src/capability.rs +++ b/crates/net/eth-wire/src/capability.rs @@ -26,7 +26,9 @@ pub enum CapabilityMessage { } /// A message indicating a supported capability and capability version. -#[derive(Clone, Debug, PartialEq, Eq, RlpEncodable, RlpDecodable, Serialize, Deserialize)] +#[derive( + Clone, Debug, PartialEq, Eq, RlpEncodable, RlpDecodable, Serialize, Deserialize, Default, +)] pub struct Capability { /// The name of the subprotocol pub name: SmolStr, diff --git a/crates/net/eth-wire/src/disconnect.rs b/crates/net/eth-wire/src/disconnect.rs index da4460cc5..6574fa610 100644 --- a/crates/net/eth-wire/src/disconnect.rs +++ b/crates/net/eth-wire/src/disconnect.rs @@ -65,6 +65,12 @@ impl Display for DisconnectReason { } } +impl Default for DisconnectReason { + fn default() -> Self { + DisconnectReason::DisconnectRequested + } +} + /// This represents an unknown disconnect reason with the given code. #[derive(Debug, Clone, Error)] #[error("unknown disconnect reason: {0}")] diff --git a/crates/net/eth-wire/src/ethstream.rs b/crates/net/eth-wire/src/ethstream.rs index d9b8dce3f..001adae17 100644 --- a/crates/net/eth-wire/src/ethstream.rs +++ b/crates/net/eth-wire/src/ethstream.rs @@ -243,7 +243,8 @@ mod tests { use super::UnauthedEthStream; use crate::{ capability::Capability, - p2pstream::{HelloMessage, ProtocolVersion, UnauthedP2PStream}, + hello::HelloMessage, + p2pstream::{ProtocolVersion, UnauthedP2PStream}, types::{broadcast::BlockHashNumber, EthMessage, EthVersion, Status}, EthStream, PassthroughCodec, }; diff --git a/crates/net/eth-wire/src/hello.rs b/crates/net/eth-wire/src/hello.rs new file mode 100644 index 000000000..a7133d234 --- /dev/null +++ b/crates/net/eth-wire/src/hello.rs @@ -0,0 +1,131 @@ +use crate::{capability::Capability, ProtocolVersion}; +use reth_primitives::PeerId; +use reth_rlp::{RlpDecodable, RlpEncodable}; +use serde::{Deserialize, Serialize}; + +// TODO: determine if we should allow for the extra fields at the end like EIP-706 suggests +/// Message used in the `p2p` handshake, containing information about the supported RLPx protocol +/// version and capabilities. +#[derive( + Clone, Debug, PartialEq, Eq, RlpEncodable, RlpDecodable, Serialize, Deserialize, Default, +)] +pub struct HelloMessage { + /// The version of the `p2p` protocol. + pub protocol_version: ProtocolVersion, + /// Specifies the client software identity, as a human-readable string (e.g. + /// "Ethereum(++)/1.0.0"). + pub client_version: String, + /// The list of supported capabilities and their versions. + pub capabilities: Vec, + /// The port that the client is listening on, zero indicates the client is not listening. + pub port: u16, + /// The secp256k1 public key corresponding to the node's private key. + pub id: PeerId, +} + +#[cfg(test)] +mod tests { + use reth_ecies::util::pk2id; + use reth_rlp::{Decodable, Encodable, EMPTY_STRING_CODE}; + use secp256k1::{SecretKey, SECP256K1}; + + use crate::{ + capability::Capability, + p2pstream::{P2PMessage, P2PMessageID}, + EthVersion, HelloMessage, ProtocolVersion, + }; + + #[test] + fn test_pong_snappy_encoding_parity() { + // encode pong using our `Encodable` implementation + let pong = P2PMessage::Pong; + let mut pong_encoded = Vec::new(); + pong.encode(&mut pong_encoded); + + // the definition of pong is 0x80 (an empty rlp string) + let pong_raw = vec![EMPTY_STRING_CODE]; + let mut snappy_encoder = snap::raw::Encoder::new(); + let pong_compressed = snappy_encoder.compress_vec(&pong_raw).unwrap(); + let mut pong_expected = vec![P2PMessageID::Pong as u8]; + pong_expected.extend(&pong_compressed); + + // ensure that the two encodings are equal + assert_eq!( + pong_expected, pong_encoded, + "left: {pong_expected:#x?}, right: {pong_encoded:#x?}" + ); + + // also ensure that the length is correct + assert_eq!(pong_expected.len(), P2PMessage::Pong.length()); + + // try to decode using Decodable + let p2p_message = P2PMessage::decode(&mut &pong_expected[..]).unwrap(); + assert_eq!(p2p_message, P2PMessage::Pong); + + // finally decode the encoded message with snappy + let mut snappy_decoder = snap::raw::Decoder::new(); + + // the message id is not compressed, only compress the latest bits + let decompressed = snappy_decoder.decompress_vec(&pong_encoded[1..]).unwrap(); + + assert_eq!(decompressed, pong_raw); + } + + #[test] + fn test_hello_encoding_round_trip() { + let secret_key = SecretKey::new(&mut rand::thread_rng()); + let id = pk2id(&secret_key.public_key(SECP256K1)); + let hello = P2PMessage::Hello(HelloMessage { + protocol_version: ProtocolVersion::V5, + client_version: "reth/0.1.0".to_string(), + capabilities: vec![Capability::new("eth".into(), EthVersion::Eth67 as usize)], + port: 30303, + id, + }); + + let mut hello_encoded = Vec::new(); + hello.encode(&mut hello_encoded); + + let hello_decoded = P2PMessage::decode(&mut &hello_encoded[..]).unwrap(); + + assert_eq!(hello, hello_decoded); + } + + #[test] + fn hello_encoding_length() { + let secret_key = SecretKey::new(&mut rand::thread_rng()); + let id = pk2id(&secret_key.public_key(SECP256K1)); + let hello = P2PMessage::Hello(HelloMessage { + protocol_version: ProtocolVersion::V5, + client_version: "reth/0.1.0".to_string(), + capabilities: vec![Capability::new("eth".into(), EthVersion::Eth67 as usize)], + port: 30303, + id, + }); + + let mut hello_encoded = Vec::new(); + hello.encode(&mut hello_encoded); + + assert_eq!(hello_encoded.len(), hello.length()); + } + + #[test] + fn hello_message_id_prefix() { + // ensure that the hello message id is prefixed + let secret_key = SecretKey::new(&mut rand::thread_rng()); + let id = pk2id(&secret_key.public_key(SECP256K1)); + let hello = P2PMessage::Hello(HelloMessage { + protocol_version: ProtocolVersion::V5, + client_version: "reth/0.1.0".to_string(), + capabilities: vec![Capability::new("eth".into(), EthVersion::Eth67 as usize)], + port: 30303, + id, + }); + + let mut hello_encoded = Vec::new(); + hello.encode(&mut hello_encoded); + + // zero is encoded as 0x80, the empty string code in RLP + assert_eq!(hello_encoded[0], EMPTY_STRING_CODE); + } +} diff --git a/crates/net/eth-wire/src/lib.rs b/crates/net/eth-wire/src/lib.rs index befcfc299..3cac4f8e6 100644 --- a/crates/net/eth-wire/src/lib.rs +++ b/crates/net/eth-wire/src/lib.rs @@ -1,4 +1,4 @@ -#![warn(missing_docs, unreachable_pub, unused_crate_dependencies)] +#![warn(missing_docs, unreachable_pub)] #![deny(unused_must_use, rust_2018_idioms)] #![doc(test( no_crate_inject, @@ -11,6 +11,7 @@ pub mod capability; mod disconnect; pub mod error; mod ethstream; +mod hello; mod p2pstream; mod pinger; pub use builder::*; @@ -25,5 +26,6 @@ pub use tokio_util::codec::{ pub use crate::{ disconnect::DisconnectReason, ethstream::{EthStream, UnauthedEthStream, MAX_MESSAGE_SIZE}, - p2pstream::{HelloMessage, P2PStream, ProtocolVersion, UnauthedP2PStream}, + hello::HelloMessage, + p2pstream::{P2PMessage, P2PMessageID, P2PStream, ProtocolVersion, UnauthedP2PStream}, }; diff --git a/crates/net/eth-wire/src/p2pstream.rs b/crates/net/eth-wire/src/p2pstream.rs index e7a3e7a2d..34fd8dca9 100644 --- a/crates/net/eth-wire/src/p2pstream.rs +++ b/crates/net/eth-wire/src/p2pstream.rs @@ -3,13 +3,12 @@ use crate::{ capability::{Capability, SharedCapability}, error::{P2PHandshakeError, P2PStreamError}, pinger::{Pinger, PingerEvent}, - DisconnectReason, + DisconnectReason, HelloMessage, }; use bytes::{Buf, Bytes, BytesMut}; use futures::{Sink, SinkExt, StreamExt}; use pin_project::pin_project; -use reth_primitives::H512 as PeerId; -use reth_rlp::{Decodable, DecodeError, Encodable, RlpDecodable, RlpEncodable, EMPTY_STRING_CODE}; +use reth_rlp::{Decodable, DecodeError, Encodable, EMPTY_STRING_CODE}; use serde::{Deserialize, Serialize}; use std::{ collections::{BTreeSet, HashMap, VecDeque}, @@ -524,7 +523,7 @@ pub fn set_capability_offsets( } /// This represents only the reserved `p2p` subprotocol messages. -#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum P2PMessage { /// The first packet sent over the connection, and sent once by both sides. Hello(HelloMessage), @@ -653,24 +652,6 @@ impl TryFrom for P2PMessageID { } } -// TODO: determine if we should allow for the extra fields at the end like EIP-706 suggests -/// Message used in the `p2p` handshake, containing information about the supported RLPx protocol -/// version and capabilities. -#[derive(Clone, Debug, PartialEq, Eq, RlpEncodable, RlpDecodable, Serialize, Deserialize)] -pub struct HelloMessage { - /// The version of the `p2p` protocol. - pub protocol_version: ProtocolVersion, - /// Specifies the client software identity, as a human-readable string (e.g. - /// "Ethereum(++)/1.0.0"). - pub client_version: String, - /// The list of supported capabilities and their versions. - pub capabilities: Vec, - /// The port that the client is listening on, zero indicates the client is not listening. - pub port: u16, - /// The secp256k1 public key corresponding to the node's private key. - pub id: PeerId, -} - /// RLPx `p2p` protocol version #[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum ProtocolVersion { @@ -701,6 +682,12 @@ impl Decodable for ProtocolVersion { } } +impl Default for ProtocolVersion { + fn default() -> Self { + ProtocolVersion::V5 + } +} + #[cfg(test)] mod tests { use super::*; @@ -842,98 +829,4 @@ mod tests { assert_eq!(decompressed, ping_raw); } - - #[test] - fn test_pong_snappy_encoding_parity() { - // encode pong using our `Encodable` implementation - let pong = P2PMessage::Pong; - let mut pong_encoded = Vec::new(); - pong.encode(&mut pong_encoded); - - // the definition of pong is 0x80 (an empty rlp string) - let pong_raw = vec![EMPTY_STRING_CODE]; - let mut snappy_encoder = snap::raw::Encoder::new(); - let pong_compressed = snappy_encoder.compress_vec(&pong_raw).unwrap(); - let mut pong_expected = vec![P2PMessageID::Pong as u8]; - pong_expected.extend(&pong_compressed); - - // ensure that the two encodings are equal - assert_eq!( - pong_expected, pong_encoded, - "left: {pong_expected:#x?}, right: {pong_encoded:#x?}" - ); - - // also ensure that the length is correct - assert_eq!(pong_expected.len(), P2PMessage::Pong.length()); - - // try to decode using Decodable - let p2p_message = P2PMessage::decode(&mut &pong_expected[..]).unwrap(); - assert_eq!(p2p_message, P2PMessage::Pong); - - // finally decode the encoded message with snappy - let mut snappy_decoder = snap::raw::Decoder::new(); - - // the message id is not compressed, only compress the latest bits - let decompressed = snappy_decoder.decompress_vec(&pong_encoded[1..]).unwrap(); - - assert_eq!(decompressed, pong_raw); - } - - #[test] - fn test_hello_encoding_round_trip() { - let secret_key = SecretKey::new(&mut rand::thread_rng()); - let id = pk2id(&secret_key.public_key(SECP256K1)); - let hello = P2PMessage::Hello(HelloMessage { - protocol_version: ProtocolVersion::V5, - client_version: "reth/0.1.0".to_string(), - capabilities: vec![Capability::new("eth".into(), EthVersion::Eth67 as usize)], - port: 30303, - id, - }); - - let mut hello_encoded = Vec::new(); - hello.encode(&mut hello_encoded); - - let hello_decoded = P2PMessage::decode(&mut &hello_encoded[..]).unwrap(); - - assert_eq!(hello, hello_decoded); - } - - #[test] - fn hello_encoding_length() { - let secret_key = SecretKey::new(&mut rand::thread_rng()); - let id = pk2id(&secret_key.public_key(SECP256K1)); - let hello = P2PMessage::Hello(HelloMessage { - protocol_version: ProtocolVersion::V5, - client_version: "reth/0.1.0".to_string(), - capabilities: vec![Capability::new("eth".into(), EthVersion::Eth67 as usize)], - port: 30303, - id, - }); - - let mut hello_encoded = Vec::new(); - hello.encode(&mut hello_encoded); - - assert_eq!(hello_encoded.len(), hello.length()); - } - - #[test] - fn hello_message_id_prefix() { - // ensure that the hello message id is prefixed - let secret_key = SecretKey::new(&mut rand::thread_rng()); - let id = pk2id(&secret_key.public_key(SECP256K1)); - let hello = P2PMessage::Hello(HelloMessage { - protocol_version: ProtocolVersion::V5, - client_version: "reth/0.1.0".to_string(), - capabilities: vec![Capability::new("eth".into(), EthVersion::Eth67 as usize)], - port: 30303, - id, - }); - - let mut hello_encoded = Vec::new(); - hello.encode(&mut hello_encoded); - - // zero is encoded as 0x80, the empty string code in RLP - assert_eq!(hello_encoded[0], EMPTY_STRING_CODE); - } } diff --git a/crates/net/eth-wire/src/types/blocks.rs b/crates/net/eth-wire/src/types/blocks.rs index f9547e4a2..dc60c2cf0 100644 --- a/crates/net/eth-wire/src/types/blocks.rs +++ b/crates/net/eth-wire/src/types/blocks.rs @@ -36,7 +36,15 @@ pub struct GetBlockHeaders { /// The response to [`GetBlockHeaders`], containing headers if any headers were found. #[derive( - Clone, Debug, PartialEq, Eq, RlpEncodableWrapper, RlpDecodableWrapper, Serialize, Deserialize, + Clone, + Debug, + PartialEq, + Eq, + RlpEncodableWrapper, + RlpDecodableWrapper, + Serialize, + Deserialize, + Default, )] pub struct BlockHeaders( /// The requested headers. @@ -51,7 +59,15 @@ impl From> for BlockHeaders { /// A request for a peer to return block bodies for the given block hashes. #[derive( - Clone, Debug, PartialEq, Eq, RlpEncodableWrapper, RlpDecodableWrapper, Serialize, Deserialize, + Clone, + Debug, + PartialEq, + Eq, + RlpEncodableWrapper, + RlpDecodableWrapper, + Serialize, + Deserialize, + Default, )] pub struct GetBlockBodies( /// The block hashes to request bodies for. @@ -66,7 +82,9 @@ impl From> for GetBlockBodies { // TODO(onbjerg): We should have this type in primitives /// A response to [`GetBlockBodies`], containing bodies if any bodies were found. -#[derive(Clone, Debug, PartialEq, Eq, RlpEncodable, RlpDecodable, Serialize, Deserialize)] +#[derive( + Clone, Debug, PartialEq, Eq, RlpEncodable, RlpDecodable, Serialize, Deserialize, Default, +)] pub struct BlockBody { /// Transactions in the block pub transactions: Vec, @@ -88,7 +106,15 @@ impl BlockBody { /// The response to [`GetBlockBodies`], containing the block bodies that the peer knows about if /// any were found. #[derive( - Clone, Debug, PartialEq, Eq, RlpEncodableWrapper, RlpDecodableWrapper, Serialize, Deserialize, + Clone, + Debug, + PartialEq, + Eq, + RlpEncodableWrapper, + RlpDecodableWrapper, + Serialize, + Deserialize, + Default, )] pub struct BlockBodies( /// The requested block bodies, each of which should correspond to a hash in the request. diff --git a/crates/net/eth-wire/src/types/broadcast.rs b/crates/net/eth-wire/src/types/broadcast.rs index 5494b68bb..c3ea5ef7a 100644 --- a/crates/net/eth-wire/src/types/broadcast.rs +++ b/crates/net/eth-wire/src/types/broadcast.rs @@ -6,7 +6,15 @@ use std::sync::Arc; /// This informs peers of new blocks that have appeared on the network. #[derive( - Clone, Debug, PartialEq, Eq, RlpEncodableWrapper, RlpDecodableWrapper, Serialize, Deserialize, + Clone, + Debug, + PartialEq, + Eq, + RlpEncodableWrapper, + RlpDecodableWrapper, + Serialize, + Deserialize, + Default, )] pub struct NewBlockHashes( /// New block hashes and the block number for each blockhash. @@ -29,7 +37,9 @@ impl NewBlockHashes { } /// A block hash _and_ a block number. -#[derive(Clone, Debug, PartialEq, Eq, RlpEncodable, RlpDecodable, Serialize, Deserialize)] +#[derive( + Clone, Debug, PartialEq, Eq, RlpEncodable, RlpDecodable, Serialize, Deserialize, Default, +)] pub struct BlockHashNumber { /// The block hash pub hash: H256, @@ -64,7 +74,9 @@ pub struct RawBlockBody { /// A new block with the current total difficulty, which includes the difficulty of the returned /// block. -#[derive(Clone, Debug, PartialEq, Eq, RlpEncodable, RlpDecodable, Serialize, Deserialize)] +#[derive( + Clone, Debug, PartialEq, Eq, RlpEncodable, RlpDecodable, Serialize, Deserialize, Default, +)] pub struct NewBlock { /// A new block. pub block: RawBlockBody, @@ -75,7 +87,15 @@ pub struct NewBlock { /// This informs peers of transactions that have appeared on the network and are not yet included /// in a block. #[derive( - Clone, Debug, PartialEq, Eq, RlpEncodableWrapper, RlpDecodableWrapper, Serialize, Deserialize, + Clone, + Debug, + PartialEq, + Eq, + RlpEncodableWrapper, + RlpDecodableWrapper, + Serialize, + Deserialize, + Default, )] pub struct Transactions( /// New transactions for the peer to include in its mempool. @@ -107,7 +127,15 @@ pub struct SharedTransactions( /// This informs peers of transaction hashes for transactions that have appeared on the network, /// but have not been included in a block. #[derive( - Clone, Debug, PartialEq, Eq, RlpEncodableWrapper, RlpDecodableWrapper, Serialize, Deserialize, + Clone, + Debug, + PartialEq, + Eq, + RlpEncodableWrapper, + RlpDecodableWrapper, + Serialize, + Deserialize, + Default, )] pub struct NewPooledTransactionHashes( /// Transaction hashes for new transactions that have appeared on the network. diff --git a/crates/net/eth-wire/src/types/receipts.rs b/crates/net/eth-wire/src/types/receipts.rs index de54f591c..da3c8f26d 100644 --- a/crates/net/eth-wire/src/types/receipts.rs +++ b/crates/net/eth-wire/src/types/receipts.rs @@ -5,7 +5,15 @@ use serde::{Deserialize, Serialize}; /// A request for transaction receipts from the given block hashes. #[derive( - Clone, Debug, PartialEq, Eq, RlpEncodableWrapper, RlpDecodableWrapper, Serialize, Deserialize, + Clone, + Debug, + PartialEq, + Eq, + RlpEncodableWrapper, + RlpDecodableWrapper, + Serialize, + Deserialize, + Default, )] pub struct GetReceipts( /// The block hashes to request receipts for. @@ -15,7 +23,15 @@ pub struct GetReceipts( /// The response to [`GetReceipts`], containing receipt lists that correspond to each block /// requested. #[derive( - Clone, Debug, PartialEq, Eq, RlpEncodableWrapper, RlpDecodableWrapper, Serialize, Deserialize, + Clone, + Debug, + PartialEq, + Eq, + RlpEncodableWrapper, + RlpDecodableWrapper, + Serialize, + Deserialize, + Default, )] pub struct Receipts( /// Each receipt hash should correspond to a block hash in the request. diff --git a/crates/net/eth-wire/src/types/state.rs b/crates/net/eth-wire/src/types/state.rs index 26f3f9e4f..3a3255119 100644 --- a/crates/net/eth-wire/src/types/state.rs +++ b/crates/net/eth-wire/src/types/state.rs @@ -7,7 +7,15 @@ use serde::{Deserialize, Serialize}; /// This message was removed in `eth/67`, only clients running `eth/66` or earlier will respond to /// this message. #[derive( - Clone, Debug, PartialEq, Eq, RlpEncodableWrapper, RlpDecodableWrapper, Serialize, Deserialize, + Clone, + Debug, + PartialEq, + Eq, + RlpEncodableWrapper, + RlpDecodableWrapper, + Serialize, + Deserialize, + Default, )] pub struct GetNodeData(pub Vec); @@ -17,7 +25,15 @@ pub struct GetNodeData(pub Vec); /// Not all nodes are guaranteed to be returned by the peer. /// This message was removed in `eth/67`. #[derive( - Clone, Debug, PartialEq, Eq, RlpEncodableWrapper, RlpDecodableWrapper, Serialize, Deserialize, + Clone, + Debug, + PartialEq, + Eq, + RlpEncodableWrapper, + RlpDecodableWrapper, + Serialize, + Deserialize, + Default, )] pub struct NodeData(pub Vec); diff --git a/crates/net/eth-wire/src/types/transactions.rs b/crates/net/eth-wire/src/types/transactions.rs index 1024dd7a1..61b19bda2 100644 --- a/crates/net/eth-wire/src/types/transactions.rs +++ b/crates/net/eth-wire/src/types/transactions.rs @@ -5,7 +5,15 @@ use serde::{Deserialize, Serialize}; /// A list of transaction hashes that the peer would like transaction bodies for. #[derive( - Clone, Debug, PartialEq, Eq, RlpEncodableWrapper, RlpDecodableWrapper, Serialize, Deserialize, + Clone, + Debug, + PartialEq, + Eq, + RlpEncodableWrapper, + RlpDecodableWrapper, + Serialize, + Deserialize, + Default, )] pub struct GetPooledTransactions( /// The transaction hashes to request transaction bodies for. @@ -29,7 +37,15 @@ where /// corresponds to a requested hash. Hashes may need to be re-requested if the bodies are not /// included in the response. #[derive( - Clone, Debug, PartialEq, Eq, RlpEncodableWrapper, RlpDecodableWrapper, Serialize, Deserialize, + Clone, + Debug, + PartialEq, + Eq, + RlpEncodableWrapper, + RlpDecodableWrapper, + Serialize, + Deserialize, + Default, )] pub struct PooledTransactions( /// The transaction bodies, each of which should correspond to a requested hash. diff --git a/crates/net/eth-wire/tests/fuzz_roundtrip.rs b/crates/net/eth-wire/tests/fuzz_roundtrip.rs new file mode 100644 index 000000000..d25c8351f --- /dev/null +++ b/crates/net/eth-wire/tests/fuzz_roundtrip.rs @@ -0,0 +1,116 @@ +//! Round-trip encoding fuzzing for the `eth-wire` crate. +use reth_rlp::{Decodable, Encodable}; +use serde::Serialize; +use std::fmt::Debug; + +/// Creates a fuzz test for a type that should be [`Encodable`](reth_rlp::Encodable) and +/// [`Decodable`](reth_rlp::Decodable). +/// +/// The test will create a random instance of the type, encode it, and then decode it. +fn roundtrip_encoding(thing: T) +where + T: Encodable + Decodable + Clone + Serialize + Debug + PartialEq + Eq, +{ + let mut encoded = Vec::new(); + thing.encode(&mut encoded); + let decoded = T::decode(&mut &encoded[..]).unwrap(); + assert_eq!(thing, decoded, "expected: {thing:?}, got: {decoded:?}"); +} + +/// Creates a fuzz test for a rlp encodable and decodable type. +macro_rules! fuzz_type_and_name { + ( $x:ty, $fuzzname:ident ) => { + /// Fuzzes the round-trip encoding of the type. + #[test_fuzz] + #[allow(non_snake_case)] + fn $fuzzname(thing: $x) { + roundtrip_encoding::<$x>(thing) + } + }; +} + +#[allow(non_snake_case)] +#[cfg(any(test, feature = "bench"))] +pub mod fuzz_rlp { + use reth_eth_wire::{ + BlockBodies, BlockHeaders, DisconnectReason, GetBlockBodies, GetBlockHeaders, GetNodeData, + GetPooledTransactions, GetReceipts, HelloMessage, NewBlock, NewBlockHashes, + NewPooledTransactionHashes, NodeData, P2PMessage, PooledTransactions, Receipts, Status, + Transactions, + }; + use reth_primitives::BlockHashOrNumber; + use reth_rlp::{RlpDecodableWrapper, RlpEncodableWrapper}; + use serde::{Deserialize, Serialize}; + use test_fuzz::test_fuzz; + + use crate::roundtrip_encoding; + + // p2p subprotocol messages + fuzz_type_and_name!(HelloMessage, fuzz_HelloMessage); + fuzz_type_and_name!(DisconnectReason, fuzz_DisconnectReason); + + // eth subprotocol messages + fuzz_type_and_name!(Status, fuzz_Status); + fuzz_type_and_name!(NewBlockHashes, fuzz_NewBlockHashes); + fuzz_type_and_name!(Transactions, fuzz_Transactions); + + // GetBlockHeaders implements all the traits required for roundtrip_encoding, so why is this + // wrapper type needed? + // + // While GetBlockHeaders implements all traits needed to work for test-fuzz, it does not have + // an obvious Default implementation since BlockHashOrNumber can be either a hash or number, + // and the default value of BlockHashOrNumber is not obvious. + // + // We just provide a default value here so test-fuzz can auto-generate a corpus file for the + // type. + #[derive( + Clone, + Debug, + PartialEq, + Eq, + Serialize, + Deserialize, + RlpEncodableWrapper, + RlpDecodableWrapper, + )] + struct GetBlockHeadersWrapper(pub GetBlockHeaders); + + impl Default for GetBlockHeadersWrapper { + fn default() -> Self { + GetBlockHeadersWrapper(GetBlockHeaders { + start_block: BlockHashOrNumber::Number(0), + limit: Default::default(), + skip: Default::default(), + direction: Default::default(), + }) + } + } + + fuzz_type_and_name!(GetBlockHeadersWrapper, fuzz_GetBlockHeaders); + + fuzz_type_and_name!(BlockHeaders, fuzz_BlockHeaders); + fuzz_type_and_name!(GetBlockBodies, fuzz_GetBlockBodies); + fuzz_type_and_name!(BlockBodies, fuzz_BlockBodies); + fuzz_type_and_name!(NewBlock, fuzz_NewBlock); + fuzz_type_and_name!(NewPooledTransactionHashes, fuzz_NewPooledTransactionHashes); + fuzz_type_and_name!(GetPooledTransactions, fuzz_GetPooledTransactions); + fuzz_type_and_name!(PooledTransactions, fuzz_PooledTransactions); + fuzz_type_and_name!(GetNodeData, fuzz_GetNodeData); + fuzz_type_and_name!(NodeData, fuzz_NodeData); + fuzz_type_and_name!(GetReceipts, fuzz_GetReceipts); + fuzz_type_and_name!(Receipts, fuzz_Receipts); + + // manually test Ping and Pong which are not covered by the above + + /// Tests the round-trip encoding of Ping + #[test] + fn roundtrip_ping() { + roundtrip_encoding::(P2PMessage::Ping) + } + + /// Tests the round-trip encoding of Pong + #[test] + fn roundtrip_pong() { + roundtrip_encoding::(P2PMessage::Pong) + } +} diff --git a/crates/storage/db/Cargo.toml b/crates/storage/db/Cargo.toml index eff42b2be..eaf85275e 100644 --- a/crates/storage/db/Cargo.toml +++ b/crates/storage/db/Cargo.toml @@ -37,12 +37,16 @@ eyre = "0.6.8" [dev-dependencies] tempfile = "3.3.0" test-fuzz = "3.0.4" + criterion = "0.4.0" iai = "0.1.1" tokio = { version = "1.21.2", features = ["full"] } arbitrary = { version = "1.1.7", features = ["derive"]} reth-db = { path = ".", features = ["test-utils","bench"]} +# needed for test-fuzz to work properly, see https://github.com/paradigmxyz/reth/pull/177#discussion_r1021172198 +secp256k1 = "0.24.0" + reth-interfaces = { path = "../../interfaces",features=["bench"] } async-trait = "0.1.58"