Repository: ProvableHQ/snarkVM
Pull request #2685: [Feat] Consensus-restricted keywords.
Author: login: d0cd
Labels: name: v3.7.0
Created: 2025-04-17T00:14:06Z | Merged: 2025-04-25T15:34:27Z

<!-- Thank you for submitting the PR! We appreciate you spending the time to work on these changes! -->

## Motivation

As programming standards evolve on Aleo, it would be useful to be able to enforce syntactic restrictions at the consensus layer. This PR:
- adds a mechanism for describing versioned syntax restrictions and enforces them in `check_transactions`.
- adds a CI runner with the `test` feature enabled for the `synthesizer` crate.

## Test Plan

A test was added verifying that syntax restrictions are not enforced before a given version height and that they are enforced after.



Diff:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 353869efda..d4319862fb 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -696,6 +696,16 @@ jobs:
           workspace_member: synthesizer
           cache_key: v1.3.0-rust-1.83.0-snarkvm-synthesizer-cache
 
+  synthesizer-test:
+    docker:
+      - image: cimg/rust:1.83.0 # Attention - Change the MSRV in Cargo.toml and rust-toolchain as well
+    resource_class: << pipeline.parameters.twoxlarge >>
+    steps:
+      - run_serial:
+          flags: --lib --bins --features test
+          workspace_member: synthesizer
+          cache_key: v1.3.0-rust-1.83.0-snarkvm-synthesizer-test-cache
+
   synthesizer-mem-heavy:
     docker:
       - image: cimg/rust:1.83.0 # Attention - Change the MSRV in Cargo.toml and rust-toolchain as well
@@ -994,6 +1004,7 @@ workflows:
       - parameters-large
       - parameters-uncached
       - synthesizer
+      - synthesizer-test
       - synthesizer-mem-heavy
       - synthesizer-integration
       - synthesizer-process
