//! Pedersen Commitments //! //! A Pedersen commitment is a cryptographic primitive that allows committing to //! a value while keeping it hidden. The commitment is: //! //! ```text //! C = g^v * h^r //! ``` //! //! Where: //! - `g` and `h` are generator points on an elliptic curve //! - `v` is the value being committed //! - `r` is a random blinding factor //! //! ## Properties //! //! 1. **Hiding**: Given C, it's computationally infeasible to determine v //! 2. **Binding**: It's infeasible to find different (v', r') that produce the same C //! 3. **Homomorphic**: C(v1, r1) + C(v2, r2) = C(v1 + v2, r1 + r2) //! //! The homomorphic property is crucial for verifying that transaction inputs //! equal outputs without revealing the amounts. use core::ops::{Add, Sub, Neg}; use curve25519_dalek::{ constants::RISTRETTO_BASEPOINT_POINT, ristretto::{CompressedRistretto, RistrettoPoint}, scalar::Scalar, }; use rand_core::{CryptoRng, RngCore}; use serde::{Deserialize, Serialize}; use borsh::{BorshSerialize, BorshDeserialize}; use sha2::{Sha512, Digest}; use zeroize::Zeroize; use crate::{Error, Result, DOMAIN_SEPARATOR}; /// Generate a random scalar using the provided RNG fn random_scalar(rng: &mut R) -> Scalar { let mut bytes = [0u8; 64]; rng.fill_bytes(&mut bytes); Scalar::from_bytes_mod_order_wide(&bytes) } /// Generator point G (base point) pub fn generator_g() -> RistrettoPoint { RISTRETTO_BASEPOINT_POINT } /// Generator point H (derived from G via hash-to-curve) /// H is chosen such that the discrete log relationship between G and H is unknown pub fn generator_h() -> RistrettoPoint { let mut hasher = Sha512::new(); hasher.update(DOMAIN_SEPARATOR); hasher.update(b"GENERATOR_H"); hasher.update(RISTRETTO_BASEPOINT_POINT.compress().as_bytes()); RistrettoPoint::from_hash(hasher) } /// A blinding factor (random scalar used in commitments) #[derive(Clone, Zeroize)] #[zeroize(drop)] pub struct BlindingFactor { scalar: Scalar, } impl BlindingFactor { /// Generate a random blinding factor pub fn random(rng: &mut R) -> Self { Self { scalar: random_scalar(rng), } } /// Create from raw bytes (32 bytes) pub fn from_bytes(bytes: &[u8; 32]) -> Result { let scalar = Scalar::from_canonical_bytes(*bytes) .into_option() .ok_or_else(|| Error::InvalidBlindingFactor("Invalid scalar bytes".into()))?; Ok(Self { scalar }) } /// Convert to bytes pub fn to_bytes(&self) -> [u8; 32] { self.scalar.to_bytes() } /// Get the inner scalar pub fn as_scalar(&self) -> &Scalar { &self.scalar } /// Create a zero blinding factor (for testing only!) #[cfg(test)] pub fn zero() -> Self { Self { scalar: Scalar::ZERO, } } /// Create from a scalar directly pub fn from_scalar(scalar: Scalar) -> Self { Self { scalar } } } impl Add for BlindingFactor { type Output = Self; fn add(self, other: Self) -> Self { Self { scalar: self.scalar + other.scalar, } } } impl Add for &BlindingFactor { type Output = BlindingFactor; fn add(self, other: &BlindingFactor) -> BlindingFactor { BlindingFactor { scalar: self.scalar + other.scalar, } } } impl Sub for BlindingFactor { type Output = Self; fn sub(self, other: Self) -> Self { Self { scalar: self.scalar - other.scalar, } } } impl Sub for &BlindingFactor { type Output = BlindingFactor; fn sub(self, other: &BlindingFactor) -> BlindingFactor { BlindingFactor { scalar: self.scalar - other.scalar, } } } impl Neg for BlindingFactor { type Output = Self; fn neg(self) -> Self { Self { scalar: -self.scalar, } } } /// Serializable wrapper for blinding factor #[derive(Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] pub struct BlindingFactorBytes { bytes: [u8; 32], } impl From<&BlindingFactor> for BlindingFactorBytes { fn from(bf: &BlindingFactor) -> Self { Self { bytes: bf.to_bytes(), } } } impl TryFrom for BlindingFactor { type Error = Error; fn try_from(bfb: BlindingFactorBytes) -> Result { BlindingFactor::from_bytes(&bfb.bytes) } } /// A Pedersen commitment to a value #[derive(Clone, Copy, PartialEq, Eq)] pub struct PedersenCommitment { point: RistrettoPoint, } impl PedersenCommitment { /// Create a commitment to a value with a random blinding factor pub fn commit_random(value: u64, rng: &mut R) -> (Self, BlindingFactor) { let blinding = BlindingFactor::random(rng); let commitment = Self::commit(value, &blinding); (commitment, blinding) } /// Create a commitment to a value with a specific blinding factor pub fn commit(value: u64, blinding: &BlindingFactor) -> Self { let g = generator_g(); let h = generator_h(); let value_scalar = Scalar::from(value); let point = g * value_scalar + h * blinding.scalar; Self { point } } /// Create a commitment to zero (for balance proofs) pub fn commit_zero(blinding: &BlindingFactor) -> Self { Self::commit(0, blinding) } /// Verify that two sets of commitments balance /// sum(inputs) - sum(outputs) should equal a commitment to 0 pub fn verify_balance( inputs: &[PedersenCommitment], outputs: &[PedersenCommitment], excess_blinding: &BlindingFactor, ) -> bool { let sum_inputs: RistrettoPoint = inputs.iter().map(|c| c.point).sum(); let sum_outputs: RistrettoPoint = outputs.iter().map(|c| c.point).sum(); // The difference should be h^excess_blinding (commitment to 0) let expected_excess = generator_h() * excess_blinding.scalar; sum_inputs - sum_outputs == expected_excess } /// Get the compressed representation (32 bytes) pub fn to_bytes(&self) -> [u8; 32] { self.point.compress().to_bytes() } /// Create from compressed bytes pub fn from_bytes(bytes: &[u8; 32]) -> Result { let compressed = CompressedRistretto::from_slice(bytes) .map_err(|_| Error::InvalidCommitment("Invalid compressed point length".into()))?; let point = compressed .decompress() .ok_or_else(|| Error::InvalidCommitment("Point not on curve".into()))?; Ok(Self { point }) } /// Get the inner point pub fn as_point(&self) -> &RistrettoPoint { &self.point } /// Create from a point directly pub fn from_point(point: RistrettoPoint) -> Self { Self { point } } } impl Add for PedersenCommitment { type Output = Self; fn add(self, other: Self) -> Self { Self { point: self.point + other.point, } } } impl Add for &PedersenCommitment { type Output = PedersenCommitment; fn add(self, other: &PedersenCommitment) -> PedersenCommitment { PedersenCommitment { point: self.point + other.point, } } } impl Sub for PedersenCommitment { type Output = Self; fn sub(self, other: Self) -> Self { Self { point: self.point - other.point, } } } impl Sub for &PedersenCommitment { type Output = PedersenCommitment; fn sub(self, other: &PedersenCommitment) -> PedersenCommitment { PedersenCommitment { point: self.point - other.point, } } } impl Neg for PedersenCommitment { type Output = Self; fn neg(self) -> Self { Self { point: -self.point } } } impl core::fmt::Debug for PedersenCommitment { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { let bytes = self.to_bytes(); write!(f, "PedersenCommitment({:02x}{:02x}{:02x}{:02x}...)", bytes[0], bytes[1], bytes[2], bytes[3]) } } /// Serializable wrapper for Pedersen commitment #[derive(Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize, Debug)] pub struct CommitmentBytes { bytes: [u8; 32], } impl From<&PedersenCommitment> for CommitmentBytes { fn from(c: &PedersenCommitment) -> Self { Self { bytes: c.to_bytes(), } } } impl TryFrom for PedersenCommitment { type Error = Error; fn try_from(cb: CommitmentBytes) -> Result { PedersenCommitment::from_bytes(&cb.bytes) } } /// Batch operations on commitments pub struct CommitmentBatch; impl CommitmentBatch { /// Sum multiple commitments pub fn sum(commitments: &[PedersenCommitment]) -> PedersenCommitment { let point: RistrettoPoint = commitments.iter().map(|c| c.point).sum(); PedersenCommitment { point } } /// Sum multiple blinding factors pub fn sum_blindings(blindings: &[BlindingFactor]) -> BlindingFactor { let scalar: Scalar = blindings.iter().map(|b| b.scalar).sum(); BlindingFactor { scalar } } /// Compute the excess blinding factor for a transaction /// excess = sum(input_blindings) - sum(output_blindings) pub fn compute_excess( input_blindings: &[BlindingFactor], output_blindings: &[BlindingFactor], ) -> BlindingFactor { let sum_inputs: Scalar = input_blindings.iter().map(|b| b.scalar).sum(); let sum_outputs: Scalar = output_blindings.iter().map(|b| b.scalar).sum(); BlindingFactor { scalar: sum_inputs - sum_outputs, } } } #[cfg(test)] mod tests { use super::*; use rand::rngs::OsRng; #[test] fn test_commitment_creation() { let mut rng = OsRng; let (commitment, _blinding) = PedersenCommitment::commit_random(1000, &mut rng); // Commitment should be a valid point let bytes = commitment.to_bytes(); let recovered = PedersenCommitment::from_bytes(&bytes).unwrap(); assert_eq!(commitment, recovered); } #[test] fn test_commitment_deterministic() { let blinding = BlindingFactor::from_bytes(&[1u8; 32]).unwrap(); let c1 = PedersenCommitment::commit(100, &blinding); let c2 = PedersenCommitment::commit(100, &blinding); assert_eq!(c1, c2); } #[test] fn test_commitment_different_values() { let blinding = BlindingFactor::from_bytes(&[1u8; 32]).unwrap(); let c1 = PedersenCommitment::commit(100, &blinding); let c2 = PedersenCommitment::commit(200, &blinding); assert_ne!(c1, c2); } #[test] fn test_commitment_different_blindings() { let b1 = BlindingFactor::from_bytes(&[1u8; 32]).unwrap(); let b2 = BlindingFactor::from_bytes(&[2u8; 32]).unwrap(); let c1 = PedersenCommitment::commit(100, &b1); let c2 = PedersenCommitment::commit(100, &b2); assert_ne!(c1, c2); } #[test] fn test_homomorphic_addition() { let mut rng = OsRng; let b1 = BlindingFactor::random(&mut rng); let b2 = BlindingFactor::random(&mut rng); let c1 = PedersenCommitment::commit(100, &b1); let c2 = PedersenCommitment::commit(200, &b2); let c_sum = c1 + c2; // c_sum should equal commitment to 300 with b1 + b2 let b_sum = &b1 + &b2; let expected = PedersenCommitment::commit(300, &b_sum); assert_eq!(c_sum, expected); } #[test] fn test_balance_verification() { let mut rng = OsRng; // Create inputs: 100 + 200 = 300 let b_in1 = BlindingFactor::random(&mut rng); let b_in2 = BlindingFactor::random(&mut rng); let c_in1 = PedersenCommitment::commit(100, &b_in1); let c_in2 = PedersenCommitment::commit(200, &b_in2); // Create outputs: 150 + 150 = 300 let b_out1 = BlindingFactor::random(&mut rng); let b_out2 = BlindingFactor::random(&mut rng); let c_out1 = PedersenCommitment::commit(150, &b_out1); let c_out2 = PedersenCommitment::commit(150, &b_out2); // Calculate excess blinding let excess = CommitmentBatch::compute_excess( &[b_in1, b_in2], &[b_out1, b_out2], ); // Verify balance assert!(PedersenCommitment::verify_balance( &[c_in1, c_in2], &[c_out1, c_out2], &excess, )); } #[test] fn test_balance_verification_fails_on_mismatch() { let mut rng = OsRng; // Inputs: 100 let b_in = BlindingFactor::random(&mut rng); let c_in = PedersenCommitment::commit(100, &b_in); // Outputs: 200 (doesn't balance!) let b_out = BlindingFactor::random(&mut rng); let c_out = PedersenCommitment::commit(200, &b_out); let excess = CommitmentBatch::compute_excess(&[b_in], &[b_out]); // Should fail - values don't balance assert!(!PedersenCommitment::verify_balance( &[c_in], &[c_out], &excess, )); } #[test] fn test_batch_sum() { let mut rng = OsRng; let (c1, b1) = PedersenCommitment::commit_random(100, &mut rng); let (c2, b2) = PedersenCommitment::commit_random(200, &mut rng); let (c3, b3) = PedersenCommitment::commit_random(300, &mut rng); let sum = CommitmentBatch::sum(&[c1, c2, c3]); let b_sum = CommitmentBatch::sum_blindings(&[b1, b2, b3]); let expected = PedersenCommitment::commit(600, &b_sum); assert_eq!(sum, expected); } #[test] fn test_blinding_factor_serialization() { let mut rng = OsRng; let bf = BlindingFactor::random(&mut rng); let bytes = bf.to_bytes(); let recovered = BlindingFactor::from_bytes(&bytes).unwrap(); assert_eq!(bf.to_bytes(), recovered.to_bytes()); } #[test] fn test_generators_are_different() { let g = generator_g(); let h = generator_h(); assert_ne!(g.compress(), h.compress()); } }