diff --git a/circuit/program/src/data/literal/mod.rs b/circuit/program/src/data/literal/mod.rs index ed52693021..a333dfca4c 100644 --- a/circuit/program/src/data/literal/mod.rs +++ b/circuit/program/src/data/literal/mod.rs @@ -21,6 +21,7 @@ mod cast_lossy; mod equal; mod from_bits; mod size_in_bits; +mod ternary; mod to_bits; mod to_fields; mod to_type; diff --git a/circuit/program/src/data/literal/ternary.rs b/circuit/program/src/data/literal/ternary.rs new file mode 100644 index 0000000000..238130dc39 --- /dev/null +++ b/circuit/program/src/data/literal/ternary.rs @@ -0,0 +1,148 @@ +// 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::*; + +impl Ternary for Literal { + type Boolean = Boolean; + type Output = Self; + + /// Returns `first` if `condition` is `true`, otherwise returns `second`. + /// The `first` and `second` literals must be the same variant, and must be a variant for which + /// ternary selection is supported. Callers are expected to enforce this via type-checking + /// before invocation; mismatched or unsupported variants are treated as unreachable. + fn ternary(condition: &::Boolean, first: &Self, second: &Self) -> ::Output { + match (first, second) { + (Self::Address(a), Self::Address(b)) => Self::Address(Address::ternary(condition, a, b)), + (Self::Boolean(a), Self::Boolean(b)) => Self::Boolean(Boolean::ternary(condition, a, b)), + (Self::Field(a), Self::Field(b)) => Self::Field(Field::ternary(condition, a, b)), + (Self::Group(a), Self::Group(b)) => Self::Group(Group::ternary(condition, a, b)), + (Self::I8(a), Self::I8(b)) => Self::I8(I8::ternary(condition, a, b)), + (Self::I16(a), Self::I16(b)) => Self::I16(I16::ternary(condition, a, b)), + (Self::I32(a), Self::I32(b)) => Self::I32(I32::ternary(condition, a, b)), + (Self::I64(a), Self::I64(b)) => Self::I64(I64::ternary(condition, a, b)), + (Self::I128(a), Self::I128(b)) => Self::I128(I128::ternary(condition, a, b)), + (Self::U8(a), Self::U8(b)) => Self::U8(U8::ternary(condition, a, b)), + (Self::U16(a), Self::U16(b)) => Self::U16(U16::ternary(condition, a, b)), + (Self::U32(a), Self::U32(b)) => Self::U32(U32::ternary(condition, a, b)), + (Self::U64(a), Self::U64(b)) => Self::U64(U64::ternary(condition, a, b)), + (Self::U128(a), Self::U128(b)) => Self::U128(U128::ternary(condition, a, b)), + (Self::Scalar(a), Self::Scalar(b)) => Self::Scalar(Scalar::ternary(condition, a, b)), + (Self::Signature(a), Self::Signature(b)) => Self::Signature(Ternary::ternary(condition, a, b)), + (Self::Identifier(a), Self::Identifier(b)) => Self::Identifier(Ternary::ternary(condition, a, b)), + _ => unreachable!("ternary operands must be the same literal variant after type-checking"), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Circuit; + use snarkvm_utilities::{TestRng, Uniform}; + + fn check_dispatch(first: Literal, second: Literal) { + let true_ = Boolean::::new(Mode::Private, true); + let false_ = Boolean::::new(Mode::Private, false); + assert_eq!(first.eject_value(), Literal::ternary(&true_, &first, &second).eject_value()); + assert_eq!(second.eject_value(), Literal::ternary(&false_, &first, &second).eject_value()); + } + + #[test] + fn test_literal_ternary_dispatches_all_supported_variants() { + let mut rng = TestRng::default(); + + check_dispatch( + Literal::Address(Address::new(Mode::Private, Uniform::rand(&mut rng))), + Literal::Address(Address::new(Mode::Private, Uniform::rand(&mut rng))), + ); + check_dispatch( + Literal::Boolean(Boolean::new(Mode::Private, true)), + Literal::Boolean(Boolean::new(Mode::Private, false)), + ); + check_dispatch( + Literal::Field(Field::new(Mode::Private, Uniform::rand(&mut rng))), + Literal::Field(Field::new(Mode::Private, Uniform::rand(&mut rng))), + ); + check_dispatch( + Literal::Group(Group::new(Mode::Private, Uniform::rand(&mut rng))), + Literal::Group(Group::new(Mode::Private, Uniform::rand(&mut rng))), + ); + check_dispatch( + Literal::I8(I8::new(Mode::Private, Uniform::rand(&mut rng))), + Literal::I8(I8::new(Mode::Private, Uniform::rand(&mut rng))), + ); + check_dispatch( + Literal::I16(I16::new(Mode::Private, Uniform::rand(&mut rng))), + Literal::I16(I16::new(Mode::Private, Uniform::rand(&mut rng))), + ); + check_dispatch( + Literal::I32(I32::new(Mode::Private, Uniform::rand(&mut rng))), + Literal::I32(I32::new(Mode::Private, Uniform::rand(&mut rng))), + ); + check_dispatch( + Literal::I64(I64::new(Mode::Private, Uniform::rand(&mut rng))), + Literal::I64(I64::new(Mode::Private, Uniform::rand(&mut rng))), + ); + check_dispatch( + Literal::I128(I128::new(Mode::Private, Uniform::rand(&mut rng))), + Literal::I128(I128::new(Mode::Private, Uniform::rand(&mut rng))), + ); + check_dispatch( + Literal::U8(U8::new(Mode::Private, Uniform::rand(&mut rng))), + Literal::U8(U8::new(Mode::Private, Uniform::rand(&mut rng))), + ); + check_dispatch( + Literal::U16(U16::new(Mode::Private, Uniform::rand(&mut rng))), + Literal::U16(U16::new(Mode::Private, Uniform::rand(&mut rng))), + ); + check_dispatch( + Literal::U32(U32::new(Mode::Private, Uniform::rand(&mut rng))), + Literal::U32(U32::new(Mode::Private, Uniform::rand(&mut rng))), + ); + check_dispatch( + Literal::U64(U64::new(Mode::Private, Uniform::rand(&mut rng))), + Literal::U64(U64::new(Mode::Private, Uniform::rand(&mut rng))), + ); + check_dispatch( + Literal::U128(U128::new(Mode::Private, Uniform::rand(&mut rng))), + Literal::U128(U128::new(Mode::Private, Uniform::rand(&mut rng))), + ); + check_dispatch( + Literal::Scalar(Scalar::new(Mode::Private, Uniform::rand(&mut rng))), + Literal::Scalar(Scalar::new(Mode::Private, Uniform::rand(&mut rng))), + ); + check_dispatch( + Literal::Identifier(Box::new(IdentifierLiteral::new( + Mode::Private, + console::IdentifierLiteral::rand(&mut rng), + ))), + Literal::Identifier(Box::new(IdentifierLiteral::new( + Mode::Private, + console::IdentifierLiteral::rand(&mut rng), + ))), + ); + } + + #[test] + #[should_panic(expected = "ternary operands must be the same literal variant")] + fn test_literal_ternary_variant_mismatch_panics() { + let mut rng = TestRng::default(); + let first = Literal::::Field(Field::new(Mode::Private, Uniform::rand(&mut rng))); + let second = Literal::::Boolean(Boolean::new(Mode::Private, true)); + let cond = Boolean::::new(Mode::Private, false); + let _ = Literal::ternary(&cond, &first, &second); + } +} diff --git a/circuit/program/src/data/plaintext/mod.rs b/circuit/program/src/data/plaintext/mod.rs index c392c94639..f171df12e1 100644 --- a/circuit/program/src/data/plaintext/mod.rs +++ b/circuit/program/src/data/plaintext/mod.rs @@ -25,6 +25,7 @@ mod from_bits; mod from_fields; mod num_randomizers; mod size_in_fields; +mod ternary; mod to_bits; mod to_bits_raw; mod to_fields; diff --git a/circuit/program/src/data/plaintext/ternary.rs b/circuit/program/src/data/plaintext/ternary.rs new file mode 100644 index 0000000000..f3b880bc89 --- /dev/null +++ b/circuit/program/src/data/plaintext/ternary.rs @@ -0,0 +1,136 @@ +// 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::*; + +impl Ternary for Plaintext { + type Boolean = Boolean; + type Output = Self; + + /// Returns `first` if `condition` is `true`, otherwise returns `second`. + /// The `first` and `second` plaintexts must have the same shape: same variant, arrays of equal + /// length with matching element shapes, or structs with matching keys in the same order. + /// Callers are expected to enforce this via type-checking before invocation; mismatched shapes + /// are treated as unreachable. + fn ternary(condition: &::Boolean, first: &Self, second: &Self) -> ::Output { + match (first, second) { + (Self::Literal(a, _), Self::Literal(b, _)) => { + Self::Literal(Literal::ternary(condition, a, b), OnceCell::new()) + } + (Self::Array(a, _), Self::Array(b, _)) if a.len() == b.len() => { + let elements = a.iter().zip_eq(b.iter()).map(|(x, y)| Plaintext::ternary(condition, x, y)).collect(); + Self::Array(elements, OnceCell::new()) + } + (Self::Struct(a, _), Self::Struct(b, _)) + if a.len() == b.len() && a.keys().zip(b.keys()).all(|(ka, kb)| ka == kb) => + { + let fields = a + .iter() + .zip_eq(b.iter()) + .map(|((key, x), (_, y))| (key.clone(), Plaintext::ternary(condition, x, y))) + .collect(); + Self::Struct(fields, OnceCell::new()) + } + _ => unreachable!("ternary operands must have equivalent shape after type-checking"), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Circuit; + + fn sample(mode: Mode, literal: &str) -> Plaintext { + let primitive = console::Plaintext::<::Network>::from_str(literal).unwrap(); + Plaintext::new(mode, primitive) + } + + fn check_both_branches(first: &Plaintext, second: &Plaintext) { + let true_ = Boolean::::new(Mode::Private, true); + let false_ = Boolean::::new(Mode::Private, false); + assert_eq!(first.eject_value(), Plaintext::ternary(&true_, first, second).eject_value()); + assert_eq!(second.eject_value(), Plaintext::ternary(&false_, first, second).eject_value()); + } + + #[test] + fn test_plaintext_ternary_literal() { + let first = sample(Mode::Private, "1field"); + let second = sample(Mode::Private, "2field"); + check_both_branches(&first, &second); + } + + #[test] + fn test_plaintext_ternary_flat_array() { + let first = sample(Mode::Private, "[ 1field, 2field, 3field ]"); + let second = sample(Mode::Private, "[ 4field, 5field, 6field ]"); + check_both_branches(&first, &second); + } + + #[test] + fn test_plaintext_ternary_nested_array() { + let first = sample(Mode::Private, "[ [ 1u8, 2u8 ], [ 3u8, 4u8 ] ]"); + let second = sample(Mode::Private, "[ [ 5u8, 6u8 ], [ 7u8, 8u8 ] ]"); + check_both_branches(&first, &second); + } + + #[test] + fn test_plaintext_ternary_struct() { + let first = sample(Mode::Private, "{ x: 1field, y: 2field }"); + let second = sample(Mode::Private, "{ x: 3field, y: 4field }"); + check_both_branches(&first, &second); + } + + #[test] + fn test_plaintext_ternary_struct_of_arrays() { + let first = sample(Mode::Private, "{ a: [ 1u8, 2u8 ], b: 3field }"); + let second = sample(Mode::Private, "{ a: [ 4u8, 5u8 ], b: 6field }"); + check_both_branches(&first, &second); + } + + #[test] + fn test_plaintext_ternary_array_of_structs() { + let first = sample(Mode::Private, "[ { x: 1field, y: 2field }, { x: 3field, y: 4field } ]"); + let second = sample(Mode::Private, "[ { x: 5field, y: 6field }, { x: 7field, y: 8field } ]"); + check_both_branches(&first, &second); + } + + #[test] + #[should_panic(expected = "ternary operands must have equivalent shape")] + fn test_plaintext_ternary_array_length_mismatch_panics() { + let first = sample(Mode::Private, "[ 1field, 2field ]"); + let second = sample(Mode::Private, "[ 3field, 4field, 5field ]"); + let cond = Boolean::::new(Mode::Private, false); + let _ = Plaintext::ternary(&cond, &first, &second); + } + + #[test] + #[should_panic(expected = "ternary operands must have equivalent shape")] + fn test_plaintext_ternary_struct_key_order_mismatch_panics() { + let first = sample(Mode::Private, "{ x: 1field, y: 2field }"); + let second = sample(Mode::Private, "{ y: 3field, x: 4field }"); + let cond = Boolean::::new(Mode::Private, false); + let _ = Plaintext::ternary(&cond, &first, &second); + } + + #[test] + #[should_panic(expected = "ternary operands must have equivalent shape")] + fn test_plaintext_ternary_variant_mismatch_panics() { + let first = sample(Mode::Private, "1field"); + let second = sample(Mode::Private, "[ 1field ]"); + let cond = Boolean::::new(Mode::Private, false); + let _ = Plaintext::ternary(&cond, &first, &second); + } +} diff --git a/circuit/types/string/src/identifier_literal/mod.rs b/circuit/types/string/src/identifier_literal/mod.rs index 2d6c64f8f6..aa3c032784 100644 --- a/circuit/types/string/src/identifier_literal/mod.rs +++ b/circuit/types/string/src/identifier_literal/mod.rs @@ -15,6 +15,7 @@ mod equal; mod helpers; +mod ternary; use snarkvm_circuit_environment::prelude::*; use snarkvm_circuit_types_boolean::Boolean; diff --git a/circuit/types/string/src/identifier_literal/ternary.rs b/circuit/types/string/src/identifier_literal/ternary.rs new file mode 100644 index 0000000000..df094c25e6 --- /dev/null +++ b/circuit/types/string/src/identifier_literal/ternary.rs @@ -0,0 +1,110 @@ +// 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::*; + +impl Ternary for IdentifierLiteral { + type Boolean = Boolean; + type Output = Self; + + /// Returns `first` if `condition` is `true`, otherwise returns `second`. + fn ternary(condition: &Self::Boolean, first: &Self, second: &Self) -> Self::Output { + // Both inputs are already validated identifier literals. Byte-wise selection with the same + // condition on each byte returns either `first.bytes` or `second.bytes` in their entirety, + // so the output is a valid identifier without revalidating. + let bytes: [U8; SIZE_IN_BYTES] = + core::array::from_fn(|i| U8::ternary(condition, &first.bytes[i], &second.bytes[i])); + Self { bytes } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use snarkvm_circuit_environment::Circuit; + use snarkvm_utilities::{TestRng, Uniform}; + + type CurrentEnvironment = Circuit; + + const ITERATIONS: usize = 16; + + fn check_ternary(mode_condition: Mode, mode_a: Mode, mode_b: Mode) { + let mut rng = TestRng::default(); + + for _ in 0..ITERATIONS { + // Sample two random identifier literals. + let first = console::IdentifierLiteral::<::Network>::rand(&mut rng); + let second = console::IdentifierLiteral::<::Network>::rand(&mut rng); + + for flag in [true, false] { + let expected = if flag { first } else { second }; + + let condition = Boolean::::new(mode_condition, flag); + let a = IdentifierLiteral::::new(mode_a, first); + let b = IdentifierLiteral::::new(mode_b, second); + + Circuit::scope(format!("ternary {mode_condition}/{mode_a}/{mode_b} flag={flag}"), || { + let candidate = IdentifierLiteral::ternary(&condition, &a, &b); + assert_eq!(expected, candidate.eject_value()); + }); + Circuit::reset(); + } + } + } + + #[test] + fn test_ternary_constant_condition_constant_inputs() { + check_ternary(Mode::Constant, Mode::Constant, Mode::Constant); + } + + #[test] + fn test_ternary_constant_condition_variable_inputs() { + check_ternary(Mode::Constant, Mode::Private, Mode::Private); + check_ternary(Mode::Constant, Mode::Public, Mode::Private); + } + + #[test] + fn test_ternary_public_condition() { + check_ternary(Mode::Public, Mode::Public, Mode::Public); + check_ternary(Mode::Public, Mode::Private, Mode::Private); + } + + #[test] + fn test_ternary_private_condition() { + check_ternary(Mode::Private, Mode::Private, Mode::Private); + check_ternary(Mode::Private, Mode::Public, Mode::Private); + } + + #[test] + fn test_ternary_matches_console() { + let mut rng = TestRng::default(); + for _ in 0..ITERATIONS { + let first = console::IdentifierLiteral::<::Network>::rand(&mut rng); + let second = console::IdentifierLiteral::<::Network>::rand(&mut rng); + for flag in [true, false] { + // Console ternary. + let console_condition = console::Boolean::<::Network>::new(flag); + let expected = console::IdentifierLiteral::ternary(&console_condition, &first, &second); + // Circuit ternary. + let condition = Boolean::::new(Mode::Private, flag); + let a = IdentifierLiteral::::new(Mode::Private, first); + let b = IdentifierLiteral::::new(Mode::Private, second); + let candidate = IdentifierLiteral::ternary(&condition, &a, &b); + assert_eq!(expected, candidate.eject_value()); + Circuit::reset(); + } + } + } +} diff --git a/console/network/src/consensus_heights.rs b/console/network/src/consensus_heights.rs index 947f605b39..5e1dbee0b5 100644 --- a/console/network/src/consensus_heights.rs +++ b/console/network/src/consensus_heights.rs @@ -58,6 +58,9 @@ pub enum ConsensusVersion { /// Increase the anchor time to 35. /// Unconditionally stores transaction rejection reasons. V15 = 15, + /// V16: Extends the `ternary` instruction to accept literal, array, struct, and identifier + /// operands in addition to registers. + V16 = 16, } impl ToBytes for ConsensusVersion { @@ -85,6 +88,7 @@ impl FromBytes for ConsensusVersion { 13 => Ok(Self::V13), 14 => Ok(Self::V14), 15 => Ok(Self::V15), + 16 => Ok(Self::V16), _ => Err(io_error("Invalid consensus version")), } } @@ -123,6 +127,7 @@ pub const CANARY_V0_CONSENSUS_VERSION_HEIGHTS: [(ConsensusVersion, u32); NUM_CON (ConsensusVersion::V13, 10_881_000), (ConsensusVersion::V14, 11_960_000), (ConsensusVersion::V15, u32::MAX), + (ConsensusVersion::V16, u32::MAX), ]; /// The consensus version height for `MainnetV0`. @@ -142,6 +147,7 @@ pub const MAINNET_V0_CONSENSUS_VERSION_HEIGHTS: [(ConsensusVersion, u32); NUM_CO (ConsensusVersion::V13, 16_850_000), (ConsensusVersion::V14, 17_700_000), (ConsensusVersion::V15, 19_264_000), + (ConsensusVersion::V16, u32::MAX), ]; /// The consensus version heights for `TestnetV0`. @@ -161,6 +167,7 @@ pub const TESTNET_V0_CONSENSUS_VERSION_HEIGHTS: [(ConsensusVersion, u32); NUM_CO (ConsensusVersion::V13, 14_906_000), (ConsensusVersion::V14, 15_370_000), (ConsensusVersion::V15, 16_886_000), + (ConsensusVersion::V16, u32::MAX), ]; /// The consensus version heights when the `test_consensus_heights` feature is enabled. @@ -180,6 +187,7 @@ pub const TEST_CONSENSUS_VERSION_HEIGHTS: [(ConsensusVersion, u32); NUM_CONSENSU (ConsensusVersion::V13, 16), (ConsensusVersion::V14, 17), (ConsensusVersion::V15, 18), + (ConsensusVersion::V16, 19), ]; #[cfg(any(test, feature = "test", feature = "test_consensus_heights"))] diff --git a/console/program/src/data/literal/mod.rs b/console/program/src/data/literal/mod.rs index 36f8e725d3..088b25e767 100644 --- a/console/program/src/data/literal/mod.rs +++ b/console/program/src/data/literal/mod.rs @@ -26,6 +26,7 @@ mod sample; mod serialize; mod size_in_bits; mod size_in_bytes; +mod ternary; mod to_bits; mod to_type; mod variant; diff --git a/console/program/src/data/literal/ternary.rs b/console/program/src/data/literal/ternary.rs new file mode 100644 index 0000000000..eacee71c1c --- /dev/null +++ b/console/program/src/data/literal/ternary.rs @@ -0,0 +1,101 @@ +// 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::*; + +impl Ternary for Literal { + type Boolean = Boolean; + type Output = Self; + + /// Returns `first` if `condition` is `true`, otherwise returns `second`. + /// The `first` and `second` literals must be the same variant, and must be a variant for which + /// ternary selection is supported. Callers are expected to enforce this via type-checking + /// before invocation; mismatched or unsupported variants are treated as unreachable. + fn ternary(condition: &::Boolean, first: &Self, second: &Self) -> ::Output { + match (first, second) { + (Self::Address(a), Self::Address(b)) => Self::Address(Address::ternary(condition, a, b)), + (Self::Boolean(a), Self::Boolean(b)) => Self::Boolean(Boolean::ternary(condition, a, b)), + (Self::Field(a), Self::Field(b)) => Self::Field(Field::ternary(condition, a, b)), + (Self::Group(a), Self::Group(b)) => Self::Group(Group::ternary(condition, a, b)), + (Self::I8(a), Self::I8(b)) => Self::I8(I8::ternary(condition, a, b)), + (Self::I16(a), Self::I16(b)) => Self::I16(I16::ternary(condition, a, b)), + (Self::I32(a), Self::I32(b)) => Self::I32(I32::ternary(condition, a, b)), + (Self::I64(a), Self::I64(b)) => Self::I64(I64::ternary(condition, a, b)), + (Self::I128(a), Self::I128(b)) => Self::I128(I128::ternary(condition, a, b)), + (Self::U8(a), Self::U8(b)) => Self::U8(U8::ternary(condition, a, b)), + (Self::U16(a), Self::U16(b)) => Self::U16(U16::ternary(condition, a, b)), + (Self::U32(a), Self::U32(b)) => Self::U32(U32::ternary(condition, a, b)), + (Self::U64(a), Self::U64(b)) => Self::U64(U64::ternary(condition, a, b)), + (Self::U128(a), Self::U128(b)) => Self::U128(U128::ternary(condition, a, b)), + (Self::Scalar(a), Self::Scalar(b)) => Self::Scalar(Scalar::ternary(condition, a, b)), + (Self::Signature(a), Self::Signature(b)) => Self::Signature(Ternary::ternary(condition, a, b)), + (Self::Identifier(a), Self::Identifier(b)) => Self::Identifier(Ternary::ternary(condition, a, b)), + _ => unreachable!("ternary operands must be the same literal variant after type-checking"), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use snarkvm_console_network::MainnetV0; + + type CurrentNetwork = MainnetV0; + + fn check_dispatch(first: Literal, second: Literal) { + let true_ = Boolean::::new(true); + let false_ = Boolean::::new(false); + assert_eq!(first, Literal::ternary(&true_, &first, &second)); + assert_eq!(second, Literal::ternary(&false_, &first, &second)); + } + + #[test] + fn test_literal_ternary_dispatches_all_supported_variants() { + let mut rng = TestRng::default(); + check_dispatch(Literal::Address(Address::rand(&mut rng)), Literal::Address(Address::rand(&mut rng))); + check_dispatch(Literal::Boolean(Boolean::new(true)), Literal::Boolean(Boolean::new(false))); + check_dispatch(Literal::Field(Field::rand(&mut rng)), Literal::Field(Field::rand(&mut rng))); + check_dispatch(Literal::Group(Group::rand(&mut rng)), Literal::Group(Group::rand(&mut rng))); + check_dispatch(Literal::I8(I8::rand(&mut rng)), Literal::I8(I8::rand(&mut rng))); + check_dispatch(Literal::I16(I16::rand(&mut rng)), Literal::I16(I16::rand(&mut rng))); + check_dispatch(Literal::I32(I32::rand(&mut rng)), Literal::I32(I32::rand(&mut rng))); + check_dispatch(Literal::I64(I64::rand(&mut rng)), Literal::I64(I64::rand(&mut rng))); + check_dispatch(Literal::I128(I128::rand(&mut rng)), Literal::I128(I128::rand(&mut rng))); + check_dispatch(Literal::U8(U8::rand(&mut rng)), Literal::U8(U8::rand(&mut rng))); + check_dispatch(Literal::U16(U16::rand(&mut rng)), Literal::U16(U16::rand(&mut rng))); + check_dispatch(Literal::U32(U32::rand(&mut rng)), Literal::U32(U32::rand(&mut rng))); + check_dispatch(Literal::U64(U64::rand(&mut rng)), Literal::U64(U64::rand(&mut rng))); + check_dispatch(Literal::U128(U128::rand(&mut rng)), Literal::U128(U128::rand(&mut rng))); + check_dispatch(Literal::Scalar(Scalar::rand(&mut rng)), Literal::Scalar(Scalar::rand(&mut rng))); + check_dispatch( + Literal::sample(LiteralType::Signature, &mut rng), + Literal::sample(LiteralType::Signature, &mut rng), + ); + check_dispatch( + Literal::sample(LiteralType::Identifier, &mut rng), + Literal::sample(LiteralType::Identifier, &mut rng), + ); + } + + #[test] + #[should_panic(expected = "ternary operands must be the same literal variant")] + fn test_literal_ternary_variant_mismatch_panics() { + let mut rng = TestRng::default(); + let first = Literal::::Field(Field::rand(&mut rng)); + let second = Literal::::Boolean(Boolean::new(true)); + let cond = Boolean::::new(false); + let _ = Literal::ternary(&cond, &first, &second); + } +} diff --git a/console/program/src/data/plaintext/mod.rs b/console/program/src/data/plaintext/mod.rs index 001d459581..829a65f734 100644 --- a/console/program/src/data/plaintext/mod.rs +++ b/console/program/src/data/plaintext/mod.rs @@ -23,6 +23,7 @@ mod num_randomizers; mod parse; mod serialize; mod size_in_fields; +mod ternary; mod to_bits; mod to_bits_raw; mod to_fields; diff --git a/console/program/src/data/plaintext/ternary.rs b/console/program/src/data/plaintext/ternary.rs new file mode 100644 index 0000000000..46332b527f --- /dev/null +++ b/console/program/src/data/plaintext/ternary.rs @@ -0,0 +1,137 @@ +// 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::*; + +impl Ternary for Plaintext { + type Boolean = Boolean; + type Output = Self; + + /// Returns `first` if `condition` is `true`, otherwise returns `second`. + /// The `first` and `second` plaintexts must have the same shape: same variant, arrays of equal + /// length with matching element shapes, or structs with matching keys in the same order. + /// Callers are expected to enforce this via type-checking before invocation; mismatched shapes + /// are treated as unreachable. + fn ternary(condition: &::Boolean, first: &Self, second: &Self) -> ::Output { + match (first, second) { + (Self::Literal(a, _), Self::Literal(b, _)) => { + Self::Literal(Literal::ternary(condition, a, b), OnceLock::new()) + } + (Self::Array(a, _), Self::Array(b, _)) if a.len() == b.len() => { + let elements = a.iter().zip_eq(b.iter()).map(|(x, y)| Plaintext::ternary(condition, x, y)).collect(); + Self::Array(elements, OnceLock::new()) + } + (Self::Struct(a, _), Self::Struct(b, _)) + if a.len() == b.len() && a.keys().zip(b.keys()).all(|(ka, kb)| ka == kb) => + { + let fields = a + .iter() + .zip_eq(b.iter()) + .map(|((key, x), (_, y))| (*key, Plaintext::ternary(condition, x, y))) + .collect(); + Self::Struct(fields, OnceLock::new()) + } + _ => unreachable!("ternary operands must have equivalent shape after type-checking"), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use snarkvm_console_network::MainnetV0; + + use core::str::FromStr; + + type CurrentNetwork = MainnetV0; + + fn check_both_branches(first: &Plaintext, second: &Plaintext) { + let true_ = Boolean::::new(true); + let false_ = Boolean::::new(false); + assert_eq!(first, &Plaintext::ternary(&true_, first, second)); + assert_eq!(second, &Plaintext::ternary(&false_, first, second)); + } + + #[test] + fn test_plaintext_ternary_literal() { + let first = Plaintext::::from_str("1field").unwrap(); + let second = Plaintext::::from_str("2field").unwrap(); + check_both_branches(&first, &second); + } + + #[test] + fn test_plaintext_ternary_flat_array() { + let first = Plaintext::::from_str("[ 1field, 2field, 3field ]").unwrap(); + let second = Plaintext::::from_str("[ 4field, 5field, 6field ]").unwrap(); + check_both_branches(&first, &second); + } + + #[test] + fn test_plaintext_ternary_nested_array() { + let first = Plaintext::::from_str("[ [ 1u8, 2u8 ], [ 3u8, 4u8 ] ]").unwrap(); + let second = Plaintext::::from_str("[ [ 5u8, 6u8 ], [ 7u8, 8u8 ] ]").unwrap(); + check_both_branches(&first, &second); + } + + #[test] + fn test_plaintext_ternary_struct() { + let first = Plaintext::::from_str("{ x: 1field, y: 2field }").unwrap(); + let second = Plaintext::::from_str("{ x: 3field, y: 4field }").unwrap(); + check_both_branches(&first, &second); + } + + #[test] + fn test_plaintext_ternary_struct_of_arrays() { + let first = Plaintext::::from_str("{ a: [ 1u8, 2u8 ], b: 3field }").unwrap(); + let second = Plaintext::::from_str("{ a: [ 4u8, 5u8 ], b: 6field }").unwrap(); + check_both_branches(&first, &second); + } + + #[test] + fn test_plaintext_ternary_array_of_structs() { + let first = + Plaintext::::from_str("[ { x: 1field, y: 2field }, { x: 3field, y: 4field } ]").unwrap(); + let second = + Plaintext::::from_str("[ { x: 5field, y: 6field }, { x: 7field, y: 8field } ]").unwrap(); + check_both_branches(&first, &second); + } + + #[test] + #[should_panic(expected = "ternary operands must have equivalent shape")] + fn test_plaintext_ternary_array_length_mismatch_panics() { + let first = Plaintext::::from_str("[ 1field, 2field ]").unwrap(); + let second = Plaintext::::from_str("[ 3field, 4field, 5field ]").unwrap(); + let cond = Boolean::::new(false); + let _ = Plaintext::ternary(&cond, &first, &second); + } + + #[test] + #[should_panic(expected = "ternary operands must have equivalent shape")] + fn test_plaintext_ternary_struct_key_order_mismatch_panics() { + let first = Plaintext::::from_str("{ x: 1field, y: 2field }").unwrap(); + let second = Plaintext::::from_str("{ y: 3field, x: 4field }").unwrap(); + let cond = Boolean::::new(false); + let _ = Plaintext::ternary(&cond, &first, &second); + } + + #[test] + #[should_panic(expected = "ternary operands must have equivalent shape")] + fn test_plaintext_ternary_variant_mismatch_panics() { + let first = Plaintext::::from_str("1field").unwrap(); + let second = Plaintext::::from_str("[ 1field ]").unwrap(); + let cond = Boolean::::new(false); + let _ = Plaintext::ternary(&cond, &first, &second); + } +} diff --git a/console/types/string/src/identifier_literal/mod.rs b/console/types/string/src/identifier_literal/mod.rs index 1b21130e4f..21be54d9ff 100644 --- a/console/types/string/src/identifier_literal/mod.rs +++ b/console/types/string/src/identifier_literal/mod.rs @@ -105,6 +105,19 @@ impl Equal for IdentifierLiteral { } } +impl Ternary for IdentifierLiteral { + type Boolean = Boolean; + type Output = Self; + + /// Returns `first` if `condition` is `true`, otherwise returns `second`. + fn ternary(condition: &Self::Boolean, first: &Self, second: &Self) -> Self::Output { + match **condition { + true => *first, + false => *second, + } + } +} + impl SizeInBits for IdentifierLiteral { /// Returns the size in bits of the identifier literal. fn size_in_bits() -> usize { diff --git a/synthesizer/process/src/cost.rs b/synthesizer/process/src/cost.rs index 81386be200..5289aab767 100644 --- a/synthesizer/process/src/cost.rs +++ b/synthesizer/process/src/cost.rs @@ -458,6 +458,9 @@ const MAPPING_PER_BYTE_COST: u64 = 10; const SET_BASE_COST: u64 = 10_000; const SET_PER_BYTE_COST: u64 = 100; +const TERNARY_BASE_COST: u64 = 500; +const TERNARY_PER_BYTE_COST: u64 = 10; + /// A helper function to determine the plaintext type in bytes. fn plaintext_size_in_bytes(stack: &Stack, plaintext_type: &PlaintextType) -> Result { match plaintext_type { @@ -918,7 +921,25 @@ pub fn cost_per_command( Command::Instruction(Instruction::SquareRoot(_)) => Ok(2_500), Command::Instruction(Instruction::Sub(_)) => Ok(500), Command::Instruction(Instruction::SubWrapped(_)) => Ok(500), - Command::Instruction(Instruction::Ternary(_)) => Ok(500), + Command::Instruction(Instruction::Ternary(ternary)) => { + // Before `ConsensusVersion::V16`, only literal branch operands are permitted, and the + // cost is a flat 500 microcredits. At `V16` and later, array or struct operands scale + // the cost by their recursive plaintext size. The literal case is preserved exactly so + // that previously deployed programs are not subject to a higher minimum fee. + let branches_are_literal = ternary.operands()[1..3].iter().try_fold(true, |acc, op| { + let is_literal = matches!( + finalize_types.get_type_from_operand(stack, op)?, + FinalizeType::Plaintext(PlaintextType::Literal(_)) + ); + Ok::<_, Error>(acc && is_literal) + })?; + if branches_are_literal { + Ok(500) + } else { + // Size only the branch operands (`first` and `second`); the Boolean condition is excluded. + cost_in_size(stack, finalize_types, &ternary.operands()[1..3], TERNARY_PER_BYTE_COST, TERNARY_BASE_COST) + } + } Command::Instruction(Instruction::Xor(_)) => Ok(500), Command::Await(_) => Ok(500), Command::Contains(command) => { diff --git a/synthesizer/program/src/logic/instruction/operation/mod.rs b/synthesizer/program/src/logic/instruction/operation/mod.rs index aa16208ca1..447cf57f78 100644 --- a/synthesizer/program/src/logic/instruction/operation/mod.rs +++ b/synthesizer/program/src/logic/instruction/operation/mod.rs @@ -57,6 +57,9 @@ pub use sign_verify::*; mod snark_verify; pub use snark_verify::*; +mod ternary; +pub use ternary::Ternary; + use crate::Opcode; use console::network::prelude::*; @@ -750,31 +753,6 @@ crate::operation!( } ); -/// Selects `first`, if `condition` is true, otherwise selects `second`, storing the result in `destination`. -pub type Ternary = TernaryLiteral>; - -crate::operation!( - pub struct TernaryOperation { - (Boolean, Address, Address) => Address, - (Boolean, Boolean, Boolean) => Boolean, - (Boolean, Field, Field) => Field, - (Boolean, Group, Group) => Group, - (Boolean, I8, I8) => I8, - (Boolean, I16, I16) => I16, - (Boolean, I32, I32) => I32, - (Boolean, I64, I64) => I64, - (Boolean, I128, I128) => I128, - (Boolean, U8, U8) => U8, - (Boolean, U16, U16) => U16, - (Boolean, U32, U32) => U32, - (Boolean, U64, U64) => U64, - (Boolean, U128, U128) => U128, - (Boolean, Scalar, Scalar) => Scalar, - (Boolean, Signature, Signature) => Signature, - // (Boolean, StringType, StringType) => StringType, - } -); - /// Performs a bitwise `xor` on `first` and `second`, storing the outcome in `destination`. pub type Xor = BinaryLiteral>; diff --git a/synthesizer/program/src/logic/instruction/operation/ternary.rs b/synthesizer/program/src/logic/instruction/operation/ternary.rs new file mode 100644 index 0000000000..6b03c2299d --- /dev/null +++ b/synthesizer/program/src/logic/instruction/operation/ternary.rs @@ -0,0 +1,337 @@ +// 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 crate::{ + FinalizeRegistersState, + FinalizeStoreTrait, + Opcode, + Operand, + RegistersCircuit, + RegistersTrait, + StackTrait, + register_types_equivalent, +}; +use console::{ + network::prelude::*, + program::{Literal, LiteralType, Plaintext, PlaintextType, Register, RegisterType, Value}, +}; + +/// Returns an error if the given plaintext type contains a `String` literal leaf, +/// since string ternary selection is not supported (strings have a variable byte-length +/// and cannot be selected by a fixed byte-wise multiplexer). +fn ensure_no_string_leaves(stack: &impl StackTrait, plaintext_type: &PlaintextType) -> Result<()> { + match plaintext_type { + PlaintextType::Literal(LiteralType::String) => { + bail!("Instruction 'ternary' does not support the 'string' type") + } + PlaintextType::Literal(_) => Ok(()), + PlaintextType::Array(array_type) => ensure_no_string_leaves(stack, array_type.next_element_type()), + PlaintextType::Struct(identifier) => { + let struct_type = stack.program().get_struct(identifier)?; + for (_, member_type) in struct_type.members() { + ensure_no_string_leaves(stack, member_type)?; + } + Ok(()) + } + PlaintextType::ExternalStruct(locator) => { + let external_stack = stack.get_external_stack(locator.program_id())?; + let struct_type = external_stack.program().get_struct(locator.resource())?; + for (_, member_type) in struct_type.members() { + ensure_no_string_leaves(&*external_stack, member_type)?; + } + Ok(()) + } + } +} + +/// Selects `first`, if `condition` is true, otherwise selects `second`, storing the result in `destination`. +#[derive(Clone, PartialEq, Eq, Hash)] +pub struct Ternary { + /// The operands: `[condition, first, second]`. + operands: Vec>, + /// The destination register. + destination: Register, +} + +impl Ternary { + /// Initializes a new `ternary` instruction. + pub fn new(operands: Vec>, destination: Register) -> Result { + ensure!(operands.len() == 3, "Instruction '{}' must have three operands", Self::opcode()); + Ok(Self { operands, destination }) + } + + /// Returns the opcode. + pub const fn opcode() -> Opcode { + Opcode::Literal("ternary") + } + + /// Returns the operands in the operation. + #[inline] + pub fn operands(&self) -> &[Operand] { + debug_assert!(self.operands.len() == 3, "Instruction '{}' must have three operands", Self::opcode()); + &self.operands + } + + /// Returns the destination register. + #[inline] + pub fn destinations(&self) -> Vec> { + vec![self.destination.clone()] + } + + /// Returns whether this instruction refers to an external struct. + #[inline] + pub fn contains_external_struct(&self) -> bool { + false + } +} + +impl Ternary { + /// Evaluates the instruction. + pub fn evaluate(&self, stack: &impl StackTrait, registers: &mut impl RegistersTrait) -> Result<()> { + // Ensure the number of operands is correct. + if self.operands.len() != 3 { + bail!("Instruction '{}' expects 3 operands, found {} operands", Self::opcode(), self.operands.len()) + } + + // Load the condition operand as a boolean literal. + let condition = match registers.load_literal(stack, &self.operands[0])? { + Literal::Boolean(boolean) => boolean, + other => bail!( + "Instruction '{}' expects the first operand to be a 'boolean', found '{}'", + Self::opcode(), + other.to_type() + ), + }; + // Load the two branch operands as plaintexts. + let first = registers.load_plaintext(stack, &self.operands[1])?; + let second = registers.load_plaintext(stack, &self.operands[2])?; + + // Select between the two branches. + let output = as console::prelude::Ternary>::ternary(&condition, &first, &second); + + // Store the output. + registers.store(stack, &self.destination, Value::Plaintext(output)) + } + + /// Executes the instruction. + pub fn execute>( + &self, + stack: &impl StackTrait, + registers: &mut impl RegistersCircuit, + ) -> Result<()> { + // Ensure the number of operands is correct. + if self.operands.len() != 3 { + bail!("Instruction '{}' expects 3 operands, found {} operands", Self::opcode(), self.operands.len()) + } + + // Load the condition operand as a boolean literal. + let condition = match registers.load_literal_circuit(stack, &self.operands[0])? { + circuit::Literal::Boolean(boolean) => boolean, + other => bail!( + "Instruction '{}' expects the first operand to be a 'boolean', found '{}'", + Self::opcode(), + other.to_type() + ), + }; + // Load the two branch operands as plaintexts. + let first = registers.load_plaintext_circuit(stack, &self.operands[1])?; + let second = registers.load_plaintext_circuit(stack, &self.operands[2])?; + + // Select between the two branches. + let output = as circuit::traits::Ternary>::ternary(&condition, &first, &second); + + // Store the output. + registers.store_circuit(stack, &self.destination, circuit::Value::Plaintext(output)) + } + + /// Finalizes the instruction. + #[inline] + pub fn finalize( + &self, + stack: &impl StackTrait, + _store: Option<&dyn FinalizeStoreTrait>, + registers: &mut impl FinalizeRegistersState, + ) -> Result<()> { + self.evaluate(stack, registers) + } + + /// Returns the output type from the given program and input types. + pub fn output_types( + &self, + stack: &impl StackTrait, + input_types: &[RegisterType], + ) -> Result>> { + // Ensure the number of input types is correct. + if input_types.len() != 3 { + bail!("Instruction '{}' expects 3 inputs, found {} inputs", Self::opcode(), input_types.len()) + } + // Ensure the number of operands is correct. + if self.operands.len() != 3 { + bail!("Instruction '{}' expects 3 operands, found {} operands", Self::opcode(), self.operands.len()) + } + + // Ensure the first input is a boolean literal. + match &input_types[0] { + RegisterType::Plaintext(PlaintextType::Literal(LiteralType::Boolean)) => {} + other => bail!( + "Instruction '{}' expects the first input to be of type 'boolean', found '{other}'", + Self::opcode() + ), + } + + // Ensure the second and third inputs are plaintexts. + let plaintext_type = match (&input_types[1], &input_types[2]) { + (RegisterType::Plaintext(plaintext_type), RegisterType::Plaintext(_)) => plaintext_type.clone(), + _ => bail!( + "Instruction '{}' expects plaintext inputs for the branches, found '{}' and '{}'", + Self::opcode(), + input_types[1], + input_types[2] + ), + }; + + // Ensure the second and third input types are equivalent. + if !register_types_equivalent(stack, &input_types[1], stack, &input_types[2])? { + bail!( + "Instruction '{}' expects the branches to have equivalent types. Found '{}' and '{}'", + Self::opcode(), + input_types[1], + input_types[2] + ) + } + + // Reject plaintext types that contain a `string` leaf: string ternary is not supported + // because strings have a variable byte-length and cannot be selected by a byte-wise MUX. + ensure_no_string_leaves(stack, &plaintext_type)?; + + // The two branches are structurally equivalent, so either one describes the output; pick the first. + Ok(vec![RegisterType::Plaintext(plaintext_type)]) + } +} + +impl Parser for Ternary { + /// Parses a string into an operation. + fn parse(string: &str) -> ParserResult { + // Parse the opcode from the string. + let (string, _) = tag(*Self::opcode())(string)?; + // Parse the whitespace from the string. + let (string, _) = Sanitizer::parse_whitespaces(string)?; + // Parse the first operand (condition) from the string. + let (string, condition) = Operand::parse(string)?; + let (string, _) = Sanitizer::parse_whitespaces(string)?; + // Parse the second operand from the string. + let (string, first) = Operand::parse(string)?; + let (string, _) = Sanitizer::parse_whitespaces(string)?; + // Parse the third operand from the string. + let (string, second) = Operand::parse(string)?; + let (string, _) = Sanitizer::parse_whitespaces(string)?; + // Parse the "into" from the string. + let (string, _) = tag("into")(string)?; + let (string, _) = Sanitizer::parse_whitespaces(string)?; + // Parse the destination register from the string. + let (string, destination) = Register::parse(string)?; + + Ok((string, Self { operands: vec![condition, first, second], destination })) + } +} + +impl FromStr for Ternary { + type Err = Error; + + /// Parses a string into an operation. + fn from_str(string: &str) -> Result { + match Self::parse(string) { + Ok((remainder, object)) => { + ensure!(remainder.is_empty(), "Failed to parse string. Found invalid character in: \"{remainder}\""); + Ok(object) + } + Err(error) => bail!("Failed to parse string. {error}"), + } + } +} + +impl Debug for Ternary { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + Display::fmt(self, f) + } +} + +impl Display for Ternary { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + if self.operands.len() != 3 { + return Err(fmt::Error); + } + write!(f, "{} ", Self::opcode())?; + self.operands.iter().try_for_each(|operand| write!(f, "{operand} "))?; + write!(f, "into {}", self.destination) + } +} + +impl FromBytes for Ternary { + fn read_le(mut reader: R) -> IoResult { + let mut operands = Vec::with_capacity(3); + for _ in 0..3 { + operands.push(Operand::read_le(&mut reader)?); + } + let destination = Register::read_le(&mut reader)?; + Ok(Self { operands, destination }) + } +} + +impl ToBytes for Ternary { + fn write_le(&self, mut writer: W) -> IoResult<()> { + // The invariant is maintained by `new`, `read_le`, and `parse`. + debug_assert_eq!(self.operands.len(), 3, "Instruction 'ternary' must have three operands"); + if self.operands.len() != 3 { + return Err(error(format!("The number of operands must be 3, found {}", self.operands.len()))); + } + self.operands.iter().try_for_each(|operand| operand.write_le(&mut writer))?; + self.destination.write_le(&mut writer) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use console::network::MainnetV0; + + type CurrentNetwork = MainnetV0; + + #[test] + fn test_parse() { + let (remainder, ternary) = Ternary::::parse("ternary r0 r1 r2 into r3").unwrap(); + assert!(remainder.is_empty(), "Parser did not consume all of the string: '{remainder}'"); + assert_eq!(ternary.operands.len(), 3); + assert_eq!(ternary.operands[0], Operand::Register(Register::Locator(0))); + assert_eq!(ternary.operands[1], Operand::Register(Register::Locator(1))); + assert_eq!(ternary.operands[2], Operand::Register(Register::Locator(2))); + assert_eq!(ternary.destination, Register::Locator(3)); + } + + #[test] + fn test_display_roundtrip() { + let input = "ternary r0 r1 r2 into r3"; + let ternary = Ternary::::from_str(input).unwrap(); + assert_eq!(input, ternary.to_string()); + } + + #[test] + fn test_bytes_roundtrip() { + let ternary = Ternary::::from_str("ternary r0 r1 r2 into r3").unwrap(); + let bytes = ternary.to_bytes_le().unwrap(); + let decoded = Ternary::::from_bytes_le(&bytes).unwrap(); + assert_eq!(ternary, decoded); + } +} diff --git a/synthesizer/src/vm/helpers/program.rs b/synthesizer/src/vm/helpers/program.rs index e14cd061f2..442f9ee8f3 100644 --- a/synthesizer/src/vm/helpers/program.rs +++ b/synthesizer/src/vm/helpers/program.rs @@ -13,12 +13,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::Stack; +use crate::{Stack, process::FinalizeTypes}; use console::{ prelude::{Network, cfg_iter}, - program::{Identifier, Locator, ValueType}, + program::{FinalizeType, Identifier, LiteralType, Locator, PlaintextType, RegisterType, ValueType}, }; -use snarkvm_synthesizer_program::{Program, StackTrait}; +use snarkvm_synthesizer_program::{Command, Instruction, Program, StackTrait}; use anyhow::{Result, anyhow, bail, ensure}; @@ -102,3 +102,96 @@ pub fn check_future_argument_bit_size( }) }) } + +/// Ensures every `ternary` instruction in the program operates on a branch operand whose type was +/// supported by `ternary` prior to `ConsensusVersion::V16`. V16 added support for the `identifier` +/// literal type plus arrays and structs; those are rejected here so a program containing them does +/// not deploy before V16 and diverge from the pre-PR network. +pub fn check_no_non_literal_ternary(program: &Program, stack: &Stack) -> Result<()> { + // The error message used when an unsupported ternary operand type is found. + const ERROR_MSG: &str = "ternary on this operand type is not allowed before `ConsensusVersion::V16`"; + + // Returns whether the given plaintext type was a valid ternary branch operand before V16. + let is_pre_v16_supported = |plaintext_type: &PlaintextType| -> bool { + matches!( + plaintext_type, + PlaintextType::Literal( + LiteralType::Address + | LiteralType::Boolean + | LiteralType::Field + | LiteralType::Group + | LiteralType::I8 + | LiteralType::I16 + | LiteralType::I32 + | LiteralType::I64 + | LiteralType::I128 + | LiteralType::U8 + | LiteralType::U16 + | LiteralType::U32 + | LiteralType::U64 + | LiteralType::U128 + | LiteralType::Scalar + | LiteralType::Signature + ) + ) + }; + + // Checks the ternary instructions within the given function or closure instruction sequence. + let check_instructions = |name: &Identifier, instructions: &[Instruction]| -> Result<()> { + let register_types = stack.get_register_types(name)?; + for instruction in instructions { + if let Instruction::Ternary(ternary) = instruction { + // The ternary operands are `[condition, first, second]`. + for operand in &ternary.operands()[1..3] { + let reg_type = register_types.get_type_from_operand(stack, operand)?; + match reg_type { + RegisterType::Plaintext(plaintext_type) => { + ensure!(is_pre_v16_supported(&plaintext_type), "{ERROR_MSG}"); + } + _ => bail!("{ERROR_MSG}"), + } + } + } + } + Ok(()) + }; + + // Checks the ternary instructions within the given finalize or constructor command sequence. + let check_commands = |finalize_types: &FinalizeTypes, commands: &[Command]| -> Result<()> { + for command in commands { + if let Command::Instruction(Instruction::Ternary(ternary)) = command { + // The ternary operands are `[condition, first, second]`. + for operand in &ternary.operands()[1..3] { + let finalize_type = finalize_types.get_type_from_operand(stack, operand)?; + match finalize_type { + FinalizeType::Plaintext(plaintext_type) => { + ensure!(is_pre_v16_supported(&plaintext_type), "{ERROR_MSG}"); + } + _ => bail!("{ERROR_MSG}"), + } + } + } + } + Ok(()) + }; + + // Check every function body and its finalize scope (if any). + for (name, function) in program.functions() { + check_instructions(name, function.instructions())?; + if let Some(finalize) = function.finalize_logic() { + let finalize_types = stack.get_finalize_types(name)?; + check_commands(&finalize_types, finalize.commands())?; + } + } + // Check every closure body. + for (name, closure) in program.closures() { + check_instructions(name, closure.instructions())?; + } + // Check the program's constructor, if any. + if let Some(constructor) = program.constructor() { + let constructor_types = stack.get_constructor_types()?; + check_commands(&constructor_types, constructor.commands())?; + } + + Ok(()) +} diff --git a/synthesizer/src/vm/tests/mod.rs b/synthesizer/src/vm/tests/mod.rs index 6d6c152535..959ff0b406 100644 --- a/synthesizer/src/vm/tests/mod.rs +++ b/synthesizer/src/vm/tests/mod.rs @@ -34,5 +34,8 @@ mod test_v14; #[cfg(feature = "test")] mod test_v15; +#[cfg(feature = "test")] +mod test_v16; + #[cfg(feature = "test")] use super::*; diff --git a/synthesizer/src/vm/tests/test_v14/identifier_literal.rs b/synthesizer/src/vm/tests/test_v14/identifier_literal.rs index 87bd17f60b..ab48174389 100644 --- a/synthesizer/src/vm/tests/test_v14/identifier_literal.rs +++ b/synthesizer/src/vm/tests/test_v14/identifier_literal.rs @@ -766,8 +766,9 @@ constructor: vm.add_next_block(&block).unwrap(); } -/// Verifies that ternary operations on identifier literals are rejected in function scope. -/// Ternary is not defined for the Identifier type, so deployment should fail. +/// Verifies that ternary operations on identifier literals are rejected before `ConsensusVersion::V16` +/// in function scope. Identifier ternary is a V16 feature, so at V14 the well-formed deployment is +/// aborted at block verification rather than rejected at `deploy` time. #[test] fn test_identifier_literal_ternary_rejected_in_function() { let rng = &mut TestRng::default(); @@ -796,15 +797,21 @@ constructor: ) .unwrap(); - // Attempt to deploy — should fail because ternary does not support Identifier. - let result = vm.deploy(&private_key, &program, None, 0, None, rng); - assert!(result.is_err(), "ternary on identifier should be rejected in function scope"); - let err = result.unwrap_err().to_string(); - assert!(err.contains("ternary"), "error should mention the ternary instruction, got: {err}"); + // The program is well-formed, so `deploy` succeeds; the deployment is aborted at block + // verification because `ternary` does not support `identifier` operands before V16. + let deployment = vm.deploy(&private_key, &program, None, 0, None, rng).unwrap(); + let block = sample_next_block(&vm, &private_key, &[deployment], rng).unwrap(); + assert_eq!( + block.transactions().num_accepted(), + 0, + "identifier ternary deployment before V16 should not be accepted in function scope" + ); + assert_eq!(block.aborted_transaction_ids().len(), 1, "identifier ternary deployment before V16 should be aborted"); } -/// Verifies that ternary operations on identifier literals are rejected in finalize scope. -/// Ternary is not defined for the Identifier type, so deployment should fail. +/// Verifies that ternary operations on identifier literals are rejected before `ConsensusVersion::V16` +/// in finalize scope. Identifier ternary is a V16 feature, so at V14 the well-formed deployment is +/// aborted at block verification rather than rejected at `deploy` time. #[test] fn test_identifier_literal_ternary_rejected_in_finalize() { let rng = &mut TestRng::default(); @@ -839,11 +846,16 @@ constructor: ) .unwrap(); - // Attempt to deploy — should fail because ternary does not support Identifier. - let result = vm.deploy(&private_key, &program, None, 0, None, rng); - assert!(result.is_err(), "ternary on identifier should be rejected in finalize scope"); - let err = result.unwrap_err().to_string(); - assert!(err.contains("ternary"), "error should mention the ternary instruction, got: {err}"); + // The program is well-formed, so `deploy` succeeds; the deployment is aborted at block + // verification because `ternary` does not support `identifier` operands before V16. + let deployment = vm.deploy(&private_key, &program, None, 0, None, rng).unwrap(); + let block = sample_next_block(&vm, &private_key, &[deployment], rng).unwrap(); + assert_eq!( + block.transactions().num_accepted(), + 0, + "identifier ternary deployment before V16 should not be accepted in finalize scope" + ); + assert_eq!(block.aborted_transaction_ids().len(), 1, "identifier ternary deployment before V16 should be aborted"); } /// Verifies that `get.dynamic` and `get.or_use.dynamic` accept identifier literals as program diff --git a/synthesizer/src/vm/tests/test_v16/mod.rs b/synthesizer/src/vm/tests/test_v16/mod.rs new file mode 100644 index 0000000000..de6b52a433 --- /dev/null +++ b/synthesizer/src/vm/tests/test_v16/mod.rs @@ -0,0 +1,31 @@ +// 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. + +// Tests on `ternary` instruction accepting array, struct, and identifier operands from V16 onward. +mod ternary_plaintext; + +use super::*; + +use crate::vm::test_helpers::{sample_vm_at_height, *}; + +use console::{ + network::ConsensusVersion, + program::{Identifier, Value}, +}; + +use snarkvm_synthesizer_program::Program; +use snarkvm_utilities::TestRng; + +use super::test_v14::add_and_test_with_costs; diff --git a/synthesizer/src/vm/tests/test_v16/ternary_plaintext.rs b/synthesizer/src/vm/tests/test_v16/ternary_plaintext.rs new file mode 100644 index 0000000000..0b6b736a60 --- /dev/null +++ b/synthesizer/src/vm/tests/test_v16/ternary_plaintext.rs @@ -0,0 +1,1500 @@ +// 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::*; + +use console::program::Plaintext; + +// A program that selects between two `[field; 3u32]` arrays with `ternary`. +const TERNARY_ARRAY_PROGRAM: &str = r" +program ternary_array_test.aleo; + +function run: + input r0 as boolean.public; + input r1 as [field; 3u32].public; + input r2 as [field; 3u32].public; + ternary r0 r1 r2 into r3; + output r3 as [field; 3u32].public; + +constructor: + assert.eq true true; +"; + +// A program that selects between two `point` structs with `ternary`. +const TERNARY_STRUCT_PROGRAM: &str = r" +program ternary_struct_test.aleo; + +struct point: + x as field; + y as field; + +function run: + input r0 as boolean.public; + input r1 as point.public; + input r2 as point.public; + ternary r0 r1 r2 into r3; + output r3 as point.public; + +constructor: + assert.eq true true; +"; + +// Tests that a program using `ternary` on array operands is aborted before +// `ConsensusVersion::V16` and accepted at `V16`. +#[test] +fn test_deploy_ternary_array_before_and_at_v16() { + let rng = &mut TestRng::default(); + let caller_private_key = sample_genesis_private_key(rng); + + // Start one block before V16 so that after the rejected block we land at V16. + let v16_height = CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V16).unwrap(); + let vm = sample_vm_at_height(v16_height - 1, rng); + + let program = Program::from_str(TERNARY_ARRAY_PROGRAM).unwrap(); + + // Deployment before V16 should be aborted. + let deployment = vm.deploy(&caller_private_key, &program, None, 0, None, rng).unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[deployment], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 0, "Array ternary deployment before V16 should not be accepted"); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 1, "Array ternary deployment before V16 should be aborted"); + vm.add_next_block(&block).unwrap(); + + // We should now be at V16. + assert_eq!(vm.block_store().current_block_height(), v16_height); + + // Deployment at V16 should succeed. + let deployment = vm.deploy(&caller_private_key, &program, None, 0, None, rng).unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[deployment], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1, "Array ternary deployment at V16 should be accepted"); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block).unwrap(); +} + +// Tests that a program using `ternary` on struct operands is aborted before +// `ConsensusVersion::V16` and accepted at `V16`. +#[test] +fn test_deploy_ternary_struct_before_and_at_v16() { + let rng = &mut TestRng::default(); + let caller_private_key = sample_genesis_private_key(rng); + + let v16_height = CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V16).unwrap(); + let vm = sample_vm_at_height(v16_height - 1, rng); + + let program = Program::from_str(TERNARY_STRUCT_PROGRAM).unwrap(); + + // Deployment before V16 should be aborted. + let deployment = vm.deploy(&caller_private_key, &program, None, 0, None, rng).unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[deployment], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 0, "Struct ternary deployment before V16 should not be accepted"); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 1, "Struct ternary deployment before V16 should be aborted"); + vm.add_next_block(&block).unwrap(); + + assert_eq!(vm.block_store().current_block_height(), v16_height); + + // Deployment at V16 should succeed. + let deployment = vm.deploy(&caller_private_key, &program, None, 0, None, rng).unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[deployment], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1, "Struct ternary deployment at V16 should be accepted"); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block).unwrap(); +} + +// A program that uses `ternary` on non-literal operands inside a `finalize` block. +const TERNARY_FINALIZE_PRE_V16_PROGRAM: &str = r" +program ternary_finalize_pre_v16.aleo; + +mapping selected: + key as u8.public; + value as [field; 3u32].public; + +function run: + input r0 as boolean.public; + input r1 as [field; 3u32].public; + input r2 as [field; 3u32].public; + async run r0 r1 r2 into r3; + output r3 as ternary_finalize_pre_v16.aleo/run.future; + +finalize run: + input r0 as boolean.public; + input r1 as [field; 3u32].public; + input r2 as [field; 3u32].public; + ternary r0 r1 r2 into r3; + set r3 into selected[0u8]; + +constructor: + assert.eq true true; +"; + +// A program that uses `ternary` on non-literal operands inside the `constructor`. +const TERNARY_CONSTRUCTOR_PRE_V16_PROGRAM: &str = r" +program ternary_constructor_pre_v16.aleo; + +struct point: + x as field; + y as field; + +function dummy: + input r0 as u32.public; + output r0 as u32.public; + +constructor: + cast 1field 2field into r0 as point; + cast 3field 4field into r1 as point; + ternary true r0 r1 into r2; + assert.eq true true; +"; + +// Tests that a program using non-literal `ternary` inside a `finalize` block is aborted before +// `ConsensusVersion::V16` and accepted at `V16`. +#[test] +fn test_deploy_ternary_in_finalize_before_and_at_v16() { + let rng = &mut TestRng::default(); + let caller_private_key = sample_genesis_private_key(rng); + + // Start one block before V16 so that after the rejected block we land at V16. + let v16_height = CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V16).unwrap(); + let vm = sample_vm_at_height(v16_height - 1, rng); + + let program = Program::from_str(TERNARY_FINALIZE_PRE_V16_PROGRAM).unwrap(); + + // Deployment before V16 should be aborted because the finalize uses ternary on arrays. + let deployment = vm.deploy(&caller_private_key, &program, None, 0, None, rng).unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[deployment], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 0, "Finalize ternary deployment before V16 should not be accepted"); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 1, "Finalize ternary deployment before V16 should be aborted"); + vm.add_next_block(&block).unwrap(); + + // We should now be at V16. + assert_eq!(vm.block_store().current_block_height(), v16_height); + + // Deployment at V16 should succeed. + let deployment = vm.deploy(&caller_private_key, &program, None, 0, None, rng).unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[deployment], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1, "Finalize ternary deployment at V16 should be accepted"); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block).unwrap(); +} + +// Tests that a program using non-literal `ternary` inside the `constructor` is aborted before +// `ConsensusVersion::V16` and accepted at `V16`. +#[test] +fn test_deploy_ternary_in_constructor_before_and_at_v16() { + let rng = &mut TestRng::default(); + let caller_private_key = sample_genesis_private_key(rng); + + let v16_height = CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V16).unwrap(); + let vm = sample_vm_at_height(v16_height - 1, rng); + + let program = Program::from_str(TERNARY_CONSTRUCTOR_PRE_V16_PROGRAM).unwrap(); + + // Deployment before V16 should be aborted because the constructor uses ternary on structs. + let deployment = vm.deploy(&caller_private_key, &program, None, 0, None, rng).unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[deployment], rng).unwrap(); + assert_eq!( + block.transactions().num_accepted(), + 0, + "Constructor ternary deployment before V16 should not be accepted" + ); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 1, "Constructor ternary deployment before V16 should be aborted"); + vm.add_next_block(&block).unwrap(); + + assert_eq!(vm.block_store().current_block_height(), v16_height); + + // Deployment at V16 should succeed. + let deployment = vm.deploy(&caller_private_key, &program, None, 0, None, rng).unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[deployment], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1, "Constructor ternary deployment at V16 should be accepted"); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block).unwrap(); +} + +// Tests that `ternary` on array operands selects the correct branch at V16. +#[test] +fn test_execute_ternary_array() { + let rng = &mut TestRng::default(); + let caller_private_key = sample_genesis_private_key(rng); + + // Initialize the VM at V16 so that non-literal ternary programs can be deployed. + let v16_height = CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V16).unwrap(); + let vm = sample_vm_at_height(v16_height, rng); + + // Deploy the array ternary program. + let program = Program::from_str(TERNARY_ARRAY_PROGRAM).unwrap(); + let deployment = vm.deploy(&caller_private_key, &program, None, 0, None, rng).unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[deployment], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1, "Array ternary deployment at V16 should be accepted"); + vm.add_next_block(&block).unwrap(); + + let first = "[ 1field, 2field, 3field ]"; + let second = "[ 4field, 5field, 6field ]"; + + // Execute with `true` condition: output should equal `first`. + let inputs_true = [ + Value::::from_str("true").unwrap(), + Value::from_str(first).unwrap(), + Value::from_str(second).unwrap(), + ]; + let execution = vm + .execute(&caller_private_key, ("ternary_array_test.aleo", "run"), inputs_true.iter(), None, 0, None, rng) + .unwrap(); + let expected_first = Plaintext::::from_str(first).unwrap(); + match &execution.transitions().next().unwrap().outputs()[0] { + Output::Public(_, Some(plaintext)) => assert_eq!(*plaintext, expected_first), + other => panic!("Expected public output, got: {other:?}"), + } + let block = sample_next_block(&vm, &caller_private_key, &[execution], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1, "Array ternary execution (true) should be accepted"); + vm.add_next_block(&block).unwrap(); + + // Execute with `false` condition: output should equal `second`. + let inputs_false = [ + Value::::from_str("false").unwrap(), + Value::from_str(first).unwrap(), + Value::from_str(second).unwrap(), + ]; + let execution = vm + .execute(&caller_private_key, ("ternary_array_test.aleo", "run"), inputs_false.iter(), None, 0, None, rng) + .unwrap(); + let expected_second = Plaintext::::from_str(second).unwrap(); + match &execution.transitions().next().unwrap().outputs()[0] { + Output::Public(_, Some(plaintext)) => assert_eq!(*plaintext, expected_second), + other => panic!("Expected public output, got: {other:?}"), + } + let block = sample_next_block(&vm, &caller_private_key, &[execution], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1, "Array ternary execution (false) should be accepted"); + vm.add_next_block(&block).unwrap(); +} + +// Tests that `ternary` on struct operands selects the correct branch at V16. +#[test] +fn test_execute_ternary_struct() { + let rng = &mut TestRng::default(); + let caller_private_key = sample_genesis_private_key(rng); + + let v16_height = CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V16).unwrap(); + let vm = sample_vm_at_height(v16_height, rng); + + let program = Program::from_str(TERNARY_STRUCT_PROGRAM).unwrap(); + let deployment = vm.deploy(&caller_private_key, &program, None, 0, None, rng).unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[deployment], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1, "Struct ternary deployment at V16 should be accepted"); + vm.add_next_block(&block).unwrap(); + + let first = "{ x: 1field, y: 2field }"; + let second = "{ x: 3field, y: 4field }"; + + // Execute with `true` condition. + let inputs_true = [ + Value::::from_str("true").unwrap(), + Value::from_str(first).unwrap(), + Value::from_str(second).unwrap(), + ]; + let execution = vm + .execute(&caller_private_key, ("ternary_struct_test.aleo", "run"), inputs_true.iter(), None, 0, None, rng) + .unwrap(); + let expected_first = Plaintext::::from_str(first).unwrap(); + match &execution.transitions().next().unwrap().outputs()[0] { + Output::Public(_, Some(plaintext)) => assert_eq!(*plaintext, expected_first), + other => panic!("Expected public output, got: {other:?}"), + } + let block = sample_next_block(&vm, &caller_private_key, &[execution], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1, "Struct ternary execution (true) should be accepted"); + vm.add_next_block(&block).unwrap(); + + // Execute with `false` condition. + let inputs_false = [ + Value::::from_str("false").unwrap(), + Value::from_str(first).unwrap(), + Value::from_str(second).unwrap(), + ]; + let execution = vm + .execute(&caller_private_key, ("ternary_struct_test.aleo", "run"), inputs_false.iter(), None, 0, None, rng) + .unwrap(); + let expected_second = Plaintext::::from_str(second).unwrap(); + match &execution.transitions().next().unwrap().outputs()[0] { + Output::Public(_, Some(plaintext)) => assert_eq!(*plaintext, expected_second), + other => panic!("Expected public output, got: {other:?}"), + } + let block = sample_next_block(&vm, &caller_private_key, &[execution], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1, "Struct ternary execution (false) should be accepted"); + vm.add_next_block(&block).unwrap(); +} + +// A program exercising `ternary` on arrays of several sizes and element types, including a +// nested array. The three functions share the same program so a single deployment suffices. +const TERNARY_ARRAY_VARIOUS_SIZES_PROGRAM: &str = r" +program ternary_array_sizes.aleo; + +function run_single: + input r0 as boolean.public; + input r1 as [u8; 1u32].public; + input r2 as [u8; 1u32].public; + ternary r0 r1 r2 into r3; + output r3 as [u8; 1u32].public; + +function run_large: + input r0 as boolean.public; + input r1 as [u32; 10u32].public; + input r2 as [u32; 10u32].public; + ternary r0 r1 r2 into r3; + output r3 as [u32; 10u32].public; + +function run_nested: + input r0 as boolean.public; + input r1 as [[field; 2u32]; 3u32].public; + input r2 as [[field; 2u32]; 3u32].public; + ternary r0 r1 r2 into r3; + output r3 as [[field; 2u32]; 3u32].public; + +constructor: + assert.eq true true; +"; + +// Tests `ternary` on arrays of varying element types, array lengths, and nesting depth. +#[test] +fn test_execute_ternary_various_array_sizes() { + let rng = &mut TestRng::default(); + let caller_private_key = sample_genesis_private_key(rng); + + let v16_height = CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V16).unwrap(); + let vm = sample_vm_at_height(v16_height, rng); + + let program = Program::from_str(TERNARY_ARRAY_VARIOUS_SIZES_PROGRAM).unwrap(); + let deployment = vm.deploy(&caller_private_key, &program, None, 0, None, rng).unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[deployment], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1, "Deployment at V16 should be accepted"); + vm.add_next_block(&block).unwrap(); + + // For each (function, first, second) tuple, run ternary with both `true` and `false` and + // verify the output matches the expected branch. + let cases: &[(&str, &str, &str)] = &[ + ("run_single", "[ 7u8 ]", "[ 42u8 ]"), + ( + "run_large", + "[ 0u32, 1u32, 2u32, 3u32, 4u32, 5u32, 6u32, 7u32, 8u32, 9u32 ]", + "[ 10u32, 11u32, 12u32, 13u32, 14u32, 15u32, 16u32, 17u32, 18u32, 19u32 ]", + ), + ( + "run_nested", + "[ [ 1field, 2field ], [ 3field, 4field ], [ 5field, 6field ] ]", + "[ [ 7field, 8field ], [ 9field, 10field ], [ 11field, 12field ] ]", + ), + ]; + + for (function, first, second) in cases { + for (cond_str, expected_str) in [("true", *first), ("false", *second)] { + let inputs = [ + Value::::from_str(cond_str).unwrap(), + Value::from_str(first).unwrap(), + Value::from_str(second).unwrap(), + ]; + let execution = vm + .execute( + &caller_private_key, + ("ternary_array_sizes.aleo", *function), + inputs.iter(), + None, + 0, + None, + rng, + ) + .unwrap(); + let expected = Plaintext::::from_str(expected_str).unwrap(); + match &execution.transitions().next().unwrap().outputs()[0] { + Output::Public(_, Some(plaintext)) => { + assert_eq!(*plaintext, expected, "{function} with condition={cond_str}") + } + other => panic!("Expected public output, got: {other:?}"), + } + let block = sample_next_block(&vm, &caller_private_key, &[execution], rng).unwrap(); + assert_eq!( + block.transactions().num_accepted(), + 1, + "{function} with condition={cond_str} should be accepted" + ); + vm.add_next_block(&block).unwrap(); + } + } +} + +// Tests that deploying a program where `ternary` branches have arrays of different lengths +// is rejected by the type checker. +#[test] +fn test_deploy_ternary_array_size_mismatch_rejected() { + let rng = &mut TestRng::default(); + let caller_private_key = sample_genesis_private_key(rng); + + let v16_height = CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V16).unwrap(); + let vm = sample_vm_at_height(v16_height, rng); + + let program = Program::from_str( + r" + program ternary_array_mismatch.aleo; + + function bad: + input r0 as boolean.public; + input r1 as [field; 2u32].public; + input r2 as [field; 3u32].public; + ternary r0 r1 r2 into r3; + output r3 as [field; 2u32].public; + + constructor: + assert.eq true true; + ", + ) + .unwrap(); + + let result = vm.deploy(&caller_private_key, &program, None, 0, None, rng); + assert!(result.is_err(), "Deployment should fail for ternary on arrays of different lengths"); +} + +// Tests that deploying a program where `ternary` branches have different struct types is +// rejected by the type checker. +#[test] +fn test_deploy_ternary_struct_mismatch_rejected() { + let rng = &mut TestRng::default(); + let caller_private_key = sample_genesis_private_key(rng); + + let v16_height = CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V16).unwrap(); + let vm = sample_vm_at_height(v16_height, rng); + + let program = Program::from_str( + r" + program ternary_struct_mismatch.aleo; + + struct point_a: + x as field; + y as field; + + struct point_b: + x as field; + z as field; + + function bad: + input r0 as boolean.public; + input r1 as point_a.public; + input r2 as point_b.public; + ternary r0 r1 r2 into r3; + output r3 as point_a.public; + + constructor: + assert.eq true true; + ", + ) + .unwrap(); + + let result = vm.deploy(&caller_private_key, &program, None, 0, None, rng); + assert!(result.is_err(), "Deployment should fail for ternary on two distinct struct types"); +} + +// Tests `ternary` on a struct imported from another program (external struct). The child +// program imports the parent's `shared_point` struct and uses it as both branch operands. +#[test] +fn test_execute_ternary_external_struct() { + let rng = &mut TestRng::default(); + let caller_private_key = sample_genesis_private_key(rng); + + let v16_height = CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V16).unwrap(); + let vm = sample_vm_at_height(v16_height, rng); + + // Parent program declares `shared_point` and has a noop function so it deploys cleanly. + let parent_program = Program::from_str( + r" + program ternary_ext_parent.aleo; + + struct shared_point: + x as field; + y as field; + + function noop: + input r0 as u32.public; + output r0 as u32.public; + + constructor: + assert.eq true true; + ", + ) + .unwrap(); + + // Child program imports the parent and uses the external struct as both ternary branches. + let child_program = Program::from_str( + r" + import ternary_ext_parent.aleo; + + program ternary_ext_child.aleo; + + function run: + input r0 as boolean.public; + input r1 as ternary_ext_parent.aleo/shared_point.public; + input r2 as ternary_ext_parent.aleo/shared_point.public; + ternary r0 r1 r2 into r3; + output r3 as ternary_ext_parent.aleo/shared_point.public; + + constructor: + assert.eq true true; + ", + ) + .unwrap(); + + // Deploy the parent, then the child. + let deployment = vm.deploy(&caller_private_key, &parent_program, None, 0, None, rng).unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[deployment], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1, "Parent deployment at V16 should be accepted"); + vm.add_next_block(&block).unwrap(); + + let deployment = vm.deploy(&caller_private_key, &child_program, None, 0, None, rng).unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[deployment], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1, "Child deployment at V16 should be accepted"); + vm.add_next_block(&block).unwrap(); + + let first = "{ x: 1field, y: 2field }"; + let second = "{ x: 3field, y: 4field }"; + + // Execute with both `true` and `false` conditions and verify the correct branch is returned. + for (cond_str, expected_str) in [("true", first), ("false", second)] { + let inputs = [ + Value::::from_str(cond_str).unwrap(), + Value::from_str(first).unwrap(), + Value::from_str(second).unwrap(), + ]; + let execution = vm + .execute(&caller_private_key, ("ternary_ext_child.aleo", "run"), inputs.iter(), None, 0, None, rng) + .unwrap(); + let expected = Plaintext::::from_str(expected_str).unwrap(); + match &execution.transitions().next().unwrap().outputs()[0] { + Output::Public(_, Some(plaintext)) => { + assert_eq!(*plaintext, expected, "external struct ternary with condition={cond_str}") + } + other => panic!("Expected public output, got: {other:?}"), + } + let block = sample_next_block(&vm, &caller_private_key, &[execution], rng).unwrap(); + assert_eq!( + block.transactions().num_accepted(), + 1, + "external struct ternary execution (condition={cond_str}) should be accepted" + ); + vm.add_next_block(&block).unwrap(); + } +} + +// A program that exercises `ternary` on arrays and structs inside finalize blocks. Each +// finalize writes the selected branch to a mapping so that the on-chain result can be +// verified by reading the mapping value back. +const TERNARY_FINALIZE_PROGRAM: &str = r" +program ternary_finalize_test.aleo; + +struct point: + x as field; + y as field; + +mapping selected_array: + key as u8.public; + value as [field; 3u32].public; + +mapping selected_struct: + key as u8.public; + value as point.public; + +function run_array: + input r0 as boolean.public; + input r1 as [field; 3u32].public; + input r2 as [field; 3u32].public; + async run_array r0 r1 r2 into r3; + output r3 as ternary_finalize_test.aleo/run_array.future; + +finalize run_array: + input r0 as boolean.public; + input r1 as [field; 3u32].public; + input r2 as [field; 3u32].public; + ternary r0 r1 r2 into r3; + set r3 into selected_array[0u8]; + +function run_struct: + input r0 as boolean.public; + input r1 as point.public; + input r2 as point.public; + async run_struct r0 r1 r2 into r3; + output r3 as ternary_finalize_test.aleo/run_struct.future; + +finalize run_struct: + input r0 as boolean.public; + input r1 as point.public; + input r2 as point.public; + ternary r0 r1 r2 into r3; + set r3 into selected_struct[0u8]; + +constructor: + assert.eq true true; +"; + +// Tests that `ternary` on arrays and structs works in a finalize block at V16: the selected +// branch is written to a mapping and verified by reading the mapping value back. +#[test] +fn test_execute_ternary_in_finalize() { + let rng = &mut TestRng::default(); + let caller_private_key = sample_genesis_private_key(rng); + + let v16_height = CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V16).unwrap(); + let vm = sample_vm_at_height(v16_height, rng); + + let program = Program::from_str(TERNARY_FINALIZE_PROGRAM).unwrap(); + let deployment = vm.deploy(&caller_private_key, &program, None, 0, None, rng).unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[deployment], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1, "Finalize ternary deployment at V16 should be accepted"); + vm.add_next_block(&block).unwrap(); + + let program_id = ProgramID::::from_str("ternary_finalize_test.aleo").unwrap(); + let zero_key = Plaintext::::from_str("0u8").unwrap(); + + // Reads the value stored under key `0u8` in the named mapping and asserts it equals the + // expected plaintext. + let assert_mapping = |mapping_name: &str, expected_str: &str| { + let mapping_id = Identifier::::from_str(mapping_name).unwrap(); + let value = vm.finalize_store().get_value_confirmed(program_id, mapping_id, &zero_key).unwrap(); + let expected = Plaintext::::from_str(expected_str).unwrap(); + match value { + Some(Value::Plaintext(p)) => assert_eq!(p, expected, "{mapping_name} mismatch"), + other => panic!("expected plaintext value in mapping {mapping_name}, got: {other:?}"), + } + }; + + // (function, first, second, mapping) tuples covering both array and struct ternary in finalize. + let cases: &[(&str, &str, &str, &str)] = &[ + ("run_array", "[ 1field, 2field, 3field ]", "[ 4field, 5field, 6field ]", "selected_array"), + ("run_struct", "{ x: 1field, y: 2field }", "{ x: 3field, y: 4field }", "selected_struct"), + ]; + + for (function, first, second, mapping) in cases { + for (cond_str, expected_str) in [("true", *first), ("false", *second)] { + let inputs = [ + Value::::from_str(cond_str).unwrap(), + Value::from_str(first).unwrap(), + Value::from_str(second).unwrap(), + ]; + let execution = vm + .execute( + &caller_private_key, + ("ternary_finalize_test.aleo", *function), + inputs.iter(), + None, + 0, + None, + rng, + ) + .unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[execution], rng).unwrap(); + assert_eq!( + block.transactions().num_accepted(), + 1, + "{function} finalize ternary (condition={cond_str}) should be accepted" + ); + vm.add_next_block(&block).unwrap(); + assert_mapping(mapping, expected_str); + } + } +} + +// Tests that `ternary` on `string` operands is rejected by the type checker at V16. +// The Aleo `string` type has a variable byte-length, so it cannot be selected by a byte-wise +// MUX circuit; `output_types` refuses any plaintext whose leaves include `string`. +#[test] +fn test_deploy_ternary_string_rejected() { + let rng = &mut TestRng::default(); + let caller_private_key = sample_genesis_private_key(rng); + + let v16_height = CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V16).unwrap(); + let vm = sample_vm_at_height(v16_height, rng); + + let program = Program::from_str( + r" + program ternary_string_bad.aleo; + + function bad: + input r0 as boolean.public; + input r1 as string.public; + input r2 as string.public; + ternary r0 r1 r2 into r3; + output r3 as string.public; + + constructor: + assert.eq true true; + ", + ) + .unwrap(); + + let result = vm.deploy(&caller_private_key, &program, None, 0, None, rng); + assert!(result.is_err(), "Deployment should fail for ternary on string operands"); +} + +// A program that selects between two `identifier` operands with `ternary`. +const TERNARY_IDENTIFIER_PROGRAM: &str = r" +program ternary_identifier_test.aleo; + +function run: + input r0 as boolean.public; + input r1 as identifier.public; + input r2 as identifier.public; + ternary r0 r1 r2 into r3; + output r3 as identifier.public; + +constructor: + assert.eq true true; +"; + +// Tests that a program using `ternary` on `identifier` operands is aborted before +// `ConsensusVersion::V16` and accepted at `V16`. Identifier ternary was not supported pre-V16, +// so `check_no_non_literal_ternary` must reject it at V14 to preserve consensus. +#[test] +fn test_deploy_ternary_identifier_before_and_at_v16() { + let rng = &mut TestRng::default(); + let caller_private_key = sample_genesis_private_key(rng); + + let v16_height = CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V16).unwrap(); + let vm = sample_vm_at_height(v16_height - 1, rng); + + let program = Program::from_str(TERNARY_IDENTIFIER_PROGRAM).unwrap(); + + // Deployment before V16 should be aborted because `identifier` is not a pre-V16 ternary operand. + let deployment = vm.deploy(&caller_private_key, &program, None, 0, None, rng).unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[deployment], rng).unwrap(); + assert_eq!( + block.transactions().num_accepted(), + 0, + "Identifier ternary deployment before V16 should not be accepted" + ); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 1, "Identifier ternary deployment before V16 should be aborted"); + vm.add_next_block(&block).unwrap(); + + assert_eq!(vm.block_store().current_block_height(), v16_height); + + // Deployment at V16 should succeed. + let deployment = vm.deploy(&caller_private_key, &program, None, 0, None, rng).unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[deployment], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1, "Identifier ternary deployment at V16 should be accepted"); + vm.add_next_block(&block).unwrap(); +} + +// Tests that `ternary` on `identifier` operands selects the correct branch at V16. +#[test] +fn test_execute_ternary_identifier() { + let rng = &mut TestRng::default(); + let caller_private_key = sample_genesis_private_key(rng); + + let v16_height = CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V16).unwrap(); + let vm = sample_vm_at_height(v16_height, rng); + + let program = Program::from_str(TERNARY_IDENTIFIER_PROGRAM).unwrap(); + let deployment = vm.deploy(&caller_private_key, &program, None, 0, None, rng).unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[deployment], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1); + vm.add_next_block(&block).unwrap(); + + let first = "'alpha'"; + let second = "'beta'"; + for (cond_str, expected_str) in [("true", first), ("false", second)] { + let inputs = [ + Value::::from_str(cond_str).unwrap(), + Value::from_str(first).unwrap(), + Value::from_str(second).unwrap(), + ]; + let execution = vm + .execute(&caller_private_key, ("ternary_identifier_test.aleo", "run"), inputs.iter(), None, 0, None, rng) + .unwrap(); + let expected = Plaintext::::from_str(expected_str).unwrap(); + match &execution.transitions().next().unwrap().outputs()[0] { + Output::Public(_, Some(plaintext)) => { + assert_eq!(*plaintext, expected, "identifier ternary with condition={cond_str}") + } + other => panic!("Expected public output, got: {other:?}"), + } + let block = sample_next_block(&vm, &caller_private_key, &[execution], rng).unwrap(); + assert_eq!( + block.transactions().num_accepted(), + 1, + "identifier ternary execution (condition={cond_str}) should be accepted" + ); + vm.add_next_block(&block).unwrap(); + } +} + +// Tests that `ternary` on a struct whose field is a `string` is rejected at V16. The +// `ensure_no_string_leaves` check walks the struct members and refuses the operation because +// strings cannot be selected by a byte-wise MUX. +#[test] +fn test_deploy_ternary_struct_with_string_rejected() { + let rng = &mut TestRng::default(); + let caller_private_key = sample_genesis_private_key(rng); + + let v16_height = CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V16).unwrap(); + let vm = sample_vm_at_height(v16_height, rng); + + let program = Program::from_str( + r" + program ternary_struct_string_bad.aleo; + + struct tagged: + payload as field; + label as string; + + function bad: + input r0 as boolean.public; + input r1 as tagged.public; + input r2 as tagged.public; + ternary r0 r1 r2 into r3; + output r3 as tagged.public; + + constructor: + assert.eq true true; + ", + ) + .unwrap(); + + let result = vm.deploy(&caller_private_key, &program, None, 0, None, rng); + assert!(result.is_err(), "Deployment should fail for ternary on a struct with a string field"); +} + +// A program that uses `ternary` on non-literal operands inside a `closure` body. Closures are +// walked by `check_no_non_literal_ternary`, so deployment must be aborted before V16. +const TERNARY_CLOSURE_PROGRAM: &str = r" +program ternary_closure_test.aleo; + +closure select_array: + input r0 as boolean; + input r1 as [field; 3u32]; + input r2 as [field; 3u32]; + ternary r0 r1 r2 into r3; + output r3 as [field; 3u32]; + +function run: + input r0 as boolean.public; + input r1 as [field; 3u32].public; + input r2 as [field; 3u32].public; + call select_array r0 r1 r2 into r3; + output r3 as [field; 3u32].public; + +constructor: + assert.eq true true; +"; + +// Tests that a program containing a `ternary` on array operands inside a closure is aborted +// before V16 and accepted at V16, and that the function calling the closure executes correctly. +#[test] +fn test_deploy_ternary_in_closure_before_and_at_v16() { + let rng = &mut TestRng::default(); + let caller_private_key = sample_genesis_private_key(rng); + + let v16_height = CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V16).unwrap(); + let vm = sample_vm_at_height(v16_height - 1, rng); + + let program = Program::from_str(TERNARY_CLOSURE_PROGRAM).unwrap(); + + // Deployment before V16 should be aborted because the closure uses ternary on arrays. + let deployment = vm.deploy(&caller_private_key, &program, None, 0, None, rng).unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[deployment], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 0, "Closure ternary deployment before V16 should not be accepted"); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 1, "Closure ternary deployment before V16 should be aborted"); + vm.add_next_block(&block).unwrap(); + + // We should now be at V16. + assert_eq!(vm.block_store().current_block_height(), v16_height); + + // Deployment at V16 should succeed. + let deployment = vm.deploy(&caller_private_key, &program, None, 0, None, rng).unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[deployment], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1, "Closure ternary deployment at V16 should be accepted"); + vm.add_next_block(&block).unwrap(); + + // Execute and confirm the closure's ternary selects the correct branch. + let first = "[ 1field, 2field, 3field ]"; + let second = "[ 4field, 5field, 6field ]"; + for (cond_str, expected_str) in [("true", first), ("false", second)] { + let inputs = [ + Value::::from_str(cond_str).unwrap(), + Value::from_str(first).unwrap(), + Value::from_str(second).unwrap(), + ]; + let execution = vm + .execute(&caller_private_key, ("ternary_closure_test.aleo", "run"), inputs.iter(), None, 0, None, rng) + .unwrap(); + let expected = Plaintext::::from_str(expected_str).unwrap(); + match &execution.transitions().next().unwrap().outputs()[0] { + Output::Public(_, Some(plaintext)) => { + assert_eq!(*plaintext, expected, "closure ternary with condition={cond_str}") + } + other => panic!("Expected public output, got: {other:?}"), + } + let block = sample_next_block(&vm, &caller_private_key, &[execution], rng).unwrap(); + assert_eq!( + block.transactions().num_accepted(), + 1, + "closure ternary execution (condition={cond_str}) should be accepted" + ); + vm.add_next_block(&block).unwrap(); + } +} + +// A program that exposes one function per supported literal variant in the ternary instruction. +// The functions share a single deployment so all variants are exercised in one VM setup. +const TERNARY_LITERAL_VARIANTS_PROGRAM: &str = r" +program ternary_lit_variants.aleo; + +function sel_address: + input r0 as boolean.public; + input r1 as address.public; + input r2 as address.public; + ternary r0 r1 r2 into r3; + output r3 as address.public; + +function sel_boolean: + input r0 as boolean.public; + input r1 as boolean.public; + input r2 as boolean.public; + ternary r0 r1 r2 into r3; + output r3 as boolean.public; + +function sel_field: + input r0 as boolean.public; + input r1 as field.public; + input r2 as field.public; + ternary r0 r1 r2 into r3; + output r3 as field.public; + +function sel_group: + input r0 as boolean.public; + input r1 as group.public; + input r2 as group.public; + ternary r0 r1 r2 into r3; + output r3 as group.public; + +function sel_scalar: + input r0 as boolean.public; + input r1 as scalar.public; + input r2 as scalar.public; + ternary r0 r1 r2 into r3; + output r3 as scalar.public; + +function sel_i8: + input r0 as boolean.public; + input r1 as i8.public; + input r2 as i8.public; + ternary r0 r1 r2 into r3; + output r3 as i8.public; + +function sel_i16: + input r0 as boolean.public; + input r1 as i16.public; + input r2 as i16.public; + ternary r0 r1 r2 into r3; + output r3 as i16.public; + +function sel_i32: + input r0 as boolean.public; + input r1 as i32.public; + input r2 as i32.public; + ternary r0 r1 r2 into r3; + output r3 as i32.public; + +function sel_i64: + input r0 as boolean.public; + input r1 as i64.public; + input r2 as i64.public; + ternary r0 r1 r2 into r3; + output r3 as i64.public; + +function sel_i128: + input r0 as boolean.public; + input r1 as i128.public; + input r2 as i128.public; + ternary r0 r1 r2 into r3; + output r3 as i128.public; + +function sel_u8: + input r0 as boolean.public; + input r1 as u8.public; + input r2 as u8.public; + ternary r0 r1 r2 into r3; + output r3 as u8.public; + +function sel_u16: + input r0 as boolean.public; + input r1 as u16.public; + input r2 as u16.public; + ternary r0 r1 r2 into r3; + output r3 as u16.public; + +function sel_u32: + input r0 as boolean.public; + input r1 as u32.public; + input r2 as u32.public; + ternary r0 r1 r2 into r3; + output r3 as u32.public; + +function sel_u64: + input r0 as boolean.public; + input r1 as u64.public; + input r2 as u64.public; + ternary r0 r1 r2 into r3; + output r3 as u64.public; + +function sel_u128: + input r0 as boolean.public; + input r1 as u128.public; + input r2 as u128.public; + ternary r0 r1 r2 into r3; + output r3 as u128.public; + +constructor: + assert.eq true true; +"; + +// Tests `ternary` end-to-end on every supported literal variant that can be used as a function +// input. Signature is intentionally omitted because signatures cannot be supplied as inputs +// directly; the dispatch for signatures is exercised in `console/program` unit tests. +#[test] +fn test_execute_ternary_all_literal_variants() { + let rng = &mut TestRng::default(); + let caller_private_key = sample_genesis_private_key(rng); + + let v16_height = CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V16).unwrap(); + let vm = sample_vm_at_height(v16_height, rng); + + let program = Program::from_str(TERNARY_LITERAL_VARIANTS_PROGRAM).unwrap(); + let deployment = vm.deploy(&caller_private_key, &program, None, 0, None, rng).unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[deployment], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1, "Deployment at V16 should be accepted"); + vm.add_next_block(&block).unwrap(); + + // Sample two distinct addresses to use as ternary branches. + let addr_first = Address::try_from(&PrivateKey::::new(rng).unwrap()).unwrap().to_string(); + let addr_second = Address::try_from(&PrivateKey::::new(rng).unwrap()).unwrap().to_string(); + + // One (function, first_value, second_value) tuple per supported literal variant. + let cases: &[(&str, &str, &str)] = &[ + ("sel_address", addr_first.as_str(), addr_second.as_str()), + ("sel_boolean", "true", "false"), + ("sel_field", "1field", "2field"), + ("sel_group", "0group", "2group"), + ("sel_scalar", "1scalar", "2scalar"), + ("sel_i8", "1i8", "-1i8"), + ("sel_i16", "1000i16", "-1000i16"), + ("sel_i32", "1000000i32", "-1000000i32"), + ("sel_i64", "1000000000i64", "-1000000000i64"), + ("sel_i128", "123456789i128", "-123456789i128"), + ("sel_u8", "7u8", "42u8"), + ("sel_u16", "7u16", "42u16"), + ("sel_u32", "7u32", "42u32"), + ("sel_u64", "7u64", "42u64"), + ("sel_u128", "7u128", "42u128"), + ]; + + for (function, first, second) in cases { + for (cond_str, expected_str) in [("true", *first), ("false", *second)] { + let inputs = [ + Value::::from_str(cond_str).unwrap(), + Value::from_str(first).unwrap(), + Value::from_str(second).unwrap(), + ]; + let execution = vm + .execute( + &caller_private_key, + ("ternary_lit_variants.aleo", *function), + inputs.iter(), + None, + 0, + None, + rng, + ) + .unwrap(); + let expected = Plaintext::::from_str(expected_str).unwrap(); + match &execution.transitions().next().unwrap().outputs()[0] { + Output::Public(_, Some(plaintext)) => { + assert_eq!(*plaintext, expected, "{function} with condition={cond_str}") + } + other => panic!("Expected public output, got: {other:?}"), + } + let block = sample_next_block(&vm, &caller_private_key, &[execution], rng).unwrap(); + assert_eq!( + block.transactions().num_accepted(), + 1, + "{function} with condition={cond_str} should be accepted" + ); + vm.add_next_block(&block).unwrap(); + } + } +} + +// Tests that `ternary` on record operands is rejected by the type checker: records are not +// plaintext and must be refused before reaching `Literal::ternary` dispatch. +#[test] +fn test_deploy_ternary_on_record_rejected() { + let rng = &mut TestRng::default(); + let caller_private_key = sample_genesis_private_key(rng); + + let v16_height = CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V16).unwrap(); + let vm = sample_vm_at_height(v16_height, rng); + + let program = Program::from_str( + r" + program ternary_record_bad.aleo; + + record token: + owner as address.private; + amount as u64.private; + + function bad: + input r0 as boolean.public; + input r1 as token.record; + input r2 as token.record; + ternary r0 r1 r2 into r3; + output r3 as token.record; + + constructor: + assert.eq true true; + ", + ) + .unwrap(); + + let result = vm.deploy(&caller_private_key, &program, None, 0, None, rng); + assert!(result.is_err(), "Deployment should fail for ternary on record operands"); +} + +// Tests that `ternary` on future operands in a finalize block is rejected: futures are not +// plaintext and must be refused by the type checker. +#[test] +fn test_deploy_ternary_on_future_rejected() { + let rng = &mut TestRng::default(); + let caller_private_key = sample_genesis_private_key(rng); + + let v16_height = CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V16).unwrap(); + let vm = sample_vm_at_height(v16_height, rng); + + // A program where finalize tries to ternary-select between two futures. The finalize + // receives two futures and attempts a ternary on them, which the type checker must reject. + let program = Program::from_str( + r" + program ternary_future_bad.aleo; + + function inner: + input r0 as u8.public; + async inner r0 into r1; + output r1 as ternary_future_bad.aleo/inner.future; + + finalize inner: + input r0 as u8.public; + assert.eq r0 r0; + + function bad: + input r0 as boolean.public; + input r1 as u8.public; + call inner r1 into r2; + call inner r1 into r3; + async bad r0 r2 r3 into r4; + output r4 as ternary_future_bad.aleo/bad.future; + + finalize bad: + input r0 as boolean.public; + input r1 as ternary_future_bad.aleo/inner.future; + input r2 as ternary_future_bad.aleo/inner.future; + ternary r0 r1 r2 into r3; + await r3; + + constructor: + assert.eq true true; + ", + ); + + // Depending on earlier parse-time validation, construction may already fail; if parsing + // succeeds, deployment must fail because futures are not plaintext. + match program { + Err(_) => {} + Ok(program) => { + let result = vm.deploy(&caller_private_key, &program, None, 0, None, rng); + assert!(result.is_err(), "Deployment should fail for ternary on future operands"); + } + } +} + +// A program exercising `ternary` at greater nesting depths than the basic tests above: a +// triply-nested array, and a struct whose field is itself an array of structs. +const TERNARY_DEEP_NESTING_PROGRAM: &str = r" +program ternary_deep_nesting.aleo; + +struct inner: + a as field; + b as field; + +struct wrapper: + items as [inner; 2u32]; + +function run_triple_array: + input r0 as boolean.public; + input r1 as [[[field; 2u32]; 2u32]; 2u32].public; + input r2 as [[[field; 2u32]; 2u32]; 2u32].public; + ternary r0 r1 r2 into r3; + output r3 as [[[field; 2u32]; 2u32]; 2u32].public; + +function run_struct_array_struct: + input r0 as boolean.public; + input r1 as wrapper.public; + input r2 as wrapper.public; + ternary r0 r1 r2 into r3; + output r3 as wrapper.public; + +constructor: + assert.eq true true; +"; + +// Tests `ternary` at greater nesting depths: a triply-nested array and a struct holding an +// array of structs. Exercises the recursive equivalence check and the `Plaintext::ternary` +// dispatch on deep structures. +#[test] +fn test_execute_ternary_deep_nesting() { + let rng = &mut TestRng::default(); + let caller_private_key = sample_genesis_private_key(rng); + + let v16_height = CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V16).unwrap(); + let vm = sample_vm_at_height(v16_height, rng); + + let program = Program::from_str(TERNARY_DEEP_NESTING_PROGRAM).unwrap(); + let deployment = vm.deploy(&caller_private_key, &program, None, 0, None, rng).unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[deployment], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1, "Deep-nesting deployment at V16 should be accepted"); + vm.add_next_block(&block).unwrap(); + + let cases: &[(&str, &str, &str)] = &[ + ( + "run_triple_array", + "[ [ [ 1field, 2field ], [ 3field, 4field ] ], [ [ 5field, 6field ], [ 7field, 8field ] ] ]", + "[ [ [ 9field, 10field ], [ 11field, 12field ] ], [ [ 13field, 14field ], [ 15field, 16field ] ] ]", + ), + ( + "run_struct_array_struct", + "{ items: [ { a: 1field, b: 2field }, { a: 3field, b: 4field } ] }", + "{ items: [ { a: 5field, b: 6field }, { a: 7field, b: 8field } ] }", + ), + ]; + + for (function, first, second) in cases { + for (cond_str, expected_str) in [("true", *first), ("false", *second)] { + let inputs = [ + Value::::from_str(cond_str).unwrap(), + Value::from_str(first).unwrap(), + Value::from_str(second).unwrap(), + ]; + let execution = vm + .execute( + &caller_private_key, + ("ternary_deep_nesting.aleo", *function), + inputs.iter(), + None, + 0, + None, + rng, + ) + .unwrap(); + let expected = Plaintext::::from_str(expected_str).unwrap(); + match &execution.transitions().next().unwrap().outputs()[0] { + Output::Public(_, Some(plaintext)) => { + assert_eq!(*plaintext, expected, "{function} with condition={cond_str}") + } + other => panic!("Expected public output, got: {other:?}"), + } + let block = sample_next_block(&vm, &caller_private_key, &[execution], rng).unwrap(); + assert_eq!( + block.transactions().num_accepted(), + 1, + "{function} with condition={cond_str} should be accepted" + ); + vm.add_next_block(&block).unwrap(); + } + } +} + +// Tests that nested arrays whose outer length matches but whose element types differ are +// rejected by the type checker. Complements `test_deploy_ternary_array_size_mismatch_rejected` +// which checks the outer length. +#[test] +fn test_deploy_ternary_nested_shape_mismatch_rejected() { + let rng = &mut TestRng::default(); + let caller_private_key = sample_genesis_private_key(rng); + + let v16_height = CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V16).unwrap(); + let vm = sample_vm_at_height(v16_height, rng); + + let program = Program::from_str( + r" + program ternary_nested_mismatch.aleo; + + function bad: + input r0 as boolean.public; + input r1 as [[field; 2u32]; 3u32].public; + input r2 as [[field; 3u32]; 3u32].public; + ternary r0 r1 r2 into r3; + output r3 as [[field; 2u32]; 3u32].public; + + constructor: + assert.eq true true; + ", + ) + .unwrap(); + + let result = vm.deploy(&caller_private_key, &program, None, 0, None, rng); + assert!(result.is_err(), "Deployment should fail for ternary on nested arrays with different inner lengths"); +} + +// A program that feeds the result of a non-literal `ternary` into a downstream instruction: +// extracts an element via `cast` semantics and asserts equality on a struct field. +const TERNARY_RESULT_USE_PROGRAM: &str = r" +program ternary_result_use.aleo; + +struct point: + x as field; + y as field; + +function use_struct_result: + input r0 as boolean.public; + input r1 as point.public; + input r2 as point.public; + ternary r0 r1 r2 into r3; + // The selected struct is cast back into its own type, then fed to assert.eq. + cast r3.x r3.y into r4 as point; + assert.eq r3 r4; + output r3 as point.public; + +function use_array_result: + input r0 as boolean.public; + input r1 as [field; 3u32].public; + input r2 as [field; 3u32].public; + ternary r0 r1 r2 into r3; + // Rebuild the array from the selected operand's elements and assert equality. + cast r3[0u32] r3[1u32] r3[2u32] into r4 as [field; 3u32]; + assert.eq r3 r4; + output r3 as [field; 3u32].public; + +constructor: + assert.eq true true; +"; + +// Tests that the result of a non-literal `ternary` can be consumed by downstream instructions +// (`cast` and `assert.eq`). Ensures the destination register is correctly typed and usable. +#[test] +fn test_execute_ternary_result_is_usable() { + let rng = &mut TestRng::default(); + let caller_private_key = sample_genesis_private_key(rng); + + let v16_height = CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V16).unwrap(); + let vm = sample_vm_at_height(v16_height, rng); + + let program = Program::from_str(TERNARY_RESULT_USE_PROGRAM).unwrap(); + let deployment = vm.deploy(&caller_private_key, &program, None, 0, None, rng).unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[deployment], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1, "Result-use deployment at V16 should be accepted"); + vm.add_next_block(&block).unwrap(); + + let cases: &[(&str, &str, &str)] = &[ + ("use_struct_result", "{ x: 1field, y: 2field }", "{ x: 3field, y: 4field }"), + ("use_array_result", "[ 1field, 2field, 3field ]", "[ 4field, 5field, 6field ]"), + ]; + + for (function, first, second) in cases { + for (cond_str, expected_str) in [("true", *first), ("false", *second)] { + let inputs = [ + Value::::from_str(cond_str).unwrap(), + Value::from_str(first).unwrap(), + Value::from_str(second).unwrap(), + ]; + let execution = vm + .execute(&caller_private_key, ("ternary_result_use.aleo", *function), inputs.iter(), None, 0, None, rng) + .unwrap(); + let expected = Plaintext::::from_str(expected_str).unwrap(); + match &execution.transitions().next().unwrap().outputs()[0] { + Output::Public(_, Some(plaintext)) => { + assert_eq!(*plaintext, expected, "{function} with condition={cond_str}") + } + other => panic!("Expected public output, got: {other:?}"), + } + let block = sample_next_block(&vm, &caller_private_key, &[execution], rng).unwrap(); + assert_eq!( + block.transactions().num_accepted(), + 1, + "{function} with condition={cond_str} should be accepted" + ); + vm.add_next_block(&block).unwrap(); + } + } +} + +// A program that exercises a non-literal `ternary` inside a finalize block, so that the +// size-based cost path (`TERNARY_BASE_COST + TERNARY_PER_BYTE_COST * size`) is hit. +const TERNARY_COST_PROGRAM: &str = r" +program ternary_cost_test.aleo; + +mapping selected: + key as u8.public; + value as [field; 4u32].public; + +function run: + input r0 as boolean.public; + input r1 as [field; 4u32].public; + input r2 as [field; 4u32].public; + async run r0 r1 r2 into r3; + output r3 as ternary_cost_test.aleo/run.future; + +finalize run: + input r0 as boolean.public; + input r1 as [field; 4u32].public; + input r2 as [field; 4u32].public; + ternary r0 r1 r2 into r3; + set r3 into selected[0u8]; + +constructor: + assert.eq true true; +"; + +// Tests that the cost of a non-literal `ternary` in a finalize block is estimated correctly by +// running the execution through `add_and_test_with_costs`, which asserts that the estimated +// and actual fees match. +#[test] +fn test_execute_ternary_finalize_cost_matches() { + let rng = &mut TestRng::default(); + let caller_private_key = sample_genesis_private_key(rng); + let caller_address = Address::try_from(&caller_private_key).unwrap(); + + let v16_height = CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V16).unwrap(); + let vm = sample_vm_at_height(v16_height, rng); + + let program = Program::from_str(TERNARY_COST_PROGRAM).unwrap(); + let deployment = vm.deploy(&caller_private_key, &program, None, 0, None, rng).unwrap(); + add_and_test_with_costs(&vm, &caller_private_key, &caller_address, None, &[deployment], rng); + + let inputs = [ + Value::::from_str("true").unwrap(), + Value::from_str("[ 1field, 2field, 3field, 4field ]").unwrap(), + Value::from_str("[ 5field, 6field, 7field, 8field ]").unwrap(), + ]; + let execution = + vm.execute(&caller_private_key, ("ternary_cost_test.aleo", "run"), inputs.iter(), None, 0, None, rng).unwrap(); + add_and_test_with_costs(&vm, &caller_private_key, &caller_address, Some(&[&inputs]), &[execution], rng); +} diff --git a/synthesizer/src/vm/verify.rs b/synthesizer/src/vm/verify.rs index 9de2f16109..b841cadf17 100644 --- a/synthesizer/src/vm/verify.rs +++ b/synthesizer/src/vm/verify.rs @@ -355,6 +355,11 @@ impl> VM { "Invalid deployment transaction '{id}' - program uses syntax that is not allowed before `ConsensusVersion::V15`" ); } + if consensus_version < ConsensusVersion::V16 { + let stack = Stack::new(&self.process, deployment.program())?; + check_no_non_literal_ternary(deployment.program(), &stack) + .map_err(|e| anyhow!("Invalid deployment transaction '{id}' - {e}"))?; + } // Checks required for current and future consensus versions (>= V9). //