Feat/disk cache (#354)

- Bump rust version to `nightly-2025-07-02` because some of the nightly features we were using have been stabilized.
- Introduce feature `disk_cache` which enables caching to disk.  Each time an artifact is retrieved from the cache it will be read and deserialized.  On a cache miss the artifact will be created, serialized and stored to disk.
- Introduce feature `mem_cache` which enables caching to memory.  All cached artifacts are kept in memory after they are created.  The mem cache implementation avoids cloning of artifacts by extending their lifetime to `'static`.  This is `unsafe` code, but I argue that this usage is safe.
- Add a `build.rs`
  - When the feature `disk_cache` is enabled, the `build.rs` will inject env variables to the process with the git commit information, which is used to index the cached artifacts
- Replace all previous cached artifacts from `LazyStatic` methods that call the cache API
- Derive `Serialize, Deserialize` for all `*Target` types so that they can be serialized for caching to disk
- Add finer level of caching: now we cache the `CircuitData` and `VerifierData` independently.  The reason for this is that `CircuitData` is a very big artifact which is not needed for verification.  So by only accessing `VerifierData` in verification we don't pay a big overhead for reading from disk and deserializing
- Add missing artifacts to the cache: like the `CircuitData` for the `MainPod` indexed by `Params`
- Add helper types to serialize and deserialize `CircuitData`, `CommonData` and `VerifierData` with the set of gates and generators used in the recursive MainPod circuit
- Tweak the ids of our custom gates so that they remain unique when their generic parameters change
- Bugfix: several tests were using the standard `vd_set` but were using MainPod circuits with non-default parameters.  This was working before because there was a bug: the MainPod circuit was reporting that the used verifier data was the standard one instead of picking the one corresponding to it's own Params.

Summary of breaking changes:
- One and only one of the features `mem_cache` or `disk_cache` need to be enabled.  By default it's `mem_cache`
  - To enable the `disk_cache` you need to disable the default features like this: `--no-default-features --features=backend_plonky2,zk,disk_cache`
- Removed `DEFAULT_PARAMS`, instead use `Params::default()`
- Removed `STANDARD_REC_MAIN_POD_CIRCUIT_DATA`, instead use `cache_get_standard_rec_main_pod_common_circuit_data`
- The library is now using `nightly-2025-07-02`.  Some rust language features are unstable in previous versions.
This commit is contained in:
Eduard S. 2025-07-24 12:15:31 +02:00 committed by GitHub
parent 745d654048
commit 8429cd224d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 831 additions and 207 deletions

View file

@ -16,11 +16,12 @@ use plonky2::{
plonk::{circuit_builder::CircuitBuilder, circuit_data::CommonCircuitData},
util::serialization::{Buffer, IoResult, Read, Write},
};
use serde::{Deserialize, Serialize};
use crate::backends::plonky2::basetypes::{D, F};
#[derive(Debug)]
struct ConditionalZeroGenerator<F: RichField + Extendable<D>, const D: usize> {
#[derive(Debug, Default, Clone)]
pub(crate) struct ConditionalZeroGenerator<F: RichField + Extendable<D>, const D: usize> {
if_zero: Target,
then_zero: Target,
quot: Target,
@ -78,9 +79,11 @@ impl<F: RichField + Extendable<D>, const D: usize> SimpleGenerator<F, D>
/// A big integer, represented in base `2^32` with 10 digits, in little endian
/// form.
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct BigUInt320Target {
#[serde(with = "serde_arrays")]
pub limbs: [Target; 10],
#[serde(with = "serde_arrays")]
pub bits: [BoolTarget; 320],
}
@ -313,7 +316,7 @@ fn biguint_limbs_to_bits(builder: &mut CircuitBuilder<F, D>, limbs: &[Target]) -
Copied from https://github.com/0xPolygonZero/plonky2/blob/82791c4809d6275682c34b926390ecdbdc2a5297/plonky2/src/gadgets/range_check.rs#L62
*/
#[derive(Debug, Default)]
#[derive(Debug, Default, Clone)]
pub struct LowHighGenerator {
integer: Target,
n_log: usize,

View file

@ -57,6 +57,7 @@ pub fn ec_field_sqrt(x: &ECField) -> Option<ECField> {
]);
// Compute x^((r-1)/2) = x^(p*((1+p)/2)*(1+p^2))
let x1 = x.frobenius();
#[allow(clippy::manual_div_ceil)]
let x2 = x1.exp_u64((1 + GoldilocksField::ORDER) / 2);
let den = x2 * x2.repeated_frobenius(2);
Some(num / den)
@ -440,7 +441,7 @@ impl Mul<Point> for &BigUint {
type FieldTarget = OEFTarget<5, QuinticExtension<GoldilocksField>>;
#[derive(Clone, Debug)]
#[derive(Clone, Default, Debug, Serialize, Deserialize)]
pub struct PointTarget {
pub x: FieldTarget,
pub u: FieldTarget,
@ -470,8 +471,8 @@ impl PointTarget {
}
}
#[derive(Clone, Debug)]
struct PointSquareRootGenerator {
#[derive(Clone, Default, Debug)]
pub(crate) struct PointSquareRootGenerator {
pub orig: PointTarget,
pub sqrt: PointTarget,
}

View file

@ -15,6 +15,7 @@ use plonky2::{
plonk::{circuit_builder::CircuitBuilder, circuit_data::CommonCircuitData},
util::serialization::{Buffer, IoError, Read, Write},
};
use serde::{Deserialize, Serialize};
//use super::gates::field::NNFMulGate;
use crate::{
@ -83,8 +84,9 @@ pub trait CircuitBuilderNNF<
}
/// Target type modelled on OEF.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OEFTarget<const DEG: usize, NNF: OEF<DEG>> {
#[serde(with = "serde_arrays")]
pub components: [Target; DEG],
_phantom_data: PhantomData<NNF>,
}
@ -106,8 +108,8 @@ impl<const DEG: usize, NNF: OEF<DEG>> Default for OEFTarget<DEG, NNF> {
/// Quotient generator for OEF targets. Allows us to automagically
/// generate quotients as witnesses.
#[derive(Debug, Default)]
struct QuotientGeneratorOEF<const DEG: usize, NNF: OEF<DEG>> {
#[derive(Debug, Default, Clone)]
pub(crate) struct QuotientGeneratorOEF<const DEG: usize, NNF: OEF<DEG>> {
numerator: OEFTarget<DEG, NNF>,
denominator: OEFTarget<DEG, NNF>,
quotient: OEFTarget<DEG, NNF>,
@ -121,7 +123,11 @@ impl<
> SimpleGenerator<F, D> for QuotientGeneratorOEF<DEG, NNF>
{
fn id(&self) -> String {
"QuotientGeneratorOEF".to_string()
format!(
"QuotientGeneratorOEF<{}, {}>",
DEG,
std::any::type_name::<NNF>()
)
}
fn dependencies(&self) -> Vec<Target> {

View file

@ -25,7 +25,7 @@ use crate::backends::plonky2::primitives::ec::{
/// operation when all its witness wire values are zero (so that when the gate is partially used,
/// the unused slots still pass the constraints). This is the reason why this gate doesn't add the
/// final offset: if it did, the constraints wouldn't pass on the zero witness values.
#[derive(Debug, Clone)]
#[derive(Debug, Default, Clone)]
pub struct ECAddHomogOffset;
impl SimpleGate for ECAddHomogOffset {

View file

@ -66,7 +66,7 @@ pub struct RecursiveGateAdapter<const D: usize, G: SimpleGate> {
_gate: PhantomData<G>,
}
#[derive(Debug)]
#[derive(Debug, Default, Clone)]
pub struct RecursiveGenerator<const D: usize, G: SimpleGate> {
row: usize,
index: usize,
@ -175,7 +175,7 @@ where
G::F: RichField + Extendable<D> + Extendable<1>,
{
fn id(&self) -> String {
G::ID.to_string()
format!("GateAdapter<{}>", std::any::type_name::<G>())
}
fn serialize(
@ -336,7 +336,7 @@ where
}
fn id(&self) -> String {
format!("Generator<{},{}>", D, G::ID)
format!("RecursiveGenerator<{}, {}>", D, std::any::type_name::<G>())
}
fn dependencies(&self) -> Vec<Target> {
@ -374,7 +374,11 @@ where
F: RichField + Extendable<D>,
{
fn id(&self) -> String {
format!("Recursive<{},{}>", D, G::ID)
format!(
"RecursiveGateAdapter<{}, {}>",
D,
std::any::type_name::<G>()
)
}
fn serialize(&self, dst: &mut Vec<u8>, _common_data: &CommonCircuitData<F, D>) -> IoResult<()> {

View file

@ -101,7 +101,7 @@ impl<'de> Deserialize<'de> for Signature {
}
/// Targets for Schnorr signature over ecGFp5.
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SignatureTarget {
pub s: BigUInt320Target,
pub e: BigUInt320Target,

View file

@ -23,6 +23,7 @@ use plonky2::{
},
plonk::circuit_builder::CircuitBuilder,
};
use serde::{Deserialize, Serialize};
use crate::{
backends::plonky2::{
@ -35,7 +36,7 @@ use crate::{
middleware::{EMPTY_HASH, EMPTY_VALUE, F, HASH_SIZE},
};
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct MerkleClaimAndProofTarget {
pub(crate) max_depth: usize,
// `enabled` determines if the merkleproof verification is enabled
@ -194,6 +195,7 @@ impl MerkleClaimAndProofTarget {
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct MerkleProofExistenceTarget {
max_depth: usize,
// `enabled` determines if the merkleproof verification is enabled

View file

@ -289,7 +289,7 @@ impl MerkleTree {
}
/// returns an iterator over the leaves of the tree
pub fn iter(&self) -> Iter {
pub fn iter(&self) -> Iter<'_> {
Iter {
state: vec![&self.root],
}

View file

@ -20,6 +20,7 @@ use plonky2::{
proof::{ProofWithPublicInputs, ProofWithPublicInputsTarget},
},
};
use serde::{Deserialize, Serialize};
use crate::{
backends::plonky2::{
@ -38,6 +39,7 @@ use crate::{
// TODO: This is a very simple wrapper over the signature verification implemented on
// `SignatureTarget`. I think we can remove this and use it directly. Also we're not using the
// `enabled` flag, so it should be straight-forward to remove this.
#[derive(Clone, Serialize, Deserialize)]
pub struct SignatureVerifyTarget {
// `enabled` determines if the signature verification is enabled
pub(crate) enabled: BoolTarget,