zebra_network/protocol/internal/
response.rs

1//! Zebra's internal peer message response format.
2
3use std::{fmt, sync::Arc, time::Duration};
4
5use zebra_chain::{
6    block::{self, Block},
7    transaction::{UnminedTx, UnminedTxId},
8};
9
10use crate::{meta_addr::MetaAddr, protocol::internal::InventoryResponse, PeerSocketAddr};
11
12#[cfg(any(test, feature = "proptest-impl"))]
13use proptest_derive::Arbitrary;
14
15use InventoryResponse::*;
16
17/// A response to a network request, represented in internal format.
18#[derive(Clone, Debug, Eq, PartialEq)]
19#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))]
20pub enum Response {
21    /// The request does not have a response.
22    ///
23    /// Either:
24    ///  * the request does not need a response, or
25    ///  * we have no useful data to provide in response to the request,
26    ///    and the request was not an inventory request.
27    ///
28    /// (Inventory requests provide a list of missing hashes if none of the hashes were available.)
29    Nil,
30
31    /// A list of peers, used to respond to `GetPeers`.
32    ///
33    /// The list contains `0..=MAX_META_ADDR` peers.
34    //
35    // TODO: make this into a HashMap<PeerSocketAddr, MetaAddr> - a unique list of peer addresses (#2244)
36    Peers(Vec<MetaAddr>),
37
38    /// A pong response containing the round-trip latency for a peer.
39    ///
40    /// Returned after a ping/pong exchange to measure RTT.
41    Pong(Duration),
42
43    /// An ordered list of block hashes.
44    ///
45    /// The list contains zero or more block hashes.
46    //
47    // TODO: make this into an IndexMap - an ordered unique list of hashes (#2244)
48    BlockHashes(Vec<block::Hash>),
49
50    /// An ordered list of block headers.
51    ///
52    /// The list contains zero or more block headers.
53    //
54    // TODO: make this into an IndexMap - an ordered unique list of headers (#2244)
55    BlockHeaders(Vec<block::CountedHeader>),
56
57    /// A list of unmined transaction IDs.
58    ///
59    /// v4 transactions use a legacy transaction ID, and
60    /// v5 transactions use a witnessed transaction ID.
61    ///
62    /// The list contains zero or more transaction IDs.
63    //
64    // TODO: make this into a HashSet - a unique list (#2244)
65    TransactionIds(Vec<UnminedTxId>),
66
67    /// A list of found blocks, and missing block hashes.
68    ///
69    /// Each list contains zero or more entries.
70    ///
71    /// When Zebra doesn't have a block or transaction, it always sends `notfound`.
72    /// `zcashd` sometimes sends no response, and sometimes sends `notfound`.
73    //
74    // TODO: make this into a HashMap<block::Hash, InventoryResponse<Arc<Block>, ()>> - a unique list (#2244)
75    Blocks(Vec<InventoryResponse<(Arc<Block>, Option<PeerSocketAddr>), block::Hash>>),
76
77    /// A list of found unmined transactions, and missing unmined transaction IDs.
78    ///
79    /// Each list contains zero or more entries.
80    //
81    // TODO: make this into a HashMap<UnminedTxId, InventoryResponse<UnminedTx, ()>> - a unique list (#2244)
82    Transactions(Vec<InventoryResponse<(UnminedTx, Option<PeerSocketAddr>), UnminedTxId>>),
83}
84
85impl fmt::Display for Response {
86    #[allow(clippy::unwrap_in_result)]
87    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
88        f.write_str(&match self {
89            Response::Nil => "Nil".to_string(),
90
91            Response::Peers(peers) => format!("Peers {{ peers: {} }}", peers.len()),
92
93            Response::Pong(duration) => format!("Pong {{ latency: {:?} }}", duration),
94
95            Response::BlockHashes(hashes) => format!("BlockHashes {{ hashes: {} }}", hashes.len()),
96            Response::BlockHeaders(headers) => {
97                format!("BlockHeaders {{ headers: {} }}", headers.len())
98            }
99            Response::TransactionIds(ids) => format!("TransactionIds {{ ids: {} }}", ids.len()),
100
101            // Display heights for single-block responses (which Zebra requests and expects)
102            Response::Blocks(blocks) if blocks.len() == 1 => {
103                match blocks.first().expect("len is 1") {
104                    Available((block, _)) => format!(
105                        "Block {{ height: {}, hash: {} }}",
106                        block
107                            .coinbase_height()
108                            .as_ref()
109                            .map(|h| h.0.to_string())
110                            .unwrap_or_else(|| "None".into()),
111                        block.hash(),
112                    ),
113                    Missing(hash) => format!("Block {{ missing: {hash} }}"),
114                }
115            }
116            Response::Blocks(blocks) => format!(
117                "Blocks {{ blocks: {}, missing: {} }}",
118                blocks.iter().filter(|r| r.is_available()).count(),
119                blocks.iter().filter(|r| r.is_missing()).count()
120            ),
121
122            Response::Transactions(transactions) => format!(
123                "Transactions {{ transactions: {}, missing: {} }}",
124                transactions.iter().filter(|r| r.is_available()).count(),
125                transactions.iter().filter(|r| r.is_missing()).count()
126            ),
127        })
128    }
129}
130
131impl Response {
132    /// Returns the Zebra internal response type as a string.
133    pub fn command(&self) -> &'static str {
134        match self {
135            Response::Nil => "Nil",
136
137            Response::Peers(_) => "Peers",
138
139            Response::Pong(_) => "Pong",
140
141            Response::BlockHashes(_) => "BlockHashes",
142            Response::BlockHeaders(_) => "BlockHeaders",
143            Response::TransactionIds(_) => "TransactionIds",
144
145            Response::Blocks(_) => "Blocks",
146            Response::Transactions(_) => "Transactions",
147        }
148    }
149
150    /// Returns true if the response is a block or transaction inventory download.
151    pub fn is_inventory_download(&self) -> bool {
152        matches!(self, Response::Blocks(_) | Response::Transactions(_))
153    }
154
155    /// Returns true if self is the [`Response::Nil`] variant.
156    #[allow(dead_code)]
157    pub fn is_nil(&self) -> bool {
158        matches!(self, Self::Nil)
159    }
160}