Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -994,6 +1004,7 @@ workflows:
- parameters-large
- parameters-uncached
- synthesizer
- synthesizer-test
- synthesizer-mem-heavy
- synthesizer-integration
- synthesizer-process
Expand Down
156 changes: 116 additions & 40 deletions synthesizer/program/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions synthesizer/src/vm/finalize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
79 changes: 79 additions & 0 deletions synthesizer/src/vm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(|| {
Expand Down Expand Up @@ -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.")
}
}
}
4 changes: 4 additions & 0 deletions synthesizer/src/vm/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down