Skip to main content

commonware_cryptography/zk/
pedersen_to_plain.rs

1//! This module provides a proof that a plain commitment and a Pedersen
2//! commitment share the same committed value.
3//!
4//! # What This Lets You Do
5//!
6//! This proof lets a prover link a transparent commitment `X = x * G` with a
7//! hiding Pedersen commitment `C = x * G + x_blind * H`, showing that both use
8//! the same committed value `x`.
9//!
10//! This is useful when one part of a protocol wants to work with a plain
11//! commitment while another part wants the same value hidden behind a Pedersen
12//! commitment. The proof lets the prover bridge those two views without
13//! revealing either the committed value or the Pedersen blinding.
14//!
15//! Concretely, the prover shows knowledge of openings `(x, x_blind)` such that:
16//!
17//! - `X = x * G`, and
18//! - `C = x * G + x_blind * H`.
19//!
20//! # Usage
21//!
22//! Construct a [`Setup`] with a value generator and an independent blinding
23//! generator. Then create a [`Witness`] and derive its public [`Claim`] with
24//! [`Witness::claim`].
25//!
26//! Given a [`Setup`], [`Claim`], and [`Witness`], call [`prove`] to create a
27//! [`Proof`]. The proof is bound to the current [`Transcript`] state, so the
28//! verifier must replay the same transcript history before calling [`verify`].
29//!
30//! [`verify`] checks the proof against a [`Synthetic`] setup, allowing for easy
31//! batching with other proofs of this kind, or other proofs entirely. For example,
32//! you can batch this proof with the result of [`crate::zk::bulletproofs`].
33//!
34//! ## Example
35//!
36//! ```rust
37//! # use commonware_cryptography::{
38//! #     bls12381::primitives::group::{G1, Scalar},
39//! #     transcript::Transcript,
40//! #     zk::pedersen_to_plain::{prove, verify, Setup, Witness},
41//! # };
42//! # use commonware_math::{
43//! #     algebra::{Additive, CryptoGroup, HashToGroup},
44//! #     synthetic::Synthetic,
45//! # };
46//! # use commonware_parallel::Sequential;
47//! # use commonware_utils::test_rng;
48//! # type F = Scalar;
49//! # type G = G1;
50//! let setup = Setup {
51//!     value_generator: G::generator(),
52//!     blinding_generator: G::hash_to_group(
53//!         b"_COMMONWARE_CRYPTOGRAPHY_ZK_PEDERSEN_TO_PLAIN",
54//!         b"blinding",
55//!     ),
56//! };
57//!
58//! let witness = Witness {
59//!     value: F::from(3u64),
60//!     blinding: F::from(5u64),
61//! };
62//! let claim = witness.claim(&setup);
63//!
64//! let mut prover_rng = test_rng();
65//! let mut prover_transcript = Transcript::new(b"pedersen-to-plain-example");
66//! prover_transcript.commit(b"context".as_slice());
67//! let proof = prove(
68//!     &mut prover_rng,
69//!     &mut prover_transcript,
70//!     &setup,
71//!     &claim,
72//!     &witness,
73//! );
74//!
75//! let mut verifier_rng = test_rng();
76//! let mut verifier_transcript = Transcript::new(b"pedersen-to-plain-example");
77//! verifier_transcript.commit(b"context".as_slice());
78//! let [g, h] = Synthetic::<F, G>::generators_array();
79//! let synthetic_setup = Setup {
80//!     value_generator: g,
81//!     blinding_generator: h,
82//! };
83//! let valid = verify(
84//!     &mut verifier_rng,
85//!     &mut verifier_transcript,
86//!     &synthetic_setup,
87//!     &claim,
88//!     proof,
89//! )
90//! .eval(
91//!     &[setup.value_generator, setup.blinding_generator],
92//!     &Sequential,
93//! ) == G::zero();
94//! assert!(valid);
95//! ```
96
97use crate::transcript::Transcript;
98use bytes::{Buf, BufMut};
99use commonware_codec::{Encode, EncodeSize, Error, Read, Write};
100use commonware_math::{
101    algebra::{CryptoGroup, Field, Random, Space},
102    synthetic::Synthetic,
103};
104use rand_core::CryptoRngCore;
105
106/// Generators used by the proof system.
107///
108/// The blinding generator must not have a known discrete-log relationship
109/// relative to the value generator.
110#[derive(Clone, Debug, PartialEq)]
111pub struct Setup<G> {
112    /// The generator used in both the plain and Pedersen commitments.
113    pub value_generator: G,
114    /// The generator used only for the Pedersen blinding term.
115    pub blinding_generator: G,
116}
117
118impl<G: Write> Write for Setup<G> {
119    fn write(&self, buf: &mut impl BufMut) {
120        self.value_generator.write(buf);
121        self.blinding_generator.write(buf);
122    }
123}
124
125impl<G: EncodeSize> EncodeSize for Setup<G> {
126    fn encode_size(&self) -> usize {
127        self.value_generator.encode_size() + self.blinding_generator.encode_size()
128    }
129}
130
131impl<G: Read> Read for Setup<G>
132where
133    G::Cfg: Clone,
134{
135    type Cfg = G::Cfg;
136
137    fn read_cfg(buf: &mut impl Buf, cfg: &Self::Cfg) -> Result<Self, Error> {
138        Ok(Self {
139            value_generator: G::read_cfg(buf, cfg)?,
140            blinding_generator: G::read_cfg(buf, cfg)?,
141        })
142    }
143}
144
145#[cfg(any(test, feature = "arbitrary"))]
146impl<G> arbitrary::Arbitrary<'_> for Setup<G>
147where
148    G: for<'a> arbitrary::Arbitrary<'a>,
149{
150    fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
151        Ok(Self {
152            value_generator: u.arbitrary()?,
153            blinding_generator: u.arbitrary()?,
154        })
155    }
156}
157
158/// A prover-side witness for the relation.
159#[derive(Clone, Debug, PartialEq)]
160pub struct Witness<F> {
161    pub value: F,
162    pub blinding: F,
163}
164
165impl<F> Witness<F> {
166    /// Create the public [`Claim`] corresponding to this witness.
167    pub fn claim<G: Space<F>>(&self, setup: &Setup<G>) -> Claim<G> {
168        let plain = setup.value_generator.clone() * &self.value;
169        Claim {
170            pedersen: plain.clone() + &(setup.blinding_generator.clone() * &self.blinding),
171            plain,
172        }
173    }
174}
175
176/// The public statement for the protocol.
177#[derive(Clone, Debug, PartialEq)]
178pub struct Claim<G> {
179    pub plain: G,
180    pub pedersen: G,
181}
182
183impl<G: Write> Write for Claim<G> {
184    fn write(&self, buf: &mut impl BufMut) {
185        self.plain.write(buf);
186        self.pedersen.write(buf);
187    }
188}
189
190impl<G: EncodeSize> EncodeSize for Claim<G> {
191    fn encode_size(&self) -> usize {
192        self.plain.encode_size() + self.pedersen.encode_size()
193    }
194}
195
196impl<G: Read> Read for Claim<G>
197where
198    G::Cfg: Clone,
199{
200    type Cfg = G::Cfg;
201
202    fn read_cfg(buf: &mut impl Buf, cfg: &Self::Cfg) -> Result<Self, Error> {
203        Ok(Self {
204            plain: G::read_cfg(buf, cfg)?,
205            pedersen: G::read_cfg(buf, cfg)?,
206        })
207    }
208}
209
210#[cfg(any(test, feature = "arbitrary"))]
211impl<G> arbitrary::Arbitrary<'_> for Claim<G>
212where
213    G: for<'a> arbitrary::Arbitrary<'a>,
214{
215    fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
216        Ok(Self {
217            plain: u.arbitrary()?,
218            pedersen: u.arbitrary()?,
219        })
220    }
221}
222
223/// A proof that the plain and Pedersen commitments share the same committed value.
224#[derive(Clone, Debug, PartialEq)]
225pub struct Proof<F, G> {
226    plain_mask: G,
227    pedersen_mask: G,
228    value_response: F,
229    blinding_response: F,
230}
231
232impl<F: Write, G: Write> Write for Proof<F, G> {
233    fn write(&self, buf: &mut impl BufMut) {
234        self.plain_mask.write(buf);
235        self.pedersen_mask.write(buf);
236        self.value_response.write(buf);
237        self.blinding_response.write(buf);
238    }
239}
240
241impl<F: EncodeSize, G: EncodeSize> EncodeSize for Proof<F, G> {
242    fn encode_size(&self) -> usize {
243        self.plain_mask.encode_size()
244            + self.pedersen_mask.encode_size()
245            + self.value_response.encode_size()
246            + self.blinding_response.encode_size()
247    }
248}
249
250impl<F: Read, G: Read> Read for Proof<F, G>
251where
252    G::Cfg: Clone,
253    F::Cfg: Clone,
254{
255    type Cfg = (G::Cfg, F::Cfg);
256
257    fn read_cfg(buf: &mut impl Buf, (g_cfg, f_cfg): &Self::Cfg) -> Result<Self, Error> {
258        Ok(Self {
259            plain_mask: G::read_cfg(buf, g_cfg)?,
260            pedersen_mask: G::read_cfg(buf, g_cfg)?,
261            value_response: F::read_cfg(buf, f_cfg)?,
262            blinding_response: F::read_cfg(buf, f_cfg)?,
263        })
264    }
265}
266
267#[cfg(any(test, feature = "arbitrary"))]
268impl<F, G> arbitrary::Arbitrary<'_> for Proof<F, G>
269where
270    F: for<'a> arbitrary::Arbitrary<'a>,
271    G: for<'a> arbitrary::Arbitrary<'a>,
272{
273    fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
274        Ok(Self {
275            plain_mask: u.arbitrary()?,
276            pedersen_mask: u.arbitrary()?,
277            value_response: u.arbitrary()?,
278            blinding_response: u.arbitrary()?,
279        })
280    }
281}
282
283/// Create a proof for a claimed witness.
284///
285/// This proves that the plain and Pedersen commitments in [`Claim`] open to
286/// the same committed value, using the openings in [`Witness`].
287///
288/// This is a low-level constructor and assumes that `claim` and `witness`
289/// correspond. It does not check that relationship for you.
290pub fn prove<F: Field + Random, G: CryptoGroup<Scalar = F> + Encode>(
291    rng: &mut impl CryptoRngCore,
292    transcript: &mut Transcript,
293    setup: &Setup<G>,
294    claim: &Claim<G>,
295    witness: &Witness<F>,
296) -> Proof<F, G>
297where
298    Claim<G>: Encode,
299{
300    // We prove that the published commitments:
301    //
302    //   X = x G
303    //   C = x G + x_blind H
304    //
305    // share the same committed value x. We do this with a single Schnorr-style
306    // protocol over both equations. The prover samples masks k and k_blind,
307    // sends:
308    //
309    //   K_plain = k G
310    //   K_pedersen = k G + k_blind H
311    //
312    // derives a challenge e from the transcript, and responds with:
313    //
314    //   s = k + e x
315    //   s_blind = k_blind + e x_blind
316    //
317    // The verifier can then check:
318    //
319    //   s G = K_plain + e X
320    //   s G + s_blind H = K_pedersen + e C
321    //
322    // which is exactly what verify checks directly.
323    transcript.commit(claim.encode());
324
325    let value_mask = F::random(&mut *rng);
326    let blinding_mask = F::random(&mut *rng);
327    let plain_mask = setup.value_generator.clone() * &value_mask;
328    let pedersen_mask = plain_mask.clone() + &(setup.blinding_generator.clone() * &blinding_mask);
329
330    transcript.commit(plain_mask.encode());
331    transcript.commit(pedersen_mask.encode());
332    let challenge = F::random(transcript.noise(b"challenge"));
333
334    Proof {
335        plain_mask,
336        pedersen_mask,
337        value_response: value_mask + &(challenge.clone() * &witness.value),
338        blinding_response: blinding_mask + &(challenge * &witness.blinding),
339    }
340}
341
342/// Verify a [`Proof`] against a [`Claim`].
343///
344/// Returns `true` if the proof is valid for the current transcript state.
345pub fn verify<F: Field + Random, G: CryptoGroup<Scalar = F> + Encode + PartialEq>(
346    rng: &mut impl CryptoRngCore,
347    transcript: &mut Transcript,
348    setup: &Setup<Synthetic<F, G>>,
349    claim: &Claim<G>,
350    proof: Proof<F, G>,
351) -> Synthetic<F, G>
352where
353    Claim<G>: Encode,
354{
355    let Proof {
356        plain_mask,
357        pedersen_mask,
358        value_response,
359        blinding_response,
360    } = proof;
361
362    transcript.commit(claim.encode());
363    transcript.commit(plain_mask.encode());
364    transcript.commit(pedersen_mask.encode());
365    let challenge = F::random(transcript.noise(b"challenge"));
366
367    let plain_valid = Synthetic::concrete([
368        (F::one(), plain_mask),
369        (challenge.clone(), claim.plain.clone()),
370    ]) - &(setup.value_generator.clone() * &value_response);
371    let pedersen_valid = Synthetic::concrete([
372        (F::one(), pedersen_mask),
373        (challenge, claim.pedersen.clone()),
374    ]) - &(setup.value_generator.clone() * &value_response)
375        - &(setup.blinding_generator.clone() * &blinding_response);
376    pedersen_valid + &(plain_valid * &F::random(&mut *rng))
377}
378
379#[cfg(all(test, feature = "arbitrary"))]
380mod conformance {
381    use super::{Claim, Proof, Setup};
382    use commonware_codec::conformance::CodecConformance;
383    use commonware_math::test::{F as TestF, G as TestG};
384
385    commonware_conformance::conformance_tests! {
386        CodecConformance<Setup<TestG>>,
387        CodecConformance<Claim<TestG>>,
388        CodecConformance<Proof<TestF, TestG>>,
389    }
390}
391
392#[commonware_macros::stability(ALPHA)]
393#[cfg(any(test, feature = "fuzz"))]
394pub mod fuzz {
395    use super::*;
396    use crate::bls12381::primitives::group::{Scalar as F, G1 as G};
397    use arbitrary::{Arbitrary, Unstructured};
398    use commonware_math::algebra::{Additive, CryptoGroup, HashToGroup};
399    use commonware_parallel::Sequential;
400    use commonware_utils::test_rng;
401    use std::sync::OnceLock;
402
403    const NAMESPACE: &[u8] = b"_COMMONWARE_CRYPTOGRAPHY_ZK_PEDERSEN_TO_PLAIN";
404    const BAD_NAMESPACE: &[u8] = b"_COMMONWARE_CRYPTOGRAPHY_ZK_PEDERSEN_TO_PLAIN_BUT_DIFFERENT";
405
406    pub(super) fn test_setup() -> &'static Setup<G> {
407        static TEST_SETUP: OnceLock<Setup<G>> = OnceLock::new();
408        TEST_SETUP.get_or_init(|| Setup {
409            value_generator: G::generator(),
410            blinding_generator: G::hash_to_group(NAMESPACE, b"blinding generator"),
411        })
412    }
413
414    struct Prover<'a> {
415        setup: &'a Setup<G>,
416        claim: Claim<G>,
417        proof: Proof<F, G>,
418        bad_namespace: bool,
419        honest: bool,
420    }
421
422    impl<'a> Prover<'a> {
423        fn new(setup: &'a Setup<G>, value: F, blinding: F) -> Self {
424            let witness = Witness { value, blinding };
425            let claim = witness.claim(setup);
426            let proof = prove(
427                &mut test_rng(),
428                &mut Transcript::new(NAMESPACE),
429                setup,
430                &claim,
431                &witness,
432            );
433            Self {
434                setup,
435                claim,
436                proof,
437                bad_namespace: false,
438                honest: true,
439            }
440        }
441
442        #[allow(clippy::missing_const_for_fn)]
443        fn bad_namespace(&mut self) {
444            self.honest = false;
445            self.bad_namespace = true;
446        }
447
448        fn tweak_plain_claim(&mut self, delta: F) {
449            if delta == F::zero() {
450                return;
451            }
452            self.honest = false;
453            self.claim.plain += &(self.setup.value_generator * &delta);
454        }
455
456        fn tweak_pedersen_claim(&mut self, value_delta: F, blinding_delta: F) {
457            if value_delta == F::zero() && blinding_delta == F::zero() {
458                return;
459            }
460            self.honest = false;
461            self.claim.pedersen += &((self.setup.value_generator * &value_delta)
462                + &(self.setup.blinding_generator * &blinding_delta));
463        }
464
465        fn tweak_mask(&mut self, tweak_plain: bool, delta: G) {
466            if delta == G::zero() {
467                return;
468            }
469            self.honest = false;
470            if tweak_plain {
471                self.proof.plain_mask += &delta;
472            } else {
473                self.proof.pedersen_mask += &delta;
474            }
475        }
476
477        fn tweak_response(&mut self, tweak_value: bool, delta: F) {
478            if delta == F::zero() {
479                return;
480            }
481            self.honest = false;
482            if tweak_value {
483                self.proof.value_response += &delta;
484            } else {
485                self.proof.blinding_response += &delta;
486            }
487        }
488
489        #[allow(clippy::missing_const_for_fn)]
490        fn honest(&self) -> bool {
491            self.honest
492        }
493
494        fn verify(self, rng: &mut impl CryptoRngCore) -> bool {
495            let ns = if self.bad_namespace {
496                BAD_NAMESPACE
497            } else {
498                NAMESPACE
499            };
500            let [g, h] = Synthetic::generators_array();
501            verify(
502                rng,
503                &mut Transcript::new(ns),
504                &Setup {
505                    value_generator: g,
506                    blinding_generator: h,
507                },
508                &self.claim,
509                self.proof,
510            )
511            .eval(
512                &[self.setup.value_generator, self.setup.blinding_generator],
513                &Sequential,
514            ) == G::zero()
515        }
516    }
517
518    #[derive(Debug)]
519    pub struct Plan {
520        value: F,
521        blinding: F,
522    }
523
524    impl<'a> Arbitrary<'a> for Plan {
525        fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
526            Ok(Self {
527                value: u.arbitrary()?,
528                blinding: u.arbitrary()?,
529            })
530        }
531    }
532
533    impl Plan {
534        pub fn run(self, u: &mut Unstructured<'_>) -> arbitrary::Result<()> {
535            let setup = test_setup();
536            let mut prover = Prover::new(setup, self.value, self.blinding);
537            if u.arbitrary::<bool>()? {
538                match u.arbitrary::<u8>()? {
539                    x if x < 51 => prover.tweak_plain_claim(u.arbitrary()?),
540                    x if x < 102 => prover.tweak_pedersen_claim(u.arbitrary()?, u.arbitrary()?),
541                    x if x < 153 => prover.tweak_mask(u.arbitrary()?, u.arbitrary()?),
542                    x if x < 204 => prover.tweak_response(u.arbitrary()?, u.arbitrary()?),
543                    _ => prover.bad_namespace(),
544                }
545            }
546            match (prover.honest(), prover.verify(&mut test_rng())) {
547                (true, true) | (false, false) => {}
548                (true, false) => panic!("prover honest, but proof didn't verify"),
549                (false, true) => panic!("prover malicious, but proof verifies"),
550            }
551            Ok(())
552        }
553    }
554
555    #[test]
556    fn prover_tweaks_cover_noops_and_failures() {
557        let setup = test_setup();
558
559        let mut honest = Prover::new(setup, F::from(3u64), F::from(5u64));
560        honest.tweak_plain_claim(F::zero());
561        honest.tweak_pedersen_claim(F::zero(), F::zero());
562        honest.tweak_mask(true, G::zero());
563        honest.tweak_response(false, F::zero());
564        assert!(honest.honest());
565        assert!(honest.verify(&mut test_rng()));
566
567        type Tweak = Box<dyn FnOnce(&mut Prover<'static>)>;
568        let failures: [Tweak; 5] = [
569            Box::new(|p| p.tweak_plain_claim(F::from(1u64))),
570            Box::new(|p| p.tweak_pedersen_claim(F::from(1u64), F::from(1u64))),
571            Box::new(|p| p.tweak_mask(false, G::generator())),
572            Box::new(|p| p.tweak_response(true, F::from(1u64))),
573            Box::new(|p| p.bad_namespace()),
574        ];
575        for tweak in failures {
576            let mut prover = Prover::new(setup, F::from(3u64), F::from(5u64));
577            tweak(&mut prover);
578            assert!(!prover.honest());
579            assert!(!prover.verify(&mut test_rng()));
580        }
581    }
582}
583
584#[cfg(test)]
585mod test {
586    use super::{fuzz, Claim, Proof, Setup};
587    use commonware_codec::{Decode, Encode};
588    use commonware_invariants::minifuzz;
589    use commonware_math::test::{F, G};
590
591    fn assert_setup_roundtrip(setup: &Setup<G>) {
592        let encoded = setup.encode();
593        let decoded: Setup<G> =
594            Setup::decode_cfg(encoded.clone(), &()).expect("setup should decode with unit cfg");
595        assert_eq!(setup, &decoded);
596        assert_eq!(decoded.encode(), encoded);
597    }
598
599    fn assert_claim_roundtrip(claim: &Claim<G>) {
600        let encoded = claim.encode();
601        let decoded: Claim<G> =
602            Claim::decode_cfg(encoded.clone(), &()).expect("claim should decode with unit cfg");
603        assert_eq!(claim, &decoded);
604        assert_eq!(decoded.encode(), encoded);
605    }
606
607    fn assert_proof_roundtrip(proof: &Proof<F, G>) {
608        let encoded = proof.encode();
609        let decoded: Proof<F, G> = Proof::decode_cfg(encoded.clone(), &((), ()))
610            .expect("proof should decode with unit cfg");
611        assert_eq!(proof, &decoded);
612        assert_eq!(decoded.encode(), encoded);
613    }
614
615    #[test]
616    fn test_codec_roundtrip() {
617        minifuzz::test(|u| {
618            assert_setup_roundtrip(&u.arbitrary::<Setup<G>>()?);
619            assert_claim_roundtrip(&u.arbitrary::<Claim<G>>()?);
620            assert_proof_roundtrip(&u.arbitrary::<Proof<F, G>>()?);
621            Ok(())
622        });
623    }
624
625    #[test]
626    fn test_fuzz() {
627        minifuzz::test(|u| {
628            u.arbitrary::<fuzz::Plan>()?.run(u)?;
629            Ok(())
630        });
631    }
632}