From 85b016756d6daeed1651507276187bd9c5e64c2b Mon Sep 17 00:00:00 2001 From: teor Date: Wed, 10 Nov 2021 06:47:50 +1000 Subject: [PATCH] Refactor addr v1 serialization using a separate AddrV1 type (#3021) * Implement addr v1 serialization using a separate AddrV1 type * Remove commented-out code * Split the address serialization code into modules * Reorder v1 and in_version fields in serialization order * Fix a missed search-and-replace * Explain conversion to MetaAddr Co-authored-by: Janito Vaqueiro Ferreira Filho Co-authored-by: Janito Vaqueiro Ferreira Filho --- zebra-chain/src/serialization.rs | 2 +- zebra-chain/src/serialization/arbitrary.rs | 13 +- zebra-chain/src/serialization/read_zcash.rs | 59 +----- zebra-chain/src/serialization/write_zcash.rs | 25 +-- .../src/serialization/zcash_deserialize.rs | 14 ++ .../src/serialization/zcash_serialize.rs | 12 +- zebra-network/src/address_book.rs | 7 +- zebra-network/src/config.rs | 4 +- zebra-network/src/isolated.rs | 16 +- zebra-network/src/meta_addr.rs | 19 +- zebra-network/src/meta_addr/arbitrary.rs | 6 +- zebra-network/src/meta_addr/tests.rs | 5 +- zebra-network/src/meta_addr/tests/prop.rs | 140 +-------------- zebra-network/src/peer/handshake.rs | 9 +- zebra-network/src/protocol/external.rs | 3 +- zebra-network/src/protocol/external/addr.rs | 75 ++------ .../src/protocol/external/addr/canonical.rs | 44 +++++ .../src/protocol/external/addr/in_version.rs | 103 +++++++++++ .../src/protocol/external/addr/v1.rs | 169 ++++++++++++++++++ .../src/protocol/external/arbitrary.rs | 27 ++- zebra-network/src/protocol/external/codec.rs | 44 ++--- .../src/protocol/external/message.rs | 18 +- .../protocol/external/tests/preallocate.rs | 31 ++-- .../src/protocol/external/tests/prop.rs | 140 ++++++++++++++- 24 files changed, 625 insertions(+), 360 deletions(-) create mode 100644 zebra-network/src/protocol/external/addr/canonical.rs create mode 100644 zebra-network/src/protocol/external/addr/in_version.rs create mode 100644 zebra-network/src/protocol/external/addr/v1.rs diff --git a/zebra-chain/src/serialization.rs b/zebra-chain/src/serialization.rs index baeb0a82..b05f13be 100644 --- a/zebra-chain/src/serialization.rs +++ b/zebra-chain/src/serialization.rs @@ -29,7 +29,7 @@ pub use compact_size::{CompactSize64, CompactSizeMessage}; pub use constraint::AtLeastOne; pub use date_time::{DateTime32, Duration32}; pub use error::SerializationError; -pub use read_zcash::{canonical_socket_addr, ReadZcashExt}; +pub use read_zcash::ReadZcashExt; pub use write_zcash::WriteZcashExt; pub use zcash_deserialize::{ zcash_deserialize_bytes_external_count, zcash_deserialize_external_count, TrustedPreallocate, diff --git a/zebra-chain/src/serialization/arbitrary.rs b/zebra-chain/src/serialization/arbitrary.rs index bee67f27..8921af3d 100644 --- a/zebra-chain/src/serialization/arbitrary.rs +++ b/zebra-chain/src/serialization/arbitrary.rs @@ -1,13 +1,11 @@ //! Arbitrary data generation for serialization proptests -use std::{convert::TryInto, net::SocketAddr}; +use std::convert::TryInto; use chrono::{TimeZone, Utc, MAX_DATETIME, MIN_DATETIME}; use proptest::{arbitrary::any, prelude::*}; -use super::{ - read_zcash::canonical_socket_addr, CompactSizeMessage, DateTime32, MAX_PROTOCOL_MESSAGE_LEN, -}; +use super::{CompactSizeMessage, DateTime32, MAX_PROTOCOL_MESSAGE_LEN}; impl Arbitrary for DateTime32 { type Parameters = (); @@ -60,13 +58,6 @@ pub fn datetime_u32() -> impl Strategy> { any::().prop_map(Into::into) } -/// Returns a random canonical Zebra `SocketAddr`. -/// -/// See [`canonical_ip_addr`] for details. -pub fn canonical_socket_addr_strategy() -> impl Strategy { - any::().prop_map(canonical_socket_addr) -} - impl Arbitrary for CompactSizeMessage { type Parameters = (); diff --git a/zebra-chain/src/serialization/read_zcash.rs b/zebra-chain/src/serialization/read_zcash.rs index bb48213b..25f87c6d 100644 --- a/zebra-chain/src/serialization/read_zcash.rs +++ b/zebra-chain/src/serialization/read_zcash.rs @@ -1,32 +1,9 @@ -use std::{ - io, - net::{IpAddr, Ipv6Addr, SocketAddr}, -}; - -use byteorder::{BigEndian, ReadBytesExt}; +use std::io; /// Extends [`Read`] with methods for writing Zcash/Bitcoin types. /// /// [`Read`]: https://doc.rust-lang.org/std/io/trait.Read.html pub trait ReadZcashExt: io::Read { - /// Read an IP address in Bitcoin format. - #[inline] - fn read_ip_addr(&mut self) -> io::Result { - let mut octets = [0u8; 16]; - self.read_exact(&mut octets)?; - let v6_addr = Ipv6Addr::from(octets); - - Ok(canonical_ip_addr(&v6_addr)) - } - - /// Read a Bitcoin-encoded `SocketAddr`. - #[inline] - fn read_socket_addr(&mut self) -> io::Result { - let ip_addr = self.read_ip_addr()?; - let port = self.read_u16::()?; - Ok(SocketAddr::new(ip_addr, port)) - } - /// Convenience method to read a `[u8; 4]`. #[inline] fn read_4_bytes(&mut self) -> io::Result<[u8; 4]> { @@ -62,37 +39,3 @@ pub trait ReadZcashExt: io::Read { /// Mark all types implementing `Read` as implementing the extension. impl ReadZcashExt for R {} - -/// Transform a Zcash-deserialized IPv6 address into a canonical Zebra IP address. -/// -/// Zcash uses IPv6-mapped IPv4 addresses in its network protocol. Zebra converts -/// those addresses to `Ipv4Addr`s, for maximum compatibility with systems that -/// don't understand IPv6. -pub fn canonical_ip_addr(v6_addr: &Ipv6Addr) -> IpAddr { - use IpAddr::*; - - // TODO: replace with `to_ipv4_mapped` when that stabilizes - // https://github.com/rust-lang/rust/issues/27709 - match v6_addr.to_ipv4() { - // workaround for unstable `to_ipv4_mapped` - Some(v4_addr) if v4_addr.to_ipv6_mapped() == *v6_addr => V4(v4_addr), - Some(_) | None => V6(*v6_addr), - } -} - -/// Transform a `SocketAddr` into a canonical Zebra `SocketAddr`, converting -/// IPv6-mapped IPv4 addresses, and removing IPv6 scope IDs and flow information. -/// -/// See [`canonical_ip_addr`] for detailed info on IPv6-mapped IPv4 addresses. -pub fn canonical_socket_addr(socket_addr: impl Into) -> SocketAddr { - use SocketAddr::*; - - let mut socket_addr = socket_addr.into(); - if let V6(v6_socket_addr) = socket_addr { - let canonical_ip = canonical_ip_addr(v6_socket_addr.ip()); - // creating a new SocketAddr removes scope IDs and flow information - socket_addr = SocketAddr::new(canonical_ip, socket_addr.port()); - } - - socket_addr -} diff --git a/zebra-chain/src/serialization/write_zcash.rs b/zebra-chain/src/serialization/write_zcash.rs index 3c21f50a..ad996092 100644 --- a/zebra-chain/src/serialization/write_zcash.rs +++ b/zebra-chain/src/serialization/write_zcash.rs @@ -1,32 +1,9 @@ -use std::{ - io, - net::{IpAddr, SocketAddr}, -}; - -use byteorder::{BigEndian, WriteBytesExt}; +use std::io; /// Extends [`Write`] with methods for writing Zcash/Bitcoin types. /// /// [`Write`]: https://doc.rust-lang.org/std/io/trait.Write.html pub trait WriteZcashExt: io::Write { - /// Write an `IpAddr` in Bitcoin format. - #[inline] - fn write_ip_addr(&mut self, addr: IpAddr) -> io::Result<()> { - use std::net::IpAddr::*; - let v6_addr = match addr { - V4(ref v4) => v4.to_ipv6_mapped(), - V6(v6) => v6, - }; - self.write_all(&v6_addr.octets()) - } - - /// Write a `SocketAddr` in Bitcoin format. - #[inline] - fn write_socket_addr(&mut self, addr: SocketAddr) -> io::Result<()> { - self.write_ip_addr(addr.ip())?; - self.write_u16::(addr.port()) - } - /// Convenience method to write exactly 32 u8's. #[inline] fn write_32_bytes(&mut self, bytes: &[u8; 32]) -> io::Result<()> { diff --git a/zebra-chain/src/serialization/zcash_deserialize.rs b/zebra-chain/src/serialization/zcash_deserialize.rs index 1e2552f1..a4340b0f 100644 --- a/zebra-chain/src/serialization/zcash_deserialize.rs +++ b/zebra-chain/src/serialization/zcash_deserialize.rs @@ -1,6 +1,7 @@ use std::{ convert::{TryFrom, TryInto}, io, + net::Ipv6Addr, }; use super::{AtLeastOne, CompactSizeMessage, SerializationError, MAX_PROTOCOL_MESSAGE_LEN}; @@ -128,6 +129,19 @@ impl ZcashDeserialize for String { } } +// We don't impl ZcashDeserialize for Ipv4Addr or SocketAddrs, +// because the IPv4 and port formats are different in addr (v1) and addrv2 messages. + +/// Read a Bitcoin-encoded IPv6 address. +impl ZcashDeserialize for Ipv6Addr { + fn zcash_deserialize(mut reader: R) -> Result { + let mut ipv6_addr = [0u8; 16]; + reader.read_exact(&mut ipv6_addr)?; + + Ok(Ipv6Addr::from(ipv6_addr)) + } +} + /// Helper for deserializing more succinctly via type inference pub trait ZcashDeserializeInto { /// Deserialize based on type inference diff --git a/zebra-chain/src/serialization/zcash_serialize.rs b/zebra-chain/src/serialization/zcash_serialize.rs index 4fd17d77..301fe0fb 100644 --- a/zebra-chain/src/serialization/zcash_serialize.rs +++ b/zebra-chain/src/serialization/zcash_serialize.rs @@ -1,4 +1,4 @@ -use std::{convert::TryInto, io}; +use std::{convert::TryInto, io, net::Ipv6Addr}; use super::{AtLeastOne, CompactSizeMessage}; @@ -174,3 +174,13 @@ impl ZcashSerialize for String { self.as_str().zcash_serialize(&mut writer) } } + +// We don't impl ZcashSerialize for Ipv4Addr or SocketAddrs, +// because the IPv4 and port formats are different in addr (v1) and addrv2 messages. + +/// Write a Bitcoin-encoded IPv6 address. +impl ZcashSerialize for Ipv6Addr { + fn zcash_serialize(&self, mut writer: W) -> Result<(), std::io::Error> { + writer.write_all(&self.octets()) + } +} diff --git a/zebra-network/src/address_book.rs b/zebra-network/src/address_book.rs index c587af54..aac50f4f 100644 --- a/zebra-network/src/address_book.rs +++ b/zebra-network/src/address_book.rs @@ -10,9 +10,10 @@ use std::{ use tracing::Span; -use zebra_chain::serialization::canonical_socket_addr; - -use crate::{meta_addr::MetaAddrChange, types::MetaAddr, PeerAddrState}; +use crate::{ + meta_addr::MetaAddrChange, protocol::external::canonical_socket_addr, types::MetaAddr, + PeerAddrState, +}; #[cfg(test)] mod tests; diff --git a/zebra-network/src/config.rs b/zebra-network/src/config.rs index e048129b..06116b66 100644 --- a/zebra-network/src/config.rs +++ b/zebra-network/src/config.rs @@ -7,9 +7,9 @@ use std::{ use serde::{de, Deserialize, Deserializer}; -use zebra_chain::{parameters::Network, serialization::canonical_socket_addr}; +use zebra_chain::parameters::Network; -use crate::{constants, BoxError}; +use crate::{constants, protocol::external::canonical_socket_addr, BoxError}; #[cfg(test)] mod tests; diff --git a/zebra-network/src/isolated.rs b/zebra-network/src/isolated.rs index 1d941b85..b5e99d85 100644 --- a/zebra-network/src/isolated.rs +++ b/zebra-network/src/isolated.rs @@ -98,20 +98,26 @@ impl Service for Wrapper { #[cfg(test)] mod tests { + use super::*; #[tokio::test] async fn connect_isolated_sends_minimally_distinguished_version_message() { - use crate::{ - protocol::external::{Codec, Message}, - types::PeerServices, - }; + use std::net::SocketAddr; + use futures::stream::StreamExt; use tokio_util::codec::Framed; + use crate::{ + protocol::external::{AddrInVersion, Codec, Message}, + types::PeerServices, + }; + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); let listen_addr = listener.local_addr().unwrap(); + let fixed_isolated_addr: SocketAddr = "0.0.0.0:8233".parse().unwrap(); + let conn = tokio::net::TcpStream::connect(listen_addr).await.unwrap(); tokio::spawn(connect_isolated(conn, "".to_string())); @@ -139,7 +145,7 @@ mod tests { assert_eq!(timestamp.timestamp() % (5 * 60), 0); assert_eq!( address_from, - (PeerServices::empty(), "0.0.0.0:8233".parse().unwrap()) + AddrInVersion::new(fixed_isolated_addr, PeerServices::empty()), ); assert_eq!(user_agent, ""); assert_eq!(start_height.0, 0); diff --git a/zebra-network/src/meta_addr.rs b/zebra-network/src/meta_addr.rs index 861b578e..d0ed86fd 100644 --- a/zebra-network/src/meta_addr.rs +++ b/zebra-network/src/meta_addr.rs @@ -7,22 +7,27 @@ use std::{ time::Instant, }; -use zebra_chain::serialization::{canonical_socket_addr, DateTime32}; +use zebra_chain::serialization::DateTime32; -use crate::{constants, protocol::types::PeerServices}; +use crate::{ + constants, + protocol::{external::canonical_socket_addr, types::PeerServices}, +}; use MetaAddrChange::*; use PeerAddrState::*; #[cfg(any(test, feature = "proptest-impl"))] use proptest_derive::Arbitrary; + #[cfg(any(test, feature = "proptest-impl"))] -use zebra_chain::serialization::arbitrary::canonical_socket_addr_strategy; +use crate::protocol::external::arbitrary::canonical_socket_addr_strategy; + #[cfg(any(test, feature = "proptest-impl"))] pub(crate) mod arbitrary; #[cfg(test)] -mod tests; +pub(crate) mod tests; /// Peer connection state, based on our interactions with the peer. /// @@ -112,6 +117,8 @@ impl PartialOrd for PeerAddrState { /// An address with metadata on its advertised services and last-seen time. /// +/// This struct can be created from `addr` or `addrv2` messages. +/// /// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#Network_address) #[derive(Copy, Clone, Debug)] #[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))] @@ -131,8 +138,8 @@ pub struct MetaAddr { /// The exact meaning depends on `last_connection_state`: /// - `Responded`: the services advertised by this peer, the last time we /// performed a handshake with it - /// - `NeverAttempted`: the unverified services provided by the remote peer - /// that sent us this address + /// - `NeverAttempted`: the unverified services advertised by another peer, + /// then gossiped by the peer that sent us this address /// - `Failed` or `AttemptPending`: unverified services via another peer, /// or services advertised in a previous handshake /// diff --git a/zebra-network/src/meta_addr/arbitrary.rs b/zebra-network/src/meta_addr/arbitrary.rs index 82a10de8..08d8f671 100644 --- a/zebra-network/src/meta_addr/arbitrary.rs +++ b/zebra-network/src/meta_addr/arbitrary.rs @@ -2,9 +2,11 @@ use std::net::SocketAddr; use proptest::{arbitrary::any, collection::vec, prelude::*}; -use super::{MetaAddr, MetaAddrChange, PeerServices}; +use zebra_chain::serialization::DateTime32; -use zebra_chain::serialization::{arbitrary::canonical_socket_addr_strategy, DateTime32}; +use crate::protocol::external::arbitrary::canonical_socket_addr_strategy; + +use super::{MetaAddr, MetaAddrChange, PeerServices}; /// The largest number of random changes we want to apply to a [`MetaAddr`]. /// diff --git a/zebra-network/src/meta_addr/tests.rs b/zebra-network/src/meta_addr/tests.rs index f4277e20..0bf19100 100644 --- a/zebra-network/src/meta_addr/tests.rs +++ b/zebra-network/src/meta_addr/tests.rs @@ -1,3 +1,6 @@ -mod check; +//! Tests for MetaAddrs + +pub(crate) mod check; + mod prop; mod vectors; diff --git a/zebra-network/src/meta_addr/tests/prop.rs b/zebra-network/src/meta_addr/tests/prop.rs index 2776518b..821b693d 100644 --- a/zebra-network/src/meta_addr/tests/prop.rs +++ b/zebra-network/src/meta_addr/tests/prop.rs @@ -15,9 +15,6 @@ use tokio::{runtime, time::Instant}; use tower::service_fn; use tracing::Span; -use zebra_chain::serialization::{canonical_socket_addr, ZcashDeserialize, ZcashSerialize}; - -use super::check; use crate::{ constants::MIN_PEER_RECONNECTION_DELAY, meta_addr::{ @@ -26,10 +23,12 @@ use crate::{ PeerAddrState::*, }, peer_set::candidate_set::CandidateSet, - protocol::types::PeerServices, + protocol::{external::canonical_socket_addr, types::PeerServices}, AddressBook, }; +use super::check; + /// The number of test cases to use for proptest that have verbose failures. /// /// Set this to the default number of proptest cases, unless you're debugging a @@ -61,139 +60,6 @@ proptest! { } } - /// Test round-trip serialization for gossiped MetaAddrs - #[test] - fn gossiped_roundtrip(gossiped_addr in MetaAddr::gossiped_strategy()) { - zebra_test::init(); - - // We require sanitization before serialization - let gossiped_addr = gossiped_addr.sanitize(); - prop_assume!(gossiped_addr.is_some()); - let gossiped_addr = gossiped_addr.unwrap(); - - // Check that malicious peers can't make Zebra's serialization fail - let addr_bytes = gossiped_addr.zcash_serialize_to_vec(); - prop_assert!( - addr_bytes.is_ok(), - "unexpected serialization error: {:?}, addr: {:?}", - addr_bytes, - gossiped_addr - ); - let addr_bytes = addr_bytes.unwrap(); - - // Assume other implementations deserialize like Zebra - let deserialized_addr = MetaAddr::zcash_deserialize(addr_bytes.as_slice()); - prop_assert!( - deserialized_addr.is_ok(), - "unexpected deserialization error: {:?}, addr: {:?}, bytes: {:?}", - deserialized_addr, - gossiped_addr, - hex::encode(addr_bytes), - ); - let deserialized_addr = deserialized_addr.unwrap(); - - // Check that the addrs are equal - prop_assert_eq!( - gossiped_addr, - deserialized_addr, - "unexpected round-trip mismatch with bytes: {:?}", - hex::encode(addr_bytes), - ); - - // Now check that the re-serialized bytes are equal - // (`impl PartialEq for MetaAddr` might not match serialization equality) - let addr_bytes2 = deserialized_addr.zcash_serialize_to_vec(); - prop_assert!( - addr_bytes2.is_ok(), - "unexpected serialization error after round-trip: {:?}, original addr: {:?}, bytes: {:?}, deserialized addr: {:?}", - addr_bytes2, - gossiped_addr, - hex::encode(addr_bytes), - deserialized_addr, - ); - let addr_bytes2 = addr_bytes2.unwrap(); - - prop_assert_eq!( - &addr_bytes, - &addr_bytes2, - "unexpected round-trip bytes mismatch: original addr: {:?}, bytes: {:?}, deserialized addr: {:?}, bytes: {:?}", - gossiped_addr, - hex::encode(&addr_bytes), - deserialized_addr, - hex::encode(&addr_bytes2), - ); - } - - /// Test round-trip serialization for all MetaAddr variants after sanitization - #[test] - fn sanitized_roundtrip(addr in any::()) { - zebra_test::init(); - - // We require sanitization before serialization, - // but we also need the original address for this test - let sanitized_addr = addr.sanitize(); - prop_assume!(sanitized_addr.is_some()); - let sanitized_addr = sanitized_addr.unwrap(); - - // Make sure sanitization avoids leaks on this address, to avoid spurious errors - check::sanitize_avoids_leaks(&addr, &sanitized_addr); - - // Check that sanitization doesn't make Zebra's serialization fail - let addr_bytes = sanitized_addr.zcash_serialize_to_vec(); - prop_assert!( - addr_bytes.is_ok(), - "unexpected serialization error: {:?}, addr: {:?}", - addr_bytes, - sanitized_addr - ); - let addr_bytes = addr_bytes.unwrap(); - - // Assume other implementations deserialize like Zebra - let deserialized_addr = MetaAddr::zcash_deserialize(addr_bytes.as_slice()); - prop_assert!( - deserialized_addr.is_ok(), - "unexpected deserialization error: {:?}, addr: {:?}, bytes: {:?}", - deserialized_addr, - sanitized_addr, - hex::encode(addr_bytes), - ); - let deserialized_addr = deserialized_addr.unwrap(); - - // Check that the addrs are equal - prop_assert_eq!( - sanitized_addr, - deserialized_addr, - "unexpected round-trip mismatch with bytes: {:?}", - hex::encode(addr_bytes), - ); - - // Check that serialization hasn't de-sanitized anything - check::sanitize_avoids_leaks(&addr, &deserialized_addr); - - // Now check that the re-serialized bytes are equal - // (`impl PartialEq for MetaAddr` might not match serialization equality) - let addr_bytes2 = deserialized_addr.zcash_serialize_to_vec(); - prop_assert!( - addr_bytes2.is_ok(), - "unexpected serialization error after round-trip: {:?}, original addr: {:?}, bytes: {:?}, deserialized addr: {:?}", - addr_bytes2, - sanitized_addr, - hex::encode(addr_bytes), - deserialized_addr, - ); - let addr_bytes2 = addr_bytes2.unwrap(); - - prop_assert_eq!( - &addr_bytes, - &addr_bytes2, - "unexpected double-serialization round-trip mismatch with original addr: {:?}, bytes: {:?}, deserialized addr: {:?}, bytes: {:?}", - sanitized_addr, - hex::encode(&addr_bytes), - deserialized_addr, - hex::encode(&addr_bytes2), - ); - } - /// Make sure that [`MetaAddrChange`]s: /// - do not modify the last seen time, unless it was None, and /// - only modify the services after a response or failure. diff --git a/zebra-network/src/peer/handshake.rs b/zebra-network/src/peer/handshake.rs index d9d4e88e..783c8c96 100644 --- a/zebra-network/src/peer/handshake.rs +++ b/zebra-network/src/peer/handshake.rs @@ -33,7 +33,7 @@ use crate::{ peer::{Client, ClientRequest, Connection, ErrorSlot, HandshakeError, PeerError}, peer_set::ConnectionTracker, protocol::{ - external::{types::*, Codec, InventoryHash, Message}, + external::{types::*, AddrInVersion, Codec, InventoryHash, Message}, internal::{Request, Response}, }, types::MetaAddr, @@ -523,9 +523,9 @@ pub async fn negotiate_version( version: constants::CURRENT_NETWORK_PROTOCOL_VERSION, services: our_services, timestamp, - address_recv: (PeerServices::NODE_NETWORK, their_addr), + address_recv: AddrInVersion::new(their_addr, PeerServices::NODE_NETWORK), // TODO: detect external address (#1893) - address_from: (our_services, our_listen_addr), + address_from: AddrInVersion::new(our_listen_addr, our_services), nonce: local_nonce, user_agent: user_agent.clone(), // The protocol works fine if we don't reveal our current block height, @@ -554,7 +554,8 @@ pub async fn negotiate_version( .. } = remote_msg { - let (address_services, canonical_addr) = address_from; + let canonical_addr = address_from.addr(); + let address_services = address_from.untrusted_services(); if address_services != services { info!( ?services, diff --git a/zebra-network/src/protocol/external.rs b/zebra-network/src/protocol/external.rs index 5c10d844..d0481822 100644 --- a/zebra-network/src/protocol/external.rs +++ b/zebra-network/src/protocol/external.rs @@ -12,10 +12,11 @@ mod message; pub mod types; #[cfg(any(test, feature = "proptest-impl"))] -mod arbitrary; +pub mod arbitrary; #[cfg(test)] mod tests; +pub use addr::{canonical_socket_addr, AddrInVersion}; pub use codec::Codec; pub use inv::InventoryHash; pub use message::Message; diff --git a/zebra-network/src/protocol/external/addr.rs b/zebra-network/src/protocol/external/addr.rs index 07a58fef..a9f8f3af 100644 --- a/zebra-network/src/protocol/external/addr.rs +++ b/zebra-network/src/protocol/external/addr.rs @@ -1,65 +1,22 @@ -//! Node address types and serialization for the Zcash wire format. +//! Zcash node address types and serialization for Zcash network messages. +//! +//! Zcash has 3 different node address formats: +//! - [`AddrV1`]: the format used in `addr` (v1) messages, +//! - [`AddrInVersion`]: the format used in `version` messages, and +//! - [`AddrV2`]: the format used in `addrv2` messages. -use std::io::{Read, Write}; +pub mod canonical; +pub mod in_version; -use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; +pub(crate) mod v1; -use zebra_chain::serialization::{ - ReadZcashExt, SerializationError, TrustedPreallocate, WriteZcashExt, ZcashDeserialize, - ZcashDeserializeInto, ZcashSerialize, -}; +pub use canonical::canonical_socket_addr; +pub use in_version::AddrInVersion; -use crate::{ - meta_addr::MetaAddr, - protocol::external::{types::PeerServices, MAX_PROTOCOL_MESSAGE_LEN}, -}; +// These types and functions should only be visible in the `external` module, +// so that they don't leak outside the serialization code. -impl ZcashSerialize for MetaAddr { - fn zcash_serialize(&self, mut writer: W) -> Result<(), std::io::Error> { - self.last_seen() - .expect( - "unexpected MetaAddr with missing last seen time: MetaAddrs should be sanitized \ - before serialization", - ) - .zcash_serialize(&mut writer)?; +pub(super) use v1::AddrV1; - writer.write_u64::( - self.services - .expect( - "unexpected MetaAddr with missing peer services: MetaAddrs should be \ - sanitized before serialization", - ) - .bits(), - )?; - - writer.write_socket_addr(self.addr)?; - - Ok(()) - } -} - -impl ZcashDeserialize for MetaAddr { - fn zcash_deserialize(mut reader: R) -> Result { - let untrusted_last_seen = (&mut reader).zcash_deserialize_into()?; - let untrusted_services = - PeerServices::from_bits_truncate(reader.read_u64::()?); - let addr = reader.read_socket_addr()?; - - Ok(MetaAddr::new_gossiped_meta_addr( - addr, - untrusted_services, - untrusted_last_seen, - )) - } -} - -/// A serialized meta addr has a 4 byte time, 8 byte services, 16 byte IP addr, and 2 byte port -pub(super) const META_ADDR_SIZE: usize = 4 + 8 + 16 + 2; - -impl TrustedPreallocate for MetaAddr { - fn max_allocation() -> u64 { - // Since a maximal serialized Vec uses at least three bytes for its length (2MB messages / 30B MetaAddr implies the maximal length is much greater than 253) - // the max allocation can never exceed (MAX_PROTOCOL_MESSAGE_LEN - 3) / META_ADDR_SIZE - ((MAX_PROTOCOL_MESSAGE_LEN - 3) / META_ADDR_SIZE) as u64 - } -} +#[cfg(any(test, feature = "proptest-impl"))] +pub(super) use v1::{ipv6_mapped_socket_addr, ADDR_V1_SIZE}; diff --git a/zebra-network/src/protocol/external/addr/canonical.rs b/zebra-network/src/protocol/external/addr/canonical.rs new file mode 100644 index 00000000..3e0f003a --- /dev/null +++ b/zebra-network/src/protocol/external/addr/canonical.rs @@ -0,0 +1,44 @@ +//! Zebra's canonical node address format. +//! +//! Zebra canonicalises all received addresses into Rust [`SocketAddr`]s. +//! If the address is an [IPv4-mapped IPv6 address], it becomes a [`SocketAddr::V4`] +//! +//! [IPv4-mapped IPv6 address]: https://en.wikipedia.org/wiki/IPv6#IPv4-mapped_IPv6_addresses + +use std::net::{IpAddr, Ipv6Addr, SocketAddr}; + +/// Transform a Zcash-deserialized IPv6 address into a canonical Zebra IP address. +/// +/// Zcash uses IPv6-mapped IPv4 addresses in its `addr` (v1) network messages. +/// Zebra converts those addresses to `Ipv4Addr`s, for maximum compatibility +/// with systems that don't understand IPv6. +/// +/// Zebra also uses this canonical format for addresses from other sources. +pub fn canonical_ip_addr(v6_addr: &Ipv6Addr) -> IpAddr { + use IpAddr::*; + + // TODO: replace with `to_ipv4_mapped` when that stabilizes + // https://github.com/rust-lang/rust/issues/27709 + match v6_addr.to_ipv4() { + // workaround for unstable `to_ipv4_mapped` + Some(v4_addr) if v4_addr.to_ipv6_mapped() == *v6_addr => V4(v4_addr), + Some(_) | None => V6(*v6_addr), + } +} + +/// Transform a `SocketAddr` into a canonical Zebra `SocketAddr`, converting +/// IPv6-mapped IPv4 addresses, and removing IPv6 scope IDs and flow information. +/// +/// See [`canonical_ip_addr`] for detailed info on IPv6-mapped IPv4 addresses. +pub fn canonical_socket_addr(socket_addr: impl Into) -> SocketAddr { + use SocketAddr::*; + + let mut socket_addr = socket_addr.into(); + if let V6(v6_socket_addr) = socket_addr { + let canonical_ip = canonical_ip_addr(v6_socket_addr.ip()); + // creating a new SocketAddr removes scope IDs and flow information + socket_addr = SocketAddr::new(canonical_ip, socket_addr.port()); + } + + socket_addr +} diff --git a/zebra-network/src/protocol/external/addr/in_version.rs b/zebra-network/src/protocol/external/addr/in_version.rs new file mode 100644 index 00000000..91e789ea --- /dev/null +++ b/zebra-network/src/protocol/external/addr/in_version.rs @@ -0,0 +1,103 @@ +//! Zcash `version` message node address serialization. +//! +//! The [`AddrInVersion`] format is the same as the `addr` ([`v1`]) message, +//! but without the timestamp field. + +use std::{ + io::{Read, Write}, + net::{SocketAddr, SocketAddrV6}, +}; + +use byteorder::{BigEndian, LittleEndian, ReadBytesExt, WriteBytesExt}; + +use zebra_chain::serialization::{ + SerializationError, ZcashDeserialize, ZcashDeserializeInto, ZcashSerialize, +}; + +use crate::protocol::external::types::PeerServices; + +#[cfg(any(test, feature = "proptest-impl"))] +use proptest_derive::Arbitrary; + +#[cfg(any(test, feature = "proptest-impl"))] +use crate::protocol::external::arbitrary::addr_v1_ipv6_mapped_socket_addr_strategy; + +use super::{canonical_socket_addr, v1::ipv6_mapped_socket_addr}; + +/// A version 1 node address and services, without a last-seen time. +/// This struct is serialized and deserialized as part of `version` messages. +/// +/// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#Network_address) +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))] +pub struct AddrInVersion { + /// The unverified services for the peer at `ipv6_addr`. + /// + /// These services were advertised by the peer at `ipv6_addr`, + /// then gossiped via another peer. + /// + /// ## Security + /// + /// `untrusted_services` on gossiped peers may be invalid due to outdated + /// records, older peer versions, or buggy or malicious peers. + untrusted_services: PeerServices, + + /// The peer's IPv6 socket address. + /// IPv4 addresses are serialized as an [IPv4-mapped IPv6 address]. + /// + /// [IPv4-mapped IPv6 address]: https://en.wikipedia.org/wiki/IPv6#IPv4-mapped_IPv6_addresses + #[cfg_attr( + any(test, feature = "proptest-impl"), + proptest(strategy = "addr_v1_ipv6_mapped_socket_addr_strategy()") + )] + ipv6_addr: SocketAddrV6, +} + +impl AddrInVersion { + /// Returns a new `version` message address based on its fields. + pub fn new(socket_addr: impl Into, untrusted_services: PeerServices) -> Self { + Self { + untrusted_services, + ipv6_addr: ipv6_mapped_socket_addr(socket_addr), + } + } + + /// Returns the canonical address for this peer. + pub fn addr(&self) -> SocketAddr { + canonical_socket_addr(self.ipv6_addr) + } + + /// Returns the services for this peer. + pub fn untrusted_services(&self) -> PeerServices { + self.untrusted_services + } +} + +impl ZcashSerialize for AddrInVersion { + fn zcash_serialize(&self, mut writer: W) -> Result<(), std::io::Error> { + writer.write_u64::(self.untrusted_services.bits())?; + + self.ipv6_addr.ip().zcash_serialize(&mut writer)?; + writer.write_u16::(self.ipv6_addr.port())?; + + Ok(()) + } +} + +impl ZcashDeserialize for AddrInVersion { + fn zcash_deserialize(mut reader: R) -> Result { + let untrusted_services = + PeerServices::from_bits_truncate(reader.read_u64::()?); + + let ipv6_addr = (&mut reader).zcash_deserialize_into()?; + let port = reader.read_u16::()?; + + // `0` is the default unspecified value for these fields. + let ipv6_addr = SocketAddrV6::new(ipv6_addr, port, 0, 0); + + Ok(AddrInVersion { + ipv6_addr, + untrusted_services, + }) + } +} diff --git a/zebra-network/src/protocol/external/addr/v1.rs b/zebra-network/src/protocol/external/addr/v1.rs new file mode 100644 index 00000000..14dcce41 --- /dev/null +++ b/zebra-network/src/protocol/external/addr/v1.rs @@ -0,0 +1,169 @@ +//! Zcash `addr` (v1) message node address serialization. +//! +//! The [`AddrV1`] format serializes all IP addresses as IPv6 addresses. +//! IPv4 addresses are converted to an [IPv4-mapped IPv6 address] before serialization. +//! +//! [IPv4-mapped IPv6 address]: https://en.wikipedia.org/wiki/IPv6#IPv4-mapped_IPv6_addresses + +use std::{ + io::{Read, Write}, + net::{IpAddr, Ipv6Addr, SocketAddr, SocketAddrV6}, +}; + +use byteorder::{BigEndian, LittleEndian, ReadBytesExt, WriteBytesExt}; + +use zebra_chain::serialization::{ + DateTime32, SerializationError, TrustedPreallocate, ZcashDeserialize, ZcashDeserializeInto, + ZcashSerialize, +}; + +use crate::{ + meta_addr::MetaAddr, + protocol::external::{types::PeerServices, MAX_PROTOCOL_MESSAGE_LEN}, +}; + +#[cfg(any(test, feature = "proptest-impl"))] +use proptest_derive::Arbitrary; + +#[cfg(any(test, feature = "proptest-impl"))] +use crate::protocol::external::arbitrary::addr_v1_ipv6_mapped_socket_addr_strategy; + +/// A version 1 node address, its advertised services, and last-seen time. +/// This struct is serialized and deserialized into `addr` messages. +/// +/// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#Network_address) +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))] +pub(in super::super) struct AddrV1 { + /// The unverified "last seen time" gossiped by the remote peer that sent us + /// this address. + /// + /// See the [`MetaAddr::last_seen`] method for details. + untrusted_last_seen: DateTime32, + + /// The unverified services for the peer at `ipv6_addr`. + /// + /// These services were advertised by the peer at `ipv6_addr`, + /// then gossiped via another peer. + /// + /// ## Security + /// + /// `untrusted_services` on gossiped peers may be invalid due to outdated + /// records, older peer versions, or buggy or malicious peers. + untrusted_services: PeerServices, + + /// The peer's IPv6 socket address. + /// IPv4 addresses are serialized as an [IPv4-mapped IPv6 address]. + /// + /// [IPv4-mapped IPv6 address]: https://en.wikipedia.org/wiki/IPv6#IPv4-mapped_IPv6_addresses + #[cfg_attr( + any(test, feature = "proptest-impl"), + proptest(strategy = "addr_v1_ipv6_mapped_socket_addr_strategy()") + )] + ipv6_addr: SocketAddrV6, +} + +impl From for AddrV1 { + fn from(meta_addr: MetaAddr) -> Self { + let ipv6_addr = ipv6_mapped_socket_addr(meta_addr.addr); + + let untrusted_services = meta_addr.services.expect( + "unexpected MetaAddr with missing peer services: \ + MetaAddrs should be sanitized before serialization", + ); + let untrusted_last_seen = meta_addr.last_seen().expect( + "unexpected MetaAddr with missing last seen time: \ + MetaAddrs should be sanitized before serialization", + ); + + AddrV1 { + untrusted_last_seen, + untrusted_services, + ipv6_addr, + } + } +} + +impl From for MetaAddr { + fn from(addr_v1: AddrV1) -> Self { + MetaAddr::new_gossiped_meta_addr( + addr_v1.ipv6_addr.into(), + addr_v1.untrusted_services, + addr_v1.untrusted_last_seen, + ) + } +} + +impl ZcashSerialize for AddrV1 { + fn zcash_serialize(&self, mut writer: W) -> Result<(), std::io::Error> { + self.untrusted_last_seen.zcash_serialize(&mut writer)?; + writer.write_u64::(self.untrusted_services.bits())?; + + self.ipv6_addr.ip().zcash_serialize(&mut writer)?; + writer.write_u16::(self.ipv6_addr.port())?; + + Ok(()) + } +} + +impl ZcashDeserialize for AddrV1 { + fn zcash_deserialize(mut reader: R) -> Result { + let untrusted_last_seen = (&mut reader).zcash_deserialize_into()?; + let untrusted_services = + PeerServices::from_bits_truncate(reader.read_u64::()?); + + let ipv6_addr = (&mut reader).zcash_deserialize_into()?; + let port = reader.read_u16::()?; + + // `0` is the default unspecified value for these fields. + let ipv6_addr = SocketAddrV6::new(ipv6_addr, port, 0, 0); + + Ok(AddrV1 { + ipv6_addr, + untrusted_services, + untrusted_last_seen, + }) + } +} + +/// A serialized `addr` (v1) has a 4 byte time, 8 byte services, 16 byte IP addr, and 2 byte port +pub(in super::super) const ADDR_V1_SIZE: usize = 4 + 8 + 16 + 2; + +impl TrustedPreallocate for AddrV1 { + fn max_allocation() -> u64 { + // Since a maximal serialized Vec uses at least three bytes for its length + // (2MB messages / 30B AddrV1 implies the maximal length is much greater than 253) + // the max allocation can never exceed (MAX_PROTOCOL_MESSAGE_LEN - 3) / META_ADDR_SIZE + ((MAX_PROTOCOL_MESSAGE_LEN - 3) / ADDR_V1_SIZE) as u64 + } +} + +/// Transform a `SocketAddr` into an IPv6-mapped IPv4 addresses. +/// +/// See [`canonical_ip_addr`] for detailed info on IPv6-mapped IPv4 addresses. +pub(in super::super) fn ipv6_mapped_ip_addr(ip_addr: &IpAddr) -> Ipv6Addr { + use IpAddr::*; + + match ip_addr { + V4(v4_addr) => v4_addr.to_ipv6_mapped(), + V6(v6_addr) => *v6_addr, + } +} + +/// Transform a `SocketAddr` into an IPv6-mapped IPv4 addresses, +/// for `addr` (v1) Zcash network messages. +/// +/// Also remove IPv6 scope IDs and flow information. +/// +/// See [`canonical_ip_addr`] for detailed info on IPv6-mapped IPv4 addresses. +pub(in super::super) fn ipv6_mapped_socket_addr( + socket_addr: impl Into, +) -> SocketAddrV6 { + let socket_addr = socket_addr.into(); + + let ipv6_mapped_ip = ipv6_mapped_ip_addr(&socket_addr.ip()); + + // Remove scope IDs and flow information. + // `0` is the default unspecified value for these fields. + SocketAddrV6::new(ipv6_mapped_ip, socket_addr.port(), 0, 0) +} diff --git a/zebra-network/src/protocol/external/arbitrary.rs b/zebra-network/src/protocol/external/arbitrary.rs index 4a20e34a..614da606 100644 --- a/zebra-network/src/protocol/external/arbitrary.rs +++ b/zebra-network/src/protocol/external/arbitrary.rs @@ -1,11 +1,18 @@ -use std::convert::TryInto; +use std::{ + convert::TryInto, + net::{SocketAddr, SocketAddrV6}, +}; use proptest::{arbitrary::any, arbitrary::Arbitrary, collection::vec, prelude::*}; -use super::{types::PeerServices, InventoryHash, Message}; - use zebra_chain::{block, transaction}; +use super::{ + addr::{canonical_socket_addr, ipv6_mapped_socket_addr}, + types::PeerServices, + InventoryHash, Message, +}; + impl InventoryHash { /// Generate a proptest strategy for [`InventoryHash::Error`]s. pub fn error_strategy() -> BoxedStrategy { @@ -104,3 +111,17 @@ impl Message { .boxed() } } + +/// Returns a random canonical Zebra `SocketAddr`. +/// +/// See [`canonical_ip_addr`] for details. +pub fn canonical_socket_addr_strategy() -> impl Strategy { + any::().prop_map(canonical_socket_addr) +} + +/// Returns a random `SocketAddrV6` for use in `addr` (v1) Zcash network messages. +/// +/// See [`canonical_ip_addr`] for details. +pub fn addr_v1_ipv6_mapped_socket_addr_strategy() -> impl Strategy { + any::().prop_map(ipv6_mapped_socket_addr) +} diff --git a/zebra-network/src/protocol/external/codec.rs b/zebra-network/src/protocol/external/codec.rs index ee794dbb..1a7bda9f 100644 --- a/zebra-network/src/protocol/external/codec.rs +++ b/zebra-network/src/protocol/external/codec.rs @@ -16,7 +16,7 @@ use zebra_chain::{ parameters::Network, serialization::{ sha256d, zcash_deserialize_bytes_external_count, FakeWriter, ReadZcashExt, - SerializationError as Error, WriteZcashExt, ZcashDeserialize, ZcashSerialize, + SerializationError as Error, ZcashDeserialize, ZcashDeserializeInto, ZcashSerialize, MAX_PROTOCOL_MESSAGE_LEN, }, transaction::Transaction, @@ -25,6 +25,7 @@ use zebra_chain::{ use crate::constants; use super::{ + addr::{AddrInVersion, AddrV1}, message::{Message, RejectReason}, types::*, }; @@ -212,13 +213,8 @@ impl Codec { // serialization can not error. writer.write_i64::(timestamp.timestamp())?; - let (recv_services, recv_addr) = address_recv; - writer.write_u64::(recv_services.bits())?; - writer.write_socket_addr(*recv_addr)?; - - let (from_services, from_addr) = address_from; - writer.write_u64::(from_services.bits())?; - writer.write_socket_addr(*from_addr)?; + address_recv.zcash_serialize(&mut writer)?; + address_from.zcash_serialize(&mut writer)?; writer.write_u64::(nonce.0)?; user_agent.zcash_serialize(&mut writer)?; @@ -245,7 +241,12 @@ impl Codec { writer.write_all(data)?; } } - Message::Addr(addrs) => addrs.zcash_serialize(&mut writer)?, + Message::Addr(addrs) => { + // Regardless of the way we received the address, + // Zebra always sends `addr` messages + let v1_addrs: Vec = addrs.iter().map(|addr| AddrV1::from(*addr)).collect(); + v1_addrs.zcash_serialize(&mut writer)? + } Message::GetAddr => { /* Empty payload -- no-op */ } Message::Block(block) => block.zcash_serialize(&mut writer)?, Message::GetBlocks { known_blocks, stop } => { @@ -455,14 +456,8 @@ impl Codec { .ok_or(Error::Parse( "version timestamp is out of range for DateTime", ))?, - address_recv: ( - PeerServices::from_bits_truncate(reader.read_u64::()?), - reader.read_socket_addr()?, - ), - address_from: ( - PeerServices::from_bits_truncate(reader.read_u64::()?), - reader.read_socket_addr()?, - ), + address_recv: AddrInVersion::zcash_deserialize(&mut reader)?, + address_from: AddrInVersion::zcash_deserialize(&mut reader)?, nonce: Nonce(reader.read_u64::()?), user_agent: String::zcash_deserialize(&mut reader)?, start_height: block::Height(reader.read_u32::()?), @@ -514,8 +509,13 @@ impl Codec { }) } + /// Deserialize an `addr` (v1) message into a list of `MetaAddr`s. fn read_addr(&self, reader: R) -> Result { - Ok(Message::Addr(Vec::zcash_deserialize(reader)?)) + let addrs: Vec = reader.zcash_deserialize_into()?; + + // Convert the received address format to Zebra's internal `MetaAddr`. + let addrs = addrs.into_iter().map(Into::into).collect(); + Ok(Message::Addr(addrs)) } fn read_getaddr(&self, mut _reader: R) -> Result { @@ -641,13 +641,13 @@ mod tests { version: crate::constants::CURRENT_NETWORK_PROTOCOL_VERSION, services, timestamp, - address_recv: ( - services, + address_recv: AddrInVersion::new( SocketAddr::new(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 6)), 8233), + services, ), - address_from: ( - services, + address_from: AddrInVersion::new( SocketAddr::new(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 6)), 8233), + services, ), nonce: Nonce(0x9082_4908_8927_9238), user_agent: "Zebra".to_owned(), diff --git a/zebra-network/src/protocol/external/message.rs b/zebra-network/src/protocol/external/message.rs index 33ea058c..0c6deed2 100644 --- a/zebra-network/src/protocol/external/message.rs +++ b/zebra-network/src/protocol/external/message.rs @@ -1,6 +1,6 @@ //! Definitions of network messages. -use std::{error::Error, fmt, net, sync::Arc}; +use std::{error::Error, fmt, sync::Arc}; use chrono::{DateTime, Utc}; @@ -11,7 +11,7 @@ use zebra_chain::{ use crate::meta_addr::MetaAddr; -use super::{inv::InventoryHash, types::*}; +use super::{addr::AddrInVersion, inv::InventoryHash, types::*}; #[cfg(any(test, feature = "proptest-impl"))] use proptest_derive::Arbitrary; @@ -65,11 +65,11 @@ pub enum Message { /// advertised network services. /// /// Q: how does the handshake know the remote peer's services already? - address_recv: (PeerServices, net::SocketAddr), + address_recv: AddrInVersion, /// The network address of the node sending this message, and its /// advertised network services. - address_from: (PeerServices, net::SocketAddr), + address_from: AddrInVersion, /// Node random nonce, randomly generated every time a version /// packet is sent. This nonce is used to detect connections @@ -138,9 +138,15 @@ pub enum Message { /// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#getaddr) GetAddr, - /// An `addr` message. + /// A sent or received `addr` message, or a received `addrv2` message. /// - /// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#addr) + /// Currently, Zebra: + /// - sends and receives `addr` messages, + /// - parses received `addrv2` messages, + /// - but does not send `addrv2` messages. + /// + /// [addr Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#addr) + /// [addrv2 ZIP 155](https://zips.z.cash/zip-0155#specification) Addr(Vec), /// A `getblocks` message. diff --git a/zebra-network/src/protocol/external/tests/preallocate.rs b/zebra-network/src/protocol/external/tests/preallocate.rs index 8dab828b..54e58c4c 100644 --- a/zebra-network/src/protocol/external/tests/preallocate.rs +++ b/zebra-network/src/protocol/external/tests/preallocate.rs @@ -8,7 +8,10 @@ use zebra_chain::serialization::{TrustedPreallocate, ZcashSerialize, MAX_PROTOCO use crate::meta_addr::MetaAddr; -use super::super::{addr::META_ADDR_SIZE, inv::InventoryHash}; +use super::super::{ + addr::{AddrV1, ADDR_V1_SIZE}, + inv::InventoryHash, +}; proptest! { /// Confirm that each InventoryHash takes the expected size in bytes when serialized. @@ -63,45 +66,47 @@ proptest! { } proptest! { - /// Confirm that each MetaAddr takes exactly META_ADDR_SIZE bytes when serialized. + /// Confirm that each AddrV1 takes exactly ADDR_V1_SIZE bytes when serialized. /// This verifies that our calculated `TrustedPreallocate::max_allocation()` is indeed an upper bound. #[test] - fn meta_addr_size_is_correct(addr in MetaAddr::arbitrary()) { + fn addr_v1_size_is_correct(addr in MetaAddr::arbitrary()) { zebra_test::init(); // We require sanitization before serialization let addr = addr.sanitize(); prop_assume!(addr.is_some()); - let addr = addr.unwrap(); + + let addr: AddrV1 = addr.unwrap().into(); let serialized = addr .zcash_serialize_to_vec() .expect("Serialization to vec must succeed"); - assert!(serialized.len() == META_ADDR_SIZE) + assert!(serialized.len() == ADDR_V1_SIZE) } /// Verifies that... - /// 1. The smallest disallowed vector of `MetaAddrs`s is too large to fit in a legal Zcash message + /// 1. The smallest disallowed vector of `AddrV1`s is too large to fit in a legal Zcash message /// 2. The largest allowed vector is small enough to fit in a legal Zcash message #[test] - fn meta_addr_max_allocation_is_correct(addr in MetaAddr::arbitrary()) { + fn addr_v1_max_allocation_is_correct(addr in MetaAddr::arbitrary()) { zebra_test::init(); // We require sanitization before serialization let addr = addr.sanitize(); prop_assume!(addr.is_some()); - let addr = addr.unwrap(); - let max_allocation: usize = MetaAddr::max_allocation().try_into().unwrap(); + let addr: AddrV1 = addr.unwrap().into(); + + let max_allocation: usize = AddrV1::max_allocation().try_into().unwrap(); let mut smallest_disallowed_vec = Vec::with_capacity(max_allocation + 1); - for _ in 0..(MetaAddr::max_allocation() + 1) { + for _ in 0..(AddrV1::max_allocation() + 1) { smallest_disallowed_vec.push(addr); } let smallest_disallowed_serialized = smallest_disallowed_vec .zcash_serialize_to_vec() .expect("Serialization to vec must succeed"); // Check that our smallest_disallowed_vec is only one item larger than the limit - assert!(((smallest_disallowed_vec.len() - 1) as u64) == MetaAddr::max_allocation()); + assert!(((smallest_disallowed_vec.len() - 1) as u64) == AddrV1::max_allocation()); // Check that our smallest_disallowed_vec is too big to send in a valid Zcash message assert!(smallest_disallowed_serialized.len() > MAX_PROTOCOL_MESSAGE_LEN); @@ -112,8 +117,8 @@ proptest! { .zcash_serialize_to_vec() .expect("Serialization to vec must succeed"); - // Check that our largest_allowed_vec contains the maximum number of MetaAddrs - assert!((largest_allowed_vec.len() as u64) == MetaAddr::max_allocation()); + // Check that our largest_allowed_vec contains the maximum number of AddrV1s + assert!((largest_allowed_vec.len() as u64) == AddrV1::max_allocation()); // Check that our largest_allowed_vec is small enough to fit in a Zcash message. assert!(largest_allowed_serialized.len() <= MAX_PROTOCOL_MESSAGE_LEN); } diff --git a/zebra-network/src/protocol/external/tests/prop.rs b/zebra-network/src/protocol/external/tests/prop.rs index 79e9d23a..d64845c0 100644 --- a/zebra-network/src/protocol/external/tests/prop.rs +++ b/zebra-network/src/protocol/external/tests/prop.rs @@ -1,11 +1,16 @@ +//! Randomised property tests for Zebra's Zcash network protocol types. + use bytes::BytesMut; use proptest::{collection::vec, prelude::*}; use tokio_util::codec::{Decoder, Encoder}; use zebra_chain::serialization::{ - SerializationError, ZcashDeserializeInto, ZcashSerialize, MAX_PROTOCOL_MESSAGE_LEN, + SerializationError, ZcashDeserialize, ZcashDeserializeInto, ZcashSerialize, + MAX_PROTOCOL_MESSAGE_LEN, }; +use crate::{meta_addr::tests::check, meta_addr::MetaAddr, protocol::external::addr::AddrV1}; + use super::super::{Codec, InventoryHash, Message}; /// Maximum number of random input bytes to try to deserialize an [`InventoryHash`] from. @@ -89,4 +94,137 @@ proptest! { prop_assert!(decoded.is_ok()); prop_assert_eq!(decoded.unwrap(), Some(message)); } + + /// Test round-trip AddrV1 serialization for gossiped MetaAddrs + #[test] + fn gossiped_roundtrip(gossiped_addr in MetaAddr::gossiped_strategy()) { + zebra_test::init(); + + // We require sanitization before serialization + let gossiped_addr = gossiped_addr.sanitize(); + prop_assume!(gossiped_addr.is_some()); + let gossiped_addr = gossiped_addr.unwrap(); + + // Check that malicious peers can't make Zebra's serialization fail + let addr_bytes = AddrV1::from(gossiped_addr).zcash_serialize_to_vec(); + prop_assert!( + addr_bytes.is_ok(), + "unexpected serialization error: {:?}, addr: {:?}", + addr_bytes, + gossiped_addr + ); + let addr_bytes = addr_bytes.unwrap(); + + // Assume other implementations deserialize like Zebra + let deserialized_addr = AddrV1::zcash_deserialize(addr_bytes.as_slice()); + prop_assert!( + deserialized_addr.is_ok(), + "unexpected deserialization error: {:?}, addr: {:?}, bytes: {:?}", + deserialized_addr, + gossiped_addr, + hex::encode(addr_bytes), + ); + let deserialized_addr = deserialized_addr.unwrap().into(); + + // Check that the addrs are equal + prop_assert_eq!( + gossiped_addr, + deserialized_addr, + "unexpected round-trip mismatch with bytes: {:?}", + hex::encode(addr_bytes), + ); + + // Now check that the re-serialized bytes are equal + // (`impl PartialEq for MetaAddr` might not match serialization equality) + let addr_bytes2 = AddrV1::from(deserialized_addr).zcash_serialize_to_vec(); + prop_assert!( + addr_bytes2.is_ok(), + "unexpected serialization error after round-trip: {:?}, original addr: {:?}, bytes: {:?}, deserialized addr: {:?}", + addr_bytes2, + gossiped_addr, + hex::encode(addr_bytes), + deserialized_addr, + ); + let addr_bytes2 = addr_bytes2.unwrap(); + + prop_assert_eq!( + &addr_bytes, + &addr_bytes2, + "unexpected round-trip bytes mismatch: original addr: {:?}, bytes: {:?}, deserialized addr: {:?}, bytes: {:?}", + gossiped_addr, + hex::encode(&addr_bytes), + deserialized_addr, + hex::encode(&addr_bytes2), + ); + } + + /// Test round-trip AddrV1 serialization for all MetaAddr variants after sanitization + #[test] + fn sanitized_roundtrip(addr in any::()) { + zebra_test::init(); + + // We require sanitization before serialization, + // but we also need the original address for this test + let sanitized_addr = addr.sanitize(); + prop_assume!(sanitized_addr.is_some()); + let sanitized_addr = sanitized_addr.unwrap(); + + // Make sure sanitization avoids leaks on this address, to avoid spurious errors + check::sanitize_avoids_leaks(&addr, &sanitized_addr); + + // Check that sanitization doesn't make Zebra's serialization fail + let addr_bytes = AddrV1::from(sanitized_addr).zcash_serialize_to_vec(); + prop_assert!( + addr_bytes.is_ok(), + "unexpected serialization error: {:?}, addr: {:?}", + addr_bytes, + sanitized_addr + ); + let addr_bytes = addr_bytes.unwrap(); + + // Assume other implementations deserialize like Zebra + let deserialized_addr = AddrV1::zcash_deserialize(addr_bytes.as_slice()); + prop_assert!( + deserialized_addr.is_ok(), + "unexpected deserialization error: {:?}, addr: {:?}, bytes: {:?}", + deserialized_addr, + sanitized_addr, + hex::encode(addr_bytes), + ); + let deserialized_addr = deserialized_addr.unwrap().into(); + + // Check that the addrs are equal + prop_assert_eq!( + sanitized_addr, + deserialized_addr, + "unexpected round-trip mismatch with bytes: {:?}", + hex::encode(addr_bytes), + ); + + // Check that serialization hasn't de-sanitized anything + check::sanitize_avoids_leaks(&addr, &deserialized_addr); + + // Now check that the re-serialized bytes are equal + // (`impl PartialEq for MetaAddr` might not match serialization equality) + let addr_bytes2 = AddrV1::from(deserialized_addr).zcash_serialize_to_vec(); + prop_assert!( + addr_bytes2.is_ok(), + "unexpected serialization error after round-trip: {:?}, original addr: {:?}, bytes: {:?}, deserialized addr: {:?}", + addr_bytes2, + sanitized_addr, + hex::encode(addr_bytes), + deserialized_addr, + ); + let addr_bytes2 = addr_bytes2.unwrap(); + + prop_assert_eq!( + &addr_bytes, + &addr_bytes2, + "unexpected double-serialization round-trip mismatch with original addr: {:?}, bytes: {:?}, deserialized addr: {:?}, bytes: {:?}", + sanitized_addr, + hex::encode(&addr_bytes), + deserialized_addr, + hex::encode(&addr_bytes2), + ); + } }