snarkvm_circuit_program/request/
verify.rs

1// Copyright 2024-2025 Aleo Network Foundation
2// This file is part of the snarkVM library.
3
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at:
7
8// http://www.apache.org/licenses/LICENSE-2.0
9
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16use super::*;
17
18impl<A: Aleo> Request<A> {
19    /// Returns `true` if the input IDs are derived correctly, the input records all belong to the signer,
20    /// and the signature is valid.
21    ///
22    /// Verifies (challenge == challenge') && (address == address') && (serial_numbers == serial_numbers') where:
23    ///     challenge' := HashToScalar(r * G, pk_sig, pr_sig, signer, \[tvk, tcm, function ID, input IDs\])
24    pub fn verify(
25        &self,
26        input_types: &[console::ValueType<A::Network>],
27        tpk: &Group<A>,
28        root_tvk: Option<Field<A>>,
29        is_root: Boolean<A>,
30    ) -> Boolean<A> {
31        // Compute the function ID.
32        let function_id = compute_function_id(&self.network_id, &self.program_id, &self.function_name);
33
34        // Compute 'is_root' as a field element.
35        let is_root = Ternary::ternary(&is_root, &Field::<A>::one(), &Field::<A>::zero());
36
37        // Construct the signature message as `[tvk, tcm, function ID, input IDs]`.
38        let mut message = Vec::with_capacity(3 + 4 * self.input_ids.len());
39        message.push(self.tvk.clone());
40        message.push(self.tcm.clone());
41        message.push(function_id);
42        message.push(is_root);
43
44        // Check the input IDs and construct the rest of the signature message.
45        let (input_checks, append_to_message) = Self::check_input_ids::<true>(
46            &self.network_id,
47            &self.program_id,
48            &self.function_name,
49            &self.input_ids,
50            &self.inputs,
51            input_types,
52            &self.signer,
53            &self.sk_tag,
54            &self.tvk,
55            &self.tcm,
56            Some(&self.signature),
57        );
58        // Append the input elements to the message.
59        match append_to_message {
60            Some(append_to_message) => message.extend(append_to_message),
61            None => A::halt("Missing input elements in request verification"),
62        }
63
64        let root_tvk = root_tvk.unwrap_or(Field::<A>::new(Mode::Private, self.tvk.eject_value()));
65
66        // Verify the transition public key and commitments are well-formed.
67        let tpk_checks = {
68            // Compute the transition commitment as `Hash(tvk)`.
69            let tcm = A::hash_psd2(&[self.tvk.clone()]);
70            // Compute the signer commitment as `Hash(signer || root_tvk)`.
71            let scm = A::hash_psd2(&[self.signer.to_field(), root_tvk]);
72
73            // Ensure the transition public key matches with the saved one from the signature.
74            tpk.is_equal(&self.to_tpk())
75            // Ensure the computed transition commitment matches.
76            & tcm.is_equal(&self.tcm)
77            // Ensure the computed signer commitment matches.
78            & scm.is_equal(&self.scm)
79        };
80
81        // Verify the signature.
82        // Note: We copy/paste the Aleo signature verification code here in order to compute `tpk` only once.
83        let signature_checks = {
84            // Retrieve pk_sig.
85            let pk_sig = self.signature.compute_key().pk_sig();
86            // Retrieve pr_sig.
87            let pr_sig = self.signature.compute_key().pr_sig();
88
89            // Construct the hash input as (r * G, pk_sig, pr_sig, address, message).
90            let mut preimage = Vec::with_capacity(4 + message.len());
91            preimage.extend([tpk, pk_sig, pr_sig].map(|point| point.to_x_coordinate()));
92            preimage.push(self.signer.to_field());
93            preimage.extend_from_slice(&message);
94
95            // Compute the candidate verifier challenge.
96            let candidate_challenge = A::hash_to_scalar_psd8(&preimage);
97            // Compute the candidate address.
98            let candidate_address = self.signature.compute_key().to_address();
99
100            // Return `true` if the challenge and address is valid.
101            self.signature.challenge().is_equal(&candidate_challenge) & self.signer.is_equal(&candidate_address)
102        };
103
104        // Verify the signature, inputs, and `tpk` are valid.
105        signature_checks & input_checks & tpk_checks
106    }
107
108    /// Returns `true` if the inputs match their input IDs.
109    /// Note: This method does **not** perform signature checks.
110    pub fn check_input_ids<const CREATE_MESSAGE: bool>(
111        network_id: &U16<A>,
112        program_id: &ProgramID<A>,
113        function_name: &Identifier<A>,
114        input_ids: &[InputID<A>],
115        inputs: &[Value<A>],
116        input_types: &[console::ValueType<A::Network>],
117        signer: &Address<A>,
118        sk_tag: &Field<A>,
119        tvk: &Field<A>,
120        tcm: &Field<A>,
121        signature: Option<&Signature<A>>,
122    ) -> (Boolean<A>, Option<Vec<Field<A>>>) {
123        // Ensure the signature response matches the `CREATE_MESSAGE` flag.
124        match CREATE_MESSAGE {
125            true => assert!(signature.is_some()),
126            false => assert!(signature.is_none()),
127        }
128
129        // Compute the function ID.
130        let function_id = compute_function_id(network_id, program_id, function_name);
131
132        // Initialize a vector for a message.
133        let mut message = Vec::new();
134
135        // Perform the input ID checks.
136        let input_checks = input_ids
137            .iter()
138            .zip_eq(inputs)
139            .zip_eq(input_types)
140            .enumerate()
141            .map(|(index, ((input_id, input), input_type))| {
142                match input_id {
143                    // A constant input is hashed (using `tcm`) to a field element.
144                    InputID::Constant(input_hash) => {
145                        // Add the input hash to the message.
146                        if CREATE_MESSAGE {
147                            message.push(input_hash.clone());
148                        }
149
150                        // Prepare the index as a constant field element.
151                        let input_index = Field::constant(console::Field::from_u16(index as u16));
152                        // Construct the preimage as `(function ID || input || tcm || index)`.
153                        let mut preimage = Vec::new();
154                        preimage.push(function_id.clone());
155                        preimage.extend(input.to_fields());
156                        preimage.push(tcm.clone());
157                        preimage.push(input_index);
158
159                        // Ensure the expected hash matches the computed hash.
160                        match &input {
161                            Value::Plaintext(..) => input_hash.is_equal(&A::hash_psd8(&preimage)),
162                            // Ensure the input is not a record or future.
163                            Value::Record(..) => A::halt("Expected a constant plaintext input, found a record input"),
164                            Value::Future(..) => A::halt("Expected a constant plaintext input, found a future input"),
165                        }
166                    }
167                    // A public input is hashed (using `tcm`) to a field element.
168                    InputID::Public(input_hash) => {
169                        // Add the input hash to the message.
170                        if CREATE_MESSAGE {
171                            message.push(input_hash.clone());
172                        }
173
174                        // Prepare the index as a constant field element.
175                        let input_index = Field::constant(console::Field::from_u16(index as u16));
176                        // Construct the preimage as `(function ID || input || tcm || index)`.
177                        let mut preimage = Vec::new();
178                        preimage.push(function_id.clone());
179                        preimage.extend(input.to_fields());
180                        preimage.push(tcm.clone());
181                        preimage.push(input_index);
182
183                        // Ensure the expected hash matches the computed hash.
184                        match &input {
185                            Value::Plaintext(..) => input_hash.is_equal(&A::hash_psd8(&preimage)),
186                            // Ensure the input is not a record or future.
187                            Value::Record(..) => A::halt("Expected a public plaintext input, found a record input"),
188                            Value::Future(..) => A::halt("Expected a public plaintext input, found a future input"),
189                        }
190                    }
191                    // A private input is encrypted (using `tvk`) and hashed to a field element.
192                    InputID::Private(input_hash) => {
193                        // Add the input hash to the message.
194                        if CREATE_MESSAGE {
195                            message.push(input_hash.clone());
196                        }
197
198                        // Prepare the index as a constant field element.
199                        let input_index = Field::constant(console::Field::from_u16(index as u16));
200                        // Compute the input view key as `Hash(function ID || tvk || index)`.
201                        let input_view_key = A::hash_psd4(&[function_id.clone(), tvk.clone(), input_index]);
202                        // Compute the ciphertext.
203                        let ciphertext = match &input {
204                            Value::Plaintext(plaintext) => plaintext.encrypt_symmetric(input_view_key),
205                            // Ensure the input is a plaintext.
206                            Value::Record(..) => A::halt("Expected a private plaintext input, found a record input"),
207                            Value::Future(..) => A::halt("Expected a private plaintext input, found a future input"),
208                        };
209
210                        // Ensure the expected hash matches the computed hash.
211                        input_hash.is_equal(&A::hash_psd8(&ciphertext.to_fields()))
212                    }
213                    // A record input is computed to its serial number.
214                    InputID::Record(commitment, gamma, serial_number, tag) => {
215                        // Retrieve the record.
216                        let record = match &input {
217                            Value::Record(record) => record,
218                            // Ensure the input is a record.
219                            Value::Plaintext(..) => A::halt("Expected a record input, found a plaintext input"),
220                            Value::Future(..) => A::halt("Expected a record input, found a future input"),
221                        };
222                        // Retrieve the record name as a `Mode::Constant`.
223                        let record_name = match input_type {
224                            console::ValueType::Record(record_name) => Identifier::constant(*record_name),
225                            // Ensure the input is a record.
226                            _ => A::halt(format!("Expected a record input at input {index}")),
227                        };
228                        // Compute the record commitment.
229                        let candidate_commitment = record.to_commitment(program_id, &record_name);
230                        // Compute the `candidate_serial_number` from `gamma`.
231                        let candidate_serial_number =
232                            Record::<A, Plaintext<A>>::serial_number_from_gamma(gamma, candidate_commitment.clone());
233                        // Compute the tag.
234                        let candidate_tag =
235                            Record::<A, Plaintext<A>>::tag(sk_tag.clone(), candidate_commitment.clone());
236
237                        if CREATE_MESSAGE {
238                            // Ensure the signature is declared.
239                            let signature = match signature {
240                                Some(signature) => signature,
241                                None => A::halt("Missing signature in logic to check input IDs"),
242                            };
243                            // Retrieve the challenge from the signature.
244                            let challenge = signature.challenge();
245                            // Retrieve the response from the signature.
246                            let response = signature.response();
247
248                            // Compute the generator `H` as `HashToGroup(commitment)`.
249                            let h = A::hash_to_group_psd2(&[A::serial_number_domain(), candidate_commitment.clone()]);
250                            // Compute `h_r` as `(challenge * gamma) + (response * H)`, equivalent to `r * H`.
251                            let h_r = (gamma.deref() * challenge) + (&h * response);
252
253                            // Add (`H`, `r * H`, `gamma`, `tag`) to the message.
254                            message.extend([h, h_r, *gamma.clone()].iter().map(|point| point.to_x_coordinate()));
255                            message.push(candidate_tag.clone());
256                        }
257
258                        // Ensure the candidate serial number matches the expected serial number.
259                        serial_number.is_equal(&candidate_serial_number)
260                            // Ensure the candidate commitment matches the expected commitment.
261                            & commitment.is_equal(&candidate_commitment)
262                            // Ensure the candidate tag matches the expected tag.
263                            & tag.is_equal(&candidate_tag)
264                            // Ensure the record belongs to the signer.
265                            & record.owner().deref().is_equal(signer)
266                    }
267                    // An external record input is hashed (using `tvk`) to a field element.
268                    InputID::ExternalRecord(input_hash) => {
269                        // Add the input hash to the message.
270                        if CREATE_MESSAGE {
271                            message.push(input_hash.clone());
272                        }
273
274                        // Retrieve the record.
275                        let record = match &input {
276                            Value::Record(record) => record,
277                            // Ensure the input is a record.
278                            Value::Plaintext(..) => {
279                                A::halt("Expected an external record input, found a plaintext input")
280                            }
281                            Value::Future(..) => A::halt("Expected an external record input, found a future input"),
282                        };
283
284                        // Prepare the index as a constant field element.
285                        let input_index = Field::constant(console::Field::from_u16(index as u16));
286                        // Construct the preimage as `(function ID || input || tvk || index)`.
287                        let mut preimage = Vec::new();
288                        preimage.push(function_id.clone());
289                        preimage.extend(record.to_fields());
290                        preimage.push(tvk.clone());
291                        preimage.push(input_index);
292
293                        // Ensure the expected hash matches the computed hash.
294                        input_hash.is_equal(&A::hash_psd8(&preimage))
295                    }
296                }
297            })
298            .fold(Boolean::constant(true), |acc, x| acc & x);
299
300        // Return the boolean, and (optional) the message.
301        match CREATE_MESSAGE {
302            true => (input_checks, Some(message)),
303            false => match message.is_empty() {
304                true => (input_checks, None),
305                false => A::halt("Malformed synthesis of the logic to check input IDs"),
306            },
307        }
308    }
309}
310
311#[cfg(all(test, feature = "console"))]
312mod tests {
313    use super::*;
314    use crate::Circuit;
315    use snarkvm_utilities::TestRng;
316
317    use anyhow::Result;
318
319    pub(crate) const ITERATIONS: usize = 50;
320
321    fn check_verify(
322        mode: Mode,
323        num_constants: u64,
324        num_public: u64,
325        num_private: u64,
326        num_constraints: u64,
327    ) -> Result<()> {
328        let rng = &mut TestRng::default();
329
330        for i in 0..ITERATIONS {
331            // Sample a random private key and address.
332            let private_key = snarkvm_console_account::PrivateKey::new(rng)?;
333            let address = snarkvm_console_account::Address::try_from(&private_key).unwrap();
334
335            // Construct a program ID and function name.
336            let program_id = console::ProgramID::from_str("token.aleo")?;
337            let function_name = console::Identifier::from_str("transfer")?;
338
339            // Prepare a record belonging to the address.
340            let record_string =
341                format!("{{ owner: {address}.private, token_amount: 100u64.private, _nonce: 0group.public }}");
342
343            // Construct the inputs.
344            let input_constant =
345                console::Value::<<Circuit as Environment>::Network>::from_str("{ token_amount: 9876543210u128 }")
346                    .unwrap();
347            let input_public =
348                console::Value::<<Circuit as Environment>::Network>::from_str("{ token_amount: 9876543210u128 }")
349                    .unwrap();
350            let input_private =
351                console::Value::<<Circuit as Environment>::Network>::from_str("{ token_amount: 9876543210u128 }")
352                    .unwrap();
353            let input_record = console::Value::<<Circuit as Environment>::Network>::from_str(&record_string).unwrap();
354            let input_external_record =
355                console::Value::<<Circuit as Environment>::Network>::from_str(&record_string).unwrap();
356            let inputs = [input_constant, input_public, input_private, input_record, input_external_record];
357
358            // Construct the input types.
359            let input_types = vec![
360                console::ValueType::from_str("amount.constant").unwrap(),
361                console::ValueType::from_str("amount.public").unwrap(),
362                console::ValueType::from_str("amount.private").unwrap(),
363                console::ValueType::from_str("token.record").unwrap(),
364                console::ValueType::from_str("token.aleo/token.record").unwrap(),
365            ];
366
367            // Sample 'root_tvk'.
368            let root_tvk = None;
369
370            // Sample 'is_root'.
371            let is_root = true;
372
373            // Compute the signed request.
374            let request = console::Request::sign(
375                &private_key,
376                program_id,
377                function_name,
378                inputs.iter(),
379                &input_types,
380                root_tvk,
381                is_root,
382                rng,
383            )?;
384            assert!(request.verify(&input_types, is_root));
385
386            // Inject the request into a circuit.
387            let tpk = Group::<Circuit>::new(mode, request.to_tpk());
388            let request = Request::<Circuit>::new(mode, request);
389            let is_root = Boolean::new(mode, is_root);
390
391            Circuit::scope(format!("Request {i}"), || {
392                let root_tvk = None;
393                let candidate = request.verify(&input_types, &tpk, root_tvk, is_root);
394                assert!(candidate.eject_value());
395                match mode.is_constant() {
396                    true => assert_scope!(<=num_constants, <=num_public, <=num_private, <=num_constraints),
397                    false => assert_scope!(<=num_constants, num_public, num_private, num_constraints),
398                }
399            });
400
401            Circuit::scope(format!("Request {i}"), || {
402                let (candidate, _) = Request::check_input_ids::<false>(
403                    request.network_id(),
404                    request.program_id(),
405                    request.function_name(),
406                    request.input_ids(),
407                    request.inputs(),
408                    &input_types,
409                    request.signer(),
410                    request.sk_tag(),
411                    request.tvk(),
412                    request.tcm(),
413                    None,
414                );
415                assert!(candidate.eject_value());
416            });
417            Circuit::reset();
418        }
419        Ok(())
420    }
421
422    #[test]
423    fn test_sign_and_verify_constant() -> Result<()> {
424        // Note: This is correct. At this (high) level of a program, we override the default mode in the `Record` case,
425        // based on the user-defined visibility in the record type. Thus, we have nonzero private and constraint values.
426        // These bounds are determined experimentally.
427        check_verify(Mode::Constant, 43000, 0, 18000, 18000)
428    }
429
430    #[test]
431    fn test_sign_and_verify_public() -> Result<()> {
432        check_verify(Mode::Public, 40131, 0, 26675, 26702)
433    }
434
435    #[test]
436    fn test_sign_and_verify_private() -> Result<()> {
437        check_verify(Mode::Private, 40131, 0, 26675, 26702)
438    }
439}