Cryptographic commitments with homomorphic properties over Ristretto
Pedersen commitments are a fundamental building block of the ZK ElGamal Proof SDK. They provide a way to commit to a value without revealing it, while maintaining powerful homomorphic properties that enable arithmetic operations on committed values.
A Pedersen commitment is a cryptographic commitment scheme that allows you to commit to a value m with randomness r, producing a commitment C = m*G + r*H, where:
m: The message (value) being committed to
r: A random scalar called the “opening” or “blinding factor”
G, H: Independent base points on the Ristretto group
C: The resulting commitment (a Ristretto point)
The security of Pedersen commitments relies on the discrete logarithm assumption: given C, it’s computationally infeasible to find m and r without knowing them, and it’s impossible to find the relationship between G and H.
The SDK uses two base points defined in zk-sdk/src/encryption/pedersen.rs:20-25:
/// Pedersen base point for encoding messages to be committedpub const G: RistrettoPoint = RISTRETTO_BASEPOINT_POINT;/// Pedersen base point for encoding the commitment openingspub static H: std::sync::LazyLock<RistrettoPoint> = std::sync::LazyLock::new(|| { RistrettoPoint::hash_from_bytes::<Sha3_512>(RISTRETTO_BASEPOINT_COMPRESSED.as_bytes())});
G: The standard Ristretto basepoint
H: Derived deterministically by hashing G with SHA3-512
The discrete log relationship between G and H is unknown (computationally infeasible to find), which is essential for the hiding property of the commitment.
use solana_zk_sdk::encryption::pedersen::Pedersen;let amount = 100u64;let (commitment, opening) = Pedersen::new(amount);// The commitment is a point on the curve: C = amount*G + r*H// The opening contains the random scalar r
let amount = 50u64;#[allow(deprecated)]let commitment = Pedersen::encode(amount);// This is C = amount*G + 0*H = amount*G
Deprecated: The encode function creates a commitment with zero blinding (r=0). This is not hiding and is vulnerable to dictionary attacks for small values. Only use this in contexts where the value does not need to be confidential.
A commitment reveals nothing about the committed value:
let amount = 1000u64;let (commitment1, _) = Pedersen::new(amount);let (commitment2, _) = Pedersen::new(amount);// Even for the same amount, commitments are different due to randomnessassert_ne!(commitment1.to_bytes(), commitment2.to_bytes());// Without the opening, the amount cannot be determined
Once created, a commitment cannot be opened to a different value:
let amount = 100u64;let (commitment, opening) = Pedersen::new(amount);// It's computationally infeasible to find a different (amount', opening')// such that Pedersen::with(amount', &opening') == commitment
let (commitment, _) = Pedersen::new(100u64);let point = commitment.get_point();// point is a &RistrettoPoint that can be used for// advanced cryptographic operations
Commitments are used as inputs to zero-knowledge proofs:
use solana_zk_sdk::zk_elgamal_proof_program::proof_data::CiphertextCommitmentEqualityProofData;// Prove that a ciphertext and commitment encode the same valuelet proof_data = CiphertextCommitmentEqualityProofData::new( &keypair, &ciphertext, &commitment, &opening, amount,)?;
Range proofs verify that committed values are in a specific range:
use solana_zk_sdk::range_proof::RangeProof;use merlin::Transcript;let amount = 1000u64;let (commitment, opening) = Pedersen::new(amount);let mut transcript = Transcript::new(b"RangeProofTest");// Prove that amount is a 32-bit value (0 to 2^32-1)let proof = RangeProof::new( vec![amount], vec![32], // bit length vec![&opening], &mut transcript)?;
Use the same opening for multiple related commitments:
let shared_opening = PedersenOpening::new_rand();let commitment_1 = Pedersen::with(100u64, &shared_opening);let commitment_2 = Pedersen::with(200u64, &shared_opening);// Useful for proving relationships between values// while keeping them hidden
Commitments with zero opening (r=0) are not hiding:
// DON'T DO THIS for sensitive valueslet zero_opening = PedersenOpening::default();let commitment = Pedersen::with(42u64, &zero_opening);// Anyone can verify this by checking C == 42*G