diff --git a/.circleci/config.yml b/.circleci/config.yml index 687257eaef..db2dc75286 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1324,7 +1324,7 @@ workflows: or pipeline.git.branch == "canary" or pipeline.git.branch == "testnet" or pipeline.git.branch == "mainnet" - or pipeline.git.branch == "fix/translated_type_tracking" + or pipeline.git.branch == "fix/external_struct_stacks" jobs: - check-unused-dependencies # This can be cleaned up before releases - check-cargo-semver-checks # This can be cleaned up before releases diff --git a/synthesizer/process/src/stack/finalize_types/initialize.rs b/synthesizer/process/src/stack/finalize_types/initialize.rs index b4a9ea8047..05e9aa70e0 100644 --- a/synthesizer/process/src/stack/finalize_types/initialize.rs +++ b/synthesizer/process/src/stack/finalize_types/initialize.rs @@ -199,7 +199,12 @@ impl FinalizeTypes { impl FinalizeTypes { /// Ensure the given input register is well-formed. - fn check_input(&mut self, stack: &Stack, register: &Register, finalize_type: &FinalizeType) -> Result<()> { + pub 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(plaintext_type) => RegisterTypes::check_plaintext_type(stack, plaintext_type)?, @@ -226,7 +231,7 @@ impl FinalizeTypes { /// Ensures the given command is well-formed. #[inline] - fn check_command( + pub fn check_command( &mut self, stack: &Stack, positions: &HashMap, usize>, @@ -986,7 +991,7 @@ impl FinalizeTypes { // 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_)?; + self.matches_struct(stack, stack, instruction.operands(), struct_)?; } CastType::Plaintext(plaintext @ PlaintextType::ExternalStruct(locator)) => { // Ensure that the type is valid. @@ -996,7 +1001,7 @@ impl FinalizeTypes { // 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_)?; + self.matches_struct(stack, &*external_stack, instruction.operands(), struct_)?; } CastType::Plaintext(plaintext @ PlaintextType::Array(array_type)) => { // Ensure that the type is valid. diff --git a/synthesizer/process/src/stack/finalize_types/matches.rs b/synthesizer/process/src/stack/finalize_types/matches.rs index c4f3569c92..e05cc6b3c6 100644 --- a/synthesizer/process/src/stack/finalize_types/matches.rs +++ b/synthesizer/process/src/stack/finalize_types/matches.rs @@ -17,7 +17,15 @@ use super::*; impl FinalizeTypes { /// Checks that the given operands matches the layout of the struct. The ordering of the operands matters. - pub fn matches_struct(&self, stack: &Stack, operands: &[Operand], struct_: &StructType) -> Result<()> { + pub fn matches_struct( + &self, + // The stack of the program containing the instruction which casts the struct + instruction_stack: &Stack, + // The stack of the program where the struct is defined (as local) + definition_stack: &Stack, + operands: &[Operand], + struct_: &StructType, + ) -> Result<()> { // Retrieve the struct name. let struct_name = struct_.name(); // Ensure the struct name is valid. @@ -52,20 +60,20 @@ impl FinalizeTypes { } // Ensure the type of the register matches the member type. Operand::Register(register) => { - // Retrieve the type. - let plaintext_type = match self.get_type(stack, register)? { - // If the register is a plaintext type, return it. - FinalizeType::Plaintext(plaintext_type) => plaintext_type, - // If the register is a future, throw an error. + // Operate depending on the register type. + match self.get_type(instruction_stack, register)? { + // Ensure the register type is not a future. FinalizeType::Future(..) => bail!("Struct member cannot be a future"), - // If the register is a dynamic future, throw an error. + // Ensure the register type is not a dynamic future. FinalizeType::DynamicFuture => bail!("Struct member cannot be a dynamic future"), + // Ensure the plaintext register type matches the member type. + FinalizeType::Plaintext(plaintext_type) => { + ensure!( + types_equivalent(instruction_stack, &plaintext_type, definition_stack, member_type)?, + "Struct member '{struct_name}.{member_name}' expects a '{member_type}', but found a '{plaintext_type}' in the operand '{operand}'.", + ) + } }; - // Ensure the register type matches the member type. - ensure!( - 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}'.", - ) } // Ensure the program ID, block height, block timestamp, network ID, generator, checksum, edition, program owner, and component checksum types matches the member type. Operand::ProgramID(..) @@ -79,7 +87,9 @@ impl FinalizeTypes { | Operand::ProgramOwner(_) | Operand::ComponentChecksum(..) => { // Retrieve the operand type. - let FinalizeType::Plaintext(program_ref_type) = self.get_type_from_operand(stack, operand)? else { + let FinalizeType::Plaintext(program_ref_type) = + self.get_type_from_operand(instruction_stack, operand)? + else { bail!( "Expected a plaintext type for the operand '{operand}' in struct member '{struct_name}.{member_name}'" ) diff --git a/synthesizer/process/src/stack/finalize_types/mod.rs b/synthesizer/process/src/stack/finalize_types/mod.rs index a145317bde..0f523d9834 100644 --- a/synthesizer/process/src/stack/finalize_types/mod.rs +++ b/synthesizer/process/src/stack/finalize_types/mod.rs @@ -74,6 +74,12 @@ pub struct FinalizeTypes { } impl FinalizeTypes { + /// Initializes a new empty instance of `FinalizeTypes`. + #[inline] + pub fn new() -> Self { + Self { inputs: IndexMap::new(), destinations: IndexMap::new() } + } + /// Initializes a new instance of `FinalizeTypes` for the given constructor. /// Checks that the given constructor is well-formed for the given stack. #[inline] diff --git a/synthesizer/process/src/stack/register_types/matches.rs b/synthesizer/process/src/stack/register_types/matches.rs index 73cd1ce529..eba3466ab5 100644 --- a/synthesizer/process/src/stack/register_types/matches.rs +++ b/synthesizer/process/src/stack/register_types/matches.rs @@ -19,8 +19,10 @@ impl RegisterTypes { /// Checks that the given operands matches the layout of the struct. The ordering of the operands matters. pub fn matches_struct( &self, - operands_stack: &Stack, - stack: &Stack, + // The stack of the program containing the instruction which casts the struct + instruction_stack: &Stack, + // The stack of the program where the struct is defined (as local) + definition_stack: &Stack, operands: &[Operand], struct_: &StructType, ) -> Result<()> { @@ -51,14 +53,15 @@ impl RegisterTypes { // Ensure the literal type matches the member type. Operand::Literal(literal) => { ensure!( + // 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.", ) } // Ensure the register type matches the member type. Operand::Register(register) => { - // Retrieve the register type. - match self.get_type(operands_stack, register)? { + // Operate depending on the register type. + match self.get_type(instruction_stack, register)? { // Ensure the register type is not a record. RegisterType::ExternalRecord(..) | RegisterType::Record(..) => { bail!("Casting a record into a struct entry is illegal") @@ -76,10 +79,10 @@ impl RegisterTypes { bail!("Casting a dynamic future into a struct entry is illegal") } // Ensure the register type matches the member type. - RegisterType::Plaintext(type_) => { + RegisterType::Plaintext(plaintext_type) => { ensure!( - types_equivalent(operands_stack, &type_, stack, member_type)?, - "Struct entry '{struct_name}.{member_name}' expects a '{member_type}', but found '{type_}' in the operand '{operand}'.", + types_equivalent(instruction_stack, &plaintext_type, definition_stack, member_type)?, + "Struct member '{struct_name}.{member_name}' expects a '{member_type}', but found a '{plaintext_type}' in the operand '{operand}'.", ) } } @@ -87,14 +90,16 @@ impl RegisterTypes { // Ensure the program ID, signer, and caller types match the member type. Operand::ProgramID(..) | Operand::Signer | Operand::Caller => { // Retrieve the operand type. - let RegisterType::Plaintext(operand_type) = self.get_type_from_operand(stack, operand)? else { + let RegisterType::Plaintext(operand_type) = + self.get_type_from_operand(instruction_stack, operand)? + else { bail!( "Expected a plaintext type for the operand '{operand}' in struct member '{struct_name}.{member_name}'" ) }; // Ensure the operand type matches the member type. ensure!( - types_equivalent(stack, &operand_type, stack, member_type)?, + types_equivalent(instruction_stack, &operand_type, definition_stack, member_type)?, "Struct member '{struct_name}.{member_name}' expects {member_type}, but found '{operand_type}' in the operand '{operand}'.", ) } diff --git a/synthesizer/program/src/lib.rs b/synthesizer/program/src/lib.rs index 9aa0f2b52e..ccb98d0bc6 100644 --- a/synthesizer/program/src/lib.rs +++ b/synthesizer/program/src/lib.rs @@ -1060,6 +1060,25 @@ impl ProgramCore { || self.views.values().any(|view| view.contains_external_struct()) } + /// Returns `true` if any of the finalize-type (finalize, constructor and view) contains a cast + /// to an external struct. + #[inline] + pub fn finalize_casts_external_struct(&self) -> bool { + // Check every finalize-type scope: function finalize blocks, the constructor, and views. + self.functions + .values() + .filter_map(|function| function.finalize_logic()) + .flat_map(|finalize| finalize.commands()) + .chain(self.constructor.iter().flat_map(|constructor| constructor.commands())) + .chain(self.views.values().flat_map(|view| view.commands())) + .any(|command| { + matches!(command, Command::Instruction(instruction) + if matches!(instruction, Instruction::Cast(cast) + if matches!(cast.cast_type(), CastType::Plaintext(PlaintextType::ExternalStruct(..)) + ))) + }) + } + /// Returns `true` if this program violates pre-V13 rules for external records /// or futures by containing registers with non-local struct types across program boundaries. /// diff --git a/synthesizer/program/src/traits/stack_and_registers.rs b/synthesizer/program/src/traits/stack_and_registers.rs index 3a33bd4c76..3f8c6769c8 100644 --- a/synthesizer/program/src/traits/stack_and_registers.rs +++ b/synthesizer/program/src/traits/stack_and_registers.rs @@ -238,9 +238,13 @@ pub fn register_types_equivalent( /// The stacks are passed because struct types need to access their stack to get their /// structure. pub fn types_equivalent( + // The first stack stack0: &impl StackTrait, + // The first type (from the perspective of the first stack) type0: &PlaintextType, + // The second stack stack1: &impl StackTrait, + // The second type (from the perspective of the second stack) type1: &PlaintextType, ) -> Result { use PlaintextType::*; diff --git a/synthesizer/src/vm/tests/test_v19/external_struct_stacks.rs b/synthesizer/src/vm/tests/test_v19/external_struct_stacks.rs new file mode 100644 index 0000000000..22ceada471 --- /dev/null +++ b/synthesizer/src/vm/tests/test_v19/external_struct_stacks.rs @@ -0,0 +1,1122 @@ +// Copyright (c) 2019-2026 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::*; + +// Checks that various instances of casts to external structs or from using external-structs members +// function as expected. This is tested in both the function (i.e. private, RegisterType) setting as +// well as the public (i.e. finalise, FinaliseType) one. +#[test] +fn test_cast_with_external_structs_only() { + let rng = &mut TestRng::default(); + + let deployer_private_key = sample_genesis_private_key(rng); + + let program_a = Program::from_str( + r" + program program_a.aleo; + + struct struct_a: + val as u8; + + struct struct_d: + a as address; + b as address; + c as address; + + function noop: + + constructor: + assert.eq edition 0u16; + ", + ) + .unwrap(); + + let program_b = Program::from_str( + r" + import program_a.aleo; + program program_b.aleo; + + struct struct_b: + a as program_a.aleo/struct_a; + + function noop: + + constructor: + assert.eq edition 0u16; + ", + ) + .unwrap(); + + // This is a distinct program from program_b.aleo, but both declare a struct with the same name + // and member specification. + let program_b2 = Program::from_str( + r" + import program_a.aleo; + program program_b2.aleo; + + struct struct_b: + a as program_a.aleo/struct_a; + + function noop: + + constructor: + assert.eq edition 0u16; + ", + ) + .unwrap(); + + let program_c = Program::from_str( + r" + import program_a.aleo; + import program_b.aleo; + import program_b2.aleo; + program program_c.aleo; + + struct struct_c: + a as program_a.aleo/struct_a; + b as program_b.aleo/struct_b; + + struct struct_e: + a as address; + b as address; + c as address; + + // Case 1 / function: The function casts external structs defined in program_a and program_b, then casts a struct_c. + function run_1: + input r0 as u8.private; + + cast r0 into r1 as program_a.aleo/struct_a; + cast r1 into r2 as program_b.aleo/struct_b; + cast r1 r2 into r3 as struct_c; + + output r3 as struct_c.private; + + // Case 2 / function: Similar to case 1 but with a cast involving a external-struct member read (r1.val) into an external-struct. + function run_2: + input r0 as u8.private; + + cast r0 into r1 as program_a.aleo/struct_a; + cast r1.val into r2 as program_a.aleo/struct_a; + cast r2 into r3 as program_b.aleo/struct_b; + cast r2 r3 into r4 as struct_c; + + output r4 as struct_c.private; + + // Case 3 / function: Here struct_c is cast from an external-struct member received as an argument and + // an external-struct member cast inside the function itself. + function run_3: + input r0 as program_a.aleo/struct_a.private; + + cast r0 into r1 as program_b.aleo/struct_b; + cast r0 r1 into r2 as struct_c; + + output r2 as struct_c.private; + + // Case 4 / function: Similar to case 2, but the target of the cast is the local struct_c. + // (`r1.a`) of the received external struct. + function run_4: + input r0 as program_a.aleo/struct_a.private; + + cast r0 into r1 as program_b.aleo/struct_b; + cast r1.a r1 into r2 as struct_c; + + output r2 as struct_c.private; + + // Case 5 / function: Here the same external struct struct_a is cast into a program_b/struct_b and a program_b2/struct_b. + function run_5: + input r0 as program_a.aleo/struct_a.private; + + cast r0 into r1 as program_b.aleo/struct_b; + cast r0 into r2 as program_b2.aleo/struct_b; + cast r2.a r1 into r3 as struct_c; + + output r3 as struct_c.private; + + + // Case 6 / function: Tests casts involving the special, private-side-only operands signer, caller and program_id. + function run_6: + cast self.signer self.caller program_a.aleo into r0 as program_a.aleo/struct_d; + cast r0.a r0.b r0.c into r1 as struct_e; + output r1 as struct_e.private; + + // Case 7 / function: Tests a three-level struct access involving external structs. + function run_7: + cast 24u8 into r0 as program_a.aleo/struct_a; + cast r0 into r1 as program_b.aleo/struct_b; + cast r0 r1 into r2 as struct_c; + cast r2.b.a.val into r3 as u8; + + output r3 as u8.private; + + // Case 8 / function: Tests a casts to external struct one of whose operands involves a two-level struct access. + function run_8: + cast 24u8 into r0 as program_a.aleo/struct_a; + cast r0 into r1 as program_b.aleo/struct_b; + cast r0 r1 into r2 as struct_c; + cast r2.b.a into r3 as program_b2.aleo/struct_b; + + output r3 as program_b2.aleo/struct_b.private; + + // Case 9 / finalize: Finalize-side mirror of case 1. + function run_9: + input r0 as u8.public; + async run_9 r0 into r1; + output r1 as program_c.aleo/run_9.future; + + finalize run_9: + input r0 as u8.public; + + cast r0 into r1 as program_a.aleo/struct_a; + cast r1 into r2 as program_b.aleo/struct_b; + cast r1 r2 into r3 as struct_c; + + // Case 10 / finalize: Finalize-side mirror of case 2. + function run_10: + input r0 as u8.public; + async run_10 r0 into r1; + output r1 as program_c.aleo/run_10.future; + + finalize run_10: + input r0 as u8.public; + + cast r0 into r1 as program_a.aleo/struct_a; + cast r1.val into r2 as program_a.aleo/struct_a; + cast r2 into r3 as program_b.aleo/struct_b; + cast r2 r3 into r4 as struct_c; + + // Case 11 / finalize: Finalize-side mirror of case 3. + function run_11: + input r0 as program_a.aleo/struct_a.public; + async run_11 r0 into r1; + output r1 as program_c.aleo/run_11.future; + + finalize run_11: + input r0 as program_a.aleo/struct_a.public; + + cast r0 into r1 as program_b.aleo/struct_b; + cast r0 r1 into r2 as struct_c; + + // Case 12 / finalize: Finalize-side mirror of case 4. + function run_12: + input r0 as program_a.aleo/struct_a.public; + async run_12 r0 into r1; + output r1 as program_c.aleo/run_12.future; + + finalize run_12: + input r0 as program_a.aleo/struct_a.public; + + cast r0 into r1 as program_b.aleo/struct_b; + cast r1.a r1 into r2 as struct_c; + + // Case 13 / finalize: Finalize-side mirror of case 5. + function run_13: + input r0 as program_a.aleo/struct_a.public; + async run_13 r0 into r1; + output r1 as program_c.aleo/run_13.future; + + finalize run_13: + input r0 as program_a.aleo/struct_a.public; + + cast r0 into r1 as program_b.aleo/struct_b; + cast r0 into r2 as program_b2.aleo/struct_b; + cast r2.a r1 into r3 as struct_c; + + // Case 14 / finalize: Finalize-side mirror of case 6. + function run_14: + async run_14 into r0; + output r0 as program_c.aleo/run_14.future; + + finalize run_14: + cast program_a.aleo program_b.aleo program_c.aleo into r0 as program_a.aleo/struct_d; + cast r0.a r0.b r0.c into r1 as struct_e; + + // Case 15 / finalize: Finalize-side mirror of case 7. + function run_15: + async run_15 into r0; + output r0 as program_c.aleo/run_15.future; + + finalize run_15: + cast 24u8 into r0 as program_a.aleo/struct_a; + cast r0 into r1 as program_b.aleo/struct_b; + cast r0 r1 into r2 as struct_c; + cast r2.b.a.val into r3 as u8; + + // Case 16 / finalize: Finalize-side mirror of case 8. + function run_16: + async run_16 into r0; + output r0 as program_c.aleo/run_16.future; + + finalize run_16: + cast 24u8 into r0 as program_a.aleo/struct_a; + cast r0 into r1 as program_b.aleo/struct_b; + cast r0 r1 into r2 as struct_c; + cast r2.b.a into r3 as program_b2.aleo/struct_b; + + constructor: + assert.eq edition 0u16; + ", + ).unwrap(); + + let vm = sample_vm_at_height(CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V19).unwrap(), rng); + + let transaction = vm.deploy(&deployer_private_key, &program_a, None, 0, None, rng).unwrap(); + add_and_test_with_costs(&vm, &deployer_private_key, None, &[transaction], rng); + + let transaction = vm.deploy(&deployer_private_key, &program_b, None, 0, None, rng).unwrap(); + add_and_test_with_costs(&vm, &deployer_private_key, None, &[transaction], rng); + + let transaction = vm.deploy(&deployer_private_key, &program_b2, None, 0, None, rng).unwrap(); + add_and_test_with_costs(&vm, &deployer_private_key, None, &[transaction], rng); + + let transaction = vm.deploy(&deployer_private_key, &program_c, None, 0, None, rng).unwrap(); + add_and_test_with_costs(&vm, &deployer_private_key, None, &[transaction], rng); + + let struct_a = "{ val: 5u8 }"; + + // The cases are documented in the source of program_c.aleo above. + + let inputs = [Value::from_str("5u8").unwrap()]; + let transaction = + vm.execute(&deployer_private_key, ("program_c.aleo", "run_1"), inputs.iter(), None, 0, None, rng).unwrap(); + add_and_test_with_costs(&vm, &deployer_private_key, Some(&[&inputs]), &[transaction], rng); + + let inputs = [Value::from_str("5u8").unwrap()]; + let transaction = + vm.execute(&deployer_private_key, ("program_c.aleo", "run_2"), inputs.iter(), None, 0, None, rng).unwrap(); + add_and_test_with_costs(&vm, &deployer_private_key, Some(&[&inputs]), &[transaction], rng); + + let inputs = [Value::from_str(struct_a).unwrap()]; + let transaction = + vm.execute(&deployer_private_key, ("program_c.aleo", "run_3"), inputs.iter(), None, 0, None, rng).unwrap(); + add_and_test_with_costs(&vm, &deployer_private_key, Some(&[&inputs]), &[transaction], rng); + + let inputs = [Value::from_str(struct_a).unwrap()]; + let transaction = + vm.execute(&deployer_private_key, ("program_c.aleo", "run_4"), inputs.iter(), None, 0, None, rng).unwrap(); + add_and_test_with_costs(&vm, &deployer_private_key, Some(&[&inputs]), &[transaction], rng); + + let inputs = [Value::from_str(struct_a).unwrap()]; + let transaction = + vm.execute(&deployer_private_key, ("program_c.aleo", "run_5"), inputs.iter(), None, 0, None, rng).unwrap(); + add_and_test_with_costs(&vm, &deployer_private_key, Some(&[&inputs]), &[transaction], rng); + + let transaction = vm + .execute(&deployer_private_key, ("program_c.aleo", "run_6"), Vec::>::new().iter(), None, 0, None, rng) + .unwrap(); + add_and_test_with_costs(&vm, &deployer_private_key, Some(&[&[]]), &[transaction], rng); + + let transaction = vm + .execute(&deployer_private_key, ("program_c.aleo", "run_7"), Vec::>::new().iter(), None, 0, None, rng) + .unwrap(); + add_and_test_with_costs(&vm, &deployer_private_key, Some(&[&[]]), &[transaction], rng); + + let transaction = vm + .execute(&deployer_private_key, ("program_c.aleo", "run_8"), Vec::>::new().iter(), None, 0, None, rng) + .unwrap(); + add_and_test_with_costs(&vm, &deployer_private_key, Some(&[&[]]), &[transaction], rng); + + let inputs = [Value::from_str("5u8").unwrap()]; + let transaction = + vm.execute(&deployer_private_key, ("program_c.aleo", "run_9"), inputs.iter(), None, 0, None, rng).unwrap(); + add_and_test_with_costs(&vm, &deployer_private_key, Some(&[&inputs]), &[transaction], rng); + + let inputs = [Value::from_str("5u8").unwrap()]; + let transaction = + vm.execute(&deployer_private_key, ("program_c.aleo", "run_10"), inputs.iter(), None, 0, None, rng).unwrap(); + add_and_test_with_costs(&vm, &deployer_private_key, Some(&[&inputs]), &[transaction], rng); + + let inputs = [Value::from_str(struct_a).unwrap()]; + let transaction = + vm.execute(&deployer_private_key, ("program_c.aleo", "run_11"), inputs.iter(), None, 0, None, rng).unwrap(); + add_and_test_with_costs(&vm, &deployer_private_key, Some(&[&inputs]), &[transaction], rng); + + let inputs = [Value::from_str(struct_a).unwrap()]; + let transaction = + vm.execute(&deployer_private_key, ("program_c.aleo", "run_12"), inputs.iter(), None, 0, None, rng).unwrap(); + add_and_test_with_costs(&vm, &deployer_private_key, Some(&[&inputs]), &[transaction], rng); + + let inputs = [Value::from_str(struct_a).unwrap()]; + let transaction = + vm.execute(&deployer_private_key, ("program_c.aleo", "run_13"), inputs.iter(), None, 0, None, rng).unwrap(); + add_and_test_with_costs(&vm, &deployer_private_key, Some(&[&inputs]), &[transaction], rng); + + let transaction = vm + .execute(&deployer_private_key, ("program_c.aleo", "run_14"), Vec::>::new().iter(), None, 0, None, rng) + .unwrap(); + add_and_test_with_costs(&vm, &deployer_private_key, Some(&[&[]]), &[transaction], rng); + + let transaction = vm + .execute(&deployer_private_key, ("program_c.aleo", "run_15"), Vec::>::new().iter(), None, 0, None, rng) + .unwrap(); + add_and_test_with_costs(&vm, &deployer_private_key, Some(&[&[]]), &[transaction], rng); + + let transaction = vm + .execute(&deployer_private_key, ("program_c.aleo", "run_16"), Vec::>::new().iter(), None, 0, None, rng) + .unwrap(); + add_and_test_with_costs(&vm, &deployer_private_key, Some(&[&[]]), &[transaction], rng); +} + +// Checks that various instances of casts to arrays involving external structs and their members, as +// well as structs having arrays as members, work as expected. This is tested in both the function +// (i.e. private, RegisterType) setting as well as the public (i.e. finalise, FinaliseType) one. +#[test] +fn test_cast_with_external_structs_in_arrays() { + let rng = &mut TestRng::default(); + + let deployer_private_key = sample_genesis_private_key(rng); + + let program_a = Program::from_str( + r" + program program_a.aleo; + + struct struct_a: + val as u8; + + function noop: + + constructor: + assert.eq edition 0u16; + ", + ) + .unwrap(); + + let program_b = Program::from_str( + r" + import program_a.aleo; + program program_b.aleo; + + struct struct_b: + a as program_a.aleo/struct_a; + + function noop: + + constructor: + assert.eq edition 0u16; + ", + ) + .unwrap(); + + // This is a distinct program from program_b.aleo, but both declare a struct with the same name + // and member specification. + let program_b2 = Program::from_str( + r" + import program_a.aleo; + program program_b2.aleo; + + struct struct_b: + a as program_a.aleo/struct_a; + + function noop: + + constructor: + assert.eq edition 0u16; + ", + ) + .unwrap(); + + let program_c = Program::from_str( + r" + import program_a.aleo; + import program_b.aleo; + import program_b2.aleo; + program program_c.aleo; + + struct struct_c: + a as program_a.aleo/struct_a; + b as program_b.aleo/struct_b; + + // struct containing an array whose elements are external structs. + struct struct_arr: + arr as [program_a.aleo/struct_a; 2u32]; + + // Case 1 / function: Cast an [struct_c; 2] from a register holding a struct_c. + function fun_1: + input r0 as u8.private; + + cast r0 into r1 as program_a.aleo/struct_a; + cast r1 into r2 as program_b.aleo/struct_b; + cast r1 r2 into r3 as struct_c; + cast r3 r3 into r4 as [struct_c; 2u32]; + + output r4 as [struct_c; 2u32].private; + + // Case 2 / function: Cast a [program_b.aleo/struct_b; 2] one of whose elements involves a + // read of an external struct from a local struct. + function fun_2: + input r0 as u8.private; + + cast r0 into r1 as program_a.aleo/struct_a; + cast r1 into r2 as program_b.aleo/struct_b; + cast r1 r2 into r3 as struct_c; + cast r2 r3.b into r4 as [program_b.aleo/struct_b; 2u32]; + + output r4 as [program_b.aleo/struct_b; 2u32].private; + + // Case 3 / function: Cast a [program_a.aleo/struct_a; 5] from elements involving various levels of nested reads. + function fun_3: + input r0 as u8.private; + + cast r0 into r1 as program_a.aleo/struct_a; + cast r1 into r2 as program_b.aleo/struct_b; + cast r1 into r3 as program_b2.aleo/struct_b; + cast r1 r2 into r4 as struct_c; + cast r1 r2.a r3.a r4.a r4.b.a into r5 as [program_a.aleo/struct_a; 5u32]; + + output r5 as [program_a.aleo/struct_a; 5u32].private; + + // Case 4 / function: Cast a local struct containing an array of external structs. + function fun_4: + input r0 as u8.private; + + cast r0 into r1 as program_a.aleo/struct_a; + cast r1 r1 into r2 as [program_a.aleo/struct_a; 2u32]; + cast r2 into r3 as struct_arr; + + output r3 as struct_arr.private; + + // Case 5 / finalize: Finalize-side mirror of case 1. + function fun_5: + input r0 as u8.public; + async fun_5 r0 into r1; + output r1 as program_c.aleo/fun_5.future; + + finalize fun_5: + input r0 as u8.public; + + cast r0 into r1 as program_a.aleo/struct_a; + cast r1 into r2 as program_b.aleo/struct_b; + cast r1 r2 into r3 as struct_c; + cast r3 r3 into r4 as [struct_c; 2u32]; + + // Case 6 / finalize: Finalize-side mirror of case 2. + function fun_6: + input r0 as u8.public; + async fun_6 r0 into r1; + output r1 as program_c.aleo/fun_6.future; + + finalize fun_6: + input r0 as u8.public; + + cast r0 into r1 as program_a.aleo/struct_a; + cast r1 into r2 as program_b.aleo/struct_b; + cast r1 r2 into r3 as struct_c; + cast r2 r3.b into r4 as [program_b.aleo/struct_b; 2u32]; + + // Case 7 / finalize: Finalize-side mirror of case 3. + function fun_7: + input r0 as u8.public; + async fun_7 r0 into r1; + output r1 as program_c.aleo/fun_7.future; + + finalize fun_7: + input r0 as u8.public; + + cast r0 into r1 as program_a.aleo/struct_a; + cast r1 into r2 as program_b.aleo/struct_b; + cast r1 into r3 as program_b2.aleo/struct_b; + cast r1 r2 into r4 as struct_c; + cast r1 r2.a r3.a r4.a r4.b.a into r5 as [program_a.aleo/struct_a; 5u32]; + + // Case 8 / finalize: Finalize-side mirror of case 4. + function fun_8: + input r0 as u8.public; + async fun_8 r0 into r1; + output r1 as program_c.aleo/fun_8.future; + + finalize fun_8: + input r0 as u8.public; + + cast r0 into r1 as program_a.aleo/struct_a; + cast r1 r1 into r2 as [program_a.aleo/struct_a; 2u32]; + cast r2 into r3 as struct_arr; + + constructor: + assert.eq edition 0u16; + ", + ).unwrap(); + + let vm = sample_vm_at_height(CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V19).unwrap(), rng); + + let transaction = vm.deploy(&deployer_private_key, &program_a, None, 0, None, rng).unwrap(); + add_and_test_with_costs(&vm, &deployer_private_key, None, &[transaction], rng); + + let transaction = vm.deploy(&deployer_private_key, &program_b, None, 0, None, rng).unwrap(); + add_and_test_with_costs(&vm, &deployer_private_key, None, &[transaction], rng); + + let transaction = vm.deploy(&deployer_private_key, &program_b2, None, 0, None, rng).unwrap(); + add_and_test_with_costs(&vm, &deployer_private_key, None, &[transaction], rng); + + let transaction = vm.deploy(&deployer_private_key, &program_c, None, 0, None, rng).unwrap(); + add_and_test_with_costs(&vm, &deployer_private_key, None, &[transaction], rng); + + // The cases are documented in the source of program_c.aleo above. + + let inputs = [Value::from_str("5u8").unwrap()]; + let transaction = + vm.execute(&deployer_private_key, ("program_c.aleo", "fun_1"), inputs.iter(), None, 0, None, rng).unwrap(); + add_and_test_with_costs(&vm, &deployer_private_key, Some(&[&inputs]), &[transaction], rng); + + let inputs = [Value::from_str("5u8").unwrap()]; + let transaction = + vm.execute(&deployer_private_key, ("program_c.aleo", "fun_2"), inputs.iter(), None, 0, None, rng).unwrap(); + add_and_test_with_costs(&vm, &deployer_private_key, Some(&[&inputs]), &[transaction], rng); + + let inputs = [Value::from_str("5u8").unwrap()]; + let transaction = + vm.execute(&deployer_private_key, ("program_c.aleo", "fun_3"), inputs.iter(), None, 0, None, rng).unwrap(); + add_and_test_with_costs(&vm, &deployer_private_key, Some(&[&inputs]), &[transaction], rng); + + let inputs = [Value::from_str("5u8").unwrap()]; + let transaction = + vm.execute(&deployer_private_key, ("program_c.aleo", "fun_4"), inputs.iter(), None, 0, None, rng).unwrap(); + add_and_test_with_costs(&vm, &deployer_private_key, Some(&[&inputs]), &[transaction], rng); + + let inputs = [Value::from_str("5u8").unwrap()]; + let transaction = + vm.execute(&deployer_private_key, ("program_c.aleo", "fun_5"), inputs.iter(), None, 0, None, rng).unwrap(); + add_and_test_with_costs(&vm, &deployer_private_key, Some(&[&inputs]), &[transaction], rng); + + let inputs = [Value::from_str("5u8").unwrap()]; + let transaction = + vm.execute(&deployer_private_key, ("program_c.aleo", "fun_6"), inputs.iter(), None, 0, None, rng).unwrap(); + add_and_test_with_costs(&vm, &deployer_private_key, Some(&[&inputs]), &[transaction], rng); + + let inputs = [Value::from_str("5u8").unwrap()]; + let transaction = + vm.execute(&deployer_private_key, ("program_c.aleo", "fun_7"), inputs.iter(), None, 0, None, rng).unwrap(); + add_and_test_with_costs(&vm, &deployer_private_key, Some(&[&inputs]), &[transaction], rng); + + let inputs = [Value::from_str("5u8").unwrap()]; + let transaction = + vm.execute(&deployer_private_key, ("program_c.aleo", "fun_8"), inputs.iter(), None, 0, None, rng).unwrap(); + add_and_test_with_costs(&vm, &deployer_private_key, Some(&[&inputs]), &[transaction], rng); +} + +// Checks that various instances of casts to records involving external structs and their members +// behave as expected. Tests only involve the function (i.e. non-finalize) scope, which records are +// restricted to. +#[test] +fn test_cast_with_external_structs_in_records() { + let rng = &mut TestRng::default(); + + let deployer_private_key = sample_genesis_private_key(rng); + + let program_a = Program::from_str( + r" + program program_a.aleo; + + struct struct_a: + val as u8; + + function noop: + + constructor: + assert.eq edition 0u16; + ", + ) + .unwrap(); + + let program_b = Program::from_str( + r" + import program_a.aleo; + program program_b.aleo; + + struct struct_b: + a as program_a.aleo/struct_a; + + function noop: + + constructor: + assert.eq edition 0u16; + ", + ) + .unwrap(); + + // This is a distinct program from program_b.aleo, but both declare a struct with the same name + // and member specification. + let program_b2 = Program::from_str( + r" + import program_a.aleo; + program program_b2.aleo; + + struct struct_b: + a as program_a.aleo/struct_a; + + function noop: + + constructor: + assert.eq edition 0u16; + ", + ) + .unwrap(); + + let program_c = Program::from_str( + r" + import program_a.aleo; + import program_b.aleo; + import program_b2.aleo; + program program_c.aleo; + + struct struct_c: + a as program_a.aleo/struct_a; + b as program_b.aleo/struct_b; + + record record_c: + owner as address.private; + a as struct_c.private; + b as struct_c.private; + + record record_b: + owner as address.private; + a as program_b.aleo/struct_b.private; + b as program_b.aleo/struct_b.private; + + record record_a: + owner as address.private; + a as program_a.aleo/struct_a.private; + b as program_a.aleo/struct_a.private; + c as program_a.aleo/struct_a.private; + d as program_a.aleo/struct_a.private; + e as program_a.aleo/struct_a.private; + + record record_mixed: + owner as address.private; + entry_1 as program_a.aleo/struct_a.private; + entry_2 as program_b.aleo/struct_b.private; + entry_3 as program_b2.aleo/struct_b.private; + entry_4 as struct_c.private; + + // Case 1: Cast a record_c with two entries from a register containing a local struct_c. + function fun_1: + input r0 as u8.private; + + cast r0 into r1 as program_a.aleo/struct_a; + cast r1 into r2 as program_b.aleo/struct_b; + cast r1 r2 into r3 as struct_c; + cast self.signer r3 r3 into r4 as record_c.record; + + output r4 as record_c.record; + + // Case 2: Cast a record_b where the first entry is a register holding an external struct_b + // and the second is a struct_b read from a register holding a struct_c (`r3.b`). + function fun_2: + input r0 as u8.private; + + cast r0 into r1 as program_a.aleo/struct_a; + cast r1 into r2 as program_b.aleo/struct_b; + cast r1 r2 into r3 as struct_c; + cast self.signer r2 r3.b into r4 as record_b.record; + + output r4 as record_b.record; + + // Case 3: Cast a record_a whose five entries come from five different sources involving + // various reads. + function fun_3: + input r0 as u8.private; + + cast r0 into r1 as program_a.aleo/struct_a; + cast r1 into r2 as program_b.aleo/struct_b; + cast r1 into r3 as program_b2.aleo/struct_b; + cast r1 r2 into r4 as struct_c; + cast self.signer r1 r2.a r3.a r4.a r4.b.a into r5 as record_a.record; + + output r5 as record_a.record; + + // Case 4: Cast a record_mixed by reading each entry directly from a register of the matching type. + function fun_4: + input r0 as u8.private; + + cast r0 into r1 as program_a.aleo/struct_a; + cast r1 into r2 as program_b.aleo/struct_b; + cast r1 into r3 as program_b2.aleo/struct_b; + cast r1 r2 into r4 as struct_c; + cast self.signer r1 r2 r3 r4 into r5 as record_mixed.record; + + output r5 as record_mixed.record; + + // Case 5: Cast a record_mixed by combining direct registers and register member reads. + function fun_5: + input r0 as u8.private; + + cast r0 into r1 as program_a.aleo/struct_a; + cast r1 into r2 as program_b.aleo/struct_b; + cast r1 into r3 as program_b2.aleo/struct_b; + cast r1 r2 into r4 as struct_c; + cast self.signer r4.a r4.b r3 r4 into r5 as record_mixed.record; + + output r5 as record_mixed.record; + + constructor: + assert.eq edition 0u16; + ", + ) + .unwrap(); + + let vm = sample_vm_at_height(CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V19).unwrap(), rng); + + let transaction = vm.deploy(&deployer_private_key, &program_a, None, 0, None, rng).unwrap(); + add_and_test_with_costs(&vm, &deployer_private_key, None, &[transaction], rng); + + let transaction = vm.deploy(&deployer_private_key, &program_b, None, 0, None, rng).unwrap(); + add_and_test_with_costs(&vm, &deployer_private_key, None, &[transaction], rng); + + let transaction = vm.deploy(&deployer_private_key, &program_b2, None, 0, None, rng).unwrap(); + add_and_test_with_costs(&vm, &deployer_private_key, None, &[transaction], rng); + + let transaction = vm.deploy(&deployer_private_key, &program_c, None, 0, None, rng).unwrap(); + add_and_test_with_costs(&vm, &deployer_private_key, None, &[transaction], rng); + + // The cases are documented in the source of program_c.aleo above. + + let inputs = [Value::from_str("5u8").unwrap()]; + let transaction = + vm.execute(&deployer_private_key, ("program_c.aleo", "fun_1"), inputs.iter(), None, 0, None, rng).unwrap(); + add_and_test_with_costs(&vm, &deployer_private_key, Some(&[&inputs]), &[transaction], rng); + + let inputs = [Value::from_str("5u8").unwrap()]; + let transaction = + vm.execute(&deployer_private_key, ("program_c.aleo", "fun_2"), inputs.iter(), None, 0, None, rng).unwrap(); + add_and_test_with_costs(&vm, &deployer_private_key, Some(&[&inputs]), &[transaction], rng); + + let inputs = [Value::from_str("5u8").unwrap()]; + let transaction = + vm.execute(&deployer_private_key, ("program_c.aleo", "fun_3"), inputs.iter(), None, 0, None, rng).unwrap(); + add_and_test_with_costs(&vm, &deployer_private_key, Some(&[&inputs]), &[transaction], rng); + + let inputs = [Value::from_str("5u8").unwrap()]; + let transaction = + vm.execute(&deployer_private_key, ("program_c.aleo", "fun_4"), inputs.iter(), None, 0, None, rng).unwrap(); + add_and_test_with_costs(&vm, &deployer_private_key, Some(&[&inputs]), &[transaction], rng); + + let inputs = [Value::from_str("5u8").unwrap()]; + let transaction = + vm.execute(&deployer_private_key, ("program_c.aleo", "fun_5"), inputs.iter(), None, 0, None, rng).unwrap(); + add_and_test_with_costs(&vm, &deployer_private_key, Some(&[&inputs]), &[transaction], rng); +} + +#[test] +// Checks that a cast-to-external-struct instruction whose operand-type resolution is incorrect in +// the old version of the FinalizeType matches_struct check fails in the same way before after V19, +// since at both points in time there is a separate, correct check. +fn test_cast_with_external_structs_false_type() { + let rng = &mut TestRng::default(); + + let deployer_private_key = sample_genesis_private_key(rng); + + let program_a = Program::from_str( + r" + program program_a.aleo; + + struct my_struct: + val_1 as u8; + val_2 as field; + + function noop: + + constructor: + assert.eq edition 0u16; + ", + ) + .unwrap(); + + let program_b = Program::from_str( + r" + import program_a.aleo; + program program_b.aleo; + + struct my_struct: + val_1 as field; + + function cast_external: + async cast_external into r0; + output r0 as program_b.aleo/cast_external.future; + + finalize cast_external: + cast 42field into r0 as my_struct; + cast r0.val_1 1field into r1 as program_a.aleo/my_struct; + + constructor: + assert.eq edition 0u16; + ", + ) + .unwrap(); + + let err_old = { + let vm_old = sample_vm_at_height(CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V17).unwrap(), rng); + + let transaction = vm_old.deploy(&deployer_private_key, &program_a, None, 0, None, rng).unwrap(); + add_and_test_with_costs(&vm_old, &deployer_private_key, None, &[transaction], rng); + + vm_old.deploy(&deployer_private_key, &program_b, None, 0, None, rng).unwrap_err().to_string() + }; + + let err_new = { + let vm_new = sample_vm_at_height(CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V19).unwrap(), rng); + + let transaction = vm_new.deploy(&deployer_private_key, &program_a, None, 0, None, rng).unwrap(); + add_and_test_with_costs(&vm_new, &deployer_private_key, None, &[transaction], rng); + + vm_new.deploy(&deployer_private_key, &program_b, None, 0, None, rng).unwrap_err().to_string() + }; + + assert_eq!(err_old, err_new); + assert!(err_old.contains("expects a 'u8', but found a 'field'")) +} + +// Tests similar cases to test_cast_with_external_structs_only, but where the casts occur in the +// `constructor` scope. +#[test] +fn test_cast_with_external_structs_in_constructor() { + let rng = &mut TestRng::default(); + + let deployer_private_key = sample_genesis_private_key(rng); + + let program_a = Program::from_str( + r" + program program_a.aleo; + + struct struct_a: + val as u8; + + function noop: + + constructor: + assert.eq edition 0u16; + ", + ) + .unwrap(); + + let program_b = Program::from_str( + r" + import program_a.aleo; + program program_b.aleo; + + struct struct_b: + a as program_a.aleo/struct_a; + + function noop: + + constructor: + assert.eq edition 0u16; + ", + ) + .unwrap(); + + // This is a distinct program from program_b.aleo, but both declare a struct with the same name + // and member specification. + let program_b2 = Program::from_str( + r" + import program_a.aleo; + program program_b2.aleo; + + struct struct_b: + a as program_a.aleo/struct_a; + + function noop: + + constructor: + assert.eq edition 0u16; + ", + ) + .unwrap(); + + // The constructor has no inputs, so every cast originates from a literal. The four cases below + // mirror the function/finalize cases but in the constructor scope. + let program_c = Program::from_str( + r" + import program_a.aleo; + import program_b.aleo; + import program_b2.aleo; + program program_c.aleo; + + struct struct_c: + a as program_a.aleo/struct_a; + b as program_b.aleo/struct_b; + + function noop: + + constructor: + assert.eq edition 0u16; + + // Case 1: literal -> external struct -> external struct -> local struct. + cast 24u8 into r0 as program_a.aleo/struct_a; + cast r0 into r1 as program_b.aleo/struct_b; + cast r0 r1 into r2 as struct_c; + + // Case 2: cast involving an external-struct member read (`r0.val`). + cast r0.val into r3 as program_a.aleo/struct_a; + cast r3 into r4 as program_b.aleo/struct_b; + cast r3 r4 into r5 as struct_c; + + // Case 3: the same struct_a cast into a program_b2/struct_b, then combined into struct_c. + cast r0 into r6 as program_b2.aleo/struct_b; + cast r6.a r1 into r7 as struct_c; + + // Case 4: three-level nested access reaching through external structs. + cast r2.b.a.val into r8 as u8; + ", + ) + .unwrap(); + + let vm = sample_vm_at_height(CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V19).unwrap(), rng); + + let transaction = vm.deploy(&deployer_private_key, &program_a, None, 0, None, rng).unwrap(); + add_and_test_with_costs(&vm, &deployer_private_key, None, &[transaction], rng); + + let transaction = vm.deploy(&deployer_private_key, &program_b, None, 0, None, rng).unwrap(); + add_and_test_with_costs(&vm, &deployer_private_key, None, &[transaction], rng); + + let transaction = vm.deploy(&deployer_private_key, &program_b2, None, 0, None, rng).unwrap(); + add_and_test_with_costs(&vm, &deployer_private_key, None, &[transaction], rng); + + // Deploying program_c.aleo runs and type-checks the constructor's external-struct casts. + let transaction = vm.deploy(&deployer_private_key, &program_c, None, 0, None, rng).unwrap(); + add_and_test_with_costs(&vm, &deployer_private_key, None, &[transaction], rng); +} + +// Tests similar cases to test_cast_with_external_structs_in_records, but where the casts occur in the +// `view` scope. +#[test] +fn test_cast_with_external_structs_in_views() { + let rng = &mut TestRng::default(); + + let deployer_private_key = sample_genesis_private_key(rng); + + let program_a = Program::from_str( + r" + program program_a.aleo; + + struct struct_a: + val as u8; + + function noop: + + constructor: + assert.eq edition 0u16; + ", + ) + .unwrap(); + + let program_b = Program::from_str( + r" + import program_a.aleo; + program program_b.aleo; + + struct struct_b: + a as program_a.aleo/struct_a; + + function noop: + + constructor: + assert.eq edition 0u16; + ", + ) + .unwrap(); + + // This is a distinct program from program_b.aleo, but both declare a struct with the same name + // and member specification. + let program_b2 = Program::from_str( + r" + import program_a.aleo; + program program_b2.aleo; + + struct struct_b: + a as program_a.aleo/struct_a; + + function noop: + + constructor: + assert.eq edition 0u16; + ", + ) + .unwrap(); + + // The four views below mirror the first four function cases, but in the view scope. Views + // share the finalize register model, so their inputs and casts type-check identically. + let program_c = Program::from_str( + r" + import program_a.aleo; + import program_b.aleo; + import program_b2.aleo; + program program_c.aleo; + + struct struct_c: + a as program_a.aleo/struct_a; + b as program_b.aleo/struct_b; + + function noop: + + // Case 1 / view: cast external structs defined in program_a and program_b, then a struct_c. + view view_1: + input r0 as u8.public; + + cast r0 into r1 as program_a.aleo/struct_a; + cast r1 into r2 as program_b.aleo/struct_b; + cast r1 r2 into r3 as struct_c; + + output r3 as struct_c.public; + + // Case 2 / view: similar to case 1 but with an external-struct member read (`r1.val`). + view view_2: + input r0 as u8.public; + + cast r0 into r1 as program_a.aleo/struct_a; + cast r1.val into r2 as program_a.aleo/struct_a; + cast r2 into r3 as program_b.aleo/struct_b; + cast r2 r3 into r4 as struct_c; + + output r4 as struct_c.public; + + // Case 3 / view: struct_c is cast from an external struct received as an input argument. + view view_3: + input r0 as program_a.aleo/struct_a.public; + + cast r0 into r1 as program_b.aleo/struct_b; + cast r0 r1 into r2 as struct_c; + + output r2 as struct_c.public; + + // Case 4 / view: the same input external struct is cast into program_b and program_b2 structs. + view view_4: + input r0 as program_a.aleo/struct_a.public; + + cast r0 into r1 as program_b.aleo/struct_b; + cast r0 into r2 as program_b2.aleo/struct_b; + cast r2.a r1 into r3 as struct_c; + + output r3 as struct_c.public; + + constructor: + assert.eq edition 0u16; + ", + ) + .unwrap(); + + let vm = sample_vm_at_height(CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V19).unwrap(), rng); + + let transaction = vm.deploy(&deployer_private_key, &program_a, None, 0, None, rng).unwrap(); + add_and_test_with_costs(&vm, &deployer_private_key, None, &[transaction], rng); + + let transaction = vm.deploy(&deployer_private_key, &program_b, None, 0, None, rng).unwrap(); + add_and_test_with_costs(&vm, &deployer_private_key, None, &[transaction], rng); + + let transaction = vm.deploy(&deployer_private_key, &program_b2, None, 0, None, rng).unwrap(); + add_and_test_with_costs(&vm, &deployer_private_key, None, &[transaction], rng); + + // Deploying program_c.aleo type-checks each view's external-struct casts. + let transaction = vm.deploy(&deployer_private_key, &program_c, None, 0, None, rng).unwrap(); + add_and_test_with_costs(&vm, &deployer_private_key, None, &[transaction], rng); +} diff --git a/synthesizer/src/vm/tests/test_v19/mod.rs b/synthesizer/src/vm/tests/test_v19/mod.rs index ed4d968eac..8f7ff151be 100644 --- a/synthesizer/src/vm/tests/test_v19/mod.rs +++ b/synthesizer/src/vm/tests/test_v19/mod.rs @@ -16,6 +16,9 @@ // Tests that the translation-marked variants of Input and Output are checked correctly. mod translated_type_checks; +// Tests on stack fetching for external-struct type checks +mod external_struct_stacks; + use super::*; use super::test_v14::add_and_test_with_costs; diff --git a/synthesizer/src/vm/tests/test_v19/translated_type_checks.rs b/synthesizer/src/vm/tests/test_v19/translated_type_checks.rs index f745afce86..c932e5f34b 100644 --- a/synthesizer/src/vm/tests/test_v19/translated_type_checks.rs +++ b/synthesizer/src/vm/tests/test_v19/translated_type_checks.rs @@ -240,7 +240,7 @@ fn test_dynamic_id_variant_checks() { ) .unwrap(); - // Initialize the VM at V18 and deploy the three programs. + // Initialize the VM at V19 and deploy the three programs. let vm = sample_vm_at_height(CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V19).unwrap(), rng); for program in [&issuer, &checker, &top] { let deployment = vm.deploy(&caller_private_key, program, None, 0, None, rng).unwrap(); diff --git a/synthesizer/src/vm/verify.rs b/synthesizer/src/vm/verify.rs index d02d49ae5e..bcfb1caf36 100644 --- a/synthesizer/src/vm/verify.rs +++ b/synthesizer/src/vm/verify.rs @@ -14,6 +14,7 @@ // limitations under the License. use console::network::varuna_version_from_consensus; +use snarkvm_synthesizer_process::FinalizeTypes; use super::*; @@ -245,8 +246,8 @@ impl> VM { // If the `CONSENSUS_VERSION` is less than `V13`, ensure that // - the program does not include V13 syntax // - the program does not use the external struct syntax `some_program.aleo/Struct` - // If the `CONSENSUS_VERSION` is greater than or equal to `V13`, then verify that: - // - the program's mappings do not use non-existent structs. + // If the `CONSENSUS_VERSION` is less than `V19`, ensure that + // - the program does not break the pre-V19 version of `matches_struct` in casts to external structs within finalise-type scopes. // If the `CONSENSUS_VERSION` is less than `V14`, ensure that // - the program does not include V14 syntax (snark.verify, aleo::GENERATOR, identifier literals/types) // - the argument bit size of futures does not exceed the maximum allowed size of u16::MAX. @@ -364,6 +365,84 @@ impl> VM { "Invalid deployment transaction '{id}' - program uses syntax that is not allowed before `ConsensusVersion::V16`" ); } + if consensus_version < ConsensusVersion::V19 { + // Legacy check that mirrors pre-V19 checks on cast-to-external-struct + // instructions, which performed a single-stack-only call to `matches_struct`. + // Since this involves building stack and record-tracking machinery, we gate it + // behind a cheap instruction-level condition. + if deployment.program().finalize_casts_external_struct() { + // Build a lightweight stack (skipping the full `initialize_and_check`) and + // validate only the external-struct casts in the finalize-type scopes. + let stack = Stack::new_raw(&self.process, deployment.program(), deployment.edition())?; + + // Auxiliary function which performs the pre-V19 version of + // cast-to-external-struct checks. + fn check_scope<'a, N: Network>( + stack: &Stack, + inputs: impl IntoIterator< + Item = (&'a console::program::Register, &'a console::program::FinalizeType), + >, + positions: &HashMap, usize>, + commands: &[Command], + ) -> Result<()> { + let mut finalize_types = FinalizeTypes::new(); + + // Accumulate the input register types. + for (register, finalize_type) in inputs { + finalize_types.check_input(stack, register, finalize_type)?; + } + + for command in commands { + // This also helps accumulate the types of non-input registers. + finalize_types.check_command(stack, positions, command)?; + + if let Command::Instruction(snarkvm_synthesizer_program::Instruction::Cast(cast)) = + command + && let snarkvm_synthesizer_program::CastType::Plaintext( + console::program::PlaintextType::ExternalStruct(locator), + ) = cast.cast_type() + { + let external_stack = stack.get_external_stack(locator.program_id())?; + let struct_ = external_stack.program().get_struct(locator.resource())?; + // This call matches the pre-V19 `matches_struct` call, which only + // admitted one stack and to which `external_stack` was passed. + finalize_types.matches_struct( + &*external_stack, + &*external_stack, + cast.operands(), + struct_, + )?; + } + } + Ok(()) + } + + // Run the legacy check on the constructor (which has no inputs). + if let Some(constructor) = stack.program().constructor() { + check_scope(&stack, [], constructor.positions(), constructor.commands())?; + } + // Run the legacy check on each function's finalize block. + for function in stack.program().functions().values() { + if let Some(finalize) = function.finalize_logic() { + check_scope( + &stack, + finalize.inputs().iter().map(|input| (input.register(), input.finalize_type())), + finalize.positions(), + finalize.commands(), + )?; + } + } + // Run the legacy check on each view. + for view in stack.program().views().values() { + check_scope( + &stack, + view.inputs().iter().map(|input| (input.register(), input.finalize_type())), + view.positions(), + view.commands(), + )?; + } + } + } // Checks required for current and future consensus versions (>= V9). //