From 4f9500c9bd3a346c4d045f79139961b6344c1968 Mon Sep 17 00:00:00 2001 From: Hansie Odendaal <39146854+hansieodendaal@users.noreply.github.com> Date: Mon, 20 Jun 2022 11:05:41 +0200 Subject: [PATCH] feat: add simple bulletproofs plus interface(#105) Added simple bulletproofs plus interface that does not support extended masks or aggregated proofs. --- Cargo.toml | 4 +- src/extended_range_proof.rs | 33 +++++- src/lib.rs | 2 +- src/rewindable_range_proof.rs | 4 +- src/ristretto/bulletproofs_plus.rs | 164 +++++++++++++++++++++++++++-- src/ristretto/ristretto_keys.rs | 6 ++ 6 files changed, 194 insertions(+), 19 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8a40f608..ff835018 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,8 +15,8 @@ tari_utilities = { git = "https://github.com/tari-project/tari_utilities.git", t base64 = "0.10.1" blake2 = "0.9.1" -bulletproofs = { package = "tari_bulletproofs", git = "https://github.com/tari-project/bulletproofs", commit = "0033c45538164eb1d81891dad054e217d8b364f4" } -bulletproofs_plus = { package = "tari_bulletproofs_plus", git = "https://github.com/tari-project/bulletproofs-plus", commit = "28725cdd7d753478c9508cae629a8063fc46be37" } +bulletproofs = { package = "tari_bulletproofs", git = "https://github.com/tari-project/bulletproofs", rev = "0033c45538164eb1d81891dad054e217d8b364f4" } +bulletproofs_plus = { package = "tari_bulletproofs_plus", git = "https://github.com/tari-project/bulletproofs-plus", rev = "28725cdd7d753478c9508cae629a8063fc46be37" } curve25519-dalek = { package = "curve25519-dalek-ng", version = "4.1", default-features = false, features = ["u64_backend", "serde", "alloc"] } digest = "0.9.0" getrandom = { version = "0.2.3", default-features = false, optional = true } diff --git a/src/extended_range_proof.rs b/src/extended_range_proof.rs index a2cc2139..576640c1 100644 --- a/src/extended_range_proof.rs +++ b/src/extended_range_proof.rs @@ -31,6 +31,31 @@ pub trait ExtendedRangeProofService { type K: SecretKey; type PK: PublicKey; + /// Construct a simple non-aggregated range proof with default minimum value promise of `0` and the ability to + /// rewind it. Requires a rewind key (seed nonce) to be included in the range proof. + fn construct_proof_with_recovery_seed_nonce( + &self, + mask: &Self::K, + value: u64, + seed_nonce: &Self::K, + ) -> Result; + + /// Recover the (unverified) mask for a simple non-aggregated proof using the provided seed-nonce. + fn recover_mask( + &self, + proof: &Self::Proof, + commitment: &HomomorphicCommitment, + seed_nonce: &Self::K, + ) -> Result; + + /// Verify a recovered mask for a simple non-aggregated proof against the commitment. + fn verify_mask( + &self, + commitment: &HomomorphicCommitment, + mask: &Self::K, + value: u64, + ) -> Result; + /// Constructs a new extended range proof, which may be aggregated, for the given set(s) of secret key(s) value(s) /// and minimum value promise(s). Other optional inputs are seed nonce(s) and mask(s) for mask embedding /// and recovery. If no mask(s) are provided together with the seed nonce(s), the secret key(s), will be embedded. @@ -67,15 +92,15 @@ pub trait ExtendedRangeProofService { statements: Vec<&AggregatedPublicStatement>, ) -> Result<(), RangeProofError>; - /// Recover the (unverified) mask for a non-aggregated proof using the provided seed-nonce. - fn recover_mask( + /// Recover the (unverified) extended mask for a non-aggregated proof using the provided seed-nonce. + fn recover_extended_mask( &self, proof: &Self::Proof, statement: &AggregatedPrivateStatement, ) -> Result>, RangeProofError>; - /// Verify a recovered mask for a non-aggregated proof against the commitment. - fn verify_mask( + /// Verify a recovered extended mask for a non-aggregated proof against the commitment. + fn verify_extended_mask( &self, commitment: &HomomorphicCommitment, extended_mask: &ExtendedMask, diff --git a/src/lib.rs b/src/lib.rs index 17529248..1e8b5a39 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,7 +27,7 @@ pub mod ristretto; pub mod wasm; pub mod errors; -mod extended_range_proof; +pub mod extended_range_proof; #[cfg(feature = "ffi")] pub mod ffi; diff --git a/src/rewindable_range_proof.rs b/src/rewindable_range_proof.rs index 655aa509..8381497e 100644 --- a/src/rewindable_range_proof.rs +++ b/src/rewindable_range_proof.rs @@ -33,7 +33,7 @@ pub trait RewindableRangeProofService { type K: SecretKey; type PK: PublicKey; - /// Construct a rangeproof with the ability to rewind it. Requires two rewind keys and a 19-byte message to be + /// Construct a range proof with the ability to rewind it. Requires two rewind keys and a 19-byte message to be /// included in the range proof. The proof can contain 23 bytes but 4 bytes are used to confirm that a rewind /// was performed correctly fn construct_proof_with_rewind_key( @@ -65,7 +65,7 @@ pub trait RewindableRangeProofService { ) -> Result, RangeProofError>; } -/// Rewind data extracted from a rangeproof containing the committed value and the 19 byte proof message. +/// Rewind data extracted from a range proof containing the committed value and the 19 byte proof message. #[derive(Debug, PartialEq)] pub struct RewindResult { pub committed_value: u64, diff --git a/src/ristretto/bulletproofs_plus.rs b/src/ristretto/bulletproofs_plus.rs index 47ebf645..b8933c0f 100644 --- a/src/ristretto/bulletproofs_plus.rs +++ b/src/ristretto/bulletproofs_plus.rs @@ -290,6 +290,34 @@ impl ExtendedRangeProofService for BulletproofsPlusService { type PK = RistrettoPublicKey; type Proof = Vec; + fn construct_proof_with_recovery_seed_nonce( + &self, + mask: &Self::K, + value: u64, + seed_nonce: &Self::K, + ) -> Result { + let commitment = self + .generators + .pc_gens() + .commit(&Scalar::from(value), &[mask.0]) + .map_err(|e| RangeProofError::ProofConstructionError(e.to_string()))?; + let opening = CommitmentOpening::new(value, vec![mask.0]); + let witness = + RangeWitness::init(vec![opening]).map_err(|e| RangeProofError::ProofConstructionError(e.to_string()))?; + let statement = RangeStatement::init( + self.generators.clone(), + vec![commitment], + vec![None], + Some(seed_nonce.0), + ) + .map_err(|e| RangeProofError::ProofConstructionError(e.to_string()))?; + + let proof = RistrettoRangeProof::prove(self.transcript_label, &statement, &witness) + .map_err(|e| RangeProofError::ProofConstructionError(e.to_string()))?; + + Ok(proof.to_bytes()) + } + fn construct_extended_proof( &self, extended_witnesses: Vec, @@ -392,6 +420,53 @@ impl ExtendedRangeProofService for BulletproofsPlusService { } fn recover_mask( + &self, + proof: &Self::Proof, + commitment: &HomomorphicCommitment, + seed_nonce: &Self::K, + ) -> Result { + match RistrettoRangeProof::from_bytes(proof).map_err(|e| RangeProofError::InvalidRangeProof(e.to_string())) { + Ok(rp) => { + let statement = RangeStatement { + generators: self.generators.clone(), + commitments: vec![commitment.0.point()], + commitments_compressed: vec![*commitment.0.compressed()], + minimum_value_promises: vec![None], + seed_nonce: Some(seed_nonce.0), + }; + // Prepare the range statement + + match RistrettoRangeProof::recover_masks_ony(self.transcript_label, &vec![statement], &[rp]) { + Ok(recovered_mask) => { + if recovered_mask.is_empty() { + Err(RangeProofError::InvalidRewind( + "Mask could not be recovered".to_string(), + )) + } else if let Some(mask) = &recovered_mask[0] { + Ok(RistrettoSecretKey( + mask.blindings() + .map_err(|e| RangeProofError::InvalidRewind(e.to_string()))?[0], + )) + } else { + Err(RangeProofError::InvalidRewind( + "Mask could not be recovered".to_string(), + )) + } + }, + Err(e) => Err(RangeProofError::InvalidRangeProof(format!( + "Internal range proof error ({})", + e + ))), + } + }, + Err(e) => Err(RangeProofError::InvalidRangeProof(format!( + "Range proof could not be deserialized ({})", + e + ))), + } + } + + fn recover_extended_mask( &self, proof: &Self::Proof, statement: &RistrettoAggregatedPrivateStatement, @@ -427,6 +502,23 @@ impl ExtendedRangeProofService for BulletproofsPlusService { } fn verify_mask( + &self, + commitment: &HomomorphicCommitment, + mask: &Self::K, + value: u64, + ) -> Result { + match self + .generators + .pc_gens() + .commit(&Scalar::from(value), &[mask.0]) + .map_err(|e| RangeProofError::ExtensionDegree(e.to_string())) + { + Ok(val) => Ok(val == commitment.0.point()), + Err(e) => Err(e), + } + } + + fn verify_extended_mask( &self, commitment: &HomomorphicCommitment, extended_mask: &RistrettoExtendedMask, @@ -506,7 +598,7 @@ mod test { /// The 'BulletproofsPlusService' interface 'construct_proof' should only accept Pedersen generators of /// 'ExtensionDegree::Zero' with 'aggregation_size == 1' and values proportional to the bit length #[test] - fn test_construct_verify_proof() { + fn test_construct_verify_proof_no_recovery() { let mut rng = rand::thread_rng(); for extension_degree in EXTENSION_DEGREE { let factory = ExtendedPedersenCommitmentFactory::new_with_extension_degree(extension_degree).unwrap(); @@ -536,7 +628,7 @@ mod test { #[test] #[allow(clippy::too_many_lines)] - fn test_construct_verify_extended_proof() { + fn test_construct_verify_extended_proof_with_recovery() { static BIT_LENGTH: [usize; 2] = [2, 64]; static AGGREGATION_SIZE: [usize; 2] = [2, 64]; let mut rng = rand::thread_rng(); @@ -622,13 +714,13 @@ mod test { // --- Only recover the masks for (i, proof) in proofs.iter().enumerate() { let recovered_private_mask = bulletproofs_plus_service - .recover_mask(proof, &statements_private[i]) + .recover_extended_mask(proof, &statements_private[i]) .unwrap(); assert_eq!(private_masks[i], recovered_private_mask); for statement in &statements_private[i].statements { if let Some(this_mask) = recovered_private_mask.clone() { assert!(bulletproofs_plus_service - .verify_mask( + .verify_extended_mask( &statement.commitment, &this_mask, *commitment_value_map_private.get(&statement.commitment).unwrap() @@ -649,7 +741,7 @@ mod test { if let Some(this_mask) = recovered_private_masks[index].clone() { // Verify the recovered mask assert!(bulletproofs_plus_service - .verify_mask( + .verify_extended_mask( &statement.commitment, &this_mask, *commitment_value_map_private.get(&statement.commitment).unwrap() @@ -679,7 +771,7 @@ mod test { } #[test] - fn test_construct_verify_simple_extended_proof() { + fn test_construct_verify_simple_extended_proof_with_recovery() { let bit_length = 64usize; let aggregation_size = 1usize; let extension_degree = CommitmentExtensionDegree::DefaultPedersen; @@ -727,18 +819,18 @@ mod test { .unwrap(); // --- Only recover the mask (use the wrong transcript label for the service - will fail) let recovered_private_mask = verifiers_bulletproofs_plus_service - .recover_mask(&proof, &statement_private) + .recover_extended_mask(&proof, &statement_private) .unwrap(); assert_ne!(private_mask, recovered_private_mask); // --- Only recover the mask (use the correct transcript label for the service) verifiers_bulletproofs_plus_service.custom_transcript_label("123 range proof"); let recovered_private_mask = verifiers_bulletproofs_plus_service - .recover_mask(&proof, &statement_private) + .recover_extended_mask(&proof, &statement_private) .unwrap(); assert_eq!(private_mask, recovered_private_mask); if let Some(this_mask) = recovered_private_mask { assert!(verifiers_bulletproofs_plus_service - .verify_mask( + .verify_extended_mask( &statement_private.statements[0].commitment, &this_mask, extended_witness.value, @@ -755,7 +847,7 @@ mod test { if let Some(this_mask) = recovered_private_masks[0].clone() { // Verify the recovered mask assert!(verifiers_bulletproofs_plus_service - .verify_mask( + .verify_extended_mask( &statement_private.statements[0].commitment, &this_mask, extended_witness.value, @@ -784,4 +876,56 @@ mod test { .verify_batch(vec![&proof], vec![&statement_public]) .is_ok()); } + + #[test] + fn test_construct_verify_simple_proof_with_recovery() { + let bit_length = 64usize; + let aggregation_size = 1usize; + let extension_degree = CommitmentExtensionDegree::DefaultPedersen; + let mut rng = rand::thread_rng(); + let factory = ExtendedPedersenCommitmentFactory::new_with_extension_degree(extension_degree).unwrap(); + #[allow(clippy::cast_possible_truncation)] + let (value_min, value_max) = (0u64, (1u128 << (bit_length - 1)) as u64); + // 1. Prover's service + let mut provers_bulletproofs_plus_service = + BulletproofsPlusService::init(bit_length, aggregation_size, factory.clone()).unwrap(); + provers_bulletproofs_plus_service.custom_transcript_label("123 range proof"); + + // 2. Create witness data + let value = rng.gen_range(value_min..value_max); + let mask = RistrettoSecretKey(Scalar::random_not_zero(&mut rng)); + let commitment = factory.commit_value(&mask, value); + + // 4. Create the proof + let seed_nonce = RistrettoSecretKey(Scalar::random_not_zero(&mut rng)); + let proof = provers_bulletproofs_plus_service + .construct_proof_with_recovery_seed_nonce(&mask, value, &seed_nonce) + .unwrap(); + + // 5. Verifier's service + let mut verifiers_bulletproofs_plus_service = + BulletproofsPlusService::init(bit_length, aggregation_size, factory.clone()).unwrap(); + + // 6. Mask recovery as the commitment owner, i.e. the prover self + // --- Recover the mask (use the wrong transcript label for the service - will fail) + let recovered_mask = verifiers_bulletproofs_plus_service + .recover_mask(&proof, &commitment, &seed_nonce) + .unwrap(); + assert_ne!(mask, recovered_mask); + // --- Recover the mask (use the correct transcript label for the service) + verifiers_bulletproofs_plus_service.custom_transcript_label("123 range proof"); + let recovered_mask = verifiers_bulletproofs_plus_service + .recover_mask(&proof, &commitment, &seed_nonce) + .unwrap(); + assert_eq!(mask, recovered_mask); + // --- Verify that the mask opens the commitment + assert!(verifiers_bulletproofs_plus_service + .verify_mask(&commitment, &recovered_mask, value) + .unwrap()); + // --- Also verify that the commitment factory can open the commitment + assert!(factory.open_value(&recovered_mask, value, &commitment)); + + // 7. Verify the proof as private or public entity + assert!(verifiers_bulletproofs_plus_service.verify(&proof, &commitment)); + } } diff --git a/src/ristretto/ristretto_keys.rs b/src/ristretto/ristretto_keys.rs index 0a0cd4f3..1a2b9238 100644 --- a/src/ristretto/ristretto_keys.rs +++ b/src/ristretto/ristretto_keys.rs @@ -179,6 +179,12 @@ impl From for RistrettoSecretKey { } } +impl From for RistrettoSecretKey { + fn from(s: Scalar) -> Self { + RistrettoSecretKey(s) + } +} + //--------------------------------------------- Borrow impl -------------------------------------------------// impl<'a> Borrow for &'a RistrettoSecretKey {