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 if non_finalized_state_change.capacity() == 0 {
139 span.in_scope(|| {
140 tracing::warn!(
141 "slow consumer, dropping non_finalized_state_change stream after \
142 buffer filled"
143 );
144 });
145 return;
146 }
147
148 let Some((hash, block)) = non_finalized_state_change.recv().await else {
149 break;
150 };
151
152 let send = response_sender.send(Ok(BlockAndHash::new(hash, block)));
153 match tokio::time::timeout(SEND_TIMEOUT, send).await {
154 Ok(Ok(())) => {}
155 Ok(Err(_)) => {
156 span.in_scope(|| {
157 tracing::info!(
158 "client disconnected, dropping non_finalized_state_change task"
159 );
160 });
161 return;
162 }
163 Err(_) => {
164 span.in_scope(|| {
165 tracing::warn!(
166 "slow consumer, dropping non_finalized_state_change stream after \
167 send timed out"
168 );
169 });
170 return;
171 }
172 }
173 }
174
175 span.in_scope(|| {
176 tracing::warn!("non-finalized state change channel has closed");
177 });
178
179 let _ = response_sender
180 .send(Err(Status::unavailable(
181 "non-finalized state change channel has closed",
182 )))
183 .await;
184 });
185
186 Ok(Response::new(Box::pin(response_stream)))
187 }
188
189 async fn mempool_change(
190 &self,
191 _: tonic::Request<Empty>,
192 ) -> Result<Response<Self::MempoolChangeStream>, Status> {
193 let span = Span::current();
194 let (response_sender, response_receiver) = tokio::sync::mpsc::channel(RESPONSE_BUFFER_SIZE);
195 let response_stream = ReceiverStream::new(response_receiver);
196 let mut mempool_change = self.mempool_change.subscribe();
197
198 tokio::spawn(async move {
199 while let Ok(change) = mempool_change.recv().await {
201 for tx_id in change.tx_ids() {
202 span.in_scope(|| {
203 tracing::debug!("mempool change: {:?}", change);
204 });
205
206 let msg = Ok(MempoolChangeMessage {
207 change_type: match change.kind() {
208 MempoolChangeKind::Added => 0,
209 MempoolChangeKind::Invalidated => 1,
210 MempoolChangeKind::Mined => 2,
211 },
212 tx_hash: tx_id.mined_id().bytes_in_display_order().to_vec(),
213 auth_digest: tx_id
214 .auth_digest()
215 .map(|d| d.bytes_in_display_order().to_vec())
216 .unwrap_or_default(),
217 });
218
219 let send = response_sender.send(msg);
220 match tokio::time::timeout(SEND_TIMEOUT, send).await {
221 Ok(Ok(())) => {}
222 Ok(Err(_)) => {
223 span.in_scope(|| {
224 tracing::info!("client disconnected, dropping mempool_change task");
225 });
226 return;
227 }
228 Err(_) => {
229 span.in_scope(|| {
230 tracing::warn!(
231 "slow consumer, dropping mempool_change stream after \
232 send timed out"
233 );
234 });
235 return;
236 }
237 }
238 }
239 }
240
241 span.in_scope(|| {
242 tracing::warn!("mempool_change channel has closed");
243 });
244
245 let _ = response_sender
246 .send(Err(Status::unavailable(
247 "mempool_change channel has closed",
248 )))
249 .await;
250 });
251
252 Ok(Response::new(Box::pin(response_stream)))
253 }
254
255 async fn get_block(
256 &self,
257 request: tonic::Request<BlockRequest>,
258 ) -> Result<Response<BlockAndHash>, Status> {
259 let hash_or_height = request.into_inner().hash_or_height;
262 let hash_or_height = match hash_or_height.len() {
263 32 => zebra_state::HashOrHeight::Hash(hash_from_display_bytes(hash_or_height)?),
264 4 => {
265 let height = u32::from_be_bytes(
266 hash_or_height
267 .try_into()
268 .expect("a 4-byte vec always converts to a [u8; 4]"),
269 );
270 let height = block::Height::try_from(height).map_err(|_| {
271 Status::invalid_argument(format!("block height out of range: {height}"))
272 })?;
273 zebra_state::HashOrHeight::Height(height)
274 }
275 len => {
276 return Err(Status::invalid_argument(format!(
277 "block request must be a 32-byte hash or a 4-byte height, got {len} bytes"
278 )));
279 }
280 };
281
282 match self
283 .read_state
284 .clone()
285 .oneshot(ReadRequest::Block(hash_or_height))
286 .await
287 {
288 Ok(ReadResponse::Block(Some(block))) => {
289 Ok(Response::new(BlockAndHash::new(block.hash(), block)))
290 }
291 Ok(ReadResponse::Block(None)) => Err(Status::not_found("block not found")),
292 Ok(_) => unreachable!("unexpected response type from ReadStateService"),
293 Err(error) => Err(Status::unavailable(format!(
294 "failed to read block: {error}"
295 ))),
296 }
297 }
298}
299
300fn decode_known_chain_tips(chain_tip_hashes: Vec<Vec<u8>>) -> Result<HashSet<block::Hash>, Status> {
312 if chain_tip_hashes.len() > MAX_NON_FINALIZED_CHAIN_FORKS {
316 return Err(Status::invalid_argument(format!(
317 "too many chain tip hashes: got {}, expected at most {MAX_NON_FINALIZED_CHAIN_FORKS}",
318 chain_tip_hashes.len(),
319 )));
320 }
321
322 chain_tip_hashes
323 .into_iter()
324 .map(hash_from_display_bytes)
325 .collect()
326}
327
328fn hash_from_display_bytes(hash: Vec<u8>) -> Result<block::Hash, Status> {
330 let bytes: [u8; 32] = hash.try_into().map_err(|hash: Vec<u8>| {
331 Status::invalid_argument(format!(
332 "invalid block hash length: expected 32 bytes, got {}",
333 hash.len()
334 ))
335 })?;
336
337 Ok(block::Hash::from_bytes_in_display_order(&bytes))
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343 use tonic::Code;
344
345 fn hash(byte: u8) -> block::Hash {
346 block::Hash::from_bytes_in_display_order(&[byte; 32])
347 }
348
349 #[test]
350 fn decode_known_chain_tips_round_trips_display_order() {
351 let hashes = [hash(1), hash(2), hash(3)];
352 let encoded = hashes
353 .iter()
354 .map(|h| h.bytes_in_display_order().to_vec())
355 .collect();
356
357 let decoded = decode_known_chain_tips(encoded).expect("valid hashes should decode");
358
359 assert_eq!(decoded, hashes.into_iter().collect());
360 }
361
362 #[test]
363 fn decode_known_chain_tips_accepts_empty() {
364 assert!(decode_known_chain_tips(Vec::new())
365 .expect("empty input should decode")
366 .is_empty());
367 }
368
369 #[test]
370 fn decode_known_chain_tips_dedups() {
371 let encoded = vec![
372 hash(7).bytes_in_display_order().to_vec(),
373 hash(7).bytes_in_display_order().to_vec(),
374 ];
375
376 let decoded = decode_known_chain_tips(encoded).expect("duplicate hashes should decode");
377
378 assert_eq!(decoded, std::iter::once(hash(7)).collect());
379 }
380
381 #[test]
382 fn decode_known_chain_tips_rejects_wrong_length() {
383 let status = decode_known_chain_tips(vec![vec![0; 31]])
384 .expect_err("a 31-byte hash should be rejected");
385
386 assert_eq!(status.code(), Code::InvalidArgument);
387 }
388
389 #[test]
390 fn decode_known_chain_tips_rejects_too_many() {
391 let encoded = (0..=MAX_NON_FINALIZED_CHAIN_FORKS as u8)
392 .map(|b| hash(b).bytes_in_display_order().to_vec())
393 .collect();
394
395 let status = decode_known_chain_tips(encoded)
396 .expect_err("more than MAX_NON_FINALIZED_CHAIN_FORKS hashes should be rejected");
397
398 assert_eq!(status.code(), Code::InvalidArgument);
399 }
400}