liminal_ark_relations/shielder/
withdraw.rs

1use liminal_ark_relation_macro::snark_relation;
2
3/// 'Withdraw' relation for the Shielder application.
4///
5/// It expresses the facts that:
6///  - `new_note` is a prefix of the result of hashing together `token_id`, `whole_token_amount`,
7///    `old_trapdoor` and `old_nullifier`,
8///  - `old_note` is a prefix of the result of hashing together `token_id`, `new_token_amount`,
9///    `new_trapdoor` and `new_nullifier`,
10///  - `new_token_amount + token_amount_out = whole_token_amount`
11///  - `merkle_path` is a valid Merkle proof for `old_note` being present at `leaf_index` in some
12///    Merkle tree with `merkle_root` hash in the root
13/// It also includes two artificial inputs `fee` and `recipient` just to strengthen the application
14/// security by treating them as public inputs (and thus integral part of the SNARK).
15/// Additionally, the relation has one constant input, `max_path_len` which specifies upper bound
16/// for the length of the merkle path (which is ~the height of the tree, ±1).
17#[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 inputs
54        #[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 inputs.
70        #[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        //-----------------------------------------------
92        // Baking `fee` and `recipient` into the circuit.
93        //-----------------------------------------------
94        let _fee = TokenAmountVar::new_input(ns!(cs, "fee"), || self.fee())?;
95        let _recipient = FpVar::new_input(ns!(cs, "recipient"), || self.recipient())?;
96
97        //------------------------------
98        // Check the old note arguments.
99        //------------------------------
100        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        //------------------------------
109        // Check the new note arguments.
110        //------------------------------
111        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        //----------------------------------
120        // Check the token values soundness.
121        //----------------------------------
122        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        //------------------------
128        // Check the merkle proof.
129        //------------------------
130        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        //                                          merkle root
181        //                placeholder                                        x
182        //        1                          x                     x                         x
183        //   2        3                x          x            x       x                 x       x
184        // 4  *5*   6   7            x   x      x   x        x   x   x   x             x   x   x   x
185        let leaf_index = 5;
186
187        let zero_note = FrontendNote::default(); // x
188
189        let sibling_note = compute_note(0, 1, [2; 4], [3; 4]); // 4
190        let parent_note = compute_parent_hash(sibling_note, old_note); // 2
191        let uncle_note = compute_note(4, 5, [6; 4], [7; 4]); // 3
192        let grandpa_root = compute_parent_hash(parent_note, uncle_note); // 1
193
194        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        // We want to take one token more than deposited...
296        circuit.token_amount_out = circuit.whole_token_amount + BackendTokenAmount::one();
297        // ... hence we need to leave in Shielder -1 token ...
298        circuit.new_token_amount = BackendTokenAmount::one().neg();
299        // ... and compute new sneaky note.
300        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        // Without `enforce_cmp` in `TokenAmountVar` this proof is valid!
313        assert!(!valid_proof);
314    }
315}