snarkvm_console_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<N: Network> Request<N> {
19    /// Returns `true` if the request is valid, and `false` otherwise.
20    ///
21    /// Verifies (challenge == challenge') && (address == address') && (serial_numbers == serial_numbers') where:
22    ///     challenge' := HashToScalar(r * G, pk_sig, pr_sig, signer, \[tvk, tcm, function ID, is_root, program checksum?, input IDs\])
23    /// The program checksum must be provided if the program has a constructor and should not be provided otherwise.
24    pub fn verify(&self, input_types: &[ValueType<N>], is_root: bool, program_checksum: Option<Field<N>>) -> bool {
25        // Verify the transition public key, transition view key, and transition commitment are well-formed.
26        {
27            // Compute the transition commitment `tcm` as `Hash(tvk)`.
28            match N::hash_psd2(&[self.tvk]) {
29                Ok(tcm) => {
30                    // Ensure the computed transition commitment matches.
31                    if tcm != self.tcm {
32                        eprintln!("Invalid transition commitment in request.");
33                        return false;
34                    }
35                }
36                Err(error) => {
37                    eprintln!("Failed to compute transition commitment in request verification: {error}");
38                    return false;
39                }
40            }
41        }
42
43        // Retrieve the challenge from the signature.
44        let challenge = self.signature.challenge();
45        // Retrieve the response from the signature.
46        let response = self.signature.response();
47
48        // Compute the function ID.
49        let function_id = match compute_function_id(&self.network_id, &self.program_id, &self.function_name) {
50            Ok(function_id) => function_id,
51            Err(error) => {
52                eprintln!("Failed to construct the function ID: {error}");
53                return false;
54            }
55        };
56
57        // Compute the 'is_root' field.
58        let is_root = if is_root { Field::<N>::one() } else { Field::<N>::zero() };
59
60        // Construct the signature message as `[tvk, tcm, function ID, input IDs]`.
61        let mut message = Vec::with_capacity(3 + self.input_ids.len());
62        message.push(self.tvk);
63        message.push(self.tcm);
64        message.push(function_id);
65        message.push(is_root);
66        // Add the program checksum to the signature message if it was provided.
67        if let Some(program_checksum) = program_checksum {
68            message.push(program_checksum);
69        }
70
71        if let Err(error) = self.input_ids.iter().zip_eq(&self.inputs).zip_eq(input_types).enumerate().try_for_each(
72            |(index, ((input_id, input), input_type))| {
73                match input_id {
74                    // A constant input is hashed (using `tcm`) to a field element.
75                    InputID::Constant(input_hash) => {
76                        // Ensure the input is a plaintext.
77                        ensure!(matches!(input, Value::Plaintext(..)), "Expected a plaintext input");
78
79                        // Construct the (console) input index as a field element.
80                        let index = Field::from_u16(u16::try_from(index).or_halt_with::<N>("Input index exceeds u16"));
81                        // Construct the preimage as `(function ID || input || tcm || index)`.
82                        let mut preimage = Vec::new();
83                        preimage.push(function_id);
84                        preimage.extend(input.to_fields()?);
85                        preimage.push(self.tcm);
86                        preimage.push(index);
87                        // Hash the input to a field element.
88                        let candidate_hash = N::hash_psd8(&preimage)?;
89                        // Ensure the input hash matches.
90                        ensure!(*input_hash == candidate_hash, "Expected a constant input with the same hash");
91
92                        // Add the input hash to the message.
93                        message.push(candidate_hash);
94                    }
95                    // A public input is hashed (using `tcm`) to a field element.
96                    InputID::Public(input_hash) => {
97                        // Ensure the input is a plaintext.
98                        ensure!(matches!(input, Value::Plaintext(..)), "Expected a plaintext input");
99
100                        // Construct the (console) input index as a field element.
101                        let index = Field::from_u16(u16::try_from(index).or_halt_with::<N>("Input index exceeds u16"));
102                        // Construct the preimage as `(function ID || input || tcm || index)`.
103                        let mut preimage = Vec::new();
104                        preimage.push(function_id);
105                        preimage.extend(input.to_fields()?);
106                        preimage.push(self.tcm);
107                        preimage.push(index);
108                        // Hash the input to a field element.
109                        let candidate_hash = N::hash_psd8(&preimage)?;
110                        // Ensure the input hash matches.
111                        ensure!(*input_hash == candidate_hash, "Expected a public input with the same hash");
112
113                        // Add the input hash to the message.
114                        message.push(candidate_hash);
115                    }
116                    // A private input is encrypted (using `tvk`) and hashed to a field element.
117                    InputID::Private(input_hash) => {
118                        // Ensure the input is a plaintext.
119                        ensure!(matches!(input, Value::Plaintext(..)), "Expected a plaintext input");
120
121                        // Construct the (console) input index as a field element.
122                        let index = Field::from_u16(u16::try_from(index).or_halt_with::<N>("Input index exceeds u16"));
123                        // Compute the input view key as `Hash(function ID || tvk || index)`.
124                        let input_view_key = N::hash_psd4(&[function_id, self.tvk, index])?;
125                        // Compute the ciphertext.
126                        let ciphertext = match &input {
127                            Value::Plaintext(plaintext) => plaintext.encrypt_symmetric(input_view_key)?,
128                            // Ensure the input is a plaintext.
129                            Value::Record(..) => bail!("Expected a plaintext input, found a record input"),
130                            Value::Future(..) => bail!("Expected a plaintext input, found a future input"),
131                        };
132                        // Hash the ciphertext to a field element.
133                        let candidate_hash = N::hash_psd8(&ciphertext.to_fields()?)?;
134                        // Ensure the input hash matches.
135                        ensure!(*input_hash == candidate_hash, "Expected a private input with the same hash");
136
137                        // Add the input hash to the message.
138                        message.push(candidate_hash);
139                    }
140                    // A record input is computed to its serial number.
141                    InputID::Record(commitment, gamma, record_view_key, serial_number, tag) => {
142                        // Retrieve the record.
143                        let record = match &input {
144                            Value::Record(record) => record,
145                            // Ensure the input is a record.
146                            Value::Plaintext(..) => bail!("Expected a record input, found a plaintext input"),
147                            Value::Future(..) => bail!("Expected a record input, found a future input"),
148                        };
149                        // Retrieve the record name.
150                        let record_name = match input_type {
151                            ValueType::Record(record_name) => record_name,
152                            // Ensure the input type is a record.
153                            _ => bail!("Expected a record type at input {index}"),
154                        };
155                        // Ensure the record belongs to the signer.
156                        ensure!(**record.owner() == self.signer, "Input record does not belong to the signer");
157
158                        // Compute the record commitment.
159                        let candidate_commitment =
160                            record.to_commitment(&self.program_id, record_name, record_view_key)?;
161                        // Ensure the commitment matches.
162                        ensure!(
163                            *commitment == candidate_commitment,
164                            "Expected a record input with the same commitment"
165                        );
166
167                        // Compute the `candidate_sn` from `gamma`.
168                        let candidate_sn = Record::<N, Plaintext<N>>::serial_number_from_gamma(gamma, *commitment)?;
169                        // Ensure the serial number matches.
170                        ensure!(*serial_number == candidate_sn, "Expected a record input with the same serial number");
171
172                        // Compute the generator `H` as `HashToGroup(commitment)`.
173                        let h = N::hash_to_group_psd2(&[N::serial_number_domain(), *commitment])?;
174                        // Compute `h_r` as `(challenge * gamma) + (response * H)`, equivalent to `r * H`.
175                        let h_r = (*gamma * challenge) + (h * response);
176
177                        // Compute the tag as `Hash(sk_tag || commitment)`.
178                        let candidate_tag = N::hash_psd2(&[self.sk_tag, *commitment])?;
179                        // Ensure the tag matches.
180                        ensure!(*tag == candidate_tag, "Expected a record input with the same tag");
181
182                        // Add (`H`, `r * H`, `gamma`, `tag`) to the message.
183                        message.extend([h, h_r, *gamma].iter().map(|point| point.to_x_coordinate()));
184                        message.push(*tag);
185                    }
186                    // An external record input is hashed (using `tvk`) to a field element.
187                    InputID::ExternalRecord(input_hash) => {
188                        // Ensure the input is a record.
189                        ensure!(matches!(input, Value::Record(..)), "Expected a record input");
190
191                        // Construct the (console) input index as a field element.
192                        let index = Field::from_u16(u16::try_from(index).or_halt_with::<N>("Input index exceeds u16"));
193                        // Construct the preimage as `(function ID || input || tvk || index)`.
194                        let mut preimage = Vec::new();
195                        preimage.push(function_id);
196                        preimage.extend(input.to_fields()?);
197                        preimage.push(self.tvk);
198                        preimage.push(index);
199                        // Hash the input to a field element.
200                        let candidate_hash = N::hash_psd8(&preimage)?;
201                        // Ensure the input hash matches.
202                        ensure!(*input_hash == candidate_hash, "Expected a locator input with the same hash");
203
204                        // Add the input hash to the message.
205                        message.push(candidate_hash);
206                    }
207                }
208                Ok(())
209            },
210        ) {
211            eprintln!("Request verification failed on input checks: {error}");
212            return false;
213        }
214
215        // Verify the signature.
216        self.signature.verify(&self.signer, &message)
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223    use snarkvm_console_account::PrivateKey;
224    use snarkvm_console_network::MainnetV0;
225
226    type CurrentNetwork = MainnetV0;
227
228    pub(crate) const ITERATIONS: usize = 1000;
229
230    #[test]
231    fn test_sign_and_verify() {
232        let rng = &mut TestRng::default();
233
234        for i in 0..ITERATIONS {
235            // Sample a random private key and address.
236            let private_key = PrivateKey::<CurrentNetwork>::new(rng).unwrap();
237            let address = Address::try_from(&private_key).unwrap();
238
239            // Construct a program ID and function name.
240            let program_id = ProgramID::from_str("token.aleo").unwrap();
241            let function_name = Identifier::from_str("transfer").unwrap();
242
243            // Prepare a record belonging to the address.
244            let record_string = format!(
245                "{{ owner: {address}.private, token_amount: 100u64.private, _nonce: 2293253577170800572742339369209137467208538700597121244293392265726446806023group.public }}"
246            );
247
248            // Construct four inputs.
249            let input_constant = Value::from_str("{ token_amount: 9876543210u128 }").unwrap();
250            let input_public = Value::from_str("{ token_amount: 9876543210u128 }").unwrap();
251            let input_private = Value::from_str("{ token_amount: 9876543210u128 }").unwrap();
252            let input_record = Value::from_str(&record_string).unwrap();
253            let input_external_record = Value::from_str(&record_string).unwrap();
254            let inputs = [input_constant, input_public, input_private, input_record, input_external_record];
255
256            // Construct the input types.
257            let input_types = vec![
258                ValueType::from_str("amount.constant").unwrap(),
259                ValueType::from_str("amount.public").unwrap(),
260                ValueType::from_str("amount.private").unwrap(),
261                ValueType::from_str("token.record").unwrap(),
262                ValueType::from_str("token.aleo/token.record").unwrap(),
263            ];
264
265            // Sample 'root_tvk'.
266            let root_tvk = None;
267            // Sample 'is_root'.
268            let is_root = Uniform::rand(rng);
269            // Sample 'program_checksum'.
270            let program_checksum = match i % 2 == 0 {
271                true => Some(Field::rand(rng)),
272                false => None,
273            };
274
275            // Compute the signed request.
276            let request = Request::sign(
277                &private_key,
278                program_id,
279                function_name,
280                inputs.into_iter(),
281                &input_types,
282                root_tvk,
283                is_root,
284                program_checksum,
285                rng,
286            )
287            .unwrap();
288            assert!(request.verify(&input_types, is_root, program_checksum));
289        }
290    }
291}