Skip to main content

snarkvm_synthesizer_program/finalize/
mod.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 crate::Command;
17
18mod input;
19use input::*;
20
21mod bytes;
22mod parse;
23
24use console::{
25    network::prelude::*,
26    program::{FinalizeType, Identifier, Register},
27};
28
29use indexmap::IndexSet;
30use std::collections::HashMap;
31
32#[derive(Clone, PartialEq, Eq)]
33pub struct FinalizeCore<N: Network> {
34    /// The name of the associated function.
35    name: Identifier<N>,
36    /// The input statements, added in order of the input registers.
37    /// Input assignments are ensured to match the ordering of the input statements.
38    inputs: IndexSet<Input<N>>,
39    /// The commands, in order of execution.
40    commands: Vec<Command<N>>,
41    /// The number of write commands.
42    num_writes: u16,
43    /// The number of `call` commands (view calls).
44    num_calls: u16,
45    /// A mapping from `Position`s to their index in `commands`.
46    positions: HashMap<Identifier<N>, usize>,
47}
48
49impl<N: Network> FinalizeCore<N> {
50    /// Initializes a new finalize with the given name.
51    pub fn new(name: Identifier<N>) -> Self {
52        Self {
53            name,
54            inputs: IndexSet::new(),
55            commands: Vec::new(),
56            num_writes: 0,
57            num_calls: 0,
58            positions: HashMap::new(),
59        }
60    }
61
62    /// Returns the name of the associated function.
63    pub const fn name(&self) -> &Identifier<N> {
64        &self.name
65    }
66
67    /// Returns the finalize inputs.
68    pub const fn inputs(&self) -> &IndexSet<Input<N>> {
69        &self.inputs
70    }
71
72    /// Returns the finalize input types.
73    pub fn input_types(&self) -> Vec<FinalizeType<N>> {
74        self.inputs.iter().map(|input| input.finalize_type()).cloned().collect()
75    }
76
77    /// Returns the finalize commands.
78    pub fn commands(&self) -> &[Command<N>] {
79        &self.commands
80    }
81
82    /// Returns the number of write commands.
83    pub const fn num_writes(&self) -> u16 {
84        self.num_writes
85    }
86
87    /// Returns the mapping of `Position`s to their index in `commands`.
88    pub const fn positions(&self) -> &HashMap<Identifier<N>, usize> {
89        &self.positions
90    }
91
92    pub fn contains_external_struct(&self) -> bool {
93        self.commands
94            .iter()
95            .any(|command| matches!(command, Command::Instruction(inst) if inst.contains_external_struct()))
96    }
97
98    /// Returns `true` if the finalize scope contains a string type.
99    pub fn contains_string_type(&self) -> bool {
100        self.input_types().iter().any(|input_type| {
101            matches!(input_type, FinalizeType::Plaintext(plaintext_type) if plaintext_type.contains_string_type())
102        }) || self.commands.iter().any(|command| {
103            command.contains_string_type()
104        })
105    }
106
107    /// Returns `true` if the finalize scope contains an identifier type in its inputs or commands.
108    pub fn contains_identifier_type(&self) -> Result<bool> {
109        for input_type in self.input_types() {
110            if let FinalizeType::Plaintext(plaintext_type) = input_type {
111                if plaintext_type.contains_identifier_type()? {
112                    return Ok(true);
113                }
114            }
115        }
116        // Check commands for identifier types in cast destinations.
117        for command in &self.commands {
118            if command.contains_identifier_type()? {
119                return Ok(true);
120            }
121        }
122        Ok(false)
123    }
124
125    /// Returns `true` if the finalize scope contains an array type with a size that exceeds the given maximum.
126    pub fn exceeds_max_array_size(&self, max_array_size: u32) -> bool {
127        self.input_types().iter().any(|input_type| {
128            matches!(input_type, FinalizeType::Plaintext(plaintext_type) if plaintext_type.exceeds_max_array_size(max_array_size))
129        }) || self.commands.iter().any(|command| {
130            command.exceeds_max_array_size(max_array_size)
131        })
132    }
133}
134
135impl<N: Network> FinalizeCore<N> {
136    /// Adds the input statement to finalize.
137    ///
138    /// # Errors
139    /// This method will halt if a command was previously added.
140    /// This method will halt if the maximum number of inputs has been reached.
141    /// This method will halt if the input statement was previously added.
142    #[inline]
143    fn add_input(&mut self, input: Input<N>) -> Result<()> {
144        // Ensure there are no commands in memory.
145        ensure!(self.commands.is_empty(), "Cannot add inputs after commands have been added");
146
147        // Ensure the maximum number of inputs has not been exceeded.
148        ensure!(self.inputs.len() < N::MAX_INPUTS, "Cannot add more than {} inputs", N::MAX_INPUTS);
149        // Ensure the input statement was not previously added.
150        ensure!(!self.inputs.contains(&input), "Cannot add duplicate input statement");
151
152        // Ensure the input register is a locator.
153        ensure!(matches!(input.register(), Register::Locator(..)), "Input register must be a locator");
154
155        // Insert the input statement.
156        self.inputs.insert(input);
157        Ok(())
158    }
159
160    /// Adds the given command to finalize.
161    ///
162    /// # Errors
163    /// This method will halt if the maximum number of commands has been reached.
164    #[inline]
165    pub fn add_command(&mut self, command: Command<N>) -> Result<()> {
166        // Ensure the maximum number of commands has not been exceeded.
167        ensure!(self.commands.len() < N::MAX_COMMANDS, "Cannot add more than {} commands", N::MAX_COMMANDS);
168        // Ensure the number of write commands has not been exceeded.
169        if command.is_write() {
170            ensure!(
171                self.num_writes < N::LATEST_MAX_WRITES(),
172                "Cannot add more than {} 'set' & 'remove' commands",
173                N::LATEST_MAX_WRITES()
174            );
175        }
176
177        // Ensure the command is not an async instruction.
178        ensure!(!command.is_async(), "Forbidden operation: Finalize cannot invoke an 'async' instruction");
179        // Allow `call` only when the target resolves to a `view` function (enforced later by the
180        // type-check at `Stack::new`). `call.dynamic` remains forbidden because we have not yet
181        // designed a way to track dynamic spend / gas usage for runtime-resolved targets.
182        ensure!(!command.is_dynamic_call(), "Forbidden operation: Finalize cannot invoke a 'call.dynamic'");
183        // Bound the number of view-calls per finalize body. This is a structural cap mirrored
184        // on `Transaction::MAX_TRANSITIONS` — without it, a finalize could chain up to
185        // `MAX_COMMANDS` calls, each into a view whose own body has up to `MAX_COMMANDS`
186        // commands, giving `O(MAX_COMMANDS^2)` worst-case work. `TRANSACTION_SPEND_LIMIT` still
187        // bounds it economically, but this gives a tight structural bound on top.
188        if command.is_call() {
189            ensure!(
190                (self.num_calls as usize) < N::MAX_CALLS,
191                "Cannot add more than {} 'call' commands in a finalize body",
192                N::MAX_CALLS
193            );
194        }
195        // Ensure the command does not operate on a record (cast-to-record or `get.record.dynamic`).
196        ensure!(!command.is_instruction_for_record(), "Forbidden operation: Finalize cannot operate on records");
197
198        // Check the destination registers.
199        for register in command.destinations() {
200            // Ensure the destination register is a locator.
201            ensure!(matches!(register, Register::Locator(..)), "Destination register must be a locator");
202        }
203
204        // Check if the command is a branch command.
205        if let Some(position) = command.branch_to() {
206            // Ensure the branch target does not reference an earlier position.
207            ensure!(!self.positions.contains_key(position), "Cannot branch to an earlier position '{position}'");
208        }
209
210        // Check if the command is a position command.
211        if let Some(position) = command.position() {
212            // Ensure the position is not yet defined.
213            ensure!(!self.positions.contains_key(position), "Cannot redefine position '{position}'");
214            // Ensure that there are less than `u8::MAX` positions.
215            ensure!(self.positions.len() < N::MAX_POSITIONS, "Cannot add more than {} positions", N::MAX_POSITIONS);
216            // Insert the position.
217            self.positions.insert(*position, self.commands.len());
218        }
219
220        // Check if the command is a write command.
221        if command.is_write() {
222            // Increment the number of write commands.
223            self.num_writes += 1;
224        }
225        // Track the number of view-calls. `is_dynamic_call` is already rejected above, so this
226        // counts only static `Call`s.
227        if command.is_call() {
228            self.num_calls += 1;
229        }
230
231        // Insert the command.
232        self.commands.push(command);
233        Ok(())
234    }
235}
236
237impl<N: Network> TypeName for FinalizeCore<N> {
238    /// Returns the type name as a string.
239    #[inline]
240    fn type_name() -> &'static str {
241        "finalize"
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248
249    use crate::{Command, Finalize};
250
251    type CurrentNetwork = console::network::MainnetV0;
252
253    #[test]
254    fn test_add_input() {
255        // Initialize a new finalize instance.
256        let name = Identifier::from_str("finalize_core_test").unwrap();
257        let mut finalize = Finalize::<CurrentNetwork>::new(name);
258
259        // Ensure that an input can be added.
260        let input = Input::<CurrentNetwork>::from_str("input r0 as field.public;").unwrap();
261        assert!(finalize.add_input(input.clone()).is_ok());
262
263        // Ensure that adding a duplicate input will fail.
264        assert!(finalize.add_input(input).is_err());
265
266        // Ensure that adding more than the maximum number of inputs will fail.
267        for i in 1..CurrentNetwork::MAX_INPUTS * 2 {
268            let input = Input::<CurrentNetwork>::from_str(&format!("input r{i} as field.public;")).unwrap();
269
270            match finalize.inputs.len() < CurrentNetwork::MAX_INPUTS {
271                true => assert!(finalize.add_input(input).is_ok()),
272                false => assert!(finalize.add_input(input).is_err()),
273            }
274        }
275    }
276
277    #[test]
278    fn test_add_command() {
279        // Initialize a new finalize instance.
280        let name = Identifier::from_str("finalize_core_test").unwrap();
281        let mut finalize = Finalize::<CurrentNetwork>::new(name);
282
283        // Ensure that a command can be added.
284        let command = Command::<CurrentNetwork>::from_str("add r0 r1 into r2;").unwrap();
285        assert!(finalize.add_command(command).is_ok());
286
287        // Ensure that adding more than the maximum number of commands will fail.
288        for i in 3..CurrentNetwork::MAX_COMMANDS * 2 {
289            let command = Command::<CurrentNetwork>::from_str(&format!("add r0 r1 into r{i};")).unwrap();
290
291            match finalize.commands.len() < CurrentNetwork::MAX_COMMANDS {
292                true => assert!(finalize.add_command(command).is_ok()),
293                false => assert!(finalize.add_command(command).is_err()),
294            }
295        }
296
297        // Ensure that adding more than the maximum number of writes will fail.
298
299        // Initialize a new finalize instance.
300        let name = Identifier::from_str("finalize_core_test").unwrap();
301        let mut finalize = Finalize::<CurrentNetwork>::new(name);
302
303        for _ in 0..CurrentNetwork::LATEST_MAX_WRITES() * 2 {
304            let command = Command::<CurrentNetwork>::from_str("remove object[r0];").unwrap();
305
306            match finalize.commands.len() < CurrentNetwork::LATEST_MAX_WRITES() as usize {
307                true => assert!(finalize.add_command(command).is_ok()),
308                false => assert!(finalize.add_command(command).is_err()),
309            }
310        }
311    }
312
313    #[test]
314    fn test_add_command_duplicate_positions() {
315        // Initialize a new finalize instance.
316        let name = Identifier::from_str("finalize_core_test").unwrap();
317        let mut finalize = Finalize::<CurrentNetwork>::new(name);
318
319        // Ensure that a command can be added.
320        let command = Command::<CurrentNetwork>::from_str("position start;").unwrap();
321        assert!(finalize.add_command(command.clone()).is_ok());
322
323        // Ensure that adding a duplicate position will fail.
324        assert!(finalize.add_command(command).is_err());
325
326        // Helper method to convert a number to a unique string.
327        #[allow(clippy::cast_possible_truncation)]
328        fn to_unique_string(mut n: usize) -> String {
329            let mut s = String::new();
330            while n > 0 {
331                s.push((b'A' + (n % 26) as u8) as char);
332                n /= 26;
333            }
334            s.chars().rev().collect::<String>()
335        }
336
337        // Ensure that adding more than the maximum number of positions will fail.
338        for i in 1..u8::MAX as usize * 2 {
339            let position = to_unique_string(i);
340            // println!("position: {position}");
341            let command = Command::<CurrentNetwork>::from_str(&format!("position {position};")).unwrap();
342
343            match finalize.commands.len() < u8::MAX as usize {
344                true => assert!(finalize.add_command(command).is_ok()),
345                false => assert!(finalize.add_command(command).is_err()),
346            }
347        }
348    }
349
350    #[test]
351    fn test_reject_cast_to_dynamic_record_in_finalize() {
352        let name = Identifier::from_str("finalize_core_test").unwrap();
353        let mut finalize = Finalize::<CurrentNetwork>::new(name);
354
355        let cmd = Command::<CurrentNetwork>::from_str("cast r0 into r1 as dynamic.record;").unwrap();
356        let err = finalize.add_command(cmd).unwrap_err();
357        assert!(err.to_string().contains("operate on records"));
358    }
359
360    #[test]
361    fn test_reject_get_record_dynamic_in_finalize() {
362        let name = Identifier::from_str("finalize_core_test").unwrap();
363        let mut finalize = Finalize::<CurrentNetwork>::new(name);
364
365        let cmd = Command::<CurrentNetwork>::from_str("get.record.dynamic r0.x into r1 as bool;").unwrap();
366        let err = finalize.add_command(cmd).unwrap_err();
367        assert!(err.to_string().contains("operate on records"));
368    }
369}