1use alloc::borrow::ToOwned;
2use alloc::boxed::Box;
3use alloc::collections::{BTreeMap, BTreeSet};
4use alloc::string::{String, ToString};
5use alloc::vec::Vec;
6use core::error::Error;
7use core::pin::Pin;
8
9use miden_protocol::vm::FutureMaybeSend;
10
11type RpcFuture<T> = Pin<Box<dyn FutureMaybeSend<T>>>;
12
13use miden_objects::DecodeMessageExt;
14use miden_protocol::account::{
15 AccountCode,
16 AccountId,
17 AccountVaultPatch,
18 StorageMapPatchEntries,
19 StorageSlotName,
20};
21use miden_protocol::address::NetworkId;
22use miden_protocol::batch::{ProposedBatch, ProvenBatch};
23use miden_protocol::block::account_tree::AccountWitness;
24use miden_protocol::block::{BlockHeader, BlockNumber, SignedBlock};
25use miden_protocol::crypto::merkle::MerklePath;
26use miden_protocol::crypto::merkle::mmr::{Forest, MmrPath, MmrProof};
27use miden_protocol::note::{NoteId, NoteScript, NoteTag};
28use miden_protocol::transaction::ProvenTransaction;
29use miden_protocol::vm::ExecutionProof;
30use miden_protocol::{EMPTY_WORD, Word};
31use miden_tx::utils::sync::RwLock;
32use tonic::Status;
33use tracing::{info, warn};
34
35use super::domain::account::{AccountProof, GetAccountRequest};
36use super::domain::note::{FetchedNote, SyncNotesBlock};
37use super::domain::nullifier::NullifierUpdate;
38use super::encryption::{
39 AttestedTransactionEncryptionKey,
40 NextTransactionEncryptionKey,
41 SealedTransactionInputs,
42 ValidatorAttestation,
43};
44use super::generated::rpc::GetAccountRequest as ProtoGetAccountRequest;
45use super::generated::rpc::get_account_request::AccountDetailRequest;
46use super::{Endpoint, NodeRpcClient, RpcEndpoint, RpcError, RpcStatusInfo};
47use crate::rpc::domain::account_vault::AccountVaultInfo;
48use crate::rpc::domain::limits::RpcLimits;
49use crate::rpc::domain::status::NetworkNoteStatusInfo;
50use crate::rpc::domain::storage_map::StorageMapInfo;
51use crate::rpc::domain::sync::{ChainMmrInfo, SyncTarget};
52use crate::rpc::domain::transaction::TransactionRecord;
53use crate::rpc::errors::node::{parse_node_error, parse_status_error};
54use crate::rpc::errors::{AcceptHeaderContext, AcceptHeaderError, GrpcError, RpcConversionError};
55use crate::rpc::generated::rpc::BlockRange;
56use crate::rpc::{AccountStateAt, generated as proto};
57
58mod api_client;
59mod retry;
60
61use api_client::api_client_wrapper::ApiClient;
62
63struct BlockPagination {
65 current_block_from: BlockNumber,
66 block_to: BlockNumber,
67 iterations: u32,
68}
69
70enum PaginationResult {
71 Continue,
72 Done {
73 chain_tip: BlockNumber,
74 block_num: BlockNumber,
75 },
76}
77
78impl BlockPagination {
79 const MAX_ITERATIONS: u32 = 1000;
84
85 fn new(block_from: BlockNumber, block_to: BlockNumber) -> Self {
86 Self {
87 current_block_from: block_from,
88 block_to,
89 iterations: 0,
90 }
91 }
92
93 fn current_block_from(&self) -> BlockNumber {
94 self.current_block_from
95 }
96
97 fn block_to(&self) -> BlockNumber {
98 self.block_to
99 }
100
101 fn advance(
102 &mut self,
103 block_num: BlockNumber,
104 chain_tip: BlockNumber,
105 ) -> Result<PaginationResult, RpcError> {
106 if self.iterations >= Self::MAX_ITERATIONS {
107 return Err(RpcError::PaginationError(
108 "too many pagination iterations, possible infinite loop".to_owned(),
109 ));
110 }
111 self.iterations += 1;
112
113 if block_num < self.current_block_from {
114 return Err(RpcError::PaginationError(
115 "invalid pagination: block_num went backwards".to_owned(),
116 ));
117 }
118
119 if block_num > self.block_to {
122 return Err(RpcError::PaginationError(format!(
123 "invalid pagination: block_num {block_num} is past the requested block_to {}",
124 self.block_to
125 )));
126 }
127
128 let target_block = self.block_to.min(chain_tip);
129
130 if block_num >= target_block {
131 return Ok(PaginationResult::Done { chain_tip, block_num });
132 }
133
134 self.current_block_from = BlockNumber::from(block_num.as_u32().saturating_add(1));
135
136 Ok(PaginationResult::Continue)
137 }
138}
139
140const DEFAULT_MAX_RESPONSE_SIZE_BYTES: usize = 4 * 1024 * 1024 * 115 / 100;
146
147pub struct GrpcClient {
157 client: RwLock<Option<ApiClient>>,
159 endpoint: String,
161 timeout_ms: u64,
163 genesis_commitment: RwLock<Option<Word>>,
165 limits: RwLock<Option<RpcLimits>>,
167 max_retries: u32,
169 retry_interval_ms: u64,
171 bearer_token: Option<String>,
175 max_decoding_message_size: usize,
178}
179
180impl GrpcClient {
181 pub fn new(endpoint: &Endpoint, timeout_ms: u64) -> GrpcClient {
184 GrpcClient {
185 client: RwLock::new(None),
186 endpoint: endpoint.to_string(),
187 timeout_ms,
188 genesis_commitment: RwLock::new(None),
189 limits: RwLock::new(None),
190 max_retries: retry::DEFAULT_MAX_RETRIES,
191 retry_interval_ms: retry::DEFAULT_RETRY_INTERVAL_MS,
192 bearer_token: None,
193 max_decoding_message_size: DEFAULT_MAX_RESPONSE_SIZE_BYTES,
194 }
195 }
196
197 #[must_use]
200 pub fn with_max_retries(mut self, max_retries: u32) -> Self {
201 self.max_retries = max_retries;
202 self
203 }
204
205 #[must_use]
208 pub fn with_retry_interval_ms(mut self, retry_interval_ms: u64) -> Self {
209 self.retry_interval_ms = retry_interval_ms;
210 self
211 }
212
213 #[must_use]
220 pub fn with_max_decoding_message_size(mut self, max_decoding_message_size: usize) -> Self {
221 self.max_decoding_message_size = max_decoding_message_size;
222 self
223 }
224
225 #[must_use]
249 pub fn with_bearer_auth(mut self, token: String) -> Self {
250 self.bearer_token = Some(token);
251 self
252 }
253
254 async fn ensure_connected(&self) -> Result<ApiClient, RpcError> {
257 if self.client.read().is_none() {
258 self.connect().await?;
259 }
260
261 Ok(self.client.read().as_ref().expect("rpc_api should be initialized").clone())
262 }
263
264 async fn connect(&self) -> Result<(), RpcError> {
267 let genesis_commitment = *self.genesis_commitment.read();
268 let mut new_client = ApiClient::new_client(
269 self.endpoint.clone(),
270 self.timeout_ms,
271 genesis_commitment,
272 self.bearer_token.clone(),
273 self.max_decoding_message_size,
274 )
275 .await?;
276 let genesis_commitment = self.genesis_commitment.read();
278 let mut client = self.client.write();
279 if let Some(commitment) = *genesis_commitment {
280 new_client.set_genesis_commitment(commitment);
281 }
282 client.replace(new_client);
283
284 Ok(())
285 }
286
287 fn rpc_error_from_status(&self, endpoint: RpcEndpoint, status: Status) -> RpcError {
288 let genesis_commitment = self
289 .genesis_commitment
290 .read()
291 .as_ref()
292 .map_or_else(|| "none".to_string(), Word::to_hex);
293 let context = AcceptHeaderContext {
294 client_version: env!("CARGO_PKG_VERSION").to_string(),
295 genesis_commitment,
296 };
297 RpcError::from_grpc_error_with_context(endpoint, status, context)
298 }
299
300 async fn call_with_retry<T: Send + 'static>(
313 &self,
314 endpoint: RpcEndpoint,
315 mut call: impl FnMut(ApiClient) -> RpcFuture<Result<tonic::Response<T>, Status>>,
316 ) -> Result<tonic::Response<T>, RpcError> {
317 let mut retry_state =
318 retry::RetryState::new(endpoint, self.max_retries, self.retry_interval_ms);
319
320 loop {
321 let rpc_api = self.ensure_connected().await?;
322
323 match call(rpc_api).await {
324 Ok(response) => return Ok(response),
325 Err(status) if retry_state.should_retry(&status).await => {},
326 Err(status) => return Err(self.rpc_error_from_status(endpoint, status)),
327 }
328 }
329 }
330
331 pub async fn get_status_unversioned(&self) -> Result<RpcStatusInfo, RpcError> {
337 let mut rpc_api = ApiClient::new_client_without_accept_header(
338 self.endpoint.clone(),
339 self.timeout_ms,
340 self.bearer_token.clone(),
341 self.max_decoding_message_size,
342 )
343 .await?;
344 rpc_api
345 .status(proto::rpc::StatusRequest {})
346 .await
347 .map_err(|status| self.rpc_error_from_status(RpcEndpoint::Status, status))
348 .map(tonic::Response::into_inner)
349 .and_then(RpcStatusInfo::try_from)
350 }
351}
352
353#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
354#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
355impl NodeRpcClient for GrpcClient {
356 fn has_genesis_commitment(&self) -> Option<Word> {
361 *self.genesis_commitment.read()
362 }
363
364 async fn set_genesis_commitment(&self, commitment: Word) -> Result<(), RpcError> {
365 let mut genesis_commitment = self.genesis_commitment.write();
367 if genesis_commitment.is_some() {
368 return Ok(());
369 }
370 genesis_commitment.replace(commitment);
371
372 let mut client_guard = self.client.write();
375 if let Some(client) = client_guard.as_mut() {
376 client.set_genesis_commitment(commitment);
377 }
378
379 Ok(())
380 }
381
382 async fn get_transaction_encryption_key(
383 &self,
384 ) -> Result<AttestedTransactionEncryptionKey, RpcError> {
385 let api_response = self
386 .call_with_retry(RpcEndpoint::GetTransactionEncryptionKey, |mut rpc_api| {
387 Box::pin(async move {
388 rpc_api
389 .get_transaction_encryption_key(
390 proto::rpc::GetTransactionEncryptionKeyRequest {},
391 )
392 .await
393 })
394 })
395 .await?;
396 let response = api_response
397 .into_inner()
398 .key
399 .ok_or(RpcError::ExpectedDataMissing("TransactionEncryptionKey".to_owned()))?;
400
401 let attestations = response
405 .attestations
406 .into_iter()
407 .filter_map(|attestation| {
408 let validator_key =
409 attestation.validator_public_key.and_then(|key| key.decode_and_verify().ok());
410 let signature =
411 attestation.signature.and_then(|signature| signature.decode_and_verify().ok());
412 let decoded = validator_key.zip(signature).map(|(validator_key, signature)| {
413 ValidatorAttestation { validator_key, signature }
414 });
415 if decoded.is_none() {
416 warn!(
417 "skipping a transaction encryption key attestation that failed to decode"
418 );
419 }
420 decoded
421 })
422 .collect::<Vec<_>>();
423
424 let wire_scheme = |scheme: i32| {
427 u32::try_from(scheme)
428 .map_err(|_| RpcError::InvalidResponse(format!("negative IES scheme '{scheme}'")))
429 };
430
431 let next_key = response
432 .next_key
433 .map(|next| {
434 Ok::<_, RpcError>(NextTransactionEncryptionKey {
435 scheme: wire_scheme(next.scheme)?,
436 key_id: next.key_id,
437 public_key: next.public_key,
438 rotation_block_num: next.rotation_block_num.into(),
439 })
440 })
441 .transpose()?;
442
443 Ok(AttestedTransactionEncryptionKey {
444 scheme: wire_scheme(response.scheme)?,
445 key_id: response.key_id,
446 public_key: response.public_key,
447 attestations,
448 next_key,
449 })
450 }
451
452 async fn submit_proven_transaction(
453 &self,
454 proven_transaction: &ProvenTransaction,
455 sealed_transaction_inputs: SealedTransactionInputs,
456 ) -> Result<BlockNumber, RpcError> {
457 let request = proto::submission::ProvenTransactionSubmission {
458 transaction: Some(proven_transaction.into()),
459 sealed_transaction_inputs: Some(sealed_transaction_inputs.into()),
460 };
461
462 let request = proto::rpc::SubmitProvenTxRequest { submission: Some(request) };
463 let api_response = self
464 .call_with_retry(RpcEndpoint::SubmitProvenTx, |mut rpc_api| {
465 let request = request.clone();
466 Box::pin(async move { rpc_api.submit_proven_tx(request).await })
467 })
468 .await?;
469
470 Ok(BlockNumber::from(api_response.into_inner().block_num))
471 }
472
473 async fn submit_proven_batch(
474 &self,
475 proven_batch: &ProvenBatch,
476 proposed_batch: &ProposedBatch,
477 sealed_transaction_inputs: Vec<SealedTransactionInputs>,
478 ) -> Result<BlockNumber, RpcError> {
479 let request = proto::submission::TransactionBatch {
480 batch: Some(proven_batch.into()),
481 proposed_batch: Some(proposed_batch.into()),
482 sealed_transaction_inputs: sealed_transaction_inputs
483 .into_iter()
484 .map(Into::into)
485 .collect(),
486 };
487
488 let request = proto::rpc::SubmitProvenTxBatchRequest { submission: Some(request) };
489 let api_response = self
490 .call_with_retry(RpcEndpoint::SubmitProvenBatch, |mut rpc_api| {
491 let request = request.clone();
492 Box::pin(async move { rpc_api.submit_proven_tx_batch(request).await })
493 })
494 .await?;
495
496 Ok(BlockNumber::from(api_response.into_inner().block_num))
497 }
498
499 async fn get_block_header_by_number(
500 &self,
501 block_num: Option<BlockNumber>,
502 include_mmr_proof: bool,
503 ) -> Result<(BlockHeader, Option<MmrProof>), RpcError> {
504 let request = proto::rpc::GetBlockHeaderByNumberRequest {
505 block_num: block_num.as_ref().map(BlockNumber::as_u32),
506 include_mmr_proof: Some(include_mmr_proof),
507 include_protocol_config: None,
508 };
509
510 info!("Calling GetBlockHeaderByNumber: {:?}", request);
511
512 let api_response = self
513 .call_with_retry(RpcEndpoint::GetBlockHeaderByNumber, |mut rpc_api| {
514 Box::pin(async move { rpc_api.get_block_header_by_number(request).await })
515 })
516 .await?;
517
518 let response = api_response.into_inner();
519
520 let block_header: BlockHeader = response
521 .block_header
522 .ok_or(RpcError::ExpectedDataMissing("BlockHeader".into()))?
523 .decode_and_build_unchecked()?;
524
525 let mmr_proof = if include_mmr_proof {
526 let forest = response
527 .chain_length
528 .ok_or(RpcError::ExpectedDataMissing("ChainLength".into()))?;
529 let merkle_path: MerklePath = response
530 .mmr_path
531 .ok_or(RpcError::ExpectedDataMissing("MmrPath".into()))?
532 .decode_and_verify()?;
533
534 let forest_size = usize::try_from(forest).expect("u64 should fit in usize");
535 let forest = Forest::new(forest_size).map_err(|_| {
536 RpcError::InvalidResponse(format!("invalid forest size: {forest_size}"))
537 })?;
538 Some(MmrProof::new(
539 MmrPath::new(forest, block_header.block_num().as_usize(), merkle_path),
540 block_header.commitment(),
541 ))
542 } else {
543 None
544 };
545
546 Ok((block_header, mmr_proof))
547 }
548
549 async fn get_notes_by_id(&self, note_ids: &[NoteId]) -> Result<Vec<FetchedNote>, RpcError> {
550 let limits = self.get_rpc_limits().await?;
551 let mut notes = Vec::with_capacity(note_ids.len());
552 for chunk in note_ids.chunks(limits.note_ids_limit as usize) {
553 let request = proto::rpc::GetNotesByIdRequest {
554 note_ids: chunk.iter().map(proto::note::NoteId::from).collect(),
555 };
556
557 let api_response = self
558 .call_with_retry(RpcEndpoint::GetNotesById, |mut rpc_api| {
559 let request = request.clone();
560 Box::pin(async move { rpc_api.get_notes_by_id(request).await })
561 })
562 .await?;
563
564 let response_notes = api_response
565 .into_inner()
566 .notes
567 .into_iter()
568 .map(FetchedNote::try_from)
569 .collect::<Result<Vec<FetchedNote>, RpcConversionError>>()?;
570
571 notes.extend(response_notes);
572 }
573 Ok(notes)
574 }
575
576 async fn sync_chain_mmr(
577 &self,
578 current_block_height: BlockNumber,
579 upper_bound: SyncTarget,
580 ) -> Result<ChainMmrInfo, RpcError> {
581 let finality_level: proto::rpc::FinalityLevel = upper_bound.into();
582
583 let request = proto::rpc::SyncChainMmrRequest {
584 current_client_block_height: current_block_height.as_u32(),
585 finality_level: finality_level.into(),
586 };
587
588 let response = self
589 .call_with_retry(RpcEndpoint::SyncChainMmr, |mut rpc_api| {
590 Box::pin(async move { rpc_api.sync_chain_mmr(request).await })
591 })
592 .await?;
593
594 response.into_inner().try_into()
595 }
596
597 async fn get_account(
609 &self,
610 request: GetAccountRequest,
611 ) -> Result<(BlockNumber, AccountProof), RpcError> {
612 let GetAccountRequest { account_id, fetch, at, known_code } = request;
613
614 let known_code_commitment = known_code.as_ref().map_or(EMPTY_WORD, AccountCode::commitment);
615 let mut known_codes_by_commitment: BTreeMap<Word, AccountCode> = BTreeMap::new();
616 if let Some(account_code) = known_code {
617 known_codes_by_commitment.insert(account_code.commitment(), account_code);
618 }
619
620 let account_details = if account_id.is_public() {
623 Some(AccountDetailRequest {
624 code_commitment: Some(known_code_commitment.into()),
625 vault_request: fetch.vault().clone().into(),
626 storage_request: fetch.storage().clone().into(),
627 })
628 } else {
629 None
630 };
631
632 let block_num = match at {
633 AccountStateAt::Block(number) => Some(number.into()),
634 AccountStateAt::ChainTip => None,
635 };
636
637 let proto_request = ProtoGetAccountRequest {
638 account_id: Some(account_id.into()),
639 block_num,
640 details: account_details,
641 };
642
643 let response = self
644 .call_with_retry(RpcEndpoint::GetAccount, |mut rpc_api| {
645 let request = proto_request.clone();
646 Box::pin(async move { rpc_api.get_account(request).await })
647 })
648 .await?
649 .into_inner();
650
651 let account_witness: AccountWitness = response
652 .witness
653 .ok_or(RpcError::ExpectedDataMissing("AccountWitness".to_string()))?
654 .decode_and_verify()?;
655
656 let response_block_num: BlockNumber = response
657 .block_num
658 .ok_or(RpcError::ExpectedDataMissing("response block num".to_string()))?
659 .block_num
660 .into();
661
662 let headers = if account_witness.id().is_public() {
664 let details = response
665 .details
666 .ok_or(RpcError::ExpectedDataMissing("Account.Details".to_string()))?
667 .into_domain(&known_codes_by_commitment, &fetch)?;
668
669 Some(details)
670 } else {
671 None
672 };
673
674 let proof = AccountProof::new(account_witness, headers)
675 .map_err(|err| RpcError::InvalidResponse(err.to_string()))?;
676
677 Ok((response_block_num, proof))
678 }
679
680 async fn register_account(
681 &self,
682 invitation_code: &str,
683 account_id: AccountId,
684 ) -> Result<(), RpcError> {
685 let request = proto::rpc::RegisterAccountRequest {
687 invitation_code: invitation_code.to_string(),
688 account_id: Some(account_id.into()),
689 };
690
691 self.call_with_retry(RpcEndpoint::RegisterAccount, |mut rpc_api| {
692 let request = request.clone();
693 Box::pin(async move { rpc_api.register_account(request).await })
694 })
695 .await?;
696
697 Ok(())
698 }
699
700 async fn is_account_allowed(&self, account_id: AccountId) -> Result<bool, RpcError> {
701 let request = proto::rpc::IsAccountAllowedRequest { account_id: Some(account_id.into()) };
702
703 let response = self
704 .call_with_retry(RpcEndpoint::IsAccountAllowed, |mut rpc_api| {
705 Box::pin(async move { rpc_api.is_account_allowed(request).await })
706 })
707 .await?;
708
709 Ok(response.into_inner().allowed)
710 }
711
712 async fn is_invitation_code_valid(&self, invitation_code: &str) -> Result<bool, RpcError> {
713 let request =
714 proto::rpc::IsInvitationCodeValidRequest { invitation_code: invitation_code.into() };
715
716 let response = self
717 .call_with_retry(RpcEndpoint::IsInvitationCodeValid, |mut rpc_api| {
718 let request = request.clone();
719 Box::pin(async move { rpc_api.is_invitation_code_valid(request).await })
720 })
721 .await?;
722
723 Ok(response.into_inner().valid)
724 }
725
726 async fn sync_notes(
732 &self,
733 block_from: BlockNumber,
734 block_to: BlockNumber,
735 note_tags: &BTreeSet<NoteTag>,
736 ) -> Result<Vec<SyncNotesBlock>, RpcError> {
737 if note_tags.is_empty() {
738 return Ok(Vec::new());
739 }
740
741 let limits = self.get_rpc_limits().await?;
742 let tags: Vec<NoteTag> = note_tags.iter().copied().collect();
743
744 let mut merged_blocks: BTreeMap<BlockNumber, SyncNotesBlock> = BTreeMap::new();
747
748 for chunk in tags.chunks(limits.note_tags_limit as usize) {
749 let proto_tags: Vec<u32> = chunk.iter().map(|&t| t.into()).collect();
750 let mut pagination = BlockPagination::new(block_from, block_to);
751
752 loop {
753 let request = proto::rpc::SyncNotesRequest {
754 block_range: Some(BlockRange {
755 block_from: pagination.current_block_from().as_u32(),
756 block_to: block_to.as_u32(),
757 }),
758 note_tags: proto_tags.clone(),
759 };
760
761 let response = self
762 .call_with_retry(RpcEndpoint::SyncNotes, |mut rpc_api| {
763 let request = request.clone();
764 Box::pin(async move { rpc_api.sync_notes(request).await })
765 })
766 .await?
767 .into_inner();
768
769 let page = response.pagination_info.ok_or(RpcError::ExpectedDataMissing(
770 "SyncNotesResponse.pagination_info".to_owned(),
771 ))?;
772 let page_chain_tip = BlockNumber::from(page.chain_tip);
773 let page_block_to = BlockNumber::from(page.block_num);
774
775 for proto_block in response.blocks {
776 let block: SyncNotesBlock = proto_block.try_into()?;
777 let bn = block.block_header.block_num();
778 if let Some(existing) = merged_blocks.get_mut(&bn) {
779 for (id, note) in block.notes {
780 existing.notes.entry(id).or_insert(note);
781 }
782 } else {
783 merged_blocks.insert(bn, block);
784 }
785 }
786
787 match pagination.advance(page_block_to, page_chain_tip)? {
788 PaginationResult::Continue => {},
789 PaginationResult::Done { .. } => break,
790 }
791 }
792 }
793
794 Ok(merged_blocks.into_values().collect())
795 }
796
797 async fn sync_nullifiers(
798 &self,
799 prefixes: &[u16],
800 block_from: BlockNumber,
801 block_to: BlockNumber,
802 ) -> Result<Vec<NullifierUpdate>, RpcError> {
803 let limits = self.get_rpc_limits().await?;
804 let mut all_nullifiers = BTreeSet::new();
805
806 for chunk in prefixes.chunks(limits.nullifiers_limit as usize) {
809 let proto_prefixes: Vec<u32> = chunk.iter().map(|&x| u32::from(x)).collect();
810 let mut pagination = BlockPagination::new(block_from, block_to);
811
812 loop {
813 let request = proto::rpc::SyncNullifiersRequest {
814 nullifiers: proto_prefixes.clone(),
815 prefix_len: 16,
816 block_range: Some(BlockRange {
817 block_from: pagination.current_block_from().as_u32(),
818 block_to: pagination.block_to().as_u32(),
819 }),
820 };
821
822 let response = self
823 .call_with_retry(RpcEndpoint::SyncNullifiers, |mut rpc_api| {
824 let request = request.clone();
825 Box::pin(async move { rpc_api.sync_nullifiers(request).await })
826 })
827 .await?
828 .into_inner();
829
830 let batch_nullifiers = response
831 .nullifiers
832 .iter()
833 .map(TryFrom::try_from)
834 .collect::<Result<Vec<NullifierUpdate>, _>>()
835 .map_err(|err| RpcError::InvalidResponse(err.to_string()))?;
836
837 all_nullifiers.extend(batch_nullifiers);
838
839 let page = response.pagination_info.ok_or(RpcError::ExpectedDataMissing(
840 "SyncNullifiersResponse.pagination_info".to_owned(),
841 ))?;
842
843 match pagination.advance(page.block_num.into(), page.chain_tip.into())? {
844 PaginationResult::Continue => {},
845 PaginationResult::Done { .. } => break,
846 }
847 }
848 }
849 Ok(all_nullifiers.into_iter().collect::<Vec<_>>())
850 }
851
852 async fn get_block_by_number(
853 &self,
854 block_num: BlockNumber,
855 include_proof: bool,
856 ) -> Result<(SignedBlock, Option<ExecutionProof>), RpcError> {
857 let request = proto::rpc::GetBlockByNumberRequest {
858 block_num: block_num.as_u32(),
859 include_proof: Some(include_proof),
860 };
861
862 let response = self
863 .call_with_retry(RpcEndpoint::GetBlockByNumber, |mut rpc_api| {
864 Box::pin(async move { rpc_api.get_block_by_number(request).await })
865 })
866 .await?;
867
868 decode_block_response(response.into_inner())
869 }
870
871 async fn get_note_script_by_root(&self, root: Word) -> Result<Option<NoteScript>, RpcError> {
872 let request = proto::rpc::GetNoteScriptByRootRequest { root: Some(root.into()) };
873
874 let response = self
875 .call_with_retry(RpcEndpoint::GetNoteScriptByRoot, |mut rpc_api| {
876 let request = request.clone();
877 Box::pin(async move { rpc_api.get_note_script_by_root(request).await })
878 })
879 .await?;
880
881 let Some(script) = response.into_inner().script else {
883 return Ok(None);
884 };
885 let note_script: NoteScript = script.decode_and_verify()?;
886
887 Ok(Some(note_script))
888 }
889
890 async fn sync_storage_maps(
891 &self,
892 block_from: BlockNumber,
893 block_to: BlockNumber,
894 account_id: AccountId,
895 ) -> Result<StorageMapInfo, RpcError> {
896 let mut pagination = BlockPagination::new(block_from, block_to);
897 let mut map_entries: BTreeMap<StorageSlotName, StorageMapPatchEntries> = BTreeMap::new();
898
899 let (chain_tip, block_number) = loop {
900 let request = proto::rpc::SyncAccountStorageMapsRequest {
901 block_range: Some(BlockRange {
902 block_from: pagination.current_block_from().as_u32(),
903 block_to: block_to.as_u32(),
904 }),
905 account_id: Some(account_id.into()),
906 };
907 let response = self
908 .call_with_retry(RpcEndpoint::SyncStorageMaps, |mut rpc_api| {
909 Box::pin(async move { rpc_api.sync_account_storage_maps(request).await })
910 })
911 .await?;
912 let page = StorageMapInfo::try_from(response.into_inner())?;
913
914 for (slot_name, entries) in page.map_entries {
915 map_entries
916 .entry(slot_name)
917 .or_default()
918 .as_map_mut()
919 .extend(entries.into_map());
920 }
921
922 match pagination.advance(page.block_number, page.chain_tip)? {
923 PaginationResult::Continue => {},
924 PaginationResult::Done {
925 chain_tip: final_chain_tip,
926 block_num: final_block_num,
927 } => break (final_chain_tip, final_block_num),
928 }
929 };
930
931 Ok(StorageMapInfo { chain_tip, block_number, map_entries })
932 }
933
934 async fn sync_account_vault(
935 &self,
936 block_from: BlockNumber,
937 block_to: BlockNumber,
938 account_id: AccountId,
939 ) -> Result<AccountVaultInfo, RpcError> {
940 let mut pagination = BlockPagination::new(block_from, block_to);
941 let mut vault_patch = AccountVaultPatch::default();
942
943 let (chain_tip, block_number) = loop {
944 let request = proto::rpc::SyncAccountVaultRequest {
945 block_range: Some(BlockRange {
946 block_from: pagination.current_block_from().as_u32(),
947 block_to: block_to.as_u32(),
948 }),
949 account_id: Some(account_id.into()),
950 };
951 let response = self
952 .call_with_retry(RpcEndpoint::SyncAccountVault, |mut rpc_api| {
953 Box::pin(async move { rpc_api.sync_account_vault(request).await })
954 })
955 .await?;
956 let page = AccountVaultInfo::try_from(response.into_inner())?;
957
958 vault_patch.merge(page.vault_patch);
959
960 match pagination.advance(page.block_number, page.chain_tip)? {
961 PaginationResult::Continue => {},
962 PaginationResult::Done {
963 chain_tip: final_chain_tip,
964 block_num: final_block_num,
965 } => break (final_chain_tip, final_block_num),
966 }
967 };
968
969 Ok(AccountVaultInfo { chain_tip, block_number, vault_patch })
970 }
971
972 async fn sync_transactions(
978 &self,
979 block_from: BlockNumber,
980 block_to: BlockNumber,
981 account_ids: Vec<AccountId>,
982 ) -> Result<Vec<TransactionRecord>, RpcError> {
983 if account_ids.is_empty() {
984 return Ok(Vec::new());
985 }
986
987 let limits = self.get_rpc_limits().await?;
988 let mut transactions: Vec<TransactionRecord> = Vec::new();
989
990 for chunk in account_ids.chunks(limits.account_ids_limit as usize) {
991 let proto_account_ids: Vec<_> = chunk.iter().map(|acc_id| (*acc_id).into()).collect();
992 let mut pagination = BlockPagination::new(block_from, block_to);
993
994 loop {
995 let request = proto::rpc::SyncTransactionsRequest {
996 block_range: Some(BlockRange {
997 block_from: pagination.current_block_from().as_u32(),
998 block_to: block_to.as_u32(),
999 }),
1000 account_ids: proto_account_ids.clone(),
1001 };
1002
1003 let response = self
1004 .call_with_retry(RpcEndpoint::SyncTransactions, |mut rpc_api| {
1005 let request = request.clone();
1006 Box::pin(async move { rpc_api.sync_transactions(request).await })
1007 })
1008 .await?
1009 .into_inner();
1010
1011 let page = response.pagination_info.ok_or(RpcError::ExpectedDataMissing(
1012 "SyncTransactionsResponse.pagination_info".to_owned(),
1013 ))?;
1014 let page_chain_tip = BlockNumber::from(page.chain_tip);
1015 let page_block_to = BlockNumber::from(page.block_num);
1016
1017 for proto_tx in response.transactions {
1018 transactions.push(TransactionRecord::try_from(proto_tx)?);
1019 }
1020
1021 match pagination.advance(page_block_to, page_chain_tip)? {
1022 PaginationResult::Continue => {},
1023 PaginationResult::Done { .. } => break,
1024 }
1025 }
1026 }
1027
1028 Ok(transactions)
1029 }
1030
1031 async fn get_network_id(&self) -> Result<NetworkId, RpcError> {
1032 let endpoint: Endpoint =
1033 Endpoint::try_from(self.endpoint.as_str()).map_err(RpcError::InvalidNodeEndpoint)?;
1034 Ok(endpoint.to_network_id())
1035 }
1036
1037 async fn get_rpc_limits(&self) -> Result<RpcLimits, RpcError> {
1038 if let Some(limits) = *self.limits.read() {
1039 return Ok(limits);
1040 }
1041
1042 let response = self
1043 .call_with_retry(RpcEndpoint::GetLimits, |mut rpc_api| {
1044 Box::pin(async move { rpc_api.get_limits(proto::rpc::GetLimitsRequest {}).await })
1045 })
1046 .await?;
1047 let limits = RpcLimits::try_from(response.into_inner()).map_err(RpcError::from)?;
1048
1049 self.limits.write().replace(limits);
1051 Ok(limits)
1052 }
1053
1054 fn has_rpc_limits(&self) -> Option<RpcLimits> {
1055 *self.limits.read()
1056 }
1057
1058 async fn set_rpc_limits(&self, limits: RpcLimits) {
1059 self.limits.write().replace(limits);
1060 }
1061
1062 async fn get_status_unversioned(&self) -> Result<RpcStatusInfo, RpcError> {
1063 GrpcClient::get_status_unversioned(self).await
1064 }
1065
1066 async fn get_network_note_status(
1067 &self,
1068 note_id: NoteId,
1069 ) -> Result<NetworkNoteStatusInfo, RpcError> {
1070 let request = proto::rpc::GetNetworkNoteStatusRequest {
1071 note_id: Some(proto::note::NoteId::from(¬e_id)),
1072 };
1073
1074 let response = self
1075 .call_with_retry(RpcEndpoint::GetNetworkNoteStatus, |mut rpc_api| {
1076 let request = request.clone();
1077 Box::pin(async move { rpc_api.get_network_note_status(request).await })
1078 })
1079 .await?;
1080
1081 response.into_inner().try_into()
1082 }
1083}
1084
1085impl RpcError {
1089 pub fn from_grpc_error_with_context(
1090 endpoint: RpcEndpoint,
1091 status: Status,
1092 context: AcceptHeaderContext,
1093 ) -> Self {
1094 if let Some(accept_error) =
1095 AcceptHeaderError::try_from_message_with_context(status.message(), context)
1096 {
1097 return Self::AcceptHeaderError(accept_error);
1098 }
1099
1100 let error_kind = GrpcError::from(&status);
1101
1102 let endpoint_error = parse_node_error(&endpoint, status.details(), status.message())
1104 .or_else(|| parse_status_error(&endpoint, &error_kind, status.message()));
1105
1106 let source = Box::new(status) as Box<dyn Error + Send + Sync + 'static>;
1107
1108 Self::RequestError {
1109 endpoint,
1110 error_kind,
1111 endpoint_error,
1112 source: Some(source),
1113 }
1114 }
1115}
1116
1117impl From<&Status> for GrpcError {
1118 fn from(status: &Status) -> Self {
1119 GrpcError::from_code(status.code() as i32, Some(status.message().to_string()))
1120 }
1121}
1122
1123fn decode_block_response(
1132 response: proto::rpc::GetBlockByNumberResponse,
1133) -> Result<(SignedBlock, Option<ExecutionProof>), RpcError> {
1134 let block: SignedBlock = response
1137 .block
1138 .ok_or(RpcError::ExpectedDataMissing("GetBlockByNumberResponse.block".to_string()))?
1139 .decode_and_build_unchecked()?;
1140
1141 let proof = response.proof.map(ExecutionProof::try_from).transpose()?;
1144
1145 Ok((block, proof))
1146}
1147
1148#[cfg(test)]
1149mod tests {
1150 use std::boxed::Box;
1151 use std::vec;
1152
1153 use miden_protocol::Word;
1154 use miden_protocol::block::{BlockNumber, SignedBlock};
1155 use miden_testing::MockChain;
1156
1157 use super::{
1158 BlockPagination,
1159 DEFAULT_MAX_RESPONSE_SIZE_BYTES,
1160 GrpcClient,
1161 PaginationResult,
1162 decode_block_response,
1163 proto,
1164 };
1165 use crate::alloc::string::ToString;
1166 use crate::rpc::{Endpoint, NodeRpcClient, RpcError};
1167
1168 fn assert_send_sync<T: Send + Sync>() {}
1169
1170 fn genesis_block_messages()
1172 -> (proto::blockchain::SignedBlock, proto::primitives::ExecutionProof) {
1173 let chain = MockChain::new();
1174 let block = chain.proven_blocks().first().expect("the chain has a genesis block").clone();
1175 let (header, body, signatures, proof) = block.into_parts();
1176
1177 (SignedBlock::new_unchecked(header, body, signatures).into(), proof.into())
1178 }
1179
1180 #[test]
1181 fn decode_block_response_reads_a_requested_proof() {
1182 let (block, proof_message) = genesis_block_messages();
1183 let response = proto::rpc::GetBlockByNumberResponse {
1184 block: Some(block),
1185 proof: Some(proof_message.clone()),
1186 };
1187
1188 let (_block, proof) = decode_block_response(response).unwrap();
1189
1190 let decoded: proto::primitives::ExecutionProof =
1191 proof.expect("the response carries a proof").into();
1192 assert_eq!(decoded, proof_message);
1193 }
1194
1195 #[test]
1196 fn decode_block_response_omits_an_absent_proof() {
1197 let (block, _) = genesis_block_messages();
1198 let response = proto::rpc::GetBlockByNumberResponse { block: Some(block), proof: None };
1199
1200 let (_block, proof) = decode_block_response(response).unwrap();
1201
1202 assert!(proof.is_none());
1203 }
1204
1205 #[test]
1206 fn decode_block_response_rejects_malformed_proof_bytes() {
1207 let (block, _) = genesis_block_messages();
1208 let response = proto::rpc::GetBlockByNumberResponse {
1209 block: Some(block),
1210 proof: Some(proto::primitives::ExecutionProof { encoded: vec![0xff; 32] }),
1211 };
1212
1213 let res = decode_block_response(response);
1214
1215 assert!(matches!(res, Err(RpcError::DeserializationError(_))));
1216 }
1217
1218 #[test]
1219 fn decode_block_response_rejects_an_absent_block() {
1220 let response = proto::rpc::GetBlockByNumberResponse { block: None, proof: None };
1221
1222 let res = decode_block_response(response);
1223
1224 assert!(matches!(res, Err(RpcError::ExpectedDataMissing(_))));
1225 }
1226
1227 #[test]
1228 fn is_send_sync() {
1229 assert_send_sync::<GrpcClient>();
1230 assert_send_sync::<Box<dyn NodeRpcClient>>();
1231 }
1232
1233 #[test]
1234 fn block_pagination_errors_when_block_num_goes_backwards() {
1235 let mut pagination = BlockPagination::new(10_u32.into(), 20_u32.into());
1236
1237 let res = pagination.advance(9_u32.into(), 20_u32.into());
1238 assert!(matches!(res, Err(RpcError::PaginationError(_))));
1239 }
1240
1241 #[test]
1242 fn block_pagination_errors_when_block_num_passes_block_to() {
1243 let mut pagination = BlockPagination::new(10_u32.into(), 20_u32.into());
1244
1245 let res = pagination.advance(21_u32.into(), 100_u32.into());
1246 assert!(matches!(res, Err(RpcError::PaginationError(_))));
1247 }
1248
1249 #[test]
1250 fn block_pagination_errors_after_max_iterations() {
1251 let mut pagination = BlockPagination::new(0_u32.into(), 10_000_u32.into());
1252 let chain_tip: BlockNumber = 10_000_u32.into();
1253
1254 for _ in 0..BlockPagination::MAX_ITERATIONS {
1255 let current = pagination.current_block_from();
1256 let res = pagination
1257 .advance(current, chain_tip)
1258 .expect("expected pagination to continue within iteration limit");
1259 assert!(matches!(res, PaginationResult::Continue));
1260 }
1261
1262 let res = pagination.advance(pagination.current_block_from(), chain_tip);
1263 assert!(matches!(res, Err(RpcError::PaginationError(_))));
1264 }
1265
1266 #[test]
1267 fn block_pagination_stops_at_min_of_block_to_and_chain_tip() {
1268 let mut pagination = BlockPagination::new(0_u32.into(), 50_u32.into());
1270
1271 let res = pagination
1272 .advance(30_u32.into(), 30_u32.into())
1273 .expect("expected pagination to succeed");
1274
1275 assert!(matches!(
1276 res,
1277 PaginationResult::Done {
1278 chain_tip,
1279 block_num
1280 } if chain_tip.as_u32() == 30 && block_num.as_u32() == 30
1281 ));
1282 }
1283
1284 #[test]
1285 fn block_pagination_advances_cursor_by_one() {
1286 let mut pagination = BlockPagination::new(5_u32.into(), 100_u32.into());
1287
1288 let res = pagination
1289 .advance(5_u32.into(), 100_u32.into())
1290 .expect("expected pagination to succeed");
1291 assert!(matches!(res, PaginationResult::Continue));
1292 assert_eq!(pagination.current_block_from().as_u32(), 6);
1293 }
1294
1295 async fn dyn_trait_send_fut(client: Box<dyn NodeRpcClient>) {
1297 let res = client.get_block_header_by_number(None, false).await;
1299 assert!(res.is_ok());
1300 }
1301
1302 #[tokio::test]
1303 async fn future_is_send() {
1304 let endpoint = &Endpoint::devnet();
1305 let client = GrpcClient::new(endpoint, 10000);
1306 let client: Box<GrpcClient> = client.into();
1307 tokio::task::spawn(async move { dyn_trait_send_fut(client).await });
1308 }
1309
1310 #[tokio::test]
1311 async fn set_genesis_commitment_sets_the_commitment_when_its_not_already_set() {
1312 let endpoint = &Endpoint::devnet();
1313 let client = GrpcClient::new(endpoint, 10000);
1314
1315 assert!(client.genesis_commitment.read().is_none());
1316
1317 let commitment = Word::default();
1318 client.set_genesis_commitment(commitment).await.unwrap();
1319
1320 assert_eq!(client.genesis_commitment.read().unwrap(), commitment);
1321 }
1322
1323 #[tokio::test]
1324 async fn set_genesis_commitment_does_nothing_if_the_commitment_is_already_set() {
1325 let endpoint = &Endpoint::devnet();
1326 let client = GrpcClient::new(endpoint, 10000);
1327
1328 let initial_commitment = Word::default();
1329 client.set_genesis_commitment(initial_commitment).await.unwrap();
1330
1331 let new_commitment = Word::from([1u32, 2, 3, 4]);
1332 client.set_genesis_commitment(new_commitment).await.unwrap();
1333
1334 assert_eq!(client.genesis_commitment.read().unwrap(), initial_commitment);
1335 }
1336
1337 #[tokio::test]
1338 async fn set_genesis_commitment_updates_the_client_if_already_connected() {
1339 let endpoint = &Endpoint::devnet();
1340 let client = GrpcClient::new(endpoint, 10000);
1341
1342 client.connect().await.unwrap();
1344
1345 let commitment = Word::default();
1346 client.set_genesis_commitment(commitment).await.unwrap();
1347
1348 assert_eq!(client.genesis_commitment.read().unwrap(), commitment);
1349 assert!(client.client.read().as_ref().is_some());
1350 }
1351
1352 #[test]
1353 fn with_bearer_auth_stores_token() {
1354 let endpoint = &Endpoint::devnet();
1355 let client = GrpcClient::new(endpoint, 10000).with_bearer_auth("token-one".to_string());
1356
1357 assert_eq!(client.bearer_token.as_deref(), Some("token-one"));
1358 }
1359
1360 #[test]
1361 fn with_bearer_auth_overwrites_on_repeat_call() {
1362 let endpoint = &Endpoint::devnet();
1363 let client = GrpcClient::new(endpoint, 10000)
1364 .with_bearer_auth("token-one".to_string())
1365 .with_bearer_auth("token-two".to_string());
1366
1367 assert_eq!(client.bearer_token.as_deref(), Some("token-two"));
1369 }
1370
1371 #[tokio::test]
1372 async fn with_bearer_auth_surfaces_invalid_ascii_value_at_connect_time() {
1373 let endpoint = &Endpoint::devnet();
1377 let client = GrpcClient::new(endpoint, 10000).with_bearer_auth("bad\nvalue".to_string());
1378
1379 let err = client.connect().await.expect_err("expected invalid token to fail connect");
1380 assert!(
1381 matches!(err, RpcError::ConnectionError(_)),
1382 "expected ConnectionError, got {err:?}",
1383 );
1384 }
1385
1386 #[tokio::test]
1387 async fn with_bearer_auth_is_preserved_across_set_genesis_commitment() {
1388 let endpoint = &Endpoint::devnet();
1389 let client = GrpcClient::new(endpoint, 10000).with_bearer_auth("token".to_string());
1390 client.connect().await.unwrap();
1391
1392 client.set_genesis_commitment(Word::default()).await.unwrap();
1393
1394 assert_eq!(client.bearer_token.as_deref(), Some("token"));
1396 assert!(client.client.read().as_ref().is_some());
1397 }
1398
1399 #[test]
1400 fn with_max_decoding_message_size_overrides_default() {
1401 let endpoint = &Endpoint::devnet();
1402
1403 let default_client = GrpcClient::new(endpoint, 10_000);
1405 assert_eq!(default_client.max_decoding_message_size, DEFAULT_MAX_RESPONSE_SIZE_BYTES);
1406
1407 let custom =
1409 GrpcClient::new(endpoint, 10_000).with_max_decoding_message_size(8 * 1024 * 1024);
1410 assert_eq!(custom.max_decoding_message_size, 8 * 1024 * 1024);
1411 }
1412
1413 #[tokio::test]
1424 #[ignore = "requires network access to public testnet"]
1425 async fn with_bearer_auth_does_not_break_real_rpc_against_testnet() {
1426 let endpoint = &Endpoint::testnet();
1427 let client = GrpcClient::new(endpoint, 10_000).with_bearer_auth("smoke-test".to_string());
1428
1429 let status = client
1430 .get_status_unversioned()
1431 .await
1432 .expect("testnet status with caller auth header must succeed");
1433 assert!(!status.version.is_empty(), "status must include a server version");
1434 }
1435}