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.
499 lines
14 KiB
Rust
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());
|
|
}
|
|
}
|