diff --git a/synthesizer/program/src/lib.rs b/synthesizer/program/src/lib.rs
index db9980e134..891e57bb21 100644
--- a/synthesizer/program/src/lib.rs
+++ b/synthesizer/program/src/lib.rs
@@ -48,49 +48,52 @@ mod parse;
 mod serialize;
 
 use console::{
-    network::prelude::{
-        Debug,
-        Deserialize,
-        Deserializer,
-        Display,
-        Err,
-        Error,
-        ErrorKind,
-        Formatter,
-        FromBytes,
-        FromBytesDeserializer,
-        FromStr,
-        IoResult,
-        Network,
-        Parser,
-        ParserResult,
-        Read,
-        Result,
-        Sanitizer,
-        Serialize,
-        Serializer,
-        ToBytes,
-        ToBytesSerializer,
-        TypeName,
-        Write,
-        anyhow,
-        bail,
-        de,
-        ensure,
-        error,
-        fmt,
-        make_error,
-        many0,
-        many1,
-        map,
-        map_res,
-        tag,
-        take,
+    network::{
+        ConsensusVersion,
+        prelude::{
+            Debug,
+            Deserialize,
+            Deserializer,
+            Display,
+            Err,
+            Error,
+            ErrorKind,
+            Formatter,
+            FromBytes,
+            FromBytesDeserializer,
+            FromStr,
+            IoResult,
+            Network,
+            Parser,
+            ParserResult,
+            Read,
+            Result,
+            Sanitizer,
+            Serialize,
+            Serializer,
+            ToBytes,
+            ToBytesSerializer,
+            TypeName,
+            Write,
+            anyhow,
+            bail,
+            de,
+            ensure,
+            error,
+            fmt,
+            make_error,
+            many0,
+            many1,
+            map,
+            map_res,
+            tag,
+            take,
+        },
     },
     program::{Identifier, PlaintextType, ProgramID, RecordType, StructType},
 };
 
-use indexmap::IndexMap;
+use indexmap::{IndexMap, IndexSet};
 
 #[derive(Copy, Clone, PartialEq, Eq, Hash)]
 enum ProgramDefinition {
@@ -567,8 +570,11 @@ impl<N: Network, Instruction: InstructionTrait<N>, Command: CommandTrait<N>> Pro
 }
 
 impl<N: Network, Instruction: InstructionTrait<N>, Command: CommandTrait<N>> ProgramCore<N, Instruction, Command> {
+    /// A list of reserved keywords for Aleo programs, enforced at the parser level.
+    // New keywords should be enforced through `RESTRICTED_KEYWORDS` instead, if possible.
+    // Adding keywords to this list will require a backwards-compatible versioning for programs.
     #[rustfmt::skip]
-    const KEYWORDS: &'static [&'static str] = &[
+    pub const KEYWORDS: &'static [&'static str] = &[
         // Mode
         "const",
         "constant",
@@ -642,6 +648,14 @@ impl<N: Network, Instruction: InstructionTrait<N>, Command: CommandTrait<N>> Pro
         "type",
         "future",
     ];
+    /// A list of restricted keywords for Aleo programs, enforced at the VM-level for program hygiene.
+    /// Each entry is a tuple of the consensus version and a list of keywords.
+    /// If the current consensus version is greater than or equal to the specified version,
+    /// the keywords in the list should be restricted.
+    #[rustfmt::skip]
+    pub const RESTRICTED_KEYWORDS: &'static [(ConsensusVersion, &'static [&'static str])] = &[
+        (ConsensusVersion::V6, &["constructor"])
+    ];
 
     /// Returns `true` if the given name does not already exist in the program.
     fn is_unique_name(&self, name: &Identifier<N>) -> bool {
@@ -660,6 +674,68 @@ impl<N: Network, Instruction: InstructionTrait<N>, Command: CommandTrait<N>> Pro
         // Check if the name is a keyword.
         Self::KEYWORDS.iter().any(|keyword| *keyword == name)
     }
+
+    /// Returns an iterator over the restricted keywords for the given consensus version.
+    pub fn restricted_keywords_for_consensus_version(
+        consensus_version: ConsensusVersion,
+    ) -> impl Iterator<Item = &'static str> {
+        Self::RESTRICTED_KEYWORDS
+            .iter()
+            .filter(move |(version, _)| *version <= consensus_version)
+            .flat_map(|(_, keywords)| *keywords)
+            .copied()
+    }
+
+    /// Checks a program for restricted keywords for the given consensus version.
+    /// Returns an error if any restricted keywords are found.
+    /// Note: Restrictions are not enforced on the import names in case they were deployed before the restrictions were added.
+    pub fn check_restricted_keywords_for_consensus_version(&self, consensus_version: ConsensusVersion) -> Result<()> {
+        // Get all keywords that are restricted for the consensus version.
+        let keywords =
+            Program::<N>::restricted_keywords_for_consensus_version(consensus_version).collect::<IndexSet<_>>();
+        // Check if the program name is a restricted keywords.
+        let program_name = self.id().name().to_string();
+        if keywords.contains(&program_name.as_str()) {
+            bail!("Program name '{program_name}' is a restricted keyword for the current consensus version")
+        }
+        // Check that all top-level program components are not restricted keywords.
+        for identifier in self.identifiers.keys() {
+            if keywords.contains(identifier.to_string().as_str()) {
+                bail!("Program component '{identifier}' is a restricted keyword for the current consensus version")
+            }
+        }
+        // Check that all record entry names are not restricted keywords.
+        for record_type in self.records().values() {
+            for entry_name in record_type.entries().keys() {
+                if keywords.contains(entry_name.to_string().as_str()) {
+                    bail!("Record entry '{entry_name}' is a restricted keyword for the current consensus version")
+                }
+            }
+        }
+        // Check that all struct member names are not restricted keywords.
+        for struct_type in self.structs().values() {
+            for member_name in struct_type.members().keys() {
+                if keywords.contains(member_name.to_string().as_str()) {
+                    bail!("Struct member '{member_name}' is a restricted keyword for the current consensus version")
+                }
+            }
+        }
+        // Check that all `finalize` positions.
+        // Note: It is sufficient to only check the positions in `FinalizeCore` since `FinalizeTypes::initialize` checks that every
+        // `Branch` instruction targets a valid position.
+        for function in self.functions().values() {
+            if let Some(finalize_logic) = function.finalize_logic() {
+                for position in finalize_logic.positions().keys() {
+                    if keywords.contains(position.to_string().as_str()) {
+                        bail!(
+                            "Finalize position '{position}' is a restricted keyword for the current consensus version"
+                        )
+                    }
+                }
+            }
+        }
+        Ok(())
+    }
 }
 
 impl<N: Network, Instruction: InstructionTrait<N>, Command: CommandTrait<N>> TypeName
diff --git a/synthesizer/src/vm/finalize.rs b/synthesizer/src/vm/finalize.rs
index d85fd50906..7db0f080dc 100644
--- a/synthesizer/src/vm/finalize.rs
+++ b/synthesizer/src/vm/finalize.rs
@@ -1933,6 +1933,7 @@ finalize transfer_public:
         assert!(Committee::new_genesis(committee_map).is_err());
     }
 
