matklad / once_cell

Rust library for single assignment cells and lazy statics without macros

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

OnceCell in an associated type

mimoo opened this issue · comments

I wanted to implement an evaluate-once cache on a trait, so I stored a OnceCell in an associated const:

//! This module includes the [CryptoDigest] trait,
//! which provides a generic interface for hashing.
//!
//! To use it, simply implement [CryptoDigest] for your type:
//!
//! ```
//! #[derive(Serialize)]
//! struct A {
//!     thing: u8,
//! }
//!
//! impl CryptoDigest for A {
//!     const PREFIX: &'static str = "kimchi-circuit";
//! }
//!
//! let expected_result = [
//!     152, 164, 51, 27, 40, 75, 52, 217, 102, 77, 212, 82, 108, 184, 145, 148, 161, 170, 206,
//!     224, 157, 72, 54, 77, 162, 131, 109, 207, 44, 37, 56, 255,
//! ];
//! assert_eq!(a.digest(), expected_result);
//!
//! let b = A { thing: 1 };
//! assert_eq!(a.digest(), b.digest());
//! ```
//!
//! Warning: make sure not to reuse the same `PREFIX`
//! for different types. This prefix is here to semantically
//! distinguish the hash of different types
//! (and thus different use-case).
//!

use once_cell::sync::OnceCell;
use serde::Serialize;
use sha2::{Digest, Sha256};

/// Returns a 32-byte hash of the given string.
/// This is useful to prevent collisions in
/// domain separation strings.
fn encode_prefix(prefix: &str) -> [u8; 32] {
    let mut hasher = Sha256::new();
    hasher.update(prefix.as_bytes());
    hasher.finalize().into()
}

pub trait CryptoDigest: Serialize {
    /// The domain separation string to use in the hash.
    /// This is to distinguish hashes for different use-cases.
    /// With this approach, a type is linked to a single usecase.
    ///
    /// Warning: careful not to use the same separation string with
    /// two different types.
    const PREFIX: &'static str;

    /// You should not override this.
    /// This is an optimization to pre-compute the state
    /// of the hash we use to produce this digest.
    const PRECOMPUTED_PREFIX: OnceCell<Sha256> = OnceCell::new();

    /// Returns the digest of `self`.
    /// Note: this is implemented as the SHA-256 of a prefix
    /// ("kimchi-circuit"), followed by the serialized gates.
    /// The gates are serialized using messagepack.
    fn digest(&self) -> [u8; 32] {
        // compute the prefixed state lazily
        let mut hasher = Self::PRECOMPUTED_PREFIX
            .get_or_init(|| {
                let mut hasher = Sha256::new();
                let prefix = encode_prefix(Self::PREFIX);
                hasher.update(&prefix);
                hasher
            })
            .clone();

        hasher.update(&rmp_serde::to_vec(self).expect("couldn't serialize the gate"));
        hasher.finalize().into()
    }
}

Cargo clippy returns the following error: https://rust-lang.github.io/rust-clippy/master/index.html#declare_interior_mutable_const

I can't seem to replace the const with a static

pub trait CryptoDigest: Serialize {
    static PRECOMPUTED_PREFIX: OnceCell<Sha256> = OnceCell::new();

I'm getting:

error: associated `static` items are not allowed
  --> utils/src/hasher.rs:57:5
   |
57 |     static PRECOMPUTED_PREFIX: OnceCell<Sha256> = OnceCell::new();

Any idea if this is at all possible :)?

I don't think it is. There are two problems here:

First, I belive the code, as written, is incorrect: fn digest(&self) takes a self as an argument (so, it might be called for different instances of the type), but you are trying to use a global cache here. These feels wrong, as the cache will be shared by unrelated things. You probably want something like this (note the absence of self):

trait Digest {
   static CACHE: OnceCell<Shap256>;
   fn digest() -> [u32; 32]
}

Sadly, that won't work either -- associated statics are not supported by a language. This has to do with monomorphisation -- for functions, separate compilation units get separate (duplicate) monomorphisation instances. This works, as functions don't have identity. statics, on the other hand, have to be unique, and this is not something currently officially suppoted by the Rust linking model.

So, I think no matter how you spin this, static CACHE: OnceCell<Sha256> needs to be written by the user at the place where they declare an impl. Though, you might make this more automated by hiding this inside a macro.

Closing, as I don't think we should do anything in this crate