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 <janito.vff@gmail.com>

Co-authored-by: Janito Vaqueiro Ferreira Filho <janito.vff@gmail.com>
This commit is contained in:
teor 2021-11-10 06:47:50 +10:00 committed by GitHub
parent 62bfa15e96
commit 85b016756d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 625 additions and 360 deletions

View File

@ -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,

View File

@ -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<Value = chrono::DateTime<Utc>> {
any::<DateTime32>().prop_map(Into::into)
}
/// Returns a random canonical Zebra `SocketAddr`.
///
/// See [`canonical_ip_addr`] for details.
pub fn canonical_socket_addr_strategy() -> impl Strategy<Value = SocketAddr> {
any::<SocketAddr>().prop_map(canonical_socket_addr)
}
impl Arbitrary for CompactSizeMessage {
type Parameters = ();

View File

@ -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<IpAddr> {
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<SocketAddr> {
let ip_addr = self.read_ip_addr()?;
let port = self.read_u16::<BigEndian>()?;
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<R: io::Read + ?Sized> 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>) -> 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
}

View File

@ -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::<BigEndian>(addr.port())
}
/// Convenience method to write exactly 32 u8's.
#[inline]
fn write_32_bytes(&mut self, bytes: &[u8; 32]) -> io::Result<()> {

View File

@ -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<R: io::Read>(mut reader: R) -> Result<Self, SerializationError> {
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

View File

@ -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<W: io::Write>(&self, mut writer: W) -> Result<(), std::io::Error> {
writer.write_all(&self.octets())
}
}

View File

@ -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;

View File

@ -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;

View File

@ -98,20 +98,26 @@ impl Service<Request> 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);

View File

@ -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
///

View File

@ -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`].
///

View File

@ -1,3 +1,6 @@
mod check;
//! Tests for MetaAddrs
pub(crate) mod check;
mod prop;
mod vectors;

View File

@ -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::<MetaAddr>()) {
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.

View File

@ -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,

View File

@ -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;

View File

@ -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<W: Write>(&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::<LittleEndian>(
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<R: Read>(mut reader: R) -> Result<Self, SerializationError> {
let untrusted_last_seen = (&mut reader).zcash_deserialize_into()?;
let untrusted_services =
PeerServices::from_bits_truncate(reader.read_u64::<LittleEndian>()?);
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<MetAddr> 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};

View File

@ -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>) -> 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
}

View File

@ -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<SocketAddr>, 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<W: Write>(&self, mut writer: W) -> Result<(), std::io::Error> {
writer.write_u64::<LittleEndian>(self.untrusted_services.bits())?;
self.ipv6_addr.ip().zcash_serialize(&mut writer)?;
writer.write_u16::<BigEndian>(self.ipv6_addr.port())?;
Ok(())
}
}
impl ZcashDeserialize for AddrInVersion {
fn zcash_deserialize<R: Read>(mut reader: R) -> Result<Self, SerializationError> {
let untrusted_services =
PeerServices::from_bits_truncate(reader.read_u64::<LittleEndian>()?);
let ipv6_addr = (&mut reader).zcash_deserialize_into()?;
let port = reader.read_u16::<BigEndian>()?;
// `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,
})
}
}

View File

@ -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<MetaAddr> 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<AddrV1> 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<W: Write>(&self, mut writer: W) -> Result<(), std::io::Error> {
self.untrusted_last_seen.zcash_serialize(&mut writer)?;
writer.write_u64::<LittleEndian>(self.untrusted_services.bits())?;
self.ipv6_addr.ip().zcash_serialize(&mut writer)?;
writer.write_u16::<BigEndian>(self.ipv6_addr.port())?;
Ok(())
}
}
impl ZcashDeserialize for AddrV1 {
fn zcash_deserialize<R: Read>(mut reader: R) -> Result<Self, SerializationError> {
let untrusted_last_seen = (&mut reader).zcash_deserialize_into()?;
let untrusted_services =
PeerServices::from_bits_truncate(reader.read_u64::<LittleEndian>()?);
let ipv6_addr = (&mut reader).zcash_deserialize_into()?;
let port = reader.read_u16::<BigEndian>()?;
// `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<AddrV1> 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<SocketAddr>,
) -> 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)
}

View File

@ -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<Self> {
@ -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<Value = SocketAddr> {
any::<SocketAddr>().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<Value = SocketAddrV6> {
any::<SocketAddr>().prop_map(ipv6_mapped_socket_addr)
}

View File

@ -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::<LittleEndian>(timestamp.timestamp())?;
let (recv_services, recv_addr) = address_recv;
writer.write_u64::<LittleEndian>(recv_services.bits())?;
writer.write_socket_addr(*recv_addr)?;
let (from_services, from_addr) = address_from;
writer.write_u64::<LittleEndian>(from_services.bits())?;
writer.write_socket_addr(*from_addr)?;
address_recv.zcash_serialize(&mut writer)?;
address_from.zcash_serialize(&mut writer)?;
writer.write_u64::<LittleEndian>(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<AddrV1> = 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::<LittleEndian>()?),
reader.read_socket_addr()?,
),
address_from: (
PeerServices::from_bits_truncate(reader.read_u64::<LittleEndian>()?),
reader.read_socket_addr()?,
),
address_recv: AddrInVersion::zcash_deserialize(&mut reader)?,
address_from: AddrInVersion::zcash_deserialize(&mut reader)?,
nonce: Nonce(reader.read_u64::<LittleEndian>()?),
user_agent: String::zcash_deserialize(&mut reader)?,
start_height: block::Height(reader.read_u32::<LittleEndian>()?),
@ -514,8 +509,13 @@ impl Codec {
})
}
/// Deserialize an `addr` (v1) message into a list of `MetaAddr`s.
fn read_addr<R: Read>(&self, reader: R) -> Result<Message, Error> {
Ok(Message::Addr(Vec::zcash_deserialize(reader)?))
let addrs: Vec<AddrV1> = 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<R: Read>(&self, mut _reader: R) -> Result<Message, Error> {
@ -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(),

View File

@ -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<MetaAddr>),
/// A `getblocks` message.

View File

@ -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);
}

View File

@ -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::<MetaAddr>()) {
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),
);
}
}