solana_zk_sdk/encryption/
pedersen.rs

1//! Pedersen commitment implementation using the Ristretto prime-order group.
2
3#[cfg(target_arch = "wasm32")]
4use wasm_bindgen::prelude::*;
5use {
6    crate::encryption::{PEDERSEN_COMMITMENT_LEN, PEDERSEN_OPENING_LEN},
7    core::ops::{Add, Mul, Sub},
8    curve25519_dalek::{
9        constants::{RISTRETTO_BASEPOINT_COMPRESSED, RISTRETTO_BASEPOINT_POINT},
10        ristretto::{CompressedRistretto, RistrettoPoint},
11        scalar::Scalar,
12        traits::MultiscalarMul,
13    },
14    rand::rngs::OsRng,
15    serde::{Deserialize, Serialize},
16    sha3::Sha3_512,
17    std::convert::TryInto,
18    subtle::{Choice, ConstantTimeEq},
19    zeroize::Zeroize,
20};
21
22/// Pedersen base point for encoding messages to be committed.
23pub const G: RistrettoPoint = RISTRETTO_BASEPOINT_POINT;
24/// Pedersen base point for encoding the commitment openings.
25pub static H: std::sync::LazyLock<RistrettoPoint> = std::sync::LazyLock::new(|| {
26    RistrettoPoint::hash_from_bytes::<Sha3_512>(RISTRETTO_BASEPOINT_COMPRESSED.as_bytes())
27});
28
29/// Algorithm handle for the Pedersen commitment scheme.
30#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
31pub struct Pedersen;
32impl Pedersen {
33    /// On input a message (numeric amount), the function returns a Pedersen commitment of the
34    /// message and the corresponding opening.
35    ///
36    /// This function is randomized. It internally samples a Pedersen opening using `OsRng`.
37    #[allow(clippy::new_ret_no_self)]
38    pub fn new<T: Into<Scalar>>(amount: T) -> (PedersenCommitment, PedersenOpening) {
39        let opening = PedersenOpening::new_rand();
40        let commitment = Pedersen::with(amount, &opening);
41
42        (commitment, opening)
43    }
44
45    /// On input a message (numeric amount) and a Pedersen opening, the function returns the
46    /// corresponding Pedersen commitment.
47    ///
48    /// This function is deterministic.
49    pub fn with<T: Into<Scalar>>(amount: T, opening: &PedersenOpening) -> PedersenCommitment {
50        let x: Scalar = amount.into();
51        let r = opening.get_scalar();
52
53        PedersenCommitment(RistrettoPoint::multiscalar_mul(&[x, *r], &[G, *H]))
54    }
55
56    /// On input a message (numeric amount), the function returns a Pedersen commitment with zero
57    /// as the opening.
58    ///
59    /// This function is deterministic.
60    pub fn encode<T: Into<Scalar>>(amount: T) -> PedersenCommitment {
61        PedersenCommitment(amount.into() * &G)
62    }
63}
64
65#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
66impl Pedersen {
67    #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = withU64))]
68    pub fn with_u64(amount: u64, opening: &PedersenOpening) -> PedersenCommitment {
69        Pedersen::with(amount, opening)
70    }
71}
72
73/// Pedersen opening type.
74///
75/// Instances of Pedersen openings are zeroized on drop.
76#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
77#[derive(Clone, Debug, Default, Serialize, Deserialize, Zeroize)]
78#[zeroize(drop)]
79pub struct PedersenOpening(Scalar);
80
81#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
82impl PedersenOpening {
83    #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = newRand))]
84    pub fn new_rand() -> Self {
85        PedersenOpening(Scalar::random(&mut OsRng))
86    }
87}
88
89impl PedersenOpening {
90    pub fn new(scalar: Scalar) -> Self {
91        Self(scalar)
92    }
93
94    pub fn get_scalar(&self) -> &Scalar {
95        &self.0
96    }
97
98    pub fn as_bytes(&self) -> &[u8; PEDERSEN_OPENING_LEN] {
99        self.0.as_bytes()
100    }
101
102    pub fn to_bytes(&self) -> [u8; PEDERSEN_OPENING_LEN] {
103        self.0.to_bytes()
104    }
105
106    pub fn from_bytes(bytes: &[u8]) -> Option<PedersenOpening> {
107        match bytes.try_into() {
108            Ok(bytes) => Scalar::from_canonical_bytes(bytes)
109                .into_option()
110                .map(PedersenOpening),
111            _ => None,
112        }
113    }
114}
115impl Eq for PedersenOpening {}
116impl PartialEq for PedersenOpening {
117    fn eq(&self, other: &Self) -> bool {
118        self.ct_eq(other).unwrap_u8() == 1u8
119    }
120}
121impl ConstantTimeEq for PedersenOpening {
122    fn ct_eq(&self, other: &Self) -> Choice {
123        self.0.ct_eq(&other.0)
124    }
125}
126
127impl<'b> Add<&'b PedersenOpening> for &PedersenOpening {
128    type Output = PedersenOpening;
129
130    fn add(self, opening: &'b PedersenOpening) -> PedersenOpening {
131        PedersenOpening(&self.0 + &opening.0)
132    }
133}
134
135define_add_variants!(
136    LHS = PedersenOpening,
137    RHS = PedersenOpening,
138    Output = PedersenOpening
139);
140
141impl<'b> Sub<&'b PedersenOpening> for &PedersenOpening {
142    type Output = PedersenOpening;
143
144    fn sub(self, opening: &'b PedersenOpening) -> PedersenOpening {
145        PedersenOpening(&self.0 - &opening.0)
146    }
147}
148
149define_sub_variants!(
150    LHS = PedersenOpening,
151    RHS = PedersenOpening,
152    Output = PedersenOpening
153);
154
155impl<'b> Mul<&'b Scalar> for &PedersenOpening {
156    type Output = PedersenOpening;
157
158    fn mul(self, scalar: &'b Scalar) -> PedersenOpening {
159        PedersenOpening(&self.0 * scalar)
160    }
161}
162
163define_mul_variants!(
164    LHS = PedersenOpening,
165    RHS = Scalar,
166    Output = PedersenOpening
167);
168
169impl<'b> Mul<&'b PedersenOpening> for &Scalar {
170    type Output = PedersenOpening;
171
172    fn mul(self, opening: &'b PedersenOpening) -> PedersenOpening {
173        PedersenOpening(self * &opening.0)
174    }
175}
176
177define_mul_variants!(
178    LHS = Scalar,
179    RHS = PedersenOpening,
180    Output = PedersenOpening
181);
182
183/// Pedersen commitment type.
184#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
185#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
186pub struct PedersenCommitment(RistrettoPoint);
187impl PedersenCommitment {
188    pub fn new(point: RistrettoPoint) -> Self {
189        Self(point)
190    }
191
192    pub fn get_point(&self) -> &RistrettoPoint {
193        &self.0
194    }
195
196    pub fn to_bytes(&self) -> [u8; PEDERSEN_COMMITMENT_LEN] {
197        self.0.compress().to_bytes()
198    }
199
200    pub fn from_bytes(bytes: &[u8]) -> Option<PedersenCommitment> {
201        if bytes.len() != PEDERSEN_COMMITMENT_LEN {
202            return None;
203        }
204
205        let Ok(compressed_ristretto) = CompressedRistretto::from_slice(bytes) else {
206            return None;
207        };
208
209        compressed_ristretto.decompress().map(PedersenCommitment)
210    }
211}
212
213impl<'b> Add<&'b PedersenCommitment> for &PedersenCommitment {
214    type Output = PedersenCommitment;
215
216    fn add(self, commitment: &'b PedersenCommitment) -> PedersenCommitment {
217        PedersenCommitment(&self.0 + &commitment.0)
218    }
219}
220
221define_add_variants!(
222    LHS = PedersenCommitment,
223    RHS = PedersenCommitment,
224    Output = PedersenCommitment
225);
226
227impl<'b> Sub<&'b PedersenCommitment> for &PedersenCommitment {
228    type Output = PedersenCommitment;
229
230    fn sub(self, commitment: &'b PedersenCommitment) -> PedersenCommitment {
231        PedersenCommitment(&self.0 - &commitment.0)
232    }
233}
234
235define_sub_variants!(
236    LHS = PedersenCommitment,
237    RHS = PedersenCommitment,
238    Output = PedersenCommitment
239);
240
241impl<'b> Mul<&'b Scalar> for &PedersenCommitment {
242    type Output = PedersenCommitment;
243
244    fn mul(self, scalar: &'b Scalar) -> PedersenCommitment {
245        PedersenCommitment(scalar * &self.0)
246    }
247}
248
249define_mul_variants!(
250    LHS = PedersenCommitment,
251    RHS = Scalar,
252    Output = PedersenCommitment
253);
254
255impl<'b> Mul<&'b PedersenCommitment> for &Scalar {
256    type Output = PedersenCommitment;
257
258    fn mul(self, commitment: &'b PedersenCommitment) -> PedersenCommitment {
259        PedersenCommitment(self * &commitment.0)
260    }
261}
262
263define_mul_variants!(
264    LHS = Scalar,
265    RHS = PedersenCommitment,
266    Output = PedersenCommitment
267);
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272
273    #[test]
274    fn test_pedersen_homomorphic_addition() {
275        let amount_0: u64 = 77;
276        let amount_1: u64 = 57;
277
278        let rng = &mut OsRng;
279        let opening_0 = PedersenOpening(Scalar::random(rng));
280        let opening_1 = PedersenOpening(Scalar::random(rng));
281
282        let commitment_0 = Pedersen::with(amount_0, &opening_0);
283        let commitment_1 = Pedersen::with(amount_1, &opening_1);
284        let commitment_addition = Pedersen::with(amount_0 + amount_1, &(opening_0 + opening_1));
285
286        assert_eq!(commitment_addition, commitment_0 + commitment_1);
287    }
288
289    #[test]
290    fn test_pedersen_homomorphic_subtraction() {
291        let amount_0: u64 = 77;
292        let amount_1: u64 = 57;
293
294        let rng = &mut OsRng;
295        let opening_0 = PedersenOpening(Scalar::random(rng));
296        let opening_1 = PedersenOpening(Scalar::random(rng));
297
298        let commitment_0 = Pedersen::with(amount_0, &opening_0);
299        let commitment_1 = Pedersen::with(amount_1, &opening_1);
300        let commitment_addition = Pedersen::with(amount_0 - amount_1, &(opening_0 - opening_1));
301
302        assert_eq!(commitment_addition, commitment_0 - commitment_1);
303    }
304
305    #[test]
306    fn test_pedersen_homomorphic_multiplication() {
307        let amount_0: u64 = 77;
308        let amount_1: u64 = 57;
309
310        let (commitment, opening) = Pedersen::new(amount_0);
311        let scalar = Scalar::from(amount_1);
312        let commitment_multiplication = Pedersen::with(amount_0 * amount_1, &(opening * scalar));
313
314        assert_eq!(commitment_multiplication, commitment * scalar);
315        assert_eq!(commitment_multiplication, scalar * commitment);
316    }
317
318    #[test]
319    fn test_pedersen_commitment_bytes() {
320        let amount: u64 = 77;
321        let (commitment, _) = Pedersen::new(amount);
322
323        let encoded = commitment.to_bytes();
324        let decoded = PedersenCommitment::from_bytes(&encoded).unwrap();
325
326        assert_eq!(commitment, decoded);
327
328        // incorrect length encoding
329        assert_eq!(PedersenCommitment::from_bytes(&[0; 33]), None);
330    }
331
332    #[test]
333    fn test_pedersen_opening_bytes() {
334        let opening = PedersenOpening(Scalar::random(&mut OsRng));
335
336        let encoded = opening.to_bytes();
337        let decoded = PedersenOpening::from_bytes(&encoded).unwrap();
338
339        assert_eq!(opening, decoded);
340
341        // incorrect length encoding
342        assert_eq!(PedersenOpening::from_bytes(&[0; 33]), None);
343    }
344
345    #[test]
346    fn test_serde_pedersen_commitment() {
347        let amount: u64 = 77;
348        let (commitment, _) = Pedersen::new(amount);
349
350        let encoded = bincode::serialize(&commitment).unwrap();
351        let decoded: PedersenCommitment = bincode::deserialize(&encoded).unwrap();
352
353        assert_eq!(commitment, decoded);
354    }
355
356    #[test]
357    fn test_serde_pedersen_opening() {
358        let opening = PedersenOpening(Scalar::random(&mut OsRng));
359
360        let encoded = bincode::serialize(&opening).unwrap();
361        let decoded: PedersenOpening = bincode::deserialize(&encoded).unwrap();
362
363        assert_eq!(opening, decoded);
364    }
365
366    #[test]
367    fn test_homomorphic_addition_with_zero() {
368        let amount: u64 = 77;
369        let (commitment_0, opening_0) = Pedersen::new(amount);
370        let (commitment_1, opening_1) = Pedersen::new(0_u64);
371
372        // C(m+0, r1+r2) should equal C(m, r1) + C(0, r2)
373        let expected_commitment = Pedersen::with(amount, &(opening_0.clone() + opening_1.clone()));
374        assert_eq!(expected_commitment, commitment_0 + commitment_1);
375    }
376
377    #[test]
378    fn test_pedersen_encode() {
379        let amount: u64 = 123;
380
381        // Create commitment with zero opening explicitly
382        let zero_opening = PedersenOpening::default(); // default is a zero scalar
383        let commitment_with_zero = Pedersen::with(amount, &zero_opening);
384
385        // Compare with the encode function
386        let encoded_commitment = Pedersen::encode(amount);
387
388        assert_eq!(encoded_commitment, commitment_with_zero);
389
390        // Also verify it's not equal to a commitment with a random opening
391        let (random_commitment, _) = Pedersen::new(amount);
392        assert_ne!(encoded_commitment, random_commitment);
393    }
394
395    #[test]
396    fn test_invalid_commitment_verification() {
397        let amount: u64 = 50;
398        let wrong_amount: u64 = 51;
399
400        let (commitment, opening) = Pedersen::new(amount);
401
402        // Re-create commitment with the WRONG amount but correct opening
403        let forged_commitment = Pedersen::with(wrong_amount, &opening);
404
405        assert_ne!(commitment, forged_commitment);
406    }
407
408    #[test]
409    fn test_pedersen_commitment_from_invalid_bytes() {
410        // These bytes have the correct length (32) but do not represent a valid
411        // point on the curve. This is the compressed form of the point of order 4,
412        // which is invalid in the Ristretto group.
413        let invalid_bytes = [
414            1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
415            0, 0, 0,
416        ];
417
418        assert_eq!(PedersenCommitment::from_bytes(&invalid_bytes), None);
419    }
420}