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}