penumbra_sdk_shielded_pool/output/
proof.rs1use 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#[derive(Clone, Debug)]
31pub struct OutputProofPublic {
32 pub balance_commitment: balance::Commitment,
34 pub note_commitment: note::StateCommitment,
36}
37
38#[derive(Clone, Debug)]
40pub struct OutputProofPrivate {
41 pub note: Note,
43 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#[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 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 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 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 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 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 #[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 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 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 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 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}