1use std::{
4 collections::{HashMap, HashSet},
5 pin::Pin,
6};
7
8use futures::{future::OptionFuture, Stream};
9use tokio_stream::wrappers::ReceiverStream;
10use tonic::{Request, Response, Status, Streaming};
11use tower::util::ServiceExt;
12
13use zebra_chain::{
14 block,
15 chain_tip::ChainTip,
16 parameters::{Network, NetworkUpgrade},
17 serialization::{BytesInDisplayOrder, ZcashSerialize},
18 subtree::NoteCommitmentSubtreeIndex,
19 transaction, transparent,
20};
21use zebra_node_services::mempool::{self, MempoolService};
22use zebra_state::{HashOrHeight, ReadRequest, ReadResponse, ReadState};
23
24use crate::methods::{
25 trees::GetTreestateResponse, GetAddressBalanceRequest, GetAddressTxIdsRequest,
26 GetAddressUtxosRequest, GetAddressUtxosResponse, RpcServer as RpcMethods,
27 MAX_REQUEST_ADDRESSES,
28};
29
30use super::{
31 compact_tx_streamer_server::CompactTxStreamer, server::LightwalletdRPC, Address, AddressList,
32 Balance, BlockId, BlockRange, ChainSpec, CompactBlock, CompactTx, Duration, Empty, Exclude,
33 GetAddressUtxosArg, GetAddressUtxosReply, GetAddressUtxosReplyList, GetSubtreeRootsArg,
34 LightdInfo, PingResponse, RawTransaction, SendResponse, ShieldedProtocol, SubtreeRoot,
35 TransparentAddressBlockFilter, TreeState, TxFilter,
36};
37
38const RESPONSE_BUFFER_SIZE: usize = 64;
40
41const MAX_EXCLUDE_ENTRIES: usize = 20_000;
47
48const MAX_RAW_TRANSACTION_BYTES: usize = block::MAX_BLOCK_BYTES as usize;
52
53const STREAM_SEND_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(60);
59
60type ResponseStream<T> = Pin<Box<dyn Stream<Item = Result<T, Status>> + Send>>;
62
63#[tonic::async_trait]
64impl<Rpc, ReadStateService, Mempool, Tip> CompactTxStreamer
65 for LightwalletdRPC<Rpc, ReadStateService, Mempool, Tip>
66where
67 Rpc: RpcMethods + Clone,
68 ReadStateService: ReadState,
69 Mempool: MempoolService,
70 Tip: ChainTip + Clone + Send + Sync + 'static,
71{
72 type GetBlockRangeStream = ResponseStream<CompactBlock>;
73 type GetBlockRangeNullifiersStream = ResponseStream<CompactBlock>;
74 type GetTaddressTxidsStream = ResponseStream<RawTransaction>;
75 type GetTaddressTransactionsStream = ResponseStream<RawTransaction>;
76 type GetMempoolTxStream = ResponseStream<CompactTx>;
77 type GetMempoolStreamStream = ResponseStream<RawTransaction>;
78 type GetSubtreeRootsStream = ResponseStream<SubtreeRoot>;
79 type GetAddressUtxosStreamStream = ResponseStream<GetAddressUtxosReply>;
80
81 async fn get_latest_block(
82 &self,
83 _request: Request<ChainSpec>,
84 ) -> Result<Response<BlockId>, Status> {
85 let (height, hash) = self
86 .latest_chain_tip
87 .best_tip_height_and_hash()
88 .ok_or_else(|| Status::unavailable("no chain tip available yet"))?;
89
90 Ok(Response::new(BlockId {
91 height: height.0.into(),
92 hash: hash.0.to_vec(),
93 }))
94 }
95
96 async fn get_block(&self, request: Request<BlockId>) -> Result<Response<CompactBlock>, Status> {
97 let hash_or_height = block_id_to_hash_or_height(request.into_inner())?;
98 let block = compact_block(
99 self.read_state.clone(),
100 &self.network,
101 hash_or_height,
102 false,
103 )
104 .await?;
105
106 Ok(Response::new(block))
107 }
108
109 async fn get_block_nullifiers(
110 &self,
111 request: Request<BlockId>,
112 ) -> Result<Response<CompactBlock>, Status> {
113 let hash_or_height = block_id_to_hash_or_height(request.into_inner())?;
114 let block =
115 compact_block(self.read_state.clone(), &self.network, hash_or_height, true).await?;
116
117 Ok(Response::new(block))
118 }
119
120 async fn get_block_range(
121 &self,
122 request: Request<BlockRange>,
123 ) -> Result<Response<Self::GetBlockRangeStream>, Status> {
124 let stream = block_range_stream(
125 self.read_state.clone(),
126 self.network.clone(),
127 request.into_inner(),
128 false,
129 )?;
130
131 Ok(Response::new(stream))
132 }
133
134 async fn get_block_range_nullifiers(
135 &self,
136 request: Request<BlockRange>,
137 ) -> Result<Response<Self::GetBlockRangeNullifiersStream>, Status> {
138 let stream = block_range_stream(
139 self.read_state.clone(),
140 self.network.clone(),
141 request.into_inner(),
142 true,
143 )?;
144
145 Ok(Response::new(stream))
146 }
147
148 async fn get_transaction(
149 &self,
150 request: Request<TxFilter>,
151 ) -> Result<Response<RawTransaction>, Status> {
152 let tx_filter = request.into_inner();
153
154 let hash: [u8; 32] = tx_filter.hash.try_into().map_err(|hash: Vec<u8>| {
156 Status::invalid_argument(format!(
157 "transaction hash must be 32 bytes, got {}",
158 hash.len()
159 ))
160 })?;
161 let hash = transaction::Hash::from(hash);
162
163 let side_chain_tx = match self
165 .read_state
166 .clone()
167 .oneshot(ReadRequest::AnyChainTransaction(hash))
168 .await
169 {
170 Ok(ReadResponse::AnyChainTransaction(Some(zebra_state::AnyTx::Mined(mined_tx)))) => {
171 return Ok(Response::new(RawTransaction {
172 data: serialize_transaction(&mined_tx.tx)?,
173 height: mined_tx.height.0.into(),
174 }));
175 }
176 Ok(ReadResponse::AnyChainTransaction(Some(zebra_state::AnyTx::Side((tx, _))))) => {
177 Some(tx)
178 }
179 Ok(ReadResponse::AnyChainTransaction(None)) => None,
180 Ok(_) => unreachable!("unexpected response type from ReadStateService"),
181 Err(error) => {
182 return Err(Status::internal(format!(
183 "failed to read transaction: {error}"
184 )));
185 }
186 };
187
188 let mempool_response = self
189 .mempool
190 .clone()
191 .oneshot(mempool::Request::TransactionsByMinedId(
192 [hash].into_iter().collect(),
193 ))
194 .await
195 .map_err(|error| Status::internal(format!("failed to query mempool: {error}")))?;
196
197 let mempool_tx = match mempool_response {
198 mempool::Response::Transactions(transactions) => transactions.into_iter().next(),
199 _ => unreachable!("unexpected response type from mempool service"),
200 };
201
202 if let Some(tx) = mempool_tx {
203 Ok(Response::new(RawTransaction {
204 data: serialize_transaction(&tx.transaction)?,
205 height: 0,
207 }))
208 } else if let Some(tx) = side_chain_tx {
209 Ok(Response::new(RawTransaction {
210 data: serialize_transaction(&tx)?,
211 height: u64::MAX,
214 }))
215 } else {
216 Err(Status::not_found("transaction not found"))
217 }
218 }
219
220 async fn send_transaction(
221 &self,
222 request: Request<RawTransaction>,
223 ) -> Result<Response<SendResponse>, Status> {
224 let data = request.into_inner().data;
225
226 if data.len() > MAX_RAW_TRANSACTION_BYTES {
229 return Err(Status::invalid_argument(format!(
230 "transaction too large: the limit is {MAX_RAW_TRANSACTION_BYTES} bytes"
231 )));
232 }
233
234 let transaction_hex = hex::encode(data);
235
236 let response = match self.rpc.send_raw_transaction(transaction_hex, None).await {
239 Ok(sent_tx) => SendResponse {
240 error_code: 0,
241 error_message: sent_tx.hash().to_string(),
242 },
243 Err(error) => SendResponse {
244 error_code: error.code(),
245 error_message: error.message().to_string(),
246 },
247 };
248
249 Ok(Response::new(response))
250 }
251
252 async fn get_taddress_txids(
253 &self,
254 request: Request<TransparentAddressBlockFilter>,
255 ) -> Result<Response<Self::GetTaddressTxidsStream>, Status> {
256 let stream =
257 taddress_transactions_stream(&self.rpc, self.read_state.clone(), request.into_inner())
258 .await?;
259
260 Ok(Response::new(stream))
261 }
262
263 async fn get_taddress_transactions(
264 &self,
265 request: Request<TransparentAddressBlockFilter>,
266 ) -> Result<Response<Self::GetTaddressTransactionsStream>, Status> {
267 let stream =
268 taddress_transactions_stream(&self.rpc, self.read_state.clone(), request.into_inner())
269 .await?;
270
271 Ok(Response::new(stream))
272 }
273
274 async fn get_taddress_balance(
275 &self,
276 request: Request<AddressList>,
277 ) -> Result<Response<Balance>, Status> {
278 let addresses = request.into_inner().addresses;
279
280 address_balance(&self.rpc, addresses)
281 .await
282 .map(Response::new)
283 }
284
285 async fn get_taddress_balance_stream(
286 &self,
287 request: Request<Streaming<Address>>,
288 ) -> Result<Response<Balance>, Status> {
289 let addresses = collect_streamed_addresses(request.into_inner()).await?;
290
291 address_balance(&self.rpc, addresses)
292 .await
293 .map(Response::new)
294 }
295
296 async fn get_mempool_tx(
297 &self,
298 request: Request<Exclude>,
299 ) -> Result<Response<Self::GetMempoolTxStream>, Status> {
300 let exclude = request.into_inner().txid;
301
302 if exclude.len() > MAX_EXCLUDE_ENTRIES {
303 return Err(Status::invalid_argument(format!(
304 "too many exclude entries: the limit is {MAX_EXCLUDE_ENTRIES}"
305 )));
306 }
307
308 let transactions = mempool_transactions(self.mempool.clone()).await?;
309
310 let txids: Vec<transaction::Hash> =
311 transactions.iter().map(|tx| tx.id.mined_id()).collect();
312 let excluded = excluded_txids(&txids, &exclude);
313
314 let compact_txs: Vec<Result<CompactTx, Status>> = transactions
315 .iter()
316 .filter(|tx| !excluded.contains(&tx.id.mined_id()))
317 .filter(|tx| super::has_compact_data(&tx.transaction))
318 .map(|tx| {
319 Ok(CompactTx::from_transaction(
320 0,
321 tx.id.mined_id(),
322 &tx.transaction,
323 false,
324 ))
325 })
326 .collect();
327
328 Ok(Response::new(Box::pin(futures::stream::iter(compact_txs))))
329 }
330
331 async fn get_mempool_stream(
332 &self,
333 _request: Request<Empty>,
334 ) -> Result<Response<Self::GetMempoolStreamStream>, Status> {
335 let (response_sender, response_receiver) = tokio::sync::mpsc::channel(RESPONSE_BUFFER_SIZE);
336 let response_stream = ReceiverStream::new(response_receiver);
337
338 let mut mempool_change = self.mempool_change.subscribe();
341 let mempool = self.mempool.clone();
342 let mut latest_chain_tip = self.latest_chain_tip.clone();
343
344 latest_chain_tip.mark_best_tip_seen();
349
350 tokio::spawn(async move {
351 let mut sent_txids: HashSet<transaction::Hash> = HashSet::new();
352
353 if !send_unseen_mempool_txs(&mempool, &response_sender, &mut sent_txids).await {
355 return;
356 }
357
358 loop {
361 tokio::select! {
362 _ = latest_chain_tip.best_tip_changed() => return,
363 change = mempool_change.recv() => {
364 let change = match change {
365 Ok(change) => change,
366 Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {
370 if !send_unseen_mempool_txs(
371 &mempool,
372 &response_sender,
373 &mut sent_txids,
374 )
375 .await
376 {
377 return;
378 }
379
380 continue;
381 }
382 Err(tokio::sync::broadcast::error::RecvError::Closed) => return,
383 };
384 if !change.is_added() {
385 continue;
386 }
387
388 let request = mempool::Request::TransactionsById(change.into_tx_ids());
389 let Ok(mempool::Response::Transactions(transactions)) =
390 mempool.clone().oneshot(request).await
391 else {
392 return;
393 };
394
395 for tx in transactions {
396 if sent_txids.contains(&tx.id.mined_id()) {
397 continue;
398 }
399 if !send_raw_mempool_tx(&response_sender, &tx).await {
400 return;
401 }
402 sent_txids.insert(tx.id.mined_id());
403 }
404 }
405 }
406 }
407 });
408
409 Ok(Response::new(Box::pin(response_stream)))
410 }
411
412 async fn get_tree_state(
413 &self,
414 request: Request<BlockId>,
415 ) -> Result<Response<TreeState>, Status> {
416 let hash_or_height = block_id_to_hash_or_height(request.into_inner())?;
417
418 let treestate = self
419 .rpc
420 .z_get_treestate(hash_or_height.to_string())
421 .await
422 .map_err(rpc_error_to_status)?;
423
424 Ok(Response::new(treestate_to_proto(&self.network, treestate)))
425 }
426
427 async fn get_latest_tree_state(
428 &self,
429 _request: Request<Empty>,
430 ) -> Result<Response<TreeState>, Status> {
431 let (_height, hash) = self
434 .latest_chain_tip
435 .best_tip_height_and_hash()
436 .ok_or_else(|| Status::unavailable("no chain tip available yet"))?;
437
438 let treestate = self
439 .rpc
440 .z_get_treestate(hash.to_string())
441 .await
442 .map_err(rpc_error_to_status)?;
443
444 Ok(Response::new(treestate_to_proto(&self.network, treestate)))
445 }
446
447 async fn get_subtree_roots(
448 &self,
449 request: Request<GetSubtreeRootsArg>,
450 ) -> Result<Response<Self::GetSubtreeRootsStream>, Status> {
451 let args = request.into_inner();
452
453 let pool = match ShieldedProtocol::try_from(args.shielded_protocol) {
454 Ok(ShieldedProtocol::Sapling) => "sapling",
455 Ok(ShieldedProtocol::Orchard) => "orchard",
456 Ok(ShieldedProtocol::Ironwood) => "ironwood",
457 Err(_) => return Err(Status::invalid_argument("unknown shielded protocol")),
458 };
459
460 let start_index = u16::try_from(args.start_index)
461 .map(NoteCommitmentSubtreeIndex)
462 .map_err(|_| Status::invalid_argument("start index must fit in 16 bits"))?;
463
464 let limit = if args.max_entries == 0 {
465 None
466 } else {
467 Some(NoteCommitmentSubtreeIndex(
469 args.max_entries.min(u16::MAX.into()) as u16,
470 ))
471 };
472
473 let subtrees = self
474 .rpc
475 .z_get_subtrees_by_index(pool.to_string(), start_index, limit)
476 .await
477 .map_err(rpc_error_to_status)?;
478
479 let read_state = self.read_state.clone();
480 let (response_sender, response_receiver) = tokio::sync::mpsc::channel(RESPONSE_BUFFER_SIZE);
481 let response_stream = ReceiverStream::new(response_receiver);
482
483 tokio::spawn(async move {
484 for subtree in subtrees.subtrees() {
485 let root_hash = match hex::decode(&subtree.root) {
486 Ok(root_hash) => root_hash,
487 Err(_) => {
488 let _ = send_bounded(
489 &response_sender,
490 Err(Status::internal("invalid subtree root hex")),
491 )
492 .await;
493 return;
494 }
495 };
496
497 let completing_block_hash = match read_state
498 .clone()
499 .oneshot(ReadRequest::BestChainBlockHash(subtree.end_height))
500 .await
501 {
502 Ok(ReadResponse::BlockHash(Some(hash))) => hash,
503 Ok(ReadResponse::BlockHash(None)) => {
504 let _ = send_bounded(
505 &response_sender,
506 Err(Status::not_found("completing block not found")),
507 )
508 .await;
509 return;
510 }
511 Ok(_) => unreachable!("unexpected response type from ReadStateService"),
512 Err(error) => {
513 let _ = send_bounded(
514 &response_sender,
515 Err(Status::internal(format!(
516 "failed to read completing block hash: {error}"
517 ))),
518 )
519 .await;
520 return;
521 }
522 };
523
524 let subtree_root = SubtreeRoot {
525 root_hash,
526 completing_block_hash: completing_block_hash.bytes_in_display_order().to_vec(),
529 completing_block_height: subtree.end_height.0.into(),
530 };
531
532 if !send_bounded(&response_sender, Ok(subtree_root)).await {
533 return;
534 }
535 }
536 });
537
538 Ok(Response::new(Box::pin(response_stream)))
539 }
540
541 async fn get_address_utxos(
542 &self,
543 request: Request<GetAddressUtxosArg>,
544 ) -> Result<Response<GetAddressUtxosReplyList>, Status> {
545 let address_utxos = address_utxos(&self.rpc, request.into_inner()).await?;
546
547 Ok(Response::new(GetAddressUtxosReplyList { address_utxos }))
548 }
549
550 async fn get_address_utxos_stream(
551 &self,
552 request: Request<GetAddressUtxosArg>,
553 ) -> Result<Response<Self::GetAddressUtxosStreamStream>, Status> {
554 let address_utxos = address_utxos(&self.rpc, request.into_inner()).await?;
555
556 Ok(Response::new(Box::pin(futures::stream::iter(
557 address_utxos.into_iter().map(Ok),
558 ))))
559 }
560
561 async fn get_lightd_info(
562 &self,
563 _request: Request<Empty>,
564 ) -> Result<Response<LightdInfo>, Status> {
565 let blockchain_info = self
566 .rpc
567 .get_blockchain_info()
568 .await
569 .map_err(rpc_error_to_status)?;
570 let info = self.rpc.get_info().await.map_err(rpc_error_to_status)?;
571
572 let (tip_branch_id, _next_branch_id) = blockchain_info.consensus().into_parts();
573
574 let lightd_info = LightdInfo {
575 version: info.build().clone(),
578 vendor: "ZcashFoundation/zebra".to_string(),
579 taddr_support: true,
580 chain_name: blockchain_info.chain().clone(),
581 sapling_activation_height: self.network.sapling_activation_height().0.into(),
582 consensus_branch_id: format!("{tip_branch_id:08x}"),
583 block_height: blockchain_info.blocks().0.into(),
584 git_commit: String::new(),
585 branch: String::new(),
586 build_date: String::new(),
587 build_user: String::new(),
588 estimated_height: blockchain_info.estimated_height().0.into(),
589 zcashd_build: info.build().clone(),
590 zcashd_subversion: info.subversion().clone(),
591 donation_address: String::new(),
592 };
593
594 Ok(Response::new(lightd_info))
595 }
596
597 async fn ping(&self, _request: Request<Duration>) -> Result<Response<PingResponse>, Status> {
598 Err(Status::unimplemented("Ping is disabled"))
601 }
602}
603
604fn block_id_to_hash_or_height(block_id: BlockId) -> Result<HashOrHeight, Status> {
607 if !block_id.hash.is_empty() {
608 let hash: [u8; 32] = block_id.hash.try_into().map_err(|hash: Vec<u8>| {
612 Status::invalid_argument(format!("block hash must be 32 bytes, got {}", hash.len()))
613 })?;
614
615 Ok(HashOrHeight::Hash(block::Hash(hash)))
616 } else if block_id.height == 0 {
617 Err(Status::invalid_argument(
621 "block id must specify a hash or a height",
622 ))
623 } else {
624 let height = u32::try_from(block_id.height)
625 .ok()
626 .and_then(|height| block::Height::try_from(height).ok())
627 .ok_or_else(|| {
628 Status::invalid_argument(format!("block height out of range: {}", block_id.height))
629 })?;
630
631 Ok(HashOrHeight::Height(height))
632 }
633}
634
635async fn compact_block<ReadStateService: ReadState>(
638 read_state: ReadStateService,
639 network: &Network,
640 hash_or_height: HashOrHeight,
641 nullifiers_only: bool,
642) -> Result<CompactBlock, Status> {
643 let block = match read_state
644 .clone()
645 .oneshot(ReadRequest::Block(hash_or_height))
646 .await
647 {
648 Ok(ReadResponse::Block(Some(block))) => block,
649 Ok(ReadResponse::Block(None)) => return Err(Status::not_found("block not found")),
650 Ok(_) => unreachable!("unexpected response type from ReadStateService"),
651 Err(error) => {
652 return Err(Status::internal(format!("failed to read block: {error}")));
653 }
654 };
655
656 let hash = block.hash();
657
658 let height = block
659 .coinbase_height()
660 .ok_or_else(|| Status::internal("block in the state must have a height"))?;
661
662 let is_ironwood_active = NetworkUpgrade::current(network, height) >= NetworkUpgrade::Nu6_3;
665
666 let (sapling_tree_size, orchard_tree_size, ironwood_tree_size) = if nullifiers_only {
669 (0, 0, 0)
670 } else {
671 let sapling_request = read_state
674 .clone()
675 .oneshot(ReadRequest::SaplingTree(hash.into()));
676 let orchard_request = read_state
677 .clone()
678 .oneshot(ReadRequest::OrchardTree(hash.into()));
679 let ironwood_request: OptionFuture<_> = is_ironwood_active
680 .then(|| read_state.oneshot(ReadRequest::IronwoodTree(hash.into())))
681 .into();
682
683 let (sapling_response, orchard_response, ironwood_response) =
684 futures::join!(sapling_request, orchard_request, ironwood_request);
685
686 let sapling_tree_size = match sapling_response {
689 Ok(ReadResponse::SaplingTree(Some(tree))) => tree.count(),
690 Ok(ReadResponse::SaplingTree(None)) => {
691 return Err(Status::aborted(
692 "sapling tree not found for this block, it may have been reorged",
693 ));
694 }
695 Ok(_) => unreachable!("unexpected response type from ReadStateService"),
696 Err(error) => {
697 return Err(Status::internal(format!(
698 "failed to read sapling tree: {error}"
699 )));
700 }
701 };
702
703 let orchard_tree_size = match orchard_response {
704 Ok(ReadResponse::OrchardTree(Some(tree))) => tree.count(),
705 Ok(ReadResponse::OrchardTree(None)) => {
706 return Err(Status::aborted(
707 "orchard tree not found for this block, it may have been reorged",
708 ));
709 }
710 Ok(_) => unreachable!("unexpected response type from ReadStateService"),
711 Err(error) => {
712 return Err(Status::internal(format!(
713 "failed to read orchard tree: {error}"
714 )));
715 }
716 };
717
718 let ironwood_tree_size = match ironwood_response {
719 None => 0,
721 Some(Ok(ReadResponse::IronwoodTree(Some(tree)))) => tree.count(),
722 Some(Ok(ReadResponse::IronwoodTree(None))) => {
723 return Err(Status::aborted(
724 "ironwood tree not found for this block, it may have been reorged",
725 ));
726 }
727 Some(Ok(_)) => unreachable!("unexpected response type from ReadStateService"),
728 Some(Err(error)) => {
729 return Err(Status::internal(format!(
730 "failed to read ironwood tree: {error}"
731 )));
732 }
733 };
734
735 (sapling_tree_size, orchard_tree_size, ironwood_tree_size)
736 };
737
738 Ok(CompactBlock::from_block(
739 &block,
740 hash,
741 height,
742 sapling_tree_size,
743 orchard_tree_size,
744 ironwood_tree_size,
745 nullifiers_only,
746 ))
747}
748
749fn block_range_stream<ReadStateService: ReadState>(
754 read_state: ReadStateService,
755 network: Network,
756 block_range: BlockRange,
757 nullifiers_only: bool,
758) -> Result<ResponseStream<CompactBlock>, Status> {
759 let range_height = |block_id: Option<BlockId>, name: &str| {
760 let block_id =
761 block_id.ok_or_else(|| Status::invalid_argument(format!("{name} not specified")))?;
762 match block_id_to_hash_or_height(block_id)? {
763 HashOrHeight::Height(height) => Ok(height.0),
764 HashOrHeight::Hash(_) => Err(Status::invalid_argument(format!(
765 "{name} must be specified as a height"
766 ))),
767 }
768 };
769
770 let start = range_height(block_range.start, "start block")?;
771 let end = range_height(block_range.end, "end block")?;
772
773 let (response_sender, response_receiver) = tokio::sync::mpsc::channel(RESPONSE_BUFFER_SIZE);
774 let response_stream = ReceiverStream::new(response_receiver);
775
776 tokio::spawn(async move {
777 let heights: Box<dyn Iterator<Item = u32> + Send> = if start <= end {
778 Box::new(start..=end)
779 } else {
780 Box::new((end..=start).rev())
781 };
782
783 for height in heights {
784 let hash_or_height = HashOrHeight::Height(block::Height(height));
785 let response = compact_block(
786 read_state.clone(),
787 &network,
788 hash_or_height,
789 nullifiers_only,
790 )
791 .await;
792 let is_err = response.is_err();
793
794 if !send_bounded(&response_sender, response).await || is_err {
795 return;
796 }
797 }
798 });
799
800 Ok(Box::pin(response_stream))
801}
802
803async fn taddress_transactions_stream<Rpc, ReadStateService>(
806 rpc: &Rpc,
807 read_state: ReadStateService,
808 request: TransparentAddressBlockFilter,
809) -> Result<ResponseStream<RawTransaction>, Status>
810where
811 Rpc: RpcMethods,
812 ReadStateService: ReadState,
813{
814 let range = request
815 .range
816 .ok_or_else(|| Status::invalid_argument("block range not specified"))?;
817
818 let range_height = |block_id: Option<BlockId>| -> Result<Option<u32>, Status> {
820 let Some(block_id) = block_id else {
821 return Ok(None);
822 };
823 if !block_id.hash.is_empty() {
824 return Err(Status::invalid_argument(
825 "block range must be specified by height",
826 ));
827 }
828 let height = u32::try_from(block_id.height).map_err(|_| {
829 Status::invalid_argument(format!("block height out of range: {}", block_id.height))
830 })?;
831 Ok((height != 0).then_some(height))
832 };
833
834 let txids = rpc
835 .get_address_tx_ids(GetAddressTxIdsRequest::new(
836 vec![request.address],
837 range_height(range.start)?,
838 range_height(range.end)?,
839 ))
840 .await
841 .map_err(rpc_arg_error_to_status)?;
842
843 let (response_sender, response_receiver) = tokio::sync::mpsc::channel(RESPONSE_BUFFER_SIZE);
844 let response_stream = ReceiverStream::new(response_receiver);
845
846 tokio::spawn(async move {
847 for txid in txids {
848 let response = match txid.parse() {
849 Ok(txid) => raw_transaction_by_txid(&read_state, txid).await,
850 Err(_) => Err(Status::internal(
851 "transaction ids must be valid hex strings",
852 )),
853 };
854 let is_err = response.is_err();
855
856 if !send_bounded(&response_sender, response).await || is_err {
857 return;
858 }
859 }
860 });
861
862 Ok(Box::pin(response_stream))
863}
864
865async fn raw_transaction_by_txid<ReadStateService: ReadState>(
867 read_state: &ReadStateService,
868 txid: transaction::Hash,
869) -> Result<RawTransaction, Status> {
870 match read_state
871 .clone()
872 .oneshot(ReadRequest::Transaction(txid))
873 .await
874 {
875 Ok(ReadResponse::Transaction(Some(mined_tx))) => Ok(RawTransaction {
876 data: serialize_transaction(&mined_tx.tx)?,
877 height: mined_tx.height.0.into(),
878 }),
879 Ok(ReadResponse::Transaction(None)) => Err(Status::not_found("transaction not found")),
880 Ok(_) => unreachable!("unexpected response type from ReadStateService"),
881 Err(error) => Err(Status::internal(format!(
882 "failed to read transaction: {error}"
883 ))),
884 }
885}
886
887fn serialize_transaction(tx: &transaction::Transaction) -> Result<Vec<u8>, Status> {
889 tx.zcash_serialize_to_vec()
890 .map_err(|_| Status::internal("failed to serialize transaction"))
891}
892
893async fn collect_streamed_addresses<S>(mut address_stream: S) -> Result<Vec<String>, Status>
900where
901 S: futures::Stream<Item = Result<Address, Status>> + Unpin,
902{
903 use futures::StreamExt;
904
905 let mut addresses = Vec::new();
906
907 while let Some(address) = address_stream.next().await.transpose()? {
908 if addresses.len() >= MAX_REQUEST_ADDRESSES {
909 return Err(Status::invalid_argument(format!(
910 "too many addresses: the limit is {MAX_REQUEST_ADDRESSES}"
911 )));
912 }
913
914 if address.address.parse::<transparent::Address>().is_err() {
915 return Err(Status::invalid_argument("invalid transparent address"));
916 }
917
918 addresses.push(address.address);
919 }
920
921 Ok(addresses)
922}
923
924fn check_address_count(addresses: &[String]) -> Result<(), Status> {
926 if addresses.len() > MAX_REQUEST_ADDRESSES {
927 return Err(Status::invalid_argument(format!(
928 "too many addresses: the limit is {MAX_REQUEST_ADDRESSES}"
929 )));
930 }
931
932 Ok(())
933}
934
935async fn address_balance<Rpc: RpcMethods>(
937 rpc: &Rpc,
938 addresses: Vec<String>,
939) -> Result<Balance, Status> {
940 check_address_count(&addresses)?;
941
942 let balance = rpc
943 .get_address_balance(GetAddressBalanceRequest::new(addresses))
944 .await
945 .map_err(rpc_arg_error_to_status)?;
946
947 Ok(Balance {
948 value_zat: balance.balance() as i64,
950 })
951}
952
953async fn address_utxos<Rpc: RpcMethods>(
959 rpc: &Rpc,
960 args: GetAddressUtxosArg,
961) -> Result<Vec<GetAddressUtxosReply>, Status> {
962 check_address_count(&args.addresses)?;
963
964 let response = rpc
965 .get_address_utxos(
966 GetAddressUtxosRequest::new(args.addresses, false)
967 .with_limits(args.start_height, args.max_entries),
968 )
969 .await
970 .map_err(rpc_arg_error_to_status)?;
971
972 let GetAddressUtxosResponse::Utxos(utxos) = response else {
973 unreachable!("chain info is never requested");
974 };
975
976 Ok(utxos
977 .iter()
978 .map(|utxo| GetAddressUtxosReply {
979 address: utxo.address().to_string(),
980 txid: utxo.txid().0.to_vec(),
981 index: utxo.output_index().index() as i32,
984 script: utxo.script().as_raw_bytes().to_vec(),
985 value_zat: utxo.satoshis() as i64,
987 height: utxo.height().0.into(),
988 })
989 .collect())
990}
991
992async fn send_unseen_mempool_txs<Mempool: MempoolService>(
998 mempool: &Mempool,
999 response_sender: &tokio::sync::mpsc::Sender<Result<RawTransaction, Status>>,
1000 sent_txids: &mut HashSet<transaction::Hash>,
1001) -> bool {
1002 let transactions = match mempool_transactions(mempool.clone()).await {
1003 Ok(transactions) => transactions,
1004 Err(status) => {
1005 let _ = send_bounded(response_sender, Err(status)).await;
1006 return false;
1007 }
1008 };
1009
1010 for tx in transactions {
1011 if sent_txids.contains(&tx.id.mined_id()) {
1012 continue;
1013 }
1014
1015 if !send_raw_mempool_tx(response_sender, &tx).await {
1016 return false;
1017 }
1018
1019 sent_txids.insert(tx.id.mined_id());
1020 }
1021
1022 true
1023}
1024
1025async fn mempool_transactions<Mempool: MempoolService>(
1027 mempool: Mempool,
1028) -> Result<Vec<transaction::UnminedTx>, Status> {
1029 let response = mempool
1030 .oneshot(mempool::Request::FullTransactions)
1031 .await
1032 .map_err(|error| Status::internal(format!("failed to query mempool: {error}")))?;
1033
1034 match response {
1035 mempool::Response::FullTransactions { transactions, .. } => {
1036 Ok(transactions.into_iter().map(|tx| tx.transaction).collect())
1037 }
1038 _ => unreachable!("unexpected response type from mempool service"),
1039 }
1040}
1041
1042fn excluded_txids(txids: &[transaction::Hash], exclude: &[Vec<u8>]) -> HashSet<transaction::Hash> {
1053 let mut buckets: HashMap<usize, HashMap<&[u8], (usize, Option<transaction::Hash>)>> =
1056 HashMap::new();
1057
1058 for suffix in exclude {
1059 if suffix.is_empty() || suffix.len() > 32 {
1063 continue;
1064 }
1065
1066 buckets
1067 .entry(suffix.len())
1068 .or_default()
1069 .entry(suffix.as_slice())
1070 .or_insert((0, None));
1071 }
1072
1073 for txid in txids {
1074 for (suffix_len, entries) in buckets.iter_mut() {
1075 if let Some((matches, first_match)) = entries.get_mut(&txid.0[32 - suffix_len..]) {
1076 *matches += 1;
1077 first_match.get_or_insert(*txid);
1078 }
1079 }
1080 }
1081
1082 buckets
1084 .values()
1085 .flat_map(|entries| entries.values())
1086 .filter_map(|&(matches, first_match)| (matches == 1).then_some(first_match).flatten())
1087 .collect()
1088}
1089
1090async fn send_bounded<T>(
1097 response_sender: &tokio::sync::mpsc::Sender<Result<T, Status>>,
1098 item: Result<T, Status>,
1099) -> bool {
1100 matches!(
1101 tokio::time::timeout(STREAM_SEND_TIMEOUT, response_sender.send(item)).await,
1102 Ok(Ok(()))
1103 )
1104}
1105
1106async fn send_raw_mempool_tx(
1109 response_sender: &tokio::sync::mpsc::Sender<Result<RawTransaction, Status>>,
1110 tx: &transaction::UnminedTx,
1111) -> bool {
1112 let Ok(data) = tx.transaction.zcash_serialize_to_vec() else {
1113 let _ = send_bounded(
1114 response_sender,
1115 Err(Status::internal("failed to serialize transaction")),
1116 )
1117 .await;
1118 return false;
1119 };
1120
1121 send_bounded(response_sender, Ok(RawTransaction { data, height: 0 })).await
1126}
1127
1128fn treestate_to_proto(network: &Network, treestate: GetTreestateResponse) -> TreeState {
1130 let final_state_hex =
1131 |commitments: &Option<Vec<u8>>| commitments.as_ref().map(hex::encode).unwrap_or_default();
1132
1133 TreeState {
1134 network: network.bip70_network_name(),
1135 height: treestate.height().0.into(),
1136 hash: treestate.hash().to_string(),
1137 time: treestate.time(),
1138 sapling_tree: final_state_hex(treestate.sapling().commitments().final_state()),
1139 orchard_tree: final_state_hex(treestate.orchard().commitments().final_state()),
1140 ironwood_tree: treestate
1142 .ironwood()
1143 .as_ref()
1144 .map(|ironwood| final_state_hex(ironwood.commitments().final_state()))
1145 .unwrap_or_default(),
1146 }
1147}
1148
1149fn rpc_error_to_status(error: jsonrpsee_types::ErrorObjectOwned) -> Status {
1152 match error.code() {
1155 -5 | -8 => Status::not_found(error.message().to_string()),
1156 _ => Status::internal(error.message().to_string()),
1157 }
1158}
1159
1160fn rpc_arg_error_to_status(error: jsonrpsee_types::ErrorObjectOwned) -> Status {
1167 match error.code() {
1169 -5 | -8 | -32602 => Status::invalid_argument(error.message().to_string()),
1170 _ => Status::internal(error.message().to_string()),
1171 }
1172}
1173
1174#[cfg(test)]
1175mod tests {
1176 use super::*;
1177 use futures::StreamExt;
1178 use std::sync::Arc;
1179 use tonic::Code;
1180 use zebra_chain::serialization::BytesInDisplayOrder;
1181
1182 #[tokio::test]
1185 async fn streamed_addresses_are_validated_before_the_stream_ends() {
1186 use std::sync::atomic::{AtomicUsize, Ordering};
1187
1188 let _init_guard = zebra_test::init();
1189
1190 let valid = "t3Vz22vK5z2LcKEdg16Yv4FFneEL1zg9ojd".to_string();
1191 let yielded = Arc::new(AtomicUsize::new(0));
1192
1193 let addresses = futures::stream::iter(
1195 [valid.clone(), "not an address".to_string(), valid.clone()]
1196 .map(|address| Ok(Address { address })),
1197 )
1198 .inspect({
1199 let yielded = yielded.clone();
1200 move |_| {
1201 yielded.fetch_add(1, Ordering::SeqCst);
1202 }
1203 });
1204
1205 let status = collect_streamed_addresses(Box::pin(addresses))
1206 .await
1207 .expect_err("an invalid address must be rejected");
1208
1209 assert_eq!(status.code(), Code::InvalidArgument);
1210 assert_eq!(
1211 yielded.load(Ordering::SeqCst),
1212 2,
1213 "the server must stop reading at the first invalid address",
1214 );
1215 }
1216
1217 #[tokio::test]
1219 async fn streamed_addresses_are_bounded() {
1220 let _init_guard = zebra_test::init();
1221
1222 let valid = "t3Vz22vK5z2LcKEdg16Yv4FFneEL1zg9ojd".to_string();
1223 let addresses = futures::stream::iter(
1224 std::iter::repeat_n(valid, MAX_REQUEST_ADDRESSES + 1)
1225 .map(|address| Ok(Address { address })),
1226 );
1227
1228 let status = collect_streamed_addresses(Box::pin(addresses))
1229 .await
1230 .expect_err("too many addresses must be rejected");
1231
1232 assert_eq!(status.code(), Code::InvalidArgument);
1233 }
1234
1235 fn txid(byte: u8) -> transaction::Hash {
1236 transaction::Hash::from_bytes_in_display_order(&[byte; 32])
1237 }
1238
1239 #[test]
1240 fn block_id_hash_round_trips_internal_byte_order() {
1241 let hash = block::Hash([7; 32]);
1242 let block_id = BlockId {
1243 height: 0,
1244 hash: hash.0.to_vec(),
1245 };
1246
1247 let hash_or_height =
1248 block_id_to_hash_or_height(block_id).expect("a 32-byte hash should convert");
1249
1250 assert_eq!(hash_or_height, HashOrHeight::Hash(hash));
1251 }
1252
1253 #[test]
1254 fn block_id_height_is_used_when_hash_is_empty() {
1255 let block_id = BlockId {
1256 height: 500_000,
1257 hash: Vec::new(),
1258 };
1259
1260 let hash_or_height =
1261 block_id_to_hash_or_height(block_id).expect("a valid height should convert");
1262
1263 assert_eq!(hash_or_height, HashOrHeight::Height(block::Height(500_000)));
1264 }
1265
1266 #[test]
1267 fn block_id_rejects_wrong_hash_length_and_out_of_range_height() {
1268 let status = block_id_to_hash_or_height(BlockId {
1269 height: 0,
1270 hash: vec![0; 31],
1271 })
1272 .expect_err("a 31-byte hash should be rejected");
1273 assert_eq!(status.code(), Code::InvalidArgument);
1274
1275 let status = block_id_to_hash_or_height(BlockId {
1276 height: u64::MAX,
1277 hash: Vec::new(),
1278 })
1279 .expect_err("an out-of-range height should be rejected");
1280 assert_eq!(status.code(), Code::InvalidArgument);
1281 }
1282
1283 #[test]
1284 fn exclude_list_matches_reversed_display_order_prefixes() {
1285 let asymmetric = transaction::Hash(core::array::from_fn(|i| i as u8));
1288 let txids = [txid(1), txid(2), asymmetric];
1289
1290 let excluded = excluded_txids(&txids, &[vec![30, 31]]);
1293 assert_eq!(excluded, [asymmetric].into_iter().collect());
1294
1295 assert!(excluded_txids(&txids, &[vec![31, 30]]).is_empty());
1297
1298 let excluded = excluded_txids(&txids, &[asymmetric.0.to_vec()]);
1300 assert_eq!(excluded, [asymmetric].into_iter().collect());
1301 }
1302
1303 #[test]
1304 fn exclude_list_keeps_transactions_with_ambiguous_or_unknown_entries() {
1305 let txids = [txid(1), txid(2)];
1306
1307 assert!(excluded_txids(&txids[..1], &[Vec::new()]).is_empty());
1310
1311 assert!(excluded_txids(&txids, &[vec![9, 9, 9]]).is_empty());
1313
1314 let txids = [txid(7), txid(7)];
1316 assert!(excluded_txids(&txids, &[vec![7, 7]]).is_empty());
1317 }
1318}