Skip to main content

linera_rpc/grpc/
client.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::{
5    collections::BTreeSet,
6    fmt,
7    future::Future,
8    iter,
9    sync::{
10        atomic::{AtomicU32, Ordering},
11        Arc,
12    },
13};
14
15use futures::{future, stream, StreamExt};
16use linera_base::{
17    crypto::CryptoHash,
18    data_types::{BlobContent, BlockHeight, NetworkDescription},
19    ensure,
20    identifiers::{BlobId, ChainId, EventId},
21    time::{Duration, Instant},
22};
23use linera_chain::{
24    data_types::{self},
25    types::{
26        self, Certificate, ConfirmedBlock, ConfirmedBlockCertificate, GenericCertificate,
27        LiteCertificate, Timeout, ValidatedBlockCertificate,
28    },
29};
30#[cfg(with_metrics)]
31mod metrics {
32    use std::sync::LazyLock;
33
34    use linera_base::prometheus_util::register_int_counter_vec;
35    use prometheus::IntCounterVec;
36
37    pub static VALIDATOR_SUBSCRIPTION_ERRORS: LazyLock<IntCounterVec> = LazyLock::new(|| {
38        register_int_counter_vec(
39            "validator_subscription_errors",
40            "Number of notification subscription stream errors per validator",
41            &["address"],
42        )
43    });
44}
45
46use linera_core::{
47    data_types::{CertificatesByHeightRequest, ChainInfoResponse},
48    node::{BlobStream, CrossChainMessageDelivery, NodeError, NotificationStream, ValidatorNode},
49    worker::Notification,
50};
51use linera_storage::Arc as CacheArc;
52use linera_version::VersionInfo;
53use tonic::{Code, IntoRequest, Request, Status};
54use tracing::{debug, instrument, trace, Level};
55
56use super::{
57    api::{self, validator_node_client::ValidatorNodeClient, SubscriptionRequest},
58    transport, GRPC_MAX_MESSAGE_SIZE,
59};
60#[cfg(feature = "opentelemetry")]
61use crate::propagation::{get_context_with_traffic_type, inject_context};
62use crate::{
63    grpc::api::RawCertificate, HandleConfirmedCertificateRequest, HandleLiteCertRequest,
64    HandleTimeoutCertificateRequest, HandleValidatedCertificateRequest,
65};
66
67/// A gRPC client for communicating with a validator node.
68#[derive(Clone)]
69pub struct GrpcClient {
70    address: String,
71    client: ValidatorNodeClient<transport::Channel>,
72    retry_delay: Duration,
73    max_retries: u32,
74    max_backoff: Duration,
75    /// Shared across all `GrpcClient` instances created by the same `GrpcNodeProvider`.
76    /// Tracks when each validator address last had a subscription failure, so that
77    /// other chains don't independently retry the same dead validator.
78    subscription_cooldowns: Arc<papaya::HashMap<String, Instant>>,
79}
80
81impl GrpcClient {
82    /// Creates a new gRPC client for the validator at the given address.
83    pub fn new(
84        address: String,
85        channel: transport::Channel,
86        retry_delay: Duration,
87        max_retries: u32,
88        max_backoff: Duration,
89        subscription_cooldowns: Arc<papaya::HashMap<String, Instant>>,
90    ) -> Self {
91        let client = ValidatorNodeClient::new(channel)
92            .max_encoding_message_size(GRPC_MAX_MESSAGE_SIZE)
93            .max_decoding_message_size(GRPC_MAX_MESSAGE_SIZE);
94        Self {
95            address,
96            client,
97            retry_delay,
98            max_retries,
99            max_backoff,
100            subscription_cooldowns,
101        }
102    }
103
104    /// Returns the address of the validator this client connects to.
105    pub fn address(&self) -> &str {
106        &self.address
107    }
108
109    /// Returns whether this gRPC status means the server stream should be reconnected to, or not.
110    /// Logs a warning on unexpected status codes.
111    fn is_retryable(status: &Status) -> bool {
112        match status.code() {
113            Code::DeadlineExceeded | Code::Aborted | Code::Unavailable | Code::Unknown => {
114                trace!("gRPC request interrupted: {status:?}; retrying");
115                true
116            }
117            Code::Ok | Code::Cancelled | Code::ResourceExhausted => {
118                trace!("Unexpected gRPC status: {status:?}; retrying");
119                true
120            }
121            Code::Internal if status.message().contains("h2 protocol error") => {
122                // HTTP/2 connection reset errors are transient network issues, not real
123                // internal errors. This happens when the server restarts and the
124                // connection is forcibly closed.
125                trace!("gRPC connection reset: {status:?}; retrying");
126                true
127            }
128            Code::Internal if status.message().contains("502 Bad Gateway") => {
129                // When a proxy/ingress returns HTTP 502 (e.g. during rolling restarts),
130                // tonic's frame decoder fails on the non-gRPC response body before the
131                // HTTP-to-gRPC status mapping can run, producing Code::Internal instead
132                // of Code::Unavailable. Per the gRPC spec, HTTP 502 maps to UNAVAILABLE
133                // which is retryable. This works around tonic#2365.
134                trace!("gRPC proxy error (502): {status:?}; retrying");
135                true
136            }
137            Code::NotFound => false, // This code is used if e.g. the validator is missing blobs.
138            Code::InvalidArgument
139            | Code::AlreadyExists
140            | Code::PermissionDenied
141            | Code::FailedPrecondition
142            | Code::OutOfRange
143            | Code::Unimplemented
144            | Code::Internal
145            | Code::DataLoss
146            | Code::Unauthenticated => {
147                trace!("Unexpected gRPC status: {status:?}");
148                false
149            }
150        }
151    }
152
153    async fn delegate<F, Fut, R, S>(
154        &self,
155        f: F,
156        request: impl TryInto<R> + fmt::Debug + Clone,
157        handler: &str,
158    ) -> Result<S, NodeError>
159    where
160        F: Fn(ValidatorNodeClient<transport::Channel>, Request<R>) -> Fut,
161        Fut: Future<Output = Result<tonic::Response<S>, Status>>,
162        R: IntoRequest<R> + Clone,
163    {
164        let mut retry_count = 0;
165        let request_inner = request.try_into().map_err(|_| NodeError::GrpcError {
166            error: "could not convert request to proto".to_string(),
167        })?;
168        loop {
169            #[allow(unused_mut)]
170            let mut request = Request::new(request_inner.clone());
171            // Inject OpenTelemetry context (trace context + baggage) into gRPC metadata.
172            // This uses get_context_with_traffic_type() to also check the LINERA_TRAFFIC_TYPE
173            // environment variable, allowing benchmark tools to mark their traffic as synthetic.
174            #[cfg(feature = "opentelemetry")]
175            inject_context(&get_context_with_traffic_type(), request.metadata_mut());
176            match f(self.client.clone(), request).await {
177                Err(s) if Self::is_retryable(&s) && retry_count < self.max_retries => {
178                    let delay = crate::jittered_backoff_delay(
179                        self.retry_delay,
180                        retry_count,
181                        self.max_backoff,
182                    );
183                    retry_count += 1;
184                    linera_base::time::timer::sleep(delay).await;
185                    continue;
186                }
187                Err(s) => {
188                    return Err(NodeError::GrpcError {
189                        error: format!("remote request [{handler}] failed with status: {s:?}"),
190                    });
191                }
192                Ok(result) => return Ok(result.into_inner()),
193            };
194        }
195    }
196
197    fn try_into_chain_info(
198        result: api::ChainInfoResult,
199    ) -> Result<linera_core::data_types::ChainInfoResponse, NodeError> {
200        let inner = result.inner.ok_or_else(|| NodeError::GrpcError {
201            error: "missing body from response".to_string(),
202        })?;
203        match inner {
204            api::chain_info_result::Inner::ChainInfoResponse(response) => {
205                Ok(response.try_into().map_err(|err| NodeError::GrpcError {
206                    error: format!("failed to unmarshal response: {err}"),
207                })?)
208            }
209            api::chain_info_result::Inner::Error(error) => Err(bincode::deserialize(&error)
210                .map_err(|err| NodeError::GrpcError {
211                    error: format!("failed to unmarshal error message: {err}"),
212                })?),
213        }
214    }
215}
216
217impl TryFrom<api::PendingBlobResult> for BlobContent {
218    type Error = NodeError;
219
220    fn try_from(result: api::PendingBlobResult) -> Result<Self, Self::Error> {
221        let inner = result.inner.ok_or_else(|| NodeError::GrpcError {
222            error: "missing body from response".to_string(),
223        })?;
224        match inner {
225            api::pending_blob_result::Inner::Blob(blob) => {
226                Ok(blob.try_into().map_err(|err| NodeError::GrpcError {
227                    error: format!("failed to unmarshal response: {err}"),
228                })?)
229            }
230            api::pending_blob_result::Inner::Error(error) => Err(bincode::deserialize(&error)
231                .map_err(|err| NodeError::GrpcError {
232                    error: format!("failed to unmarshal error message: {err}"),
233                })?),
234        }
235    }
236}
237
238macro_rules! client_delegate {
239    ($self:ident, $handler:ident, $req:ident) => {{
240        debug!(
241            handler = stringify!($handler),
242            request = ?$req,
243            "sending gRPC request"
244        );
245        $self
246            .delegate(
247                |mut client, req| async move { client.$handler(req).await },
248                $req,
249                stringify!($handler),
250            )
251            .await
252    }};
253}
254
255impl ValidatorNode for GrpcClient {
256    type NotificationStream = NotificationStream;
257
258    fn address(&self) -> String {
259        self.address.clone()
260    }
261
262    #[instrument(target = "grpc_client", skip_all, err(level = Level::DEBUG), fields(address = self.address))]
263    async fn handle_block_proposal(
264        &self,
265        proposal: data_types::BlockProposal,
266    ) -> Result<linera_core::data_types::ChainInfoResponse, NodeError> {
267        GrpcClient::try_into_chain_info(client_delegate!(self, handle_block_proposal, proposal)?)
268    }
269
270    #[instrument(target = "grpc_client", skip_all, fields(address = self.address))]
271    async fn handle_lite_certificate(
272        &self,
273        certificate: types::LiteCertificate<'_>,
274        delivery: CrossChainMessageDelivery,
275    ) -> Result<linera_core::data_types::ChainInfoResponse, NodeError> {
276        let wait_for_outgoing_messages = delivery.wait_for_outgoing_messages();
277        let request = HandleLiteCertRequest {
278            certificate,
279            wait_for_outgoing_messages,
280        };
281        GrpcClient::try_into_chain_info(client_delegate!(self, handle_lite_certificate, request)?)
282    }
283
284    #[instrument(target = "grpc_client", skip_all, err(level = Level::DEBUG), fields(address = self.address))]
285    async fn handle_confirmed_certificate(
286        &self,
287        certificate: CacheArc<ConfirmedBlockCertificate>,
288        delivery: CrossChainMessageDelivery,
289    ) -> Result<linera_core::data_types::ChainInfoResponse, NodeError> {
290        let wait_for_outgoing_messages: bool = delivery.wait_for_outgoing_messages();
291        let request = HandleConfirmedCertificateRequest {
292            certificate: CacheArc::unwrap_or_clone(certificate),
293            wait_for_outgoing_messages,
294        };
295        GrpcClient::try_into_chain_info(client_delegate!(
296            self,
297            handle_confirmed_certificate,
298            request
299        )?)
300    }
301
302    #[instrument(target = "grpc_client", skip_all, err(level = Level::DEBUG), fields(address = self.address))]
303    async fn handle_validated_certificate(
304        &self,
305        certificate: ValidatedBlockCertificate,
306    ) -> Result<linera_core::data_types::ChainInfoResponse, NodeError> {
307        let request = HandleValidatedCertificateRequest { certificate };
308        GrpcClient::try_into_chain_info(client_delegate!(
309            self,
310            handle_validated_certificate,
311            request
312        )?)
313    }
314
315    #[instrument(target = "grpc_client", skip_all, err(level = Level::DEBUG), fields(address = self.address))]
316    async fn handle_timeout_certificate(
317        &self,
318        certificate: GenericCertificate<Timeout>,
319    ) -> Result<linera_core::data_types::ChainInfoResponse, NodeError> {
320        let request = HandleTimeoutCertificateRequest { certificate };
321        GrpcClient::try_into_chain_info(client_delegate!(
322            self,
323            handle_timeout_certificate,
324            request
325        )?)
326    }
327
328    #[instrument(target = "grpc_client", skip_all, err(level = Level::DEBUG), fields(address = self.address))]
329    async fn handle_chain_info_query(
330        &self,
331        query: linera_core::data_types::ChainInfoQuery,
332    ) -> Result<linera_core::data_types::ChainInfoResponse, NodeError> {
333        GrpcClient::try_into_chain_info(client_delegate!(self, handle_chain_info_query, query)?)
334    }
335
336    #[instrument(target = "grpc_client", skip_all, err(level = Level::DEBUG), fields(address = self.address))]
337    async fn subscribe(&self, chains: Vec<ChainId>) -> Result<Self::NotificationStream, NodeError> {
338        let retry_delay = self.retry_delay;
339        let max_retries = self.max_retries;
340        let max_backoff = self.max_backoff;
341        let address = self.address.clone();
342        let subscription_cooldowns = self.subscription_cooldowns.clone();
343
344        // Fast-fail if another subscription to this address recently failed.
345        // Prevents N chains from independently retrying the same dead validator.
346        {
347            let pinned = subscription_cooldowns.pin();
348            if let Some(&last_failure) = pinned.get(&address) {
349                if last_failure.elapsed() < max_backoff {
350                    return Err(NodeError::SubscriptionFailed {
351                        status: format!(
352                            "validator {address} on cooldown after recent subscription failure"
353                        ),
354                    });
355                }
356            }
357        }
358
359        // Use shared atomic counter so unfold can reset it on successful reconnection.
360        let retry_count = Arc::new(AtomicU32::new(0));
361        let subscription_request = SubscriptionRequest {
362            chain_ids: chains.into_iter().map(|chain| chain.into()).collect(),
363        };
364        let mut client = self.client.clone();
365
366        // Make the first connection attempt before returning from this method.
367        let mut stream = Some(
368            client
369                .subscribe(subscription_request.clone())
370                .await
371                .map_err(|status| {
372                    subscription_cooldowns
373                        .pin()
374                        .insert(address.clone(), Instant::now());
375                    NodeError::SubscriptionFailed {
376                        status: status.to_string(),
377                    }
378                })?
379                .into_inner(),
380        );
381
382        // A stream of `Result<grpc::Notification, tonic::Status>` that keeps calling
383        // `client.subscribe(request)` endlessly and without delay.
384        let retry_count_for_unfold = retry_count.clone();
385        let cooldowns_for_unfold = subscription_cooldowns.clone();
386        let address_for_unfold = address.clone();
387        let endlessly_retrying_notification_stream = stream::unfold((), move |()| {
388            let mut client = client.clone();
389            let subscription_request = subscription_request.clone();
390            let mut stream = stream.take();
391            let retry_count = retry_count_for_unfold.clone();
392            let cooldowns = cooldowns_for_unfold.clone();
393            let cooldown_address = address_for_unfold.clone();
394            async move {
395                let stream = if let Some(stream) = stream.take() {
396                    future::Either::Right(stream)
397                } else {
398                    match client.subscribe(subscription_request.clone()).await {
399                        Err(err) => future::Either::Left(stream::iter(iter::once(Err(err)))),
400                        Ok(response) => {
401                            // Reset retry count on successful reconnection.
402                            retry_count.store(0, Ordering::Relaxed);
403                            cooldowns.pin().remove(&cooldown_address);
404                            trace!("Successfully reconnected subscription stream");
405                            future::Either::Right(response.into_inner())
406                        }
407                    }
408                };
409                Some((stream, ()))
410            }
411        })
412        .flatten();
413
414        let span = tracing::info_span!("notification stream");
415        #[cfg(with_metrics)]
416        let address_for_metrics = self.address.clone();
417        let cooldowns_for_take_while = subscription_cooldowns;
418        let address_for_take_while = self.address.clone();
419        // The stream of `Notification`s that inserts increasing delays after retriable errors, and
420        // terminates after unexpected or fatal errors.
421        let notification_stream = endlessly_retrying_notification_stream
422            .map(|result| {
423                Option::<Notification>::try_from(result?).map_err(|err| {
424                    let message = format!("Could not deserialize notification: {err}");
425                    tonic::Status::new(Code::Internal, message)
426                })
427            })
428            .take_while(move |result| {
429                let Err(status) = result else {
430                    retry_count.store(0, Ordering::Relaxed);
431                    return future::Either::Left(future::ready(true));
432                };
433
434                #[cfg(with_metrics)]
435                metrics::VALIDATOR_SUBSCRIPTION_ERRORS
436                    .with_label_values(&[&address_for_metrics])
437                    .inc();
438
439                let current_retry_count = retry_count.load(Ordering::Relaxed);
440                if !span.in_scope(|| Self::is_retryable(status))
441                    || current_retry_count >= max_retries
442                {
443                    cooldowns_for_take_while
444                        .pin()
445                        .insert(address_for_take_while.clone(), Instant::now());
446                    return future::Either::Left(future::ready(false));
447                }
448                let delay =
449                    crate::jittered_backoff_delay(retry_delay, current_retry_count, max_backoff);
450                retry_count.fetch_add(1, Ordering::Relaxed);
451                future::Either::Right(async move {
452                    linera_base::time::timer::sleep(delay).await;
453                    true
454                })
455            })
456            .filter_map(move |result| {
457                future::ready(match result {
458                    Ok(notification @ Some(_)) => notification,
459                    Ok(None) => None,
460                    Err(err) => {
461                        debug!(%address, "{}", err);
462                        None
463                    }
464                })
465            });
466
467        Ok(Box::pin(notification_stream))
468    }
469
470    #[instrument(target = "grpc_client", skip_all, err(level = Level::DEBUG), fields(address = self.address))]
471    async fn get_version_info(&self) -> Result<VersionInfo, NodeError> {
472        let req = ();
473        Ok(client_delegate!(self, get_version_info, req)?.into())
474    }
475
476    #[instrument(target = "grpc_client", skip_all, err(level = Level::DEBUG), fields(address = self.address))]
477    async fn get_network_description(&self) -> Result<NetworkDescription, NodeError> {
478        let req = ();
479        Ok(client_delegate!(self, get_network_description, req)?.try_into()?)
480    }
481
482    #[instrument(target = "grpc_client", skip(self), err(level = Level::DEBUG), fields(address = self.address))]
483    async fn upload_blob(&self, content: BlobContent) -> Result<BlobId, NodeError> {
484        Ok(client_delegate!(self, upload_blob, content)?.try_into()?)
485    }
486
487    #[instrument(target = "grpc_client", skip(self), err(level = Level::DEBUG), fields(address = self.address))]
488    async fn download_blob(&self, blob_id: BlobId) -> Result<BlobContent, NodeError> {
489        Ok(client_delegate!(self, download_blob, blob_id)?.try_into()?)
490    }
491
492    #[instrument(target = "grpc_client", skip(self), err(level = Level::DEBUG), fields(address = self.address))]
493    async fn download_blobs(&self, blob_ids: Vec<BlobId>) -> Result<BlobStream, NodeError> {
494        debug!(
495            handler = "download_blobs",
496            num_blobs = blob_ids.len(),
497            "sending gRPC request"
498        );
499        let request = api::BlobIds::try_from(blob_ids)?;
500        let stream = self
501            .client
502            .clone()
503            .download_blobs(request)
504            .await
505            .map_err(|status| NodeError::GrpcError {
506                error: status.to_string(),
507            })?
508            .into_inner();
509        let blob_stream = stream.map(|result| match result {
510            Ok(proto_blob) => BlobContent::try_from(proto_blob).map_err(NodeError::from),
511            Err(status) => Err(NodeError::GrpcError {
512                error: status.to_string(),
513            }),
514        });
515        Ok(Box::pin(blob_stream))
516    }
517
518    #[instrument(target = "grpc_client", skip(self), err(level = Level::DEBUG), fields(address = self.address))]
519    async fn download_pending_blob(
520        &self,
521        chain_id: ChainId,
522        blob_id: BlobId,
523    ) -> Result<BlobContent, NodeError> {
524        let req = (chain_id, blob_id);
525        client_delegate!(self, download_pending_blob, req)?.try_into()
526    }
527
528    #[instrument(target = "grpc_client", skip(self), err(level = Level::DEBUG), fields(address = self.address))]
529    async fn handle_pending_blob(
530        &self,
531        chain_id: ChainId,
532        blob: BlobContent,
533    ) -> Result<ChainInfoResponse, NodeError> {
534        let req = (chain_id, blob);
535        GrpcClient::try_into_chain_info(client_delegate!(self, handle_pending_blob, req)?)
536    }
537
538    #[instrument(target = "grpc_client", skip_all, err(level = Level::DEBUG), fields(address = self.address))]
539    async fn download_certificate(
540        &self,
541        hash: CryptoHash,
542    ) -> Result<ConfirmedBlockCertificate, NodeError> {
543        ConfirmedBlockCertificate::try_from(Certificate::try_from(client_delegate!(
544            self,
545            download_certificate,
546            hash
547        )?)?)
548        .map_err(|_| NodeError::UnexpectedCertificateValue)
549    }
550
551    #[instrument(target = "grpc_client", skip_all, err(level = Level::DEBUG), fields(address = self.address))]
552    async fn download_certificates(
553        &self,
554        hashes: Vec<CryptoHash>,
555    ) -> Result<Vec<ConfirmedBlockCertificate>, NodeError> {
556        let mut missing_hashes = hashes;
557        let mut certs_collected = Vec::with_capacity(missing_hashes.len());
558        while !missing_hashes.is_empty() {
559            // Macro doesn't compile if we pass `missing_hashes.clone()` directly to `client_delegate!`.
560            let missing = missing_hashes.clone();
561            let mut received: Vec<_> = Vec::<Certificate>::try_from(client_delegate!(
562                self,
563                download_certificates,
564                missing
565            )?)?
566            .into_iter()
567            .map(|cert| {
568                ConfirmedBlockCertificate::try_from(cert)
569                    .map_err(|_| NodeError::UnexpectedCertificateValue)
570            })
571            .collect::<Result<_, _>>()?;
572
573            // In the case of the server not returning any certificates, we break the loop.
574            if received.is_empty() {
575                break;
576            }
577
578            // Honest validator should return certificates in the same order as the requested hashes.
579            missing_hashes = missing_hashes[received.len()..].to_vec();
580            certs_collected.append(&mut received);
581        }
582        ensure!(
583            missing_hashes.is_empty(),
584            NodeError::MissingCertificates(missing_hashes)
585        );
586        Ok(certs_collected)
587    }
588
589    #[instrument(target = "grpc_client", skip(self), err(level = Level::DEBUG), fields(address = self.address))]
590    async fn download_certificates_by_heights(
591        &self,
592        chain_id: ChainId,
593        heights: Vec<BlockHeight>,
594    ) -> Result<Vec<ConfirmedBlockCertificate>, NodeError> {
595        let mut missing = heights.into_iter().collect::<BTreeSet<_>>();
596        let mut certs_collected = vec![];
597        while !missing.is_empty() {
598            let request = CertificatesByHeightRequest {
599                chain_id,
600                heights: missing.iter().copied().collect(),
601            };
602            let mut received: Vec<_> =
603                client_delegate!(self, download_raw_certificates_by_heights, request)?
604                    .certificates
605                    .into_iter()
606                    .map(
607                        |RawCertificate {
608                             lite_certificate,
609                             confirmed_block,
610                         }|
611                         -> Result<ConfirmedBlockCertificate, NodeError> {
612                            let cert = bcs::from_bytes::<LiteCertificate>(&lite_certificate)
613                                .map_err(|_| NodeError::UnexpectedCertificateValue)?;
614
615                            let block = bcs::from_bytes::<ConfirmedBlock>(&confirmed_block)
616                                .map_err(|_| NodeError::UnexpectedCertificateValue)?;
617
618                            cert.into_confirmed_certificate(block)
619                                .ok_or(NodeError::UnexpectedCertificateValue)
620                        },
621                    )
622                    .collect::<Result<_, _>>()?;
623
624            if received.is_empty() {
625                break;
626            }
627
628            // Remove only the heights we actually received from missing set.
629            for cert in &received {
630                missing.remove(&cert.inner().height());
631            }
632            certs_collected.append(&mut received);
633        }
634        certs_collected.sort_by_key(|cert| cert.inner().height());
635        Ok(certs_collected)
636    }
637
638    #[instrument(target = "grpc_client", skip(self), err(level = Level::DEBUG), fields(address = self.address))]
639    async fn blob_last_used_by(&self, blob_id: BlobId) -> Result<CryptoHash, NodeError> {
640        Ok(client_delegate!(self, blob_last_used_by, blob_id)?.try_into()?)
641    }
642
643    #[instrument(target = "grpc_client", skip(self), err(level = Level::DEBUG), fields(address = self.address))]
644    async fn blob_last_used_by_certificate(
645        &self,
646        blob_id: BlobId,
647    ) -> Result<ConfirmedBlockCertificate, NodeError> {
648        Ok(client_delegate!(self, blob_last_used_by_certificate, blob_id)?.try_into()?)
649    }
650
651    #[instrument(target = "grpc_client", skip(self), err(level = Level::DEBUG), fields(address = self.address))]
652    async fn event_block_heights(
653        &self,
654        event_ids: Vec<EventId>,
655    ) -> Result<Vec<Option<BlockHeight>>, NodeError> {
656        let request = api::EventBlockHeightsRequest::from(event_ids);
657        Ok(client_delegate!(self, event_block_heights, request)?.try_into()?)
658    }
659
660    #[instrument(target = "grpc_client", skip(self), err(level = Level::DEBUG), fields(address = self.address))]
661    async fn missing_blob_ids(&self, blob_ids: Vec<BlobId>) -> Result<Vec<BlobId>, NodeError> {
662        Ok(client_delegate!(self, missing_blob_ids, blob_ids)?.try_into()?)
663    }
664
665    #[expect(
666        clippy::cast_possible_truncation,
667        reason = "shard counts are bounded by validator config and fit in usize on supported targets"
668    )]
669    #[instrument(target = "grpc_client", skip(self), err(level = Level::DEBUG), fields(address = self.address))]
670    async fn get_shard_info(
671        &self,
672        chain_id: ChainId,
673    ) -> Result<linera_core::data_types::ShardInfo, NodeError> {
674        let response = client_delegate!(self, get_shard_info, chain_id)?;
675        Ok(linera_core::data_types::ShardInfo {
676            shard_id: response.shard_id as usize,
677            total_shards: response.total_shards as usize,
678        })
679    }
680}