1use 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#[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 subscription_cooldowns: Arc<papaya::HashMap<String, Instant>>,
79}
80
81impl GrpcClient {
82 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 pub fn address(&self) -> &str {
106 &self.address
107 }
108
109 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 trace!("gRPC connection reset: {status:?}; retrying");
126 true
127 }
128 Code::Internal if status.message().contains("502 Bad Gateway") => {
129 trace!("gRPC proxy error (502): {status:?}; retrying");
135 true
136 }
137 Code::NotFound => false, 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 #[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 {
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 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 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 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 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 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 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 if received.is_empty() {
575 break;
576 }
577
578 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 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}