1use elliptic_curve::rand_core::{CryptoRng, RngCore};
4use merlin::Transcript;
5#[cfg(feature = "serde")]
6use serde::{Deserialize, Serialize};
7
8#[cfg(feature = "serde")]
9use crate::serde::ScalarHelper;
10use crate::{
11 Ciphertext, CiphertextWithValue, PublicKey, SecretKey,
12 group::Group,
13 proofs::{TranscriptForGroup, VerificationError},
14};
15
16#[derive(Debug, Clone)]
113#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
114#[cfg_attr(feature = "serde", serde(bound = ""))]
115pub struct CommitmentEquivalenceProof<G: Group> {
116 #[cfg_attr(feature = "serde", serde(with = "ScalarHelper::<G>"))]
117 challenge: G::Scalar,
118 #[cfg_attr(feature = "serde", serde(with = "ScalarHelper::<G>"))]
119 randomness_response: G::Scalar,
120 #[cfg_attr(feature = "serde", serde(with = "ScalarHelper::<G>"))]
121 value_response: G::Scalar,
122 #[cfg_attr(feature = "serde", serde(with = "ScalarHelper::<G>"))]
123 commitment_response: G::Scalar,
124}
125
126impl<G: Group> CommitmentEquivalenceProof<G> {
127 pub fn new<R: RngCore + CryptoRng>(
135 ciphertext: &CiphertextWithValue<G>,
136 receiver: &PublicKey<G>,
137 commitment_blinding: &SecretKey<G>,
138 commitment_blinding_base: G::Element,
139 transcript: &mut Transcript,
140 rng: &mut R,
141 ) -> (Self, G::Element) {
142 let commitment = G::multi_mul(
143 [ciphertext.value(), commitment_blinding.expose_scalar()],
144 [G::generator(), commitment_blinding_base],
145 );
146
147 transcript.start_proof(b"commitment_equivalence");
148 transcript.append_element_bytes(b"K", receiver.as_bytes());
149 transcript.append_element::<G>(b"R", &ciphertext.inner().random_element);
150 transcript.append_element::<G>(b"B", &ciphertext.inner().blinded_element);
151 transcript.append_element::<G>(b"C", &commitment);
152
153 let random_scalar = SecretKey::<G>::generate(rng);
154 let value_scalar = SecretKey::<G>::generate(rng);
155 let commitment_scalar = SecretKey::<G>::generate(rng);
156 let random_commitment = G::mul_generator(random_scalar.expose_scalar());
157 transcript.append_element::<G>(b"[e_r]G", &random_commitment);
158
159 let value_element = G::mul_generator(value_scalar.expose_scalar());
160 let enc_blinding_commitment =
161 value_element + receiver.as_element() * random_scalar.expose_scalar();
162 transcript.append_element::<G>(b"[e_v]G + [e_r]K", &enc_blinding_commitment);
163 let commitment_commitment =
164 value_element + commitment_blinding_base * commitment_scalar.expose_scalar();
165 transcript.append_element::<G>(b"[e_v]G + [e_c]H", &commitment_commitment);
166
167 let challenge = transcript.challenge_scalar::<G>(b"c");
168 let randomness_response =
169 challenge * ciphertext.randomness().expose_scalar() + random_scalar.expose_scalar();
170 let value_response = challenge * ciphertext.value() + value_scalar.expose_scalar();
171 let commitment_response =
172 challenge * commitment_blinding.expose_scalar() + commitment_scalar.expose_scalar();
173
174 let proof = Self {
175 challenge,
176 randomness_response,
177 value_response,
178 commitment_response,
179 };
180 (proof, commitment)
181 }
182
183 pub fn verify(
187 &self,
188 ciphertext: &Ciphertext<G>,
189 receiver: &PublicKey<G>,
190 commitment: G::Element,
191 commitment_blinding_base: G::Element,
192 transcript: &mut Transcript,
193 ) -> Result<(), VerificationError> {
194 transcript.start_proof(b"commitment_equivalence");
195 transcript.append_element_bytes(b"K", receiver.as_bytes());
196 transcript.append_element::<G>(b"R", &ciphertext.random_element);
197 transcript.append_element::<G>(b"B", &ciphertext.blinded_element);
198 transcript.append_element::<G>(b"C", &commitment);
199
200 let neg_challenge = -self.challenge;
201 let random_commitment = G::vartime_double_mul_generator(
202 &neg_challenge,
203 ciphertext.random_element,
204 &self.randomness_response,
205 );
206 transcript.append_element::<G>(b"[e_r]G", &random_commitment);
207
208 let enc_blinding_commitment = G::vartime_multi_mul(
209 [
210 &self.value_response,
211 &self.randomness_response,
212 &neg_challenge,
213 ],
214 [
215 G::generator(),
216 receiver.as_element(),
217 ciphertext.blinded_element,
218 ],
219 );
220 transcript.append_element::<G>(b"[e_v]G + [e_r]K", &enc_blinding_commitment);
221
222 let commitment_commitment = G::vartime_multi_mul(
223 [
224 &self.value_response,
225 &self.commitment_response,
226 &neg_challenge,
227 ],
228 [G::generator(), commitment_blinding_base, commitment],
229 );
230 transcript.append_element::<G>(b"[e_v]G + [e_c]H", &commitment_commitment);
231
232 let expected_challenge = transcript.challenge_scalar::<G>(b"c");
233 if expected_challenge == self.challenge {
234 Ok(())
235 } else {
236 Err(VerificationError::ChallengeMismatch)
237 }
238 }
239}
240
241#[cfg(all(test, feature = "curve25519-dalek"))]
242mod tests {
243 use bulletproofs::PedersenGens;
244
245 use super::*;
246 use crate::{
247 Keypair,
248 group::{ElementOps, Ristretto},
249 };
250
251 fn downgrade_scalar(x: curve25519_dalek::Scalar) -> bulletproofs_curve::Scalar {
252 bulletproofs_curve::Scalar::from_bytes_mod_order(x.to_bytes())
253 }
254
255 fn upgrade_point(x: bulletproofs_curve::RistrettoPoint) -> curve25519_dalek::RistrettoPoint {
256 let compressed = curve25519_dalek::ristretto::CompressedRistretto(x.compress().0);
257 compressed.decompress().unwrap()
258 }
259
260 #[test]
261 fn equivalence_proof_basics() {
262 let mut rng = rand::rng();
263 let (receiver, _) = Keypair::<Ristretto>::generate(&mut rng).into_tuple();
264 let value = 1234_u64;
265 let ciphertext = CiphertextWithValue::new(value, &receiver, &mut rng).generalize();
266
267 let commitment_gens = PedersenGens::default();
268 assert_eq!(upgrade_point(commitment_gens.B), Ristretto::generator());
269 let blinding = SecretKey::generate(&mut rng);
270
271 let (proof, commitment) = CommitmentEquivalenceProof::new(
272 &ciphertext,
273 &receiver,
274 &blinding,
275 upgrade_point(commitment_gens.B_blinding),
276 &mut Transcript::new(b"test"),
277 &mut rng,
278 );
279 assert_eq!(
280 commitment,
281 upgrade_point(commitment_gens.commit(
282 downgrade_scalar(*ciphertext.value()),
283 downgrade_scalar(*blinding.expose_scalar()),
284 ))
285 );
286
287 let ciphertext = ciphertext.into();
288 proof
289 .verify(
290 &ciphertext,
291 &receiver,
292 commitment,
293 upgrade_point(commitment_gens.B_blinding),
294 &mut Transcript::new(b"test"),
295 )
296 .unwrap();
297
298 let other_ciphertext = receiver.encrypt(8_u64, &mut rng);
299 let err = proof
300 .verify(
301 &other_ciphertext,
302 &receiver,
303 commitment,
304 upgrade_point(commitment_gens.B_blinding),
305 &mut Transcript::new(b"test"),
306 )
307 .unwrap_err();
308 assert!(matches!(err, VerificationError::ChallengeMismatch));
309
310 let err = proof
311 .verify(
312 &ciphertext,
313 &receiver,
314 commitment + Ristretto::generator(),
315 upgrade_point(commitment_gens.B_blinding),
316 &mut Transcript::new(b"test"),
317 )
318 .unwrap_err();
319 assert!(matches!(err, VerificationError::ChallengeMismatch));
320
321 let err = proof
322 .verify(
323 &ciphertext,
324 &receiver,
325 commitment,
326 upgrade_point(commitment_gens.B_blinding),
327 &mut Transcript::new(b"other_test"),
328 )
329 .unwrap_err();
330 assert!(matches!(err, VerificationError::ChallengeMismatch));
331 }
332}