From cd7588ee8eaebe862fe9cf5d7c3fd92981703e87 Mon Sep 17 00:00:00 2001 From: Aaron Feickert <66188213+AaronFeickert@users.noreply.github.com> Date: Mon, 19 Jun 2023 09:30:33 -0500 Subject: [PATCH] feat: use precomputation on (most) fixed generators (#19) 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](https://github.com/tari-project/bulletproofs-plus/issues/18). --- Cargo.toml | 1 + benches/range_proof.rs | 91 ++++++++++++-------------- src/generators/bulletproof_gens.rs | 90 +++++++++++++++----------- src/generators/generators_chain.rs | 9 --- src/generators/mod.rs | 23 ------- src/range_parameters.rs | 26 ++++---- src/range_proof.rs | 100 ++++++++++++++--------------- src/range_statement.rs | 8 +-- src/ristretto.rs | 8 ++- src/traits.rs | 8 +++ src/transcripts.rs | 4 +- 11 files changed, 179 insertions(+), 189 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d8feb4d..3e2859a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/benches/range_proof.rs b/benches/range_proof.rs index 1f2565d..b17ac1a 100644 --- a/benches/range_proof.rs +++ b/benches/range_proof.rs @@ -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}; @@ -197,65 +195,60 @@ 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(); @@ -263,8 +256,8 @@ fn verify_batched_rangeproofs_helper(bit_length: usize, extension_degree: Extens VerifyAction::RecoverOnly => { let _masks = RangeProof::verify_batch( transcript_label, - statements, - proofs, + &statements, + &proofs, VerifyAction::RecoverOnly, ) .unwrap(); diff --git a/src/generators/bulletproof_gens.rs b/src/generators/bulletproof_gens.rs index 178ea87..de47028 100644 --- a/src/generators/bulletproof_gens.rs +++ b/src/generators/bulletproof_gens.rs @@ -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. @@ -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
{
+#[derive(Clone)]
+pub struct BulletproofGens {
pub(crate) g_vec: Vec {
+impl {
/// Create a new `BulletproofGens` object.
///
/// # Inputs
@@ -48,46 +62,35 @@ impl {
///
/// * `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 ::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:: ::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`.
@@ -112,3 +115,18 @@ impl {
}
}
}
+
+impl Debug for BulletproofGens
+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()
+ }
+}
diff --git a/src/generators/generators_chain.rs b/src/generators/generators_chain.rs
index f3e7979..a85cc33 100644
--- a/src/generators/generators_chain.rs
+++ b/src/generators/generators_chain.rs
@@ -30,15 +30,6 @@ impl GeneratorsChain {
_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 Default for GeneratorsChain {
diff --git a/src/generators/mod.rs b/src/generators/mod.rs
index a1e51b6..e84f0d1 100644
--- a/src/generators/mod.rs
+++ b/src/generators/mod.rs
@@ -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 ,
/// The pair of base points for Pedersen commitments.
@@ -25,7 +28,7 @@ pub struct RangeParameters RangeParameters
-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 ) -> Result {
- self.hi_base_iter().cloned().collect()
- }
-
/// Return the non-public mask iterator to the bulletproof generators
pub fn gi_base_iter(&self) -> impl Iterator {
- self.gi_base_iter().cloned().collect()
+ /// Return the interleaved precomputation tables
+ pub fn precomp(&self) -> Arc Debug for RangeParameters
where
- P: Compressable + Debug,
+ P: Compressable + Debug + Precomputable,
P::Compressed: Debug,
{
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
diff --git a/src/range_proof.rs b/src/range_proof.rs
index f20b228..ba2a826 100644
--- a/src/range_proof.rs
+++ b/src/range_proof.rs
@@ -13,8 +13,9 @@ use std::{
use curve25519_dalek::{
scalar::Scalar,
- traits::{Identity, IsIdentity},
+ traits::{Identity, IsIdentity, VartimePrecomputedMultiscalarMul},
};
+use itertools::Itertools;
use merlin::Transcript;
use rand::thread_rng;
use serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer};
@@ -31,7 +32,7 @@ use crate::{
},
range_statement::RangeStatement,
range_witness::RangeWitness,
- traits::{Compressable, Decompressable, FixedBytesRepr},
+ traits::{Compressable, Decompressable, FixedBytesRepr, Precomputable},
transcripts,
utils::generic::{bit_vector_of_scalars, nonce, read_1_byte, read_32_bytes},
};
@@ -186,8 +187,8 @@ impl RangeProof
where
for<'p> &'p P: Mul