synor/crates/synor-privacy/src/pedersen.rs
Gulshan Yadav 49ba05168c feat(privacy): add Phase 14 Milestone 2 - Privacy Layer
Implements comprehensive privacy primitives for confidential transactions:

- synor-privacy crate:
  - Pedersen commitments for hidden amounts with homomorphic properties
  - Simplified range proofs (bit-wise) for value validity
  - Stealth addresses with ViewKey/SpendKey for receiver privacy
  - LSAG ring signatures for sender anonymity
  - Key images for double-spend prevention
  - Confidential transaction type combining all primitives

- contracts/confidential-token:
  - WASM smart contract for privacy-preserving tokens
  - UTXO-based model (similar to Monero/Zcash)
  - Methods: mint, transfer, burn with ring signature verification

42 passing tests, 45KB WASM output.
2026-01-19 17:58:11 +05:30

499 lines
14 KiB
Rust

//! 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<R: RngCore + CryptoRng>(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<R: RngCore + CryptoRng>(rng: &mut R) -> Self {
Self {
scalar: random_scalar(rng),
}
}
/// Create from raw bytes (32 bytes)
pub fn from_bytes(bytes: &[u8; 32]) -> Result<Self> {
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<BlindingFactorBytes> for BlindingFactor {
type Error = Error;
fn try_from(bfb: BlindingFactorBytes) -> Result<Self> {
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<R: RngCore + CryptoRng>(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<Self> {
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<CommitmentBytes> for PedersenCommitment {
type Error = Error;
fn try_from(cb: CommitmentBytes) -> Result<Self> {
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());
}
}