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