diff --git a/zebra-chain/src/block.rs b/zebra-chain/src/block.rs index be009544c..5115ccdfc 100644 --- a/zebra-chain/src/block.rs +++ b/zebra-chain/src/block.rs @@ -24,7 +24,7 @@ pub use root_hash::RootHash; use serde::{Deserialize, Serialize}; -use crate::{parameters::Network, transaction::Transaction, transparent}; +use crate::{fmt::DisplayToDebug, parameters::Network, transaction::Transaction, transparent}; /// A Zcash block, containing a header and a list of transactions. #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] @@ -46,17 +46,6 @@ impl fmt::Display for Block { } } -struct DisplayToDebug(T); - -impl fmt::Debug for DisplayToDebug -where - T: fmt::Display, -{ - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) - } -} - impl Block { /// Return the block height reported in the coinbase transaction, if any. pub fn coinbase_height(&self) -> Option { diff --git a/zebra-chain/src/fmt.rs b/zebra-chain/src/fmt.rs new file mode 100644 index 000000000..36f60e5eb --- /dev/null +++ b/zebra-chain/src/fmt.rs @@ -0,0 +1,28 @@ +//! Format wrappers for Zebra + +use std::fmt; + +pub struct DisplayToDebug(pub T); + +impl fmt::Debug for DisplayToDebug +where + T: fmt::Display, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +pub struct SummaryDebug(pub T); + +impl fmt::Debug for SummaryDebug> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}, len={}", std::any::type_name::(), self.0.len()) + } +} + +impl fmt::Debug for SummaryDebug<&Vec> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}, len={}", std::any::type_name::(), self.0.len()) + } +} diff --git a/zebra-chain/src/lib.rs b/zebra-chain/src/lib.rs index c35a8afde..a7c269fdc 100644 --- a/zebra-chain/src/lib.rs +++ b/zebra-chain/src/lib.rs @@ -16,6 +16,7 @@ extern crate serde; pub mod amount; pub mod block; +pub mod fmt; pub mod parameters; pub mod primitives; pub mod sapling; diff --git a/zebra-network/src/protocol/external/message.rs b/zebra-network/src/protocol/external/message.rs index e0e035460..fdbd5e897 100644 --- a/zebra-network/src/protocol/external/message.rs +++ b/zebra-network/src/protocol/external/message.rs @@ -1,12 +1,13 @@ //! Definitions of network messages. use std::error::Error; -use std::{net, sync::Arc}; +use std::{fmt, net, sync::Arc}; use chrono::{DateTime, Utc}; use zebra_chain::{ block::{self, Block}, + fmt::{DisplayToDebug, SummaryDebug}, transaction::Transaction, }; @@ -30,7 +31,7 @@ use crate::meta_addr::MetaAddr; /// during serialization). /// /// [btc_wiki_protocol]: https://en.bitcoin.it/wiki/Protocol_documentation -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Eq, PartialEq)] pub enum Message { /// A `version` message. /// @@ -307,3 +308,93 @@ pub enum RejectReason { Checkpoint = 0x43, Other = 0x50, } + +/// Summarise `Vec`s when debugging messages +impl fmt::Debug for Message { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Message::Version { + version, + services, + timestamp, + address_recv, + address_from, + nonce, + user_agent, + start_height, + relay, + } => f + .debug_struct("Version") + .field("version", version) + .field("services", services) + .field("timestamp", timestamp) + .field("address_recv", address_recv) + .field("address_from", address_from) + .field("nonce", nonce) + .field("user_agent", user_agent) + .field("start_height", start_height) + .field("relay", relay) + .finish(), + Message::Verack => f.debug_tuple("Verack").finish(), + Message::Ping(nonce) => f.debug_tuple("Ping").field(nonce).finish(), + Message::Pong(nonce) => f.debug_tuple("Pong").field(nonce).finish(), + Message::Reject { + message, + ccode, + reason, + data, + } => f + .debug_struct("Reject") + .field("message", message) + .field("ccode", ccode) + .field("reason", reason) + .field("data", data) + .finish(), + Message::GetAddr => f.debug_tuple("GetAddr").finish(), + Message::Addr(addr) => f.debug_tuple("Addr").field(&SummaryDebug(addr)).finish(), + Message::GetBlocks { known_blocks, stop } => f + .debug_struct("GetBlocks") + .field("known_blocks", &SummaryDebug(known_blocks)) + .field("stop", stop) + .finish(), + Message::Inv(inv) => f.debug_tuple("Inv").field(&SummaryDebug(inv)).finish(), + Message::GetHeaders { known_blocks, stop } => f + .debug_struct("GetHeaders") + .field("known_blocks", &SummaryDebug(known_blocks)) + .field("stop", stop) + .finish(), + Message::Headers(headers) => f + .debug_tuple("Headers") + .field(&SummaryDebug(headers)) + .finish(), + Message::GetData(data) => f.debug_tuple("GetData").field(&SummaryDebug(data)).finish(), + Message::Block(block) => f + .debug_tuple("Block") + .field(&DisplayToDebug(block)) + .finish(), + Message::Tx(tx) => f.debug_tuple("Tx").field(&tx).finish(), + Message::NotFound(not_found) => f + .debug_tuple("NotFound") + .field(&SummaryDebug(not_found)) + .finish(), + Message::Mempool => f.debug_tuple("Mempool").finish(), + Message::FilterLoad { + filter, + hash_functions_count, + tweak, + flags, + } => f + .debug_struct("FilterLoad") + .field("filter", filter) + .field("hash_functions_count", hash_functions_count) + .field("tweak", tweak) + .field("flags", flags) + .finish(), + Message::FilterAdd { data } => f + .debug_struct("FilterAdd") + .field("data", &SummaryDebug(data)) + .finish(), + Message::FilterClear => f.debug_tuple("FilterClear").finish(), + } + } +} diff --git a/zebra-state/src/request.rs b/zebra-state/src/request.rs index 8f9fa3974..6bfb28aca 100644 --- a/zebra-state/src/request.rs +++ b/zebra-state/src/request.rs @@ -239,4 +239,50 @@ pub enum Request { /// Code making this request should apply a timeout layer to the service to /// handle missing UTXOs. AwaitUtxo(transparent::OutPoint), + + /// Finds the first hash that's in the peer's `known_blocks` and the local best chain. + /// Returns a list of hashes that follow that intersection, from the best chain. + /// + /// If there is no matching hash in the best chain, starts from the genesis hash. + /// + /// Stops the list of hashes after: + /// * adding the best tip, + /// * adding the `stop` hash to the list, if it is in the best chain, or + /// * adding 500 hashes to the list. + /// + /// Returns an empty list if the state is empty. + /// + /// Returns + /// + /// [`Response::BlockHashes(Vec)`](Response::BlockHashes). + /// See https://en.bitcoin.it/wiki/Protocol_documentation#getblocks + FindBlockHashes { + /// Hashes of known blocks, ordered from highest height to lowest height. + known_blocks: Vec, + /// Optionally, the last block hash to request. + stop: Option, + }, + + /// Finds the first hash that's in the peer's `known_blocks` and the local best chain. + /// Returns a list of headers that follow that intersection, from the best chain. + /// + /// If there is no matching hash in the best chain, starts from the genesis header. + /// + /// Stops the list of headers after: + /// * adding the best tip, + /// * adding the header matching the `stop` hash to the list, if it is in the best chain, or + /// * adding 160 headers to the list. + /// + /// Returns an empty list if the state is empty. + /// + /// Returns + /// + /// [`Response::BlockHeaders(Vec)`](Response::BlockHeaders). + /// See https://en.bitcoin.it/wiki/Protocol_documentation#getheaders + FindBlockHeaders { + /// Hashes of known blocks, ordered from highest height to lowest height. + known_blocks: Vec, + /// Optionally, the hash of the last header to request. + stop: Option, + }, } diff --git a/zebra-state/src/response.rs b/zebra-state/src/response.rs index fd6df1af1..c8d51737b 100644 --- a/zebra-state/src/response.rs +++ b/zebra-state/src/response.rs @@ -33,6 +33,12 @@ pub enum Response { /// Response to [`Request::Block`] with the specified block. Block(Option>), - /// The response to a `AwaitUtxo` request + /// The response to a `AwaitUtxo` request. Utxo(Utxo), + + /// The response to a `FindBlockHashes` request. + BlockHashes(Vec), + + /// The response to a `FindBlockHeaders` request. + BlockHeaders(Vec), } diff --git a/zebra-state/src/service.rs b/zebra-state/src/service.rs index 8470ebbd7..6c5392b03 100644 --- a/zebra-state/src/service.rs +++ b/zebra-state/src/service.rs @@ -208,7 +208,7 @@ impl StateService { let mut hashes = Vec::with_capacity(heights.len()); for height in heights { - if let Some(hash) = self.hash(height) { + if let Some(hash) = self.best_hash(height) { hashes.push(hash); } } @@ -224,16 +224,19 @@ impl StateService { /// Return the depth of block `hash` in the current best chain. pub fn depth(&self, hash: block::Hash) -> Option { let tip = self.tip()?.0; - let height = self.mem.height(hash).or_else(|| self.disk.height(hash))?; + let height = self + .mem + .best_height_by_hash(hash) + .or_else(|| self.disk.height(hash))?; Some(tip.0 - height.0) } /// Return the block identified by either its `height` or `hash` if it exists /// in the current best chain. - pub fn block(&self, hash_or_height: HashOrHeight) -> Option> { + pub fn best_block(&self, hash_or_height: HashOrHeight) -> Option> { self.mem - .block(hash_or_height) + .best_block(hash_or_height) .or_else(|| self.disk.block(hash_or_height)) } @@ -246,14 +249,29 @@ impl StateService { } /// Return the hash for the block at `height` in the current best chain. - pub fn hash(&self, height: block::Height) -> Option { - self.mem.hash(height).or_else(|| self.disk.hash(height)) + pub fn best_hash(&self, height: block::Height) -> Option { + self.mem + .best_hash(height) + .or_else(|| self.disk.hash(height)) + } + + /// Return true if `hash` is in the current best chain. + pub fn best_chain_contains(&self, hash: block::Hash) -> bool { + self.best_height_by_hash(hash).is_some() + } + + /// Return the height for the block at `hash`, if `hash` is in the best chain. + pub fn best_height_by_hash(&self, hash: block::Hash) -> Option { + self.mem + .best_height_by_hash(hash) + .or_else(|| self.disk.height(hash)) } /// Return the height for the block at `hash` in any chain. - pub fn height_by_hash(&self, hash: block::Hash) -> Option { + #[allow(dead_code)] + pub fn any_height_by_hash(&self, hash: block::Hash) -> Option { self.mem - .height_by_hash(hash) + .any_height_by_hash(hash) .or_else(|| self.disk.height(hash)) } @@ -276,6 +294,143 @@ impl StateService { state: IterState::NonFinalized(hash), } } + + /// Find the first hash that's in the peer's `known_blocks` and the local best chain. + /// + /// Returns `None` if: + /// * there is no matching hash in the best chain, or + /// * the state is empty. + fn find_chain_intersection(&self, known_blocks: Vec) -> Option { + // We can get a block locator request before we have downloaded the genesis block + self.tip()?; + + known_blocks + .iter() + .find(|&&hash| self.best_chain_contains(hash)) + .cloned() + } + + /// Returns a list of block hashes in the best chain, following the `intersection` with the best + /// chain. If there is no intersection with the best chain, starts from the genesis hash. + /// + /// Includes finalized and non-finalized blocks. + /// + /// Stops the list of hashes after: + /// * adding the best tip, + /// * adding the `stop` hash to the list, if it is in the best chain, or + /// * adding `max_len` hashes to the list. + /// + /// Returns an empty list if the state is empty. + pub fn collect_chain_hashes( + &self, + intersection: Option, + stop: Option, + max_len: usize, + ) -> Vec { + assert!(max_len > 0, "max_len must be at least 1"); + + // We can get a block locator request before we have downloaded the genesis block + let chain_tip_height = if let Some((height, _)) = self.tip() { + height + } else { + return Vec::new(); + }; + + let intersection_height = intersection.map(|hash| { + self.best_height_by_hash(hash) + .expect("the intersection hash must be in the best chain") + }); + let max_len_height = if let Some(intersection_height) = intersection_height { + // start after the intersection_height, and return max_len hashes + (intersection_height + (max_len as i32)) + .expect("the Find response height does not exceed Height::MAX") + } else { + // start at genesis, and return max_len hashes + block::Height((max_len - 1) as _) + }; + + let stop_height = stop.map(|hash| self.best_height_by_hash(hash)).flatten(); + + // Compute the final height, making sure it is: + // * at or below our chain tip, and + // * at or below the height of the stop hash. + let final_height = std::cmp::min(max_len_height, chain_tip_height); + let final_height = stop_height + .map(|stop_height| std::cmp::min(final_height, stop_height)) + .unwrap_or(final_height); + let final_hash = self + .best_hash(final_height) + .expect("final height must have a hash"); + + // We can use an "any chain" method here, because `final_hash` is in the best chain + let mut res: Vec<_> = self + .chain(final_hash) + .map(|block| block.hash()) + .take_while(|&hash| Some(hash) != intersection) + .inspect(|hash| { + tracing::trace!( + ?hash, + height = ?self.best_height_by_hash(*hash) + .expect("if hash is in the state then it should have an associated height"), + "adding hash to peer Find response", + ) + }) + .collect(); + res.reverse(); + + tracing::info!( + ?final_height, + response_len = ?res.len(), + ?chain_tip_height, + ?stop_height, + ?intersection_height, + "responding to peer GetBlocks or GetHeaders", + ); + + // Check the function implements the Find protocol + assert!( + res.len() <= max_len, + "a Find response must not exceed the maximum response length" + ); + assert!( + intersection + .map(|hash| !res.contains(&hash)) + .unwrap_or(true), + "the list must not contain the intersection hash" + ); + assert!( + stop.map(|hash| !res[..(res.len() - 1)].contains(&hash)) + .unwrap_or(true), + "if the stop hash is in the list, it must be the final hash" + ); + + res + } + + /// Finds the first hash that's in the peer's `known_blocks` and the local best chain. + /// Returns a list of hashes that follow that intersection, from the best chain. + /// + /// Starts from the first matching hash in the best chain, ignoring all other hashes in + /// `known_blocks`. If there is no matching hash in the best chain, starts from the genesis + /// hash. + /// + /// Includes finalized and non-finalized blocks. + /// + /// Stops the list of hashes after: + /// * adding the best tip, + /// * adding the `stop` hash to the list, if it is in the best chain, or + /// * adding 500 hashes to the list. + /// + /// Returns an empty list if the state is empty. + pub fn find_chain_hashes( + &self, + known_blocks: Vec, + stop: Option, + max_len: usize, + ) -> Vec { + let intersection = self.find_chain_intersection(known_blocks); + self.collect_chain_hashes(intersection, stop, max_len) + } } struct Iter<'a> { @@ -361,7 +516,7 @@ impl ExactSizeIterator for Iter<'_> { match self.state { IterState::NonFinalized(hash) => self .service - .height_by_hash(hash) + .best_height_by_hash(hash) .map(|height| (height.0 + 1) as _) .unwrap_or(0), IterState::Finalized(height) => (height.0 + 1) as _, @@ -463,7 +618,7 @@ impl Service for StateService { } Request::Block(hash_or_height) => { metrics::counter!("state.requests", 1, "type" => "block"); - let rsp = Ok(self.block(hash_or_height)).map(Response::Block); + let rsp = Ok(self.best_block(hash_or_height)).map(Response::Block); async move { rsp }.boxed() } Request::AwaitUtxo(outpoint) => { @@ -477,6 +632,25 @@ impl Service for StateService { fut.boxed() } + Request::FindBlockHashes { known_blocks, stop } => { + const MAX_FIND_BLOCK_HASHES_RESULTS: usize = 500; + let res = self.find_chain_hashes(known_blocks, stop, MAX_FIND_BLOCK_HASHES_RESULTS); + async move { Ok(Response::BlockHashes(res)) }.boxed() + } + Request::FindBlockHeaders { known_blocks, stop } => { + const MAX_FIND_BLOCK_HEADERS_RESULTS: usize = 160; + let res = + self.find_chain_hashes(known_blocks, stop, MAX_FIND_BLOCK_HEADERS_RESULTS); + let res: Vec<_> = res + .iter() + .map(|&hash| { + self.best_block(hash.into()) + .expect("block for found hash is in the best chain") + .header + }) + .collect(); + async move { Ok(Response::BlockHeaders(res)) }.boxed() + } } } } diff --git a/zebra-state/src/service/non_finalized_state.rs b/zebra-state/src/service/non_finalized_state.rs index 5b1fdc750..7829e24fe 100644 --- a/zebra-state/src/service/non_finalized_state.rs +++ b/zebra-state/src/service/non_finalized_state.rs @@ -169,7 +169,7 @@ impl NonFinalizedState { } /// Returns the `block` at a given height or hash in the best chain. - pub fn block(&self, hash_or_height: HashOrHeight) -> Option> { + pub fn best_block(&self, hash_or_height: HashOrHeight) -> Option> { let best_chain = self.best_chain()?; let height = hash_or_height.height_or_else(|hash| best_chain.height_by_hash.get(&hash).cloned())?; @@ -181,8 +181,11 @@ impl NonFinalizedState { } /// Returns the hash for a given `block::Height` if it is present in the best chain. - pub fn hash(&self, height: block::Height) -> Option { - self.block(height.into()).map(|block| block.hash()) + pub fn best_hash(&self, height: block::Height) -> Option { + self.best_chain()? + .blocks + .get(&height) + .map(|prepared| prepared.hash) } /// Returns the tip of the best chain. @@ -194,15 +197,15 @@ impl NonFinalizedState { Some((height, hash)) } - /// Returns the depth of `hash` in the best chain. - pub fn height(&self, hash: block::Hash) -> Option { + /// Returns the height of `hash` in the best chain. + pub fn best_height_by_hash(&self, hash: block::Hash) -> Option { let best_chain = self.best_chain()?; let height = *best_chain.height_by_hash.get(&hash)?; Some(height) } /// Returns the height of `hash` in any chain. - pub fn height_by_hash(&self, hash: block::Hash) -> Option { + pub fn any_height_by_hash(&self, hash: block::Hash) -> Option { for chain in self.chain_set.iter().rev() { if let Some(height) = chain.height_by_hash.get(&hash) { return Some(*height); diff --git a/zebra-state/src/service/non_finalized_state/chain.rs b/zebra-state/src/service/non_finalized_state/chain.rs index 5ea43426a..39c0e26c8 100644 --- a/zebra-state/src/service/non_finalized_state/chain.rs +++ b/zebra-state/src/service/non_finalized_state/chain.rs @@ -385,12 +385,13 @@ impl Ord for Chain { #[cfg(test)] mod tests { - use std::{env, fmt, sync::Arc}; + use std::{env, sync::Arc}; - use zebra_chain::serialization::ZcashDeserializeInto; use zebra_chain::{ block::Block, + fmt::SummaryDebug, parameters::{Network, NetworkUpgrade}, + serialization::ZcashDeserializeInto, LedgerState, }; use zebra_test::prelude::*; @@ -400,14 +401,6 @@ mod tests { use self::assert_eq; use super::*; - struct SummaryDebug(T); - - impl fmt::Debug for SummaryDebug> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}, len={}", std::any::type_name::(), self.0.len()) - } - } - #[test] fn construct_empty() { zebra_test::init(); diff --git a/zebrad/src/components/inbound.rs b/zebrad/src/components/inbound.rs index e190cbbd2..416d56c32 100644 --- a/zebrad/src/components/inbound.rs +++ b/zebrad/src/components/inbound.rs @@ -180,13 +180,23 @@ impl Service for Inbound { debug!("ignoring unimplemented request"); async { Ok(zn::Response::Nil) }.boxed() } - zn::Request::FindBlocks { .. } => { - debug!("ignoring unimplemented request"); - async { Ok(zn::Response::Nil) }.boxed() + zn::Request::FindBlocks { known_blocks, stop } => { + let request = zs::Request::FindBlockHashes { known_blocks, stop }; + self.state.call(request).map_ok(|resp| match resp { + zs::Response::BlockHashes(hashes) if hashes.is_empty() => zn::Response::Nil, + zs::Response::BlockHashes(hashes) => zn::Response::BlockHashes(hashes), + _ => unreachable!("zebra-state should always respond to a `FindBlockHashes` request with a `BlockHashes` response"), + }) + .boxed() } - zn::Request::FindHeaders { .. } => { - debug!("ignoring unimplemented request"); - async { Ok(zn::Response::Nil) }.boxed() + zn::Request::FindHeaders { known_blocks, stop } => { + let request = zs::Request::FindBlockHeaders { known_blocks, stop }; + self.state.call(request).map_ok(|resp| match resp { + zs::Response::BlockHeaders(headers) if headers.is_empty() => zn::Response::Nil, + zs::Response::BlockHeaders(headers) => zn::Response::BlockHeaders(headers), + _ => unreachable!("zebra-state should always respond to a `FindBlockHeaders` request with a `BlockHeaders` response"), + }) + .boxed() } zn::Request::PushTransaction(_transaction) => { debug!("ignoring unimplemented request");