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}