diff --git a/console/network/src/lib.rs b/console/network/src/lib.rs index eea3d7dc82..a3b18115d7 100644 --- a/console/network/src/lib.rs +++ b/console/network/src/lib.rs @@ -129,6 +129,8 @@ pub trait Network: const STARTING_SUPPLY: u64 = 1_500_000_000_000_000; // 1.5B credits /// The cost in microcredits per byte for the deployment transaction. const DEPLOYMENT_FEE_MULTIPLIER: u64 = 1_000; // 1 millicredit per byte + /// The multiplier in microcredits for each command in the constructor. + const CONSTRUCTOR_FEE_MULTIPLIER: u64 = 100; // 100x per command /// The constant that divides the storage polynomial. const EXECUTION_STORAGE_FEE_SCALING_FACTOR: u64 = 5000; /// The maximum size execution transactions can be before a quadratic storage penalty applies. @@ -141,7 +143,7 @@ pub trait Network: const MAX_DEPLOYMENT_CONSTRAINTS: u64 = 1 << 20; // 1,048,576 constraints /// The maximum number of microcredits that can be spent as a fee. const MAX_FEE: u64 = 1_000_000_000_000_000; - /// The maximum number of microcredits that can be spent on a transaction's finalize scope. + /// The maximum number of microcredits that can be spent on a constructor or finalize scope. const TRANSACTION_SPEND_LIMIT: u64 = 100_000_000; /// The anchor height, defined as the expected number of blocks to reach the coinbase target. diff --git a/ledger/block/src/transaction/merkle.rs b/ledger/block/src/transaction/merkle.rs index 4a5bce4ef7..1ad78c1292 100644 --- a/ledger/block/src/transaction/merkle.rs +++ b/ledger/block/src/transaction/merkle.rs @@ -216,7 +216,7 @@ impl Transaction { // Ensure the number of functions is within the allowed range. ensure!( num_transitions < Self::MAX_TRANSITIONS, // Note: Observe we hold back 1 for the fee. - "Execution must contain less than {num_transitions} transitions, found {}", + "Execution must contain less than {} transitions, found {num_transitions}", Self::MAX_TRANSITIONS, ); Ok(()) diff --git a/ledger/block/src/transactions/confirmed/mod.rs b/ledger/block/src/transactions/confirmed/mod.rs index 7149b0e1cc..e60a8c5935 100644 --- a/ledger/block/src/transactions/confirmed/mod.rs +++ b/ledger/block/src/transactions/confirmed/mod.rs @@ -27,12 +27,16 @@ pub type NumFinalizeSize = u16; #[derive(Clone, PartialEq, Eq)] pub enum ConfirmedTransaction { /// The accepted deploy transaction is composed of `(index, deploy_transaction, finalize_operations)`. + /// The finalize operations may contain operations from the executing the constructor and fee transition. AcceptedDeploy(u32, Transaction, Vec>), /// The accepted execute transaction is composed of `(index, execute_transaction, finalize_operations)`. + /// The finalize operations can contain operations from the executing the finalize scope and fee transition. AcceptedExecute(u32, Transaction, Vec>), /// The rejected deploy transaction is composed of `(index, fee_transaction, rejected_deployment, finalize_operations)`. + /// The finalize operations can contain operations from the fee transition. RejectedDeploy(u32, Transaction, Rejected, Vec>), /// The rejected execute transaction is composed of `(index, fee_transaction, rejected_execution, finalize_operations)`. + /// The finalize operations can contain operations from the fee transition. RejectedExecute(u32, Transaction, Rejected, Vec>), } @@ -51,11 +55,13 @@ impl ConfirmedTransaction { } }; - // Count the number of `InitializeMapping` and `UpdateKeyValue` finalize operations. - let (num_initialize_mappings, num_update_key_values) = - finalize_operations.iter().try_fold((0, 0), |(init, update), operation| match operation { - FinalizeOperation::InitializeMapping(..) => Ok((init + 1, update)), - FinalizeOperation::UpdateKeyValue(..) => Ok((init, update + 1)), + // Count the number of `InitializeMapping` and `*KeyValue` finalize operations. + let (num_initialize_mappings, num_key_values) = + finalize_operations.iter().try_fold((0, 0), |(init, key_value), operation| match operation { + FinalizeOperation::InitializeMapping(..) => Ok((init + 1, key_value)), + FinalizeOperation::InsertKeyValue(..) // At the time of writing, `InsertKeyValue` is only used in tests. However, it is added for completeness, as it is a valid operation. + | FinalizeOperation::RemoveKeyValue(..) + | FinalizeOperation::UpdateKeyValue(..) => Ok((init, key_value + 1)), op => { bail!("Transaction '{}' (deploy) contains an invalid finalize operation ({op})", transaction.id()) } @@ -63,8 +69,8 @@ impl ConfirmedTransaction { // Perform safety checks on the finalize operations. { - // Ensure the number of finalize operations matches the number of 'InitializeMapping' and 'UpdateKeyValue' finalize operations. - if num_initialize_mappings + num_update_key_values != finalize_operations.len() { + // Ensure the number of finalize operations matches the number of 'InitializeMapping' and '*KeyValue' finalize operations. + if num_initialize_mappings + num_key_values != finalize_operations.len() { bail!( "Transaction '{}' (deploy) must contain '{}' operations", transaction.id(), @@ -79,14 +85,23 @@ impl ConfirmedTransaction { program.mappings().len(), ) } - // Ensure the number of finalize operations matches the number of 'UpdateKeyValue' finalize operations. - if num_update_key_values != fee.num_finalize_operations() { - bail!( - "Transaction '{}' (deploy) must contain {} 'UpdateKeyValue' operations (found '{num_update_key_values}')", - transaction.id(), - fee.num_finalize_operations() - ); - } + // Ensure the number of fee finalize operations lower bounds the number of '*KeyValue' finalize operations. + // The lower bound is due to the fact that constructors can issue '*KeyValue' operations as part of the deployment. + ensure!( + fee.num_finalize_operations() <= num_key_values, + "Transaction '{}' (deploy) must contain at least {} '*KeyValue' operations (found '{num_key_values}')", + transaction.id(), + fee.num_finalize_operations() + ); + // Ensure the number of fee finalize operations and the number of "write" operations in the constructor upper bounds the number of '*KeyValue' finalize operations. + // This is an upper bound because a constructor may contain `branch.*` commands so that a subset of writes are executed. + let num_constructor_writes = usize::from(program.constructor().map(|c| c.num_writes()).unwrap_or_default()); + ensure!( + fee.num_finalize_operations().saturating_add(num_constructor_writes) >= num_key_values, + "Transaction '{}' (deploy) must contain at most {} '*KeyValue' operations (found '{num_key_values}')", + transaction.id(), + fee.num_finalize_operations().saturating_add(num_constructor_writes) + ); } // Return the accepted deploy transaction. diff --git a/ledger/store/src/program/finalize.rs b/ledger/store/src/program/finalize.rs index 99e9f05750..540100b2a5 100644 --- a/ledger/store/src/program/finalize.rs +++ b/ledger/store/src/program/finalize.rs @@ -590,11 +590,16 @@ impl> FinalizeStore { } impl> FinalizeStoreTrait for FinalizeStore { - /// Returns `true` if the given `program ID` and `mapping name` exist. + /// Returns `true` if the given `program ID` and `mapping name` is confirmed to exist. fn contains_mapping_confirmed(&self, program_id: &ProgramID, mapping_name: &Identifier) -> Result { self.storage.contains_mapping_confirmed(program_id, mapping_name) } + /// Returns `true` if the given `program ID` and `mapping name` exist. + fn contains_mapping_speculative(&self, program_id: &ProgramID, mapping_name: &Identifier) -> Result { + self.storage.contains_mapping_speculative(program_id, mapping_name) + } + /// Returns `true` if the given `program ID`, `mapping name`, and `key` exist. fn contains_key_speculative( &self, diff --git a/synthesizer/process/src/cost.rs b/synthesizer/process/src/cost.rs index 13281fcfef..2a3581fdc5 100644 --- a/synthesizer/process/src/cost.rs +++ b/synthesizer/process/src/cost.rs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::{Process, Stack, StackProgramTypes}; +use crate::{FinalizeTypes, Process, Stack, StackProgramTypes}; use crate::stack::StackRef; use console::{ @@ -21,10 +21,13 @@ use console::{ program::{FinalizeType, Identifier, LiteralType, PlaintextType}, }; use ledger_block::{Deployment, Execution, Transaction}; -use synthesizer_program::{CastType, Command, Finalize, Instruction, Operand, StackProgram}; +use synthesizer_program::{CastType, Command, Instruction, Operand, StackProgram}; -/// Returns the *minimum* cost in microcredits to publish the given deployment (total cost, (storage cost, synthesis cost, namespace cost)). -pub fn deployment_cost(deployment: &Deployment) -> Result<(u64, (u64, u64, u64))> { +/// Returns the *minimum* cost in microcredits to publish the given deployment (total cost, (storage cost, synthesis cost, constructor cost, namespace cost)). +pub fn deployment_cost( + process: &Process, + deployment: &Deployment, +) -> Result<(u64, (u64, u64, u64, u64))> { // Determine the number of bytes in the deployment. let size_in_bytes = deployment.size_in_bytes()?; // Retrieve the program ID. @@ -44,7 +47,10 @@ pub fn deployment_cost(deployment: &Deployment) -> Result<(u64, ( // Compute the synthesis cost in microcredits. let synthesis_cost = num_combined_variables.saturating_add(num_combined_constraints) * N::SYNTHESIS_FEE_MULTIPLIER; - // Compute the namespace cost in credits: 10^(10 - num_characters). + // Compute the constructor cost in microcredits. + let constructor_cost = constructor_cost_in_microcredits(&Stack::new(process, deployment.program())?)?; + + // Compute the namespace cost in microcredits: 10^(10 - num_characters) * 1e6 let namespace_cost = 10u64 .checked_pow(10u32.saturating_sub(num_characters)) .ok_or(anyhow!("The namespace cost computation overflowed for a deployment"))? @@ -53,10 +59,11 @@ pub fn deployment_cost(deployment: &Deployment) -> Result<(u64, ( // Compute the total cost in microcredits. let total_cost = storage_cost .checked_add(synthesis_cost) + .and_then(|x| x.checked_add(constructor_cost)) .and_then(|x| x.checked_add(namespace_cost)) .ok_or(anyhow!("The total cost computation overflowed for a deployment"))?; - Ok((total_cost, (storage_cost, synthesis_cost, namespace_cost))) + Ok((total_cost, (storage_cost, synthesis_cost, constructor_cost, namespace_cost))) } /// Returns the *minimum* cost in microcredits to publish the given execution (total cost, (storage cost, finalize cost)). @@ -169,27 +176,24 @@ fn plaintext_size_in_bytes(stack: &Stack, plaintext_type: &Plaint /// A helper function to compute the following: base_cost + (byte_multiplier * size_of_operands). fn cost_in_size<'a, N: Network>( stack: &Stack, - finalize: &Finalize, + finalize_types: &FinalizeTypes, operands: impl IntoIterator>, byte_multiplier: u64, base_cost: u64, ) -> Result { - // Retrieve the finalize types. - let finalize_types = stack.get_finalize_types(finalize.name())?; // Compute the size of the operands. let size_of_operands = operands.into_iter().try_fold(0u64, |acc, operand| { // Determine the size of the operand. let operand_size = match finalize_types.get_type_from_operand(stack, operand)? { FinalizeType::Plaintext(plaintext_type) => plaintext_size_in_bytes(stack, &plaintext_type)?, FinalizeType::Future(future) => { - bail!("Future '{future}' is not a valid operand in the finalize scope"); + bail!("Future '{future}' is not a valid operand"); } }; // Safely add the size to the accumulator. acc.checked_add(operand_size).ok_or(anyhow!( - "Overflowed while computing the size of the operand '{operand}' in '{}/{}' (finalize)", + "Overflowed while computing the size of the operand '{operand}' in '{}'", stack.program_id(), - finalize.name() )) })?; // Return the cost. @@ -199,7 +203,7 @@ fn cost_in_size<'a, N: Network>( /// Returns the the cost of a command in a finalize scope. pub fn cost_per_command( stack: &Stack, - finalize: &Finalize, + finalize_types: &FinalizeTypes, command: &Command, consensus_fee_version: ConsensusFeeVersion, ) -> Result { @@ -239,28 +243,26 @@ pub fn cost_per_command( | CastType::ExternalRecord(_) => Ok(500), }, Command::Instruction(Instruction::CommitBHP256(commit)) => { - cost_in_size(stack, finalize, commit.operands(), HASH_BHP_PER_BYTE_COST, HASH_BHP_BASE_COST) + cost_in_size(stack, finalize_types, commit.operands(), HASH_BHP_PER_BYTE_COST, HASH_BHP_BASE_COST) } Command::Instruction(Instruction::CommitBHP512(commit)) => { - cost_in_size(stack, finalize, commit.operands(), HASH_BHP_PER_BYTE_COST, HASH_BHP_BASE_COST) + cost_in_size(stack, finalize_types, commit.operands(), HASH_BHP_PER_BYTE_COST, HASH_BHP_BASE_COST) } Command::Instruction(Instruction::CommitBHP768(commit)) => { - cost_in_size(stack, finalize, commit.operands(), HASH_BHP_PER_BYTE_COST, HASH_BHP_BASE_COST) + cost_in_size(stack, finalize_types, commit.operands(), HASH_BHP_PER_BYTE_COST, HASH_BHP_BASE_COST) } Command::Instruction(Instruction::CommitBHP1024(commit)) => { - cost_in_size(stack, finalize, commit.operands(), HASH_BHP_PER_BYTE_COST, HASH_BHP_BASE_COST) + cost_in_size(stack, finalize_types, commit.operands(), HASH_BHP_PER_BYTE_COST, HASH_BHP_BASE_COST) } Command::Instruction(Instruction::CommitPED64(commit)) => { - cost_in_size(stack, finalize, commit.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST) + cost_in_size(stack, finalize_types, commit.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST) } Command::Instruction(Instruction::CommitPED128(commit)) => { - cost_in_size(stack, finalize, commit.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST) + cost_in_size(stack, finalize_types, commit.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST) } Command::Instruction(Instruction::Div(div)) => { // Ensure `div` has exactly two operands. ensure!(div.operands().len() == 2, "'div' must contain exactly 2 operands"); - // Retrieve the finalize types. - let finalize_types = stack.get_finalize_types(finalize.name())?; // Retrieve the price by the operand type. match finalize_types.get_type_from_operand(stack, &div.operands()[0])? { FinalizeType::Plaintext(PlaintextType::Literal(LiteralType::Field)) => Ok(1_500), @@ -275,49 +277,49 @@ pub fn cost_per_command( Command::Instruction(Instruction::GreaterThan(_)) => Ok(500), Command::Instruction(Instruction::GreaterThanOrEqual(_)) => Ok(500), Command::Instruction(Instruction::HashBHP256(hash)) => { - cost_in_size(stack, finalize, hash.operands(), HASH_BHP_PER_BYTE_COST, HASH_BHP_BASE_COST) + cost_in_size(stack, finalize_types, hash.operands(), HASH_BHP_PER_BYTE_COST, HASH_BHP_BASE_COST) } Command::Instruction(Instruction::HashBHP512(hash)) => { - cost_in_size(stack, finalize, hash.operands(), HASH_BHP_PER_BYTE_COST, HASH_BHP_BASE_COST) + cost_in_size(stack, finalize_types, hash.operands(), HASH_BHP_PER_BYTE_COST, HASH_BHP_BASE_COST) } Command::Instruction(Instruction::HashBHP768(hash)) => { - cost_in_size(stack, finalize, hash.operands(), HASH_BHP_PER_BYTE_COST, HASH_BHP_BASE_COST) + cost_in_size(stack, finalize_types, hash.operands(), HASH_BHP_PER_BYTE_COST, HASH_BHP_BASE_COST) } Command::Instruction(Instruction::HashBHP1024(hash)) => { - cost_in_size(stack, finalize, hash.operands(), HASH_BHP_PER_BYTE_COST, HASH_BHP_BASE_COST) + cost_in_size(stack, finalize_types, hash.operands(), HASH_BHP_PER_BYTE_COST, HASH_BHP_BASE_COST) } Command::Instruction(Instruction::HashKeccak256(hash)) => { - cost_in_size(stack, finalize, hash.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST) + cost_in_size(stack, finalize_types, hash.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST) } Command::Instruction(Instruction::HashKeccak384(hash)) => { - cost_in_size(stack, finalize, hash.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST) + cost_in_size(stack, finalize_types, hash.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST) } Command::Instruction(Instruction::HashKeccak512(hash)) => { - cost_in_size(stack, finalize, hash.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST) + cost_in_size(stack, finalize_types, hash.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST) } Command::Instruction(Instruction::HashPED64(hash)) => { - cost_in_size(stack, finalize, hash.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST) + cost_in_size(stack, finalize_types, hash.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST) } Command::Instruction(Instruction::HashPED128(hash)) => { - cost_in_size(stack, finalize, hash.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST) + cost_in_size(stack, finalize_types, hash.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST) } Command::Instruction(Instruction::HashPSD2(hash)) => { - cost_in_size(stack, finalize, hash.operands(), HASH_PSD_PER_BYTE_COST, HASH_PSD_BASE_COST) + cost_in_size(stack, finalize_types, hash.operands(), HASH_PSD_PER_BYTE_COST, HASH_PSD_BASE_COST) } Command::Instruction(Instruction::HashPSD4(hash)) => { - cost_in_size(stack, finalize, hash.operands(), HASH_PSD_PER_BYTE_COST, HASH_PSD_BASE_COST) + cost_in_size(stack, finalize_types, hash.operands(), HASH_PSD_PER_BYTE_COST, HASH_PSD_BASE_COST) } Command::Instruction(Instruction::HashPSD8(hash)) => { - cost_in_size(stack, finalize, hash.operands(), HASH_PSD_PER_BYTE_COST, HASH_PSD_BASE_COST) + cost_in_size(stack, finalize_types, hash.operands(), HASH_PSD_PER_BYTE_COST, HASH_PSD_BASE_COST) } Command::Instruction(Instruction::HashSha3_256(hash)) => { - cost_in_size(stack, finalize, hash.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST) + cost_in_size(stack, finalize_types, hash.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST) } Command::Instruction(Instruction::HashSha3_384(hash)) => { - cost_in_size(stack, finalize, hash.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST) + cost_in_size(stack, finalize_types, hash.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST) } Command::Instruction(Instruction::HashSha3_512(hash)) => { - cost_in_size(stack, finalize, hash.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST) + cost_in_size(stack, finalize_types, hash.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST) } Command::Instruction(Instruction::HashManyPSD2(_)) => { bail!("`hash_many.psd2` is not supported in finalize") @@ -337,8 +339,6 @@ pub fn cost_per_command( Command::Instruction(Instruction::Mul(mul)) => { // Ensure `mul` has exactly two operands. ensure!(mul.operands().len() == 2, "'mul' must contain exactly 2 operands"); - // Retrieve the finalize types. - let finalize_types = stack.get_finalize_types(finalize.name())?; // Retrieve the price by operand type. match finalize_types.get_type_from_operand(stack, &mul.operands()[0])? { FinalizeType::Plaintext(PlaintextType::Literal(LiteralType::Group)) => Ok(10_000), @@ -358,8 +358,6 @@ pub fn cost_per_command( Command::Instruction(Instruction::Pow(pow)) => { // Ensure `pow` has at least one operand. ensure!(!pow.operands().is_empty(), "'pow' must contain at least 1 operand"); - // Retrieve the finalize types. - let finalize_types = stack.get_finalize_types(finalize.name())?; // Retrieve the price by operand type. match finalize_types.get_type_from_operand(stack, &pow.operands()[0])? { FinalizeType::Plaintext(PlaintextType::Literal(LiteralType::Field)) => Ok(1_500), @@ -373,7 +371,7 @@ pub fn cost_per_command( Command::Instruction(Instruction::Rem(_)) => Ok(500), Command::Instruction(Instruction::RemWrapped(_)) => Ok(500), Command::Instruction(Instruction::SignVerify(sign)) => { - cost_in_size(stack, finalize, sign.operands(), HASH_PSD_PER_BYTE_COST, HASH_PSD_BASE_COST) + cost_in_size(stack, finalize_types, sign.operands(), HASH_PSD_PER_BYTE_COST, HASH_PSD_BASE_COST) } Command::Instruction(Instruction::Shl(_)) => Ok(500), Command::Instruction(Instruction::ShlWrapped(_)) => Ok(500), @@ -387,24 +385,46 @@ pub fn cost_per_command( Command::Instruction(Instruction::Xor(_)) => Ok(500), Command::Await(_) => Ok(500), Command::Contains(command) => { - cost_in_size(stack, finalize, [command.key()], MAPPING_PER_BYTE_COST, mapping_base_cost) + cost_in_size(stack, finalize_types, [command.key()], MAPPING_PER_BYTE_COST, mapping_base_cost) } Command::Get(command) => { - cost_in_size(stack, finalize, [command.key()], MAPPING_PER_BYTE_COST, mapping_base_cost) + cost_in_size(stack, finalize_types, [command.key()], MAPPING_PER_BYTE_COST, mapping_base_cost) } Command::GetOrUse(command) => { - cost_in_size(stack, finalize, [command.key()], MAPPING_PER_BYTE_COST, mapping_base_cost) + cost_in_size(stack, finalize_types, [command.key()], MAPPING_PER_BYTE_COST, mapping_base_cost) } Command::RandChaCha(_) => Ok(25_000), Command::Remove(_) => Ok(SET_BASE_COST), Command::Set(command) => { - cost_in_size(stack, finalize, [command.key(), command.value()], SET_PER_BYTE_COST, SET_BASE_COST) + cost_in_size(stack, finalize_types, [command.key(), command.value()], SET_PER_BYTE_COST, SET_BASE_COST) } Command::BranchEq(_) | Command::BranchNeq(_) => Ok(500), Command::Position(_) => Ok(100), } } +/// Returns the minimum number of microcredits required to run the constructor in the given stack. +/// If a constructor does not exist, no cost is incurred. +pub fn constructor_cost_in_microcredits(stack: &Stack) -> Result { + match stack.program().constructor() { + Some(constructor) => { + // Get the constructor types. + let constructor_types = stack.get_constructor_types()?; + // Get the base cost of the constructor. + let base_cost = constructor + .commands() + .iter() + .map(|command| cost_per_command(stack, constructor_types, command, ConsensusFeeVersion::V2)) + .try_fold(0u64, |acc, res| { + res.and_then(|x| acc.checked_add(x).ok_or(anyhow!("Constructor cost overflowed"))) + })?; + // Scale by the multiplier. + base_cost.checked_mul(N::CONSTRUCTOR_FEE_MULTIPLIER).ok_or(anyhow!("Constructor cost overflowed")) + } + None => Ok(0), + } +} + /// Returns the minimum number of microcredits required to run the finalize. pub fn cost_in_microcredits_v2(stack: &Stack, function_name: &Identifier) -> Result { cost_in_microcredits(stack, function_name, ConsensusFeeVersion::V2) @@ -449,11 +469,13 @@ fn cost_in_microcredits( finalizes.push((StackRef::External(external_stack), *future.resource())); } } + // Get the finalize types. + let finalize_types = stack_ref.get_finalize_types(finalize.name())?; // Iterate over the commands in the finalize block. for command in finalize.commands() { // Sum the cost of all commands in the current future into the total running cost. finalize_cost = finalize_cost - .checked_add(cost_per_command(&stack_ref, finalize, command, consensus_fee_version)?) + .checked_add(cost_per_command(&stack_ref, finalize_types, command, consensus_fee_version)?) .ok_or(anyhow!("Finalize cost overflowed"))?; } } @@ -465,6 +487,7 @@ fn cost_in_microcredits( mod tests { use super::*; use crate::test_helpers::get_execution; + use circuit::{Aleo, AleoCanaryV0, AleoTestnetV0, AleoV0}; use console::network::{CanaryV0, MainnetV0, TestnetV0}; use synthesizer_program::Program; @@ -548,4 +571,95 @@ function over_five_thousand: assert_eq!(storage_cost_under_5000, execution_storage_cost::(execution_size_under_5000)); assert_eq!(storage_cost_over_5000, execution_storage_cost::(execution_size_over_5000)); } + + #[test] + fn test_deployment_cost_with_constructors() { + // A helper to run the test. + fn run_test>() { + let process = Process::::load().unwrap(); + let rng = &mut TestRng::default(); + + // Define the programs. + let program_0 = Program::from_str( + r" +program program_with_constructor.aleo; + +constructor: + assert.eq true true; + +mapping foo: + key as field.public; + value as field.public; + +function dummy:", + ) + .unwrap(); + + let program_1 = Program::from_str( + r" +program program_with_constructor.aleo; + +constructor: + assert.eq edition 0u16; + +mapping foo: + key as field.public; + value as field.public; + +function dummy:", + ) + .unwrap(); + + let program_2 = Program::from_str( + r" +program program_with_constructor.aleo; + +constructor: + get foo[0field] into r0; + +mapping foo: + key as field.public; + value as field.public; + +function dummy:", + ) + .unwrap(); + + let program_3 = Program::from_str( + r" +program program_with_constructor.aleo; + +constructor: + set 0field into foo[0field]; + +mapping foo: + key as field.public; + value as field.public; + +function dummy:", + ) + .unwrap(); + + // Verify the deployment costs. + let deployment_0 = process.deploy::(&program_0, rng).unwrap(); + assert_eq!(deployment_cost(&process, &deployment_0).unwrap(), (2442725, (815000, 577725, 50000, 1000000))); + + let deployment_1 = process.deploy::(&program_1, rng).unwrap(); + assert_eq!(deployment_cost(&process, &deployment_1).unwrap(), (2441725, (814000, 577725, 50000, 1000000))); + + let deployment_2 = process.deploy::(&program_2, rng).unwrap(); + assert_eq!(deployment_cost(&process, &deployment_2).unwrap(), (2606725, (847000, 577725, 182000, 1000000))); + + let deployment_3 = process.deploy::(&program_3, rng).unwrap(); + assert_eq!( + deployment_cost(&process, &deployment_3).unwrap(), + (4096725, (879000, 577725, 1640000, 1000000)) + ); + } + + // Run the tests for all networks. + run_test::(); + run_test::(); + run_test::(); + } } diff --git a/synthesizer/process/src/finalize.rs b/synthesizer/process/src/finalize.rs index b289881af0..62856e3656 100644 --- a/synthesizer/process/src/finalize.rs +++ b/synthesizer/process/src/finalize.rs @@ -66,8 +66,17 @@ impl Process { // Initialize the mapping. finalize_operations.push(store.initialize_mapping(*program_id, *mapping.name())?); } - finish!(timer, "Initialize the program mappings"); + lap!(timer, "Initialize the program mappings"); + + // If the program has a constructor, execute it and extend the finalize operations. + // This must happen after the mappings are initialized as the constructor may depend on them. + if deployment.program().contains_constructor() { + let operations = finalize_constructor(state, store, &stack, *fee.transition_id())?; + finalize_operations.extend(operations); + lap!(timer, "Execute the constructor"); + } + finish!(timer, "Finished finalizing the deployment"); // Return the stack and finalize operations. Ok((stack, finalize_operations)) }) @@ -183,6 +192,101 @@ fn finalize_fee_transition>( } } +/// Finalizes the constructor. +fn finalize_constructor>( + state: FinalizeGlobalState, + store: &FinalizeStore, + stack: &Stack, + transition_id: N::TransitionID, +) -> Result>> { + // Retrieve the program ID. + let program_id = stack.program_id(); + #[cfg(debug_assertions)] + println!("Finalizing constructor for {}...", stack.program_id()); + + // Initialize a list for finalize operations. + let mut finalize_operations = Vec::new(); + + // Initialize a nonce for the constructor registers. + // Currently, this nonce is set to zero for every constructor. + let nonce = 0; + + // Get the constructor logic. If the program does not have a constructor, return early. + let Some(constructor) = stack.program().constructor() else { + return Ok(finalize_operations); + }; + + // Get the constructor types. + let constructor_types = stack.get_constructor_types()?.clone(); + + // Initialize the finalize registers. + let mut registers = FinalizeRegisters::new(state, transition_id, *program_id.name(), constructor_types, nonce); + + // Initialize a counter for the commands. + let mut counter = 0; + + // Evaluate the commands. + while counter < constructor.commands().len() { + // Retrieve the command. + let command = &constructor.commands()[counter]; + // Finalize the command. + match &command { + Command::BranchEq(branch_eq) => { + let result = + try_vm_runtime!(|| branch_to(counter, branch_eq, constructor.positions(), stack, ®isters)); + match result { + Ok(Ok(new_counter)) => { + counter = new_counter; + } + // If the evaluation fails, bail and return the error. + Ok(Err(error)) => bail!("'constructor' failed to evaluate command ({command}): {error}"), + // If the evaluation fails, bail and return the error. + Err(_) => bail!("'constructor' failed to evaluate command ({command})"), + } + } + Command::BranchNeq(branch_neq) => { + let result = + try_vm_runtime!(|| branch_to(counter, branch_neq, constructor.positions(), stack, ®isters)); + match result { + Ok(Ok(new_counter)) => { + counter = new_counter; + } + // If the evaluation fails, bail and return the error. + Ok(Err(error)) => bail!("'constructor' failed to evaluate command ({command}): {error}"), + // If the evaluation fails, bail and return the error. + Err(_) => bail!("'constructor' failed to evaluate command ({command})"), + } + } + Command::Await(_) => { + bail!("Cannot `await` a Future in a constructor") + } + _ => { + let result = try_vm_runtime!(|| command.finalize(stack, store, &mut registers)); + match result { + // If the evaluation succeeds with an operation, add it to the list. + Ok(Ok(Some(finalize_operation))) => finalize_operations.push(finalize_operation), + // If the evaluation succeeds with no operation, continue. + Ok(Ok(None)) => {} + // If the evaluation fails, bail and return the error. + Ok(Err(error)) => { + println!("'constructor' failed to evaluate command ({command}): {error}"); + bail!("'constructor' failed to evaluate command ({command}): {error}") + } + // If the evaluation fails, bail and return the error. + Err(_) => { + println!("'constructor' failed to evaluate command ({command})"); + bail!("'constructor' failed to evaluate command ({command})") + } + } + counter += 1; + } + }; + } + + // Return the finalize operations. + Ok(finalize_operations) +} + /// Finalizes the given transition. fn finalize_transition>( state: FinalizeGlobalState, diff --git a/synthesizer/process/src/stack/evaluate.rs b/synthesizer/process/src/stack/evaluate.rs index 6f3cd4c0df..3124782095 100644 --- a/synthesizer/process/src/stack/evaluate.rs +++ b/synthesizer/process/src/stack/evaluate.rs @@ -85,6 +85,22 @@ impl StackEvaluate for Stack { Operand::BlockHeight => bail!("Cannot retrieve the block height from a closure scope."), // If the operand is the network id, throw an error. Operand::NetworkID => bail!("Cannot retrieve the network ID from a closure scope."), + // If the operand is the program checksum, retrieve the checksum from the stack. + Operand::Checksum(program_id) => { + let checksum = match program_id { + Some(program_id) => *self.get_external_stack(program_id)?.program_checksum(), + None => *self.program_checksum(), + }; + Ok(Value::Plaintext(Plaintext::from(Literal::Field(checksum)))) + } + // If the operand is the program edition, retrieve the edition from the stack. + Operand::Edition(program_id) => { + let edition = match program_id { + Some(program_id) => *self.get_external_stack(program_id)?.program_edition(), + None => *self.program_edition(), + }; + Ok(Value::Plaintext(Plaintext::from(Literal::U16(edition)))) + } } }) .collect(); @@ -218,6 +234,22 @@ impl StackEvaluate for Stack { Operand::BlockHeight => bail!("Cannot retrieve the block height from a function scope."), // If the operand is the network id, throw an error. Operand::NetworkID => bail!("Cannot retrieve the network ID from a function scope."), + // If the operand is the program checksum, retrieve the checksum from the stack. + Operand::Checksum(program_id) => { + let checksum = match program_id { + Some(program_id) => *self.get_external_stack(program_id)?.program_checksum(), + None => *self.program_checksum(), + }; + Ok(Value::Plaintext(Plaintext::from(Literal::Field(checksum)))) + } + // If the operand is the program edition, retrieve the edition from the stack. + Operand::Edition(program_id) => { + let edition = match program_id { + Some(program_id) => *self.get_external_stack(program_id)?.program_edition(), + None => *self.program_edition(), + }; + Ok(Value::Plaintext(Plaintext::from(Literal::U16(edition)))) + } } }) .collect::>>()?; diff --git a/synthesizer/process/src/stack/execute.rs b/synthesizer/process/src/stack/execute.rs index 5dc96f8dc6..23f750c379 100644 --- a/synthesizer/process/src/stack/execute.rs +++ b/synthesizer/process/src/stack/execute.rs @@ -120,6 +120,26 @@ impl StackExecute for Stack { Operand::NetworkID => { bail!("Illegal operation: cannot retrieve the network id in a closure scope") } + // If the operand is the checksum, retrieve the checksum from the stack. + Operand::Checksum(program_id) => { + let checksum = match program_id { + Some(program_id) => *self.get_external_stack(program_id)?.program_checksum(), + None => *self.program_checksum(), + }; + Ok(circuit::Value::Plaintext(circuit::Plaintext::from(circuit::Literal::Field( + circuit::Field::new(circuit::Mode::Constant, checksum), + )))) + } + // If the operand is the edition, retrieve the edition from the stack. + Operand::Edition(program_id) => { + let edition = match program_id { + Some(program_id) => *self.get_external_stack(program_id)?.program_edition(), + None => *self.program_edition(), + }; + Ok(circuit::Value::Plaintext(circuit::Plaintext::from(circuit::Literal::U16( + circuit::U16::new(circuit::Mode::Constant, edition), + )))) + } } }) .collect(); @@ -355,6 +375,26 @@ impl StackExecute for Stack { Operand::NetworkID => { bail!("Illegal operation: cannot retrieve the network id in a function scope") } + // If the operand is the checksum, retrieve the checksum from the stack. + Operand::Checksum(program_id) => { + let checksum = match program_id { + Some(program_id) => *self.get_external_stack(program_id)?.program_checksum(), + None => *self.program_checksum(), + }; + Ok(circuit::Value::Plaintext(circuit::Plaintext::from(circuit::Literal::Field( + circuit::Field::new(circuit::Mode::Constant, checksum), + )))) + } + // If the operand is the edition, retrieve the edition from the stack. + Operand::Edition(program_id) => { + let edition = match program_id { + Some(program_id) => *self.get_external_stack(program_id)?.program_edition(), + None => *self.program_edition(), + }; + Ok(circuit::Value::Plaintext(circuit::Plaintext::from(circuit::Literal::U16( + circuit::U16::new(circuit::Mode::Constant, edition), + )))) + } } }) .collect::>>()?; diff --git a/synthesizer/process/src/stack/finalize_registers/load.rs b/synthesizer/process/src/stack/finalize_registers/load.rs index b00fdbcdd5..1c55aff943 100644 --- a/synthesizer/process/src/stack/finalize_registers/load.rs +++ b/synthesizer/process/src/stack/finalize_registers/load.rs @@ -46,6 +46,22 @@ impl RegistersLoad for FinalizeRegisters { Operand::NetworkID => { return Ok(Value::Plaintext(Plaintext::from(Literal::U16(U16::new(N::ID))))); } + // If the operand is the checksum, load the checksum. + Operand::Checksum(program_id) => { + let checksum = match program_id { + Some(program_id) => *stack.get_external_stack(program_id)?.program_checksum(), + None => *stack.program_checksum(), + }; + return Ok(Value::Plaintext(Plaintext::from(Literal::Field(checksum)))); + } + // If the operand is the edition, load the edition. + Operand::Edition(program_id) => { + let edition = match program_id { + Some(program_id) => *stack.get_external_stack(program_id)?.program_edition(), + None => *stack.program_edition(), + }; + return Ok(Value::Plaintext(Plaintext::from(Literal::U16(edition)))); + } }; // Retrieve the value. diff --git a/synthesizer/process/src/stack/finalize_types/initialize.rs b/synthesizer/process/src/stack/finalize_types/initialize.rs index 08c6a2d5c7..54362ef568 100644 --- a/synthesizer/process/src/stack/finalize_types/initialize.rs +++ b/synthesizer/process/src/stack/finalize_types/initialize.rs @@ -16,6 +16,29 @@ use super::*; impl FinalizeTypes { + /// Initializes a new instance of `FinalizeTypes` for the given constructor. + /// Checks that the given constructor is well-formed for the given stack. + #[inline] + pub(super) fn initialize_finalize_types_from_constructor( + stack: &(impl StackMatches + StackProgram), + constructor: &Constructor, + ) -> Result { + // Initialize a map of registers to their types. + let mut finalize_types = Self { inputs: IndexMap::new(), destinations: IndexMap::new() }; + + // Check the commands are well-formed. + for command in constructor.commands() { + // Verify that the command is not an await. + if command.is_await() { + bail!("`await` commands are not allowed in constructors.") + } + // Check the command opcode, operands, and destinations. + finalize_types.check_command(stack, constructor.positions(), command)?; + } + + Ok(finalize_types) + } + /// Initializes a new instance of `FinalizeTypes` for the given finalize. /// Checks that the given finalize is well-formed for the given stack. /// @@ -25,7 +48,7 @@ impl FinalizeTypes { /// whose finalize is not well-formed, but it is not possible to execute a program whose finalize /// is not well-formed. #[inline] - pub(super) fn initialize_finalize_types( + pub(super) fn initialize_finalize_types_from_finalize( stack: &(impl StackMatches + StackProgram), finalize: &Finalize, ) -> Result { @@ -52,7 +75,7 @@ impl FinalizeTypes { // Step 2. Check the commands are well-formed. Make sure all the input futures are awaited. for command in finalize.commands() { // Check the command opcode, operands, and destinations. - finalize_types.check_command(stack, finalize, command)?; + finalize_types.check_command(stack, finalize.positions(), command)?; // If the command is an `await`, add the future to the set of consumed futures. if let Command::Await(await_) = command { @@ -144,7 +167,13 @@ impl FinalizeTypes { RegisterTypes::check_struct(stack, struct_name)? } FinalizeType::Plaintext(PlaintextType::Array(array_type)) => RegisterTypes::check_array(stack, array_type)?, - FinalizeType::Future(..) => (), + FinalizeType::Future(locator) => { + ensure!( + stack.program().contains_import(locator.program_id()), + "Program '{locator}' is not imported by '{}'.", + stack.program().id() + ) + } }; // Insert the input register. @@ -163,20 +192,20 @@ impl FinalizeTypes { fn check_command( &mut self, stack: &(impl StackMatches + StackProgram), - finalize: &Finalize, + positions: &HashMap, usize>, command: &Command, ) -> Result<()> { match command { - Command::Instruction(instruction) => self.check_instruction(stack, finalize.name(), instruction)?, + Command::Instruction(instruction) => self.check_instruction(stack, instruction)?, Command::Await(await_) => self.check_await(stack, await_)?, Command::Contains(contains) => self.check_contains(stack, contains)?, Command::Get(get) => self.check_get(stack, get)?, Command::GetOrUse(get_or_use) => self.check_get_or_use(stack, get_or_use)?, - Command::RandChaCha(rand_chacha) => self.check_rand_chacha(stack, finalize.name(), rand_chacha)?, - Command::Remove(remove) => self.check_remove(stack, finalize.name(), remove)?, - Command::Set(set) => self.check_set(stack, finalize.name(), set)?, - Command::BranchEq(branch_eq) => self.check_branch(stack, finalize, branch_eq)?, - Command::BranchNeq(branch_neq) => self.check_branch(stack, finalize, branch_neq)?, + Command::RandChaCha(rand_chacha) => self.check_rand_chacha(stack, rand_chacha)?, + Command::Remove(remove) => self.check_remove(stack, remove)?, + Command::Set(set) => self.check_set(stack, set)?, + Command::BranchEq(branch_eq) => self.check_branch(stack, positions, branch_eq)?, + Command::BranchNeq(branch_neq) => self.check_branch(stack, positions, branch_neq)?, // Note that the `Position`s are checked for uniqueness when constructing `Finalize`. Command::Position(_) => (), } @@ -207,7 +236,7 @@ impl FinalizeTypes { fn check_branch( &mut self, stack: &(impl StackMatches + StackProgram), - finalize: &Finalize, + positions: &HashMap, usize>, branch: &Branch, ) -> Result<()> { // Get the type of the first operand. @@ -234,7 +263,7 @@ impl FinalizeTypes { ); // Check that the `Position` has been defined. ensure!( - finalize.positions().get(branch.position()).is_some(), + positions.get(branch.position()).is_some(), "Command '{}' expects a defined position to jump to. Found undefined position '{}'", Branch::::opcode(), branch.position() @@ -459,7 +488,6 @@ impl FinalizeTypes { fn check_rand_chacha( &mut self, _stack: &(impl StackMatches + StackProgram), - _finalize_name: &Identifier, rand_chacha: &RandChaCha, ) -> Result<()> { // Ensure the number of operands is within bounds. @@ -487,15 +515,10 @@ impl FinalizeTypes { /// Ensures the given `set` command is well-formed. #[inline] - fn check_set( - &self, - stack: &(impl StackMatches + StackProgram), - finalize_name: &Identifier, - set: &Set, - ) -> Result<()> { + fn check_set(&self, stack: &(impl StackMatches + StackProgram), set: &Set) -> Result<()> { // Ensure the declared mapping in `set` is defined in the program. if !stack.program().contains_mapping(set.mapping_name()) { - bail!("Mapping '{}' in '{}/{finalize_name}' is not defined.", set.mapping_name(), stack.program_id()) + bail!("Mapping '{}' in '{}' is not defined.", set.mapping_name(), stack.program_id()) } // Retrieve the mapping from the program. // Note that the unwrap is safe, as we have already checked the mapping exists. @@ -533,15 +556,10 @@ impl FinalizeTypes { /// Ensures the given `remove` command is well-formed. #[inline] - fn check_remove( - &self, - stack: &(impl StackMatches + StackProgram), - finalize_name: &Identifier, - remove: &Remove, - ) -> Result<()> { + fn check_remove(&self, stack: &(impl StackMatches + StackProgram), remove: &Remove) -> Result<()> { // Ensure the declared mapping in `remove` is defined in the program. if !stack.program().contains_mapping(remove.mapping_name()) { - bail!("Mapping '{}' in '{}/{finalize_name}' is not defined.", remove.mapping_name(), stack.program_id()) + bail!("Mapping '{}' in '{}' is not defined.", remove.mapping_name(), stack.program_id()) } // Retrieve the mapping from the program. // Note that the unwrap is safe, as we have already checked the mapping exists. @@ -567,11 +585,10 @@ impl FinalizeTypes { fn check_instruction( &mut self, stack: &(impl StackMatches + StackProgram), - finalize_name: &Identifier, instruction: &Instruction, ) -> Result<()> { // Ensure the opcode is well-formed. - self.check_instruction_opcode(stack, finalize_name, instruction)?; + self.check_instruction_opcode(stack, instruction)?; // Initialize a vector to store the register types of the operands. let mut operand_types = Vec::with_capacity(instruction.operands().len()); @@ -608,7 +625,6 @@ impl FinalizeTypes { fn check_instruction_opcode( &mut self, stack: &(impl StackMatches + StackProgram), - finalize_name: &Identifier, instruction: &Instruction, ) -> Result<()> { match instruction.opcode() { @@ -709,7 +725,7 @@ impl FinalizeTypes { _ => bail!("Instruction '{instruction}' is not for opcode '{opcode}'."), }, Opcode::Command(opcode) => { - bail!("Fatal error: Cannot check command '{opcode}' as an instruction in 'finalize {finalize_name}'.") + bail!("Fatal error: Cannot check command '{opcode}' as an instruction.") } Opcode::Commit(opcode) => RegisterTypes::check_commit_opcode(opcode, instruction)?, Opcode::Hash(opcode) => RegisterTypes::check_hash_opcode(opcode, instruction)?, diff --git a/synthesizer/process/src/stack/finalize_types/matches.rs b/synthesizer/process/src/stack/finalize_types/matches.rs index 55b03abdbc..a3dae53bd8 100644 --- a/synthesizer/process/src/stack/finalize_types/matches.rs +++ b/synthesizer/process/src/stack/finalize_types/matches.rs @@ -107,6 +107,26 @@ impl FinalizeTypes { "Struct member '{struct_name}.{member_name}' expects {member_type}, but found '{network_id_type}' in the operand '{operand}'.", ) } + // Ensure the checksum type (field) matches the member type. + Operand::Checksum(_) => { + // Retrieve the checksum type. + let checksum_type = PlaintextType::Literal(LiteralType::Field); + // Ensure the checksum type matches the member type. + ensure!( + &checksum_type == member_type, + "Struct member '{struct_name}.{member_name}' expects {member_type}, but found '{checksum_type}' in the operand '{operand}'.", + ) + } + // Ensure the edition type (u16) matches the member type. + Operand::Edition(_) => { + // Retrieve the edition type. + let edition_type = PlaintextType::Literal(LiteralType::U16); + // Ensure the edition type matches the member type. + ensure!( + &edition_type == member_type, + "Struct member '{struct_name}.{member_name}' expects {member_type}, but found '{edition_type}' in the operand '{operand}'.", + ) + } } } Ok(()) @@ -199,6 +219,28 @@ impl FinalizeTypes { array_type.next_element_type() ) } + // Ensure the checksum type (field) matches the member type. + Operand::Checksum(_) => { + // Retrieve the checksum type. + let checksum_type = PlaintextType::Literal(LiteralType::Field); + // Ensure the checksum type matches the member type. + ensure!( + &checksum_type == array_type.next_element_type(), + "Array element expects {}, but found '{checksum_type}' in the operand '{operand}'.", + array_type.next_element_type() + ) + } + // Ensure the edition type (u16) matches the member type. + Operand::Edition(_) => { + // Retrieve the edition type. + let edition_type = PlaintextType::Literal(LiteralType::U16); + // Ensure the edition type matches the member type. + ensure!( + &edition_type == array_type.next_element_type(), + "Array element expects {}, but found '{edition_type}' in the operand '{operand}'.", + array_type.next_element_type() + ) + } } } Ok(()) diff --git a/synthesizer/process/src/stack/finalize_types/mod.rs b/synthesizer/process/src/stack/finalize_types/mod.rs index 98fbbbdf12..fe9d7bbd10 100644 --- a/synthesizer/process/src/stack/finalize_types/mod.rs +++ b/synthesizer/process/src/stack/finalize_types/mod.rs @@ -38,6 +38,8 @@ use synthesizer_program::{ CallOperator, CastType, Command, + CommandTrait, + Constructor, Contains, Finalize, Get, @@ -56,7 +58,7 @@ use synthesizer_program::{ }; use indexmap::IndexMap; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; #[derive(Clone, Default, PartialEq, Eq)] pub struct FinalizeTypes { @@ -69,11 +71,21 @@ pub struct FinalizeTypes { } impl FinalizeTypes { + /// Initializes a new instance of `FinalizeTypes` for the given constructor. + /// Checks that the given constructor is well-formed for the given stack. + #[inline] + pub fn from_constructor( + stack: &(impl StackMatches + StackProgram), + constructor: &Constructor, + ) -> Result { + Self::initialize_finalize_types_from_constructor(stack, constructor) + } + /// Initializes a new instance of `FinalizeTypes` for the given finalize. /// Checks that the given finalize is well-formed for the given stack. #[inline] pub fn from_finalize(stack: &(impl StackMatches + StackProgram), finalize: &Finalize) -> Result { - Self::initialize_finalize_types(stack, finalize) + Self::initialize_finalize_types_from_finalize(stack, finalize) } /// Returns `true` if the given register exists. @@ -104,6 +116,8 @@ impl FinalizeTypes { Operand::Caller => bail!("'self.caller' is not a valid operand in a finalize context."), Operand::BlockHeight => FinalizeType::Plaintext(PlaintextType::Literal(LiteralType::U32)), Operand::NetworkID => FinalizeType::Plaintext(PlaintextType::Literal(LiteralType::U16)), + Operand::Checksum(_) => FinalizeType::Plaintext(PlaintextType::Literal(LiteralType::Field)), + Operand::Edition(_) => FinalizeType::Plaintext(PlaintextType::Literal(LiteralType::U16)), }) } diff --git a/synthesizer/process/src/stack/helpers/initialize.rs b/synthesizer/process/src/stack/helpers/initialize.rs index 89e22bc887..da32142557 100644 --- a/synthesizer/process/src/stack/helpers/initialize.rs +++ b/synthesizer/process/src/stack/helpers/initialize.rs @@ -23,12 +23,15 @@ impl Stack { let mut stack = Self { program: program.clone(), stacks: Arc::downgrade(&process.stacks), + constructor_types: Default::default(), register_types: Default::default(), finalize_types: Default::default(), universal_srs: process.universal_srs().clone(), proving_keys: Default::default(), verifying_keys: Default::default(), program_address: program.id().to_address()?, + program_checksum: program.checksum()?, + program_edition: U16::new(N::EDITION), }; // Add all the imports into the stack. @@ -40,6 +43,21 @@ impl Stack { bail!("Cannot add program '{}' because its import '{import}' must be added first", program.id()) } } + + // Add the constructor to the stack if it exists. + if let Some(constructor) = program.constructor() { + // Add the constructor to the stack. + stack.insert_constructor(constructor)?; + // Get the constructor cost. + let constructor_cost = constructor_cost_in_microcredits(&stack)?; + // Check that the constructor cost does not exceed the maximum. + ensure!( + constructor_cost <= N::TRANSACTION_SPEND_LIMIT, + "Constructor has a cost '{constructor_cost}' which exceeds the transaction spend limit '{}'", + N::TRANSACTION_SPEND_LIMIT + ); + } + // Add the program closures to the stack. for closure in program.closures().values() { // Add the closure to the stack. @@ -71,6 +89,20 @@ impl Stack { } impl Stack { + /// Adds the constructor to the stack. + #[inline] + fn insert_constructor(&mut self, constructor: &Constructor) -> Result<()> { + // Ensure that the constsuctor is not already added. + ensure!(self.constructor_types.is_none(), "Constructor already exists"); + + // Compute the constructor types. + let constructor_types = FinalizeTypes::from_constructor(self, constructor)?; + // Add the constructor types to the stack. + self.constructor_types = Some(constructor_types); + // Return success. + Ok(()) + } + /// Inserts the given closure to the stack. #[inline] fn insert_closure(&mut self, closure: &Closure) -> Result<()> { @@ -78,7 +110,6 @@ impl Stack { let name = closure.name(); // Ensure the closure name is not already added. ensure!(!self.register_types.contains_key(name), "Closure '{name}' already exists"); - // Compute the register types. let register_types = RegisterTypes::from_closure(self, closure)?; // Add the closure name and register types to the stack. diff --git a/synthesizer/process/src/stack/mod.rs b/synthesizer/process/src/stack/mod.rs index 68a08ab14f..40ae5de78e 100644 --- a/synthesizer/process/src/stack/mod.rs +++ b/synthesizer/process/src/stack/mod.rs @@ -37,7 +37,7 @@ mod evaluate; mod execute; mod helpers; -use crate::{CallMetrics, Process, Trace, cost_in_microcredits_v2, traits::*}; +use crate::{CallMetrics, Process, Trace, constructor_cost_in_microcredits, cost_in_microcredits_v2, traits::*}; use console::{ account::{Address, PrivateKey}, network::prelude::*, @@ -62,10 +62,10 @@ use console::{ Value, ValueType, }, - types::{Field, Group}, + types::{Field, Group, U16}, }; use ledger_block::{Deployment, Transaction, Transition}; -use synthesizer_program::{CallOperator, Closure, Function, Instruction, Operand, Program, traits::*}; +use synthesizer_program::{CallOperator, Closure, Constructor, Function, Instruction, Operand, Program, traits::*}; use synthesizer_snark::{Certificate, ProvingKey, UniversalSRS, VerifyingKey}; use aleo_std::prelude::{finish, lap, timer}; @@ -189,6 +189,8 @@ pub struct Stack { program: Program, /// A reference to the global stack map. stacks: Weak, Arc>>>>, + /// The register types for the program constructor, if it exists. + constructor_types: Option>, /// The mapping of closure and function names to their register types. register_types: IndexMap, RegisterTypes>, /// The mapping of finalize names to their register types. @@ -201,6 +203,10 @@ pub struct Stack { verifying_keys: Arc, VerifyingKey>>>, /// The program address. program_address: Address, + /// The program checksum. + program_checksum: Field, + /// The program edition. + program_edition: U16, } impl Stack { @@ -324,6 +330,18 @@ impl StackProgram for Stack { &self.program_address } + /// Returns the program checksum. + #[inline] + fn program_checksum(&self) -> &Field { + &self.program_checksum + } + + /// Returns the program edition. + #[inline] + fn program_edition(&self) -> &U16 { + &self.program_edition + } + /// Returns the external stack for the given program ID. /// /// Attention - this function is used to check the existence of the external program. @@ -461,6 +479,12 @@ impl StackProgram for Stack { } impl StackProgramTypes for Stack { + /// Returns the constructor types for the program. + #[inline] + fn get_constructor_types(&self) -> Result<&FinalizeTypes> { + self.constructor_types.as_ref().ok_or_else(|| anyhow!("Constructor types do not exist")) + } + /// Returns the register types for the given closure or function name. #[inline] fn get_register_types(&self, name: &Identifier) -> Result<&RegisterTypes> { diff --git a/synthesizer/process/src/stack/register_types/matches.rs b/synthesizer/process/src/stack/register_types/matches.rs index bb36a25083..76755171ff 100644 --- a/synthesizer/process/src/stack/register_types/matches.rs +++ b/synthesizer/process/src/stack/register_types/matches.rs @@ -93,6 +93,26 @@ impl RegisterTypes { Operand::NetworkID => bail!( "Struct member '{struct_name}.{member_name}' cannot be from a network ID in a non-finalize scope" ), + // Ensure the checksum type (field) matches the member type. + Operand::Checksum(_) => { + // Retrieve the operand type. + let operand_type = PlaintextType::Literal(LiteralType::Field); + // Ensure the operand type matches the member type. + ensure!( + &operand_type == member_type, + "Struct member '{struct_name}.{member_name}' expects {member_type}, but found '{operand_type}' in the operand '{operand}'.", + ) + } + // Ensure the edition type (u16) matches the member type. + Operand::Edition(_) => { + // Retrieve the operand type. + let operand_type = PlaintextType::Literal(LiteralType::U16); + // Ensure the operand type matches the member type. + ensure!( + &operand_type == member_type, + "Struct member '{struct_name}.{member_name}' expects {member_type}, but found '{operand_type}' in the operand '{operand}'.", + ) + } } } Ok(()) @@ -169,6 +189,28 @@ impl RegisterTypes { Operand::BlockHeight => bail!("Array element cannot be from a block height in a non-finalize scope"), // If the operand is a network ID type, throw an error. Operand::NetworkID => bail!("Array element cannot be from a network ID in a non-finalize scope"), + // Ensure the checksum type (field) matches the element type. + Operand::Checksum(_) => { + // Retrieve the operand type. + let operand_type = PlaintextType::Literal(LiteralType::Field); + // Ensure the operand type matches the element type. + ensure!( + &operand_type == array_type.next_element_type(), + "Array element expects {}, but found '{operand_type}' in the operand '{operand}'.", + array_type.next_element_type() + ) + } + // Ensure the edition type (u16) matches the element type. + Operand::Edition(_) => { + // Retrieve the operand type. + let operand_type = PlaintextType::Literal(LiteralType::U16); + // Ensure the operand type matches the element type. + ensure!( + &operand_type == array_type.next_element_type(), + "Array element expects {}, but found '{operand_type}' in the operand '{operand}'.", + array_type.next_element_type() + ) + } } } Ok(()) @@ -234,6 +276,12 @@ impl RegisterTypes { Operand::NetworkID => { bail!("Forbidden operation: Cannot cast a network ID as a record owner") } + Operand::Checksum(_) => { + bail!("Forbidden operation: Cannot cast a checksum as a record owner") + } + Operand::Edition(_) => { + bail!("Forbidden operation: Cannot cast an edition as a record owner") + } } // Ensure the operand types match the record entry types. @@ -295,6 +343,26 @@ impl RegisterTypes { "Record entry '{record_name}.{entry_name}' expects a '{plaintext_type}', but found a network ID in the operand '{operand}'." ) } + // Ensure the checksum type (field) matches the entry type. + Operand::Checksum(_) => { + // Retrieve the operand type. + let operand_type = &PlaintextType::Literal(LiteralType::Field); + // Ensure the operand type matches the entry type. + ensure!( + operand_type == plaintext_type, + "Record entry '{record_name}.{entry_name}' expects a '{plaintext_type}', but found '{operand_type}' in the operand '{operand}'.", + ) + } + // Ensure the edition type (u16) matches the entry type. + Operand::Edition(_) => { + // Retrieve the operand type. + let operand_type = &PlaintextType::Literal(LiteralType::U16); + // Ensure the operand type matches the entry type. + ensure!( + operand_type == plaintext_type, + "Record entry '{record_name}.{entry_name}' expects a '{plaintext_type}', but found '{operand_type}' in the operand '{operand}'.", + ) + } } } } diff --git a/synthesizer/process/src/stack/register_types/mod.rs b/synthesizer/process/src/stack/register_types/mod.rs index bcca7d17ee..88ab7b08c0 100644 --- a/synthesizer/process/src/stack/register_types/mod.rs +++ b/synthesizer/process/src/stack/register_types/mod.rs @@ -100,6 +100,8 @@ impl RegisterTypes { } Operand::BlockHeight => bail!("'block.height' is not a valid operand in a non-finalize context."), Operand::NetworkID => bail!("'network.id' is not a valid operand in a non-finalize context."), + Operand::Checksum(_) => RegisterType::Plaintext(PlaintextType::Literal(LiteralType::Field)), + Operand::Edition(_) => RegisterType::Plaintext(PlaintextType::Literal(LiteralType::U16)), }) } diff --git a/synthesizer/process/src/stack/registers/load.rs b/synthesizer/process/src/stack/registers/load.rs index 2f91d1c0d2..87161a09b1 100644 --- a/synthesizer/process/src/stack/registers/load.rs +++ b/synthesizer/process/src/stack/registers/load.rs @@ -41,6 +41,22 @@ impl> RegistersLoad for Registers bail!("Cannot load the block height in a non-finalize context"), // If the operand is the network ID, throw an error. Operand::NetworkID => bail!("Cannot load the network ID in a non-finalize context"), + // If the operand is the checksum, load the value of the checksum. + Operand::Checksum(program_id) => { + let checksum = match program_id { + Some(program_id) => *stack.get_external_stack(program_id)?.program_checksum(), + None => *stack.program_checksum(), + }; + return Ok(Value::Plaintext(Plaintext::from(Literal::Field(checksum)))); + } + // If the operand is the edition, load the value of the edition. + Operand::Edition(program_id) => { + let edition = match program_id { + Some(program_id) => *stack.get_external_stack(program_id)?.program_edition(), + None => *stack.program_edition(), + }; + return Ok(Value::Plaintext(Plaintext::from(Literal::U16(edition)))); + } }; // Retrieve the stack value. @@ -126,6 +142,26 @@ impl> RegistersLoadCircuit for R Operand::BlockHeight => bail!("Cannot load the block height in a non-finalize context"), // If the operand is the network ID, throw an error. Operand::NetworkID => bail!("Cannot load the network ID in a non-finalize context"), + // If the operand is the checksum, load the value of the checksum. + Operand::Checksum(program_id) => { + let checksum = match program_id { + Some(program_id) => *stack.get_external_stack(program_id)?.program_checksum(), + None => *stack.program_checksum(), + }; + return Ok(circuit::Value::Plaintext(circuit::Plaintext::from(circuit::Literal::constant( + Literal::Field(checksum), + )))); + } + // If the operand is the edition, load the value of the edition. + Operand::Edition(program_id) => { + let edition = match program_id { + Some(program_id) => *stack.get_external_stack(program_id)?.program_edition(), + None => *stack.program_edition(), + }; + return Ok(circuit::Value::Plaintext(circuit::Plaintext::from(circuit::Literal::constant( + Literal::U16(edition), + )))); + } }; // Retrieve the circuit value. diff --git a/synthesizer/process/src/traits/mod.rs b/synthesizer/process/src/traits/mod.rs index 85cd992780..5d1c22cd65 100644 --- a/synthesizer/process/src/traits/mod.rs +++ b/synthesizer/process/src/traits/mod.rs @@ -79,6 +79,9 @@ pub trait StackExecute { } pub trait StackProgramTypes { + /// Returns the constructor types for the program. + fn get_constructor_types(&self) -> Result<&FinalizeTypes>; + /// Returns the register types for the given closure or function name. fn get_register_types(&self, name: &Identifier) -> Result<&RegisterTypes>; diff --git a/synthesizer/program/src/bytes.rs b/synthesizer/program/src/bytes.rs index b7384ccf56..8907541f95 100644 --- a/synthesizer/program/src/bytes.rs +++ b/synthesizer/program/src/bytes.rs @@ -30,13 +30,13 @@ impl, Command: CommandTrait> Fro let id = ProgramID::read_le(&mut reader)?; // Initialize the program. - let mut program = ProgramCore::new(id).map_err(|e| error(e.to_string()))?; + let mut program = ProgramCore::new(id).map_err(error)?; // Read the number of program imports. let imports_len = u8::read_le(&mut reader)?; // Read the program imports. for _ in 0..imports_len { - program.add_import(Import::read_le(&mut reader)?).map_err(|e| error(e.to_string()))?; + program.add_import(Import::read_le(&mut reader)?).map_err(error)?; } // Read the number of components. @@ -47,15 +47,17 @@ impl, Command: CommandTrait> Fro // Match the variant. match variant { // Read the mapping. - 0 => program.add_mapping(Mapping::read_le(&mut reader)?).map_err(|e| error(e.to_string()))?, + 0 => program.add_mapping(Mapping::read_le(&mut reader)?).map_err(error)?, // Read the struct. - 1 => program.add_struct(StructType::read_le(&mut reader)?).map_err(|e| error(e.to_string()))?, + 1 => program.add_struct(StructType::read_le(&mut reader)?).map_err(error)?, // Read the record. - 2 => program.add_record(RecordType::read_le(&mut reader)?).map_err(|e| error(e.to_string()))?, + 2 => program.add_record(RecordType::read_le(&mut reader)?).map_err(error)?, // Read the closure. - 3 => program.add_closure(ClosureCore::read_le(&mut reader)?).map_err(|e| error(e.to_string()))?, + 3 => program.add_closure(ClosureCore::read_le(&mut reader)?).map_err(error)?, // Read the function. - 4 => program.add_function(FunctionCore::read_le(&mut reader)?).map_err(|e| error(e.to_string()))?, + 4 => program.add_function(FunctionCore::read_le(&mut reader)?).map_err(error)?, + // Read the constructor. + 5 => program.add_constructor(ConstructorCore::read_le(&mut reader)?).map_err(error)?, // Invalid variant. _ => return Err(error(format!("Failed to parse program. Invalid component variant '{variant}'"))), } @@ -76,15 +78,17 @@ impl, Command: CommandTrait> ToB self.id.write_le(&mut writer)?; // Write the number of program imports. - u8::try_from(self.imports.len()).map_err(|e| error(e.to_string()))?.write_le(&mut writer)?; + u8::try_from(self.imports.len()).map_err(error)?.write_le(&mut writer)?; // Write the program imports. for import in self.imports.values() { import.write_le(&mut writer)?; } - // Write the number of components. - u16::try_from(self.identifiers.len()).map_err(|e| error(e.to_string()))?.write_le(&mut writer)?; - // Write the components. + // Write the number of components. A component is either an identifier or a constructor. + let number_of_components = self.identifiers.len() + self.constructor.is_some() as usize; + u16::try_from(number_of_components).map_err(error)?.write_le(&mut writer)?; + + // Write the components that are identifiers. for (identifier, definition) in self.identifiers.iter() { match definition { ProgramDefinition::Mapping => match self.mappings.get(identifier) { @@ -135,6 +139,14 @@ impl, Command: CommandTrait> ToB } } + // Write the constructor, if it exists. + if let Some(constructor) = &self.constructor { + // Write the variant. + 5u8.write_le(&mut writer)?; + // Write the constructor. + constructor.write_le(&mut writer)?; + } + Ok(()) } } diff --git a/synthesizer/program/src/constructor/bytes.rs b/synthesizer/program/src/constructor/bytes.rs new file mode 100644 index 0000000000..0b3abe7135 --- /dev/null +++ b/synthesizer/program/src/constructor/bytes.rs @@ -0,0 +1,94 @@ +// Copyright (c) 2019-2025 Provable Inc. +// This file is part of the snarkVM library. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::*; + +impl> FromBytes for ConstructorCore { + /// Reads the constructor from a buffer. + #[inline] + fn read_le(mut reader: R) -> IoResult { + // Read the commands. + let num_commands = u16::read_le(&mut reader)?; + if num_commands.is_zero() { + return Err(error("Failed to deserialize constructor: needs at least one command")); + } + if num_commands > u16::try_from(N::MAX_COMMANDS).map_err(error)? { + return Err(error(format!("Failed to deserialize constructor: too many commands ({num_commands})"))); + } + let mut commands = Vec::with_capacity(num_commands as usize); + for _ in 0..num_commands { + commands.push(Command::read_le(&mut reader)?); + } + // Initialize a new constructor. + let mut constructor = Self { commands: Default::default(), num_writes: 0, positions: Default::default() }; + commands.into_iter().try_for_each(|command| constructor.add_command(command)).map_err(error)?; + + Ok(constructor) + } +} + +impl> ToBytes for ConstructorCore { + /// Writes the constructor to a buffer. + #[inline] + fn write_le(&self, mut writer: W) -> IoResult<()> { + // Write the number of commands for the constructor. + let num_commands = self.commands.len(); + match 0 < num_commands && num_commands <= N::MAX_COMMANDS { + true => u16::try_from(num_commands).map_err(error)?.write_le(&mut writer)?, + false => return Err(error(format!("Failed to write {num_commands} commands as bytes"))), + } + // Write the commands. + for command in self.commands.iter() { + command.write_le(&mut writer)?; + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Constructor; + use console::network::MainnetV0; + + type CurrentNetwork = MainnetV0; + + #[test] + fn test_constructor_bytes() -> Result<()> { + let constructor_string = r" +constructor: + add r0 r1 into r2; + add r0 r1 into r3; + add r0 r1 into r4; + add r0 r1 into r5; + add r0 r1 into r6; + add r0 r1 into r7; + add r0 r1 into r8; + add r0 r1 into r9; + add r0 r1 into r10; + add r0 r1 into r11; + get accounts[r0] into r12; + get accounts[r1] into r13;"; + + let expected = Constructor::::from_str(constructor_string)?; + let expected_bytes = expected.to_bytes_le()?; + println!("String size: {:?}, Bytecode size: {:?}", constructor_string.as_bytes().len(), expected_bytes.len()); + + let candidate = Constructor::::from_bytes_le(&expected_bytes)?; + assert_eq!(expected.to_string(), candidate.to_string()); + assert_eq!(expected_bytes, candidate.to_bytes_le()?); + Ok(()) + } +} diff --git a/synthesizer/program/src/constructor/mod.rs b/synthesizer/program/src/constructor/mod.rs new file mode 100644 index 0000000000..e40dc24a82 --- /dev/null +++ b/synthesizer/program/src/constructor/mod.rs @@ -0,0 +1,202 @@ +// Copyright (c) 2019-2025 Provable Inc. +// This file is part of the snarkVM library. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +mod bytes; +mod parse; + +use crate::traits::CommandTrait; + +use console::{ + network::prelude::*, + program::{Identifier, Register}, +}; + +use std::collections::HashMap; + +#[derive(Clone, PartialEq, Eq)] +pub struct ConstructorCore> { + /// The commands, in order of execution. + commands: Vec, + /// The number of write commands. + num_writes: u16, + /// A mapping from `Position`s to their index in `commands`. + positions: HashMap, usize>, +} + +impl> ConstructorCore { + /// Returns the constructor commands. + pub fn commands(&self) -> &[Command] { + &self.commands + } + + /// Returns the number of write commands. + pub const fn num_writes(&self) -> u16 { + self.num_writes + } + + /// Returns the mapping of `Position`s to their index in `commands`. + pub const fn positions(&self) -> &HashMap, usize> { + &self.positions + } +} + +impl> ConstructorCore { + /// Adds the given command to constructor. + /// + /// # Errors + /// This method will halt if the maximum number of commands has been reached. + #[inline] + pub fn add_command(&mut self, command: Command) -> Result<()> { + // Ensure the maximum number of commands has not been exceeded. + ensure!(self.commands.len() < N::MAX_COMMANDS, "Cannot add more than {} commands", N::MAX_COMMANDS); + // Ensure the number of write commands has not been exceeded. + ensure!(self.num_writes < N::MAX_WRITES, "Cannot add more than {} 'set' & 'remove' commands", N::MAX_WRITES); + + // Ensure the command is not a call instruction. + ensure!(!command.is_call(), "Forbidden operation: Constructor cannot invoke a 'call'"); + // Ensure the command is not a cast to record instruction. + ensure!(!command.is_cast_to_record(), "Forbidden operation: Constructor cannot cast to a record"); + // Ensure the command is not an await command. + ensure!(!command.is_await(), "Forbidden operation: Constructor cannot 'await'"); + + // Check the destination registers. + for register in command.destinations() { + // Ensure the destination register is a locator. + ensure!(matches!(register, Register::Locator(..)), "Destination register must be a locator"); + } + + // Check if the command is a branch command. + if let Some(position) = command.branch_to() { + // Ensure the branch target does not reference an earlier position. + ensure!(!self.positions.contains_key(position), "Cannot branch to an earlier position '{position}'"); + } + + // Check if the command is a position command. + if let Some(position) = command.position() { + // Ensure the position is not yet defined. + ensure!(!self.positions.contains_key(position), "Cannot redefine position '{position}'"); + // Ensure that there are less than `u8::MAX` positions. + ensure!(self.positions.len() < u8::MAX as usize, "Cannot add more than {} positions", u8::MAX); + // Insert the position. + self.positions.insert(*position, self.commands.len()); + } + + // Check if the command is a write command. + if command.is_write() { + // Increment the number of write commands. + self.num_writes += 1; + } + + // Insert the command. + self.commands.push(command); + Ok(()) + } +} + +impl> TypeName for ConstructorCore { + /// Returns the type name as a string. + #[inline] + fn type_name() -> &'static str { + "constructor" + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::{Command, Constructor}; + + type CurrentNetwork = console::network::MainnetV0; + + #[test] + fn test_add_command() { + // Initialize a new constructor instance. + let mut constructor = Constructor:: { + commands: Default::default(), + num_writes: 0, + positions: Default::default(), + }; + + // Ensure that a command can be added. + let command = Command::::from_str("add r0 r1 into r2;").unwrap(); + assert!(constructor.add_command(command).is_ok()); + + // Ensure that adding more than the maximum number of commands will fail. + for i in 3..CurrentNetwork::MAX_COMMANDS * 2 { + let command = Command::::from_str(&format!("add r0 r1 into r{i};")).unwrap(); + + match constructor.commands.len() < CurrentNetwork::MAX_COMMANDS { + true => assert!(constructor.add_command(command).is_ok()), + false => assert!(constructor.add_command(command).is_err()), + } + } + + // Ensure that adding more than the maximum number of writes will fail. + + // Initialize a new constructor instance. + let mut constructor = Constructor:: { + commands: Default::default(), + num_writes: 0, + positions: Default::default(), + }; + + for _ in 0..CurrentNetwork::MAX_WRITES * 2 { + let command = Command::::from_str("remove object[r0];").unwrap(); + + match constructor.commands.len() < CurrentNetwork::MAX_WRITES as usize { + true => assert!(constructor.add_command(command).is_ok()), + false => assert!(constructor.add_command(command).is_err()), + } + } + } + + #[test] + fn test_add_command_duplicate_positions() { + // Initialize a new constructor instance. + let mut constructor = + Constructor { commands: Default::default(), num_writes: 0, positions: Default::default() }; + + // Ensure that a command can be added. + let command = Command::::from_str("position start;").unwrap(); + assert!(constructor.add_command(command.clone()).is_ok()); + + // Ensure that adding a duplicate position will fail. + assert!(constructor.add_command(command).is_err()); + + // Helper method to convert a number to a unique string. + #[allow(clippy::cast_possible_truncation)] + fn to_unique_string(mut n: usize) -> String { + let mut s = String::new(); + while n > 0 { + s.push((b'A' + (n % 26) as u8) as char); + n /= 26; + } + s.chars().rev().collect::() + } + + // Ensure that adding more than the maximum number of positions will fail. + for i in 1..u8::MAX as usize * 2 { + let position = to_unique_string(i); + println!("position: {}", position); + let command = Command::::from_str(&format!("position {position};")).unwrap(); + + match constructor.commands.len() < u8::MAX as usize { + true => assert!(constructor.add_command(command).is_ok()), + false => assert!(constructor.add_command(command).is_err()), + } + } + } +} diff --git a/synthesizer/program/src/constructor/parse.rs b/synthesizer/program/src/constructor/parse.rs new file mode 100644 index 0000000000..7e8a6106fe --- /dev/null +++ b/synthesizer/program/src/constructor/parse.rs @@ -0,0 +1,141 @@ +// Copyright (c) 2019-2025 Provable Inc. +// This file is part of the snarkVM library. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::*; + +impl> Parser for ConstructorCore { + /// Parses a string into constructor. + #[inline] + fn parse(string: &str) -> ParserResult { + // Parse the whitespace and comments from the string. + let (string, _) = Sanitizer::parse(string)?; + // Parse the 'constructor' keyword from the string. + let (string, _) = tag(Self::type_name())(string)?; + // Parse the whitespace from the string. + let (string, _) = Sanitizer::parse_whitespaces(string)?; + // Parse the colon ':' keyword from the string. + let (string, _) = tag(":")(string)?; + + // Parse the commands from the string. + let (string, commands) = many1(Command::parse)(string)?; + + map_res(take(0usize), move |_| { + // Initialize a new constructor. + let mut constructor = Self { commands: Default::default(), num_writes: 0, positions: Default::default() }; + if let Err(error) = commands.iter().cloned().try_for_each(|command| constructor.add_command(command)) { + eprintln!("{error}"); + return Err(error); + } + Ok::<_, Error>(constructor) + })(string) + } +} + +impl> FromStr for ConstructorCore { + type Err = Error; + + /// Returns a constructor from a string literal. + fn from_str(string: &str) -> Result { + match Self::parse(string) { + Ok((remainder, object)) => { + // Ensure the remainder is empty. + ensure!(remainder.is_empty(), "Failed to parse string. Found invalid character in: \"{remainder}\""); + // Return the object. + Ok(object) + } + Err(error) => bail!("Failed to parse string. {error}"), + } + } +} + +impl> Debug for ConstructorCore { + /// Prints the constructor as a string. + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + Display::fmt(self, f) + } +} + +impl> Display for ConstructorCore { + /// Prints the constructor as a string. + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + // Write the constructor to a string. + write!(f, "{}:", Self::type_name())?; + self.commands.iter().try_for_each(|command| write!(f, "\n {command}")) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Constructor; + use console::network::MainnetV0; + + type CurrentNetwork = MainnetV0; + + #[test] + fn test_constructor_parse() { + let constructor = Constructor::::parse( + r" +constructor: + add r0 r1 into r2;", + ) + .unwrap() + .1; + assert_eq!(1, constructor.commands().len()); + + // Constructor with 0 inputs. + let constructor = Constructor::::parse( + r" +constructor: + add 1u32 2u32 into r0;", + ) + .unwrap() + .1; + assert_eq!(1, constructor.commands().len()); + } + + #[test] + fn test_constructor_parse_cast() { + let constructor = Constructor::::parse( + r" +constructor: + cast 1u8 2u8 into r1 as token;", + ) + .unwrap() + .1; + assert_eq!(1, constructor.commands().len()); + } + + #[test] + fn test_constructor_display() { + let expected = r"constructor: + add r0 r1 into r2;"; + let constructor = Constructor::::parse(expected).unwrap().1; + assert_eq!(expected, format!("{constructor}"),); + } + + #[test] + fn test_empty_constructor() { + // Test that parsing an empty constructor fails. + assert!(Constructor::::parse("constructor:").is_err()); + // Test that attempting to serialize an empty constructor fails. + let constructor = Constructor:: { + commands: Default::default(), + num_writes: 0, + positions: Default::default(), + }; + assert!(constructor.write_le(Vec::new()).is_err()); + } +} diff --git a/synthesizer/program/src/lib.rs b/synthesizer/program/src/lib.rs index a7f70a0e75..fbd518c49d 100644 --- a/synthesizer/program/src/lib.rs +++ b/synthesizer/program/src/lib.rs @@ -21,10 +21,14 @@ pub type Program = crate::ProgramCore, Command>; pub type Function = crate::FunctionCore, Command>; pub type Finalize = crate::FinalizeCore>; pub type Closure = crate::ClosureCore>; +pub type Constructor = crate::ConstructorCore>; mod closure; pub use closure::*; +mod constructor; +pub use constructor::*; + pub mod finalize; pub use finalize::*; @@ -90,7 +94,9 @@ use console::{ take, }, }, + prelude::ToBits, program::{Identifier, PlaintextType, ProgramID, RecordType, StructType}, + types::Field, }; use indexmap::{IndexMap, IndexSet}; @@ -116,6 +122,8 @@ pub struct ProgramCore, Command: Co id: ProgramID, /// A map of the declared imports for the program. imports: IndexMap, Import>, + /// An optional constructor for the program. + constructor: Option>, /// A map of identifiers to their program declaration. identifiers: IndexMap, ProgramDefinition>, /// A map of the declared mappings for the program. @@ -140,6 +148,7 @@ impl, Command: CommandTrait> Pro Ok(Self { id, imports: IndexMap::new(), + constructor: None, identifiers: IndexMap::new(), mappings: IndexMap::new(), structs: IndexMap::new(), @@ -160,11 +169,21 @@ impl, Command: CommandTrait> Pro &self.id } + /// Returns the checksum of the program. + pub fn checksum(&self) -> Result> { + N::hash_bhp1024(&self.to_bytes_le()?.to_bits_le()) + } + /// Returns the imports in the program. pub const fn imports(&self) -> &IndexMap, Import> { &self.imports } + /// Returns the constructor for the program. + pub const fn constructor(&self) -> Option<&ConstructorCore> { + self.constructor.as_ref() + } + /// Returns the mappings in the program. pub const fn mappings(&self) -> &IndexMap, Mapping> { &self.mappings @@ -195,6 +214,11 @@ impl, Command: CommandTrait> Pro self.imports.contains_key(id) } + /// Returns `true` if the program contains a constructor. + pub const fn contains_constructor(&self) -> bool { + self.constructor.is_some() + } + /// Returns `true` if the program contains a mapping with the given name. pub fn contains_mapping(&self, name: &Identifier) -> bool { self.mappings.contains_key(name) @@ -326,6 +350,21 @@ impl, Command: CommandTrait> Pro Ok(()) } + /// Adds a constructor to the program. + /// + /// # Errors + /// This method will halt if a constructor was previously added. + /// This method will halt if a constructor exceeds the maximum number of commands. + fn add_constructor(&mut self, constructor: ConstructorCore) -> Result<()> { + // Ensure the program does not already have a constructor. + ensure!(self.constructor.is_none(), "Program already has a constructor."); + // Ensure the number of commands is within the allowed range. + ensure!(constructor.commands().len() <= N::MAX_COMMANDS, "Constructor exceeds maximum number of commands"); + // Add the constructor to the program. + self.constructor = Some(constructor); + Ok(()) + } + /// Adds a new mapping to the program. /// /// # Errors @@ -968,4 +1007,52 @@ function swap: Ok(()) } + + #[test] + fn test_program_with_constructor() { + // Initialize a new program. + let program_string = r"import credits.aleo; + +program good_constructor.aleo; + +constructor: + assert.eq edition 0u16; + assert.eq credits.aleo/edition 0u16; + assert.neq checksum 0field; + assert.eq credits.aleo/checksum 6192738754253668739186185034243585975029374333074931926190215457304721124008field; + set 1u8 into data[0u8]; + +mapping data: + key as u8.public; + value as u8.public; + +function dummy: + +function check: + async check into r0; + output r0 as good_constructor.aleo/check.future; + +finalize check: + get data[0u8] into r0; + assert.eq r0 1u8; +"; + let program = Program::::from_str(program_string).unwrap(); + + // Check that the string and bytes (de)serialization works. + let serialized = program.to_string(); + let deserialized = Program::::from_str(&serialized).unwrap(); + assert_eq!(program, deserialized); + + let serialized = program.to_bytes_le().unwrap(); + let deserialized = Program::::from_bytes_le(&serialized).unwrap(); + assert_eq!(program, deserialized); + + // Check that the display works. + let display = format!("{}", program); + assert_eq!(display, program_string); + + // Ensure the program contains a constructor. + assert!(program.contains_constructor()); + assert_eq!(program.constructor().unwrap().commands().len(), 5); + } } diff --git a/synthesizer/program/src/logic/command/contains.rs b/synthesizer/program/src/logic/command/contains.rs index 7cf4bee394..2d6d52f795 100644 --- a/synthesizer/program/src/logic/command/contains.rs +++ b/synthesizer/program/src/logic/command/contains.rs @@ -84,9 +84,9 @@ impl Contains { CallOperator::Resource(mapping_name) => (*stack.program_id(), mapping_name), }; - // Ensure the mapping exists in storage. - if !store.contains_mapping_confirmed(&program_id, &mapping_name)? { - bail!("Mapping '{program_id}/{mapping_name}' does not exist in storage"); + // Ensure the mapping exists. + if !store.contains_mapping_speculative(&program_id, &mapping_name)? { + bail!("Mapping '{program_id}/{mapping_name}' does not exist"); } // Load the operand as a plaintext. diff --git a/synthesizer/program/src/logic/command/get.rs b/synthesizer/program/src/logic/command/get.rs index 7fdaea139e..661016c671 100644 --- a/synthesizer/program/src/logic/command/get.rs +++ b/synthesizer/program/src/logic/command/get.rs @@ -103,9 +103,9 @@ impl Get { CallOperator::Resource(mapping_name) => (*stack.program_id(), mapping_name), }; - // Ensure the mapping exists in storage. - if !store.contains_mapping_confirmed(&program_id, &mapping_name)? { - bail!("Mapping '{program_id}/{mapping_name}' does not exist in storage"); + // Ensure the mapping exists. + if !store.contains_mapping_speculative(&program_id, &mapping_name)? { + bail!("Mapping '{program_id}/{mapping_name}' does not exist"); } // Load the operand as a plaintext. diff --git a/synthesizer/program/src/logic/command/get_or_use.rs b/synthesizer/program/src/logic/command/get_or_use.rs index 3a4bfeb5f2..b7eff041b9 100644 --- a/synthesizer/program/src/logic/command/get_or_use.rs +++ b/synthesizer/program/src/logic/command/get_or_use.rs @@ -114,9 +114,9 @@ impl GetOrUse { CallOperator::Resource(mapping_name) => (*stack.program_id(), mapping_name), }; - // Ensure the mapping exists in storage. - if !store.contains_mapping_confirmed(&program_id, &mapping_name)? { - bail!("Mapping '{program_id}/{mapping_name}' does not exist in storage"); + // Ensure the mapping exists. + if !store.contains_mapping_speculative(&program_id, &mapping_name)? { + bail!("Mapping '{program_id}/{mapping_name}' does not exist"); } // Load the operand as a plaintext. diff --git a/synthesizer/program/src/logic/command/mod.rs b/synthesizer/program/src/logic/command/mod.rs index 4c34ab4fa4..aa3b5be58d 100644 --- a/synthesizer/program/src/logic/command/mod.rs +++ b/synthesizer/program/src/logic/command/mod.rs @@ -143,6 +143,12 @@ impl CommandTrait for Command { fn is_write(&self) -> bool { matches!(self, Command::Set(_) | Command::Remove(_)) } + + /// Returns `true` if the command is an await command. + #[inline] + fn is_await(&self) -> bool { + matches!(self, Command::Await(_)) + } } impl Command { diff --git a/synthesizer/program/src/logic/command/remove.rs b/synthesizer/program/src/logic/command/remove.rs index ce06363a70..822d7233ab 100644 --- a/synthesizer/program/src/logic/command/remove.rs +++ b/synthesizer/program/src/logic/command/remove.rs @@ -66,9 +66,9 @@ impl Remove { store: &impl FinalizeStoreTrait, registers: &mut impl RegistersLoad, ) -> Result>> { - // Ensure the mapping exists in storage. - if !store.contains_mapping_confirmed(stack.program_id(), &self.mapping)? { - bail!("Mapping '{}/{}' does not exist in storage", stack.program_id(), self.mapping); + // Ensure the mapping exists. + if !store.contains_mapping_speculative(stack.program_id(), &self.mapping)? { + bail!("Mapping '{}/{}' does not exist", stack.program_id(), self.mapping); } // Load the key operand as a plaintext. diff --git a/synthesizer/program/src/logic/command/set.rs b/synthesizer/program/src/logic/command/set.rs index 1d1689a3eb..87d9cbd28e 100644 --- a/synthesizer/program/src/logic/command/set.rs +++ b/synthesizer/program/src/logic/command/set.rs @@ -77,9 +77,9 @@ impl Set { store: &impl FinalizeStoreTrait, registers: &mut impl RegistersLoad, ) -> Result> { - // Ensure the mapping exists in storage. - if !store.contains_mapping_confirmed(stack.program_id(), &self.mapping)? { - bail!("Mapping '{}/{}' does not exist in storage", stack.program_id(), self.mapping); + // Ensure the mapping exists. + if !store.contains_mapping_speculative(stack.program_id(), &self.mapping)? { + bail!("Mapping '{}/{}' does not exist", stack.program_id(), self.mapping); } // Load the key operand as a plaintext. diff --git a/synthesizer/program/src/logic/instruction/operand/bytes.rs b/synthesizer/program/src/logic/instruction/operand/bytes.rs index a1f94ee15a..170ccb7eeb 100644 --- a/synthesizer/program/src/logic/instruction/operand/bytes.rs +++ b/synthesizer/program/src/logic/instruction/operand/bytes.rs @@ -25,6 +25,24 @@ impl FromBytes for Operand { 4 => Ok(Self::Caller), 5 => Ok(Self::BlockHeight), 6 => Ok(Self::NetworkID), + 7 => { + // Read the program ID. + let program_id = match u8::read_le(&mut reader)? { + 0 => None, + 1 => Some(ProgramID::read_le(&mut reader)?), + variant => return Err(error(format!("Invalid program ID variant '{variant}' for the checksum"))), + }; + Ok(Self::Checksum(program_id)) + } + 8 => { + // Read the program ID. + let program_id = match u8::read_le(&mut reader)? { + 0 => None, + 1 => Some(ProgramID::read_le(&mut reader)?), + variant => return Err(error(format!("Invalid program ID variant '{variant}' for the edition"))), + }; + Ok(Self::Edition(program_id)) + } variant => Err(error(format!("Failed to deserialize operand variant {variant}"))), } } @@ -49,6 +67,28 @@ impl ToBytes for Operand { Self::Caller => 4u8.write_le(&mut writer), Self::BlockHeight => 5u8.write_le(&mut writer), Self::NetworkID => 6u8.write_le(&mut writer), + Self::Checksum(program_id) => { + 7u8.write_le(&mut writer)?; + // Write the program ID. + match program_id { + None => 0u8.write_le(&mut writer), + Some(ref program_id) => { + 1u8.write_le(&mut writer)?; + program_id.write_le(&mut writer) + } + } + } + Self::Edition(program_id) => { + 8u8.write_le(&mut writer)?; + // Write the program ID. + match program_id { + None => 0u8.write_le(&mut writer), + Some(ref program_id) => { + 1u8.write_le(&mut writer)?; + program_id.write_le(&mut writer) + } + } + } } } } diff --git a/synthesizer/program/src/logic/instruction/operand/mod.rs b/synthesizer/program/src/logic/instruction/operand/mod.rs index 9a8684db75..b5cd905e07 100644 --- a/synthesizer/program/src/logic/instruction/operand/mod.rs +++ b/synthesizer/program/src/logic/instruction/operand/mod.rs @@ -44,6 +44,14 @@ pub enum Operand { /// The operand is the network ID. /// Note: This variant is only accessible in the `finalize` scope. NetworkID, + /// The operand is the program checksum. + /// If no program ID is specified, the checksum is for the current program. + /// If a program ID is specified, the checksum is for an external program. + Checksum(Option>), + /// The operand is the program edition. + /// If no program ID is specified, the edition is for the current program. + /// If a program ID is specified, the edition is for an external program. + Edition(Option>), } impl From> for Operand { diff --git a/synthesizer/program/src/logic/instruction/operand/parse.rs b/synthesizer/program/src/logic/instruction/operand/parse.rs index 5de2cb1911..b7145df568 100644 --- a/synthesizer/program/src/logic/instruction/operand/parse.rs +++ b/synthesizer/program/src/logic/instruction/operand/parse.rs @@ -28,6 +28,13 @@ impl Parser for Operand { map(tag("self.caller"), |_| Self::Caller), map(tag("block.height"), |_| Self::BlockHeight), map(tag("network.id"), |_| Self::NetworkID), + // Note that `Operand::Checksum` and `Operand::Edition` must be parsed before `Operand::ProgramID`s, since an edition or checksum may be prefixed with a program ID. + map(pair(opt(terminated(ProgramID::parse, tag("/"))), tag("checksum")), |(program_id, _)| { + Self::Checksum(program_id) + }), + map(pair(opt(terminated(ProgramID::parse, tag("/"))), tag("edition")), |(program_id, _)| { + Self::Edition(program_id) + }), // Note that `Operand::ProgramID`s must be parsed before `Operand::Literal`s, since a program ID can be implicitly parsed as a literal address. // This ensures that the string representation of a program uses the `Operand::ProgramID` variant. map(ProgramID::parse, |program_id| Self::ProgramID(program_id)), @@ -80,6 +87,16 @@ impl Display for Operand { Self::BlockHeight => write!(f, "block.height"), // Prints the identifier for the network ID, i.e. network.id Self::NetworkID => write!(f, "network.id"), + // Prints the optional program ID with the checksum keyword, i.e. `checksum` or `token.aleo/checksum` + Self::Checksum(program_id) => match program_id { + Some(program_id) => write!(f, "{program_id}/checksum"), + None => write!(f, "checksum"), + }, + // Prints the optional program ID with the edition keyword, i.e. `edition` or `token.aleo/edition` + Self::Edition(program_id) => match program_id { + Some(program_id) => write!(f, "{program_id}/edition"), + None => write!(f, "edition"), + }, } } } @@ -120,6 +137,18 @@ mod tests { let operand = Operand::::parse("group::GEN").unwrap().1; assert_eq!(Operand::Literal(Literal::Group(Group::generator())), operand); + let operand = Operand::::parse("checksum").unwrap().1; + assert_eq!(Operand::Checksum(None), operand); + + let operand = Operand::::parse("token.aleo/checksum").unwrap().1; + assert_eq!(Operand::Checksum(Some(ProgramID::from_str("token.aleo")?)), operand); + + let operand = Operand::::parse("edition").unwrap().1; + assert_eq!(Operand::Edition(None), operand); + + let operand = Operand::::parse("token.aleo/edition").unwrap().1; + assert_eq!(Operand::Edition(Some(ProgramID::from_str("token.aleo")?)), operand); + // Sanity check a failure case. let (remainder, operand) = Operand::::parse("1field.private").unwrap(); assert_eq!(Operand::Literal(Literal::from_str("1field")?), operand); @@ -148,6 +177,18 @@ mod tests { let operand = Operand::::parse("self.caller").unwrap().1; assert_eq!(format!("{operand}"), "self.caller"); + let operand = Operand::::parse("checksum").unwrap().1; + assert_eq!(format!("{operand}"), "checksum"); + + let operand = Operand::::parse("foo.aleo/checksum").unwrap().1; + assert_eq!(format!("{operand}"), "foo.aleo/checksum"); + + let operand = Operand::::parse("edition").unwrap().1; + assert_eq!(format!("{operand}"), "edition"); + + let operand = Operand::::parse("foo.aleo/edition").unwrap().1; + assert_eq!(format!("{operand}"), "foo.aleo/edition"); + let operand = Operand::::parse("group::GEN").unwrap().1; assert_eq!( format!("{operand}"), diff --git a/synthesizer/program/src/parse.rs b/synthesizer/program/src/parse.rs index 3e33422e6f..6996164fed 100644 --- a/synthesizer/program/src/parse.rs +++ b/synthesizer/program/src/parse.rs @@ -23,8 +23,9 @@ impl, Command: CommandTrait> Par fn parse(string: &str) -> ParserResult { // A helper to parse a program. enum P, Command: CommandTrait> { + Constructor(ConstructorCore), M(Mapping), - I(StructType), + S(StructType), R(RecordType), C(ClosureCore), F(FunctionCore), @@ -51,10 +52,14 @@ impl, Command: CommandTrait> Par // Parse the whitespace and comments from the string. let (string, _) = Sanitizer::parse(string)?; - if string.starts_with(Mapping::::type_name()) { + if string.starts_with(ConstructorCore::::type_name()) { + map(ConstructorCore::parse, |constructor| P::::Constructor(constructor))( + string, + ) + } else if string.starts_with(Mapping::::type_name()) { map(Mapping::parse, |mapping| P::::M(mapping))(string) } else if string.starts_with(StructType::::type_name()) { - map(StructType::parse, |struct_| P::::I(struct_))(string) + map(StructType::parse, |struct_| P::::S(struct_))(string) } else if string.starts_with(RecordType::::type_name()) { map(RecordType::parse, |record| P::::R(record))(string) } else if string.starts_with(ClosureCore::::type_name()) { @@ -82,8 +87,9 @@ impl, Command: CommandTrait> Par // Construct the program with the parsed components. for component in components { let result = match component { + P::Constructor(constructor) => program.add_constructor(constructor), P::M(mapping) => program.add_mapping(mapping), - P::I(struct_) => program.add_struct(struct_), + P::S(struct_) => program.add_struct(struct_), P::R(record) => program.add_record(record), P::C(closure) => program.add_closure(closure), P::F(function) => program.add_function(function), @@ -161,6 +167,11 @@ impl, Command: CommandTrait> Dis // Print the program name. write!(f, "{} {};\n\n", Self::type_name(), self.id)?; + // Write the constructor, if it exists. + if let Some(constructor) = &self.constructor { + writeln!(f, "{constructor}\n")?; + } + let mut identifier_iter = self.identifiers.iter().peekable(); while let Some((identifier, definition)) = identifier_iter.next() { match definition { diff --git a/synthesizer/program/src/traits/command.rs b/synthesizer/program/src/traits/command.rs index 59338707ff..b943ddff68 100644 --- a/synthesizer/program/src/traits/command.rs +++ b/synthesizer/program/src/traits/command.rs @@ -32,4 +32,6 @@ pub trait CommandTrait: Clone + Parser + FromBytes + ToBytes { fn is_cast_to_record(&self) -> bool; /// Returns `true` if the command is a write operation. fn is_write(&self) -> bool; + /// Returns `true` if the command is an await command. + fn is_await(&self) -> bool; } diff --git a/synthesizer/program/src/traits/finalize_store.rs b/synthesizer/program/src/traits/finalize_store.rs index 38c61b7d3f..2c79f3d706 100644 --- a/synthesizer/program/src/traits/finalize_store.rs +++ b/synthesizer/program/src/traits/finalize_store.rs @@ -21,9 +21,16 @@ use console::{ }; pub trait FinalizeStoreTrait { - /// Returns `true` if the given `program ID` and `mapping name` exist. + /// Returns `true` if the given `program ID` and `mapping name` is confirmed to exist. fn contains_mapping_confirmed(&self, program_id: &ProgramID, mapping_name: &Identifier) -> Result; + /// Returns `true` if the given `program ID` and `mapping name` exist. + /// This method was added to support execution of constructors during deployment. + /// Prior to supporting program upgrades, `contains_mapping_confirmed` was used to check that a mapping exists before executing a command like `set`, `get`, `remove`, etc. + /// However, during deployment, the mapping only speculatively exists, so `contains_mapping_speculative` should be used instead. + /// This usage is safe because the mappings used in a program are statically verified to exist in `FinalizeTypes::initialize` before the deployment or upgrade's constructor is executed. + fn contains_mapping_speculative(&self, program_id: &ProgramID, mapping_name: &Identifier) -> Result; + /// Returns `true` if the given `program ID`, `mapping name`, and `key` exist. fn contains_key_speculative( &self, diff --git a/synthesizer/program/src/traits/stack_and_registers.rs b/synthesizer/program/src/traits/stack_and_registers.rs index 9162a1f55c..6a8681c6d9 100644 --- a/synthesizer/program/src/traits/stack_and_registers.rs +++ b/synthesizer/program/src/traits/stack_and_registers.rs @@ -34,7 +34,7 @@ use console::{ Value, ValueType, }, - types::{Address, Field}, + types::{Address, Field, U16}, }; use rand::{CryptoRng, Rng}; use synthesizer_snark::{ProvingKey, VerifyingKey}; @@ -95,6 +95,12 @@ pub trait StackProgram { /// Returns the program address. fn program_address(&self) -> &Address; + /// Returns the program checksum. + fn program_checksum(&self) -> &Field; + + /// Returns the program edition. + fn program_edition(&self) -> &U16; + /// Returns the external stack for the given program ID. fn get_external_stack(&self, program_id: &ProgramID) -> Result>; diff --git a/synthesizer/src/vm/deploy.rs b/synthesizer/src/vm/deploy.rs index e32f84924e..3f0d03fb22 100644 --- a/synthesizer/src/vm/deploy.rs +++ b/synthesizer/src/vm/deploy.rs @@ -41,7 +41,7 @@ impl> VM { let owner = ProgramOwner::new(private_key, deployment_id, rng)?; // Compute the minimum deployment cost. - let (minimum_deployment_cost, _) = deployment_cost(&deployment)?; + let (minimum_deployment_cost, _) = deployment_cost(&self.process().read(), &deployment)?; // Authorize the fee. let fee_authorization = match fee_record { Some(record) => self.authorize_fee_private( diff --git a/synthesizer/src/vm/execute.rs b/synthesizer/src/vm/execute.rs index a65dbcef4d..7fb2f5bafe 100644 --- a/synthesizer/src/vm/execute.rs +++ b/synthesizer/src/vm/execute.rs @@ -254,7 +254,7 @@ mod tests { types::Field, }; use ledger_block::Transition; - use synthesizer_process::{ConsensusFeeVersion, cost_per_command, execution_cost_v2}; + use synthesizer_process::{ConsensusFeeVersion, StackProgramTypes, cost_per_command, execution_cost_v2}; use synthesizer_program::StackProgram; use indexmap::IndexMap; @@ -922,6 +922,8 @@ finalize test: let function_name = transition.function_name(); // Get the stack. let stack = vm.process().read().get_stack(program_id).unwrap().clone(); + // Get the finalize types. + let finalize_types = stack.get_finalize_types(function_name).unwrap(); // Get the finalize block of the transition and sum the cost of each command. let cost = match stack.get_function(function_name).unwrap().finalize_logic() { None => 0, @@ -930,7 +932,7 @@ finalize test: finalize_logic .commands() .iter() - .map(|command| cost_per_command(&stack, finalize_logic, command, ConsensusFeeVersion::V2)) + .map(|command| cost_per_command(&stack, finalize_types, command, ConsensusFeeVersion::V2)) .try_fold(0u64, |acc, res| { res.and_then(|x| acc.checked_add(x).ok_or(anyhow!("Finalize cost overflowed"))) }) @@ -1058,6 +1060,8 @@ finalize test: let function_name = transition.function_name(); // Get the stack. let stack = vm.process().read().get_stack(program_id).unwrap().clone(); + // Get the finalize types. + let finalize_types = stack.get_finalize_types(function_name).unwrap(); // Get the finalize block of the transition and sum the cost of each command. let cost = match stack.get_function(function_name).unwrap().finalize_logic() { None => 0, @@ -1066,7 +1070,7 @@ finalize test: finalize_logic .commands() .iter() - .map(|command| cost_per_command(&stack, finalize_logic, command, ConsensusFeeVersion::V2)) + .map(|command| cost_per_command(&stack, finalize_types, command, ConsensusFeeVersion::V2)) .try_fold(0u64, |acc, res| { res.and_then(|x| acc.checked_add(x).ok_or(anyhow!("Finalize cost overflowed"))) }) diff --git a/synthesizer/src/vm/mod.rs b/synthesizer/src/vm/mod.rs index e0bc80c1f5..1ddbdcde54 100644 --- a/synthesizer/src/vm/mod.rs +++ b/synthesizer/src/vm/mod.rs @@ -3191,6 +3191,92 @@ function adder: assert!(vm.process().read().contains_program(&ProgramID::from_str("child_program.aleo").unwrap())); } + #[cfg(feature = "test")] + #[test] + fn test_deploy_and_execute_in_same_block_fails() { + let rng = &mut TestRng::default(); + + // Initialize a new caller. + let caller_private_key = crate::vm::test_helpers::sample_genesis_private_key(rng); + + // Initialize the genesis block. + let genesis = crate::vm::test_helpers::sample_genesis_block(rng); + + // Initialize the VM. + let vm = crate::vm::test_helpers::sample_vm(); + vm.add_next_block(&genesis).unwrap(); + + // Fund an account to pay for the deployment. + let private_key = PrivateKey::new(rng).unwrap(); + let address = Address::try_from(&private_key).unwrap(); + + let tx = vm + .execute( + &caller_private_key, + ("credits.aleo", "transfer_public"), + [Value::from_str(&format!("{address}")).unwrap(), Value::from_str("100000000u64").unwrap()].iter(), + None, + 0, + None, + rng, + ) + .unwrap(); + + let block = sample_next_block(&vm, &caller_private_key, &[tx], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1); + vm.add_next_block(&block).unwrap(); + + // Deploy and execute a program in the same block. + let program = Program::from_str( + r" +program adder_program.aleo; + +function adder: + input r0 as u64.public; + input r1 as u64.public; + add r0 r1 into r2; + output r2 as u64.public; + ", + ) + .unwrap(); + + // Initialize an "off-chain" VM to generate the deployment and execution. + let off_chain_vm = sample_vm(); + off_chain_vm.add_next_block(&genesis).unwrap(); + off_chain_vm.add_next_block(&block).unwrap(); + // Deploy the program. + let deployment = off_chain_vm.deploy(&private_key, &program, None, 0, None, rng).unwrap(); + // Check that the account has enough to pay for the deployment. + assert_eq!(*deployment.fee_amount().unwrap(), 2483025); + // Add the program to the off-chain VM. + off_chain_vm.process().write().add_program(&program).unwrap(); + // Execute the program. + let transaction = off_chain_vm + .execute( + &private_key, + ("adder_program.aleo", "adder"), + [Value::from_str("1u64").unwrap(), Value::from_str("2u64").unwrap()].iter(), + None, + 0, + None, + rng, + ) + .unwrap(); + // Verify the transaction. + off_chain_vm.check_transaction(&transaction, None, rng).unwrap(); + // Check that the account has enough to pay for the execution. + assert_eq!(*transaction.fee_amount().unwrap(), 1283); + // Drop the off-chain VM. + drop(off_chain_vm); + + let block = sample_next_block(&vm, &caller_private_key, &[deployment, transaction], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1); + vm.add_next_block(&block).unwrap(); + + // Check that the program was deployed. + assert!(vm.process().read().contains_program(&ProgramID::from_str("adder_program.aleo").unwrap())); + } + #[cfg(feature = "test")] #[test] fn test_versioned_keyword_restrictions() { diff --git a/synthesizer/src/vm/verify.rs b/synthesizer/src/vm/verify.rs index c2c32e53f9..2718d253b0 100644 --- a/synthesizer/src/vm/verify.rs +++ b/synthesizer/src/vm/verify.rs @@ -215,7 +215,7 @@ impl> VM { // Ensure the rejected ID is not present. ensure!(rejected_id.is_none(), "Transaction '{id}' should not have a rejected ID (deployment)"); // Compute the minimum deployment cost. - let (cost, _) = deployment_cost(deployment)?; + let (cost, _) = deployment_cost(&self.process().read(), deployment)?; // Ensure the fee is sufficient to cover the cost. if *fee.base_amount()? < cost { bail!("Transaction '{id}' has an insufficient base fee (deployment) - requires {cost} microcredits") diff --git a/synthesizer/tests/expectations/vm/execute_and_finalize/bad_constructor_fail.out b/synthesizer/tests/expectations/vm/execute_and_finalize/bad_constructor_fail.out new file mode 100644 index 0000000000..b95dbb5605 --- /dev/null +++ b/synthesizer/tests/expectations/vm/execute_and_finalize/bad_constructor_fail.out @@ -0,0 +1,5 @@ +errors: [] +outputs: +- execute: Program 'bad_constructor.aleo' does not exist +additional: +- {} diff --git a/synthesizer/tests/expectations/vm/execute_and_finalize/good_constructor.out b/synthesizer/tests/expectations/vm/execute_and_finalize/good_constructor.out new file mode 100644 index 0000000000..5f792c8f3c --- /dev/null +++ b/synthesizer/tests/expectations/vm/execute_and_finalize/good_constructor.out @@ -0,0 +1,14 @@ +errors: [] +outputs: +- verified: true + execute: + good_constructor.aleo/check: + outputs: + - '{"type":"future","id":"4840279410846804147436041831820290497261407528526240026855847981786978386825field","value":"{\n program_id: good_constructor.aleo,\n function_name: check,\n arguments: []\n}"}' + speculate: the execution was accepted + add_next_block: succeeded. +additional: +- child_outputs: + credits.aleo/fee_public: + outputs: + - '{"type":"future","id":"966409966695246773656312280131645182827272260712453148901940367292843867854field","value":"{\n program_id: credits.aleo,\n function_name: fee_public,\n arguments: [\n aleo1qr2ha4pfs5l28aze88yn6fhleeythklkczrule2v838uwj65n5gqxt9djx,\n 11724u64\n ]\n}"}' diff --git a/synthesizer/tests/expectations/vm/execute_and_finalize/unknown_mapping_fail.out b/synthesizer/tests/expectations/vm/execute_and_finalize/unknown_mapping_fail.out index eb8a147f5c..cf557db01e 100644 --- a/synthesizer/tests/expectations/vm/execute_and_finalize/unknown_mapping_fail.out +++ b/synthesizer/tests/expectations/vm/execute_and_finalize/unknown_mapping_fail.out @@ -1,3 +1,3 @@ errors: -- 'Failed to run `VM::deploy for program registry.aleo: Mapping ''foo'' in ''registry.aleo/register'' is not defined.' +- 'Failed to run `VM::deploy for program registry.aleo: Mapping ''foo'' in ''registry.aleo'' is not defined.' outputs: [] diff --git a/synthesizer/tests/tests/vm/execute_and_finalize/bad_constructor_fail.aleo b/synthesizer/tests/tests/vm/execute_and_finalize/bad_constructor_fail.aleo new file mode 100644 index 0000000000..5fa8873161 --- /dev/null +++ b/synthesizer/tests/tests/vm/execute_and_finalize/bad_constructor_fail.aleo @@ -0,0 +1,21 @@ +/* +randomness: 45791624 +cases: + - program: bad_constructor.aleo + function: foo + inputs: ["1field", "2field"] +*/ + +program bad_constructor.aleo; + +constructor: + assert.eq true false; + +function foo: + input r0 as field.private; + input r1 as field.private; + add r0 r1 into r2; + output r2 as field.private; + + + diff --git a/synthesizer/tests/tests/vm/execute_and_finalize/good_constructor.aleo b/synthesizer/tests/tests/vm/execute_and_finalize/good_constructor.aleo new file mode 100644 index 0000000000..9e32a7fe51 --- /dev/null +++ b/synthesizer/tests/tests/vm/execute_and_finalize/good_constructor.aleo @@ -0,0 +1,47 @@ +/* +randomness: 45791624 +cases: + - program: good_constructor.aleo + function: check + inputs: [] +*/ + +import credits.aleo; + +program good_constructor.aleo; + +mapping data: + key as u8.public; + value as u8.public; + +constructor: + assert.eq edition 0u16; + assert.eq credits.aleo/edition 0u16; + assert.neq checksum 0field; + assert.eq credits.aleo/checksum 6192738754253668739186185034243585975029374333074931926190215457304721124008field; + contains data[0u8] into r0; // Check `contains` without value + assert.eq r0 false; + get.or_use data[0u8] 8u8 into r1; // Check `get.or_use` without value + assert.eq r1 8u8; + set 1u8 into data[0u8]; // Check `set` + contains data[0u8] into r2; // Check `contains` with value + assert.eq r2 true; + get.or_use data[0u8] 0u8 into r3; // Check `get.or_use` without value + assert.eq r3 1u8; + get data[0u8] into r4; // Check `get` with value + assert.eq r4 1u8; + remove data[0u8]; // Check `remove` + contains data[0u8] into r5; // Check `contains` after removal + assert.eq r5 false; + set 1u8 into data[0u8]; // Final set + + +function check: + async check into r0; + output r0 as good_constructor.aleo/check.future; +finalize check: + get data[0u8] into r0; + assert.eq r0 1u8; + + +