solana_zk_token_sdk/encryption/
pedersen.rs

1//! Pedersen commitment implementation using the Ristretto prime-order group.
2
3#[cfg(not(target_os = "solana"))]
4use rand::rngs::OsRng;
5use {
6    crate::{RISTRETTO_POINT_LEN, SCALAR_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    serde::{Deserialize, Serialize},
15    sha3::Sha3_512,
16    std::convert::TryInto,
17    subtle::{Choice, ConstantTimeEq},
18    zeroize::Zeroize,
19};
20
21/// Byte length of a Pedersen opening.
22const PEDERSEN_OPENING_LEN: usize = SCALAR_LEN;
23
24/// Byte length of a Pedersen commitment.
25pub(crate) const PEDERSEN_COMMITMENT_LEN: usize = RISTRETTO_POINT_LEN;
26
27lazy_static::lazy_static! {
28    /// Pedersen base point for encoding messages to be committed.
29    pub static ref G: RistrettoPoint = RISTRETTO_BASEPOINT_POINT;
30    /// Pedersen base point for encoding the commitment openings.
31    pub static ref H: RistrettoPoint =
32        RistrettoPoint::hash_from_bytes::<Sha3_512>(RISTRETTO_BASEPOINT_COMPRESSED.as_bytes());
33}
34
35/// Algorithm handle for the Pedersen commitment scheme.
36pub struct Pedersen;
37impl Pedersen {
38    /// On input a message (numeric amount), the function returns a Pedersen commitment of the
39    /// message and the corresponding opening.
40    ///
41    /// This function is randomized. It internally samples a Pedersen opening using `OsRng`.
42    #[cfg(not(target_os = "solana"))]
43    #[allow(clippy::new_ret_no_self)]
44    pub fn new<T: Into<Scalar>>(amount: T) -> (PedersenCommitment, PedersenOpening) {
45        let opening = PedersenOpening::new_rand();
46        let commitment = Pedersen::with(amount, &opening);
47
48        (commitment, opening)
49    }
50
51    /// On input a message (numeric amount) and a Pedersen opening, the function returns the
52    /// corresponding Pedersen commitment.
53    ///
54    /// This function is deterministic.
55    #[allow(non_snake_case)]
56    pub fn with<T: Into<Scalar>>(amount: T, opening: &PedersenOpening) -> PedersenCommitment {
57        let x: Scalar = amount.into();
58        let r = opening.get_scalar();
59
60        PedersenCommitment(RistrettoPoint::multiscalar_mul(&[x, *r], &[*G, *H]))
61    }
62
63    /// On input a message (numeric amount), the function returns a Pedersen commitment with zero
64    /// as the opening.
65    ///
66    /// This function is deterministic.
67    pub fn encode<T: Into<Scalar>>(amount: T) -> PedersenCommitment {
68        PedersenCommitment(amount.into() * &(*G))
69    }
70}
71
72/// Pedersen opening type.
73///
74/// Instances of Pedersen openings are zeroized on drop.
75#[derive(Clone, Debug, Default, Serialize, Deserialize, Zeroize)]
76#[zeroize(drop)]
77pub struct PedersenOpening(Scalar);
78impl PedersenOpening {
79    pub fn new(scalar: Scalar) -> Self {
80        Self(scalar)
81    }
82
83    pub fn get_scalar(&self) -> &Scalar {
84        &self.0
85    }
86
87    #[cfg(not(target_os = "solana"))]
88    pub fn new_rand() -> Self {
89        PedersenOpening(Scalar::random(&mut OsRng))
90    }
91
92    pub fn as_bytes(&self) -> &[u8; PEDERSEN_OPENING_LEN] {
93        self.0.as_bytes()
94    }
95
96    pub fn to_bytes(&self) -> [u8; PEDERSEN_OPENING_LEN] {
97        self.0.to_bytes()
98    }
99
100    pub fn from_bytes(bytes: &[u8]) -> Option<PedersenOpening> {
101        match bytes.try_into() {
102            Ok(bytes) => Scalar::from_canonical_bytes(bytes)
103                .map(PedersenOpening)
104                .into(),
105            _ => None,
106        }
107    }
108}
109impl Eq for PedersenOpening {}
110impl PartialEq for PedersenOpening {
111    fn eq(&self, other: &Self) -> bool {
112        self.ct_eq(other).unwrap_u8() == 1u8
113    }
114}
115impl ConstantTimeEq for PedersenOpening {
116    fn ct_eq(&self, other: &Self) -> Choice {
117        self.0.ct_eq(&other.0)
118    }
119}
120
121impl<'b> Add<&'b PedersenOpening> for &PedersenOpening {
122    type Output = PedersenOpening;
123
124    fn add(self, opening: &'b PedersenOpening) -> PedersenOpening {
125        PedersenOpening(&self.0 + &opening.0)
126    }
127}
128
129define_add_variants!(
130    LHS = PedersenOpening,
131    RHS = PedersenOpening,
132    Output = PedersenOpening
133);
134
135impl<'b> Sub<&'b PedersenOpening> for &PedersenOpening {
136    type Output = PedersenOpening;
137
138    fn sub(self, opening: &'b PedersenOpening) -> PedersenOpening {
139        PedersenOpening(&self.0 - &opening.0)
140    }
141}
142
143define_sub_variants!(
144    LHS = PedersenOpening,
145    RHS = PedersenOpening,
146    Output = PedersenOpening
147);
148
149impl<'b> Mul<&'b Scalar> for &PedersenOpening {
150    type Output = PedersenOpening;
151
152    fn mul(self, scalar: &'b Scalar) -> PedersenOpening {
153        PedersenOpening(&self.0 * scalar)
154    }
155}
156
157define_mul_variants!(
158    LHS = PedersenOpening,
159    RHS = Scalar,
160    Output = PedersenOpening
161);
162
163impl<'b> Mul<&'b PedersenOpening> for &Scalar {
164    type Output = PedersenOpening;
165
166    fn mul(self, opening: &'b PedersenOpening) -> PedersenOpening {
167        PedersenOpening(self * &opening.0)
168    }
169}
170
171define_mul_variants!(
172    LHS = Scalar,
173    RHS = PedersenOpening,
174    Output = PedersenOpening
175);
176
177/// Pedersen commitment type.
178#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
179pub struct PedersenCommitment(RistrettoPoint);
180impl PedersenCommitment {
181    pub fn new(point: RistrettoPoint) -> Self {
182        Self(point)
183    }
184
185    pub fn get_point(&self) -> &RistrettoPoint {
186        &self.0
187    }
188
189    pub fn to_bytes(&self) -> [u8; PEDERSEN_COMMITMENT_LEN] {
190        self.0.compress().to_bytes()
191    }
192
193    pub fn from_bytes(bytes: &[u8]) -> Option<PedersenCommitment> {
194        if bytes.len() != PEDERSEN_COMMITMENT_LEN {
195            return None;
196        }
197        let Ok(compressed_ristretto) = CompressedRistretto::from_slice(bytes) else {
198            return None;
199        };
200
201        compressed_ristretto.decompress().map(PedersenCommitment)
202    }
203}
204
205impl<'b> Add<&'b PedersenCommitment> for &PedersenCommitment {
206    type Output = PedersenCommitment;
207
208    fn add(self, commitment: &'b PedersenCommitment) -> PedersenCommitment {
209        PedersenCommitment(&self.0 + &commitment.0)
210    }
211}
212
213define_add_variants!(
214    LHS = PedersenCommitment,
215    RHS = PedersenCommitment,
216    Output = PedersenCommitment
217);
218
219impl<'b> Sub<&'b PedersenCommitment> for &PedersenCommitment {
220    type Output = PedersenCommitment;
221
222    fn sub(self, commitment: &'b PedersenCommitment) -> PedersenCommitment {
223        PedersenCommitment(&self.0 - &commitment.0)
224    }
225}
226
227define_sub_variants!(
228    LHS = PedersenCommitment,
229    RHS = PedersenCommitment,
230    Output = PedersenCommitment
231);
232
233impl<'b> Mul<&'b Scalar> for &PedersenCommitment {
234    type Output = PedersenCommitment;
235
236    fn mul(self, scalar: &'b Scalar) -> PedersenCommitment {
237        PedersenCommitment(scalar * &self.0)
238    }
239}
240
241define_mul_variants!(
242    LHS = PedersenCommitment,
243    RHS = Scalar,
244    Output = PedersenCommitment
245);
246
247impl<'b> Mul<&'b PedersenCommitment> for &Scalar {
248    type Output = PedersenCommitment;
249
250    fn mul(self, commitment: &'b PedersenCommitment) -> PedersenCommitment {
251        PedersenCommitment(self * &commitment.0)
252    }
253}
254
255define_mul_variants!(
256    LHS = Scalar,
257    RHS = PedersenCommitment,
258    Output = PedersenCommitment
259);
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264
265    #[test]
266    fn test_pedersen_homomorphic_addition() {
267        let amount_0: u64 = 77;
268        let amount_1: u64 = 57;
269
270        let rng = &mut OsRng;
271        let opening_0 = PedersenOpening(Scalar::random(rng));
272        let opening_1 = PedersenOpening(Scalar::random(rng));
273
274        let commitment_0 = Pedersen::with(amount_0, &opening_0);
275        let commitment_1 = Pedersen::with(amount_1, &opening_1);
276        let commitment_addition = Pedersen::with(amount_0 + amount_1, &(opening_0 + opening_1));
277
278        assert_eq!(commitment_addition, commitment_0 + commitment_1);
279    }
280
281    #[test]
282    fn test_pedersen_homomorphic_subtraction() {
283        let amount_0: u64 = 77;
284        let amount_1: u64 = 57;
285
286        let rng = &mut OsRng;
287        let opening_0 = PedersenOpening(Scalar::random(rng));
288        let opening_1 = PedersenOpening(Scalar::random(rng));
289
290        let commitment_0 = Pedersen::with(amount_0, &opening_0);
291        let commitment_1 = Pedersen::with(amount_1, &opening_1);
292        let commitment_addition = Pedersen::with(amount_0 - amount_1, &(opening_0 - opening_1));
293
294        assert_eq!(commitment_addition, commitment_0 - commitment_1);
295    }
296
297    #[test]
298    fn test_pedersen_homomorphic_multiplication() {
299        let amount_0: u64 = 77;
300        let amount_1: u64 = 57;
301
302        let (commitment, opening) = Pedersen::new(amount_0);
303        let scalar = Scalar::from(amount_1);
304        let commitment_addition = Pedersen::with(amount_0 * amount_1, &(opening * scalar));
305
306        assert_eq!(commitment_addition, commitment * scalar);
307        assert_eq!(commitment_addition, scalar * commitment);
308    }
309
310    #[test]
311    fn test_pedersen_commitment_bytes() {
312        let amount: u64 = 77;
313        let (commitment, _) = Pedersen::new(amount);
314
315        let encoded = commitment.to_bytes();
316        let decoded = PedersenCommitment::from_bytes(&encoded).unwrap();
317
318        assert_eq!(commitment, decoded);
319
320        // incorrect length encoding
321        assert_eq!(PedersenCommitment::from_bytes(&[0; 33]), None);
322    }
323
324    #[test]
325    fn test_pedersen_opening_bytes() {
326        let opening = PedersenOpening(Scalar::random(&mut OsRng));
327
328        let encoded = opening.to_bytes();
329        let decoded = PedersenOpening::from_bytes(&encoded).unwrap();
330
331        assert_eq!(opening, decoded);
332
333        // incorrect length encoding
334        assert_eq!(PedersenOpening::from_bytes(&[0; 33]), None);
335    }
336
337    #[test]
338    fn test_serde_pedersen_commitment() {
339        let amount: u64 = 77;
340        let (commitment, _) = Pedersen::new(amount);
341
342        let encoded = bincode::serialize(&commitment).unwrap();
343        let decoded: PedersenCommitment = bincode::deserialize(&encoded).unwrap();
344
345        assert_eq!(commitment, decoded);
346    }
347
348    #[test]
349    fn test_serde_pedersen_opening() {
350        let opening = PedersenOpening(Scalar::random(&mut OsRng));
351
352        let encoded = bincode::serialize(&opening).unwrap();
353        let decoded: PedersenOpening = bincode::deserialize(&encoded).unwrap();
354
355        assert_eq!(opening, decoded);
356    }
357}