Skip to main content

iota_types/
signature_verification.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5#[cfg(not(target_arch = "wasm32"))]
6use std::hash::Hash;
7
8#[cfg(not(target_arch = "wasm32"))]
9use lru::LruCache;
10use nonempty::NonEmpty;
11#[cfg(not(target_arch = "wasm32"))]
12use parking_lot::RwLock;
13#[cfg(not(target_arch = "wasm32"))]
14use prometheus_filtered::IntCounter;
15
16use crate::{
17    error::{IotaError, IotaResult},
18    signature::{AuthenticatorTrait, VerifyParams},
19    transaction::{SenderSignedData, SenderSignedTransactionAPI, TransactionDataAPI},
20};
21
22// Cache up to this many verified certs. We will need to tune this number in the
23// future - a decent guess to start with is that it should be 10-20 times larger
24// than peak transactions per second, on the assumption that we should see most
25// certs twice within about 10-20 seconds at most: Once via RPC, once via
26// consensus.
27#[cfg(not(target_arch = "wasm32"))]
28const VERIFIED_CERTIFICATE_CACHE_SIZE: usize = 100_000;
29
30#[cfg(not(target_arch = "wasm32"))]
31pub struct VerifiedDigestCache<D> {
32    inner: RwLock<LruCache<D, ()>>,
33    cache_hits_counter: IntCounter,
34    cache_misses_counter: IntCounter,
35    cache_evictions_counter: IntCounter,
36}
37
38#[cfg(not(target_arch = "wasm32"))]
39impl<D: Hash + Eq + Copy> VerifiedDigestCache<D> {
40    pub fn new(
41        cache_hits_counter: IntCounter,
42        cache_misses_counter: IntCounter,
43        cache_evictions_counter: IntCounter,
44    ) -> Self {
45        Self {
46            inner: RwLock::new(LruCache::new(
47                std::num::NonZeroUsize::new(VERIFIED_CERTIFICATE_CACHE_SIZE).unwrap(),
48            )),
49            cache_hits_counter,
50            cache_misses_counter,
51            cache_evictions_counter,
52        }
53    }
54
55    pub fn is_cached(&self, digest: &D) -> bool {
56        let inner = self.inner.read();
57        if inner.contains(digest) {
58            self.cache_hits_counter.inc();
59            true
60        } else {
61            self.cache_misses_counter.inc();
62            false
63        }
64    }
65
66    pub fn cache_digest(&self, digest: D) {
67        let mut inner = self.inner.write();
68        if let Some(old) = inner.push(digest, ()) {
69            if old.0 != digest {
70                self.cache_evictions_counter.inc();
71            }
72        }
73    }
74
75    pub fn cache_digests(&self, digests: Vec<D>) {
76        let mut inner = self.inner.write();
77        digests.into_iter().for_each(|d| {
78            if let Some(old) = inner.push(d, ()) {
79                if old.0 != d {
80                    self.cache_evictions_counter.inc();
81                }
82            }
83        });
84    }
85
86    pub fn is_verified<F, G>(&self, digest: D, verify_callback: F, uncached_checks: G) -> IotaResult
87    where
88        F: FnOnce() -> IotaResult,
89        G: FnOnce() -> IotaResult,
90    {
91        if !self.is_cached(&digest) {
92            verify_callback()?;
93            self.cache_digest(digest);
94        } else {
95            // Checks that are required to be performed outside the cache.
96            uncached_checks()?;
97        }
98        Ok(())
99    }
100
101    pub fn clear(&self) {
102        let mut inner = self.inner.write();
103        inner.clear();
104    }
105
106    // Initialize an empty cache when the cache is not needed (in testing scenarios
107    // and graphql initialization).
108    pub fn new_empty() -> Self {
109        Self::new(
110            IntCounter::new("test_cache_hits", "test cache hits").unwrap(),
111            IntCounter::new("test_cache_misses", "test cache misses").unwrap(),
112            IntCounter::new("test_cache_evictions", "test cache evictions").unwrap(),
113        )
114    }
115}
116
117/// Does crypto validation for a transaction which may be user-provided, or may
118/// be from a checkpoint.
119pub fn verify_sender_signed_data_message_signatures(
120    txn: &SenderSignedData,
121    verify_params: &VerifyParams,
122) -> IotaResult {
123    let tx = txn.transaction();
124
125    // 1. System transactions do not require signatures. User-submitted transactions
126    //    are verified not to
127    // be system transactions before this point
128    if tx.is_system_tx() {
129        return Ok(());
130    }
131
132    // 2. One signature per signer is required.
133    let signers: NonEmpty<_> = tx.signers();
134    fp_ensure!(
135        txn.signatures().len() == signers.len(),
136        IotaError::SignerSignatureNumberMismatch {
137            actual: txn.signatures().len(),
138            expected: signers.len()
139        }
140    );
141
142    // 3. Each signer must provide a signature.
143    let present_sigs = txn.get_signer_sig_mapping()?;
144    for s in signers {
145        if !present_sigs.contains_key(&s) {
146            return Err(IotaError::SignerSignatureAbsent {
147                expected: s.to_string(),
148                actual: present_sigs.keys().map(|s| s.to_string()).collect(),
149            });
150        }
151    }
152
153    // 4. Every signature must be valid.
154    let intent_message = txn.intent_message();
155    for (signer, signature) in present_sigs {
156        signature.verify_claims(&intent_message, signer, verify_params)?;
157    }
158    Ok(())
159}