snarkvm_circuit_program/request/
verify.rs

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