penumbra_sdk_shielded_pool/output/
proof.rs

1use base64::prelude::*;
2use std::str::FromStr;
3
4use anyhow::Result;
5use ark_groth16::r1cs_to_qap::LibsnarkReduction;
6use ark_r1cs_std::uint8::UInt8;
7use ark_serialize::{CanonicalDeserialize, CanonicalSerialize};
8use decaf377::{Bls12_377, Fq, Fr};
9use decaf377_fmd as fmd;
10use decaf377_ka as ka;
11
12use ark_ff::ToConstraintField;
13use ark_groth16::{Groth16, PreparedVerifyingKey, Proof, ProvingKey};
14use ark_r1cs_std::prelude::*;
15use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystemRef};
16use ark_snark::SNARK;
17use penumbra_sdk_keys::{keys::Diversifier, Address};
18use penumbra_sdk_proto::{penumbra::core::component::shielded_pool::v1 as pb, DomainType};
19use penumbra_sdk_tct::r1cs::StateCommitmentVar;
20
21use crate::{note, Note, Rseed};
22use penumbra_sdk_asset::{
23    balance,
24    balance::{commitment::BalanceCommitmentVar, BalanceVar},
25    Value,
26};
27use penumbra_sdk_proof_params::{DummyWitness, VerifyingKeyExt, GROTH16_PROOF_LENGTH_BYTES};
28
29/// The public input for an [`OutputProof`].
30#[derive(Clone, Debug)]
31pub struct OutputProofPublic {
32    /// A hiding commitment to the balance.
33    pub balance_commitment: balance::Commitment,
34    /// A hiding commitment to the note.
35    pub note_commitment: note::StateCommitment,
36}
37
38/// The private input for an [`OutputProof`].
39#[derive(Clone, Debug)]
40pub struct OutputProofPrivate {
41    /// The note being created.
42    pub note: Note,
43    /// A blinding factor to hide the balance of the transaction.
44    pub balance_blinding: Fr,
45}
46
47#[cfg(test)]
48fn check_satisfaction(public: &OutputProofPublic, private: &OutputProofPrivate) -> Result<()> {
49    use penumbra_sdk_asset::Balance;
50
51    if private.note.diversified_generator() == decaf377::Element::default() {
52        anyhow::bail!("diversified generator is identity");
53    }
54
55    let balance_commitment =
56        (-Balance::from(private.note.value())).commit(private.balance_blinding);
57    if balance_commitment != public.balance_commitment {
58        anyhow::bail!("balance commitment did not match public input");
59    }
60
61    let note_commitment = private.note.commit();
62    if note_commitment != public.note_commitment {
63        anyhow::bail!("note commitment did not match public input");
64    }
65
66    Ok(())
67}
68
69#[cfg(test)]
70fn check_circuit_satisfaction(
71    public: OutputProofPublic,
72    private: OutputProofPrivate,
73) -> Result<()> {
74    use ark_relations::r1cs::{self, ConstraintSystem};
75
76    let cs = ConstraintSystem::new_ref();
77    let circuit = OutputCircuit { public, private };
78    cs.set_optimization_goal(r1cs::OptimizationGoal::Constraints);
79    circuit
80        .generate_constraints(cs.clone())
81        .expect("can generate constraints from circuit");
82    cs.finalize();
83    if !cs.is_satisfied()? {
84        anyhow::bail!("constraints are not satisfied");
85    }
86    Ok(())
87}
88
89/// Public:
90/// * vcm (balance commitment)
91/// * ncm (note commitment)
92///
93/// Witnesses:
94/// * g_d (point)
95/// * pk_d (point)
96/// * v (u128 value plus asset ID (scalar))
97/// * vblind (Fr)
98/// * nblind (Fq)
99#[derive(Clone, Debug)]
100pub struct OutputCircuit {
101    public: OutputProofPublic,
102    private: OutputProofPrivate,
103}
104
105impl OutputCircuit {
106    fn new(public: OutputProofPublic, private: OutputProofPrivate) -> Self {
107        Self { public, private }
108    }
109}
110
111impl ConstraintSynthesizer<Fq> for OutputCircuit {
112    fn generate_constraints(self, cs: ConstraintSystemRef<Fq>) -> ark_relations::r1cs::Result<()> {
113        // Witnesses
114        // Note: In the allocation of the address on `NoteVar`, we check the diversified base is not identity.
115        let note_var = note::NoteVar::new_witness(cs.clone(), || Ok(self.private.note.clone()))?;
116        let balance_blinding_arr: [u8; 32] = self.private.balance_blinding.to_bytes();
117        let balance_blinding_vars = UInt8::new_witness_vec(cs.clone(), &balance_blinding_arr)?;
118
119        // Public inputs
120        let claimed_note_commitment =
121            StateCommitmentVar::new_input(cs.clone(), || Ok(self.public.note_commitment))?;
122        let claimed_balance_commitment =
123            BalanceCommitmentVar::new_input(cs.clone(), || Ok(self.public.balance_commitment))?;
124
125        // Check integrity of balance commitment.
126        let balance_commitment =
127            BalanceVar::from_negative_value_var(note_var.value()).commit(balance_blinding_vars)?;
128        balance_commitment.enforce_equal(&claimed_balance_commitment)?;
129
130        // Note commitment integrity
131        let note_commitment = note_var.commit()?;
132        note_commitment.enforce_equal(&claimed_note_commitment)?;
133
134        Ok(())
135    }
136}
137
138impl DummyWitness for OutputCircuit {
139    fn with_dummy_witness() -> Self {
140        let diversifier_bytes = [1u8; 16];
141        let pk_d_bytes = decaf377::Element::GENERATOR.vartime_compress().0;
142        let clue_key_bytes = [1; 32];
143        let diversifier = Diversifier(diversifier_bytes);
144        let address = Address::from_components(
145            diversifier,
146            ka::Public(pk_d_bytes),
147            fmd::ClueKey(clue_key_bytes),
148        )
149        .expect("generated 1 address");
150        let note = Note::from_parts(
151            address,
152            Value::from_str("1upenumbra").expect("valid value"),
153            Rseed([1u8; 32]),
154        )
155        .expect("can make a note");
156        let balance_blinding = Fr::from(1u64);
157
158        let public = OutputProofPublic {
159            note_commitment: note.commit(),
160            balance_commitment: balance::Commitment(decaf377::Element::GENERATOR),
161        };
162        let private = OutputProofPrivate {
163            note,
164            balance_blinding,
165        };
166        OutputCircuit { public, private }
167    }
168}
169
170#[derive(Clone, Debug)]
171pub struct OutputProof([u8; GROTH16_PROOF_LENGTH_BYTES]);
172
173impl OutputProof {
174    #![allow(clippy::too_many_arguments)]
175    /// Generate an [`OutputProof`] given the proving key, public inputs,
176    /// witness data, and two random elements `blinding_r` and `blinding_s`.
177    pub fn prove(
178        blinding_r: Fq,
179        blinding_s: Fq,
180        pk: &ProvingKey<Bls12_377>,
181        public: OutputProofPublic,
182        private: OutputProofPrivate,
183    ) -> anyhow::Result<Self> {
184        let circuit = OutputCircuit::new(public, private);
185        let proof = Groth16::<Bls12_377, LibsnarkReduction>::create_proof_with_reduction(
186            circuit, pk, blinding_r, blinding_s,
187        )
188        .map_err(|err| anyhow::anyhow!(err))?;
189        let mut proof_bytes = [0u8; GROTH16_PROOF_LENGTH_BYTES];
190        Proof::serialize_compressed(&proof, &mut proof_bytes[..]).expect("can serialize Proof");
191        Ok(Self(proof_bytes))
192    }
193
194    /// Called to verify the proof using the provided public inputs.
195    ///
196    /// The public inputs are:
197    /// * balance commitment of the new note,
198    /// * note commitment of the new note,
199    // For debugging proof verification failures:
200    // to check that the proof data and verification keys are consistent.
201    #[tracing::instrument(level="debug", skip(self, vk), fields(self = ?BASE64_STANDARD.encode(self.clone().encode_to_vec()), vk = ?vk.debug_id()))]
202    pub fn verify(
203        &self,
204        vk: &PreparedVerifyingKey<Bls12_377>,
205        public: OutputProofPublic,
206    ) -> anyhow::Result<()> {
207        let proof =
208            Proof::deserialize_compressed_unchecked(&self.0[..]).map_err(|e| anyhow::anyhow!(e))?;
209
210        let mut public_inputs = Vec::new();
211        public_inputs.extend(
212            public
213                .note_commitment
214                .0
215                .to_field_elements()
216                .ok_or_else(|| anyhow::anyhow!("note commitment is not a valid field element"))?,
217        );
218        public_inputs.extend(
219            public
220                .balance_commitment
221                .0
222                .to_field_elements()
223                .ok_or_else(|| {
224                    anyhow::anyhow!("balance commitment is not a valid field element")
225                })?,
226        );
227
228        tracing::trace!(?public_inputs);
229        let start = std::time::Instant::now();
230        let proof_result = Groth16::<Bls12_377, LibsnarkReduction>::verify_with_processed_vk(
231            vk,
232            public_inputs.as_slice(),
233            &proof,
234        )
235        .map_err(|err| anyhow::anyhow!(err))?;
236        tracing::debug!(?proof_result, elapsed = ?start.elapsed());
237        proof_result
238            .then_some(())
239            .ok_or_else(|| anyhow::anyhow!("output proof did not verify"))
240    }
241}
242
243impl DomainType for OutputProof {
244    type Proto = pb::ZkOutputProof;
245}
246
247impl From<OutputProof> for pb::ZkOutputProof {
248    fn from(proof: OutputProof) -> Self {
249        pb::ZkOutputProof {
250            inner: proof.0.to_vec(),
251        }
252    }
253}
254
255impl TryFrom<pb::ZkOutputProof> for OutputProof {
256    type Error = anyhow::Error;
257
258    fn try_from(proto: pb::ZkOutputProof) -> Result<Self, Self::Error> {
259        Ok(OutputProof(proto.inner[..].try_into()?))
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266    use decaf377::{Fq, Fr};
267    use penumbra_sdk_asset::{asset, Balance, Value};
268    use penumbra_sdk_keys::keys::{Bip44Path, SeedPhrase, SpendKey};
269    use penumbra_sdk_num::Amount;
270    use proptest::prelude::*;
271
272    use crate::{note, Note};
273
274    fn fq_strategy() -> BoxedStrategy<Fq> {
275        any::<[u8; 32]>()
276            .prop_map(|bytes| Fq::from_le_bytes_mod_order(&bytes[..]))
277            .boxed()
278    }
279
280    fn fr_strategy() -> BoxedStrategy<Fr> {
281        any::<[u8; 32]>()
282            .prop_map(|bytes| Fr::from_le_bytes_mod_order(&bytes[..]))
283            .boxed()
284    }
285
286    prop_compose! {
287        fn arb_valid_output_statement()(seed_phrase_randomness in any::<[u8; 32]>(), rseed_randomness in any::<[u8; 32]>(), amount in any::<u64>(),  balance_blinding in fr_strategy(), asset_id64 in any::<u64>(), address_index in any::<u32>()) -> (OutputProofPublic, OutputProofPrivate) {
288            let seed_phrase = SeedPhrase::from_randomness(&seed_phrase_randomness);
289            let sk_recipient = SpendKey::from_seed_phrase_bip44(seed_phrase, &Bip44Path::new(0));
290            let fvk_recipient = sk_recipient.full_viewing_key();
291            let ivk_recipient = fvk_recipient.incoming();
292            let (dest, _dtk_d) = ivk_recipient.payment_address(address_index.into());
293
294            let value_to_send = Value {
295                amount: Amount::from(amount),
296                asset_id: asset::Id(Fq::from(asset_id64)),
297            };
298            let note = Note::from_parts(
299                dest,
300                value_to_send,
301                Rseed(rseed_randomness),
302            ).expect("should be able to create note");
303            let note_commitment = note.commit();
304            let balance_commitment = (-Balance::from(value_to_send)).commit(balance_blinding);
305
306            let public = OutputProofPublic { balance_commitment, note_commitment };
307            let private = OutputProofPrivate { note, balance_blinding};
308
309            (public, private)
310        }
311    }
312
313    proptest! {
314        #[test]
315        fn output_proof_happy_path((public, private) in arb_valid_output_statement()) {
316            assert!(check_satisfaction(&public, &private).is_ok());
317            assert!(check_circuit_satisfaction(public, private).is_ok());
318        }
319    }
320
321    prop_compose! {
322        // This strategy generates an output statement, but then replaces the note commitment
323        // with one generated using an invalid note blinding factor.
324        fn arb_invalid_output_note_commitment_integrity()(seed_phrase_randomness in any::<[u8; 32]>(), rseed_randomness in any::<[u8; 32]>(), amount in any::<u64>(),  balance_blinding in fr_strategy(), asset_id64 in any::<u64>(), address_index in any::<u32>(), incorrect_note_blinding in fq_strategy()) -> (OutputProofPublic, OutputProofPrivate) {
325            let seed_phrase = SeedPhrase::from_randomness(&seed_phrase_randomness);
326            let sk_recipient = SpendKey::from_seed_phrase_bip44(seed_phrase, &Bip44Path::new(0));
327            let fvk_recipient = sk_recipient.full_viewing_key();
328            let ivk_recipient = fvk_recipient.incoming();
329            let (dest, _dtk_d) = ivk_recipient.payment_address(address_index.into());
330
331            let value_to_send = Value {
332                amount: Amount::from(amount),
333                asset_id: asset::Id(Fq::from(asset_id64)),
334            };
335            let note = Note::from_parts(
336                dest,
337                value_to_send,
338                Rseed(rseed_randomness),
339            ).expect("should be able to create note");
340            let balance_commitment = (-Balance::from(value_to_send)).commit(balance_blinding);
341
342            let incorrect_note_commitment = note::commitment(
343                incorrect_note_blinding,
344                value_to_send,
345                note.diversified_generator(),
346                note.transmission_key_s(),
347                note.clue_key(),
348            );
349
350            let bad_public = OutputProofPublic { balance_commitment, note_commitment: incorrect_note_commitment };
351            let private = OutputProofPrivate { note, balance_blinding};
352
353            (bad_public, private)
354        }
355    }
356
357    proptest! {
358        #[test]
359        /// Check that the `OutputCircuit` is not satisfied when the note commitment is invalid.
360        fn output_proof_verification_fails_note_commitment_integrity((public, private) in arb_invalid_output_note_commitment_integrity()) {
361            assert!(check_satisfaction(&public, &private).is_err());
362            assert!(check_circuit_satisfaction(public, private).is_err());
363        }
364    }
365
366    prop_compose! {
367        // This strategy generates an output statement, but then replaces the balance commitment
368        // with one generated using an invalid value blinding factor.
369        fn arb_invalid_output_balance_commitment_integrity()(seed_phrase_randomness in any::<[u8; 32]>(), rseed_randomness in any::<[u8; 32]>(), amount in any::<u64>(),  balance_blinding in fr_strategy(), asset_id64 in any::<u64>(), address_index in any::<u32>(), incorrect_v_blinding in fr_strategy()) -> (OutputProofPublic, OutputProofPrivate) {
370            let seed_phrase = SeedPhrase::from_randomness(&seed_phrase_randomness);
371            let sk_recipient = SpendKey::from_seed_phrase_bip44(seed_phrase, &Bip44Path::new(0));
372            let fvk_recipient = sk_recipient.full_viewing_key();
373            let ivk_recipient = fvk_recipient.incoming();
374            let (dest, _dtk_d) = ivk_recipient.payment_address(address_index.into());
375
376            let value_to_send = Value {
377                amount: Amount::from(amount),
378                asset_id: asset::Id(Fq::from(asset_id64)),
379            };
380            let note = Note::from_parts(
381                dest,
382                value_to_send,
383                Rseed(rseed_randomness),
384            ).expect("should be able to create note");
385            let note_commitment = note.commit();
386
387            let incorrect_balance_commitment = (-Balance::from(value_to_send)).commit(incorrect_v_blinding);
388            let bad_public = OutputProofPublic { balance_commitment: incorrect_balance_commitment, note_commitment  };
389
390            let private = OutputProofPrivate { note, balance_blinding};
391
392            (bad_public, private)
393        }
394    }
395
396    proptest! {
397        #[test]
398        /// Check that the `OutputCircuit` is not satisfied when the balance commitment is invalid.
399        fn output_proof_verification_fails_balance_commitment_integrity((public, private) in arb_invalid_output_balance_commitment_integrity()) {
400            assert!(check_satisfaction(&public, &private).is_err());
401            assert!(check_circuit_satisfaction(public, private).is_err());
402        }
403    }
404}