+    #[cfg(not(feature = "test"))]
     #[test]
     #[allow(clippy::assertions_on_constants)]
     fn test_migration_v3_maximum_validator_increase() {
diff --git a/synthesizer/src/vm/mod.rs b/synthesizer/src/vm/mod.rs
index 01fe3a4fb6..118abc53d8 100644
--- a/synthesizer/src/vm/mod.rs
+++ b/synthesizer/src/vm/mod.rs
@@ -474,6 +474,21 @@ pub(crate) mod test_helpers {
         VM::from(ConsensusStore::open(StorageMode::new_test(None)).unwrap()).unwrap()
     }
 
+    #[cfg(feature = "test")]
+    pub(crate) fn sample_vm_at_height(height: u32, rng: &mut TestRng) -> VM<CurrentNetwork, LedgerType> {
+        // Initialize the VM with a genesis block.
+        let vm = sample_vm_with_genesis_block(rng);
+        // Get the genesis private key.
+        let genesis_private_key = sample_genesis_private_key(rng);
+        // Advance the VM to the given height.
+        for _ in 0..height {
+            let block = sample_next_block(&vm, &genesis_private_key, &[], rng).unwrap();
+            vm.add_next_block(&block).unwrap();
+        }
+        // Return the VM.
+        vm
+    }
+
     pub(crate) fn sample_genesis_private_key(rng: &mut TestRng) -> PrivateKey<CurrentNetwork> {
         static INSTANCE: OnceCell<PrivateKey<CurrentNetwork>> = OnceCell::new();
         *INSTANCE.get_or_init(|| {
@@ -3036,4 +3051,68 @@ function adder:
         // Check that only `child_program.aleo` is in the VM.
         assert!(vm.process().read().contains_program(&ProgramID::from_str("child_program.aleo").unwrap()));
     }
+
+    #[cfg(feature = "test")]
+    #[test]
+    fn test_versioned_keyword_restrictions() {
+        let rng = &mut TestRng::default();
+
+        // Initialize a new caller.
+        let caller_private_key = crate::vm::test_helpers::sample_genesis_private_key(rng);
+
+        // Initialize the VM at a specific height.
+        // We subtract by 7 to deploy the 7 invalid programs.
+        let vm = sample_vm_at_height(CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V6).unwrap() - 7, rng);
+
+        // Define the invalid program bodies.
+        let invalid_program_bodies = [
+            "function constructor:",
+            "function dummy:\nclosure constructor: input r0 as u8; assert.eq r0 0u8;",
+            "function dummy:\nmapping constructor: key as boolean.public; value as boolean.public;",
+            "function dummy:\nrecord constructor: owner as address.private;",
+            "function dummy:\nrecord foo: owner as address.public; constructor as address.public;",
+            "function dummy:\nstruct constructor: foo as address;",
+            "function dummy:\nstruct foo: constructor as address;",
+        ];
+
+        println!("Current height: {}", vm.block_store().current_block_height());
+
+        // Deploy a test program for each of the invalid program bodies.
+        // They should all be accepted by the VM, because the restriction is not yet in place.
+        for (i, body) in invalid_program_bodies.iter().enumerate() {
+            println!("Deploying 'valid' test program {}: {}", i, body);
+            let program = Program::from_str(&format!("program test_valid_{}.aleo;\n{}", i, body)).unwrap();
+            let deployment = vm.deploy(&caller_private_key, &program, None, 0, None, rng).unwrap();
+            let block = sample_next_block(&vm, &caller_private_key, &[deployment], rng).unwrap();
+            assert_eq!(block.transactions().num_accepted(), 1);
+            assert_eq!(block.transactions().num_rejected(), 0);
+            assert_eq!(block.aborted_transaction_ids().len(), 0);
+            vm.add_next_block(&block).unwrap();
+        }
+
+        println!("Current height: {}", vm.block_store().current_block_height());
+
+        // Deploy a test program for each of the invalid program bodies.
+        // Verify that `check_transaction` fails for each of them.
+        for (i, body) in invalid_program_bodies.iter().enumerate() {
+            println!("Deploying 'invalid' test program {}: {}", i, body);
+            let program = Program::from_str(&format!("program test_invalid_{}.aleo;\n{}", i, body)).unwrap();
+            let deployment = vm.deploy(&caller_private_key, &program, None, 0, None, rng).unwrap();
+            if let Err(e) = vm.check_transaction(&deployment, None, rng) {
+                println!("Error: {}", e);
+            } else {
+                panic!("Expected an error, but the deployment was accepted.")
+            }
+        }
+
+        // Attempt to deploy a program with the name `constructor`.
+        // Verify that `check_transaction` fails.
+        let program = Program::from_str(r"program constructor.aleo; function dummy:").unwrap();
+        let deployment = vm.deploy(&caller_private_key, &program, None, 0, None, rng).unwrap();
+        if let Err(e) = vm.check_transaction(&deployment, None, rng) {
+            println!("Error: {}", e);
+        } else {
+            panic!("Expected an error, but the deployment was accepted.")
+        }
+    }
 }
diff --git a/synthesizer/src/vm/verify.rs b/synthesizer/src/vm/verify.rs
index d148b1bc8f..31bc509157 100644
--- a/synthesizer/src/vm/verify.rs
+++ b/synthesizer/src/vm/verify.rs
@@ -161,6 +161,10 @@ impl<N: Network, C: ConsensusStorage<N>> VM<N, C> {
                 if self.contains_program(deployment.program_id()) {
                     bail!("Program ID '{}' already exists", deployment.program_id());
                 }
+                // Enforce the syntax restrictions on the programs based on the current consensus version.
+                let current_block_height = self.block_store().current_block_height();
+                let consensus_version = N::CONSENSUS_VERSION(current_block_height)?;
+                deployment.program().check_restricted_keywords_for_consensus_version(consensus_version)?;
                 // Verify the deployment if it has not been verified before.
                 if !is_partially_verified {
                     // Verify the deployment.


Reviews:
user: login: vicsn
body: LGTM! Left two style suggestions.

synthesizer CI is failing when run with the `--test` feature because `test_migration_v3_maximum_validator_increase` assumes max validator set increases at consensus v3, whereas with the `test` feature enabled the max validator set is static at 100.

In other words, our usage of the `test` feature is a bit of an undocumented mess.


As a "quickfix", one option would be to use the same `sample_finalize_state` approach as in `test_migration_v3_maximum_validator_increase`, and then we can also get rid of introducing yet another CI job.
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-17T14:05:23Z
user: login: raychu86
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-17T14:57:46Z
user: login: raychu86
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-17T15:21:57Z
user: login: d0cd
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-17T15:34:10Z
user: login: d0cd
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-17T15:45:48Z
user: login: d0cd
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-17T16:42:54Z
user: login: vicsn
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-23T10:35:25Z
user: login: niklaslong
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-23T12:21:56Z
user: login: niklaslong
body: LGTM overall, pending the changes requested by other reviewers. 
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-23T12:22:48Z
user: login: d0cd
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-23T18:31:26Z
user: login: vicsn
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-23T18:53:51Z
user: login: vicsn
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-23T18:54:01Z
user: login: d0cd
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-24T19:46:14Z
user: login: raychu86
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-25T15:21:44Z
user: login: d0cd
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-25T15:32:07Z
user: login: d0cd
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-25T15:37:46Z

Review comments:
diff_hunk: @@ -161,6 +161,49 @@ impl<N: Network, C: ConsensusStorage<N>> VM<N, C> {
                 if self.contains_program(deployment.program_id()) {
                     bail!("Program ID '{}' already exists", deployment.program_id());
                 }
+                // Enforce the syntax restrictions on the programs based on the current consensus version.
+                // Note: We intentionally do not enforce restrictions on the import names in case they were deployed before the restrictions were added.
+                let current_block_height = self.block_store().current_block_height();
+                let consensus_version = N::CONSENSUS_VERSION(current_block_height)?;
+                // Get all keywords that are restricted for the current version.
+                let keywords =
+                    Program::<N>::restricted_keywords_for_consensus_version(consensus_version).collect::<Vec<_>>();
+                // Check if the program name is a restricted keywords.
path: synthesizer/src/vm/verify.rs
user: login: vicsn
body: Slight preference for moving this logic into its own function, perhaps `Program::<N>::contains_restricted_keywords(keywords)`
created_at: 2025-04-17T13:46:13Z
author_association: COLLABORATOR
diff_hunk: @@ -642,6 +650,15 @@ impl<N: Network, Instruction: InstructionTrait<N>, Command: CommandTrait<N>> Pro
         "type",
         "future",
     ];
+    /// A list of restricted keywords for Aleo programs.
+    /// These restrictions are used to enforce program hygiene.
+    /// Each entry is a tuple of the consensus version and a list of keywords.
+    /// If the current consensus version is greater than or equal to the specified version,
+    /// the keywords in the list should be restricted.
+    #[rustfmt::skip]
+    const RESTRICTED_KEYWORDS: &'static [(ConsensusVersion, &'static [&'static str])] = &[
path: synthesizer/program/src/lib.rs
user: login: vicsn
body: Can we document the relation to `KEYWORDS`?

And also add a docblock above `KEYWORDS` that this new const exists for keywords which were newly added.
created_at: 2025-04-17T13:47:24Z
author_association: COLLABORATOR
diff_hunk: @@ -161,6 +161,49 @@ impl<N: Network, C: ConsensusStorage<N>> VM<N, C> {
                 if self.contains_program(deployment.program_id()) {
                     bail!("Program ID '{}' already exists", deployment.program_id());
                 }
+                // Enforce the syntax restrictions on the programs based on the current consensus version.
+                // Note: We intentionally do not enforce restrictions on the import names in case they were deployed before the restrictions were added.
+                let current_block_height = self.block_store().current_block_height();
+                let consensus_version = N::CONSENSUS_VERSION(current_block_height)?;
+                // Get all keywords that are restricted for the current version.
+                let keywords =
+                    Program::<N>::restricted_keywords_for_consensus_version(consensus_version).collect::<Vec<_>>();
+                // Check if the program name is a restricted keywords.
path: synthesizer/src/vm/verify.rs
user: login: raychu86
body: +1 on this. `check_transaction` is already way too large as it is
created_at: 2025-04-17T14:57:45Z
author_association: COLLABORATOR
diff_hunk: @@ -161,6 +161,49 @@ impl<N: Network, C: ConsensusStorage<N>> VM<N, C> {
                 if self.contains_program(deployment.program_id()) {
                     bail!("Program ID '{}' already exists", deployment.program_id());
                 }
+                // Enforce the syntax restrictions on the programs based on the current consensus version.
+                // Note: We intentionally do not enforce restrictions on the import names in case they were deployed before the restrictions were added.
+                let current_block_height = self.block_store().current_block_height();
+                let consensus_version = N::CONSENSUS_VERSION(current_block_height)?;
+                // Get all keywords that are restricted for the current version.
+                let keywords =
+                    Program::<N>::restricted_keywords_for_consensus_version(consensus_version).collect::<Vec<_>>();
+                // Check if the program name is a restricted keywords.
+                let program_name = deployment.program().id().name().to_string();
+                if keywords.iter().any(|keyword| program_name == *keyword) {
+                    bail!("Program name '{}' is a restricted keyword for the current consensus version", program_name)
+                }
+                // Check that all top-level program components are not restricted keywords.
+                for identifier in deployment.program().identifiers().keys() {
path: synthesizer/src/vm/verify.rs
user: login: raychu86
body: Does this encompass branch position names?

Ditto for the original checks, were we checking branch position names with `is_reserved_keyword`?
created_at: 2025-04-17T15:21:57Z
author_association: COLLABORATOR
diff_hunk: @@ -161,6 +161,49 @@ impl<N: Network, C: ConsensusStorage<N>> VM<N, C> {
                 if self.contains_program(deployment.program_id()) {
                     bail!("Program ID '{}' already exists", deployment.program_id());
                 }
+                // Enforce the syntax restrictions on the programs based on the current consensus version.
+                // Note: We intentionally do not enforce restrictions on the import names in case they were deployed before the restrictions were added.
+                let current_block_height = self.block_store().current_block_height();
+                let consensus_version = N::CONSENSUS_VERSION(current_block_height)?;
+                // Get all keywords that are restricted for the current version.
+                let keywords =
+                    Program::<N>::restricted_keywords_for_consensus_version(consensus_version).collect::<Vec<_>>();
+                // Check if the program name is a restricted keywords.
+                let program_name = deployment.program().id().name().to_string();
+                if keywords.iter().any(|keyword| program_name == *keyword) {
+                    bail!("Program name '{}' is a restricted keyword for the current consensus version", program_name)
+                }
+                // Check that all top-level program components are not restricted keywords.
+                for identifier in deployment.program().identifiers().keys() {
path: synthesizer/src/vm/verify.rs
user: login: d0cd
body: It doesn't and we weren't enforcing for `Position` or `Branch` instructions.
created_at: 2025-04-17T15:34:09Z
author_association: COLLABORATOR
diff_hunk: @@ -161,6 +161,49 @@ impl<N: Network, C: ConsensusStorage<N>> VM<N, C> {
                 if self.contains_program(deployment.program_id()) {
                     bail!("Program ID '{}' already exists", deployment.program_id());
                 }
+                // Enforce the syntax restrictions on the programs based on the current consensus version.
+                // Note: We intentionally do not enforce restrictions on the import names in case they were deployed before the restrictions were added.
+                let current_block_height = self.block_store().current_block_height();
+                let consensus_version = N::CONSENSUS_VERSION(current_block_height)?;
+                // Get all keywords that are restricted for the current version.
+                let keywords =
+                    Program::<N>::restricted_keywords_for_consensus_version(consensus_version).collect::<Vec<_>>();
+                // Check if the program name is a restricted keywords.
path: synthesizer/src/vm/verify.rs
user: login: d0cd
body: Good call out! Moving it back to `Program`, which should also simplify the code a bit more.
created_at: 2025-04-17T15:45:48Z
author_association: COLLABORATOR
diff_hunk: @@ -161,6 +161,49 @@ impl<N: Network, C: ConsensusStorage<N>> VM<N, C> {
                 if self.contains_program(deployment.program_id()) {
                     bail!("Program ID '{}' already exists", deployment.program_id());
                 }
+                // Enforce the syntax restrictions on the programs based on the current consensus version.
+                // Note: We intentionally do not enforce restrictions on the import names in case they were deployed before the restrictions were added.
+                let current_block_height = self.block_store().current_block_height();
+                let consensus_version = N::CONSENSUS_VERSION(current_block_height)?;
+                // Get all keywords that are restricted for the current version.
+                let keywords =
+                    Program::<N>::restricted_keywords_for_consensus_version(consensus_version).collect::<Vec<_>>();
+                // Check if the program name is a restricted keywords.
+                let program_name = deployment.program().id().name().to_string();
+                if keywords.iter().any(|keyword| program_name == *keyword) {
+                    bail!("Program name '{}' is a restricted keyword for the current consensus version", program_name)
+                }
+                // Check that all top-level program components are not restricted keywords.
+                for identifier in deployment.program().identifiers().keys() {
path: synthesizer/src/vm/verify.rs
user: login: d0cd
body: We do have to option start enforcing these in the same way at the VM layer.
I did a quick scan (using [this](https://github.com/ProvableHQ/aleo-program-regressions/pull/19)) and didn't find any programs on `testnet`, `canary`, or `mainnet` that have keywords in their positions. (Likely because Leo uses a very specific structure for the name)
created_at: 2025-04-17T16:42:54Z
author_association: COLLABORATOR
diff_hunk: @@ -642,6 +648,14 @@ impl<N: Network, Instruction: InstructionTrait<N>, Command: CommandTrait<N>> Pro
         "type",
         "future",
     ];
+    /// A list of restricted keywords for Aleo programs, enforced at the VM-level for program hygiene.
+    /// Each entry is a tuple of the consensus version and a list of keywords.
+    /// If the current consensus version is greater than or equal to the specified version,
+    /// the keywords in the list should be restricted.
+    #[rustfmt::skip]
+    pub const RESTRICTED_KEYWORDS: &'static [(ConsensusVersion, &'static [&'static str])] = &[
+        (ConsensusVersion::V6, &["constructor"])
path: synthesizer/program/src/lib.rs
user: login: vicsn
body: Given that our transaction validity rules change from the V6 consensus height onwards, we need to clear the `partially_verified_transactions` cache.

Given that this is a one-time action (as opposed to program upgrades which can happen more frequently), you can just copy this approach: https://github.com/ProvableHQ/snarkVM/blob/mainnet/synthesizer/src/vm/mod.rs#L440

@raychu86 what do you think about just automatically clearing the entire cache on every consensus version upgrade so we don't have to think about it anymore. This could be e.g. sth like `if N::CONSENSUS_VERSION_HEIGHTS.iter().map(|_,h|h).contains(block.height()) { clear the cache }`.

created_at: 2025-04-23T10:35:22Z
author_association: COLLABORATOR
diff_hunk: @@ -660,6 +674,68 @@ impl<N: Network, Instruction: InstructionTrait<N>, Command: CommandTrait<N>> Pro
         // Check if the name is a keyword.
         Self::KEYWORDS.iter().any(|keyword| *keyword == name)
     }
+
+    /// Returns an iterator over the restricted keywords for the given consensus version.
+    pub fn restricted_keywords_for_consensus_version(
+        consensus_version: ConsensusVersion,
+    ) -> impl Iterator<Item = &'static str> {
+        Self::RESTRICTED_KEYWORDS
+            .iter()
+            .filter(move |(version, _)| *version <= consensus_version)
+            .flat_map(|(_, keywords)| *keywords)
+            .copied()
+    }
+
+    /// Checks a program for restricted keywords for the given consensus version.
+    /// Returns an error if any restricted keywords are found.
+    /// Note: Restrictions are not enforced on the import names in case they were deployed before the restrictions were added.
+    pub fn check_restricted_keywords_for_consensus_version(&self, consensus_version: ConsensusVersion) -> Result<()> {
+        // Get all keywords that are restricted for the consensus version.
+        let keywords =
+            Program::<N>::restricted_keywords_for_consensus_version(consensus_version).collect::<IndexSet<_>>();
+        // Check if the program name is a restricted keywords.
+        let program_name = self.id().name().to_string();
+        if keywords.contains(&program_name.as_str()) {
+            bail!("Program name '{program_name}' is a restricted keyword for the current consensus version")
+        }
+        // Check that all top-level program components are not restricted keywords.
+        for identifier in self.identifiers.keys() {
+            if keywords.contains(identifier.to_string().as_str()) {
path: synthesizer/program/src/lib.rs
user: login: niklaslong
body: I wish we could avoid these allocations by storing the keywords as `Identifier`s but I don't think that's possible with the type's current implementations?  
created_at: 2025-04-23T12:21:56Z
author_association: COLLABORATOR
diff_hunk: @@ -642,6 +648,14 @@ impl<N: Network, Instruction: InstructionTrait<N>, Command: CommandTrait<N>> Pro
         "type",
         "future",
     ];
+    /// A list of restricted keywords for Aleo programs, enforced at the VM-level for program hygiene.
+    /// Each entry is a tuple of the consensus version and a list of keywords.
+    /// If the current consensus version is greater than or equal to the specified version,
+    /// the keywords in the list should be restricted.
+    #[rustfmt::skip]
+    pub const RESTRICTED_KEYWORDS: &'static [(ConsensusVersion, &'static [&'static str])] = &[
+        (ConsensusVersion::V6, &["constructor"])
path: synthesizer/program/src/lib.rs
user: login: d0cd
body: Just to double check my understanding, it seems like `partially_verified_transactions` is only read once [here](https://github.com/ProvableHQ/snarkVM/blob/5d0d1745d05619e2d3aecdcf54cf65106e52f4b7/synthesizer/src/vm/verify.rs#L141).

If `is_partially_verified` is `true`, then an internal check in `check_fee` and `check_deployment_internal` are skipped. 

The check is added [here](https://github.com/ProvableHQ/snarkVM/blob/45c3ba4723d5fe0d119aec83dc6e3ac5abf5d324/synthesizer/src/vm/verify.rs#L167), so would that mean it's run every time, despite whether or not it is cached?
created_at: 2025-04-23T18:31:26Z
author_association: COLLABORATOR
diff_hunk: @@ -642,6 +648,14 @@ impl<N: Network, Instruction: InstructionTrait<N>, Command: CommandTrait<N>> Pro
         "type",
         "future",
     ];
+    /// A list of restricted keywords for Aleo programs, enforced at the VM-level for program hygiene.
+    /// Each entry is a tuple of the consensus version and a list of keywords.
+    /// If the current consensus version is greater than or equal to the specified version,
+    /// the keywords in the list should be restricted.
+    #[rustfmt::skip]
+    pub const RESTRICTED_KEYWORDS: &'static [(ConsensusVersion, &'static [&'static str])] = &[
+        (ConsensusVersion::V6, &["constructor"])
path: synthesizer/program/src/lib.rs
user: login: vicsn
body: You are right, I think I got this right the first time I reviewed but I forgot, thx for catching.
created_at: 2025-04-23T18:53:51Z
author_association: COLLABORATOR
diff_hunk: @@ -660,6 +674,68 @@ impl<N: Network, Instruction: InstructionTrait<N>, Command: CommandTrait<N>> Pro
         // Check if the name is a keyword.
         Self::KEYWORDS.iter().any(|keyword| *keyword == name)
     }
+
+    /// Returns an iterator over the restricted keywords for the given consensus version.
+    pub fn restricted_keywords_for_consensus_version(
+        consensus_version: ConsensusVersion,
+    ) -> impl Iterator<Item = &'static str> {
+        Self::RESTRICTED_KEYWORDS
+            .iter()
+            .filter(move |(version, _)| *version <= consensus_version)
+            .flat_map(|(_, keywords)| *keywords)
+            .copied()
+    }
+
+    /// Checks a program for restricted keywords for the given consensus version.
+    /// Returns an error if any restricted keywords are found.
+    /// Note: Restrictions are not enforced on the import names in case they were deployed before the restrictions were added.
+    pub fn check_restricted_keywords_for_consensus_version(&self, consensus_version: ConsensusVersion) -> Result<()> {
+        // Get all keywords that are restricted for the consensus version.
+        let keywords =
+            Program::<N>::restricted_keywords_for_consensus_version(consensus_version).collect::<IndexSet<_>>();
+        // Check if the program name is a restricted keywords.
+        let program_name = self.id().name().to_string();
+        if keywords.contains(&program_name.as_str()) {
+            bail!("Program name '{program_name}' is a restricted keyword for the current consensus version")
+        }
+        // Check that all top-level program components are not restricted keywords.
+        for identifier in self.identifiers.keys() {
+            if keywords.contains(identifier.to_string().as_str()) {
path: synthesizer/program/src/lib.rs
user: login: d0cd
body: We could initialize the keywords as `Identifier`s and store them in an `IndexSet`.
You'll only have a fixed number of allocations.
created_at: 2025-04-24T19:46:14Z
author_association: COLLABORATOR
diff_hunk: @@ -660,6 +674,68 @@ impl<N: Network, Instruction: InstructionTrait<N>, Command: CommandTrait<N>> Pro
         // Check if the name is a keyword.
         Self::KEYWORDS.iter().any(|keyword| *keyword == name)
     }
+
+    /// Returns an iterator over the restricted keywords for the given consensus version.
+    pub fn restricted_keywords_for_consensus_version(
+        consensus_version: ConsensusVersion,
+    ) -> impl Iterator<Item = &'static str> {
+        Self::RESTRICTED_KEYWORDS
+            .iter()
+            .filter(move |(version, _)| *version <= consensus_version)
+            .flat_map(|(_, keywords)| *keywords)
+            .copied()
+    }
+
+    /// Checks a program for restricted keywords for the given consensus version.
+    /// Returns an error if any restricted keywords are found.
+    /// Note: Restrictions are not enforced on the import names in case they were deployed before the restrictions were added.
+    pub fn check_restricted_keywords_for_consensus_version(&self, consensus_version: ConsensusVersion) -> Result<()> {
+        // Get all keywords that are restricted for the consensus version.
+        let keywords =
+            Program::<N>::restricted_keywords_for_consensus_version(consensus_version).collect::<IndexSet<_>>();
+        // Check if the program name is a restricted keywords.
+        let program_name = self.id().name().to_string();
+        if keywords.contains(&program_name.as_str()) {
+            bail!("Program name '{program_name}' is a restricted keyword for the current consensus version")
+        }
+        // Check that all top-level program components are not restricted keywords.
+        for identifier in self.identifiers.keys() {
+            if keywords.contains(identifier.to_string().as_str()) {
path: synthesizer/program/src/lib.rs
user: login: d0cd
body: Added a benchmark, current impl is at ~4us on a 2021 MBP Pro M1 MAX with 32GB RAM.
created_at: 2025-04-25T15:32:07Z
author_association: COLLABORATOR
diff_hunk: @@ -660,6 +674,68 @@ impl<N: Network, Instruction: InstructionTrait<N>, Command: CommandTrait<N>> Pro
         // Check if the name is a keyword.
         Self::KEYWORDS.iter().any(|keyword| *keyword == name)
     }
+
+    /// Returns an iterator over the restricted keywords for the given consensus version.
+    pub fn restricted_keywords_for_consensus_version(
+        consensus_version: ConsensusVersion,
+    ) -> impl Iterator<Item = &'static str> {
+        Self::RESTRICTED_KEYWORDS
+            .iter()
+            .filter(move |(version, _)| *version <= consensus_version)
+            .flat_map(|(_, keywords)| *keywords)
+            .copied()
+    }
+
+    /// Checks a program for restricted keywords for the given consensus version.
+    /// Returns an error if any restricted keywords are found.
+    /// Note: Restrictions are not enforced on the import names in case they were deployed before the restrictions were added.
+    pub fn check_restricted_keywords_for_consensus_version(&self, consensus_version: ConsensusVersion) -> Result<()> {
+        // Get all keywords that are restricted for the consensus version.
+        let keywords =
+            Program::<N>::restricted_keywords_for_consensus_version(consensus_version).collect::<IndexSet<_>>();
+        // Check if the program name is a restricted keywords.
+        let program_name = self.id().name().to_string();
+        if keywords.contains(&program_name.as_str()) {
+            bail!("Program name '{program_name}' is a restricted keyword for the current consensus version")
+        }
+        // Check that all top-level program components are not restricted keywords.
+        for identifier in self.identifiers.keys() {
+            if keywords.contains(identifier.to_string().as_str()) {
path: synthesizer/program/src/lib.rs
user: login: d0cd
body: Actually, you're correct. The current impl wouldn't parse `address` as an identifier.
created_at: 2025-04-25T15:37:46Z
author_association: COLLABORATOR

PR comments:
user: login: d0cd
created_at: 2025-04-17T00:18:14Z
body: @reviewers I'm not too happy with the name `RESTRICTED_KEYWORDS`, any ideas for a better one?
author_association: COLLABORATOR
user: login: acoglio
created_at: 2025-04-17T10:22:45Z
body: `RESERVED_KEYWORDS` is perhaps more common?
author_association: COLLABORATOR
user: login: d0cd
created_at: 2025-04-17T15:35:32Z
body: > `RESERVED_KEYWORDS` is perhaps more common?

I had mapped the concept of "reserved" keywords to the `KEYWORDS` array.
This feels more like syntactic recommendations to me.
Like linting...?
author_association: COLLABORATOR
user: login: d0cd
created_at: 2025-04-17T16:08:37Z
body: > As a "quickfix", one option would be to use the same `sample_finalize_state` approach as in `test_migration_v3_maximum_validator_increase`, and then we can also get rid of introducing yet another CI job.

If it's not too costly, it might be nice to have a CI run with the `test` feature enabled.
I noticed a few other tests ([1](https://github.com/ProvableHQ/snarkVM/blob/643bf03d9db60a95f1ec283ad57e1f17d24e3d43/synthesizer/src/vm/execute.rs#L374), [2](https://github.com/ProvableHQ/snarkVM/blob/643bf03d9db60a95f1ec283ad57e1f17d24e3d43/synthesizer/src/vm/execute.rs#L404), [3](https://github.com/ProvableHQ/snarkVM/blob/643bf03d9db60a95f1ec283ad57e1f17d24e3d43/synthesizer/src/vm/finalize.rs#L1913), [4](https://github.com/ProvableHQ/snarkVM/blob/643bf03d9db60a95f1ec283ad57e1f17d24e3d43/synthesizer/src/vm/verify.rs#L798), [5](https://github.com/ProvableHQ/snarkVM/blob/643bf03d9db60a95f1ec283ad57e1f17d24e3d43/synthesizer/src/vm/verify.rs#L892)) that also use it.


author_association: COLLABORATOR