Skip to main content

dusk_vm/host_queries/
cache.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4//
5// Copyright (c) DUSK NETWORK. All rights reserved.
6
7use core::cell::Cell;
8use std::env;
9use std::num::NonZeroUsize;
10use std::sync::{Mutex, MutexGuard, OnceLock};
11
12use dusk_core::plonk::PlonkVersion;
13use lru::LruCache;
14
15use super::HardFork;
16
17// These caches are process-global and survive across block executions.
18// Cache keys for consensus-sensitive host queries must therefore include every
19// execution-policy input that can change semantics across heights or releases.
20
21const fn plonk_cache_revision(version: PlonkVersion) -> u32 {
22    match version {
23        PlonkVersion::V1 => 1,
24        PlonkVersion::V2 => 2,
25        PlonkVersion::V3 => 3,
26        _ => u32::MAX,
27    }
28}
29
30const fn bls_cache_revision(hard_fork: HardFork) -> u32 {
31    match hard_fork {
32        HardFork::PreFork => 0,
33        HardFork::Aegis | HardFork::Boreas => 1,
34    }
35}
36
37/// Active execution policy used by VM host queries.
38#[derive(Debug, Clone, Copy, Eq, PartialEq)]
39pub struct HostQueryPolicy {
40    /// PLONK verifier version selected for this execution context.
41    pub plonk_version: PlonkVersion,
42    /// Active hardfork used for BLS verification semantics.
43    pub hard_fork: HardFork,
44}
45
46impl HostQueryPolicy {
47    /// Creates a policy from the active PLONK and hardfork versions.
48    pub const fn from_versions(
49        plonk_version: PlonkVersion,
50        hard_fork: HardFork,
51    ) -> Self {
52        Self {
53            plonk_version,
54            hard_fork,
55        }
56    }
57}
58
59thread_local! {
60    // Default to V2 for safety: if the node forgets to set a version for a
61    // consensus-critical call path, we'd rather reject than accept.
62    static HOST_QUERY_POLICY: Cell<HostQueryPolicy> = const {
63        Cell::new(HostQueryPolicy::from_versions(
64            PlonkVersion::V2,
65            HardFork::PreFork,
66        ))
67    };
68}
69
70#[derive(Debug, Clone, Copy, Eq, PartialEq)]
71pub(super) enum CacheDomain {
72    Hash,
73    PoseidonHash,
74    Plonk,
75    Groth16Bn254,
76    Schnorr,
77    Bls,
78    BlsMultisig,
79    Keccak256,
80    Sha256,
81    Kzg,
82    Secp256k1Recover,
83}
84
85impl CacheDomain {
86    const fn tag(self) -> u8 {
87        match self {
88            CacheDomain::Hash => 0,
89            CacheDomain::PoseidonHash => 1,
90            CacheDomain::Plonk => 2,
91            CacheDomain::Groth16Bn254 => 3,
92            CacheDomain::Schnorr => 4,
93            CacheDomain::Bls => 5,
94            CacheDomain::BlsMultisig => 6,
95            CacheDomain::Keccak256 => 7,
96            CacheDomain::Sha256 => 8,
97            CacheDomain::Kzg => 9,
98            CacheDomain::Secp256k1Recover => 10,
99        }
100    }
101}
102
103// Cache revisions must be derived from execution policy inputs that change at
104// explicit activation boundaries. That keeps long-lived nodes aligned with
105// fresh nodes across fork and feature transitions.
106const fn cache_revision(policy: HostQueryPolicy, domain: CacheDomain) -> u32 {
107    match domain {
108        CacheDomain::Plonk => plonk_cache_revision(policy.plonk_version),
109        CacheDomain::Bls | CacheDomain::BlsMultisig => {
110            bls_cache_revision(policy.hard_fork)
111        }
112        CacheDomain::Hash
113        | CacheDomain::PoseidonHash
114        | CacheDomain::Groth16Bn254
115        | CacheDomain::Schnorr
116        | CacheDomain::Keccak256
117        | CacheDomain::Sha256
118        | CacheDomain::Kzg
119        | CacheDomain::Secp256k1Recover => 0,
120    }
121}
122
123/// Guard that restores the previous host-query policy when dropped.
124#[derive(Debug)]
125pub struct HostQueryPolicyGuard {
126    prev: HostQueryPolicy,
127}
128
129impl Drop for HostQueryPolicyGuard {
130    fn drop(&mut self) {
131        HOST_QUERY_POLICY.with(|m| m.set(self.prev));
132    }
133}
134
135/// Returns the current thread's host-query policy.
136pub fn host_query_policy() -> HostQueryPolicy {
137    HOST_QUERY_POLICY.with(|m| m.get())
138}
139
140/// Sets the current thread's host-query policy.
141///
142/// The previous policy is restored when the returned guard is dropped.
143pub fn set_host_query_policy(policy: HostQueryPolicy) -> HostQueryPolicyGuard {
144    let prev = HOST_QUERY_POLICY.with(|m| {
145        let prev = m.get();
146        m.set(policy);
147        prev
148    });
149    HostQueryPolicyGuard { prev }
150}
151
152/// Returns the current thread's PLONK version (defaults to `V2`).
153pub fn plonk_version() -> PlonkVersion {
154    host_query_policy().plonk_version
155}
156
157/// Returns the active hardfork for this thread.
158pub fn hard_fork() -> HardFork {
159    host_query_policy().hard_fork
160}
161
162pub(super) fn cache_key_with_revision(
163    domain: CacheDomain,
164    revision: u32,
165    arg_buf: &[u8],
166) -> [u8; blake2b_simd::OUTBYTES] {
167    // Domain-separate cache entries by query domain and semantics revision.
168    let mut state = blake2b_simd::Params::new()
169        .hash_length(blake2b_simd::OUTBYTES)
170        .to_state();
171    state.update(&[domain.tag()]);
172    state.update(&revision.to_le_bytes());
173    state.update(arg_buf);
174    *state.finalize().as_array()
175}
176
177pub(super) fn cache_key(
178    domain: CacheDomain,
179    arg_buf: &[u8],
180) -> [u8; blake2b_simd::OUTBYTES] {
181    let revision = cache_revision(host_query_policy(), domain);
182    cache_key_with_revision(domain, revision, arg_buf)
183}
184
185type ScalarCacheValue = dusk_core::BlsScalar;
186type RecoverCacheValue = Option<[u8; 65]>;
187
188macro_rules! define_cache {
189    ($get_func:ident, $put_func:ident, $cache_func:ident, $type:ty, $size:literal, $var:literal) => {
190        /// Gets an entry out of the cache. Returns `None` if there is no
191        /// element in the cache. `Some` signifies that there is a
192        /// cache element.
193        pub fn $get_func(hash: [u8; blake2b_simd::OUTBYTES]) -> Option<$type> {
194            // SAFETY: the closure never panics
195            unsafe { $cache_func(|mut cache| cache.get(&hash).cloned()) }
196        }
197
198        /// Put an entry into the cache.
199        pub fn $put_func(hash: [u8; blake2b_simd::OUTBYTES], value: $type) {
200            // SAFETY: The closure never panics
201            unsafe {
202                $cache_func(|mut cache| {
203                    cache.put(hash, value);
204                });
205            }
206        }
207
208        /// A simple LRU cache.
209        ///
210        /// # Safety
211        /// `f` should *never* panic, otherwise we poison the Mutex.
212        unsafe fn $cache_func<T, F>(f: F) -> T
213        where
214            F: FnOnce(
215                MutexGuard<LruCache<[u8; blake2b_simd::OUTBYTES], $type>>,
216            ) -> T,
217        {
218            const DEFAULT_SIZE: usize = $size;
219
220            static CACHE: OnceLock<
221                Mutex<LruCache<[u8; blake2b_simd::OUTBYTES], $type>>,
222            > = OnceLock::new();
223
224            CACHE
225                .get_or_init(|| {
226                    let mut cache_size = None;
227
228                    if let Ok(s) = env::var($var) {
229                        cache_size = s.parse().ok();
230                    }
231
232                    let mut cache_size = cache_size.unwrap_or(DEFAULT_SIZE);
233                    if cache_size == 0 {
234                        cache_size = DEFAULT_SIZE;
235                    }
236
237                    Mutex::new(LruCache::new(
238                        NonZeroUsize::new(cache_size).unwrap(),
239                    ))
240                })
241                .lock()
242                .map(f)
243                .unwrap()
244        }
245    };
246}
247
248define_cache!(
249    get_plonk_verification,
250    put_plonk_verification,
251    with_plonk_cache,
252    bool,
253    2048,
254    "DUSK_VM_PLONK_CACHE_SIZE"
255);
256define_cache!(
257    get_groth16_verification,
258    put_groth16_verification,
259    with_groth16_cache,
260    bool,
261    2048,
262    "DUSK_VM_GROTH16_CACHE_SIZE"
263);
264define_cache!(
265    get_bls_verification,
266    put_bls_verification,
267    with_bls_cache,
268    bool,
269    2048,
270    "DUSK_VM_BLS_CACHE_SIZE"
271);
272define_cache!(
273    get_hash,
274    put_hash,
275    with_hash_cache,
276    ScalarCacheValue,
277    2048,
278    "DUSK_VM_HASH_CACHE_SIZE"
279);
280define_cache!(
281    get_poseidon_hash,
282    put_poseidon_hash,
283    with_poseidon_hash_cache,
284    ScalarCacheValue,
285    2048,
286    "DUSK_VM_POSEIDON_HASH_CACHE_SIZE"
287);
288define_cache!(
289    get_schnorr_verification,
290    put_schnorr_verification,
291    with_schnorr_cache,
292    bool,
293    2048,
294    "DUSK_VM_SCHNORR_CACHE_SIZE"
295);
296define_cache!(
297    get_bls_multisig_verification,
298    put_bls_multisig_verification,
299    with_bls_multisig_cache,
300    bool,
301    2048,
302    "DUSK_VM_BLS_MULTISIG_CACHE_SIZE"
303);
304define_cache!(
305    get_keccak256,
306    put_keccak256,
307    with_keccak256_cache,
308    [u8; 32],
309    2048,
310    "DUSK_VM_KECCAK256_CACHE_SIZE"
311);
312define_cache!(
313    get_sha256,
314    put_sha256,
315    with_sha256_cache,
316    [u8; 32],
317    2048,
318    "DUSK_VM_SHA256_CACHE_SIZE"
319);
320define_cache!(
321    get_kzg_verification,
322    put_kzg_verification,
323    with_kzg_cache,
324    bool,
325    2048,
326    "DUSK_VM_KZG_CACHE_SIZE"
327);
328define_cache!(
329    get_secp256k1_recover,
330    put_secp256k1_recover,
331    with_secp256k1_recover_cache,
332    RecoverCacheValue,
333    2048,
334    "DUSK_VM_SECP256K1_RECOVER_CACHE_SIZE"
335);
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340
341    #[test]
342    fn host_query_policy_revisions_follow_versions() {
343        let prefork =
344            HostQueryPolicy::from_versions(PlonkVersion::V1, HardFork::PreFork);
345        let aegis =
346            HostQueryPolicy::from_versions(PlonkVersion::V3, HardFork::Aegis);
347        let boreas =
348            HostQueryPolicy::from_versions(PlonkVersion::V3, HardFork::Boreas);
349
350        assert_ne!(
351            cache_revision(prefork, CacheDomain::Plonk),
352            cache_revision(aegis, CacheDomain::Plonk)
353        );
354        assert_ne!(
355            cache_revision(prefork, CacheDomain::Bls),
356            cache_revision(aegis, CacheDomain::Bls)
357        );
358        assert_eq!(
359            cache_revision(aegis, CacheDomain::Bls),
360            cache_revision(aegis, CacheDomain::BlsMultisig)
361        );
362        assert_eq!(
363            cache_revision(aegis, CacheDomain::Bls),
364            cache_revision(boreas, CacheDomain::Bls)
365        );
366        assert_eq!(cache_revision(prefork, CacheDomain::Schnorr), 0);
367    }
368
369    #[test]
370    fn cache_key_is_domain_and_revision_separated() {
371        let arg = [1u8, 2, 3, 4];
372
373        let hash_key = cache_key_with_revision(CacheDomain::Hash, 0, &arg);
374        let schnorr_key =
375            cache_key_with_revision(CacheDomain::Schnorr, 0, &arg);
376        let bumped_hash_key =
377            cache_key_with_revision(CacheDomain::Hash, 1, &arg);
378
379        assert_ne!(hash_key, schnorr_key);
380        assert_ne!(hash_key, bumped_hash_key);
381    }
382
383    #[test]
384    fn set_host_query_policy_restores_previous_policy() {
385        let prev = host_query_policy();
386        let next =
387            HostQueryPolicy::from_versions(PlonkVersion::V3, HardFork::Boreas);
388
389        {
390            let _guard = set_host_query_policy(next);
391            assert_eq!(host_query_policy(), next);
392        }
393
394        assert_eq!(host_query_policy(), prev);
395    }
396
397    #[test]
398    fn host_query_policy_updates_cache_revisions() {
399        let prev = host_query_policy();
400
401        {
402            let _guard = set_host_query_policy(HostQueryPolicy::from_versions(
403                PlonkVersion::V1,
404                prev.hard_fork,
405            ));
406            assert_eq!(plonk_version(), PlonkVersion::V1);
407            assert_eq!(
408                cache_revision(host_query_policy(), CacheDomain::Plonk),
409                plonk_cache_revision(PlonkVersion::V1)
410            );
411        }
412
413        assert_eq!(host_query_policy(), prev);
414
415        {
416            let _guard = set_host_query_policy(HostQueryPolicy::from_versions(
417                prev.plonk_version,
418                HardFork::Boreas,
419            ));
420            assert_eq!(hard_fork(), HardFork::Boreas);
421            assert_eq!(
422                cache_revision(host_query_policy(), CacheDomain::Bls),
423                bls_cache_revision(HardFork::Boreas)
424            );
425            assert_eq!(
426                cache_revision(host_query_policy(), CacheDomain::BlsMultisig),
427                bls_cache_revision(HardFork::Boreas)
428            );
429        }
430
431        assert_eq!(host_query_policy(), prev);
432    }
433}