/ circuit / program / src / request / verify.rs
verify.rs
  1  // Copyright (c) 2019-2025 Alpha-Delta Network Inc.
  2  // This file is part of the deltavm 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  
 16  use super::*;
 17  
 18  impl<A: Alpha> 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 Alpha 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)]
319  mod tests {
320      use super::*;
321      use crate::Circuit;
322      use deltavm_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 = deltavm_console_account::PrivateKey::new(rng)?;
341              let address = deltavm_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.delta")?;
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.delta/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  }