1use base64::prelude::*;
2use std::str::FromStr;
3use tct::Root;
4
5use anyhow::Result;
6use ark_r1cs_std::{
7 prelude::{EqGadget, FieldVar},
8 uint8::UInt8,
9 ToBitsGadget,
10};
11use ark_serialize::{CanonicalDeserialize, CanonicalSerialize};
12use decaf377::{r1cs::FqVar, Bls12_377, Fq, Fr};
13
14use ark_ff::ToConstraintField;
15use ark_groth16::{
16 r1cs_to_qap::LibsnarkReduction, Groth16, PreparedVerifyingKey, Proof, ProvingKey,
17};
18use ark_r1cs_std::prelude::AllocVar;
19use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystemRef};
20use ark_snark::SNARK;
21use decaf377_rdsa::{SpendAuth, VerificationKey};
22use penumbra_sdk_proto::{penumbra::core::component::shielded_pool::v1 as pb, DomainType};
23use penumbra_sdk_tct as tct;
24use penumbra_sdk_tct::r1cs::StateCommitmentVar;
25
26use crate::{note, Note, Rseed};
27use penumbra_sdk_asset::{
28 balance::commitment::BalanceCommitmentVar,
29 balance::{self, Commitment},
30 Value,
31};
32use penumbra_sdk_keys::keys::{
33 AuthorizationKeyVar, Bip44Path, IncomingViewingKeyVar, NullifierKey, NullifierKeyVar,
34 RandomizedVerificationKey, SeedPhrase, SpendAuthRandomizerVar, SpendKey,
35};
36use penumbra_sdk_proof_params::{DummyWitness, VerifyingKeyExt, GROTH16_PROOF_LENGTH_BYTES};
37use penumbra_sdk_sct::{Nullifier, NullifierVar};
38use tap::Tap;
39
40#[derive(Clone, Debug)]
42pub struct SpendProofPublic {
43 pub anchor: tct::Root,
45 pub balance_commitment: balance::Commitment,
47 pub nullifier: Nullifier,
49 pub rk: VerificationKey<SpendAuth>,
51}
52
53#[derive(Clone, Debug)]
55pub struct SpendProofPrivate {
56 pub state_commitment_proof: tct::Proof,
58 pub note: Note,
60 pub v_blinding: Fr,
62 pub spend_auth_randomizer: Fr,
64 pub ak: VerificationKey<SpendAuth>,
66 pub nk: NullifierKey,
68}
69
70#[cfg(test)]
71fn check_satisfaction(public: &SpendProofPublic, private: &SpendProofPrivate) -> Result<()> {
72 use penumbra_sdk_keys::keys::FullViewingKey;
73
74 let note_commitment = private.note.commit();
75 if note_commitment != private.state_commitment_proof.commitment() {
76 anyhow::bail!("note commitment did not match state commitment proof");
77 }
78
79 let nullifier = Nullifier::derive(
80 &private.nk,
81 private.state_commitment_proof.position(),
82 ¬e_commitment,
83 );
84 if nullifier != public.nullifier {
85 anyhow::bail!("nullifier did not match public input");
86 }
87
88 let amount_u128: u128 = private.note.value().amount.into();
89 if amount_u128 != 0u128 {
90 private.state_commitment_proof.verify(public.anchor)?;
91 }
92
93 let rk = private.ak.randomize(&private.spend_auth_randomizer);
94 if rk != public.rk {
95 anyhow::bail!("randomized spend auth key did not match public input");
96 }
97
98 let fvk = FullViewingKey::from_components(private.ak, private.nk);
99 let ivk = fvk.incoming();
100 let transmission_key = ivk.diversified_public(&private.note.diversified_generator());
101 if transmission_key != *private.note.transmission_key() {
102 anyhow::bail!("transmission key did not match note");
103 }
104
105 let balance_commitment = private.note.value().commit(private.v_blinding);
106 if balance_commitment != public.balance_commitment {
107 anyhow::bail!("balance commitment did not match public input");
108 }
109
110 if private.note.diversified_generator() == decaf377::Element::default() {
111 anyhow::bail!("diversified generator is identity");
112 }
113 if private.ak.is_identity() {
114 anyhow::bail!("ak is identity");
115 }
116
117 Ok(())
118}
119
120#[cfg(test)]
121fn check_circuit_satisfaction(public: SpendProofPublic, private: SpendProofPrivate) -> Result<()> {
122 use ark_relations::r1cs::{self, ConstraintSystem};
123
124 let cs = ConstraintSystem::new_ref();
125 let circuit = SpendCircuit { public, private };
126 cs.set_optimization_goal(r1cs::OptimizationGoal::Constraints);
127 circuit
128 .generate_constraints(cs.clone())
129 .expect("can generate constraints from circuit");
130 cs.finalize();
131 if !cs.is_satisfied()? {
132 anyhow::bail!("constraints are not satisfied");
133 }
134 Ok(())
135}
136
137#[derive(Clone, Debug)]
139pub struct SpendCircuit {
140 public: SpendProofPublic,
141 private: SpendProofPrivate,
142}
143
144impl ConstraintSynthesizer<Fq> for SpendCircuit {
145 fn generate_constraints(self, cs: ConstraintSystemRef<Fq>) -> ark_relations::r1cs::Result<()> {
146 let note_var = note::NoteVar::new_witness(cs.clone(), || Ok(self.private.note.clone()))?;
149 let claimed_note_commitment = StateCommitmentVar::new_witness(cs.clone(), || {
150 Ok(self.private.state_commitment_proof.commitment())
151 })?;
152
153 let position_var = tct::r1cs::PositionVar::new_witness(cs.clone(), || {
154 Ok(self.private.state_commitment_proof.position())
155 })?;
156 let position_bits = position_var.to_bits_le()?;
157 let merkle_path_var = tct::r1cs::MerkleAuthPathVar::new_witness(cs.clone(), || {
158 Ok(self.private.state_commitment_proof)
159 })?;
160
161 let v_blinding_arr: [u8; 32] = self.private.v_blinding.to_bytes();
162 let v_blinding_vars = UInt8::new_witness_vec(cs.clone(), &v_blinding_arr)?;
163
164 let spend_auth_randomizer_var = SpendAuthRandomizerVar::new_witness(cs.clone(), || {
165 Ok(self.private.spend_auth_randomizer)
166 })?;
167 let ak_element_var: AuthorizationKeyVar =
169 AuthorizationKeyVar::new_witness(cs.clone(), || Ok(self.private.ak))?;
170 let nk_var = NullifierKeyVar::new_witness(cs.clone(), || Ok(self.private.nk))?;
171
172 let anchor_var = FqVar::new_input(cs.clone(), || Ok(Fq::from(self.public.anchor)))?;
174 let claimed_balance_commitment_var =
175 BalanceCommitmentVar::new_input(cs.clone(), || Ok(self.public.balance_commitment))?;
176 let claimed_nullifier_var =
177 NullifierVar::new_input(cs.clone(), || Ok(self.public.nullifier))?;
178 let rk_var = RandomizedVerificationKey::new_input(cs.clone(), || Ok(self.public.rk))?;
179
180 let note_commitment_var = note_var.commit()?;
182 note_commitment_var.enforce_equal(&claimed_note_commitment)?;
183
184 let nullifier_var = NullifierVar::derive(&nk_var, &position_var, &claimed_note_commitment)?;
186 nullifier_var.enforce_equal(&claimed_nullifier_var)?;
187
188 let is_not_dummy = note_var.amount().is_eq(&FqVar::zero())?.not();
193 merkle_path_var.verify(
194 cs.clone(),
195 &is_not_dummy,
196 &position_bits,
197 anchor_var,
198 claimed_note_commitment.inner(),
199 )?;
200
201 let computed_rk_var = ak_element_var.randomize(&spend_auth_randomizer_var)?;
203 computed_rk_var.enforce_equal(&rk_var)?;
204
205 let ivk = IncomingViewingKeyVar::derive(&nk_var, &ak_element_var)?;
207 let computed_transmission_key =
208 ivk.diversified_public(¬e_var.diversified_generator())?;
209 computed_transmission_key.enforce_equal(¬e_var.transmission_key())?;
210
211 let balance_commitment = note_var.value().commit(v_blinding_vars)?;
213 balance_commitment.enforce_equal(&claimed_balance_commitment_var)?;
214
215 Ok(())
216 }
217}
218
219impl DummyWitness for SpendCircuit {
220 fn with_dummy_witness() -> Self {
221 let seed_phrase = SeedPhrase::from_randomness(&[b'f'; 32]);
222 let sk_sender = SpendKey::from_seed_phrase_bip44(seed_phrase, &Bip44Path::new(0));
223 let fvk_sender = sk_sender.full_viewing_key();
224 let ivk_sender = fvk_sender.incoming();
225 let (address, _dtk_d) = ivk_sender.payment_address(0u32.into());
226
227 let spend_auth_randomizer = Fr::from(1u64);
228 let rsk = sk_sender.spend_auth_key().randomize(&spend_auth_randomizer);
229 let nk = *sk_sender.nullifier_key();
230 let ak = sk_sender.spend_auth_key().into();
231 let note = Note::from_parts(
232 address,
233 Value::from_str("1upenumbra").expect("valid value"),
234 Rseed([1u8; 32]),
235 )
236 .expect("can make a note");
237 let v_blinding = Fr::from(1u64);
238 let rk: VerificationKey<SpendAuth> = rsk.into();
239 let nullifier = Nullifier(Fq::from(1u64));
240 let mut sct = tct::Tree::new();
241 let anchor: tct::Root = sct.root();
242 let note_commitment = note.commit();
243 sct.insert(tct::Witness::Keep, note_commitment)
244 .expect("able to insert note commitment into SCT");
245 let state_commitment_proof = sct
246 .witness(note_commitment)
247 .expect("able to witness just-inserted note commitment");
248
249 let public = SpendProofPublic {
250 anchor,
251 balance_commitment: balance::Commitment(decaf377::Element::GENERATOR),
252 nullifier,
253 rk,
254 };
255 let private = SpendProofPrivate {
256 state_commitment_proof,
257 note,
258 v_blinding,
259 spend_auth_randomizer,
260 ak,
261 nk,
262 };
263
264 Self { public, private }
265 }
266}
267
268#[derive(Clone, Debug)]
269pub struct SpendProof([u8; GROTH16_PROOF_LENGTH_BYTES]);
270
271#[derive(Debug, thiserror::Error)]
272pub enum VerificationError {
273 #[error("error deserializing compressed proof: {0:?}")]
274 ProofDeserialize(ark_serialize::SerializationError),
275 #[error("Fq types are Bls12-377 field members")]
276 Anchor,
277 #[error("balance commitment is a Bls12-377 field member")]
278 BalanceCommitment,
279 #[error("nullifier is a Bls12-377 field member")]
280 Nullifier,
281 #[error("could not decompress element points: {0:?}")]
282 DecompressRk(decaf377::EncodingError),
283 #[error("randomized spend key is a Bls12-377 field member")]
284 Rk,
285 #[error("start position is a Bls12-377 field member")]
286 StartPosition,
287 #[error("error verifying proof: {0:?}")]
288 SynthesisError(ark_relations::r1cs::SynthesisError),
289 #[error("spend proof did not verify")]
290 InvalidProof,
291}
292
293impl SpendProof {
294 pub fn prove(
297 blinding_r: Fq,
298 blinding_s: Fq,
299 pk: &ProvingKey<Bls12_377>,
300 public: SpendProofPublic,
301 private: SpendProofPrivate,
302 ) -> anyhow::Result<Self> {
303 let circuit = SpendCircuit { public, private };
304 let proof = Groth16::<Bls12_377, LibsnarkReduction>::create_proof_with_reduction(
305 circuit, pk, blinding_r, blinding_s,
306 )
307 .map_err(|err| anyhow::anyhow!(err))?;
308 let mut proof_bytes = [0u8; GROTH16_PROOF_LENGTH_BYTES];
309 Proof::serialize_compressed(&proof, &mut proof_bytes[..]).expect("can serialize Proof");
310 Ok(Self(proof_bytes))
311 }
312
313 #[tracing::instrument(level="debug", skip(self, vk), fields(self = ?BASE64_STANDARD.encode(self.clone().encode_to_vec()), vk = ?vk.debug_id()))]
317 pub fn verify(
318 &self,
319 vk: &PreparedVerifyingKey<Bls12_377>,
320 SpendProofPublic {
321 anchor: Root(anchor),
322 balance_commitment: Commitment(balance_commitment),
323 nullifier: Nullifier(nullifier),
324 rk,
325 }: SpendProofPublic,
326 ) -> Result<(), VerificationError> {
327 let proof = Proof::deserialize_compressed_unchecked(&self.0[..])
328 .map_err(VerificationError::ProofDeserialize)?;
329 let element_rk = decaf377::Encoding(rk.to_bytes())
330 .vartime_decompress()
331 .map_err(VerificationError::DecompressRk)?;
332
333 macro_rules! to_field_elements {
335 ($fe:expr, $err:expr) => {
336 $fe.to_field_elements().ok_or($err)?
337 };
338 }
339
340 use VerificationError::*;
341 let public_inputs = [
342 to_field_elements!(Fq::from(anchor), Anchor),
343 to_field_elements!(balance_commitment, BalanceCommitment),
344 to_field_elements!(nullifier, Nullifier),
345 to_field_elements!(element_rk, Rk),
346 ]
347 .into_iter()
348 .flatten()
349 .collect::<Vec<_>>()
350 .tap(|public_inputs| tracing::trace!(?public_inputs));
351
352 let start = std::time::Instant::now();
353 Groth16::<Bls12_377, LibsnarkReduction>::verify_with_processed_vk(
354 vk,
355 public_inputs.as_slice(),
356 &proof,
357 )
358 .map_err(VerificationError::SynthesisError)?
359 .tap(|proof_result| tracing::debug!(?proof_result, elapsed = ?start.elapsed()))
360 .then_some(())
361 .ok_or(VerificationError::InvalidProof)
362 }
363}
364
365impl DomainType for SpendProof {
366 type Proto = pb::ZkSpendProof;
367}
368
369impl From<SpendProof> for pb::ZkSpendProof {
370 fn from(proof: SpendProof) -> Self {
371 pb::ZkSpendProof {
372 inner: proof.0.to_vec(),
373 }
374 }
375}
376
377impl TryFrom<pb::ZkSpendProof> for SpendProof {
378 type Error = anyhow::Error;
379
380 fn try_from(proto: pb::ZkSpendProof) -> Result<Self, Self::Error> {
381 Ok(SpendProof(proto.inner[..].try_into()?))
382 }
383}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388 use ark_r1cs_std::prelude::Boolean;
389 use decaf377::{Fq, Fr};
390 use penumbra_sdk_asset::{asset, Value};
391 use penumbra_sdk_keys::{
392 keys::{Bip44Path, SeedPhrase, SpendKey},
393 Address,
394 };
395 use penumbra_sdk_num::Amount;
396 use penumbra_sdk_proof_params::generate_prepared_test_parameters;
397 use penumbra_sdk_sct::Nullifier;
398 use penumbra_sdk_tct::StateCommitment;
399 use proptest::prelude::*;
400
401 use crate::Note;
402 use decaf377_rdsa::{SpendAuth, VerificationKey};
403 use penumbra_sdk_tct as tct;
404 use rand_core::OsRng;
405
406 fn fr_strategy() -> BoxedStrategy<Fr> {
407 any::<[u8; 32]>()
408 .prop_map(|bytes| Fr::from_le_bytes_mod_order(&bytes[..]))
409 .boxed()
410 }
411
412 prop_compose! {
413 fn arb_valid_spend_statement()(v_blinding in fr_strategy(), spend_auth_randomizer in fr_strategy(), asset_id64 in any::<u64>(), address_index in any::<u32>(), amount in any::<u64>(), seed_phrase_randomness in any::<[u8; 32]>(), rseed_randomness in any::<[u8; 32]>(), num_commitments in 0..100) -> (SpendProofPublic, SpendProofPrivate) {
414 let seed_phrase = SeedPhrase::from_randomness(&seed_phrase_randomness);
415 let sk_sender = SpendKey::from_seed_phrase_bip44(seed_phrase, &Bip44Path::new(0));
416 let fvk_sender = sk_sender.full_viewing_key();
417 let ivk_sender = fvk_sender.incoming();
418 let (sender, _dtk_d) = ivk_sender.payment_address(address_index.into());
419 let value_to_send = Value {
420 amount: Amount::from(amount),
421 asset_id: asset::Id(Fq::from(asset_id64)),
422 };
423 let note = Note::from_parts(
424 sender.clone(),
425 value_to_send,
426 Rseed(rseed_randomness),
427 ).expect("should be able to create note");
428 let note_commitment = note.commit();
429 let rsk = sk_sender.spend_auth_key().randomize(&spend_auth_randomizer);
430 let nk = *sk_sender.nullifier_key();
431 let ak: VerificationKey<SpendAuth> = sk_sender.spend_auth_key().into();
432
433 let mut sct = tct::Tree::new();
434
435 for i in 0..num_commitments {
438 let rseed = Rseed([i as u8; 32]);
440 let dummy_note_commitment = Note::from_parts(sender.clone(), value_to_send, rseed).expect("can create note").commit();
441 sct.insert(tct::Witness::Keep, dummy_note_commitment).expect("should be able to insert note commitments into the SCT");
442 }
443
444 sct.insert(tct::Witness::Keep, note_commitment).expect("should be able to insert note commitments into the SCT");
445 let anchor = sct.root();
446 let state_commitment_proof = sct.witness(note_commitment).expect("can witness note commitment");
447 let balance_commitment = value_to_send.commit(v_blinding);
448 let rk: VerificationKey<SpendAuth> = rsk.into();
449 let nullifier = Nullifier::derive(&nk, state_commitment_proof.position(), ¬e_commitment);
450
451 let public = SpendProofPublic {
452 anchor,
453 balance_commitment,
454 nullifier,
455 rk,
456 };
457 let private = SpendProofPrivate {
458 state_commitment_proof,
459 note,
460 v_blinding,
461 spend_auth_randomizer,
462 ak,
463 nk,
464 };
465 (public, private)
466 }
467 }
468
469 proptest! {
470 #[test]
471 fn spend_proof_happy_path((public, private) in arb_valid_spend_statement()) {
472 assert!(check_satisfaction(&public, &private).is_ok());
473 assert!(check_circuit_satisfaction(public, private).is_ok());
474 }
475 }
476
477 prop_compose! {
478 fn arb_invalid_spend_statement_incorrect_anchor()(v_blinding in fr_strategy(), spend_auth_randomizer in fr_strategy(), asset_id64 in any::<u64>(), address_index in any::<u32>(), amount in any::<u64>(), seed_phrase_randomness in any::<[u8; 32]>(), rseed_randomness in any::<[u8; 32]>(), num_commitments in 0..100) -> (SpendProofPublic, SpendProofPrivate) {
483 let seed_phrase = SeedPhrase::from_randomness(&seed_phrase_randomness);
484 let sk_sender = SpendKey::from_seed_phrase_bip44(seed_phrase, &Bip44Path::new(0));
485 let fvk_sender = sk_sender.full_viewing_key();
486 let ivk_sender = fvk_sender.incoming();
487 let (sender, _dtk_d) = ivk_sender.payment_address(address_index.into());
488 let value_to_send = Value {
489 amount: Amount::from(amount),
490 asset_id: asset::Id(Fq::from(asset_id64)),
491 };
492 let note = Note::from_parts(
493 sender.clone(),
494 value_to_send,
495 Rseed(rseed_randomness),
496 ).expect("should be able to create note");
497 let note_commitment = note.commit();
498 let rsk = sk_sender.spend_auth_key().randomize(&spend_auth_randomizer);
499 let nk = *sk_sender.nullifier_key();
500 let ak: VerificationKey<SpendAuth> = sk_sender.spend_auth_key().into();
501
502 let mut sct = tct::Tree::new();
503
504 for i in 0..num_commitments {
507 let rseed = Rseed([i as u8; 32]);
509 let dummy_note_commitment = Note::from_parts(sender.clone(), value_to_send, rseed).expect("can create note").commit();
510 sct.insert(tct::Witness::Keep, dummy_note_commitment).expect("should be able to insert note commitments into the SCT");
511 }
512 let incorrect_anchor = sct.root();
513
514 sct.insert(tct::Witness::Keep, note_commitment).expect("should be able to insert note commitments into the SCT");
515 let state_commitment_proof = sct.witness(note_commitment).expect("can witness note commitment");
516 let balance_commitment = value_to_send.commit(v_blinding);
517 let rk: VerificationKey<SpendAuth> = rsk.into();
518 let nullifier = Nullifier::derive(&nk, state_commitment_proof.position(), ¬e_commitment);
519
520 let public = SpendProofPublic {
521 anchor: incorrect_anchor,
522 balance_commitment,
523 nullifier,
524 rk,
525 };
526 let private = SpendProofPrivate {
527 state_commitment_proof,
528 note,
529 v_blinding,
530 spend_auth_randomizer,
531 ak,
532 nk,
533 };
534 (public, private)
535 }
536 }
537
538 proptest! {
539 #[test]
540 fn spend_proof_verification_merkle_path_integrity_failure((public, private) in arb_invalid_spend_statement_incorrect_anchor()) {
543 assert!(check_satisfaction(&public, &private).is_err());
544 assert!(check_circuit_satisfaction(public, private).is_err());
545 }
546 }
547
548 prop_compose! {
549 fn arb_invalid_spend_statement_diversified_address()(v_blinding in fr_strategy(), spend_auth_randomizer in fr_strategy(), asset_id64 in any::<u64>(), address_index in any::<u32>(), amount in any::<u64>(), seed_phrase_randomness in any::<[u8; 32]>(), incorrect_seed_phrase_randomness in any::<[u8; 32]>(), rseed_randomness in any::<[u8; 32]>()) -> (SpendProofPublic, SpendProofPrivate) {
561 let seed_phrase = SeedPhrase::from_randomness(&seed_phrase_randomness);
562 let sk_sender = SpendKey::from_seed_phrase_bip44(seed_phrase, &Bip44Path::new(0));
563 let fvk_sender = sk_sender.full_viewing_key();
564 let ivk_sender = fvk_sender.incoming();
565 let (_sender, _dtk_d) = ivk_sender.payment_address(address_index.into());
566 let value_to_send = Value {
567 amount: Amount::from(amount),
568 asset_id: asset::Id(Fq::from(asset_id64)),
569 };
570
571 let wrong_seed_phrase = SeedPhrase::from_randomness(&incorrect_seed_phrase_randomness);
572 let wrong_sk_sender = SpendKey::from_seed_phrase_bip44(wrong_seed_phrase, &Bip44Path::new(0));
573 let wrong_fvk_sender = wrong_sk_sender.full_viewing_key();
574 let wrong_ivk_sender = wrong_fvk_sender.incoming();
575 let (wrong_sender, _dtk_d) = wrong_ivk_sender.payment_address(address_index.into());
576
577 let note = Note::from_parts(
578 wrong_sender,
579 value_to_send,
580 Rseed(rseed_randomness),
581 ).expect("should be able to create note");
582 let note_commitment = note.commit();
583 let rsk = sk_sender.spend_auth_key().randomize(&spend_auth_randomizer);
584 let nk = *sk_sender.nullifier_key();
585 let ak: VerificationKey<SpendAuth> = sk_sender.spend_auth_key().into();
586
587 let mut sct = tct::Tree::new();
588 sct.insert(tct::Witness::Keep, note_commitment).expect("should be able to insert note commitments into the SCT");
589 let anchor = sct.root();
590 let state_commitment_proof = sct.witness(note_commitment).expect("can witness note commitment");
591 let balance_commitment = value_to_send.commit(v_blinding);
592 let rk: VerificationKey<SpendAuth> = rsk.into();
593 let nullifier = Nullifier::derive(&nk, state_commitment_proof.position(), ¬e_commitment);
594
595 let public = SpendProofPublic {
596 anchor,
597 balance_commitment,
598 nullifier,
599 rk,
600 };
601 let private = SpendProofPrivate {
602 state_commitment_proof,
603 note,
604 v_blinding,
605 spend_auth_randomizer,
606 ak,
607 nk,
608 };
609 (public, private)
610 }
611 }
612
613 proptest! {
614 #[test]
615 fn spend_proof_verification_diversified_address_integrity_failure((public, private) in arb_invalid_spend_statement_diversified_address()) {
617 assert!(check_satisfaction(&public, &private).is_err());
618 assert!(check_circuit_satisfaction(public, private).is_err());
619 }
620 }
621
622 prop_compose! {
623 fn arb_invalid_spend_statement_nullifier()(v_blinding in fr_strategy(), spend_auth_randomizer in fr_strategy(), asset_id64 in any::<u64>(), address_index in any::<u32>(), amount in any::<u64>(), seed_phrase_randomness in any::<[u8; 32]>(), rseed_randomness in any::<[u8; 32]>(), num_commitments in 0..100) -> (SpendProofPublic, SpendProofPrivate) {
626 let seed_phrase = SeedPhrase::from_randomness(&seed_phrase_randomness);
627 let sk_sender = SpendKey::from_seed_phrase_bip44(seed_phrase, &Bip44Path::new(0));
628 let fvk_sender = sk_sender.full_viewing_key();
629 let ivk_sender = fvk_sender.incoming();
630 let (sender, _dtk_d) = ivk_sender.payment_address(address_index.into());
631 let value_to_send = Value {
632 amount: Amount::from(amount),
633 asset_id: asset::Id(Fq::from(asset_id64)),
634 };
635 let note = Note::from_parts(
636 sender.clone(),
637 value_to_send,
638 Rseed(rseed_randomness),
639 ).expect("should be able to create note");
640 let note_commitment = note.commit();
641 let rsk = sk_sender.spend_auth_key().randomize(&spend_auth_randomizer);
642 let nk = *sk_sender.nullifier_key();
643 let ak: VerificationKey<SpendAuth> = sk_sender.spend_auth_key().into();
644
645 let mut sct = tct::Tree::new();
646
647 for i in 0..num_commitments {
650 let rseed = Rseed([i as u8; 32]);
652 let dummy_note_commitment = Note::from_parts(sender.clone(), value_to_send, rseed).expect("can create note").commit();
653 sct.insert(tct::Witness::Keep, dummy_note_commitment).expect("should be able to insert note commitments into the SCT");
654 }
655 let rseed = Rseed([num_commitments as u8; 32]);
657 let dummy_note_commitment = Note::from_parts(sender.clone(), value_to_send, rseed).expect("can create note").commit();
658 sct.insert(tct::Witness::Keep, dummy_note_commitment).expect("should be able to insert note commitments into the SCT");
659 let incorrect_position = sct.witness(dummy_note_commitment).expect("can witness note commitment").position();
660
661 sct.insert(tct::Witness::Keep, note_commitment).expect("should be able to insert note commitments into the SCT");
662 let anchor = sct.root();
663 let state_commitment_proof = sct.witness(note_commitment).expect("can witness note commitment");
664 let balance_commitment = value_to_send.commit(v_blinding);
665 let rk: VerificationKey<SpendAuth> = rsk.into();
666 let incorrect_nf = Nullifier::derive(&nk, incorrect_position, ¬e_commitment);
667
668 let public = SpendProofPublic {
669 anchor,
670 balance_commitment,
671 nullifier: incorrect_nf,
672 rk,
673 };
674 let private = SpendProofPrivate {
675 state_commitment_proof,
676 note,
677 v_blinding,
678 spend_auth_randomizer,
679 ak,
680 nk,
681 };
682 (public, private)
683 }
684 }
685
686 proptest! {
687 #[test]
688 fn spend_proof_verification_nullifier_integrity_failure((public, private) in arb_invalid_spend_statement_nullifier()) {
691 assert!(check_satisfaction(&public, &private).is_err());
692 assert!(check_circuit_satisfaction(public, private).is_err());
693 }
694 }
695
696 prop_compose! {
697 fn arb_invalid_spend_statement_v_blinding_factor()(v_blinding in fr_strategy(), incorrect_v_blinding in fr_strategy(), spend_auth_randomizer in fr_strategy(), asset_id64 in any::<u64>(), address_index in any::<u32>(), amount in any::<u64>(), seed_phrase_randomness in any::<[u8; 32]>(), rseed_randomness in any::<[u8; 32]>(), num_commitments in 0..100) -> (SpendProofPublic, SpendProofPrivate) {
700 let seed_phrase = SeedPhrase::from_randomness(&seed_phrase_randomness);
701 let sk_sender = SpendKey::from_seed_phrase_bip44(seed_phrase, &Bip44Path::new(0));
702 let fvk_sender = sk_sender.full_viewing_key();
703 let ivk_sender = fvk_sender.incoming();
704 let (sender, _dtk_d) = ivk_sender.payment_address(address_index.into());
705 let value_to_send = Value {
706 amount: Amount::from(amount),
707 asset_id: asset::Id(Fq::from(asset_id64)),
708 };
709 let note = Note::from_parts(
710 sender.clone(),
711 value_to_send,
712 Rseed(rseed_randomness),
713 ).expect("should be able to create note");
714 let note_commitment = note.commit();
715 let rsk = sk_sender.spend_auth_key().randomize(&spend_auth_randomizer);
716 let nk = *sk_sender.nullifier_key();
717 let ak: VerificationKey<SpendAuth> = sk_sender.spend_auth_key().into();
718
719 let mut sct = tct::Tree::new();
720
721 for i in 0..num_commitments {
724 let rseed = Rseed([i as u8; 32]);
726 let dummy_note_commitment = Note::from_parts(sender.clone(), value_to_send, rseed).expect("can create note").commit();
727 sct.insert(tct::Witness::Keep, dummy_note_commitment).expect("should be able to insert note commitments into the SCT");
728 }
729
730 sct.insert(tct::Witness::Keep, note_commitment).expect("should be able to insert note commitments into the SCT");
731 let anchor = sct.root();
732 let state_commitment_proof = sct.witness(note_commitment).expect("can witness note commitment");
733 let balance_commitment = value_to_send.commit(v_blinding);
734 let rk: VerificationKey<SpendAuth> = rsk.into();
735 let nullifier = Nullifier::derive(&nk, state_commitment_proof.position(), ¬e_commitment);
736
737 let public = SpendProofPublic {
738 anchor,
739 balance_commitment,
740 nullifier,
741 rk,
742 };
743 let private = SpendProofPrivate {
744 state_commitment_proof,
745 note,
746 v_blinding: incorrect_v_blinding,
747 spend_auth_randomizer,
748 ak,
749 nk,
750 };
751 (public, private)
752 }
753 }
754
755 proptest! {
756 #[test]
757 fn spend_proof_verification_balance_commitment_integrity_failure((public, private) in arb_invalid_spend_statement_v_blinding_factor()) {
760 assert!(check_satisfaction(&public, &private).is_err());
761 assert!(check_circuit_satisfaction(public, private).is_err());
762 }
763 }
764
765 prop_compose! {
766 fn arb_invalid_spend_statement_rk_integrity()(v_blinding in fr_strategy(), spend_auth_randomizer in fr_strategy(), asset_id64 in any::<u64>(), address_index in any::<u32>(), amount in any::<u64>(), seed_phrase_randomness in any::<[u8; 32]>(), rseed_randomness in any::<[u8; 32]>(), num_commitments in 0..100, incorrect_spend_auth_randomizer in fr_strategy()) -> (SpendProofPublic, SpendProofPrivate) {
769 let seed_phrase = SeedPhrase::from_randomness(&seed_phrase_randomness);
770 let sk_sender = SpendKey::from_seed_phrase_bip44(seed_phrase, &Bip44Path::new(0));
771 let fvk_sender = sk_sender.full_viewing_key();
772 let ivk_sender = fvk_sender.incoming();
773 let (sender, _dtk_d) = ivk_sender.payment_address(address_index.into());
774 let value_to_send = Value {
775 amount: Amount::from(amount),
776 asset_id: asset::Id(Fq::from(asset_id64)),
777 };
778 let note = Note::from_parts(
779 sender.clone(),
780 value_to_send,
781 Rseed(rseed_randomness),
782 ).expect("should be able to create note");
783 let note_commitment = note.commit();
784 let nk = *sk_sender.nullifier_key();
785 let ak: VerificationKey<SpendAuth> = sk_sender.spend_auth_key().into();
786
787 let mut sct = tct::Tree::new();
788
789 for i in 0..num_commitments {
792 let rseed = Rseed([i as u8; 32]);
794 let dummy_note_commitment = Note::from_parts(sender.clone(), value_to_send, rseed).expect("can create note").commit();
795 sct.insert(tct::Witness::Keep, dummy_note_commitment).expect("should be able to insert note commitments into the SCT");
796 }
797
798 sct.insert(tct::Witness::Keep, note_commitment).expect("should be able to insert note commitments into the SCT");
799 let anchor = sct.root();
800 let state_commitment_proof = sct.witness(note_commitment).expect("can witness note commitment");
801 let balance_commitment = value_to_send.commit(v_blinding);
802 let nullifier = Nullifier::derive(&nk, state_commitment_proof.position(), ¬e_commitment);
803
804 let incorrect_rsk = sk_sender
805 .spend_auth_key()
806 .randomize(&incorrect_spend_auth_randomizer);
807 let incorrect_rk: VerificationKey<SpendAuth> = incorrect_rsk.into();
808
809 let public = SpendProofPublic {
810 anchor,
811 balance_commitment,
812 nullifier,
813 rk: incorrect_rk,
814 };
815 let private = SpendProofPrivate {
816 state_commitment_proof,
817 note,
818 v_blinding,
819 spend_auth_randomizer,
820 ak,
821 nk,
822 };
823 (public, private)
824 }
825 }
826
827 proptest! {
828 #[test]
829 fn spend_proof_verification_fails_rk_integrity((public, private) in arb_invalid_spend_statement_rk_integrity()) {
831 assert!(check_satisfaction(&public, &private).is_err());
832 assert!(check_circuit_satisfaction(public, private).is_err());
833 }
834 }
835
836 prop_compose! {
837 fn arb_valid_dummy_spend_statement()(v_blinding in fr_strategy(), spend_auth_randomizer in fr_strategy(), asset_id64 in any::<u64>(), address_index in any::<u32>(), seed_phrase_randomness in any::<[u8; 32]>(), rseed_randomness in any::<[u8; 32]>()) -> (SpendProofPublic, SpendProofPrivate) {
838 let seed_phrase = SeedPhrase::from_randomness(&seed_phrase_randomness);
839 let sk_sender = SpendKey::from_seed_phrase_bip44(seed_phrase, &Bip44Path::new(0));
840 let fvk_sender = sk_sender.full_viewing_key();
841 let ivk_sender = fvk_sender.incoming();
842 let (sender, _dtk_d) = ivk_sender.payment_address(address_index.into());
843 let value_to_send = Value {
844 amount: Amount::from(0u64),
845 asset_id: asset::Id(Fq::from(asset_id64)),
846 };
847 let note = Note::from_parts(
848 sender.clone(),
849 value_to_send,
850 Rseed(rseed_randomness),
851 ).expect("should be able to create note");
852 let note_commitment = note.commit();
853 let rsk = sk_sender.spend_auth_key().randomize(&spend_auth_randomizer);
854 let nk = *sk_sender.nullifier_key();
855 let ak: VerificationKey<SpendAuth> = sk_sender.spend_auth_key().into();
856
857 let mut sct = tct::Tree::new();
858 sct.insert(tct::Witness::Keep, note_commitment).expect("should be able to insert note commitments into the SCT");
859
860 let state_commitment_proof = sct.witness(note_commitment).expect("can witness note commitment");
861 let balance_commitment = value_to_send.commit(v_blinding);
862 let rk: VerificationKey<SpendAuth> = rsk.into();
863 let nullifier = Nullifier::derive(&nk, state_commitment_proof.position(), ¬e_commitment);
864
865 let invalid_anchor = tct::Tree::new().root();
868
869 let public = SpendProofPublic {
870 anchor: invalid_anchor,
871 balance_commitment,
872 nullifier,
873 rk,
874 };
875 let private = SpendProofPrivate {
876 state_commitment_proof,
877 note,
878 v_blinding,
879 spend_auth_randomizer,
880 ak,
881 nk,
882 };
883 (public, private)
884 }
885 }
886
887 proptest! {
888 #[test]
889 fn spend_proof_dummy_verification_suceeds((public, private) in arb_valid_dummy_spend_statement()) {
891 assert!(check_satisfaction(&public, &private).is_ok());
892 assert!(check_circuit_satisfaction(public, private).is_ok());
893 }
894 }
895
896 struct MerkleProofCircuit {
897 state_commitment_proof: tct::Proof,
899 pub anchor: tct::Root,
901 pub epoch: Fq,
902 pub block: Fq,
903 pub commitment_index: Fq,
904 }
905
906 impl ConstraintSynthesizer<Fq> for MerkleProofCircuit {
907 fn generate_constraints(
908 self,
909 cs: ConstraintSystemRef<Fq>,
910 ) -> ark_relations::r1cs::Result<()> {
911 let anchor_var = FqVar::new_input(cs.clone(), || Ok(Fq::from(self.anchor)))?;
913 let epoch_var = FqVar::new_input(cs.clone(), || Ok(self.epoch))?;
914 let block_var = FqVar::new_input(cs.clone(), || Ok(self.block))?;
915 let commitment_index_var = FqVar::new_input(cs.clone(), || Ok(self.commitment_index))?;
916
917 let merkle_path_var = tct::r1cs::MerkleAuthPathVar::new_witness(cs.clone(), || {
919 Ok(self.state_commitment_proof.clone())
920 })?;
921 let claimed_note_commitment = StateCommitmentVar::new_witness(cs.clone(), || {
922 Ok(self.state_commitment_proof.commitment())
923 })?;
924 let position_var = tct::r1cs::PositionVar::new_witness(cs.clone(), || {
925 Ok(self.state_commitment_proof.position())
926 })?;
927 let position_bits = position_var.to_bits_le()?;
928 merkle_path_var.verify(
929 cs,
930 &Boolean::TRUE,
931 &position_bits,
932 anchor_var,
933 claimed_note_commitment.inner(),
934 )?;
935
936 let computed_epoch = position_var.epoch()?;
939 let computed_block = position_var.block()?;
940 let computed_commitment_index = position_var.commitment()?;
941 computed_epoch.enforce_equal(&epoch_var)?;
942 computed_block.enforce_equal(&block_var)?;
943 computed_commitment_index.enforce_equal(&commitment_index_var)?;
944 Ok(())
945 }
946 }
947
948 impl DummyWitness for MerkleProofCircuit {
949 fn with_dummy_witness() -> Self {
950 let seed_phrase = SeedPhrase::from_randomness(&[b'f'; 32]);
951 let sk_sender = SpendKey::from_seed_phrase_bip44(seed_phrase, &Bip44Path::new(0));
952 let fvk_sender = sk_sender.full_viewing_key();
953 let ivk_sender = fvk_sender.incoming();
954 let (address, _dtk_d) = ivk_sender.payment_address(0u32.into());
955
956 let note = Note::from_parts(
957 address,
958 Value::from_str("1upenumbra").expect("valid value"),
959 Rseed([1u8; 32]),
960 )
961 .expect("can make a note");
962 let mut sct = tct::Tree::new();
963 let note_commitment = note.commit();
964 sct.insert(tct::Witness::Keep, note_commitment)
965 .expect("able to insert note commitment into SCT");
966 let anchor = sct.root();
967 let state_commitment_proof = sct
968 .witness(note_commitment)
969 .expect("able to witness just-inserted note commitment");
970 let position = state_commitment_proof.position();
971 let epoch = Fq::from(position.epoch());
972 let block = Fq::from(position.block());
973 let commitment_index = Fq::from(position.commitment());
974
975 Self {
976 state_commitment_proof,
977 anchor,
978 epoch,
979 block,
980 commitment_index,
981 }
982 }
983 }
984
985 fn make_random_note_commitment(address: Address) -> StateCommitment {
986 let note = Note::from_parts(
987 address,
988 Value::from_str("1upenumbra").expect("valid value"),
989 Rseed([1u8; 32]),
990 )
991 .expect("can make a note");
992 note.commit()
993 }
994
995 #[test]
996 fn merkle_proof_verification_succeeds() {
997 let mut rng = OsRng;
998 let (pk, vk) = generate_prepared_test_parameters::<MerkleProofCircuit>(&mut rng);
999
1000 let seed_phrase = SeedPhrase::from_randomness(&[b'f'; 32]);
1001 let sk_sender = SpendKey::from_seed_phrase_bip44(seed_phrase, &Bip44Path::new(0));
1002 let fvk_sender = sk_sender.full_viewing_key();
1003 let ivk_sender = fvk_sender.incoming();
1004 let (address, _dtk_d) = ivk_sender.payment_address(0u32.into());
1005 let mut sct = tct::Tree::new();
1008
1009 for _ in 0..5 {
1010 let note_commitment = make_random_note_commitment(address.clone());
1011 sct.insert(tct::Witness::Keep, note_commitment).unwrap();
1012 let anchor = sct.root();
1013 let state_commitment_proof = sct.witness(note_commitment).unwrap();
1014 let position = state_commitment_proof.position();
1015 let epoch = Fq::from(position.epoch());
1016 let block = Fq::from(position.block());
1017 let commitment_index = Fq::from(position.commitment());
1018 let circuit = MerkleProofCircuit {
1019 state_commitment_proof,
1020 anchor,
1021 epoch,
1022 block,
1023 commitment_index,
1024 };
1025 let proof = Groth16::<Bls12_377, LibsnarkReduction>::prove(&pk, circuit, &mut rng)
1026 .expect("should be able to form proof");
1027
1028 let proof_result = Groth16::<Bls12_377, LibsnarkReduction>::verify_with_processed_vk(
1029 &vk,
1030 &[Fq::from(anchor), epoch, block, commitment_index],
1031 &proof,
1032 );
1033 assert!(proof_result.is_ok());
1034 }
1035
1036 sct.end_block().expect("can end block");
1037 for _ in 0..100 {
1038 let note_commitment = make_random_note_commitment(address.clone());
1039 sct.insert(tct::Witness::Forget, note_commitment).unwrap();
1040 }
1041
1042 for _ in 0..5 {
1043 let note_commitment = make_random_note_commitment(address.clone());
1044 sct.insert(tct::Witness::Keep, note_commitment).unwrap();
1045 let anchor = sct.root();
1046 let state_commitment_proof = sct.witness(note_commitment).unwrap();
1047 let position = state_commitment_proof.position();
1048 let epoch = Fq::from(position.epoch());
1049 let block = Fq::from(position.block());
1050 let commitment_index = Fq::from(position.commitment());
1051 let circuit = MerkleProofCircuit {
1052 state_commitment_proof,
1053 anchor,
1054 epoch,
1055 block,
1056 commitment_index,
1057 };
1058 let proof = Groth16::<Bls12_377, LibsnarkReduction>::prove(&pk, circuit, &mut rng)
1059 .expect("should be able to form proof");
1060
1061 let proof_result = Groth16::<Bls12_377, LibsnarkReduction>::verify_with_processed_vk(
1062 &vk,
1063 &[Fq::from(anchor), epoch, block, commitment_index],
1064 &proof,
1065 );
1066 assert!(proof_result.is_ok());
1067 }
1068
1069 sct.end_epoch().expect("can end epoch");
1070 for _ in 0..100 {
1071 let note_commitment = make_random_note_commitment(address.clone());
1072 sct.insert(tct::Witness::Forget, note_commitment).unwrap();
1073 }
1074
1075 for _ in 0..5 {
1076 let note_commitment = make_random_note_commitment(address.clone());
1077 sct.insert(tct::Witness::Keep, note_commitment).unwrap();
1078 let anchor = sct.root();
1079 let state_commitment_proof = sct.witness(note_commitment).unwrap();
1080 let position = state_commitment_proof.position();
1081 let epoch = Fq::from(position.epoch());
1082 let block = Fq::from(position.block());
1083 let commitment_index = Fq::from(position.commitment());
1084 let circuit = MerkleProofCircuit {
1085 state_commitment_proof,
1086 anchor,
1087 epoch,
1088 block,
1089 commitment_index,
1090 };
1091 let proof = Groth16::<Bls12_377, LibsnarkReduction>::prove(&pk, circuit, &mut rng)
1092 .expect("should be able to form proof");
1093
1094 let proof_result = Groth16::<Bls12_377, LibsnarkReduction>::verify_with_processed_vk(
1095 &vk,
1096 &[Fq::from(anchor), epoch, block, commitment_index],
1097 &proof,
1098 );
1099 assert!(proof_result.is_ok());
1100 }
1101 }
1102}