Skip to content

Commit

Permalink
feat: use precomputation on (most) fixed generators (#19)
Browse files Browse the repository at this point in the history
Uses precomputation of fixed generator vectors to speed up verification,
at the cost of storing precomputation tables between verification
operations. This doesn't use precomputation on the Pedersen generators,
since those can be set independently of the others, and we can't
mix-and-match precomputation tables due to upstream limitations.

Note that this requires and uses a custom curve library fork. The fork
supports partial precomputation by removing an existing restriction
about matching the number of static points and scalars used for
precomputation evaluation. It also implements `Clone` on the underlying
types used for precomputation. This is unfortunate, since due to their
size (several megabytes in total) such tables should almost certainly
never be cloned. However, it's done for the reason explained below.

The generator tables are wrapped in an `Arc` for shared ownership. This
is done because precomputation evaluation is an instance method on a
precomputation type, not a static method that takes a reference to the
underlying tables. I have no idea why this design was chosen (static
methods are used for other types of multiscalar multiplication),
especially because there's no mutation involved. But because of this,
the verifier needs to own the precomputation structure containing the
tables, even though those tables are expected to be reused (that's the
entire point of precomputation). Using an `Arc` takes care of this
nicely, and avoids cloning.

However, apparently `#[derive(Clone)]` only plays nicely with structs if
all included generic types implement `Clone`, which means even though
cloning the table `Arc` isn't any kind of deep copy, we can't use that
attribute unless the precomputation tables implement `Clone`. Manually
implementing `Clone` on the containing struct is a headache, so it
seemed easier just to add `#[derive(Clone)]` at the curve library level.

This means it's probably _very important_ to ensure that precomputation
tables are used very carefully to avoid unintended cloning. I did some
testing and confirmed that the current implementation handles this as
expected, and won't clone any of the tables, despite the compiler
requiring they implement `Clone`.

Closes [issue
#18](#18).
  • Loading branch information
AaronFeickert committed Jun 19, 2023
1 parent 394843f commit cd7588e
Show file tree
Hide file tree
Showing 11 changed files with 179 additions and 189 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Expand Up @@ -13,6 +13,7 @@ curve25519-dalek = { package="tari-curve25519-dalek", version = "4.0.2", default
derive_more = "0.99.17"
derivative = "2.2.0"
digest = { version = "0.9.0", default-features = false }
itertools = "0.6.0"
lazy_static = "1.4.0"
merlin = { version = "2", default-features = false }
rand = "0.7"
Expand Down
91 changes: 42 additions & 49 deletions benches/range_proof.rs
Expand Up @@ -9,8 +9,6 @@
#[macro_use]
extern crate criterion;

use std::convert::TryInto;

use criterion::{Criterion, SamplingMode};
use curve25519_dalek::scalar::Scalar;
use rand::{self, Rng};
Expand Down Expand Up @@ -197,74 +195,69 @@ fn verify_batched_rangeproofs_helper(bit_length: usize, extension_degree: Extens
#[allow(clippy::cast_possible_truncation)]
let (value_min, value_max) = (0u64, (1u128 << (bit_length - 1)) as u64);

let max_range_proofs = BATCHED_SIZES
.to_vec()
.iter()
.fold(u32::MIN, |a, &b| a.max(b.try_into().unwrap()));
// 0. Batch data
let mut statements = vec![];
let mut proofs = vec![];
let pc_gens = ristretto::create_pedersen_gens_with_extension_degree(extension_degree);

// 1. Generators
let generators = RangeParameters::init(bit_length, 1, pc_gens).unwrap();

let mut rng = rand::thread_rng();
for _ in 0..max_range_proofs {
// 2. Create witness data
let mut openings = vec![];
let value = rng.gen_range(value_min, value_max);
let blindings = vec![Scalar::random_not_zero(&mut rng); extension_degree as usize];
openings.push(CommitmentOpening::new(value, blindings.clone()));
let witness = RangeWitness::init(openings).unwrap();

// 3. Generate the statement
let seed_nonce = Some(Scalar::random_not_zero(&mut rng));
let statement = RangeStatement::init(
generators.clone(),
vec![generators
.pc_gens()
.commit(&Scalar::from(value), blindings.as_slice())
.unwrap()],
vec![Some(value / 3)],
seed_nonce,
)
.unwrap();
statements.push(statement.clone());

// 4. Create the proof
let proof = RistrettoRangeProof::prove(transcript_label, &statement, &witness).unwrap();
proofs.push(proof);
}

for extract_masks in EXTRACT_MASKS {
for number_of_range_proofs in BATCHED_SIZES {
let label = format!(
"Batched {}-bit BP+ verify {} deg {:?} masks {:?}",
bit_length, number_of_range_proofs, extension_degree, extract_masks
);
let statements = &statements[0..number_of_range_proofs];
let proofs = &proofs[0..number_of_range_proofs];

// Generators
let pc_gens = ristretto::create_pedersen_gens_with_extension_degree(extension_degree);
let generators = RangeParameters::init(bit_length, 1, pc_gens).unwrap();

let mut rng = rand::thread_rng();

group.bench_function(&label, move |b| {
// Batch data
let mut statements = vec![];
let mut proofs = vec![];

for _ in 0..number_of_range_proofs {
// Witness data
let mut openings = vec![];
let value = rng.gen_range(value_min, value_max);
let blindings = vec![Scalar::random_not_zero(&mut rng); extension_degree as usize];
openings.push(CommitmentOpening::new(value, blindings.clone()));
let witness = RangeWitness::init(openings).unwrap();

// Statement data
let seed_nonce = Some(Scalar::random_not_zero(&mut rng));
let statement = RangeStatement::init(
generators.clone(),
vec![generators
.pc_gens()
.commit(&Scalar::from(value), blindings.as_slice())
.unwrap()],
vec![Some(value / 3)],
seed_nonce,
)
.unwrap();
statements.push(statement.clone());

// Proof
let proof = RistrettoRangeProof::prove(transcript_label, &statement, &witness).unwrap();
proofs.push(proof);
}

// Benchmark this code
b.iter(|| {
// 5. Verify the entire batch of single proofs
// Verify the entire batch of proofs
match extract_masks {
VerifyAction::VerifyOnly => {
let _masks = RangeProof::verify_batch(
transcript_label,
statements,
proofs,
&statements,
&proofs,
VerifyAction::VerifyOnly,
)
.unwrap();
},
VerifyAction::RecoverOnly => {
let _masks = RangeProof::verify_batch(
transcript_label,
statements,
proofs,
&statements,
&proofs,
VerifyAction::RecoverOnly,
)
.unwrap();
Expand Down
90 changes: 54 additions & 36 deletions src/generators/bulletproof_gens.rs
Expand Up @@ -4,7 +4,19 @@
// Copyright (c) 2018 Chain, Inc.
// SPDX-License-Identifier: MIT

use crate::{generators::aggregated_gens_iter::AggregatedGensIter, traits::FromUniformBytes};
use std::{
fmt::{Debug, Formatter},
sync::Arc,
};

use byteorder::{ByteOrder, LittleEndian};
use curve25519_dalek::traits::VartimePrecomputedMultiscalarMul;
use itertools::Itertools;

use crate::{
generators::{aggregated_gens_iter::AggregatedGensIter, generators_chain::GeneratorsChain},
traits::{Compressable, FromUniformBytes, Precomputable},
};

/// The `BulletproofGens` struct contains all the generators needed for aggregating up to `m` range proofs of up to `n`
/// bits each.
Expand All @@ -25,8 +37,8 @@ use crate::{generators::aggregated_gens_iter::AggregatedGensIter, traits::FromUn
/// This construction is also forward-compatible with constraint system proofs, which use a much larger slice of the
/// generator chain, and even forward-compatible to multiparty aggregation of constraint system proofs, since the
/// generators are namespaced by their party index.
#[derive(Clone, Debug)]
pub struct BulletproofGens<P> {
#[derive(Clone)]
pub struct BulletproofGens<P: Precomputable> {
/// The maximum number of usable generators for each party.
pub gens_capacity: usize,
/// Number of values or parties
Expand All @@ -35,9 +47,11 @@ pub struct BulletproofGens<P> {
pub(crate) g_vec: Vec<Vec<P>>,
/// Precomputed \\(\mathbf H\\) generators for each party.
pub(crate) h_vec: Vec<Vec<P>>,
/// Interleaved precomputed generators
pub(crate) precomp: Arc<P::Precomputation>,
}

impl<P: FromUniformBytes> BulletproofGens<P> {
impl<P: FromUniformBytes + Precomputable> BulletproofGens<P> {
/// Create a new `BulletproofGens` object.
///
/// # Inputs
Expand All @@ -48,46 +62,35 @@ impl<P: FromUniformBytes> BulletproofGens<P> {
///
/// * `party_capacity` is the maximum number of parties that can produce an aggregated proof.
pub fn new(gens_capacity: usize, party_capacity: usize) -> Self {
let mut gens = BulletproofGens {
gens_capacity: 0,
party_capacity,
g_vec: (0..party_capacity).map(|_| Vec::new()).collect(),
h_vec: (0..party_capacity).map(|_| Vec::new()).collect(),
};
gens.increase_capacity(gens_capacity);
gens
}

/// Increases the generators' capacity to the amount specified. If less than or equal to the current capacity,
/// does nothing.
pub fn increase_capacity(&mut self, new_capacity: usize) {
use byteorder::{ByteOrder, LittleEndian};

use crate::generators::generators_chain::GeneratorsChain;

if self.gens_capacity >= new_capacity {
return;
}
let mut g_vec: Vec<Vec<P>> = (0..party_capacity).map(|_| Vec::new()).collect();
let mut h_vec: Vec<Vec<P>> = (0..party_capacity).map(|_| Vec::new()).collect();

for i in 0..self.party_capacity {
// Generate the points
for i in 0..party_capacity {
#[allow(clippy::cast_possible_truncation)]
let party_index = i as u32;

let mut label = [b'G', 0, 0, 0, 0];
LittleEndian::write_u32(&mut label[1..5], party_index);
self.g_vec[i].extend(
&mut GeneratorsChain::new(&label)
.fast_forward(self.gens_capacity)
.take(new_capacity - self.gens_capacity),
);
g_vec[i].extend(&mut GeneratorsChain::<P>::new(&label).take(gens_capacity));

label[0] = b'H';
self.h_vec[i].extend(
&mut GeneratorsChain::new(&label)
.fast_forward(self.gens_capacity)
.take(new_capacity - self.gens_capacity),
);
h_vec[i].extend(&mut GeneratorsChain::<P>::new(&label).take(gens_capacity));
}

// Generate a flattened interleaved iterator for the precomputation tables
let iter_g_vec = g_vec.iter().flat_map(move |g_j| g_j.iter());
let iter_h_vec = h_vec.iter().flat_map(move |h_j| h_j.iter());
let iter_interleaved = iter_g_vec.interleave(iter_h_vec);
let precomp = Arc::new(P::Precomputation::new(iter_interleaved));

BulletproofGens {
gens_capacity,
party_capacity,
g_vec,
h_vec,
precomp,
}
self.gens_capacity = new_capacity;
}

/// Return an iterator over the aggregation of the parties' G generators with given size `n`.
Expand All @@ -112,3 +115,18 @@ impl<P: FromUniformBytes> BulletproofGens<P> {
}
}
}

impl<P> Debug for BulletproofGens<P>
where
P: Compressable + Debug + Precomputable,
P::Compressed: Debug,
{
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RangeParameters")
.field("gens_capacity", &self.gens_capacity)
.field("party_capacity", &self.party_capacity)
.field("g_vec", &self.g_vec)
.field("h_vec", &self.h_vec)
.finish()
}
}
9 changes: 0 additions & 9 deletions src/generators/generators_chain.rs
Expand Up @@ -30,15 +30,6 @@ impl<P> GeneratorsChain<P> {
_phantom: PhantomData,
}
}

/// Advances the reader n times, squeezing and discarding the result
pub(crate) fn fast_forward(mut self, n: usize) -> Self {
let mut buf = [0u8; 64];
for _ in 0..n {
self.reader.read(&mut buf);
}
self
}
}

impl<P> Default for GeneratorsChain<P> {
Expand Down
23 changes: 0 additions & 23 deletions src/generators/mod.rs
Expand Up @@ -61,27 +61,4 @@ mod tests {
helper(16, 2);
helper(16, 1);
}

#[test]
fn resizing_small_gens_matches_creating_bigger_gens() {
let gens = BulletproofGens::new(64, 8);

let mut gen_resized = BulletproofGens::new(32, 8);
gen_resized.increase_capacity(64);

let helper = |n: usize, m: usize| {
let gens_g: Vec<RistrettoPoint> = gens.g_iter(n, m).copied().collect();
let gens_h: Vec<RistrettoPoint> = gens.h_iter(n, m).copied().collect();

let resized_g: Vec<RistrettoPoint> = gen_resized.g_iter(n, m).copied().collect();
let resized_h: Vec<RistrettoPoint> = gen_resized.h_iter(n, m).copied().collect();

assert_eq!(gens_g, resized_g);
assert_eq!(gens_h, resized_h);
};

helper(64, 8);
helper(32, 8);
helper(16, 8);
}
}
26 changes: 13 additions & 13 deletions src/range_parameters.rs
Expand Up @@ -3,7 +3,10 @@

//! Bulletproofs+ range parameters (generators and base points) needed for a batch of range proofs

use std::fmt::{Debug, Formatter};
use std::{
fmt::{Debug, Formatter},
sync::Arc,
};

use crate::{
errors::ProofError,
Expand All @@ -12,20 +15,20 @@ use crate::{
pedersen_gens::{ExtensionDegree, PedersenGens},
},
range_proof::MAX_RANGE_PROOF_BIT_LENGTH,
traits::{Compressable, FromUniformBytes},
traits::{Compressable, FromUniformBytes, Precomputable},
};

/// Contains all the generators and base points needed for a batch of range proofs
#[derive(Clone)]
pub struct RangeParameters<P: Compressable> {
pub struct RangeParameters<P: Compressable + Precomputable> {
/// Generators needed for aggregating up to `m` range proofs of up to `n` bits each.
bp_gens: BulletproofGens<P>,
/// The pair of base points for Pedersen commitments.
pc_gens: PedersenGens<P>,
}

impl<P> RangeParameters<P>
where P: FromUniformBytes + Compressable + Clone
where P: FromUniformBytes + Compressable + Clone + Precomputable
{
/// Initialize a new 'RangeParameters' with sanity checks
pub fn init(bit_length: usize, aggregation_factor: usize, pc_gens: PedersenGens<P>) -> Result<Self, ProofError> {
Expand Down Expand Up @@ -107,11 +110,6 @@ where P: FromUniformBytes + Compressable + Clone
self.hi_base_iter().collect()
}

/// Return the non-public value bulletproof generator references
pub fn hi_base_copied(&self) -> Vec<P> {
self.hi_base_iter().cloned().collect()
}

/// Return the non-public mask iterator to the bulletproof generators
pub fn gi_base_iter(&self) -> impl Iterator<Item = &P> {
self.bp_gens.g_iter(self.bit_length(), self.aggregation_factor())
Expand All @@ -122,15 +120,17 @@ where P: FromUniformBytes + Compressable + Clone
self.gi_base_iter().collect()
}

/// Return the non-public mask bulletproof generators
pub fn gi_base_copied(&self) -> Vec<P> {
self.gi_base_iter().cloned().collect()
/// Return the interleaved precomputation tables
pub fn precomp(&self) -> Arc<P::Precomputation> {
// We use shared ownership since precomputation evaluation is an instance method and we don't want to actually
// clone
Arc::clone(&self.bp_gens.precomp)
}
}

impl<P> Debug for RangeParameters<P>
where
P: Compressable + Debug,
P: Compressable + Debug + Precomputable,
P::Compressed: Debug,
{
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
Expand Down

0 comments on commit cd7588e

Please sign in to comment.