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::crypto::Intent;
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::IntCounter;
16
17use crate::{
18    error::{IotaError, IotaResult},
19    signature::VerifyParams,
20    transaction::{SenderSignedData, TransactionDataAPI},
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: &SenderSignedData,
122    verify_params: &VerifyParams,
123) -> IotaResult {
124    let intent_message = txn.intent_message();
125    assert_eq!(intent_message.intent, Intent::iota_transaction());
126
127    // 1. System transactions do not require signatures. User-submitted transactions
128    //    are verified not to
129    // be system transactions before this point
130    if intent_message.value.is_system_tx() {
131        return Ok(());
132    }
133
134    // 2. One signature per signer is required.
135    let signers: NonEmpty<_> = txn.intent_message().value.signers();
136    fp_ensure!(
137        txn.inner().tx_signatures.len() == signers.len(),
138        IotaError::SignerSignatureNumberMismatch {
139            actual: txn.inner().tx_signatures.len(),
140            expected: signers.len()
141        }
142    );
143
144    // 3. Each signer must provide a signature.
145    let present_sigs = txn.get_signer_sig_mapping()?;
146    for s in signers {
147        if !present_sigs.contains_key(&s) {
148            return Err(IotaError::SignerSignatureAbsent {
149                expected: s.to_string(),
150                actual: present_sigs.keys().map(|s| s.to_string()).collect(),
151            });
152        }
153    }
154
155    // 4. Every signature must be valid.
156    for (signer, signature) in present_sigs {
157        signature.verify_authenticator(intent_message, signer, verify_params)?;
158    }
159    Ok(())
160}