1use liminal_ark_relation_macro::snark_relation;
2
3#[snark_relation]
18mod relation {
19 #[cfg(feature = "circuit")]
20 use {
21 crate::shielder::{
22 check_merkle_proof, note_var::NoteVarBuilder, path_shape_var::PathShapeVar,
23 token_amount_var::TokenAmountVar,
24 },
25 ark_r1cs_std::{
26 alloc::{
27 AllocVar,
28 AllocationMode::{Input, Witness},
29 },
30 eq::EqGadget,
31 fields::fp::FpVar,
32 },
33 ark_relations::ns,
34 core::ops::Add,
35 };
36
37 use crate::shielder::{
38 convert_account, convert_hash, convert_vec,
39 types::{
40 BackendAccount, BackendLeafIndex, BackendMerklePath, BackendMerkleRoot, BackendNote,
41 BackendNullifier, BackendTokenAmount, BackendTokenId, BackendTrapdoor, FrontendAccount,
42 FrontendLeafIndex, FrontendMerklePath, FrontendMerkleRoot, FrontendNote,
43 FrontendNullifier, FrontendTokenAmount, FrontendTokenId, FrontendTrapdoor,
44 },
45 };
46
47 #[relation_object_definition]
48 #[derive(Clone, Debug)]
49 struct WithdrawRelation {
50 #[constant]
51 pub max_path_len: u8,
52
53 #[public_input(frontend_type = "FrontendTokenAmount")]
55 pub fee: BackendTokenAmount,
56 #[public_input(frontend_type = "FrontendAccount", parse_with = "convert_account")]
57 pub recipient: BackendAccount,
58 #[public_input(frontend_type = "FrontendTokenId")]
59 pub token_id: BackendTokenId,
60 #[public_input(frontend_type = "FrontendNullifier", parse_with = "convert_hash")]
61 pub old_nullifier: BackendNullifier,
62 #[public_input(frontend_type = "FrontendNote", parse_with = "convert_hash")]
63 pub new_note: BackendNote,
64 #[public_input(frontend_type = "FrontendTokenAmount")]
65 pub token_amount_out: BackendTokenAmount,
66 #[public_input(frontend_type = "FrontendMerkleRoot", parse_with = "convert_hash")]
67 pub merkle_root: BackendMerkleRoot,
68
69 #[private_input(frontend_type = "FrontendTrapdoor", parse_with = "convert_hash")]
71 pub old_trapdoor: BackendTrapdoor,
72 #[private_input(frontend_type = "FrontendTrapdoor", parse_with = "convert_hash")]
73 pub new_trapdoor: BackendTrapdoor,
74 #[private_input(frontend_type = "FrontendNullifier", parse_with = "convert_hash")]
75 pub new_nullifier: BackendNullifier,
76 #[private_input(frontend_type = "FrontendMerklePath", parse_with = "convert_vec")]
77 pub merkle_path: BackendMerklePath,
78 #[private_input(frontend_type = "FrontendLeafIndex")]
79 pub leaf_index: BackendLeafIndex,
80 #[private_input(frontend_type = "FrontendNote", parse_with = "convert_hash")]
81 pub old_note: BackendNote,
82 #[private_input(frontend_type = "FrontendTokenAmount")]
83 pub whole_token_amount: BackendTokenAmount,
84 #[private_input(frontend_type = "FrontendTokenAmount")]
85 pub new_token_amount: BackendTokenAmount,
86 }
87
88 #[cfg(feature = "circuit")]
89 #[circuit_definition]
90 fn generate_constraints() {
91 let _fee = TokenAmountVar::new_input(ns!(cs, "fee"), || self.fee())?;
95 let _recipient = FpVar::new_input(ns!(cs, "recipient"), || self.recipient())?;
96
97 let old_note = NoteVarBuilder::new(cs.clone())
101 .with_note(self.old_note(), Witness)?
102 .with_token_id(self.token_id(), Input)?
103 .with_token_amount(self.whole_token_amount(), Witness)?
104 .with_trapdoor(self.old_trapdoor(), Witness)?
105 .with_nullifier(self.old_nullifier(), Input)?
106 .build()?;
107
108 let new_note = NoteVarBuilder::new(cs.clone())
112 .with_token_id_var(old_note.token_id.clone())
113 .with_note(self.new_note(), Input)?
114 .with_token_amount(self.new_token_amount(), Witness)?
115 .with_trapdoor(self.new_trapdoor(), Witness)?
116 .with_nullifier(self.new_nullifier(), Witness)?
117 .build()?;
118
119 let token_amount_out =
123 TokenAmountVar::new_input(ns!(cs, "token amount out"), || self.token_amount_out())?;
124 let token_sum = token_amount_out.add(new_note.token_amount)?;
125 token_sum.enforce_equal(&old_note.token_amount)?;
126
127 let merkle_root = FpVar::new_input(ns!(cs, "merkle root"), || self.merkle_root())?;
131 let path_shape = PathShapeVar::new_witness(ns!(cs, "path shape"), || {
132 Ok((*self.max_path_len(), self.leaf_index().cloned()))
133 })?;
134
135 check_merkle_proof(
136 merkle_root,
137 path_shape,
138 old_note.note,
139 self.merkle_path().cloned().unwrap_or_default(),
140 *self.max_path_len(),
141 cs,
142 )
143 }
144}
145
146#[cfg(all(test, feature = "circuit"))]
147mod tests {
148 use std::ops::Neg;
149
150 use ark_bls12_381::Bls12_381;
151 use ark_ff::One;
152 use ark_groth16::Groth16;
153 use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystem};
154 use ark_snark::SNARK;
155
156 use super::*;
157 use crate::shielder::{
158 note::{compute_note, compute_parent_hash},
159 types::{BackendNote, BackendTokenAmount, FrontendAccount},
160 };
161
162 const MAX_PATH_LEN: u8 = 4;
163
164 fn get_circuit_with_full_input() -> WithdrawRelationWithFullInput {
165 let token_id: FrontendTokenId = 1;
166
167 let old_trapdoor: FrontendTrapdoor = [17; 4];
168 let old_nullifier: FrontendNullifier = [19; 4];
169 let whole_token_amount: FrontendTokenAmount = 10;
170
171 let new_trapdoor: FrontendTrapdoor = [27; 4];
172 let new_nullifier: FrontendNullifier = [87; 4];
173 let new_token_amount: FrontendTokenAmount = 3;
174
175 let token_amount_out: FrontendTokenAmount = 7;
176
177 let old_note = compute_note(token_id, whole_token_amount, old_trapdoor, old_nullifier);
178 let new_note = compute_note(token_id, new_token_amount, new_trapdoor, new_nullifier);
179
180 let leaf_index = 5;
186
187 let zero_note = FrontendNote::default(); let sibling_note = compute_note(0, 1, [2; 4], [3; 4]); let parent_note = compute_parent_hash(sibling_note, old_note); let uncle_note = compute_note(4, 5, [6; 4], [7; 4]); let grandpa_root = compute_parent_hash(parent_note, uncle_note); let placeholder = compute_parent_hash(grandpa_root, zero_note);
195 let merkle_root = compute_parent_hash(placeholder, zero_note);
196
197 let merkle_path = vec![sibling_note, uncle_note];
198
199 let fee: FrontendTokenAmount = 1;
200 let recipient: FrontendAccount = [
201 212, 53, 147, 199, 21, 253, 211, 28, 97, 20, 26, 189, 4, 169, 159, 214, 130, 44, 133,
202 88, 133, 76, 205, 227, 154, 86, 132, 231, 165, 109, 162, 125,
203 ];
204
205 WithdrawRelationWithFullInput::new(
206 MAX_PATH_LEN,
207 fee,
208 recipient,
209 token_id,
210 old_nullifier,
211 new_note,
212 token_amount_out,
213 merkle_root,
214 old_trapdoor,
215 new_trapdoor,
216 new_nullifier,
217 merkle_path,
218 leaf_index,
219 old_note,
220 whole_token_amount,
221 new_token_amount,
222 )
223 }
224
225 #[test]
226 fn withdraw_constraints_correctness() {
227 let circuit = get_circuit_with_full_input();
228
229 let cs = ConstraintSystem::new_ref();
230 circuit.generate_constraints(cs.clone()).unwrap();
231
232 let is_satisfied = cs.is_satisfied().unwrap();
233 if !is_satisfied {
234 println!("{:?}", cs.which_is_unsatisfied());
235 }
236
237 assert!(is_satisfied);
238 }
239
240 #[test]
241 fn withdraw_proving_procedure() {
242 let circuit_without_input = WithdrawRelationWithoutInput::new(MAX_PATH_LEN);
243
244 let mut rng = ark_std::test_rng();
245 let (pk, vk) =
246 Groth16::<Bls12_381>::circuit_specific_setup(circuit_without_input, &mut rng).unwrap();
247
248 let circuit = get_circuit_with_full_input();
249 let proof = Groth16::prove(&pk, circuit, &mut rng).unwrap();
250
251 let circuit = WithdrawRelationWithPublicInput::from(get_circuit_with_full_input());
252 let input = circuit.serialize_public_input();
253 let valid_proof = Groth16::verify(&vk, &input, &proof).unwrap();
254 assert!(valid_proof);
255 }
256
257 #[test]
258 fn neither_fee_nor_recipient_are_simplified_out() {
259 let circuit_without_input = WithdrawRelationWithoutInput::new(MAX_PATH_LEN);
260
261 let mut rng = ark_std::test_rng();
262 let (pk, vk) =
263 Groth16::<Bls12_381>::circuit_specific_setup(circuit_without_input, &mut rng).unwrap();
264
265 let circuit = get_circuit_with_full_input();
266 let proof = Groth16::prove(&pk, circuit, &mut rng).unwrap();
267
268 let circuit: WithdrawRelationWithPublicInput = get_circuit_with_full_input().into();
269 let true_input = circuit.serialize_public_input();
270 let mut input_with_corrupted_fee = true_input.clone();
271 input_with_corrupted_fee[0] = BackendTokenAmount::from(2);
272 assert_ne!(true_input[0], input_with_corrupted_fee[0]);
273
274 let valid_proof = Groth16::verify(&vk, &input_with_corrupted_fee, &proof).unwrap();
275 assert!(!valid_proof);
276
277 let mut input_with_corrupted_recipient = true_input.clone();
278 let fake_recipient = [41; 32];
279 input_with_corrupted_recipient[1] = convert_account(fake_recipient);
280 assert_ne!(true_input[1], input_with_corrupted_recipient[1]);
281
282 let valid_proof = Groth16::verify(&vk, &input_with_corrupted_recipient, &proof).unwrap();
283 assert!(!valid_proof);
284 }
285
286 #[test]
287 fn cannot_create_sneaky_note() {
288 let circuit_without_input = WithdrawRelationWithoutInput::new(MAX_PATH_LEN);
289
290 let mut rng = ark_std::test_rng();
291 let (pk, vk) =
292 Groth16::<Bls12_381>::circuit_specific_setup(circuit_without_input, &mut rng).unwrap();
293
294 let mut circuit = get_circuit_with_full_input();
295 circuit.token_amount_out = circuit.whole_token_amount + BackendTokenAmount::one();
297 circuit.new_token_amount = BackendTokenAmount::one().neg();
299 circuit.new_note = BackendNote::new(ark_ff::BigInteger256([
301 875544533870975309,
302 17340113879898921273,
303 17290319916917063854,
304 4489249721891001805,
305 ]));
306
307 let proof = Groth16::prove(&pk, circuit.clone(), &mut rng).unwrap();
308
309 let circuit = WithdrawRelationWithPublicInput::from(circuit);
310 let input = circuit.serialize_public_input();
311 let valid_proof = Groth16::verify(&vk, &input, &proof).unwrap();
312 assert!(!valid_proof);
314 }
315}