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, Command: CommandTrait> Pro } impl, Command: CommandTrait> ProgramCore { + /// 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, Command: CommandTrait> 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) -> bool { @@ -660,6 +674,68 @@ impl, Command: CommandTrait> 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 { + 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::::restricted_keywords_for_consensus_version(consensus_version).collect::>(); + // 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, Command: CommandTrait> 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 { + // 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 { static INSTANCE: OnceCell> = 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> VM { 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.