Skip to main content

snarkvm_console_program/request/
verify.rs

1// Copyright (c) 2019-2026 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, is_root, program checksum?, input IDs]`.
61        // Capacity: 5 fixed fields + up to 4 elements per input (record inputs contribute H_x, (rH)_x, gamma_x, tag).
62        let mut message = Vec::with_capacity(5 + 4 * self.input_ids.len());
63        message.push(self.tvk);
64        message.push(self.tcm);
65        message.push(function_id);
66        message.push(is_root);
67
68        // Add the program checksum to the signature message if it was provided.
69        if let Some(program_checksum) = program_checksum {
70            message.push(program_checksum);
71        }
72
73        if let Err(error) = self.input_ids.iter().zip_eq(&self.inputs).zip_eq(input_types).enumerate().try_for_each(
74            |(index, ((input_id, input), input_type))| {
75                // Convert index to u16.
76                let index = u16::try_from(index).or_halt_with::<N>("Input index exceeds u16");
77
78                match input_id {
79                    // A constant input is hashed (using `tcm`) to a field element.
80                    InputID::Constant(input_hash) => {
81                        let candidate = InputID::constant(function_id, input, self.tcm, index)?;
82                        ensure!(*input_hash == *candidate.id(), "Expected a constant input with the same hash");
83                        message.push(*candidate.id());
84                    }
85                    // A public input is hashed (using `tcm`) to a field element.
86                    InputID::Public(input_hash) => {
87                        let candidate = InputID::public(function_id, input, self.tcm, index)?;
88                        ensure!(*input_hash == *candidate.id(), "Expected a public input with the same hash");
89                        message.push(*candidate.id());
90                    }
91                    // A private input is encrypted (using `tvk`) and hashed to a field element.
92                    InputID::Private(input_hash) => {
93                        let candidate = InputID::private(function_id, input, self.tvk, index)?;
94                        ensure!(*input_hash == *candidate.id(), "Expected a private input with the same hash");
95                        message.push(*candidate.id());
96                    }
97                    // A record input is computed to its serial number.
98                    InputID::Record(commitment, gamma, record_view_key, serial_number, tag) => {
99                        // Retrieve the record.
100                        let record = match &input {
101                            Value::Record(record) => record,
102                            Value::Plaintext(..) => bail!("Expected a record input, found a plaintext input"),
103                            Value::Future(..) => bail!("Expected a record input, found a future input"),
104                            Value::DynamicRecord(..) => bail!("Expected a record input, found a dynamic record input"),
105                            Value::DynamicFuture(..) => bail!("Expected a record input, found a dynamic future input"),
106                        };
107                        // Retrieve the record name.
108                        let record_name = match input_type {
109                            ValueType::Record(record_name) => record_name,
110                            _ => bail!("Expected a record type at input {index}"),
111                        };
112                        // Ensure the record belongs to the signer.
113                        ensure!(**record.owner() == self.signer, "Input record does not belong to the signer");
114
115                        // Compute the record commitment.
116                        let candidate_commitment =
117                            record.to_commitment(&self.program_id, record_name, record_view_key)?;
118                        ensure!(
119                            *commitment == candidate_commitment,
120                            "Expected a record input with the same commitment"
121                        );
122
123                        // Compute the candidate serial number from `gamma`.
124                        let candidate_sn = Record::<N, Plaintext<N>>::serial_number_from_gamma(gamma, *commitment)?;
125                        ensure!(*serial_number == candidate_sn, "Expected a record input with the same serial number");
126
127                        // Compute the generator `H` as `HashToGroup(commitment)`.
128                        let h = N::hash_to_group_psd2(&[N::serial_number_domain(), *commitment])?;
129                        // Compute `h_r` as `(challenge * gamma) + (response * H)`, equivalent to `r * H`.
130                        let h_r = (*gamma * challenge) + (h * response);
131
132                        // Compute the tag as `Hash(sk_tag || commitment)`.
133                        let candidate_tag = N::hash_psd2(&[self.sk_tag, *commitment])?;
134                        ensure!(*tag == candidate_tag, "Expected a record input with the same tag");
135
136                        // Add (`H`, `r * H`, `gamma`, `tag`) to the message.
137                        message.extend([h, h_r, *gamma].iter().map(|point| point.to_x_coordinate()));
138                        message.push(*tag);
139                    }
140                    // An external record input is hashed (using `tvk`) to a field element.
141                    InputID::ExternalRecord(input_hash) => {
142                        let candidate = InputID::external_record(function_id, input, self.tvk, index)?;
143                        ensure!(*input_hash == *candidate.id(), "Expected an external record input with the same hash");
144                        message.push(*candidate.id());
145                    }
146                    // A dynamic record input is hashed (using `tvk`) to a field element.
147                    InputID::DynamicRecord(input_hash) => {
148                        let candidate = InputID::dynamic_record(function_id, input, self.tvk, index)?;
149                        ensure!(*input_hash == *candidate.id(), "Expected a dynamic record input with the same hash");
150                        message.push(*candidate.id());
151                    }
152                }
153                Ok(())
154            },
155        ) {
156            eprintln!("Request verification failed on input checks: {error}");
157            return false;
158        }
159
160        // Verify the signature.
161        self.signature.verify(&self.signer, &message)
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168    use snarkvm_console_account::PrivateKey;
169    use snarkvm_console_network::MainnetV0;
170
171    type CurrentNetwork = MainnetV0;
172
173    pub(crate) const ITERATIONS: usize = 1000;
174
175    #[test]
176    fn test_sign_and_verify() {
177        let rng = &mut TestRng::default();
178
179        for _i in 0..ITERATIONS {
180            // Sample a random private key and address.
181            let private_key = PrivateKey::<CurrentNetwork>::new(rng).unwrap();
182            let address = Address::try_from(&private_key).unwrap();
183
184            // Construct a program ID and function name.
185            let program_id = ProgramID::from_str("token.aleo").unwrap();
186            let function_name = Identifier::from_str("transfer").unwrap();
187
188            // Prepare a record belonging to the address.
189            let record_string = format!(
190                "{{ owner: {address}.private, token_amount: 100u64.private, _nonce: 2293253577170800572742339369209137467208538700597121244293392265726446806023group.public }}"
191            );
192
193            // Construct four inputs.
194            let input_constant = Value::from_str("{ token_amount: 9876543210u128 }").unwrap();
195            let input_public = Value::from_str("{ token_amount: 9876543210u128 }").unwrap();
196            let input_private = Value::from_str("{ token_amount: 9876543210u128 }").unwrap();
197            let input_record = Value::from_str(&record_string).unwrap();
198            let input_external_record = Value::from_str(&record_string).unwrap();
199            let inputs = [input_constant, input_public, input_private, input_record, input_external_record];
200
201            // Construct the input types.
202            let input_types = vec![
203                ValueType::from_str("amount.constant").unwrap(),
204                ValueType::from_str("amount.public").unwrap(),
205                ValueType::from_str("amount.private").unwrap(),
206                ValueType::from_str("token.record").unwrap(),
207                ValueType::from_str("token.aleo/token.record").unwrap(),
208            ];
209
210            // Sample 'root_tvk'.
211            let root_tvk = None;
212            // Sample 'is_root'.
213            let is_root = Uniform::rand(rng);
214            // Sample 'program_checksum'.
215            let program_checksum = match bool::rand(rng) {
216                true => Some(Field::rand(rng)),
217                false => None,
218            };
219
220            // Randomly choose whether to sign as static or dynamic.
221            let is_dynamic = bool::rand(rng);
222            // Compute the signed request.
223            let request = Request::sign(
224                &private_key,
225                program_id,
226                function_name,
227                inputs.into_iter(),
228                &input_types,
229                root_tvk,
230                is_root,
231                program_checksum,
232                is_dynamic,
233                rng,
234            )
235            .unwrap();
236            assert!(request.verify(&input_types, is_root, program_checksum));
237        }
238    }
239
240    #[test]
241    fn test_sign_record_as_dynamic_record() {
242        let rng = &mut TestRng::default();
243
244        for _ in 0..ITERATIONS {
245            // Sample a random private key and address.
246            let private_key = PrivateKey::<CurrentNetwork>::new(rng).unwrap();
247            let address = Address::try_from(&private_key).unwrap();
248
249            // Construct a program ID and function name.
250            let program_id = ProgramID::from_str("token.aleo").unwrap();
251            let function_name = Identifier::from_str("transfer").unwrap();
252
253            // Prepare a record belonging to the address.
254            let record_string = format!(
255                "{{ owner: {address}.private, token_amount: 100u64.private, _nonce: 2293253577170800572742339369209137467208538700597121244293392265726446806023group.public }}"
256            );
257
258            // Construct a Value::Record input.
259            let input_record = Value::from_str(&record_string).unwrap();
260            assert!(matches!(input_record, Value::Record(..)));
261            let inputs = [input_record];
262
263            // Declare the input type as DynamicRecord.
264            let input_types = vec![ValueType::DynamicRecord];
265
266            // Sample 'is_root'.
267            let is_root = Uniform::rand(rng);
268            // Sample 'program_checksum'.
269            let program_checksum = match bool::rand(rng) {
270                true => Some(Field::rand(rng)),
271                false => None,
272            };
273
274            // Sign the request — should succeed because Record is implicitly converted to DynamicRecord.
275            let request = Request::sign(
276                &private_key,
277                program_id,
278                function_name,
279                inputs.into_iter(),
280                &input_types,
281                None,
282                is_root,
283                program_checksum,
284                true,
285                rng,
286            )
287            .unwrap();
288
289            // Assert the stored input is Value::DynamicRecord (not Value::Record).
290            assert!(matches!(request.inputs()[0], Value::DynamicRecord(..)));
291
292            // Assert verification passes.
293            assert!(request.verify(&input_types, is_root, program_checksum));
294        }
295    }
296}