1use std::{collections::HashSet, pin::Pin, time::Duration};
4
5use futures::Stream;
6use tokio_stream::wrappers::ReceiverStream;
7use tonic::{Response, Status};
8use tower::util::ServiceExt;
9
10use tracing::Span;
11use zebra_chain::{block, chain_tip::ChainTip, serialization::BytesInDisplayOrder};
12use zebra_node_services::mempool::MempoolChangeKind;
13use zebra_state::{ReadRequest, ReadResponse, ReadState, MAX_NON_FINALIZED_CHAIN_FORKS};
14
15use super::{
16 indexer_server::Indexer, server::IndexerRPC, BlockAndHash, BlockHashAndHeight, BlockRequest,
17 Empty, MempoolChangeMessage, NonFinalizedStateChangeRequest,
18};
19
20const RESPONSE_BUFFER_SIZE: usize = 64;
22
23const SEND_TIMEOUT: Duration = Duration::from_secs(60);
30
31#[tonic::async_trait]
32impl<ReadStateService, Tip> Indexer for IndexerRPC<ReadStateService, Tip>
33where
34 ReadStateService: ReadState,
35 Tip: ChainTip + Clone + Send + Sync + 'static,
36{
37 type ChainTipChangeStream =
38 Pin<Box<dyn Stream<Item = Result<BlockHashAndHeight, Status>> + Send>>;
39 type NonFinalizedStateChangeStream =
40 Pin<Box<dyn Stream<Item = Result<BlockAndHash, Status>> + Send>>;
41 type MempoolChangeStream =
42 Pin<Box<dyn Stream<Item = Result<MempoolChangeMessage, Status>> + Send>>;
43
44 async fn chain_tip_change(
45 &self,
46 _: tonic::Request<Empty>,
47 ) -> Result<Response<Self::ChainTipChangeStream>, Status> {
48 let span = Span::current();
49 let (response_sender, response_receiver) = tokio::sync::mpsc::channel(RESPONSE_BUFFER_SIZE);
50 let response_stream = ReceiverStream::new(response_receiver);
51 let mut chain_tip_change = self.chain_tip_change.clone();
52
53 tokio::spawn(async move {
54 while let Ok(()) = chain_tip_change.best_tip_changed().await {
56 let Some((tip_height, tip_hash)) = chain_tip_change.best_tip_height_and_hash()
57 else {
58 continue;
59 };
60
61 let send = response_sender.send(Ok(BlockHashAndHeight::new(tip_hash, tip_height)));
62 match tokio::time::timeout(SEND_TIMEOUT, send).await {
63 Ok(Ok(())) => {}
64 Ok(Err(_)) => {
65 span.in_scope(|| {
66 tracing::info!("client disconnected, dropping chain_tip_change task");
67 });
68 return;
69 }
70 Err(_) => {
71 span.in_scope(|| {
72 tracing::warn!(
73 "slow consumer, dropping chain_tip_change stream after \
74 send timed out"
75 );
76 });
77 return;
78 }
79 }
80 }
81
82 span.in_scope(|| {
83 tracing::warn!("chain_tip_change channel has closed");
84 });
85
86 let _ = response_sender
87 .send(Err(Status::unavailable(
88 "chain_tip_change channel has closed",
89 )))
90 .await;
91 });
92
93 Ok(Response::new(Box::pin(response_stream)))
94 }
95
96 async fn non_finalized_state_change(
97 &self,
98 request: tonic::Request<NonFinalizedStateChangeRequest>,
99 ) -> Result<Response<Self::NonFinalizedStateChangeStream>, Status> {
100 let span = Span::current();
101 let read_state = self.read_state.clone();
102 let (response_sender, response_receiver) = tokio::sync::mpsc::channel(RESPONSE_BUFFER_SIZE);
103 let response_stream = ReceiverStream::new(response_receiver);
104
105 let known_chain_tips = decode_known_chain_tips(request.into_inner().chain_tip_hashes)?;
108
109 tokio::spawn(async move {
110 let mut non_finalized_state_change = match read_state
111 .oneshot(ReadRequest::NonFinalizedBlocksListener { known_chain_tips })
112 .await
113 {
114 Ok(ReadResponse::NonFinalizedBlocksListener(listener)) => listener.unwrap(),
115 Ok(_) => unreachable!("unexpected response type from ReadStateService"),
116 Err(error) => {
117 span.in_scope(|| {
118 tracing::error!(
119 ?error,
120 "failed to subscribe to non-finalized state changes"
121 );
122 });
123
124 let _ = response_sender
125 .send(Err(Status::unavailable(
126 "failed to subscribe to non-finalized state changes",
127 )))
128 .await;
129 return;
130 }
131 };
132
133 loop {
134 let Some((hash, block)) = non_finalized_state_change.recv().await else {
135 break;
136 };
137
138 let send = response_sender.send(Ok(BlockAndHash::new(hash, block)));
139 match tokio::time::timeout(SEND_TIMEOUT, send).await {
140 Ok(Ok(())) => {}
141 Ok(Err(_)) => {
142 span.in_scope(|| {
143 tracing::info!(
144 "client disconnected, dropping non_finalized_state_change task"
145 );
146 });
147 return;
148 }
149 Err(_) => {
150 span.in_scope(|| {
151 tracing::warn!(
152 "slow consumer, dropping non_finalized_state_change stream after \
153 send timed out"
154 );
155 });
156 return;
157 }
158 }
159 }
160
161 span.in_scope(|| {
162 tracing::warn!("non-finalized state change channel has closed");
163 });
164
165 let _ = response_sender
166 .send(Err(Status::unavailable(
167 "non-finalized state change channel has closed",
168 )))
169 .await;
170 });
171
172 Ok(Response::new(Box::pin(response_stream)))
173 }
174
175 async fn mempool_change(
176 &self,
177 _: tonic::Request<Empty>,
178 ) -> Result<Response<Self::MempoolChangeStream>, Status> {
179 let span = Span::current();
180 let (response_sender, response_receiver) = tokio::sync::mpsc::channel(RESPONSE_BUFFER_SIZE);
181 let response_stream = ReceiverStream::new(response_receiver);
182 let mut mempool_change = self.mempool_change.subscribe();
183
184 tokio::spawn(async move {
185 while let Ok(change) = mempool_change.recv().await {
187 for tx_id in change.tx_ids() {
188 span.in_scope(|| {
189 tracing::debug!("mempool change: {:?}", change);
190 });
191
192 let msg = Ok(MempoolChangeMessage {
193 change_type: match change.kind() {
194 MempoolChangeKind::Added => 0,
195 MempoolChangeKind::Invalidated => 1,
196 MempoolChangeKind::Mined => 2,
197 },
198 tx_hash: tx_id.mined_id().bytes_in_display_order().to_vec(),
199 auth_digest: tx_id
200 .auth_digest()
201 .map(|d| d.bytes_in_display_order().to_vec())
202 .unwrap_or_default(),
203 });
204
205 let send = response_sender.send(msg);
206 match tokio::time::timeout(SEND_TIMEOUT, send).await {
207 Ok(Ok(())) => {}
208 Ok(Err(_)) => {
209 span.in_scope(|| {
210 tracing::info!("client disconnected, dropping mempool_change task");
211 });
212 return;
213 }
214 Err(_) => {
215 span.in_scope(|| {
216 tracing::warn!(
217 "slow consumer, dropping mempool_change stream after \
218 send timed out"
219 );
220 });
221 return;
222 }
223 }
224 }
225 }
226
227 span.in_scope(|| {
228 tracing::warn!("mempool_change channel has closed");
229 });
230
231 let _ = response_sender
232 .send(Err(Status::unavailable(
233 "mempool_change channel has closed",
234 )))
235 .await;
236 });
237
238 Ok(Response::new(Box::pin(response_stream)))
239 }
240
241 async fn get_block(
242 &self,
243 request: tonic::Request<BlockRequest>,
244 ) -> Result<Response<BlockAndHash>, Status> {
245 let hash_or_height = request.into_inner().hash_or_height;
248 let hash_or_height = match hash_or_height.len() {
249 32 => zebra_state::HashOrHeight::Hash(hash_from_display_bytes(hash_or_height)?),
250 4 => {
251 let height = u32::from_be_bytes(
252 hash_or_height
253 .try_into()
254 .expect("a 4-byte vec always converts to a [u8; 4]"),
255 );
256 let height = block::Height::try_from(height).map_err(|_| {
257 Status::invalid_argument(format!("block height out of range: {height}"))
258 })?;
259 zebra_state::HashOrHeight::Height(height)
260 }
261 len => {
262 return Err(Status::invalid_argument(format!(
263 "block request must be a 32-byte hash or a 4-byte height, got {len} bytes"
264 )));
265 }
266 };
267
268 match self
269 .read_state
270 .clone()
271 .oneshot(ReadRequest::Block(hash_or_height))
272 .await
273 {
274 Ok(ReadResponse::Block(Some(block))) => {
275 Ok(Response::new(BlockAndHash::new(block.hash(), block)))
276 }
277 Ok(ReadResponse::Block(None)) => Err(Status::not_found("block not found")),
278 Ok(_) => unreachable!("unexpected response type from ReadStateService"),
279 Err(error) => Err(Status::unavailable(format!(
280 "failed to read block: {error}"
281 ))),
282 }
283 }
284}
285
286fn decode_known_chain_tips(chain_tip_hashes: Vec<Vec<u8>>) -> Result<HashSet<block::Hash>, Status> {
298 if chain_tip_hashes.len() > MAX_NON_FINALIZED_CHAIN_FORKS {
302 return Err(Status::invalid_argument(format!(
303 "too many chain tip hashes: got {}, expected at most {MAX_NON_FINALIZED_CHAIN_FORKS}",
304 chain_tip_hashes.len(),
305 )));
306 }
307
308 chain_tip_hashes
309 .into_iter()
310 .map(hash_from_display_bytes)
311 .collect()
312}
313
314fn hash_from_display_bytes(hash: Vec<u8>) -> Result<block::Hash, Status> {
316 let bytes: [u8; 32] = hash.try_into().map_err(|hash: Vec<u8>| {
317 Status::invalid_argument(format!(
318 "invalid block hash length: expected 32 bytes, got {}",
319 hash.len()
320 ))
321 })?;
322
323 Ok(block::Hash::from_bytes_in_display_order(&bytes))
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329 use tonic::Code;
330
331 fn hash(byte: u8) -> block::Hash {
332 block::Hash::from_bytes_in_display_order(&[byte; 32])
333 }
334
335 #[test]
336 fn decode_known_chain_tips_round_trips_display_order() {
337 let hashes = [hash(1), hash(2), hash(3)];
338 let encoded = hashes
339 .iter()
340 .map(|h| h.bytes_in_display_order().to_vec())
341 .collect();
342
343 let decoded = decode_known_chain_tips(encoded).expect("valid hashes should decode");
344
345 assert_eq!(decoded, hashes.into_iter().collect());
346 }
347
348 #[test]
349 fn decode_known_chain_tips_accepts_empty() {
350 assert!(decode_known_chain_tips(Vec::new())
351 .expect("empty input should decode")
352 .is_empty());
353 }
354
355 #[test]
356 fn decode_known_chain_tips_dedups() {
357 let encoded = vec![
358 hash(7).bytes_in_display_order().to_vec(),
359 hash(7).bytes_in_display_order().to_vec(),
360 ];
361
362 let decoded = decode_known_chain_tips(encoded).expect("duplicate hashes should decode");
363
364 assert_eq!(decoded, std::iter::once(hash(7)).collect());
365 }
366
367 #[test]
368 fn decode_known_chain_tips_rejects_wrong_length() {
369 let status = decode_known_chain_tips(vec![vec![0; 31]])
370 .expect_err("a 31-byte hash should be rejected");
371
372 assert_eq!(status.code(), Code::InvalidArgument);
373 }
374
375 #[test]
376 fn decode_known_chain_tips_rejects_too_many() {
377 let encoded = (0..=MAX_NON_FINALIZED_CHAIN_FORKS as u8)
378 .map(|b| hash(b).bytes_in_display_order().to_vec())
379 .collect();
380
381 let status = decode_known_chain_tips(encoded)
382 .expect_err("more than MAX_NON_FINALIZED_CHAIN_FORKS hashes should be rejected");
383
384 assert_eq!(status.code(), Code::InvalidArgument);
385 }
386}