penumbra_sdk_shielded_pool/
nullifier_derivation.rs1use base64::prelude::*;
2use std::str::FromStr;
3
4use anyhow::Result;
5use ark_r1cs_std::prelude::*;
6use ark_serialize::{CanonicalDeserialize, CanonicalSerialize};
7use decaf377::{Bls12_377, Fq};
8
9use ark_ff::ToConstraintField;
10use ark_groth16::{
11 r1cs_to_qap::LibsnarkReduction, Groth16, PreparedVerifyingKey, Proof, ProvingKey,
12};
13use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystemRef};
14use ark_snark::SNARK;
15use penumbra_sdk_proto::{penumbra::core::component::shielded_pool::v1 as pb, DomainType};
16use penumbra_sdk_tct as tct;
17use rand::{CryptoRng, Rng};
18use tct::StateCommitment;
19
20use crate::{Note, Rseed};
21use penumbra_sdk_asset::Value;
22use penumbra_sdk_keys::keys::{Bip44Path, NullifierKey, NullifierKeyVar, SeedPhrase, SpendKey};
23use penumbra_sdk_proof_params::{DummyWitness, VerifyingKeyExt, GROTH16_PROOF_LENGTH_BYTES};
24use penumbra_sdk_sct::{Nullifier, NullifierVar};
25
26#[derive(Clone, Debug)]
28pub struct NullifierDerivationProofPublic {
29 pub position: tct::Position,
31 pub note_commitment: StateCommitment,
33 pub nullifier: Nullifier,
35}
36
37#[derive(Clone, Debug)]
39pub struct NullifierDerivationProofPrivate {
40 pub nk: NullifierKey,
42}
43
44#[cfg(test)]
45fn check_satisfaction(
46 public: &NullifierDerivationProofPublic,
47 private: &NullifierDerivationProofPrivate,
48) -> Result<()> {
49 let nullifier = Nullifier::derive(&private.nk, public.position, &public.note_commitment);
50 if nullifier != public.nullifier {
51 anyhow::bail!("nullifier did not match public input");
52 }
53 Ok(())
54}
55
56#[cfg(test)]
57fn check_circuit_satisfaction(
58 public: NullifierDerivationProofPublic,
59 private: NullifierDerivationProofPrivate,
60) -> Result<()> {
61 use ark_relations::r1cs::{self, ConstraintSystem};
62
63 let cs = ConstraintSystem::new_ref();
64 let circuit = NullifierDerivationCircuit { public, private };
65 cs.set_optimization_goal(r1cs::OptimizationGoal::Constraints);
66 circuit
67 .generate_constraints(cs.clone())
68 .expect("can generate constraints from circuit");
69 cs.finalize();
70 if !cs.is_satisfied()? {
71 anyhow::bail!("constraints are not satisfied");
72 }
73 Ok(())
74}
75
76#[derive(Clone, Debug)]
83pub struct NullifierDerivationCircuit {
84 public: NullifierDerivationProofPublic,
85 private: NullifierDerivationProofPrivate,
86}
87
88impl ConstraintSynthesizer<Fq> for NullifierDerivationCircuit {
89 fn generate_constraints(self, cs: ConstraintSystemRef<Fq>) -> ark_relations::r1cs::Result<()> {
90 let nk_var = NullifierKeyVar::new_witness(cs.clone(), || Ok(self.private.nk))?;
92
93 let claimed_nullifier_var =
95 NullifierVar::new_input(cs.clone(), || Ok(self.public.nullifier))?;
96 let note_commitment_var = tct::r1cs::StateCommitmentVar::new_input(cs.clone(), || {
97 Ok(self.public.note_commitment)
98 })?;
99 let position_var = tct::r1cs::PositionVar::new_input(cs, || Ok(self.public.position))?;
100
101 let nullifier_var = NullifierVar::derive(&nk_var, &position_var, ¬e_commitment_var)?;
103 nullifier_var.conditional_enforce_equal(&claimed_nullifier_var, &Boolean::TRUE)?;
104
105 Ok(())
106 }
107}
108
109impl DummyWitness for NullifierDerivationCircuit {
110 fn with_dummy_witness() -> Self {
111 let seed_phrase = SeedPhrase::from_randomness(&[b'f'; 32]);
112 let sk_sender = SpendKey::from_seed_phrase_bip44(seed_phrase, &Bip44Path::new(0));
113 let fvk_sender = sk_sender.full_viewing_key();
114 let ivk_sender = fvk_sender.incoming();
115 let (address, _dtk_d) = ivk_sender.payment_address(0u32.into());
116
117 let nk = *sk_sender.nullifier_key();
118 let note = Note::from_parts(
119 address,
120 Value::from_str("1upenumbra").expect("valid value"),
121 Rseed([1u8; 32]),
122 )
123 .expect("can make a note");
124 let nullifier = Nullifier(Fq::from(1u64));
125 let mut sct = tct::Tree::new();
126 let note_commitment = note.commit();
127 sct.insert(tct::Witness::Keep, note_commitment)
128 .expect("able to insert note commitment into SCT");
129 let state_commitment_proof = sct
130 .witness(note_commitment)
131 .expect("able to witness just-inserted note commitment");
132 let position = state_commitment_proof.position();
133
134 let public = NullifierDerivationProofPublic {
135 position,
136 note_commitment,
137 nullifier,
138 };
139 let private = NullifierDerivationProofPrivate { nk };
140
141 Self { public, private }
142 }
143}
144
145#[derive(Clone, Debug)]
146pub struct NullifierDerivationProof([u8; GROTH16_PROOF_LENGTH_BYTES]);
147
148impl NullifierDerivationProof {
149 pub fn prove<R: CryptoRng + Rng>(
150 rng: &mut R,
151 pk: &ProvingKey<Bls12_377>,
152 public: NullifierDerivationProofPublic,
153 private: NullifierDerivationProofPrivate,
154 ) -> anyhow::Result<Self> {
155 let circuit = NullifierDerivationCircuit { public, private };
156 let proof = Groth16::<Bls12_377, LibsnarkReduction>::prove(pk, circuit, rng)
157 .map_err(|err| anyhow::anyhow!(err))?;
158 let mut proof_bytes = [0u8; GROTH16_PROOF_LENGTH_BYTES];
159 Proof::serialize_compressed(&proof, &mut proof_bytes[..]).expect("can serialize Proof");
160 Ok(Self(proof_bytes))
161 }
162
163 #[tracing::instrument(level="debug", skip(self, vk), fields(self = ?BASE64_STANDARD.encode(&self.0), vk = ?vk.debug_id()))]
165 pub fn verify(
166 &self,
167 vk: &PreparedVerifyingKey<Bls12_377>,
168 public: NullifierDerivationProofPublic,
169 ) -> anyhow::Result<()> {
170 let proof =
171 Proof::deserialize_compressed_unchecked(&self.0[..]).map_err(|e| anyhow::anyhow!(e))?;
172
173 let mut public_inputs = Vec::new();
174 public_inputs.extend(
175 public
176 .nullifier
177 .0
178 .to_field_elements()
179 .ok_or_else(|| anyhow::anyhow!("could not convert nullifier to field elements"))?,
180 );
181 public_inputs.extend(
182 public
183 .note_commitment
184 .0
185 .to_field_elements()
186 .ok_or_else(|| {
187 anyhow::anyhow!("could not convert note commitment to field elements")
188 })?,
189 );
190 public_inputs.extend(
191 public
192 .position
193 .to_field_elements()
194 .ok_or_else(|| anyhow::anyhow!("could not convert position to field elements"))?,
195 );
196
197 tracing::trace!(?public_inputs);
198 let start = std::time::Instant::now();
199 let proof_result = Groth16::<Bls12_377, LibsnarkReduction>::verify_with_processed_vk(
200 vk,
201 public_inputs.as_slice(),
202 &proof,
203 )
204 .map_err(|err| anyhow::anyhow!(err))?;
205 tracing::debug!(?proof_result, elapsed = ?start.elapsed());
206 proof_result
207 .then_some(())
208 .ok_or_else(|| anyhow::anyhow!("nullifier derivation proof did not verify"))
209 }
210}
211
212impl DomainType for NullifierDerivationProof {
213 type Proto = pb::ZkNullifierDerivationProof;
214}
215
216impl From<NullifierDerivationProof> for pb::ZkNullifierDerivationProof {
217 fn from(proof: NullifierDerivationProof) -> Self {
218 pb::ZkNullifierDerivationProof {
219 inner: proof.0.to_vec(),
220 }
221 }
222}
223
224impl TryFrom<pb::ZkNullifierDerivationProof> for NullifierDerivationProof {
225 type Error = anyhow::Error;
226
227 fn try_from(proto: pb::ZkNullifierDerivationProof) -> Result<Self, Self::Error> {
228 Ok(NullifierDerivationProof(proto.inner[..].try_into()?))
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235 use penumbra_sdk_asset::{asset, Value};
236 use penumbra_sdk_keys::keys::{SeedPhrase, SpendKey};
237 use penumbra_sdk_num::Amount;
238 use penumbra_sdk_sct::Nullifier;
239 use proptest::prelude::*;
240
241 use crate::Note;
242
243 prop_compose! {
244 fn arb_valid_nullifier_derivation_statement()(amount in any::<u64>(), address_index in any::<u32>(), position in any::<(u16, u16, u16)>(), asset_id64 in any::<u64>(), seed_phrase_randomness in any::<[u8; 32]>(), rseed_randomness in any::<[u8; 32]>()) -> (NullifierDerivationProofPublic, NullifierDerivationProofPrivate) {
245 let seed_phrase = SeedPhrase::from_randomness(&seed_phrase_randomness);
246 let sk_sender = SpendKey::from_seed_phrase_bip44(seed_phrase, &Bip44Path::new(0));
247 let fvk_sender = sk_sender.full_viewing_key();
248 let ivk_sender = fvk_sender.incoming();
249 let (sender, _dtk_d) = ivk_sender.payment_address(address_index.into());
250 let nk = *sk_sender.nullifier_key();
251 let note = Note::from_parts(
252 sender,
253 Value {
254 amount: Amount::from(amount),
255 asset_id: asset::Id(Fq::from(asset_id64)),
256 },
257 Rseed(rseed_randomness),
258 ).expect("should be able to create note");
259 let nullifier = Nullifier::derive(&nk, position.into(), ¬e.commit());
260 let public = NullifierDerivationProofPublic {
261 position: position.into(),
262 note_commitment: note.commit(),
263 nullifier
264 };
265 let private = NullifierDerivationProofPrivate {
266 nk,
267 };
268 (public, private)
269 }
270 }
271
272 prop_compose! {
273 fn arb_invalid_nullifier_derivation_statement()(amount in any::<u64>(), address_index in any::<u32>(), position in any::<(u16, u16, u16)>(), invalid_nk_randomness in any::<[u8; 32]>(), asset_id64 in any::<u64>(), seed_phrase_randomness in any::<[u8; 32]>(), rseed_randomness in any::<[u8; 32]>()) -> (NullifierDerivationProofPublic, NullifierDerivationProofPrivate) {
278 let seed_phrase = SeedPhrase::from_randomness(&seed_phrase_randomness);
279 let sk_sender = SpendKey::from_seed_phrase_bip44(seed_phrase, &Bip44Path::new(0));
280 let fvk_sender = sk_sender.full_viewing_key();
281 let ivk_sender = fvk_sender.incoming();
282 let (sender, _dtk_d) = ivk_sender.payment_address(address_index.into());
283 let nk = *sk_sender.nullifier_key();
284 let incorrect_nk = NullifierKey(nk.0 + Fq::from_le_bytes_mod_order(&invalid_nk_randomness));
285 let note = Note::from_parts(
286 sender,
287 Value {
288 amount: Amount::from(amount),
289 asset_id: asset::Id(Fq::from(asset_id64)),
290 },
291 Rseed(rseed_randomness),
292 ).expect("should be able to create note");
293 let nullifier = Nullifier::derive(&nk, position.into(), ¬e.commit());
294
295 let public = NullifierDerivationProofPublic {
296 position: position.into(),
297 note_commitment: note.commit(),
298 nullifier
299 };
300 let private = NullifierDerivationProofPrivate {
301 nk: incorrect_nk,
302 };
303 (public, private)
304 }
305 }
306
307 proptest! {
308 #[test]
309 fn nullifier_derivation_proof_happy_path((public, private) in arb_valid_nullifier_derivation_statement()) {
310 assert!(check_satisfaction(&public, &private).is_ok());
311 assert!(check_circuit_satisfaction(public, private).is_ok());
312 }
313 }
314
315 proptest! {
316 #[test]
317 fn nullifier_derivation_proof_unhappy_path((public, private) in arb_invalid_nullifier_derivation_statement()) {
318 assert!(check_satisfaction(&public, &private).is_err());
319 assert!(check_circuit_satisfaction(public, private).is_err());
320 }
321 }
322}