Skip to main content

zebra_rpc/indexer/
methods.rs

1//! Implements `Indexer` methods on the `IndexerRPC` type
2
3use 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
20/// The maximum number of messages that can be queued to be streamed to a client.
21const RESPONSE_BUFFER_SIZE: usize = 64;
22
23/// How long to wait for a backpressured send before treating the consumer as hung and dropping
24/// the subscription.
25///
26/// All three indexer streams apply backpressure so a slow consumer doesn't miss notifications,
27/// but without a bound a consumer whose connection is half-open (dead TCP not yet detected) would
28/// block the listener task indefinitely.
29const 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            // Notify the client of chain tip changes until the channel is closed
55            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        // The caller may provide the hashes of the chain tips it already has so the server only
106        // streams blocks after those tips. Malformed hashes (wrong length) are rejected up front.
107        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            // Notify the client of chain tip changes until the channel is closed
186            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        // The request carries a single `hash_or_height` byte string: a 32-byte block hash in
246        // display order, or a 4-byte big-endian block height. The length tells the two apart.
247        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
286/// Decodes the chain tip hashes from a [`NonFinalizedStateChangeRequest`] into a set of
287/// [`block::Hash`]es.
288///
289/// Each hash is expected to be 32 bytes in display order, matching the encoding used when the
290/// server streams [`BlockAndHash`] messages back to the caller.
291///
292/// # Errors
293///
294/// Returns an [`invalid_argument`](Status::invalid_argument) status if there are more hashes than
295/// the non-finalized state can hold chains ([`MAX_NON_FINALIZED_CHAIN_FORKS`]), or if any hash is
296/// not exactly 32 bytes long.
297fn decode_known_chain_tips(chain_tip_hashes: Vec<Vec<u8>>) -> Result<HashSet<block::Hash>, Status> {
298    // The non-finalized state holds at most `MAX_NON_FINALIZED_CHAIN_FORKS` chains, so a caller can
299    // never legitimately have more chain tips than that. Bound the untrusted input up front rather
300    // than allocating a set sized by the request.
301    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
314/// Decodes a 32-byte block hash in display order, rejecting wrong-length input.
315fn 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}