From 558625f380507757f8bc87d845fe754b6890b470 Mon Sep 17 00:00:00 2001 From: Michael Benfield Date: Wed, 25 Jun 2025 12:58:01 -0700 Subject: [PATCH 01/25] Allow external structs. Specifically, 1. Introduce a PlaintextType::ExternalStruct, and allow referring to such a type in .aleo code by a Locator. 2. Allow casting to external struct types. 3. Let two struct types be considered the same type as long as they are structurally the same. --- circuit/program/src/data/value/mod.rs | 21 ++ .../src/data_types/array_type/bytes.rs | 11 +- .../src/data_types/array_type/parse.rs | 8 +- .../src/data_types/plaintext_type/bytes.rs | 7 +- .../src/data_types/plaintext_type/mod.rs | 46 ++- .../src/data_types/plaintext_type/parse.rs | 2 + .../src/data_types/register_type/mod.rs | 25 +- synthesizer/process/src/cost.rs | 16 +- .../src/stack/finalize_types/initialize.rs | 31 +- .../process/src/stack/finalize_types/mod.rs | 16 +- .../process/src/stack/helpers/sample.rs | 5 + .../process/src/stack/helpers/stack_trait.rs | 5 + .../src/stack/register_types/initialize.rs | 79 +++-- .../src/stack/register_types/matches.rs | 4 +- .../process/src/stack/register_types/mod.rs | 18 +- synthesizer/program/src/closure/mod.rs | 5 + synthesizer/program/src/lib.rs | 52 +++- .../src/logic/instruction/operation/call.rs | 47 ++- .../src/logic/instruction/operation/cast.rs | 277 ++++++++++-------- .../src/logic/instruction/operation/hash.rs | 3 +- .../logic/instruction/operation/literals.rs | 1 + synthesizer/program/src/parse.rs | 22 +- .../program/src/traits/stack_and_registers.rs | 79 +++++ .../parser/program/external_struct.out | 1 + .../process/execute/external_struct.out | 38 +++ .../tests/parser/program/external_struct.aleo | 5 + .../process/execute/external_struct.aleo | 118 ++++++++ 27 files changed, 703 insertions(+), 239 deletions(-) create mode 100644 synthesizer/tests/expectations/parser/program/external_struct.out create mode 100644 synthesizer/tests/expectations/process/execute/external_struct.out create mode 100644 synthesizer/tests/tests/parser/program/external_struct.aleo create mode 100644 synthesizer/tests/tests/process/execute/external_struct.aleo diff --git a/circuit/program/src/data/value/mod.rs b/circuit/program/src/data/value/mod.rs index a898624d6e..2af29b8fd4 100644 --- a/circuit/program/src/data/value/mod.rs +++ b/circuit/program/src/data/value/mod.rs @@ -66,3 +66,24 @@ impl Eject for Value { } } } + +impl From> for Value { + /// Initializes the value from a plaintext. + fn from(plaintext: Plaintext) -> Self { + Self::Plaintext(plaintext) + } +} + +impl From>> for Value { + /// Initializes the value from a record. + fn from(record: Record>) -> Self { + Self::Record(record) + } +} + +impl From> for Value { + /// Initializes the value from a future. + fn from(future: Future) -> Self { + Self::Future(future) + } +} diff --git a/console/program/src/data_types/array_type/bytes.rs b/console/program/src/data_types/array_type/bytes.rs index 60108d8264..dea57da750 100644 --- a/console/program/src/data_types/array_type/bytes.rs +++ b/console/program/src/data_types/array_type/bytes.rs @@ -14,7 +14,7 @@ // limitations under the License. use super::*; -use crate::{Identifier, LiteralType}; +use crate::{Identifier, LiteralType, Locator}; impl FromBytes for ArrayType { fn read_le(mut reader: R) -> IoResult { @@ -23,7 +23,8 @@ impl FromBytes for ArrayType { let element_type = match variant { 0 => PlaintextType::Literal(LiteralType::read_le(&mut reader)?), 1 => PlaintextType::Struct(Identifier::read_le(&mut reader)?), - 2.. => return Err(error(format!("Failed to deserialize element type {variant}"))), + 3 => PlaintextType::ExternalStruct(Locator::read_le(&mut reader)?), + _ => return Err(error(format!("Failed to deserialize element type {variant}"))), }; // Read the number of dimensions of the array. @@ -58,7 +59,7 @@ impl ToBytes for ArrayType { // Note that the lengths are in the order of the outermost dimension to the innermost dimension. for _ in 1..N::MAX_DATA_DEPTH { element_type = match element_type { - PlaintextType::Literal(_) | PlaintextType::Struct(_) => break, + PlaintextType::Literal(_) | PlaintextType::Struct(_) | PlaintextType::ExternalStruct(_) => break, PlaintextType::Array(array_type) => { lengths.push(*array_type.length()); array_type.next_element_type().clone() @@ -86,6 +87,10 @@ impl ToBytes for ArrayType { // out of an abundance of caution. return Err(error(format!("Array type exceeds the maximum depth of {}.", N::MAX_DATA_DEPTH))); } + PlaintextType::ExternalStruct(locator) => { + 3u8.write_le(&mut writer)?; + locator.write_le(&mut writer)?; + } } // Write the number of dimensions of the array. diff --git a/console/program/src/data_types/array_type/parse.rs b/console/program/src/data_types/array_type/parse.rs index 1c0de98112..bd694646ac 100644 --- a/console/program/src/data_types/array_type/parse.rs +++ b/console/program/src/data_types/array_type/parse.rs @@ -14,7 +14,7 @@ // limitations under the License. use super::*; -use crate::{Identifier, LiteralType}; +use crate::{Identifier, LiteralType, Locator}; impl Parser for ArrayType { /// Parses a string into a literal type. @@ -22,7 +22,11 @@ impl Parser for ArrayType { fn parse(string: &str) -> ParserResult { // A helper function to parse the innermost element type. fn parse_inner_element_type(string: &str) -> ParserResult> { - alt((map(LiteralType::parse, PlaintextType::from), map(Identifier::parse, PlaintextType::from)))(string) + alt(( + map(Locator::parse, PlaintextType::from), + map(LiteralType::parse, PlaintextType::from), + map(Identifier::parse, PlaintextType::from), + ))(string) } // A helper function to parse the length of each dimension. diff --git a/console/program/src/data_types/plaintext_type/bytes.rs b/console/program/src/data_types/plaintext_type/bytes.rs index 5251f3ef02..db493b676e 100644 --- a/console/program/src/data_types/plaintext_type/bytes.rs +++ b/console/program/src/data_types/plaintext_type/bytes.rs @@ -23,7 +23,8 @@ impl FromBytes for PlaintextType { 0 => Ok(Self::Literal(LiteralType::read_le(&mut reader)?)), 1 => Ok(Self::Struct(Identifier::read_le(&mut reader)?)), 2 => Ok(Self::Array(ArrayType::read_le(&mut reader)?)), - 3.. => Err(error(format!("Failed to deserialize annotation variant {variant}"))), + 3 => Ok(Self::ExternalStruct(Locator::read_le(&mut reader)?)), + 4.. => Err(error(format!("Failed to deserialize annotation variant {variant}"))), } } } @@ -44,6 +45,10 @@ impl ToBytes for PlaintextType { 2u8.write_le(&mut writer)?; array_type.write_le(&mut writer) } + Self::ExternalStruct(locator) => { + 3u8.write_le(&mut writer)?; + locator.write_le(&mut writer) + } } } } diff --git a/console/program/src/data_types/plaintext_type/mod.rs b/console/program/src/data_types/plaintext_type/mod.rs index a69f9baa40..1fc8dc20e7 100644 --- a/console/program/src/data_types/plaintext_type/mod.rs +++ b/console/program/src/data_types/plaintext_type/mod.rs @@ -17,7 +17,7 @@ mod bytes; mod parse; mod serialize; -use crate::{ArrayType, Identifier, LiteralType}; +use crate::{ArrayType, Identifier, LiteralType, Locator, ProgramID}; use snarkvm_console_network::prelude::*; /// A `PlaintextType` defines the type parameter for a literal, struct, or array. @@ -26,14 +26,49 @@ pub enum PlaintextType { /// A literal type contains its type name. /// The format of the type is ``. Literal(LiteralType), - /// An struct type contains its identifier. + /// A struct type contains its identifier. /// The format of the type is ``. Struct(Identifier), + /// An external struct type contains its locator. + /// The format of the type is `/`. + ExternalStruct(Locator), /// An array type contains its element type and length. /// The format of the type is `[; ]`. Array(ArrayType), } +impl PlaintextType { + /// Are the two types equivalent for the purposes of static checking? + /// + /// Since struct types are compared by structure, we can't determine equality + /// by only looking at their names. + pub fn equal_or_structs(&self, rhs: &Self) -> bool { + use PlaintextType::*; + + match (self, rhs) { + (ExternalStruct(..) | Struct(..), ExternalStruct(..) | Struct(..)) => true, + (Literal(lit0), Literal(lit1)) => lit0 == lit1, + (Array(array0), Array(array1)) => { + array0.length() == array1.length() + && array0.base_element_type().equal_or_structs(array1.base_element_type()) + } + _ => false, + } + } + + // Make unqualified structs into external ones with the given `id`. + pub fn qualify(self, id: ProgramID) -> Self { + match self { + PlaintextType::ExternalStruct(..) | PlaintextType::Literal(..) => self, + PlaintextType::Struct(name) => PlaintextType::ExternalStruct(Locator::new(id, name)), + PlaintextType::Array(array_type) => { + let element_type = array_type.next_element_type().clone().qualify(id); + PlaintextType::Array(ArrayType::new(element_type, vec![*array_type.length()]).unwrap()) + } + } + } +} + impl From for PlaintextType { /// Initializes a plaintext type from a literal type. fn from(literal: LiteralType) -> Self { @@ -48,6 +83,13 @@ impl From> for PlaintextType { } } +impl From> for PlaintextType { + /// Initializes a plaintext type from an external struct type. + fn from(locator: Locator) -> Self { + PlaintextType::ExternalStruct(locator) + } +} + impl From> for PlaintextType { /// Initializes a plaintext type from an array type. fn from(array: ArrayType) -> Self { diff --git a/console/program/src/data_types/plaintext_type/parse.rs b/console/program/src/data_types/plaintext_type/parse.rs index 74a9ce1a38..05cae4afcc 100644 --- a/console/program/src/data_types/plaintext_type/parse.rs +++ b/console/program/src/data_types/plaintext_type/parse.rs @@ -22,6 +22,7 @@ impl Parser for PlaintextType { // Parse to determine the plaintext type (order matters). alt(( map(ArrayType::parse, |type_| Self::Array(type_)), + map(Locator::parse, |locator| Self::ExternalStruct(locator)), map(Identifier::parse, |identifier| Self::Struct(identifier)), map(LiteralType::parse, |type_| Self::Literal(type_)), ))(string) @@ -60,6 +61,7 @@ impl Display for PlaintextType { Self::Literal(literal) => Display::fmt(literal, f), // Prints the struct, i.e. signature Self::Struct(struct_) => Display::fmt(struct_, f), + Self::ExternalStruct(locator) => Display::fmt(locator, f), // Prints the array type, i.e. [field; 2u32] Self::Array(array) => Display::fmt(array, f), } diff --git a/console/program/src/data_types/register_type/mod.rs b/console/program/src/data_types/register_type/mod.rs index 5c94b41bae..e1def9398d 100644 --- a/console/program/src/data_types/register_type/mod.rs +++ b/console/program/src/data_types/register_type/mod.rs @@ -17,7 +17,7 @@ mod bytes; mod parse; mod serialize; -use crate::{FinalizeType, Identifier, Locator, PlaintextType, ValueType}; +use crate::{FinalizeType, Identifier, Locator, PlaintextType, ProgramID, ValueType}; use snarkvm_console_network::prelude::*; use enum_index::EnumIndex; @@ -34,6 +34,29 @@ pub enum RegisterType { Future(Locator), } +impl RegisterType { + /// Are the two types equivalent for the purposes of static checking? + /// + /// Since struct types are compared by structure, we can't determine equality + /// by only looking at their names. + pub fn equal_or_structs(&self, rhs: &Self) -> bool { + use RegisterType::*; + match (self, rhs) { + (Plaintext(a), Plaintext(b)) => a.equal_or_structs(b), + _ => self == rhs, + } + } + + // Make unqualified structs or records into external ones with the given `id`. + pub fn qualify(self, id: ProgramID) -> Self { + match self { + RegisterType::Plaintext(plaintext_type) => RegisterType::Plaintext(plaintext_type.qualify(id)), + RegisterType::Record(name) => RegisterType::ExternalRecord(Locator::new(id, name)), + RegisterType::ExternalRecord(..) | RegisterType::Future(..) => self, + } + } +} + impl From> for RegisterType { /// Converts a value type to a register type. fn from(value: ValueType) -> Self { diff --git a/synthesizer/process/src/cost.rs b/synthesizer/process/src/cost.rs index 449acc7812..7b582c36a8 100644 --- a/synthesizer/process/src/cost.rs +++ b/synthesizer/process/src/cost.rs @@ -155,6 +155,10 @@ fn plaintext_size_in_bytes(stack: &Stack, plaintext_type: &Plaint // Return the size of the struct. Ok(size_of_name.saturating_add(size_of_members)) } + PlaintextType::ExternalStruct(locator) => { + let external_stack = stack.get_external_stack(locator.program_id())?; + plaintext_size_in_bytes(&*external_stack, &PlaintextType::Struct(*locator.resource())) + } PlaintextType::Array(array_type) => { // Retrieve the number of elements in the array. let num_elements = **array_type.length() as u64; @@ -266,7 +270,9 @@ pub fn cost_per_command( FinalizeType::Plaintext(PlaintextType::Literal(LiteralType::Field)) => Ok(1_500), FinalizeType::Plaintext(PlaintextType::Literal(_)) => Ok(500), FinalizeType::Plaintext(PlaintextType::Array(_)) => bail!("'div' does not support arrays"), - FinalizeType::Plaintext(PlaintextType::Struct(_)) => bail!("'div' does not support structs"), + FinalizeType::Plaintext(PlaintextType::Struct(_) | PlaintextType::ExternalStruct(_)) => { + bail!("'div' does not support structs") + } FinalizeType::Future(_) => bail!("'div' does not support futures"), } } @@ -345,7 +351,9 @@ pub fn cost_per_command( FinalizeType::Plaintext(PlaintextType::Literal(LiteralType::Scalar)) => Ok(10_000), FinalizeType::Plaintext(PlaintextType::Literal(_)) => Ok(500), FinalizeType::Plaintext(PlaintextType::Array(_)) => bail!("'mul' does not support arrays"), - FinalizeType::Plaintext(PlaintextType::Struct(_)) => bail!("'mul' does not support structs"), + FinalizeType::Plaintext(PlaintextType::Struct(_) | PlaintextType::ExternalStruct(_)) => { + bail!("'mul' does not support structs") + } FinalizeType::Future(_) => bail!("'mul' does not support futures"), } } @@ -365,7 +373,9 @@ pub fn cost_per_command( FinalizeType::Plaintext(PlaintextType::Literal(LiteralType::Field)) => Ok(1_500), FinalizeType::Plaintext(PlaintextType::Literal(_)) => Ok(500), FinalizeType::Plaintext(PlaintextType::Array(_)) => bail!("'pow' does not support arrays"), - FinalizeType::Plaintext(PlaintextType::Struct(_)) => bail!("'pow' does not support structs"), + FinalizeType::Plaintext(PlaintextType::Struct(_) | PlaintextType::ExternalStruct(_)) => { + bail!("'pow' does not support structs") + } FinalizeType::Future(_) => bail!("'pow' does not support futures"), } } diff --git a/synthesizer/process/src/stack/finalize_types/initialize.rs b/synthesizer/process/src/stack/finalize_types/initialize.rs index cc3b8bb1bd..47530b93d3 100644 --- a/synthesizer/process/src/stack/finalize_types/initialize.rs +++ b/synthesizer/process/src/stack/finalize_types/initialize.rs @@ -127,15 +127,10 @@ impl FinalizeTypes { impl FinalizeTypes { /// Ensure the given input register is well-formed. - #[inline] fn check_input(&mut self, stack: &Stack, register: &Register, finalize_type: &FinalizeType) -> Result<()> { // Ensure the register type is defined in the program. match finalize_type { - FinalizeType::Plaintext(PlaintextType::Literal(..)) => (), - FinalizeType::Plaintext(PlaintextType::Struct(struct_name)) => { - RegisterTypes::check_struct(stack, struct_name)? - } - FinalizeType::Plaintext(PlaintextType::Array(array_type)) => RegisterTypes::check_array(stack, array_type)?, + FinalizeType::Plaintext(plaintext_type) => RegisterTypes::check_plaintext_type(stack, plaintext_type)?, FinalizeType::Future(..) => (), }; @@ -630,19 +625,27 @@ impl FinalizeTypes { | CastType::Plaintext(PlaintextType::Literal(..)) => { ensure!(instruction.operands().len() == 1, "Expected 1 operand."); } - CastType::Plaintext(PlaintextType::Struct(struct_name)) => { - // Ensure the struct name exists in the program. - if !stack.program().contains_struct(struct_name) { - bail!("Struct '{struct_name}' is not defined.") - } + CastType::Plaintext(plaintext @ PlaintextType::Struct(struct_name)) => { + // Ensure that the type is valid. + RegisterTypes::check_plaintext_type(stack, plaintext)?; // Retrieve the struct. let struct_ = stack.program().get_struct(struct_name)?; // Ensure the operand types match the struct. self.matches_struct(stack, instruction.operands(), struct_)?; } - CastType::Plaintext(PlaintextType::Array(array_type)) => { - // Ensure that the array type is valid. - RegisterTypes::check_array(stack, array_type)?; + CastType::Plaintext(plaintext @ PlaintextType::ExternalStruct(locator)) => { + // Ensure that the type is valid. + RegisterTypes::check_plaintext_type(stack, plaintext)?; + let external_stack = stack.get_external_stack(locator.program_id())?; + let struct_name = locator.resource(); + // Retrieve the struct. + let struct_ = external_stack.program().get_struct(struct_name)?; + // Ensure the operand types match the struct. + self.matches_struct(&*external_stack, instruction.operands(), struct_)?; + } + CastType::Plaintext(plaintext @ PlaintextType::Array(array_type)) => { + // Ensure that the type is valid. + RegisterTypes::check_plaintext_type(stack, plaintext)?; // Ensure the operand types match the element type. self.matches_array(stack, instruction.operands(), array_type)?; } diff --git a/synthesizer/process/src/stack/finalize_types/mod.rs b/synthesizer/process/src/stack/finalize_types/mod.rs index d79a1c0f44..3a5dd429e5 100644 --- a/synthesizer/process/src/stack/finalize_types/mod.rs +++ b/synthesizer/process/src/stack/finalize_types/mod.rs @@ -142,6 +142,17 @@ impl FinalizeTypes { None => bail!("'{identifier}' does not exist in struct '{struct_name}'"), } } + // Access the member on the path to output the register type. + (FinalizeType::Plaintext(PlaintextType::ExternalStruct(locator)), Access::Member(identifier)) => { + // Retrieve the member type from the struct and check that it exists. + let external_stack = stack.get_external_stack(locator.program_id())?; + match external_stack.program().get_struct(locator.resource())?.members().get(identifier) { + // Retrieve the member and update `finalize_type` for the next iteration. + Some(member_type) => finalize_type = FinalizeType::Plaintext(member_type.clone()), + // Halts if the member does not exist. + None => bail!("'{identifier}' does not exist in struct '{locator}'"), + } + } // Access the member on the path to output the register type and check that it is in bounds. (FinalizeType::Plaintext(PlaintextType::Array(array_type)), Access::Index(index)) => { match index < array_type.length() { @@ -178,7 +189,10 @@ impl FinalizeTypes { None => bail!("Index out of bounds"), } } - (FinalizeType::Plaintext(PlaintextType::Struct(..)), Access::Index(..)) + ( + FinalizeType::Plaintext(PlaintextType::Struct(..) | PlaintextType::ExternalStruct(..)), + Access::Index(..), + ) | (FinalizeType::Plaintext(PlaintextType::Array(..)), Access::Member(..)) | (FinalizeType::Future(..), Access::Member(..)) => { bail!("Invalid access `{access}`") diff --git a/synthesizer/process/src/stack/helpers/sample.rs b/synthesizer/process/src/stack/helpers/sample.rs index 082e09b548..ab1f9f5bd4 100644 --- a/synthesizer/process/src/stack/helpers/sample.rs +++ b/synthesizer/process/src/stack/helpers/sample.rs @@ -140,6 +140,11 @@ impl Stack { Plaintext::Struct(members, Default::default()) } + PlaintextType::ExternalStruct(locator) => { + let external_stack = self.get_external_stack(locator.program_id())?; + let new_type = PlaintextType::Struct(*locator.resource()); + return external_stack.sample_plaintext_internal(&new_type, depth, rng); + } // Sample an array. PlaintextType::Array(array_type) => { // Sample each element of the array. diff --git a/synthesizer/process/src/stack/helpers/stack_trait.rs b/synthesizer/process/src/stack/helpers/stack_trait.rs index 99a1bc3743..574a7778f1 100644 --- a/synthesizer/process/src/stack/helpers/stack_trait.rs +++ b/synthesizer/process/src/stack/helpers/stack_trait.rs @@ -421,6 +421,11 @@ impl Stack { // If `plaintext` is an array, this is a mismatch. Plaintext::Array(..) => bail!("'{plaintext_type}' is invalid: expected literal, found array"), }, + PlaintextType::ExternalStruct(locator) => { + let external_stack = self.get_external_stack(locator.program_id())?; + let new_type = PlaintextType::Struct(*locator.resource()); + external_stack.matches_plaintext_internal(plaintext, &new_type, depth) + } PlaintextType::Struct(struct_name) => { // Ensure the struct name is valid. ensure!(!Program::is_reserved_keyword(struct_name), "Struct '{struct_name}' is reserved"); diff --git a/synthesizer/process/src/stack/register_types/initialize.rs b/synthesizer/process/src/stack/register_types/initialize.rs index 7494855cd4..dd4a476a0a 100644 --- a/synthesizer/process/src/stack/register_types/initialize.rs +++ b/synthesizer/process/src/stack/register_types/initialize.rs @@ -254,9 +254,7 @@ impl RegisterTypes { fn check_input(&mut self, stack: &Stack, register: &Register, register_type: &RegisterType) -> Result<()> { // Ensure the register type is defined in the program. match register_type { - RegisterType::Plaintext(PlaintextType::Literal(..)) => (), - RegisterType::Plaintext(PlaintextType::Struct(struct_name)) => Self::check_struct(stack, struct_name)?, - RegisterType::Plaintext(PlaintextType::Array(array_type)) => Self::check_array(stack, array_type)?, + RegisterType::Plaintext(plaintext_type) => Self::check_plaintext_type(stack, plaintext_type)?, RegisterType::Record(identifier) => { // Ensure the record type is defined in the program. if !stack.program().contains_record(identifier) { @@ -285,7 +283,6 @@ impl RegisterTypes { } /// Ensure the given output register is well-formed. - #[inline] fn check_output(&self, stack: &Stack, operand: &Operand, register_type: &RegisterType) -> Result<()> { #[cfg(feature = "aleo-cli")] match operand { @@ -303,9 +300,7 @@ impl RegisterTypes { // Ensure the register type is defined in the program. match register_type { - RegisterType::Plaintext(PlaintextType::Literal(..)) => (), - RegisterType::Plaintext(PlaintextType::Struct(struct_name)) => Self::check_struct(stack, struct_name)?, - RegisterType::Plaintext(PlaintextType::Array(array_type)) => Self::check_array(stack, array_type)?, + RegisterType::Plaintext(plaintext_type) => Self::check_plaintext_type(stack, plaintext_type)?, RegisterType::Record(identifier) => { // Ensure the record type is defined in the program. if !stack.program().contains_record(identifier) { @@ -332,10 +327,11 @@ impl RegisterTypes { }; // Ensure the operand type and the output type match. - if *register_type != self.get_type_from_operand(stack, operand)? { + let operand_type = self.get_type_from_operand(stack, operand)?; + if !register_types_structurally_equivalent(stack, register_type, stack, &operand_type)? { bail!( "Output '{operand}' does not match the expected output operand type: expected '{}', found '{}'", - self.get_type_from_operand(stack, operand)?, + operand_type, register_type ) } @@ -378,7 +374,6 @@ impl RegisterTypes { /// Ensures the opcode is a valid opcode and corresponds to the correct instruction. /// This method is called when adding a new closure or function to the program. - #[inline] fn check_instruction_opcode( &self, stack: &Stack, @@ -510,19 +505,27 @@ impl RegisterTypes { | CastType::Plaintext(PlaintextType::Literal(..)) => { ensure!(instruction.operands().len() == 1, "Expected 1 operand."); } - CastType::Plaintext(PlaintextType::Struct(struct_name)) => { - // Ensure the struct name exists in the program. - if !stack.program().contains_struct(struct_name) { - bail!("Struct '{struct_name}' is not defined.") - } + CastType::Plaintext(plaintext @ PlaintextType::Struct(struct_name)) => { + // Ensure the type is valid. + Self::check_plaintext_type(stack, plaintext)?; // Retrieve the struct. let struct_ = stack.program().get_struct(struct_name)?; // Ensure the operand types match the struct. self.matches_struct(stack, instruction.operands(), struct_)?; } - CastType::Plaintext(PlaintextType::Array(array_type)) => { - // Ensure that the array type is valid. - RegisterTypes::check_array(stack, array_type)?; + CastType::Plaintext(plaintext @ PlaintextType::ExternalStruct(locator)) => { + // Ensure the type is valid. + Self::check_plaintext_type(stack, plaintext)?; + let external_stack = stack.get_external_stack(locator.program_id())?; + let struct_name = locator.resource(); + // Retrieve the struct. + let struct_ = external_stack.program().get_struct(struct_name)?; + // Ensure the operand types match the struct. + self.matches_struct(&*external_stack, instruction.operands(), struct_)?; + } + CastType::Plaintext(plaintext @ PlaintextType::Array(array_type)) => { + // Ensure the type is valid. + Self::check_plaintext_type(stack, plaintext)?; // Ensure the operand types match the element type. self.matches_array(stack, instruction.operands(), array_type)?; } @@ -640,34 +643,24 @@ impl RegisterTypes { // Ok(()) // } - /// Ensures the struct exists in the program, and recursively-checks for array members. - pub(crate) fn check_struct(stack: &Stack, struct_name: &Identifier) -> Result<()> { - // Retrieve the struct from the program. - let Ok(struct_) = stack.program().get_struct(struct_name) else { - bail!("Struct '{struct_name}' in '{}' is not defined.", stack.program_id()) - }; - - // If the struct contains arrays, ensure their base element types are defined in the program. - for member in struct_.members().values() { - match member { - PlaintextType::Literal(..) => (), - PlaintextType::Struct(struct_name) => Self::check_struct(stack, struct_name)?, - PlaintextType::Array(array_type) => Self::check_array(stack, array_type)?, + /// Ensure any struct referenced directly or otherwise exists. + pub(crate) fn check_plaintext_type(stack: &Stack, type_: &PlaintextType) -> Result<()> { + match type_ { + PlaintextType::Literal(..) => Ok(()), + PlaintextType::Struct(struct_name) => { + // Retrieve the struct from the program. + let Ok(struct_) = stack.program().get_struct(struct_name) else { + bail!("Struct '{struct_name}' in '{}' is not defined.", stack.program_id()) + }; + struct_.members().values().try_for_each(|member| Self::check_plaintext_type(stack, member)) } - } - Ok(()) - } - - /// Ensure the base element type of the array is defined in the program. - pub(crate) fn check_array(stack: &Stack, array_type: &ArrayType) -> Result<()> { - // If the base element type is a struct, check that it is defined in the program. - if let PlaintextType::Struct(struct_name) = array_type.base_element_type() { - // Ensure the struct is defined in the program. - if !stack.program().contains_struct(struct_name) { - bail!("Struct '{struct_name}' in '{}' is not defined.", stack.program_id()) + PlaintextType::ExternalStruct(locator) => { + let external_stack = stack.get_external_stack(locator.program_id())?; + let struct_type = PlaintextType::Struct(*locator.resource()); + Self::check_plaintext_type(&*external_stack, &struct_type) } + PlaintextType::Array(array_type) => Self::check_plaintext_type(stack, array_type.base_element_type()), } - Ok(()) } /// Ensures the opcode is a valid opcode and corresponds to the `commit` instruction. diff --git a/synthesizer/process/src/stack/register_types/matches.rs b/synthesizer/process/src/stack/register_types/matches.rs index ef05ab0faa..2e07351132 100644 --- a/synthesizer/process/src/stack/register_types/matches.rs +++ b/synthesizer/process/src/stack/register_types/matches.rs @@ -64,7 +64,7 @@ impl RegisterTypes { // Ensure the register type matches the member type. RegisterType::Plaintext(type_) => { ensure!( - &type_ == member_type, + types_structurally_equivalent(stack, &type_, stack, member_type)?, "Struct entry '{struct_name}.{member_name}' expects a '{member_type}', but found '{type_}' in the operand '{operand}'.", ) } @@ -137,7 +137,7 @@ impl RegisterTypes { // Ensure the register type matches the element type. RegisterType::Plaintext(type_) => { ensure!( - &type_ == array_type.next_element_type(), + types_structurally_equivalent(stack, &type_, stack, array_type.next_element_type())?, "Array element expects a '{}', but found '{type_}' in the operand '{operand}'.", array_type.next_element_type() ) diff --git a/synthesizer/process/src/stack/register_types/mod.rs b/synthesizer/process/src/stack/register_types/mod.rs index 617559607c..7b19814bab 100644 --- a/synthesizer/process/src/stack/register_types/mod.rs +++ b/synthesizer/process/src/stack/register_types/mod.rs @@ -44,12 +44,14 @@ use snarkvm_synthesizer_program::{ Operand, Program, StackTrait, + register_types_structurally_equivalent, + types_structurally_equivalent, }; use console::program::{FinalizeType, Locator}; use indexmap::{IndexMap, IndexSet}; -#[derive(Clone, Default, PartialEq, Eq)] +#[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct RegisterTypes { /// The mapping of all input registers to their defined types. inputs: IndexMap>, @@ -213,6 +215,15 @@ impl RegisterTypes { None => bail!("'{identifier}' does not exist in struct '{struct_name}'"), } } + (RegisterAccessType::Plaintext(PlaintextType::ExternalStruct(locator)), Access::Member(identifier)) => { + let external_stack = stack.get_external_stack(locator.program_id())?; + // Retrieve the member type from the struct. + match external_stack.program().get_struct(locator.resource())?.members().get(identifier) { + // Update the member type. + Some(member_type) => register_type = RegisterAccessType::Plaintext(member_type.clone()), + None => bail!("'{identifier}' does not exist in struct '{locator}'"), + } + } // Traverse the path to output the register type. (RegisterAccessType::Plaintext(PlaintextType::Array(array_type)), Access::Index(index)) => { match index < array_type.length() { @@ -254,7 +265,10 @@ impl RegisterTypes { None => bail!("Index out of bounds"), } } - (RegisterAccessType::Plaintext(PlaintextType::Struct(..)), Access::Index(..)) + ( + RegisterAccessType::Plaintext(PlaintextType::Struct(..) | PlaintextType::ExternalStruct(..)), + Access::Index(..), + ) | (RegisterAccessType::Plaintext(PlaintextType::Array(..)), Access::Member(..)) | (RegisterAccessType::Future(..), Access::Member(..)) => { bail!("Invalid access `{access}`") diff --git a/synthesizer/program/src/closure/mod.rs b/synthesizer/program/src/closure/mod.rs index f4057fa5b0..4f2724e9e1 100644 --- a/synthesizer/program/src/closure/mod.rs +++ b/synthesizer/program/src/closure/mod.rs @@ -69,6 +69,11 @@ impl ClosureCore { pub const fn outputs(&self) -> &IndexSet> { &self.outputs } + + /// Returns the closure output types. + pub fn output_types(&self) -> Vec> { + self.outputs.iter().map(|output| output.register_type()).cloned().collect() + } } impl ClosureCore { diff --git a/synthesizer/program/src/lib.rs b/synthesizer/program/src/lib.rs index c99dfa8d29..5c485a498e 100644 --- a/synthesizer/program/src/lib.rs +++ b/synthesizer/program/src/lib.rs @@ -516,12 +516,32 @@ impl ProgramCore { bail!("'{member_identifier}' in struct '{}' is not defined.", struct_name) } } + PlaintextType::ExternalStruct(locator) => { + if !self.imports.contains_key(locator.program_id()) { + bail!( + "External program {} referenced in struct '{struct_name}' does not exist", + locator.program_id() + ); + } + } PlaintextType::Array(array_type) => { - if let PlaintextType::Struct(struct_name) = array_type.base_element_type() { + match array_type.base_element_type() { + PlaintextType::Struct(struct_name) => // Ensure the member struct name exists in the program. - if !self.structs.contains_key(struct_name) { - bail!("'{struct_name}' in array '{array_type}' is not defined.") + { + if !self.structs.contains_key(struct_name) { + bail!("'{struct_name}' in array '{array_type}' is not defined.") + } + } + PlaintextType::ExternalStruct(locator) => { + if !self.imports.contains_key(locator.program_id()) { + bail!( + "External program {} in array '{array_type}' does not exist", + locator.program_id() + ); + } } + PlaintextType::Array(..) | PlaintextType::Literal(..) => {} } } } @@ -573,12 +593,32 @@ impl ProgramCore { bail!("Struct '{identifier}' in record '{record_name}' is not defined.") } } + PlaintextType::ExternalStruct(locator) => { + if !self.imports.contains_key(locator.program_id()) { + bail!( + "External program {} referenced in record '{record_name}' does not exist", + locator.program_id() + ); + } + } PlaintextType::Array(array_type) => { - if let PlaintextType::Struct(struct_name) = array_type.base_element_type() { + match array_type.base_element_type() { + PlaintextType::Struct(struct_name) => // Ensure the member struct name exists in the program. - if !self.structs.contains_key(struct_name) { - bail!("'{struct_name}' in array '{array_type}' is not defined.") + { + if !self.structs.contains_key(struct_name) { + bail!("'{struct_name}' in array '{array_type}' is not defined.") + } + } + PlaintextType::ExternalStruct(locator) => { + if !self.imports.contains_key(locator.program_id()) { + bail!( + "External program {} in array '{array_type}' does not exist", + locator.program_id() + ); + } } + PlaintextType::Array(..) | PlaintextType::Literal(..) => {} } } } diff --git a/synthesizer/program/src/logic/instruction/operation/call.rs b/synthesizer/program/src/logic/instruction/operation/call.rs index 694dddb970..141d031dbc 100644 --- a/synthesizer/program/src/logic/instruction/operation/call.rs +++ b/synthesizer/program/src/logic/instruction/operation/call.rs @@ -16,7 +16,7 @@ use crate::{Opcode, Operand, RegistersCircuit, RegistersTrait, StackTrait}; use console::{ network::prelude::*, - program::{Identifier, Locator, Register, RegisterType, ValueType}, + program::{Identifier, Locator, Register, RegisterType}, }; /// The operator references a function name or closure name. @@ -181,16 +181,17 @@ impl Call { } /// Returns the output type from the given program and input types. - #[inline] pub fn output_types( &self, stack: &impl StackTrait, input_types: &[RegisterType], ) -> Result>> { - // Retrieve the external stack, if needed, and the resource. - let (external_stack, resource) = match &self.operator { + let stack_value; + let (is_external, program, name) = match &self.operator { CallOperator::Locator(locator) => { - (Some(stack.get_external_stack(locator.program_id())?), locator.resource()) + let program_name = locator.program_id(); + stack_value = Some(stack.get_external_stack(program_name)?); + (true, stack_value.as_ref().unwrap().program(), locator.resource()) } CallOperator::Resource(resource) => { // TODO (howardwu): Revisit this decision to forbid calling internal functions. A record cannot be spent again. @@ -199,16 +200,12 @@ impl Call { if stack.program().contains_function(resource) { bail!("Cannot call '{resource}'. Use a closure ('closure {resource}:') instead.") } - (None, resource) + (false, stack.program(), resource) } }; - // Retrieve the program. - let (is_external, program) = match &external_stack { - Some(external_stack) => (true, external_stack.program()), - None => (false, stack.program()), - }; + // If the operator is a closure, retrieve the closure and compute the output types. - if let Ok(closure) = program.get_closure(resource) { + if let Ok(closure) = program.get_closure(name) { // Ensure the number of operands matches the number of input statements. if closure.inputs().len() != self.operands.len() { bail!("Expected {} inputs, found {}", closure.inputs().len(), self.operands.len()) @@ -222,10 +219,16 @@ impl Call { bail!("Expected {} outputs, found {}", closure.outputs().len(), self.destinations.len()) } // Return the output register types. - Ok(closure.outputs().iter().map(|output| output.register_type()).cloned().collect()) + Ok(closure + .output_types() + .into_iter() + // If the function is an external program, we need to qualify its structs or records with + // the appropriate ProgramID. + .map(|output_type| if is_external { output_type.qualify(*program.id()) } else { output_type }) + .collect::>()) } // If the operator is a function, retrieve the function and compute the output types. - else if let Ok(function) = program.get_function(resource) { + else if let Ok(function) = program.get_function(name) { // Ensure the number of operands matches the number of input statements. if function.inputs().len() != self.operands.len() { bail!("Expected {} inputs, found {}", function.inputs().len(), self.operands.len()) @@ -239,18 +242,14 @@ impl Call { bail!("Expected {} outputs, found {}", function.outputs().len(), self.destinations.len()) } // Return the output register types. - function + Ok(function .output_types() .into_iter() - .map(|output_type| match (is_external, output_type) { - // If the output is a record and the function is external, return the external record type. - (true, ValueType::Record(record_name)) => Ok(RegisterType::ExternalRecord(Locator::from_str( - &format!("{}/{}", program.id(), record_name), - )?)), - // Else, return the register type. - (_, output_type) => Ok(RegisterType::from(output_type)), - }) - .collect::>>() + .map(RegisterType::from) + // If the function is an external program, we need to qualify its structs or records with + // the appropriate ProgramID. + .map(|register_type| if is_external { register_type.qualify(*program.id()) } else { register_type }) + .collect::>()) } // Else, throw an error. else { diff --git a/synthesizer/program/src/logic/instruction/operation/cast.rs b/synthesizer/program/src/logic/instruction/operation/cast.rs index 0c506d4b5a..bb08c1c144 100644 --- a/synthesizer/program/src/logic/instruction/operation/cast.rs +++ b/synthesizer/program/src/logic/instruction/operation/cast.rs @@ -30,6 +30,7 @@ use console::{ Record, Register, RegisterType, + StructType, Value, ValueType, }, @@ -230,7 +231,13 @@ impl CastOperation { registers.store(stack, &self.destination, Value::Plaintext(Plaintext::from(value))) } CastType::Plaintext(PlaintextType::Struct(struct_name)) => { - self.cast_to_struct(stack, registers, *struct_name, inputs) + let plaintext = self.evaluate_cast_to_struct(stack, *struct_name, inputs)?; + registers.store(stack, &self.destination, plaintext.into()) + } + CastType::Plaintext(PlaintextType::ExternalStruct(locator)) => { + let external_stack = stack.get_external_stack(locator.program_id())?; + let plaintext = self.evaluate_cast_to_struct(&*external_stack, *locator.resource(), inputs)?; + registers.store(stack, &self.destination, plaintext.into()) } CastType::Plaintext(PlaintextType::Array(array_type)) => { self.cast_to_array(stack, registers, array_type, inputs) @@ -381,57 +388,16 @@ impl CastOperation { circuit::Value::Plaintext(circuit::Plaintext::from(value)), ) } - CastType::Plaintext(PlaintextType::Struct(struct_)) => { - // Ensure the operands length is at least the minimum. - if inputs.len() < N::MIN_STRUCT_ENTRIES { - bail!("Casting to a struct requires at least {} operand(s)", N::MIN_STRUCT_ENTRIES) - } - // Ensure the number of members does not exceed the maximum. - if inputs.len() > N::MAX_STRUCT_ENTRIES { - bail!("Casting to struct '{struct_}' cannot exceed {} members", N::MAX_STRUCT_ENTRIES) - } - - // Retrieve the struct and ensure it is defined in the program. - let struct_ = stack.program().get_struct(struct_)?; - - // Ensure that the number of operands is equal to the number of struct members. - if inputs.len() != struct_.members().len() { - bail!( - "Casting to the struct {} requires {} operands, but {} were provided", - struct_.name(), - struct_.members().len(), - inputs.len() - ) - } - - // Initialize the struct members. - let mut members = IndexMap::new(); - for (member, (member_name, member_type)) in inputs.iter().zip_eq(struct_.members()) { - // Retrieve the plaintext value from the entry. - let plaintext = match member { - circuit::Value::Plaintext(plaintext) => { - // Ensure the member matches the register type. - stack.matches_plaintext(&plaintext.eject_value(), member_type)?; - // Output the plaintext. - plaintext.clone() - } - // Ensure the struct member is not a record. - circuit::Value::Record(..) => { - bail!("Casting a record into a struct member is illegal") - } - // Ensure the struct member is not a future. - circuit::Value::Future(..) => { - bail!("Casting a future into a struct member is illegal") - } - }; - // Append the member to the struct members. - members.insert(circuit::Identifier::constant(*member_name), plaintext); - } - - // Construct the struct. - let struct_ = circuit::Plaintext::Struct(members, Default::default()); + CastType::Plaintext(PlaintextType::ExternalStruct(locator)) => { + let external_stack = stack.get_external_stack(locator.program_id())?; + let plaintext = self.execute_cast_to_struct(&*external_stack, *locator.resource(), inputs)?; // Store the struct. - registers.store_circuit(stack, &self.destination, circuit::Value::Plaintext(struct_)) + registers.store_circuit(stack, &self.destination, plaintext.into()) + } + CastType::Plaintext(PlaintextType::Struct(struct_name)) => { + let plaintext = self.execute_cast_to_struct(stack, *struct_name, inputs)?; + // Store the struct. + registers.store_circuit(stack, &self.destination, plaintext.into()) } CastType::Plaintext(PlaintextType::Array(array_type)) => { // Ensure the operands length is at least the minimum. @@ -618,7 +584,13 @@ impl CastOperation { registers.store(stack, &self.destination, Value::Plaintext(Plaintext::from(value))) } CastType::Plaintext(PlaintextType::Struct(struct_name)) => { - self.cast_to_struct(stack, registers, *struct_name, inputs) + let plaintext = self.evaluate_cast_to_struct(stack, *struct_name, inputs)?; + registers.store(stack, &self.destination, plaintext.into()) + } + CastType::Plaintext(PlaintextType::ExternalStruct(locator)) => { + let external_stack = stack.get_external_stack(locator.program_id())?; + let plaintext = self.evaluate_cast_to_struct(&*external_stack, *locator.resource(), inputs)?; + registers.store(stack, &self.destination, plaintext.into()) } CastType::Plaintext(PlaintextType::Array(array_type)) => { self.cast_to_array(stack, registers, array_type, inputs) @@ -655,6 +627,54 @@ impl CastOperation { self.operands.len(), ); + let struct_checks = |struct_type: &StructType| { + let struct_name = struct_type.name(); + + // Ensure the input types length is at least the minimum. + if input_types.len() < N::MIN_STRUCT_ENTRIES { + bail!("Casting to a struct requires at least {} operand(s)", N::MIN_STRUCT_ENTRIES) + } + // Ensure the number of members does not exceed the maximum. + if input_types.len() > N::MAX_STRUCT_ENTRIES { + bail!("Casting to struct '{struct_type}' cannot exceed {} members", N::MAX_STRUCT_ENTRIES) + } + + // Ensure that the number of input types is equal to the number of struct members. + ensure!( + input_types.len() == struct_type.members().len(), + "Casting to the struct {} requires {} operands, but {} were provided", + struct_type.name(), + struct_type.members().len(), + input_types.len() + ); + // Ensure the input types match the struct. + for ((_, member_type), input_type) in struct_type.members().iter().zip_eq(input_types) { + match input_type { + // Ensure the plaintext type matches the member type. + RegisterType::Plaintext(plaintext_type) => { + ensure!( + member_type.equal_or_structs(plaintext_type), + "Struct '{struct_name}' member type mismatch: expected '{member_type}', found '{plaintext_type}'" + ) + } + // Ensure the input type cannot be a record (this is unsupported behavior). + RegisterType::Record(record_name) => bail!( + "Struct '{struct_name}' member type mismatch: expected '{member_type}', found record '{record_name}'" + ), + // Ensure the input type cannot be an external record (this is unsupported behavior). + RegisterType::ExternalRecord(locator) => bail!( + "Struct '{struct_name}' member type mismatch: expected '{member_type}', found external record '{locator}'" + ), + // Ensure the input type cannot be a future (this is unsupported behavior). + RegisterType::Future(..) => { + bail!("Struct '{struct_name}' member type mismatch: expected '{member_type}', found future") + } + } + } + + Ok(()) + }; + // Ensure the output type is defined in the program. match &self.cast_type { CastType::GroupXCoordinate | CastType::GroupYCoordinate => { @@ -668,51 +688,15 @@ impl CastOperation { CastType::Plaintext(PlaintextType::Literal(..)) => { ensure!(input_types.len() == 1, "Casting to a literal requires exactly 1 operand"); } + CastType::Plaintext(PlaintextType::ExternalStruct(locator)) => { + let external_stack = stack.get_external_stack(locator.program_id())?; + let struct_type = external_stack.program().get_struct(locator.resource())?; + struct_checks(struct_type)?; + } CastType::Plaintext(PlaintextType::Struct(struct_name)) => { // Retrieve the struct and ensure it is defined in the program. - let struct_ = stack.program().get_struct(struct_name)?; - - // Ensure the input types length is at least the minimum. - if input_types.len() < N::MIN_STRUCT_ENTRIES { - bail!("Casting to a struct requires at least {} operand(s)", N::MIN_STRUCT_ENTRIES) - } - // Ensure the number of members does not exceed the maximum. - if input_types.len() > N::MAX_STRUCT_ENTRIES { - bail!("Casting to struct '{struct_}' cannot exceed {} members", N::MAX_STRUCT_ENTRIES) - } - - // Ensure that the number of input types is equal to the number of struct members. - ensure!( - input_types.len() == struct_.members().len(), - "Casting to the struct {} requires {} operands, but {} were provided", - struct_.name(), - struct_.members().len(), - input_types.len() - ); - // Ensure the input types match the struct. - for ((_, member_type), input_type) in struct_.members().iter().zip_eq(input_types) { - match input_type { - // Ensure the plaintext type matches the member type. - RegisterType::Plaintext(plaintext_type) => { - ensure!( - member_type == plaintext_type, - "Struct '{struct_name}' member type mismatch: expected '{member_type}', found '{plaintext_type}'" - ) - } - // Ensure the input type cannot be a record (this is unsupported behavior). - RegisterType::Record(record_name) => bail!( - "Struct '{struct_name}' member type mismatch: expected '{member_type}', found record '{record_name}'" - ), - // Ensure the input type cannot be an external record (this is unsupported behavior). - RegisterType::ExternalRecord(locator) => bail!( - "Struct '{struct_name}' member type mismatch: expected '{member_type}', found external record '{locator}'" - ), - // Ensure the input type cannot be a future (this is unsupported behavior). - RegisterType::Future(..) => { - bail!("Struct '{struct_name}' member type mismatch: expected '{member_type}', found future") - } - } - } + let struct_type = stack.program().get_struct(struct_name)?; + struct_checks(struct_type)?; } CastType::Plaintext(PlaintextType::Array(array_type)) => { // Ensure the input types length is at least the minimum. @@ -740,7 +724,7 @@ impl CastOperation { // Ensure the plaintext type matches the member type. RegisterType::Plaintext(plaintext_type) => { ensure!( - plaintext_type == array_type.next_element_type(), + plaintext_type.equal_or_structs(array_type.next_element_type()), "Array element type mismatch: expected '{}', found '{plaintext_type}'", array_type.next_element_type() ) @@ -836,57 +820,102 @@ impl CastOperation { } } -impl CastOperation { - /// A helper method to handle casting to a struct. - fn cast_to_struct( - &self, - stack: &impl StackTrait, - registers: &mut impl RegistersTrait, - struct_name: Identifier, - inputs: Vec>, - ) -> Result<()> { +macro_rules! cast_to_struct_common { + ($N: ident, $struct_name: expr, $inputs: expr, $stack: expr, $plaintext: path, $record: path, $future: path, + $eject_value: expr, $process_member: expr + ) => {{ // Ensure the operands length is at least the minimum. - if inputs.len() < N::MIN_STRUCT_ENTRIES { - bail!("Casting to a struct requires at least {} operand", N::MIN_STRUCT_ENTRIES) + if $inputs.len() < $N::MIN_STRUCT_ENTRIES { + bail!("Casting to a struct requires at least {} operand(s)", N::MIN_STRUCT_ENTRIES) + } + // Ensure the number of members does not exceed the maximum. + if $inputs.len() > $N::MAX_STRUCT_ENTRIES { + bail!("Casting to a struct cannot exceed {} members", N::MAX_STRUCT_ENTRIES) } // Retrieve the struct and ensure it is defined in the program. - let struct_ = stack.program().get_struct(&struct_name)?; + let struct_ = $stack.program().get_struct($struct_name)?; // Ensure that the number of operands is equal to the number of struct members. - if inputs.len() != struct_.members().len() { + if $inputs.len() != struct_.members().len() { bail!( "Casting to the struct {} requires {} operands, but {} were provided", struct_.name(), struct_.members().len(), - inputs.len() + $inputs.len() ) } // Initialize the struct members. let mut members = IndexMap::new(); - for (member, (member_name, member_type)) in inputs.iter().zip_eq(struct_.members()) { + for (member, (member_name, member_type)) in $inputs.iter().zip_eq(struct_.members()) { // Retrieve the plaintext value from the entry. let plaintext = match member { - Value::Plaintext(plaintext) => { - // Ensure the plaintext matches the member type. - stack.matches_plaintext(plaintext, member_type)?; + $plaintext(plaintext) => { + // Ensure the member matches the register type. + $stack.matches_plaintext(&$eject_value(plaintext), member_type)?; // Output the plaintext. plaintext.clone() } // Ensure the struct member is not a record. - Value::Record(..) => bail!("Casting a record into a struct member is illegal"), + $record(..) => { + bail!("Casting a record into a struct member is illegal") + } // Ensure the struct member is not a future. - Value::Future(..) => bail!("Casting a future into a struct member is illegal"), + $future(..) => { + bail!("Casting a future into a struct member is illegal") + } }; // Append the member to the struct members. - members.insert(*member_name, plaintext); + members.insert($process_member(member_name), plaintext); } + members + }}; +} + +impl CastOperation { + fn evaluate_cast_to_struct( + &self, + stack: &impl StackTrait, + struct_name: Identifier, + inputs: Vec>, + ) -> Result> { + let members = cast_to_struct_common!( + N, + &struct_name, + inputs, + stack, + Value::Plaintext, + Value::Record, + Value::Future, + |x: &Plaintext<_>| x.clone(), + |x: &Identifier<_>| *x + ); + // Construct the struct. + Ok(Plaintext::Struct(members, Default::default())) + } + + fn execute_cast_to_struct>( + &self, + stack: &impl StackTrait, + struct_name: Identifier, + inputs: Vec>, + ) -> Result> { + use circuit::{Eject, Inject}; + let members = cast_to_struct_common!( + N, + &struct_name, + inputs, + stack, + circuit::Value::Plaintext, + circuit::Value::Record, + circuit::Value::Future, + |x: &circuit::Plaintext<_>| x.eject_value(), + |x: &Identifier| circuit::Identifier::constant(*x) + ); // Construct the struct. - let struct_ = Plaintext::Struct(members, Default::default()); - // Store the struct. - registers.store(stack, &self.destination, Value::Plaintext(struct_)) + Ok(circuit::Plaintext::Struct(members, Default::default())) } /// A helper method to handle casting to an array. @@ -975,7 +1004,7 @@ impl Parser for CastOperation { CastType::GroupXCoordinate | CastType::GroupYCoordinate | CastType::Plaintext(PlaintextType::Literal(_)) => 1, - CastType::Plaintext(PlaintextType::Struct(_)) => N::MAX_STRUCT_ENTRIES, + CastType::Plaintext(PlaintextType::Struct(_) | PlaintextType::ExternalStruct(_)) => N::MAX_STRUCT_ENTRIES, CastType::Plaintext(PlaintextType::Array(_)) => N::MAX_ARRAY_ELEMENTS, CastType::Record(_) | CastType::ExternalRecord(_) => N::MAX_RECORD_ENTRIES, }; @@ -1023,7 +1052,7 @@ impl Display for CastOperation { CastType::GroupYCoordinate | CastType::GroupXCoordinate | CastType::Plaintext(PlaintextType::Literal(_)) => 1, - CastType::Plaintext(PlaintextType::Struct(_)) => N::MAX_STRUCT_ENTRIES, + CastType::Plaintext(PlaintextType::Struct(_) | PlaintextType::ExternalStruct(_)) => N::MAX_STRUCT_ENTRIES, CastType::Plaintext(PlaintextType::Array(_)) => N::MAX_ARRAY_ELEMENTS, CastType::Record(_) | CastType::ExternalRecord(_) => N::MAX_RECORD_ENTRIES, }; @@ -1068,7 +1097,7 @@ impl FromBytes for CastOperation { CastType::GroupYCoordinate | CastType::GroupXCoordinate | CastType::Plaintext(PlaintextType::Literal(_)) => 1, - CastType::Plaintext(PlaintextType::Struct(_)) => N::MAX_STRUCT_ENTRIES, + CastType::Plaintext(PlaintextType::Struct(_) | PlaintextType::ExternalStruct(_)) => N::MAX_STRUCT_ENTRIES, CastType::Plaintext(PlaintextType::Array(_)) => N::MAX_ARRAY_ELEMENTS, CastType::Record(_) | CastType::ExternalRecord(_) => N::MAX_RECORD_ENTRIES, }; @@ -1089,7 +1118,7 @@ impl ToBytes for CastOperation { CastType::GroupYCoordinate | CastType::GroupXCoordinate | CastType::Plaintext(PlaintextType::Literal(_)) => 1, - CastType::Plaintext(PlaintextType::Struct(_)) => N::MAX_STRUCT_ENTRIES, + CastType::Plaintext(PlaintextType::Struct(_) | PlaintextType::ExternalStruct(_)) => N::MAX_STRUCT_ENTRIES, CastType::Plaintext(PlaintextType::Array(_)) => N::MAX_ARRAY_ELEMENTS, CastType::Record(_) | CastType::ExternalRecord(_) => N::MAX_RECORD_ENTRIES, }; diff --git a/synthesizer/program/src/logic/instruction/operation/hash.rs b/synthesizer/program/src/logic/instruction/operation/hash.rs index e1e582ce37..79c24c2e50 100644 --- a/synthesizer/program/src/logic/instruction/operation/hash.rs +++ b/synthesizer/program/src/logic/instruction/operation/hash.rs @@ -108,6 +108,7 @@ fn is_valid_destination_type(destination_type: &PlaintextType) -> PlaintextType::Literal(LiteralType::Boolean) | PlaintextType::Literal(LiteralType::String) | PlaintextType::Struct(..) + | PlaintextType::ExternalStruct(..) | PlaintextType::Array(..) ) } @@ -203,7 +204,7 @@ macro_rules! do_hash { let literal_type = match $destination_type { PlaintextType::Literal(literal_type) => *literal_type, - PlaintextType::Struct(..) => bail!("Cannot hash into a struct"), + PlaintextType::Struct(..) | PlaintextType::ExternalStruct(..) => bail!("Cannot hash into a struct"), PlaintextType::Array(..) => bail!("Cannot hash into an array (yet)"), }; diff --git a/synthesizer/program/src/logic/instruction/operation/literals.rs b/synthesizer/program/src/logic/instruction/operation/literals.rs index cb633c3428..eab21aabae 100644 --- a/synthesizer/program/src/logic/instruction/operation/literals.rs +++ b/synthesizer/program/src/logic/instruction/operation/literals.rs @@ -159,6 +159,7 @@ impl, LiteralType, NUM_OPERANDS>, const N .map(|input_type| match input_type { RegisterType::Plaintext(PlaintextType::Literal(literal_type)) => Ok(*literal_type), RegisterType::Plaintext(PlaintextType::Struct(..)) + | RegisterType::Plaintext(PlaintextType::ExternalStruct(..)) | RegisterType::Plaintext(PlaintextType::Array(..)) | RegisterType::Record(..) | RegisterType::ExternalRecord(..) diff --git a/synthesizer/program/src/parse.rs b/synthesizer/program/src/parse.rs index f9e9133806..4eeaff1098 100644 --- a/synthesizer/program/src/parse.rs +++ b/synthesizer/program/src/parse.rs @@ -75,6 +75,18 @@ impl Parser for ProgramCore { return map_res(take(0usize), Err)(string); } }; + + // Add the imports (if any) to the program. + for import in imports { + match program.add_import(import) { + Ok(_) => (), + Err(error) => { + eprintln!("{error}"); + return map_res(take(0usize), Err)(string); + } + } + } + // Construct the program with the parsed components. for component in components { let result = match component { @@ -93,16 +105,6 @@ impl Parser for ProgramCore { } } } - // Lastly, add the imports (if any) to the program. - for import in imports { - match program.add_import(import) { - Ok(_) => (), - Err(error) => { - eprintln!("{error}"); - return map_res(take(0usize), Err)(string); - } - } - } Ok((string, program)) } diff --git a/synthesizer/program/src/traits/stack_and_registers.rs b/synthesizer/program/src/traits/stack_and_registers.rs index 1c4d451a1d..5635ed646b 100644 --- a/synthesizer/program/src/traits/stack_and_registers.rs +++ b/synthesizer/program/src/traits/stack_and_registers.rs @@ -31,6 +31,7 @@ use console::{ Record, Register, RegisterType, + StructType, Value, ValueType, }, @@ -137,6 +138,84 @@ pub trait StackTrait { ) -> Result>>; } +/// Are the two types either the same, or both structurally equivalent `PlaintextType`s? +pub fn register_types_structurally_equivalent( + stack0: &impl StackTrait, + type0: &RegisterType, + stack1: &impl StackTrait, + type1: &RegisterType, +) -> Result { + use RegisterType::*; + if let (Plaintext(plaintext0), Plaintext(plaintext1)) = (type0, type1) { + types_structurally_equivalent(stack0, plaintext0, stack1, plaintext1) + } else { + Ok(type0 == type1) + } +} + +/// Determines whether two `PlaintextType` values are structurally equivalent. +/// +/// Structural equivalence means that the types have the same shape and member layout, +/// but not necessarily the same identity. For example, two struct types with the same +/// member names and types (recursively, ordered) are considered equivalent, +/// even if they are declared separately or come from different programs. +/// +/// The stacks are passed because struct types need to access their stack to get their +/// structure. +pub fn types_structurally_equivalent( + stack0: &impl StackTrait, + type0: &PlaintextType, + stack1: &impl StackTrait, + type1: &PlaintextType, +) -> Result { + use PlaintextType::*; + + let struct_compare = |stack0, st0: &StructType, stack1, st1: &StructType| -> Result { + if st0.members().len() != st1.members().len() { + return Ok(false); + } + + for ((name0, type0), (name1, type1)) in st0.members().iter().zip(st1.members()) { + if name0 != name1 || !types_structurally_equivalent(stack0, type0, stack1, type1)? { + return Ok(false); + } + } + + Ok(true) + }; + + match (type0, type1) { + (Array(array0), Array(array1)) => Ok(array0.length() == array1.length() + && types_structurally_equivalent(stack0, array0.next_element_type(), stack1, array1.next_element_type())?), + (Literal(lit0), Literal(lit1)) => Ok(lit0 == lit1), + (Struct(id0), Struct(id1)) => { + let struct_type0 = stack0.program().get_struct(id0)?; + let struct_type1 = stack1.program().get_struct(id1)?; + struct_compare(stack0, struct_type0, stack1, struct_type1) + } + (ExternalStruct(loc0), ExternalStruct(loc1)) => { + let external_stack0 = stack0.get_external_stack(loc0.program_id())?; + let struct_type0 = external_stack0.program().get_struct(loc0.resource())?; + let external_stack1 = stack1.get_external_stack(loc1.program_id())?; + let struct_type1 = external_stack1.program().get_struct(loc1.resource())?; + struct_compare(&*external_stack0, struct_type0, &*external_stack1, struct_type1) + } + (ExternalStruct(loc), Struct(id)) => { + let external_stack = stack0.get_external_stack(loc.program_id())?; + let struct_type0 = external_stack.program().get_struct(loc.resource())?; + let struct_type1 = stack1.program().get_struct(id)?; + struct_compare(&*external_stack, struct_type0, stack1, struct_type1) + } + (Struct(id), ExternalStruct(loc)) => { + let struct_type0 = stack0.program().get_struct(id)?; + let external_stack = stack1.get_external_stack(loc.program_id())?; + let struct_type1 = external_stack.program().get_struct(loc.resource())?; + struct_compare(stack0, struct_type0, &*external_stack, struct_type1) + } + _ => Ok(false), + } +} + pub trait FinalizeRegistersState: RegistersTrait { /// Returns the global state for the finalize scope. fn state(&self) -> &FinalizeGlobalState; diff --git a/synthesizer/tests/expectations/parser/program/external_struct.out b/synthesizer/tests/expectations/parser/program/external_struct.out new file mode 100644 index 0000000000..b7dc87efed --- /dev/null +++ b/synthesizer/tests/expectations/parser/program/external_struct.out @@ -0,0 +1 @@ +Program was successfully parsed. \ No newline at end of file diff --git a/synthesizer/tests/expectations/process/execute/external_struct.out b/synthesizer/tests/expectations/process/execute/external_struct.out new file mode 100644 index 0000000000..c4784e0e2a --- /dev/null +++ b/synthesizer/tests/expectations/process/execute/external_struct.out @@ -0,0 +1,38 @@ +errors: [] +outputs: +- - |- + [ + { + x: 4field + }, + { + x: 4field + } + ] +- - |- + { + x: 12field + } +- - |- + { + x: 12field + } +- - |- + { + x: 3field, + s: { + x: 3field + } + } +- - 1field +- - 2field +- - |- + { + x: 101field + } +- - |- + { + s: { + x: 100field + } + } diff --git a/synthesizer/tests/tests/parser/program/external_struct.aleo b/synthesizer/tests/tests/parser/program/external_struct.aleo new file mode 100644 index 0000000000..769ff0e494 --- /dev/null +++ b/synthesizer/tests/tests/parser/program/external_struct.aleo @@ -0,0 +1,5 @@ +program child.aleo; + +function child: + call parent.aleo/parent into r0; + output r0 as parent.aleo/S.public; diff --git a/synthesizer/tests/tests/process/execute/external_struct.aleo b/synthesizer/tests/tests/process/execute/external_struct.aleo new file mode 100644 index 0000000000..2b8d7103c6 --- /dev/null +++ b/synthesizer/tests/tests/process/execute/external_struct.aleo @@ -0,0 +1,118 @@ +/* +randomness: 45791624 +cases: + - program: child.aleo + function: call_make_array + inputs: [] + - program: child.aleo + function: make_s + inputs: [] + - program: child.aleo + function: make_t + inputs: [] + - program: child.aleo + function: call_make_outer + inputs: [] + - program: child.aleo + function: pass_s + inputs: [] + - program: child.aleo + function: pass_t + inputs: [] + - program: child.aleo + function: cast_s + inputs: [] + - program: child2.aleo + function: call_make_outer2 + inputs: [] +*/ + +program parent.aleo; + +struct S: + x as field; + +struct Outer: + x as field; + s as S; + +function make_s: + cast 12field into r0 as S; + output r0 as S.public; + +function make_outer: + input r0 as field.public; + cast r0 into r1 as S; + cast r0 r1 into r2 as Outer; + output r2 as Outer.public; + +function make_array: + input r0 as field.public; + cast r0 into r1 as S; + cast r1 r1 into r2 as [S; 2u32]; + output r2 as [S; 2u32].public; + +function accept_s: + input r0 as S.public; + output r0.x as field.public; + +///////////////////////////////////////////////// + +import parent.aleo; + +program child.aleo; + +struct T: + x as field; + +struct OuterT: + x as field; + s as T; + +struct Outer2: + s as parent.aleo/S; + +function make_s: + call parent.aleo/make_s into r0; + output r0 as parent.aleo/S.public; + +function make_t: + call parent.aleo/make_s into r0; + output r0 as T.public; + +function call_make_outer: + call parent.aleo/make_outer 3field into r0; + output r0 as OuterT.public; + +function call_make_array: + call parent.aleo/make_array 4field into r0; + output r0 as [T; 2u32].public; + +function pass_s: + cast 1field into r0 as parent.aleo/S; + call parent.aleo/accept_s r0 into r1; + output r1 as field.public; + +function pass_t: + cast 2field into r0 as T; + call parent.aleo/accept_s r0 into r1; + output r1 as field.public; + +function make_outer2: + cast 100field into r0 as parent.aleo/S; + cast r0 into r1 as Outer2; + output r1 as Outer2.public; + +function cast_s: + cast 101field into r0 as parent.aleo/S; + output r0 as parent.aleo/S.public; + +///////////////////////////////////////////////// + +import child.aleo; + +program child2.aleo; + +function call_make_outer2: + call child.aleo/make_outer2 into r0; + output r0 as child.aleo/Outer2.public; From e3601da7e7a8397d9097644499542ece745d4e31 Mon Sep 17 00:00:00 2001 From: Michael Benfield Date: Fri, 25 Jul 2025 11:02:41 -0700 Subject: [PATCH 02/25] Disallow deployments using external struct before ConsensusVersion::V10. --- .../src/data_types/plaintext_type/mod.rs | 11 ++ .../src/data_types/register_type/mod.rs | 4 + .../program/src/data_types/value_type/mod.rs | 9 ++ synthesizer/program/src/closure/mod.rs | 7 ++ synthesizer/program/src/constructor/mod.rs | 10 ++ synthesizer/program/src/function/mod.rs | 7 ++ synthesizer/program/src/lib.rs | 21 ++++ .../program/src/logic/instruction/mod.rs | 10 ++ .../src/logic/instruction/operation/cast.rs | 7 ++ synthesizer/program/src/mapping/mod.rs | 4 + synthesizer/src/vm/verify.rs | 104 ++++++++++-------- 11 files changed, 150 insertions(+), 44 deletions(-) diff --git a/console/program/src/data_types/plaintext_type/mod.rs b/console/program/src/data_types/plaintext_type/mod.rs index 1fc8dc20e7..ddff4423c3 100644 --- a/console/program/src/data_types/plaintext_type/mod.rs +++ b/console/program/src/data_types/plaintext_type/mod.rs @@ -56,6 +56,17 @@ impl PlaintextType { } } + /// Does this type refer to an external struct explicitly? + pub fn contains_external_struct(&self) -> bool { + use PlaintextType::*; + + match self { + Literal(..) | Struct(..) => false, + ExternalStruct(..) => true, + Array(array_type) => array_type.base_element_type().contains_external_struct(), + } + } + // Make unqualified structs into external ones with the given `id`. pub fn qualify(self, id: ProgramID) -> Self { match self { diff --git a/console/program/src/data_types/register_type/mod.rs b/console/program/src/data_types/register_type/mod.rs index e1def9398d..6aaaad4fcb 100644 --- a/console/program/src/data_types/register_type/mod.rs +++ b/console/program/src/data_types/register_type/mod.rs @@ -55,6 +55,10 @@ impl RegisterType { RegisterType::ExternalRecord(..) | RegisterType::Future(..) => self, } } + + pub fn contains_external_struct(&self) -> bool { + matches!(self, RegisterType::Plaintext(t) if t.contains_external_struct()) + } } impl From> for RegisterType { diff --git a/console/program/src/data_types/value_type/mod.rs b/console/program/src/data_types/value_type/mod.rs index c5a0c99c12..24ed6c4896 100644 --- a/console/program/src/data_types/value_type/mod.rs +++ b/console/program/src/data_types/value_type/mod.rs @@ -52,6 +52,15 @@ impl ValueType { ValueType::Future(..) => 5, } } + + /// Does this type refer to an external struct explicitly? + pub fn contains_external_struct(&self) -> bool { + use ValueType::*; + matches!( + self, + Constant(plaintext) | Public(plaintext) | Private(plaintext) if plaintext.contains_external_struct() + ) + } } impl From> for ValueType { diff --git a/synthesizer/program/src/closure/mod.rs b/synthesizer/program/src/closure/mod.rs index 4f2724e9e1..ecbab40263 100644 --- a/synthesizer/program/src/closure/mod.rs +++ b/synthesizer/program/src/closure/mod.rs @@ -74,6 +74,13 @@ impl ClosureCore { pub fn output_types(&self) -> Vec> { self.outputs.iter().map(|output| output.register_type()).cloned().collect() } + + /// Does this closure refer to an external struct explicitly? + pub fn contains_external_struct(&self) -> bool { + self.inputs.iter().any(|input| input.register_type().contains_external_struct()) + || self.outputs.iter().any(|output| output.register_type().contains_external_struct()) + || self.instructions.iter().any(|instruction| instruction.contains_external_struct()) + } } impl ClosureCore { diff --git a/synthesizer/program/src/constructor/mod.rs b/synthesizer/program/src/constructor/mod.rs index 76a3565b46..e51bec3090 100644 --- a/synthesizer/program/src/constructor/mod.rs +++ b/synthesizer/program/src/constructor/mod.rs @@ -50,6 +50,16 @@ impl ConstructorCore { pub const fn positions(&self) -> &HashMap, usize> { &self.positions } + + /// Does this constructor refer to an external struct explicitly? + pub fn contains_external_struct(&self) -> bool { + self.commands.iter().any(|command| { + matches!( + command, + Command::Instruction(instruction) if instruction.contains_external_struct() + ) + }) + } } impl ConstructorCore { diff --git a/synthesizer/program/src/function/mod.rs b/synthesizer/program/src/function/mod.rs index 89e9882701..6a7d249c6e 100644 --- a/synthesizer/program/src/function/mod.rs +++ b/synthesizer/program/src/function/mod.rs @@ -95,6 +95,13 @@ impl FunctionCore { pub const fn finalize_logic(&self) -> Option<&FinalizeCore> { self.finalize_logic.as_ref() } + + /// Does this function refer to an external struct explicitly? + pub fn contains_external_struct(&self) -> bool { + self.inputs.iter().any(|input| input.value_type().contains_external_struct()) + || self.outputs.iter().any(|output| output.value_type().contains_external_struct()) + || self.instructions.iter().any(|instruction| instruction.contains_external_struct()) + } } impl FunctionCore { diff --git a/synthesizer/program/src/lib.rs b/synthesizer/program/src/lib.rs index 40200319a5..39ce9b6d37 100644 --- a/synthesizer/program/src/lib.rs +++ b/synthesizer/program/src/lib.rs @@ -952,6 +952,27 @@ impl ProgramCore { // Return `false` since no V9 syntax was found. false } + + /// Does this program explicitly reference an external struct, like `other_program.aleo/StructType`? + /// + /// This function exists to check if programs to be deployed use external structs so they can be gated + /// by consensus version. + pub fn contains_external_struct(&self) -> bool { + self.mappings.values().any(|mapping| mapping.contains_external_struct()) + || self + .structs + .values() + .flat_map(|struct_| struct_.members().values()) + .any(|plaintext_type| plaintext_type.contains_external_struct()) + || self + .records + .values() + .flat_map(|record| record.entries().values()) + .any(|entry| entry.plaintext_type().contains_external_struct()) + || self.closures.values().any(|closure| closure.contains_external_struct()) + || self.functions.values().any(|function| function.contains_external_struct()) + || self.constructor.iter().any(|constructor| constructor.contains_external_struct()) + } } impl TypeName for ProgramCore { diff --git a/synthesizer/program/src/logic/instruction/mod.rs b/synthesizer/program/src/logic/instruction/mod.rs index c28e056cbd..b1829be830 100644 --- a/synthesizer/program/src/logic/instruction/mod.rs +++ b/synthesizer/program/src/logic/instruction/mod.rs @@ -430,6 +430,16 @@ impl Instruction { ) -> Result>> { instruction!(self, |instruction| instruction.output_types(stack, input_types)) } + + /// Does this instruction refer to an external struct explicitly? + pub fn contains_external_struct(&self) -> bool { + // Only cast instructions may contain an explicit reference to an external struct. + // Calls may produce them, but they don't explicitly reference the type, and that's + // always been allowed. + matches!(self, + Instruction::Cast(instruction) if instruction.cast_type().contains_external_struct() + ) + } } impl Debug for Instruction { diff --git a/synthesizer/program/src/logic/instruction/operation/cast.rs b/synthesizer/program/src/logic/instruction/operation/cast.rs index bb08c1c144..ce72103c39 100644 --- a/synthesizer/program/src/logic/instruction/operation/cast.rs +++ b/synthesizer/program/src/logic/instruction/operation/cast.rs @@ -135,6 +135,13 @@ impl FromBytes for CastType { } } +impl CastType { + /// Does this type refer to an external struct explicitly? + pub fn contains_external_struct(&self) -> bool { + matches!(self, CastType::Plaintext(plaintext_type) if plaintext_type.contains_external_struct()) + } +} + /// The `cast` instruction. pub type Cast = CastOperation; /// The `cast.lossy` instruction. diff --git a/synthesizer/program/src/mapping/mod.rs b/synthesizer/program/src/mapping/mod.rs index 7d268039d9..1b35416b2d 100644 --- a/synthesizer/program/src/mapping/mod.rs +++ b/synthesizer/program/src/mapping/mod.rs @@ -54,6 +54,10 @@ impl Mapping { pub const fn value(&self) -> &MapValue { &self.value } + + pub fn contains_external_struct(&self) -> bool { + self.key.plaintext_type().contains_external_struct() || self.value.plaintext_type().contains_external_struct() + } } impl TypeName for Mapping { diff --git a/synthesizer/src/vm/verify.rs b/synthesizer/src/vm/verify.rs index 98c5e1c4cf..28a2a32d70 100644 --- a/synthesizer/src/vm/verify.rs +++ b/synthesizer/src/vm/verify.rs @@ -188,50 +188,8 @@ impl> VM { ); // Verify the signature corresponds to the transaction ID. ensure!(owner.verify(*deployment_id), "Invalid owner signature for deployment transaction '{id}'"); - // If the `CONSENSUS_VERSION` is less than `V8`, ensure that - // - the deployment edition is zero. - // If the `CONSENSUS_VERSION` is less than `V9` ensure that - // - the deployment edition is zero or one. - // - the program checksum is **not** present in the deployment, - // - the program owner is **not** present in the deployment - // - the program does not use constructors, `Operand::Checksum`, `Operand::Edition`, or `Operand::ProgramOwner` - // If the `CONSENSUS_VERSION` is greater than or equal to `V9`, then verify that: - // - the program checksum is present in the deployment - // - the program owner is present in the deployment - if consensus_version < ConsensusVersion::V8 { - ensure!( - deployment.edition().is_zero(), - "Invalid deployment transaction '{id}' - edition should be zero before `ConsensusVersion::V8`", - ); - } - if consensus_version < ConsensusVersion::V9 { - ensure!( - deployment.edition() <= 1, - "Invalid deployment transaction '{id}' - edition should be zero or one for before `ConsensusVersion::V9`" - ); - ensure!( - deployment.program_checksum().is_none(), - "Invalid deployment transaction '{id}' - should not contain program checksum" - ); - ensure!( - deployment.program_owner().is_none(), - "Invalid deployment transaction '{id}' - should not contain program owner" - ); - ensure!( - !deployment.program().contains_v9_syntax(), - "Invalid deployment transaction '{id}' - program uses syntax that is not allowed before `ConsensusVersion::V9`" - ); - } - if consensus_version >= ConsensusVersion::V9 { - ensure!( - deployment.program_checksum().is_some(), - "Invalid deployment transaction '{id}' - missing program checksum" - ); - ensure!( - deployment.program_owner().is_some(), - "Invalid deployment transaction '{id}' - missing program owner" - ); - } + + ensure_deployment_valid_for_consensus_version(consensus_version, deployment, id)?; // If the program owner exists in the deployment, then verify that it matches the owner in the transaction. if let Some(given_owner) = deployment.program_owner() { @@ -456,6 +414,64 @@ impl> VM { } } +fn ensure_deployment_valid_for_consensus_version( + consensus_version: ConsensusVersion, + deployment: &Deployment, + id: &N::TransactionID, +) -> Result<()> { + // If the `CONSENSUS_VERSION` is less than `V8`, ensure that + // - the deployment edition is zero. + // If the `CONSENSUS_VERSION` is less than `V9` ensure that + // - the deployment edition is zero or one. + // - the program checksum is **not** present in the deployment, + // - the program owner is **not** present in the deployment + // - the program does not use constructors, `Operand::Checksum`, `Operand::Edition`, or `Operand::ProgramOwner` + // If the `CONSENSUS_VERSION` is greater than or equal to `V9`, then verify that: + // - the program checksum is present in the deployment + // - the program owner is present in the deployment + // If the `CONSENSUS_VERSION` is less than or equal to `V9`, then verify that: + // - the program does not use the external struct syntax `some_program.aleo/StructT` + if consensus_version < ConsensusVersion::V8 { + ensure!( + deployment.edition().is_zero(), + "Invalid deployment transaction '{id}' - edition should be zero before `ConsensusVersion::V8`", + ); + } + if consensus_version < ConsensusVersion::V9 { + ensure!( + deployment.edition() <= 1, + "Invalid deployment transaction '{id}' - edition should be zero or one for before `ConsensusVersion::V9`" + ); + ensure!( + deployment.program_checksum().is_none(), + "Invalid deployment transaction '{id}' - should not contain program checksum" + ); + ensure!( + deployment.program_owner().is_none(), + "Invalid deployment transaction '{id}' - should not contain program owner" + ); + ensure!( + !deployment.program().contains_v9_syntax(), + "Invalid deployment transaction '{id}' - program uses syntax that is not allowed before `ConsensusVersion::V9`" + ); + } + if consensus_version >= ConsensusVersion::V9 { + ensure!( + deployment.program_checksum().is_some(), + "Invalid deployment transaction '{id}' - missing program checksum" + ); + ensure!(deployment.program_owner().is_some(), "Invalid deployment transaction '{id}' - missing program owner"); + } + if consensus_version <= ConsensusVersion::V9 { + ensure!( + !deployment.program().contains_external_struct(), + "Invalid deployment transaction '{id}' - external structs may only be used beginning with Consensus version 10" + ); + } + + Ok(()) +} + impl> VM { /// Verifies the given deployment. On failure, returns an error. /// From 839f17d6fd9fc2d29da6b532c527535c1a9b832b Mon Sep 17 00:00:00 2001 From: Michael Benfield Date: Tue, 29 Jul 2025 09:43:53 -0700 Subject: [PATCH 03/25] Added more varied uses of external structs in parsing test. Added a failing test using a non-existant external struct. --- .../src/data_types/register_type/mod.rs | 1 + .../process/execute/external_struct_fail.out | 3 ++ .../tests/parser/program/external_struct.aleo | 12 +++++ .../process/execute/external_struct_fail.aleo | 51 +++++++++++++++++++ 4 files changed, 67 insertions(+) create mode 100644 synthesizer/tests/expectations/process/execute/external_struct_fail.out create mode 100644 synthesizer/tests/tests/process/execute/external_struct_fail.aleo diff --git a/console/program/src/data_types/register_type/mod.rs b/console/program/src/data_types/register_type/mod.rs index 6aaaad4fcb..1bd4e2ba07 100644 --- a/console/program/src/data_types/register_type/mod.rs +++ b/console/program/src/data_types/register_type/mod.rs @@ -56,6 +56,7 @@ impl RegisterType { } } + /// Does this type refer to an external struct explicitly? pub fn contains_external_struct(&self) -> bool { matches!(self, RegisterType::Plaintext(t) if t.contains_external_struct()) } diff --git a/synthesizer/tests/expectations/process/execute/external_struct_fail.out b/synthesizer/tests/expectations/process/execute/external_struct_fail.out new file mode 100644 index 0000000000..14bc936611 --- /dev/null +++ b/synthesizer/tests/expectations/process/execute/external_struct_fail.out @@ -0,0 +1,3 @@ +errors: +- Struct 'S2' in 'parent.aleo' is not defined. +outputs: [] diff --git a/synthesizer/tests/tests/parser/program/external_struct.aleo b/synthesizer/tests/tests/parser/program/external_struct.aleo index 769ff0e494..5c11d1ebaa 100644 --- a/synthesizer/tests/tests/parser/program/external_struct.aleo +++ b/synthesizer/tests/tests/parser/program/external_struct.aleo @@ -1,5 +1,17 @@ program child.aleo; +mapping refers_to_external_struct: + key as parent.aleo/SomeStruct.public; + value as parent.aleo/AnotherStruct.public; + +struct my_struct: + member as parent.aleo/S; + member2 as parent.aleo/S2; + function child: call parent.aleo/parent into r0; output r0 as parent.aleo/S.public; + +function child2: + input r0 as parent.aleo/S.public; + output r0 as parent.aleo/S.public; diff --git a/synthesizer/tests/tests/process/execute/external_struct_fail.aleo b/synthesizer/tests/tests/process/execute/external_struct_fail.aleo new file mode 100644 index 0000000000..38afebf150 --- /dev/null +++ b/synthesizer/tests/tests/process/execute/external_struct_fail.aleo @@ -0,0 +1,51 @@ +/* +randomness: 45791624 +cases: + - program: child.aleo + function: call_make_array + inputs: [] + - program: child.aleo + function: make_s + inputs: [] + - program: child.aleo + function: make_t + inputs: [] + - program: child.aleo + function: call_make_outer + inputs: [] + - program: child.aleo + function: pass_s + inputs: [] + - program: child.aleo + function: pass_t + inputs: [] + - program: child.aleo + function: cast_s + inputs: [] + - program: child2.aleo + function: call_make_outer2 + inputs: [] +*/ + +program parent.aleo; + +struct S: + x as field; + +function make_s: + cast 12field into r0 as S; + output r0 as S.public; + +///////////////////////////////////////////////// + +import parent.aleo; + +program child.aleo; + +struct T: + x as field; + +function make_s: + // This should fail as the struct doesn't exist. + cast 2field into r0 as parent.aleo/S2; + output r0 as parent.aleo/S2.public; From 00857360cabcc723e3f9505f228481a0f2166e56 Mon Sep 17 00:00:00 2001 From: Michael Benfield Date: Wed, 30 Jul 2025 07:42:22 -0700 Subject: [PATCH 04/25] Fix external_struct_fail test --- .../process/execute/external_struct_fail.aleo | 26 +------------------ 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/synthesizer/tests/tests/process/execute/external_struct_fail.aleo b/synthesizer/tests/tests/process/execute/external_struct_fail.aleo index 38afebf150..ef6b00efbb 100644 --- a/synthesizer/tests/tests/process/execute/external_struct_fail.aleo +++ b/synthesizer/tests/tests/process/execute/external_struct_fail.aleo @@ -1,30 +1,6 @@ /* randomness: 45791624 -cases: - - program: child.aleo - function: call_make_array - inputs: [] - - program: child.aleo - function: make_s - inputs: [] - - program: child.aleo - function: make_t - inputs: [] - - program: child.aleo - function: call_make_outer - inputs: [] - - program: child.aleo - function: pass_s - inputs: [] - - program: child.aleo - function: pass_t - inputs: [] - - program: child.aleo - function: cast_s - inputs: [] - - program: child2.aleo - function: call_make_outer2 - inputs: [] +cases: [] */ program parent.aleo; From 04b2a49c1d101c87977054c52c2a65d87518fb82 Mon Sep 17 00:00:00 2001 From: Michael Benfield Date: Wed, 30 Jul 2025 09:43:24 -0700 Subject: [PATCH 05/25] Fix tests --- .../process/execute/external_struct_fail.out | 2 +- .../tests/tests/parser/program/external_struct.aleo | 8 -------- .../tests/process/execute/external_struct_fail.aleo | 10 +++++----- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/synthesizer/tests/expectations/process/execute/external_struct_fail.out b/synthesizer/tests/expectations/process/execute/external_struct_fail.out index 14bc936611..07a276f1ea 100644 --- a/synthesizer/tests/expectations/process/execute/external_struct_fail.out +++ b/synthesizer/tests/expectations/process/execute/external_struct_fail.out @@ -1,3 +1,3 @@ errors: -- Struct 'S2' in 'parent.aleo' is not defined. +- Struct 'S2' in 'external_struct_fail_parent.aleo' is not defined. outputs: [] diff --git a/synthesizer/tests/tests/parser/program/external_struct.aleo b/synthesizer/tests/tests/parser/program/external_struct.aleo index 5c11d1ebaa..7d42ee5b23 100644 --- a/synthesizer/tests/tests/parser/program/external_struct.aleo +++ b/synthesizer/tests/tests/parser/program/external_struct.aleo @@ -1,13 +1,5 @@ program child.aleo; -mapping refers_to_external_struct: - key as parent.aleo/SomeStruct.public; - value as parent.aleo/AnotherStruct.public; - -struct my_struct: - member as parent.aleo/S; - member2 as parent.aleo/S2; - function child: call parent.aleo/parent into r0; output r0 as parent.aleo/S.public; diff --git a/synthesizer/tests/tests/process/execute/external_struct_fail.aleo b/synthesizer/tests/tests/process/execute/external_struct_fail.aleo index ef6b00efbb..f0d574188a 100644 --- a/synthesizer/tests/tests/process/execute/external_struct_fail.aleo +++ b/synthesizer/tests/tests/process/execute/external_struct_fail.aleo @@ -3,7 +3,7 @@ randomness: 45791624 cases: [] */ -program parent.aleo; +program external_struct_fail_parent.aleo; struct S: x as field; @@ -14,14 +14,14 @@ function make_s: ///////////////////////////////////////////////// -import parent.aleo; +import external_struct_fail_parent.aleo; -program child.aleo; +program external_struct_fail_child.aleo; struct T: x as field; function make_s: // This should fail as the struct doesn't exist. - cast 2field into r0 as parent.aleo/S2; - output r0 as parent.aleo/S2.public; + cast 2field into r0 as external_struct_fail_parent.aleo/S2; + output r0 as external_struct_fail_parent.aleo/S2.public; From 2c1ca39f554a5e1af0c1f1d5a6f22f656e5f9ac7 Mon Sep 17 00:00:00 2001 From: Michael Benfield Date: Fri, 1 Aug 2025 10:42:19 -0700 Subject: [PATCH 06/25] Gate external structs by < V10 rather than <= V9 --- synthesizer/src/vm/verify.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synthesizer/src/vm/verify.rs b/synthesizer/src/vm/verify.rs index fd01496203..ffddff6d34 100644 --- a/synthesizer/src/vm/verify.rs +++ b/synthesizer/src/vm/verify.rs @@ -455,7 +455,7 @@ fn ensure_deployment_valid_for_consensus_version( ); ensure!(deployment.program_owner().is_some(), "Invalid deployment transaction '{id}' - missing program owner"); } - if consensus_version <= ConsensusVersion::V9 { + if consensus_version < ConsensusVersion::V10 { ensure!( !deployment.program().contains_external_struct(), "Invalid deployment transaction '{id}' - external structs may only be used beginning with Consensus version 10" From ad6c26223a0d1cccd0f7c5e9b3c7ee3a8832743d Mon Sep 17 00:00:00 2001 From: Michael Benfield Date: Fri, 1 Aug 2025 11:41:15 -0700 Subject: [PATCH 07/25] Correct doc comments to say external structs --- console/network/src/consensus_heights.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/console/network/src/consensus_heights.rs b/console/network/src/consensus_heights.rs index a5ac0ebf65..25ecf5feff 100644 --- a/console/network/src/consensus_heights.rs +++ b/console/network/src/consensus_heights.rs @@ -37,7 +37,7 @@ pub enum ConsensusVersion { V8 = 8, /// V9: Support for program upgradability. V9 = 9, - /// V10: Support for external records. + /// V10: Support for external structs. V10 = 10, } From bb0b0f9550d91d6c546001ba04b53b2212d0393d Mon Sep 17 00:00:00 2001 From: Michael Benfield Date: Mon, 11 Aug 2025 09:09:52 -0700 Subject: [PATCH 08/25] Respond to review comments. - Comment style. - vm_execute_and_finalize test. - check finalize_logic for contains_external_struct. --- .../src/data_types/array_type/parse.rs | 1 + .../src/data_types/plaintext_type/mod.rs | 8 +- .../src/data_types/plaintext_type/parse.rs | 1 + .../src/data_types/register_type/mod.rs | 2 +- .../program/src/data_types/value_type/mod.rs | 2 +- synthesizer/program/src/closure/mod.rs | 2 +- synthesizer/program/src/constructor/mod.rs | 2 +- synthesizer/program/src/finalize/mod.rs | 6 + synthesizer/program/src/function/mod.rs | 3 +- synthesizer/program/src/lib.rs | 2 +- .../program/src/logic/instruction/mod.rs | 2 +- .../src/logic/instruction/operation/cast.rs | 2 +- .../execute_and_finalize/external_struct.out | 17 +++ .../execute_and_finalize/external_struct.aleo | 128 ++++++++++++++++++ 14 files changed, 166 insertions(+), 12 deletions(-) create mode 100644 synthesizer/tests/expectations/vm/execute_and_finalize/external_struct.out create mode 100644 synthesizer/tests/tests/vm/execute_and_finalize/external_struct.aleo diff --git a/console/program/src/data_types/array_type/parse.rs b/console/program/src/data_types/array_type/parse.rs index bd694646ac..ae58b5b5fe 100644 --- a/console/program/src/data_types/array_type/parse.rs +++ b/console/program/src/data_types/array_type/parse.rs @@ -22,6 +22,7 @@ impl Parser for ArrayType { fn parse(string: &str) -> ParserResult { // A helper function to parse the innermost element type. fn parse_inner_element_type(string: &str) -> ParserResult> { + // Order matters - we shouldn't try to parse Identifier before Locator. alt(( map(Locator::parse, PlaintextType::from), map(LiteralType::parse, PlaintextType::from), diff --git a/console/program/src/data_types/plaintext_type/mod.rs b/console/program/src/data_types/plaintext_type/mod.rs index ddff4423c3..a47928d68b 100644 --- a/console/program/src/data_types/plaintext_type/mod.rs +++ b/console/program/src/data_types/plaintext_type/mod.rs @@ -29,12 +29,12 @@ pub enum PlaintextType { /// A struct type contains its identifier. /// The format of the type is ``. Struct(Identifier), - /// An external struct type contains its locator. - /// The format of the type is `/`. - ExternalStruct(Locator), /// An array type contains its element type and length. /// The format of the type is `[; ]`. Array(ArrayType), + /// An external struct type contains its locator. + /// The format of the type is `/`. + ExternalStruct(Locator), } impl PlaintextType { @@ -56,7 +56,7 @@ impl PlaintextType { } } - /// Does this type refer to an external struct explicitly? + /// Returns whether this type refers to an external struct. pub fn contains_external_struct(&self) -> bool { use PlaintextType::*; diff --git a/console/program/src/data_types/plaintext_type/parse.rs b/console/program/src/data_types/plaintext_type/parse.rs index 05cae4afcc..ce2ee0743d 100644 --- a/console/program/src/data_types/plaintext_type/parse.rs +++ b/console/program/src/data_types/plaintext_type/parse.rs @@ -21,6 +21,7 @@ impl Parser for PlaintextType { fn parse(string: &str) -> ParserResult { // Parse to determine the plaintext type (order matters). alt(( + // Order matters - we shouldn't try to parse Identifier before Locator. map(ArrayType::parse, |type_| Self::Array(type_)), map(Locator::parse, |locator| Self::ExternalStruct(locator)), map(Identifier::parse, |identifier| Self::Struct(identifier)), diff --git a/console/program/src/data_types/register_type/mod.rs b/console/program/src/data_types/register_type/mod.rs index 1bd4e2ba07..af880127d2 100644 --- a/console/program/src/data_types/register_type/mod.rs +++ b/console/program/src/data_types/register_type/mod.rs @@ -56,7 +56,7 @@ impl RegisterType { } } - /// Does this type refer to an external struct explicitly? + /// Returns whether this type refers to an external struct. pub fn contains_external_struct(&self) -> bool { matches!(self, RegisterType::Plaintext(t) if t.contains_external_struct()) } diff --git a/console/program/src/data_types/value_type/mod.rs b/console/program/src/data_types/value_type/mod.rs index 24ed6c4896..be3cf895ef 100644 --- a/console/program/src/data_types/value_type/mod.rs +++ b/console/program/src/data_types/value_type/mod.rs @@ -53,7 +53,7 @@ impl ValueType { } } - /// Does this type refer to an external struct explicitly? + /// Returns whether this type references an external struct. pub fn contains_external_struct(&self) -> bool { use ValueType::*; matches!( diff --git a/synthesizer/program/src/closure/mod.rs b/synthesizer/program/src/closure/mod.rs index ecbab40263..c54f34dec9 100644 --- a/synthesizer/program/src/closure/mod.rs +++ b/synthesizer/program/src/closure/mod.rs @@ -75,7 +75,7 @@ impl ClosureCore { self.outputs.iter().map(|output| output.register_type()).cloned().collect() } - /// Does this closure refer to an external struct explicitly? + /// Returns whether the closure refers to an external struct. pub fn contains_external_struct(&self) -> bool { self.inputs.iter().any(|input| input.register_type().contains_external_struct()) || self.outputs.iter().any(|output| output.register_type().contains_external_struct()) diff --git a/synthesizer/program/src/constructor/mod.rs b/synthesizer/program/src/constructor/mod.rs index e51bec3090..4968faa6cc 100644 --- a/synthesizer/program/src/constructor/mod.rs +++ b/synthesizer/program/src/constructor/mod.rs @@ -51,7 +51,7 @@ impl ConstructorCore { &self.positions } - /// Does this constructor refer to an external struct explicitly? + /// Returns whether this constructor refers to an external struct. pub fn contains_external_struct(&self) -> bool { self.commands.iter().any(|command| { matches!( diff --git a/synthesizer/program/src/finalize/mod.rs b/synthesizer/program/src/finalize/mod.rs index faa578a759..df2cd862c5 100644 --- a/synthesizer/program/src/finalize/mod.rs +++ b/synthesizer/program/src/finalize/mod.rs @@ -79,6 +79,12 @@ impl FinalizeCore { pub const fn positions(&self) -> &HashMap, usize> { &self.positions } + + pub fn contains_external_struct(&self) -> bool { + self.commands + .iter() + .any(|command| matches!(command, Command::Instruction(inst) if inst.contains_external_struct())) + } } impl FinalizeCore { diff --git a/synthesizer/program/src/function/mod.rs b/synthesizer/program/src/function/mod.rs index 6a7d249c6e..c9270451ec 100644 --- a/synthesizer/program/src/function/mod.rs +++ b/synthesizer/program/src/function/mod.rs @@ -96,11 +96,12 @@ impl FunctionCore { self.finalize_logic.as_ref() } - /// Does this function refer to an external struct explicitly? + /// Returns whether this function refers to an external struct. pub fn contains_external_struct(&self) -> bool { self.inputs.iter().any(|input| input.value_type().contains_external_struct()) || self.outputs.iter().any(|output| output.value_type().contains_external_struct()) || self.instructions.iter().any(|instruction| instruction.contains_external_struct()) + || self.finalize_logic.iter().any(|finalize| finalize.contains_external_struct()) } } diff --git a/synthesizer/program/src/lib.rs b/synthesizer/program/src/lib.rs index 39ce9b6d37..8eba5933e9 100644 --- a/synthesizer/program/src/lib.rs +++ b/synthesizer/program/src/lib.rs @@ -953,7 +953,7 @@ impl ProgramCore { false } - /// Does this program explicitly reference an external struct, like `other_program.aleo/StructType`? + /// Returns whether this program explicitly refers to an external struct, like `other_program.aleo/StructType`? /// /// This function exists to check if programs to be deployed use external structs so they can be gated /// by consensus version. diff --git a/synthesizer/program/src/logic/instruction/mod.rs b/synthesizer/program/src/logic/instruction/mod.rs index b1829be830..30d5ddb2e9 100644 --- a/synthesizer/program/src/logic/instruction/mod.rs +++ b/synthesizer/program/src/logic/instruction/mod.rs @@ -431,7 +431,7 @@ impl Instruction { instruction!(self, |instruction| instruction.output_types(stack, input_types)) } - /// Does this instruction refer to an external struct explicitly? + /// Returns whether this instruction refers to an external struct. pub fn contains_external_struct(&self) -> bool { // Only cast instructions may contain an explicit reference to an external struct. // Calls may produce them, but they don't explicitly reference the type, and that's diff --git a/synthesizer/program/src/logic/instruction/operation/cast.rs b/synthesizer/program/src/logic/instruction/operation/cast.rs index ce72103c39..3b1e9075df 100644 --- a/synthesizer/program/src/logic/instruction/operation/cast.rs +++ b/synthesizer/program/src/logic/instruction/operation/cast.rs @@ -136,7 +136,7 @@ impl FromBytes for CastType { } impl CastType { - /// Does this type refer to an external struct explicitly? + /// Returns whether this type refers to an external struct. pub fn contains_external_struct(&self) -> bool { matches!(self, CastType::Plaintext(plaintext_type) if plaintext_type.contains_external_struct()) } diff --git a/synthesizer/tests/expectations/vm/execute_and_finalize/external_struct.out b/synthesizer/tests/expectations/vm/execute_and_finalize/external_struct.out new file mode 100644 index 0000000000..18bae492e5 --- /dev/null +++ b/synthesizer/tests/expectations/vm/execute_and_finalize/external_struct.out @@ -0,0 +1,17 @@ +errors: [] +outputs: +- verified: true + execute: + child.aleo/call_make_array: + outputs: + - '{"type":"public","id":"2027171917915276592063090567731717467068016169769609299932930674142258586381field","value":"[\n {\n x: 4field\n },\n {\n x: 4field\n }\n]"}' + speculate: the execution was accepted + add_next_block: succeeded. +additional: +- child_outputs: + parent.aleo/make_array: + outputs: + - '{"type":"public","id":"3120892635297290312904018321451954322326669610907028614644850213160964762734field","value":"[\n {\n x: 4field\n },\n {\n x: 4field\n }\n]"}' + credits.aleo/fee_public: + outputs: + - '{"type":"future","id":"3913779327809909308925001883109788018061342119177215232615598733432422627140field","value":"{\n program_id: credits.aleo,\n function_name: fee_public,\n arguments: [\n aleo1qr2ha4pfs5l28aze88yn6fhleeythklkczrule2v838uwj65n5gqxt9djx,\n 2111u64\n ]\n}"}' diff --git a/synthesizer/tests/tests/vm/execute_and_finalize/external_struct.aleo b/synthesizer/tests/tests/vm/execute_and_finalize/external_struct.aleo new file mode 100644 index 0000000000..36a3d11788 --- /dev/null +++ b/synthesizer/tests/tests/vm/execute_and_finalize/external_struct.aleo @@ -0,0 +1,128 @@ +/* +start_height: 19 +randomness: 45791624 +cases: + - program: child.aleo + function: call_make_array + inputs: [] +*/ + +// - program: child.aleo +// function: make_s +// inputs: [] +// - program: child.aleo +// function: make_t +// inputs: [] +// - program: child.aleo +// function: call_make_outer +// inputs: [] +// - program: child.aleo +// function: pass_s +// inputs: [] +// - program: child.aleo +// function: pass_t +// inputs: [] +// - program: child.aleo +// function: cast_s +// inputs: [] +// - program: child2.aleo +// function: call_make_outer2 +// inputs: [] +program parent.aleo; + +struct S: + x as field; + +struct Outer: + x as field; + s as S; + +function make_s: + cast 12field into r0 as S; + output r0 as S.public; + +function make_outer: + input r0 as field.public; + cast r0 into r1 as S; + cast r0 r1 into r2 as Outer; + output r2 as Outer.public; + +function make_array: + input r0 as field.public; + cast r0 into r1 as S; + cast r1 r1 into r2 as [S; 2u32]; + output r2 as [S; 2u32].public; + +function accept_s: + input r0 as S.public; + output r0.x as field.public; + +constructor: + assert.eq edition 0u16; + +///////////////////////////////////////////////// + +import parent.aleo; + +program child.aleo; + +struct T: + x as field; + +struct OuterT: + x as field; + s as T; + +struct Outer2: + s as parent.aleo/S; + +function make_s: + call parent.aleo/make_s into r0; + output r0 as parent.aleo/S.public; + +function make_t: + call parent.aleo/make_s into r0; + output r0 as T.public; + +function call_make_outer: + call parent.aleo/make_outer 3field into r0; + output r0 as OuterT.public; + +function call_make_array: + call parent.aleo/make_array 4field into r0; + output r0 as [T; 2u32].public; + +function pass_s: + cast 1field into r0 as parent.aleo/S; + call parent.aleo/accept_s r0 into r1; + output r1 as field.public; + +function pass_t: + cast 2field into r0 as T; + call parent.aleo/accept_s r0 into r1; + output r1 as field.public; + +function make_outer2: + cast 100field into r0 as parent.aleo/S; + cast r0 into r1 as Outer2; + output r1 as Outer2.public; + +function cast_s: + cast 101field into r0 as parent.aleo/S; + output r0 as parent.aleo/S.public; + +constructor: + assert.eq edition 0u16; + +///////////////////////////////////////////////// + +import child.aleo; + +program child2.aleo; + +function call_make_outer2: + call child.aleo/make_outer2 into r0; + output r0 as child.aleo/Outer2.public; + +constructor: + assert.eq edition 0u16; From 3d9c3a36d3aa490cac11e8b0b6675bdfb723d2ac Mon Sep 17 00:00:00 2001 From: Michael Benfield Date: Wed, 13 Aug 2025 19:14:03 -0700 Subject: [PATCH 09/25] Fix some comments in response to review --- synthesizer/program/src/logic/instruction/operation/call.rs | 3 ++- synthesizer/src/vm/verify.rs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/synthesizer/program/src/logic/instruction/operation/call.rs b/synthesizer/program/src/logic/instruction/operation/call.rs index 141d031dbc..6a5a43e227 100644 --- a/synthesizer/program/src/logic/instruction/operation/call.rs +++ b/synthesizer/program/src/logic/instruction/operation/call.rs @@ -186,6 +186,7 @@ impl Call { stack: &impl StackTrait, input_types: &[RegisterType], ) -> Result>> { + // Retrieve the program (external if necessary) and the name of the function or closure. let stack_value; let (is_external, program, name) = match &self.operator { CallOperator::Locator(locator) => { @@ -222,7 +223,7 @@ impl Call { Ok(closure .output_types() .into_iter() - // If the function is an external program, we need to qualify its structs or records with + // If the function is an external program, we need to qualify its structs with // the appropriate ProgramID. .map(|output_type| if is_external { output_type.qualify(*program.id()) } else { output_type }) .collect::>()) diff --git a/synthesizer/src/vm/verify.rs b/synthesizer/src/vm/verify.rs index ffddff6d34..866e1a5fdd 100644 --- a/synthesizer/src/vm/verify.rs +++ b/synthesizer/src/vm/verify.rs @@ -422,7 +422,7 @@ fn ensure_deployment_valid_for_consensus_version( // If the `CONSENSUS_VERSION` is greater than or equal to `V9`, then verify that: // - the program checksum is present in the deployment // - the program owner is present in the deployment - // If the `CONSENSUS_VERSION` is less than or equal to `V9`, then verify that: + // If the `CONSENSUS_VERSION` is less than `V10`, then verify that: // - the program does not use the external struct syntax `some_program.aleo/StructT` if consensus_version < ConsensusVersion::V8 { ensure!( From 9c604cccb9bc8a37d987afe563140141093f5280 Mon Sep 17 00:00:00 2001 From: Michael Benfield Date: Wed, 13 Aug 2025 19:19:52 -0700 Subject: [PATCH 10/25] next_element_type in equal_or_structs --- console/program/src/data_types/plaintext_type/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/console/program/src/data_types/plaintext_type/mod.rs b/console/program/src/data_types/plaintext_type/mod.rs index a47928d68b..7744867e48 100644 --- a/console/program/src/data_types/plaintext_type/mod.rs +++ b/console/program/src/data_types/plaintext_type/mod.rs @@ -50,7 +50,7 @@ impl PlaintextType { (Literal(lit0), Literal(lit1)) => lit0 == lit1, (Array(array0), Array(array1)) => { array0.length() == array1.length() - && array0.base_element_type().equal_or_structs(array1.base_element_type()) + && array0.next_element_type().equal_or_structs(array1.next_element_type()) } _ => false, } From c854945672554d19d5e2f39fc519f98bd4b661dd Mon Sep 17 00:00:00 2001 From: Pranav Gaddamadugu <23022326+d0cd@users.noreply.github.com> Date: Fri, 15 Aug 2025 09:58:15 -0700 Subject: [PATCH 11/25] Add some tests --- .../execute_and_finalize/external_struct.out | 95 +++++++++++++++++++ .../external_struct_in_mapping.out | 53 +++++++++++ .../mapping_with_nonexistent_structs.out | 3 + .../execute_and_finalize/external_struct.aleo | 43 +++++---- .../external_struct_in_mapping.aleo | 89 +++++++++++++++++ .../mapping_with_nonexistent_structs.aleo | 32 +++++++ 6 files changed, 294 insertions(+), 21 deletions(-) create mode 100644 synthesizer/tests/expectations/vm/execute_and_finalize/external_struct_in_mapping.out create mode 100644 synthesizer/tests/expectations/vm/execute_and_finalize/mapping_with_nonexistent_structs.out create mode 100644 synthesizer/tests/tests/vm/execute_and_finalize/external_struct_in_mapping.aleo create mode 100644 synthesizer/tests/tests/vm/execute_and_finalize/mapping_with_nonexistent_structs.aleo diff --git a/synthesizer/tests/expectations/vm/execute_and_finalize/external_struct.out b/synthesizer/tests/expectations/vm/execute_and_finalize/external_struct.out index 18bae492e5..fd59ac60dd 100644 --- a/synthesizer/tests/expectations/vm/execute_and_finalize/external_struct.out +++ b/synthesizer/tests/expectations/vm/execute_and_finalize/external_struct.out @@ -7,6 +7,55 @@ outputs: - '{"type":"public","id":"2027171917915276592063090567731717467068016169769609299932930674142258586381field","value":"[\n {\n x: 4field\n },\n {\n x: 4field\n }\n]"}' speculate: the execution was accepted add_next_block: succeeded. +- verified: true + execute: + child.aleo/make_s: + outputs: + - '{"type":"public","id":"4750427766648830651166584027609282234713015139481687149167910419529537257554field","value":"{\n x: 12field\n}"}' + speculate: the execution was accepted + add_next_block: succeeded. +- verified: true + execute: + child.aleo/make_t: + outputs: + - '{"type":"public","id":"5492181262512347406000739172214861931225393951815491668509492823879670201987field","value":"{\n x: 12field\n}"}' + speculate: the execution was accepted + add_next_block: succeeded. +- verified: true + execute: + child.aleo/call_make_outer: + outputs: + - '{"type":"public","id":"1340881132204615540349596627463794999166876253063145251151805776267123007642field","value":"{\n x: 3field,\n s: {\n x: 3field\n }\n}"}' + speculate: the execution was accepted + add_next_block: succeeded. +- verified: true + execute: + child.aleo/pass_s: + outputs: + - '{"type":"public","id":"6313190575363436423284259470587525091633417917830870506642940621133833992587field","value":"1field"}' + speculate: the execution was accepted + add_next_block: succeeded. +- verified: true + execute: + child.aleo/pass_t: + outputs: + - '{"type":"public","id":"5099193956116254326301865824650515759661166533012116811702426270105546658956field","value":"2field"}' + speculate: the execution was accepted + add_next_block: succeeded. +- verified: true + execute: + child.aleo/cast_s: + outputs: + - '{"type":"public","id":"6020168615134560098026494961763116628579457498050048492013205512361306359748field","value":"{\n x: 101field\n}"}' + speculate: the execution was accepted + add_next_block: succeeded. +- verified: true + execute: + child2.aleo/call_make_outer2: + outputs: + - '{"type":"public","id":"1372676901506734937432867521667975611427617106929751632328445666954166495024field","value":"{\n s: {\n x: 100field\n }\n}"}' + speculate: the execution was accepted + add_next_block: succeeded. additional: - child_outputs: parent.aleo/make_array: @@ -15,3 +64,49 @@ additional: credits.aleo/fee_public: outputs: - '{"type":"future","id":"3913779327809909308925001883109788018061342119177215232615598733432422627140field","value":"{\n program_id: credits.aleo,\n function_name: fee_public,\n arguments: [\n aleo1qr2ha4pfs5l28aze88yn6fhleeythklkczrule2v838uwj65n5gqxt9djx,\n 2111u64\n ]\n}"}' +- child_outputs: + parent.aleo/make_s: + outputs: + - '{"type":"public","id":"4546663708967746351596397581969693164334208260029598769783783395966625688963field","value":"{\n x: 12field\n}"}' + credits.aleo/fee_public: + outputs: + - '{"type":"future","id":"237392909470385502141532631977687665349704055588306645349871052349954931212field","value":"{\n program_id: credits.aleo,\n function_name: fee_public,\n arguments: [\n aleo1qr2ha4pfs5l28aze88yn6fhleeythklkczrule2v838uwj65n5gqxt9djx,\n 1929u64\n ]\n}"}' +- child_outputs: + parent.aleo/make_s: + outputs: + - '{"type":"public","id":"6512224489424717674311963176985160297411015952990101511441709935979126186758field","value":"{\n x: 12field\n}"}' + credits.aleo/fee_public: + outputs: + - '{"type":"future","id":"544083005215301434796082725189829847248917378066236527806824692138522327596field","value":"{\n program_id: credits.aleo,\n function_name: fee_public,\n arguments: [\n aleo1qr2ha4pfs5l28aze88yn6fhleeythklkczrule2v838uwj65n5gqxt9djx,\n 1929u64\n ]\n}"}' +- child_outputs: + parent.aleo/make_outer: + outputs: + - '{"type":"public","id":"8090139744747507649746880252865494620549538644511767765734843646307260343432field","value":"{\n x: 3field,\n s: {\n x: 3field\n }\n}"}' + credits.aleo/fee_public: + outputs: + - '{"type":"future","id":"3342862405394538967824524517969661113584031243026480565150360127410813125020field","value":"{\n program_id: credits.aleo,\n function_name: fee_public,\n arguments: [\n aleo1qr2ha4pfs5l28aze88yn6fhleeythklkczrule2v838uwj65n5gqxt9djx,\n 2101u64\n ]\n}"}' +- child_outputs: + parent.aleo/accept_s: + outputs: + - '{"type":"public","id":"1551881531234777064365658590941283107031662046472124720256507595422003650761field","value":"1field"}' + credits.aleo/fee_public: + outputs: + - '{"type":"future","id":"3082020932608068274000266892322264646991789296316604813666445202761944988578field","value":"{\n program_id: credits.aleo,\n function_name: fee_public,\n arguments: [\n aleo1qr2ha4pfs5l28aze88yn6fhleeythklkczrule2v838uwj65n5gqxt9djx,\n 1994u64\n ]\n}"}' +- child_outputs: + parent.aleo/accept_s: + outputs: + - '{"type":"public","id":"7978721134161787923837197569741916914311001260880141204643210310498476128580field","value":"2field"}' + credits.aleo/fee_public: + outputs: + - '{"type":"future","id":"6763280032768530776952606535677828068072863052633876108602399418899368682104field","value":"{\n program_id: credits.aleo,\n function_name: fee_public,\n arguments: [\n aleo1qr2ha4pfs5l28aze88yn6fhleeythklkczrule2v838uwj65n5gqxt9djx,\n 1994u64\n ]\n}"}' +- child_outputs: + credits.aleo/fee_public: + outputs: + - '{"type":"future","id":"5470159535708433650737959437903009777511896181924083748157652800492118338171field","value":"{\n program_id: credits.aleo,\n function_name: fee_public,\n arguments: [\n aleo1qr2ha4pfs5l28aze88yn6fhleeythklkczrule2v838uwj65n5gqxt9djx,\n 1216u64\n ]\n}"}' +- child_outputs: + child.aleo/make_outer2: + outputs: + - '{"type":"public","id":"8267366305051630788564397831422770212002552512823527043472102251973318674586field","value":"{\n s: {\n x: 100field\n }\n}"}' + credits.aleo/fee_public: + outputs: + - '{"type":"future","id":"1210104830958693804573149645502547926351367863793028498795065760300913872351field","value":"{\n program_id: credits.aleo,\n function_name: fee_public,\n arguments: [\n aleo1qr2ha4pfs5l28aze88yn6fhleeythklkczrule2v838uwj65n5gqxt9djx,\n 1956u64\n ]\n}"}' diff --git a/synthesizer/tests/expectations/vm/execute_and_finalize/external_struct_in_mapping.out b/synthesizer/tests/expectations/vm/execute_and_finalize/external_struct_in_mapping.out new file mode 100644 index 0000000000..4ed5323178 --- /dev/null +++ b/synthesizer/tests/expectations/vm/execute_and_finalize/external_struct_in_mapping.out @@ -0,0 +1,53 @@ +errors: [] +outputs: +- verified: true + execute: + parent.aleo/store_t: + outputs: + - '{"type":"future","id":"5156950801145169537669009747157321932147267606917954059095229590621596317558field","value":"{\n program_id: parent.aleo,\n function_name: store_t,\n arguments: [\n {\n x: 12field\n}\n ]\n}"}' + speculate: the execution was accepted + add_next_block: succeeded. +- verified: true + execute: + parent.aleo/store_s: + outputs: + - '{"type":"future","id":"3536429601035031221536064521737537064728529805456995459759090711197590968904field","value":"{\n program_id: parent.aleo,\n function_name: store_s,\n arguments: [\n {\n x: 12field\n}\n ]\n}"}' + speculate: the execution was accepted + add_next_block: succeeded. +- verified: true + execute: + parent.aleo/store_s_as_t: + outputs: + - '{"type":"future","id":"4059613284155828593649148683363481039153998827084982120202239631116761077539field","value":"{\n program_id: parent.aleo,\n function_name: store_s_as_t,\n arguments: [\n {\n x: 12field\n}\n ]\n}"}' + speculate: the execution was accepted + add_next_block: succeeded. +- verified: true + execute: + parent.aleo/compare: + outputs: + - '{"type":"future","id":"599984621506371769970208532698818450215019892731507626787420093675049916523field","value":"{\n program_id: parent.aleo,\n function_name: compare,\n arguments: []\n}"}' + speculate: the execution was accepted + add_next_block: succeeded. +additional: +- child_outputs: + credits.aleo/fee_public: + outputs: + - '{"type":"future","id":"3077534165282048476651182034927959704102009637768584675657634181464198851648field","value":"{\n program_id: credits.aleo,\n function_name: fee_public,\n arguments: [\n aleo1qr2ha4pfs5l28aze88yn6fhleeythklkczrule2v838uwj65n5gqxt9djx,\n 17842u64\n ]\n}"}' +- child_outputs: + child.aleo/make_s: + outputs: + - '{"type":"public","id":"8357804394006689049643614990561262851191212004864212878785279736486409935348field","value":"{\n x: 12field\n}"}' + credits.aleo/fee_public: + outputs: + - '{"type":"future","id":"6423060729030407108337685286277924679730527590289056343090240277713705336980field","value":"{\n program_id: credits.aleo,\n function_name: fee_public,\n arguments: [\n aleo1qr2ha4pfs5l28aze88yn6fhleeythklkczrule2v838uwj65n5gqxt9djx,\n 18554u64\n ]\n}"}' +- child_outputs: + child.aleo/make_s: + outputs: + - '{"type":"public","id":"7574377799660344748771224282773834068174701455625760560698920840088755673712field","value":"{\n x: 12field\n}"}' + credits.aleo/fee_public: + outputs: + - '{"type":"future","id":"2645278696162344307543607738900689987512504974551459357913512732894441614319field","value":"{\n program_id: credits.aleo,\n function_name: fee_public,\n arguments: [\n aleo1qr2ha4pfs5l28aze88yn6fhleeythklkczrule2v838uwj65n5gqxt9djx,\n 20084u64\n ]\n}"}' +- child_outputs: + credits.aleo/fee_public: + outputs: + - '{"type":"future","id":"7038695923461775559991406419975410061163444193339031735011873989822584323603field","value":"{\n program_id: credits.aleo,\n function_name: fee_public,\n arguments: [\n aleo1qr2ha4pfs5l28aze88yn6fhleeythklkczrule2v838uwj65n5gqxt9djx,\n 5338u64\n ]\n}"}' diff --git a/synthesizer/tests/expectations/vm/execute_and_finalize/mapping_with_nonexistent_structs.out b/synthesizer/tests/expectations/vm/execute_and_finalize/mapping_with_nonexistent_structs.out new file mode 100644 index 0000000000..b54ad3cd02 --- /dev/null +++ b/synthesizer/tests/expectations/vm/execute_and_finalize/mapping_with_nonexistent_structs.out @@ -0,0 +1,3 @@ +errors: [] +outputs: [] +additional: [] diff --git a/synthesizer/tests/tests/vm/execute_and_finalize/external_struct.aleo b/synthesizer/tests/tests/vm/execute_and_finalize/external_struct.aleo index 36a3d11788..5130eaa7d7 100644 --- a/synthesizer/tests/tests/vm/execute_and_finalize/external_struct.aleo +++ b/synthesizer/tests/tests/vm/execute_and_finalize/external_struct.aleo @@ -5,29 +5,30 @@ cases: - program: child.aleo function: call_make_array inputs: [] + - program: child.aleo + function: make_s + inputs: [] + - program: child.aleo + function: make_t + inputs: [] + - program: child.aleo + function: call_make_outer + inputs: [] + - program: child.aleo + function: pass_s + inputs: [] + - program: child.aleo + function: pass_t + inputs: [] + - program: child.aleo + function: cast_s + inputs: [] + - program: child2.aleo + function: call_make_outer2 + inputs: [] */ -// - program: child.aleo -// function: make_s -// inputs: [] -// - program: child.aleo -// function: make_t -// inputs: [] -// - program: child.aleo -// function: call_make_outer -// inputs: [] -// - program: child.aleo -// function: pass_s -// inputs: [] -// - program: child.aleo -// function: pass_t -// inputs: [] -// - program: child.aleo -// function: cast_s -// inputs: [] -// - program: child2.aleo -// function: call_make_outer2 -// inputs: [] + program parent.aleo; struct S: diff --git a/synthesizer/tests/tests/vm/execute_and_finalize/external_struct_in_mapping.aleo b/synthesizer/tests/tests/vm/execute_and_finalize/external_struct_in_mapping.aleo new file mode 100644 index 0000000000..3e8282ec8e --- /dev/null +++ b/synthesizer/tests/tests/vm/execute_and_finalize/external_struct_in_mapping.aleo @@ -0,0 +1,89 @@ +/* +start_height: 19 +randomness: 45791624 +cases: + - program: parent.aleo + function: store_t + inputs: [] + - program: parent.aleo + function: store_s + inputs: [] + - program: parent.aleo + function: store_s_as_t + inputs: [] + - program: parent.aleo + function: compare + inputs: [] +*/ + +program child.aleo; + +struct S: + x as field; + +function make_s: + cast 12field into r0 as S; + output r0 as S.public; + +constructor: + assert.eq edition 0u16; + +///////////////////////////////////////////////// + +import child.aleo; + +program parent.aleo; + +struct T: + x as field; + +mapping foo: + key as field.public; + value as T.public; + +mapping bar: + key as field.public; + value as child.aleo/S.public; + +closure make_t: + input r0 as field; + cast r0 into r1 as T; + output r1 as T; + +function store_t: + call make_t 12field into r0; + async store_t r0 into r1; + output r1 as parent.aleo/store_t.future; +finalize store_t: + input r0 as T.public; + set r0 into foo[0field]; + +function store_s: + call child.aleo/make_s into r0; + async store_s r0 into r1; + output r1 as parent.aleo/store_s.future; +finalize store_s: + input r0 as child.aleo/S.public; + set r0 into bar[1field]; + +function store_s_as_t: + call child.aleo/make_s into r0; + async store_s_as_t r0 into r1; + output r1 as parent.aleo/store_s_as_t.future; +finalize store_s_as_t: + input r0 as child.aleo/S.public; + cast r0.x into r1 as T; + set r1 into foo[2field]; + +function compare: + async compare into r0; + output r0 as parent.aleo/compare.future; +finalize compare: + get foo[0field] into r0; + get bar[1field] into r1; + assert.eq r0.x r1.x; + + +constructor: + assert.eq edition 0u16; + diff --git a/synthesizer/tests/tests/vm/execute_and_finalize/mapping_with_nonexistent_structs.aleo b/synthesizer/tests/tests/vm/execute_and_finalize/mapping_with_nonexistent_structs.aleo new file mode 100644 index 0000000000..d1a21672cd --- /dev/null +++ b/synthesizer/tests/tests/vm/execute_and_finalize/mapping_with_nonexistent_structs.aleo @@ -0,0 +1,32 @@ +/* +start_height: 19 +randomness: 45791624 +cases: [] +*/ + +program child.aleo; + +mapping foo: + key as field.public; + value as S.public; + +function dummy: + +constructor: + assert.eq edition 0u16; + +///////////////////////////////////////////////// + +import child.aleo; + +program parent.aleo; + +mapping foo: + key as field.public; + value as child.aleo/S.public; + +function dummy: + +constructor: + assert.eq edition 0u16; + From d4f084255715a19b250910c0c350c85dc0db0174 Mon Sep 17 00:00:00 2001 From: Pranav Gaddamadugu <23022326+d0cd@users.noreply.github.com> Date: Fri, 15 Aug 2025 10:53:45 -0700 Subject: [PATCH 12/25] Add regression --- .../execute_and_finalize/external_struct.out | 115 +-------------- .../execute_and_finalize/external_struct.aleo | 135 ++++-------------- 2 files changed, 30 insertions(+), 220 deletions(-) diff --git a/synthesizer/tests/expectations/vm/execute_and_finalize/external_struct.out b/synthesizer/tests/expectations/vm/execute_and_finalize/external_struct.out index fd59ac60dd..12d28aab06 100644 --- a/synthesizer/tests/expectations/vm/execute_and_finalize/external_struct.out +++ b/synthesizer/tests/expectations/vm/execute_and_finalize/external_struct.out @@ -1,112 +1,3 @@ -errors: [] -outputs: -- verified: true - execute: - child.aleo/call_make_array: - outputs: - - '{"type":"public","id":"2027171917915276592063090567731717467068016169769609299932930674142258586381field","value":"[\n {\n x: 4field\n },\n {\n x: 4field\n }\n]"}' - speculate: the execution was accepted - add_next_block: succeeded. -- verified: true - execute: - child.aleo/make_s: - outputs: - - '{"type":"public","id":"4750427766648830651166584027609282234713015139481687149167910419529537257554field","value":"{\n x: 12field\n}"}' - speculate: the execution was accepted - add_next_block: succeeded. -- verified: true - execute: - child.aleo/make_t: - outputs: - - '{"type":"public","id":"5492181262512347406000739172214861931225393951815491668509492823879670201987field","value":"{\n x: 12field\n}"}' - speculate: the execution was accepted - add_next_block: succeeded. -- verified: true - execute: - child.aleo/call_make_outer: - outputs: - - '{"type":"public","id":"1340881132204615540349596627463794999166876253063145251151805776267123007642field","value":"{\n x: 3field,\n s: {\n x: 3field\n }\n}"}' - speculate: the execution was accepted - add_next_block: succeeded. -- verified: true - execute: - child.aleo/pass_s: - outputs: - - '{"type":"public","id":"6313190575363436423284259470587525091633417917830870506642940621133833992587field","value":"1field"}' - speculate: the execution was accepted - add_next_block: succeeded. -- verified: true - execute: - child.aleo/pass_t: - outputs: - - '{"type":"public","id":"5099193956116254326301865824650515759661166533012116811702426270105546658956field","value":"2field"}' - speculate: the execution was accepted - add_next_block: succeeded. -- verified: true - execute: - child.aleo/cast_s: - outputs: - - '{"type":"public","id":"6020168615134560098026494961763116628579457498050048492013205512361306359748field","value":"{\n x: 101field\n}"}' - speculate: the execution was accepted - add_next_block: succeeded. -- verified: true - execute: - child2.aleo/call_make_outer2: - outputs: - - '{"type":"public","id":"1372676901506734937432867521667975611427617106929751632328445666954166495024field","value":"{\n s: {\n x: 100field\n }\n}"}' - speculate: the execution was accepted - add_next_block: succeeded. -additional: -- child_outputs: - parent.aleo/make_array: - outputs: - - '{"type":"public","id":"3120892635297290312904018321451954322326669610907028614644850213160964762734field","value":"[\n {\n x: 4field\n },\n {\n x: 4field\n }\n]"}' - credits.aleo/fee_public: - outputs: - - '{"type":"future","id":"3913779327809909308925001883109788018061342119177215232615598733432422627140field","value":"{\n program_id: credits.aleo,\n function_name: fee_public,\n arguments: [\n aleo1qr2ha4pfs5l28aze88yn6fhleeythklkczrule2v838uwj65n5gqxt9djx,\n 2111u64\n ]\n}"}' -- child_outputs: - parent.aleo/make_s: - outputs: - - '{"type":"public","id":"4546663708967746351596397581969693164334208260029598769783783395966625688963field","value":"{\n x: 12field\n}"}' - credits.aleo/fee_public: - outputs: - - '{"type":"future","id":"237392909470385502141532631977687665349704055588306645349871052349954931212field","value":"{\n program_id: credits.aleo,\n function_name: fee_public,\n arguments: [\n aleo1qr2ha4pfs5l28aze88yn6fhleeythklkczrule2v838uwj65n5gqxt9djx,\n 1929u64\n ]\n}"}' -- child_outputs: - parent.aleo/make_s: - outputs: - - '{"type":"public","id":"6512224489424717674311963176985160297411015952990101511441709935979126186758field","value":"{\n x: 12field\n}"}' - credits.aleo/fee_public: - outputs: - - '{"type":"future","id":"544083005215301434796082725189829847248917378066236527806824692138522327596field","value":"{\n program_id: credits.aleo,\n function_name: fee_public,\n arguments: [\n aleo1qr2ha4pfs5l28aze88yn6fhleeythklkczrule2v838uwj65n5gqxt9djx,\n 1929u64\n ]\n}"}' -- child_outputs: - parent.aleo/make_outer: - outputs: - - '{"type":"public","id":"8090139744747507649746880252865494620549538644511767765734843646307260343432field","value":"{\n x: 3field,\n s: {\n x: 3field\n }\n}"}' - credits.aleo/fee_public: - outputs: - - '{"type":"future","id":"3342862405394538967824524517969661113584031243026480565150360127410813125020field","value":"{\n program_id: credits.aleo,\n function_name: fee_public,\n arguments: [\n aleo1qr2ha4pfs5l28aze88yn6fhleeythklkczrule2v838uwj65n5gqxt9djx,\n 2101u64\n ]\n}"}' -- child_outputs: - parent.aleo/accept_s: - outputs: - - '{"type":"public","id":"1551881531234777064365658590941283107031662046472124720256507595422003650761field","value":"1field"}' - credits.aleo/fee_public: - outputs: - - '{"type":"future","id":"3082020932608068274000266892322264646991789296316604813666445202761944988578field","value":"{\n program_id: credits.aleo,\n function_name: fee_public,\n arguments: [\n aleo1qr2ha4pfs5l28aze88yn6fhleeythklkczrule2v838uwj65n5gqxt9djx,\n 1994u64\n ]\n}"}' -- child_outputs: - parent.aleo/accept_s: - outputs: - - '{"type":"public","id":"7978721134161787923837197569741916914311001260880141204643210310498476128580field","value":"2field"}' - credits.aleo/fee_public: - outputs: - - '{"type":"future","id":"6763280032768530776952606535677828068072863052633876108602399418899368682104field","value":"{\n program_id: credits.aleo,\n function_name: fee_public,\n arguments: [\n aleo1qr2ha4pfs5l28aze88yn6fhleeythklkczrule2v838uwj65n5gqxt9djx,\n 1994u64\n ]\n}"}' -- child_outputs: - credits.aleo/fee_public: - outputs: - - '{"type":"future","id":"5470159535708433650737959437903009777511896181924083748157652800492118338171field","value":"{\n program_id: credits.aleo,\n function_name: fee_public,\n arguments: [\n aleo1qr2ha4pfs5l28aze88yn6fhleeythklkczrule2v838uwj65n5gqxt9djx,\n 1216u64\n ]\n}"}' -- child_outputs: - child.aleo/make_outer2: - outputs: - - '{"type":"public","id":"8267366305051630788564397831422770212002552512823527043472102251973318674586field","value":"{\n s: {\n x: 100field\n }\n}"}' - credits.aleo/fee_public: - outputs: - - '{"type":"future","id":"1210104830958693804573149645502547926351367863793028498795065760300913872351field","value":"{\n program_id: credits.aleo,\n function_name: fee_public,\n arguments: [\n aleo1qr2ha4pfs5l28aze88yn6fhleeythklkczrule2v838uwj65n5gqxt9djx,\n 1956u64\n ]\n}"}' +errors: +- 'Failed to run `VM::deploy for program external_struct_test.aleo: Instruction ''assert.eq'' expects inputs of the same type. Found inputs of type ''external_struct.aleo/S'' and ''S''' +outputs: [] diff --git a/synthesizer/tests/tests/vm/execute_and_finalize/external_struct.aleo b/synthesizer/tests/tests/vm/execute_and_finalize/external_struct.aleo index 5130eaa7d7..3bd6b79ad9 100644 --- a/synthesizer/tests/tests/vm/execute_and_finalize/external_struct.aleo +++ b/synthesizer/tests/tests/vm/execute_and_finalize/external_struct.aleo @@ -2,128 +2,47 @@ start_height: 19 randomness: 45791624 cases: - - program: child.aleo - function: call_make_array - inputs: [] - - program: child.aleo - function: make_s - inputs: [] - - program: child.aleo - function: make_t - inputs: [] - - program: child.aleo - function: call_make_outer - inputs: [] - - program: child.aleo - function: pass_s - inputs: [] - - program: child.aleo - function: pass_t - inputs: [] - - program: child.aleo - function: cast_s - inputs: [] - - program: child2.aleo - function: call_make_outer2 - inputs: [] + - program: external_struct_test.aleo + function: main + inputs: ["0u8"] */ - -program parent.aleo; +program external_struct.aleo; struct S: - x as field; - -struct Outer: - x as field; - s as S; + data as u8; -function make_s: - cast 12field into r0 as S; - output r0 as S.public; - -function make_outer: - input r0 as field.public; +function make_struct: + input r0 as u8.private; cast r0 into r1 as S; - cast r0 r1 into r2 as Outer; - output r2 as Outer.public; - -function make_array: - input r0 as field.public; - cast r0 into r1 as S; - cast r1 r1 into r2 as [S; 2u32]; - output r2 as [S; 2u32].public; - -function accept_s: - input r0 as S.public; - output r0.x as field.public; + output r1 as S.private; constructor: assert.eq edition 0u16; -///////////////////////////////////////////////// - -import parent.aleo; - -program child.aleo; - -struct T: - x as field; - -struct OuterT: - x as field; - s as T; - -struct Outer2: - s as parent.aleo/S; - -function make_s: - call parent.aleo/make_s into r0; - output r0 as parent.aleo/S.public; - -function make_t: - call parent.aleo/make_s into r0; - output r0 as T.public; - -function call_make_outer: - call parent.aleo/make_outer 3field into r0; - output r0 as OuterT.public; - -function call_make_array: - call parent.aleo/make_array 4field into r0; - output r0 as [T; 2u32].public; - -function pass_s: - cast 1field into r0 as parent.aleo/S; - call parent.aleo/accept_s r0 into r1; - output r1 as field.public; - -function pass_t: - cast 2field into r0 as T; - call parent.aleo/accept_s r0 into r1; - output r1 as field.public; - -function make_outer2: - cast 100field into r0 as parent.aleo/S; - cast r0 into r1 as Outer2; - output r1 as Outer2.public; - -function cast_s: - cast 101field into r0 as parent.aleo/S; - output r0 as parent.aleo/S.public; - -constructor: - assert.eq edition 0u16; ///////////////////////////////////////////////// -import child.aleo; - -program child2.aleo; +import external_struct.aleo; +program external_struct_test.aleo; -function call_make_outer2: - call child.aleo/make_outer2 into r0; - output r0 as child.aleo/Outer2.public; +struct S: + data as u8; + +closure check_structs: + input r0 as S; + input r1 as S; + assert.eq r0 r1; + +function main: + input r0 as u8.private; + call external_struct.aleo/make_struct r0 into r1; + call external_struct.aleo/make_struct r0 into r2; + cast r0 into r3 as S; + assert.eq r1 r2; + assert.eq r1 r3; + call check_structs r1 r2; + call check_structs r1 r3; constructor: assert.eq edition 0u16; From 3b5fb89902883b81424be199385fe217a224cde5 Mon Sep 17 00:00:00 2001 From: Michael Benfield Date: Mon, 18 Aug 2025 11:41:27 -0700 Subject: [PATCH 13/25] Check types for structural equivalence. --- .../src/data_types/plaintext_type/mod.rs | 18 -------- .../src/data_types/register_type/mod.rs | 12 ----- .../src/stack/finalize_types/initialize.rs | 10 ++-- .../process/src/stack/finalize_types/mod.rs | 1 + .../src/stack/register_types/matches.rs | 8 ++-- .../src/logic/instruction/operation/assert.rs | 6 +-- .../src/logic/instruction/operation/async_.rs | 4 +- .../src/logic/instruction/operation/cast.rs | 19 ++++++-- .../src/logic/instruction/operation/is.rs | 6 +-- .../process/execute/external_struct2.out | 3 ++ .../execute_and_finalize/external_struct.out | 19 ++++++-- .../external_struct_in_mapping.out | 26 ++++++++--- .../process/execute/external_struct.aleo | 28 +++++------ .../process/execute/external_struct2.aleo | 46 +++++++++++++++++++ .../external_struct_in_mapping.aleo | 15 ++++++ 15 files changed, 148 insertions(+), 73 deletions(-) create mode 100644 synthesizer/tests/expectations/process/execute/external_struct2.out create mode 100644 synthesizer/tests/tests/process/execute/external_struct2.aleo diff --git a/console/program/src/data_types/plaintext_type/mod.rs b/console/program/src/data_types/plaintext_type/mod.rs index 7744867e48..48c1c7d25f 100644 --- a/console/program/src/data_types/plaintext_type/mod.rs +++ b/console/program/src/data_types/plaintext_type/mod.rs @@ -38,24 +38,6 @@ pub enum PlaintextType { } impl PlaintextType { - /// Are the two types equivalent for the purposes of static checking? - /// - /// Since struct types are compared by structure, we can't determine equality - /// by only looking at their names. - pub fn equal_or_structs(&self, rhs: &Self) -> bool { - use PlaintextType::*; - - match (self, rhs) { - (ExternalStruct(..) | Struct(..), ExternalStruct(..) | Struct(..)) => true, - (Literal(lit0), Literal(lit1)) => lit0 == lit1, - (Array(array0), Array(array1)) => { - array0.length() == array1.length() - && array0.next_element_type().equal_or_structs(array1.next_element_type()) - } - _ => false, - } - } - /// Returns whether this type refers to an external struct. pub fn contains_external_struct(&self) -> bool { use PlaintextType::*; diff --git a/console/program/src/data_types/register_type/mod.rs b/console/program/src/data_types/register_type/mod.rs index af880127d2..67ea1712e0 100644 --- a/console/program/src/data_types/register_type/mod.rs +++ b/console/program/src/data_types/register_type/mod.rs @@ -35,18 +35,6 @@ pub enum RegisterType { } impl RegisterType { - /// Are the two types equivalent for the purposes of static checking? - /// - /// Since struct types are compared by structure, we can't determine equality - /// by only looking at their names. - pub fn equal_or_structs(&self, rhs: &Self) -> bool { - use RegisterType::*; - match (self, rhs) { - (Plaintext(a), Plaintext(b)) => a.equal_or_structs(b), - _ => self == rhs, - } - } - // Make unqualified structs or records into external ones with the given `id`. pub fn qualify(self, id: ProgramID) -> Self { match self { diff --git a/synthesizer/process/src/stack/finalize_types/initialize.rs b/synthesizer/process/src/stack/finalize_types/initialize.rs index d0a1097ad0..d0d0b597d3 100644 --- a/synthesizer/process/src/stack/finalize_types/initialize.rs +++ b/synthesizer/process/src/stack/finalize_types/initialize.rs @@ -324,7 +324,7 @@ impl FinalizeTypes { FinalizeType::Future(..) => bail!("A future cannot be used as a key in a `contains` command"), }; // Check that the key type in the mapping matches the key type in the instruction. - if *mapping_key_type != key_type { + if !types_structurally_equivalent(stack, mapping_key_type, stack, &key_type)? { bail!( "Key type in `contains` '{key_type}' does not match the key type in the mapping '{mapping_key_type}'." ) @@ -389,7 +389,7 @@ impl FinalizeTypes { FinalizeType::Future(..) => bail!("A future cannot be used as a key in a `get` command"), }; // Check that the key type in the mapping matches the key type in the instruction. - if *mapping_key_type != key_type { + if !types_structurally_equivalent(stack, mapping_key_type, stack, &key_type)? { bail!("Key type in `get` '{key_type}' does not match the key type in the mapping '{mapping_key_type}'.") } // Get the destination register. @@ -452,7 +452,7 @@ impl FinalizeTypes { FinalizeType::Future(..) => bail!("A future cannot be used as a key in a `get.or_use` command"), }; // Check that the key type in the mapping matches the key type. - if *mapping_key_type != key_type { + if !types_structurally_equivalent(stack, mapping_key_type, stack, &key_type)? { bail!( "Key type in `get.or_use` '{key_type}' does not match the key type in the mapping '{mapping_key_type}'." ) @@ -527,7 +527,7 @@ impl FinalizeTypes { FinalizeType::Future(..) => bail!("A future cannot be used as a key in a `set` command"), }; // Check that the key type in the mapping matches the key type. - if *mapping_key_type != key_type { + if !types_structurally_equivalent(stack, mapping_key_type, stack, &key_type)? { bail!("Key type in `set` '{key_type}' does not match the key type in the mapping '{mapping_key_type}'.") } // Retrieve the type of the value. @@ -566,7 +566,7 @@ impl FinalizeTypes { FinalizeType::Future(..) => bail!("A future cannot be used as a key in a `remove` command"), }; // Check that the key type in the mapping matches the key type. - if *mapping_key_type != key_type { + if !types_structurally_equivalent(stack, mapping_key_type, stack, &key_type)? { bail!("Key type in `remove` '{key_type}' does not match the key type in the mapping '{mapping_key_type}'.") } Ok(()) diff --git a/synthesizer/process/src/stack/finalize_types/mod.rs b/synthesizer/process/src/stack/finalize_types/mod.rs index b228d90aaf..ae8ee9a847 100644 --- a/synthesizer/process/src/stack/finalize_types/mod.rs +++ b/synthesizer/process/src/stack/finalize_types/mod.rs @@ -53,6 +53,7 @@ use snarkvm_synthesizer_program::{ Remove, Set, StackTrait, + types_structurally_equivalent, }; use indexmap::IndexMap; diff --git a/synthesizer/process/src/stack/register_types/matches.rs b/synthesizer/process/src/stack/register_types/matches.rs index 8a9078e01a..8c41aefac5 100644 --- a/synthesizer/process/src/stack/register_types/matches.rs +++ b/synthesizer/process/src/stack/register_types/matches.rs @@ -80,7 +80,7 @@ impl RegisterTypes { }; // Ensure the operand type matches the member type. ensure!( - &operand_type == member_type, + types_structurally_equivalent(stack, &operand_type, stack, member_type)?, "Struct member '{struct_name}.{member_name}' expects {member_type}, but found '{operand_type}' in the operand '{operand}'.", ) } @@ -174,7 +174,7 @@ impl RegisterTypes { }; // Ensure the operand type matches the element type. ensure!( - &operand_type == array_type.next_element_type(), + types_structurally_equivalent(stack, &operand_type, stack, array_type.next_element_type())?, "Array element expects {}, but found '{operand_type}' in the operand '{operand}'.", array_type.next_element_type() ) @@ -297,7 +297,7 @@ impl RegisterTypes { // Ensure the register type matches the entry type. RegisterType::Plaintext(type_) => { ensure!( - &type_ == plaintext_type, + types_structurally_equivalent(stack, &type_, stack, plaintext_type)?, "Record entry '{record_name}.{entry_name}' expects a '{plaintext_type}', but found '{type_}' in the operand '{operand}'.", ) } @@ -314,7 +314,7 @@ impl RegisterTypes { }; // Ensure the operand type matches the entry type. ensure!( - &operand_type == plaintext_type, + types_structurally_equivalent(stack, &operand_type, stack, plaintext_type)?, "Record entry '{record_name}.{entry_name}' expects a '{plaintext_type}', but found '{operand_type}' in the operand '{operand}'.", ) } diff --git a/synthesizer/program/src/logic/instruction/operation/assert.rs b/synthesizer/program/src/logic/instruction/operation/assert.rs index 38a35cc2eb..615f463068 100644 --- a/synthesizer/program/src/logic/instruction/operation/assert.rs +++ b/synthesizer/program/src/logic/instruction/operation/assert.rs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::{Opcode, Operand, RegistersCircuit, RegistersTrait, StackTrait}; +use crate::{Opcode, Operand, RegistersCircuit, RegistersTrait, StackTrait, register_types_structurally_equivalent}; use console::{ network::prelude::*, program::{Register, RegisterType}, @@ -134,7 +134,7 @@ impl AssertInstruction { /// Returns the output type from the given program and input types. pub fn output_types( &self, - _stack: &impl StackTrait, + stack: &impl StackTrait, input_types: &[RegisterType], ) -> Result>> { // Ensure the number of input types is correct. @@ -142,7 +142,7 @@ impl AssertInstruction { bail!("Instruction '{}' expects 2 inputs, found {} inputs", Self::opcode(), input_types.len()) } // Ensure the operands are of the same type. - if input_types[0] != input_types[1] { + if !register_types_structurally_equivalent(stack, &input_types[0], stack, &input_types[1])? { bail!( "Instruction '{}' expects inputs of the same type. Found inputs of type '{}' and '{}'", Self::opcode(), diff --git a/synthesizer/program/src/logic/instruction/operation/async_.rs b/synthesizer/program/src/logic/instruction/operation/async_.rs index 8c92c8704c..1ce0b614f3 100644 --- a/synthesizer/program/src/logic/instruction/operation/async_.rs +++ b/synthesizer/program/src/logic/instruction/operation/async_.rs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::{Opcode, Operand, RegistersCircuit, RegistersTrait, Result, StackTrait}; +use crate::{Opcode, Operand, RegistersCircuit, RegistersTrait, Result, StackTrait, types_structurally_equivalent}; use circuit::{Inject, Mode}; use console::{ @@ -159,7 +159,7 @@ impl Async { match (input_type, finalize_type) { (RegisterType::Plaintext(input_type), FinalizeType::Plaintext(finalize_type)) => { ensure!( - input_type == &finalize_type, + types_structurally_equivalent(stack, input_type, stack, &finalize_type)?, "'{}/{}' finalize expects a '{}' argument, found a '{}' argument", stack.program_id(), self.function_name(), diff --git a/synthesizer/program/src/logic/instruction/operation/cast.rs b/synthesizer/program/src/logic/instruction/operation/cast.rs index 3b1e9075df..3c4397efbf 100644 --- a/synthesizer/program/src/logic/instruction/operation/cast.rs +++ b/synthesizer/program/src/logic/instruction/operation/cast.rs @@ -13,7 +13,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::{Opcode, Operand, RegistersCircuit, RegistersSigner, RegistersTrait, StackTrait}; +use crate::{ + Opcode, + Operand, + RegistersCircuit, + RegistersSigner, + RegistersTrait, + StackTrait, + types_structurally_equivalent, +}; use console::{ network::prelude::*, program::{ @@ -660,7 +668,7 @@ impl CastOperation { // Ensure the plaintext type matches the member type. RegisterType::Plaintext(plaintext_type) => { ensure!( - member_type.equal_or_structs(plaintext_type), + types_structurally_equivalent(stack, member_type, stack, plaintext_type,)?, "Struct '{struct_name}' member type mismatch: expected '{member_type}', found '{plaintext_type}'" ) } @@ -731,7 +739,12 @@ impl CastOperation { // Ensure the plaintext type matches the member type. RegisterType::Plaintext(plaintext_type) => { ensure!( - plaintext_type.equal_or_structs(array_type.next_element_type()), + types_structurally_equivalent( + stack, + plaintext_type, + stack, + array_type.next_element_type() + )?, "Array element type mismatch: expected '{}', found '{plaintext_type}'", array_type.next_element_type() ) diff --git a/synthesizer/program/src/logic/instruction/operation/is.rs b/synthesizer/program/src/logic/instruction/operation/is.rs index cd573fe6a9..c543cbaed8 100644 --- a/synthesizer/program/src/logic/instruction/operation/is.rs +++ b/synthesizer/program/src/logic/instruction/operation/is.rs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::{Opcode, Operand, RegistersCircuit, RegistersTrait, StackTrait}; +use crate::{Opcode, Operand, RegistersCircuit, RegistersTrait, StackTrait, register_types_structurally_equivalent}; use console::{ network::prelude::*, program::{Literal, LiteralType, Plaintext, PlaintextType, Register, RegisterType, Value}, @@ -131,7 +131,7 @@ impl IsInstruction { /// Returns the output type from the given program and input types. pub fn output_types( &self, - _stack: &impl StackTrait, + stack: &impl StackTrait, input_types: &[RegisterType], ) -> Result>> { // Ensure the number of input types is correct. @@ -139,7 +139,7 @@ impl IsInstruction { bail!("Instruction '{}' expects 2 inputs, found {} inputs", Self::opcode(), input_types.len()) } // Ensure the operands are of the same type. - if input_types[0] != input_types[1] { + if !register_types_structurally_equivalent(stack, &input_types[0], stack, &input_types[1])? { bail!( "Instruction '{}' expects inputs of the same type. Found inputs of type '{}' and '{}'", Self::opcode(), diff --git a/synthesizer/tests/expectations/process/execute/external_struct2.out b/synthesizer/tests/expectations/process/execute/external_struct2.out new file mode 100644 index 0000000000..06d59c487f --- /dev/null +++ b/synthesizer/tests/expectations/process/execute/external_struct2.out @@ -0,0 +1,3 @@ +errors: [] +outputs: +- [] diff --git a/synthesizer/tests/expectations/vm/execute_and_finalize/external_struct.out b/synthesizer/tests/expectations/vm/execute_and_finalize/external_struct.out index 12d28aab06..8443836774 100644 --- a/synthesizer/tests/expectations/vm/execute_and_finalize/external_struct.out +++ b/synthesizer/tests/expectations/vm/execute_and_finalize/external_struct.out @@ -1,3 +1,16 @@ -errors: -- 'Failed to run `VM::deploy for program external_struct_test.aleo: Instruction ''assert.eq'' expects inputs of the same type. Found inputs of type ''external_struct.aleo/S'' and ''S''' -outputs: [] +errors: [] +outputs: +- verified: true + execute: + external_struct_test.aleo/main: + outputs: [] + speculate: the execution was accepted + add_next_block: succeeded. +additional: +- child_outputs: + external_struct.aleo/make_struct: + outputs: + - '{"type":"private","id":"950054391566066141831813426021543606350122839180822212323121576500426543341field","value":"ciphertext1qyq9gpt3enh23ry02wmvj4zyk3w3jkk6gga04054jxpq6ctqz8hz6qgh2g64u"}' + credits.aleo/fee_public: + outputs: + - '{"type":"future","id":"3048490372427538183609819194439442577604813977270947453894798278641990313104field","value":"{\n program_id: credits.aleo,\n function_name: fee_public,\n arguments: [\n aleo1qr2ha4pfs5l28aze88yn6fhleeythklkczrule2v838uwj65n5gqxt9djx,\n 2454u64\n ]\n}"}' diff --git a/synthesizer/tests/expectations/vm/execute_and_finalize/external_struct_in_mapping.out b/synthesizer/tests/expectations/vm/execute_and_finalize/external_struct_in_mapping.out index 4ed5323178..33f64bad8d 100644 --- a/synthesizer/tests/expectations/vm/execute_and_finalize/external_struct_in_mapping.out +++ b/synthesizer/tests/expectations/vm/execute_and_finalize/external_struct_in_mapping.out @@ -4,14 +4,14 @@ outputs: execute: parent.aleo/store_t: outputs: - - '{"type":"future","id":"5156950801145169537669009747157321932147267606917954059095229590621596317558field","value":"{\n program_id: parent.aleo,\n function_name: store_t,\n arguments: [\n {\n x: 12field\n}\n ]\n}"}' + - '{"type":"future","id":"6801504684493219302823789405686224386785657517434152274736013398978329144845field","value":"{\n program_id: parent.aleo,\n function_name: store_t,\n arguments: [\n {\n x: 12field\n}\n ]\n}"}' speculate: the execution was accepted add_next_block: succeeded. - verified: true execute: parent.aleo/store_s: outputs: - - '{"type":"future","id":"3536429601035031221536064521737537064728529805456995459759090711197590968904field","value":"{\n program_id: parent.aleo,\n function_name: store_s,\n arguments: [\n {\n x: 12field\n}\n ]\n}"}' + - '{"type":"future","id":"391874225078724773571924710844101851974151637265974521076487544577584656434field","value":"{\n program_id: parent.aleo,\n function_name: store_s,\n arguments: [\n {\n x: 12field\n}\n ]\n}"}' speculate: the execution was accepted add_next_block: succeeded. - verified: true @@ -21,22 +21,29 @@ outputs: - '{"type":"future","id":"4059613284155828593649148683363481039153998827084982120202239631116761077539field","value":"{\n program_id: parent.aleo,\n function_name: store_s_as_t,\n arguments: [\n {\n x: 12field\n}\n ]\n}"}' speculate: the execution was accepted add_next_block: succeeded. +- verified: true + execute: + parent.aleo/baz_keys: + outputs: + - '{"type":"future","id":"4178532581508837032088555072114372004008624381630945776472369807190726177644field","value":"{\n program_id: parent.aleo,\n function_name: baz_keys,\n arguments: [\n {\n x: 12field\n}\n ]\n}"}' + speculate: the execution was accepted + add_next_block: succeeded. - verified: true execute: parent.aleo/compare: outputs: - - '{"type":"future","id":"599984621506371769970208532698818450215019892731507626787420093675049916523field","value":"{\n program_id: parent.aleo,\n function_name: compare,\n arguments: []\n}"}' + - '{"type":"future","id":"1414123528844326309044566546573721844182310369101304450030781589321056115963field","value":"{\n program_id: parent.aleo,\n function_name: compare,\n arguments: []\n}"}' speculate: the execution was accepted add_next_block: succeeded. additional: - child_outputs: credits.aleo/fee_public: outputs: - - '{"type":"future","id":"3077534165282048476651182034927959704102009637768584675657634181464198851648field","value":"{\n program_id: credits.aleo,\n function_name: fee_public,\n arguments: [\n aleo1qr2ha4pfs5l28aze88yn6fhleeythklkczrule2v838uwj65n5gqxt9djx,\n 17842u64\n ]\n}"}' + - '{"type":"future","id":"5931083473252536185719147680329323401761022184518569458483931692885063239858field","value":"{\n program_id: credits.aleo,\n function_name: fee_public,\n arguments: [\n aleo1qr2ha4pfs5l28aze88yn6fhleeythklkczrule2v838uwj65n5gqxt9djx,\n 17842u64\n ]\n}"}' - child_outputs: child.aleo/make_s: outputs: - - '{"type":"public","id":"8357804394006689049643614990561262851191212004864212878785279736486409935348field","value":"{\n x: 12field\n}"}' + - '{"type":"public","id":"6985903130429112305577914343133100080684294095291138773079739129624829511258field","value":"{\n x: 12field\n}"}' credits.aleo/fee_public: outputs: - '{"type":"future","id":"6423060729030407108337685286277924679730527590289056343090240277713705336980field","value":"{\n program_id: credits.aleo,\n function_name: fee_public,\n arguments: [\n aleo1qr2ha4pfs5l28aze88yn6fhleeythklkczrule2v838uwj65n5gqxt9djx,\n 18554u64\n ]\n}"}' @@ -47,7 +54,14 @@ additional: credits.aleo/fee_public: outputs: - '{"type":"future","id":"2645278696162344307543607738900689987512504974551459357913512732894441614319field","value":"{\n program_id: credits.aleo,\n function_name: fee_public,\n arguments: [\n aleo1qr2ha4pfs5l28aze88yn6fhleeythklkczrule2v838uwj65n5gqxt9djx,\n 20084u64\n ]\n}"}' +- child_outputs: + child.aleo/make_s: + outputs: + - '{"type":"public","id":"5642997165471718018783739778741660536733800233179096103171448587842404180286field","value":"{\n x: 12field\n}"}' + credits.aleo/fee_public: + outputs: + - '{"type":"future","id":"6458128924800203602226324183919154340534363938397245013050876831071587836128field","value":"{\n program_id: credits.aleo,\n function_name: fee_public,\n arguments: [\n aleo1qr2ha4pfs5l28aze88yn6fhleeythklkczrule2v838uwj65n5gqxt9djx,\n 18556u64\n ]\n}"}' - child_outputs: credits.aleo/fee_public: outputs: - - '{"type":"future","id":"7038695923461775559991406419975410061163444193339031735011873989822584323603field","value":"{\n program_id: credits.aleo,\n function_name: fee_public,\n arguments: [\n aleo1qr2ha4pfs5l28aze88yn6fhleeythklkczrule2v838uwj65n5gqxt9djx,\n 5338u64\n ]\n}"}' + - '{"type":"future","id":"1448272572280864368327708519450049907934875151862273228624942884364036863439field","value":"{\n program_id: credits.aleo,\n function_name: fee_public,\n arguments: [\n aleo1qr2ha4pfs5l28aze88yn6fhleeythklkczrule2v838uwj65n5gqxt9djx,\n 5338u64\n ]\n}"}' diff --git a/synthesizer/tests/tests/process/execute/external_struct.aleo b/synthesizer/tests/tests/process/execute/external_struct.aleo index 2b8d7103c6..978dbb807f 100644 --- a/synthesizer/tests/tests/process/execute/external_struct.aleo +++ b/synthesizer/tests/tests/process/execute/external_struct.aleo @@ -27,7 +27,7 @@ cases: inputs: [] */ -program parent.aleo; +program external_struct_parent.aleo; struct S: x as field; @@ -58,7 +58,7 @@ function accept_s: ///////////////////////////////////////////////// -import parent.aleo; +import external_struct_parent.aleo; program child.aleo; @@ -70,42 +70,42 @@ struct OuterT: s as T; struct Outer2: - s as parent.aleo/S; + s as external_struct_parent.aleo/S; function make_s: - call parent.aleo/make_s into r0; - output r0 as parent.aleo/S.public; + call external_struct_parent.aleo/make_s into r0; + output r0 as external_struct_parent.aleo/S.public; function make_t: - call parent.aleo/make_s into r0; + call external_struct_parent.aleo/make_s into r0; output r0 as T.public; function call_make_outer: - call parent.aleo/make_outer 3field into r0; + call external_struct_parent.aleo/make_outer 3field into r0; output r0 as OuterT.public; function call_make_array: - call parent.aleo/make_array 4field into r0; + call external_struct_parent.aleo/make_array 4field into r0; output r0 as [T; 2u32].public; function pass_s: - cast 1field into r0 as parent.aleo/S; - call parent.aleo/accept_s r0 into r1; + cast 1field into r0 as external_struct_parent.aleo/S; + call external_struct_parent.aleo/accept_s r0 into r1; output r1 as field.public; function pass_t: cast 2field into r0 as T; - call parent.aleo/accept_s r0 into r1; + call external_struct_parent.aleo/accept_s r0 into r1; output r1 as field.public; function make_outer2: - cast 100field into r0 as parent.aleo/S; + cast 100field into r0 as external_struct_parent.aleo/S; cast r0 into r1 as Outer2; output r1 as Outer2.public; function cast_s: - cast 101field into r0 as parent.aleo/S; - output r0 as parent.aleo/S.public; + cast 101field into r0 as external_struct_parent.aleo/S; + output r0 as external_struct_parent.aleo/S.public; ///////////////////////////////////////////////// diff --git a/synthesizer/tests/tests/process/execute/external_struct2.aleo b/synthesizer/tests/tests/process/execute/external_struct2.aleo new file mode 100644 index 0000000000..6f6781e4b6 --- /dev/null +++ b/synthesizer/tests/tests/process/execute/external_struct2.aleo @@ -0,0 +1,46 @@ +/* +randomness: 45791624 +cases: + - program: external_struct2_child.aleo + function: main + inputs: [1u8] +*/ + +program external_struct2_parent.aleo; + +struct S: + data as u8; + +function make_struct: + input r0 as u8.private; + cast r0 into r1 as S; + output r1 as S.private; + +///////////////////////////////////////////////// + + +import external_struct2_parent.aleo; + +program external_struct2_child.aleo; + +struct S: + data as u8; + +closure check_structs: + input r0 as external_struct2_parent.aleo/S; + input r1 as S; + is.eq r0 r1 into r2; + is.neq r0 r1 into r3; + assert.eq r0 r1; + assert.eq r2 true; + assert.eq r3 false; + +function main: + input r0 as u8.private; + call external_struct2_parent.aleo/make_struct r0 into r1; + call external_struct2_parent.aleo/make_struct r0 into r2; + cast r0 into r3 as S; + assert.eq r1 r2; + assert.eq r1 r3; + call check_structs r1 r2; + call check_structs r1 r3; diff --git a/synthesizer/tests/tests/vm/execute_and_finalize/external_struct_in_mapping.aleo b/synthesizer/tests/tests/vm/execute_and_finalize/external_struct_in_mapping.aleo index 3e8282ec8e..91d918416e 100644 --- a/synthesizer/tests/tests/vm/execute_and_finalize/external_struct_in_mapping.aleo +++ b/synthesizer/tests/tests/vm/execute_and_finalize/external_struct_in_mapping.aleo @@ -11,6 +11,9 @@ cases: - program: parent.aleo function: store_s_as_t inputs: [] + - program: parent.aleo + function: baz_keys + inputs: [] - program: parent.aleo function: compare inputs: [] @@ -45,6 +48,10 @@ mapping bar: key as field.public; value as child.aleo/S.public; +mapping baz: + key as T.public; + value as field.public; + closure make_t: input r0 as field; cast r0 into r1 as T; @@ -75,6 +82,14 @@ finalize store_s_as_t: cast r0.x into r1 as T; set r1 into foo[2field]; +function baz_keys: + call child.aleo/make_s into r0; + async baz_keys r0 into r1; + output r1 as parent.aleo/baz_keys.future; +finalize baz_keys: + input r0 as child.aleo/S.public; + set 1field into baz[r0]; + function compare: async compare into r0; output r0 as parent.aleo/compare.future; From 0f4bb1bc73502d25d7ab49573fb388d38bc6ddc7 Mon Sep 17 00:00:00 2001 From: Michael Benfield Date: Sun, 24 Aug 2025 12:44:06 -0700 Subject: [PATCH 14/25] Address review comments. Change tag for ArrayType element type. Introduce and use finalize_types_structurally_equivalent. Use types_structurally_equivalent in synthesizer/process/src/stack/finalize_types/matches.rs `ProgramID` with backticks in comments. --- Cargo.lock | 1 - .../program/src/data_types/array_type/bytes.rs | 10 +++++----- .../src/data_types/plaintext_type/parse.rs | 1 + .../src/stack/finalize_types/initialize.rs | 10 ++++++---- .../process/src/stack/finalize_types/matches.rs | 8 ++++++-- .../process/src/stack/finalize_types/mod.rs | 15 +++++++++++++++ .../src/logic/instruction/operation/call.rs | 4 ++-- 7 files changed, 35 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7860061ccf..18d029781e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3767,7 +3767,6 @@ name = "snarkvm-synthesizer-snark" version = "4.1.0" dependencies = [ "bincode", - "colored 3.0.0", "serde_json", "snarkvm-algorithms", "snarkvm-circuit", diff --git a/console/program/src/data_types/array_type/bytes.rs b/console/program/src/data_types/array_type/bytes.rs index dea57da750..001ad36290 100644 --- a/console/program/src/data_types/array_type/bytes.rs +++ b/console/program/src/data_types/array_type/bytes.rs @@ -23,7 +23,7 @@ impl FromBytes for ArrayType { let element_type = match variant { 0 => PlaintextType::Literal(LiteralType::read_le(&mut reader)?), 1 => PlaintextType::Struct(Identifier::read_le(&mut reader)?), - 3 => PlaintextType::ExternalStruct(Locator::read_le(&mut reader)?), + 2 => PlaintextType::ExternalStruct(Locator::read_le(&mut reader)?), _ => return Err(error(format!("Failed to deserialize element type {variant}"))), }; @@ -82,15 +82,15 @@ impl ToBytes for ArrayType { 1u8.write_le(&mut writer)?; identifier.write_le(&mut writer)?; } + PlaintextType::ExternalStruct(locator) => { + 2u8.write_le(&mut writer)?; + locator.write_le(&mut writer)?; + } PlaintextType::Array(_) => { // This is technically unreachable by definition, however we return an error // out of an abundance of caution. return Err(error(format!("Array type exceeds the maximum depth of {}.", N::MAX_DATA_DEPTH))); } - PlaintextType::ExternalStruct(locator) => { - 3u8.write_le(&mut writer)?; - locator.write_le(&mut writer)?; - } } // Write the number of dimensions of the array. diff --git a/console/program/src/data_types/plaintext_type/parse.rs b/console/program/src/data_types/plaintext_type/parse.rs index ce2ee0743d..6de10b1ed2 100644 --- a/console/program/src/data_types/plaintext_type/parse.rs +++ b/console/program/src/data_types/plaintext_type/parse.rs @@ -62,6 +62,7 @@ impl Display for PlaintextType { Self::Literal(literal) => Display::fmt(literal, f), // Prints the struct, i.e. signature Self::Struct(struct_) => Display::fmt(struct_, f), + // Prints the external struct, i.e. foo.aleo/bar Self::ExternalStruct(locator) => Display::fmt(locator, f), // Prints the array type, i.e. [field; 2u32] Self::Array(array) => Display::fmt(array, f), diff --git a/synthesizer/process/src/stack/finalize_types/initialize.rs b/synthesizer/process/src/stack/finalize_types/initialize.rs index d0d0b597d3..338269b893 100644 --- a/synthesizer/process/src/stack/finalize_types/initialize.rs +++ b/synthesizer/process/src/stack/finalize_types/initialize.rs @@ -13,6 +13,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +use snarkvm_synthesizer_program::types_structurally_equivalent; + use super::*; impl FinalizeTypes { @@ -169,7 +171,7 @@ impl FinalizeTypes { self.add_input(register.clone(), finalize_type.clone())?; // Ensure the register type and the input type match. - if finalize_type != &self.get_type(stack, register)? { + if !finalize_types_structurally_equivalent(stack, finalize_type, stack, &self.get_type(stack, register)?)? { bail!("Input '{register}' does not match the expected input register type.") } @@ -259,7 +261,7 @@ impl FinalizeTypes { }; // Check that the operands have the same type. ensure!( - first_type == second_type, + types_structurally_equivalent(stack, &first_type, stack, &second_type)?, "Command '{}' expects operands of the same type. Found operands of type '{}' and '{}'", Branch::::opcode(), first_type, @@ -465,7 +467,7 @@ impl FinalizeTypes { FinalizeType::Future(..) => bail!("A default value cannot be a future"), }; // Check that the value type in the mapping matches the default value type. - if mapping_value_type != &default_value_type { + if !types_structurally_equivalent(stack, mapping_value_type, stack, &default_value_type)? { bail!( "Default value type in `get.or_use` '{default_value_type}' does not match the value type in the mapping '{mapping_value_type}'." ) @@ -538,7 +540,7 @@ impl FinalizeTypes { FinalizeType::Future(..) => bail!("A future cannot be used as a value in a `set` command"), }; // Check that the value type in the mapping matches the type of the value. - if mapping_value_type != &value_type { + if !types_structurally_equivalent(stack, mapping_value_type, stack, &value_type)? { bail!( "Value type in `set` '{value_type}' does not match the value type in the mapping '{mapping_value_type}'." ) diff --git a/synthesizer/process/src/stack/finalize_types/matches.rs b/synthesizer/process/src/stack/finalize_types/matches.rs index 67bdecf067..073513788b 100644 --- a/synthesizer/process/src/stack/finalize_types/matches.rs +++ b/synthesizer/process/src/stack/finalize_types/matches.rs @@ -45,6 +45,7 @@ impl FinalizeTypes { // Ensure the literal type matches the member type. Operand::Literal(literal) => { ensure!( + // No need to call `types_structurally_equivalent`, since it can't be a struct. &PlaintextType::Literal(literal.to_type()) == member_type, "Struct member '{struct_name}.{member_name}' expects a {member_type}, but found '{operand}' in the operand.", ) @@ -60,7 +61,7 @@ impl FinalizeTypes { }; // Ensure the register type matches the member type. ensure!( - &plaintext_type == member_type, + types_structurally_equivalent(stack, &plaintext_type, stack, member_type)?, "Struct member '{struct_name}.{member_name}' expects {member_type}, but found '{plaintext_type}' in the operand '{operand}'.", ) } @@ -79,6 +80,7 @@ impl FinalizeTypes { }; // Ensure the operand type matches the member type. ensure!( + // No need to call `types_structurally_equivalent`, since `program_ref_type` cannot be a struct. &program_ref_type == member_type, "Struct member '{struct_name}.{member_name}' expects {member_type}, but found '{program_ref_type}' in the operand '{operand}'.", ) @@ -120,6 +122,7 @@ impl FinalizeTypes { // Ensure the literal type matches the element type. Operand::Literal(literal) => { ensure!( + // No need to call `types_structurally_equivalent`, since it can't be a struct. &PlaintextType::Literal(literal.to_type()) == array_type.next_element_type(), "Array element expects {}, but found '{operand}' in the operand.", array_type.next_element_type() @@ -136,7 +139,7 @@ impl FinalizeTypes { }; // Ensure the register type matches the element type. ensure!( - &plaintext_type == array_type.next_element_type(), + types_structurally_equivalent(stack, &plaintext_type, stack, array_type.next_element_type())?, "Array element expects {}, but found '{plaintext_type}' in the operand '{operand}'.", array_type.next_element_type() ) @@ -154,6 +157,7 @@ impl FinalizeTypes { }; // Ensure the operand type matches the element type. ensure!( + // No need to call `types_structurally_equivalent`, since `program_ref_type` cannot be a struct. &program_ref_type == array_type.next_element_type(), "Array element expects {}, but found '{program_ref_type}' in the operand '{operand}'.", array_type.next_element_type() diff --git a/synthesizer/process/src/stack/finalize_types/mod.rs b/synthesizer/process/src/stack/finalize_types/mod.rs index ae8ee9a847..0c322c392e 100644 --- a/synthesizer/process/src/stack/finalize_types/mod.rs +++ b/synthesizer/process/src/stack/finalize_types/mod.rs @@ -220,3 +220,18 @@ impl FinalizeTypes { Ok(finalize_type) } } + +pub fn finalize_types_structurally_equivalent( + stack0: &impl StackTrait, + type0: &FinalizeType, + stack1: &impl StackTrait, + type1: &FinalizeType, +) -> Result { + match (type0, type1) { + (FinalizeType::Plaintext(plaintext0), FinalizeType::Plaintext(plaintext1)) => { + types_structurally_equivalent(stack0, &plaintext0, stack1, &plaintext1) + } + (FinalizeType::Future(future0), FinalizeType::Future(future1)) => Ok(future0 == future1), + _ => Ok(false), + } +} diff --git a/synthesizer/program/src/logic/instruction/operation/call.rs b/synthesizer/program/src/logic/instruction/operation/call.rs index 6a5a43e227..45401373ff 100644 --- a/synthesizer/program/src/logic/instruction/operation/call.rs +++ b/synthesizer/program/src/logic/instruction/operation/call.rs @@ -224,7 +224,7 @@ impl Call { .output_types() .into_iter() // If the function is an external program, we need to qualify its structs with - // the appropriate ProgramID. + // the appropriate `ProgramID`. .map(|output_type| if is_external { output_type.qualify(*program.id()) } else { output_type }) .collect::>()) } @@ -248,7 +248,7 @@ impl Call { .into_iter() .map(RegisterType::from) // If the function is an external program, we need to qualify its structs or records with - // the appropriate ProgramID. + // the appropriate `ProgramID`. .map(|register_type| if is_external { register_type.qualify(*program.id()) } else { register_type }) .collect::>()) } From 71aef7f15ead8015a6840e8bb3e3a71c86c958aa Mon Sep 17 00:00:00 2001 From: Michael Benfield Date: Sun, 24 Aug 2025 13:49:11 -0700 Subject: [PATCH 15/25] Test accessing a field of an external struct --- .../expectations/process/execute/external_struct.out | 4 ++++ .../tests/tests/process/execute/external_struct.aleo | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/synthesizer/tests/expectations/process/execute/external_struct.out b/synthesizer/tests/expectations/process/execute/external_struct.out index c4784e0e2a..541f7977a9 100644 --- a/synthesizer/tests/expectations/process/execute/external_struct.out +++ b/synthesizer/tests/expectations/process/execute/external_struct.out @@ -36,3 +36,7 @@ outputs: x: 100field } } +- - |- + { + x: 100field + } diff --git a/synthesizer/tests/tests/process/execute/external_struct.aleo b/synthesizer/tests/tests/process/execute/external_struct.aleo index 978dbb807f..e23e38424d 100644 --- a/synthesizer/tests/tests/process/execute/external_struct.aleo +++ b/synthesizer/tests/tests/process/execute/external_struct.aleo @@ -25,6 +25,9 @@ cases: - program: child2.aleo function: call_make_outer2 inputs: [] + - program: child2.aleo + function: call_make_outer2_access + inputs: [] */ program external_struct_parent.aleo; @@ -110,9 +113,14 @@ function cast_s: ///////////////////////////////////////////////// import child.aleo; +import external_struct_parent.aleo; program child2.aleo; function call_make_outer2: call child.aleo/make_outer2 into r0; output r0 as child.aleo/Outer2.public; + +function call_make_outer2_access: + call child.aleo/make_outer2 into r0; + output r0.s as external_struct_parent.aleo/S.public; From e5f7e3bebc1cd8eade9b91f6488477167cf26fca Mon Sep 17 00:00:00 2001 From: Michael Benfield Date: Sun, 24 Aug 2025 16:06:23 -0700 Subject: [PATCH 16/25] clippy --- synthesizer/process/src/stack/finalize_types/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synthesizer/process/src/stack/finalize_types/mod.rs b/synthesizer/process/src/stack/finalize_types/mod.rs index 0c322c392e..3d5320287f 100644 --- a/synthesizer/process/src/stack/finalize_types/mod.rs +++ b/synthesizer/process/src/stack/finalize_types/mod.rs @@ -229,7 +229,7 @@ pub fn finalize_types_structurally_equivalent( ) -> Result { match (type0, type1) { (FinalizeType::Plaintext(plaintext0), FinalizeType::Plaintext(plaintext1)) => { - types_structurally_equivalent(stack0, &plaintext0, stack1, &plaintext1) + types_structurally_equivalent(stack0, plaintext0, stack1, plaintext1) } (FinalizeType::Future(future0), FinalizeType::Future(future1)) => Ok(future0 == future1), _ => Ok(false), From 377e066539f9e76251ce57c103bcff1cdad52e9e Mon Sep 17 00:00:00 2001 From: Michael Benfield Date: Thu, 28 Aug 2025 09:12:40 -0700 Subject: [PATCH 17/25] Disallow nonexistent structs in mappings --- synthesizer/process/src/stack/mod.rs | 6 ++++ .../mapping_with_nonexistent_structs.out | 4 +-- ...mapping_with_nonexistent_structs_child.out | 3 ++ .../mapping_with_nonexistent_structs.aleo | 16 ----------- ...apping_with_nonexistent_structs_child.aleo | 28 +++++++++++++++++++ 5 files changed, 39 insertions(+), 18 deletions(-) create mode 100644 synthesizer/tests/expectations/vm/execute_and_finalize/mapping_with_nonexistent_structs_child.out create mode 100644 synthesizer/tests/tests/vm/execute_and_finalize/mapping_with_nonexistent_structs_child.aleo diff --git a/synthesizer/process/src/stack/mod.rs b/synthesizer/process/src/stack/mod.rs index 44ed3220d3..abc73559c3 100644 --- a/synthesizer/process/src/stack/mod.rs +++ b/synthesizer/process/src/stack/mod.rs @@ -332,6 +332,12 @@ impl Stack { } } + for mapping in self.program.mappings().values() { + // These calls make sure structs exist. + RegisterTypes::check_plaintext_type(self, mapping.key().plaintext_type())?; + RegisterTypes::check_plaintext_type(self, mapping.value().plaintext_type())?; + } + // Drop the locks since the types have been initialized. drop(constructor_types); drop(register_types); diff --git a/synthesizer/tests/expectations/vm/execute_and_finalize/mapping_with_nonexistent_structs.out b/synthesizer/tests/expectations/vm/execute_and_finalize/mapping_with_nonexistent_structs.out index b54ad3cd02..708d0467a4 100644 --- a/synthesizer/tests/expectations/vm/execute_and_finalize/mapping_with_nonexistent_structs.out +++ b/synthesizer/tests/expectations/vm/execute_and_finalize/mapping_with_nonexistent_structs.out @@ -1,3 +1,3 @@ -errors: [] +errors: +- 'Failed to run `VM::deploy for program child.aleo: Struct ''S'' in ''child.aleo'' is not defined.' outputs: [] -additional: [] diff --git a/synthesizer/tests/expectations/vm/execute_and_finalize/mapping_with_nonexistent_structs_child.out b/synthesizer/tests/expectations/vm/execute_and_finalize/mapping_with_nonexistent_structs_child.out new file mode 100644 index 0000000000..dca56e01f1 --- /dev/null +++ b/synthesizer/tests/expectations/vm/execute_and_finalize/mapping_with_nonexistent_structs_child.out @@ -0,0 +1,3 @@ +errors: +- 'Failed to run `VM::deploy for program parent.aleo: Struct ''S'' in ''child.aleo'' is not defined.' +outputs: [] diff --git a/synthesizer/tests/tests/vm/execute_and_finalize/mapping_with_nonexistent_structs.aleo b/synthesizer/tests/tests/vm/execute_and_finalize/mapping_with_nonexistent_structs.aleo index d1a21672cd..83aecf0f6f 100644 --- a/synthesizer/tests/tests/vm/execute_and_finalize/mapping_with_nonexistent_structs.aleo +++ b/synthesizer/tests/tests/vm/execute_and_finalize/mapping_with_nonexistent_structs.aleo @@ -14,19 +14,3 @@ function dummy: constructor: assert.eq edition 0u16; - -///////////////////////////////////////////////// - -import child.aleo; - -program parent.aleo; - -mapping foo: - key as field.public; - value as child.aleo/S.public; - -function dummy: - -constructor: - assert.eq edition 0u16; - diff --git a/synthesizer/tests/tests/vm/execute_and_finalize/mapping_with_nonexistent_structs_child.aleo b/synthesizer/tests/tests/vm/execute_and_finalize/mapping_with_nonexistent_structs_child.aleo new file mode 100644 index 0000000000..651dd3d193 --- /dev/null +++ b/synthesizer/tests/tests/vm/execute_and_finalize/mapping_with_nonexistent_structs_child.aleo @@ -0,0 +1,28 @@ +/* +start_height: 19 +randomness: 45791624 +cases: [] +*/ + +program child.aleo; + +function dummy: + +constructor: + assert.eq edition 0u16; + +///////////////////////////////////////////////// + +import child.aleo; + +program parent.aleo; + +mapping foo: + key as field.public; + value as child.aleo/S.public; + +function dummy: + +constructor: + assert.eq edition 0u16; + From c015c7d01044d9bf3e78d95266eba4ee7f6d87d0 Mon Sep 17 00:00:00 2001 From: Michael Benfield Date: Tue, 16 Sep 2025 10:22:30 -0700 Subject: [PATCH 18/25] Move type checks for mapping types to check_transaction --- .../process/src/stack/register_types/initialize.rs | 2 +- synthesizer/src/vm/verify.rs | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/synthesizer/process/src/stack/register_types/initialize.rs b/synthesizer/process/src/stack/register_types/initialize.rs index ba8da29742..548341e0c7 100644 --- a/synthesizer/process/src/stack/register_types/initialize.rs +++ b/synthesizer/process/src/stack/register_types/initialize.rs @@ -646,7 +646,7 @@ impl RegisterTypes { // } /// Ensure any struct referenced directly or otherwise exists. - pub(crate) fn check_plaintext_type(stack: &Stack, type_: &PlaintextType) -> Result<()> { + pub fn check_plaintext_type(stack: &Stack, type_: &PlaintextType) -> Result<()> { match type_ { PlaintextType::Literal(..) => Ok(()), PlaintextType::Struct(struct_name) => { diff --git a/synthesizer/src/vm/verify.rs b/synthesizer/src/vm/verify.rs index 55b61159d5..7e5606326b 100644 --- a/synthesizer/src/vm/verify.rs +++ b/synthesizer/src/vm/verify.rs @@ -13,6 +13,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +use snarkvm_synthesizer_process::{RegisterTypes, Stack}; + use super::*; /// Ensures the given iterator has no duplicate elements, and that the ledger @@ -189,7 +191,8 @@ impl> VM { // Verify the signature corresponds to the transaction ID. ensure!(owner.verify(*deployment_id), "Invalid owner signature for deployment transaction '{id}'"); - ensure_deployment_valid_for_consensus_version(consensus_version, deployment, id)?; + let stack = self.process.read().get_stack(deployment.program_id())?; + ensure_deployment_valid_for_consensus_version(&*stack, consensus_version, deployment, id)?; // If the program owner exists in the deployment, then verify that it matches the owner in the transaction. if let Some(given_owner) = deployment.program_owner() { @@ -440,6 +443,7 @@ impl> VM { } fn ensure_deployment_valid_for_consensus_version( + stack: &Stack, consensus_version: ConsensusVersion, deployment: &Deployment, id: &N::TransactionID, @@ -494,6 +498,14 @@ fn ensure_deployment_valid_for_consensus_version( ); } + if consensus_version >= ConsensusVersion::V11 { + for mapping in stack.program().mappings().values() { + // These calls make sure structs exist. + RegisterTypes::check_plaintext_type(stack, mapping.key().plaintext_type())?; + RegisterTypes::check_plaintext_type(stack, mapping.value().plaintext_type())?; + } + } + Ok(()) } From 7e02f209a02f975215a60df1e409d43b2828b15a Mon Sep 17 00:00:00 2001 From: Michael Benfield Date: Tue, 16 Sep 2025 15:43:07 -0700 Subject: [PATCH 19/25] Fix mapping type check --- synthesizer/src/vm/verify.rs | 159 +++++++++++++++++++++-------------- 1 file changed, 94 insertions(+), 65 deletions(-) diff --git a/synthesizer/src/vm/verify.rs b/synthesizer/src/vm/verify.rs index 7e5606326b..31866a7dfd 100644 --- a/synthesizer/src/vm/verify.rs +++ b/synthesizer/src/vm/verify.rs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use snarkvm_synthesizer_process::{RegisterTypes, Stack}; +use console::program::PlaintextType; use super::*; @@ -191,8 +191,7 @@ impl> VM { // Verify the signature corresponds to the transaction ID. ensure!(owner.verify(*deployment_id), "Invalid owner signature for deployment transaction '{id}'"); - let stack = self.process.read().get_stack(deployment.program_id())?; - ensure_deployment_valid_for_consensus_version(&*stack, consensus_version, deployment, id)?; + self.ensure_deployment_valid_for_consensus_version(consensus_version, deployment, id)?; // If the program owner exists in the deployment, then verify that it matches the owner in the transaction. if let Some(given_owner) = deployment.program_owner() { @@ -440,73 +439,103 @@ impl> VM { } Ok(()) } -} -fn ensure_deployment_valid_for_consensus_version( - stack: &Stack, - consensus_version: ConsensusVersion, - deployment: &Deployment, - id: &N::TransactionID, -) -> Result<()> { - // If the `CONSENSUS_VERSION` is less than `V8`, ensure that - // - the deployment edition is zero. - // If the `CONSENSUS_VERSION` is less than `V9` ensure that - // - the deployment edition is zero or one. - // - the program checksum is **not** present in the deployment, - // - the program owner is **not** present in the deployment - // - the program does not use constructors, `Operand::Checksum`, `Operand::Edition`, or `Operand::ProgramOwner` - // If the `CONSENSUS_VERSION` is greater than or equal to `V9`, then verify that: - // - the program checksum is present in the deployment - // - the program owner is present in the deployment - // If the `CONSENSUS_VERSION` is less than `V10`, then verify that: - // - the program does not use the external struct syntax `some_program.aleo/StructT` - if consensus_version < ConsensusVersion::V8 { - ensure!( - deployment.edition().is_zero(), - "Invalid deployment transaction '{id}' - edition should be zero before `ConsensusVersion::V8`", - ); - } - if consensus_version < ConsensusVersion::V9 { - ensure!( - deployment.edition() <= 1, - "Invalid deployment transaction '{id}' - edition should be zero or one for before `ConsensusVersion::V9`" - ); - ensure!( - deployment.program_checksum().is_none(), - "Invalid deployment transaction '{id}' - should not contain program checksum" - ); - ensure!( - deployment.program_owner().is_none(), - "Invalid deployment transaction '{id}' - should not contain program owner" - ); - ensure!( - !deployment.program().contains_v9_syntax(), - "Invalid deployment transaction '{id}' - program uses syntax that is not allowed before `ConsensusVersion::V9`" - ); - } - if consensus_version >= ConsensusVersion::V9 { - ensure!( - deployment.program_checksum().is_some(), - "Invalid deployment transaction '{id}' - missing program checksum" - ); - ensure!(deployment.program_owner().is_some(), "Invalid deployment transaction '{id}' - missing program owner"); - } - if consensus_version < ConsensusVersion::V11 { - ensure!( - !deployment.program().contains_external_struct(), - "Invalid deployment transaction '{id}' - external structs may only be used beginning with Consensus version 10" - ); - } + fn ensure_deployment_valid_for_consensus_version( + &self, + consensus_version: ConsensusVersion, + deployment: &Deployment, + id: &N::TransactionID, + ) -> Result<()> { + // If the `CONSENSUS_VERSION` is less than `V8`, ensure that + // - the deployment edition is zero. + // If the `CONSENSUS_VERSION` is less than `V9` ensure that + // - the deployment edition is zero or one. + // - the program checksum is **not** present in the deployment, + // - the program owner is **not** present in the deployment + // - the program does not use constructors, `Operand::Checksum`, `Operand::Edition`, or `Operand::ProgramOwner` + // If the `CONSENSUS_VERSION` is greater than or equal to `V9`, then verify that: + // - the program checksum is present in the deployment + // - the program owner is present in the deployment + // If the `CONSENSUS_VERSION` is less than `V10`, then verify that: + // - the program does not use the external struct syntax `some_program.aleo/StructT` + if consensus_version < ConsensusVersion::V8 { + ensure!( + deployment.edition().is_zero(), + "Invalid deployment transaction '{id}' - edition should be zero before `ConsensusVersion::V8`", + ); + } + if consensus_version < ConsensusVersion::V9 { + ensure!( + deployment.edition() <= 1, + "Invalid deployment transaction '{id}' - edition should be zero or one for before `ConsensusVersion::V9`" + ); + ensure!( + deployment.program_checksum().is_none(), + "Invalid deployment transaction '{id}' - should not contain program checksum" + ); + ensure!( + deployment.program_owner().is_none(), + "Invalid deployment transaction '{id}' - should not contain program owner" + ); + ensure!( + !deployment.program().contains_v9_syntax(), + "Invalid deployment transaction '{id}' - program uses syntax that is not allowed before `ConsensusVersion::V9`" + ); + } + if consensus_version >= ConsensusVersion::V9 { + ensure!( + deployment.program_checksum().is_some(), + "Invalid deployment transaction '{id}' - missing program checksum" + ); + ensure!( + deployment.program_owner().is_some(), + "Invalid deployment transaction '{id}' - missing program owner" + ); + } + if consensus_version < ConsensusVersion::V11 { + ensure!( + !deployment.program().contains_external_struct(), + "Invalid deployment transaction '{id}' - external structs may only be used beginning with Consensus version 10" + ); + } - if consensus_version >= ConsensusVersion::V11 { - for mapping in stack.program().mappings().values() { - // These calls make sure structs exist. - RegisterTypes::check_plaintext_type(stack, mapping.key().plaintext_type())?; - RegisterTypes::check_plaintext_type(stack, mapping.value().plaintext_type())?; + if consensus_version >= ConsensusVersion::V11 { + for mapping in deployment.program().mappings().values() { + // These calls make sure structs exist. + self.plaintext_exists(mapping.key().plaintext_type(), deployment.program())?; + self.plaintext_exists(mapping.value().plaintext_type(), deployment.program())?; + } } + + Ok(()) } - Ok(()) + // If `type_` is a struct or an array containing a struct, ensure the struct type exists. + fn plaintext_exists(&self, type_: &PlaintextType, program: &Program) -> Result<()> { + match type_ { + PlaintextType::Literal(..) => Ok(()), + PlaintextType::Struct(struct_name) => { + // Retrieve the struct from the program. + ensure!( + program.get_struct(struct_name).is_ok(), + "Struct '{struct_name}' in '{}' is not defined.", + program.id() + ); + Ok(()) + } + PlaintextType::ExternalStruct(locator) => { + let stack = self.process.read().get_stack(locator.program_id())?; + ensure!( + stack.program().get_struct(locator.resource()).is_ok(), + "Struct '{}' in '{}' is not defined.", + locator.resource(), + stack.program().id(), + ); + Ok(()) + } + PlaintextType::Array(array_type) => self.plaintext_exists(array_type.base_element_type(), program), + } + } } impl> VM { From 2985f4eeee5277ee02db98cef2d7501d54eba174 Mon Sep 17 00:00:00 2001 From: Michael Benfield Date: Fri, 19 Sep 2025 08:02:25 -0700 Subject: [PATCH 20/25] comments --- synthesizer/src/vm/verify.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/synthesizer/src/vm/verify.rs b/synthesizer/src/vm/verify.rs index 31866a7dfd..8db1dd2598 100644 --- a/synthesizer/src/vm/verify.rs +++ b/synthesizer/src/vm/verify.rs @@ -456,8 +456,9 @@ impl> VM { // If the `CONSENSUS_VERSION` is greater than or equal to `V9`, then verify that: // - the program checksum is present in the deployment // - the program owner is present in the deployment - // If the `CONSENSUS_VERSION` is less than `V10`, then verify that: + // If the `CONSENSUS_VERSION` is less than `V11`, then verify that: // - the program does not use the external struct syntax `some_program.aleo/StructT` + // - the program's mappings do not use non-existent structs. if consensus_version < ConsensusVersion::V8 { ensure!( deployment.edition().is_zero(), From 55038dcfdd805e20158dc5c549f643fbda91545b Mon Sep 17 00:00:00 2001 From: Michael Benfield Date: Fri, 19 Sep 2025 17:19:30 -0700 Subject: [PATCH 21/25] test_v11.rs Also change type equivalence to require structs have the same name. --- .../src/stack/finalize_types/initialize.rs | 20 ++-- .../src/stack/finalize_types/matches.rs | 12 +- .../process/src/stack/finalize_types/mod.rs | 6 +- .../src/stack/register_types/initialize.rs | 2 +- .../src/stack/register_types/matches.rs | 12 +- .../process/src/stack/register_types/mod.rs | 4 +- .../src/logic/instruction/operation/assert.rs | 4 +- .../src/logic/instruction/operation/async_.rs | 4 +- .../src/logic/instruction/operation/cast.rs | 6 +- .../src/logic/instruction/operation/is.rs | 4 +- .../program/src/traits/stack_and_registers.rs | 45 ++++++-- synthesizer/src/vm/tests/mod.rs | 3 + synthesizer/src/vm/tests/test_v11.rs | 105 ++++++++++++++++++ 13 files changed, 180 insertions(+), 47 deletions(-) create mode 100644 synthesizer/src/vm/tests/test_v11.rs diff --git a/synthesizer/process/src/stack/finalize_types/initialize.rs b/synthesizer/process/src/stack/finalize_types/initialize.rs index 338269b893..28d14322d7 100644 --- a/synthesizer/process/src/stack/finalize_types/initialize.rs +++ b/synthesizer/process/src/stack/finalize_types/initialize.rs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use snarkvm_synthesizer_program::types_structurally_equivalent; +use snarkvm_synthesizer_program::types_equivalent; use super::*; @@ -171,7 +171,7 @@ impl FinalizeTypes { self.add_input(register.clone(), finalize_type.clone())?; // Ensure the register type and the input type match. - if !finalize_types_structurally_equivalent(stack, finalize_type, stack, &self.get_type(stack, register)?)? { + if !finalize_types_equivalent(stack, finalize_type, stack, &self.get_type(stack, register)?)? { bail!("Input '{register}' does not match the expected input register type.") } @@ -261,7 +261,7 @@ impl FinalizeTypes { }; // Check that the operands have the same type. ensure!( - types_structurally_equivalent(stack, &first_type, stack, &second_type)?, + types_equivalent(stack, &first_type, stack, &second_type)?, "Command '{}' expects operands of the same type. Found operands of type '{}' and '{}'", Branch::::opcode(), first_type, @@ -326,7 +326,7 @@ impl FinalizeTypes { FinalizeType::Future(..) => bail!("A future cannot be used as a key in a `contains` command"), }; // Check that the key type in the mapping matches the key type in the instruction. - if !types_structurally_equivalent(stack, mapping_key_type, stack, &key_type)? { + if !types_equivalent(stack, mapping_key_type, stack, &key_type)? { bail!( "Key type in `contains` '{key_type}' does not match the key type in the mapping '{mapping_key_type}'." ) @@ -391,7 +391,7 @@ impl FinalizeTypes { FinalizeType::Future(..) => bail!("A future cannot be used as a key in a `get` command"), }; // Check that the key type in the mapping matches the key type in the instruction. - if !types_structurally_equivalent(stack, mapping_key_type, stack, &key_type)? { + if !types_equivalent(stack, mapping_key_type, stack, &key_type)? { bail!("Key type in `get` '{key_type}' does not match the key type in the mapping '{mapping_key_type}'.") } // Get the destination register. @@ -454,7 +454,7 @@ impl FinalizeTypes { FinalizeType::Future(..) => bail!("A future cannot be used as a key in a `get.or_use` command"), }; // Check that the key type in the mapping matches the key type. - if !types_structurally_equivalent(stack, mapping_key_type, stack, &key_type)? { + if !types_equivalent(stack, mapping_key_type, stack, &key_type)? { bail!( "Key type in `get.or_use` '{key_type}' does not match the key type in the mapping '{mapping_key_type}'." ) @@ -467,7 +467,7 @@ impl FinalizeTypes { FinalizeType::Future(..) => bail!("A default value cannot be a future"), }; // Check that the value type in the mapping matches the default value type. - if !types_structurally_equivalent(stack, mapping_value_type, stack, &default_value_type)? { + if !types_equivalent(stack, mapping_value_type, stack, &default_value_type)? { bail!( "Default value type in `get.or_use` '{default_value_type}' does not match the value type in the mapping '{mapping_value_type}'." ) @@ -529,7 +529,7 @@ impl FinalizeTypes { FinalizeType::Future(..) => bail!("A future cannot be used as a key in a `set` command"), }; // Check that the key type in the mapping matches the key type. - if !types_structurally_equivalent(stack, mapping_key_type, stack, &key_type)? { + if !types_equivalent(stack, mapping_key_type, stack, &key_type)? { bail!("Key type in `set` '{key_type}' does not match the key type in the mapping '{mapping_key_type}'.") } // Retrieve the type of the value. @@ -540,7 +540,7 @@ impl FinalizeTypes { FinalizeType::Future(..) => bail!("A future cannot be used as a value in a `set` command"), }; // Check that the value type in the mapping matches the type of the value. - if !types_structurally_equivalent(stack, mapping_value_type, stack, &value_type)? { + if !types_equivalent(stack, mapping_value_type, stack, &value_type)? { bail!( "Value type in `set` '{value_type}' does not match the value type in the mapping '{mapping_value_type}'." ) @@ -568,7 +568,7 @@ impl FinalizeTypes { FinalizeType::Future(..) => bail!("A future cannot be used as a key in a `remove` command"), }; // Check that the key type in the mapping matches the key type. - if !types_structurally_equivalent(stack, mapping_key_type, stack, &key_type)? { + if !types_equivalent(stack, mapping_key_type, stack, &key_type)? { bail!("Key type in `remove` '{key_type}' does not match the key type in the mapping '{mapping_key_type}'.") } Ok(()) diff --git a/synthesizer/process/src/stack/finalize_types/matches.rs b/synthesizer/process/src/stack/finalize_types/matches.rs index 073513788b..8a7175da95 100644 --- a/synthesizer/process/src/stack/finalize_types/matches.rs +++ b/synthesizer/process/src/stack/finalize_types/matches.rs @@ -45,7 +45,7 @@ impl FinalizeTypes { // Ensure the literal type matches the member type. Operand::Literal(literal) => { ensure!( - // No need to call `types_structurally_equivalent`, since it can't be a struct. + // No need to call `types_equivalent`, since it can't be a struct. &PlaintextType::Literal(literal.to_type()) == member_type, "Struct member '{struct_name}.{member_name}' expects a {member_type}, but found '{operand}' in the operand.", ) @@ -61,7 +61,7 @@ impl FinalizeTypes { }; // Ensure the register type matches the member type. ensure!( - types_structurally_equivalent(stack, &plaintext_type, stack, member_type)?, + types_equivalent(stack, &plaintext_type, stack, member_type)?, "Struct member '{struct_name}.{member_name}' expects {member_type}, but found '{plaintext_type}' in the operand '{operand}'.", ) } @@ -80,7 +80,7 @@ impl FinalizeTypes { }; // Ensure the operand type matches the member type. ensure!( - // No need to call `types_structurally_equivalent`, since `program_ref_type` cannot be a struct. + // No need to call `types_equivalent`, since `program_ref_type` cannot be a struct. &program_ref_type == member_type, "Struct member '{struct_name}.{member_name}' expects {member_type}, but found '{program_ref_type}' in the operand '{operand}'.", ) @@ -122,7 +122,7 @@ impl FinalizeTypes { // Ensure the literal type matches the element type. Operand::Literal(literal) => { ensure!( - // No need to call `types_structurally_equivalent`, since it can't be a struct. + // No need to call `types_equivalent`, since it can't be a struct. &PlaintextType::Literal(literal.to_type()) == array_type.next_element_type(), "Array element expects {}, but found '{operand}' in the operand.", array_type.next_element_type() @@ -139,7 +139,7 @@ impl FinalizeTypes { }; // Ensure the register type matches the element type. ensure!( - types_structurally_equivalent(stack, &plaintext_type, stack, array_type.next_element_type())?, + types_equivalent(stack, &plaintext_type, stack, array_type.next_element_type())?, "Array element expects {}, but found '{plaintext_type}' in the operand '{operand}'.", array_type.next_element_type() ) @@ -157,7 +157,7 @@ impl FinalizeTypes { }; // Ensure the operand type matches the element type. ensure!( - // No need to call `types_structurally_equivalent`, since `program_ref_type` cannot be a struct. + // No need to call `types_equivalent`, since `program_ref_type` cannot be a struct. &program_ref_type == array_type.next_element_type(), "Array element expects {}, but found '{program_ref_type}' in the operand '{operand}'.", array_type.next_element_type() diff --git a/synthesizer/process/src/stack/finalize_types/mod.rs b/synthesizer/process/src/stack/finalize_types/mod.rs index 3d5320287f..e7f8e99983 100644 --- a/synthesizer/process/src/stack/finalize_types/mod.rs +++ b/synthesizer/process/src/stack/finalize_types/mod.rs @@ -53,7 +53,7 @@ use snarkvm_synthesizer_program::{ Remove, Set, StackTrait, - types_structurally_equivalent, + types_equivalent, }; use indexmap::IndexMap; @@ -221,7 +221,7 @@ impl FinalizeTypes { } } -pub fn finalize_types_structurally_equivalent( +pub fn finalize_types_equivalent( stack0: &impl StackTrait, type0: &FinalizeType, stack1: &impl StackTrait, @@ -229,7 +229,7 @@ pub fn finalize_types_structurally_equivalent( ) -> Result { match (type0, type1) { (FinalizeType::Plaintext(plaintext0), FinalizeType::Plaintext(plaintext1)) => { - types_structurally_equivalent(stack0, plaintext0, stack1, plaintext1) + types_equivalent(stack0, plaintext0, stack1, plaintext1) } (FinalizeType::Future(future0), FinalizeType::Future(future1)) => Ok(future0 == future1), _ => Ok(false), diff --git a/synthesizer/process/src/stack/register_types/initialize.rs b/synthesizer/process/src/stack/register_types/initialize.rs index 548341e0c7..7a2cd4384c 100644 --- a/synthesizer/process/src/stack/register_types/initialize.rs +++ b/synthesizer/process/src/stack/register_types/initialize.rs @@ -330,7 +330,7 @@ impl RegisterTypes { // Ensure the operand type and the output type match. let operand_type = self.get_type_from_operand(stack, operand)?; - if !register_types_structurally_equivalent(stack, register_type, stack, &operand_type)? { + if !register_types_equivalent(stack, register_type, stack, &operand_type)? { bail!( "Output '{operand}' does not match the expected output operand type: expected '{}', found '{}'", operand_type, diff --git a/synthesizer/process/src/stack/register_types/matches.rs b/synthesizer/process/src/stack/register_types/matches.rs index 8c41aefac5..b69d078e57 100644 --- a/synthesizer/process/src/stack/register_types/matches.rs +++ b/synthesizer/process/src/stack/register_types/matches.rs @@ -64,7 +64,7 @@ impl RegisterTypes { // Ensure the register type matches the member type. RegisterType::Plaintext(type_) => { ensure!( - types_structurally_equivalent(stack, &type_, stack, member_type)?, + types_equivalent(stack, &type_, stack, member_type)?, "Struct entry '{struct_name}.{member_name}' expects a '{member_type}', but found '{type_}' in the operand '{operand}'.", ) } @@ -80,7 +80,7 @@ impl RegisterTypes { }; // Ensure the operand type matches the member type. ensure!( - types_structurally_equivalent(stack, &operand_type, stack, member_type)?, + types_equivalent(stack, &operand_type, stack, member_type)?, "Struct member '{struct_name}.{member_name}' expects {member_type}, but found '{operand_type}' in the operand '{operand}'.", ) } @@ -159,7 +159,7 @@ impl RegisterTypes { // Ensure the register type matches the element type. RegisterType::Plaintext(type_) => { ensure!( - types_structurally_equivalent(stack, &type_, stack, array_type.next_element_type())?, + types_equivalent(stack, &type_, stack, array_type.next_element_type())?, "Array element expects a '{}', but found '{type_}' in the operand '{operand}'.", array_type.next_element_type() ) @@ -174,7 +174,7 @@ impl RegisterTypes { }; // Ensure the operand type matches the element type. ensure!( - types_structurally_equivalent(stack, &operand_type, stack, array_type.next_element_type())?, + types_equivalent(stack, &operand_type, stack, array_type.next_element_type())?, "Array element expects {}, but found '{operand_type}' in the operand '{operand}'.", array_type.next_element_type() ) @@ -297,7 +297,7 @@ impl RegisterTypes { // Ensure the register type matches the entry type. RegisterType::Plaintext(type_) => { ensure!( - types_structurally_equivalent(stack, &type_, stack, plaintext_type)?, + types_equivalent(stack, &type_, stack, plaintext_type)?, "Record entry '{record_name}.{entry_name}' expects a '{plaintext_type}', but found '{type_}' in the operand '{operand}'.", ) } @@ -314,7 +314,7 @@ impl RegisterTypes { }; // Ensure the operand type matches the entry type. ensure!( - types_structurally_equivalent(stack, &operand_type, stack, plaintext_type)?, + types_equivalent(stack, &operand_type, stack, 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 ee7122c599..74c8999ad7 100644 --- a/synthesizer/process/src/stack/register_types/mod.rs +++ b/synthesizer/process/src/stack/register_types/mod.rs @@ -46,8 +46,8 @@ use snarkvm_synthesizer_program::{ Operand, Program, StackTrait, - register_types_structurally_equivalent, - types_structurally_equivalent, + register_types_equivalent, + types_equivalent, }; use snarkvm_utilities::dev_eprintln; diff --git a/synthesizer/program/src/logic/instruction/operation/assert.rs b/synthesizer/program/src/logic/instruction/operation/assert.rs index 615f463068..77a0284214 100644 --- a/synthesizer/program/src/logic/instruction/operation/assert.rs +++ b/synthesizer/program/src/logic/instruction/operation/assert.rs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::{Opcode, Operand, RegistersCircuit, RegistersTrait, StackTrait, register_types_structurally_equivalent}; +use crate::{Opcode, Operand, RegistersCircuit, RegistersTrait, StackTrait, register_types_equivalent}; use console::{ network::prelude::*, program::{Register, RegisterType}, @@ -142,7 +142,7 @@ impl AssertInstruction { bail!("Instruction '{}' expects 2 inputs, found {} inputs", Self::opcode(), input_types.len()) } // Ensure the operands are of the same type. - if !register_types_structurally_equivalent(stack, &input_types[0], stack, &input_types[1])? { + if !register_types_equivalent(stack, &input_types[0], stack, &input_types[1])? { bail!( "Instruction '{}' expects inputs of the same type. Found inputs of type '{}' and '{}'", Self::opcode(), diff --git a/synthesizer/program/src/logic/instruction/operation/async_.rs b/synthesizer/program/src/logic/instruction/operation/async_.rs index 1ce0b614f3..113bc8e564 100644 --- a/synthesizer/program/src/logic/instruction/operation/async_.rs +++ b/synthesizer/program/src/logic/instruction/operation/async_.rs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::{Opcode, Operand, RegistersCircuit, RegistersTrait, Result, StackTrait, types_structurally_equivalent}; +use crate::{Opcode, Operand, RegistersCircuit, RegistersTrait, Result, StackTrait, types_equivalent}; use circuit::{Inject, Mode}; use console::{ @@ -159,7 +159,7 @@ impl Async { match (input_type, finalize_type) { (RegisterType::Plaintext(input_type), FinalizeType::Plaintext(finalize_type)) => { ensure!( - types_structurally_equivalent(stack, input_type, stack, &finalize_type)?, + types_equivalent(stack, input_type, stack, &finalize_type)?, "'{}/{}' finalize expects a '{}' argument, found a '{}' argument", stack.program_id(), self.function_name(), diff --git a/synthesizer/program/src/logic/instruction/operation/cast.rs b/synthesizer/program/src/logic/instruction/operation/cast.rs index 3c4397efbf..0c6ad3978d 100644 --- a/synthesizer/program/src/logic/instruction/operation/cast.rs +++ b/synthesizer/program/src/logic/instruction/operation/cast.rs @@ -20,7 +20,7 @@ use crate::{ RegistersSigner, RegistersTrait, StackTrait, - types_structurally_equivalent, + types_equivalent, }; use console::{ network::prelude::*, @@ -668,7 +668,7 @@ impl CastOperation { // Ensure the plaintext type matches the member type. RegisterType::Plaintext(plaintext_type) => { ensure!( - types_structurally_equivalent(stack, member_type, stack, plaintext_type,)?, + types_equivalent(stack, member_type, stack, plaintext_type,)?, "Struct '{struct_name}' member type mismatch: expected '{member_type}', found '{plaintext_type}'" ) } @@ -739,7 +739,7 @@ impl CastOperation { // Ensure the plaintext type matches the member type. RegisterType::Plaintext(plaintext_type) => { ensure!( - types_structurally_equivalent( + types_equivalent( stack, plaintext_type, stack, diff --git a/synthesizer/program/src/logic/instruction/operation/is.rs b/synthesizer/program/src/logic/instruction/operation/is.rs index c543cbaed8..f42435b4e2 100644 --- a/synthesizer/program/src/logic/instruction/operation/is.rs +++ b/synthesizer/program/src/logic/instruction/operation/is.rs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::{Opcode, Operand, RegistersCircuit, RegistersTrait, StackTrait, register_types_structurally_equivalent}; +use crate::{Opcode, Operand, RegistersCircuit, RegistersTrait, StackTrait, register_types_equivalent}; use console::{ network::prelude::*, program::{Literal, LiteralType, Plaintext, PlaintextType, Register, RegisterType, Value}, @@ -139,7 +139,7 @@ impl IsInstruction { bail!("Instruction '{}' expects 2 inputs, found {} inputs", Self::opcode(), input_types.len()) } // Ensure the operands are of the same type. - if !register_types_structurally_equivalent(stack, &input_types[0], stack, &input_types[1])? { + if !register_types_equivalent(stack, &input_types[0], stack, &input_types[1])? { bail!( "Instruction '{}' expects inputs of the same type. Found inputs of type '{}' and '{}'", Self::opcode(), diff --git a/synthesizer/program/src/traits/stack_and_registers.rs b/synthesizer/program/src/traits/stack_and_registers.rs index 22fe6db5fa..209a5474d0 100644 --- a/synthesizer/program/src/traits/stack_and_registers.rs +++ b/synthesizer/program/src/traits/stack_and_registers.rs @@ -152,7 +152,7 @@ pub trait StackTrait { } /// Are the two types either the same, or both structurally equivalent `PlaintextType`s? -pub fn register_types_structurally_equivalent( +pub fn register_types_equivalent( stack0: &impl StackTrait, type0: &RegisterType, stack1: &impl StackTrait, @@ -160,22 +160,35 @@ pub fn register_types_structurally_equivalent( ) -> Result { use RegisterType::*; if let (Plaintext(plaintext0), Plaintext(plaintext1)) = (type0, type1) { - types_structurally_equivalent(stack0, plaintext0, stack1, plaintext1) + types_equivalent(stack0, plaintext0, stack1, plaintext1) } else { Ok(type0 == type1) } } -/// Determines whether two `PlaintextType` values are structurally equivalent. +/// Determines whether two `PlaintextType` values are equivalent. /// -/// Structural equivalence means that the types have the same shape and member layout, -/// but not necessarily the same identity. For example, two struct types with the same -/// member names and types (recursively, ordered) are considered equivalent, -/// even if they are declared separately or come from different programs. +/// Equivalence of literals means they're the same type. +/// +/// Equivalence of structs means they have the same local names (regardless of whether +/// they're local or external), and their members have the same names and equivalent +/// types in the same order, recursively. +/// +/// Equivalence of arrays means they have the same length and their element types are +/// equivalent. +/// +/// This definition of equivalence was chosen to balance these concerns: +/// +/// 1. All programs from before the existence of external structs will continue to work - +/// thus it's necessary for a struct created from another program to be considered equivalent +/// to a local one with the same name and structure, as in practice that was the behavior. +/// 2. We don't want to allow a fork. Thus we do need to check names, not just structural +/// equivalence - otherwise we could get a program deployable to a node which is using +/// this check, but not deployable to a node running an earlier SnarkVM. /// /// The stacks are passed because struct types need to access their stack to get their /// structure. -pub fn types_structurally_equivalent( +pub fn types_equivalent( stack0: &impl StackTrait, type0: &PlaintextType, stack1: &impl StackTrait, @@ -189,7 +202,7 @@ pub fn types_structurally_equivalent( } for ((name0, type0), (name1, type1)) in st0.members().iter().zip(st1.members()) { - if name0 != name1 || !types_structurally_equivalent(stack0, type0, stack1, type1)? { + if name0 != name1 || !types_equivalent(stack0, type0, stack1, type1)? { return Ok(false); } } @@ -199,14 +212,20 @@ pub fn types_structurally_equivalent( match (type0, type1) { (Array(array0), Array(array1)) => Ok(array0.length() == array1.length() - && types_structurally_equivalent(stack0, array0.next_element_type(), stack1, array1.next_element_type())?), + && types_equivalent(stack0, array0.next_element_type(), stack1, array1.next_element_type())?), (Literal(lit0), Literal(lit1)) => Ok(lit0 == lit1), (Struct(id0), Struct(id1)) => { + if id0 != id1 { + return Ok(false); + } let struct_type0 = stack0.program().get_struct(id0)?; let struct_type1 = stack1.program().get_struct(id1)?; struct_compare(stack0, struct_type0, stack1, struct_type1) } (ExternalStruct(loc0), ExternalStruct(loc1)) => { + if loc0.resource() != loc1.resource() { + return Ok(false); + } let external_stack0 = stack0.get_external_stack(loc0.program_id())?; let struct_type0 = external_stack0.program().get_struct(loc0.resource())?; let external_stack1 = stack1.get_external_stack(loc1.program_id())?; @@ -214,12 +233,18 @@ pub fn types_structurally_equivalent( struct_compare(&*external_stack0, struct_type0, &*external_stack1, struct_type1) } (ExternalStruct(loc), Struct(id)) => { + if loc.resource() != id { + return Ok(false); + } let external_stack = stack0.get_external_stack(loc.program_id())?; let struct_type0 = external_stack.program().get_struct(loc.resource())?; let struct_type1 = stack1.program().get_struct(id)?; struct_compare(&*external_stack, struct_type0, stack1, struct_type1) } (Struct(id), ExternalStruct(loc)) => { + if id != loc.resource() { + return Ok(false); + } let struct_type0 = stack0.program().get_struct(id)?; let external_stack = stack1.get_external_stack(loc.program_id())?; let struct_type1 = external_stack.program().get_struct(loc.resource())?; diff --git a/synthesizer/src/vm/tests/mod.rs b/synthesizer/src/vm/tests/mod.rs index ec990bda48..95809eae29 100644 --- a/synthesizer/src/vm/tests/mod.rs +++ b/synthesizer/src/vm/tests/mod.rs @@ -22,5 +22,8 @@ mod test_v9; #[cfg(feature = "test")] mod test_v10; +#[cfg(feature = "test")] +mod test_v11; + #[cfg(feature = "test")] use super::*; diff --git a/synthesizer/src/vm/tests/test_v11.rs b/synthesizer/src/vm/tests/test_v11.rs new file mode 100644 index 0000000000..6c48ec6836 --- /dev/null +++ b/synthesizer/src/vm/tests/test_v11.rs @@ -0,0 +1,105 @@ +// 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::*; + +use crate::vm::test_helpers::*; + +use snarkvm_synthesizer_program::Program; + +use console::network::ConsensusVersion; +use snarkvm_utilities::TestRng; + +// This test verifies that a program with external structs cannot be deployed on +// consensus version 9. +#[test] +fn test_deploy_external_structs_v9() { + // Use V9 rather than V10 to make sure we still won't be on V11 + // when deploying the second program. + let block = deploy_programs(ConsensusVersion::V9); + + assert_eq!(block.transactions().num_accepted(), 0); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 1); +} + +// This test verifies that a program with external structs can be deployed on +// consensus version 11. +#[test] +fn test_deploy_external_structs_v11() { + let block = deploy_programs(ConsensusVersion::V11); + + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); +} + +fn deploy_programs(consensus_version: ConsensusVersion) -> Block { + let rng = &mut TestRng::default(); + + // Initialize a new caller. + let caller_private_key = crate::vm::test_helpers::sample_genesis_private_key(rng); + + // Initialize the VM at the correct height. + let v10_height = CurrentNetwork::CONSENSUS_HEIGHT(consensus_version).unwrap(); + let vm = crate::vm::test_helpers::sample_vm_at_height(v10_height, rng); + + // Define the first program with a record. + let program_one = Program::from_str( + r" +program test_one.aleo; + +constructor: + assert.eq true true; + +struct S: + x as field; + +function make_s: + cast 0field into r0 as S; + output r0 as S.public; +", + ) + .unwrap(); + + // Define the second program which refers to the external struct type. + let program_two = Program::from_str( + r" +import test_one.aleo; + +program test_two.aleo; + +constructor: + assert.eq true true; + +function second: + call test_one.aleo/make_s into r0; + output r0 as test_one.aleo/S.public; +", + ) + .unwrap(); + + // Deploy the first program. + let deployment_one = vm.deploy(&caller_private_key, &program_one, None, 0, None, rng).unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[deployment_one], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block).unwrap(); + + // Deploy the second program. + let deployment_two = vm.deploy(&caller_private_key, &program_two, None, 0, None, rng).unwrap(); + sample_next_block(&vm, &caller_private_key, &[deployment_two], rng).unwrap() +} From 1204947412c121e6d1bcbad0067f0c4c4536d25d Mon Sep 17 00:00:00 2001 From: Michael Benfield Date: Thu, 25 Sep 2025 12:07:28 -0700 Subject: [PATCH 22/25] Review comments. - external_struct_fail_name test - `plaintext_exists` moved to `Process` as `mapping_types_exist`. --- synthesizer/process/src/lib.rs | 37 +++++++++++++++++++ synthesizer/src/vm/verify.rs | 36 +----------------- .../execute/external_struct_fail_name.out | 3 ++ .../process/execute/external_struct.aleo | 14 +++---- .../execute/external_struct_fail_name.aleo | 27 ++++++++++++++ 5 files changed, 76 insertions(+), 41 deletions(-) create mode 100644 synthesizer/tests/expectations/process/execute/external_struct_fail_name.out create mode 100644 synthesizer/tests/tests/process/execute/external_struct_fail_name.aleo diff --git a/synthesizer/process/src/lib.rs b/synthesizer/process/src/lib.rs index e725ac0585..da7c2f088d 100644 --- a/synthesizer/process/src/lib.rs +++ b/synthesizer/process/src/lib.rs @@ -51,6 +51,7 @@ use console::{ Literal, Locator, Plaintext, + PlaintextType, ProgramID, Record, Request, @@ -182,6 +183,42 @@ impl Process { } } } + + /// Ensure that the types referred to in this program's mappings exist. + pub fn mapping_types_exist(&self, program: &Program) -> Result<()> { + for mapping in program.mappings().values() { + self.plaintext_exists(mapping.key().plaintext_type(), program)?; + self.plaintext_exists(mapping.value().plaintext_type(), program)?; + } + Ok(()) + } + + // If `type_` is a struct or an array containing a struct, ensure the struct type exists. + fn plaintext_exists(&self, type_: &PlaintextType, program: &Program) -> Result<()> { + match type_ { + PlaintextType::Literal(..) => Ok(()), + PlaintextType::Struct(struct_name) => { + // Retrieve the struct from the program. + ensure!( + program.get_struct(struct_name).is_ok(), + "Struct '{struct_name}' in '{}' is not defined.", + program.id() + ); + Ok(()) + } + PlaintextType::ExternalStruct(locator) => { + let stack = self.get_stack(locator.program_id())?; + ensure!( + stack.program().get_struct(locator.resource()).is_ok(), + "Struct '{}' in '{}' is not defined.", + locator.resource(), + stack.program().id(), + ); + Ok(()) + } + PlaintextType::Array(array_type) => self.plaintext_exists(array_type.base_element_type(), program), + } + } } impl Process { diff --git a/synthesizer/src/vm/verify.rs b/synthesizer/src/vm/verify.rs index 8db1dd2598..0dbf7078de 100644 --- a/synthesizer/src/vm/verify.rs +++ b/synthesizer/src/vm/verify.rs @@ -13,8 +13,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -use console::program::PlaintextType; - use super::*; /// Ensures the given iterator has no duplicate elements, and that the ledger @@ -458,6 +456,7 @@ impl> VM { // - the program owner is present in the deployment // If the `CONSENSUS_VERSION` is less than `V11`, then verify that: // - the program does not use the external struct syntax `some_program.aleo/StructT` + // If the `CONSENSUS_VERSION` is greater than or equal to `V11`, then verify that: // - the program's mappings do not use non-existent structs. if consensus_version < ConsensusVersion::V8 { ensure!( @@ -501,42 +500,11 @@ impl> VM { } if consensus_version >= ConsensusVersion::V11 { - for mapping in deployment.program().mappings().values() { - // These calls make sure structs exist. - self.plaintext_exists(mapping.key().plaintext_type(), deployment.program())?; - self.plaintext_exists(mapping.value().plaintext_type(), deployment.program())?; - } + self.process.read().mapping_types_exist(deployment.program())?; } Ok(()) } - - // If `type_` is a struct or an array containing a struct, ensure the struct type exists. - fn plaintext_exists(&self, type_: &PlaintextType, program: &Program) -> Result<()> { - match type_ { - PlaintextType::Literal(..) => Ok(()), - PlaintextType::Struct(struct_name) => { - // Retrieve the struct from the program. - ensure!( - program.get_struct(struct_name).is_ok(), - "Struct '{struct_name}' in '{}' is not defined.", - program.id() - ); - Ok(()) - } - PlaintextType::ExternalStruct(locator) => { - let stack = self.process.read().get_stack(locator.program_id())?; - ensure!( - stack.program().get_struct(locator.resource()).is_ok(), - "Struct '{}' in '{}' is not defined.", - locator.resource(), - stack.program().id(), - ); - Ok(()) - } - PlaintextType::Array(array_type) => self.plaintext_exists(array_type.base_element_type(), program), - } - } } impl> VM { diff --git a/synthesizer/tests/expectations/process/execute/external_struct_fail_name.out b/synthesizer/tests/expectations/process/execute/external_struct_fail_name.out new file mode 100644 index 0000000000..c04e858cf1 --- /dev/null +++ b/synthesizer/tests/expectations/process/execute/external_struct_fail_name.out @@ -0,0 +1,3 @@ +errors: +- 'Output ''r0'' does not match the expected output operand type: expected ''external_struct_fail_parent.aleo/S'', found ''T''' +outputs: [] diff --git a/synthesizer/tests/tests/process/execute/external_struct.aleo b/synthesizer/tests/tests/process/execute/external_struct.aleo index e23e38424d..5ac3d9d8c1 100644 --- a/synthesizer/tests/tests/process/execute/external_struct.aleo +++ b/synthesizer/tests/tests/process/execute/external_struct.aleo @@ -65,12 +65,12 @@ import external_struct_parent.aleo; program child.aleo; -struct T: +struct S: x as field; -struct OuterT: +struct Outer: x as field; - s as T; + s as S; struct Outer2: s as external_struct_parent.aleo/S; @@ -81,15 +81,15 @@ function make_s: function make_t: call external_struct_parent.aleo/make_s into r0; - output r0 as T.public; + output r0 as S.public; function call_make_outer: call external_struct_parent.aleo/make_outer 3field into r0; - output r0 as OuterT.public; + output r0 as Outer.public; function call_make_array: call external_struct_parent.aleo/make_array 4field into r0; - output r0 as [T; 2u32].public; + output r0 as [S; 2u32].public; function pass_s: cast 1field into r0 as external_struct_parent.aleo/S; @@ -97,7 +97,7 @@ function pass_s: output r1 as field.public; function pass_t: - cast 2field into r0 as T; + cast 2field into r0 as S; call external_struct_parent.aleo/accept_s r0 into r1; output r1 as field.public; diff --git a/synthesizer/tests/tests/process/execute/external_struct_fail_name.aleo b/synthesizer/tests/tests/process/execute/external_struct_fail_name.aleo new file mode 100644 index 0000000000..716de8b55d --- /dev/null +++ b/synthesizer/tests/tests/process/execute/external_struct_fail_name.aleo @@ -0,0 +1,27 @@ +/* +randomness: 45791624 +cases: [] +*/ + +program external_struct_fail_parent.aleo; + +struct S: + x as field; + +function make_s: + cast 12field into r0 as S; + output r0 as S.public; + +///////////////////////////////////////////////// + +import external_struct_fail_parent.aleo; + +program external_struct_fail_child.aleo; + +struct T: + x as field; + +function make_s: + call external_struct_fail_parent.aleo/make_s into r0; + // This should fail as the struct names don't match. + output r0 as T.public; From b5826a2564b0f10a76c8d5b6856d591cdd1c39c4 Mon Sep 17 00:00:00 2001 From: Michael Benfield Date: Mon, 29 Sep 2025 20:18:35 -0700 Subject: [PATCH 23/25] Fix external_struct_in_mapping test --- .../src/stack/register_types/initialize.rs | 4 +--- .../src/logic/instruction/operation/cast.rs | 17 ++--------------- .../program/src/traits/stack_and_registers.rs | 10 +++++----- .../external_struct_in_mapping.out | 10 +++++----- .../external_struct_in_mapping.aleo | 14 +++++++------- 5 files changed, 20 insertions(+), 35 deletions(-) diff --git a/synthesizer/process/src/stack/register_types/initialize.rs b/synthesizer/process/src/stack/register_types/initialize.rs index 7a2cd4384c..cd3efa5e42 100644 --- a/synthesizer/process/src/stack/register_types/initialize.rs +++ b/synthesizer/process/src/stack/register_types/initialize.rs @@ -332,9 +332,7 @@ impl RegisterTypes { let operand_type = self.get_type_from_operand(stack, operand)?; if !register_types_equivalent(stack, register_type, stack, &operand_type)? { bail!( - "Output '{operand}' does not match the expected output operand type: expected '{}', found '{}'", - operand_type, - register_type + "Output '{operand}' does not match the expected output operand type: expected '{operand_type}', found '{register_type}'", ) } Ok(()) diff --git a/synthesizer/program/src/logic/instruction/operation/cast.rs b/synthesizer/program/src/logic/instruction/operation/cast.rs index 0c6ad3978d..4e1bc0c63a 100644 --- a/synthesizer/program/src/logic/instruction/operation/cast.rs +++ b/synthesizer/program/src/logic/instruction/operation/cast.rs @@ -13,15 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::{ - Opcode, - Operand, - RegistersCircuit, - RegistersSigner, - RegistersTrait, - StackTrait, - types_equivalent, -}; +use crate::{Opcode, Operand, RegistersCircuit, RegistersSigner, RegistersTrait, StackTrait, types_equivalent}; use console::{ network::prelude::*, program::{ @@ -739,12 +731,7 @@ impl CastOperation { // Ensure the plaintext type matches the member type. RegisterType::Plaintext(plaintext_type) => { ensure!( - types_equivalent( - stack, - plaintext_type, - stack, - array_type.next_element_type() - )?, + types_equivalent(stack, plaintext_type, stack, array_type.next_element_type())?, "Array element type mismatch: expected '{}', found '{plaintext_type}'", array_type.next_element_type() ) diff --git a/synthesizer/program/src/traits/stack_and_registers.rs b/synthesizer/program/src/traits/stack_and_registers.rs index 209a5474d0..99e74bc52c 100644 --- a/synthesizer/program/src/traits/stack_and_registers.rs +++ b/synthesizer/program/src/traits/stack_and_registers.rs @@ -179,12 +179,12 @@ pub fn register_types_equivalent( /// /// This definition of equivalence was chosen to balance these concerns: /// -/// 1. All programs from before the existence of external structs will continue to work - -/// thus it's necessary for a struct created from another program to be considered equivalent -/// to a local one with the same name and structure, as in practice that was the behavior. +/// 1. All programs from before the existence of external structs will continue to work; +/// thus it's necessary for a struct created from another program to be considered equivalent +/// to a local one with the same name and structure, as in practice that was the behavior. /// 2. We don't want to allow a fork. Thus we do need to check names, not just structural -/// equivalence - otherwise we could get a program deployable to a node which is using -/// this check, but not deployable to a node running an earlier SnarkVM. +/// equivalence - otherwise we could get a program deployable to a node which is using +/// this check, but not deployable to a node running an earlier SnarkVM. /// /// The stacks are passed because struct types need to access their stack to get their /// structure. diff --git a/synthesizer/tests/expectations/vm/execute_and_finalize/external_struct_in_mapping.out b/synthesizer/tests/expectations/vm/execute_and_finalize/external_struct_in_mapping.out index 33f64bad8d..c6dfd70a5e 100644 --- a/synthesizer/tests/expectations/vm/execute_and_finalize/external_struct_in_mapping.out +++ b/synthesizer/tests/expectations/vm/execute_and_finalize/external_struct_in_mapping.out @@ -39,29 +39,29 @@ additional: - child_outputs: credits.aleo/fee_public: outputs: - - '{"type":"future","id":"5931083473252536185719147680329323401761022184518569458483931692885063239858field","value":"{\n program_id: credits.aleo,\n function_name: fee_public,\n arguments: [\n aleo1qr2ha4pfs5l28aze88yn6fhleeythklkczrule2v838uwj65n5gqxt9djx,\n 17842u64\n ]\n}"}' + - '{"type":"future","id":"1258946943149191346174440740558254412317036443743799871691007628548951010151field","value":"{\n program_id: credits.aleo,\n function_name: fee_public,\n arguments: [\n aleo1qr2ha4pfs5l28aze88yn6fhleeythklkczrule2v838uwj65n5gqxt9djx,\n 1906u64\n ]\n}"}' - child_outputs: child.aleo/make_s: outputs: - '{"type":"public","id":"6985903130429112305577914343133100080684294095291138773079739129624829511258field","value":"{\n x: 12field\n}"}' credits.aleo/fee_public: outputs: - - '{"type":"future","id":"6423060729030407108337685286277924679730527590289056343090240277713705336980field","value":"{\n program_id: credits.aleo,\n function_name: fee_public,\n arguments: [\n aleo1qr2ha4pfs5l28aze88yn6fhleeythklkczrule2v838uwj65n5gqxt9djx,\n 18554u64\n ]\n}"}' + - '{"type":"future","id":"246687940996289351695445329752398582117713847287771695321740966976889183612field","value":"{\n program_id: credits.aleo,\n function_name: fee_public,\n arguments: [\n aleo1qr2ha4pfs5l28aze88yn6fhleeythklkczrule2v838uwj65n5gqxt9djx,\n 2618u64\n ]\n}"}' - child_outputs: child.aleo/make_s: outputs: - '{"type":"public","id":"7574377799660344748771224282773834068174701455625760560698920840088755673712field","value":"{\n x: 12field\n}"}' credits.aleo/fee_public: outputs: - - '{"type":"future","id":"2645278696162344307543607738900689987512504974551459357913512732894441614319field","value":"{\n program_id: credits.aleo,\n function_name: fee_public,\n arguments: [\n aleo1qr2ha4pfs5l28aze88yn6fhleeythklkczrule2v838uwj65n5gqxt9djx,\n 20084u64\n ]\n}"}' + - '{"type":"future","id":"5440840329189881100372243535086981229911786836863090756813358008537885548138field","value":"{\n program_id: credits.aleo,\n function_name: fee_public,\n arguments: [\n aleo1qr2ha4pfs5l28aze88yn6fhleeythklkczrule2v838uwj65n5gqxt9djx,\n 2688u64\n ]\n}"}' - child_outputs: child.aleo/make_s: outputs: - '{"type":"public","id":"5642997165471718018783739778741660536733800233179096103171448587842404180286field","value":"{\n x: 12field\n}"}' credits.aleo/fee_public: outputs: - - '{"type":"future","id":"6458128924800203602226324183919154340534363938397245013050876831071587836128field","value":"{\n program_id: credits.aleo,\n function_name: fee_public,\n arguments: [\n aleo1qr2ha4pfs5l28aze88yn6fhleeythklkczrule2v838uwj65n5gqxt9djx,\n 18556u64\n ]\n}"}' + - '{"type":"future","id":"7991640413705189295055666104444253675861288502787109993559864263765680782635field","value":"{\n program_id: credits.aleo,\n function_name: fee_public,\n arguments: [\n aleo1qr2ha4pfs5l28aze88yn6fhleeythklkczrule2v838uwj65n5gqxt9djx,\n 2620u64\n ]\n}"}' - child_outputs: credits.aleo/fee_public: outputs: - - '{"type":"future","id":"1448272572280864368327708519450049907934875151862273228624942884364036863439field","value":"{\n program_id: credits.aleo,\n function_name: fee_public,\n arguments: [\n aleo1qr2ha4pfs5l28aze88yn6fhleeythklkczrule2v838uwj65n5gqxt9djx,\n 5338u64\n ]\n}"}' + - '{"type":"future","id":"4087075296851859690375077386496570802162611548430680128084524488091740020645field","value":"{\n program_id: credits.aleo,\n function_name: fee_public,\n arguments: [\n aleo1qr2ha4pfs5l28aze88yn6fhleeythklkczrule2v838uwj65n5gqxt9djx,\n 1363u64\n ]\n}"}' diff --git a/synthesizer/tests/tests/vm/execute_and_finalize/external_struct_in_mapping.aleo b/synthesizer/tests/tests/vm/execute_and_finalize/external_struct_in_mapping.aleo index 91d918416e..686c42ce30 100644 --- a/synthesizer/tests/tests/vm/execute_and_finalize/external_struct_in_mapping.aleo +++ b/synthesizer/tests/tests/vm/execute_and_finalize/external_struct_in_mapping.aleo @@ -37,32 +37,32 @@ import child.aleo; program parent.aleo; -struct T: +struct S: x as field; mapping foo: key as field.public; - value as T.public; + value as S.public; mapping bar: key as field.public; value as child.aleo/S.public; mapping baz: - key as T.public; + key as S.public; value as field.public; closure make_t: input r0 as field; - cast r0 into r1 as T; - output r1 as T; + cast r0 into r1 as S; + output r1 as S; function store_t: call make_t 12field into r0; async store_t r0 into r1; output r1 as parent.aleo/store_t.future; finalize store_t: - input r0 as T.public; + input r0 as S.public; set r0 into foo[0field]; function store_s: @@ -79,7 +79,7 @@ function store_s_as_t: output r1 as parent.aleo/store_s_as_t.future; finalize store_s_as_t: input r0 as child.aleo/S.public; - cast r0.x into r1 as T; + cast r0.x into r1 as S; set r1 into foo[2field]; function baz_keys: From 7a16a805ef59a3851381b547cf6e4c119e37dfa2 Mon Sep 17 00:00:00 2001 From: Mohammad Fawaz Date: Tue, 4 Nov 2025 09:51:02 -0500 Subject: [PATCH 24/25] Fix a few things --- synthesizer/src/vm/tests/mod.rs | 3 + synthesizer/src/vm/tests/test_v12.rs | 105 +++++++++++++++++++++++++++ synthesizer/src/vm/verify.rs | 91 ++++------------------- 3 files changed, 123 insertions(+), 76 deletions(-) create mode 100644 synthesizer/src/vm/tests/test_v12.rs diff --git a/synthesizer/src/vm/tests/mod.rs b/synthesizer/src/vm/tests/mod.rs index 95809eae29..361f46e0c8 100644 --- a/synthesizer/src/vm/tests/mod.rs +++ b/synthesizer/src/vm/tests/mod.rs @@ -25,5 +25,8 @@ mod test_v10; #[cfg(feature = "test")] mod test_v11; +#[cfg(feature = "test")] +mod test_v12; + #[cfg(feature = "test")] use super::*; diff --git a/synthesizer/src/vm/tests/test_v12.rs b/synthesizer/src/vm/tests/test_v12.rs new file mode 100644 index 0000000000..c7b2577d85 --- /dev/null +++ b/synthesizer/src/vm/tests/test_v12.rs @@ -0,0 +1,105 @@ +// 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::*; + +use crate::vm::test_helpers::*; + +use snarkvm_synthesizer_program::Program; + +use console::network::ConsensusVersion; +use snarkvm_utilities::TestRng; + +// This test verifies that a program with external structs cannot be deployed on +// consensus version 9. +#[test] +fn test_deploy_external_structs_v10() { + // Use V10 rather than V11 to make sure we still won't be on V12 + // when deploying the second program. + let block = deploy_programs(ConsensusVersion::V10); + + assert_eq!(block.transactions().num_accepted(), 0); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 1); +} + +// This test verifies that a program with external structs can be deployed on +// consensus version 12. +#[test] +fn test_deploy_external_structs_v12() { + let block = deploy_programs(ConsensusVersion::V12); + + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); +} + +fn deploy_programs(consensus_version: ConsensusVersion) -> Block { + let rng = &mut TestRng::default(); + + // Initialize a new caller. + let caller_private_key = crate::vm::test_helpers::sample_genesis_private_key(rng); + + // Initialize the VM at the correct height. + let v10_height = CurrentNetwork::CONSENSUS_HEIGHT(consensus_version).unwrap(); + let vm = crate::vm::test_helpers::sample_vm_at_height(v10_height, rng); + + // Define the first program with a record. + let program_one = Program::from_str( + r" +program test_one.aleo; + +constructor: + assert.eq true true; + +struct S: + x as field; + +function make_s: + cast 0field into r0 as S; + output r0 as S.public; +", + ) + .unwrap(); + + // Define the second program which refers to the external struct type. + let program_two = Program::from_str( + r" +import test_one.aleo; + +program test_two.aleo; + +constructor: + assert.eq true true; + +function second: + call test_one.aleo/make_s into r0; + output r0 as test_one.aleo/S.public; +", + ) + .unwrap(); + + // Deploy the first program. + let deployment_one = vm.deploy(&caller_private_key, &program_one, None, 0, None, rng).unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[deployment_one], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block).unwrap(); + + // Deploy the second program. + let deployment_two = vm.deploy(&caller_private_key, &program_two, None, 0, None, rng).unwrap(); + sample_next_block(&vm, &caller_private_key, &[deployment_two], rng).unwrap() +} diff --git a/synthesizer/src/vm/verify.rs b/synthesizer/src/vm/verify.rs index 57ce3519c6..f41a5cfbac 100644 --- a/synthesizer/src/vm/verify.rs +++ b/synthesizer/src/vm/verify.rs @@ -189,8 +189,6 @@ impl> VM { // Verify the signature corresponds to the transaction ID. ensure!(owner.verify(*deployment_id), "Invalid owner signature for deployment transaction '{id}'"); - self.ensure_deployment_valid_for_consensus_version(consensus_version, deployment, id)?; - // If the `CONSENSUS_VERSION` is less than `V8`, ensure that // - the deployment edition is zero. // If the `CONSENSUS_VERSION` is less than `V9` ensure that @@ -248,6 +246,21 @@ impl> VM { ); } + // If the `CONSENSUS_VERSION` is less than `V12`, then verify that: + // - the program does not use the external struct syntax `some_program.aleo/StructT` + // If the `CONSENSUS_VERSION` is greater than or equal to `V12`, then verify that: + // - the program's mappings do not use non-existent structs. + if consensus_version < ConsensusVersion::V12 { + ensure!( + !deployment.program().contains_external_struct(), + "Invalid deployment transaction '{id}' - external structs may only be used beginning with Consensus version 10" + ); + } + + if consensus_version >= ConsensusVersion::V12 { + self.process.read().mapping_types_exist(deployment.program())?; + } + // If the program owner exists in the deployment, then verify that it matches the owner in the transaction. if let Some(given_owner) = deployment.program_owner() { // Ensure the program owner matches the owner in the transaction. @@ -506,80 +519,6 @@ impl> VM { } Ok(()) } - - fn ensure_deployment_valid_for_consensus_version( - &self, - consensus_version: ConsensusVersion, - deployment: &Deployment, - id: &N::TransactionID, - ) -> Result<()> { - // If the `CONSENSUS_VERSION` is less than `V8`, ensure that - // - the deployment edition is zero. - // If the `CONSENSUS_VERSION` is less than `V9` ensure that - // - the deployment edition is zero or one. - // - the program checksum is **not** present in the deployment, - // - the program owner is **not** present in the deployment - // - the program does not use constructors, `Operand::Checksum`, `Operand::Edition`, or `Operand::ProgramOwner` - // If the `CONSENSUS_VERSION` is greater than or equal to `V9`, then verify that: - // - the program checksum is present in the deployment - // - the program owner is present in the deployment - // If the `CONSENSUS_VERSION` is less than `V11`, then verify that: - // - the program does not use the external struct syntax `some_program.aleo/StructT` - // If the `CONSENSUS_VERSION` is greater than or equal to `V11`, then verify that: - // - the program's mappings do not use non-existent structs. - if consensus_version < ConsensusVersion::V8 { - ensure!( - deployment.edition().is_zero(), - "Invalid deployment transaction '{id}' - edition should be zero before `ConsensusVersion::V8`", - ); - } - if consensus_version < ConsensusVersion::V9 { - ensure!( - deployment.edition() <= 1, - "Invalid deployment transaction '{id}' - edition should be zero or one for before `ConsensusVersion::V9`" - ); - ensure!( - deployment.program_checksum().is_none(), - "Invalid deployment transaction '{id}' - should not contain program checksum" - ); - ensure!( - deployment.program_owner().is_none(), - "Invalid deployment transaction '{id}' - should not contain program owner" - ); - ensure!( - !deployment.program().contains_v9_syntax(), - "Invalid deployment transaction '{id}' - program uses syntax that is not allowed before `ConsensusVersion::V9`" - ); - } - if consensus_version >= ConsensusVersion::V9 { - ensure!( - deployment.program_checksum().is_some(), - "Invalid deployment transaction '{id}' - missing program checksum" - ); - ensure!( - deployment.program_owner().is_some(), - "Invalid deployment transaction '{id}' - missing program owner" - ); - } - if consensus_version < ConsensusVersion::V11 { - ensure!( - !deployment.program().contains_external_struct(), - "Invalid deployment transaction '{id}' - external structs may only be used beginning with Consensus version 10" - ); - } - if consensus_version < ConsensusVersion::V12 { - ensure!( - !deployment.program().contains_external_struct(), - "Invalid deployment transaction '{id}' - external structs may only be used beginning with Consensus version 10" - ); - } - - if consensus_version >= ConsensusVersion::V12 { - self.process.read().mapping_types_exist(deployment.program())?; - } - - Ok(()) - } } impl> VM { From 985a954e151a9ac7e1464ed0aba0fbce318f227d Mon Sep 17 00:00:00 2001 From: Mohammad Fawaz Date: Tue, 4 Nov 2025 13:41:26 -0500 Subject: [PATCH 25/25] Support serialize/deserialize and `size_in_bits` for external structs --- .../data_types/finalize_type/size_in_bits.rs | 46 ++++++-- .../data_types/plaintext_type/size_in_bits.rs | 89 ++++++++++++--- .../data_types/record_type/size_in_bits.rs | 16 +-- .../data_types/register_type/size_in_bits.rs | 58 ++++++---- .../src/stack/finalize_types/initialize.rs | 18 +-- .../process/src/tests/test_serializers.rs | 107 +++++++++++++++--- .../src/logic/instruction/operation/assert.rs | 4 +- .../instruction/operation/deserialize.rs | 78 +++++++++---- .../instruction/operation/ecdsa_verify.rs | 21 +++- .../src/logic/instruction/operation/hash.rs | 21 +++- .../logic/instruction/operation/serialize.rs | 11 +- .../program/tests/instruction/deserialize.rs | 7 +- .../program/tests/instruction/serialize.rs | 7 +- 13 files changed, 367 insertions(+), 116 deletions(-) diff --git a/console/program/src/data_types/finalize_type/size_in_bits.rs b/console/program/src/data_types/finalize_type/size_in_bits.rs index c89c6bb26b..11049c2c91 100644 --- a/console/program/src/data_types/finalize_type/size_in_bits.rs +++ b/console/program/src/data_types/finalize_type/size_in_bits.rs @@ -20,26 +20,41 @@ use super::*; impl FinalizeType { /// Returns the number of bits of a finalize type. /// Note. The plaintext variant is assumed to be an argument of a `Future` and this does not have a "raw" serialization. - pub fn future_size_in_bits(locator: &Locator, get_struct: &F0, get_future: &F1) -> Result + pub fn future_size_in_bits( + locator: &Locator, + get_struct: &F0, + get_external_struct: &F1, + get_future: &F2, + ) -> Result where F0: Fn(&Identifier) -> Result>, - F1: Fn(&Locator) -> Result>>, + F1: Fn(&Locator) -> Result>, + F2: Fn(&Locator) -> Result>>, { - FinalizeType::Future(*locator).size_in_bits_internal(get_struct, get_future, 0) + FinalizeType::Future(*locator).size_in_bits_internal(get_struct, get_external_struct, get_future, 0) } /// A helper function to determine the number of bits of a plaintext type, while tracking the depth of the data. /// Note. The plaintext variant is assumed to be an argument of a `Future` and thus does not have a "raw" serialization. - fn size_in_bits_internal(&self, get_struct: &F0, get_future: &F1, depth: usize) -> Result + fn size_in_bits_internal( + &self, + get_struct: &F0, + get_external_struct: &F1, + get_future: &F2, + depth: usize, + ) -> Result where F0: Fn(&Identifier) -> Result>, - F1: Fn(&Locator) -> Result>>, + F1: Fn(&Locator) -> Result>, + F2: Fn(&Locator) -> Result>>, { // Ensure that the depth is within the maximum limit. ensure!(depth <= N::MAX_DATA_DEPTH, "Finalize type depth exceeds maximum limit: {}", N::MAX_DATA_DEPTH); match self { - Self::Plaintext(plaintext_type) => plaintext_type.size_in_bits_internal(get_struct, depth), + Self::Plaintext(plaintext_type) => { + plaintext_type.size_in_bits_internal(get_struct, get_external_struct, depth) + } Self::Future(locator) => { // Initialize the size in bits. let mut size = 0usize; @@ -79,7 +94,12 @@ impl FinalizeType { // Account for the argument bits. size = size - .checked_add(argument.size_in_bits_internal(get_struct, get_future, depth + 1)?) + .checked_add(argument.size_in_bits_internal( + get_struct, + get_external_struct, + get_future, + depth + 1, + )?) .ok_or(anyhow!("`size_in_bits` overflowed"))?; } @@ -89,11 +109,17 @@ impl FinalizeType { } /// Returns the number of raw bits of a finlaize type. - pub fn future_size_in_bits_raw(locator: &Locator, get_struct: &F0, get_future: &F1) -> Result + pub fn future_size_in_bits_raw( + locator: &Locator, + get_struct: &F0, + get_external_struct: &F1, + get_future: &F2, + ) -> Result where F0: Fn(&Identifier) -> Result>, - F1: Fn(&Locator) -> Result>>, + F1: Fn(&Locator) -> Result>, + F2: Fn(&Locator) -> Result>>, { - Self::future_size_in_bits(locator, get_struct, get_future) + Self::future_size_in_bits(locator, get_struct, get_external_struct, get_future) } } diff --git a/console/program/src/data_types/plaintext_type/size_in_bits.rs b/console/program/src/data_types/plaintext_type/size_in_bits.rs index 8e49f01d8d..843c3efaf3 100644 --- a/console/program/src/data_types/plaintext_type/size_in_bits.rs +++ b/console/program/src/data_types/plaintext_type/size_in_bits.rs @@ -17,17 +17,24 @@ use super::*; impl PlaintextType { /// Returns the number of bits of a plaintext type. - pub fn size_in_bits(&self, get_struct: &F) -> Result + pub fn size_in_bits(&self, get_struct: &F0, get_external_struct: &F1) -> Result where - F: Fn(&Identifier) -> Result>, + F0: Fn(&Identifier) -> Result>, + F1: Fn(&Locator) -> Result>, { - self.size_in_bits_internal(get_struct, 0) + self.size_in_bits_internal(get_struct, get_external_struct, 0) } /// A helper function to determine the number of bits of a plaintext type, while tracking the depth of the data. - pub(crate) fn size_in_bits_internal(&self, get_struct: &F, depth: usize) -> Result + pub(crate) fn size_in_bits_internal( + &self, + get_struct: &F0, + get_external_struct: &F1, + depth: usize, + ) -> Result where - F: Fn(&Identifier) -> Result>, + F0: Fn(&Identifier) -> Result>, + F1: Fn(&Locator) -> Result>, { // Ensure that the depth is within the maximum limit. ensure!(depth <= N::MAX_DATA_DEPTH, "Plaintext depth exceeds maximum limit: {}", N::MAX_DATA_DEPTH); @@ -67,20 +74,45 @@ impl PlaintextType { // Account for the size of the member total = total.checked_add(16).ok_or(anyhow!("`size_in_bits` overflowed"))?; // Account for the member itself. - let member_size = member_type.size_in_bits_internal(get_struct, depth + 1)?; + let member_size = member_type.size_in_bits_internal(get_struct, get_external_struct, depth + 1)?; + total = total.checked_add(member_size).ok_or(anyhow!("`size_in_bits` overflowed"))?; + } + + Ok(total) + } + PlaintextType::ExternalStruct(locator) => { + // Look up the struct + let struct_ = get_external_struct(locator)?; + + // Account for the plaintext variant bits. + let mut total = PlaintextType::::STRUCT_PREFIX_BITS.len(); + // Account for the number of members in the struct. + total = total.checked_add(8).ok_or(anyhow!("`size_in_bits` overflowed"))?; + // Add up the sizes of each member. + for (identifier, member_type) in struct_.members() { + // Account for the size of the identifier. + total = total.checked_add(8).ok_or(anyhow!("`size_in_bits` overflowed"))?; + // Account for the identifier. + total = total + .checked_add(identifier.size_in_bits() as usize) + .ok_or(anyhow!("`size_in_bits` overflowed"))?; + // Account for the size of the member + total = total.checked_add(16).ok_or(anyhow!("`size_in_bits` overflowed"))?; + // Account for the member itself. + let member_size = member_type.size_in_bits_internal(get_struct, get_external_struct, depth + 1)?; total = total.checked_add(member_size).ok_or(anyhow!("`size_in_bits` overflowed"))?; } Ok(total) } - PlaintextType::ExternalStruct(_identifier) => todo!(), PlaintextType::Array(array_type) => { // Account for the plaintext variant bits. let mut total = PlaintextType::::ARRAY_PREFIX_BITS.len(); // Account for the size of the array length. total = total.checked_add(32).ok_or(anyhow!("`size_in_bits` overflowed"))?; // Get the size of the element type. - let element_size = array_type.next_element_type().size_in_bits_internal(get_struct, depth + 1)?; + let element_size = + array_type.next_element_type().size_in_bits_internal(get_struct, get_external_struct, depth + 1)?; // Get the total size of an element. let element_total = 16usize.checked_add(element_size).ok_or(anyhow!("`size_in_bits` overflowed"))?; // Multiply by the length of the array, ensuring no overflow occurs. @@ -98,17 +130,24 @@ impl PlaintextType { } /// Returns the number of raw bits of a plaintext type. - pub fn size_in_bits_raw(&self, get_struct: &F) -> Result + pub fn size_in_bits_raw(&self, get_struct: &F0, get_external_struct: &F1) -> Result where - F: Fn(&Identifier) -> Result>, + F0: Fn(&Identifier) -> Result>, + F1: Fn(&Locator) -> Result>, { - self.size_in_bits_raw_internal(get_struct, 0) + self.size_in_bits_raw_internal(get_struct, get_external_struct, 0) } // A helper function to determine the number of raw bits of a plaintext type, while tracking the depth of the data. - fn size_in_bits_raw_internal(&self, get_struct: &F, depth: usize) -> Result + fn size_in_bits_raw_internal( + &self, + get_struct: &F0, + get_external_struct: &F1, + depth: usize, + ) -> Result where - F: Fn(&Identifier) -> Result>, + F0: Fn(&Identifier) -> Result>, + F1: Fn(&Locator) -> Result>, { // Ensure that the depth is within the maximum limit. ensure!(depth <= N::MAX_DATA_DEPTH, "Plaintext depth exceeds maximum limit: {}", N::MAX_DATA_DEPTH); @@ -122,16 +161,34 @@ impl PlaintextType { let mut total = 0usize; for member_type in struct_.members().values() { // Get the size of the member. - let member_size = member_type.size_in_bits_raw_internal(get_struct, depth + 1)?; + let member_size = + member_type.size_in_bits_raw_internal(get_struct, get_external_struct, depth + 1)?; + // Add to the total size, ensuring no overflow occurs. + total = total.checked_add(member_size).ok_or(anyhow!("`size_in_bits_raw` overflowed"))?; + } + Ok(total) + } + PlaintextType::ExternalStruct(locator) => { + // Look up the struct. + let struct_ = get_external_struct(locator)?; + // Add up the sizes of each member. + let mut total = 0usize; + for member_type in struct_.members().values() { + // Get the size of the member. + let member_size = + member_type.size_in_bits_raw_internal(get_struct, get_external_struct, depth + 1)?; // Add to the total size, ensuring no overflow occurs. total = total.checked_add(member_size).ok_or(anyhow!("`size_in_bits_raw` overflowed"))?; } Ok(total) } - PlaintextType::ExternalStruct(_identifier) => todo!(), PlaintextType::Array(array_type) => { // Get the size of the element type. - let element_size = array_type.next_element_type().size_in_bits_raw_internal(get_struct, depth + 1)?; + let element_size = array_type.next_element_type().size_in_bits_raw_internal( + get_struct, + get_external_struct, + depth + 1, + )?; // Multiply by the length of the array, ensuring no overflow occurs. let total = element_size .checked_mul(**array_type.length() as usize) diff --git a/console/program/src/data_types/record_type/size_in_bits.rs b/console/program/src/data_types/record_type/size_in_bits.rs index 130dbf2064..3fe1996d06 100644 --- a/console/program/src/data_types/record_type/size_in_bits.rs +++ b/console/program/src/data_types/record_type/size_in_bits.rs @@ -15,15 +15,16 @@ use snarkvm_console_types::{Address, Group, U8}; -use crate::StructType; +use crate::{Locator, StructType}; use super::*; impl RecordType { /// Returns the number of bits of a record type. - pub fn size_in_bits(&self, get_struct: &F) -> Result + pub fn size_in_bits(&self, get_struct: &F0, get_external_struct: &F1) -> Result where - F: Fn(&Identifier) -> Result>, + F0: Fn(&Identifier) -> Result>, + F1: Fn(&Locator) -> Result>, { // Initialize the counter. let mut size = 0usize; @@ -45,7 +46,7 @@ impl RecordType { data_size = data_size.checked_add(2).ok_or(anyhow!("`size_in_bits` overflowed"))?; // Account for the entry data bits. data_size = data_size - .checked_add(entry_type.plaintext_type().size_in_bits(get_struct)?) + .checked_add(entry_type.plaintext_type().size_in_bits(get_struct, get_external_struct)?) .ok_or(anyhow!("`size_in_bits` overflowed"))?; } @@ -73,10 +74,11 @@ impl RecordType { } /// Returns the number of raw bits of a record type. - pub fn size_in_bits_raw(&self, get_struct: &F) -> Result + pub fn size_in_bits_raw(&self, get_struct: &F0, get_external_struct: &F1) -> Result where - F: Fn(&Identifier) -> Result>, + F0: Fn(&Identifier) -> Result>, + F1: Fn(&Locator) -> Result>, { - self.size_in_bits(get_struct) + self.size_in_bits(get_struct, get_external_struct) } } diff --git a/console/program/src/data_types/register_type/size_in_bits.rs b/console/program/src/data_types/register_type/size_in_bits.rs index 91742b781b..97e195347d 100644 --- a/console/program/src/data_types/register_type/size_in_bits.rs +++ b/console/program/src/data_types/register_type/size_in_bits.rs @@ -19,46 +19,60 @@ use super::*; impl RegisterType { /// Returns the number of bits of a register type. - pub fn size_in_bits( + pub fn size_in_bits( &self, get_struct: &F0, - get_record: &F1, - get_external_record: &F2, - get_future: &F3, + get_external_struct: &F1, + get_record: &F2, + get_external_record: &F3, + get_future: &F4, ) -> Result where F0: Fn(&Identifier) -> Result>, - F1: Fn(&Identifier) -> Result>, - F2: Fn(&Locator) -> Result>, - F3: Fn(&Locator) -> Result>>, + F1: Fn(&Locator) -> Result>, + F2: Fn(&Identifier) -> Result>, + F3: Fn(&Locator) -> Result>, + F4: Fn(&Locator) -> Result>>, { match self { - RegisterType::Plaintext(plaintext_type) => plaintext_type.size_in_bits(get_struct), - RegisterType::Record(identifier) => get_record(identifier)?.size_in_bits(get_struct), - RegisterType::ExternalRecord(locator) => get_external_record(locator)?.size_in_bits(get_struct), - RegisterType::Future(locator) => FinalizeType::future_size_in_bits(locator, get_struct, get_future), + RegisterType::Plaintext(plaintext_type) => plaintext_type.size_in_bits(get_struct, get_external_struct), + RegisterType::Record(identifier) => get_record(identifier)?.size_in_bits(get_struct, get_external_struct), + RegisterType::ExternalRecord(locator) => { + get_external_record(locator)?.size_in_bits(get_struct, get_external_struct) + } + RegisterType::Future(locator) => { + FinalizeType::future_size_in_bits(locator, get_struct, get_external_struct, get_future) + } } } /// Returns the number of raw bits of a register type. - pub fn size_in_bits_raw( + pub fn size_in_bits_raw( &self, get_struct: &F0, - get_record: &F1, - get_external_record: &F2, - get_future: &F3, + get_external_struct: &F1, + get_record: &F2, + get_external_record: &F3, + get_future: &F4, ) -> Result where F0: Fn(&Identifier) -> Result>, - F1: Fn(&Identifier) -> Result>, - F2: Fn(&Locator) -> Result>, - F3: Fn(&Locator) -> Result>>, + F1: Fn(&Locator) -> Result>, + F2: Fn(&Identifier) -> Result>, + F3: Fn(&Locator) -> Result>, + F4: Fn(&Locator) -> Result>>, { match self { - RegisterType::Plaintext(plaintext_type) => plaintext_type.size_in_bits_raw(get_struct), - RegisterType::Record(identifier) => get_record(identifier)?.size_in_bits_raw(get_struct), - RegisterType::ExternalRecord(locator) => get_external_record(locator)?.size_in_bits_raw(get_struct), - RegisterType::Future(locator) => FinalizeType::future_size_in_bits_raw(locator, get_struct, get_future), + RegisterType::Plaintext(plaintext_type) => plaintext_type.size_in_bits_raw(get_struct, get_external_struct), + RegisterType::Record(identifier) => { + get_record(identifier)?.size_in_bits_raw(get_struct, get_external_struct) + } + RegisterType::ExternalRecord(locator) => { + get_external_record(locator)?.size_in_bits_raw(get_struct, get_external_struct) + } + RegisterType::Future(locator) => { + FinalizeType::future_size_in_bits_raw(locator, get_struct, get_external_struct, get_future) + } } } } diff --git a/synthesizer/process/src/stack/finalize_types/initialize.rs b/synthesizer/process/src/stack/finalize_types/initialize.rs index 623fad763a..8f9b502d7c 100644 --- a/synthesizer/process/src/stack/finalize_types/initialize.rs +++ b/synthesizer/process/src/stack/finalize_types/initialize.rs @@ -170,7 +170,7 @@ impl FinalizeTypes { // Insert the input register. self.add_input(register.clone(), finalize_type.clone())?; - // Ensure the register type and the input type match. + // Ensure the register type and the input type are equivalent. if !finalize_types_equivalent(stack, finalize_type, stack, &self.get_type(stack, register)?)? { bail!("Input '{register}' does not match the expected input register type.") } @@ -259,7 +259,7 @@ impl FinalizeTypes { // If the register is a future, throw an error. FinalizeType::Future(..) => bail!("A future cannot be used in a `branch` command"), }; - // Check that the operands have the same type. + // Check that the operands have equivalent types. ensure!( types_equivalent(stack, &first_type, stack, &second_type)?, "Command '{}' expects operands of the same type. Found operands of type '{}' and '{}'", @@ -325,7 +325,7 @@ impl FinalizeTypes { // If the register is a future, throw an error. FinalizeType::Future(..) => bail!("A future cannot be used as a key in a `contains` command"), }; - // Check that the key type in the mapping matches the key type in the instruction. + // Check that the key type in the mapping is equivalent to the key type in the instruction. if !types_equivalent(stack, mapping_key_type, stack, &key_type)? { bail!( "Key type in `contains` '{key_type}' does not match the key type in the mapping '{mapping_key_type}'." @@ -390,7 +390,7 @@ impl FinalizeTypes { // If the register is a future, throw an error. FinalizeType::Future(..) => bail!("A future cannot be used as a key in a `get` command"), }; - // Check that the key type in the mapping matches the key type in the instruction. + // Check that the key type in the mapping is equivalent to the key type in the instruction. if !types_equivalent(stack, mapping_key_type, stack, &key_type)? { bail!("Key type in `get` '{key_type}' does not match the key type in the mapping '{mapping_key_type}'.") } @@ -453,7 +453,7 @@ impl FinalizeTypes { // If the register is a future, throw an error. FinalizeType::Future(..) => bail!("A future cannot be used as a key in a `get.or_use` command"), }; - // Check that the key type in the mapping matches the key type. + // Check that the key type in the mapping is equivalent to the key type. if !types_equivalent(stack, mapping_key_type, stack, &key_type)? { bail!( "Key type in `get.or_use` '{key_type}' does not match the key type in the mapping '{mapping_key_type}'." @@ -466,7 +466,7 @@ impl FinalizeTypes { // If the register is a future, throw an error. FinalizeType::Future(..) => bail!("A default value cannot be a future"), }; - // Check that the value type in the mapping matches the default value type. + // Check that the value type in the mapping is equivalent to the default value type. if !types_equivalent(stack, mapping_value_type, stack, &default_value_type)? { bail!( "Default value type in `get.or_use` '{default_value_type}' does not match the value type in the mapping '{mapping_value_type}'." @@ -528,7 +528,7 @@ impl FinalizeTypes { // If the register is a future, throw an error. FinalizeType::Future(..) => bail!("A future cannot be used as a key in a `set` command"), }; - // Check that the key type in the mapping matches the key type. + // Check that the key type in the mapping is equivalent the key type. if !types_equivalent(stack, mapping_key_type, stack, &key_type)? { bail!("Key type in `set` '{key_type}' does not match the key type in the mapping '{mapping_key_type}'.") } @@ -539,7 +539,7 @@ impl FinalizeTypes { // If the register is a future, throw an error. FinalizeType::Future(..) => bail!("A future cannot be used as a value in a `set` command"), }; - // Check that the value type in the mapping matches the type of the value. + // Check that the value type in the mapping is equivalent the type of the value. if !types_equivalent(stack, mapping_value_type, stack, &value_type)? { bail!( "Value type in `set` '{value_type}' does not match the value type in the mapping '{mapping_value_type}'." @@ -567,7 +567,7 @@ impl FinalizeTypes { // If the register is a future, throw an error. FinalizeType::Future(..) => bail!("A future cannot be used as a key in a `remove` command"), }; - // Check that the key type in the mapping matches the key type. + // Check that the key type in the mapping is equivalent the key type. if !types_equivalent(stack, mapping_key_type, stack, &key_type)? { bail!("Key type in `remove` '{key_type}' does not match the key type in the mapping '{mapping_key_type}'.") } diff --git a/synthesizer/process/src/tests/test_serializers.rs b/synthesizer/process/src/tests/test_serializers.rs index f5adeb862c..6aa75f0c0d 100644 --- a/synthesizer/process/src/tests/test_serializers.rs +++ b/synthesizer/process/src/tests/test_serializers.rs @@ -101,11 +101,12 @@ function test_serde_equivalence: // Structs are not supported. let fail_get_struct = |_: &Identifier| bail!("structs are not supported"); + let fail_get_external_struct = |_: &Locator| bail!("structs are not supported"); // Get the bits type. let num_bits = match is_raw { - true => type_.size_in_bits_raw(&fail_get_struct).unwrap(), - false => type_.size_in_bits(&fail_get_struct).unwrap(), + true => type_.size_in_bits_raw(&fail_get_struct, &fail_get_external_struct).unwrap(), + false => type_.size_in_bits(&fail_get_struct, &fail_get_external_struct).unwrap(), }; let num_bits = u32::try_from(num_bits).unwrap(); let bits_type = ArrayType::new(PlaintextType::Literal(LiteralType::Boolean), vec![U32::new(num_bits)]).unwrap(); @@ -191,10 +192,53 @@ fn test_value_size_in_bits() { // Load a process. let mut process = Process::::load().unwrap(); + // Define a program . + let program0 = Program::::from_str( + r" +program test0.aleo; + +struct A: + one as field; + two as u8; + three as signature; + +struct B: + one as [scalar; 32u32]; + two as [A; 4u32]; + +record credits: + owner as address.private; + microcredits as u64.private; + +record C: + owner as address.private; + data as [u8; 16u32].private; + amount as u32.private; + ayyy as A.private; + bees as [B; 2u32].private; + +function dummy: + input r0 as A.public; + input r1 as B.public; + async dummy r0 r1 into r2; + output r2 as test0.aleo/dummy.future; +finalize dummy: + input r0 as A.public; + input r1 as B.public; + assert.eq r0.one r1.two[0u32].one; + ", + ) + .unwrap(); + + // Add the program to the process. + process.add_program(&program0).unwrap(); + // Define a program with data types that we want to test. - let program = Program::::from_str( + let program1 = Program::::from_str( r" -program test.aleo; +import test0.aleo; + +program test1.aleo; struct A: one as field; @@ -220,7 +264,7 @@ function dummy: input r0 as A.public; input r1 as B.public; async dummy r0 r1 into r2; - output r2 as test.aleo/dummy.future; + output r2 as test1.aleo/dummy.future; finalize dummy: input r0 as A.public; input r1 as B.public; @@ -230,19 +274,26 @@ finalize dummy: .unwrap(); // Add the program to the process. - process.add_program(&program).unwrap(); + process.add_program(&program1).unwrap(); // Get the stack. - let stack = process.get_stack(program.id()).unwrap(); + let stack = process.get_stack(program1.id()).unwrap(); - // A helper function to get the struct. + // A helper function to get a struct declaration. let get_struct = |id: &Identifier| stack.program().get_struct(id).cloned(); + // A helper function to get an external struct declaration. + let get_external_struct = |locator: &Locator| { + stack.get_external_stack(locator.program_id())?.program().get_struct(locator.resource()).cloned() + }; + // A helper to get a record declaration. let get_record = |identifier: &Identifier| stack.program().get_record(identifier).cloned(); // A helper to get an external record declaration. - let get_external_record = |_locator: &Locator| unimplemented!("Not tested"); + let get_external_record = |locator: &Locator| { + stack.get_external_stack(locator.program_id())?.program().get_record(locator.resource()).cloned() + }; // A helper to get the argument types of a future. let get_future = |locator: &Locator| { @@ -292,15 +343,31 @@ finalize dummy: )), RegisterType::Plaintext(PlaintextType::Struct(Identifier::from_str("A").unwrap())), RegisterType::Plaintext(PlaintextType::Struct(Identifier::from_str("B").unwrap())), + RegisterType::Plaintext(PlaintextType::ExternalStruct(Locator::from_str("test0.aleo/A").unwrap())), + RegisterType::Plaintext(PlaintextType::ExternalStruct(Locator::from_str("test0.aleo/B").unwrap())), RegisterType::Plaintext(PlaintextType::Array( ArrayType::new(PlaintextType::Struct(Identifier::from_str("A").unwrap()), vec![U32::new(3)]).unwrap(), )), RegisterType::Plaintext(PlaintextType::Array( ArrayType::new(PlaintextType::Struct(Identifier::from_str("B").unwrap()), vec![U32::new(2)]).unwrap(), )), + RegisterType::Plaintext(PlaintextType::Array( + ArrayType::new(PlaintextType::ExternalStruct(Locator::from_str("test0.aleo/A").unwrap()), vec![U32::new( + 3, + )]) + .unwrap(), + )), + RegisterType::Plaintext(PlaintextType::Array( + ArrayType::new(PlaintextType::ExternalStruct(Locator::from_str("test0.aleo/B").unwrap()), vec![U32::new( + 2, + )]) + .unwrap(), + )), RegisterType::Record(Identifier::from_str("credits").unwrap()), RegisterType::Record(Identifier::from_str("C").unwrap()), - RegisterType::Future(Locator::from_str("test.aleo/dummy").unwrap()), + RegisterType::ExternalRecord(Locator::from_str("test0.aleo/credits").unwrap()), + RegisterType::ExternalRecord(Locator::from_str("test0.aleo/C").unwrap()), + RegisterType::Future(Locator::from_str("test1.aleo/dummy").unwrap()), ]; for is_raw in [false, true] { @@ -313,10 +380,18 @@ finalize dummy: // Get the size in bits. let size_in_bits = match is_raw { - true => { - type_.size_in_bits_raw(&get_struct, &get_record, &get_external_record, &get_future).unwrap() - } - false => type_.size_in_bits(&get_struct, &get_record, &get_external_record, &get_future).unwrap(), + true => type_ + .size_in_bits_raw( + &get_struct, + &get_external_struct, + &get_record, + &get_external_record, + &get_future, + ) + .unwrap(), + false => type_ + .size_in_bits(&get_struct, &get_external_struct, &get_record, &get_external_record, &get_future) + .unwrap(), }; // Sample the value. @@ -329,7 +404,9 @@ finalize dummy: }; // Check that the number of bits matches the expected size. - println!("Expected size in bits: {size_in_bits}, Actual size in bits: {}", bits.len()); + if bits.len() != size_in_bits { + println!("Expected size in bits: {size_in_bits}, Actual size in bits: {}", bits.len()); + } assert_eq!(bits.len(), size_in_bits); } }) diff --git a/synthesizer/program/src/logic/instruction/operation/assert.rs b/synthesizer/program/src/logic/instruction/operation/assert.rs index 77a0284214..f887fe9fb7 100644 --- a/synthesizer/program/src/logic/instruction/operation/assert.rs +++ b/synthesizer/program/src/logic/instruction/operation/assert.rs @@ -141,10 +141,10 @@ impl AssertInstruction { if input_types.len() != 2 { bail!("Instruction '{}' expects 2 inputs, found {} inputs", Self::opcode(), input_types.len()) } - // Ensure the operands are of the same type. + // Ensure the operands have equivalent types. if !register_types_equivalent(stack, &input_types[0], stack, &input_types[1])? { bail!( - "Instruction '{}' expects inputs of the same type. Found inputs of type '{}' and '{}'", + "Instruction '{}' expects inputs of equivalent types. Found inputs of type '{}' and '{}'", Self::opcode(), input_types[0], input_types[1] diff --git a/synthesizer/program/src/logic/instruction/operation/deserialize.rs b/synthesizer/program/src/logic/instruction/operation/deserialize.rs index d3bcbc82cc..338c6cab54 100644 --- a/synthesizer/program/src/logic/instruction/operation/deserialize.rs +++ b/synthesizer/program/src/logic/instruction/operation/deserialize.rs @@ -21,6 +21,7 @@ use console::{ Identifier, Literal, LiteralType, + Locator, Plaintext, PlaintextType, Register, @@ -192,27 +193,31 @@ impl DeserializeInstruction { /// /// This allows running `deserialize` without the machinery of stacks and registers. /// This is necessary for the Leo interpreter. -pub fn evaluate_deserialize( +pub fn evaluate_deserialize( variant: DeserializeVariant, bits: &[bool], destination_type: &PlaintextType, - get_struct: &F, + get_struct: &F0, + get_external_struct: &F1, ) -> Result> where - F: Fn(&Identifier) -> Result>, + F0: Fn(&Identifier) -> Result>, + F1: Fn(&Locator) -> Result>, { - evaluate_deserialize_internal(variant as u8, bits, destination_type, get_struct, 0) + evaluate_deserialize_internal(variant as u8, bits, destination_type, get_struct, get_external_struct, 0) } -fn evaluate_deserialize_internal( +fn evaluate_deserialize_internal( variant: u8, bits: &[bool], destination_type: &PlaintextType, - get_struct: &F, + get_struct: &F0, + get_external_struct: &F1, depth: usize, ) -> Result> where - F: Fn(&Identifier) -> Result>, + F0: Fn(&Identifier) -> Result>, + F1: Fn(&Locator) -> Result>, { // Ensure that the depth is within the maximum limit. if depth > N::MAX_DATA_DEPTH { @@ -222,8 +227,8 @@ where // A helper to get the number of bits needed. let get_size_in_bits = |plaintext_type: &PlaintextType| -> Result { match DeserializeVariant::from_u8(variant) { - DeserializeVariant::FromBits => plaintext_type.size_in_bits(&get_struct), - DeserializeVariant::FromBitsRaw => plaintext_type.size_in_bits_raw(&get_struct), + DeserializeVariant::FromBits => plaintext_type.size_in_bits(get_struct, &get_external_struct), + DeserializeVariant::FromBitsRaw => plaintext_type.size_in_bits_raw(get_struct, &get_external_struct), } }; @@ -338,6 +343,7 @@ where next_bits(expected_member_size)?, member_type, get_struct, + get_external_struct, depth + 1, )?; @@ -387,6 +393,7 @@ where next_bits(expected_element_size)?, expected_element_type, get_struct, + get_external_struct, depth + 1, )?; elements.push(element); @@ -398,15 +405,17 @@ where } } -fn execute_deserialize_internal, N: Network, F>( +fn execute_deserialize_internal, N: Network, F0, F1>( variant: u8, bits: &[circuit::Boolean], destination_type: &PlaintextType, - get_struct: &F, + get_struct: &F0, + get_external_struct: &F1, depth: usize, ) -> Result> where - F: Fn(&Identifier) -> Result>, + F0: Fn(&Identifier) -> Result>, + F1: Fn(&Locator) -> Result>, { use snarkvm_circuit::{Inject, traits::FromBits}; @@ -418,8 +427,8 @@ where // A helper to get the number of bits needed. let get_size_in_bits = |plaintext_type: &PlaintextType| -> Result { match DeserializeVariant::from_u8(variant) { - DeserializeVariant::FromBits => plaintext_type.size_in_bits(get_struct), - DeserializeVariant::FromBitsRaw => plaintext_type.size_in_bits_raw(get_struct), + DeserializeVariant::FromBits => plaintext_type.size_in_bits(get_struct, get_external_struct), + DeserializeVariant::FromBitsRaw => plaintext_type.size_in_bits_raw(get_struct, get_external_struct), } }; @@ -525,6 +534,7 @@ where next_bits(expected_member_size as usize)?, member_type, get_struct, + get_external_struct, depth + 1, )?; @@ -569,6 +579,7 @@ where next_bits(expected_element_size as usize)?, expected_element_type, get_struct, + get_external_struct, depth + 1, )?; elements.push(element); @@ -605,10 +616,15 @@ impl DeserializeInstruction { // A helper to get a struct declaration. let get_struct = |identifier: &Identifier| stack.program().get_struct(identifier).cloned(); + // A helper to get an external struct declaration. + let get_external_struct = |locator: &Locator| { + stack.get_external_stack(locator.program_id())?.program().get_struct(locator.resource()).cloned() + }; + // Get the size in bits of the operand. let size_in_bits = match VARIANT { - 0 => self.destination_type.size_in_bits(&get_struct)?, - 1 => self.destination_type.size_in_bits_raw(&get_struct)?, + 0 => self.destination_type.size_in_bits(&get_struct, &get_external_struct)?, + 1 => self.destination_type.size_in_bits_raw(&get_struct, &get_external_struct)?, variant => bail!("Invalid `deserialize` variant '{variant}'"), }; @@ -620,7 +636,14 @@ impl DeserializeInstruction { ); // Deserialize into the desired output. - let output = evaluate_deserialize_internal(VARIANT, &bits, &self.destination_type, &get_struct, 0)?; + let output = evaluate_deserialize_internal( + VARIANT, + &bits, + &self.destination_type, + &get_struct, + &get_external_struct, + 0, + )?; // Store the output. registers.store(stack, &self.destination, Value::Plaintext(output)) @@ -651,10 +674,15 @@ impl DeserializeInstruction { // A helper to get a struct declaration. let get_struct = |identifier: &Identifier| stack.program().get_struct(identifier).cloned(); + // A helper to get an external struct declaration. + let get_external_struct = |locator: &Locator| { + stack.get_external_stack(locator.program_id())?.program().get_struct(locator.resource()).cloned() + }; + // Get the size in bits of the operand. let size_in_bits = match VARIANT { - 0 => self.destination_type.size_in_bits(&get_struct)?, - 1 => self.destination_type.size_in_bits_raw(&get_struct)?, + 0 => self.destination_type.size_in_bits(&get_struct, &get_external_struct)?, + 1 => self.destination_type.size_in_bits_raw(&get_struct, &get_external_struct)?, variant => bail!("Invalid `deserialize` variant '{variant}'"), }; @@ -666,7 +694,8 @@ impl DeserializeInstruction { ); // Deserialize the bits into the desired literal type. - let output = execute_deserialize_internal(VARIANT, &bits, &self.destination_type, &get_struct, 0)?; + let output = + execute_deserialize_internal(VARIANT, &bits, &self.destination_type, &get_struct, &get_external_struct, 0)?; // Store the output. registers.store_circuit(stack, &self.destination, circuit::Value::Plaintext(output)) @@ -701,10 +730,15 @@ impl DeserializeInstruction { // A helper to get a struct declaration. let get_struct = |identifier: &Identifier| stack.program().get_struct(identifier).cloned(); + // A helper to get an external struct declaration. + let get_external_struct = |locator: &Locator| { + stack.get_external_stack(locator.program_id())?.program().get_struct(locator.resource()).cloned() + }; + // Get the size in bits of the operand. let size_in_bits = match VARIANT { - 0 => self.destination_type.size_in_bits(&get_struct)?, - 1 => self.destination_type.size_in_bits_raw(&get_struct)?, + 0 => self.destination_type.size_in_bits(&get_struct, &get_external_struct)?, + 1 => self.destination_type.size_in_bits_raw(&get_struct, &get_external_struct)?, variant => bail!("Invalid `deserialize` variant '{variant}'"), }; diff --git a/synthesizer/program/src/logic/instruction/operation/ecdsa_verify.rs b/synthesizer/program/src/logic/instruction/operation/ecdsa_verify.rs index 5423fcfb19..629da9d814 100644 --- a/synthesizer/program/src/logic/instruction/operation/ecdsa_verify.rs +++ b/synthesizer/program/src/logic/instruction/operation/ecdsa_verify.rs @@ -437,6 +437,11 @@ impl ECDSAVerify { // A helper to get a struct declaration. let get_struct = |identifier: &Identifier| stack.program().get_struct(identifier).cloned(); + // A helper to get an external struct declaration. + let get_external_struct = |locator: &Locator| { + stack.get_external_stack(locator.program_id())?.program().get_struct(locator.resource()).cloned() + }; + // A helper to get a record declaration. let get_record = |identifier: &Identifier| stack.program().get_record(identifier).cloned(); @@ -466,8 +471,20 @@ impl ECDSAVerify { // Get the size in bits of the message. let size_in_bits = match variant.is_raw() { - false => input_types[2].size_in_bits(&get_struct, &get_record, &get_external_record, &get_future)?, - true => input_types[2].size_in_bits_raw(&get_struct, &get_record, &get_external_record, &get_future)?, + false => input_types[2].size_in_bits( + &get_struct, + &get_external_struct, + &get_record, + &get_external_record, + &get_future, + )?, + true => input_types[2].size_in_bits_raw( + &get_struct, + &get_external_struct, + &get_record, + &get_external_record, + &get_future, + )?, }; // Check the number of bits. ensure!( diff --git a/synthesizer/program/src/logic/instruction/operation/hash.rs b/synthesizer/program/src/logic/instruction/operation/hash.rs index 7f9ac95aec..fd7dc12a16 100644 --- a/synthesizer/program/src/logic/instruction/operation/hash.rs +++ b/synthesizer/program/src/logic/instruction/operation/hash.rs @@ -670,6 +670,11 @@ impl HashInstruction { // A helper to get a struct declaration. let get_struct = |identifier: &Identifier| stack.program().get_struct(identifier).cloned(); + // A helper to get an external struct declaration. + let get_external_struct = |locator: &Locator| { + stack.get_external_stack(locator.program_id())?.program().get_struct(locator.resource()).cloned() + }; + // A helper to get a record declaration. let get_record = |identifier: &Identifier| stack.program().get_record(identifier).cloned(); @@ -699,8 +704,20 @@ impl HashInstruction { // Get the size in bits. let size_in_bits = match variant.is_raw() { - false => input_types[0].size_in_bits(&get_struct, &get_record, &get_external_record, &get_future)?, - true => input_types[0].size_in_bits_raw(&get_struct, &get_record, &get_external_record, &get_future)?, + false => input_types[0].size_in_bits( + &get_struct, + &get_external_struct, + &get_record, + &get_external_record, + &get_future, + )?, + true => input_types[0].size_in_bits_raw( + &get_struct, + &get_external_struct, + &get_record, + &get_external_record, + &get_future, + )?, }; // Check the number of bits. ensure!( diff --git a/synthesizer/program/src/logic/instruction/operation/serialize.rs b/synthesizer/program/src/logic/instruction/operation/serialize.rs index 6cfb25baa4..63026adb32 100644 --- a/synthesizer/program/src/logic/instruction/operation/serialize.rs +++ b/synthesizer/program/src/logic/instruction/operation/serialize.rs @@ -16,7 +16,7 @@ use crate::{Opcode, Operand, RegistersCircuit, RegistersTrait, StackTrait}; use console::{ network::prelude::*, - program::{ArrayType, Identifier, LiteralType, Plaintext, PlaintextType, Register, RegisterType, Value}, + program::{ArrayType, Identifier, LiteralType, Locator, Plaintext, PlaintextType, Register, RegisterType, Value}, }; /// Serializes the bits of the input. @@ -299,10 +299,15 @@ impl SerializeInstruction { // A helper to get a struct declaration. let get_struct = |identifier: &Identifier| stack.program().get_struct(identifier).cloned(); + // A helper to get an external struct declaration. + let get_external_struct = |locator: &Locator| { + stack.get_external_stack(locator.program_id())?.program().get_struct(locator.resource()).cloned() + }; + // Get the size in bits of the operand. let size_in_bits = match VARIANT { - 0 => self.operand_type.size_in_bits(&get_struct)?, - 1 => self.operand_type.size_in_bits_raw(&get_struct)?, + 0 => self.operand_type.size_in_bits(&get_struct, &get_external_struct)?, + 1 => self.operand_type.size_in_bits_raw(&get_struct, &get_external_struct)?, variant => bail!("Invalid `serialize` variant '{variant}'"), }; diff --git a/synthesizer/program/tests/instruction/deserialize.rs b/synthesizer/program/tests/instruction/deserialize.rs index cd65d78f36..125b0b560c 100644 --- a/synthesizer/program/tests/instruction/deserialize.rs +++ b/synthesizer/program/tests/instruction/deserialize.rs @@ -19,7 +19,7 @@ use circuit::{AleoV0, Eject}; use console::{ network::MainnetV0, prelude::*, - program::{ArrayType, Identifier, LiteralType, Plaintext, PlaintextType, Register, U32, Value}, + program::{ArrayType, Identifier, LiteralType, Locator, Plaintext, PlaintextType, Register, U32, Value}, }; use snarkvm_synthesizer_process::{Process, Stack}; use snarkvm_synthesizer_program::{ @@ -97,11 +97,12 @@ fn check_deserialize( // Struct definitions are not supported. let fail_get_struct = |_: &Identifier| bail!("structs are not supported"); + let fail_get_external_struct = |_: &Locator| bail!("structs are not supported"); // Get the size in bits. let size_in_bits = match VARIANT { - 0 => type_.size_in_bits(&fail_get_struct).unwrap(), - 1 => type_.size_in_bits_raw(&fail_get_struct).unwrap(), + 0 => type_.size_in_bits(&fail_get_struct, &fail_get_external_struct).unwrap(), + 1 => type_.size_in_bits_raw(&fail_get_struct, &fail_get_external_struct).unwrap(), _ => panic!("Invalid 'deserialize' variant"), }; let size_in_bits = u32::try_from(size_in_bits).unwrap(); diff --git a/synthesizer/program/tests/instruction/serialize.rs b/synthesizer/program/tests/instruction/serialize.rs index a3ecd689b1..f8b6b76b5b 100644 --- a/synthesizer/program/tests/instruction/serialize.rs +++ b/synthesizer/program/tests/instruction/serialize.rs @@ -19,7 +19,7 @@ use circuit::{AleoV0, Eject}; use console::{ network::MainnetV0, prelude::*, - program::{ArrayType, Identifier, LiteralType, PlaintextType, Register, U32, Value}, + program::{ArrayType, Identifier, LiteralType, Locator, PlaintextType, Register, U32, Value}, }; use snarkvm_synthesizer_process::{Process, Stack}; use snarkvm_synthesizer_program::{ @@ -97,11 +97,12 @@ fn check_serialize( // Struct definitions are not supported. let fail_get_struct = |_: &Identifier| bail!("structs are not supported"); + let fail_get_external_struct = |_: &Locator| bail!("structs are not supported"); // Get the size in bits. let size_in_bits = match VARIANT { - 0 => type_.size_in_bits(&fail_get_struct).unwrap(), - 1 => type_.size_in_bits_raw(&fail_get_struct).unwrap(), + 0 => type_.size_in_bits(&fail_get_struct, &fail_get_external_struct).unwrap(), + 1 => type_.size_in_bits_raw(&fail_get_struct, &fail_get_external_struct).unwrap(), _ => panic!("Invalid 'serialize' variant"), }; let size_in_bits = u32::try_from(size_in_bits).unwrap();