Skip to main content

linera_rpc/grpc/
server.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::{
5    net::{IpAddr, SocketAddr},
6    str::FromStr,
7    task::{Context, Poll},
8};
9
10use futures::{
11    channel::mpsc, future::BoxFuture, stream::FuturesUnordered, FutureExt as _, StreamExt as _,
12};
13#[cfg(with_metrics)]
14use linera_base::time::Instant;
15use linera_base::{data_types::Blob, identifiers::ChainId, time::Duration};
16use linera_core::{
17    join_set_ext::JoinSet,
18    node::NodeError,
19    worker::{NetworkActions, Notification, Reason, WorkerState},
20    JoinSetExt as _, ProcessConfirmedBlockMode, TaskHandle,
21};
22use linera_storage::Storage;
23use tokio::sync::{broadcast::error::RecvError, oneshot};
24use tokio_util::sync::CancellationToken;
25use tonic::{transport::Channel, Request, Response, Status};
26use tower::{builder::ServiceBuilder, Layer, Service};
27use tracing::{debug, error, info, instrument, trace, warn};
28
29use super::{
30    api::{
31        self,
32        notifier_service_client::NotifierServiceClient,
33        validator_worker_client::ValidatorWorkerClient,
34        validator_worker_server::{ValidatorWorker as ValidatorWorkerRpc, ValidatorWorkerServer},
35        BlockProposal, ChainInfoQuery, ChainInfoResult, CrossChainRequest,
36        HandlePendingBlobRequest, LiteCertificate, PendingBlobRequest, PendingBlobResult,
37    },
38    pool::GrpcConnectionPool,
39    GrpcError, GRPC_MAX_MESSAGE_SIZE,
40};
41#[cfg(feature = "opentelemetry")]
42use crate::propagation::get_traffic_type_from_request;
43use crate::{
44    config::{CrossChainConfig, NotificationConfig, ShardId, ValidatorInternalNetworkConfig},
45    cross_chain_message_queue, HandleConfirmedCertificateRequest, HandleLiteCertRequest,
46    HandleTimeoutCertificateRequest, HandleValidatedCertificateRequest,
47};
48
49type CrossChainSender = mpsc::Sender<(linera_core::data_types::CrossChainRequest, ShardId)>;
50type NotificationSender = tokio::sync::broadcast::Sender<Notification>;
51
52#[cfg(with_metrics)]
53pub(crate) mod metrics {
54    use linera_base::prometheus_util::{
55        exponential_bucket_interval, linear_bucket_interval, register_histogram,
56        register_histogram_vec, register_int_counter, register_int_counter_vec,
57    };
58    use prometheus::{Histogram, HistogramVec, IntCounter, IntCounterVec};
59
60    use super::super::{ERROR_TYPE_LABEL, METHOD_NAME_LABEL, TRAFFIC_TYPE_LABEL};
61
62    linera_base::declare_metrics! {
63        pub static SERVER_REQUEST_LATENCY: HistogramVec =
64            register_histogram_vec(
65                "server_request_latency",
66                "Server request latency",
67                &[METHOD_NAME_LABEL, TRAFFIC_TYPE_LABEL],
68                linear_bucket_interval(1.0, 50.0, 5000.0),
69            );
70
71        pub static SERVER_REQUEST_COUNT: IntCounterVec =
72            register_int_counter_vec(
73                "server_request_count",
74                "Server request count",
75                &[METHOD_NAME_LABEL, TRAFFIC_TYPE_LABEL],
76            );
77
78        pub static SERVER_REQUEST_SUCCESS: IntCounterVec =
79            register_int_counter_vec(
80                "server_request_success",
81                "Server request success",
82                &[METHOD_NAME_LABEL, TRAFFIC_TYPE_LABEL],
83            );
84
85        pub static SERVER_REQUEST_ERROR: IntCounterVec =
86            register_int_counter_vec(
87                "server_request_error",
88                "Server request error",
89                &[METHOD_NAME_LABEL, TRAFFIC_TYPE_LABEL, ERROR_TYPE_LABEL],
90            );
91
92        pub static SERVER_REQUEST_CANCELLED: IntCounterVec =
93            register_int_counter_vec(
94                "server_request_cancelled",
95                "Server requests whose handler future was dropped before completion (e.g. client-side timeout / disconnect)",
96                &[METHOD_NAME_LABEL, TRAFFIC_TYPE_LABEL],
97            );
98
99        pub static CROSS_CHAIN_MESSAGE_CHANNEL_FULL: IntCounter =
100            register_int_counter(
101                "cross_chain_message_channel_full",
102                "Cross-chain message channel full",
103            );
104
105        pub static NOTIFICATIONS_SKIPPED_RECEIVER_LAG: IntCounter =
106            register_int_counter(
107                "notifications_skipped_receiver_lag",
108                "Number of notifications skipped because receiver lagged behind sender",
109            );
110
111        pub static NOTIFICATIONS_DROPPED_NO_RECEIVER: IntCounter =
112            register_int_counter(
113                "notifications_dropped_no_receiver",
114                "Number of notifications dropped because no receiver was available",
115            );
116
117        pub static NOTIFICATION_BATCH_SIZE: Histogram =
118            register_histogram(
119                "notification_batch_size",
120                "Number of notifications per batch sent to proxy",
121                exponential_bucket_interval(1.0, 250.0),
122            );
123
124        pub static NOTIFICATION_BATCHES_SENT: IntCounterVec =
125            register_int_counter_vec(
126                "notification_batches_sent",
127                "Total notification batches sent",
128                &["status"],
129            );
130    }
131}
132
133/// Handles batched forwarding of notifications to proxy and exporters.
134struct BatchForwarder {
135    nickname: String,
136    client: NotifierServiceClient<Channel>,
137    exporter_clients: Vec<NotifierServiceClient<Channel>>,
138    pending_notifications: Vec<Notification>,
139    futures: FuturesUnordered<BoxFuture<'static, ()>>,
140    batch_limit: usize,
141    max_tasks: usize,
142}
143
144impl BatchForwarder {
145    /// Spawns batch send tasks up to max_tasks limit.
146    fn spawn_batches(&mut self) {
147        while !self.pending_notifications.is_empty() && self.futures.len() < self.max_tasks {
148            let chunk_size = std::cmp::min(self.batch_limit, self.pending_notifications.len());
149            let batch: Vec<Notification> = self.pending_notifications.drain(..chunk_size).collect();
150
151            #[cfg(with_metrics)]
152            metrics::NOTIFICATION_BATCH_SIZE.observe(batch.len() as f64);
153
154            let client = self.client.clone();
155            let exporter_clients = self.exporter_clients.clone();
156            let nickname = self.nickname.clone();
157
158            self.futures.push(
159                async move {
160                    Self::send_batch(nickname, client, exporter_clients, batch).await;
161                }
162                .boxed(),
163            );
164        }
165    }
166
167    /// Returns true if there are no pending notifications and no in-flight tasks.
168    fn is_fully_drained(&self) -> bool {
169        self.pending_notifications.is_empty() && self.futures.is_empty()
170    }
171
172    /// Sends a batch of notifications to the proxy and exporters.
173    async fn send_batch(
174        nickname: String,
175        mut client: NotifierServiceClient<Channel>,
176        mut exporter_clients: Vec<NotifierServiceClient<Channel>>,
177        batch: Vec<Notification>,
178    ) {
179        // Convert to proto notifications, logging any deserialization errors
180        let mut proto_notifications = Vec::with_capacity(batch.len());
181        for notification in &batch {
182            match notification.clone().try_into() {
183                Ok(proto) => proto_notifications.push(proto),
184                Err(error) => {
185                    warn!(
186                        %error,
187                        nickname,
188                        ?notification.chain_id,
189                        ?notification.reason,
190                        "could not deserialize notification"
191                    );
192                }
193            }
194        }
195
196        // Collect chain_ids for error logging
197        let chain_ids: Vec<_> = batch.iter().map(|n| n.chain_id).collect();
198
199        // Send batch to proxy
200        let request = Request::new(api::NotificationBatch {
201            notifications: proto_notifications.clone(),
202        });
203        let result = client.notify_batch(request).await;
204
205        #[cfg(with_metrics)]
206        {
207            let status = if result.is_ok() { "success" } else { "error" };
208            metrics::NOTIFICATION_BATCHES_SENT
209                .with_label_values(&[status])
210                .inc();
211        }
212
213        if let Err(error) = result {
214            error!(
215                %error,
216                nickname,
217                batch_size = proto_notifications.len(),
218                ?chain_ids,
219                "proxy: could not send notification batch",
220            );
221        }
222
223        // Send NewBlock notifications to exporters
224        let new_block_notifications: Vec<_> = batch
225            .iter()
226            .filter(|n| matches!(n.reason, Reason::NewBlock { .. }))
227            .collect();
228
229        let exporter_notifications: Vec<api::Notification> = new_block_notifications
230            .iter()
231            .filter_map(|n| (*n).clone().try_into().ok())
232            .collect();
233
234        if !exporter_notifications.is_empty() {
235            let exporter_chain_ids: Vec<_> =
236                new_block_notifications.iter().map(|n| n.chain_id).collect();
237
238            for exporter_client in &mut exporter_clients {
239                let request = Request::new(api::NotificationBatch {
240                    notifications: exporter_notifications.clone(),
241                });
242                if let Err(error) = exporter_client.notify_batch(request).await {
243                    error!(
244                        %error,
245                        nickname,
246                        batch_size = exporter_notifications.len(),
247                        ?exporter_chain_ids,
248                        "block exporter: could not send notification batch",
249                    );
250                }
251            }
252        }
253    }
254}
255
256/// A gRPC server exposing a validator's worker as a network service.
257#[derive(Clone)]
258pub struct GrpcServer<S>
259where
260    S: Storage,
261{
262    state: WorkerState<S>,
263    shard_id: ShardId,
264    network: ValidatorInternalNetworkConfig,
265    cross_chain_sender: CrossChainSender,
266    notification_sender: NotificationSender,
267}
268
269/// A handle to a running [`GrpcServer`] task.
270pub struct GrpcServerHandle {
271    handle: TaskHandle<Result<(), GrpcError>>,
272}
273
274impl GrpcServerHandle {
275    /// Waits for the server task to complete.
276    pub async fn join(self) -> Result<(), GrpcError> {
277        self.handle.await?
278    }
279}
280
281#[cfg(with_metrics)]
282struct ServerRequestCancellationGuard {
283    method_name: String,
284    traffic_type: &'static str,
285    completed: bool,
286}
287
288#[cfg(with_metrics)]
289impl Drop for ServerRequestCancellationGuard {
290    fn drop(&mut self) {
291        if !self.completed {
292            metrics::SERVER_REQUEST_CANCELLED
293                .with_label_values(&[&self.method_name, self.traffic_type])
294                .inc();
295        }
296    }
297}
298
299/// A Tower layer that records Prometheus metrics for gRPC requests.
300#[derive(Clone)]
301pub struct GrpcPrometheusMetricsMiddlewareLayer;
302
303/// The Tower service produced by [`GrpcPrometheusMetricsMiddlewareLayer`].
304#[derive(Clone)]
305pub struct GrpcPrometheusMetricsMiddlewareService<T> {
306    service: T,
307}
308
309impl<S> Layer<S> for GrpcPrometheusMetricsMiddlewareLayer {
310    type Service = GrpcPrometheusMetricsMiddlewareService<S>;
311
312    fn layer(&self, service: S) -> Self::Service {
313        GrpcPrometheusMetricsMiddlewareService { service }
314    }
315}
316
317impl<S, B> Service<http::Request<B>> for GrpcPrometheusMetricsMiddlewareService<S>
318where
319    S::Future: Send + 'static,
320    S: Service<http::Request<B>> + std::marker::Send,
321    B: Send + 'static,
322{
323    type Response = S::Response;
324    type Error = S::Error;
325    type Future = BoxFuture<'static, Result<S::Response, S::Error>>;
326
327    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
328        self.service.poll_ready(cx)
329    }
330
331    fn call(&mut self, request: http::Request<B>) -> Self::Future {
332        #[cfg(with_metrics)]
333        let start = Instant::now();
334
335        #[cfg(with_metrics)]
336        let method_name = super::extract_grpc_method_name(request.uri().path()).to_owned();
337
338        // Extract traffic type from request extensions (set by OtelContextLayer).
339        // When opentelemetry is enabled but no baggage is set, defaults to "organic".
340        // When opentelemetry is disabled, defaults to "unknown".
341        #[cfg(all(with_metrics, feature = "opentelemetry"))]
342        let traffic_type: &'static str = get_traffic_type_from_request(&request);
343        #[cfg(all(with_metrics, not(feature = "opentelemetry")))]
344        let traffic_type: &'static str = "unknown";
345
346        let future = self.service.call(request);
347        async move {
348            #[cfg(with_metrics)]
349            let mut cancellation_guard = ServerRequestCancellationGuard {
350                method_name,
351                traffic_type,
352                completed: false,
353            };
354            let response = future.await?;
355            #[cfg(with_metrics)]
356            {
357                cancellation_guard.completed = true;
358                metrics::SERVER_REQUEST_LATENCY
359                    .with_label_values(&[&cancellation_guard.method_name, traffic_type])
360                    .observe(start.elapsed().as_secs_f64() * 1000.0);
361                metrics::SERVER_REQUEST_COUNT
362                    .with_label_values(&[&cancellation_guard.method_name, traffic_type])
363                    .inc();
364            }
365            Ok(response)
366        }
367        .boxed()
368    }
369}
370
371impl<S> GrpcServer<S>
372where
373    S: Storage + Clone + Send + Sync + 'static,
374{
375    /// Spawns the gRPC server on the given host and port, returning a handle to the task.
376    #[expect(clippy::too_many_arguments)]
377    pub fn spawn(
378        host: String,
379        port: u16,
380        state: WorkerState<S>,
381        shard_id: ShardId,
382        internal_network: ValidatorInternalNetworkConfig,
383        cross_chain_config: &CrossChainConfig,
384        notification_config: &NotificationConfig,
385        shutdown_signal: CancellationToken,
386        join_set: &mut JoinSet,
387    ) -> GrpcServerHandle {
388        info!(
389            "spawning gRPC server on {}:{} for shard {}",
390            host, port, shard_id
391        );
392
393        let (cross_chain_sender, cross_chain_receiver) =
394            mpsc::channel(cross_chain_config.queue_size);
395
396        // Give the worker a shard-routing sender for cross-chain requests generated
397        // outside the normal `NetworkActions` return path (specifically, the
398        // `RevertConfirm`s emitted after resetting a corrupted chain).
399        let state = {
400            let routing_network = internal_network.clone();
401            let routing_sender = cross_chain_sender.clone();
402            state.with_outbound_cross_chain_sender(std::sync::Arc::new(move |request| {
403                let shard_id = routing_network.get_shard_id(request.target_chain_id());
404                if let Err(error) = routing_sender.clone().try_send((request, shard_id)) {
405                    error!(%error, "dropping cross-chain request");
406                }
407            }))
408        };
409
410        let (notification_sender, _) =
411            tokio::sync::broadcast::channel(notification_config.notification_queue_size);
412
413        join_set.spawn_task({
414            info!(
415                nickname = state.nickname(),
416                "spawning cross-chain queries thread on {} for shard {}", host, shard_id
417            );
418            Self::forward_cross_chain_queries(
419                state.nickname().to_string(),
420                internal_network.clone(),
421                cross_chain_config.max_retries,
422                Duration::from_millis(cross_chain_config.retry_delay_ms),
423                Duration::from_millis(cross_chain_config.max_backoff_ms),
424                Duration::from_millis(cross_chain_config.sender_delay_ms),
425                cross_chain_config.sender_failure_rate,
426                shard_id,
427                cross_chain_receiver,
428            )
429        });
430
431        let mut exporter_forwarded = false;
432        for proxy in &internal_network.proxies {
433            let receiver = notification_sender.subscribe();
434            join_set.spawn_task({
435                info!(
436                    nickname = state.nickname(),
437                    "spawning notifications thread on {} for shard {}", host, shard_id
438                );
439                let exporter_addresses = if exporter_forwarded {
440                    vec![]
441                } else {
442                    exporter_forwarded = true;
443                    internal_network.exporter_addresses()
444                };
445                Self::forward_notifications(
446                    state.nickname().to_string(),
447                    proxy.internal_address(&internal_network.protocol),
448                    exporter_addresses,
449                    receiver,
450                    notification_config.clone(),
451                )
452            });
453        }
454
455        let (health_reporter, health_service) = tonic_health::server::health_reporter();
456
457        let grpc_server = GrpcServer {
458            state,
459            shard_id,
460            network: internal_network,
461            cross_chain_sender,
462            notification_sender,
463        };
464
465        let worker_node = ValidatorWorkerServer::new(grpc_server)
466            .max_encoding_message_size(GRPC_MAX_MESSAGE_SIZE)
467            .max_decoding_message_size(GRPC_MAX_MESSAGE_SIZE);
468
469        let handle = join_set.spawn_task(async move {
470            let server_address = SocketAddr::from((IpAddr::from_str(&host)?, port));
471
472            let reflection_service = tonic_reflection::server::Builder::configure()
473                .register_encoded_file_descriptor_set(crate::FILE_DESCRIPTOR_SET)
474                .build_v1()?;
475
476            health_reporter
477                .set_serving::<ValidatorWorkerServer<Self>>()
478                .await;
479
480            #[cfg(feature = "opentelemetry")]
481            let server = tonic::transport::Server::builder().layer(
482                ServiceBuilder::new()
483                    .layer(crate::propagation::OtelContextLayer)
484                    .layer(GrpcPrometheusMetricsMiddlewareLayer)
485                    .into_inner(),
486            );
487            #[cfg(not(feature = "opentelemetry"))]
488            let server = tonic::transport::Server::builder().layer(
489                ServiceBuilder::new()
490                    .layer(GrpcPrometheusMetricsMiddlewareLayer)
491                    .into_inner(),
492            );
493            server
494                // Every dialer is part of this validator (proxies, peer cross-chain
495                // forwarders), so this cap times a known connection count bounds
496                // in-flight work -- unlike hyper's 200 default, fixed at any shard size.
497                .max_concurrent_streams(Some(10_000))
498                .add_service(health_service)
499                .add_service(reflection_service)
500                .add_service(worker_node)
501                .serve_with_shutdown(server_address, shutdown_signal.cancelled_owned())
502                .await?;
503
504            Ok(())
505        });
506
507        GrpcServerHandle { handle }
508    }
509
510    /// Continuously waits for receiver to receive notifications and sends them to
511    /// the proxy in batches for improved throughput.
512    #[instrument(skip(receiver, config))]
513    async fn forward_notifications(
514        nickname: String,
515        proxy_address: String,
516        exporter_addresses: Vec<String>,
517        mut receiver: tokio::sync::broadcast::Receiver<Notification>,
518        config: NotificationConfig,
519    ) {
520        let channel = tonic::transport::Channel::from_shared(proxy_address.clone())
521            .expect("Proxy URI should be valid")
522            .connect_lazy();
523        let client = NotifierServiceClient::new(channel)
524            .max_encoding_message_size(GRPC_MAX_MESSAGE_SIZE)
525            .max_decoding_message_size(GRPC_MAX_MESSAGE_SIZE);
526
527        let exporter_clients: Vec<NotifierServiceClient<Channel>> = exporter_addresses
528            .iter()
529            .map(|address| {
530                let channel = tonic::transport::Channel::from_shared(address.clone())
531                    .expect("Exporter URI should be valid")
532                    .connect_lazy();
533                NotifierServiceClient::new(channel)
534                    .max_encoding_message_size(GRPC_MAX_MESSAGE_SIZE)
535                    .max_decoding_message_size(GRPC_MAX_MESSAGE_SIZE)
536            })
537            .collect::<Vec<_>>();
538
539        let mut forwarder = BatchForwarder {
540            nickname: nickname.clone(),
541            client,
542            exporter_clients,
543            pending_notifications: Vec::new(),
544            futures: FuturesUnordered::new(),
545            batch_limit: config.notification_batch_size,
546            max_tasks: config.notification_max_in_flight,
547        };
548
549        loop {
550            tokio::select! {
551                biased;
552
553                result = receiver.recv() => {
554                    match result {
555                        Ok(notification) => {
556                            forwarder.pending_notifications.push(notification);
557
558                            if forwarder.futures.is_empty()
559                               || (forwarder.pending_notifications.len() >= forwarder.batch_limit
560                                   && forwarder.futures.len() < forwarder.max_tasks) {
561                                forwarder.spawn_batches();
562                            }
563                        }
564                        Err(RecvError::Lagged(skipped_count)) => {
565                            warn!(
566                                nickname,
567                                skipped_count, "notification receiver lagged, messages were skipped"
568                            );
569                            #[cfg(with_metrics)]
570                            metrics::NOTIFICATIONS_SKIPPED_RECEIVER_LAG
571                                .inc_by(skipped_count);
572                        }
573                        Err(RecvError::Closed) => {
574                            warn!(
575                                nickname,
576                                "notification channel closed, draining pending notifications"
577                            );
578                            // Drain all pending notifications before exiting
579                            loop {
580                                forwarder.spawn_batches();
581                                if forwarder.is_fully_drained() {
582                                    break;
583                                }
584                                forwarder.futures.next().await;
585                            }
586                            break;
587                        }
588                    }
589                }
590
591                Some(()) = forwarder.futures.next() => {
592                    forwarder.spawn_batches();
593                }
594            }
595        }
596    }
597
598    fn handle_network_actions(&self, actions: NetworkActions) {
599        let mut cross_chain_sender = self.cross_chain_sender.clone();
600        let notification_sender = self.notification_sender.clone();
601
602        for request in actions.cross_chain_requests {
603            let shard_id = self.network.get_shard_id(request.target_chain_id());
604            trace!(
605                source_shard_id = self.shard_id,
606                target_shard_id = shard_id,
607                "Scheduling cross-chain query",
608            );
609
610            if let Err(error) = cross_chain_sender.try_send((request, shard_id)) {
611                error!(%error, "dropping cross-chain request");
612                #[cfg(with_metrics)]
613                if error.is_full() {
614                    metrics::CROSS_CHAIN_MESSAGE_CHANNEL_FULL.inc();
615                }
616            }
617        }
618
619        for notification in actions.notifications {
620            trace!("Scheduling notification query");
621            if let Err(error) = notification_sender.send(notification) {
622                error!(%error, "dropping notification");
623                #[cfg(with_metrics)]
624                metrics::NOTIFICATIONS_DROPPED_NO_RECEIVER.inc();
625            }
626        }
627    }
628
629    #[instrument(skip_all, fields(nickname, %this_shard))]
630    #[expect(clippy::too_many_arguments)]
631    async fn forward_cross_chain_queries(
632        nickname: String,
633        network: ValidatorInternalNetworkConfig,
634        cross_chain_max_retries: u32,
635        cross_chain_retry_delay: Duration,
636        cross_chain_max_backoff: Duration,
637        cross_chain_sender_delay: Duration,
638        cross_chain_sender_failure_rate: f32,
639        this_shard: ShardId,
640        receiver: mpsc::Receiver<(linera_core::data_types::CrossChainRequest, ShardId)>,
641    ) {
642        let pool = GrpcConnectionPool::default();
643        let handle_request =
644            move |shard_id: ShardId, request: linera_core::data_types::CrossChainRequest| {
645                let channel_result = pool.channel(network.shard(shard_id).http_address());
646                async move {
647                    let mut client = ValidatorWorkerClient::new(channel_result?)
648                        .max_encoding_message_size(GRPC_MAX_MESSAGE_SIZE)
649                        .max_decoding_message_size(GRPC_MAX_MESSAGE_SIZE);
650                    client
651                        .handle_cross_chain_request(Request::new(request.try_into()?))
652                        .await?;
653                    anyhow::Result::<_, anyhow::Error>::Ok(())
654                }
655            };
656        cross_chain_message_queue::forward_cross_chain_queries(
657            nickname,
658            cross_chain_max_retries,
659            cross_chain_retry_delay,
660            cross_chain_max_backoff,
661            cross_chain_sender_delay,
662            cross_chain_sender_failure_rate,
663            this_shard,
664            receiver,
665            handle_request,
666        )
667        .await;
668    }
669
670    fn log_request_success(method_name: &str, traffic_type: &str) {
671        #![cfg_attr(not(with_metrics), allow(unused_variables))]
672        #[cfg(with_metrics)]
673        metrics::SERVER_REQUEST_SUCCESS
674            .with_label_values(&[method_name, traffic_type])
675            .inc();
676    }
677
678    fn log_request_error(method_name: &str, traffic_type: &str, error_type: &str) {
679        #![cfg_attr(not(with_metrics), allow(unused_variables))]
680        #[cfg(with_metrics)]
681        metrics::SERVER_REQUEST_ERROR
682            .with_label_values(&[method_name, traffic_type, error_type])
683            .inc();
684    }
685
686    /// Extracts traffic type from a tonic request's extensions.
687    #[cfg(feature = "opentelemetry")]
688    fn get_traffic_type<R>(request: &Request<R>) -> &'static str {
689        get_traffic_type_from_request(request)
690    }
691
692    /// Returns "unknown" when opentelemetry feature is disabled.
693    #[cfg(not(feature = "opentelemetry"))]
694    fn get_traffic_type<R>(_request: &Request<R>) -> &'static str {
695        "unknown"
696    }
697
698    fn log_error(&self, error: &linera_core::worker::WorkerError, context: &str) {
699        let nickname = self.state.nickname();
700        if error.is_local() {
701            error!(nickname, %error, "{}", context);
702        } else {
703            debug!(nickname, %error, "{}", context);
704        }
705    }
706}
707
708#[tonic::async_trait]
709impl<S> ValidatorWorkerRpc for GrpcServer<S>
710where
711    S: Storage + Clone + Send + Sync + 'static,
712{
713    #[instrument(
714        target = "grpc_server",
715        skip_all,
716        err,
717        fields(
718            nickname = self.state.nickname(),
719            chain_id = ?request.get_ref().chain_id()
720        )
721    )]
722    async fn handle_block_proposal(
723        &self,
724        request: Request<BlockProposal>,
725    ) -> Result<Response<ChainInfoResult>, Status> {
726        let traffic_type = Self::get_traffic_type(&request);
727        let proposal = request.into_inner().try_into()?;
728        trace!(?proposal, "Handling block proposal");
729        let (result, actions) = self.state.clone().handle_block_proposal(proposal).await;
730        // Dispatch actions whether or not the proposal was accepted: a rejected
731        // proposal can still advance the manager's `current_round` (via
732        // `update_signed_proposal` on the `HasIncompatibleConfirmedVote` recovery
733        // path), and subscribers need the resulting `NewRound` notification.
734        self.handle_network_actions(actions);
735        Ok(Response::new(match result {
736            Ok(info) => {
737                Self::log_request_success("handle_block_proposal", traffic_type);
738                info.try_into()?
739            }
740            Err(error) => {
741                Self::log_request_error("handle_block_proposal", traffic_type, &error.error_type());
742                self.log_error(&error, "Failed to handle block proposal");
743                NodeError::from(error).try_into()?
744            }
745        }))
746    }
747
748    #[instrument(
749        target = "grpc_server",
750        skip_all,
751        err,
752        fields(
753            nickname = self.state.nickname(),
754            chain_id = ?request.get_ref().chain_id()
755        )
756    )]
757    async fn handle_lite_certificate(
758        &self,
759        request: Request<LiteCertificate>,
760    ) -> Result<Response<ChainInfoResult>, Status> {
761        let traffic_type = Self::get_traffic_type(&request);
762        let HandleLiteCertRequest {
763            certificate,
764            wait_for_outgoing_messages,
765        } = request.into_inner().try_into()?;
766        trace!(?certificate, "Handling lite certificate");
767        let (sender, receiver) = wait_for_outgoing_messages.then(oneshot::channel).unzip();
768        match Box::pin(
769            self.state
770                .clone()
771                .handle_lite_certificate(certificate, sender),
772        )
773        .await
774        {
775            Ok((info, actions)) => {
776                Self::log_request_success("handle_lite_certificate", traffic_type);
777                self.handle_network_actions(actions);
778                if let Some(receiver) = receiver {
779                    if let Err(e) = receiver.await {
780                        error!("Failed to wait for message delivery: {e}");
781                    }
782                }
783                Ok(Response::new(info.try_into()?))
784            }
785            Err(error) => {
786                Self::log_request_error(
787                    "handle_lite_certificate",
788                    traffic_type,
789                    &error.error_type(),
790                );
791                self.log_error(&error, "Failed to handle lite certificate");
792                Ok(Response::new(NodeError::from(error).try_into()?))
793            }
794        }
795    }
796
797    #[instrument(
798        target = "grpc_server",
799        skip_all,
800        err,
801        fields(
802            nickname = self.state.nickname(),
803            chain_id = ?request.get_ref().chain_id()
804        )
805    )]
806    async fn handle_confirmed_certificate(
807        &self,
808        request: Request<api::HandleConfirmedCertificateRequest>,
809    ) -> Result<Response<ChainInfoResult>, Status> {
810        let traffic_type = Self::get_traffic_type(&request);
811        let HandleConfirmedCertificateRequest {
812            certificate,
813            wait_for_outgoing_messages,
814        } = request.into_inner().try_into()?;
815        trace!(?certificate, "Handling certificate");
816        let (sender, receiver) = wait_for_outgoing_messages.then(oneshot::channel).unzip();
817        match self
818            .state
819            .clone()
820            .handle_confirmed_certificate(certificate, ProcessConfirmedBlockMode::Auto, sender)
821            .await
822        {
823            Ok((info, actions)) => {
824                Self::log_request_success("handle_confirmed_certificate", traffic_type);
825                self.handle_network_actions(actions);
826                if let Some(receiver) = receiver {
827                    if let Err(e) = receiver.await {
828                        error!("Failed to wait for message delivery: {e}");
829                    }
830                }
831                Ok(Response::new(info.try_into()?))
832            }
833            Err(error) => {
834                Self::log_request_error(
835                    "handle_confirmed_certificate",
836                    traffic_type,
837                    &error.error_type(),
838                );
839                self.log_error(&error, "Failed to handle confirmed certificate");
840                Ok(Response::new(NodeError::from(error).try_into()?))
841            }
842        }
843    }
844
845    #[instrument(
846        target = "grpc_server",
847        skip_all,
848        err,
849        fields(
850            nickname = self.state.nickname(),
851            chain_id = ?request.get_ref().chain_id()
852        )
853    )]
854    async fn handle_validated_certificate(
855        &self,
856        request: Request<api::HandleValidatedCertificateRequest>,
857    ) -> Result<Response<ChainInfoResult>, Status> {
858        let traffic_type = Self::get_traffic_type(&request);
859        let HandleValidatedCertificateRequest { certificate } = request.into_inner().try_into()?;
860        trace!(?certificate, "Handling certificate");
861        match self
862            .state
863            .clone()
864            .handle_validated_certificate(certificate)
865            .await
866        {
867            Ok((info, actions)) => {
868                Self::log_request_success("handle_validated_certificate", traffic_type);
869                self.handle_network_actions(actions);
870                Ok(Response::new(info.try_into()?))
871            }
872            Err(error) => {
873                Self::log_request_error(
874                    "handle_validated_certificate",
875                    traffic_type,
876                    &error.error_type(),
877                );
878                self.log_error(&error, "Failed to handle validated certificate");
879                Ok(Response::new(NodeError::from(error).try_into()?))
880            }
881        }
882    }
883
884    #[instrument(
885        target = "grpc_server",
886        skip_all,
887        err,
888        fields(
889            nickname = self.state.nickname(),
890            chain_id = ?request.get_ref().chain_id()
891        )
892    )]
893    async fn handle_timeout_certificate(
894        &self,
895        request: Request<api::HandleTimeoutCertificateRequest>,
896    ) -> Result<Response<ChainInfoResult>, Status> {
897        let traffic_type = Self::get_traffic_type(&request);
898        let HandleTimeoutCertificateRequest { certificate } = request.into_inner().try_into()?;
899        trace!(?certificate, "Handling Timeout certificate");
900        match self
901            .state
902            .clone()
903            .handle_timeout_certificate(certificate)
904            .await
905        {
906            Ok((info, _actions)) => {
907                Self::log_request_success("handle_timeout_certificate", traffic_type);
908                Ok(Response::new(info.try_into()?))
909            }
910            Err(error) => {
911                Self::log_request_error(
912                    "handle_timeout_certificate",
913                    traffic_type,
914                    &error.error_type(),
915                );
916                self.log_error(&error, "Failed to handle timeout certificate");
917                Ok(Response::new(NodeError::from(error).try_into()?))
918            }
919        }
920    }
921
922    #[instrument(
923        target = "grpc_server",
924        skip_all,
925        err,
926        fields(
927            nickname = self.state.nickname(),
928            chain_id = ?request.get_ref().chain_id()
929        )
930    )]
931    async fn handle_chain_info_query(
932        &self,
933        request: Request<ChainInfoQuery>,
934    ) -> Result<Response<ChainInfoResult>, Status> {
935        let traffic_type = Self::get_traffic_type(&request);
936        let query = request.into_inner().try_into()?;
937        trace!(?query, "Handling chain info query");
938        match self.state.clone().handle_chain_info_query(query).await {
939            Ok(info) => {
940                Self::log_request_success("handle_chain_info_query", traffic_type);
941                Ok(Response::new(info.try_into()?))
942            }
943            Err(error) => {
944                Self::log_request_error(
945                    "handle_chain_info_query",
946                    traffic_type,
947                    &error.error_type(),
948                );
949                self.log_error(&error, "Failed to handle chain info query");
950                Ok(Response::new(NodeError::from(error).try_into()?))
951            }
952        }
953    }
954
955    #[instrument(
956        target = "grpc_server",
957        skip_all,
958        err,
959        fields(
960            nickname = self.state.nickname(),
961            chain_id = ?request.get_ref().chain_id()
962        )
963    )]
964    async fn download_pending_blob(
965        &self,
966        request: Request<PendingBlobRequest>,
967    ) -> Result<Response<PendingBlobResult>, Status> {
968        let traffic_type = Self::get_traffic_type(&request);
969        let (chain_id, blob_id) = request.into_inner().try_into()?;
970        trace!(?blob_id, "Download pending blob");
971        match self
972            .state
973            .clone()
974            .download_pending_blob(chain_id, blob_id)
975            .await
976        {
977            Ok(blob) => {
978                Self::log_request_success("download_pending_blob", traffic_type);
979                Ok(Response::new(blob.content().clone().try_into()?))
980            }
981            Err(error) => {
982                Self::log_request_error("download_pending_blob", traffic_type, &error.error_type());
983                self.log_error(&error, "Failed to download pending blob");
984                Ok(Response::new(NodeError::from(error).try_into()?))
985            }
986        }
987    }
988
989    #[instrument(
990        target = "grpc_server",
991        skip_all,
992        err,
993        fields(
994            nickname = self.state.nickname(),
995            chain_id = ?request.get_ref().chain_id()
996        )
997    )]
998    async fn handle_pending_blob(
999        &self,
1000        request: Request<HandlePendingBlobRequest>,
1001    ) -> Result<Response<ChainInfoResult>, Status> {
1002        let traffic_type = Self::get_traffic_type(&request);
1003        let (chain_id, blob_content) = request.into_inner().try_into()?;
1004        let blob = Blob::new(blob_content);
1005        let blob_id = blob.id();
1006        trace!(?blob_id, "Handle pending blob");
1007        match self.state.clone().handle_pending_blob(chain_id, blob).await {
1008            Ok(info) => {
1009                Self::log_request_success("handle_pending_blob", traffic_type);
1010                Ok(Response::new(info.try_into()?))
1011            }
1012            Err(error) => {
1013                Self::log_request_error("handle_pending_blob", traffic_type, &error.error_type());
1014                self.log_error(&error, "Failed to handle pending blob");
1015                Ok(Response::new(NodeError::from(error).try_into()?))
1016            }
1017        }
1018    }
1019
1020    #[instrument(
1021        target = "grpc_server",
1022        skip_all,
1023        err,
1024        fields(
1025            nickname = self.state.nickname(),
1026            chain_id = ?request.get_ref().chain_id()
1027        )
1028    )]
1029    async fn handle_cross_chain_request(
1030        &self,
1031        request: Request<CrossChainRequest>,
1032    ) -> Result<Response<()>, Status> {
1033        let traffic_type = Self::get_traffic_type(&request);
1034        let cross_chain_request = request.into_inner().try_into()?;
1035        trace!(?cross_chain_request, "Handling cross-chain request");
1036        match self
1037            .state
1038            .clone()
1039            .handle_cross_chain_request(cross_chain_request)
1040            .await
1041        {
1042            Ok(actions) => {
1043                Self::log_request_success("handle_cross_chain_request", traffic_type);
1044                self.handle_network_actions(actions)
1045            }
1046            Err(error) => {
1047                Self::log_request_error(
1048                    "handle_cross_chain_request",
1049                    traffic_type,
1050                    &error.error_type(),
1051                );
1052                self.log_error(&error, "Failed to handle cross-chain request");
1053            }
1054        }
1055        Ok(Response::new(()))
1056    }
1057}
1058
1059/// Types which are proxyable and expose the appropriate methods to be handled
1060/// by the `GrpcProxy`
1061pub trait GrpcProxyable {
1062    /// Returns the chain ID this message is destined for, if any.
1063    fn chain_id(&self) -> Option<ChainId>;
1064}
1065
1066impl GrpcProxyable for BlockProposal {
1067    fn chain_id(&self) -> Option<ChainId> {
1068        self.chain_id.clone()?.try_into().ok()
1069    }
1070}
1071
1072impl GrpcProxyable for LiteCertificate {
1073    fn chain_id(&self) -> Option<ChainId> {
1074        self.chain_id.clone()?.try_into().ok()
1075    }
1076}
1077
1078impl GrpcProxyable for api::HandleConfirmedCertificateRequest {
1079    fn chain_id(&self) -> Option<ChainId> {
1080        self.chain_id.clone()?.try_into().ok()
1081    }
1082}
1083
1084impl GrpcProxyable for api::HandleTimeoutCertificateRequest {
1085    fn chain_id(&self) -> Option<ChainId> {
1086        self.chain_id.clone()?.try_into().ok()
1087    }
1088}
1089
1090impl GrpcProxyable for api::HandleValidatedCertificateRequest {
1091    fn chain_id(&self) -> Option<ChainId> {
1092        self.chain_id.clone()?.try_into().ok()
1093    }
1094}
1095
1096impl GrpcProxyable for ChainInfoQuery {
1097    fn chain_id(&self) -> Option<ChainId> {
1098        self.chain_id.clone()?.try_into().ok()
1099    }
1100}
1101
1102impl GrpcProxyable for PendingBlobRequest {
1103    fn chain_id(&self) -> Option<ChainId> {
1104        self.chain_id.clone()?.try_into().ok()
1105    }
1106}
1107
1108impl GrpcProxyable for HandlePendingBlobRequest {
1109    fn chain_id(&self) -> Option<ChainId> {
1110        self.chain_id.clone()?.try_into().ok()
1111    }
1112}
1113
1114impl GrpcProxyable for CrossChainRequest {
1115    fn chain_id(&self) -> Option<ChainId> {
1116        use super::api::cross_chain_request::Inner;
1117
1118        match self.inner.as_ref()? {
1119            Inner::UpdateRecipient(api::UpdateRecipient { recipient, .. })
1120            | Inner::ConfirmUpdatedRecipient(api::ConfirmUpdatedRecipient { recipient, .. })
1121            | Inner::RevertConfirm(api::RevertConfirm { recipient, .. }) => {
1122                recipient.clone()?.try_into().ok()
1123            }
1124        }
1125    }
1126}