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