Skip to main content

iota_core/
safe_client.rs

1// Copyright (c) 2021, Facebook, Inc. and its affiliates
2// Copyright (c) Mysten Labs, Inc.
3// Modifications Copyright (c) 2024 IOTA Stiftung
4// SPDX-License-Identifier: Apache-2.0
5
6use std::{collections::HashMap, net::SocketAddr, sync::Arc};
7
8use iota_sdk_types::{TransactionDigest, TransactionEffectsDigest};
9use iota_types::{
10    base_types::*,
11    committee::*,
12    crypto::AuthorityPublicKeyBytes,
13    effects::{SignedTransactionEffects, TransactionEffectsAPI, TransactionEffectsExt},
14    error::{IotaError, IotaResult},
15    fp_ensure,
16    iota_system_state::IotaSystemState,
17    messages_checkpoint::{CheckpointRequest, CheckpointResponse},
18    messages_grpc::{
19        GetTxStatusRequest, HandleCapabilityNotificationRequestV1,
20        HandleCapabilityNotificationResponseV1, HandleCertificateRequestV1,
21        HandleCertificateResponseV1, ObjectInfoRequest, ObjectInfoResponse, SystemStateRequest,
22        TransactionInfoRequest, TransactionStatus, TxStatusUpdate, ValidatorHealthRequest,
23        ValidatorHealthResponse, VerifiedObjectInfoResponse,
24    },
25    messages_safe_client::PlainTransactionInfoResponse,
26    transaction::*,
27};
28use prometheus_filtered::{
29    Histogram, HistogramVec, IntCounterVec, Registry, core::GenericCounter,
30    register_histogram_vec_with_registry, register_int_counter_vec_with_registry,
31};
32use tap::TapFallible;
33use tracing::{debug, error, instrument};
34
35use crate::{authority_client::AuthorityAPI, epoch::committee_store::CommitteeStore};
36
37macro_rules! check_error {
38    ($address:expr, $cond:expr, $msg:expr) => {
39        $cond.tap_err(|err| {
40            if err.individual_error_indicates_epoch_change() {
41                debug!(?err, authority=?$address, "Not a real client error");
42            } else {
43                error!(?err, authority=?$address, $msg);
44            }
45        })
46    }
47}
48
49#[derive(Clone)]
50pub struct SafeClientMetricsBase {
51    total_requests_by_address_method: IntCounterVec,
52    total_responses_by_address_method: IntCounterVec,
53    latency: HistogramVec,
54}
55
56impl SafeClientMetricsBase {
57    pub fn new(registry: &Registry) -> Self {
58        Self {
59            total_requests_by_address_method: register_int_counter_vec_with_registry!(
60                "safe_client_total_requests_by_address_method",
61                "Total requests to validators group by address and method",
62                &["address", "method"],
63                registry,
64            )
65            .unwrap(),
66            total_responses_by_address_method: register_int_counter_vec_with_registry!(
67                "safe_client_total_responses_by_address_method",
68                "Total good (OK) responses from validators group by address and method",
69                &["address", "method"],
70                registry,
71            )
72            .unwrap(),
73            // Address label is removed to reduce high cardinality, can be added back if needed
74            latency: register_histogram_vec_with_registry!(
75                "safe_client_latency",
76                "RPC latency observed by safe client aggregator, group by method",
77                &["method"],
78                iota_metrics::COARSE_LATENCY_SEC_BUCKETS.to_vec(),
79                registry,
80            )
81            .unwrap(),
82        }
83    }
84}
85
86/// Prometheus metrics which can be displayed in Grafana, queried and alerted on
87#[derive(Clone)]
88pub struct SafeClientMetrics {
89    total_requests_handle_transaction_info_request:
90        GenericCounter<prometheus_filtered::core::AtomicU64>,
91    total_ok_responses_handle_transaction_info_request:
92        GenericCounter<prometheus_filtered::core::AtomicU64>,
93    total_requests_handle_object_info_request: GenericCounter<prometheus_filtered::core::AtomicU64>,
94    total_ok_responses_handle_object_info_request:
95        GenericCounter<prometheus_filtered::core::AtomicU64>,
96    handle_transaction_latency: Histogram,
97    handle_certificate_latency: Histogram,
98    handle_obj_info_latency: Histogram,
99    handle_tx_info_latency: Histogram,
100    submit_tx_latency: Histogram,
101    get_tx_status_latency: Histogram,
102    notify_capabilities_v2_latency: Histogram,
103    health_check_latency: Histogram,
104    get_checkpoint_v2_latency: Histogram,
105}
106
107impl SafeClientMetrics {
108    pub fn new(metrics_base: &SafeClientMetricsBase, validator_address: AuthorityName) -> Self {
109        let validator_address = validator_address.to_string();
110
111        let total_requests_handle_transaction_info_request = metrics_base
112            .total_requests_by_address_method
113            .with_label_values(&[
114                validator_address.as_str(),
115                "handle_transaction_info_request",
116            ]);
117        let total_ok_responses_handle_transaction_info_request = metrics_base
118            .total_responses_by_address_method
119            .with_label_values(&[
120                validator_address.as_str(),
121                "handle_transaction_info_request",
122            ]);
123
124        let total_requests_handle_object_info_request = metrics_base
125            .total_requests_by_address_method
126            .with_label_values(&[validator_address.as_str(), "handle_object_info_request"]);
127        let total_ok_responses_handle_object_info_request = metrics_base
128            .total_responses_by_address_method
129            .with_label_values(&[validator_address.as_str(), "handle_object_info_request"]);
130
131        let handle_transaction_latency = metrics_base
132            .latency
133            .with_label_values(&["handle_transaction"]);
134        let handle_certificate_latency = metrics_base
135            .latency
136            .with_label_values(&["handle_certificate"]);
137        let handle_obj_info_latency = metrics_base
138            .latency
139            .with_label_values(&["handle_object_info_request"]);
140        let handle_tx_info_latency = metrics_base
141            .latency
142            .with_label_values(&["handle_transaction_info_request"]);
143        let submit_tx_latency = metrics_base.latency.with_label_values(&["submit_tx"]);
144        let get_tx_status_latency = metrics_base.latency.with_label_values(&["get_tx_status"]);
145        let notify_capabilities_v2_latency = metrics_base
146            .latency
147            .with_label_values(&["notify_capabilities_v2"]);
148        let health_check_latency = metrics_base.latency.with_label_values(&["health_check"]);
149        let get_checkpoint_v2_latency = metrics_base
150            .latency
151            .with_label_values(&["get_checkpoint_v2"]);
152
153        Self {
154            total_requests_handle_transaction_info_request,
155            total_ok_responses_handle_transaction_info_request,
156            total_requests_handle_object_info_request,
157            total_ok_responses_handle_object_info_request,
158            handle_transaction_latency,
159            handle_certificate_latency,
160            handle_obj_info_latency,
161            handle_tx_info_latency,
162            submit_tx_latency,
163            get_tx_status_latency,
164            notify_capabilities_v2_latency,
165            health_check_latency,
166            get_checkpoint_v2_latency,
167        }
168    }
169
170    pub fn new_for_tests(validator_address: AuthorityName) -> Self {
171        let registry = Registry::new();
172        let metrics_base = SafeClientMetricsBase::new(&registry);
173        Self::new(&metrics_base, validator_address)
174    }
175}
176
177/// See `SafeClientMetrics::new` for description of each metrics.
178/// The metrics are per validator client.
179#[derive(Clone)]
180pub struct SafeClient<C> {
181    authority_client: C,
182    committee_store: Arc<CommitteeStore>,
183    address: AuthorityPublicKeyBytes,
184    metrics: SafeClientMetrics,
185}
186
187impl<C> SafeClient<C> {
188    pub fn new(
189        authority_client: C,
190        committee_store: Arc<CommitteeStore>,
191        address: AuthorityPublicKeyBytes,
192        metrics: SafeClientMetrics,
193    ) -> Self {
194        Self {
195            authority_client,
196            committee_store,
197            address,
198            metrics,
199        }
200    }
201
202    pub fn authority_client(&self) -> &C {
203        &self.authority_client
204    }
205
206    #[cfg(test)]
207    pub fn authority_client_mut(&mut self) -> &mut C {
208        &mut self.authority_client
209    }
210
211    fn get_committee(&self, epoch_id: &EpochId) -> IotaResult<Arc<Committee>> {
212        self.committee_store
213            .get_committee(epoch_id)?
214            .ok_or(IotaError::MissingCommitteeAtEpoch(*epoch_id))
215    }
216
217    fn check_signed_effects_plain(
218        &self,
219        digest: &TransactionDigest,
220        signed_effects: SignedTransactionEffects,
221        expected_effects_digest: Option<&TransactionEffectsDigest>,
222    ) -> IotaResult<SignedTransactionEffects> {
223        // Check it has the right signer
224        fp_ensure!(
225            signed_effects.auth_sig().authority == self.address,
226            IotaError::ByzantineAuthoritySuspicion {
227                authority: self.address,
228                reason: format!(
229                    "Unexpected validator address in the signed effects signature: {:?}",
230                    signed_effects.auth_sig().authority
231                ),
232            }
233        );
234        // Checks it concerns the right tx
235        fp_ensure!(
236            signed_effects.data().transaction_digest() == digest,
237            IotaError::ByzantineAuthoritySuspicion {
238                authority: self.address,
239                reason: "Unexpected tx digest in the signed effects".to_string()
240            }
241        );
242        // check that the effects digest is correct.
243        if let Some(effects_digest) = expected_effects_digest {
244            fp_ensure!(
245                signed_effects.digest() == effects_digest,
246                IotaError::ByzantineAuthoritySuspicion {
247                    authority: self.address,
248                    reason: "Effects digest does not match with expected digest".to_string()
249                }
250            );
251        }
252        self.get_committee(&signed_effects.epoch())?;
253        Ok(signed_effects)
254    }
255
256    fn check_transaction_info(
257        &self,
258        digest: &TransactionDigest,
259        transaction: TransactionEnvelope,
260        status: TransactionStatus,
261    ) -> IotaResult<PlainTransactionInfoResponse> {
262        fp_ensure!(
263            digest == transaction.digest(),
264            IotaError::ByzantineAuthoritySuspicion {
265                authority: self.address,
266                reason: "Signed transaction digest does not match with expected digest".to_string()
267            }
268        );
269        match status {
270            TransactionStatus::Signed(signed) => {
271                self.get_committee(&signed.epoch)?;
272                Ok(PlainTransactionInfoResponse::Signed(
273                    SignedTransaction::new_from_data_and_sig(transaction.into_data(), signed),
274                ))
275            }
276            TransactionStatus::Executed(cert_opt, effects, events) => {
277                let signed_effects = self.check_signed_effects_plain(digest, effects, None)?;
278                match cert_opt {
279                    Some(cert) => {
280                        let committee = self.get_committee(&cert.epoch)?;
281                        let ct = CertifiedTransaction::new_from_data_and_sig(
282                            transaction.into_data(),
283                            cert,
284                        );
285                        ct.verify_committee_sigs_only(&committee).map_err(|e| {
286                            IotaError::FailedToVerifyTxCertWithExecutedEffects {
287                                validator_name: self.address,
288                                error: e.to_string(),
289                            }
290                        })?;
291                        Ok(PlainTransactionInfoResponse::ExecutedWithCert(
292                            ct,
293                            signed_effects,
294                            events,
295                        ))
296                    }
297                    None => Ok(PlainTransactionInfoResponse::ExecutedWithoutCert(
298                        transaction,
299                        signed_effects,
300                        events,
301                    )),
302                }
303            }
304        }
305    }
306
307    fn check_object_response(
308        &self,
309        request: &ObjectInfoRequest,
310        response: ObjectInfoResponse,
311    ) -> IotaResult<VerifiedObjectInfoResponse> {
312        let ObjectInfoResponse {
313            object,
314            layout: _,
315            lock_for_debugging: _,
316        } = response;
317
318        fp_ensure!(
319            request.object_id == object.id(),
320            IotaError::ByzantineAuthoritySuspicion {
321                authority: self.address,
322                reason: "Object id mismatch in the response".to_string()
323            }
324        );
325
326        Ok(VerifiedObjectInfoResponse { object })
327    }
328
329    pub fn address(&self) -> &AuthorityPublicKeyBytes {
330        &self.address
331    }
332}
333
334impl<C> SafeClient<C>
335where
336    C: AuthorityAPI + Send + Sync + 'static,
337{
338    /// Initiate a new transfer to an IOTA or Primary account.
339    pub async fn handle_transaction(
340        &self,
341        transaction: TransactionEnvelope,
342        client_addr: Option<SocketAddr>,
343    ) -> Result<PlainTransactionInfoResponse, IotaError> {
344        let _timer = self.metrics.handle_transaction_latency.start_timer();
345        let digest = *transaction.digest();
346        let response = self
347            .authority_client
348            .handle_transaction(transaction.clone(), client_addr)
349            .await?;
350        let response = check_error!(
351            self.address,
352            self.check_transaction_info(&digest, transaction, response.status),
353            "Client error in handle_transaction"
354        )?;
355        Ok(response)
356    }
357
358    fn verify_certificate_response_v1(
359        &self,
360        digest: &TransactionDigest,
361        HandleCertificateResponseV1 {
362            signed_effects,
363            events,
364            input_objects,
365            output_objects,
366            auxiliary_data,
367        }: HandleCertificateResponseV1,
368    ) -> IotaResult<HandleCertificateResponseV1> {
369        let signed_effects = self.check_signed_effects_plain(digest, signed_effects, None)?;
370
371        // Check Events
372        match (&events, signed_effects.events_digest()) {
373            (None, None) | (None, Some(_)) => {}
374            (Some(events), None) => {
375                if !events.is_empty() {
376                    return Err(IotaError::ByzantineAuthoritySuspicion {
377                        authority: self.address,
378                        reason: "Returned events but no event digest present in the signed effects"
379                            .to_string(),
380                    });
381                }
382            }
383            (Some(events), Some(events_digest)) => {
384                fp_ensure!(
385                    &events.digest() == events_digest,
386                    IotaError::ByzantineAuthoritySuspicion {
387                        authority: self.address,
388                        reason: "Returned events don't match events digest in the signed effects"
389                            .to_string()
390                    }
391                );
392            }
393        }
394
395        // Check Input Objects
396        if let Some(input_objects) = &input_objects {
397            let expected: HashMap<_, _> = signed_effects
398                .old_object_metadata()
399                .into_iter()
400                .map(|old| (old.reference().object_id, *old.reference()))
401                .collect();
402
403            for object in input_objects {
404                let object_ref = object.object_ref();
405                if expected
406                    .get(&object_ref.object_id)
407                    .is_none_or(|expect| &object_ref != expect)
408                {
409                    return Err(IotaError::ByzantineAuthoritySuspicion {
410                        authority: self.address,
411                        reason: "Returned input object that wasn't present in the signed effects"
412                            .to_string(),
413                    });
414                }
415            }
416        }
417
418        // Check Output Objects
419        if let Some(output_objects) = &output_objects {
420            let expected: HashMap<_, _> = signed_effects
421                .all_changed_objects()
422                .into_iter()
423                .map(|(changed, _)| (changed.reference().object_id, *changed.reference()))
424                .collect();
425
426            for object in output_objects {
427                let object_ref = object.object_ref();
428                if expected
429                    .get(&object_ref.object_id)
430                    .is_none_or(|expect| &object_ref != expect)
431                {
432                    return Err(IotaError::ByzantineAuthoritySuspicion {
433                        authority: self.address,
434                        reason: "Returned output object that wasn't present in the signed effects"
435                            .to_string(),
436                    });
437                }
438            }
439        }
440
441        Ok(HandleCertificateResponseV1 {
442            signed_effects,
443            events,
444            input_objects,
445            output_objects,
446            auxiliary_data,
447        })
448    }
449
450    /// Execute a certificate.
451    pub async fn handle_certificate_v1(
452        &self,
453        request: HandleCertificateRequestV1,
454        client_addr: Option<SocketAddr>,
455    ) -> Result<HandleCertificateResponseV1, IotaError> {
456        let digest = *request.certificate.digest();
457        let _timer = self.metrics.handle_certificate_latency.start_timer();
458        let response = self
459            .authority_client
460            .handle_certificate_v1(request, client_addr)
461            .await?;
462
463        let verified = check_error!(
464            self.address,
465            self.verify_certificate_response_v1(&digest, response),
466            "Client error in handle_certificate"
467        )?;
468        Ok(verified)
469    }
470
471    pub async fn handle_object_info_request(
472        &self,
473        request: ObjectInfoRequest,
474    ) -> Result<VerifiedObjectInfoResponse, IotaError> {
475        self.metrics.total_requests_handle_object_info_request.inc();
476
477        let _timer = self.metrics.handle_obj_info_latency.start_timer();
478        let response = self
479            .authority_client
480            .handle_object_info_request(request.clone())
481            .await?;
482        let response = self
483            .check_object_response(&request, response)
484            .tap_err(|err| error!(?err, authority=?self.address, "Client error in handle_object_info_request"))?;
485
486        self.metrics
487            .total_ok_responses_handle_object_info_request
488            .inc();
489        Ok(response)
490    }
491
492    /// Handle Transaction information requests for a given digest.
493    #[instrument(level = "trace", skip_all, fields(authority = ?self.address.concise()))]
494    pub async fn handle_transaction_info_request(
495        &self,
496        request: TransactionInfoRequest,
497    ) -> Result<PlainTransactionInfoResponse, IotaError> {
498        self.metrics
499            .total_requests_handle_transaction_info_request
500            .inc();
501
502        let _timer = self.metrics.handle_tx_info_latency.start_timer();
503
504        let transaction_info = self
505            .authority_client
506            .handle_transaction_info_request(request.clone())
507            .await?;
508
509        let transaction = TransactionEnvelope::new(transaction_info.transaction);
510        let transaction_info = self.check_transaction_info(
511            &request.transaction_digest,
512            transaction,
513            transaction_info.status,
514        ).tap_err(|err| {
515            error!(?err, authority=?self.address, "Client error in handle_transaction_info_request");
516        })?;
517        self.metrics
518            .total_ok_responses_handle_transaction_info_request
519            .inc();
520        Ok(transaction_info)
521    }
522
523    #[instrument(level = "trace", skip_all, fields(authority = ?self.address.concise()))]
524    pub async fn handle_system_state_object(&self) -> Result<IotaSystemState, IotaError> {
525        self.authority_client
526            .handle_system_state_object(SystemStateRequest { _unused: false })
527            .await
528    }
529
530    // --- ValidatorV2 wrappers ---
531
532    #[instrument(level = "trace", skip_all, fields(authority = ?self.address.concise()))]
533    pub async fn submit_tx(
534        &self,
535        transactions: Vec<TransactionEnvelope>,
536        client_addr: Option<SocketAddr>,
537    ) -> Result<Vec<(TransactionDigest, TxStatusUpdate)>, IotaError> {
538        let _timer = self.metrics.submit_tx_latency.start_timer();
539        check_error!(
540            self.address,
541            self.authority_client
542                .submit_tx(transactions, client_addr)
543                .await,
544            "Client error in submit_tx"
545        )
546    }
547
548    #[instrument(level = "trace", skip_all, fields(authority = ?self.address.concise()))]
549    pub async fn get_tx_status(
550        &self,
551        request: GetTxStatusRequest,
552        client_addr: Option<SocketAddr>,
553    ) -> Result<Vec<(TransactionDigest, TxStatusUpdate)>, IotaError> {
554        let _timer = self.metrics.get_tx_status_latency.start_timer();
555        check_error!(
556            self.address,
557            self.authority_client
558                .get_tx_status(request, client_addr)
559                .await,
560            "Client error in get_tx_status"
561        )
562    }
563
564    #[instrument(level = "trace", skip_all, fields(authority = ?self.address.concise()))]
565    pub async fn notify_capabilities_v2(
566        &self,
567        request: HandleCapabilityNotificationRequestV1,
568    ) -> Result<HandleCapabilityNotificationResponseV1, IotaError> {
569        let _timer = self.metrics.notify_capabilities_v2_latency.start_timer();
570        check_error!(
571            self.address,
572            self.authority_client.notify_capabilities_v2(request).await,
573            "Client error in notify_capabilities_v2"
574        )
575    }
576
577    #[instrument(level = "trace", skip_all, fields(authority = ?self.address.concise()))]
578    pub async fn health_check(
579        &self,
580        request: ValidatorHealthRequest,
581    ) -> Result<ValidatorHealthResponse, IotaError> {
582        let _timer = self.metrics.health_check_latency.start_timer();
583        check_error!(
584            self.address,
585            self.authority_client.health_check(request).await,
586            "Client error in health_check"
587        )
588    }
589
590    // --- ValidatorPeer wrappers ---
591
592    #[instrument(level = "trace", skip_all, fields(authority = ?self.address.concise()))]
593    pub async fn get_checkpoint_v2(
594        &self,
595        request: CheckpointRequest,
596    ) -> Result<CheckpointResponse, IotaError> {
597        let _timer = self.metrics.get_checkpoint_v2_latency.start_timer();
598        check_error!(
599            self.address,
600            self.authority_client.get_checkpoint_v2(request).await,
601            "Client error in get_checkpoint_v2"
602        )
603    }
604}