From adc421f7fee6e14d54d233a08df259b234b1ca20 Mon Sep 17 00:00:00 2001 From: Henry de Valence Date: Sun, 15 Sep 2019 12:11:41 -0700 Subject: [PATCH] Implement ZcashDeserialize for Message::Version. This has a test that the serialization implementation round trips correctly, but is very much a work in progress. Issues with this code include: The deserialization logic for message headers is somewhat ugly because of a lack of a convenient idiom for reading a few bytes at a time into a fixed-size array. Perhaps this could be fixed with an extension trait, or avoided altogether by using `u32`s instead of `[u8; 4]`, even when the latter is the "morally correct type". Deserialization does an extra allocation, copying the body into a temporary buffer. This avoids two problems: 1) capping the number of bytes that can be read by the `Read`er passed into the body parser and 2) allows making two passes over the body data, one to parse it and one to compute the checksum. We could avoid making two passes over the body by computing the checksum simultaneously with the parsing. A convenient way to do this would be to define a ``` struct ChecksumReader { inner: R, digest: Sha256, } impl Read for ChecksumReader { /* ... */ } ``` and implement `Read` for `ChecksumReader` to forward reads from the inner reader, copying data into the digest object as it does so. (It could also have a maximum length to enforce that we do not read past the nominal end of the body). A similar `ChecksumWriter` could be used during serialization, although because the checksum is at the beginning rather than the end of the message it does not help avoid an allocation there. It could also be generic over a `Digest` implementation, although because we need a truncated double-SHA256 we would have to write a custom `Digest` implementation, so this is probably not worthwhile unless we have other checksum types. Finally, this does very minimal testing -- just round-trip serialization on a single message. It would be good to build in support for property-based testing, probably using `proptest`; if we could define generation and shrinking strategies for every component type of every message, we could do strong randomized testing of the serialization. --- zebra-network/src/message.rs | 312 +++++++++++++++++++++++++++++++++-- 1 file changed, 297 insertions(+), 15 deletions(-) diff --git a/zebra-network/src/message.rs b/zebra-network/src/message.rs index a5f55e2a1..e9a2ff1d9 100644 --- a/zebra-network/src/message.rs +++ b/zebra-network/src/message.rs @@ -2,14 +2,14 @@ use std::io; -use byteorder::{BigEndian, LittleEndian, WriteBytesExt}; -use chrono::{DateTime, Utc}; +use byteorder::{BigEndian, LittleEndian, ReadBytesExt, WriteBytesExt}; +use chrono::{DateTime, TimeZone, Utc}; use zebra_chain::{ serialization::{ ReadZcashExt, SerializationError, WriteZcashExt, ZcashDeserialize, ZcashSerialize, }, - types::Sha256dChecksum, + types::{BlockHeight, Sha256dChecksum}, }; use crate::meta_addr::MetaAddr; @@ -77,7 +77,7 @@ pub enum Message { user_agent: String, /// The last block received by the emitting node. - start_height: zebra_chain::types::BlockHeight, + start_height: BlockHeight, /// Whether the remote peer should announce relayed /// transactions or not, see [BIP 0037](https://github.com/bitcoin/bips/blob/master/bip-0037.mediawiki) @@ -388,18 +388,300 @@ impl Message { /// that trait, because message serialization requires additional parameters /// (the network magic and the network version). pub fn zcash_deserialize( - _reader: R, - _magic: Magic, - _version: Version, + mut reader: R, + magic: Magic, + version: Version, ) -> Result { - unimplemented!() - } + use SerializationError::ParseError; + let message_magic = { + let mut bytes = [0u8; 4]; + reader.read_exact(&mut bytes)?; + Magic(bytes) + }; + if magic != message_magic { + return Err(ParseError("Message has incorrect magic value")); + } - fn deserialize_body( - _reader: R, - _magic: Magic, - _version: Version, - ) -> Result { - unimplemented!() + let command = { + let mut bytes = [0u8; 12]; + reader.read_exact(&mut bytes)?; + bytes + }; + + let body_len = reader.read_u32::()? as usize; + // XXX ugly + let checksum = { + let mut bytes = [0u8; 4]; + reader.read_exact(&mut bytes)?; + Sha256dChecksum(bytes) + }; + + // XXX add a ChecksumReader(R) wrapper and avoid this + let body = { + let mut bytes = vec![0; body_len]; + reader.read_exact(&mut bytes)?; + bytes + }; + + if checksum != Sha256dChecksum::from(&body[..]) { + return Err(SerializationError::ParseError("checksum does not match")); + } + + let body_reader = io::Cursor::new(&body); + match &command { + b"version\0\0\0\0\0" => try_read_version(body_reader, version), + b"verack\0\0\0\0\0\0" => try_read_verack(body_reader, version), + b"ping\0\0\0\0\0\0\0\0" => try_read_ping(body_reader, version), + b"pong\0\0\0\0\0\0\0\0" => try_read_pong(body_reader, version), + b"reject\0\0\0\0\0\0" => try_read_reject(body_reader, version), + b"addr\0\0\0\0\0\0\0\0" => try_read_addr(body_reader, version), + b"getaddr\0\0\0\0\0" => try_read_getaddr(body_reader, version), + b"block\0\0\0\0\0\0\0" => try_read_block(body_reader, version), + b"getblocks\0\0\0" => try_read_getblocks(body_reader, version), + b"headers\0\0\0\0\0" => try_read_headers(body_reader, version), + b"getheaders\0\0" => try_read_getheaders(body_reader, version), + b"inv\0\0\0\0\0\0\0\0\0" => try_read_inv(body_reader, version), + b"getdata\0\0\0\0\0" => try_read_getdata(body_reader, version), + b"notfound\0\0\0\0" => try_read_notfound(body_reader, version), + b"tx\0\0\0\0\0\0\0\0\0\0" => try_read_tx(body_reader, version), + b"mempool\0\0\0\0\0" => try_read_mempool(body_reader, version), + b"filterload\0\0" => try_read_filterload(body_reader, version), + b"filteradd\0\0\0" => try_read_filteradd(body_reader, version), + b"filterclear\0" => try_read_filterclear(body_reader, version), + b"merkleblock\0" => try_read_merkleblock(body_reader, version), + _ => Err(ParseError("Unknown command")), + } + } +} + +fn try_read_version( + mut reader: R, + _parsing_version: Version, +) -> Result { + let version = Version(reader.read_u32::()?); + let services = Services(reader.read_u64::()?); + let timestamp = Utc.timestamp(reader.read_i64::()?, 0); + + // We represent a Bitcoin `net_addr` as a `MetaAddr` internally. However, + // the version message encodes `net_addr`s without timestamps, so we fill in + // the timestamp field of the `MetaAddr`s with the version message's + // timestamp. + let address_receiving = MetaAddr { + services: Services(reader.read_u64::()?), + addr: reader.read_socket_addr()?, + last_seen: timestamp, + }; + let address_from = MetaAddr { + services: Services(reader.read_u64::()?), + addr: reader.read_socket_addr()?, + last_seen: timestamp, + }; + + let nonce = Nonce(reader.read_u64::()?); + let user_agent = reader.read_string()?; + let start_height = BlockHeight(reader.read_u32::()?); + let relay = match reader.read_u8()? { + 0 => false, + 1 => true, + _ => return Err(SerializationError::ParseError("non-bool value")), + }; + + Ok(Message::Version { + version, + services, + timestamp, + address_receiving, + address_from, + nonce, + user_agent, + start_height, + relay, + }) +} + +fn try_read_verack( + mut _reader: R, + _version: Version, +) -> Result { + unimplemented!() +} + +fn try_read_ping( + mut _reader: R, + _version: Version, +) -> Result { + unimplemented!() +} + +fn try_read_pong( + mut _reader: R, + _version: Version, +) -> Result { + unimplemented!() +} + +fn try_read_reject( + mut _reader: R, + _version: Version, +) -> Result { + unimplemented!() +} + +fn try_read_addr( + mut _reader: R, + _version: Version, +) -> Result { + unimplemented!() +} + +fn try_read_getaddr( + mut _reader: R, + _version: Version, +) -> Result { + unimplemented!() +} + +fn try_read_block( + mut _reader: R, + _version: Version, +) -> Result { + unimplemented!() +} + +fn try_read_getblocks( + mut _reader: R, + _version: Version, +) -> Result { + unimplemented!() +} + +fn try_read_headers( + mut _reader: R, + _version: Version, +) -> Result { + unimplemented!() +} + +fn try_read_getheaders( + mut _reader: R, + _version: Version, +) -> Result { + unimplemented!() +} + +fn try_read_inv( + mut _reader: R, + _version: Version, +) -> Result { + unimplemented!() +} + +fn try_read_getdata( + mut _reader: R, + _version: Version, +) -> Result { + unimplemented!() +} + +fn try_read_notfound( + mut _reader: R, + _version: Version, +) -> Result { + unimplemented!() +} + +fn try_read_tx( + mut _reader: R, + _version: Version, +) -> Result { + unimplemented!() +} + +fn try_read_mempool( + mut _reader: R, + _version: Version, +) -> Result { + unimplemented!() +} + +fn try_read_filterload( + mut _reader: R, + _version: Version, +) -> Result { + unimplemented!() +} + +fn try_read_filteradd( + mut _reader: R, + _version: Version, +) -> Result { + unimplemented!() +} + +fn try_read_filterclear( + mut _reader: R, + _version: Version, +) -> Result { + unimplemented!() +} + +fn try_read_merkleblock( + mut _reader: R, + _version: Version, +) -> Result { + unimplemented!() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn version_message_round_trip() { + use std::net::{IpAddr, Ipv4Addr, SocketAddr}; + let services = Services(0x1); + let timestamp = Utc.timestamp(1568000000, 0); + + let v = Message::Version { + version: crate::constants::CURRENT_VERSION, + services, + timestamp, + // XXX maybe better to have Version keep only (Services, SocketAddr) + address_receiving: MetaAddr { + services, + addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 6)), 8233), + last_seen: timestamp, + }, + address_from: MetaAddr { + services, + addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 6)), 8233), + last_seen: timestamp, + }, + nonce: Nonce(0x9082_4908_8927_9238), + user_agent: "Zebra".to_owned(), + start_height: BlockHeight(540_000), + relay: true, + }; + + use std::io::Cursor; + + let v_bytes = { + let mut bytes = Vec::new(); + v.zcash_serialize( + Cursor::new(&mut bytes), + crate::constants::magics::MAINNET, + crate::constants::CURRENT_VERSION, + ); + bytes + }; + + let v_parsed = Message::zcash_deserialize( + Cursor::new(&v_bytes), + crate::constants::magics::MAINNET, + crate::constants::CURRENT_VERSION, + ) + .expect("message should parse successfully"); + + assert_eq!(v, v_parsed); } }