From 5d33a97ab2e0760b00419b6e973217abbbb6498d Mon Sep 17 00:00:00 2001 From: Mikhail Zabaluev Date: Mon, 11 Dec 2023 12:23:34 +0200 Subject: [PATCH] Remove p2p, std-ext, test crates Do not continue the p2p crate as part of cometbft-rs, before the need for it is clear. Also drop the auxiliary crates whose only use was to provide functionality for, or test, tendermint-p2p. --- Cargo.toml | 3 - p2p/Cargo.toml | 52 -- p2p/README.md | 19 - p2p/src/error.rs | 69 -- p2p/src/lib.rs | 28 - p2p/src/secret_connection.rs | 662 ------------------ p2p/src/secret_connection/amino_types.rs | 63 -- p2p/src/secret_connection/kdf.rs | 57 -- p2p/src/secret_connection/nonce.rs | 35 - p2p/src/secret_connection/protocol.rs | 261 ------- p2p/src/secret_connection/public_key.rs | 66 -- p2p/src/transport.rs | 143 ---- release.sh | 2 - std-ext/Cargo.toml | 20 - std-ext/README.md | 24 - std-ext/src/lib.rs | 8 - std-ext/src/try_clone.rs | 27 - test/Cargo.toml | 27 - test/src/lib.rs | 5 - test/src/pipe.rs | 193 ----- test/src/test.rs | 1 - test/src/test/unit.rs | 1 - test/src/test/unit/p2p.rs | 1 - test/src/test/unit/p2p/secret_connection.rs | 192 ----- .../test/unit/p2p/secret_connection/nonce.rs | 35 - .../unit/p2p/secret_connection/public_key.rs | 16 - 26 files changed, 2010 deletions(-) delete mode 100644 p2p/Cargo.toml delete mode 100644 p2p/README.md delete mode 100644 p2p/src/error.rs delete mode 100644 p2p/src/lib.rs delete mode 100644 p2p/src/secret_connection.rs delete mode 100644 p2p/src/secret_connection/amino_types.rs delete mode 100644 p2p/src/secret_connection/kdf.rs delete mode 100644 p2p/src/secret_connection/nonce.rs delete mode 100644 p2p/src/secret_connection/protocol.rs delete mode 100644 p2p/src/secret_connection/public_key.rs delete mode 100644 p2p/src/transport.rs delete mode 100644 std-ext/Cargo.toml delete mode 100644 std-ext/README.md delete mode 100644 std-ext/src/lib.rs delete mode 100644 std-ext/src/try_clone.rs delete mode 100644 test/Cargo.toml delete mode 100644 test/src/lib.rs delete mode 100644 test/src/pipe.rs delete mode 100644 test/src/test.rs delete mode 100644 test/src/test/unit.rs delete mode 100644 test/src/test/unit/p2p.rs delete mode 100644 test/src/test/unit/p2p/secret_connection.rs delete mode 100644 test/src/test/unit/p2p/secret_connection/nonce.rs delete mode 100644 test/src/test/unit/p2p/secret_connection/public_key.rs diff --git a/Cargo.toml b/Cargo.toml index 971bd413..93e4f549 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,12 +9,9 @@ members = [ "light-client-detector", "light-client-cli", "light-client-js", - "p2p", "pbt-gen", "proto", "rpc", - "std-ext", - "test", "testgen" ] diff --git a/p2p/Cargo.toml b/p2p/Cargo.toml deleted file mode 100644 index 4a0fce2d..00000000 --- a/p2p/Cargo.toml +++ /dev/null @@ -1,52 +0,0 @@ -[package] -name = "cometbft-p2p" -version = "0.34.0" -edition = "2021" -license = "Apache-2.0" -repository = "https://github.com/cometbft/cometbft-rs" -homepage = "https://cometbft.com" -readme = "README.md" -keywords = ["p2p", "cometbft", "tendermint", "cosmos"] -categories = ["cryptography::cryptocurrencies", "network-programming"] -authors = [ - "Informal Systems ", - "Alexander Simmerl ", - "Tony Arcieri ", - "Ismail Khoffi ", -] - -description = """ - The CometBFT P2P stack in Rust. - """ - -[lib] -test = false - -[features] -default = ["flex-error/std", "flex-error/eyre_tracer"] -amino = ["prost-derive"] - -[dependencies] -chacha20poly1305 = { version = "0.10", default-features = false, features = ["reduced-round"] } -curve25519-dalek-ng = { version = "4", default-features = false } -ed25519-consensus = { version = "2", default-features = false } -eyre = { version = "0.6", default-features = false } -flume = { version = "0.10.7", default-features = false } -hkdf = { version = "0.12.3", default-features = false } -merlin = { version = "2", default-features = false } -prost = { version = "0.12", default-features = false } -rand_core = { version = "0.6", default-features = false, features = ["std"] } -sha2 = { version = "0.10", default-features = false } -subtle = { version = "2", default-features = false } -zeroize = { version = "1", default-features = false } -signature = { version = "2", default-features = false } -aead = { version = "0.5", default-features = false } -flex-error = { version = "0.4.4", default-features = false } - -# path dependencies -cometbft = { path = "../cometbft", version = "0.34.0", default-features = false } -cometbft-proto = { path = "../proto", version = "0.34.0", default-features = false } -cometbft-std-ext = { path = "../std-ext", version = "0.34.0", default-features = false } - -# optional dependencies -prost-derive = { version = "0.12", optional = true } diff --git a/p2p/README.md b/p2p/README.md deleted file mode 100644 index ee1b3ed4..00000000 --- a/p2p/README.md +++ /dev/null @@ -1,19 +0,0 @@ -[![Crate][crate-image]][crate-link] -[![Docs][docs-image]][docs-link] - -See the [repo root] for build status, license, Rust version, etc. - -# tendermint-p2p - -The Tendermint P2P stack. - -[//]: # (badges) - -[crate-image]: https://img.shields.io/crates/v/tendermint-p2p.svg -[crate-link]: https://crates.io/crates/tendermint-p2p -[docs-image]: https://docs.rs/tendermint-p2p/badge.svg -[docs-link]: https://docs.rs/tendermint-p2p/ - -[//]: # (general links) - -[repo root]: https://github.com/informalsystems/tendermint-rs diff --git a/p2p/src/error.rs b/p2p/src/error.rs deleted file mode 100644 index 053649d4..00000000 --- a/p2p/src/error.rs +++ /dev/null @@ -1,69 +0,0 @@ -//! Error types - -// Related to the `Error` definition below. -// TODO(soares): Update flex-error accordingly to address this. -#![allow(clippy::use_self)] - -use flex_error::{define_error, DisplayOnly}; -use prost::DecodeError; - -define_error! { - Error { - Crypto - | _ | { "cryptographic error" }, - - InvalidKey - | _ | { "invalid key" }, - - LowOrderKey - | _ | { "low-order points found (potential MitM attack!)" }, - - Protocol - | _ | { "protocol error" }, - - MalformedHandshake - | _ | { "malformed handshake message (protocol version mismatch?)" }, - - Io - [ DisplayOnly ] - | _ | { "io error" }, - - Decode - [ DisplayOnly ] - | _ | { "malformed handshake message (protocol version mismatch?)" }, - - MissingSecret - | _ | { "missing secret: forgot to call Handshake::new?" }, - - MissingKey - | _ | { "public key missing" }, - - Signature - | _ | { "signature error" }, - - UnsupportedKey - | _ | { "secp256k1 is not supported" }, - - Aead - [ DisplayOnly ] - | _ | { "aead error" }, - - ShortCiphertext - { tag_size: usize } - | e | { format_args!("ciphertext must be at least as long as a MAC tag {}", e.tag_size) }, - - SmallOutputBuffer - | _ | { "output buffer is too small" }, - - TransportClone - { detail: String } - | e | { format_args!("failed to clone underlying transport: {}", e.detail) } - - } -} - -impl From for Error { - fn from(e: std::io::Error) -> Self { - Self::io(e) - } -} diff --git a/p2p/src/lib.rs b/p2p/src/lib.rs deleted file mode 100644 index efcc9a17..00000000 --- a/p2p/src/lib.rs +++ /dev/null @@ -1,28 +0,0 @@ -//! The Tendermint P2P stack. - -#![forbid(unsafe_code)] -#![deny( - nonstandard_style, - private_in_public, - rust_2018_idioms, - trivial_casts, - trivial_numeric_casts, - unused_import_braces, - unused_qualifications -)] -#![warn( - clippy::all, - clippy::cargo, - clippy::nursery, - clippy::pedantic, - clippy::unwrap_used, - unused_import_braces, - unused_qualifications -)] -#![doc( - html_logo_url = "https://raw.githubusercontent.com/informalsystems/tendermint-rs/master/img/logo-tendermint-rs_3961x4001.png" -)] - -pub mod error; -pub mod secret_connection; -pub mod transport; diff --git a/p2p/src/secret_connection.rs b/p2p/src/secret_connection.rs deleted file mode 100644 index ded1fd47..00000000 --- a/p2p/src/secret_connection.rs +++ /dev/null @@ -1,662 +0,0 @@ -//! `SecretConnection`: Transport layer encryption for Tendermint P2P connections. - -use std::{ - cmp, - convert::{TryFrom, TryInto}, - io::{self, Read, Write}, - marker::{Send, Sync}, - slice, - sync::{ - atomic::{AtomicBool, Ordering}, - Arc, - }, -}; - -use chacha20poly1305::{ - aead::{generic_array::GenericArray, AeadInPlace, KeyInit}, - ChaCha20Poly1305, -}; -use cometbft_proto::v0_38 as proto; -use cometbft_std_ext::TryClone; -use curve25519_dalek_ng::{ - constants::X25519_BASEPOINT, montgomery::MontgomeryPoint as EphemeralPublic, - scalar::Scalar as EphemeralSecret, -}; -use merlin::Transcript; -use rand_core::OsRng; -use subtle::ConstantTimeEq; - -pub use self::{ - kdf::Kdf, - nonce::{Nonce, SIZE as NONCE_SIZE}, - protocol::Version, - public_key::PublicKey, -}; -use crate::error::Error; - -#[cfg(feature = "amino")] -mod amino_types; - -mod kdf; -mod nonce; -mod protocol; -mod public_key; - -/// Size of the MAC tag -pub const TAG_SIZE: usize = 16; - -/// Maximum size of a message -pub const DATA_MAX_SIZE: usize = 1024; - -/// 4 + 1024 == 1028 total frame size -const DATA_LEN_SIZE: usize = 4; -const TOTAL_FRAME_SIZE: usize = DATA_MAX_SIZE + DATA_LEN_SIZE; - -/// Handshake is a process of establishing the `SecretConnection` between two peers. -/// [Specification](https://github.com/tendermint/spec/blob/master/spec/p2p/peer.md#authenticated-encryption-handshake) -pub struct Handshake { - protocol_version: Version, - state: S, -} - -/// Handshake states - -/// `AwaitingEphKey` means we're waiting for the remote ephemeral pubkey. -pub struct AwaitingEphKey { - local_privkey: ed25519_consensus::SigningKey, - local_eph_privkey: Option, -} - -/// `AwaitingAuthSig` means we're waiting for the remote authenticated signature. -pub struct AwaitingAuthSig { - sc_mac: [u8; 32], - kdf: Kdf, - recv_cipher: ChaCha20Poly1305, - send_cipher: ChaCha20Poly1305, - local_signature: ed25519_consensus::Signature, -} - -#[allow(clippy::use_self)] -impl Handshake { - /// Initiate a handshake. - #[must_use] - pub fn new( - local_privkey: ed25519_consensus::SigningKey, - protocol_version: Version, - ) -> (Self, EphemeralPublic) { - // Generate an ephemeral key for perfect forward secrecy. - let local_eph_privkey = EphemeralSecret::random(&mut OsRng); - let local_eph_pubkey = X25519_BASEPOINT * &local_eph_privkey; - - ( - Self { - protocol_version, - state: AwaitingEphKey { - local_privkey, - local_eph_privkey: Some(local_eph_privkey), - }, - }, - local_eph_pubkey, - ) - } - - /// Performs a Diffie-Hellman key agreement and creates a local signature. - /// Transitions Handshake into `AwaitingAuthSig` state. - /// - /// # Errors - /// - /// * if protocol order was violated, e.g. handshake missing - /// * if challenge signing fails - pub fn got_key( - &mut self, - remote_eph_pubkey: EphemeralPublic, - ) -> Result, Error> { - let Some(local_eph_privkey) = self.state.local_eph_privkey.take() else { - return Err(Error::missing_secret()); - }; - let local_eph_pubkey = X25519_BASEPOINT * &local_eph_privkey; - - // Compute common shared secret. - let shared_secret = &local_eph_privkey * &remote_eph_pubkey; - - let mut transcript = Transcript::new(b"TENDERMINT_SECRET_CONNECTION_TRANSCRIPT_HASH"); - - // Reject all-zero outputs from X25519 (i.e. from low-order points) - // - // See the following for information on potential attacks this check - // aids in mitigating: - // - // - https://github.com/tendermint/kms/issues/142 - // - https://eprint.iacr.org/2019/526.pdf - if shared_secret.as_bytes().ct_eq(&[0x00; 32]).unwrap_u8() == 1 { - return Err(Error::low_order_key()); - } - - // Sort by lexical order. - let local_eph_pubkey_bytes = *local_eph_pubkey.as_bytes(); - let (low_eph_pubkey_bytes, high_eph_pubkey_bytes) = - sort32(local_eph_pubkey_bytes, *remote_eph_pubkey.as_bytes()); - - transcript.append_message(b"EPHEMERAL_LOWER_PUBLIC_KEY", &low_eph_pubkey_bytes); - transcript.append_message(b"EPHEMERAL_UPPER_PUBLIC_KEY", &high_eph_pubkey_bytes); - transcript.append_message(b"DH_SECRET", shared_secret.as_bytes()); - - // Check if the local ephemeral public key was the least, lexicographically sorted. - let loc_is_least = local_eph_pubkey_bytes == low_eph_pubkey_bytes; - - let kdf = Kdf::derive_secrets_and_challenge(shared_secret.as_bytes(), loc_is_least); - - let mut sc_mac: [u8; 32] = [0; 32]; - - transcript.challenge_bytes(b"SECRET_CONNECTION_MAC", &mut sc_mac); - - // Sign the challenge bytes for authentication. - let local_signature = if self.protocol_version.has_transcript() { - self.state.local_privkey.sign(&sc_mac) - } else { - self.state.local_privkey.sign(&kdf.challenge) - }; - - Ok(Handshake { - protocol_version: self.protocol_version, - state: AwaitingAuthSig { - sc_mac, - recv_cipher: ChaCha20Poly1305::new(&kdf.recv_secret.into()), - send_cipher: ChaCha20Poly1305::new(&kdf.send_secret.into()), - kdf, - local_signature, - }, - }) - } -} - -impl Handshake { - /// Returns a verified pubkey of the remote peer. - /// - /// # Errors - /// - /// * if signature scheme isn't supported - pub fn got_signature( - &mut self, - auth_sig_msg: proto::p2p::AuthSigMessage, - ) -> Result { - let pk_sum = auth_sig_msg - .pub_key - .and_then(|key| key.sum) - .ok_or_else(Error::missing_key)?; - - let remote_pubkey = match pk_sum { - proto::crypto::public_key::Sum::Ed25519(ref bytes) => { - ed25519_consensus::VerificationKey::try_from(&bytes[..]) - .map_err(|_| Error::signature()) - }, - proto::crypto::public_key::Sum::Secp256k1(_) => Err(Error::unsupported_key()), - }?; - - let remote_sig = ed25519_consensus::Signature::try_from(auth_sig_msg.sig.as_slice()) - .map_err(|_| Error::signature())?; - - if self.protocol_version.has_transcript() { - remote_pubkey - .verify(&remote_sig, &self.state.sc_mac) - .map_err(|_| Error::signature())?; - } else { - remote_pubkey - .verify(&remote_sig, &self.state.kdf.challenge) - .map_err(|_| Error::signature())?; - } - - // We've authorized. - Ok(remote_pubkey.into()) - } -} - -// Macro usage allows us to avoid unnecessarily cloning the Arc -// that indicates whether we need to terminate the connection. -// -// Limitation: this only checks once prior to the execution of an I/O operation -// whether we need to terminate. This should be sufficient for our purposes -// though. -macro_rules! checked_io { - ($term:expr, $f:expr) => {{ - if $term.load(Ordering::SeqCst) { - return Err(io::Error::new( - io::ErrorKind::Other, - "secret connection was terminated elsewhere by previous error", - )); - } - let result = { $f }; - if result.is_err() { - $term.store(true, Ordering::SeqCst); - } - result - }}; -} - -/// Encrypted connection between peers in a Tendermint network. -/// -/// ## Connection integrity and failures -/// -/// Due to the underlying encryption mechanism (currently [RFC 8439]), when a -/// read or write failure occurs, it is necessary to disconnect from the remote -/// peer and attempt to reconnect. -/// -/// ## Half- and full-duplex connections -/// By default, a `SecretConnection` facilitates half-duplex operations (i.e. -/// one can either read from the connection or write to it at a given time, but -/// not both simultaneously). -/// -/// If, however, the underlying I/O handler class implements -/// [`cometbft_std_ext::TryClone`], then you can use -/// [`SecretConnection::split`] to split the `SecretConnection` into its -/// sending and receiving halves. Each of these halves can then be used in a -/// separate thread to facilitate full-duplex communication. -/// -/// ## Contracts -/// -/// When reading data, data smaller than [`DATA_MAX_SIZE`] is read atomically. -/// -/// [RFC 8439]: https://www.rfc-editor.org/rfc/rfc8439.html -pub struct SecretConnection { - io_handler: IoHandler, - protocol_version: Version, - remote_pubkey: Option, - send_state: SendState, - recv_state: ReceiveState, - terminate: Arc, -} - -impl SecretConnection { - /// Returns the remote pubkey. Panics if there's no key. - pub fn remote_pubkey(&self) -> PublicKey { - self.remote_pubkey.expect("remote_pubkey uninitialized") - } - - /// Performs a handshake and returns a new `SecretConnection`. - /// - /// # Errors - /// - /// * if sharing of the pubkey fails - /// * if sharing of the signature fails - /// * if receiving the signature fails - pub fn new( - mut io_handler: IoHandler, - local_privkey: ed25519_consensus::SigningKey, - protocol_version: Version, - ) -> Result { - // Start a handshake process. - let local_pubkey = PublicKey::from(&local_privkey); - let (mut h, local_eph_pubkey) = Handshake::new(local_privkey, protocol_version); - - // Write local ephemeral pubkey and receive one too. - let remote_eph_pubkey = - share_eph_pubkey(&mut io_handler, &local_eph_pubkey, protocol_version)?; - - // Compute a local signature (also recv_cipher & send_cipher) - let mut h = h.got_key(remote_eph_pubkey)?; - - let mut sc = Self { - io_handler, - protocol_version, - remote_pubkey: None, - send_state: SendState { - cipher: h.state.send_cipher.clone(), - nonce: Nonce::default(), - }, - recv_state: ReceiveState { - cipher: h.state.recv_cipher.clone(), - nonce: Nonce::default(), - buffer: vec![], - }, - terminate: Arc::new(AtomicBool::new(false)), - }; - - // Share each other's pubkey & challenge signature. - // NOTE: the data must be encrypted/decrypted using ciphers. - let auth_sig_msg = match local_pubkey { - PublicKey::Ed25519(ref pk) => { - share_auth_signature(&mut sc, pk, &h.state.local_signature)? - }, - }; - - // Authenticate remote pubkey. - let remote_pubkey = h.got_signature(auth_sig_msg)?; - - // All good! - sc.remote_pubkey = Some(remote_pubkey); - Ok(sc) - } -} - -impl SecretConnection -where - IoHandler: TryClone, - ::Error: std::error::Error + Send + Sync + 'static, -{ - /// For secret connections whose underlying I/O layer implements - /// [`cometbft_std_ext::TryClone`], this attempts to split such a - /// connection into its sending and receiving halves. - /// - /// This facilitates full-duplex communications when each half is used in - /// a separate thread. - /// - /// ## Errors - /// Fails when the `try_clone` operation for the underlying I/O handler - /// fails. - pub fn split(self) -> Result<(Sender, Receiver), Error> { - let remote_pubkey = self.remote_pubkey.expect("remote_pubkey to be initialized"); - Ok(( - Sender { - io_handler: self - .io_handler - .try_clone() - .map_err(|e| Error::transport_clone(e.to_string()))?, - remote_pubkey, - state: self.send_state, - terminate: self.terminate.clone(), - }, - Receiver { - io_handler: self.io_handler, - remote_pubkey, - state: self.recv_state, - terminate: self.terminate, - }, - )) - } -} - -impl Read for SecretConnection { - fn read(&mut self, data: &mut [u8]) -> io::Result { - checked_io!( - self.terminate, - read_and_decrypt(&mut self.io_handler, &mut self.recv_state, data) - ) - } -} - -impl Write for SecretConnection { - fn write(&mut self, data: &[u8]) -> io::Result { - checked_io!( - self.terminate, - encrypt_and_write(&mut self.io_handler, &mut self.send_state, data) - ) - } - - fn flush(&mut self) -> io::Result<()> { - checked_io!(self.terminate, self.io_handler.flush()) - } -} - -// Sending state for a `SecretConnection`. -struct SendState { - cipher: ChaCha20Poly1305, - nonce: Nonce, -} - -// Receiving state for a `SecretConnection`. -struct ReceiveState { - cipher: ChaCha20Poly1305, - nonce: Nonce, - buffer: Vec, -} - -/// The sending end of a [`SecretConnection`]. -pub struct Sender { - io_handler: IoHandler, - remote_pubkey: PublicKey, - state: SendState, - terminate: Arc, -} - -impl Sender { - /// Returns the remote pubkey. Panics if there's no key. - pub const fn remote_pubkey(&self) -> PublicKey { - self.remote_pubkey - } -} - -impl Write for Sender { - fn write(&mut self, buf: &[u8]) -> io::Result { - checked_io!( - self.terminate, - encrypt_and_write(&mut self.io_handler, &mut self.state, buf) - ) - } - - fn flush(&mut self) -> io::Result<()> { - checked_io!(self.terminate, self.io_handler.flush()) - } -} - -/// The receiving end of a [`SecretConnection`]. -pub struct Receiver { - io_handler: IoHandler, - remote_pubkey: PublicKey, - state: ReceiveState, - terminate: Arc, -} - -impl Receiver { - /// Returns the remote pubkey. Panics if there's no key. - pub const fn remote_pubkey(&self) -> PublicKey { - self.remote_pubkey - } -} - -impl Read for Receiver { - fn read(&mut self, buf: &mut [u8]) -> io::Result { - checked_io!( - self.terminate, - read_and_decrypt(&mut self.io_handler, &mut self.state, buf) - ) - } -} - -/// Returns `remote_eph_pubkey` -fn share_eph_pubkey( - handler: &mut IoHandler, - local_eph_pubkey: &EphemeralPublic, - protocol_version: Version, -) -> Result { - // Send our pubkey and receive theirs in tandem. - // TODO(ismail): on the go side this is done in parallel, here we do send and receive after - // each other. thread::spawn would require a static lifetime. - // Should still work though. - handler.write_all(&protocol_version.encode_initial_handshake(local_eph_pubkey))?; - - let mut response_len = 0_u8; - handler.read_exact(slice::from_mut(&mut response_len))?; - - let mut buf = vec![0; response_len as usize]; - handler.read_exact(&mut buf)?; - protocol_version.decode_initial_handshake(&buf) -} - -// TODO(ismail): change from DecodeError to something more generic -// this can also fail while writing / sending -fn share_auth_signature( - sc: &mut SecretConnection, - pubkey: &ed25519_consensus::VerificationKey, - local_signature: &ed25519_consensus::Signature, -) -> Result { - let buf = sc - .protocol_version - .encode_auth_signature(pubkey, local_signature); - - sc.write_all(&buf)?; - - let mut buf = vec![0; sc.protocol_version.auth_sig_msg_response_len()]; - sc.read_exact(&mut buf)?; - sc.protocol_version.decode_auth_signature(&buf) -} - -/// Return is of the form lo, hi -#[must_use] -pub fn sort32(first: [u8; 32], second: [u8; 32]) -> ([u8; 32], [u8; 32]) { - if second > first { - (first, second) - } else { - (second, first) - } -} - -/// Encrypt AEAD authenticated data -#[allow(clippy::cast_possible_truncation)] -fn encrypt( - chunk: &[u8], - send_cipher: &ChaCha20Poly1305, - send_nonce: &Nonce, - sealed_frame: &mut [u8; TAG_SIZE + TOTAL_FRAME_SIZE], -) -> Result<(), Error> { - assert!(!chunk.is_empty(), "chunk is empty"); - assert!( - chunk.len() <= TOTAL_FRAME_SIZE - DATA_LEN_SIZE, - "chunk is too big: {}! max: {}", - chunk.len(), - DATA_MAX_SIZE, - ); - sealed_frame[..DATA_LEN_SIZE].copy_from_slice(&(chunk.len() as u32).to_le_bytes()); - sealed_frame[DATA_LEN_SIZE..DATA_LEN_SIZE + chunk.len()].copy_from_slice(chunk); - - let tag = send_cipher - .encrypt_in_place_detached( - GenericArray::from_slice(send_nonce.to_bytes()), - b"", - &mut sealed_frame[..TOTAL_FRAME_SIZE], - ) - .map_err(Error::aead)?; - - sealed_frame[TOTAL_FRAME_SIZE..].copy_from_slice(tag.as_slice()); - - Ok(()) -} - -// Writes encrypted frames of `TAG_SIZE` + `TOTAL_FRAME_SIZE` -fn encrypt_and_write( - io_handler: &mut IoHandler, - send_state: &mut SendState, - data: &[u8], -) -> io::Result { - let mut n = 0_usize; - let mut data_copy = data; - while !data_copy.is_empty() { - let chunk: &[u8]; - if DATA_MAX_SIZE < data.len() { - chunk = &data[..DATA_MAX_SIZE]; - data_copy = &data_copy[DATA_MAX_SIZE..]; - } else { - chunk = data_copy; - data_copy = &[0_u8; 0]; - } - let sealed_frame = &mut [0_u8; TAG_SIZE + TOTAL_FRAME_SIZE]; - encrypt(chunk, &send_state.cipher, &send_state.nonce, sealed_frame) - .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; - send_state.nonce.increment(); - // end encryption - - io_handler.write_all(&sealed_frame[..])?; - n = n - .checked_add(chunk.len()) - .expect("overflow when adding chunk lengths"); - } - - Ok(n) -} - -/// Decrypt AEAD authenticated data -fn decrypt( - ciphertext: &[u8], - recv_cipher: &ChaCha20Poly1305, - recv_nonce: &Nonce, - out: &mut [u8], -) -> Result { - if ciphertext.len() < TAG_SIZE { - return Err(Error::short_ciphertext(TAG_SIZE)); - } - - // Split ChaCha20 ciphertext from the Poly1305 tag - let (ct, tag) = ciphertext.split_at(ciphertext.len() - TAG_SIZE); - - if out.len() < ct.len() { - return Err(Error::small_output_buffer()); - } - - let in_out = &mut out[..ct.len()]; - in_out.copy_from_slice(ct); - - recv_cipher - .decrypt_in_place_detached( - GenericArray::from_slice(recv_nonce.to_bytes()), - b"", - in_out, - tag.into(), - ) - .map_err(Error::aead)?; - - Ok(in_out.len()) -} - -fn read_and_decrypt( - io_handler: &mut IoHandler, - recv_state: &mut ReceiveState, - data: &mut [u8], -) -> io::Result { - if !recv_state.buffer.is_empty() { - let n = cmp::min(data.len(), recv_state.buffer.len()); - data.copy_from_slice(&recv_state.buffer[..n]); - let mut leftover_portion = vec![ - 0; - recv_state - .buffer - .len() - .checked_sub(n) - .expect("leftover calculation failed") - ]; - leftover_portion.clone_from_slice(&recv_state.buffer[n..]); - recv_state.buffer = leftover_portion; - - return Ok(n); - } - - let mut sealed_frame = [0_u8; TAG_SIZE + TOTAL_FRAME_SIZE]; - io_handler.read_exact(&mut sealed_frame)?; - - // decrypt the frame - let mut frame = [0_u8; TOTAL_FRAME_SIZE]; - let res = decrypt( - &sealed_frame, - &recv_state.cipher, - &recv_state.nonce, - &mut frame, - ); - - if let Err(err) = res { - return Err(io::Error::new(io::ErrorKind::Other, err.to_string())); - } - - recv_state.nonce.increment(); - // end decryption - - let chunk_length = u32::from_le_bytes(frame[..4].try_into().expect("chunk framing failed")); - - if chunk_length as usize > DATA_MAX_SIZE { - return Err(io::Error::new( - io::ErrorKind::Other, - format!("chunk is too big: {chunk_length}! max: {DATA_MAX_SIZE}"), - )); - } - - let mut chunk = vec![0; chunk_length as usize]; - chunk.clone_from_slice( - &frame[DATA_LEN_SIZE - ..(DATA_LEN_SIZE - .checked_add(chunk_length as usize) - .expect("chunk size addition overflow"))], - ); - - let n = cmp::min(data.len(), chunk.len()); - data[..n].copy_from_slice(&chunk[..n]); - recv_state.buffer.copy_from_slice(&chunk[n..]); - - Ok(n) -} diff --git a/p2p/src/secret_connection/amino_types.rs b/p2p/src/secret_connection/amino_types.rs deleted file mode 100644 index 31e660e2..00000000 --- a/p2p/src/secret_connection/amino_types.rs +++ /dev/null @@ -1,63 +0,0 @@ -//! Amino types used by Secret Connection - -use cometbft_proto::v0_38 as proto; -use core::convert::TryFrom; -use prost_derive::Message; - -use crate::error::Error; - -/// Amino prefix for "tendermint/PubKeyEd25519" (4-bytes) + length prefix (1-byte) -const PUB_KEY_ED25519_AMINO_PREFIX: [u8; 5] = [0x16, 0x24, 0xde, 0x64, 0x20]; - -/// Authentication signature message -#[derive(Clone, PartialEq, Eq, Message)] -pub struct AuthSigMessage { - /// Public key - #[prost(bytes, tag = "1")] - pub pub_key: Vec, - - /// Signature - #[prost(bytes, tag = "2")] - pub sig: Vec, -} - -impl AuthSigMessage { - pub fn new( - pub_key: &ed25519_consensus::VerificationKey, - sig: &ed25519_consensus::Signature, - ) -> Self { - let mut pub_key_bytes = Vec::from(PUB_KEY_ED25519_AMINO_PREFIX); - pub_key_bytes.extend_from_slice(pub_key.as_bytes()); - - Self { - pub_key: pub_key_bytes, - sig: sig.to_bytes().to_vec(), - } - } -} - -impl TryFrom for proto::p2p::AuthSigMessage { - type Error = Error; - - fn try_from(amino_msg: AuthSigMessage) -> Result { - // Strip Amino prefix - if amino_msg.pub_key.len() < 5 { - return Err(Error::protocol()); - } - - let (amino_prefix, pub_key) = amino_msg.pub_key.split_at(5); - - if amino_prefix != PUB_KEY_ED25519_AMINO_PREFIX { - return Err(Error::protocol()); - } - - let pub_key = proto::crypto::PublicKey { - sum: Some(proto::crypto::public_key::Sum::Ed25519(pub_key.to_vec())), - }; - - Ok(Self { - pub_key: Some(pub_key), - sig: amino_msg.sig, - }) - } -} diff --git a/p2p/src/secret_connection/kdf.rs b/p2p/src/secret_connection/kdf.rs deleted file mode 100644 index b23675e5..00000000 --- a/p2p/src/secret_connection/kdf.rs +++ /dev/null @@ -1,57 +0,0 @@ -use hkdf::Hkdf; -use sha2::Sha256; -use zeroize::Zeroize; - -/// "Info" parameter to HKDF we use to personalize the derivation -const HKDF_INFO: &[u8] = b"TENDERMINT_SECRET_CONNECTION_KEY_AND_CHALLENGE_GEN"; - -/// Key Derivation Function for `SecretConnection` (HKDF) -pub struct Kdf { - /// Receiver's secret - pub recv_secret: [u8; 32], - - /// Sender's secret - pub send_secret: [u8; 32], - - /// Challenge to be signed by peer - pub challenge: [u8; 32], -} - -impl Kdf { - /// Returns recv secret, send secret, challenge as 32 byte arrays - #[must_use] - pub fn derive_secrets_and_challenge(shared_secret: &[u8; 32], loc_is_lo: bool) -> Self { - let mut key_material = [0_u8; 96]; - - Hkdf::::new(None, shared_secret) - .expand(HKDF_INFO, &mut key_material) - .expect("secret expansion failed"); - - let [mut recv_secret, mut send_secret, mut challenge] = [[0_u8; 32]; 3]; - - if loc_is_lo { - recv_secret.copy_from_slice(&key_material[0..32]); - send_secret.copy_from_slice(&key_material[32..64]); - } else { - send_secret.copy_from_slice(&key_material[0..32]); - recv_secret.copy_from_slice(&key_material[32..64]); - } - - challenge.copy_from_slice(&key_material[64..96]); - key_material.as_mut().zeroize(); - - Self { - recv_secret, - send_secret, - challenge, - } - } -} - -impl Drop for Kdf { - fn drop(&mut self) { - self.recv_secret.zeroize(); - self.send_secret.zeroize(); - self.challenge.zeroize(); - } -} diff --git a/p2p/src/secret_connection/nonce.rs b/p2p/src/secret_connection/nonce.rs deleted file mode 100644 index 77cdadf9..00000000 --- a/p2p/src/secret_connection/nonce.rs +++ /dev/null @@ -1,35 +0,0 @@ -//! Secret Connection nonces - -use std::convert::TryInto; - -/// Size of a `ChaCha20` (IETF) nonce -pub const SIZE: usize = 12; - -/// `SecretConnection` nonces (i.e. `ChaCha20` nonces) -pub struct Nonce(pub [u8; SIZE]); - -impl Default for Nonce { - fn default() -> Self { - Self([0_u8; SIZE]) - } -} - -impl Nonce { - /// Increment the nonce's counter by 1 - pub fn increment(&mut self) { - let counter: u64 = u64::from_le_bytes(self.0[4..].try_into().expect("framing failed")); - self.0[4..].copy_from_slice( - &counter - .checked_add(1) - .expect("overflow in counter addition") - .to_le_bytes(), - ); - } - - /// Serialize nonce as bytes (little endian) - #[inline] - #[must_use] - pub fn to_bytes(&self) -> &[u8] { - &self.0[..] - } -} diff --git a/p2p/src/secret_connection/protocol.rs b/p2p/src/secret_connection/protocol.rs deleted file mode 100644 index bd1baf48..00000000 --- a/p2p/src/secret_connection/protocol.rs +++ /dev/null @@ -1,261 +0,0 @@ -//! Secret Connection Protocol: message framing and versioning - -use std::convert::TryInto; - -use cometbft_proto::v0_38 as proto; -use curve25519_dalek_ng::montgomery::MontgomeryPoint as EphemeralPublic; -use prost::Message as _; - -#[cfg(feature = "amino")] -use super::amino_types; -use crate::error::Error; - -/// Size of an X25519 or Ed25519 public key -const PUBLIC_KEY_SIZE: usize = 32; - -/// Protocol version (based on the Tendermint version) -#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] -#[allow(non_camel_case_types)] -pub enum Version { - /// Tendermint v0.34 - V0_34, - - /// Tendermint v0.33 - V0_33, - - /// Pre-Tendermint v0.33 - Legacy, -} - -impl Version { - /// Does this version of Secret Connection use a transcript hash - #[must_use] - pub fn has_transcript(self) -> bool { - self != Self::Legacy - } - - /// Are messages encoded using Protocol Buffers? - #[must_use] - pub const fn is_protobuf(self) -> bool { - match self { - Self::V0_34 => true, - Self::V0_33 | Self::Legacy => false, - } - } - - /// Encode the initial handshake message (i.e. first one sent by both peers) - #[allow(clippy::cast_possible_truncation)] - #[must_use] - pub fn encode_initial_handshake(self, eph_pubkey: &EphemeralPublic) -> Vec { - if self.is_protobuf() { - // Equivalent Go implementation: - // https://github.com/tendermint/tendermint/blob/9e98c74/p2p/conn/secret_connection.go#L307-L312 - // TODO(tarcieri): proper protobuf framing - let mut buf = Vec::new(); - buf.extend_from_slice(&[0x22, 0x0a, 0x20]); - buf.extend_from_slice(eph_pubkey.as_bytes()); - buf - } else { - // Legacy Amino encoded handshake message - // Equivalent Go implementation: - // https://github.com/tendermint/tendermint/blob/013b9ce/p2p/conn/secret_connection.go#L213-L217 - // - // Note: this is not regular protobuf encoding but raw length prefixed amino encoding; - // amino prefixes with the total length, and the raw bytes array's length, too: - let mut buf = vec![PUBLIC_KEY_SIZE as u8 + 1, PUBLIC_KEY_SIZE as u8]; - buf.extend_from_slice(eph_pubkey.as_bytes()); - buf - } - } - - /// Decode the initial handshake message - /// - /// # Errors - /// - /// * if the message is malformed - pub fn decode_initial_handshake(self, bytes: &[u8]) -> Result { - let eph_pubkey = if self.is_protobuf() { - // Equivalent Go implementation: - // https://github.com/tendermint/tendermint/blob/9e98c74/p2p/conn/secret_connection.go#L315-L323 - // TODO(tarcieri): proper protobuf framing - if bytes.len() != 34 || bytes[..2] != [0x0a, 0x20] { - return Err(Error::malformed_handshake()); - } - - let eph_pubkey_bytes: [u8; 32] = bytes[2..].try_into().expect("framing failed"); - EphemeralPublic(eph_pubkey_bytes) - } else { - // Equivalent Go implementation: - // https://github.com/tendermint/tendermint/blob/013b9ce/p2p/conn/secret_connection.go#L220-L225 - // - // Check that the length matches what we expect and the length prefix is correct - if bytes.len() != 33 || bytes[0] != 32 { - return Err(Error::malformed_handshake()); - } - - let eph_pubkey_bytes: [u8; 32] = bytes[1..].try_into().expect("framing failed"); - EphemeralPublic(eph_pubkey_bytes) - }; - - // Reject the key if it is of low order - if is_low_order_point(&eph_pubkey) { - return Err(Error::low_order_key()); - } - - Ok(eph_pubkey) - } - - /// Encode signature which authenticates the handshake - #[must_use] - pub fn encode_auth_signature( - self, - pub_key: &ed25519_consensus::VerificationKey, - signature: &ed25519_consensus::Signature, - ) -> Vec { - if self.is_protobuf() { - // Protobuf `AuthSigMessage` - let pub_key = proto::crypto::PublicKey { - sum: Some(proto::crypto::public_key::Sum::Ed25519( - pub_key.as_ref().to_vec(), - )), - }; - - let msg = proto::p2p::AuthSigMessage { - pub_key: Some(pub_key), - sig: signature.to_bytes().to_vec(), - }; - - let mut buf = Vec::new(); - msg.encode_length_delimited(&mut buf) - .expect("couldn't encode AuthSigMessage proto"); - buf - } else { - self.encode_auth_signature_amino(pub_key, signature) - } - } - - /// Get the length of the auth message response for this protocol version - #[must_use] - pub const fn auth_sig_msg_response_len(self) -> usize { - if self.is_protobuf() { - // 32 + 64 + (proto overhead = 1 prefix + 2 fields + 2 lengths + total length) - 103 - } else { - // 32 + 64 + (amino overhead = 2 fields + 2 lengths + 4 prefix bytes + total length) - 106 - } - } - - /// Decode signature message which authenticates the handshake - /// - /// # Errors - /// - /// * if the decoding of the bytes fails - pub fn decode_auth_signature(self, bytes: &[u8]) -> Result { - if self.is_protobuf() { - // Parse Protobuf-encoded `AuthSigMessage` - proto::p2p::AuthSigMessage::decode_length_delimited(bytes).map_err(Error::decode) - } else { - self.decode_auth_signature_amino(bytes) - } - } - - #[allow(clippy::unused_self)] - #[cfg(feature = "amino")] - fn encode_auth_signature_amino( - self, - pub_key: &ed25519_consensus::VerificationKey, - signature: &ed25519_consensus::Signature, - ) -> Vec { - // Legacy Amino encoded `AuthSigMessage` - let msg = amino_types::AuthSigMessage::new(pub_key, signature); - - let mut buf = Vec::new(); - msg.encode_length_delimited(&mut buf) - .expect("encode_auth_signature failed"); - buf - } - - #[allow(clippy::unused_self)] - #[cfg(not(feature = "amino"))] - const fn encode_auth_signature_amino( - self, - _: &ed25519_consensus::VerificationKey, - _: &ed25519_consensus::Signature, - ) -> Vec { - panic!("attempted to encode auth signature using amino, but 'amino' feature is not present") - } - - #[allow(clippy::unused_self)] - #[cfg(feature = "amino")] - fn decode_auth_signature_amino( - self, - bytes: &[u8], - ) -> Result { - // Legacy Amino encoded `AuthSigMessage` - let amino_msg = - amino_types::AuthSigMessage::decode_length_delimited(bytes).map_err(Error::decode)?; - - amino_msg.try_into() - } - - #[allow(clippy::unused_self)] - #[cfg(not(feature = "amino"))] - const fn decode_auth_signature_amino( - self, - _: &[u8], - ) -> Result { - panic!("attempted to decode auth signature using amino, but 'amino' feature is not present") - } -} - -/// Reject low order points listed on -/// -/// These points contain low-order X25519 field elements. Rejecting them is -/// suggested in the "May the Fourth" paper under Section 5: -/// Software Countermeasures (see "Rejecting Known Bad Points" subsection): -/// -/// -#[allow(clippy::match_same_arms, clippy::match_like_matches_macro)] -fn is_low_order_point(point: &EphemeralPublic) -> bool { - // Note: as these are public points and do not interact with secret-key - // material in any way, this check does not need to be performed in - // constant-time. - match point.as_bytes() { - // 0 (order 4) - &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] => { - true - }, - - // 1 (order 1) - [0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] => { - true - }, - - // 325606250916557431795983626356110631294008115727848805560023387167927233504 (order 8) - &[0xe0, 0xeb, 0x7a, 0x7c, 0x3b, 0x41, 0xb8, 0xae, 0x16, 0x56, 0xe3, 0xfa, 0xf1, 0x9f, 0xc4, 0x6a, 0xda, 0x09, 0x8d, 0xeb, 0x9c, 0x32, 0xb1, 0xfd, 0x86, 0x62, 0x05, 0x16, 0x5f, 0x49, 0xb8, 0x00] => { - true - }, - - // 39382357235489614581723060781553021112529911719440698176882885853963445705823 (order 8) - &[0x5f, 0x9c, 0x95, 0xbc, 0xa3, 0x50, 0x8c, 0x24, 0xb1, 0xd0, 0xb1, 0x55, 0x9c, 0x83, 0xef, 0x5b, 0x04, 0x44, 0x5c, 0xc4, 0x58, 0x1c, 0x8e, 0x86, 0xd8, 0x22, 0x4e, 0xdd, 0xd0, 0x9f, 0x11, 0x57] => { - true - }, - - // p - 1 (order 2) - [0xec, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f] => { - true - }, - - // p (order 4) */ - [0xed, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f] => { - true - }, - - // p + 1 (order 1) - [0xee, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f] => { - true - }, - _ => false, - } -} diff --git a/p2p/src/secret_connection/public_key.rs b/p2p/src/secret_connection/public_key.rs deleted file mode 100644 index 5698a82a..00000000 --- a/p2p/src/secret_connection/public_key.rs +++ /dev/null @@ -1,66 +0,0 @@ -//! Secret Connection peer public keys - -use std::fmt::{self, Display}; - -use cometbft::{error::Error, node}; -use sha2::{digest::Digest, Sha256}; - -/// Secret Connection peer public keys (signing, presently Ed25519-only) -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub enum PublicKey { - /// Ed25519 Secret Connection Keys - Ed25519(ed25519_consensus::VerificationKey), -} - -impl PublicKey { - /// From raw Ed25519 public key bytes - /// - /// # Errors - /// - /// * if the bytes given are invalid - pub fn from_raw_ed25519(bytes: &[u8]) -> Result { - ed25519_consensus::VerificationKey::try_from(bytes) - .map(Self::Ed25519) - .map_err(|_| Error::signature()) - } - - /// Get Ed25519 public key - #[must_use] - pub const fn ed25519(self) -> Option { - match self { - Self::Ed25519(pk) => Some(pk), - } - } - - /// Get the remote Peer ID - #[must_use] - pub fn peer_id(self) -> node::Id { - match self { - Self::Ed25519(pk) => { - // TODO(tarcieri): use `cometbft::node::Id::from` - let digest = Sha256::digest(pk.as_bytes()); - let mut bytes = [0_u8; 20]; - bytes.copy_from_slice(&digest[..20]); - node::Id::new(bytes) - }, - } - } -} - -impl Display for PublicKey { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.peer_id()) - } -} - -impl From<&ed25519_consensus::SigningKey> for PublicKey { - fn from(sk: &ed25519_consensus::SigningKey) -> Self { - Self::Ed25519(sk.verification_key()) - } -} - -impl From for PublicKey { - fn from(pk: ed25519_consensus::VerificationKey) -> Self { - Self::Ed25519(pk) - } -} diff --git a/p2p/src/transport.rs b/p2p/src/transport.rs deleted file mode 100644 index 114cd5e9..00000000 --- a/p2p/src/transport.rs +++ /dev/null @@ -1,143 +0,0 @@ -//! Abstractions that describe types which support the physical transport - i.e. connection -//! management - used in the p2p stack. - -use std::net::{SocketAddr, ToSocketAddrs}; - -use cometbft::{node, public_key::PublicKey}; -use eyre::Result; - -/// Information which resources to bind to and how to identify on the network. -pub struct BindInfo -where - A: ToSocketAddrs, -{ - /// List of addresses to be communicated as publicly reachable to other nodes, which in turn - /// can use that to share with third parties. - /// - /// TODO(xla): Depending on where this information is going to be disseminated it might be - /// better placed in a higher-level protocol. What stands in opposition to that is the fact - /// that advertised addresses will be helpful for hole punching and other involved network - /// traversals. - pub advertise_addrs: A, - /// Local address(es) to bind to and accept connections on. - pub bind_addrs: A, - /// Public key of the peer used for identity on the network. - pub public_key: PublicKey, -} - -/// Information to establish a connection to a remote peer and validate its identity. -pub struct ConnectInfo -where - A: ToSocketAddrs, -{ - /// Known address(es) of the peer. - pub addrs: A, - /// The expected id of the remote peer. - pub id: node::Id, -} - -/// Known list of typed streams. -#[derive(Clone, Copy, Hash, Eq, PartialEq)] -pub enum StreamId { - /// Stream to exchange message concerning Peer Exchange. - Pex, -} - -/// Envelope to trace the original direction of an established connection. -pub enum Direction { - /// A peer that connected to the local node. - Incoming(Conn), - /// A remote peer the local node established a connection to. - Outgoing(Conn), -} - -/// Trait that describes the send end of a stream. -pub trait StreamSend { - /// Sends the message to the peer over the open stream. `msg` should be a valid and properly - /// encoded byte array according to the supported messages of the stream. - /// - /// # Errors - /// - /// * If the underlying I/O operations fail. - /// * If the stream is closed. - /// * If the peer is gone - fn send>(msg: B) -> Result<()>; -} - -/// Trait which describes the core concept of a connection between two peers established by -/// `[Transport]`. -pub trait Connection: Send { - /// Errors emitted by the connection. - type Error; - /// Read end of a bidirectional stream. Carries a finite stream of framed messages. Decoding is - /// left to the caller and should correspond to the type of stream. - type StreamRead: Iterator>> + Send; - /// Send end of a stream. - type StreamSend: StreamSend; - - /// Returns the list of advertised addresses known for this connection. - fn advertised_addrs(&self) -> Vec; - /// Tears down the connection and releases all attached resources. - /// - /// # Errors - /// - /// * If release of attached resources failed. - fn close(&self) -> Result<()>; - /// Returns the local address for the connection. - fn local_addr(&self) -> SocketAddr; - /// Opens a new bi-bidirectional stream for the given [`StreamId`]. - /// - /// # Errors - /// - /// * If the stream type is not supported. - /// * If the peer is gone. - /// * If resources necessary for the stream creation aren't available/accessible. - fn open_bidirectional( - &self, - stream_id: StreamId, - ) -> Result<(Self::StreamRead, Self::StreamSend), Self::Error>; - /// Public key of the remote peer. - fn public_key(&self) -> PublicKey; - /// Local address(es) to the endpoint listens on. - fn remote_addr(&self) -> SocketAddr; -} - -/// Local handle on a resource which allows connecting to remote peers. -pub trait Endpoint: Send -where - A: ToSocketAddrs, -{ - /// Core type that represents a connection between two peers established through the transport. - type Connection; - - /// Establishes a new connection to a remote peer. - /// - /// # Errors - /// - /// * If the remote is not reachable. - /// * If resources necessary for the connection creation aren't available/accessible. - fn connect(&self, info: ConnectInfo) -> Result; - /// Local address(es) the endpoint listens on. - fn listen_addrs(&self) -> Vec; -} - -/// Trait that describes types which support connection management of the p2p stack. -pub trait Transport -where - A: ToSocketAddrs, -{ - /// Core type that represents a connection between two peers established through the transport. - type Connection: Connection; - /// Local handle on a resource which allows connecting to remote peers. - type Endpoint: Endpoint>::Connection> + Drop; - /// Infinite stream of inbound connections. - type Incoming: Iterator>::Connection>> + Send; - - /// Consumes the transport to bind the resources in exchange for the `Endpoint` and `Incoming` - /// stream. - /// - /// # Errors - /// - /// * If resource allocation fails for lack of privileges or being not available. - fn bind(self, bind_info: BindInfo) -> Result<(Self::Endpoint, Self::Incoming)>; -} diff --git a/release.sh b/release.sh index 071ce121..36118447 100755 --- a/release.sh +++ b/release.sh @@ -19,11 +19,9 @@ set -e # which they must be published. It's important to respect this order, since # each subsequent crate depends on one or more of the preceding ones. DEFAULT_CRATES="cometbft-proto \ - cometbft-std-ext \ cometbft \ cometbft-config \ cometbft-rpc \ - cometbft-p2p \ cometbft-light-client-verifier \ cometbft-light-client \ cometbft-light-client-detector \ diff --git a/std-ext/Cargo.toml b/std-ext/Cargo.toml deleted file mode 100644 index 3947f02a..00000000 --- a/std-ext/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "cometbft-std-ext" -version = "0.34.0" -edition = "2021" -license = "Apache-2.0" -homepage = "https://cometbft.com/" -repository = "https://github.com/cometbft/cometbft-rs" -readme = "README.md" -keywords = ["blockchain", "cosmos", "cometbft"] -categories = ["development-tools"] -authors = ["Informal Systems "] - -description = """ - cometbft-std-ext contains extensions to the Rust standard library for use - from cometbft-rs. - """ - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] diff --git a/std-ext/README.md b/std-ext/README.md deleted file mode 100644 index 145ecd2b..00000000 --- a/std-ext/README.md +++ /dev/null @@ -1,24 +0,0 @@ -[![Crate][crate-image]][crate-link] -[![Docs][docs-image]][docs-link] - -See the [repo root] for build status, license, rust version, etc. - -# tendermint-std-ext - -Extensions to the [Rust standard library][std] for use by Tendermint in Rust. - -## Documentation - -See documentation on [crates.io][docs-link]. - -[//]: # (badges) - -[crate-image]: https://img.shields.io/crates/v/tendermint-std-ext.svg -[crate-link]: https://crates.io/crates/tendermint-std-ext -[docs-image]: https://docs.rs/tendermint-std-ext/badge.svg -[docs-link]: https://docs.rs/tendermint-std-ext/ - -[//]: # (general links) - -[repo root]: https://github.com/informalsystems/tendermint-rs -[std]: https://doc.rust-lang.org/std/ diff --git a/std-ext/src/lib.rs b/std-ext/src/lib.rs deleted file mode 100644 index 6fb7389c..00000000 --- a/std-ext/src/lib.rs +++ /dev/null @@ -1,8 +0,0 @@ -//! Extensions to the [Rust standard library][std] for use by [tendermint-rs]. -//! -//! [std]: https://doc.rust-lang.org/std/ -//! [tendermint-rs]: https://github.com/informalsystems/tendermint-rs/ - -mod try_clone; - -pub use try_clone::TryClone; diff --git a/std-ext/src/try_clone.rs b/std-ext/src/try_clone.rs deleted file mode 100644 index 2ea599a7..00000000 --- a/std-ext/src/try_clone.rs +++ /dev/null @@ -1,27 +0,0 @@ -//! Rust standard library types that can be fallibly cloned. - -use std::net::TcpStream; - -/// Types that can be cloned where success is not guaranteed can implement this -/// trait. -pub trait TryClone: Sized { - /// The type of error that can be returned when an attempted clone - /// operation fails. - type Error: std::error::Error; - - /// Attempt to clone this instance. - /// - /// # Errors - /// Can fail if the underlying instance cannot be cloned (e.g. the OS could - /// be out of file descriptors, or some low-level OS-specific error could - /// be produced). - fn try_clone(&self) -> Result; -} - -impl TryClone for TcpStream { - type Error = std::io::Error; - - fn try_clone(&self) -> Result { - TcpStream::try_clone(self) - } -} diff --git a/test/Cargo.toml b/test/Cargo.toml deleted file mode 100644 index a550be9e..00000000 --- a/test/Cargo.toml +++ /dev/null @@ -1,27 +0,0 @@ -[package] -name = "cometbft-test" -description = "cometbft workspace tests and common utilities for testing." -version = "0.34.0" -edition = "2021" -license = "Apache-2.0" -categories = ["development", "test", "tools"] -repository = "https://github.com/cometbft/cometbft-rs" -keywords = ["blockchain", "cometbft", "testing"] -readme = "README.md" -authors = ["Alexander Simmerl "] - -[lib] -test = true - -[dev-dependencies] -curve25519-dalek-ng = { version = "4", default-features = false } -ed25519-consensus = { version = "2", default-features = false } -flex-error = { version = "0.4.4", default-features = false } -flume = { version = "0.10", default-features = false } -rand_core = { version = "0.6", default-features = false, features = ["std"] } -readwrite = { version = "^0.1.1", default-features = false } -subtle-encoding = { version = "0.5", default-features = false } - -cometbft = { path = "../cometbft", default-features = false } -cometbft-p2p = { path = "../p2p", default-features = false } -cometbft-proto = { path = "../proto", default-features = false } diff --git a/test/src/lib.rs b/test/src/lib.rs deleted file mode 100644 index 980d67a5..00000000 --- a/test/src/lib.rs +++ /dev/null @@ -1,5 +0,0 @@ -#[cfg(test)] -pub mod pipe; - -#[cfg(test)] -mod test; diff --git a/test/src/pipe.rs b/test/src/pipe.rs deleted file mode 100644 index 6710c0ae..00000000 --- a/test/src/pipe.rs +++ /dev/null @@ -1,193 +0,0 @@ -// Copyright (c) 2015 arcnmx - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -//! Asynchronous in-memory pipe - -use std::{ - cmp::min, - io::{self, BufRead, Read, Write}, -}; - -use flume::{self, Receiver, SendError, Sender, TrySendError}; - -// value for libstd -const DEFAULT_BUF_SIZE: usize = 8 * 1024; - -/// The `Read` end of a pipe (see `pipe()`) -pub struct Reader { - receiver: Receiver>, - buffer: Vec, - position: usize, -} - -/// The `Write` end of a pipe (see `pipe()`) that will buffer small writes before sending -/// to the reader end. -pub struct BufWriter { - sender: Option>>, - buffer: Vec, - size: usize, -} - -/// Creates an asynchronous memory pipe with buffered writer -pub fn async_pipe_buffered() -> (Reader, BufWriter) { - let (tx, rx) = flume::unbounded(); - - ( - Reader { - receiver: rx, - buffer: Vec::new(), - position: 0, - }, - BufWriter { - sender: Some(tx), - buffer: Vec::with_capacity(DEFAULT_BUF_SIZE), - size: DEFAULT_BUF_SIZE, - }, - ) -} - -/// Creates a pair of pipes for bidirectional communication using buffered writer, a bit like UNIX's -/// `socketpair(2)`. -pub fn async_bipipe_buffered() -> ( - readwrite::ReadWrite, - readwrite::ReadWrite, -) { - let (r1, w1) = async_pipe_buffered(); - let (r2, w2) = async_pipe_buffered(); - ((r1, w2).into(), (r2, w1).into()) -} - -fn epipe() -> io::Error { - io::Error::new(io::ErrorKind::BrokenPipe, "pipe reader has been dropped") -} - -impl BufWriter { - #[inline] - /// Gets a reference to the underlying `Sender` - pub fn sender(&self) -> &Sender> { - // SAFETY: this is safe as long as `into_inner()` is the only method - // that clears the sender, and this fn is never called afterward - self.sender.as_ref().expect("sender to be present") - } -} - -impl BufRead for Reader { - fn fill_buf(&mut self) -> io::Result<&[u8]> { - while self.position >= self.buffer.len() { - match self.receiver.recv() { - // The only existing error is EOF - Err(_) => break, - Ok(data) => { - self.buffer = data; - self.position = 0; - }, - } - } - - Ok(&self.buffer[self.position..]) - } - - fn consume(&mut self, amt: usize) { - debug_assert!(self.buffer.len() - self.position >= amt); - self.position += amt - } -} - -impl Read for Reader { - fn read(&mut self, buf: &mut [u8]) -> io::Result { - if buf.is_empty() { - return Ok(0); - } - - let internal = self.fill_buf()?; - - let len = min(buf.len(), internal.len()); - if len > 0 { - buf[..len].copy_from_slice(&internal[..len]); - self.consume(len); - } - Ok(len) - } -} - -impl Write for BufWriter { - fn write(&mut self, buf: &[u8]) -> io::Result { - let buffer_len = self.buffer.len(); - let bytes_written = if buf.len() > self.size { - // bypass buffering for big writes - buf.len() - } else { - // avoid resizing of the buffer - min(buf.len(), self.size - buffer_len) - }; - self.buffer.extend_from_slice(&buf[..bytes_written]); - - if self.buffer.len() >= self.size { - self.flush()?; - } else { - // reserve capacity later to avoid needless allocations - let data = std::mem::take(&mut self.buffer); - - // buffer still has space but try to send it in case the other side already awaits - match self.sender().try_send(data) { - Ok(_) => self.buffer.reserve(self.size), - Err(TrySendError::Full(data)) => self.buffer = data, - Err(TrySendError::Disconnected(data)) => { - self.buffer = data; - self.buffer.truncate(buffer_len); - return Err(epipe()); - }, - } - } - - Ok(bytes_written) - } - - fn flush(&mut self) -> io::Result<()> { - if self.buffer.is_empty() { - Ok(()) - } else { - let data = std::mem::take(&mut self.buffer); - match self.sender().send(data) { - Ok(_) => { - self.buffer.reserve(self.size); - Ok(()) - }, - Err(SendError(data)) => { - self.buffer = data; - Err(epipe()) - }, - } - } - } -} - -/// Flushes the contents of the buffer before the writer is dropped. Errors are ignored, so it is -/// recommended that `flush()` be used explicitly instead of relying on Drop. -/// -/// This final flush can be avoided by using `drop(writer.into_inner())`. -impl Drop for BufWriter { - fn drop(&mut self) { - if !self.buffer.is_empty() { - let data = std::mem::take(&mut self.buffer); - self.sender().send(data).ok(); - } - } -} diff --git a/test/src/test.rs b/test/src/test.rs deleted file mode 100644 index d5a9c941..00000000 --- a/test/src/test.rs +++ /dev/null @@ -1 +0,0 @@ -mod unit; diff --git a/test/src/test/unit.rs b/test/src/test/unit.rs deleted file mode 100644 index b4b78355..00000000 --- a/test/src/test/unit.rs +++ /dev/null @@ -1 +0,0 @@ -mod p2p; diff --git a/test/src/test/unit/p2p.rs b/test/src/test/unit/p2p.rs deleted file mode 100644 index 916b8276..00000000 --- a/test/src/test/unit/p2p.rs +++ /dev/null @@ -1 +0,0 @@ -mod secret_connection; diff --git a/test/src/test/unit/p2p/secret_connection.rs b/test/src/test/unit/p2p/secret_connection.rs deleted file mode 100644 index 9c59a7a4..00000000 --- a/test/src/test/unit/p2p/secret_connection.rs +++ /dev/null @@ -1,192 +0,0 @@ -use std::{ - io::{Read as _, Write as _}, - net::{TcpListener, TcpStream}, - thread, -}; - -use cometbft_p2p::secret_connection::{sort32, Handshake, SecretConnection, Version}; -use cometbft_proto::v0_38 as proto; -use curve25519_dalek_ng::{ - constants::X25519_BASEPOINT, montgomery::MontgomeryPoint as EphemeralPublic, -}; -use rand_core::OsRng; - -use crate::pipe; - -mod nonce; -mod public_key; - -#[test] -fn test_handshake() { - let (pipe1, pipe2) = pipe::async_bipipe_buffered(); - - let peer1 = thread::spawn(|| { - let conn1 = new_peer_conn(pipe2); - assert!(conn1.is_ok()); - }); - - let peer2 = thread::spawn(|| { - let conn2 = new_peer_conn(pipe1); - assert!(conn2.is_ok()); - }); - - peer1.join().expect("peer1 thread has panicked"); - peer2.join().expect("peer2 thread has panicked"); -} - -#[test] -fn test_read_write_single_message() { - const MESSAGE: &str = "The Queen's Gambit"; - - let (pipe1, pipe2) = pipe::async_bipipe_buffered(); - - let sender = thread::spawn(move || { - let mut conn1 = new_peer_conn(pipe2).expect("handshake to succeed"); - - conn1 - .write_all(MESSAGE.as_bytes()) - .expect("expected to write message"); - }); - - let receiver = thread::spawn(move || { - let mut conn2 = new_peer_conn(pipe1).expect("handshake to succeed"); - - let mut buf = [0; MESSAGE.len()]; - conn2 - .read_exact(&mut buf) - .expect("expected to read message"); - assert_eq!(MESSAGE.as_bytes(), &buf); - }); - - sender.join().expect("sender thread has panicked"); - receiver.join().expect("receiver thread has panicked"); -} - -#[test] -fn test_evil_peer_shares_invalid_eph_key() { - let csprng = OsRng {}; - let local_privkey = ed25519_consensus::SigningKey::new(csprng); - let (mut h, _) = Handshake::new(local_privkey, Version::V0_34); - let bytes: [u8; 32] = [0; 32]; - let res = h.got_key(EphemeralPublic(bytes)); - assert!(res.is_err()); -} - -#[test] -fn test_evil_peer_shares_invalid_auth_sig() { - let csprng = OsRng {}; - let local_privkey = ed25519_consensus::SigningKey::new(csprng); - let (mut h, _) = Handshake::new(local_privkey, Version::V0_34); - let res = h.got_key(X25519_BASEPOINT); - assert!(res.is_ok()); - - let mut h = res.unwrap(); - let res = h.got_signature(proto::p2p::AuthSigMessage { - pub_key: None, - sig: vec![], - }); - assert!(res.is_err()); -} - -#[test] -fn test_sort() { - // sanity check - let t1 = [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, - ]; - let t2 = [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 1, - ]; - let (ref t3, ref t4) = sort32(t1, t2); - assert_eq!(t1, *t3); - assert_eq!(t2, *t4); -} - -#[test] -fn test_split_secret_connection() { - const MESSAGES_1_TO_2: &[&str] = &["one", "three", "five", "seven"]; - const MESSAGES_2_TO_1: &[&str] = &["two", "four", "six", "eight"]; - let peer1_listener = TcpListener::bind("127.0.0.1:0").expect("to be able to bind to 127.0.0.1"); - let peer1_addr = peer1_listener.local_addr().unwrap(); - println!("peer1 bound to {peer1_addr:?}"); - - let peer1 = thread::spawn(move || { - let stream = peer1_listener - .incoming() - .next() - .unwrap() - .expect("an incoming TCP stream from peer 2"); - let mut conn_to_peer2 = new_peer_conn(stream).expect("handshake to succeed"); - println!("peer1 handshake concluded"); - for msg_counter in 0..MESSAGES_1_TO_2.len() { - // Peer 1 sends first - conn_to_peer2 - .write_all(MESSAGES_1_TO_2[msg_counter].as_bytes()) - .expect("to write message successfully to peer 2"); - // Peer 1 expects a response - let mut buf = [0u8; 10]; - let br = conn_to_peer2 - .read(&mut buf) - .expect("to read a message from peer 2"); - let msg = String::from_utf8_lossy(&buf[0..br]).to_string(); - println!("Got message from peer2: {msg}"); - assert_eq!(msg, MESSAGES_2_TO_1[msg_counter]); - } - }); - - // Peer 2 attempts to initiate the secret connection to peer 1 - let peer2_to_peer1 = TcpStream::connect(peer1_addr).expect("to be able to connect to peer 1"); - println!("peer2 connected to peer1"); - let conn_to_peer1 = new_peer_conn(peer2_to_peer1).expect("handshake to succeed"); - println!("peer2 handshake concluded"); - - let (mut write_conn, mut read_conn) = conn_to_peer1 - .split() - .expect("to be able to clone the underlying TcpStream"); - let (write_tx, write_rx) = std::sync::mpsc::channel::(); - - // We spawn a standalone thread that makes use of peer2's secret connection - // purely to write outgoing messages. - let peer2_writer = thread::spawn(move || { - for _ in 0..MESSAGES_2_TO_1.len() { - let msg = write_rx - .recv() - .expect("to successfully receive a message to be sent to peer1"); - write_conn - .write_all(msg.as_bytes()) - .expect("to be able to write to peer 1"); - } - }); - - for msg_counter in 0..MESSAGES_2_TO_1.len() { - // Wait for peer 1 to send first - let mut buf = [0u8; 10]; - let br = read_conn - .read(&mut buf) - .expect("to receive a message from peer 1"); - let msg = String::from_utf8_lossy(&buf[0..br]).to_string(); - println!("Got message from peer1: {msg}"); - assert_eq!(msg, MESSAGES_1_TO_2[msg_counter]); - write_tx - .send(MESSAGES_2_TO_1[msg_counter].to_string()) - .expect("to be able to communicate with peer2's writer thread"); - } - - peer2_writer - .join() - .expect("peer 2's writer thread to run to completion"); - peer1.join().expect("peer 1's thread to run to completion") -} - -fn new_peer_conn( - io_handler: IoHandler, -) -> Result, cometbft_p2p::error::Error> -where - IoHandler: std::io::Read + std::io::Write + Send + Sync, -{ - let csprng = OsRng {}; - let privkey1 = ed25519_consensus::SigningKey::new(csprng); - SecretConnection::new(io_handler, privkey1, Version::V0_34) -} diff --git a/test/src/test/unit/p2p/secret_connection/nonce.rs b/test/src/test/unit/p2p/secret_connection/nonce.rs deleted file mode 100644 index 76c27973..00000000 --- a/test/src/test/unit/p2p/secret_connection/nonce.rs +++ /dev/null @@ -1,35 +0,0 @@ -use std::collections::HashMap; - -use cometbft_p2p::secret_connection::{Nonce, NONCE_SIZE}; - -#[test] -fn test_incr_nonce() { - // make sure we match the golang implementation - let mut check_points: HashMap = HashMap::new(); - check_points.insert(0, &[0_u8, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0]); - check_points.insert(1, &[0_u8, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0]); - check_points.insert(510, &[0_u8, 0, 0, 0, 255, 1, 0, 0, 0, 0, 0, 0]); - check_points.insert(511, &[0_u8, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0]); - check_points.insert(512, &[0_u8, 0, 0, 0, 1, 2, 0, 0, 0, 0, 0, 0]); - check_points.insert(1023, &[0_u8, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0]); - - let mut nonce = Nonce::default(); - assert_eq!(nonce.to_bytes().len(), NONCE_SIZE); - - for i in 0..1024 { - nonce.increment(); - if let Some(want) = check_points.get(&i) { - let got = &nonce.to_bytes(); - assert_eq!(got, want); - } - } -} -#[test] -#[should_panic] -fn test_incr_nonce_overflow() { - // other than in the golang implementation we panic if we incremented more than 64 - // bits allow. - // In golang this would reset to an all zeroes nonce. - let mut nonce = Nonce([0_u8, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255]); - nonce.increment(); -} diff --git a/test/src/test/unit/p2p/secret_connection/public_key.rs b/test/src/test/unit/p2p/secret_connection/public_key.rs deleted file mode 100644 index b8a4c07a..00000000 --- a/test/src/test/unit/p2p/secret_connection/public_key.rs +++ /dev/null @@ -1,16 +0,0 @@ -use cometbft_p2p::secret_connection::PublicKey; -use subtle_encoding::hex; - -const EXAMPLE_SECRET_CONN_KEY: &str = - "F7FEB0B5BA0760B2C58893E329475D1EA81781DD636E37144B6D599AD38AA825"; - -#[test] -fn test_secret_connection_pubkey_serialization() { - let example_key = - PublicKey::from_raw_ed25519(&hex::decode_upper(EXAMPLE_SECRET_CONN_KEY).unwrap()).unwrap(); - - assert_eq!( - example_key.to_string(), - "117c95c4fd7e636c38d303493302d2c271a39669" - ); -}