diff --git a/.circleci/config.yml b/.circleci/config.yml index d4319862fb..97c235286a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -835,6 +835,16 @@ jobs: workspace_member: synthesizer/snark cache_key: v1.3.0-rust-1.83.0-snarkvm-synthesizer-snark-cache + synthesizer-upgrade: + docker: + - image: cimg/rust:1.83.0 # Attention - Change the MSRV in Cargo.toml and rust-toolchain as well + resource_class: << pipeline.parameters.xlarge >> + steps: + - run_serial: + flags: test_vm_upgrade --features=test + workspace_member: synthesizer + cache_key: v1.3.0-rust-1.83.0-snarkvm-synthesizer-upgrade-cache + utilities: docker: - image: cimg/rust:1.83.0 # Attention - Change the MSRV in Cargo.toml and rust-toolchain as well @@ -1018,6 +1028,7 @@ workflows: - synthesizer-program-integration-instruction-equal - synthesizer-program-integration-instruction-commit - synthesizer-snark + - synthesizer-upgrade - utilities - utilities-derives - wasm diff --git a/Cargo.lock b/Cargo.lock index f5b5329eb0..c029164b46 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3734,6 +3734,7 @@ dependencies = [ "snarkvm-console", "snarkvm-synthesizer-process", "snarkvm-synthesizer-snark", + "tiny-keccak", ] [[package]] diff --git a/circuit/program/src/data/plaintext/mod.rs b/circuit/program/src/data/plaintext/mod.rs index d7844df5a2..bf39b8b5e6 100644 --- a/circuit/program/src/data/plaintext/mod.rs +++ b/circuit/program/src/data/plaintext/mod.rs @@ -99,6 +99,32 @@ impl From<&Literal> for Plaintext { } } +// A macro that derives the `From` implementation for an array of literals. +// The array element type should be generic and so should the size. +macro_rules! impl_plaintext_from_array { + ($element:ident, $($size:literal),+) => { + $( + impl From<[$element; $size]> for Plaintext { + fn from(value: [$element; $size]) -> Self { + Self::Array( + value + .into_iter() + .map(|element| Plaintext::from(Literal::$element(element))) + .collect(), + OnceCell::new(), + ) + } + } + )+ + }; +} + +// Implement for `[U8, SIZE]` for sizes 1 through 32. +impl_plaintext_from_array!( + U8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, + 31, 32 +); + #[cfg(all(test, feature = "console"))] mod tests { use super::*; diff --git a/console/algorithms/benches/bhp.rs b/console/algorithms/benches/bhp.rs index 4dcc191771..528d7d9f02 100644 --- a/console/algorithms/benches/bhp.rs +++ b/console/algorithms/benches/bhp.rs @@ -66,10 +66,30 @@ fn bhp1024(c: &mut Criterion) { c.bench_function(&format!("BHP1024 Hash - input size {}", input.len()), |b| b.iter(|| hash.hash(&input))); } +fn bhp1024_large(c: &mut Criterion) { + const SIZE_IN_BYTES: [usize; 4] = + [1_000 /* 1 kB */, 10_000 /* 10 KB */, 100_000 /* 100 KB */, 1_000_000 /* 1 MiB */]; + let rng = &mut TestRng::default(); + + // Benchmark the BHP1024 hash function for different input sizes. + for size in SIZE_IN_BYTES.iter() { + let input = (0..size * 8).map(|_| bool::rand(rng)).collect::>(); + c.bench_function(&format!("BHP1024 Hash - input size {} bytes", size), |b| { + b.iter(|| BHP1024::::setup("BHP1024").unwrap().hash(&input)) + }); + } +} + criterion_group! { name = bhp; config = Criterion::default().sample_size(1000); targets = bhp256, bhp512, bhp768, bhp1024 } -criterion_main!(bhp); +criterion_group! { + name = bhp_large; + config = Criterion::default().sample_size(100); + targets = bhp1024_large +} + +criterion_main!(bhp, bhp_large); diff --git a/console/network/src/canary_v0.rs b/console/network/src/canary_v0.rs index ae7cae84e2..a6df61301b 100644 --- a/console/network/src/canary_v0.rs +++ b/console/network/src/canary_v0.rs @@ -136,7 +136,7 @@ impl Network for CanaryV0 { /// A list of (consensus_version, block_height) pairs indicating when each consensus version takes effect. /// Documentation for what is changed at each version can be found in `ConsensusVersion`. #[cfg(not(any(test, feature = "test", feature = "test_consensus_heights")))] - const CONSENSUS_VERSION_HEIGHTS: [(ConsensusVersion, u32); 7] = [ + const CONSENSUS_VERSION_HEIGHTS: [(ConsensusVersion, u32); 8] = [ (ConsensusVersion::V1, 0), (ConsensusVersion::V2, 2_900_000), (ConsensusVersion::V3, 4_560_000), @@ -144,11 +144,12 @@ impl Network for CanaryV0 { (ConsensusVersion::V5, 5_780_000), (ConsensusVersion::V6, 6_240_000), (ConsensusVersion::V7, 6_895_000), + (ConsensusVersion::V8, 999_999_999), ]; /// A list of (consensus_version, block_height) pairs indicating when each consensus version takes effect. /// Documentation for what is changed at each version can be found in `ConsensusVersion`. #[cfg(any(test, feature = "test", feature = "test_consensus_heights"))] - const CONSENSUS_VERSION_HEIGHTS: [(ConsensusVersion, u32); 7] = [ + const CONSENSUS_VERSION_HEIGHTS: [(ConsensusVersion, u32); 8] = [ (ConsensusVersion::V1, 0), (ConsensusVersion::V2, 10), (ConsensusVersion::V3, 11), @@ -156,9 +157,8 @@ impl Network for CanaryV0 { (ConsensusVersion::V5, 13), (ConsensusVersion::V6, 14), (ConsensusVersion::V7, 15), + (ConsensusVersion::V8, 16), ]; - /// The network edition. - const EDITION: u16 = 0; /// The genesis block coinbase target. #[cfg(not(feature = "test_targets"))] const GENESIS_COINBASE_TARGET: u64 = (1u64 << 29).saturating_sub(1); diff --git a/console/network/src/lib.rs b/console/network/src/lib.rs index a3b18115d7..bc8a08d765 100644 --- a/console/network/src/lib.rs +++ b/console/network/src/lib.rs @@ -87,6 +87,8 @@ pub enum ConsensusVersion { V6 = 6, /// V7: Update to program rules. V7 = 7, + /// V8: Support for program upgradability. + V8 = 8, } pub trait Network: @@ -108,8 +110,6 @@ pub trait Network: const ID: u16; /// The network name. const NAME: &'static str; - /// The network edition. - const EDITION: u16; /// The function name for the inclusion circuit. const INCLUSION_FUNCTION_NAME: &'static str; @@ -228,7 +228,7 @@ pub trait Network: /// A list of (consensus_version, block_height) pairs indicating when each consensus version takes effect. /// Documentation for what is changed at each version can be found in `N::CONSENSUS_VERSION` - const CONSENSUS_VERSION_HEIGHTS: [(ConsensusVersion, u32); 7]; + const CONSENSUS_VERSION_HEIGHTS: [(ConsensusVersion, u32); 8]; /// A list of (consensus_version, size) pairs indicating the maximum number of validators in a committee. // Note: This value must **not** decrease without considering the impact on serialization. // Decreasing this value will break backwards compatibility of serialization without explicit diff --git a/console/network/src/mainnet_v0.rs b/console/network/src/mainnet_v0.rs index 4e5229046c..be73bce301 100644 --- a/console/network/src/mainnet_v0.rs +++ b/console/network/src/mainnet_v0.rs @@ -137,7 +137,7 @@ impl Network for MainnetV0 { /// A list of (consensus_version, block_height) pairs indicating when each consensus version takes effect. /// Documentation for what is changed at each version can be found in `ConsensusVersion`. #[cfg(not(any(test, feature = "test")))] - const CONSENSUS_VERSION_HEIGHTS: [(ConsensusVersion, u32); 7] = [ + const CONSENSUS_VERSION_HEIGHTS: [(ConsensusVersion, u32); 8] = [ (ConsensusVersion::V1, 0), (ConsensusVersion::V2, 2_800_000), (ConsensusVersion::V3, 4_900_000), @@ -145,11 +145,12 @@ impl Network for MainnetV0 { (ConsensusVersion::V5, 7_060_000), (ConsensusVersion::V6, 7_560_000), (ConsensusVersion::V7, 7_570_000), + (ConsensusVersion::V8, 999_999_999), ]; /// A list of (consensus_version, block_height) pairs indicating when each consensus version takes effect. /// Documentation for what is changed at each version can be found in `ConsensusVersion`. #[cfg(any(test, feature = "test"))] - const CONSENSUS_VERSION_HEIGHTS: [(ConsensusVersion, u32); 7] = [ + const CONSENSUS_VERSION_HEIGHTS: [(ConsensusVersion, u32); 8] = [ (ConsensusVersion::V1, 0), (ConsensusVersion::V2, 10), (ConsensusVersion::V3, 11), @@ -157,9 +158,8 @@ impl Network for MainnetV0 { (ConsensusVersion::V5, 13), (ConsensusVersion::V6, 14), (ConsensusVersion::V7, 15), + (ConsensusVersion::V8, 16), ]; - /// The network edition. - const EDITION: u16 = 0; /// The genesis block coinbase target. #[cfg(not(feature = "test"))] const GENESIS_COINBASE_TARGET: u64 = (1u64 << 29).saturating_sub(1); diff --git a/console/network/src/testnet_v0.rs b/console/network/src/testnet_v0.rs index 6bec3d4b42..0044b992d6 100644 --- a/console/network/src/testnet_v0.rs +++ b/console/network/src/testnet_v0.rs @@ -136,7 +136,7 @@ impl Network for TestnetV0 { /// A list of (consensus_version, block_height) pairs indicating when each consensus version takes effect. /// Documentation for what is changed at each version can be found in `ConsensusVersion`. #[cfg(not(any(test, feature = "test", feature = "test_consensus_heights")))] - const CONSENSUS_VERSION_HEIGHTS: [(ConsensusVersion, u32); 7] = [ + const CONSENSUS_VERSION_HEIGHTS: [(ConsensusVersion, u32); 8] = [ (ConsensusVersion::V1, 0), (ConsensusVersion::V2, 2_950_000), (ConsensusVersion::V3, 4_800_000), @@ -144,11 +144,12 @@ impl Network for TestnetV0 { (ConsensusVersion::V5, 6_765_000), (ConsensusVersion::V6, 7_600_000), (ConsensusVersion::V7, 8_365_000), + (ConsensusVersion::V8, 999_999_999), ]; /// A list of (consensus_version, block_height) pairs indicating when each consensus version takes effect. /// Documentation for what is changed at each version can be found in `ConsensusVersion`. #[cfg(any(test, feature = "test", feature = "test_consensus_heights"))] - const CONSENSUS_VERSION_HEIGHTS: [(ConsensusVersion, u32); 7] = [ + const CONSENSUS_VERSION_HEIGHTS: [(ConsensusVersion, u32); 8] = [ (ConsensusVersion::V1, 0), (ConsensusVersion::V2, 10), (ConsensusVersion::V3, 11), @@ -156,9 +157,8 @@ impl Network for TestnetV0 { (ConsensusVersion::V5, 13), (ConsensusVersion::V6, 14), (ConsensusVersion::V7, 15), + (ConsensusVersion::V8, 16), ]; - /// The network edition. - const EDITION: u16 = 0; /// The genesis block coinbase target. #[cfg(not(feature = "test_targets"))] const GENESIS_COINBASE_TARGET: u64 = (1u64 << 29).saturating_sub(1); diff --git a/console/program/src/data/plaintext/mod.rs b/console/program/src/data/plaintext/mod.rs index e65de1a951..4bfc016964 100644 --- a/console/program/src/data/plaintext/mod.rs +++ b/console/program/src/data/plaintext/mod.rs @@ -57,6 +57,32 @@ impl From<&Literal> for Plaintext { } } +// A macro that derives the `From` implementation for an array of literals. +// The array element type should be generic and so should the size. +macro_rules! impl_plaintext_from_array { + ($element:ident, $($size:literal),+) => { + $( + impl From<[$element; $size]> for Plaintext { + fn from(value: [$element; $size]) -> Self { + Self::Array( + value + .into_iter() + .map(|element| Plaintext::from(Literal::$element(element))) + .collect(), + OnceCell::new(), + ) + } + } + )+ + }; +} + +// Implement for `[U8, SIZE]` for sizes 1 through 32. +impl_plaintext_from_array!( + U8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, + 31, 32 +); + #[cfg(test)] mod tests { use super::*; diff --git a/console/program/src/state_path/configuration/mod.rs b/console/program/src/state_path/configuration/mod.rs index 3f36d9ef29..8d79112d03 100644 --- a/console/program/src/state_path/configuration/mod.rs +++ b/console/program/src/state_path/configuration/mod.rs @@ -57,15 +57,16 @@ pub type TransactionsTree = BHPMerkleTree; /// The Merkle path for a transaction in a block. pub type TransactionsPath = MerklePath; -/// The Merkle tree for the execution. -pub type ExecutionTree = BHPMerkleTree; -/// The Merkle tree for the deployment. -pub type DeploymentTree = BHPMerkleTree; /// The Merkle tree for the transaction. pub type TransactionTree = BHPMerkleTree; /// The Merkle path for a function or transition in the transaction. pub type TransactionPath = MerklePath; +/// The Merkle tree for the execution. +pub type ExecutionTree = BHPMerkleTree; +/// The Merkle tree for the deployment. +pub type DeploymentTree = BHPMerkleTree; + /// The Merkle tree for the transition. pub type TransitionTree = BHPMerkleTree; /// The Merkle path for an input or output ID in the transition. diff --git a/ledger/block/src/transaction/bytes.rs b/ledger/block/src/transaction/bytes.rs index 8dd29cf098..9e980082e9 100644 --- a/ledger/block/src/transaction/bytes.rs +++ b/ledger/block/src/transaction/bytes.rs @@ -96,7 +96,8 @@ impl ToBytes for Transaction { 1u8.write_le(&mut writer)?; // Write the transaction. - // We don't write the deployment or execution id, which are recomputed when creating the transaction. + // Note: We purposefully do not write out the deployment or execution ID, + // and instead recompute it when reconstructing the transaction, to ensure there was no malleability. match self { Self::Deploy(id, _, owner, deployment, fee) => { // Write the variant. @@ -147,8 +148,10 @@ mod tests { let rng = &mut TestRng::default(); for expected in [ - crate::transaction::test_helpers::sample_deployment_transaction(true, rng), - crate::transaction::test_helpers::sample_deployment_transaction(false, rng), + crate::transaction::test_helpers::sample_deployment_transaction(1, true, rng), + crate::transaction::test_helpers::sample_deployment_transaction(1, false, rng), + crate::transaction::test_helpers::sample_deployment_transaction(2, true, rng), + crate::transaction::test_helpers::sample_deployment_transaction(2, false, rng), crate::transaction::test_helpers::sample_execution_transaction_with_fee(true, rng), crate::transaction::test_helpers::sample_execution_transaction_with_fee(false, rng), ] diff --git a/ledger/block/src/transaction/deployment/bytes.rs b/ledger/block/src/transaction/deployment/bytes.rs index c2e1dccbb9..9240641b72 100644 --- a/ledger/block/src/transaction/deployment/bytes.rs +++ b/ledger/block/src/transaction/deployment/bytes.rs @@ -18,12 +18,12 @@ use super::*; impl FromBytes for Deployment { /// Reads the deployment from a buffer. fn read_le(mut reader: R) -> IoResult { - // Read the version. - let version = u8::read_le(&mut reader)?; - // Ensure the version is valid. - if version != 1 { - return Err(error("Invalid deployment version")); - } + // Read the version and ensure the version is valid. + let version = match u8::read_le(&mut reader)? { + 1 => DeploymentVersion::V1, + 2 => DeploymentVersion::V2, + version => return Err(error(format!("Invalid deployment version: {}", version))), + }; // Read the edition. let edition = u16::read_le(&mut reader)?; @@ -45,8 +45,27 @@ impl FromBytes for Deployment { verifying_keys.push((identifier, (verifying_key, certificate))); } + // If the deployment version is 2, read the program checksum and verify it. + let program_checksum = match version { + DeploymentVersion::V1 => None, + DeploymentVersion::V2 => { + // Read the program checksum. + let bytes: [u8; 32] = FromBytes::read_le(&mut reader)?; + let checksum = bytes.map(U8::new); + // Verify the checksum. + if checksum != program.to_checksum() { + return Err(error(format!( + "Invalid checksum in the deployment: expected [{}], got [{}]", + program.to_checksum().iter().join(", "), + checksum.iter().join(", ") + ))); + } + Some(checksum) + } + }; + // Return the deployment. - Self::new(edition, program, verifying_keys).map_err(|err| error(format!("{err}"))) + Self::new(edition, program, verifying_keys, program_checksum).map_err(|err| error(format!("{err}"))) } } @@ -54,7 +73,7 @@ impl ToBytes for Deployment { /// Writes the deployment to a buffer. fn write_le(&self, mut writer: W) -> IoResult<()> { // Write the version. - 1u8.write_le(&mut writer)?; + (self.version() as u8).write_le(&mut writer)?; // Write the edition. self.edition.write_le(&mut writer)?; // Write the program. @@ -70,6 +89,12 @@ impl ToBytes for Deployment { // Write the certificate. certificate.write_le(&mut writer)?; } + // Write the checksum, if it exists. + if let Some(program_checksum) = &self.program_checksum { + for byte in program_checksum { + byte.write_le(&mut writer)?; + } + } Ok(()) } } @@ -82,12 +107,13 @@ mod tests { fn test_bytes() -> Result<()> { let rng = &mut TestRng::default(); - // Construct a new deployment. - let expected = test_helpers::sample_deployment(rng); + // Construct the deployments. + for expected in [test_helpers::sample_deployment_v1(rng), test_helpers::sample_deployment_v2(rng)] { + // Check the byte representation. + let expected_bytes = expected.to_bytes_le()?; + assert_eq!(expected, Deployment::read_le(&expected_bytes[..])?); + } - // Check the byte representation. - let expected_bytes = expected.to_bytes_le()?; - assert_eq!(expected, Deployment::read_le(&expected_bytes[..])?); Ok(()) } } diff --git a/ledger/block/src/transaction/deployment/mod.rs b/ledger/block/src/transaction/deployment/mod.rs index ecc7a567c1..dbb3ea03b0 100644 --- a/ledger/block/src/transaction/deployment/mod.rs +++ b/ledger/block/src/transaction/deployment/mod.rs @@ -23,12 +23,12 @@ use crate::Transaction; use console::{ network::prelude::*, program::{Identifier, ProgramID}, - types::Field, + types::{Field, U8}, }; use synthesizer_program::Program; use synthesizer_snark::{Certificate, VerifyingKey}; -#[derive(Clone, PartialEq, Eq)] +#[derive(Clone)] pub struct Deployment { /// The edition. edition: u16, @@ -36,17 +36,31 @@ pub struct Deployment { program: Program, /// The mapping of function names to their verifying key and certificate. verifying_keys: Vec<(Identifier, (VerifyingKey, Certificate))>, + /// An optional checksum for the program. + /// This field creates a backwards-compatible implicit versioning mechanism for deployments. + /// Before the migration height where this feature is enabled, the checksum will **not** be allowed. + /// After the migration height where this feature is enabled, the checksum will be required. + program_checksum: Option<[U8; 32]>, } +impl PartialEq for Deployment { + fn eq(&self, other: &Self) -> bool { + self.edition == other.edition && self.verifying_keys == other.verifying_keys && self.program == other.program + } +} + +impl Eq for Deployment {} + impl Deployment { /// Initializes a new deployment. pub fn new( edition: u16, program: Program, verifying_keys: Vec<(Identifier, (VerifyingKey, Certificate))>, + program_checksum: Option<[U8; 32]>, ) -> Result { // Construct the deployment. - let deployment = Self { edition, program, verifying_keys }; + let deployment = Self { edition, program, verifying_keys, program_checksum }; // Ensure the deployment is ordered. deployment.check_is_ordered()?; // Return the deployment. @@ -57,13 +71,14 @@ impl Deployment { pub fn check_is_ordered(&self) -> Result<()> { let program_id = self.program.id(); - // Ensure the edition matches. - ensure!( - self.edition == N::EDITION, - "Deployed the wrong edition (expected '{}', found '{}').", - N::EDITION, - self.edition - ); + // Ensure that if the program checksum is absent, then the edition is zero. + if self.program_checksum.is_none() { + ensure!( + self.edition == 0, + "If the program checksum is absent, but the edition must be zero {}", + self.edition + ); + } // Ensure the program contains functions. ensure!( !self.program.functions().is_empty(), @@ -113,15 +128,10 @@ impl Deployment { } /// Returns the number of program functions in the deployment. - pub fn len(&self) -> usize { + pub fn num_functions(&self) -> usize { self.program.functions().len() } - /// Returns `true` if the deployment is empty. - pub fn is_empty(&self) -> bool { - self.program.functions().is_empty() - } - /// Returns the edition. pub const fn edition(&self) -> u16 { self.edition @@ -132,6 +142,11 @@ impl Deployment { &self.program } + /// Returns the program checksum, if it was stored. + pub const fn program_checksum(&self) -> Option<&[U8; 32]> { + self.program_checksum.as_ref() + } + /// Returns the program. pub const fn program_id(&self) -> &ProgramID { self.program.id() @@ -182,6 +197,29 @@ impl Deployment { } } +impl Deployment { + /// Sets the program checksum. + /// Note: This method is intended to be used by the synthesizer **only**, and should not be called by the user. + #[doc(hidden)] + pub fn set_program_checksum_raw(&mut self, program_checksum: Option<[U8; 32]>) { + self.program_checksum = program_checksum; + } + + /// An internal function to return the implicit deployment version. + fn version(&self) -> DeploymentVersion { + match self.program_checksum { + None => DeploymentVersion::V1, + Some(_) => DeploymentVersion::V2, + } + } +} + +// An internal enum to represent the deployment version. +enum DeploymentVersion { + V1 = 1, + V2 = 2, +} + #[cfg(test)] pub mod test_helpers { use super::*; @@ -193,14 +231,48 @@ pub mod test_helpers { type CurrentNetwork = MainnetV0; type CurrentAleo = circuit::network::AleoV0; - pub(crate) fn sample_deployment(rng: &mut TestRng) -> Deployment { + pub(crate) fn sample_deployment_v1(rng: &mut TestRng) -> Deployment { + static INSTANCE: OnceCell> = OnceCell::new(); + INSTANCE + .get_or_init(|| { + // Initialize a new program. + let (string, program) = Program::::parse( + r" +program testing_three.aleo; + +mapping store: + key as u32.public; + value as u32.public; + +function compute: + input r0 as u32.private; + add r0 r0 into r1; + output r1 as u32.public;", + ) + .unwrap(); + assert!(string.is_empty(), "Parser did not consume all of the string: '{string}'"); + + // Construct the process. + let process = Process::load().unwrap(); + // Compute the deployment. + let mut deployment = process.deploy::(&program, rng).unwrap(); + // Unset the checksum. + deployment.set_program_checksum_raw(None); + // Return the deployment. + // Note: This is a testing-only hack to adhere to Rust's dependency cycle rules. + Deployment::from_str(&deployment.to_string()).unwrap() + }) + .clone() + } + + pub(crate) fn sample_deployment_v2(rng: &mut TestRng) -> Deployment { static INSTANCE: OnceCell> = OnceCell::new(); INSTANCE .get_or_init(|| { // Initialize a new program. let (string, program) = Program::::parse( r" -program testing.aleo; +program testing_four.aleo; mapping store: key as u32.public; @@ -218,6 +290,8 @@ function compute: let process = Process::load().unwrap(); // Compute the deployment. let deployment = process.deploy::(&program, rng).unwrap(); + // Assert that the deployment has a checksum. + assert!(deployment.program_checksum().is_some(), "Deployment does not have a checksum"); // Return the deployment. // Note: This is a testing-only hack to adhere to Rust's dependency cycle rules. Deployment::from_str(&deployment.to_string()).unwrap() diff --git a/ledger/block/src/transaction/deployment/serialize.rs b/ledger/block/src/transaction/deployment/serialize.rs index 998f65b295..574516de8c 100644 --- a/ledger/block/src/transaction/deployment/serialize.rs +++ b/ledger/block/src/transaction/deployment/serialize.rs @@ -20,10 +20,17 @@ impl Serialize for Deployment { fn serialize(&self, serializer: S) -> Result { match serializer.is_human_readable() { true => { - let mut deployment = serializer.serialize_struct("Deployment", 3)?; + let len = match self.version() { + DeploymentVersion::V1 => 3, + DeploymentVersion::V2 => 4, + }; + let mut deployment = serializer.serialize_struct("Deployment", len)?; deployment.serialize_field("edition", &self.edition)?; deployment.serialize_field("program", &self.program)?; deployment.serialize_field("verifying_keys", &self.verifying_keys)?; + if let Some(program_checksum) = &self.program_checksum { + deployment.serialize_field("program_checksum", program_checksum)?; + } deployment.end() } false => ToBytesSerializer::serialize_with_size_encoding(self, serializer), @@ -47,6 +54,11 @@ impl<'de, N: Network> Deserialize<'de> for Deployment { DeserializeExt::take_from_value::(&mut deployment, "program")?, // Retrieve the verifying keys. DeserializeExt::take_from_value::(&mut deployment, "verifying_keys")?, + // Retrieve the program checksum, if it exists. + serde_json::from_value( + deployment.get_mut("program_checksum").unwrap_or(&mut serde_json::Value::Null).take(), + ) + .map_err(de::Error::custom)?, ) .map_err(de::Error::custom)?; @@ -65,17 +77,17 @@ mod tests { fn test_serde_json() -> Result<()> { let rng = &mut TestRng::default(); - // Sample the deployment. - let expected = test_helpers::sample_deployment(rng); + // Sample the deployments. + for expected in [test_helpers::sample_deployment_v1(rng), test_helpers::sample_deployment_v2(rng)] { + // Serialize + let expected_string = &expected.to_string(); + let candidate_string = serde_json::to_string(&expected)?; + assert_eq!(expected, serde_json::from_str(&candidate_string)?); - // Serialize - let expected_string = &expected.to_string(); - let candidate_string = serde_json::to_string(&expected)?; - assert_eq!(expected, serde_json::from_str(&candidate_string)?); - - // Deserialize - assert_eq!(expected, Deployment::from_str(expected_string)?); - assert_eq!(expected, serde_json::from_str(&candidate_string)?); + // Deserialize + assert_eq!(expected, Deployment::from_str(expected_string)?); + assert_eq!(expected, serde_json::from_str(&candidate_string)?); + } Ok(()) } @@ -84,17 +96,17 @@ mod tests { fn test_bincode() -> Result<()> { let rng = &mut TestRng::default(); - // Sample the deployment. - let expected = test_helpers::sample_deployment(rng); + // Sample the deployments + for expected in [test_helpers::sample_deployment_v1(rng), test_helpers::sample_deployment_v2(rng)] { + // Serialize + let expected_bytes = expected.to_bytes_le()?; + let expected_bytes_with_size_encoding = bincode::serialize(&expected)?; + assert_eq!(&expected_bytes[..], &expected_bytes_with_size_encoding[8..]); - // Serialize - let expected_bytes = expected.to_bytes_le()?; - let expected_bytes_with_size_encoding = bincode::serialize(&expected)?; - assert_eq!(&expected_bytes[..], &expected_bytes_with_size_encoding[8..]); - - // Deserialize - assert_eq!(expected, Deployment::read_le(&expected_bytes[..])?); - assert_eq!(expected, bincode::deserialize(&expected_bytes_with_size_encoding[..])?); + // Deserialize + assert_eq!(expected, Deployment::read_le(&expected_bytes[..])?); + assert_eq!(expected, bincode::deserialize(&expected_bytes_with_size_encoding[..])?); + } Ok(()) } diff --git a/ledger/block/src/transaction/merkle.rs b/ledger/block/src/transaction/merkle.rs index 1ad78c1292..3da6907a72 100644 --- a/ledger/block/src/transaction/merkle.rs +++ b/ledger/block/src/transaction/merkle.rs @@ -93,17 +93,11 @@ impl Transaction { match self { // Compute the deployment tree. Transaction::Deploy(_, _, _, deployment, fee) => { - let deployment_tree = Self::deployment_tree(deployment)?; - Self::transaction_tree(deployment_tree, deployment.len(), fee) + Self::transaction_tree(Self::deployment_tree(deployment)?, Some(fee)) } // Compute the execution tree. Transaction::Execute(_, _, execution, fee) => { - let execution_tree = Self::execution_tree(execution)?; - if let Some(fee) = fee { - Ok(Transaction::transaction_tree(execution_tree, execution.len(), fee)?) - } else { - Ok(execution_tree) - } + Self::transaction_tree(Self::execution_tree(execution)?, fee.as_ref()) } // Compute the fee tree. Transaction::Fee(_, fee) => Self::fee_tree(fee), @@ -112,28 +106,46 @@ impl Transaction { } impl Transaction { + /// Returns the Merkle tree for the given transaction tree, fee index, and fee. + pub fn transaction_tree( + mut deployment_or_execution_tree: TransactionTree, + fee: Option<&Fee>, + ) -> Result> { + // Retrieve the fee index, defined as the last index in the transaction tree. + let fee_index = deployment_or_execution_tree.number_of_leaves(); + // Ensure the fee index is within the Merkle tree size. + ensure!( + fee_index <= N::MAX_FUNCTIONS, + "The fee index ('{fee_index}') in the transaction tree must be less than {}", + N::MAX_FUNCTIONS + ); + // Ensure the fee index is within the Merkle tree size. + ensure!( + fee_index < Self::MAX_TRANSITIONS, + "The fee index ('{fee_index}') in the transaction tree must be less than {}", + Self::MAX_TRANSITIONS + ); + + // If a fee is provided, append the fee leaf to the transaction tree. + if let Some(fee) = fee { + // Construct the transaction leaf. + let leaf = TransactionLeaf::new_fee(u16::try_from(fee_index)?, **fee.transition_id()).to_bits_le(); + // Append the fee leaf to the transaction tree. + deployment_or_execution_tree.append(&[leaf])?; + } + // Return the transaction tree. + Ok(deployment_or_execution_tree) + } + /// Returns the Merkle tree for the given deployment. pub fn deployment_tree(deployment: &Deployment) -> Result> { - // Ensure the number of leaves is within the Merkle tree size. - Self::check_deployment_size(deployment)?; - // Retrieve the program. - let program = deployment.program(); - // Prepare the leaves. - let leaves = program - .functions() - .values() - .enumerate() - .map(|(index, function)| { - // Construct the transaction leaf. - Ok(TransactionLeaf::new_deployment( - u16::try_from(index)?, - N::hash_bhp1024(&to_bits_le![program.id(), function.to_bytes_le()?])?, - ) - .to_bits_le()) - }) - .collect::>>()?; - // Compute the deployment tree. - N::merkle_tree_bhp::(&leaves) + // Use the V1 or V2 deployment tree based on whether or not the program checksum exists. + // Note: `ConsensusVersion::V8` requires the program checksum to be present, while prior versions require it to be absent. + // Note: After `ConsensusVersion::V8`, the program checksum is used in the header of the hash instead of the program ID. + match deployment.program_checksum().is_some() { + false => Self::deployment_tree_v1(deployment), + true => Self::deployment_tree_v2(deployment), + } } /// Returns the Merkle tree for the given execution. @@ -150,29 +162,12 @@ impl Transaction { // Ensure the number of leaves is within the Merkle tree size. Self::check_execution_size(num_transitions)?; // Prepare the leaves. - let leaves = transitions - .enumerate() - .map(|(index, transition)| { - // Construct the transaction leaf. - Ok::<_, Error>(TransactionLeaf::new_execution(u16::try_from(index)?, **transition.id()).to_bits_le()) - }) - .collect::, _>>()?; + let leaves = transitions.enumerate().map(|(index, transition)| { + // Construct the transaction leaf. + Ok::<_, Error>(TransactionLeaf::new_execution(u16::try_from(index)?, **transition.id()).to_bits_le()) + }); // Compute the execution tree. - N::merkle_tree_bhp::(&leaves) - } - - /// Returns the Merkle tree for the given 1. transaction or deployment tree and 2. fee. - pub fn transaction_tree( - mut deployment_or_execution_tree: TransactionTree, - fee_index: usize, - fee: &Fee, - ) -> Result> { - // Construct the transaction leaf. - let leaf = TransactionLeaf::new_fee(u16::try_from(fee_index)?, **fee.transition_id()).to_bits_le(); - // Compute the updated transaction tree. - deployment_or_execution_tree.append(&[leaf])?; - - Ok(deployment_or_execution_tree) + N::merkle_tree_bhp::(&leaves.collect::, _>>()?) } /// Returns the Merkle tree for the given fee. @@ -191,20 +186,28 @@ impl Transaction { let functions = program.functions(); // Retrieve the verifying keys. let verifying_keys = deployment.verifying_keys(); + // Retrieve the number of functions. + let num_functions = functions.len(); // Ensure the number of functions and verifying keys match. ensure!( - functions.len() == verifying_keys.len(), - "Number of functions ('{}') and verifying keys ('{}') do not match", - functions.len(), + num_functions == verifying_keys.len(), + "Number of functions ('{num_functions}') and verifying keys ('{}') do not match", verifying_keys.len() ); + // Ensure there are functions. + ensure!(num_functions > 0, "Deployment must contain at least one function"); // Ensure the number of functions is within the allowed range. ensure!( - functions.len() < Self::MAX_TRANSITIONS, // Note: Observe we hold back 1 for the fee. - "Deployment must contain less than {} functions, found {}", + num_functions <= N::MAX_FUNCTIONS, + "Deployment must contain at most {} functions, found {num_functions}", + N::MAX_FUNCTIONS, + ); + // Ensure the number of functions is within the allowed range. + ensure!( + num_functions < Self::MAX_TRANSITIONS, // Note: Observe we hold back 1 for the fee. + "Deployment must contain less than {} functions, found {num_functions}", Self::MAX_TRANSITIONS, - functions.len() ); Ok(()) } @@ -222,3 +225,67 @@ impl Transaction { Ok(()) } } + +impl Transaction { + /// Returns the V1 deployment tree. + pub fn deployment_tree_v1(deployment: &Deployment) -> Result> { + // Ensure the number of leaves is within the Merkle tree size. + Self::check_deployment_size(deployment)?; + // Prepare the header for the hash. + let header = deployment.program().id().to_bits_le(); + // Prepare the leaves. + let leaves = deployment.program().functions().values().enumerate().map(|(index, function)| { + // Construct the transaction leaf. + Ok(TransactionLeaf::new_deployment( + u16::try_from(index)?, + N::hash_bhp1024(&to_bits_le![header, function.to_bytes_le()?])?, + ) + .to_bits_le()) + }); + // Compute the deployment tree. + N::merkle_tree_bhp::(&leaves.collect::>>()?) + } + + /// Returns the V2 deployment tree. + pub fn deployment_tree_v2(deployment: &Deployment) -> Result> { + // Ensure the number of leaves is within the Merkle tree size. + Self::check_deployment_size(deployment)?; + // Prepare the header for the hash. + let header = match deployment.program_checksum() { + None => deployment.program().to_checksum().to_bits_le(), + Some(program_checksum) => program_checksum.to_bits_le(), + }; + // Prepare the leaves. + let leaves = deployment.program().functions().values().enumerate().map(|(index, function)| { + // Construct the transaction leaf. + Ok(TransactionLeaf::new_deployment( + u16::try_from(index)?, + N::hash_bhp1024(&to_bits_le![header, function.to_bytes_le()?])?, + ) + .to_bits_le()) + }); + // Compute the deployment tree. + N::merkle_tree_bhp::(&leaves.collect::>>()?) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + type CurrentNetwork = console::network::MainnetV0; + + #[test] + fn test_transaction_depth_is_correct() { + // We ensure 2^TRANSACTION_DEPTH == MAX_FUNCTIONS + 1. + // The "1 extra" is for the fee transition. + assert_eq!( + 2u32.checked_pow(TRANSACTION_DEPTH as u32).unwrap() as usize, + Transaction::::MAX_TRANSITIONS + ); + assert_eq!( + CurrentNetwork::MAX_FUNCTIONS.checked_add(1).unwrap(), + Transaction::::MAX_TRANSITIONS + ); + } +} diff --git a/ledger/block/src/transaction/mod.rs b/ledger/block/src/transaction/mod.rs index 0e05683854..8a9704945a 100644 --- a/ledger/block/src/transaction/mod.rs +++ b/ledger/block/src/transaction/mod.rs @@ -67,7 +67,7 @@ impl Transaction { // Compute the deployment ID. let deployment_id = *deployment_tree.root(); // Compute the transaction ID - let transaction_id = *Self::transaction_tree(deployment_tree, deployment.len(), &fee)?.root(); + let transaction_id = *Self::transaction_tree(deployment_tree, Some(&fee))?.root(); // Ensure the owner signed the correct transaction ID. ensure!(owner.verify(deployment_id), "Attempted to create a deployment transaction with an invalid owner"); // Construct the deployment transaction. @@ -82,14 +82,8 @@ impl Transaction { let execution_tree = Self::execution_tree(&execution)?; // Compute the execution ID. let execution_id = *execution_tree.root(); - // Compute the transaction ID - let transaction_id = match &fee { - Some(fee) => { - // Compute the root of the transaction tree. - *Self::transaction_tree(execution_tree, execution.len(), fee)?.root() - } - None => execution_id, - }; + // Compute the transaction ID. + let transaction_id = *Self::transaction_tree(execution_tree, fee.as_ref())?.root(); // Construct the execution transaction. Ok(Self::Execute(transaction_id.into(), execution_id, Box::new(execution), fee)) } @@ -438,11 +432,19 @@ pub mod test_helpers { type CurrentNetwork = MainnetV0; /// Samples a random deployment transaction with a private or public fee. - pub fn sample_deployment_transaction(is_fee_private: bool, rng: &mut TestRng) -> Transaction { + pub fn sample_deployment_transaction( + version: u8, + is_fee_private: bool, + rng: &mut TestRng, + ) -> Transaction { // Sample a private key. let private_key = PrivateKey::new(rng).unwrap(); // Sample a deployment. - let deployment = crate::transaction::deployment::test_helpers::sample_deployment(rng); + let deployment = match version { + 1 => crate::transaction::deployment::test_helpers::sample_deployment_v1(rng), + 2 => crate::transaction::deployment::test_helpers::sample_deployment_v2(rng), + _ => panic!("Invalid deployment version."), + }; // Compute the deployment ID. let deployment_id = deployment.to_deployment_id().unwrap(); @@ -506,8 +508,10 @@ mod tests { // Transaction IDs are created using `transaction_tree`. for expected in [ - crate::transaction::test_helpers::sample_deployment_transaction(true, rng), - crate::transaction::test_helpers::sample_deployment_transaction(false, rng), + crate::transaction::test_helpers::sample_deployment_transaction(1, true, rng), + crate::transaction::test_helpers::sample_deployment_transaction(1, false, rng), + crate::transaction::test_helpers::sample_deployment_transaction(2, true, rng), + crate::transaction::test_helpers::sample_deployment_transaction(2, false, rng), crate::transaction::test_helpers::sample_execution_transaction_with_fee(true, rng), crate::transaction::test_helpers::sample_execution_transaction_with_fee(false, rng), ] diff --git a/ledger/block/src/transaction/serialize.rs b/ledger/block/src/transaction/serialize.rs index 9d513f2891..b320ed8ac4 100644 --- a/ledger/block/src/transaction/serialize.rs +++ b/ledger/block/src/transaction/serialize.rs @@ -18,8 +18,9 @@ use super::*; impl Serialize for Transaction { /// Serializes the transaction to a JSON-string or buffer. fn serialize(&self, serializer: S) -> Result { + // Note: We purposefully do not write out the deployment or execution ID, + // and instead recompute it when reconstructing the transaction, to ensure there was no malleability. match serializer.is_human_readable() { - // We don't write the deployment or execution id, which are recomputed when creating the Transaction. true => match self { Self::Deploy(id, _, owner, deployment, fee) => { let mut transaction = serializer.serialize_struct("Transaction", 5)?; @@ -119,8 +120,10 @@ mod tests { let rng = &mut TestRng::default(); for expected in [ - crate::transaction::test_helpers::sample_deployment_transaction(true, rng), - crate::transaction::test_helpers::sample_deployment_transaction(false, rng), + crate::transaction::test_helpers::sample_deployment_transaction(1, true, rng), + crate::transaction::test_helpers::sample_deployment_transaction(1, false, rng), + crate::transaction::test_helpers::sample_deployment_transaction(2, true, rng), + crate::transaction::test_helpers::sample_deployment_transaction(2, false, rng), crate::transaction::test_helpers::sample_execution_transaction_with_fee(true, rng), crate::transaction::test_helpers::sample_execution_transaction_with_fee(false, rng), ] @@ -142,8 +145,10 @@ mod tests { let rng = &mut TestRng::default(); for expected in [ - crate::transaction::test_helpers::sample_deployment_transaction(true, rng), - crate::transaction::test_helpers::sample_deployment_transaction(false, rng), + crate::transaction::test_helpers::sample_deployment_transaction(1, true, rng), + crate::transaction::test_helpers::sample_deployment_transaction(1, false, rng), + crate::transaction::test_helpers::sample_deployment_transaction(2, true, rng), + crate::transaction::test_helpers::sample_deployment_transaction(2, false, rng), crate::transaction::test_helpers::sample_execution_transaction_with_fee(true, rng), crate::transaction::test_helpers::sample_execution_transaction_with_fee(false, rng), ] diff --git a/ledger/block/src/transactions/confirmed/mod.rs b/ledger/block/src/transactions/confirmed/mod.rs index e60a8c5935..7b0d75cd6a 100644 --- a/ledger/block/src/transactions/confirmed/mod.rs +++ b/ledger/block/src/transactions/confirmed/mod.rs @@ -77,14 +77,14 @@ impl ConfirmedTransaction { finalize_operations.len() ); } - // Ensure the number of program mappings matches the number of 'InitializeMapping' finalize operations. - if num_initialize_mappings != program.mappings().len() { - bail!( - "Transaction '{}' (deploy) must contain '{}' 'InitializeMapping' operations (found '{num_initialize_mappings}')", - transaction.id(), - program.mappings().len(), - ) - } + // Ensure the number of program mappings upper bounds the number of 'InitializeMapping' finalize operations. + // The upper bound is due to the fact that some mappings may have been initialized in earlier deployments or upgrades. + ensure!( + num_initialize_mappings <= program.mappings().len(), + "Transaction '{}' (deploy) must contain at most '{}' 'InitializeMapping' operations (found '{num_initialize_mappings}')", + transaction.id(), + program.mappings().len(), + ); // Ensure the number of fee finalize operations lower bounds the number of '*KeyValue' finalize operations. // The lower bound is due to the fact that constructors can issue '*KeyValue' operations as part of the deployment. ensure!( @@ -360,12 +360,13 @@ pub mod test_helpers { /// Samples an accepted deploy transaction at the given index. pub(crate) fn sample_accepted_deploy( + version: u8, index: u32, is_fee_private: bool, rng: &mut TestRng, ) -> ConfirmedTransaction { // Sample a deploy transaction. - let tx = crate::transaction::test_helpers::sample_deployment_transaction(is_fee_private, rng); + let tx = crate::transaction::test_helpers::sample_deployment_transaction(version, is_fee_private, rng); // Construct the finalize operations based on if the fee is public or private. let finalize_operations = match is_fee_private { @@ -394,6 +395,7 @@ pub mod test_helpers { /// Samples a rejected deploy transaction at the given index. pub(crate) fn sample_rejected_deploy( + version: u8, index: u32, is_fee_private: bool, rng: &mut TestRng, @@ -405,7 +407,7 @@ pub mod test_helpers { }; // Extract the rejected deployment. - let rejected = crate::rejected::test_helpers::sample_rejected_deployment(is_fee_private, rng); + let rejected = crate::rejected::test_helpers::sample_rejected_deployment(version, is_fee_private, rng); // Return the confirmed transaction. ConfirmedTransaction::rejected_deploy(index, fee_transaction, rejected, vec![]).unwrap() @@ -435,20 +437,28 @@ pub mod test_helpers { let rng = &mut TestRng::default(); vec![ - sample_accepted_deploy(0, true, rng), - sample_accepted_deploy(0, false, rng), + sample_accepted_deploy(1, 0, true, rng), + sample_accepted_deploy(1, 0, false, rng), + sample_accepted_deploy(2, 0, true, rng), + sample_accepted_deploy(2, 0, false, rng), sample_accepted_execute(1, true, rng), sample_accepted_execute(1, false, rng), - sample_rejected_deploy(2, true, rng), - sample_rejected_deploy(2, false, rng), + sample_rejected_deploy(1, 2, true, rng), + sample_rejected_deploy(1, 2, false, rng), + sample_rejected_deploy(2, 2, true, rng), + sample_rejected_deploy(2, 2, false, rng), sample_rejected_execute(3, true, rng), sample_rejected_execute(3, false, rng), - sample_accepted_deploy(Uniform::rand(rng), true, rng), - sample_accepted_deploy(Uniform::rand(rng), false, rng), + sample_accepted_deploy(1, Uniform::rand(rng), true, rng), + sample_accepted_deploy(1, Uniform::rand(rng), false, rng), + sample_accepted_deploy(2, Uniform::rand(rng), true, rng), + sample_accepted_deploy(2, Uniform::rand(rng), false, rng), sample_accepted_execute(Uniform::rand(rng), true, rng), sample_accepted_execute(Uniform::rand(rng), false, rng), - sample_rejected_deploy(Uniform::rand(rng), true, rng), - sample_rejected_deploy(Uniform::rand(rng), false, rng), + sample_rejected_deploy(1, Uniform::rand(rng), true, rng), + sample_rejected_deploy(1, Uniform::rand(rng), false, rng), + sample_rejected_deploy(2, Uniform::rand(rng), true, rng), + sample_rejected_deploy(2, Uniform::rand(rng), false, rng), sample_rejected_execute(Uniform::rand(rng), true, rng), sample_rejected_execute(Uniform::rand(rng), false, rng), ] @@ -507,9 +517,13 @@ mod test { }; // Ensure that the unconfirmed transaction ID of an accepted deployment is equivalent to its confirmed transaction ID. - let accepted_deploy = test_helpers::sample_accepted_deploy(Uniform::rand(rng), true, rng); + let accepted_deploy = test_helpers::sample_accepted_deploy(1, Uniform::rand(rng), true, rng); + check_contains_unconfirmed_transaction_id(accepted_deploy); + let accepted_deploy = test_helpers::sample_accepted_deploy(1, Uniform::rand(rng), false, rng); + check_contains_unconfirmed_transaction_id(accepted_deploy); + let accepted_deploy = test_helpers::sample_accepted_deploy(2, Uniform::rand(rng), true, rng); check_contains_unconfirmed_transaction_id(accepted_deploy); - let accepted_deploy = test_helpers::sample_accepted_deploy(Uniform::rand(rng), false, rng); + let accepted_deploy = test_helpers::sample_accepted_deploy(2, Uniform::rand(rng), false, rng); check_contains_unconfirmed_transaction_id(accepted_deploy); // Ensure that the unconfirmed transaction ID of an accepted execute is equivalent to its confirmed transaction ID. @@ -519,9 +533,13 @@ mod test { check_contains_unconfirmed_transaction_id(accepted_execution); // Ensure that the unconfirmed transaction ID of a rejected deployment is not equivalent to its confirmed transaction ID. - let rejected_deploy = test_helpers::sample_rejected_deploy(Uniform::rand(rng), true, rng); + let rejected_deploy = test_helpers::sample_rejected_deploy(1, Uniform::rand(rng), true, rng); check_contains_unconfirmed_transaction_id(rejected_deploy); - let rejected_deploy = test_helpers::sample_rejected_deploy(Uniform::rand(rng), false, rng); + let rejected_deploy = test_helpers::sample_rejected_deploy(1, Uniform::rand(rng), false, rng); + check_contains_unconfirmed_transaction_id(rejected_deploy); + let rejected_deploy = test_helpers::sample_rejected_deploy(2, Uniform::rand(rng), true, rng); + check_contains_unconfirmed_transaction_id(rejected_deploy); + let rejected_deploy = test_helpers::sample_rejected_deploy(2, Uniform::rand(rng), false, rng); check_contains_unconfirmed_transaction_id(rejected_deploy); // Ensure that the unconfirmed transaction ID of a rejected execute is not equivalent to its confirmed transaction ID. @@ -536,9 +554,13 @@ mod test { let rng = &mut TestRng::default(); // Ensure that the unconfirmed transaction ID of an accepted deployment is equivalent to its confirmed transaction ID. - let accepted_deploy = test_helpers::sample_accepted_deploy(Uniform::rand(rng), true, rng); + let accepted_deploy = test_helpers::sample_accepted_deploy(1, Uniform::rand(rng), true, rng); + assert_eq!(accepted_deploy.to_unconfirmed_transaction_id().unwrap(), accepted_deploy.id()); + let accepted_deploy = test_helpers::sample_accepted_deploy(1, Uniform::rand(rng), false, rng); + assert_eq!(accepted_deploy.to_unconfirmed_transaction_id().unwrap(), accepted_deploy.id()); + let accepted_deploy = test_helpers::sample_accepted_deploy(2, Uniform::rand(rng), true, rng); assert_eq!(accepted_deploy.to_unconfirmed_transaction_id().unwrap(), accepted_deploy.id()); - let accepted_deploy = test_helpers::sample_accepted_deploy(Uniform::rand(rng), false, rng); + let accepted_deploy = test_helpers::sample_accepted_deploy(2, Uniform::rand(rng), false, rng); assert_eq!(accepted_deploy.to_unconfirmed_transaction_id().unwrap(), accepted_deploy.id()); // Ensure that the unconfirmed transaction ID of an accepted execute is equivalent to its confirmed transaction ID. @@ -548,9 +570,13 @@ mod test { assert_eq!(accepted_execution.to_unconfirmed_transaction_id().unwrap(), accepted_execution.id()); // Ensure that the unconfirmed transaction ID of a rejected deployment is not equivalent to its confirmed transaction ID. - let rejected_deploy = test_helpers::sample_rejected_deploy(Uniform::rand(rng), true, rng); + let rejected_deploy = test_helpers::sample_rejected_deploy(1, Uniform::rand(rng), true, rng); assert_ne!(rejected_deploy.to_unconfirmed_transaction_id().unwrap(), rejected_deploy.id()); - let rejected_deploy = test_helpers::sample_rejected_deploy(Uniform::rand(rng), false, rng); + let rejected_deploy = test_helpers::sample_rejected_deploy(1, Uniform::rand(rng), false, rng); + assert_ne!(rejected_deploy.to_unconfirmed_transaction_id().unwrap(), rejected_deploy.id()); + let rejected_deploy = test_helpers::sample_rejected_deploy(2, Uniform::rand(rng), true, rng); + assert_ne!(rejected_deploy.to_unconfirmed_transaction_id().unwrap(), rejected_deploy.id()); + let rejected_deploy = test_helpers::sample_rejected_deploy(2, Uniform::rand(rng), false, rng); assert_ne!(rejected_deploy.to_unconfirmed_transaction_id().unwrap(), rejected_deploy.id()); // Ensure that the unconfirmed transaction ID of a rejected execute is not equivalent to its confirmed transaction ID. @@ -565,9 +591,13 @@ mod test { let rng = &mut TestRng::default(); // Ensure that the unconfirmed transaction of an accepted deployment is equivalent to its confirmed transaction. - let accepted_deploy = test_helpers::sample_accepted_deploy(Uniform::rand(rng), true, rng); + let accepted_deploy = test_helpers::sample_accepted_deploy(1, Uniform::rand(rng), true, rng); + assert_eq!(&accepted_deploy.to_unconfirmed_transaction().unwrap(), accepted_deploy.transaction()); + let accepted_deploy = test_helpers::sample_accepted_deploy(1, Uniform::rand(rng), false, rng); + assert_eq!(&accepted_deploy.to_unconfirmed_transaction().unwrap(), accepted_deploy.transaction()); + let accepted_deploy = test_helpers::sample_accepted_deploy(2, Uniform::rand(rng), true, rng); assert_eq!(&accepted_deploy.to_unconfirmed_transaction().unwrap(), accepted_deploy.transaction()); - let accepted_deploy = test_helpers::sample_accepted_deploy(Uniform::rand(rng), false, rng); + let accepted_deploy = test_helpers::sample_accepted_deploy(2, Uniform::rand(rng), false, rng); assert_eq!(&accepted_deploy.to_unconfirmed_transaction().unwrap(), accepted_deploy.transaction()); // Ensure that the unconfirmed transaction of an accepted execute is equivalent to its confirmed transaction. @@ -577,7 +607,25 @@ mod test { assert_eq!(&accepted_execution.to_unconfirmed_transaction().unwrap(), accepted_execution.transaction()); // Ensure that the unconfirmed transaction of a rejected deployment is not equivalent to its confirmed transaction. - let deployment_transaction = crate::transaction::test_helpers::sample_deployment_transaction(true, rng); + let deployment_transaction = crate::transaction::test_helpers::sample_deployment_transaction(1, true, rng); + let rejected = Rejected::new_deployment( + *deployment_transaction.owner().unwrap(), + deployment_transaction.deployment().unwrap().clone(), + ); + let fee = Transaction::from_fee(deployment_transaction.fee_transition().unwrap()).unwrap(); + let rejected_deploy = ConfirmedTransaction::rejected_deploy(Uniform::rand(rng), fee, rejected, vec![]).unwrap(); + assert_eq!(rejected_deploy.to_unconfirmed_transaction_id().unwrap(), deployment_transaction.id()); + assert_eq!(rejected_deploy.to_unconfirmed_transaction().unwrap(), deployment_transaction); + let deployment_transaction = crate::transaction::test_helpers::sample_deployment_transaction(1, false, rng); + let rejected = Rejected::new_deployment( + *deployment_transaction.owner().unwrap(), + deployment_transaction.deployment().unwrap().clone(), + ); + let fee = Transaction::from_fee(deployment_transaction.fee_transition().unwrap()).unwrap(); + let rejected_deploy = ConfirmedTransaction::rejected_deploy(Uniform::rand(rng), fee, rejected, vec![]).unwrap(); + assert_eq!(rejected_deploy.to_unconfirmed_transaction_id().unwrap(), deployment_transaction.id()); + assert_eq!(rejected_deploy.to_unconfirmed_transaction().unwrap(), deployment_transaction); + let deployment_transaction = crate::transaction::test_helpers::sample_deployment_transaction(2, true, rng); let rejected = Rejected::new_deployment( *deployment_transaction.owner().unwrap(), deployment_transaction.deployment().unwrap().clone(), @@ -586,7 +634,7 @@ mod test { let rejected_deploy = ConfirmedTransaction::rejected_deploy(Uniform::rand(rng), fee, rejected, vec![]).unwrap(); assert_eq!(rejected_deploy.to_unconfirmed_transaction_id().unwrap(), deployment_transaction.id()); assert_eq!(rejected_deploy.to_unconfirmed_transaction().unwrap(), deployment_transaction); - let deployment_transaction = crate::transaction::test_helpers::sample_deployment_transaction(false, rng); + let deployment_transaction = crate::transaction::test_helpers::sample_deployment_transaction(2, false, rng); let rejected = Rejected::new_deployment( *deployment_transaction.owner().unwrap(), deployment_transaction.deployment().unwrap().clone(), diff --git a/ledger/block/src/transactions/rejected/mod.rs b/ledger/block/src/transactions/rejected/mod.rs index 47b63753c0..c81224af90 100644 --- a/ledger/block/src/transactions/rejected/mod.rs +++ b/ledger/block/src/transactions/rejected/mod.rs @@ -85,15 +85,13 @@ impl Rejected { /// When a transaction is rejected, its fee transition is used to construct the confirmed transaction ID, /// changing the original transaction ID. pub fn to_unconfirmed_id(&self, fee: &Option>) -> Result> { - let (tree, fee_index) = match self { - Self::Deployment(_, deployment) => (Transaction::deployment_tree(deployment)?, deployment.len()), - Self::Execution(execution) => (Transaction::execution_tree(execution)?, execution.len()), + // Compute the deployment or execution tree. + let tree = match self { + Self::Deployment(_, deployment) => Transaction::deployment_tree(deployment)?, + Self::Execution(execution) => Transaction::execution_tree(execution)?, }; - if let Some(fee) = fee { - Ok(*Transaction::transaction_tree(tree, fee_index, fee)?.root()) - } else { - Ok(*tree.root()) - } + // Construct the transaction tree and return the unconfirmed transaction ID. + Ok(*Transaction::transaction_tree(tree, fee.as_ref())?.root()) } } @@ -105,12 +103,17 @@ pub mod test_helpers { type CurrentNetwork = MainnetV0; /// Samples a rejected deployment. - pub(crate) fn sample_rejected_deployment(is_fee_private: bool, rng: &mut TestRng) -> Rejected { + pub(crate) fn sample_rejected_deployment( + version: u8, + is_fee_private: bool, + rng: &mut TestRng, + ) -> Rejected { // Sample a deploy transaction. - let deployment = match crate::transaction::test_helpers::sample_deployment_transaction(is_fee_private, rng) { - Transaction::Deploy(_, _, _, deployment, _) => (*deployment).clone(), - _ => unreachable!(), - }; + let deployment = + match crate::transaction::test_helpers::sample_deployment_transaction(version, is_fee_private, rng) { + Transaction::Deploy(_, _, _, deployment, _) => (*deployment).clone(), + _ => unreachable!(), + }; // Sample a new program owner. let private_key = PrivateKey::new(rng).unwrap(); @@ -139,8 +142,10 @@ pub mod test_helpers { let rng = &mut TestRng::default(); vec![ - sample_rejected_deployment(true, rng), - sample_rejected_deployment(false, rng), + sample_rejected_deployment(1, true, rng), + sample_rejected_deployment(1, false, rng), + sample_rejected_deployment(2, true, rng), + sample_rejected_deployment(2, false, rng), sample_rejected_execution(true, rng), sample_rejected_execution(false, rng), ] diff --git a/ledger/block/src/transactions/serialize.rs b/ledger/block/src/transactions/serialize.rs index ba73f39463..a2de0864c0 100644 --- a/ledger/block/src/transactions/serialize.rs +++ b/ledger/block/src/transactions/serialize.rs @@ -75,8 +75,10 @@ mod tests { fn sample_transactions(index: u32, rng: &mut TestRng) -> Transactions { if index == 0 { [ - crate::transactions::confirmed::test_helpers::sample_accepted_deploy(0, true, rng), - crate::transactions::confirmed::test_helpers::sample_accepted_deploy(1, false, rng), + crate::transactions::confirmed::test_helpers::sample_accepted_deploy(1, 0, true, rng), + crate::transactions::confirmed::test_helpers::sample_accepted_deploy(1, 1, false, rng), + crate::transactions::confirmed::test_helpers::sample_accepted_deploy(2, 0, true, rng), + crate::transactions::confirmed::test_helpers::sample_accepted_deploy(2, 1, false, rng), ] .into_iter() .collect() @@ -89,7 +91,8 @@ mod tests { .collect() } else if index == 2 { [ - crate::transactions::confirmed::test_helpers::sample_accepted_deploy(0, true, rng), + crate::transactions::confirmed::test_helpers::sample_accepted_deploy(1, 0, true, rng), + crate::transactions::confirmed::test_helpers::sample_accepted_deploy(2, 0, true, rng), crate::transactions::confirmed::test_helpers::sample_accepted_execute(1, true, rng), crate::transactions::confirmed::test_helpers::sample_accepted_execute(2, false, rng), ] @@ -98,22 +101,27 @@ mod tests { } else if index == 3 { [ crate::transactions::confirmed::test_helpers::sample_accepted_execute(0, true, rng), - crate::transactions::confirmed::test_helpers::sample_accepted_deploy(1, true, rng), + crate::transactions::confirmed::test_helpers::sample_accepted_deploy(1, 1, true, rng), + crate::transactions::confirmed::test_helpers::sample_accepted_deploy(2, 1, true, rng), crate::transactions::confirmed::test_helpers::sample_rejected_execute(2, false, rng), - crate::transactions::confirmed::test_helpers::sample_rejected_deploy(3, false, rng), + crate::transactions::confirmed::test_helpers::sample_rejected_deploy(1, 3, false, rng), + crate::transactions::confirmed::test_helpers::sample_rejected_deploy(2, 3, false, rng), ] .into_iter() .collect() } else { [ crate::transactions::confirmed::test_helpers::sample_accepted_execute(0, true, rng), - crate::transactions::confirmed::test_helpers::sample_rejected_deploy(1, true, rng), - crate::transactions::confirmed::test_helpers::sample_accepted_deploy(2, true, rng), + crate::transactions::confirmed::test_helpers::sample_rejected_deploy(1, 1, true, rng), + crate::transactions::confirmed::test_helpers::sample_accepted_deploy(1, 2, true, rng), + crate::transactions::confirmed::test_helpers::sample_rejected_deploy(2, 1, true, rng), + crate::transactions::confirmed::test_helpers::sample_accepted_deploy(2, 2, true, rng), crate::transactions::confirmed::test_helpers::sample_rejected_execute(3, true, rng), crate::transactions::confirmed::test_helpers::sample_accepted_execute(4, false, rng), crate::transactions::confirmed::test_helpers::sample_rejected_execute(5, false, rng), crate::transactions::confirmed::test_helpers::sample_accepted_execute(6, false, rng), - crate::transactions::confirmed::test_helpers::sample_rejected_deploy(7, false, rng), + crate::transactions::confirmed::test_helpers::sample_rejected_deploy(1, 7, false, rng), + crate::transactions::confirmed::test_helpers::sample_rejected_deploy(2, 7, false, rng), ] .into_iter() .collect() diff --git a/ledger/narwhal/data/src/lib.rs b/ledger/narwhal/data/src/lib.rs index 2521820a2f..e5ddcbdf90 100644 --- a/ledger/narwhal/data/src/lib.rs +++ b/ledger/narwhal/data/src/lib.rs @@ -251,8 +251,10 @@ mod tests { // Sample transactions let transactions = [ - ledger_test_helpers::sample_deployment_transaction(true, rng), - ledger_test_helpers::sample_deployment_transaction(false, rng), + ledger_test_helpers::sample_deployment_transaction(1, true, rng), + ledger_test_helpers::sample_deployment_transaction(1, false, rng), + ledger_test_helpers::sample_deployment_transaction(2, true, rng), + ledger_test_helpers::sample_deployment_transaction(2, false, rng), ledger_test_helpers::sample_execution_transaction_with_fee(true, rng), ledger_test_helpers::sample_execution_transaction_with_fee(false, rng), ledger_test_helpers::sample_fee_private_transaction(rng), diff --git a/ledger/puzzle/epoch/src/synthesis/program/mod.rs b/ledger/puzzle/epoch/src/synthesis/program/mod.rs index 8893182b40..d3ea2e3bb6 100644 --- a/ledger/puzzle/epoch/src/synthesis/program/mod.rs +++ b/ledger/puzzle/epoch/src/synthesis/program/mod.rs @@ -26,7 +26,7 @@ use console::{ program::{Field, Identifier, Literal, LiteralType, Value}, }; use snarkvm_synthesizer_process::{CallStack, Process, Registers, Stack, StackProgramTypes}; -use snarkvm_synthesizer_program::{Instruction, Program, RegistersStoreCircuit, StackProgram}; +use snarkvm_synthesizer_program::{Instruction, InstructionTrait, Program, RegistersStoreCircuit, StackProgram}; use aleo_std::prelude::{finish, lap, timer}; use anyhow::{Result, anyhow, bail, ensure}; diff --git a/ledger/query/src/query.rs b/ledger/query/src/query.rs index 937e8e5596..c52949a591 100644 --- a/ledger/query/src/query.rs +++ b/ledger/query/src/query.rs @@ -184,9 +184,9 @@ impl> Query { /// Returns the program for the given program ID. pub fn get_program(&self, program_id: &ProgramID) -> Result> { match self { - Self::VM(block_store) => { - block_store.get_program(program_id)?.ok_or_else(|| anyhow!("Program {program_id} not found in storage")) - } + Self::VM(block_store) => block_store + .get_latest_program(program_id)? + .ok_or_else(|| anyhow!("Program {program_id} not found in storage")), Self::REST(url) => match N::ID { console::network::MainnetV0::ID => { Ok(Self::get_request(&format!("{url}/mainnet/program/{program_id}"))?.into_json()?) @@ -206,9 +206,9 @@ impl> Query { #[cfg(feature = "async")] pub async fn get_program_async(&self, program_id: &ProgramID) -> Result> { match self { - Self::VM(block_store) => { - block_store.get_program(program_id)?.ok_or_else(|| anyhow!("Program {program_id} not found in storage")) - } + Self::VM(block_store) => block_store + .get_latest_program(program_id)? + .ok_or_else(|| anyhow!("Program {program_id} not found in storage")), Self::REST(url) => match N::ID { console::network::MainnetV0::ID => { Ok(Self::get_request_async(&format!("{url}/mainnet/program/{program_id}")).await?.json().await?) diff --git a/ledger/src/find.rs b/ledger/src/find.rs index 96099f27fc..94a91c6653 100644 --- a/ledger/src/find.rs +++ b/ledger/src/find.rs @@ -31,9 +31,21 @@ impl> Ledger { self.vm.block_store().find_block_height_from_solution_id(solution_id) } - /// Returns the transaction ID that contains the given `program ID`. - pub fn find_transaction_id_from_program_id(&self, program_id: &ProgramID) -> Result> { - self.vm.transaction_store().find_transaction_id_from_program_id(program_id) + /// Returns the latest transaction ID that contains the given `program ID`. + pub fn find_latest_transaction_id_from_program_id( + &self, + program_id: &ProgramID, + ) -> Result> { + self.vm.transaction_store().find_latest_transaction_id_from_program_id(program_id) + } + + /// Returns the transaction ID that contains the given `program ID` and `edition`. + pub fn find_transaction_id_from_program_id_and_edition( + &self, + program_id: &ProgramID, + edition: u16, + ) -> Result> { + self.vm.transaction_store().find_transaction_id_from_program_id_and_edition(program_id, edition) } /// Returns the transaction ID that contains the given `transition ID`. diff --git a/ledger/src/get.rs b/ledger/src/get.rs index 0f20a3373f..2043ab8a65 100644 --- a/ledger/src/get.rs +++ b/ledger/src/get.rs @@ -231,14 +231,30 @@ impl> Ledger { } } - /// Returns the program for the given program ID. + /// Returns the latest edition for the given `program ID`. + pub fn get_latest_edition_for_program(&self, program_id: &ProgramID) -> Result { + match self.vm.block_store().get_latest_edition_for_program(program_id)? { + Some(edition) => Ok(edition), + None => bail!("Missing latest edition for program ID {program_id}"), + } + } + + /// Returns the latest program for the given `program ID`. pub fn get_program(&self, program_id: ProgramID) -> Result> { - match self.vm.block_store().get_program(&program_id)? { + match self.vm.block_store().get_latest_program(&program_id)? { Some(program) => Ok(program), None => bail!("Missing program for ID {program_id}"), } } + /// Returns the program for the given `program ID` and `edition`. + pub fn get_program_for_edition(&self, program_id: ProgramID, edition: u16) -> Result> { + match self.vm.block_store().get_program_for_edition(&program_id, edition)? { + Some(program) => Ok(program), + None => bail!("Missing program for ID {program_id} and edition {edition}"), + } + } + /// Returns the block solutions for the given block height. pub fn get_solutions(&self, height: u32) -> Result> { // If the height is 0, return the genesis block solutions. diff --git a/ledger/src/tests.rs b/ledger/src/tests.rs index 7c5e5efd8e..ae9486ee89 100644 --- a/ledger/src/tests.rs +++ b/ledger/src/tests.rs @@ -2018,6 +2018,7 @@ finalize foo2: // Enforce that the block transactions were correct. assert_eq!(block.transactions().num_accepted(), 1); assert_eq!(block.transactions().num_rejected(), 1); + assert_eq!(block.aborted_transaction_ids().len(), 0); // Enforce that the first program was deployed and the second was rejected. assert_eq!(ledger.get_program(*program_1.id()).unwrap(), program_1); @@ -3168,6 +3169,83 @@ mod valid_solutions { let block_aborted_solution_id = block.aborted_solution_ids().first().unwrap(); assert_eq!(*block_aborted_solution_id, invalid_solution.id(), "Aborted solutions do not match"); } + + // This test checks that a program can only be upgraded after a certain block height. + // While this test does not need a low proof target, it needs the `test` feature enabled to use a sufficiently low consensus height. + #[test] + fn test_upgrade_after_block_height() -> Result<()> { + let rng = &mut TestRng::default(); + + // Sample the test environment. + let crate::test_helpers::TestEnv { ledger, private_key, .. } = crate::test_helpers::sample_test_env(rng); + let caller_private_key = private_key; + + // Advance the ledger past the V8 block height. + let v8_height = CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V8)?; + for _ in ledger.latest_height()..v8_height { + let block = + ledger.prepare_advance_to_next_beacon_block(&caller_private_key, vec![], vec![], vec![], rng)?; + ledger.advance_to_next_block(&block)?; + } + + // Define the programs. + let program_v0 = Program::from_str( + r" +program upgradable.aleo; +function foo: +constructor: + branch.eq edition 0u16 to end; + gt block.height 18u32 into r0; + assert.eq r0 true; + position end; + ", + )?; + + let program_v1 = Program::from_str( + r" +program upgradable.aleo; +function foo: +function bar: +constructor: + branch.eq edition 0u16 to end; + gt block.height 18u32 into r0; + assert.eq r0 true; + position end; + ", + )?; + + // Deploy the first version of the program. + let transaction = ledger.vm().deploy(&caller_private_key, &program_v0, None, 0, None, rng)?; + let block = + ledger.prepare_advance_to_next_beacon_block(&caller_private_key, vec![], vec![], vec![transaction], rng)?; + assert_eq!(block.height(), 17); + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + ledger.advance_to_next_block(&block)?; + + // Attempt to deploy the second version of the program before block height 18. + let transaction = ledger.vm().deploy(&caller_private_key, &program_v1, None, 0, None, rng)?; + let block = + ledger.prepare_advance_to_next_beacon_block(&caller_private_key, vec![], vec![], vec![transaction], rng)?; + assert_eq!(block.height(), 18); + assert_eq!(block.transactions().num_accepted(), 0); + assert_eq!(block.transactions().num_rejected(), 1); + assert_eq!(block.aborted_transaction_ids().len(), 0); + ledger.advance_to_next_block(&block)?; + + // Attempt to deploy the second version of the program at block height 18. + let transaction = ledger.vm().deploy(&caller_private_key, &program_v1, None, 0, None, rng)?; + let block = + ledger.prepare_advance_to_next_beacon_block(&caller_private_key, vec![], vec![], vec![transaction], rng)?; + assert_eq!(block.height(), 19); + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + ledger.advance_to_next_block(&block)?; + + Ok(()) + } } /// Tests multiple attacks where the subDAG of a block is invalid diff --git a/ledger/store/src/block/confirmed_tx_type/mod.rs b/ledger/store/src/block/confirmed_tx_type/mod.rs index da6ec79306..77a0f06205 100644 --- a/ledger/store/src/block/confirmed_tx_type/mod.rs +++ b/ledger/store/src/block/confirmed_tx_type/mod.rs @@ -51,9 +51,9 @@ pub mod test_helpers { } /// Samples a rejected deploy. - pub(crate) fn sample_rejected_deploy(rng: &mut TestRng) -> ConfirmedTxType { + pub(crate) fn sample_rejected_deploy(version: u8, rng: &mut TestRng) -> ConfirmedTxType { // Sample the rejected deployment. - let rejected = ledger_test_helpers::sample_rejected_deployment(rng.gen(), rng); + let rejected = ledger_test_helpers::sample_rejected_deployment(version, rng.gen(), rng); // Return the rejected deploy. ConfirmedTxType::RejectedDeploy(rng.gen(), rejected) } @@ -73,7 +73,8 @@ pub mod test_helpers { vec![ sample_accepted_deploy(rng), sample_accepted_execution(rng), - sample_rejected_deploy(rng), + sample_rejected_deploy(1, rng), + sample_rejected_deploy(2, rng), sample_rejected_execute(rng), ] } diff --git a/ledger/store/src/block/mod.rs b/ledger/store/src/block/mod.rs index 972ca4bbb0..856499fb06 100644 --- a/ledger/store/src/block/mod.rs +++ b/ledger/store/src/block/mod.rs @@ -1286,9 +1286,19 @@ impl> BlockStore { self.storage.get_block(block_hash) } - /// Returns the program for the given `program ID`. - pub fn get_program(&self, program_id: &ProgramID) -> Result>> { - self.storage.transaction_store().get_program(program_id) + /// Returns the latest edition for the given `program ID`. + pub fn get_latest_edition_for_program(&self, program_id: &ProgramID) -> Result> { + self.storage.transaction_store().get_latest_edition_for_program(program_id) + } + + /// Returns the latest program for the given `program ID`. + pub fn get_latest_program(&self, program_id: &ProgramID) -> Result>> { + self.storage.transaction_store().get_latest_program(program_id) + } + + /// Returns the program for the given `program ID` and `edition`. + pub fn get_program_for_edition(&self, program_id: &ProgramID, edition: u16) -> Result>> { + self.storage.transaction_store().get_program_for_edition(program_id, edition) } /// Returns true if there is a block for the given certificate. diff --git a/ledger/store/src/helpers/memory/internal/map.rs b/ledger/store/src/helpers/memory/internal/map.rs index b896aa2640..9d68b5b729 100644 --- a/ledger/store/src/helpers/memory/internal/map.rs +++ b/ledger/store/src/helpers/memory/internal/map.rs @@ -36,7 +36,7 @@ use std::{ #[derive(Clone)] pub struct MemoryMap< K: Copy + Clone + PartialEq + Eq + Hash + Serialize + for<'de> Deserialize<'de> + Send + Sync, - V: Clone + PartialEq + Eq + Serialize + for<'de> Deserialize<'de> + Send + Sync, + V: Clone + Serialize + for<'de> Deserialize<'de> + Send + Sync, > { // The reason for using BTreeMap with binary keys is for the order of items to be the same as // the one in the RocksDB-backed DataMap; if not for that, it could be any map @@ -49,7 +49,7 @@ pub struct MemoryMap< impl< K: Copy + Clone + PartialEq + Eq + Hash + Serialize + for<'de> Deserialize<'de> + Send + Sync, - V: Clone + PartialEq + Eq + Serialize + for<'de> Deserialize<'de> + Send + Sync, + V: Clone + Serialize + for<'de> Deserialize<'de> + Send + Sync, > Default for MemoryMap { fn default() -> Self { @@ -64,7 +64,7 @@ impl< impl< K: Copy + Clone + PartialEq + Eq + Hash + Serialize + for<'de> Deserialize<'de> + Send + Sync, - V: Clone + PartialEq + Eq + Serialize + for<'de> Deserialize<'de> + Send + Sync, + V: Clone + Serialize + for<'de> Deserialize<'de> + Send + Sync, > FromIterator<(K, V)> for MemoryMap { /// Initializes a new `MemoryMap` from the given iterator. @@ -85,7 +85,7 @@ impl< impl< 'a, K: 'a + Copy + Clone + PartialEq + Eq + Hash + Serialize + for<'de> Deserialize<'de> + Send + Sync, - V: 'a + Clone + PartialEq + Eq + Serialize + for<'de> Deserialize<'de> + Send + Sync, + V: 'a + Clone + Serialize + for<'de> Deserialize<'de> + Send + Sync, > Map<'a, K, V> for MemoryMap { /// @@ -256,7 +256,7 @@ impl< impl< 'a, K: 'a + Copy + Clone + PartialEq + Eq + Hash + Serialize + for<'de> Deserialize<'de> + Send + Sync, - V: 'a + Clone + PartialEq + Eq + Serialize + for<'de> Deserialize<'de> + Send + Sync, + V: 'a + Clone + Serialize + for<'de> Deserialize<'de> + Send + Sync, > MapRead<'a, K, V> for MemoryMap { type Iterator = core::iter::Map, V>, fn((Vec, V)) -> (Cow<'a, K>, Cow<'a, V>)>; @@ -374,7 +374,7 @@ impl< impl< K: Copy + Clone + PartialEq + Eq + Hash + Serialize + for<'de> Deserialize<'de> + Send + Sync, - V: Clone + PartialEq + Eq + Serialize + for<'de> Deserialize<'de> + Send + Sync, + V: Clone + Serialize + for<'de> Deserialize<'de> + Send + Sync, > Deref for MemoryMap { type Target = Arc, V>>>; diff --git a/ledger/store/src/helpers/memory/internal/nested_map.rs b/ledger/store/src/helpers/memory/internal/nested_map.rs index 325751c5ce..6d50fd9823 100644 --- a/ledger/store/src/helpers/memory/internal/nested_map.rs +++ b/ledger/store/src/helpers/memory/internal/nested_map.rs @@ -36,7 +36,7 @@ use std::{ pub struct NestedMemoryMap< M: Copy + Clone + PartialEq + Eq + Hash + Serialize + for<'de> Deserialize<'de> + Send + Sync, K: Clone + PartialEq + Eq + Serialize + for<'de> Deserialize<'de> + Send + Sync, - V: Clone + PartialEq + Eq + Serialize + for<'de> Deserialize<'de> + Send + Sync, + V: Clone + Serialize + for<'de> Deserialize<'de> + Send + Sync, > { // The reason for using BTreeMap with binary keys is for the order of items to be the same as // the one in the RocksDB-backed DataMap; if not for that, it could be any map @@ -51,7 +51,7 @@ pub struct NestedMemoryMap< impl< M: Copy + Clone + PartialEq + Eq + Hash + Serialize + for<'de> Deserialize<'de> + Send + Sync, K: Clone + PartialEq + Eq + Serialize + for<'de> Deserialize<'de> + Send + Sync, - V: Clone + PartialEq + Eq + Serialize + for<'de> Deserialize<'de> + Send + Sync, + V: Clone + Serialize + for<'de> Deserialize<'de> + Send + Sync, > Default for NestedMemoryMap { fn default() -> Self { @@ -68,7 +68,7 @@ impl< impl< M: Copy + Clone + PartialEq + Eq + Hash + Serialize + for<'de> Deserialize<'de> + Send + Sync, K: Clone + PartialEq + Eq + Serialize + for<'de> Deserialize<'de> + Send + Sync, - V: Clone + PartialEq + Eq + Serialize + for<'de> Deserialize<'de> + Send + Sync, + V: Clone + Serialize + for<'de> Deserialize<'de> + Send + Sync, > FromIterator<(M, K, V)> for NestedMemoryMap { /// Initializes a new `NestedMemoryMap` from the given iterator. @@ -97,7 +97,7 @@ impl< 'a, M: 'a + Copy + Clone + PartialEq + Eq + Hash + Serialize + for<'de> Deserialize<'de> + Send + Sync, K: 'a + Clone + PartialEq + Eq + Serialize + for<'de> Deserialize<'de> + Send + Sync, - V: 'a + Clone + PartialEq + Eq + Serialize + for<'de> Deserialize<'de> + Send + Sync, + V: 'a + Clone + Serialize + for<'de> Deserialize<'de> + Send + Sync, > NestedMap<'a, M, K, V> for NestedMemoryMap { /// @@ -242,7 +242,7 @@ impl< 'a, M: 'a + Copy + Clone + PartialEq + Eq + Hash + Serialize + for<'de> Deserialize<'de> + Send + Sync, K: 'a + Clone + PartialEq + Eq + Serialize + for<'de> Deserialize<'de> + Send + Sync, - V: 'a + Clone + PartialEq + Eq + Serialize + for<'de> Deserialize<'de> + Send + Sync, + V: 'a + Clone + Serialize + for<'de> Deserialize<'de> + Send + Sync, > NestedMapRead<'a, M, K, V> for NestedMemoryMap { // type Iterator = core::iter::FlatMap< @@ -510,7 +510,7 @@ impl< fn insert< M: Copy + Clone + PartialEq + Eq + Hash + Serialize + for<'de> Deserialize<'de> + Send + Sync, K: Clone + PartialEq + Eq + Serialize + for<'de> Deserialize<'de> + Send + Sync, - V: Clone + PartialEq + Eq + Serialize + for<'de> Deserialize<'de> + Send + Sync, + V: Clone + Serialize + for<'de> Deserialize<'de> + Send + Sync, >( map: &mut BTreeMap, BTreeSet>>, map_inner: &mut BTreeMap, V>, @@ -534,7 +534,7 @@ fn insert< /// Removes the given map-key pair. fn remove_map< M: Copy + Clone + PartialEq + Eq + Hash + Serialize + for<'de> Deserialize<'de> + Send + Sync, - V: Clone + PartialEq + Eq + Serialize + for<'de> Deserialize<'de> + Send + Sync, + V: Clone + Serialize + for<'de> Deserialize<'de> + Send + Sync, >( map: &mut BTreeMap, BTreeSet>>, map_inner: &mut BTreeMap, V>, @@ -560,7 +560,7 @@ fn remove_map< fn remove_key< M: Copy + Clone + PartialEq + Eq + Hash + Serialize + for<'de> Deserialize<'de> + Send + Sync, K: Clone + PartialEq + Eq + Serialize + for<'de> Deserialize<'de> + Send + Sync, - V: Clone + PartialEq + Eq + Serialize + for<'de> Deserialize<'de> + Send + Sync, + V: Clone + Serialize + for<'de> Deserialize<'de> + Send + Sync, >( map: &mut BTreeMap, BTreeSet>>, map_inner: &mut BTreeMap, V>, diff --git a/ledger/store/src/helpers/memory/transaction.rs b/ledger/store/src/helpers/memory/transaction.rs index e1736ffaed..accbd71c6b 100644 --- a/ledger/store/src/helpers/memory/transaction.rs +++ b/ledger/store/src/helpers/memory/transaction.rs @@ -28,6 +28,7 @@ use crate::{ use console::{ prelude::*, program::{Identifier, ProgramID, ProgramOwner}, + types::U8, }; use synthesizer_program::Program; use synthesizer_snark::{Certificate, Proof, VerifyingKey}; @@ -92,6 +93,8 @@ impl TransactionStorage for TransactionMemory { pub struct DeploymentMemory { /// The ID map. id_map: MemoryMap>, + /// The ID edition map. + id_edition_map: MemoryMap, /// The edition map. edition_map: MemoryMap, u16>, /// The reverse ID map. @@ -100,6 +103,8 @@ pub struct DeploymentMemory { owner_map: MemoryMap<(ProgramID, u16), ProgramOwner>, /// The program map. program_map: MemoryMap<(ProgramID, u16), Program>, + /// The checksum map. + checksum_map: MemoryMap<(ProgramID, u16), [U8; 32]>, /// The verifying key map. verifying_key_map: MemoryMap<(ProgramID, Identifier, u16), VerifyingKey>, /// The certificate map. @@ -111,10 +116,12 @@ pub struct DeploymentMemory { #[rustfmt::skip] impl DeploymentStorage for DeploymentMemory { type IDMap = MemoryMap>; + type IDEditionMap = MemoryMap; type EditionMap = MemoryMap, u16>; type ReverseIDMap = MemoryMap<(ProgramID, u16), N::TransactionID>; type OwnerMap = MemoryMap<(ProgramID, u16), ProgramOwner>; type ProgramMap = MemoryMap<(ProgramID, u16), Program>; + type ChecksumMap = MemoryMap<(ProgramID, u16), [U8; 32]>; type VerifyingKeyMap = MemoryMap<(ProgramID, Identifier, u16), VerifyingKey>; type CertificateMap = MemoryMap<(ProgramID, Identifier, u16), Certificate>; type FeeStorage = FeeMemory; @@ -123,10 +130,12 @@ impl DeploymentStorage for DeploymentMemory { fn open(fee_store: FeeStore) -> Result { Ok(Self { id_map: MemoryMap::default(), + id_edition_map: MemoryMap::default(), edition_map: MemoryMap::default(), reverse_id_map: MemoryMap::default(), owner_map: MemoryMap::default(), program_map: MemoryMap::default(), + checksum_map: MemoryMap::default(), verifying_key_map: MemoryMap::default(), certificate_map: MemoryMap::default(), fee_store, @@ -138,6 +147,11 @@ impl DeploymentStorage for DeploymentMemory { &self.id_map } + /// Returns the ID edition map. + fn id_edition_map(&self) -> &Self::IDEditionMap { + &self.id_edition_map + } + /// Returns the edition map. fn edition_map(&self) -> &Self::EditionMap { &self.edition_map @@ -158,6 +172,11 @@ impl DeploymentStorage for DeploymentMemory { &self.program_map } + /// Returns the checksum map. + fn checksum_map(&self) -> &Self::ChecksumMap { + &self.checksum_map + } + /// Returns the verifying key map. fn verifying_key_map(&self) -> &Self::VerifyingKeyMap { &self.verifying_key_map diff --git a/ledger/store/src/helpers/rocksdb/internal/id.rs b/ledger/store/src/helpers/rocksdb/internal/id.rs index 77849df0e3..3d99c61e32 100644 --- a/ledger/store/src/helpers/rocksdb/internal/id.rs +++ b/ledger/store/src/helpers/rocksdb/internal/id.rs @@ -106,10 +106,12 @@ pub enum CommitteeMap { #[repr(u16)] pub enum DeploymentMap { ID = DataID::DeploymentIDMap as u16, + IDEdition = DataID::IDEditionMap as u16, Edition = DataID::DeploymentEditionMap as u16, ReverseID = DataID::DeploymentReverseIDMap as u16, Owner = DataID::DeploymentOwnerMap as u16, Program = DataID::DeploymentProgramMap as u16, + Checksum = DataID::DeploymentChecksumMap as u16, VerifyingKey = DataID::DeploymentVerifyingKeyMap as u16, Certificate = DataID::DeploymentCertificateMap as u16, } @@ -293,6 +295,10 @@ enum DataID { // Program ProgramIDMap, KeyValueMap, + // Track edition based on transaction ID + IDEditionMap, + // Track deployments that contain an optional checksum + DeploymentChecksumMap, // Testing #[cfg(test)] diff --git a/ledger/store/src/helpers/rocksdb/internal/map.rs b/ledger/store/src/helpers/rocksdb/internal/map.rs index c33fcc97d9..bf97e14878 100644 --- a/ledger/store/src/helpers/rocksdb/internal/map.rs +++ b/ledger/store/src/helpers/rocksdb/internal/map.rs @@ -51,7 +51,7 @@ pub struct InnerDataMap Map<'a, K, V> for DataMap { /// @@ -258,7 +258,7 @@ impl< impl< 'a, K: 'a + Copy + Clone + Debug + PartialEq + Eq + Hash + Serialize + DeserializeOwned + Send + Sync, - V: 'a + Clone + PartialEq + Eq + Serialize + DeserializeOwned + Send + Sync, + V: 'a + Clone + Serialize + DeserializeOwned + Send + Sync, > MapRead<'a, K, V> for DataMap { type Iterator = Iter<'a, K, V>; @@ -404,17 +404,14 @@ impl< pub struct Iter< 'a, K: 'a + Debug + PartialEq + Eq + Hash + Serialize + DeserializeOwned, - V: 'a + PartialEq + Eq + Serialize + DeserializeOwned, + V: 'a + Serialize + DeserializeOwned, > { db_iter: rocksdb::DBRawIterator<'a>, _phantom: PhantomData<(K, V)>, } -impl< - 'a, - K: 'a + Debug + PartialEq + Eq + Hash + Serialize + DeserializeOwned, - V: 'a + PartialEq + Eq + Serialize + DeserializeOwned, -> Iter<'a, K, V> +impl<'a, K: 'a + Debug + PartialEq + Eq + Hash + Serialize + DeserializeOwned, V: 'a + Serialize + DeserializeOwned> + Iter<'a, K, V> { pub(super) fn new(db_iter: rocksdb::DBIterator<'a>) -> Self { Self { db_iter: db_iter.into(), _phantom: PhantomData } @@ -424,7 +421,7 @@ impl< impl< 'a, K: 'a + Clone + Debug + PartialEq + Eq + Hash + Serialize + DeserializeOwned, - V: 'a + Clone + PartialEq + Eq + Serialize + DeserializeOwned, + V: 'a + Clone + Serialize + DeserializeOwned, > Iterator for Iter<'a, K, V> { type Item = (Cow<'a, K>, Cow<'a, V>); @@ -488,18 +485,18 @@ impl<'a, K: 'a + Clone + Debug + PartialEq + Eq + Hash + Serialize + Deserialize } /// An iterator over the values of a prefix. -pub struct Values<'a, V: 'a + PartialEq + Eq + Serialize + DeserializeOwned> { +pub struct Values<'a, V: 'a + Serialize + DeserializeOwned> { db_iter: rocksdb::DBRawIterator<'a>, _phantom: PhantomData, } -impl<'a, V: 'a + PartialEq + Eq + Serialize + DeserializeOwned> Values<'a, V> { +impl<'a, V: 'a + Serialize + DeserializeOwned> Values<'a, V> { pub(crate) fn new(db_iter: rocksdb::DBIterator<'a>) -> Self { Self { db_iter: db_iter.into(), _phantom: PhantomData } } } -impl<'a, V: 'a + Clone + PartialEq + Eq + Serialize + DeserializeOwned> Iterator for Values<'a, V> { +impl<'a, V: 'a + Clone + Serialize + DeserializeOwned> Iterator for Values<'a, V> { type Item = Cow<'a, V>; fn next(&mut self) -> Option { diff --git a/ledger/store/src/helpers/rocksdb/internal/nested_map.rs b/ledger/store/src/helpers/rocksdb/internal/nested_map.rs index 5c2419be46..c798be9072 100644 --- a/ledger/store/src/helpers/rocksdb/internal/nested_map.rs +++ b/ledger/store/src/helpers/rocksdb/internal/nested_map.rs @@ -105,7 +105,7 @@ impl< 'a, M: 'a + Copy + Clone + Debug + PartialEq + Eq + Hash + Serialize + DeserializeOwned + Send + Sync, K: 'a + Clone + Debug + PartialEq + Eq + Serialize + DeserializeOwned + Send + Sync, - V: 'a + Clone + PartialEq + Eq + Serialize + DeserializeOwned + Send + Sync, + V: 'a + Clone + Serialize + DeserializeOwned + Send + Sync, > NestedMap<'a, M, K, V> for NestedDataMap { /// @@ -346,7 +346,7 @@ impl< 'a, M: 'a + Copy + Clone + Debug + PartialEq + Eq + Hash + Serialize + DeserializeOwned + Send + Sync, K: 'a + Clone + Debug + PartialEq + Eq + Serialize + DeserializeOwned + Send + Sync, - V: 'a + Clone + PartialEq + Eq + Serialize + DeserializeOwned + Send + Sync, + V: 'a + Clone + Serialize + DeserializeOwned + Send + Sync, > NestedMapRead<'a, M, K, V> for NestedDataMap { type Iterator = NestedIter<'a, M, K, V>; @@ -597,7 +597,7 @@ pub struct NestedIter< 'a, M: 'a + Debug + PartialEq + Eq + Hash + Serialize + DeserializeOwned, K: 'a + Debug + PartialEq + Eq + Serialize + DeserializeOwned, - V: 'a + PartialEq + Eq + Serialize + DeserializeOwned, + V: 'a + Serialize + DeserializeOwned, > { db_iter: rocksdb::DBRawIterator<'a>, _phantom: PhantomData<(M, K, V)>, @@ -607,7 +607,7 @@ impl< 'a, M: 'a + Debug + PartialEq + Eq + Hash + Serialize + DeserializeOwned, K: 'a + Debug + PartialEq + Eq + Serialize + DeserializeOwned, - V: 'a + PartialEq + Eq + Serialize + DeserializeOwned, + V: 'a + Serialize + DeserializeOwned, > NestedIter<'a, M, K, V> { pub(super) fn new(db_iter: rocksdb::DBIterator<'a>) -> Self { @@ -619,7 +619,7 @@ impl< 'a, M: 'a + Clone + Debug + PartialEq + Eq + Hash + Serialize + DeserializeOwned, K: 'a + Clone + Debug + PartialEq + Eq + Serialize + DeserializeOwned, - V: 'a + Clone + PartialEq + Eq + Serialize + DeserializeOwned, + V: 'a + Clone + Serialize + DeserializeOwned, > Iterator for NestedIter<'a, M, K, V> { type Item = (Cow<'a, M>, Cow<'a, K>, Cow<'a, V>); @@ -724,18 +724,18 @@ impl< } /// An iterator over the values of a prefix. -pub struct NestedValues<'a, V: 'a + PartialEq + Eq + Serialize + DeserializeOwned> { +pub struct NestedValues<'a, V: 'a + Serialize + DeserializeOwned> { db_iter: rocksdb::DBRawIterator<'a>, _phantom: PhantomData, } -impl<'a, V: 'a + PartialEq + Eq + Serialize + DeserializeOwned> NestedValues<'a, V> { +impl<'a, V: 'a + Serialize + DeserializeOwned> NestedValues<'a, V> { pub(crate) fn new(db_iter: rocksdb::DBIterator<'a>) -> Self { Self { db_iter: db_iter.into(), _phantom: PhantomData } } } -impl<'a, V: 'a + Clone + PartialEq + Eq + Serialize + DeserializeOwned> Iterator for NestedValues<'a, V> { +impl<'a, V: 'a + Clone + Serialize + DeserializeOwned> Iterator for NestedValues<'a, V> { type Item = Cow<'a, V>; fn next(&mut self) -> Option { diff --git a/ledger/store/src/helpers/rocksdb/transaction.rs b/ledger/store/src/helpers/rocksdb/transaction.rs index 82373667f7..a3f87ca819 100644 --- a/ledger/store/src/helpers/rocksdb/transaction.rs +++ b/ledger/store/src/helpers/rocksdb/transaction.rs @@ -38,6 +38,7 @@ use crate::{ use console::{ prelude::*, program::{Identifier, ProgramID, ProgramOwner}, + types::U8, }; use synthesizer_program::Program; use synthesizer_snark::{Certificate, Proof, VerifyingKey}; @@ -102,6 +103,8 @@ impl TransactionStorage for TransactionDB { pub struct DeploymentDB { /// The ID map. id_map: DataMap>, + /// The ID edition map. + id_edition_map: DataMap, /// The edition map. edition_map: DataMap, u16>, /// The reverse ID map. @@ -110,6 +113,8 @@ pub struct DeploymentDB { owner_map: DataMap<(ProgramID, u16), ProgramOwner>, /// The program map. program_map: DataMap<(ProgramID, u16), Program>, + /// The checksum map. + checksum_map: DataMap<(ProgramID, u16), [U8; 32]>, /// The verifying key map. verifying_key_map: DataMap<(ProgramID, Identifier, u16), VerifyingKey>, /// The certificate map. @@ -121,10 +126,12 @@ pub struct DeploymentDB { #[rustfmt::skip] impl DeploymentStorage for DeploymentDB { type IDMap = DataMap>; + type IDEditionMap = DataMap; type EditionMap = DataMap, u16>; type ReverseIDMap = DataMap<(ProgramID, u16), N::TransactionID>; type OwnerMap = DataMap<(ProgramID, u16), ProgramOwner>; type ProgramMap = DataMap<(ProgramID, u16), Program>; + type ChecksumMap = DataMap<(ProgramID, u16), [U8; 32]>; type VerifyingKeyMap = DataMap<(ProgramID, Identifier, u16), VerifyingKey>; type CertificateMap = DataMap<(ProgramID, Identifier, u16), Certificate>; type FeeStorage = FeeDB; @@ -135,10 +142,12 @@ impl DeploymentStorage for DeploymentDB { let storage_mode = fee_store.storage_mode(); Ok(Self { id_map: rocksdb::RocksDB::open_map(N::ID, storage_mode.clone(), MapID::Deployment(DeploymentMap::ID))?, + id_edition_map: rocksdb::RocksDB::open_map(N::ID, storage_mode.clone(), MapID::Deployment(DeploymentMap::IDEdition))?, edition_map: rocksdb::RocksDB::open_map(N::ID, storage_mode.clone(), MapID::Deployment(DeploymentMap::Edition))?, reverse_id_map: rocksdb::RocksDB::open_map(N::ID, storage_mode.clone(), MapID::Deployment(DeploymentMap::ReverseID))?, owner_map: rocksdb::RocksDB::open_map(N::ID, storage_mode.clone(), MapID::Deployment(DeploymentMap::Owner))?, program_map: rocksdb::RocksDB::open_map(N::ID, storage_mode.clone(), MapID::Deployment(DeploymentMap::Program))?, + checksum_map: rocksdb::RocksDB::open_map(N::ID, storage_mode.clone(), MapID::Deployment(DeploymentMap::Checksum))?, verifying_key_map: rocksdb::RocksDB::open_map(N::ID, storage_mode.clone(), MapID::Deployment(DeploymentMap::VerifyingKey))?, certificate_map: rocksdb::RocksDB::open_map(N::ID, storage_mode.clone(), MapID::Deployment(DeploymentMap::Certificate))?, fee_store, @@ -150,6 +159,11 @@ impl DeploymentStorage for DeploymentDB { &self.id_map } + /// Returns the ID edition map. + fn id_edition_map(&self) -> &Self::IDEditionMap { + &self.id_edition_map + } + /// Returns the edition map. fn edition_map(&self) -> &Self::EditionMap { &self.edition_map @@ -170,6 +184,11 @@ impl DeploymentStorage for DeploymentDB { &self.program_map } + /// Returns the checksum map. + fn checksum_map(&self) -> &Self::ChecksumMap { + &self.checksum_map + } + /// Returns the verifying key map. fn verifying_key_map(&self) -> &Self::VerifyingKeyMap { &self.verifying_key_map diff --git a/ledger/store/src/helpers/traits/map.rs b/ledger/store/src/helpers/traits/map.rs index 91fb3b2efb..7b5334c98e 100644 --- a/ledger/store/src/helpers/traits/map.rs +++ b/ledger/store/src/helpers/traits/map.rs @@ -22,7 +22,7 @@ use std::borrow::Cow; pub trait Map< 'a, K: 'a + Copy + Clone + PartialEq + Eq + Hash + Serialize + Deserialize<'a> + Send + Sync, - V: 'a + Clone + PartialEq + Eq + Serialize + Deserialize<'a> + Send + Sync, + V: 'a + Clone + Serialize + Deserialize<'a> + Send + Sync, >: Clone + MapRead<'a, K, V> + Send + Sync { /// @@ -93,7 +93,7 @@ pub trait Map< pub trait MapRead< 'a, K: 'a + Copy + Clone + PartialEq + Eq + Hash + Serialize + Deserialize<'a> + Sync, - V: 'a + Clone + PartialEq + Eq + Serialize + Deserialize<'a> + Sync, + V: 'a + Clone + Serialize + Deserialize<'a> + Sync, > { type PendingIterator: Iterator, Option>)>; diff --git a/ledger/store/src/helpers/traits/nested_map.rs b/ledger/store/src/helpers/traits/nested_map.rs index 0b3153e469..4019bbeed8 100644 --- a/ledger/store/src/helpers/traits/nested_map.rs +++ b/ledger/store/src/helpers/traits/nested_map.rs @@ -23,7 +23,7 @@ pub trait NestedMap< 'a, M: 'a + Copy + Clone + PartialEq + Eq + Hash + Serialize + Deserialize<'a> + Send + Sync, K: 'a + Clone + PartialEq + Eq + Serialize + Deserialize<'a> + Send + Sync, - V: 'a + Clone + PartialEq + Eq + Serialize + Deserialize<'a> + Send + Sync, + V: 'a + Clone + Serialize + Deserialize<'a> + Send + Sync, >: Clone + NestedMapRead<'a, M, K, V> + Send + Sync { /// @@ -87,7 +87,7 @@ pub trait NestedMapRead< 'a, M: 'a + Copy + Clone + PartialEq + Eq + Hash + Serialize + Deserialize<'a> + Sync, K: 'a + Clone + PartialEq + Eq + Serialize + Deserialize<'a> + Sync, - V: 'a + Clone + PartialEq + Eq + Serialize + Deserialize<'a> + Sync, + V: 'a + Clone + Serialize + Deserialize<'a> + Sync, > { type PendingIterator: Iterator, Option>, Option>)>; diff --git a/ledger/store/src/transaction/deployment.rs b/ledger/store/src/transaction/deployment.rs index 2d506684af..27e0c9c91b 100644 --- a/ledger/store/src/transaction/deployment.rs +++ b/ledger/store/src/transaction/deployment.rs @@ -24,6 +24,7 @@ use crate::{ use console::{ network::prelude::*, program::{Identifier, ProgramID, ProgramOwner}, + types::U8, }; use ledger_block::{Deployment, Fee, Transaction}; use synthesizer_program::Program; @@ -38,7 +39,9 @@ use std::borrow::Cow; pub trait DeploymentStorage: Clone + Send + Sync { /// The mapping of `transaction ID` to `program ID`. type IDMap: for<'a> Map<'a, N::TransactionID, ProgramID>; - /// The mapping of `program ID` to `edition`. + /// The mapping of `transaction ID` to `edition`. + type IDEditionMap: for<'a> Map<'a, N::TransactionID, u16>; + /// The mapping of `program ID` to the **latest** `edition`. type EditionMap: for<'a> Map<'a, ProgramID, u16>; /// The mapping of `(program ID, edition)` to `transaction ID`. type ReverseIDMap: for<'a> Map<'a, (ProgramID, u16), N::TransactionID>; @@ -46,6 +49,8 @@ pub trait DeploymentStorage: Clone + Send + Sync { type OwnerMap: for<'a> Map<'a, (ProgramID, u16), ProgramOwner>; /// The mapping of `(program ID, edition)` to `program`. type ProgramMap: for<'a> Map<'a, (ProgramID, u16), Program>; + /// The mapping of `(program ID, edition)` to `checksum`. + type ChecksumMap: for<'a> Map<'a, (ProgramID, u16), [U8; 32]>; /// The mapping of `(program ID, function name, edition)` to `verifying key`. type VerifyingKeyMap: for<'a> Map<'a, (ProgramID, Identifier, u16), VerifyingKey>; /// The mapping of `(program ID, function name, edition)` to `certificate`. @@ -58,6 +63,8 @@ pub trait DeploymentStorage: Clone + Send + Sync { /// Returns the ID map. fn id_map(&self) -> &Self::IDMap; + /// Returns the ID edition map. + fn id_edition_map(&self) -> &Self::IDEditionMap; /// Returns the edition map. fn edition_map(&self) -> &Self::EditionMap; /// Returns the reverse ID map. @@ -66,6 +73,8 @@ pub trait DeploymentStorage: Clone + Send + Sync { fn owner_map(&self) -> &Self::OwnerMap; /// Returns the program map. fn program_map(&self) -> &Self::ProgramMap; + /// Returns the checksum map. + fn checksum_map(&self) -> &Self::ChecksumMap; /// Returns the verifying key map. fn verifying_key_map(&self) -> &Self::VerifyingKeyMap; /// Returns the certificate map. @@ -81,10 +90,12 @@ pub trait DeploymentStorage: Clone + Send + Sync { /// Starts an atomic batch write operation. fn start_atomic(&self) { self.id_map().start_atomic(); + self.id_edition_map().start_atomic(); self.edition_map().start_atomic(); self.reverse_id_map().start_atomic(); self.owner_map().start_atomic(); self.program_map().start_atomic(); + self.checksum_map().start_atomic(); self.verifying_key_map().start_atomic(); self.certificate_map().start_atomic(); self.fee_store().start_atomic(); @@ -93,10 +104,12 @@ pub trait DeploymentStorage: Clone + Send + Sync { /// Checks if an atomic batch is in progress. fn is_atomic_in_progress(&self) -> bool { self.id_map().is_atomic_in_progress() + || self.id_edition_map().is_atomic_in_progress() || self.edition_map().is_atomic_in_progress() || self.reverse_id_map().is_atomic_in_progress() || self.owner_map().is_atomic_in_progress() || self.program_map().is_atomic_in_progress() + || self.checksum_map().is_atomic_in_progress() || self.verifying_key_map().is_atomic_in_progress() || self.certificate_map().is_atomic_in_progress() || self.fee_store().is_atomic_in_progress() @@ -105,10 +118,12 @@ pub trait DeploymentStorage: Clone + Send + Sync { /// Checkpoints the atomic batch. fn atomic_checkpoint(&self) { self.id_map().atomic_checkpoint(); + self.id_edition_map().atomic_checkpoint(); self.edition_map().atomic_checkpoint(); self.reverse_id_map().atomic_checkpoint(); self.owner_map().atomic_checkpoint(); self.program_map().atomic_checkpoint(); + self.checksum_map().atomic_checkpoint(); self.verifying_key_map().atomic_checkpoint(); self.certificate_map().atomic_checkpoint(); self.fee_store().atomic_checkpoint(); @@ -117,10 +132,12 @@ pub trait DeploymentStorage: Clone + Send + Sync { /// Clears the latest atomic batch checkpoint. fn clear_latest_checkpoint(&self) { self.id_map().clear_latest_checkpoint(); + self.id_edition_map().clear_latest_checkpoint(); self.edition_map().clear_latest_checkpoint(); self.reverse_id_map().clear_latest_checkpoint(); self.owner_map().clear_latest_checkpoint(); self.program_map().clear_latest_checkpoint(); + self.checksum_map().clear_latest_checkpoint(); self.verifying_key_map().clear_latest_checkpoint(); self.certificate_map().clear_latest_checkpoint(); self.fee_store().clear_latest_checkpoint(); @@ -129,10 +146,12 @@ pub trait DeploymentStorage: Clone + Send + Sync { /// Rewinds the atomic batch to the previous checkpoint. fn atomic_rewind(&self) { self.id_map().atomic_rewind(); + self.id_edition_map().atomic_rewind(); self.edition_map().atomic_rewind(); self.reverse_id_map().atomic_rewind(); self.owner_map().atomic_rewind(); self.program_map().atomic_rewind(); + self.checksum_map().atomic_rewind(); self.verifying_key_map().atomic_rewind(); self.certificate_map().atomic_rewind(); self.fee_store().atomic_rewind(); @@ -141,10 +160,12 @@ pub trait DeploymentStorage: Clone + Send + Sync { /// Aborts an atomic batch write operation. fn abort_atomic(&self) { self.id_map().abort_atomic(); + self.id_edition_map().abort_atomic(); self.edition_map().abort_atomic(); self.reverse_id_map().abort_atomic(); self.owner_map().abort_atomic(); self.program_map().abort_atomic(); + self.checksum_map().abort_atomic(); self.verifying_key_map().abort_atomic(); self.certificate_map().abort_atomic(); self.fee_store().abort_atomic(); @@ -153,10 +174,12 @@ pub trait DeploymentStorage: Clone + Send + Sync { /// Finishes an atomic batch write operation. fn finish_atomic(&self) -> Result<()> { self.id_map().finish_atomic()?; + self.id_edition_map().finish_atomic()?; self.edition_map().finish_atomic()?; self.reverse_id_map().finish_atomic()?; self.owner_map().finish_atomic()?; self.program_map().finish_atomic()?; + self.checksum_map().finish_atomic()?; self.verifying_key_map().finish_atomic()?; self.certificate_map().finish_atomic()?; self.fee_store().finish_atomic() @@ -177,16 +200,19 @@ pub trait DeploymentStorage: Clone + Send + Sync { } // Retrieve the edition. + // Note: The VM enforces that the edition is always 0 for the first deployment of a program and that subsequent upgrades increment the edition. let edition = deployment.edition(); // Retrieve the program. let program = deployment.program(); // Retrieve the program ID. let program_id = *program.id(); + // Retrieve the checksum. + let checksum = deployment.program_checksum(); atomic_batch_scope!(self, { // Store the program ID. self.id_map().insert(*transaction_id, program_id)?; - // Store the edition. + // Store the latest edition for the program ID. self.edition_map().insert(program_id, edition)?; // Store the reverse program ID. @@ -195,6 +221,13 @@ pub trait DeploymentStorage: Clone + Send + Sync { self.owner_map().insert((program_id, edition), *owner)?; // Store the program. self.program_map().insert((program_id, edition), program.clone())?; + // If the checksum exists, then store it and also store the edition into the `IDEditionMap`. + // This is done because the existence of the checksum implies a migration at the V8 consensus height. + // This migration enables program upgrades. + if let Some(checksum) = checksum { + self.id_edition_map().insert(*transaction_id, edition)?; + self.checksum_map().insert((program_id, edition), *checksum)?; + } // Store the verifying keys and certificates. for (function_name, (verifying_key, certificate)) in deployment.verifying_keys() { @@ -213,16 +246,29 @@ pub trait DeploymentStorage: Clone + Send + Sync { /// Removes the deployment transaction for the given `transaction ID`. fn remove(&self, transaction_id: &N::TransactionID) -> Result<()> { - // Retrieve the program ID. - let program_id = match self.get_program_id(transaction_id)? { - Some(edition) => edition, - None => bail!("Failed to get the program ID for transaction '{transaction_id}'"), + // Retrieve the program ID for the transaction ID. + let Some(program_id) = self.get_program_id(transaction_id)? else { + bail!("Failed to get the program ID for transaction '{transaction_id}'"); }; - // Retrieve the edition. - let edition = match self.get_edition(&program_id)? { - Some(edition) => edition, - None => bail!("Failed to locate the edition for program '{program_id}'"), + // Retrieve the edition for the transaction ID. + let Some(edition) = self.get_edition_for_transaction(transaction_id)? else { + bail!("Failed to locate the edition for transaction '{transaction_id}'"); + }; + // Retrieve the latest edition for the program ID. + let Some(latest_edition) = self.get_latest_edition_for_program(&program_id)? else { + bail!("Failed to locate the latest edition for program '{program_id}'"); }; + // Verify that the removed edition is latest edition. + // Note: This is condition should always hold true because: + // - The VM enforces that exactly one deployment or upgrade is allowed per program per block. + // - The only time a transaction is removed is when `remove_last_n` is invoked. + // - `remove_last_n` is only invoked when finalization for the latest block fails. + // - `remove_last_n` is only invoked with the parameter `1`. + // If any of these conditions are changed, then this check is no longer valid. + ensure!( + edition == latest_edition, + "Failed to remove the deployment for transaction '{transaction_id}' because it is not the latest edition" + ); // Retrieve the program. let program = match self.program_map().get_confirmed(&(program_id, edition))? { Some(program) => cow_to_cloned!(program), @@ -232,8 +278,31 @@ pub trait DeploymentStorage: Clone + Send + Sync { atomic_batch_scope!(self, { // Remove the program ID. self.id_map().remove(transaction_id)?; - // Remove the edition. - self.edition_map().remove(&program_id)?; + // Remove the edition for the transaction ID. + self.id_edition_map().remove(transaction_id)?; + // Update the latest edition. + match edition.is_zero() { + // If the removed edition is 0, then remove the program ID from the latest edition map. + true => self.edition_map().remove(&program_id)?, + // Otherwise, decrement the edition. + false => self.edition_map().insert(program_id, edition.saturating_sub(1))?, + } + match (edition, latest_edition) { + // If the removed and latest edition are 0, remove the program ID from the latest edition map. + (0, 0) => self.edition_map().remove(&program_id)?, + // If the removed edition is the latest one, update the latest edition map by decrementing the edition. + // Note: It is safe to remove in this manner instead of walking backwards through the editions for the + // following reasons: + // - The VM enforces that exactly one deployment or upgrade is allowed per program per block. + // - The only time a transaction is removed is when `remove_last_n` is invoked when finalization fails. + // - `remove_last_n` is only invoked with the parameter `1`. + // If any of these conditions are changed, then this method is no longer safe. + (edition, latest_edition) if edition == latest_edition => { + self.edition_map().insert(program_id, edition.saturating_sub(1))? + } + // Otherwise, do nothing. + _ => {} + } // Remove the reverse program ID. self.reverse_id_map().remove(&(program_id, edition))?; @@ -241,6 +310,8 @@ pub trait DeploymentStorage: Clone + Send + Sync { self.owner_map().remove(&(program_id, edition))?; // Remove the program. self.program_map().remove(&(program_id, edition))?; + // Remove the checksum. + self.checksum_map().remove(&(program_id, edition))?; // Remove the verifying keys and certificates. for function_name in program.functions().keys() { @@ -257,8 +328,11 @@ pub trait DeploymentStorage: Clone + Send + Sync { }) } - /// Returns the transaction ID that contains the given `program ID`. - fn find_transaction_id_from_program_id(&self, program_id: &ProgramID) -> Result> { + /// Returns the latest transaction ID that contains the given `program ID`. + fn find_latest_transaction_id_from_program_id( + &self, + program_id: &ProgramID, + ) -> Result> { // Check if the program ID is for 'credits.aleo'. // This case is handled separately, as it is a default program of the VM. // TODO (howardwu): After we update 'fee' rules and 'Ratify' in genesis, we can remove this. @@ -266,8 +340,8 @@ pub trait DeploymentStorage: Clone + Send + Sync { return Ok(None); } - // Retrieve the edition. - let edition = match self.get_edition(program_id)? { + // Retrieve the latest edition. + let edition = match self.get_latest_edition_for_program(program_id)? { Some(edition) => edition, None => return Ok(None), }; @@ -278,6 +352,25 @@ pub trait DeploymentStorage: Clone + Send + Sync { } } + /// Returns the transaction ID that contains the given `program ID` and `edition`. + fn find_transaction_id_from_program_id_and_edition( + &self, + program_id: &ProgramID, + edition: u16, + ) -> Result> { + // Check if the program ID is for 'credits.aleo'. + // This case is handled separately, as it is a default program of the VM. + // TODO (howardwu): After we update 'fee' rules and 'Ratify' in genesis, we can remove this. + if program_id == &ProgramID::from_str("credits.aleo")? { + return Ok(None); + } + // Retrieve the transaction ID. + match self.reverse_id_map().get_confirmed(&(*program_id, edition))? { + Some(transaction_id) => Ok(Some(cow_to_copied!(transaction_id))), + None => Ok(None), + } + } + /// Returns the transaction ID that contains the given `transition ID`. fn find_transaction_id_from_transition_id( &self, @@ -295,8 +388,8 @@ pub trait DeploymentStorage: Clone + Send + Sync { } } - /// Returns the edition for the given `program ID`. - fn get_edition(&self, program_id: &ProgramID) -> Result> { + /// Returns the latest edition for the given `program ID`. + fn get_latest_edition_for_program(&self, program_id: &ProgramID) -> Result> { // Check if the program ID is for 'credits.aleo'. // This case is handled separately, as it is a default program of the VM. // TODO (howardwu): After we update 'fee' rules and 'Ratify' in genesis, we can remove this. @@ -310,8 +403,34 @@ pub trait DeploymentStorage: Clone + Send + Sync { } } - /// Returns the program for the given `program ID`. - fn get_program(&self, program_id: &ProgramID) -> Result>> { + /// Returns the edition for the given `transaction ID`. + fn get_edition_for_transaction(&self, transaction_id: &N::TransactionID) -> Result> { + // Retrieve the edition. + match self.id_edition_map().get_confirmed(transaction_id)? { + Some(edition) => Ok(Some(cow_to_copied!(edition))), + None => { + // Get the program ID associated with the transaction ID. + let program_id = match self.get_program_id(transaction_id)? { + Some(program_id) => program_id, + None => return Ok(None), + }; + // Get the latest edition for the program ID. + let latest_edition = match self.get_latest_edition_for_program(&program_id)? { + Some(edition) => edition, + None => return Ok(None), + }; + // Verify that the latest edition is zero. + // Prior to `ConsensusVersion::V8`, this must be the case because if a program is not in the `IDEditionMap` but exists, + // then it must have been deployed before program upgrades were introduced. + ensure!(latest_edition == 0, "Failed to get the edition for transaction '{transaction_id}'"); + // Return the edition. + Ok(Some(0)) + } + } + } + + /// Returns the latest program for the given `program ID`. + fn get_latest_program(&self, program_id: &ProgramID) -> Result>> { // Check if the program ID is for 'credits.aleo'. // This case is handled separately, as it is a default program of the VM. // TODO (howardwu): After we update 'fee' rules and 'Ratify' in genesis, we can remove this. @@ -319,8 +438,8 @@ pub trait DeploymentStorage: Clone + Send + Sync { return Ok(Some(Program::credits()?)); } - // Retrieve the edition. - let edition = match self.get_edition(program_id)? { + // Retrieve the latest edition. + let edition = match self.get_latest_edition_for_program(program_id)? { Some(edition) => edition, None => return Ok(None), }; @@ -331,8 +450,24 @@ pub trait DeploymentStorage: Clone + Send + Sync { } } - /// Returns the verifying key for the given `program ID` and `function name`. - fn get_verifying_key( + /// Returns the program for the given `program ID` and `edition`. + fn get_program_for_edition(&self, program_id: &ProgramID, edition: u16) -> Result>> { + // Check if the program ID is for 'credits.aleo'. + // This case is handled separately, as it is a default program of the VM. + // TODO (howardwu): After we update 'fee' rules and 'Ratify' in genesis, we can remove this. + if program_id == &ProgramID::from_str("credits.aleo")? { + return Ok(Some(Program::credits()?)); + } + + // Retrieve the program. + match self.program_map().get_confirmed(&(*program_id, edition))? { + Some(program) => Ok(Some(cow_to_cloned!(program))), + None => bail!("Failed to get program '{program_id}' (edition {edition})"), + } + } + + /// Returns the latest verifying key for the given `program ID` and `function name`. + fn get_latest_verifying_key( &self, program_id: &ProgramID, function_name: &Identifier, @@ -351,8 +486,8 @@ pub trait DeploymentStorage: Clone + Send + Sync { return Ok(Some(VerifyingKey::new(verifying_key.clone(), num_variables))); } - // Retrieve the edition. - let edition = match self.get_edition(program_id)? { + // Retrieve the latest edition. + let edition = match self.get_latest_edition_for_program(program_id)? { Some(edition) => edition, None => return Ok(None), }; @@ -363,8 +498,36 @@ pub trait DeploymentStorage: Clone + Send + Sync { } } - /// Returns the certificate for the given `program ID` and `function name`. - fn get_certificate( + /// Returns the verifying key for the given `program ID`, `function name` and `edition`. + fn get_verifying_key_with_edition( + &self, + program_id: &ProgramID, + function_name: &Identifier, + edition: u16, + ) -> Result>> { + // Check if the program ID is for 'credits.aleo'. + // This case is handled separately, as it is a default program of the VM. + // TODO (howardwu): After we update 'fee' rules and 'Ratify' in genesis, we can remove this. + if program_id == &ProgramID::from_str("credits.aleo")? { + // Load the verifying key. + let verifying_key = N::get_credits_verifying_key(function_name.to_string())?; + // Retrieve the number of public and private variables. + // Note: This number does *NOT* include the number of constants. This is safe because + // this program is never deployed, as it is a first-class citizen of the protocol. + let num_variables = verifying_key.circuit_info.num_public_and_private_variables as u64; + // Return the verifying key. + return Ok(Some(VerifyingKey::new(verifying_key.clone(), num_variables))); + } + + // Retrieve the verifying key. + match self.verifying_key_map().get_confirmed(&(*program_id, *function_name, edition))? { + Some(verifying_key) => Ok(Some(cow_to_cloned!(verifying_key))), + None => bail!("Failed to get the verifying key for '{program_id}/{function_name}' (edition {edition})"), + } + } + + /// Returns the latest certificate for the given `program ID` and `function name`. + fn get_latest_certificate( &self, program_id: &ProgramID, function_name: &Identifier, @@ -376,10 +539,9 @@ pub trait DeploymentStorage: Clone + Send + Sync { return Ok(None); } - // Retrieve the edition. - let edition = match self.get_edition(program_id)? { - Some(edition) => edition, - None => return Ok(None), + // Retrieve the latest edition. + let Some(edition) = self.get_latest_edition_for_program(program_id)? else { + return Ok(None); }; // Retrieve the certificate. match self.certificate_map().get_confirmed(&(*program_id, *function_name, edition))? { @@ -388,6 +550,27 @@ pub trait DeploymentStorage: Clone + Send + Sync { } } + /// Returns the certificate for the given `program ID`, `function name`, and `edition`. + fn get_certificate_with_edition( + &self, + program_id: &ProgramID, + function_name: &Identifier, + edition: u16, + ) -> Result>> { + // Check if the program ID is for 'credits.aleo'. + // This case is handled separately, as it is a default program of the VM. + // TODO (howardwu): After we update 'fee' rules and 'Ratify' in genesis, we can remove this. + if program_id == &ProgramID::from_str("credits.aleo")? { + return Ok(None); + } + + // Retrieve the certificate. + match self.certificate_map().get_confirmed(&(*program_id, *function_name, edition))? { + Some(certificate) => Ok(Some(cow_to_cloned!(certificate))), + None => bail!("Failed to get the certificate for '{program_id}/{function_name}' (edition {edition})"), + } + } + /// Returns the deployment for the given `transaction ID`. fn get_deployment(&self, transaction_id: &N::TransactionID) -> Result>> { // Retrieve the program ID. @@ -396,7 +579,7 @@ pub trait DeploymentStorage: Clone + Send + Sync { None => return Ok(None), }; // Retrieve the edition. - let edition = match self.get_edition(&program_id)? { + let edition = match self.get_edition_for_transaction(transaction_id)? { Some(edition) => edition, None => bail!("Failed to get the edition for program '{program_id}'"), }; @@ -405,6 +588,9 @@ pub trait DeploymentStorage: Clone + Send + Sync { Some(program) => cow_to_cloned!(program), None => bail!("Failed to get the deployed program '{program_id}' (edition {edition})"), }; + // Retrieve the checksum. + let program_checksum = + self.checksum_map().get_confirmed(&(program_id, edition))?.map(|checksum| cow_to_copied!(checksum)); // Initialize a vector for the verifying keys and certificates. let mut verifying_keys = Vec::with_capacity(program.functions().len()); @@ -426,7 +612,7 @@ pub trait DeploymentStorage: Clone + Send + Sync { } // Return the deployment. - Ok(Some(Deployment::new(edition, program, verifying_keys)?)) + Ok(Some(Deployment::new(edition, program, verifying_keys, program_checksum)?)) } /// Returns the fee for the given `transaction ID`. @@ -434,8 +620,8 @@ pub trait DeploymentStorage: Clone + Send + Sync { self.fee_store().get_fee(transaction_id) } - /// Returns the owner for the given `program ID`. - fn get_owner(&self, program_id: &ProgramID) -> Result>> { + /// Returns the latest owner for the given `program ID`. + fn get_latest_owner(&self, program_id: &ProgramID) -> Result>> { // Check if the program ID is for 'credits.aleo'. // This case is handled separately, as it is a default program of the VM. // TODO (howardwu): After we update 'fee' rules and 'Ratify' in genesis, we can remove this. @@ -444,8 +630,8 @@ pub trait DeploymentStorage: Clone + Send + Sync { } // TODO (raychu86): Consider program upgrades and edition changes. - // Retrieve the edition. - let edition = match self.get_edition(program_id)? { + // Retrieve the latest edition. + let edition = match self.get_latest_edition_for_program(program_id)? { Some(edition) => edition, None => return Ok(None), }; @@ -457,6 +643,22 @@ pub trait DeploymentStorage: Clone + Send + Sync { } } + /// Returns the owner for the given `program ID` and `edition`. + fn get_owner_with_edition(&self, program_id: &ProgramID, edition: u16) -> Result>> { + // Check if the program ID is for 'credits.aleo'. + // This case is handled separately, as it is a default program of the VM. + // TODO (howardwu): After we update 'fee' rules and 'Ratify' in genesis, we can remove this. + if program_id == &ProgramID::from_str("credits.aleo")? { + return Ok(None); + } + + // Retrieve the owner. + match self.owner_map().get_confirmed(&(*program_id, edition))? { + Some(owner) => Ok(Some(cow_to_copied!(owner))), + None => bail!("Failed to find the Owner for program '{program_id}' (edition {edition})"), + } + } + /// Returns the transaction for the given `transaction ID`. fn get_transaction(&self, transaction_id: &N::TransactionID) -> Result>> { // Retrieve the deployment. @@ -471,7 +673,7 @@ pub trait DeploymentStorage: Clone + Send + Sync { }; // Retrieve the owner. - let owner = match self.get_owner(deployment.program_id())? { + let owner = match self.get_latest_owner(deployment.program_id())? { Some(owner) => owner, None => bail!("Failed to get the owner for transaction '{transaction_id}'"), }; @@ -571,9 +773,14 @@ impl> DeploymentStore { self.storage.get_deployment(transaction_id) } - /// Returns the edition for the given `program ID`. - pub fn get_edition(&self, program_id: &ProgramID) -> Result> { - self.storage.get_edition(program_id) + /// Returns the latest edition for the given `program ID`. + pub fn get_latest_edition_for_program(&self, program_id: &ProgramID) -> Result> { + self.storage.get_latest_edition_for_program(program_id) + } + + /// Returns the edition for the given `transaction ID`. + pub fn get_edition_for_transaction(&self, transaction_id: &N::TransactionID) -> Result> { + self.storage.get_edition_for_transaction(transaction_id) } /// Returns the program ID for the given `transaction ID`. @@ -581,27 +788,52 @@ impl> DeploymentStore { self.storage.get_program_id(transaction_id) } - /// Returns the program for the given `program ID`. - pub fn get_program(&self, program_id: &ProgramID) -> Result>> { - self.storage.get_program(program_id) + /// Returns the latest program for the given `program ID`. + pub fn get_latest_program(&self, program_id: &ProgramID) -> Result>> { + self.storage.get_latest_program(program_id) + } + + /// Returns the program for the given `program ID` and `edition`. + pub fn get_program_for_edition(&self, program_id: &ProgramID, edition: u16) -> Result>> { + self.storage.get_program_for_edition(program_id, edition) + } + + /// Returns the latest verifying key for the given `(program ID, function name)`. + pub fn get_latest_verifying_key( + &self, + program_id: &ProgramID, + function_name: &Identifier, + ) -> Result>> { + self.storage.get_latest_verifying_key(program_id, function_name) } - /// Returns the verifying key for the given `(program ID, function name)`. - pub fn get_verifying_key( + /// Returns the verifying key for the given `(program ID, function name, edition)`. + pub fn get_verifying_key_with_edition( &self, program_id: &ProgramID, function_name: &Identifier, + edition: u16, ) -> Result>> { - self.storage.get_verifying_key(program_id, function_name) + self.storage.get_verifying_key_with_edition(program_id, function_name, edition) } - /// Returns the certificate for the given `(program ID, function name)`. - pub fn get_certificate( + /// Returns the latest certificate for the given `(program ID, function name)`. + pub fn get_latest_certificate( &self, program_id: &ProgramID, function_name: &Identifier, ) -> Result>> { - self.storage.get_certificate(program_id, function_name) + self.storage.get_latest_certificate(program_id, function_name) + } + + /// Returns the certificate for the given `(program ID, function name, edition)`. + pub fn get_certificate_with_edition( + &self, + program_id: &ProgramID, + function_name: &Identifier, + edition: u16, + ) -> Result>> { + self.storage.get_certificate_with_edition(program_id, function_name, edition) } /// Returns the fee for the given `transaction ID`. @@ -611,9 +843,21 @@ impl> DeploymentStore { } impl> DeploymentStore { - /// Returns the transaction ID that deployed the given `program ID`. - pub fn find_transaction_id_from_program_id(&self, program_id: &ProgramID) -> Result> { - self.storage.find_transaction_id_from_program_id(program_id) + /// Returns the latest transaction ID that deployed or upgraded the given `program ID`. + pub fn find_latest_transaction_id_from_program_id( + &self, + program_id: &ProgramID, + ) -> Result> { + self.storage.find_latest_transaction_id_from_program_id(program_id) + } + + /// Returns the transaction `ID` that deployed the given `program ID` and `edition`. + pub fn find_transaction_id_from_program_id_and_edition( + &self, + program_id: &ProgramID, + edition: u16, + ) -> Result> { + self.storage.find_transaction_id_from_program_id_and_edition(program_id, edition) } /// Returns the transaction ID that deployed the given `transition ID`. @@ -630,8 +874,14 @@ impl> DeploymentStore { pub fn contains_program_id(&self, program_id: &ProgramID) -> Result { self.storage.edition_map().contains_key_confirmed(program_id) } + + /// Returns `true` if the given program ID and edition exists. + pub fn contains_program_id_and_edition(&self, program_id: &ProgramID, edition: u16) -> Result { + self.storage.reverse_id_map().contains_key_confirmed(&(*program_id, edition)) + } } +type ProgramIDEdition = (ProgramID, u16); type ProgramTriplet = (ProgramID, Identifier, u16); impl> DeploymentStore { @@ -641,6 +891,7 @@ impl> DeploymentStore { } /// Returns an iterator over the program IDs, for all deployments. + /// Note: If a program upgraded, this method will return duplicates of the program ID. pub fn program_ids(&self) -> impl '_ + Iterator>> { self.storage.id_map().values_confirmed().map(|id| match id { Cow::Borrowed(id) => Cow::Borrowed(id), @@ -648,7 +899,13 @@ impl> DeploymentStore { }) } + /// Returns an iterator over the program IDs and latest editions. + pub fn program_ids_and_latest_editions(&self) -> impl '_ + Iterator>, Cow<'_, u16>)> { + self.storage.edition_map().iter_confirmed() + } + /// Returns an iterator over the programs, for all deployments. + /// If a program has been upgraded, all instances of the program will be returned. pub fn programs(&self) -> impl '_ + Iterator>> { self.storage.program_map().values_confirmed().map(|program| match program { Cow::Borrowed(program) => Cow::Borrowed(program), @@ -656,6 +913,13 @@ impl> DeploymentStore { }) } + /// Returns an iterator over the programs and editions, for all deployments. + pub fn programs_with_editions( + &self, + ) -> impl '_ + Iterator>, Cow<'_, Program>)> { + self.storage.program_map().iter_confirmed() + } + /// Returns an iterator over the `((program ID, function name, edition), verifying key)`, for all deployments. pub fn verifying_keys(&self) -> impl '_ + Iterator>, Cow<'_, VerifyingKey>)> { self.storage.verifying_key_map().iter_confirmed() @@ -677,12 +941,16 @@ mod tests { let rng = &mut TestRng::default(); // Sample the transactions. - let transaction_0 = ledger_test_helpers::sample_deployment_transaction(true, rng); - let transaction_1 = ledger_test_helpers::sample_deployment_transaction(false, rng); - let transactions = vec![transaction_0, transaction_1]; + let transaction_0 = ledger_test_helpers::sample_deployment_transaction(1, true, rng); + let transaction_1 = ledger_test_helpers::sample_deployment_transaction(1, false, rng); + let transaction_2 = ledger_test_helpers::sample_deployment_transaction(2, true, rng); + let transaction_3 = ledger_test_helpers::sample_deployment_transaction(2, false, rng); + let transactions = vec![transaction_0, transaction_1, transaction_2, transaction_3]; for transaction in transactions { let transaction_id = transaction.id(); + let program_id = *transaction.deployment().unwrap().program_id(); + let checksum = transaction.deployment().unwrap().program_checksum(); // Initialize a new transition store. let transition_store = TransitionStore::open(StorageMode::Test(None)).unwrap(); @@ -698,16 +966,47 @@ mod tests { // Insert the deployment transaction. deployment_store.insert(&transaction).unwrap(); + // If the deployment has a checksum, then verify that the checksum exists in the `ChecksumMap` and that the edition exists in the `IDEditionMap`. + // Otherwise, verify that the checksum does not exist and that the ID-edition map is empty. + match checksum { + Some(checksum) => { + let candidate = deployment_store.checksum_map().get_confirmed(&(program_id, 0)).unwrap(); + assert_eq!(Some(checksum), candidate.as_deref()); + let candidate = deployment_store.id_edition_map().get_confirmed(&transaction_id).unwrap(); + assert_eq!(Some(0), candidate.as_deref().copied()); + } + None => { + let candidate = deployment_store.checksum_map().get_confirmed(&(program_id, 0)).unwrap(); + assert_eq!(None, candidate); + let candidate = deployment_store.id_edition_map().get_confirmed(&transaction_id).unwrap(); + assert_eq!(None, candidate); + } + } + // Retrieve the deployment transaction. let candidate = deployment_store.get_transaction(&transaction_id).unwrap(); assert_eq!(Some(transaction), candidate); + // Retrieve the latest edition and verify that it is zero. + let edition = deployment_store.get_edition_for_transaction(&transaction_id).unwrap(); + assert_eq!(Some(0), edition); + // Remove the deployment. deployment_store.remove(&transaction_id).unwrap(); // Ensure the deployment transaction does not exist. let candidate = deployment_store.get_transaction(&transaction_id).unwrap(); assert_eq!(None, candidate); + + // Ensure the edition is not found. + let candidate = deployment_store.edition_map().get_confirmed(&program_id).unwrap(); + assert_eq!(None, candidate); + let candidate = deployment_store.id_edition_map().get_confirmed(&transaction_id).unwrap(); + assert_eq!(None, candidate); + + // Ensure the checksum is not found. + let candidate = deployment_store.checksum_map().get_confirmed(&(program_id, 0)).unwrap(); + assert_eq!(None, candidate); } } @@ -716,9 +1015,11 @@ mod tests { let rng = &mut TestRng::default(); // Sample the transactions. - let transaction_0 = ledger_test_helpers::sample_deployment_transaction(true, rng); - let transaction_1 = ledger_test_helpers::sample_deployment_transaction(false, rng); - let transactions = vec![transaction_0, transaction_1]; + let transaction_0 = ledger_test_helpers::sample_deployment_transaction(1, true, rng); + let transaction_1 = ledger_test_helpers::sample_deployment_transaction(1, false, rng); + let transaction_2 = ledger_test_helpers::sample_deployment_transaction(2, true, rng); + let transaction_3 = ledger_test_helpers::sample_deployment_transaction(2, false, rng); + let transactions = vec![transaction_0, transaction_1, transaction_2, transaction_3]; for transaction in transactions { let transaction_id = transaction.id(); @@ -739,21 +1040,21 @@ mod tests { assert_eq!(None, candidate); // Ensure the transaction ID is not found. - let candidate = deployment_store.find_transaction_id_from_program_id(&program_id).unwrap(); + let candidate = deployment_store.find_latest_transaction_id_from_program_id(&program_id).unwrap(); assert_eq!(None, candidate); // Insert the deployment. deployment_store.insert(&transaction).unwrap(); // Find the transaction ID. - let candidate = deployment_store.find_transaction_id_from_program_id(&program_id).unwrap(); + let candidate = deployment_store.find_latest_transaction_id_from_program_id(&program_id).unwrap(); assert_eq!(Some(transaction_id), candidate); // Remove the deployment. deployment_store.remove(&transaction_id).unwrap(); // Ensure the transaction ID is not found. - let candidate = deployment_store.find_transaction_id_from_program_id(&program_id).unwrap(); + let candidate = deployment_store.find_latest_transaction_id_from_program_id(&program_id).unwrap(); assert_eq!(None, candidate); } } diff --git a/ledger/store/src/transaction/mod.rs b/ledger/store/src/transaction/mod.rs index 2b075207dc..1c96b3c0d9 100644 --- a/ledger/store/src/transaction/mod.rs +++ b/ledger/store/src/transaction/mod.rs @@ -195,6 +195,23 @@ pub trait TransactionStorage: Clone + Send + Sync { }) } + /// Returns the latest transaction ID that contains the given `program ID`. + fn find_latest_transaction_id_from_program_id( + &self, + program_id: &ProgramID, + ) -> Result> { + self.deployment_store().find_latest_transaction_id_from_program_id(program_id) + } + + /// Returns the transaction ID that contains the given `program ID` and `edition`. + fn find_transaction_id_from_program_id_and_edition( + &self, + program_id: &ProgramID, + edition: u16, + ) -> Result> { + self.deployment_store().find_transaction_id_from_program_id_and_edition(program_id, edition) + } + /// Returns the transaction ID that contains the given `transition ID`. fn find_transaction_id_from_transition_id( &self, @@ -203,11 +220,6 @@ pub trait TransactionStorage: Clone + Send + Sync { self.execution_store().find_transaction_id_from_transition_id(transition_id) } - /// Returns the transaction ID that contains the given `program ID`. - fn find_transaction_id_from_program_id(&self, program_id: &ProgramID) -> Result> { - self.deployment_store().find_transaction_id_from_program_id(program_id) - } - /// Returns the transaction for the given `transaction ID`. fn get_transaction(&self, transaction_id: &N::TransactionID) -> Result>> { // Retrieve the transaction type. @@ -356,6 +368,11 @@ impl> TransactionStore { } } + /// Returns the latest edition for the given `program ID`. + pub fn get_latest_edition_for_program(&self, program_id: &ProgramID) -> Result> { + self.storage.deployment_store().get_latest_edition_for_program(program_id) + } + /// Returns the edition for the given `transaction ID`. pub fn get_edition(&self, transaction_id: &N::TransactionID) -> Result> { // Retrieve the transaction type. @@ -365,15 +382,7 @@ impl> TransactionStore { }; // Retrieve the edition. match transaction_type { - TransactionType::Deploy => { - // Retrieve the program ID. - let program_id = self.storage.deployment_store().get_program_id(transaction_id)?; - // Return the edition. - match program_id { - Some(program_id) => self.storage.deployment_store().get_edition(&program_id), - None => bail!("Failed to get the program ID for deployment transaction '{transaction_id}'"), - } - } + TransactionType::Deploy => self.storage.deployment_store().get_edition_for_transaction(transaction_id), // Return 'None'. TransactionType::Execute => Ok(None), // Return 'None'. @@ -386,34 +395,71 @@ impl> TransactionStore { self.storage.deployment_store().get_program_id(transaction_id) } - /// Returns the program for the given `program ID`. - pub fn get_program(&self, program_id: &ProgramID) -> Result>> { - self.storage.deployment_store().get_program(program_id) + /// Returns the latest program for the given `program ID`. + pub fn get_latest_program(&self, program_id: &ProgramID) -> Result>> { + self.storage.deployment_store().get_latest_program(program_id) + } + + /// Returns the program for the given `program ID` and `edition`. + pub fn get_program_for_edition(&self, program_id: &ProgramID, edition: u16) -> Result>> { + self.storage.deployment_store().get_program_for_edition(program_id, edition) } - /// Returns the verifying key for the given `(program ID, function name)`. - pub fn get_verifying_key( + /// Returns the latest verifying key for the given `(program ID, function name)`. + pub fn get_latest_verifying_key( &self, program_id: &ProgramID, function_name: &Identifier, ) -> Result>> { - self.storage.deployment_store().get_verifying_key(program_id, function_name) + self.storage.deployment_store().get_latest_verifying_key(program_id, function_name) } - /// Returns the certificate for the given `(program ID, function name)`. - pub fn get_certificate( + /// Returns the verifying key for the given `(program ID, function name, edition)`. + pub fn get_verifying_key_with_edition( + &self, + program_id: &ProgramID, + function_name: &Identifier, + edition: u16, + ) -> Result>> { + self.storage.deployment_store().get_verifying_key_with_edition(program_id, function_name, edition) + } + + /// Returns the latest certificate for the given `(program ID, function name)`. + pub fn get_latest_certificate( &self, program_id: &ProgramID, function_name: &Identifier, ) -> Result>> { - self.storage.deployment_store().get_certificate(program_id, function_name) + self.storage.deployment_store().get_latest_certificate(program_id, function_name) + } + + /// Returns the certificate for the given `(program ID, function name, edition)`. + pub fn get_certificate_with_edition( + &self, + program_id: &ProgramID, + function_name: &Identifier, + edition: u16, + ) -> Result>> { + self.storage.deployment_store().get_certificate_with_edition(program_id, function_name, edition) } } impl> TransactionStore { - /// Returns the transaction ID that contains the given `program ID`. - pub fn find_transaction_id_from_program_id(&self, program_id: &ProgramID) -> Result> { - self.storage.deployment_store().find_transaction_id_from_program_id(program_id) + /// Returns the latest transaction ID that contains the given `program ID`. + pub fn find_latest_transaction_id_from_program_id( + &self, + program_id: &ProgramID, + ) -> Result> { + self.storage.deployment_store().find_latest_transaction_id_from_program_id(program_id) + } + + /// Returns the transaction ID that contains the given `program ID` and `edition`. + pub fn find_transaction_id_from_program_id_and_edition( + &self, + program_id: &ProgramID, + edition: u16, + ) -> Result> { + self.storage.deployment_store().find_transaction_id_from_program_id_and_edition(program_id, edition) } /// Returns the transaction ID that contains the given `transition ID`. @@ -435,8 +481,14 @@ impl> TransactionStore { pub fn contains_program_id(&self, program_id: &ProgramID) -> Result { self.storage.deployment_store().contains_program_id(program_id) } + + /// Returns `true` if the given program ID and edition exists. + pub fn contains_program_id_and_edition(&self, program_id: &ProgramID, edition: u16) -> Result { + self.storage.deployment_store().contains_program_id_and_edition(program_id, edition) + } } +type ProgramIDEdition = (ProgramID, u16); type ProgramTriplet = (ProgramID, Identifier, u16); impl> TransactionStore { @@ -456,15 +508,29 @@ impl> TransactionStore { } /// Returns an iterator over the program IDs, for all deployments. + /// Note: If a program upgraded, this method will return duplicates of the program ID. pub fn program_ids(&self) -> impl '_ + Iterator>> { self.storage.deployment_store().program_ids() } + /// Returns an iterator over the program IDs and latest editions. + pub fn program_ids_and_latest_editions(&self) -> impl '_ + Iterator>, Cow<'_, u16>)> { + self.storage.deployment_store().program_ids_and_latest_editions() + } + /// Returns an iterator over the programs, for all deployments. + /// If a program has been upgraded, all instances of the program will be returned. pub fn programs(&self) -> impl '_ + Iterator>> { self.storage.deployment_store().programs() } + /// Returns an iterator over the programs and editions, for all deployments. + pub fn programs_with_editions( + &self, + ) -> impl '_ + Iterator>, Cow<'_, Program>)> { + self.storage.deployment_store().programs_with_editions() + } + /// Returns an iterator over the `((program ID, function name, edition), verifying key)`, for all deployments. pub fn verifying_keys(&self) -> impl '_ + Iterator>, Cow<'_, VerifyingKey>)> { self.storage.deployment_store().verifying_keys() @@ -487,8 +553,10 @@ mod tests { // Sample the transactions. for transaction in [ - ledger_test_helpers::sample_deployment_transaction(true, rng), - ledger_test_helpers::sample_deployment_transaction(false, rng), + ledger_test_helpers::sample_deployment_transaction(1, true, rng), + ledger_test_helpers::sample_deployment_transaction(1, false, rng), + ledger_test_helpers::sample_deployment_transaction(2, true, rng), + ledger_test_helpers::sample_deployment_transaction(2, false, rng), ledger_test_helpers::sample_execution_transaction_with_fee(true, rng), ledger_test_helpers::sample_execution_transaction_with_fee(false, rng), ledger_test_helpers::sample_fee_private_transaction(rng), @@ -528,8 +596,10 @@ mod tests { // Sample the transactions. for transaction in [ - ledger_test_helpers::sample_deployment_transaction(true, rng), - ledger_test_helpers::sample_deployment_transaction(false, rng), + ledger_test_helpers::sample_deployment_transaction(1, true, rng), + ledger_test_helpers::sample_deployment_transaction(1, false, rng), + ledger_test_helpers::sample_deployment_transaction(2, true, rng), + ledger_test_helpers::sample_deployment_transaction(2, false, rng), ledger_test_helpers::sample_execution_transaction_with_fee(true, rng), ledger_test_helpers::sample_execution_transaction_with_fee(false, rng), ledger_test_helpers::sample_fee_private_transaction(rng), diff --git a/ledger/test-helpers/src/lib.rs b/ledger/test-helpers/src/lib.rs index 4615e0ffad..6d6363dfa1 100644 --- a/ledger/test-helpers/src/lib.rs +++ b/ledger/test-helpers/src/lib.rs @@ -130,14 +130,48 @@ pub fn sample_outputs() -> Vec<(::TransitionID, Outpu /******************************************* Deployment *******************************************/ -pub fn sample_deployment(rng: &mut TestRng) -> Deployment { +pub fn sample_deployment_v1(rng: &mut TestRng) -> Deployment { static INSTANCE: OnceCell> = OnceCell::new(); INSTANCE .get_or_init(|| { // Initialize a new program. let (string, program) = Program::::parse( r" -program testing.aleo; +program testing_one.aleo; + +mapping store: + key as u32.public; + value as u32.public; + +function compute: + input r0 as u32.private; + add r0 r0 into r1; + output r1 as u32.public;", + ) + .unwrap(); + assert!(string.is_empty(), "Parser did not consume all of the string: '{string}'"); + + // Construct the process. + let process = Process::load().unwrap(); + // Compute the deployment. + let mut deployment = process.deploy::(&program, rng).unwrap(); + // Unset the checksum. + deployment.set_program_checksum_raw(None); + // Return the deployment. + // Note: This is a testing-only hack to adhere to Rust's dependency cycle rules. + Deployment::from_str(&deployment.to_string()).unwrap() + }) + .clone() +} + +pub fn sample_deployment_v2(rng: &mut TestRng) -> Deployment { + static INSTANCE: OnceCell> = OnceCell::new(); + INSTANCE + .get_or_init(|| { + // Initialize a new program. + let (string, program) = Program::::parse( + r" +program testing_two.aleo; mapping store: key as u32.public; @@ -155,6 +189,8 @@ function compute: let process = Process::load().unwrap(); // Compute the deployment. let deployment = process.deploy::(&program, rng).unwrap(); + // Verify that the checksum is set. + assert!(deployment.program_checksum().is_some(), "Deployment does not have a checksum"); // Return the deployment. // Note: This is a testing-only hack to adhere to Rust's dependency cycle rules. Deployment::from_str(&deployment.to_string()).unwrap() @@ -163,9 +199,9 @@ function compute: } /// Samples a rejected deployment. -pub fn sample_rejected_deployment(is_fee_private: bool, rng: &mut TestRng) -> Rejected { +pub fn sample_rejected_deployment(version: u8, is_fee_private: bool, rng: &mut TestRng) -> Rejected { // Sample a deploy transaction. - let deployment = match crate::sample_deployment_transaction(is_fee_private, rng) { + let deployment = match crate::sample_deployment_transaction(version, is_fee_private, rng) { Transaction::Deploy(_, _, _, deployment, _) => (*deployment).clone(), _ => unreachable!(), }; @@ -353,11 +389,19 @@ function large_transaction: /****************************************** Transaction *******************************************/ /// Samples a random deployment transaction with a private or public fee. -pub fn sample_deployment_transaction(is_fee_private: bool, rng: &mut TestRng) -> Transaction { +pub fn sample_deployment_transaction( + version: u8, + is_fee_private: bool, + rng: &mut TestRng, +) -> Transaction { // Sample a private key. let private_key = PrivateKey::new(rng).unwrap(); // Sample a deployment. - let deployment = crate::sample_deployment(rng); + let deployment = match version { + 1 => crate::sample_deployment_v1(rng), + 2 => crate::sample_deployment_v2(rng), + _ => panic!("Invalid deployment version: {version}"), + }; // Compute the deployment ID. let deployment_id = deployment.to_deployment_id().unwrap(); diff --git a/synthesizer/process/src/cost.rs b/synthesizer/process/src/cost.rs index 2a3581fdc5..fa3e120235 100644 --- a/synthesizer/process/src/cost.rs +++ b/synthesizer/process/src/cost.rs @@ -642,18 +642,18 @@ function dummy:", // Verify the deployment costs. let deployment_0 = process.deploy::(&program_0, rng).unwrap(); - assert_eq!(deployment_cost(&process, &deployment_0).unwrap(), (2442725, (815000, 577725, 50000, 1000000))); + assert_eq!(deployment_cost(&process, &deployment_0).unwrap(), (2474725, (847000, 577725, 50000, 1000000))); let deployment_1 = process.deploy::(&program_1, rng).unwrap(); - assert_eq!(deployment_cost(&process, &deployment_1).unwrap(), (2441725, (814000, 577725, 50000, 1000000))); + assert_eq!(deployment_cost(&process, &deployment_1).unwrap(), (2473725, (846000, 577725, 50000, 1000000))); let deployment_2 = process.deploy::(&program_2, rng).unwrap(); - assert_eq!(deployment_cost(&process, &deployment_2).unwrap(), (2606725, (847000, 577725, 182000, 1000000))); + assert_eq!(deployment_cost(&process, &deployment_2).unwrap(), (2638725, (879000, 577725, 182000, 1000000))); let deployment_3 = process.deploy::(&program_3, rng).unwrap(); assert_eq!( deployment_cost(&process, &deployment_3).unwrap(), - (4096725, (879000, 577725, 1640000, 1000000)) + (4128725, (911000, 577725, 1640000, 1000000)) ); } diff --git a/synthesizer/process/src/finalize.rs b/synthesizer/process/src/finalize.rs index 62856e3656..4da6348477 100644 --- a/synthesizer/process/src/finalize.rs +++ b/synthesizer/process/src/finalize.rs @@ -44,6 +44,26 @@ impl Process { } lap!(timer, "Insert the verifying keys"); + // Determine which mappings must be initialized. + let mappings = match deployment.edition().is_zero() { + true => deployment.program().mappings().values().collect::>(), + false => { + // Get the existing stack. + let existing_stack = self.get_stack(deployment.program_id())?; + // Get the existing mappings. + let existing_mappings = existing_stack.program().mappings(); + // Determine and return the new mappings + let mut new_mappings = Vec::new(); + for mapping in deployment.program().mappings().values() { + if !existing_mappings.contains_key(mapping.name()) { + new_mappings.push(mapping); + } + } + new_mappings + } + }; + lap!(timer, "Retrieve the mappings to initialize"); + // Initialize the mappings, and store their finalize operations. atomic_batch_scope!(store, { // Initialize a list for the finalize operations. @@ -61,8 +81,8 @@ impl Process { // Retrieve the program ID. let program_id = deployment.program_id(); - // Iterate over the mappings. - for mapping in deployment.program().mappings().values() { + // Iterate over the mappings that must be initialized. + for mapping in mappings { // Initialize the mapping. finalize_operations.push(store.initialize_mapping(*program_id, *mapping.name())?); } @@ -336,7 +356,7 @@ fn finalize_transition>( // Get the finalize logic. let Some(finalize) = stack.get_function_ref(registers.function_name())?.finalize_logic() else { bail!( - "The function '{}/{}' does not have an associated finalize block", + "The function '{}/{}' does not have an associated finalize scope", stack.program_id(), registers.function_name() ) @@ -474,7 +494,7 @@ fn finalize_transition>( Ok(finalize_operations) } -// A helper struct to track the execution of a finalize block. +// A helper struct to track the execution of a finalize scope. struct FinalizeState { // A counter for the index of the commands. counter: usize, @@ -502,13 +522,12 @@ fn initialize_finalize_state( false => stack.get_external_stack(future.program_id())?, }; // Get the finalize logic and check that it exists. - let finalize = match stack.get_function_ref(future.function_name())?.finalize_logic() { - Some(finalize) => finalize, - None => bail!( - "The function '{}/{}' does not have an associated finalize block", + let Some(finalize) = stack.get_function_ref(future.function_name())?.finalize_logic() else { + bail!( + "The function '{}/{}' does not have an associated finalize scope", future.program_id(), future.function_name() - ), + ) }; // Initialize the registers. let mut registers = FinalizeRegisters::new( diff --git a/synthesizer/process/src/stack/deploy.rs b/synthesizer/process/src/stack/deploy.rs index ebbbdc86df..c9109144ec 100644 --- a/synthesizer/process/src/stack/deploy.rs +++ b/synthesizer/process/src/stack/deploy.rs @@ -51,7 +51,7 @@ impl Stack { finish!(timer); // Return the deployment. - Deployment::new(N::EDITION, self.program.clone(), verifying_keys) + Deployment::new(*self.program_edition, self.program.clone(), verifying_keys, Some(self.program_checksum)) } /// Checks each function in the program on the given verifying key and certificate. @@ -69,6 +69,18 @@ impl Stack { deployment.check_is_ordered()?; // Ensure the program in the stack and deployment matches. ensure!(&self.program == deployment.program(), "The stack program does not match the deployment program"); + // Ensure that edition in the stack and deployment matches. + ensure!( + *self.program_edition == deployment.edition(), + "The stack edition does not match the deployment edition" + ); + // If the deployment contains a checksum, ensure it matches the one computed by the stack. + if let Some(program_checksum) = deployment.program_checksum() { + ensure!( + *program_checksum == self.program_checksum, + "The deployment checksum does not match the stack checksum" + ); + } // Check Verifying Keys // diff --git a/synthesizer/process/src/stack/evaluate.rs b/synthesizer/process/src/stack/evaluate.rs index 3124782095..25b4b6e57c 100644 --- a/synthesizer/process/src/stack/evaluate.rs +++ b/synthesizer/process/src/stack/evaluate.rs @@ -91,7 +91,7 @@ impl StackEvaluate for Stack { Some(program_id) => *self.get_external_stack(program_id)?.program_checksum(), None => *self.program_checksum(), }; - Ok(Value::Plaintext(Plaintext::from(Literal::Field(checksum)))) + Ok(Value::Plaintext(Plaintext::from(checksum))) } // If the operand is the program edition, retrieve the edition from the stack. Operand::Edition(program_id) => { @@ -240,7 +240,7 @@ impl StackEvaluate for Stack { Some(program_id) => *self.get_external_stack(program_id)?.program_checksum(), None => *self.program_checksum(), }; - Ok(Value::Plaintext(Plaintext::from(Literal::Field(checksum)))) + Ok(Value::Plaintext(Plaintext::from(checksum))) } // If the operand is the program edition, retrieve the edition from the stack. Operand::Edition(program_id) => { diff --git a/synthesizer/process/src/stack/execute.rs b/synthesizer/process/src/stack/execute.rs index 23f750c379..7201bec931 100644 --- a/synthesizer/process/src/stack/execute.rs +++ b/synthesizer/process/src/stack/execute.rs @@ -126,9 +126,7 @@ impl StackExecute for Stack { Some(program_id) => *self.get_external_stack(program_id)?.program_checksum(), None => *self.program_checksum(), }; - Ok(circuit::Value::Plaintext(circuit::Plaintext::from(circuit::Literal::Field( - circuit::Field::new(circuit::Mode::Constant, checksum), - )))) + Ok(circuit::Value::Plaintext(circuit::Plaintext::from(checksum.map(circuit::U8::constant)))) } // If the operand is the edition, retrieve the edition from the stack. Operand::Edition(program_id) => { @@ -381,9 +379,7 @@ impl StackExecute for Stack { Some(program_id) => *self.get_external_stack(program_id)?.program_checksum(), None => *self.program_checksum(), }; - Ok(circuit::Value::Plaintext(circuit::Plaintext::from(circuit::Literal::Field( - circuit::Field::new(circuit::Mode::Constant, checksum), - )))) + Ok(circuit::Value::Plaintext(circuit::Plaintext::from(checksum.map(circuit::U8::constant)))) } // If the operand is the edition, retrieve the edition from the stack. Operand::Edition(program_id) => { diff --git a/synthesizer/process/src/stack/finalize_registers/load.rs b/synthesizer/process/src/stack/finalize_registers/load.rs index 1c55aff943..0f2245ca0e 100644 --- a/synthesizer/process/src/stack/finalize_registers/load.rs +++ b/synthesizer/process/src/stack/finalize_registers/load.rs @@ -52,7 +52,7 @@ impl RegistersLoad for FinalizeRegisters { Some(program_id) => *stack.get_external_stack(program_id)?.program_checksum(), None => *stack.program_checksum(), }; - return Ok(Value::Plaintext(Plaintext::from(Literal::Field(checksum)))); + return Ok(Value::Plaintext(Plaintext::from(checksum))); } // If the operand is the edition, load the edition. Operand::Edition(program_id) => { diff --git a/synthesizer/process/src/stack/finalize_types/matches.rs b/synthesizer/process/src/stack/finalize_types/matches.rs index a3dae53bd8..d506940906 100644 --- a/synthesizer/process/src/stack/finalize_types/matches.rs +++ b/synthesizer/process/src/stack/finalize_types/matches.rs @@ -110,7 +110,10 @@ impl FinalizeTypes { // Ensure the checksum type (field) matches the member type. Operand::Checksum(_) => { // Retrieve the checksum type. - let checksum_type = PlaintextType::Literal(LiteralType::Field); + let checksum_type = PlaintextType::Array(ArrayType::new( + PlaintextType::Literal(LiteralType::U8), + vec![U32::new(32)], + )?); // Ensure the checksum type matches the member type. ensure!( &checksum_type == member_type, @@ -222,7 +225,10 @@ impl FinalizeTypes { // Ensure the checksum type (field) matches the member type. Operand::Checksum(_) => { // Retrieve the checksum type. - let checksum_type = PlaintextType::Literal(LiteralType::Field); + let checksum_type = PlaintextType::Array(ArrayType::new( + PlaintextType::Literal(LiteralType::U8), + vec![U32::new(32)], + )?); // Ensure the checksum type matches the member type. ensure!( &checksum_type == array_type.next_element_type(), diff --git a/synthesizer/process/src/stack/finalize_types/mod.rs b/synthesizer/process/src/stack/finalize_types/mod.rs index fe9d7bbd10..120c990777 100644 --- a/synthesizer/process/src/stack/finalize_types/mod.rs +++ b/synthesizer/process/src/stack/finalize_types/mod.rs @@ -31,6 +31,7 @@ use console::{ RegisterType, StructType, }, + types::U32, }; use synthesizer_program::{ Await, @@ -116,7 +117,10 @@ impl FinalizeTypes { Operand::Caller => bail!("'self.caller' is not a valid operand in a finalize context."), Operand::BlockHeight => FinalizeType::Plaintext(PlaintextType::Literal(LiteralType::U32)), Operand::NetworkID => FinalizeType::Plaintext(PlaintextType::Literal(LiteralType::U16)), - Operand::Checksum(_) => FinalizeType::Plaintext(PlaintextType::Literal(LiteralType::Field)), + Operand::Checksum(_) => FinalizeType::Plaintext(PlaintextType::Array(ArrayType::new( + PlaintextType::Literal(LiteralType::U8), + vec![U32::new(32)], + )?)), Operand::Edition(_) => FinalizeType::Plaintext(PlaintextType::Literal(LiteralType::U16)), }) } diff --git a/synthesizer/process/src/stack/helpers/check_upgrade.rs b/synthesizer/process/src/stack/helpers/check_upgrade.rs new file mode 100644 index 0000000000..50c7fac4d1 --- /dev/null +++ b/synthesizer/process/src/stack/helpers/check_upgrade.rs @@ -0,0 +1,127 @@ +// Copyright (c) 2019-2025 Provable Inc. +// This file is part of the snarkVM library. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::*; + +impl Stack { + /// Checks that the new program is a valid upgrade. + /// At a high-level, an upgrade must preserve the existing interfaces of the original program. + /// An upgrade may add new components, except for constructors, and modify logic **only** in functions and finalize scopes. + /// + /// An detailed overview of what an upgrade can and cannot do is given below: + /// | Program Component | Delete | Modify | Add | + /// |-------------------|--------|--------------|-------| + /// | import | ❌ | ❌ | ✅ | + /// | constructor | ❌ | ❌ | ❌ | + /// | mapping | ❌ | ❌ | ✅ | + /// | struct | ❌ | ❌ | ✅ | + /// | record | ❌ | ❌ | ✅ | + /// | closure | ❌ | ❌ | ✅ | + /// | function | ❌ | ✅ (logic) | ✅ | + /// | finalize | ❌ | ✅ (logic) | ✅ | + /// |-------------------|--------|--------------|-------| + /// + #[inline] + pub(crate) fn check_upgrade_is_valid(process: &Process, new_program: &Program) -> Result<()> { + // Get the new program ID. + let program_id = new_program.id(); + // Get the old program. + let stack = process.get_stack(program_id)?; + let old_program = stack.program(); + // Check that the old program is upgradable, meaning that it has a constructor. + ensure!( + old_program.constructor().is_some(), + "Cannot upgrade '{program_id}' because it does not have a constructor" + ); + // Ensure the program ID matches. + ensure!(old_program.id() == new_program.id(), "Cannot upgrade '{program_id}' with different program ID"); + // Ensure that the old program is not the same as the new program. + ensure!(old_program != new_program, "Cannot upgrade '{program_id}' with the same program"); + // Ensure that all of the imports in the old program exist in the new program. + for old_import in old_program.imports().keys() { + if !new_program.contains_import(old_import) { + bail!("Cannot upgrade '{program_id}' because it is missing the original import '{old_import}'"); + } + } + // Ensure that the constructors in both programs are exactly the same. + ensure!( + old_program.constructor() == new_program.constructor(), + "Cannot upgrade '{program_id}' because the constructor does not match" + ); + // Ensure that all of the mappings in the old program exist in the new program. + for (old_mapping_id, old_mapping_type) in old_program.mappings() { + let new_mapping_type = new_program.get_mapping(old_mapping_id)?; + ensure!( + *old_mapping_type == new_mapping_type, + "Cannot upgrade '{program_id}' because the mapping '{old_mapping_id}' does not match" + ); + } + // Ensure that all of the structs in the old program exist in the new program. + for (old_struct_id, old_struct_type) in old_program.structs() { + let new_struct_type = new_program.get_struct(old_struct_id)?; + ensure!( + old_struct_type == new_struct_type, + "Cannot upgrade '{program_id}' because the struct '{old_struct_id}' does not match" + ); + } + // Ensure that all of the records in the old program exist in the new program. + for (old_record_id, old_record_type) in old_program.records() { + let new_record_type = new_program.get_record(old_record_id)?; + ensure!( + old_record_type == new_record_type, + "Cannot upgrade '{program_id}' because the record '{old_record_id}' does not match" + ); + } + // Ensure that the old program closures exist in the new program, with the exact same definition. + for old_closure in old_program.closures().values() { + let old_closure_name = old_closure.name(); + let new_closure = new_program.get_closure(old_closure_name)?; + ensure!( + old_closure == &new_closure, + "Cannot upgrade '{program_id}' because the closure '{old_closure_name}' does not match" + ); + } + // Ensure that the old program functions exist in the new program, with the same input and output types. + // If the function has an associated `finalize` block, then ensure that the finalize block exists in the new program. + for old_function in old_program.functions().values() { + let old_function_name = old_function.name(); + let new_function = new_program.get_function_ref(old_function_name)?; + ensure!( + old_function.input_types() == new_function.input_types(), + "Cannot upgrade '{program_id}' because the inputs to the function '{old_function_name}' do not match" + ); + ensure!( + old_function.output_types() == new_function.output_types(), + "Cannot upgrade '{program_id}' because the outputs of the function '{old_function_name}' do not match" + ); + match (old_function.finalize_logic(), new_function.finalize_logic()) { + (None, None) => {} // Do nothing + (None, Some(_)) => bail!( + "Cannot upgrade '{program_id}' because the function '{old_function_name}' should not have a finalize block" + ), + (Some(_), None) => bail!( + "Cannot upgrade '{program_id}' because the function '{old_function_name}' should have a finalize block" + ), + (Some(old_finalize), Some(new_finalize)) => { + ensure!( + old_finalize.input_types() == new_finalize.input_types(), + "Cannot upgrade '{program_id}' because the finalize inputs to the function '{old_function_name}' do not match" + ); + } + } + } + Ok(()) + } +} diff --git a/synthesizer/process/src/stack/helpers/initialize.rs b/synthesizer/process/src/stack/helpers/initialize.rs index da32142557..825f2f352b 100644 --- a/synthesizer/process/src/stack/helpers/initialize.rs +++ b/synthesizer/process/src/stack/helpers/initialize.rs @@ -19,6 +19,28 @@ impl Stack { /// Initializes a new stack, given the process and program. #[inline] pub(crate) fn initialize(process: &Process, program: &Program) -> Result { + // Compute the appropriate edition for the stack. + let edition = match process.contains_program(program.id()) { + // If the program does not exist in the process, use edition zero. + false => 0u16, + // If the new program matches the existing program, use the existing edition. + // Otherwise, increment the edition. + true => { + // Retrieve the stack for the program. + let stack = process.get_stack(program.id())?; + // Retrieve the program edition. + let mut edition = **stack.program_edition(); + // If the program does not match the existing program, increment the edition. + if stack.program() != program { + edition = edition + .checked_add(1) + .ok_or_else(|| anyhow!("Overflow while incrementing the program edition"))?; + } + // Output the edition + edition + } + }; + // Construct the stack for the program. let mut stack = Self { program: program.clone(), @@ -30,8 +52,8 @@ impl Stack { proving_keys: Default::default(), verifying_keys: Default::default(), program_address: program.id().to_address()?, - program_checksum: program.checksum()?, - program_edition: U16::new(N::EDITION), + program_checksum: program.to_checksum(), + program_edition: U16::new(edition), }; // Add all the imports into the stack. diff --git a/synthesizer/process/src/stack/helpers/mod.rs b/synthesizer/process/src/stack/helpers/mod.rs index 269c69bc16..f1ab8c7eaa 100644 --- a/synthesizer/process/src/stack/helpers/mod.rs +++ b/synthesizer/process/src/stack/helpers/mod.rs @@ -15,6 +15,7 @@ use super::*; +mod check_upgrade; mod initialize; mod matches; mod sample; diff --git a/synthesizer/process/src/stack/mod.rs b/synthesizer/process/src/stack/mod.rs index 40ae5de78e..53cbd23d95 100644 --- a/synthesizer/process/src/stack/mod.rs +++ b/synthesizer/process/src/stack/mod.rs @@ -62,7 +62,7 @@ use console::{ Value, ValueType, }, - types::{Field, Group, U16}, + types::{Field, Group, U8, U16}, }; use ledger_block::{Deployment, Transaction, Transition}; use synthesizer_program::{CallOperator, Closure, Constructor, Function, Instruction, Operand, Program, traits::*}; @@ -204,7 +204,7 @@ pub struct Stack { /// The program address. program_address: Address, /// The program checksum. - program_checksum: Field, + program_checksum: [U8; 32], /// The program edition. program_edition: U16, } @@ -215,10 +215,12 @@ impl Stack { pub fn new(process: &Process, program: &Program) -> Result { // Retrieve the program ID. let program_id = program.id(); - // Ensure the program does not already exist in the process. - ensure!(!process.contains_program(program_id), "Program '{program_id}' already exists"); // Ensure the program contains functions. ensure!(!program.functions().is_empty(), "No functions present in the deployment for program '{program_id}'"); + // If the program exists in the process, check that the upgrade is valid. + if process.contains_program(program_id) { + Self::check_upgrade_is_valid(process, program)?; + } // Serialize the program into bytes. let program_bytes = program.to_bytes_le()?; @@ -332,7 +334,7 @@ impl StackProgram for Stack { /// Returns the program checksum. #[inline] - fn program_checksum(&self) -> &Field { + fn program_checksum(&self) -> &[U8; 32] { &self.program_checksum } diff --git a/synthesizer/process/src/stack/register_types/matches.rs b/synthesizer/process/src/stack/register_types/matches.rs index 76755171ff..ea59083a39 100644 --- a/synthesizer/process/src/stack/register_types/matches.rs +++ b/synthesizer/process/src/stack/register_types/matches.rs @@ -96,7 +96,10 @@ impl RegisterTypes { // Ensure the checksum type (field) matches the member type. Operand::Checksum(_) => { // Retrieve the operand type. - let operand_type = PlaintextType::Literal(LiteralType::Field); + let operand_type = PlaintextType::Array(ArrayType::new( + PlaintextType::Literal(LiteralType::U8), + vec![U32::new(32)], + )?); // Ensure the operand type matches the member type. ensure!( &operand_type == member_type, @@ -192,7 +195,10 @@ impl RegisterTypes { // Ensure the checksum type (field) matches the element type. Operand::Checksum(_) => { // Retrieve the operand type. - let operand_type = PlaintextType::Literal(LiteralType::Field); + let operand_type = PlaintextType::Array(ArrayType::new( + PlaintextType::Literal(LiteralType::U8), + vec![U32::new(32)], + )?); // Ensure the operand type matches the element type. ensure!( &operand_type == array_type.next_element_type(), @@ -346,7 +352,10 @@ impl RegisterTypes { // Ensure the checksum type (field) matches the entry type. Operand::Checksum(_) => { // Retrieve the operand type. - let operand_type = &PlaintextType::Literal(LiteralType::Field); + let operand_type = + &PlaintextType::Array(ArrayType::new(PlaintextType::Literal(LiteralType::U8), vec![ + U32::new(32), + ])?); // Ensure the operand type matches the entry type. ensure!( operand_type == plaintext_type, diff --git a/synthesizer/process/src/stack/register_types/mod.rs b/synthesizer/process/src/stack/register_types/mod.rs index 88ab7b08c0..5910e8b58e 100644 --- a/synthesizer/process/src/stack/register_types/mod.rs +++ b/synthesizer/process/src/stack/register_types/mod.rs @@ -22,8 +22,10 @@ use console::{ Access, ArrayType, EntryType, + FinalizeType, Identifier, LiteralType, + Locator, PlaintextType, RecordType, Register, @@ -31,6 +33,7 @@ use console::{ StructType, ValueType, }, + types::U32, }; use synthesizer_program::{ CallOperator, @@ -46,7 +49,6 @@ use synthesizer_program::{ StackProgram, }; -use console::program::{FinalizeType, Locator}; use indexmap::{IndexMap, IndexSet}; #[derive(Clone, Default, PartialEq, Eq)] @@ -100,7 +102,10 @@ impl RegisterTypes { } Operand::BlockHeight => bail!("'block.height' is not a valid operand in a non-finalize context."), Operand::NetworkID => bail!("'network.id' is not a valid operand in a non-finalize context."), - Operand::Checksum(_) => RegisterType::Plaintext(PlaintextType::Literal(LiteralType::Field)), + Operand::Checksum(_) => RegisterType::Plaintext(PlaintextType::Array(ArrayType::new( + PlaintextType::Literal(LiteralType::U8), + vec![U32::new(32)], + )?)), Operand::Edition(_) => RegisterType::Plaintext(PlaintextType::Literal(LiteralType::U16)), }) } diff --git a/synthesizer/process/src/stack/registers/load.rs b/synthesizer/process/src/stack/registers/load.rs index 87161a09b1..4e52cefd2c 100644 --- a/synthesizer/process/src/stack/registers/load.rs +++ b/synthesizer/process/src/stack/registers/load.rs @@ -47,7 +47,7 @@ impl> RegistersLoad for Registers *stack.get_external_stack(program_id)?.program_checksum(), None => *stack.program_checksum(), }; - return Ok(Value::Plaintext(Plaintext::from(Literal::Field(checksum)))); + return Ok(Value::Plaintext(Plaintext::from(checksum))); } // If the operand is the edition, load the value of the edition. Operand::Edition(program_id) => { @@ -148,9 +148,7 @@ impl> RegistersLoadCircuit for R Some(program_id) => *stack.get_external_stack(program_id)?.program_checksum(), None => *stack.program_checksum(), }; - return Ok(circuit::Value::Plaintext(circuit::Plaintext::from(circuit::Literal::constant( - Literal::Field(checksum), - )))); + return Ok(circuit::Value::Plaintext(circuit::Plaintext::from(checksum.map(circuit::U8::constant)))); } // If the operand is the edition, load the value of the edition. Operand::Edition(program_id) => { diff --git a/synthesizer/process/src/tests/mod.rs b/synthesizer/process/src/tests/mod.rs index fe7a0d593f..a93bb4cc69 100644 --- a/synthesizer/process/src/tests/mod.rs +++ b/synthesizer/process/src/tests/mod.rs @@ -16,4 +16,5 @@ pub mod test_credits; pub mod test_execute; pub mod test_random; +pub mod test_upgrade; pub mod test_utils; diff --git a/synthesizer/process/src/tests/test_upgrade.rs b/synthesizer/process/src/tests/test_upgrade.rs new file mode 100644 index 0000000000..f3eae5b65b --- /dev/null +++ b/synthesizer/process/src/tests/test_upgrade.rs @@ -0,0 +1,995 @@ +// Copyright (c) 2019-2025 Provable Inc. +// This file is part of the snarkVM library. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// The purpose of these tests are to ensure that an upgrade made to a program is syntactically correct. +/// These rules are defined in `check_upgrade_is_valid`. +/// These tests *DO NOT*: check the semantic correctness of the upgrades. +use crate::Process; +use console::network::{MainnetV0, prelude::*}; +use synthesizer_program::{Program, StackProgram}; + +type CurrentNetwork = MainnetV0; + +// A helper function to sample the default process. +fn sample_process() -> Result, Error> { + let mut process = Process::load()?; + // Add the default program to the process. + let default_program = Program::from_str( + r" +program test.aleo; +function foo: +constructor: + assert.eq true true; + ", + )?; + process.add_program(&default_program)?; + // Return the process. + Ok(process) +} + +#[test] +fn test_add_simple_program() -> Result<()> { + // Sample the default process. + let mut process = Process::::load()?; + // Add a simple program to the process. + let initial_program = Program::from_str( + r" +program test.aleo; +function foo: + ", + )?; + // Add the new program to the process. + process.add_program(&initial_program)?; + // Get the program from the process. + let stack = process.get_stack("test.aleo")?; + let program = stack.program(); + // Check that the program is the same as the initial program. + assert_eq!(program, &initial_program); + Ok(()) +} + +#[test] +fn test_upgrade_without_constructor() -> Result<()> { + // Sample the default process. + let mut process = Process::::load()?; + // Add a program without a constructor to the process. + let initial_program = Program::from_str( + r" +program test.aleo; +function foo: + ", + )?; + process.add_program(&initial_program)?; + // Attempt to upgrade the program. + let new_program = Program::from_str( + r" +program test.aleo; +function foo: + ", + )?; + // Verify that the upgrade was not successful. + assert!(process.add_program(&new_program).is_err()); + Ok(()) +} + +#[test] +fn test_upgrade_with_constructor() -> Result<()> { + // Sample the default process. + let mut process = sample_process()?; + // Upgrade the program. + let new_program = Program::from_str( + r" +program test.aleo; +constructor: + assert.eq true true; +function foo: + ", + )?; + // Verify that the upgrade was successful. + process.add_program(&new_program)?; + Ok(()) +} + +#[test] +fn test_add_import() -> Result<()> { + // Sample the default process. + let mut process = sample_process()?; + // Upgrade the program. + let new_program = Program::from_str( + r" +import credits.aleo; +program test.aleo; +constructor: + assert.eq true true; +function foo: + ", + )?; + process.add_program(&new_program)?; + // Verify that the upgrade was successful. + let stack = process.get_stack("test.aleo")?; + let program = stack.program(); + assert_eq!(program, &new_program); + Ok(()) +} + +#[test] +fn test_add_struct() -> Result<()> { + // Sample the default process. + let mut process = sample_process()?; + // Upgrade the program. + let new_program = Program::from_str( + r" +program test.aleo; +constructor: + assert.eq true true; +struct bar: + data as u8; +function foo: + ", + )?; + process.add_program(&new_program)?; + // Verify that the upgrade was successful. + let stack = process.get_stack("test.aleo")?; + let program = stack.program(); + assert_eq!(program, &new_program); + Ok(()) +} + +#[test] +fn test_add_record() -> Result<()> { + // Sample the default process. + let mut process = sample_process()?; + // Upgrade the program. + let new_program = Program::from_str( + r" +program test.aleo; +constructor: + assert.eq true true; +record bar: + owner as address.private; + data as u8.private; +function foo: + ", + )?; + process.add_program(&new_program)?; + // Verify that the upgrade was successful. + let stack = process.get_stack("test.aleo")?; + let program = stack.program(); + assert_eq!(program, &new_program); + Ok(()) +} + +#[test] +fn test_add_mapping() -> Result<()> { + // Sample the default process. + let mut process = sample_process()?; + // Upgrade the program. + let new_program = Program::from_str( + r" +program test.aleo; +constructor: + assert.eq true true; +mapping onchain: + key as u8.public; + value as u16.public; +function foo: + ", + )?; + process.add_program(&new_program)?; + // Verify that the upgrade was successful. + let stack = process.get_stack("test.aleo")?; + let program = stack.program(); + assert_eq!(program, &new_program); + Ok(()) +} + +#[test] +fn test_add_closure() -> Result<()> { + // Sample the default process. + let mut process = sample_process()?; + // Upgrade the program. + let new_program = Program::from_str( + r" +program test.aleo; +constructor: + assert.eq true true; +closure sum: + input r0 as u8; + input r1 as u8; + add r0 r1 into r2; + output r2 as u8; +function foo: + ", + )?; + process.add_program(&new_program)?; + // Verify that the upgrade was successful. + let stack = process.get_stack("test.aleo")?; + let program = stack.program(); + assert_eq!(program, &new_program); + Ok(()) +} + +#[test] +fn test_add_function() -> Result<()> { + // Sample the default process. + let mut process = sample_process()?; + // Upgrade the program. + let new_program = Program::from_str( + r" +program test.aleo; +constructor: + assert.eq true true; +function adder: + input r0 as u8.private; + input r1 as u8.private; + add r0 r1 into r2; + output r2 as u8.private; +function foo: + ", + )?; + process.add_program(&new_program)?; + // Verify that the upgrade was successful. + let stack = process.get_stack("test.aleo")?; + let program = stack.program(); + assert_eq!(program, &new_program); + Ok(()) +} + +#[test] +fn test_modify_function_logic() -> Result<()> { + // Sample the default process. + let mut process = sample_process()?; + // Add the initial program to the process. + let initial_program = Program::from_str( + r" +program basic.aleo; +constructor: + assert.eq true true; +function adder: + input r0 as u8.private; + input r1 as u8.private; + add r0 r1 into r2; + output r2 as u8.private; + ", + )?; + process.add_program(&initial_program)?; + // Upgrade the program. + let new_program = Program::from_str( + r" +program basic.aleo; +constructor: + assert.eq true true; +function adder: + input r0 as u8.private; + input r1 as u8.private; + sub r0 r1 into r2; + output r2 as u8.private; + ", + )?; + process.add_program(&new_program)?; + // Verify that the upgrade was successful. + let stack = process.get_stack("basic.aleo")?; + let program = stack.program(); + assert_eq!(program, &new_program); + Ok(()) +} + +#[test] +fn test_modify_function_signature() -> Result<()> { + // Sample the default process. + let mut process = sample_process()?; + // Add the initial program to the process. + let initial_program = Program::from_str( + r" +program basic.aleo; +constructor: + assert.eq true true; +function adder: + input r0 as u8.private; + input r1 as u8.private; + add r0 r1 into r2; + output r2 as u8.private; + ", + )?; + process.add_program(&initial_program)?; + // Upgrade the program. + let new_program = Program::from_str( + r" +program basic.aleo; +constructor: + assert.eq true true; +function adder: + input r0 as u16.private; + input r1 as u16.private; + add r0 r1 into r2; + output r2 as u16.private; + ", + )?; + // Verify that the upgrade was not successful. + assert!(process.add_program(&new_program).is_err()); + Ok(()) +} + +#[test] +fn test_modify_finalize_logic() -> Result<()> { + // Sample the default process. + let mut process = sample_process()?; + // Add the initial program to the process. + let initial_program = Program::from_str( + r" +program basic.aleo; +constructor: + assert.eq true true; +function assert_on_chain: + input r0 as u8.public; + input r1 as u8.public; + async assert_on_chain r0 r1 into r2; + output r2 as basic.aleo/assert_on_chain.future; +finalize assert_on_chain: + input r0 as u8.public; + input r1 as u8.public; + assert.eq r0 r1; + ", + )?; + process.add_program(&initial_program)?; + // Upgrade the program. + let new_program = Program::from_str( + r" +program basic.aleo; +constructor: + assert.eq true true; +function assert_on_chain: + input r0 as u8.public; + input r1 as u8.public; + async assert_on_chain r0 r1 into r2; + output r2 as basic.aleo/assert_on_chain.future; +finalize assert_on_chain: + input r0 as u8.public; + input r1 as u8.public; + assert.neq r0 r1; + ", + )?; + process.add_program(&new_program)?; + // Verify that the upgrade was successful. + let stack = process.get_stack("basic.aleo")?; + let program = stack.program(); + assert_eq!(program, &new_program); + Ok(()) +} + +#[test] +fn test_modify_finalize_signature() -> Result<()> { + // Sample the default process. + let mut process = sample_process()?; + // Add the initial program to the process. + let initial_program = Program::from_str( + r" +program basic.aleo; +constructor: + assert.eq true true; +function assert_on_chain: + input r0 as u8.public; + input r1 as u8.public; + async assert_on_chain r0 r1 into r2; + output r2 as basic.aleo/assert_on_chain.future; +finalize assert_on_chain: + input r0 as u8.public; + input r1 as u8.public; + assert.eq r0 r1; + ", + )?; + process.add_program(&initial_program)?; + // Upgrade the program. + let new_program = Program::from_str( + r" +program basic.aleo; +constructor: + assert.eq true true; +function assert_on_chain: + input r0 as u8.public; + input r1 as u8.public; + async assert_on_chain 0u16 1u16 into r2; + output r2 as basic.aleo/assert_on_chain.future; +finalize assert_on_chain: + input r0 as u16.public; + input r1 as u16.public; + assert.eq r0 r1; + ", + )?; + // Verify that the upgrade was not successful. + assert!(process.add_program(&new_program).is_err()); + Ok(()) +} + +#[test] +fn test_modify_struct() -> Result<()> { + // Sample the default process. + let mut process = sample_process()?; + // Add the initial program to the process. + let initial_program = Program::from_str( + r" +program basic.aleo; +constructor: + assert.eq true true; +struct bar: + data as u8; +function foo: + ", + )?; + process.add_program(&initial_program)?; + // Upgrade the program. + let new_program = Program::from_str( + r" +program basic.aleo; +constructor: + assert.eq true true; +struct bar: + data as u16; +function foo: + ", + )?; + // Verify that the upgrade was not successful. + assert!(process.add_program(&new_program).is_err()); + Ok(()) +} + +#[test] +fn test_modify_record() -> Result<()> { + // Sample the default process. + let mut process = sample_process()?; + // Add the initial program to the process. + let initial_program = Program::from_str( + r" +program basic.aleo; +constructor: + assert.eq true true; +record bar: + owner as address.private; + data as u8.private; +function foo: + ", + )?; + process.add_program(&initial_program)?; + // Upgrade the program. + let new_program = Program::from_str( + r" +program basic.aleo; +constructor: + assert.eq true true; +record bar: + owner as address.private; + data as u16.private; +function foo: + ", + )?; + // Verify that the upgrade was not successful. + assert!(process.add_program(&new_program).is_err()); + Ok(()) +} + +#[test] +fn test_modify_mapping() -> Result<()> { + // Sample the default process. + let mut process = sample_process()?; + // Add the initial program to the process. + let initial_program = Program::from_str( + r" +program basic.aleo; +constructor: + assert.eq true true; +mapping onchain: + key as u8.public; + value as u16.public; +function foo: + ", + )?; + process.add_program(&initial_program)?; + // Upgrade the program. + let new_program = Program::from_str( + r" +program basic.aleo; +constructor: + assert.eq true true; +mapping onchain: + key as u8.public; + value as u8.public; +function foo: + ", + )?; + // Verify that the upgrade was not successful. + assert!(process.add_program(&new_program).is_err()); + Ok(()) +} + +#[test] +fn test_modify_closure_logic() -> Result<()> { + // Sample the default process. + let mut process = sample_process()?; + // Add the initial program to the process. + let initial_program = Program::from_str( + r" +program basic.aleo; +constructor: + assert.eq true true; +closure sum: + input r0 as u8; + input r1 as u8; + add r0 r1 into r2; + output r2 as u8; +function foo: + ", + )?; + process.add_program(&initial_program)?; + // Upgrade the program. + let new_program = Program::from_str( + r" +program basic.aleo; +constructor: + assert.eq true true; +closure sum: + input r0 as u8; + input r1 as u8; + sub r0 r1 into r2; + output r2 as u8; +function foo: + ", + )?; + // Verify that the upgrade was not successful. + assert!(process.add_program(&new_program).is_err()); + Ok(()) +} + +#[test] +fn test_modify_closure_signature() -> Result<()> { + // Sample the default process. + let mut process = sample_process()?; + // Add the initial program to the process. + let initial_program = Program::from_str( + r" +program basic.aleo; +constructor: + assert.eq true true; +closure sum: + input r0 as u8; + input r1 as u8; + add r0 r1 into r2; + output r2 as u8; +function foo: + ", + )?; + process.add_program(&initial_program)?; + // Upgrade the program. + let new_program = Program::from_str( + r" +program basic.aleo; +constructor: + assert.eq true true; +closure sum: + input r0 as u16; + input r1 as u16; + add r0 r1 into r2; + output r2 as u16; +function foo: + ", + )?; + // Verify that the upgrade was not successful. + assert!(process.add_program(&new_program).is_err()); + Ok(()) +} + +#[test] +fn test_remove_import() -> Result<()> { + // Sample the default process. + let mut process = sample_process()?; + // Add the initial program to the process. + let initial_program = Program::from_str( + r" +import credits.aleo; +program basic.aleo; +constructor: + assert.eq true true; +function foo: + ", + )?; + process.add_program(&initial_program)?; + // Upgrade the program. + let new_program = Program::from_str( + r" +program basic.aleo; +constructor: + assert.eq true true; +function foo: + ", + )?; + // Verify that the upgrade was not successful. + assert!(process.add_program(&new_program).is_err()); + Ok(()) +} + +#[test] +fn test_remove_struct() -> Result<()> { + // Sample the default process. + let mut process = sample_process()?; + // Add the initial program to the process. + let initial_program = Program::from_str( + r" +program basic.aleo; +constructor: + assert.eq true true; +struct bar: + data as u8; +function foo: + ", + )?; + process.add_program(&initial_program)?; + // Upgrade the program. + let new_program = Program::from_str( + r" +program basic.aleo; +constructor: + assert.eq true true; +function foo: + ", + )?; + // Verify that the upgrade was not successful. + assert!(process.add_program(&new_program).is_err()); + Ok(()) +} + +#[test] +fn test_remove_record() -> Result<()> { + // Sample the default process. + let mut process = sample_process()?; + // Add the initial program to the process. + let initial_program = Program::from_str( + r" +program basic.aleo; +constructor: + assert.eq true true; +record bar: + owner as address.private; + data as u8.private; +function foo: + ", + )?; + process.add_program(&initial_program)?; + // Upgrade the program. + let new_program = Program::from_str( + r" +program basic.aleo; +constructor: + assert.eq true true; +function foo: + ", + )?; + // Verify that the upgrade was not successful. + assert!(process.add_program(&new_program).is_err()); + Ok(()) +} + +#[test] +fn test_remove_mapping() -> Result<()> { + // Sample the default process. + let mut process = sample_process()?; + // Add the initial program to the process. + let initial_program = Program::from_str( + r" +program basic.aleo; +constructor: + assert.eq true true; +mapping onchain: + key as u8.public; + value as u16.public; +function foo: + ", + )?; + process.add_program(&initial_program)?; + // Upgrade the program. + let new_program = Program::from_str( + r" +program basic.aleo; +constructor: + assert.eq true true; +function foo: + ", + )?; + // Verify that the upgrade was not successful. + assert!(process.add_program(&new_program).is_err()); + Ok(()) +} + +#[test] +fn test_remove_closure() -> Result<()> { + // Sample the default process. + let mut process = sample_process()?; + // Add the initial program to the process. + let initial_program = Program::from_str( + r" +program basic.aleo; +constructor: + assert.eq true true; +closure sum: + input r0 as u8; + input r1 as u8; + add r0 r1 into r2; + output r2 as u8; +function foo: + ", + )?; + process.add_program(&initial_program)?; + // Upgrade the program. + let new_program = Program::from_str( + r" +program basic.aleo; +constructor: + assert.eq true true; +function foo: + ", + )?; + // Verify that the upgrade was not successful. + assert!(process.add_program(&new_program).is_err()); + Ok(()) +} + +#[test] +fn test_remove_function() -> Result<()> { + // Sample the default process. + let mut process = sample_process()?; + // Add the initial program to the process. + let initial_program = Program::from_str( + r" +program basic.aleo; +constructor: + assert.eq true true; +function adder: + input r0 as u8.private; + input r1 as u8.private; + add r0 r1 into r2; + output r2 as u8.private; +function foo: + ", + )?; + process.add_program(&initial_program)?; + // Upgrade the program. + let new_program = Program::from_str( + r" +program basic.aleo; +constructor: + assert.eq true true; +function foo: + ", + )?; + // Verify that the upgrade was not successful. + assert!(process.add_program(&new_program).is_err()); + Ok(()) +} + +#[test] +fn test_add_call_to_non_async_transition() -> Result<()> { + // Sample the default process. + let mut process = sample_process()?; + // Add a program with a non-async transition. + let new_program = Program::from_str( + r" +program non_async.aleo; +constructor: + assert.eq true true; +function foo: + input r0 as u8.private; + input r1 as u8.private; + add r0 r1 into r2; + output r2 as u8.private;", + )?; + process.add_program(&new_program)?; + // Add the initial program to the process. + let initial_program = Program::from_str( + r" +import non_async.aleo; +program basic.aleo; +constructor: + assert.eq true true; +function adder: + input r0 as u8.private; + input r1 as u8.private; + add r0 r1 into r2; + output r2 as u8.private; + ", + )?; + process.add_program(&initial_program)?; + // Upgrade the program. + let new_program = Program::from_str( + r" +import non_async.aleo; +program basic.aleo; +constructor: + assert.eq true true; +function adder: + input r0 as u8.private; + input r1 as u8.private; + call non_async.aleo/foo r0 r1 into r2; + output r2 as u8.private; + ", + )?; + process.add_program(&new_program)?; + // Verify that the upgrade was successful. + let stack = process.get_stack("basic.aleo")?; + let program = stack.program(); + assert_eq!(program, &new_program); + Ok(()) +} + +#[test] +fn test_add_call_to_async_transition() -> Result<()> { + // Sample the default process. + let mut process = sample_process()?; + // Add a program with an async transition. + let new_program = Program::from_str( + r" +program async_example.aleo; +constructor: + assert.eq true true; +function foo: + input r0 as u8.private; + input r1 as u8.private; + async foo r0 r1 into r2; + add r0 r1 into r3; + output r3 as u8.private; + output r2 as async_example.aleo/foo.future; +finalize foo: + input r0 as u8.public; + input r1 as u8.public; + assert.eq r0 r1;", + )?; + process.add_program(&new_program)?; + // Add the initial program to the process. + let initial_program = Program::from_str( + r" +import async_example.aleo; +program basic.aleo; +constructor: + assert.eq true true; +function adder: + input r0 as u8.private; + input r1 as u8.private; + add r0 r1 into r2; + output r2 as u8.private; + ", + )?; + process.add_program(&initial_program)?; + // Upgrade the program. + let new_program = Program::from_str( + r" +import async_example.aleo; +program basic.aleo; +constructor: + assert.eq true true; +function adder: + input r0 as u8.private; + input r1 as u8.private; + call async_example.aleo/foo r0 r1 into r2 r3; + async adder r3 into r4; + output r2 as u8.private; + output r4 as basic.aleo/adder.future; +finalize adder: + input r0 as async_example.aleo/foo.future; + await r0;", + )?; + // Verify that the upgrade was not successful. + assert!(process.add_program(&new_program).is_err()); + Ok(()) +} + +#[test] +fn test_add_import_cycle() -> Result<()> { + // Sample the default process. + let mut process = sample_process()?; + + // Add the initial program to the process. + let initial_program = Program::from_str( + r" +program basic.aleo; +constructor: + assert.eq true true; +function adder: + input r0 as u8.private; + input r1 as u8.private; + add r0 r1 into r2; + output r2 as u8.private; + ", + )?; + process.add_program(&initial_program)?; + + // Verify that self-import cycles are not allowed. + let new_program = Program::from_str( + r" +import basic.aleo; +program basic.aleo; +constructor: + assert.eq true true; +function adder: + input r0 as u8.private; + input r1 as u8.private; + add r0 r1 into r2; + output r2 as u8.private; + ", + )?; + assert!(process.add_program(&new_program).is_err()); + + // Add a program dependent on `basic.aleo`. + let dependent_program = Program::from_str( + r" +import basic.aleo; +program dependent.aleo; +constructor: + assert.eq true true; +function foo: + input r0 as u8.private; + input r1 as u8.private; + call basic.aleo/adder r0 r1 into r2; + output r2 as u8.private;", + )?; + process.add_program(&dependent_program)?; + // Verify that the upgrade was successful. + let stack = process.get_stack("dependent.aleo")?; + let program = stack.program(); + assert_eq!(program, &dependent_program); + + // Upgrade basic.aleo to import dependent.aleo. + // This is allowed since we do not do cycle detection across programs. + let new_program = Program::from_str( + r" +import dependent.aleo; +program basic.aleo; +constructor: + assert.eq true true; +function adder: + input r0 as u8.private; + input r1 as u8.private; + add r0 r1 into r2; + output r2 as u8.private; + ", + )?; + // Verify that the upgrade was successful. + process.add_program(&new_program)?; + Ok(()) +} + +#[test] +fn test_constructor_upgrade() -> Result<()> { + // Sample the default process. + let mut process = sample_process()?; + // Add the initial program to the process. + let initial_program = Program::from_str( + r" +program basic.aleo; +function foo: +constructor: + assert.eq 1u8 1u8; + ", + )?; + process.add_program(&initial_program)?; + // Upgrade the program. + let new_program = Program::from_str( + r" +program basic.aleo; +function foo: +constructor: + assert.eq 2u8 2u8; + ", + )?; + // Verify that the upgrade was not successful. + assert!(process.add_program(&new_program).is_err()); + Ok(()) +} diff --git a/synthesizer/process/src/verify_deployment.rs b/synthesizer/process/src/verify_deployment.rs index a65f020096..9aa5d2145f 100644 --- a/synthesizer/process/src/verify_deployment.rs +++ b/synthesizer/process/src/verify_deployment.rs @@ -27,8 +27,18 @@ impl Process { // Retrieve the program ID. let program_id = deployment.program().id(); - // Ensure the program does not already exist in the process. - ensure!(!self.contains_program(program_id), "Program '{program_id}' already exists"); + // If the edition is zero, then verify that the program does not exist. + // Otherwise, verify that the program exists. + match deployment.edition().is_zero() { + true => ensure!( + !self.contains_program(program_id), + "Program '{program_id}' already exists, but the deployment edition is zero" + ), + false => ensure!( + self.contains_program(program_id), + "Program '{program_id}' does not exist, but the deployment edition is non-zero" + ), + } // Ensure the program is well-formed, by computing the stack. let stack = Stack::new(self, deployment.program())?; diff --git a/synthesizer/process/src/verify_fee.rs b/synthesizer/process/src/verify_fee.rs index 048ab6db93..b4739559cf 100644 --- a/synthesizer/process/src/verify_fee.rs +++ b/synthesizer/process/src/verify_fee.rs @@ -245,8 +245,10 @@ mod tests { // Fetch transactions. let transactions = [ - ledger_test_helpers::sample_deployment_transaction(true, rng), - ledger_test_helpers::sample_deployment_transaction(false, rng), + ledger_test_helpers::sample_deployment_transaction(1, true, rng), + ledger_test_helpers::sample_deployment_transaction(1, false, rng), + ledger_test_helpers::sample_deployment_transaction(2, true, rng), + ledger_test_helpers::sample_deployment_transaction(2, false, rng), ledger_test_helpers::sample_execution_transaction_with_fee(true, rng), ledger_test_helpers::sample_execution_transaction_with_fee(false, rng), ledger_test_helpers::sample_fee_private_transaction(rng), diff --git a/synthesizer/program/Cargo.toml b/synthesizer/program/Cargo.toml index 94ae8ca8f3..b0fde36e01 100644 --- a/synthesizer/program/Cargo.toml +++ b/synthesizer/program/Cargo.toml @@ -62,6 +62,10 @@ package = "snarkvm-synthesizer-snark" path = "../snark" version = "=3.7.1" +[dependencies.tiny-keccak] +version = "2" +features = [ "sha3" ] + [dev-dependencies.bincode] version = "1" diff --git a/synthesizer/program/src/bytes.rs b/synthesizer/program/src/bytes.rs index 8907541f95..2ea1398016 100644 --- a/synthesizer/program/src/bytes.rs +++ b/synthesizer/program/src/bytes.rs @@ -84,69 +84,75 @@ impl, Command: CommandTrait> ToB import.write_le(&mut writer)?; } - // Write the number of components. A component is either an identifier or a constructor. - let number_of_components = self.identifiers.len() + self.constructor.is_some() as usize; - u16::try_from(number_of_components).map_err(error)?.write_le(&mut writer)?; - - // Write the components that are identifiers. - for (identifier, definition) in self.identifiers.iter() { - match definition { - ProgramDefinition::Mapping => match self.mappings.get(identifier) { - Some(mapping) => { + // Write the number of components. + u16::try_from(self.components.len()).map_err(error)?.write_le(&mut writer)?; + + // Write the components. + for (label, definition) in self.components.iter() { + match label { + ProgramLabel::Constructor => { + // Write the constructor, if it exists. + if let Some(constructor) = &self.constructor { // Write the variant. - 0u8.write_le(&mut writer)?; - // Write the mapping. - mapping.write_le(&mut writer)?; + 5u8.write_le(&mut writer)?; + // Write the constructor. + constructor.write_le(&mut writer)?; } - None => return Err(error(format!("Mapping '{identifier}' is not defined"))), - }, - ProgramDefinition::Struct => match self.structs.get(identifier) { - Some(struct_) => { - // Write the variant. - 1u8.write_le(&mut writer)?; - // Write the struct. - struct_.write_le(&mut writer)?; - } - None => return Err(error(format!("Struct '{identifier}' is not defined."))), - }, - ProgramDefinition::Record => match self.records.get(identifier) { - Some(record) => { - // Write the variant. - 2u8.write_le(&mut writer)?; - // Write the record. - record.write_le(&mut writer)?; - } - None => return Err(error(format!("Record '{identifier}' is not defined."))), - }, - ProgramDefinition::Closure => match self.closures.get(identifier) { - Some(closure) => { - // Write the variant. - 3u8.write_le(&mut writer)?; - // Write the closure. - closure.write_le(&mut writer)?; + } + ProgramLabel::Identifier(identifier) => { + match definition { + ProgramDefinition::Constructor => { + return Err(error("A program constructor cannot have a named label")); + } + ProgramDefinition::Mapping => match self.mappings.get(identifier) { + Some(mapping) => { + // Write the variant. + 0u8.write_le(&mut writer)?; + // Write the mapping. + mapping.write_le(&mut writer)?; + } + None => return Err(error(format!("Mapping '{identifier}' is not defined"))), + }, + ProgramDefinition::Struct => match self.structs.get(identifier) { + Some(struct_) => { + // Write the variant. + 1u8.write_le(&mut writer)?; + // Write the struct. + struct_.write_le(&mut writer)?; + } + None => return Err(error(format!("Struct '{identifier}' is not defined."))), + }, + ProgramDefinition::Record => match self.records.get(identifier) { + Some(record) => { + // Write the variant. + 2u8.write_le(&mut writer)?; + // Write the record. + record.write_le(&mut writer)?; + } + None => return Err(error(format!("Record '{identifier}' is not defined."))), + }, + ProgramDefinition::Closure => match self.closures.get(identifier) { + Some(closure) => { + // Write the variant. + 3u8.write_le(&mut writer)?; + // Write the closure. + closure.write_le(&mut writer)?; + } + None => return Err(error(format!("Closure '{identifier}' is not defined."))), + }, + ProgramDefinition::Function => match self.functions.get(identifier) { + Some(function) => { + // Write the variant. + 4u8.write_le(&mut writer)?; + // Write the function. + function.write_le(&mut writer)?; + } + None => return Err(error(format!("Function '{identifier}' is not defined."))), + }, } - None => return Err(error(format!("Closure '{identifier}' is not defined."))), - }, - ProgramDefinition::Function => match self.functions.get(identifier) { - Some(function) => { - // Write the variant. - 4u8.write_le(&mut writer)?; - // Write the function. - function.write_le(&mut writer)?; - } - None => return Err(error(format!("Function '{identifier}' is not defined."))), - }, + } } } - - // Write the constructor, if it exists. - if let Some(constructor) = &self.constructor { - // Write the variant. - 5u8.write_le(&mut writer)?; - // Write the constructor. - constructor.write_le(&mut writer)?; - } - Ok(()) } } diff --git a/synthesizer/program/src/lib.rs b/synthesizer/program/src/lib.rs index fbd518c49d..fef693a3ba 100644 --- a/synthesizer/program/src/lib.rs +++ b/synthesizer/program/src/lib.rs @@ -50,6 +50,7 @@ pub use traits::*; mod bytes; mod parse; mod serialize; +mod to_checksum; use console::{ network::{ @@ -67,6 +68,7 @@ use console::{ FromBytesDeserializer, FromStr, IoResult, + Itertools, Network, Parser, ParserResult, @@ -94,16 +96,26 @@ use console::{ take, }, }, - prelude::ToBits, program::{Identifier, PlaintextType, ProgramID, RecordType, StructType}, - types::Field, + types::U8, }; use indexmap::{IndexMap, IndexSet}; use std::collections::BTreeSet; +use tiny_keccak::{Hasher, Sha3 as TinySha3}; + +#[derive(Copy, Clone, PartialEq, Eq, Hash)] +enum ProgramLabel { + /// A program constructor. + Constructor, + /// A named component. + Identifier(Identifier), +} #[derive(Copy, Clone, PartialEq, Eq, Hash)] enum ProgramDefinition { + /// A program constructor. + Constructor, /// A program mapping. Mapping, /// A program struct. @@ -116,16 +128,16 @@ enum ProgramDefinition { Function, } -#[derive(Clone, PartialEq, Eq)] +#[derive(Clone)] pub struct ProgramCore, Command: CommandTrait> { /// The ID of the program. id: ProgramID, /// A map of the declared imports for the program. imports: IndexMap, Import>, + /// A map of program labels to their program definitions. + components: IndexMap, ProgramDefinition>, /// An optional constructor for the program. constructor: Option>, - /// A map of identifiers to their program declaration. - identifiers: IndexMap, ProgramDefinition>, /// A map of the declared mappings for the program. mappings: IndexMap, Mapping>, /// A map of the declared structs for the program. @@ -138,6 +150,39 @@ pub struct ProgramCore, Command: Co functions: IndexMap, FunctionCore>, } +impl, Command: CommandTrait> PartialEq + for ProgramCore +{ + /// Compares two programs for equality, verifying that the components are in the same order. + /// The order of the components must match to ensure that deployment tree is well-formed. + fn eq(&self, other: &Self) -> bool { + // Check that the number of components is the same. + if self.components.len() != other.components.len() { + return false; + } + // Check that the components match in order. + for (left, right) in self.components.iter().zip_eq(other.components.iter()) { + if left != right { + return false; + } + } + // Check that the remaining fields match. + self.id == other.id + && self.imports == other.imports + && self.constructor == other.constructor + && self.mappings == other.mappings + && self.structs == other.structs + && self.records == other.records + && self.closures == other.closures + && self.functions == other.functions + } +} + +impl, Command: CommandTrait> Eq + for ProgramCore +{ +} + impl, Command: CommandTrait> ProgramCore { /// Initializes an empty program. #[inline] @@ -148,8 +193,8 @@ impl, Command: CommandTrait> Pro Ok(Self { id, imports: IndexMap::new(), + components: IndexMap::new(), constructor: None, - identifiers: IndexMap::new(), mappings: IndexMap::new(), structs: IndexMap::new(), records: IndexMap::new(), @@ -169,11 +214,6 @@ impl, Command: CommandTrait> Pro &self.id } - /// Returns the checksum of the program. - pub fn checksum(&self) -> Result> { - N::hash_bhp1024(&self.to_bytes_le()?.to_bits_le()) - } - /// Returns the imports in the program. pub const fn imports(&self) -> &IndexMap, Import> { &self.imports @@ -360,6 +400,10 @@ impl, Command: CommandTrait> Pro ensure!(self.constructor.is_none(), "Program already has a constructor."); // Ensure the number of commands is within the allowed range. ensure!(constructor.commands().len() <= N::MAX_COMMANDS, "Constructor exceeds maximum number of commands"); + // Add the constructor to the components. + if self.components.insert(ProgramLabel::Constructor, ProgramDefinition::Constructor).is_some() { + bail!("Constructor already exists in the program.") + } // Add the constructor to the program. self.constructor = Some(constructor); Ok(()) @@ -385,8 +429,8 @@ impl, Command: CommandTrait> Pro // Ensure the mapping name is not a reserved opcode. ensure!(!Self::is_reserved_opcode(&mapping_name.to_string()), "'{mapping_name}' is a reserved opcode."); - // Add the mapping name to the identifiers. - if self.identifiers.insert(mapping_name, ProgramDefinition::Mapping).is_some() { + // Add the mapping name to the components. + if self.components.insert(ProgramLabel::Identifier(mapping_name), ProgramDefinition::Mapping).is_some() { bail!("'{mapping_name}' already exists in the program.") } // Add the mapping to the program. @@ -446,8 +490,9 @@ impl, Command: CommandTrait> Pro } } - // Add the struct name to the identifiers. - if self.identifiers.insert(struct_name, ProgramDefinition::Struct).is_some() { + // Add the struct name to the components. + + if self.components.insert(ProgramLabel::Identifier(struct_name), ProgramDefinition::Struct).is_some() { bail!("'{}' already exists in the program.", struct_name) } // Add the struct to the program. @@ -503,8 +548,8 @@ impl, Command: CommandTrait> Pro } } - // Add the record name to the identifiers. - if self.identifiers.insert(record_name, ProgramDefinition::Record).is_some() { + // Add the record name to the components. + if self.components.insert(ProgramLabel::Identifier(record_name), ProgramDefinition::Record).is_some() { bail!("'{record_name}' already exists in the program.") } // Add the record to the program. @@ -551,8 +596,8 @@ impl, Command: CommandTrait> Pro // Ensure the number of outputs is within the allowed range. ensure!(closure.outputs().len() <= N::MAX_OUTPUTS, "Closure exceeds maximum number of outputs"); - // Add the function name to the identifiers. - if self.identifiers.insert(closure_name, ProgramDefinition::Closure).is_some() { + // Add the function name to the components. + if self.components.insert(ProgramLabel::Identifier(closure_name), ProgramDefinition::Closure).is_some() { bail!("'{closure_name}' already exists in the program.") } // Add the closure to the program. @@ -597,8 +642,8 @@ impl, Command: CommandTrait> Pro // Ensure the number of outputs is within the allowed range. ensure!(function.outputs().len() <= N::MAX_OUTPUTS, "Function exceeds maximum number of outputs"); - // Add the function name to the identifiers. - if self.identifiers.insert(function_name, ProgramDefinition::Function).is_some() { + // Add the function name to the components. + if self.components.insert(ProgramLabel::Identifier(function_name), ProgramDefinition::Function).is_some() { bail!("'{function_name}' already exists in the program.") } // Add the function to the program. @@ -609,6 +654,66 @@ impl, Command: CommandTrait> Pro } } +impl, Command: CommandTrait> ProgramCore { + /// Returns `true` if a program uses constructors, `Operand::Edition`, and `Operand::Checksum`. + /// This is enforced to be `false` for programs before `ConsensusVersion::V8`. + #[inline] + pub fn uses_constructor_checksum_or_edition(&self) -> bool { + // Check if the program contains a constructor. + if self.contains_constructor() { + return true; + } + // Check each instruction and output in each closure for the use of `Operand::Checksum` and `Operand::Edition`. + for closure in self.closures().values() { + // Check the instruction operands. + for instruction in closure.instructions() { + for operand in instruction.operands() { + if matches!(operand, Operand::Checksum(_) | Operand::Edition(_)) { + return true; + } + } + } + // Check the output operands. + for output in closure.outputs() { + if matches!(output.operand(), Operand::Checksum(_) | Operand::Edition(_)) { + return true; + } + } + } + // Check each instruction and output in each function for the use of `Operand::Checksum` and `Operand::Edition`. + // If the function has an associated finalize scope, then check its commands as well. + for function in self.functions().values() { + // Check the instruction oeprands. + for instruction in function.instructions() { + for operand in instruction.operands() { + if matches!(operand, Operand::Checksum(_) | Operand::Edition(_)) { + return true; + } + } + } + // Check the output operands. + for output in function.outputs() { + if matches!(output.operand(), Operand::Checksum(_) | Operand::Edition(_)) { + return true; + } + } + // Check the finalize scope if it exists. + if let Some(finalize_logic) = function.finalize_logic() { + // Check the command operands. + for command in finalize_logic.commands() { + for operand in command.operands() { + if matches!(operand, Operand::Checksum(_) | Operand::Edition(_)) { + return true; + } + } + } + } + } + // Return `false` since no V8 syntax was found. + false + } +} + impl, Command: CommandTrait> ProgramCore { /// A list of reserved keywords for Aleo programs, enforced at the parser level. // New keywords should be enforced through `RESTRICTED_KEYWORDS` instead, if possible. @@ -699,7 +804,7 @@ impl, Command: CommandTrait> Pro /// Returns `true` if the given name does not already exist in the program. fn is_unique_name(&self, name: &Identifier) -> bool { - !self.identifiers.contains_key(name) + !self.components.contains_key(&ProgramLabel::Identifier(*name)) } /// Returns `true` if the given name is a reserved opcode. @@ -739,9 +844,16 @@ impl, Command: CommandTrait> Pro bail!("Program name '{program_name}' is a restricted keyword for the current consensus version") } // Check that all top-level program components are not restricted keywords. - for identifier in self.identifiers.keys() { - if keywords.contains(identifier.to_string().as_str()) { - bail!("Program component '{identifier}' is a restricted keyword for the current consensus version") + for component in self.components.keys() { + match component { + ProgramLabel::Identifier(identifier) => { + if keywords.contains(identifier.to_string().as_str()) { + bail!( + "Program component '{identifier}' is a restricted keyword for the current consensus version" + ) + } + } + ProgramLabel::Constructor => continue, } } // Check that all record entry names are not restricted keywords. @@ -1055,4 +1167,35 @@ finalize check: assert!(program.contains_constructor()); assert_eq!(program.constructor().unwrap().commands().len(), 5); } + + #[test] + fn test_program_equality_and_checksum() { + fn run_test(program1: &str, program2: &str, expected_equal: bool) { + println!("Comparing programs:\n{}\n{}", program1, program2); + let program1 = Program::::from_str(program1).unwrap(); + let program2 = Program::::from_str(program2).unwrap(); + assert_eq!(program1 == program2, expected_equal); + assert_eq!(program1.to_checksum() == program2.to_checksum(), expected_equal); + } + + // Test two identical programs, with different whitespace. + run_test(r"program test.aleo; function dummy: ", r"program test.aleo; function dummy: ", true); + + // Test two programs, one with a different function name. + run_test(r"program test.aleo; function dummy: ", r"program test.aleo; function bummy: ", false); + + // Test two programs, one with a constructor and one without. + run_test( + r"program test.aleo; function dummy: ", + r"program test.aleo; constructor: assert.eq true true; function dummy: ", + false, + ); + + // Test two programs, both with a struct and function, but in different order. + run_test( + r"program test.aleo; struct foo: data as u8; function dummy:", + r"program test.aleo; function dummy: struct foo: data as u8;", + false, + ); + } } diff --git a/synthesizer/program/src/logic/command/await_.rs b/synthesizer/program/src/logic/command/await_.rs index 3c4a797880..696d24d9ee 100644 --- a/synthesizer/program/src/logic/command/await_.rs +++ b/synthesizer/program/src/logic/command/await_.rs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::Opcode; +use crate::{Opcode, Operand}; use console::{network::prelude::*, program::Register}; /// An await command, e.g. `await r0;`. @@ -21,8 +21,9 @@ use console::{network::prelude::*, program::Register}; /// Note that asynchronous calls currently do not return a value. #[derive(Clone, PartialEq, Eq, Hash)] pub struct Await { - /// The register containing the future. - register: Register, + /// The operands. + /// Note: Byte and string parsing guarantees that this is a single register operand. + operands: [Operand; 1], } impl Await { @@ -32,10 +33,21 @@ impl Await { Opcode::Command("await") } + /// Returns the operands in the command. + #[inline] + pub fn operands(&self) -> &[Operand] { + &self.operands + } + /// Returns the register containing the future. #[inline] - pub const fn register(&self) -> &Register { - &self.register + pub fn register(&self) -> &Register { + // Note: This byte and string parsing guarantees that the operand is a single register. + let Operand::Register(register) = &self.operands[0] else { + unreachable!("The operands of an await command must be a single register.") + }; + // Return the register. + register } } @@ -56,7 +68,7 @@ impl Parser for Await { // Parse the ';' from the string. let (string, _) = tag(";")(string)?; - Ok((string, Self { register })) + Ok((string, Self { operands: [Operand::Register(register)] })) } } @@ -89,7 +101,7 @@ impl Display for Await { /// Prints the command to a string. fn fmt(&self, f: &mut Formatter) -> fmt::Result { // Print the command. - write!(f, "await {};", self.register) + write!(f, "await {};", self.register()) } } @@ -99,7 +111,7 @@ impl FromBytes for Await { // Read the register. let register = Register::read_le(&mut reader)?; - Ok(Self { register }) + Ok(Self { operands: [Operand::Register(register)] }) } } @@ -107,7 +119,7 @@ impl ToBytes for Await { /// Writes the operation to a buffer. fn write_le(&self, mut writer: W) -> IoResult<()> { // Write the register. - self.register.write_le(&mut writer)?; + self.register().write_le(&mut writer)?; Ok(()) } } diff --git a/synthesizer/program/src/logic/command/branch.rs b/synthesizer/program/src/logic/command/branch.rs index 7ed7258030..f02bd5e497 100644 --- a/synthesizer/program/src/logic/command/branch.rs +++ b/synthesizer/program/src/logic/command/branch.rs @@ -29,10 +29,8 @@ enum Variant { /// Compares `first` and `second` and jumps to `position`, if the condition is met. #[derive(Clone, PartialEq, Eq, Hash)] pub struct Branch { - /// The first operand. - first: Operand, - /// The second operand. - second: Operand, + /// The operands. + operands: [Operand; 2], /// The position. position: Identifier, } @@ -48,21 +46,27 @@ impl Branch { } } + /// Returns the operands. + #[inline] + pub const fn operands(&self) -> &[Operand] { + &self.operands + } + /// Returns the first operand. #[inline] - pub fn first(&self) -> &Operand { - &self.first + pub const fn first(&self) -> &Operand { + &self.operands[0] } /// Returns the second operand. #[inline] - pub fn second(&self) -> &Operand { - &self.second + pub const fn second(&self) -> &Operand { + &self.operands[1] } /// Returns the position. #[inline] - pub fn position(&self) -> &Identifier { + pub const fn position(&self) -> &Identifier { &self.position } } @@ -100,7 +104,7 @@ impl Parser for Branch { // Parse the ";" from the string. let (string, _) = tag(";")(string)?; - Ok((string, Self { first, second, position })) + Ok((string, Self { operands: [first, second], position })) } } @@ -133,7 +137,7 @@ impl Display for Branch { /// Prints the command to a string. fn fmt(&self, f: &mut Formatter) -> fmt::Result { // Print the command. - write!(f, "{} {} {} to {};", Self::opcode(), self.first, self.second, self.position) + write!(f, "{} {} {} to {};", Self::opcode(), self.first(), self.second(), self.position) } } @@ -148,7 +152,7 @@ impl FromBytes for Branch { let position = Identifier::read_le(&mut reader)?; // Return the command. - Ok(Self { first, second, position }) + Ok(Self { operands: [first, second], position }) } } @@ -156,9 +160,9 @@ impl ToBytes for Branch { /// Writes the command to a buffer. fn write_le(&self, mut writer: W) -> IoResult<()> { // Write the first operand. - self.first.write_le(&mut writer)?; + self.first().write_le(&mut writer)?; // Write the second operand. - self.second.write_le(&mut writer)?; + self.second().write_le(&mut writer)?; // Write the position. self.position.write_le(&mut writer) } @@ -178,14 +182,14 @@ mod tests { fn test_parse() { let (string, branch) = BranchEq::::parse("branch.eq r0 r1 to exit;").unwrap(); assert!(string.is_empty(), "Parser did not consume all of the string: '{string}'"); - assert_eq!(branch.first, Operand::Register(Register::Locator(0)), "The first operand is incorrect"); - assert_eq!(branch.second, Operand::Register(Register::Locator(1)), "The second operand is incorrect"); + assert_eq!(branch.first(), &Operand::Register(Register::Locator(0)), "The first operand is incorrect"); + assert_eq!(branch.second(), &Operand::Register(Register::Locator(1)), "The second operand is incorrect"); assert_eq!(branch.position, Identifier::from_str("exit").unwrap(), "The position is incorrect"); let (string, branch) = BranchNeq::::parse("branch.neq r3 r4 to start;").unwrap(); assert!(string.is_empty(), "Parser did not consume all of the string: '{string}'"); - assert_eq!(branch.first, Operand::Register(Register::Locator(3)), "The first operand is incorrect"); - assert_eq!(branch.second, Operand::Register(Register::Locator(4)), "The second operand is incorrect"); + assert_eq!(branch.first(), &Operand::Register(Register::Locator(3)), "The first operand is incorrect"); + assert_eq!(branch.second(), &Operand::Register(Register::Locator(4)), "The second operand is incorrect"); assert_eq!(branch.position, Identifier::from_str("start").unwrap(), "The position is incorrect"); } } diff --git a/synthesizer/program/src/logic/command/contains.rs b/synthesizer/program/src/logic/command/contains.rs index 2d6d52f795..173a9b8da0 100644 --- a/synthesizer/program/src/logic/command/contains.rs +++ b/synthesizer/program/src/logic/command/contains.rs @@ -31,8 +31,8 @@ use console::{ pub struct Contains { /// The mapping name. mapping: CallOperator, - /// The key to access the mapping. - key: Operand, + /// The operands. + operands: [Operand; 1], /// The destination register. destination: Register, } @@ -46,8 +46,8 @@ impl Contains { /// Returns the operands in the operation. #[inline] - pub fn operands(&self) -> Vec> { - vec![self.key.clone()] + pub fn operands(&self) -> &[Operand] { + &self.operands } /// Returns the mapping. @@ -59,7 +59,7 @@ impl Contains { /// Returns the operand containing the key. #[inline] pub const fn key(&self) -> &Operand { - &self.key + &self.operands[0] } /// Returns the destination register. @@ -90,7 +90,7 @@ impl Contains { } // Load the operand as a plaintext. - let key = registers.load_plaintext(stack, &self.key)?; + let key = registers.load_plaintext(stack, self.key())?; // Determine if the key exists in the mapping. let contains_key = store.contains_key_speculative(program_id, mapping_name, &key)?; @@ -140,7 +140,7 @@ impl Parser for Contains { // Parse the ";" from the string. let (string, _) = tag(";")(string)?; - Ok((string, Self { mapping, key, destination })) + Ok((string, Self { mapping, operands: [key], destination })) } } @@ -175,7 +175,7 @@ impl Display for Contains { // Print the command. write!(f, "{} ", Self::opcode())?; // Print the mapping and key operand. - write!(f, "{}[{}] into ", self.mapping, self.key)?; + write!(f, "{}[{}] into ", self.mapping, self.key())?; // Print the destination register. write!(f, "{};", self.destination) } @@ -191,7 +191,7 @@ impl FromBytes for Contains { // Read the destination register. let destination = Register::read_le(&mut reader)?; // Return the command. - Ok(Self { mapping, key, destination }) + Ok(Self { mapping, operands: [key], destination }) } } @@ -201,7 +201,7 @@ impl ToBytes for Contains { // Write the mapping name. self.mapping.write_le(&mut writer)?; // Write the key operand. - self.key.write_le(&mut writer)?; + self.key().write_le(&mut writer)?; // Write the destination register. self.destination.write_le(&mut writer) } @@ -220,7 +220,7 @@ mod tests { assert!(string.is_empty(), "Parser did not consume all of the string: '{string}'"); assert_eq!(contains.mapping, CallOperator::from_str("account").unwrap()); assert_eq!(contains.operands().len(), 1, "The number of operands is incorrect"); - assert_eq!(contains.key, Operand::Register(Register::Locator(0)), "The first operand is incorrect"); + assert_eq!(contains.key(), &Operand::Register(Register::Locator(0)), "The first operand is incorrect"); assert_eq!(contains.destination, Register::Locator(1), "The second operand is incorrect"); let (string, contains) = @@ -228,7 +228,7 @@ mod tests { assert!(string.is_empty(), "Parser did not consume all of the string: '{string}'"); assert_eq!(contains.mapping, CallOperator::from_str("credits.aleo/account").unwrap()); assert_eq!(contains.operands().len(), 1, "The number of operands is incorrect"); - assert_eq!(contains.key, Operand::Register(Register::Locator(0)), "The first operand is incorrect"); + assert_eq!(contains.key(), &Operand::Register(Register::Locator(0)), "The first operand is incorrect"); assert_eq!(contains.destination, Register::Locator(1), "The second operand is incorrect"); } diff --git a/synthesizer/program/src/logic/command/get.rs b/synthesizer/program/src/logic/command/get.rs index 661016c671..98f6e3f90e 100644 --- a/synthesizer/program/src/logic/command/get.rs +++ b/synthesizer/program/src/logic/command/get.rs @@ -30,8 +30,8 @@ use console::{ pub struct Get { /// The mapping. mapping: CallOperator, - /// The key to access the mapping. - key: Operand, + /// The operands. + operands: [Operand; 1], /// The destination register. destination: Register, } @@ -40,7 +40,7 @@ impl PartialEq for Get { /// Returns true if the two objects are equal. #[inline] fn eq(&self, other: &Self) -> bool { - self.mapping == other.mapping && self.key == other.key && self.destination == other.destination + self.mapping == other.mapping && self.key() == other.key() && self.destination == other.destination } } @@ -51,7 +51,7 @@ impl std::hash::Hash for Get { #[inline] fn hash(&self, state: &mut H) { self.mapping.hash(state); - self.key.hash(state); + self.key().hash(state); self.destination.hash(state); } } @@ -65,8 +65,8 @@ impl Get { /// Returns the operands in the operation. #[inline] - pub fn operands(&self) -> Vec> { - vec![self.key.clone()] + pub fn operands(&self) -> &[Operand] { + &self.operands } /// Returns the mapping. @@ -78,7 +78,7 @@ impl Get { /// Returns the operand containing the key. #[inline] pub const fn key(&self) -> &Operand { - &self.key + &self.operands[0] } /// Returns the destination register. @@ -109,7 +109,7 @@ impl Get { } // Load the operand as a plaintext. - let key = registers.load_plaintext(stack, &self.key)?; + let key = registers.load_plaintext(stack, self.key())?; // Retrieve the value from storage as a literal. let value = match store.get_value_speculative(program_id, mapping_name, &key)? { @@ -165,7 +165,7 @@ impl Parser for Get { // Parse the ";" from the string. let (string, _) = tag(";")(string)?; - Ok((string, Self { mapping, key, destination })) + Ok((string, Self { mapping, operands: [key], destination })) } } @@ -200,7 +200,7 @@ impl Display for Get { // Print the command. write!(f, "{} ", Self::opcode())?; // Print the mapping and key operand. - write!(f, "{}[{}] into ", self.mapping, self.key)?; + write!(f, "{}[{}] into ", self.mapping, self.key())?; // Print the destination register. write!(f, "{};", self.destination) } @@ -216,7 +216,7 @@ impl FromBytes for Get { // Read the destination register. let destination = Register::read_le(&mut reader)?; // Return the command. - Ok(Self { mapping, key, destination }) + Ok(Self { mapping, operands: [key], destination }) } } @@ -226,7 +226,7 @@ impl ToBytes for Get { // Write the mapping name. self.mapping.write_le(&mut writer)?; // Write the key operand. - self.key.write_le(&mut writer)?; + self.key().write_le(&mut writer)?; // Write the destination register. self.destination.write_le(&mut writer) } @@ -245,14 +245,14 @@ mod tests { assert!(string.is_empty(), "Parser did not consume all of the string: '{string}'"); assert_eq!(get.mapping, CallOperator::from_str("account").unwrap()); assert_eq!(get.operands().len(), 1, "The number of operands is incorrect"); - assert_eq!(get.key, Operand::Register(Register::Locator(0)), "The first operand is incorrect"); + assert_eq!(get.key(), &Operand::Register(Register::Locator(0)), "The first operand is incorrect"); assert_eq!(get.destination, Register::Locator(1), "The second operand is incorrect"); let (string, get) = Get::::parse("get token.aleo/balances[r0] into r1;").unwrap(); assert!(string.is_empty(), "Parser did not consume all of the string: '{string}'"); assert_eq!(get.mapping, CallOperator::from_str("token.aleo/balances").unwrap()); assert_eq!(get.operands().len(), 1, "The number of operands is incorrect"); - assert_eq!(get.key, Operand::Register(Register::Locator(0)), "The first operand is incorrect"); + assert_eq!(get.key(), &Operand::Register(Register::Locator(0)), "The first operand is incorrect"); assert_eq!(get.destination, Register::Locator(1), "The second operand is incorrect"); } diff --git a/synthesizer/program/src/logic/command/get_or_use.rs b/synthesizer/program/src/logic/command/get_or_use.rs index b7eff041b9..96e8231c1f 100644 --- a/synthesizer/program/src/logic/command/get_or_use.rs +++ b/synthesizer/program/src/logic/command/get_or_use.rs @@ -31,10 +31,8 @@ use console::{ pub struct GetOrUse { /// The mapping. mapping: CallOperator, - /// The key to access the mapping. - key: Operand, - /// The default value. - default: Operand, + /// The operands. + operands: [Operand; 2], /// The destination register. destination: Register, } @@ -43,8 +41,8 @@ impl PartialEq for GetOrUse { #[inline] fn eq(&self, other: &Self) -> bool { self.mapping == other.mapping - && self.key == other.key - && self.default == other.default + && self.key() == other.key() + && self.default() == other.default() && self.destination == other.destination } } @@ -55,8 +53,8 @@ impl std::hash::Hash for GetOrUse { #[inline] fn hash(&self, state: &mut H) { self.mapping.hash(state); - self.key.hash(state); - self.default.hash(state); + self.key().hash(state); + self.default().hash(state); self.destination.hash(state); } } @@ -70,8 +68,8 @@ impl GetOrUse { /// Returns the operands in the operation. #[inline] - pub fn operands(&self) -> Vec> { - vec![self.key.clone(), self.default.clone()] + pub fn operands(&self) -> &[Operand] { + &self.operands } /// Returns the mapping. @@ -83,13 +81,13 @@ impl GetOrUse { /// Returns the operand containing the key. #[inline] pub const fn key(&self) -> &Operand { - &self.key + &self.operands[0] } /// Returns the default value. #[inline] pub const fn default(&self) -> &Operand { - &self.default + &self.operands[1] } /// Returns the destination register. @@ -120,7 +118,7 @@ impl GetOrUse { } // Load the operand as a plaintext. - let key = registers.load_plaintext(stack, &self.key)?; + let key = registers.load_plaintext(stack, self.key())?; // Retrieve the value from storage as a literal. let value = match store.get_value_speculative(program_id, mapping_name, &key)? { @@ -128,7 +126,7 @@ impl GetOrUse { Some(Value::Record(..)) => bail!("Cannot 'get.or_use' a 'record'"), Some(Value::Future(..)) => bail!("Cannot 'get.or_use' a 'future'"), // If a key does not exist, then use the default value. - None => Value::Plaintext(registers.load_plaintext(stack, &self.default)?), + None => Value::Plaintext(registers.load_plaintext(stack, self.default())?), }; // Assign the value to the destination register. @@ -181,7 +179,7 @@ impl Parser for GetOrUse { // Parse the ";" from the string. let (string, _) = tag(";")(string)?; - Ok((string, Self { mapping, key, default, destination })) + Ok((string, Self { mapping, operands: [key, default], destination })) } } @@ -216,7 +214,7 @@ impl Display for GetOrUse { // Print the command. write!(f, "{} ", Self::opcode())?; // Print the mapping and key operand. - write!(f, "{}[{}] {} into ", self.mapping, self.key, self.default)?; + write!(f, "{}[{}] {} into ", self.mapping, self.key(), self.default())?; // Print the destination register. write!(f, "{};", self.destination) } @@ -234,7 +232,7 @@ impl FromBytes for GetOrUse { // Read the destination register. let destination = Register::read_le(&mut reader)?; // Return the command. - Ok(Self { mapping, key, default, destination }) + Ok(Self { mapping, operands: [key, default], destination }) } } @@ -244,9 +242,9 @@ impl ToBytes for GetOrUse { // Write the mapping name. self.mapping.write_le(&mut writer)?; // Write the key operand. - self.key.write_le(&mut writer)?; + self.key().write_le(&mut writer)?; // Write the default value. - self.default.write_le(&mut writer)?; + self.default().write_le(&mut writer)?; // Write the destination register. self.destination.write_le(&mut writer) } @@ -265,8 +263,8 @@ mod tests { assert!(string.is_empty(), "Parser did not consume all of the string: '{string}'"); assert_eq!(get_or_use.mapping, CallOperator::from_str("account").unwrap()); assert_eq!(get_or_use.operands().len(), 2, "The number of operands is incorrect"); - assert_eq!(get_or_use.key, Operand::Register(Register::Locator(0)), "The first operand is incorrect"); - assert_eq!(get_or_use.default, Operand::Register(Register::Locator(1)), "The second operand is incorrect"); + assert_eq!(get_or_use.key(), &Operand::Register(Register::Locator(0)), "The first operand is incorrect"); + assert_eq!(get_or_use.default(), &Operand::Register(Register::Locator(1)), "The second operand is incorrect"); assert_eq!(get_or_use.destination, Register::Locator(2), "The second operand is incorrect"); let (string, get_or_use) = @@ -274,8 +272,8 @@ mod tests { assert!(string.is_empty(), "Parser did not consume all of the string: '{string}'"); assert_eq!(get_or_use.mapping, CallOperator::from_str("token.aleo/balances").unwrap()); assert_eq!(get_or_use.operands().len(), 2, "The number of operands is incorrect"); - assert_eq!(get_or_use.key, Operand::Register(Register::Locator(0)), "The first operand is incorrect"); - assert_eq!(get_or_use.default, Operand::Register(Register::Locator(1)), "The second operand is incorrect"); + assert_eq!(get_or_use.key(), &Operand::Register(Register::Locator(0)), "The first operand is incorrect"); + assert_eq!(get_or_use.default(), &Operand::Register(Register::Locator(1)), "The second operand is incorrect"); assert_eq!(get_or_use.destination, Register::Locator(2), "The second operand is incorrect"); } diff --git a/synthesizer/program/src/logic/command/mod.rs b/synthesizer/program/src/logic/command/mod.rs index aa3b5be58d..c2a4cc76b9 100644 --- a/synthesizer/program/src/logic/command/mod.rs +++ b/synthesizer/program/src/logic/command/mod.rs @@ -45,6 +45,7 @@ use crate::{ FinalizeOperation, FinalizeRegistersState, Instruction, + Operand, traits::{ CommandTrait, FinalizeStoreTrait, @@ -88,24 +89,6 @@ pub enum Command { } impl CommandTrait for Command { - /// Returns the destination registers of the command. - #[inline] - fn destinations(&self) -> Vec> { - match self { - Command::Instruction(instruction) => instruction.destinations(), - Command::Contains(contains) => vec![contains.destination().clone()], - Command::Get(get) => vec![get.destination().clone()], - Command::GetOrUse(get_or_use) => vec![get_or_use.destination().clone()], - Command::RandChaCha(rand_chacha) => vec![rand_chacha.destination().clone()], - Command::Await(_) - | Command::BranchEq(_) - | Command::BranchNeq(_) - | Command::Position(_) - | Command::Remove(_) - | Command::Set(_) => vec![], - } - } - /// Returns the branch target, if the command is a branch command. /// Otherwise, returns `None`. #[inline] @@ -149,6 +132,42 @@ impl CommandTrait for Command { fn is_await(&self) -> bool { matches!(self, Command::Await(_)) } + + /// Returns the operands of the command. + #[inline] + fn operands(&self) -> &[Operand] { + match self { + Command::Instruction(c) => c.operands(), + Command::Await(c) => c.operands(), + Command::Contains(c) => c.operands(), + Command::Get(c) => c.operands(), + Command::GetOrUse(c) => c.operands(), + Command::RandChaCha(c) => c.operands(), + Command::Remove(c) => c.operands(), + Command::Set(c) => c.operands(), + Command::BranchEq(c) => c.operands(), + Command::BranchNeq(c) => c.operands(), + Command::Position(_) => Default::default(), + } + } + + /// Returns the destination registers of the command. + #[inline] + fn destinations(&self) -> Vec> { + match self { + Command::Instruction(instruction) => instruction.destinations(), + Command::Contains(contains) => vec![contains.destination().clone()], + Command::Get(get) => vec![get.destination().clone()], + Command::GetOrUse(get_or_use) => vec![get_or_use.destination().clone()], + Command::RandChaCha(rand_chacha) => vec![rand_chacha.destination().clone()], + Command::Await(_) + | Command::BranchEq(_) + | Command::BranchNeq(_) + | Command::Position(_) + | Command::Remove(_) + | Command::Set(_) => vec![], + } + } } impl Command { diff --git a/synthesizer/program/src/logic/command/rand_chacha.rs b/synthesizer/program/src/logic/command/rand_chacha.rs index df66ec0018..fc6eaecf15 100644 --- a/synthesizer/program/src/logic/command/rand_chacha.rs +++ b/synthesizer/program/src/logic/command/rand_chacha.rs @@ -55,8 +55,8 @@ impl RandChaCha { /// Returns the operands in the operation. #[inline] - pub fn operands(&self) -> Vec> { - self.operands.clone() + pub fn operands(&self) -> &[Operand] { + &self.operands } /// Returns the destination register. diff --git a/synthesizer/program/src/logic/command/remove.rs b/synthesizer/program/src/logic/command/remove.rs index 822d7233ab..750426bb29 100644 --- a/synthesizer/program/src/logic/command/remove.rs +++ b/synthesizer/program/src/logic/command/remove.rs @@ -27,8 +27,8 @@ use console::{network::prelude::*, program::Identifier}; pub struct Remove { /// The mapping name. mapping: Identifier, - /// The key to access the mapping. - key: Operand, + /// The operands + operands: [Operand; 1], } impl Remove { @@ -40,8 +40,8 @@ impl Remove { /// Returns the operands in the operation. #[inline] - pub fn operands(&self) -> Vec> { - vec![self.key.clone()] + pub fn operands(&self) -> &[Operand] { + &self.operands } /// Returns the mapping name. @@ -53,7 +53,7 @@ impl Remove { /// Returns the operand containing the key. #[inline] pub const fn key(&self) -> &Operand { - &self.key + &self.operands[0] } } @@ -72,7 +72,7 @@ impl Remove { } // Load the key operand as a plaintext. - let key = registers.load_plaintext(stack, &self.key)?; + let key = registers.load_plaintext(stack, self.key())?; // Update the value in storage, and return the finalize operation. store.remove_key_value(*stack.program_id(), self.mapping, &key) } @@ -106,7 +106,7 @@ impl Parser for Remove { // Parse the ";" from the string. let (string, _) = tag(";")(string)?; - Ok((string, Self { mapping, key })) + Ok((string, Self { mapping, operands: [key] })) } } @@ -139,7 +139,7 @@ impl Display for Remove { /// Prints the command to a string. fn fmt(&self, f: &mut Formatter) -> fmt::Result { // Print the command, mapping, and key operand. - write!(f, "{} {}[{}];", Self::opcode(), self.mapping, self.key) + write!(f, "{} {}[{}];", Self::opcode(), self.mapping, self.key()) } } @@ -151,7 +151,7 @@ impl FromBytes for Remove { // Read the key operand. let key = Operand::read_le(&mut reader)?; // Return the command. - Ok(Self { mapping, key }) + Ok(Self { mapping, operands: [key] }) } } @@ -161,7 +161,7 @@ impl ToBytes for Remove { // Write the mapping name. self.mapping.write_le(&mut writer)?; // Write the key operand. - self.key.write_le(&mut writer) + self.key().write_le(&mut writer) } } @@ -178,6 +178,6 @@ mod tests { assert!(string.is_empty(), "Parser did not consume all of the string: '{string}'"); assert_eq!(remove.mapping, Identifier::from_str("account").unwrap()); assert_eq!(remove.operands().len(), 1, "The number of operands is incorrect"); - assert_eq!(remove.key, Operand::Register(Register::Locator(1)), "The first operand is incorrect"); + assert_eq!(remove.key(), &Operand::Register(Register::Locator(1)), "The first operand is incorrect"); } } diff --git a/synthesizer/program/src/logic/command/set.rs b/synthesizer/program/src/logic/command/set.rs index 87d9cbd28e..b2b4bb2fc0 100644 --- a/synthesizer/program/src/logic/command/set.rs +++ b/synthesizer/program/src/logic/command/set.rs @@ -30,10 +30,8 @@ use console::{ pub struct Set { /// The mapping name. mapping: Identifier, - /// The key to access the mapping. - key: Operand, - /// The value to be set. - value: Operand, + /// The operands. + operands: [Operand; 2], } impl Set { @@ -45,8 +43,8 @@ impl Set { /// Returns the operands in the operation. #[inline] - pub fn operands(&self) -> Vec> { - vec![self.value.clone(), self.key.clone()] + pub fn operands(&self) -> &[Operand] { + &self.operands } /// Returns the mapping name. @@ -58,13 +56,13 @@ impl Set { /// Returns the operand containing the key. #[inline] pub const fn key(&self) -> &Operand { - &self.key + &self.operands[0] } /// Returns the operand containing the value. #[inline] pub const fn value(&self) -> &Operand { - &self.value + &self.operands[1] } } @@ -83,9 +81,9 @@ impl Set { } // Load the key operand as a plaintext. - let key = registers.load_plaintext(stack, &self.key)?; + let key = registers.load_plaintext(stack, self.key())?; // Load the value operand as a plaintext. - let value = Value::Plaintext(registers.load_plaintext(stack, &self.value)?); + let value = Value::Plaintext(registers.load_plaintext(stack, self.value())?); // Update the value in storage, and return the finalize operation. store.update_key_value(*stack.program_id(), self.mapping, key, value) @@ -130,7 +128,7 @@ impl Parser for Set { // Parse the ";" from the string. let (string, _) = tag(";")(string)?; - Ok((string, Self { mapping, key, value })) + Ok((string, Self { mapping, operands: [key, value] })) } } @@ -165,9 +163,9 @@ impl Display for Set { // Print the command. write!(f, "{} ", Self::opcode())?; // Print the value operand. - write!(f, "{} into ", self.value)?; + write!(f, "{} into ", self.value())?; // Print the mapping and key operand. - write!(f, "{}[{}];", self.mapping, self.key) + write!(f, "{}[{}];", self.mapping, self.key()) } } @@ -181,7 +179,7 @@ impl FromBytes for Set { // Read the value operand. let value = Operand::read_le(&mut reader)?; // Return the command. - Ok(Self { mapping, key, value }) + Ok(Self { mapping, operands: [key, value] }) } } @@ -191,9 +189,9 @@ impl ToBytes for Set { // Write the mapping name. self.mapping.write_le(&mut writer)?; // Write the key operand. - self.key.write_le(&mut writer)?; + self.key().write_le(&mut writer)?; // Write the value operand. - self.value.write_le(&mut writer) + self.value().write_le(&mut writer) } } @@ -210,7 +208,7 @@ mod tests { assert!(string.is_empty(), "Parser did not consume all of the string: '{string}'"); assert_eq!(set.mapping, Identifier::from_str("account").unwrap()); assert_eq!(set.operands().len(), 2, "The number of operands is incorrect"); - assert_eq!(set.value, Operand::Register(Register::Locator(0)), "The first operand is incorrect"); - assert_eq!(set.key, Operand::Register(Register::Locator(1)), "The second operand is incorrect"); + assert_eq!(set.value(), &Operand::Register(Register::Locator(0)), "The first operand is incorrect"); + assert_eq!(set.key(), &Operand::Register(Register::Locator(1)), "The second operand is incorrect"); } } diff --git a/synthesizer/program/src/logic/instruction/mod.rs b/synthesizer/program/src/logic/instruction/mod.rs index 797e95522d..4e00b25cab 100644 --- a/synthesizer/program/src/logic/instruction/mod.rs +++ b/synthesizer/program/src/logic/instruction/mod.rs @@ -373,18 +373,24 @@ macro_rules! opcodes { } impl InstructionTrait for Instruction { - /// Returns the destination registers of the instruction. - #[inline] - fn destinations(&self) -> Vec> { - instruction!(self, |instruction| instruction.destinations()) - } - /// Returns `true` if the given name is a reserved opcode. #[inline] fn is_reserved_opcode(name: &str) -> bool { // Check if the given name matches any opcode (in its entirety; including past the first '.' if it exists). Instruction::::OPCODES.iter().any(|opcode| **opcode == name) } + + /// Returns the operands of the instruction. + #[inline] + fn operands(&self) -> &[Operand] { + instruction!(self, |instruction| instruction.operands()) + } + + /// Returns the destination registers of the instruction. + #[inline] + fn destinations(&self) -> Vec> { + instruction!(self, |instruction| instruction.destinations()) + } } impl Instruction { @@ -397,12 +403,6 @@ impl Instruction { instruction!(self, |InstructionMember| InstructionMember::::opcode()) } - /// Returns the operands of the instruction. - #[inline] - pub fn operands(&self) -> &[Operand] { - instruction!(self, |instruction| instruction.operands()) - } - /// Evaluates the instruction. #[inline] pub fn evaluate( diff --git a/synthesizer/program/src/parse.rs b/synthesizer/program/src/parse.rs index 6996164fed..8f7da42b64 100644 --- a/synthesizer/program/src/parse.rs +++ b/synthesizer/program/src/parse.rs @@ -167,37 +167,43 @@ impl, Command: CommandTrait> Dis // Print the program name. write!(f, "{} {};\n\n", Self::type_name(), self.id)?; - // Write the constructor, if it exists. - if let Some(constructor) = &self.constructor { - writeln!(f, "{constructor}\n")?; - } - - let mut identifier_iter = self.identifiers.iter().peekable(); - while let Some((identifier, definition)) = identifier_iter.next() { - match definition { - ProgramDefinition::Mapping => match self.mappings.get(identifier) { - Some(mapping) => writeln!(f, "{mapping}")?, - None => return Err(fmt::Error), - }, - ProgramDefinition::Struct => match self.structs.get(identifier) { - Some(struct_) => writeln!(f, "{struct_}")?, - None => return Err(fmt::Error), - }, - ProgramDefinition::Record => match self.records.get(identifier) { - Some(record) => writeln!(f, "{record}")?, - None => return Err(fmt::Error), - }, - ProgramDefinition::Closure => match self.closures.get(identifier) { - Some(closure) => writeln!(f, "{closure}")?, - None => return Err(fmt::Error), - }, - ProgramDefinition::Function => match self.functions.get(identifier) { - Some(function) => writeln!(f, "{function}")?, - None => return Err(fmt::Error), + // Write the components. + let mut components_iter = self.components.iter().peekable(); + while let Some((label, definition)) = components_iter.next() { + match label { + ProgramLabel::Constructor => { + // Write the constructor, if it exists. + if let Some(constructor) = &self.constructor { + writeln!(f, "{constructor}")?; + } + } + ProgramLabel::Identifier(identifier) => match definition { + ProgramDefinition::Constructor => return Err(fmt::Error), + ProgramDefinition::Mapping => match self.mappings.get(identifier) { + Some(mapping) => writeln!(f, "{mapping}")?, + None => return Err(fmt::Error), + }, + ProgramDefinition::Struct => match self.structs.get(identifier) { + Some(struct_) => writeln!(f, "{struct_}")?, + None => return Err(fmt::Error), + }, + ProgramDefinition::Record => match self.records.get(identifier) { + Some(record) => writeln!(f, "{record}")?, + None => return Err(fmt::Error), + }, + ProgramDefinition::Closure => match self.closures.get(identifier) { + Some(closure) => writeln!(f, "{closure}")?, + None => return Err(fmt::Error), + }, + ProgramDefinition::Function => match self.functions.get(identifier) { + Some(function) => writeln!(f, "{function}")?, + None => return Err(fmt::Error), + }, }, } + // Omit the last newline. - if identifier_iter.peek().is_some() { + if components_iter.peek().is_some() { writeln!(f)?; } } diff --git a/synthesizer/program/src/to_checksum.rs b/synthesizer/program/src/to_checksum.rs new file mode 100644 index 0000000000..92405b76e7 --- /dev/null +++ b/synthesizer/program/src/to_checksum.rs @@ -0,0 +1,31 @@ +// Copyright (c) 2019-2025 Provable Inc. +// This file is part of the snarkVM library. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::*; + +impl, Command: CommandTrait> ProgramCore { + /// Returns the checksum of the program. + /// + /// The checksum is a 32-byte hash of the program's source code in string format. + /// This ensures a strict definition of program equivalence, useful for program upgradability. + pub fn to_checksum(&self) -> [U8; 32] { + let mut keccak = TinySha3::v256(); + keccak.update(self.to_string().as_bytes()); + + let mut hash = [0u8; 32]; + keccak.finalize(&mut hash); + hash.map(U8::new) + } +} diff --git a/synthesizer/program/src/traits/command.rs b/synthesizer/program/src/traits/command.rs index b943ddff68..73411295f2 100644 --- a/synthesizer/program/src/traits/command.rs +++ b/synthesizer/program/src/traits/command.rs @@ -13,15 +13,14 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::Operand; use console::{ network::Network, prelude::{FromBytes, Parser, ToBytes}, program::{Identifier, Register}, }; -pub trait CommandTrait: Clone + Parser + FromBytes + ToBytes { - /// Returns the destination registers of the command. - fn destinations(&self) -> Vec>; +pub trait CommandTrait: Clone + PartialEq + Eq + Parser + FromBytes + ToBytes { /// Returns the branch target, if the command is a branch command. fn branch_to(&self) -> Option<&Identifier>; /// Returns the position name, if the command is a position command. @@ -34,4 +33,8 @@ pub trait CommandTrait: Clone + Parser + FromBytes + ToBytes { fn is_write(&self) -> bool; /// Returns `true` if the command is an await command. fn is_await(&self) -> bool; + /// Returns the operands of the command. + fn operands(&self) -> &[Operand]; + /// Returns the destination registers of the command. + fn destinations(&self) -> Vec>; } diff --git a/synthesizer/program/src/traits/instruction.rs b/synthesizer/program/src/traits/instruction.rs index c5b26bc52d..c72394769c 100644 --- a/synthesizer/program/src/traits/instruction.rs +++ b/synthesizer/program/src/traits/instruction.rs @@ -13,15 +13,18 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::Operand; use console::{ network::Network, prelude::{FromBytes, Parser, ToBytes}, program::Register, }; -pub trait InstructionTrait: Clone + Parser + FromBytes + ToBytes { - /// Returns the destination registers of the instruction. - fn destinations(&self) -> Vec>; +pub trait InstructionTrait: Clone + PartialEq + Eq + Parser + FromBytes + ToBytes { /// Returns `true` if the given name is a reserved opcode. fn is_reserved_opcode(name: &str) -> bool; + /// Returns the operands of the instruction. + fn operands(&self) -> &[Operand]; + /// Returns the destination registers of the instruction. + fn destinations(&self) -> Vec>; } diff --git a/synthesizer/program/src/traits/stack_and_registers.rs b/synthesizer/program/src/traits/stack_and_registers.rs index 6a8681c6d9..7fef6b2474 100644 --- a/synthesizer/program/src/traits/stack_and_registers.rs +++ b/synthesizer/program/src/traits/stack_and_registers.rs @@ -34,7 +34,7 @@ use console::{ Value, ValueType, }, - types::{Address, Field, U16}, + types::{Address, Field, U8, U16}, }; use rand::{CryptoRng, Rng}; use synthesizer_snark::{ProvingKey, VerifyingKey}; @@ -96,7 +96,7 @@ pub trait StackProgram { fn program_address(&self) -> &Address; /// Returns the program checksum. - fn program_checksum(&self) -> &Field; + fn program_checksum(&self) -> &[U8; 32]; /// Returns the program edition. fn program_edition(&self) -> &U16; diff --git a/synthesizer/src/vm/deploy.rs b/synthesizer/src/vm/deploy.rs index 3f0d03fb22..31138145ee 100644 --- a/synthesizer/src/vm/deploy.rs +++ b/synthesizer/src/vm/deploy.rs @@ -32,9 +32,15 @@ impl> VM { rng: &mut R, ) -> Result> { // Compute the deployment. - let deployment = self.deploy_raw(program, rng)?; + let mut deployment = self.deploy_raw(program, rng)?; // Ensure the transaction is not empty. ensure!(!deployment.program().functions().is_empty(), "Attempted to create an empty transaction deployment"); + // Unset the checksum if the `CONSENSUS_VERSION` is less than `V8`. + let query = query.clone().unwrap_or(Query::VM(self.block_store().clone())); + let consensus_version = N::CONSENSUS_VERSION(query.current_block_height()?)?; + if consensus_version < ConsensusVersion::V8 { + deployment.set_program_checksum_raw(None) + } // Compute the deployment ID. let deployment_id = deployment.to_deployment_id()?; // Construct the owner. @@ -61,7 +67,7 @@ impl> VM { )?, }; // Compute the fee. - let fee = self.execute_fee_authorization(fee_authorization, query, rng)?; + let fee = self.execute_fee_authorization(fee_authorization, Some(query), rng)?; // Return the deploy transaction. Transaction::from_deployment(owner, deployment, fee) diff --git a/synthesizer/src/vm/finalize.rs b/synthesizer/src/vm/finalize.rs index ff02b8ab94..36ad74bdeb 100644 --- a/synthesizer/src/vm/finalize.rs +++ b/synthesizer/src/vm/finalize.rs @@ -318,6 +318,7 @@ impl> VM { &output_ids, &tpks, &deployment_payers, + &deployments, ) { // Store the aborted transaction. aborted.push((transaction.clone(), reason)); @@ -786,6 +787,11 @@ impl> VM { /// - The transaction is producing a duplicate output /// - The transaction is producing a duplicate transition public key /// - The transaction is another deployment in the block from the same public fee payer. + /// - The transaction is an execution for a program following its deployment or redeployment in this block. + /// + /// - Note: If a transaction is a deployment for a program following its deployment or redeployment in this block, + /// it is not aborted. Instead, it will be rejected and its fee will be consumed. + #[allow(clippy::too_many_arguments)] fn should_abort_transaction( &self, transaction: &Transaction, @@ -794,6 +800,7 @@ impl> VM { output_ids: &IndexSet>, tpks: &IndexSet>, deployment_payers: &IndexSet>, + deployments: &IndexSet>, ) -> Option { // Ensure that the transaction is not producing a duplicate transition. for transition_id in transaction.transition_ids() { @@ -840,6 +847,18 @@ impl> VM { } } + // If the transaction is an execution, ensure that its corresponding program(s) + // have not been deployed or redeployed prior to this transaction in this block. + // Note: This logic is compatible with deployments prior to `ConsensusVersion::V8`. + if let Transaction::Execute(_, _, execution, _) = transaction { + // If one of the component programs have been deployed or upgraded in this block, abort the transaction. + for program_id in execution.transitions().map(|t| t.program_id()) { + if deployments.contains(program_id) { + return Some(format!("Program {program_id} has been deployed or upgraded in this block")); + } + } + } + // Return `None` because the transaction is well-formed. None } @@ -860,6 +879,8 @@ impl> VM { let mut valid_transactions = Vec::with_capacity(transactions.len()); let mut aborted_transactions = Vec::with_capacity(transactions.len()); + // Initialize a list of the successful deployments. + let mut deployments = IndexSet::new(); // Initialize a list of created transition IDs. let mut transition_ids: IndexSet = Default::default(); // Initialize a list of spent input IDs. @@ -888,6 +909,7 @@ impl> VM { &output_ids, &tpks, &deployment_payers, + &deployments, ) { // Store the aborted transaction. Some(reason) => aborted_transactions.push((*transaction, reason.to_string())), @@ -902,8 +924,10 @@ impl> VM { // Add the transition public keys to the set of produced transition public keys. tpks.extend(transaction.transition_public_keys()); // Add any public deployment payer to the set of deployment payers. - if let Transaction::Deploy(_, _, _, _, fee) = transaction { + // Add the program ID to the list of deployed programs. + if let Transaction::Deploy(_, _, _, deployment, fee) = transaction { fee.payer().map(|payer| deployment_payers.insert(payer)); + deployments.insert(*deployment.program_id()); } // Add the transaction to the list of transactions to verify. diff --git a/synthesizer/src/vm/mod.rs b/synthesizer/src/vm/mod.rs index 1ddbdcde54..d7d2f76450 100644 --- a/synthesizer/src/vm/mod.rs +++ b/synthesizer/src/vm/mod.rs @@ -22,6 +22,9 @@ mod execute; mod finalize; mod verify; +#[cfg(test)] +mod tests; + use crate::{Restrictions, cast_mut_ref, cast_ref, convert, process}; use algorithms::snark::varuna::VarunaVersion; use console::{ @@ -60,7 +63,7 @@ use ledger_store::{ atomic_finalize, }; use synthesizer_process::{Authorization, Process, Trace, deployment_cost, execution_cost_v1, execution_cost_v2}; -use synthesizer_program::{FinalizeGlobalState, FinalizeOperation, FinalizeStoreTrait, Program}; +use synthesizer_program::{FinalizeGlobalState, FinalizeOperation, FinalizeStoreTrait, Program, StackProgram}; use utilities::try_vm_runtime; use aleo_std::prelude::{finish, lap, timer}; @@ -1475,8 +1478,13 @@ function do: let fee = vm.execute_fee_authorization(fee_authorization, None, rng).unwrap(); // Create a new deployment transaction with the overreported verifying keys. - let adjusted_deployment = - Deployment::new(deployment.edition(), deployment.program().clone(), vks_with_overreport).unwrap(); + let adjusted_deployment = Deployment::new( + deployment.edition(), + deployment.program().clone(), + vks_with_overreport, + deployment.program_checksum().cloned(), + ) + .unwrap(); let adjusted_transaction = Transaction::from_deployment(program_owner, adjusted_deployment, fee).unwrap(); // Verify the deployment transaction. It should error when certificate checking for constraint count mismatch. @@ -1530,8 +1538,13 @@ function do: } // Create a new deployment transaction with the underreported verifying keys. - let adjusted_deployment = - Deployment::new(deployment.edition(), deployment.program().clone(), vks_with_underreport).unwrap(); + let adjusted_deployment = Deployment::new( + deployment.edition(), + deployment.program().clone(), + vks_with_underreport, + deployment.program_checksum().cloned(), + ) + .unwrap(); let deployment_id = adjusted_deployment.to_deployment_id().unwrap(); let adjusted_transaction = Transaction::Deploy(txid, deployment_id, program_owner, Box::new(adjusted_deployment), fee); @@ -1605,8 +1618,13 @@ function do: } // Create a new deployment transaction with the underreported verifying keys. - let adjusted_deployment = - Deployment::new(deployment.edition(), deployment.program().clone(), vks_with_underreport).unwrap(); + let adjusted_deployment = Deployment::new( + deployment.edition(), + deployment.program().clone(), + vks_with_underreport, + deployment.program_checksum().cloned(), + ) + .unwrap(); let deployment_id = adjusted_deployment.to_deployment_id().unwrap(); let adjusted_transaction = Transaction::Deploy(txid, deployment_id, program_owner, Box::new(adjusted_deployment), fee); @@ -3105,6 +3123,7 @@ function check: vm.add_next_block(&genesis).unwrap(); // Fund two accounts to pay for the deployment. + // This has to be done because only one deployment can be made per fee-paying address per block. let private_key_1 = PrivateKey::new(rng).unwrap(); let private_key_2 = PrivateKey::new(rng).unwrap(); let address_1 = Address::try_from(&private_key_1).unwrap(); @@ -3135,6 +3154,8 @@ function check: let block = sample_next_block(&vm, &caller_private_key, &[tx_1, tx_2], rng).unwrap(); assert_eq!(block.transactions().num_accepted(), 2); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); vm.add_next_block(&block).unwrap(); // Deploy two programs that depend on each other. @@ -3185,6 +3206,8 @@ function adder: let block = sample_next_block(&vm, &caller_private_key, &[deployment_1, deployment_2], rng).unwrap(); assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 1); vm.add_next_block(&block).unwrap(); // Check that only `child_program.aleo` is in the VM. @@ -3206,26 +3229,6 @@ function adder: let vm = crate::vm::test_helpers::sample_vm(); vm.add_next_block(&genesis).unwrap(); - // Fund an account to pay for the deployment. - let private_key = PrivateKey::new(rng).unwrap(); - let address = Address::try_from(&private_key).unwrap(); - - let tx = vm - .execute( - &caller_private_key, - ("credits.aleo", "transfer_public"), - [Value::from_str(&format!("{address}")).unwrap(), Value::from_str("100000000u64").unwrap()].iter(), - None, - 0, - None, - rng, - ) - .unwrap(); - - let block = sample_next_block(&vm, &caller_private_key, &[tx], rng).unwrap(); - assert_eq!(block.transactions().num_accepted(), 1); - vm.add_next_block(&block).unwrap(); - // Deploy and execute a program in the same block. let program = Program::from_str( r" @@ -3243,9 +3246,8 @@ function adder: // Initialize an "off-chain" VM to generate the deployment and execution. let off_chain_vm = sample_vm(); off_chain_vm.add_next_block(&genesis).unwrap(); - off_chain_vm.add_next_block(&block).unwrap(); // Deploy the program. - let deployment = off_chain_vm.deploy(&private_key, &program, None, 0, None, rng).unwrap(); + let deployment = off_chain_vm.deploy(&caller_private_key, &program, None, 0, None, rng).unwrap(); // Check that the account has enough to pay for the deployment. assert_eq!(*deployment.fee_amount().unwrap(), 2483025); // Add the program to the off-chain VM. @@ -3253,7 +3255,7 @@ function adder: // Execute the program. let transaction = off_chain_vm .execute( - &private_key, + &caller_private_key, ("adder_program.aleo", "adder"), [Value::from_str("1u64").unwrap(), Value::from_str("2u64").unwrap()].iter(), None, @@ -3271,12 +3273,416 @@ function adder: let block = sample_next_block(&vm, &caller_private_key, &[deployment, transaction], rng).unwrap(); assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 1); vm.add_next_block(&block).unwrap(); // Check that the program was deployed. assert!(vm.process().read().contains_program(&ProgramID::from_str("adder_program.aleo").unwrap())); } + // Note: Do not change the prefix `test_vm_upgrade` of this test as CI uses it to detect the test. + #[cfg(feature = "test")] + #[test] + fn test_vm_upgrade_and_execute_in_same_block() { + let rng = &mut TestRng::default(); + + // Initialize a new caller. + let caller_private_key = crate::vm::test_helpers::sample_genesis_private_key(rng); + + // Initialize the VM at the V8 height. + let v8_height = CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V8).unwrap(); + let vm = crate::vm::test_helpers::sample_vm_at_height(v8_height, rng); + + // Deploy the upgradable program. + let program_v0 = Program::from_str( + r" +program test_one.aleo; + +constructor: + assert.eq true true; + +mapping first: + key as field.public; + value as field.public; + +function set_first: + input r0 as field.public; + input r1 as field.public; + async set_first r0 r1 into r2; + output r2 as test_one.aleo/set_first.future; +finalize set_first: + input r0 as field.public; + input r1 as field.public; + set r0 into first[r1];", + ) + .unwrap(); + + let deployment_v0 = vm.deploy(&caller_private_key, &program_v0, None, 0, None, rng).unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[deployment_v0], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block).unwrap(); + + // Execute the program. + let transaction = vm + .execute( + &caller_private_key, + ("test_one.aleo", "set_first"), + [Value::from_str("0field").unwrap(), Value::from_str("0field").unwrap()].iter(), + None, + 0, + None, + rng, + ) + .unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[transaction], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block).unwrap(); + + // Verify that the entry exists in the mapping. + let Some(Value::Plaintext(Plaintext::Literal(Literal::Field(field), _))) = vm + .finalize_store() + .get_value_confirmed( + ProgramID::from_str("test_one.aleo").unwrap(), + Identifier::from_str("first").unwrap(), + &Plaintext::from_str("0field").unwrap(), + ) + .unwrap() + else { + panic!("Expected a valid field."); + }; + assert_eq!(field, Field::from_str("0field").unwrap()); + + // Upgrade the program. + let program_v1 = Program::from_str( + r" +program test_one.aleo; + +constructor: + assert.eq true true; + +mapping first: + key as field.public; + value as field.public; + +mapping second: + key as field.public; + value as field.public; + +function set_first: + input r0 as field.public; + input r1 as field.public; + async set_first r0 r1 into r2; + output r2 as test_one.aleo/set_first.future; +finalize set_first: + input r0 as field.public; + input r1 as field.public; + set r0 into first[r1]; + set r0 into second[r1];", + ) + .unwrap(); + + // Deploy the upgraded program. + let deployment_v1 = vm.deploy(&caller_private_key, &program_v1, None, 0, None, rng).unwrap(); + // Execute the program, note that this is the original program as the deployment has not been added to the VM. + let transaction = vm + .execute( + &caller_private_key, + ("test_one.aleo", "set_first"), + [Value::from_str("1field").unwrap(), Value::from_str("1field").unwrap()].iter(), + None, + 0, + None, + rng, + ) + .unwrap(); + + // Add the upgrade and execution to the VM. + let block = sample_next_block(&vm, &caller_private_key, &[deployment_v1, transaction], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 1); + vm.add_next_block(&block).unwrap(); + + // Check that the program was upgraded. + let program = + vm.process().read().get_stack(ProgramID::from_str("test_one.aleo").unwrap()).unwrap().program().clone(); + assert_eq!(&program_v1, &program); + + // Attempt to execute the program again. + let transaction = vm + .execute( + &caller_private_key, + ("test_one.aleo", "set_first"), + [Value::from_str("1field").unwrap(), Value::from_str("1field").unwrap()].iter(), + None, + 0, + None, + rng, + ) + .unwrap(); + + // Add the transaction to a block and update the VM. + let block = sample_next_block(&vm, &caller_private_key, &[transaction], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block).unwrap(); + + // Verify that the entry exists in the first mapping. + let Some(Value::Plaintext(Plaintext::Literal(Literal::Field(field), _))) = vm + .finalize_store() + .get_value_confirmed( + ProgramID::from_str("test_one.aleo").unwrap(), + Identifier::from_str("first").unwrap(), + &Plaintext::from_str("1field").unwrap(), + ) + .unwrap() + else { + panic!("Expected a valid field."); + }; + assert_eq!(field, Field::from_str("1field").unwrap()); + + // Verify that the entry exists in the second mapping. + let Some(Value::Plaintext(Plaintext::Literal(Literal::Field(field), _))) = vm + .finalize_store() + .get_value_confirmed( + ProgramID::from_str("test_one.aleo").unwrap(), + Identifier::from_str("second").unwrap(), + &Plaintext::from_str("1field").unwrap(), + ) + .unwrap() + else { + panic!("Expected a valid field."); + }; + assert_eq!(field, Field::from_str("1field").unwrap()); + } + + // Note: Do not change the prefix `test_vm_upgrade` of this test as CI uses it to detect the test. + #[cfg(feature = "test")] + #[test] + fn test_vm_upgrade_and_upgrade_in_same_block() { + let rng = &mut TestRng::default(); + + // Initialize a new caller. + let caller_private_key = crate::vm::test_helpers::sample_genesis_private_key(rng); + + // Initialize the VM at the V8 height. + let v8_height = CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V8).unwrap(); + let vm = crate::vm::test_helpers::sample_vm_at_height(v8_height, rng); + + // Deploy the upgradable program. + let program_v0 = Program::from_str( + r" +program test_program.aleo; +constructor: + assert.eq true true; +function foo: + ", + ) + .unwrap(); + + let deployment_v0 = vm.deploy(&caller_private_key, &program_v0, None, 0, None, rng).unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[deployment_v0], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block).unwrap(); + + // Fund two accounts to pay for the two upgrades. + // This has to be done because only one deployment can be made per fee-paying address per block. + let private_key_1 = PrivateKey::new(rng).unwrap(); + let private_key_2 = PrivateKey::new(rng).unwrap(); + let address_1 = Address::try_from(&private_key_1).unwrap(); + let address_2 = Address::try_from(&private_key_2).unwrap(); + + let tx_1 = vm + .execute( + &caller_private_key, + ("credits.aleo", "transfer_public"), + [Value::from_str(&format!("{address_1}")).unwrap(), Value::from_str("100_000_000u64").unwrap()].iter(), + None, + 0, + None, + rng, + ) + .unwrap(); + let tx_2 = vm + .execute( + &caller_private_key, + ("credits.aleo", "transfer_public"), + [Value::from_str(&format!("{address_2}")).unwrap(), Value::from_str("100_000_000u64").unwrap()].iter(), + None, + 0, + None, + rng, + ) + .unwrap(); + + let block = sample_next_block(&vm, &caller_private_key, &[tx_1, tx_2], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 2); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block).unwrap(); + + // Upgrade the program twice. + let program_v1 = Program::from_str( + r" +program test_program.aleo; +constructor: + assert.eq true true; +function foo: +function bar: + ", + ) + .unwrap(); + + let program_v2 = Program::from_str( + r" +program test_program.aleo; +constructor: + assert.eq true true; +function foo: +function bar: +function baz: + ", + ) + .unwrap(); + + // Generate the deployments. + // Note that we are attempting to upgrade twice with consecutive editions. + let mut process = Process::load().unwrap(); + process.add_program(&program_v0).unwrap(); + let deployment_v1 = process.deploy::(&program_v1, rng).unwrap(); + assert_eq!(deployment_v1.edition(), 1); + process.add_program(&program_v1).unwrap(); + let deployment_v2 = process.deploy::(&program_v2, rng).unwrap(); + assert_eq!(deployment_v2.edition(), 2); + + // Construct the transactions. + let fee_1 = vm + .execute_fee_authorization( + process + .authorize_fee_public::( + &private_key_1, + 10_000_000, + 0, + deployment_v1.to_deployment_id().unwrap(), + rng, + ) + .unwrap(), + None, + rng, + ) + .unwrap(); + let owner_1 = ProgramOwner::new(&private_key_1, deployment_v1.to_deployment_id().unwrap(), rng).unwrap(); + let transaction_1 = Transaction::from_deployment(owner_1, deployment_v1, fee_1).unwrap(); + let fee_2 = vm + .execute_fee_authorization( + process + .authorize_fee_public::( + &private_key_2, + 10_000_000, + 0, + deployment_v2.to_deployment_id().unwrap(), + rng, + ) + .unwrap(), + None, + rng, + ) + .unwrap(); + let owner_2 = ProgramOwner::new(&private_key_2, deployment_v2.to_deployment_id().unwrap(), rng).unwrap(); + let transaction_2 = Transaction::from_deployment(owner_2, deployment_v2, fee_2).unwrap(); + + // Attempt to upgrade the program twice. + let block = sample_next_block(&vm, &caller_private_key, &[transaction_1, transaction_2], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 1); + vm.add_next_block(&block).unwrap(); + + // Attempt to upgrade twice using the same edition. + let program_v3 = Program::from_str( + r" +program test_program.aleo; +constructor: + assert.eq true true; +function foo: +function bar: +function baz: +function qux: + ", + ) + .unwrap(); + + let program_v4 = Program::from_str( + r" +program test_program.aleo; +constructor: + assert.eq true true; +function foo: +function bar: +function baz: +function qux: +function fly: + ", + ) + .unwrap(); + + // Generate the deployments. + let deployment_v3 = process.deploy::(&program_v3, rng).unwrap(); + assert_eq!(deployment_v3.edition(), 2); + let deployment_v4 = process.deploy::(&program_v4, rng).unwrap(); + assert_eq!(deployment_v4.edition(), 2); + + // Construct the transactions. + let fee_1 = vm + .execute_fee_authorization( + process + .authorize_fee_public::( + &private_key_1, + 10_000_000, + 0, + deployment_v3.to_deployment_id().unwrap(), + rng, + ) + .unwrap(), + None, + rng, + ) + .unwrap(); + let owner_1 = ProgramOwner::new(&private_key_1, deployment_v3.to_deployment_id().unwrap(), rng).unwrap(); + let transaction_1 = Transaction::from_deployment(owner_1, deployment_v3, fee_1).unwrap(); + let fee_2 = vm + .execute_fee_authorization( + process + .authorize_fee_public::( + &private_key_2, + 10_000_000, + 0, + deployment_v4.to_deployment_id().unwrap(), + rng, + ) + .unwrap(), + None, + rng, + ) + .unwrap(); + let owner_2 = ProgramOwner::new(&private_key_2, deployment_v4.to_deployment_id().unwrap(), rng).unwrap(); + let transaction_2 = Transaction::from_deployment(owner_2, deployment_v4, fee_2).unwrap(); + + // Attempt to upgrade the program twice. + let block = sample_next_block(&vm, &caller_private_key, &[transaction_1, transaction_2], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 1); + assert_eq!(block.aborted_transaction_ids().len(), 0); + } + #[cfg(feature = "test")] #[test] fn test_versioned_keyword_restrictions() { diff --git a/synthesizer/src/vm/tests/mod.rs b/synthesizer/src/vm/tests/mod.rs new file mode 100644 index 0000000000..9a342a77d7 --- /dev/null +++ b/synthesizer/src/vm/tests/mod.rs @@ -0,0 +1,20 @@ +// Copyright (c) 2019-2025 Provable Inc. +// This file is part of the snarkVM library. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#[cfg(feature = "test")] +mod test_vm_upgrade; + +#[cfg(feature = "test")] +use super::*; diff --git a/synthesizer/src/vm/tests/test_vm_upgrade.rs b/synthesizer/src/vm/tests/test_vm_upgrade.rs new file mode 100644 index 0000000000..c41d495c09 --- /dev/null +++ b/synthesizer/src/vm/tests/test_vm_upgrade.rs @@ -0,0 +1,1902 @@ +// Copyright (c) 2019-2025 Provable Inc. +// This file is part of the snarkVM library. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::*; + +use crate::vm::test_helpers::*; + +use console::{account::ViewKey, program::Value}; +use synthesizer_program::{Program, StackProgram}; + +use std::panic::AssertUnwindSafe; + +// This test checks that a program with a constructor cannot be deployed before the V8 consensus height. +#[test] +fn test_constructor_requires_v8() -> Result<()> { + let rng = &mut TestRng::default(); + + // Initialize a new caller. + let caller_private_key = sample_genesis_private_key(rng); + + // Initialize the VM. + let vm = sample_vm_at_height(CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V8)? - 1, rng); + + // Initialize the program. + let program = Program::from_str( + r" +program constructor_test.aleo; + +constructor: + assert.eq true true; + +function dummy: + ", + )?; + + // Attempt to deploy the program. + let deployment = vm.deploy(&caller_private_key, &program, None, 0, None, rng)?; + let block = sample_next_block(&vm, &caller_private_key, &[deployment], rng)?; + assert_eq!(block.transactions().num_accepted(), 0); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 1); + vm.add_next_block(&block)?; + + // Verify that the program can now be deployed. + let transaction = vm.deploy(&caller_private_key, &program, None, 0, None, rng)?; + let block = sample_next_block(&vm, &caller_private_key, &[transaction], rng)?; + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + + Ok(()) +} + +// This test checks that: +// - the logic of a simple transition without records can be upgraded. +// - once a program is upgraded, the old executions are no longer valid. +#[test] +fn test_simple_upgrade() -> Result<()> { + let rng = &mut TestRng::default(); + + // Initialize a new caller. + let caller_private_key = sample_genesis_private_key(rng); + + // Initialize the VM. + let vm = sample_vm_at_height(CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V8)?, rng); + + // Initialize the program. + let program = Program::from_str( + r" +program adder.aleo; + +function binary_add: + input r0 as u8.public; + input r1 as u8.public; + add r0 r1 into r2; + output r2 as u8.public; + +constructor: + assert.eq true true; + ", + )?; + + // Deploy the program. + let transaction = vm.deploy(&caller_private_key, &program, None, 0, None, rng)?; + let block = sample_next_block(&vm, &caller_private_key, &[transaction], rng)?; + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block)?; + + // Check that the program is deployed. + let stack = vm.process().read().get_stack("adder.aleo")?; + assert_eq!(stack.program_id(), &ProgramID::from_str("adder.aleo")?); + assert_eq!(**stack.program_edition(), 0); + + // Execute the program. + let original_execution = vm.execute( + &caller_private_key, + ("adder.aleo", "binary_add"), + vec![Value::from_str("1u8")?, Value::from_str("1u8")?].into_iter(), + None, + 0, + None, + rng, + )?; + assert!(vm.check_transaction(&original_execution, None, rng).is_ok()); + + // Check that the output is correct. + let output = match original_execution.transitions().next().unwrap().outputs().last().unwrap() { + Output::Public(_, Some(Plaintext::Literal(Literal::U8(value), _))) => **value, + output => bail!(format!("Unexpected output: {output}")), + }; + assert_eq!(output, 2u8); + + // Update the program. + let upgraded_program = Program::from_str( + r" +program adder.aleo; + +function binary_add: + input r0 as u8.public; + input r1 as u8.public; + add.w r0 r1 into r2; + output r2 as u8.public; + +constructor: + assert.eq true true; + ", + )?; + + // Deploy the upgraded program. + let transaction = vm.deploy(&caller_private_key, &upgraded_program, None, 0, None, rng)?; + assert_eq!(transaction.deployment().unwrap().edition(), 1); + let block = sample_next_block(&vm, &caller_private_key, &[transaction], rng)?; + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block)?; + + // Check that the program is upgraded. + let stack = vm.process().read().get_stack("adder.aleo")?; + assert_eq!(stack.program_id(), &ProgramID::from_str("adder.aleo")?); + assert_eq!(**stack.program_edition(), 1); + + // Check that the old execution is no longer valid. + vm.partially_verified_transactions().write().clear(); + assert!(vm.check_transaction(&original_execution, None, rng).is_err()); + + // Execute the upgraded program. + let new_execution = vm.execute( + &caller_private_key, + ("adder.aleo", "binary_add"), + vec![Value::from_str("1u8")?, Value::from_str("1u8")?].into_iter(), + None, + 0, + None, + rng, + )?; + assert!(vm.check_transaction(&new_execution, None, rng).is_ok()); + + // Check that the output is correct. + let output = match new_execution.transitions().next().unwrap().outputs().last().unwrap() { + Output::Public(_, Some(Plaintext::Literal(Literal::U8(value), _))) => **value, + output => bail!(format!("Unexpected output: {output}")), + }; + assert_eq!(output, 2u8); + + Ok(()) +} + +#[test] +fn test_program_without_constructor_is_not_upgradable() -> Result<()> { + let rng = &mut TestRng::default(); + + // Initialize a new caller. + let caller_private_key = sample_genesis_private_key(rng); + + // Initialize the VM. + let vm = sample_vm_at_height(CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V8)?, rng); + + // Initialize the program. + let program = Program::from_str( + r" +program basic.aleo; +function foo: + ", + )?; + + // Initialize the upgraded program. + let upgraded_program = Program::from_str( + r" +program basic.aleo; +function foo: +function bar: + ", + )?; + + // Deploy the program. + let transaction_0 = vm.deploy(&caller_private_key, &program, None, 0, None, rng)?; + let transaction_1 = vm.deploy(&caller_private_key, &upgraded_program, None, 0, None, rng)?; + let block = sample_next_block(&vm, &caller_private_key, &[transaction_0], rng)?; + vm.add_next_block(&block)?; + + // Attempt to deploy the upgraded program. + assert!(vm.deploy(&caller_private_key, &upgraded_program, None, 0, None, rng).is_err()); + let block = sample_next_block(&vm, &caller_private_key, &[transaction_1], rng)?; + assert_eq!(block.transactions().num_accepted(), 0); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 1); + vm.add_next_block(&block)?; + + // Initialize the upgraded program. + let upgraded_program = Program::from_str( + r" +program basic.aleo; +function foo: +function bar: +constructor: + assert.eq true true; + ", + )?; + + // Attempt to deploy the upgraded program using `VM::deploy`. + assert!(vm.deploy(&caller_private_key, &upgraded_program, None, 0, None, rng).is_err()); + + // Initialize the upgraded program. + let upgraded_program = Program::from_str( + r" +program basic.aleo; +function foo: +function bar: +constructor: + assert.eq true true; + ", + )?; + + // Attempt to deploy the upgraded program using `VM::deploy`. + assert!(vm.deploy(&caller_private_key, &upgraded_program, None, 0, None, rng).is_err()); + + Ok(()) +} + +// This test checks that: +// - the first instance of a program must be the zero-th edition. +// - subsequent upgrades to the program must be sequential. +#[test] +fn test_editions_are_sequential() -> Result<()> { + let rng = &mut TestRng::default(); + + // Initialize a new caller. + let caller_private_key = sample_genesis_private_key(rng); + + // Initialize two VMs. + let off_chain_vm = sample_vm_at_height(CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V8)?, rng); + let on_chain_vm = sample_vm_at_height(CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V8)?, rng); + + // Define the three versions of the program. + let program_v0 = Program::from_str( + r" +program basic.aleo; +function foo: +constructor: + assert.eq true true; + ", + )?; + let program_v1 = Program::from_str( + r" +program basic.aleo; +function foo: +function bar: +constructor: + assert.eq true true; + ", + )?; + let program_v2_as_v1 = Program::from_str( + r" +program basic.aleo; +function foo: +function bar: +function baz: +constructor: + assert.eq true true; + ", + )?; + let program_v2 = Program::from_str( + r" +program basic.aleo; +function foo: +function bar: +function baz: +constructor: + assert.eq true true; + ", + )?; + + // Using the off-chain VM, generate a sequence of deployments. + let deployment_v0_pass = off_chain_vm.deploy(&caller_private_key, &program_v0, None, 0, None, rng)?; + off_chain_vm.process().write().add_program(&program_v0)?; + let deployment_v1_fail = off_chain_vm.deploy(&caller_private_key, &program_v1, None, 0, None, rng)?; + let deployment_v1_pass = off_chain_vm.deploy(&caller_private_key, &program_v1, None, 0, None, rng)?; + let deployment_v2_as_v1_fail = off_chain_vm.deploy(&caller_private_key, &program_v2_as_v1, None, 0, None, rng)?; + off_chain_vm.process().write().add_program(&program_v1)?; + let deployment_v2_fail = off_chain_vm.deploy(&caller_private_key, &program_v2, None, 0, None, rng)?; + let deployment_v2_pass = off_chain_vm.deploy(&caller_private_key, &program_v2, None, 0, None, rng)?; + + // Deploy the programs to the on-chain VM individually in the following sequence: + // - deployment_v1_fail + // - deployment_v0_pass + // - deployment_v2_fail + // - deployment_v1_pass + // - deployment_v2_as_v1_fail + // - deployment_v2_pass + // Their name indicate whether the deployment should pass or fail. + + // This deployment should fail because the it is not the zero-th edition. + let block = sample_next_block(&on_chain_vm, &caller_private_key, &[deployment_v1_fail], rng)?; + assert_eq!(block.transactions().num_accepted(), 0); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 1); + on_chain_vm.add_next_block(&block)?; + + // This deployment should pass. + let block = sample_next_block(&on_chain_vm, &caller_private_key, &[deployment_v0_pass], rng)?; + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + on_chain_vm.add_next_block(&block)?; + let stack = on_chain_vm.process().read().get_stack("basic.aleo")?; + assert_eq!(**stack.program_edition(), 0); + + // This deployment should fail because it does not increment the edition. + let block = sample_next_block(&on_chain_vm, &caller_private_key, &[deployment_v2_fail], rng)?; + assert_eq!(block.transactions().num_accepted(), 0); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 1); + on_chain_vm.add_next_block(&block)?; + + // This deployment should pass. + let block = sample_next_block(&on_chain_vm, &caller_private_key, &[deployment_v1_pass], rng)?; + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + on_chain_vm.add_next_block(&block)?; + let stack = on_chain_vm.process().read().get_stack("basic.aleo")?; + assert_eq!(**stack.program_edition(), 1); + + // This deployment should fail because it attempt to redeploy at the same edition. + let block = sample_next_block(&on_chain_vm, &caller_private_key, &[deployment_v2_as_v1_fail], rng)?; + assert_eq!(block.transactions().num_accepted(), 0); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 1); + on_chain_vm.add_next_block(&block)?; + + // This deployment should pass. + let block = sample_next_block(&on_chain_vm, &caller_private_key, &[deployment_v2_pass], rng)?; + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + on_chain_vm.add_next_block(&block)?; + let stack = on_chain_vm.process().read().get_stack("basic.aleo")?; + assert_eq!(**stack.program_edition(), 2); + + Ok(()) +} + +// This test checks that: +// - records created before an upgrade are still valid after an upgrade. +// - records created after an upgrade can be created and used in the upgraded program. +// - records are semantically distinct (old records cannot be used in functions that require new records). +// - functions can be disabled using `assert.neq self.caller self.caller`. +#[test] +fn test_upgrade_with_records() -> Result<()> { + let rng = &mut TestRng::default(); + + // Initialize a new caller. + let caller_private_key = sample_genesis_private_key(rng); + let caller_view_key = ViewKey::try_from(&caller_private_key)?; + + // Initialize the VM. + let vm = sample_vm_at_height(CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V8)?, rng); + + // Define the two versions of the program. + let program_v0 = Program::from_str( + r" +program record_test.aleo; + +record data_v1: + owner as address.private; + data as u8.public; + +function mint: + input r0 as u8.public; + cast self.caller r0 into r1 as data_v1.record; + output r1 as data_v1.record; + +constructor: + assert.eq true true; + ", + )?; + + let program_v1 = Program::from_str( + r" +program record_test.aleo; + +record data_v1: + owner as address.private; + data as u8.public; + +record data_v2: + owner as address.private; + data as u8.public; + +function mint: + input r0 as u8.public; + assert.neq self.caller self.caller; + cast self.caller r0 into r1 as data_v1.record; + output r1 as data_v1.record; + +function convert: + input r0 as data_v1.record; + cast r0.owner r0.data into r1 as data_v2.record; + output r1 as data_v2.record; + +function burn: + input r0 as data_v2.record; + +constructor: + assert.eq true true; + ", + )?; + + // Deploy the first version of the program. + let transaction = vm.deploy(&caller_private_key, &program_v0, None, 0, None, rng)?; + let block = sample_next_block(&vm, &caller_private_key, &[transaction], rng)?; + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block)?; + + // Execute the mint function twice. + let mint_execution_0 = vm.execute( + &caller_private_key, + ("record_test.aleo", "mint"), + vec![Value::from_str("0u8")?].into_iter(), + None, + 0, + None, + rng, + )?; + let mint_execution_1 = vm.execute( + &caller_private_key, + ("record_test.aleo", "mint"), + vec![Value::from_str("1u8")?].into_iter(), + None, + 0, + None, + rng, + )?; + let block = sample_next_block(&vm, &caller_private_key, &[mint_execution_0, mint_execution_1], rng)?; + assert_eq!(block.transactions().num_accepted(), 2); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + let mut v1_records = block + .records() + .map(|(_, record)| record.decrypt(&caller_view_key)) + .collect::>>>>()?; + assert_eq!(v1_records.len(), 2); + vm.add_next_block(&block)?; + + // Update the program. + let transaction = vm.deploy(&caller_private_key, &program_v1, None, 0, None, rng)?; + let block = sample_next_block(&vm, &caller_private_key, &[transaction], rng)?; + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block)?; + + // Attempt to execute the mint function. + assert!( + vm.execute( + &caller_private_key, + ("record_test.aleo", "mint"), + vec![Value::from_str("0u8")?].into_iter(), + None, + 0, + None, + rng + ) + .is_err() + ); + + // Get the first record and execute the convert function. + let record = v1_records.pop().unwrap(); + let convert_execution = vm.execute( + &caller_private_key, + ("record_test.aleo", "convert"), + vec![Value::Record(record)].into_iter(), + None, + 0, + None, + rng, + )?; + let block = sample_next_block(&vm, &caller_private_key, &[convert_execution], rng)?; + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + let mut v2_records = block + .records() + .map(|(_, record)| record.decrypt(&caller_view_key)) + .collect::>>>>()?; + assert_eq!(v2_records.len(), 1); + vm.add_next_block(&block)?; + + // Get the v2 record and execute the burn function. + let record = v2_records.pop().unwrap(); + let burn_execution = vm.execute( + &caller_private_key, + ("record_test.aleo", "burn"), + vec![Value::Record(record)].into_iter(), + None, + 0, + None, + rng, + )?; + let block = sample_next_block(&vm, &caller_private_key, &[burn_execution], rng)?; + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block)?; + + // Attempt to execute the burn function with the remaining v1 record. + let record = v1_records.pop().unwrap(); + assert!( + vm.execute( + &caller_private_key, + ("record_test.aleo", "burn"), + vec![Value::Record(record)].into_iter(), + None, + 0, + None, + rng + ) + .is_err() + ); + + Ok(()) +} + +// This test checks that: +// - mappings created before an upgrade are still valid after an upgrade. +// - mappings created by and upgraded are correctly initialized and usable in the program. +// - functions can be disabled by inserting a failing condition in the on-chain logic. +#[test] +fn test_upgrade_with_mappings() -> Result<()> { + let rng = &mut TestRng::default(); + + // Initialize a new caller. + let caller_private_key = sample_genesis_private_key(rng); + + // Initialize the VM. + let vm = sample_vm_at_height(CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V8)?, rng); + + // Define the two versions of the program. + let program_v0 = Program::from_str( + r" +program mapping_test.aleo; + +mapping data_v1: + key as u8.public; + value as u8.public; + +function store_data_v1: + input r0 as u8.public; + input r1 as u8.public; + async store_data_v1 r0 r1 into r2; + output r2 as mapping_test.aleo/store_data_v1.future; +finalize store_data_v1: + input r0 as u8.public; + input r1 as u8.public; + set r1 into data_v1[r0]; + +constructor: + assert.eq true true; + ", + )?; + + let program_v1 = Program::from_str( + r" +program mapping_test.aleo; + +mapping data_v1: + key as u8.public; + value as u8.public; + +mapping data_v2: + key as u8.public; + value as u8.public; + +function store_data_v1: + input r0 as u8.public; + input r1 as u8.public; + async store_data_v1 r0 r1 into r2; + output r2 as mapping_test.aleo/store_data_v1.future; +finalize store_data_v1: + input r0 as u8.public; + input r1 as u8.public; + assert.neq true true; + +function migrate_data_v1_to_v2: + input r0 as u8.public; + async migrate_data_v1_to_v2 r0 into r1; + output r1 as mapping_test.aleo/migrate_data_v1_to_v2.future; +finalize migrate_data_v1_to_v2: + input r0 as u8.public; + get data_v1[r0] into r1; + remove data_v1[r0]; + set r1 into data_v2[r0]; + +function store_data_v2: + input r0 as u8.public; + input r1 as u8.public; + async store_data_v2 r0 r1 into r2; + output r2 as mapping_test.aleo/store_data_v2.future; +finalize store_data_v2: + input r0 as u8.public; + input r1 as u8.public; + set r1 into data_v2[r0]; + +constructor: + assert.eq true true; + ", + )?; + + // Deploy the first version of the program. + let transaction = vm.deploy(&caller_private_key, &program_v0, None, 0, None, rng)?; + let block = sample_next_block(&vm, &caller_private_key, &[transaction], rng)?; + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block)?; + + // Execute the store_data_v1 function. + let store_data_v1_execution = vm.execute( + &caller_private_key, + ("mapping_test.aleo", "store_data_v1"), + vec![Value::from_str("0u8")?, Value::from_str("0u8")?].into_iter(), + None, + 0, + None, + rng, + )?; + let block = sample_next_block(&vm, &caller_private_key, &[store_data_v1_execution], rng)?; + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block)?; + + // Check that the value was stored correctly. + let value = match vm.finalize_store().get_value_confirmed( + ProgramID::from_str("mapping_test.aleo")?, + Identifier::from_str("data_v1")?, + &Plaintext::from_str("0u8")?, + )? { + Some(Value::Plaintext(Plaintext::Literal(Literal::U8(value), _))) => *value, + value => bail!(format!("Unexpected value: {:?}", value)), + }; + assert_eq!(value, 0u8); + + // Update the program. + let transaction = vm.deploy(&caller_private_key, &program_v1, None, 0, None, rng)?; + let block = sample_next_block(&vm, &caller_private_key, &[transaction], rng)?; + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block)?; + + // Attempt to execute the store_data_v1 function. + let transaction = vm.execute( + &caller_private_key, + ("mapping_test.aleo", "store_data_v1"), + vec![Value::from_str("1u8")?, Value::from_str("1u8")?].into_iter(), + None, + 0, + None, + rng, + )?; + let block = sample_next_block(&vm, &caller_private_key, &[transaction], rng)?; + assert_eq!(block.transactions().num_accepted(), 0); + assert_eq!(block.transactions().num_rejected(), 1); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block)?; + + // Execute the migrate_data_v1_to_v2 function. + let migrate_data_v1_to_v2_execution = vm.execute( + &caller_private_key, + ("mapping_test.aleo", "migrate_data_v1_to_v2"), + vec![Value::from_str("0u8")?].into_iter(), + None, + 0, + None, + rng, + )?; + let block = sample_next_block(&vm, &caller_private_key, &[migrate_data_v1_to_v2_execution], rng)?; + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block)?; + + // Check that the value was migrated correctly. + let value = match vm.finalize_store().get_value_confirmed( + ProgramID::from_str("mapping_test.aleo")?, + Identifier::from_str("data_v2")?, + &Plaintext::from_str("0u8")?, + )? { + Some(Value::Plaintext(Plaintext::Literal(Literal::U8(value), _))) => *value, + value => bail!(format!("Unexpected value: {:?}", value)), + }; + assert_eq!(value, 0u8); + + // Check that the old value was removed. + assert!( + vm.finalize_store() + .get_value_confirmed( + ProgramID::from_str("mapping_test.aleo")?, + Identifier::from_str("data_v1")?, + &Plaintext::from_str("0u8")? + )? + .is_none() + ); + + // Execute the store_data_v2 function. + let store_data_v2_execution = vm.execute( + &caller_private_key, + ("mapping_test.aleo", "store_data_v2"), + vec![Value::from_str("1u8")?, Value::from_str("1u8")?].into_iter(), + None, + 0, + None, + rng, + )?; + let block = sample_next_block(&vm, &caller_private_key, &[store_data_v2_execution], rng)?; + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block)?; + + // Check that the value was stored correctly. + let value = match vm.finalize_store().get_value_confirmed( + ProgramID::from_str("mapping_test.aleo")?, + Identifier::from_str("data_v2")?, + &Plaintext::from_str("1u8")?, + )? { + Some(Value::Plaintext(Plaintext::Literal(Literal::U8(value), _))) => *value, + value => bail!(format!("Unexpected value: {:?}", value)), + }; + assert_eq!(value, 1u8); + + Ok(()) +} + +// This test checks that: +// - a dependent program accepts an upgrade to off-chain logic +// - a dependent program accepts an upgrade to on-chain logic +// - a dependent program can fix a specific version of the dependency +// - old executions of the dependent program are no longer valid after an upgrade +#[test] +fn test_upgrade_with_dependents() -> Result<()> { + let rng = &mut TestRng::default(); + + // Initialize a new caller. + let caller_private_key = sample_genesis_private_key(rng); + + // Initialize the VM. + let vm = sample_vm_at_height(CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V8)?, rng); + + // Define the two versions of the dependency program. + let dependency_v0 = Program::from_str( + r" +program dependency.aleo; + +function sum: + input r0 as u8.public; + input r1 as u8.public; + add r0 r1 into r2; + output r0 as u8.public; + +function sum_and_check: + input r0 as u8.public; + input r1 as u8.public; + add r0 r1 into r2; + async sum_and_check into r3; + output r2 as u8.public; + output r3 as dependency.aleo/sum_and_check.future; +finalize sum_and_check: + assert.eq true true; + +constructor: + assert.eq true true; + ", + )?; + + let dependency_v1 = Program::from_str( + r" +program dependency.aleo; + +function sum: + input r0 as u8.public; + input r1 as u8.public; + add.w r0 r1 into r2; + output r0 as u8.public; + +function sum_and_check: + input r0 as u8.public; + input r1 as u8.public; + add.w r0 r1 into r2; + async sum_and_check into r3; + output r2 as u8.public; + output r3 as dependency.aleo/sum_and_check.future; +finalize sum_and_check: + assert.eq true false; + +constructor: + assert.eq true true; + ", + )?; + + // Define the two versions of the dependent program. + let dependent_v0 = Program::from_str( + r" +import dependency.aleo; + +program dependent.aleo; + +function sum_unchecked: + input r0 as u8.public; + input r1 as u8.public; + call dependency.aleo/sum r0 r1 into r2; + output r2 as u8.public; + +function sum: + input r0 as u8.public; + input r1 as u8.public; + call dependency.aleo/sum r0 r1 into r2; + async sum into r3; + output r2 as u8.public; + output r3 as dependent.aleo/sum.future; +finalize sum: + assert.eq dependency.aleo/edition 0u16; + +function sum_and_check: + input r0 as u8.public; + input r1 as u8.public; + call dependency.aleo/sum_and_check r0 r1 into r2 r3; + async sum_and_check r3 into r4; + output r2 as u8.public; + output r4 as dependent.aleo/sum_and_check.future; +finalize sum_and_check: + input r0 as dependency.aleo/sum_and_check.future; + await r0; + +constructor: + assert.eq true true; + ", + )?; + + let dependent_v1 = Program::from_str( + r" +import dependency.aleo; + +program dependent.aleo; + +function sum_unchecked: + input r0 as u8.public; + input r1 as u8.public; + call dependency.aleo/sum r0 r1 into r2; + output r2 as u8.public; + +function sum: + input r0 as u8.public; + input r1 as u8.public; + call dependency.aleo/sum r0 r1 into r2; + async sum into r3; + output r2 as u8.public; + output r3 as dependent.aleo/sum.future; +finalize sum: + assert.eq dependency.aleo/edition 1u16; + +function sum_and_check: + input r0 as u8.public; + input r1 as u8.public; + call dependency.aleo/sum_and_check r0 r1 into r2 r3; + async sum_and_check r3 into r4; + output r2 as u8.public; + output r4 as dependent.aleo/sum_and_check.future; +finalize sum_and_check: + input r0 as dependency.aleo/sum_and_check.future; + await r0; + +constructor: + assert.eq true true; + ", + )?; + + // At a high level, this test will: + // 1. Deploy the v0 dependency and v0 dependent. + // 2. Verify that the the dependent program can be correctly executed. + // 3. Update the dependency to v1. + // 4. Verify that the call to `sum_and_check` automatically uses the new logic, however, the call `sum` fails because the edition is not 0. + // 5. Update the dependent to v1. + // 6. Verify that the call to `sum` now passes because the edition is 1. + + // Deploy the v0 dependency. + let transaction = vm.deploy(&caller_private_key, &dependency_v0, None, 0, None, rng)?; + let block = sample_next_block(&vm, &caller_private_key, &[transaction], rng)?; + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block)?; + + // Deploy the v0 dependent. + let transaction = vm.deploy(&caller_private_key, &dependent_v0, None, 0, None, rng)?; + let block = sample_next_block(&vm, &caller_private_key, &[transaction], rng)?; + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block)?; + + // Execute the functions. + let tx_1 = vm.execute( + &caller_private_key, + ("dependent.aleo", "sum"), + vec![Value::from_str("1u8")?, Value::from_str("1u8")?].into_iter(), + None, + 0, + None, + rng, + )?; + let tx_2 = vm.execute( + &caller_private_key, + ("dependent.aleo", "sum_and_check"), + vec![Value::from_str("1u8")?, Value::from_str("1u8")?].into_iter(), + None, + 0, + None, + rng, + )?; + let block = sample_next_block(&vm, &caller_private_key, &[tx_1, tx_2], rng)?; + assert_eq!(block.transactions().num_accepted(), 2); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block)?; + + // Verify that the sum function fails on overflow. + let result = std::panic::catch_unwind(AssertUnwindSafe(|| { + vm.execute( + &caller_private_key, + ("dependent.aleo", "sum"), + vec![Value::from_str("255u8")?, Value::from_str("1u8")?].into_iter(), + None, + 0, + None, + rng, + ) + })); + assert!(result.is_err()); + + // Get a valid execution before the dependency upgrade. + let sum_unchecked = vm.execute( + &caller_private_key, + ("dependent.aleo", "sum_unchecked"), + vec![Value::from_str("1u8")?, Value::from_str("1u8")?].into_iter(), + None, + 0, + None, + rng, + )?; + assert!(vm.check_transaction(&sum_unchecked, None, rng).is_ok()); + + // Update the dependency to v1. + let transaction = vm.deploy(&caller_private_key, &dependency_v1, None, 0, None, rng)?; + let block = sample_next_block(&vm, &caller_private_key, &[transaction], rng)?; + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block)?; + + // Verify that the original sum transaction fails after the dependency upgrade. + vm.partially_verified_transactions().write().clear(); + assert!(vm.check_transaction(&sum_unchecked, None, rng).is_err()); + let block = sample_next_block(&vm, &caller_private_key, &[sum_unchecked], rng)?; + assert_eq!(block.transactions().num_accepted(), 0); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 1); + vm.add_next_block(&block)?; + + // Verify that the sum function fails on edition check. + let tx_1 = vm.execute( + &caller_private_key, + ("dependent.aleo", "sum"), + vec![Value::from_str("1u8")?, Value::from_str("1u8")?].into_iter(), + None, + 0, + None, + rng, + )?; + let tx_2 = vm.execute( + &caller_private_key, + ("dependent.aleo", "sum_and_check"), + vec![Value::from_str("1u8")?, Value::from_str("1u8")?].into_iter(), + None, + 0, + None, + rng, + )?; + let block = sample_next_block(&vm, &caller_private_key, &[tx_1, tx_2], rng)?; + assert_eq!(block.transactions().num_accepted(), 0); + assert_eq!(block.transactions().num_rejected(), 2); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block)?; + + // Update the dependent to v1. + let transaction = vm.deploy(&caller_private_key, &dependent_v1, None, 0, None, rng)?; + let block = sample_next_block(&vm, &caller_private_key, &[transaction], rng)?; + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block)?; + + // Verify that the sum function passes. + let tx_1 = vm.execute( + &caller_private_key, + ("dependent.aleo", "sum"), + vec![Value::from_str("1u8")?, Value::from_str("1u8")?].into_iter(), + None, + 0, + None, + rng, + )?; + let tx_2 = vm.execute( + &caller_private_key, + ("dependent.aleo", "sum"), + vec![Value::from_str("255u8")?, Value::from_str("1u8")?].into_iter(), + None, + 0, + None, + rng, + )?; + let block = sample_next_block(&vm, &caller_private_key, &[tx_1, tx_2], rng)?; + assert_eq!(block.transactions().num_accepted(), 2); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block)?; + + Ok(()) +} + +// This test checks that: +// - programs can be upgraded to create cycles in the dependency graph. +// - programs can be upgraded to create cycles in the call graph. +// - executions of cyclic programs w.r.t. to the call graph are rejected. +#[test] +fn test_upgrade_with_cycles() -> Result<()> { + let rng = &mut TestRng::default(); + + // Initialize a new caller. + let caller_private_key = sample_genesis_private_key(rng); + + // Initialize the VM. + let vm = sample_vm_at_height(CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V8)?, rng); + + // Define the programs. + let first_v0 = Program::from_str( + r" +program first.aleo; + +function foo: + input r0 as u8.public; + output r0 as u8.public; + +constructor: + assert.eq true true; + ", + )?; + + let second_v0 = Program::from_str( + r" +import first.aleo; + +program second.aleo; + +function foo: + input r0 as u8.public; + call first.aleo/foo r0 into r1; + output r1 as u8.public; + +constructor: + assert.eq true true; + ", + )?; + + let first_v1 = Program::from_str( + r" +import second.aleo; + +program first.aleo; + +function foo: + input r0 as u8.public; + output r0 as u8.public; + +constructor: + assert.eq true true; + ", + )?; + + let first_v2 = Program::from_str( + r" +import second.aleo; + +program first.aleo; + +function foo: + input r0 as u8.public; + call second.aleo/foo r0 into r1; + output r1 as u8.public; + +constructor: + assert.eq true true; + ", + )?; + + // Deploy the first version of the programs. + let transaction = vm.deploy(&caller_private_key, &first_v0, None, 0, None, rng)?; + let block = sample_next_block(&vm, &caller_private_key, &[transaction], rng)?; + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block)?; + + let transaction = vm.deploy(&caller_private_key, &second_v0, None, 0, None, rng)?; + let block = sample_next_block(&vm, &caller_private_key, &[transaction], rng)?; + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block)?; + + // Verify that both can be executed correctly. + let tx_1 = vm.execute( + &caller_private_key, + ("first.aleo", "foo"), + vec![Value::from_str("1u8")?].into_iter(), + None, + 0, + None, + rng, + )?; + let tx_2 = vm.execute( + &caller_private_key, + ("second.aleo", "foo"), + vec![Value::from_str("1u8")?].into_iter(), + None, + 0, + None, + rng, + )?; + let block = sample_next_block(&vm, &caller_private_key, &[tx_1, tx_2], rng)?; + assert_eq!(block.transactions().num_accepted(), 2); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block)?; + + // Update the first program to create a cycle in the dependency graph. + let transaction = vm.deploy(&caller_private_key, &first_v1, None, 0, None, rng)?; + let block = sample_next_block(&vm, &caller_private_key, &[transaction], rng)?; + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block)?; + + // Verify that both programs can be executed correctly. + let tx_1 = vm.execute( + &caller_private_key, + ("first.aleo", "foo"), + vec![Value::from_str("1u8")?].into_iter(), + None, + 0, + None, + rng, + )?; + let tx_2 = vm.execute( + &caller_private_key, + ("second.aleo", "foo"), + vec![Value::from_str("1u8")?].into_iter(), + None, + 0, + None, + rng, + )?; + let block = sample_next_block(&vm, &caller_private_key, &[tx_1, tx_2], rng)?; + assert_eq!(block.transactions().num_accepted(), 2); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block)?; + + // Update the first program to create mutual recursion. + let transaction = vm.deploy(&caller_private_key, &first_v2, None, 0, None, rng)?; + let block = sample_next_block(&vm, &caller_private_key, &[transaction], rng)?; + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block)?; + + // Verify that the first program is no longer executable. + assert!( + vm.execute( + &caller_private_key, + ("first.aleo", "foo"), + vec![Value::from_str("1u8")?].into_iter(), + None, + 0, + None, + rng, + ) + .is_err() + ); + + Ok(()) +} + +// This test checks that a deployment with a failing _init block is rejected. +#[test] +fn test_failing_init_block() -> Result<()> { + let rng = &mut TestRng::default(); + + // Initialize a new caller. + let caller_private_key = sample_genesis_private_key(rng); + + // Initialize the VM. + let vm = sample_vm_at_height(CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V8)?, rng); + + // Define the programs. + let passing_program = Program::from_str( + r" +program hello1.aleo; + +function foo: + input r0 as u8.public; + output r0 as u8.public; + +constructor: + assert.eq true true; + ", + )?; + + let failing_program = Program::from_str( + r" +program hello2.aleo; + +function foo: + input r0 as u8.public; + output r0 as u8.public; + +constructor: + assert.eq true false; + ", + )?; + + // Deploy the passing program. + let transaction = vm.deploy(&caller_private_key, &passing_program, None, 0, None, rng)?; + let block = sample_next_block(&vm, &caller_private_key, &[transaction], rng)?; + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block)?; + + // Deploy the failing program. + let transaction = vm.deploy(&caller_private_key, &failing_program, None, 0, None, rng)?; + let block = sample_next_block(&vm, &caller_private_key, &[transaction], rng)?; + assert_eq!(block.transactions().num_accepted(), 0); + assert_eq!(block.transactions().num_rejected(), 1); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block)?; + + Ok(()) +} + +// This tests verifies that anyone can upgrade a program whose `upgradable` metadata is set to `true` and has an intentionally empty constructor. +#[test] +fn test_anyone_can_upgrade() -> Result<()> { + let rng = &mut TestRng::default(); + + // Initialize a new caller. + let caller_private_key = sample_genesis_private_key(rng); + + // Initialize unrelated callers. + let unrelated_caller_private_key_0 = PrivateKey::new(rng)?; + let unrelated_caller_address_0 = Address::try_from(&unrelated_caller_private_key_0)?; + let unrelated_caller_private_key_1 = PrivateKey::new(rng)?; + let unrelated_caller_address_1 = Address::try_from(&unrelated_caller_private_key_1)?; + + // Initialize the VM. + let vm = sample_vm_at_height(CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V8)?, rng); + + // Fund the unrelated callers. + let transfer_1 = vm.execute( + &caller_private_key, + ("credits.aleo", "transfer_public"), + vec![Value::from_str(&format!("{}", unrelated_caller_address_0))?, Value::from_str("1_000_000_000_000u64")?] + .into_iter(), + None, + 0, + None, + rng, + )?; + let transfer_2 = vm.execute( + &caller_private_key, + ("credits.aleo", "transfer_public"), + vec![Value::from_str(&format!("{}", unrelated_caller_address_1))?, Value::from_str("1_000_000_000_000u64")?] + .into_iter(), + None, + 0, + None, + rng, + )?; + let block = sample_next_block(&vm, &caller_private_key, &[transfer_1, transfer_2], rng)?; + assert_eq!(block.transactions().num_accepted(), 2); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block)?; + + // Define the programs. + let program_v0 = Program::from_str( + r" +program upgradable.aleo; +function foo: +constructor: + assert.eq true true; + ", + )?; + + let program_v1 = Program::from_str( + r" +program upgradable.aleo; +function foo: +function bar: +constructor: + assert.eq true true; + ", + )?; + + let program_v2 = Program::from_str( + r" +program upgradable.aleo; +function foo: +function bar: +function baz: +constructor: + assert.eq true true; + ", + )?; + + // Deploy the first version of the program. + let transaction = vm.deploy(&caller_private_key, &program_v0, None, 0, None, rng)?; + let block = sample_next_block(&vm, &caller_private_key, &[transaction], rng)?; + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block)?; + + // Deploy the second version of the program. + let transaction = vm.deploy(&unrelated_caller_private_key_0, &program_v1, None, 0, None, rng)?; + let block = sample_next_block(&vm, &caller_private_key, &[transaction], rng)?; + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block)?; + + // Deploy the third version of the program. + let transaction = vm.deploy(&unrelated_caller_private_key_1, &program_v2, None, 0, None, rng)?; + let block = sample_next_block(&vm, &caller_private_key, &[transaction], rng)?; + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block)?; + + Ok(()) +} + +// This test checks that the following program variants cannot be upgraded: +// - a program with no constructor +// - a program with a constructor that restricts upgrades +#[test] +fn test_non_upgradable_programs() -> Result<()> { + let rng = &mut TestRng::default(); + + // Initialize a new caller. + let caller_private_key = sample_genesis_private_key(rng); + + // Initialize the VM. + let vm = sample_vm_at_height(CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V8)?, rng); + + // Define the programs. + let program_0_v0 = Program::from_str( + r" +program non_upgradable_0.aleo; +function foo: + ", + )?; + + let program_0_v1 = Program::from_str( + r" +program non_upgradable_0.aleo; +function foo: +function bar: + ", + )?; + + // Deploy the programs and then attempt to upgrade. The upgrade should fail. + let transaction = vm.deploy(&caller_private_key, &program_0_v0, None, 0, None, rng)?; + let block = sample_next_block(&vm, &caller_private_key, &[transaction], rng)?; + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block)?; + assert!(vm.deploy(&caller_private_key, &program_0_v1, None, 0, None, rng).is_err()); + + let program_1_v0 = Program::from_str( + r" +program non_upgradable_1.aleo; +function foo: +constructor: + assert.eq edition 0u16; + ", + )?; + + let program_1_v1 = Program::from_str( + r" +program non_upgradable_1.aleo; +function foo: +function bar: +constructor: + assert.eq edition 0u16; + ", + )?; + + // Deploy the program and then upgrade. The upgrade should fail to be finalized. + let transaction = vm.deploy(&caller_private_key, &program_1_v0, None, 0, None, rng)?; + let block = sample_next_block(&vm, &caller_private_key, &[transaction], rng)?; + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block)?; + + let transaction = vm.deploy(&caller_private_key, &program_1_v1, None, 0, None, rng)?; + let block = sample_next_block(&vm, &caller_private_key, &[transaction], rng)?; + assert_eq!(block.transactions().num_accepted(), 0); + assert_eq!(block.transactions().num_rejected(), 1); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block)?; + + Ok(()) +} + +// This test checks that a program can be made non-upgradable after being upgradable. +#[test] +fn test_downgrade_upgradable_program() -> Result<()> { + let rng = &mut TestRng::default(); + + // Initialize a new caller. + let caller_private_key = sample_genesis_private_key(rng); + + // Initialize the VM. + let vm = sample_vm_at_height(CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V8)?, rng); + + // Define the programs. + let program_v0 = Program::from_str( + r" +program upgradable.aleo; +mapping locked: + key as boolean.public; + value as boolean.public; +function set_lock: + async set_lock into r0; + output r0 as upgradable.aleo/set_lock.future; +finalize set_lock: + set true into locked[true]; +function foo: +constructor: + contains locked[true] into r0; + assert.eq r0 false; + ", + )?; + + let program_v1 = Program::from_str( + r" +program upgradable.aleo; +mapping locked: + key as boolean.public; + value as boolean.public; +function set_lock: + async set_lock into r0; + output r0 as upgradable.aleo/set_lock.future; +finalize set_lock: + set true into locked[true]; +function foo: +function bar: +constructor: + contains locked[true] into r0; + assert.eq r0 false; + ", + )?; + + let program_v2 = Program::from_str( + r" +program upgradable.aleo; +mapping locked: + key as boolean.public; + value as boolean.public; +function set_lock: + async set_lock into r0; + output r0 as upgradable.aleo/set_lock.future; +finalize set_lock: + set true into locked[true]; +function foo: +function bar: +function baz: +constructor: + contains locked[true] into r0; + assert.eq r0 false; + ", + )?; + + // Deploy the first version of the program. + let transaction = vm.deploy(&caller_private_key, &program_v0, None, 0, None, rng)?; + let block = sample_next_block(&vm, &caller_private_key, &[transaction], rng)?; + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block)?; + + // Deploy the second version of the program. + let transaction = vm.deploy(&caller_private_key, &program_v1, None, 0, None, rng)?; + let block = sample_next_block(&vm, &caller_private_key, &[transaction], rng)?; + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block)?; + + // Set the lock. + let transaction = vm.execute( + &caller_private_key, + ("upgradable.aleo", "set_lock"), + Vec::>::new().into_iter(), + None, + 0, + None, + rng, + )?; + let block = sample_next_block(&vm, &caller_private_key, &[transaction], rng)?; + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block)?; + + // Attempt to deploy the third version of the program. + let transaction = vm.deploy(&caller_private_key, &program_v2, None, 0, None, rng)?; + let block = sample_next_block(&vm, &caller_private_key, &[transaction], rng)?; + assert_eq!(block.transactions().num_accepted(), 0); + assert_eq!(block.transactions().num_rejected(), 1); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block)?; + + Ok(()) +} + +// This test checks that an upgrade can be locked to a checksum. +// The checksum is managed by an admin address. +#[test] +fn test_lock_upgrade_to_checksum() -> Result<()> { + let rng = &mut TestRng::default(); + + // Initialize a new caller. + let caller_private_key = sample_genesis_private_key(rng); + let caller_address = Address::try_from(&caller_private_key)?; + + // Initialize the VM. + let vm = sample_vm_at_height(CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V8)?, rng); + + // Define the programs. + let program_v0 = Program::from_str(&format!( + r" +program locked_upgrade.aleo; +mapping admin: + key as boolean.public; + value as address.public; +mapping expected_checksum: + key as boolean.public; + value as [u8; 32u32].public; +function set_expected: + input r0 as [u8; 32u32].public; + async set_expected self.caller r0 into r1; + output r1 as locked_upgrade.aleo/set_expected.future; +finalize set_expected: + input r0 as address.public; + input r1 as [u8; 32u32].public; + get admin[true] into r2; + assert.eq r0 r2; + set r1 into expected_checksum[true]; +constructor: + branch.neq edition 0u16 to rest; + set {caller_address} into admin[true]; + branch.eq true true to end; + position rest; + get expected_checksum[true] into r0; + assert.eq r0 checksum; + position end; + " + ))?; + + let program_v1 = Program::from_str(&format!( + r" +program locked_upgrade.aleo; +mapping admin: + key as boolean.public; + value as address.public; +mapping expected_checksum: + key as boolean.public; + value as [u8; 32u32].public; +function bar: +function set_expected: + input r0 as [u8; 32u32].public; + async set_expected self.caller r0 into r1; + output r1 as locked_upgrade.aleo/set_expected.future; +finalize set_expected: + input r0 as address.public; + input r1 as [u8; 32u32].public; + get admin[true] into r2; + assert.eq r0 r2; + set r1 into expected_checksum[true]; +constructor: + branch.neq edition 0u16 to rest; + set {caller_address} into admin[true]; + branch.eq true true to end; + position rest; + get expected_checksum[true] into r0; + assert.eq r0 checksum; + position end; + " + ))?; + + let program_v1_mismatch = Program::from_str(&format!( + r" +program locked_upgrade.aleo; +mapping admin: + key as boolean.public; + value as address.public; +mapping expected_checksum: + key as boolean.public; + value as [u8; 32u32].public; +function baz: +function set_expected: + input r0 as [u8; 32u32].public; + async set_expected self.caller r0 into r1; + output r1 as locked_upgrade.aleo/set_expected.future; +finalize set_expected: + input r0 as address.public; + input r1 as [u8; 32u32].public; + get admin[true] into r2; + assert.eq r0 r2; + set r1 into expected_checksum[true]; +constructor: + branch.neq edition 0u16 to rest; + set {caller_address} into admin[true]; + branch.eq true true to end; + position rest; + get expected_checksum[true] into r0; + assert.eq r0 checksum; + position end; + " + ))?; + + // Deploy the first version of the program. + let transaction = vm.deploy(&caller_private_key, &program_v0, None, 0, None, rng)?; + let block = sample_next_block(&vm, &caller_private_key, &[transaction], rng)?; + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block)?; + + // Check that the caller is the admin. + let Some(Value::Plaintext(Plaintext::Literal(Literal::Address(admin), _))) = + vm.finalize_store().get_value_confirmed( + ProgramID::from_str("locked_upgrade.aleo")?, + Identifier::from_str("admin")?, + &Plaintext::from_str("true")?, + )? + else { + bail!("Unexpected entry in admin mapping"); + }; + assert_eq!(admin, caller_address); + + // Attempt to upgrade without setting the expected checksum. + let transaction = vm.deploy(&caller_private_key, &program_v1, None, 0, None, rng)?; + let block = sample_next_block(&vm, &caller_private_key, &[transaction], rng)?; + assert_eq!(block.transactions().num_accepted(), 0); + assert_eq!(block.transactions().num_rejected(), 1); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block)?; + + // Attempt to set the expected checksum with the wrong admin. + let checksum = Value::from_str( + r"[ + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8 + ]", + )?; + let admin_private_key = PrivateKey::new(rng)?; + let transaction = vm.execute( + &admin_private_key, + ("locked_upgrade.aleo", "set_expected"), + vec![checksum].into_iter(), + None, + 0, + None, + rng, + )?; + let block = sample_next_block(&vm, &caller_private_key, &[transaction], rng)?; + assert_eq!(block.transactions().num_accepted(), 0); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 1); + vm.add_next_block(&block)?; + + // Check that there is no expected checksum set. + assert!( + vm.finalize_store() + .get_value_confirmed( + ProgramID::from_str("locked_upgrade.aleo")?, + Identifier::from_str("expected_checksum")?, + &Plaintext::from_str("true")?, + )? + .is_none() + ); + + // Set the expected checksum. + let checksum = program_v1.to_checksum(); + let transaction = vm.execute( + &caller_private_key, + ("locked_upgrade.aleo", "set_expected"), + vec![Value::from_str(&format!("[{}]", checksum.iter().join(", ")))].into_iter(), + None, + 0, + None, + rng, + )?; + let block = sample_next_block(&vm, &caller_private_key, &[transaction], rng)?; + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block)?; + + // Check that the expected checksum is set. + let Some(Value::Plaintext(expected)) = vm.finalize_store().get_value_confirmed( + ProgramID::from_str("locked_upgrade.aleo")?, + Identifier::from_str("expected_checksum")?, + &Plaintext::from_str("true")?, + )? + else { + bail!("Unexpected entry in expected_checksum mapping"); + }; + assert_eq!(Plaintext::from(checksum), expected); + + // Attempt to upgrade with a mismatched program. + let transaction = vm.deploy(&caller_private_key, &program_v1_mismatch, None, 0, None, rng)?; + let block = sample_next_block(&vm, &caller_private_key, &[transaction], rng)?; + assert_eq!(block.transactions().num_accepted(), 0); + assert_eq!(block.transactions().num_rejected(), 1); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block)?; + + // Update with the expected checksum set. + let transaction = vm.deploy(&caller_private_key, &program_v1, None, 0, None, rng)?; + let block = sample_next_block(&vm, &caller_private_key, &[transaction], rng)?; + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block)?; + + Ok(()) +} + +#[test] +fn test_upgrade_without_changing_contents_fails() -> Result<()> { + let rng = &mut TestRng::default(); + + // Initialize a new caller. + let caller_private_key = sample_genesis_private_key(rng); + + // Initialize the VM. + let vm = sample_vm_at_height(CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V8)?, rng); + + // Define the program. + let program_v0 = Program::from_str( + r" +program upgradable.aleo; +constructor: + assert.eq true true; +function dummy:", + )?; + + // Define a variant of the program that contains an extra mapping. + let program_v1 = Program::from_str( + r" +program upgradable.aleo; +constructor: + assert.eq true true; +mapping foo: + key as boolean.public; + value as boolean.public; +function dummy:", + )?; + + // Construct the first deployment. + let transaction_first = vm.deploy(&caller_private_key, &program_v0, None, 0, None, rng)?; + let transaction_id_first = transaction_first.id(); + let deployment_id_first = transaction_first.deployment().unwrap().to_deployment_id()?; + + // Construct the second deployment. + let Transaction::Deploy(_, _, owner, deployment, fee) = + vm.deploy(&caller_private_key, &program_v0, None, 0, None, rng)? + else { + bail!("Expected deployment"); + }; + let deployment = Deployment::new( + 1, + deployment.program().clone(), + deployment.verifying_keys().clone(), + deployment.program_checksum().cloned(), + )?; + let transaction_second = Transaction::from_deployment(owner, deployment, fee)?; + let transaction_id_second = transaction_second.id(); + let deployment_id_second = transaction_second.deployment().unwrap().to_deployment_id()?; + + // Construct the third deployment. + let Transaction::Deploy(_, _, owner, deployment, fee) = + vm.deploy(&caller_private_key, &program_v1, None, 0, None, rng)? + else { + bail!("Expected deployment"); + }; + let deployment = Deployment::new( + 1, + deployment.program().clone(), + deployment.verifying_keys().clone(), + deployment.program_checksum().cloned(), + )?; + let transaction_third = Transaction::from_deployment(owner, deployment, fee)?; + let transaction_id_third = transaction_third.id(); + let deployment_id_third = transaction_third.deployment().unwrap().to_deployment_id()?; + + // Check that the first and second deployment IDs are the same. + assert_eq!(deployment_id_first, deployment_id_second); + // Check that the first and third deployment IDs are different. + assert_ne!(deployment_id_first, deployment_id_third); + // Check that the first, second, and third transaction IDs are different. + assert_ne!(transaction_id_first, transaction_id_second); + assert_ne!(transaction_id_first, transaction_id_third); + + // Add the first deployment to the chain. + let block = sample_next_block(&vm, &caller_private_key, &[transaction_first], rng)?; + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block)?; + + // Deploy the program again as an upgrade. + let block = sample_next_block(&vm, &caller_private_key, &[transaction_second], rng)?; + assert_eq!(block.transactions().num_accepted(), 0); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 1); + vm.add_next_block(&block)?; + + // Deploy the program with an extra mapping as an upgrade. + let block = sample_next_block(&vm, &caller_private_key, &[transaction_third], rng)?; + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + + Ok(()) +} diff --git a/synthesizer/src/vm/verify.rs b/synthesizer/src/vm/verify.rs index 2718d253b0..9d548be703 100644 --- a/synthesizer/src/vm/verify.rs +++ b/synthesizer/src/vm/verify.rs @@ -149,17 +149,84 @@ impl> VM { Transaction::Deploy(id, deployment_id, owner, deployment, _) => { // Verify the signature corresponds to the transaction ID. ensure!(owner.verify(*deployment_id), "Invalid owner signature for deployment transaction '{id}'"); - // Ensure the edition is correct. - if deployment.edition() != N::EDITION { - bail!("Invalid deployment transaction '{id}' - expected edition {}", N::EDITION) + // If the `CONSENSUS_VERSION` is `V8` or greater, then verify that the program checksum is present. + // Otherwise, verify that the deployment edition is zero, that the program checksum is **not** present in the deployment, + // and that the program does not use constructors, `Operand::Checksum`, or `Operand::Edition`. + let consensus_version = N::CONSENSUS_VERSION(self.block_store().current_block_height())?; + match consensus_version >= ConsensusVersion::V8 { + true => ensure!( + deployment.program_checksum().is_some(), + "Invalid deployment transaction '{id}' - missing program checksum" + ), + false => { + ensure!( + deployment.edition().is_zero(), + "Invalid deployment transaction '{id}' - edition should be zero" + ); + ensure!( + deployment.program_checksum().is_none(), + "Invalid deployment transaction '{id}' - should not contain program checksum" + ); + ensure!( + !deployment.program().uses_constructor_checksum_or_edition(), + "Invalid deployment transaction '{id}' - should not use 'constructor's, the 'checksum' operand, or 'edition' operand " + ); + } } - // Ensure the program ID does not already exist in the store. - if self.transaction_store().contains_program_id(deployment.program_id())? { - bail!("Program ID '{}' is already deployed", deployment.program_id()) + // If the program checksum exists, then verify that it is correct. + if let Some(given_checksum) = deployment.program_checksum() { + // Compute the expected checksum. + let expected_checksum = deployment.program().to_checksum(); + ensure!( + given_checksum == &expected_checksum, + "The checksum given in the deployment did not match the expected checksum\n('[{}]' != '[{}]')", + given_checksum.iter().join(", "), + expected_checksum.iter().join(", ") + ); } - // Ensure the program does not already exist in the process. - if self.contains_program(deployment.program_id()) { - bail!("Program ID '{}' already exists", deployment.program_id()); + // If the edition is zero, then check that: + // - The program does not exist in the store or process. + // Otherwise, check that: + // - The program exists in the store and process. + // - The existing program is upgradable, meaning that it has a constructor. + // - The new edition increments the old edition. + let is_program_in_storage = self.transaction_store().contains_program_id(deployment.program_id())?; + let is_program_in_process = self.contains_program(deployment.program_id()); + match deployment.edition() { + 0 => { + // Ensure the program ID does not already exist in the store. + ensure!(!is_program_in_storage, "Program ID '{}' is already deployed", deployment.program_id()); + // Ensure the program does not already exist in the process. + ensure!(!is_program_in_process, "Program ID '{}' already exists", deployment.program_id()); + } + new_edition => { + // Check that the program exists. + ensure!( + is_program_in_storage, + "Invalid deployment transaction '{id}' - program does not exist in the store" + ); + ensure!( + is_program_in_process, + "Invalid deployment transaction '{id}' - program does not exist in the process" + ); + // Get the existing program. + // It should be the case that the stored program matches the process program. + let stack = self.process().read().get_stack(deployment.program_id())?; + // Check that the program is upgradable, meaning that it has a constructor. + ensure!( + stack.program().contains_constructor(), + "Invalid deployment transaction '{id}' - program is not upgradable because it does not contain a constructor" + ); + // Check that the new edition increments the old edition. + let old_edition = **stack.program_edition(); + let expected_edition = old_edition + .checked_add(1) + .ok_or_else(|| anyhow!("Invalid deployment transaction '{id}' - next edition overflows"))?; + ensure!( + expected_edition == new_edition, + "Invalid deployment transaction '{id}' - next edition ('{new_edition}') does not match the expected edition ('{expected_edition}')", + ); + } } // Enforce the syntax restrictions on the programs based on the current consensus version. let current_block_height = self.block_store().current_block_height(); diff --git a/synthesizer/tests/expectations/vm/execute_and_finalize/good_constructor.out b/synthesizer/tests/expectations/vm/execute_and_finalize/good_constructor.out index 5f792c8f3c..63397c2b0e 100644 --- a/synthesizer/tests/expectations/vm/execute_and_finalize/good_constructor.out +++ b/synthesizer/tests/expectations/vm/execute_and_finalize/good_constructor.out @@ -4,11 +4,11 @@ outputs: execute: good_constructor.aleo/check: outputs: - - '{"type":"future","id":"4840279410846804147436041831820290497261407528526240026855847981786978386825field","value":"{\n program_id: good_constructor.aleo,\n function_name: check,\n arguments: []\n}"}' + - '{"type":"future","id":"7193918481944815281366135374398693434460015709838433843365738057926267099778field","value":"{\n program_id: good_constructor.aleo,\n function_name: check,\n arguments: []\n}"}' speculate: the execution was accepted add_next_block: succeeded. additional: - child_outputs: credits.aleo/fee_public: outputs: - - '{"type":"future","id":"966409966695246773656312280131645182827272260712453148901940367292843867854field","value":"{\n program_id: credits.aleo,\n function_name: fee_public,\n arguments: [\n aleo1qr2ha4pfs5l28aze88yn6fhleeythklkczrule2v838uwj65n5gqxt9djx,\n 11724u64\n ]\n}"}' + - '{"type":"future","id":"341861574175661095501351520594605156279706665066223111854585628429923358450field","value":"{\n program_id: credits.aleo,\n function_name: fee_public,\n arguments: [\n aleo1qr2ha4pfs5l28aze88yn6fhleeythklkczrule2v838uwj65n5gqxt9djx,\n 3224u64\n ]\n}"}' diff --git a/synthesizer/tests/expectations/vm/execute_and_finalize/test_rand.out b/synthesizer/tests/expectations/vm/execute_and_finalize/test_rand.out index 0af538e48d..333d00e45d 100644 --- a/synthesizer/tests/expectations/vm/execute_and_finalize/test_rand.out +++ b/synthesizer/tests/expectations/vm/execute_and_finalize/test_rand.out @@ -25,7 +25,7 @@ outputs: execute: test_rand.aleo/rand_chacha_check: outputs: - - '{"type":"future","id":"818878742790741579153893179075772445872751227433677932822653185952935999557field","value":"{\n program_id: test_rand.aleo,\n function_name: rand_chacha_check,\n arguments: [\n 1field,\n true\n ]\n}"}' + - '{"type":"future","id":"3094980085105288375234060174899683221046711858965162333618502474712664334238field","value":"{\n program_id: test_rand.aleo,\n function_name: rand_chacha_check,\n arguments: [\n 2field,\n true\n ]\n}"}' speculate: the execution was accepted add_next_block: succeeded. additional: diff --git a/synthesizer/tests/test_vm_execute_and_finalize.rs b/synthesizer/tests/test_vm_execute_and_finalize.rs index f02ecc4958..e1a94c394c 100644 --- a/synthesizer/tests/test_vm_execute_and_finalize.rs +++ b/synthesizer/tests/test_vm_execute_and_finalize.rs @@ -76,7 +76,7 @@ fn run_test(test: &ProgramTest) -> serde_yaml::Mapping { let genesis_private_key = PrivateKey::::new(rng).unwrap(); // Initialize the VM. - let (vm, _) = initialize_vm(&genesis_private_key, rng); + let (vm, _) = initialize_vm(&genesis_private_key, test.start_height(), rng); // Fund the additional keys. for key in test.keys() { @@ -370,6 +370,7 @@ fn run_test(test: &ProgramTest) -> serde_yaml::Mapping { #[allow(clippy::type_complexity)] fn initialize_vm( private_key: &PrivateKey, + height: u32, rng: &mut R, ) -> (VM>, Vec>>) { // Initialize a VM. @@ -387,6 +388,36 @@ fn initialize_vm( // Add the genesis block to the VM. vm.add_next_block(&genesis).unwrap(); + // If the desired height is greater than zero, add additional blocks to the VM. + for _ in 0..height { + let time_since_last_block = CurrentNetwork::BLOCK_TIME as i64; + let (ratifications, transactions, aborted_transaction_ids, ratified_finalize_operations) = vm + .speculate( + construct_finalize_global_state(&vm), + time_since_last_block, + Some(0u64), + vec![], + &None.into(), + [].into_iter(), + rng, + ) + .unwrap(); + assert!(aborted_transaction_ids.is_empty()); + + let block = construct_next_block( + &vm, + time_since_last_block, + private_key, + ratifications, + transactions, + aborted_transaction_ids, + ratified_finalize_operations, + rng, + ) + .unwrap(); + vm.add_next_block(&block).unwrap(); + } + (vm, records) } diff --git a/synthesizer/tests/tests/vm/execute_and_finalize/bad_constructor_fail.aleo b/synthesizer/tests/tests/vm/execute_and_finalize/bad_constructor_fail.aleo index 5fa8873161..c82afabb54 100644 --- a/synthesizer/tests/tests/vm/execute_and_finalize/bad_constructor_fail.aleo +++ b/synthesizer/tests/tests/vm/execute_and_finalize/bad_constructor_fail.aleo @@ -1,5 +1,6 @@ /* randomness: 45791624 +start_height: 16 cases: - program: bad_constructor.aleo function: foo diff --git a/synthesizer/tests/tests/vm/execute_and_finalize/good_constructor.aleo b/synthesizer/tests/tests/vm/execute_and_finalize/good_constructor.aleo index 9e32a7fe51..0a6518991e 100644 --- a/synthesizer/tests/tests/vm/execute_and_finalize/good_constructor.aleo +++ b/synthesizer/tests/tests/vm/execute_and_finalize/good_constructor.aleo @@ -1,5 +1,6 @@ /* randomness: 45791624 +start_height: 16 cases: - program: good_constructor.aleo function: check @@ -17,22 +18,23 @@ mapping data: constructor: assert.eq edition 0u16; assert.eq credits.aleo/edition 0u16; - assert.neq checksum 0field; - assert.eq credits.aleo/checksum 6192738754253668739186185034243585975029374333074931926190215457304721124008field; - contains data[0u8] into r0; // Check `contains` without value - assert.eq r0 false; - get.or_use data[0u8] 8u8 into r1; // Check `get.or_use` without value - assert.eq r1 8u8; + cast 225u8 167u8 254u8 122u8 87u8 199u8 147u8 168u8 125u8 197u8 178u8 254u8 68u8 130u8 140u8 31u8 208u8 229u8 200u8 142u8 181u8 33u8 217u8 47u8 8u8 12u8 40u8 190u8 52u8 133u8 68u8 224u8 into r0 as [u8; 32u32]; + assert.eq credits.aleo/checksum r0; + assert.neq checksum r0; + contains data[0u8] into r1; // Check `contains` without value + assert.eq r1 false; + get.or_use data[0u8] 8u8 into r2; // Check `get.or_use` without value + assert.eq r2 8u8; set 1u8 into data[0u8]; // Check `set` - contains data[0u8] into r2; // Check `contains` with value - assert.eq r2 true; - get.or_use data[0u8] 0u8 into r3; // Check `get.or_use` without value - assert.eq r3 1u8; - get data[0u8] into r4; // Check `get` with value + contains data[0u8] into r3; // Check `contains` with value + assert.eq r3 true; + get.or_use data[0u8] 0u8 into r4; // Check `get.or_use` without value assert.eq r4 1u8; + get data[0u8] into r5; // Check `get` with value + assert.eq r5 1u8; remove data[0u8]; // Check `remove` - contains data[0u8] into r5; // Check `contains` after removal - assert.eq r5 false; + contains data[0u8] into r6; // Check `contains` after removal + assert.eq r6 false; set 1u8 into data[0u8]; // Final set diff --git a/synthesizer/tests/tests/vm/execute_and_finalize/test_rand.aleo b/synthesizer/tests/tests/vm/execute_and_finalize/test_rand.aleo index 530d359f1f..7611ca3974 100644 --- a/synthesizer/tests/tests/vm/execute_and_finalize/test_rand.aleo +++ b/synthesizer/tests/tests/vm/execute_and_finalize/test_rand.aleo @@ -12,7 +12,7 @@ cases: inputs: [0field, false] - program: test_rand.aleo function: rand_chacha_check - inputs: [1field, true] + inputs: [2field, true] */ program test_rand.aleo; diff --git a/synthesizer/tests/utilities/tests/program_test.rs b/synthesizer/tests/utilities/tests/program_test.rs index 78127669fa..3e5d65dbac 100644 --- a/synthesizer/tests/utilities/tests/program_test.rs +++ b/synthesizer/tests/utilities/tests/program_test.rs @@ -46,6 +46,8 @@ pub struct ProgramTest { randomness: Option, /// Additional keys for the test. keys: Vec>, + /// The start height for the test. + start_height: u32, } impl ProgramTest { @@ -68,6 +70,11 @@ impl ProgramTest { pub fn keys(&self) -> &[PrivateKey] { &self.keys } + + /// Returns the start height for the test. + pub fn start_height(&self) -> u32 { + self.start_height + } } impl ExpectedTest for ProgramTest { @@ -92,6 +99,13 @@ impl ExpectedTest for ProgramTest { // If the `randomness` field is present in the config, parse it as a `u64`. let randomness = test_config.get("randomness").map(|value| value.as_u64().expect("`randomness` must be a u64")); + // If the `start_height` field is present in the config, parse it as a `u32`. + // Otherwise use the default value of 0. + let start_height = test_config + .get("start_height") + .map(|value| value.as_u64().expect("`start_height` must be a u32")) + .unwrap_or(0) as u32; + // If the `keys` field is present in the config, parse it as a sequence of `PrivateKey`s. let keys = match test_config.get("keys") { None => Vec::new(), @@ -131,7 +145,7 @@ impl ExpectedTest for ProgramTest { } }; - Self { programs, cases, expected, path, rewrite, randomness, keys } + Self { programs, cases, expected, path, rewrite, randomness, keys, start_height } } fn check(&self, output: &Self::Output) -> Result<()> { diff --git a/vm/package/deploy.rs b/vm/package/deploy.rs index 618d61dc49..9426cd2395 100644 --- a/vm/package/deploy.rs +++ b/vm/package/deploy.rs @@ -167,9 +167,6 @@ impl Package { #[cfg(test)] mod tests { - use super::*; - - type CurrentNetwork = snarkvm_console::network::MainnetV0; type CurrentAleo = snarkvm_circuit::network::AleoV0; #[test] @@ -181,7 +178,7 @@ mod tests { let deployment = package.deploy::(None).unwrap(); // Ensure the deployment edition matches. - assert_eq!(::EDITION, deployment.edition()); + assert_eq!(0, deployment.edition()); // Ensure the deployment program ID matches. assert_eq!(package.program().id(), deployment.program_id()); // Ensure the deployment program matches. @@ -200,7 +197,7 @@ mod tests { let deployment = package.deploy::(None).unwrap(); // Ensure the deployment edition matches. - assert_eq!(::EDITION, deployment.edition()); + assert_eq!(0, deployment.edition()); // Ensure the deployment program ID matches. assert_eq!(package.program().id(), deployment.program_id()); // Ensure the deployment program matches.