diff --git a/hs/spec_compliance/ic.did b/hs/spec_compliance/ic.did index c0d68446174..319159fd354 100644 --- a/hs/spec_compliance/ic.did +++ b/hs/spec_compliance/ic.did @@ -46,6 +46,18 @@ type change = record { details : change_details; }; +type canister_install_mode = variant { + install; + reinstall; + upgrade : opt record { + skip_pre_upgrade: opt bool; + wasm_memory_persistence : opt variant { + keep; + replace; + }; + }; +}; + type chunk_hash = blob; type http_header = record { name: text; value: text }; @@ -130,26 +142,14 @@ service ic : { clear_chunk_store: (record {canister_id : canister_id}) -> (); stored_chunks: (record {canister_id : canister_id}) -> (vec chunk_hash); install_code : (record { - mode : variant { - install; - reinstall; - upgrade : opt record { - skip_pre_upgrade: opt bool; - } - }; + mode : canister_install_mode; canister_id : canister_id; wasm_module : wasm_module; arg : blob; sender_canister_version : opt nat64; }) -> (); install_chunked_code: (record { - mode : variant { - install; - reinstall; - upgrade : opt record { - skip_pre_upgrade: opt bool; - }; - }; + mode : canister_install_mode; target_canister: canister_id; storage_canister: opt canister_id; chunk_hashes_list: vec chunk_hash; diff --git a/hs/spec_compliance/src/IC/Management.hs b/hs/spec_compliance/src/IC/Management.hs index f05975e5096..14a004f7422 100644 --- a/hs/spec_compliance/src/IC/Management.hs +++ b/hs/spec_compliance/src/IC/Management.hs @@ -36,9 +36,17 @@ type SenderCanisterVersion = type InstallMode = [candidType| - variant {install : null; reinstall : null; upgrade : opt record { + variant { + install : null; + reinstall : null; + upgrade : opt record { skip_pre_upgrade : opt bool; - }} + wasm_memory_persistence : opt variant { + keep; + replace; + }; + }; + } |] type RunState = diff --git a/rs/drun/BUILD.bazel b/rs/drun/BUILD.bazel index c87e47ef5cb..91bf527b975 100644 --- a/rs/drun/BUILD.bazel +++ b/rs/drun/BUILD.bazel @@ -36,6 +36,7 @@ DEPENDENCIES = [ "@crate_index//:tokio", "@crate_index//:tower", "@crate_index//:rand", + "@crate_index//:wasmparser", ] rust_library( diff --git a/rs/drun/Cargo.toml b/rs/drun/Cargo.toml index 7658b31e23b..ebdafb7f68f 100644 --- a/rs/drun/Cargo.toml +++ b/rs/drun/Cargo.toml @@ -45,6 +45,7 @@ tokio = { workspace = true } rand = "0.8" tower = { workspace = true } futures.workspace = true +wasmparser = "0.115.0" [[bin]] name = "drun" diff --git a/rs/drun/README.adoc b/rs/drun/README.adoc index b9facc7df36..56bd0fcbb77 100644 --- a/rs/drun/README.adoc +++ b/rs/drun/README.adoc @@ -45,6 +45,7 @@ Code installation messages have the following format: ---- * `` is one of `install`, `reinstall` or `upgrade` +- `drun` automatically detects whether the Wasm binary expects enhanced orthogonal persistence (as used in Motoko) and if so, sets the `wasm_memory_persistence` upgrade option. * `` is the desired ID for the canister to be installed, given in textual representation (e.g. `rwlgt-iiaaa-aaaaa-aaaaa-cai`) as specified in https://internetcomputer.org/docs/current/references/ic-interface-spec#textual-ids. diff --git a/rs/drun/src/message.rs b/rs/drun/src/message.rs index ea9fd898fc3..4a690a0d04e 100644 --- a/rs/drun/src/message.rs +++ b/rs/drun/src/message.rs @@ -1,7 +1,10 @@ use super::CanisterId; use hex::decode; -use ic_management_canister_types::{self as ic00, CanisterInstallMode, Payload}; +use ic_execution_environment::execution::upgrade::ENHANCED_ORTHOGONAL_PERSISTENCE_SECTION; +use ic_management_canister_types::{ + self as ic00, CanisterInstallModeV2, CanisterUpgradeOptions, Payload, WasmMemoryPersistence, +}; use ic_types::{ messages::{SignedIngress, UserQuery}, time::expiry_time_from_now, @@ -9,7 +12,6 @@ use ic_types::{ }; use std::{ - convert::TryFrom, fmt, fs::File, io::{self, Read}, @@ -216,6 +218,21 @@ fn parse_create(nonce: u64) -> Result { Ok(Message::Create(signed_ingress)) } +fn contains_icp_private_custom_section(wasm_binary: &[u8], name: &str) -> Result { + use wasmparser::{Parser, Payload::CustomSection}; + + let icp_section_name = format!("icp:private {name}"); + let parser = Parser::new(0); + for payload in parser.parse_all(wasm_binary) { + if let CustomSection(reader) = payload.map_err(|e| format!("Wasm parsing error: {}", e))? { + if reader.name() == icp_section_name { + return Ok(true); + } + } + } + Ok(false) +} + fn parse_install( nonce: u64, canister_id: &str, @@ -235,14 +252,36 @@ fn parse_install( let canister_id = parse_canister_id(canister_id)?; let payload = parse_octet_string(payload)?; + let install_mode = match mode { + "install" => CanisterInstallModeV2::Install, + "reinstall" => CanisterInstallModeV2::Reinstall, + "upgrade" => { + let wasm_memory_persistence = if contains_icp_private_custom_section( + wasm_data.as_ref(), + ENHANCED_ORTHOGONAL_PERSISTENCE_SECTION, + )? { + Some(WasmMemoryPersistence::Keep) + } else { + None + }; + CanisterInstallModeV2::Upgrade(Some(CanisterUpgradeOptions { + skip_pre_upgrade: None, + wasm_memory_persistence, + })) + } + _ => { + return Err(String::from("Unsupported install mode: {mode}")); + } + }; + let signed_ingress = SignedIngressBuilder::new() // `source` should become a self-authenticating id according // to https://internetcomputer.org/docs/current/references/ic-interface-spec#id-classes .canister_id(ic00::IC_00) .method_name(ic00::Method::InstallCode) .method_payload( - ic00::InstallCodeArgs::new( - CanisterInstallMode::try_from(mode.to_string()).unwrap(), + ic00::InstallCodeArgsV2::new( + install_mode, canister_id, wasm_data, payload, diff --git a/rs/execution_environment/src/canister_manager.rs b/rs/execution_environment/src/canister_manager.rs index 7813dfdbbe7..747949bce0e 100644 --- a/rs/execution_environment/src/canister_manager.rs +++ b/rs/execution_environment/src/canister_manager.rs @@ -881,14 +881,14 @@ impl CanisterManager { /// /// There are three modes of installation that are supported: /// - /// 1. `CanisterInstallMode::Install` + /// 1. `CanisterInstallModeV2::Install` /// Used for installing code on an empty canister. /// - /// 2. `CanisterInstallMode::Reinstall` + /// 2. `CanisterInstallModeV2::Reinstall` /// Used for installing code on a _non-empty_ canister. All existing /// state in the canister is cleared. /// - /// 3. `CanisterInstallMode::Upgrade` + /// 3. `CanisterInstallModeV2::Upgrade` /// Used for upgrading a canister while providing a mechanism to /// preserve its state. /// @@ -2097,6 +2097,12 @@ pub(crate) enum CanisterManagerError { canister_id: CanisterId, snapshot_id: SnapshotId, }, + MissingUpgradeOptionError { + message: String, + }, + InvalidUpgradeOptionError { + message: String, + }, } impl AsErrorHelp for CanisterManagerError { @@ -2132,12 +2138,12 @@ impl AsErrorHelp for CanisterManagerError { | CanisterManagerError::WasmChunkStoreError { .. } | CanisterManagerError::CanisterSnapshotNotFound { .. } | CanisterManagerError::CanisterHeapDeltaRateLimited { .. } - | CanisterManagerError::CanisterSnapshotInvalidOwnership { .. } => { - ErrorHelp::UserError { - suggestion: "".to_string(), - doc_link: "".to_string(), - } - } + | CanisterManagerError::CanisterSnapshotInvalidOwnership { .. } + | CanisterManagerError::MissingUpgradeOptionError { .. } + | CanisterManagerError::InvalidUpgradeOptionError { .. } => ErrorHelp::UserError { + suggestion: "".to_string(), + doc_link: "".to_string(), + }, } } } @@ -2405,6 +2411,22 @@ impl From for UserError { ) ) } + MissingUpgradeOptionError { message } => { + Self::new( + ErrorCode::CanisterContractViolation, + format!( + "Missing upgrade option: {}", message + ) + ) + } + InvalidUpgradeOptionError { message } => { + Self::new( + ErrorCode::CanisterContractViolation, + format!( + "Invalid upgrade option: {}", message + ) + ) + } } } } diff --git a/rs/execution_environment/src/canister_manager/tests.rs b/rs/execution_environment/src/canister_manager/tests.rs index 24bbde9f658..8fd8e5e8ba2 100644 --- a/rs/execution_environment/src/canister_manager/tests.rs +++ b/rs/execution_environment/src/canister_manager/tests.rs @@ -28,6 +28,7 @@ use ic_management_canister_types::{ CanisterStatusResultV2, CanisterStatusType, CanisterUpgradeOptions, ChunkHash, ClearChunkStoreArgs, CreateCanisterArgs, EmptyBlob, InstallCodeArgsV2, Method, Payload, StoredChunksArgs, StoredChunksReply, UpdateSettingsArgs, UploadChunkArgs, UploadChunkReply, + WasmMemoryPersistence, }; use ic_metrics::MetricsRegistry; use ic_registry_provisional_whitelist::ProvisionalWhitelist; @@ -4279,6 +4280,227 @@ fn test_upgrade_preserves_stable_memory() { assert_eq!(reply, data); } +#[test] +fn test_enhanced_orthogonal_persistence_upgrade_preserves_main_memory() { + let mut test = ExecutionTestBuilder::new().build(); + + let version1_wat = r#" + (module + (func $start + call $initialize + call $check + ) + (func $initialize + global.get 0 + i32.const 1234 + i32.store + global.get 1 + i32.const 5678 + i32.store + ) + (func $check_word (param i32) (param i32) + block + local.get 0 + i32.load + local.get 1 + i32.eq + br_if 0 + unreachable + end + ) + (func $check + global.get 0 + i32.const 1234 + call $check_word + global.get 1 + i32.const 5678 + call $check_word + ) + (start $start) + (memory 160) + (global (mut i32) (i32.const 8500000)) + (global (mut i32) (i32.const 9000000)) + (@custom "icp:private enhanced-orthogonal-persistence" "") + ) + "#; + let version1_wasm = wat::parse_str(version1_wat).unwrap(); + let canister_id = test.create_canister(Cycles::new(1_000_000_000_000_000)); + test.install_canister(canister_id, version1_wasm).unwrap(); + + let version2_wat = r#" + (module + (func $check_word (param i32) (param i32) + block + local.get 0 + i32.load + local.get 1 + i32.eq + br_if 0 + unreachable + end + ) + (func $check + global.get 0 + i32.const 1234 + call $check_word + global.get 1 + i32.const 5678 + call $check_word + ) + (start $check) + (memory 160) + (global (mut i32) (i32.const 8500000)) + (global (mut i32) (i32.const 9000000)) + (@custom "icp:private enhanced-orthogonal-persistence" "") + ) + "#; + + let version2_wasm = wat::parse_str(version2_wat).unwrap(); + test.upgrade_canister_v2( + canister_id, + version2_wasm, + CanisterUpgradeOptions { + skip_pre_upgrade: None, + wasm_memory_persistence: Some(WasmMemoryPersistence::Keep), + }, + ) + .unwrap(); +} + +#[test] +fn fails_with_missing_main_memory_option_for_enhanced_orthogonal_persistence() { + let mut test = ExecutionTestBuilder::new().build(); + + let version1_wat = r#" + (module + (memory 1) + (@custom "icp:private enhanced-orthogonal-persistence" "") + ) + "#; + let version1_wasm = wat::parse_str(version1_wat).unwrap(); + let canister_id = test.create_canister(Cycles::new(1_000_000_000_000_000)); + test.install_canister(canister_id, version1_wasm).unwrap(); + + let version2_wat = r#" + (module + (memory 1) + ) + "#; + + let version2_wasm = wat::parse_str(version2_wat).unwrap(); + let error = test + .upgrade_canister_v2( + canister_id, + version2_wasm, + CanisterUpgradeOptions { + skip_pre_upgrade: None, + wasm_memory_persistence: None, + }, + ) + .unwrap_err(); + assert_eq!(error.code(), ErrorCode::CanisterContractViolation); + assert_eq!(error.description(), "Missing upgrade option: Enhanced orthogonal persistence requires the `wasm_memory_persistence` upgrade option."); +} + +#[test] +fn fails_with_missing_upgrade_option_for_enhanced_orthogonal_persistence() { + let mut test = ExecutionTestBuilder::new().build(); + + let version1_wat = r#" + (module + (memory 1) + (@custom "icp:private enhanced-orthogonal-persistence" "") + ) + "#; + let version1_wasm = wat::parse_str(version1_wat).unwrap(); + let canister_id = test.create_canister(Cycles::new(1_000_000_000_000_000)); + test.install_canister(canister_id, version1_wasm).unwrap(); + + let version2_wat = r#" + (module + (memory 1) + ) + "#; + + let version2_wasm = wat::parse_str(version2_wat).unwrap(); + let error = test + .upgrade_canister(canister_id, version2_wasm) + .unwrap_err(); + assert_eq!(error.code(), ErrorCode::CanisterContractViolation); + assert_eq!(error.description(), "Missing upgrade option: Enhanced orthogonal persistence requires the `wasm_memory_persistence` upgrade option."); +} + +#[test] +fn fails_when_keeping_main_memory_without_enhanced_orthogonal_persistence() { + let mut test = ExecutionTestBuilder::new().build(); + + let classical_persistence = r#" + (module + (memory 1) + ) + "#; + let orthogonal_persistence = r#" + (module + (memory 1) + (@custom "icp:private enhanced-orthogonal-persistence" "") + ) + "#; + + for (version1_wat, version2_wat) in [ + (classical_persistence, classical_persistence), + (orthogonal_persistence, classical_persistence), + ] { + let version1_wasm = wat::parse_str(version1_wat).unwrap(); + let canister_id = test.create_canister(Cycles::new(1_000_000_000_000_000)); + test.install_canister(canister_id, version1_wasm).unwrap(); + + let version2_wasm = wat::parse_str(version2_wat).unwrap(); + let error = test + .upgrade_canister_v2( + canister_id, + version2_wasm, + CanisterUpgradeOptions { + skip_pre_upgrade: None, + wasm_memory_persistence: Some(WasmMemoryPersistence::Keep), + }, + ) + .unwrap_err(); + assert_eq!(error.code(), ErrorCode::CanisterContractViolation); + assert_eq!(error.description(), "Invalid upgrade option: The `wasm_memory_persistence: opt Keep` upgrade option requires that the new canister module supports enhanced orthogonal persistence."); + } +} + +#[test] +fn test_upgrade_to_enhanced_orthogonal_persistence() { + let mut test = ExecutionTestBuilder::new().build(); + + let version1_wat = r#" + (module + (memory 1) + ) + "#; + let version1_wasm = wat::parse_str(version1_wat).unwrap(); + let canister_id = test.create_canister(Cycles::new(1_000_000_000_000_000)); + test.install_canister(canister_id, version1_wasm).unwrap(); + + let version2_wat = r#" + (module + (memory 1) + (@custom "icp:private enhanced-orthogonal-persistence" "") + ) + "#; + let version2_wasm = wat::parse_str(version2_wat).unwrap(); + test.upgrade_canister_v2( + canister_id, + version2_wasm, + CanisterUpgradeOptions { + skip_pre_upgrade: None, + wasm_memory_persistence: Some(WasmMemoryPersistence::Keep), + }, + ) + .unwrap(); +} + fn create_canisters(test: &mut ExecutionTest, canisters: usize) { for _ in 1..=canisters { test.canister_from_binary(MINIMAL_WASM.to_vec()).unwrap(); @@ -6241,6 +6463,7 @@ fn test_upgrade_with_skip_pre_upgrade_preserves_stable_memory() { UNIVERSAL_CANISTER_WASM.to_vec(), CanisterUpgradeOptions { skip_pre_upgrade: Some(true), + wasm_memory_persistence: None, }, ) .unwrap(); @@ -6260,6 +6483,7 @@ fn test_upgrade_with_skip_pre_upgrade_preserves_stable_memory() { UNIVERSAL_CANISTER_WASM.to_vec(), CanisterUpgradeOptions { skip_pre_upgrade: None, + wasm_memory_persistence: None, }, ) .unwrap_err(); diff --git a/rs/execution_environment/src/execution/install.rs b/rs/execution_environment/src/execution/install.rs index 8c00993e172..dabfe7efdad 100644 --- a/rs/execution_environment/src/execution/install.rs +++ b/rs/execution_environment/src/execution/install.rs @@ -8,8 +8,8 @@ use crate::canister_manager::{ }; use crate::execution::common::{ingress_status_with_processing_state, update_round_limits}; use crate::execution::install_code::{ - canister_layout, finish_err, InstallCodeHelper, OriginalContext, PausedInstallCodeHelper, - StableMemoryHandling, + canister_layout, finish_err, CanisterMemoryHandling, InstallCodeHelper, MemoryHandling, + OriginalContext, PausedInstallCodeHelper, }; use crate::execution_environment::{RoundContext, RoundLimits}; use ic_base_types::PrincipalId; @@ -115,7 +115,10 @@ pub(crate) fn execute_install( if let Err(err) = helper.replace_execution_state_and_allocations( instructions_from_compilation, result, - StableMemoryHandling::Replace, + CanisterMemoryHandling { + stable_memory_handling: MemoryHandling::Replace, + main_memory_handling: MemoryHandling::Replace, + }, &original, ) { let instructions_left = helper.instructions_left(); diff --git a/rs/execution_environment/src/execution/install_code.rs b/rs/execution_environment/src/execution/install_code.rs index a36c6f6badf..6e295fb5013 100644 --- a/rs/execution_environment/src/execution/install_code.rs +++ b/rs/execution_environment/src/execution/install_code.rs @@ -37,14 +37,30 @@ use crate::{ #[cfg(test)] mod tests; -/// Indicates whether to keep the old stable memory or replace it with the new -/// (empty) stable memory. +/// Indicates whether the memory is kept or replaced with new (initial) memory. +/// Applicable to both the stable memory and the main memory of a canister. #[derive(Clone, Copy, Debug, PartialEq)] -pub(crate) enum StableMemoryHandling { +pub(crate) enum MemoryHandling { + /// Retain the memory. Keep, + /// Reset the memory. Replace, } +/// Specifies the handling of the canister's memories. +/// * On install and re-install: +/// - Replace both the stable memory and the main memory. +/// * On upgrade: +/// - For canisters with enhanced orthogonal persistence (Motoko): +/// Retain both the main memory and the stable memory. +/// - For all other canisters: +/// Retain only the stable memory and erase the main memory. +#[derive(Clone, Copy, Debug, PartialEq)] +pub(crate) struct CanisterMemoryHandling { + pub stable_memory_handling: MemoryHandling, + pub main_memory_handling: MemoryHandling, +} + /// The main steps of `install_code` execution that may fail with an error or /// change the canister state. #[derive(Clone, Debug)] @@ -54,7 +70,7 @@ pub(crate) enum InstallCodeStep { ReplaceExecutionStateAndAllocations { instructions_from_compilation: NumInstructions, maybe_execution_state: HypervisorResult, - stable_memory_handling: StableMemoryHandling, + memory_handling: CanisterMemoryHandling, }, ClearCertifiedData, DeactivateGlobalTimer, @@ -500,8 +516,8 @@ impl InstallCodeHelper { } /// Replaces the execution state of the current canister with the freshly - /// created execution state. The stable memory is conditionally replaced - /// based on the given `stable_memory_handling`. + /// created execution state. The stable memory and the main memory are + /// conditionally replaced based on the given `memory_handling`. /// /// It also updates the compute and memory allocations with the requested /// values in `original` context. @@ -509,14 +525,14 @@ impl InstallCodeHelper { &mut self, instructions_from_compilation: NumInstructions, maybe_execution_state: HypervisorResult, - stable_memory_handling: StableMemoryHandling, + memory_handling: CanisterMemoryHandling, original: &OriginalContext, ) -> Result<(), CanisterManagerError> { self.steps .push(InstallCodeStep::ReplaceExecutionStateAndAllocations { instructions_from_compilation, maybe_execution_state: maybe_execution_state.clone(), - stable_memory_handling, + memory_handling, }); self.reduce_instructions_by(instructions_from_compilation); @@ -536,13 +552,17 @@ impl InstallCodeHelper { let new_wasm_custom_sections_memory_used = execution_state.metadata.memory_usage(); - execution_state.stable_memory = - match (stable_memory_handling, self.canister.execution_state.take()) { - (StableMemoryHandling::Keep, Some(old)) => old.stable_memory, - (StableMemoryHandling::Keep, None) | (StableMemoryHandling::Replace, _) => { - execution_state.stable_memory - } - }; + if let Some(old) = self.canister.execution_state.take() { + match memory_handling.stable_memory_handling { + MemoryHandling::Keep => execution_state.stable_memory = old.stable_memory, + MemoryHandling::Replace => {} + } + match memory_handling.main_memory_handling { + MemoryHandling::Keep => execution_state.wasm_memory = old.wasm_memory, + MemoryHandling::Replace => {} + } + }; + self.canister.execution_state = Some(execution_state); // Update the compute allocation. @@ -752,11 +772,11 @@ impl InstallCodeHelper { InstallCodeStep::ReplaceExecutionStateAndAllocations { instructions_from_compilation, maybe_execution_state, - stable_memory_handling, + memory_handling, } => self.replace_execution_state_and_allocations( instructions_from_compilation, maybe_execution_state, - stable_memory_handling, + memory_handling, original, ), InstallCodeStep::ClearCertifiedData => { diff --git a/rs/execution_environment/src/execution/upgrade.rs b/rs/execution_environment/src/execution/upgrade.rs index 3435baa43cb..075dfdebacc 100644 --- a/rs/execution_environment/src/execution/upgrade.rs +++ b/rs/execution_environment/src/execution/upgrade.rs @@ -5,21 +5,25 @@ use crate::as_round_instructions; use crate::canister_manager::{ - DtsInstallCodeResult, InstallCodeContext, PausedInstallCodeExecution, + CanisterManagerError, DtsInstallCodeResult, InstallCodeContext, PausedInstallCodeExecution, }; use crate::execution::common::{ingress_status_with_processing_state, update_round_limits}; use crate::execution::install_code::{ - canister_layout, finish_err, InstallCodeHelper, OriginalContext, PausedInstallCodeHelper, - StableMemoryHandling, + canister_layout, finish_err, CanisterMemoryHandling, InstallCodeHelper, OriginalContext, + PausedInstallCodeHelper, }; use crate::execution_environment::{RoundContext, RoundLimits}; use ic_base_types::PrincipalId; use ic_embedders::wasm_executor::{CanisterStateChanges, PausedWasmExecution, WasmExecutionResult}; -use ic_interfaces::execution_environment::{HypervisorError, WasmExecutionOutput}; +use ic_interfaces::execution_environment::{ + HypervisorError, HypervisorResult, WasmExecutionOutput, +}; use ic_logger::{info, warn, ReplicaLogger}; -use ic_management_canister_types::CanisterInstallModeV2; +use ic_management_canister_types::{ + CanisterInstallModeV2, CanisterUpgradeOptions, WasmMemoryPersistence, +}; use ic_replicated_state::{ - metadata_state::subnet_call_context_manager::InstallCodeCallId, CanisterState, + metadata_state::subnet_call_context_manager::InstallCodeCallId, CanisterState, ExecutionState, }; use ic_system_api::ApiType; use ic_types::methods::{FuncRef, SystemMethod, WasmMethod}; @@ -28,9 +32,13 @@ use ic_types::{ messages::{CanisterCall, RequestMetadata}, }; +use super::install_code::MemoryHandling; + #[cfg(test)] mod tests; +pub const ENHANCED_ORTHOGONAL_PERSISTENCE_SECTION: &str = "enhanced-orthogonal-persistence"; + /// Performs a canister upgrade. The algorithm consists of six stages: /// - Stage 0: validate input. /// - Stage 1: invoke `canister_pre_upgrade()` (if present) using the old code. @@ -275,10 +283,27 @@ fn upgrade_stage_2_and_3a_create_execution_state_and_call_start( original.compilation_cost_handling, ); + let main_memory_handling = match determine_main_memory_handling( + context.mode, + &helper.canister().execution_state, + &result, + ) { + Ok(memory_handling) => memory_handling, + Err(err) => { + let instructions_left = helper.instructions_left(); + return finish_err(clean_canister, instructions_left, original, round, err); + } + }; + + let memory_handling = CanisterMemoryHandling { + stable_memory_handling: MemoryHandling::Keep, + main_memory_handling, + }; + if let Err(err) = helper.replace_execution_state_and_allocations( instructions_from_compilation, result, - StableMemoryHandling::Keep, + memory_handling, &original, ) { let instructions_left = helper.instructions_left(); @@ -785,3 +810,66 @@ impl PausedInstallCodeExecution for PausedPostUpgradeExecution { (self.original.message, self.original.call_id, Cycles::zero()) } } + +/// Determines main memory handling based on the `wasm_memory_persistence` upgrade options. +/// Integrates two safety checks: +/// - The `wasm_memory_persistence` upgrade option is not omitted in error, when +/// the old canister implementation uses enhanced orthogonal persistence. +/// - The `wasm_memory_persistence: opt keep` option is not applied to a new canister +/// implementation that does not support enhanced orthogonal persistence. +fn determine_main_memory_handling( + install_mode: CanisterInstallModeV2, + old_state: &Option, + new_state_candidate: &HypervisorResult, +) -> Result { + let old_state_uses_orthogonal_persistence = || { + old_state + .as_ref() + .map_or(false, expects_enhanced_orthogonal_persistence) + }; + let new_state_uses_orthogonal_persistence = || { + new_state_candidate + .as_ref() + .map_or(false, expects_enhanced_orthogonal_persistence) + }; + + match install_mode { + CanisterInstallModeV2::Upgrade(None) + | CanisterInstallModeV2::Upgrade(Some(CanisterUpgradeOptions { + wasm_memory_persistence: None, + .. + })) => { + // Safety guard checking that the `wasm_memory_persistence` upgrade option has not been omitted in error. + if old_state_uses_orthogonal_persistence() { + let message = "Enhanced orthogonal persistence requires the `wasm_memory_persistence` upgrade option.".to_string(); + return Err(CanisterManagerError::MissingUpgradeOptionError { message }); + } + Ok(MemoryHandling::Replace) + } + CanisterInstallModeV2::Upgrade(Some(CanisterUpgradeOptions { + wasm_memory_persistence: Some(WasmMemoryPersistence::Keep), + .. + })) => { + // Safety guard checking that the enhanced orthogonal persistence upgrade option is only applied to canisters that support such. + if !new_state_uses_orthogonal_persistence() { + let message = "The `wasm_memory_persistence: opt Keep` upgrade option requires that the new canister module supports enhanced orthogonal persistence.".to_string(); + return Err(CanisterManagerError::InvalidUpgradeOptionError { message }); + } + Ok(MemoryHandling::Keep) + } + CanisterInstallModeV2::Upgrade(Some(CanisterUpgradeOptions { + wasm_memory_persistence: Some(WasmMemoryPersistence::Replace), + .. + })) => Ok(MemoryHandling::Replace), + // These two modes cannot occur during an upgrade. + CanisterInstallModeV2::Install | CanisterInstallModeV2::Reinstall => unreachable!(), + } +} + +/// Helper function to check whether the state expects enhanced orthogonal persistence. +fn expects_enhanced_orthogonal_persistence(execution_state: &ExecutionState) -> bool { + execution_state + .metadata + .get_custom_section(ENHANCED_ORTHOGONAL_PERSISTENCE_SECTION) + .is_some() +} diff --git a/rs/execution_environment/src/execution/upgrade/tests.rs b/rs/execution_environment/src/execution/upgrade/tests.rs index 5bb0c1b9756..878d7a50401 100644 --- a/rs/execution_environment/src/execution/upgrade/tests.rs +++ b/rs/execution_environment/src/execution/upgrade/tests.rs @@ -360,7 +360,10 @@ fn test_pre_upgrade_execution_with_canister_install_mode_v2() { let result = test.upgrade_canister_v2( canister_id, new_empty_binary(), - CanisterUpgradeOptions { skip_pre_upgrade }, + CanisterUpgradeOptions { + skip_pre_upgrade, + wasm_memory_persistence: None, + }, ); if skip_pre_upgrade == Some(true) { @@ -392,7 +395,10 @@ fn test_upgrade_execution_with_canister_install_mode_v2() { let result = test.upgrade_canister_v2( canister_id, binary(&[(Function::PostUpgrade, Execution::ShortTrap)]), - CanisterUpgradeOptions { skip_pre_upgrade }, + CanisterUpgradeOptions { + skip_pre_upgrade, + wasm_memory_persistence: None, + }, ); assert_eq!(result.unwrap_err().code(), ErrorCode::CanisterTrapped); @@ -989,6 +995,7 @@ fn upgrade_with_skip_pre_upgrade_fails_on_no_execution_state() { new_empty_binary(), CanisterUpgradeOptions { skip_pre_upgrade: Some(true), + wasm_memory_persistence: None, }, ); assert_eq!( @@ -1009,6 +1016,7 @@ fn upgrade_with_skip_pre_upgrade_ok_with_no_pre_upgrade() { new_empty_binary(), CanisterUpgradeOptions { skip_pre_upgrade: Some(true), + wasm_memory_persistence: None, }, ); assert_eq!(result, Ok(())); diff --git a/rs/execution_environment/src/hypervisor/tests.rs b/rs/execution_environment/src/hypervisor/tests.rs index 5003160749d..cccb21edce5 100644 --- a/rs/execution_environment/src/hypervisor/tests.rs +++ b/rs/execution_environment/src/hypervisor/tests.rs @@ -6312,6 +6312,7 @@ fn upgrade_with_skip_pre_upgrade_preserves_stable_memory() { wat::parse_str(wat.clone()).unwrap(), CanisterUpgradeOptions { skip_pre_upgrade: Some(true), + wasm_memory_persistence: None, }, ) .unwrap(); @@ -6323,6 +6324,7 @@ fn upgrade_with_skip_pre_upgrade_preserves_stable_memory() { wat::parse_str(wat).unwrap(), CanisterUpgradeOptions { skip_pre_upgrade: Some(false), + wasm_memory_persistence: None, }, ) .unwrap_err(); diff --git a/rs/protobuf/def/types/v1/management_canister_types.proto b/rs/protobuf/def/types/v1/management_canister_types.proto index c28dc25b143..1c8a4e0accd 100644 --- a/rs/protobuf/def/types/v1/management_canister_types.proto +++ b/rs/protobuf/def/types/v1/management_canister_types.proto @@ -9,8 +9,15 @@ enum CanisterInstallMode { CANISTER_INSTALL_MODE_UPGRADE = 3; } +enum WasmMemoryPersistence { + WASM_MEMORY_PERSISTENCE_UNSPECIFIED = 0; + WASM_MEMORY_PERSISTENCE_KEEP = 1; + WASM_MEMORY_PERSISTENCE_REPLACE = 2; +} + message CanisterUpgradeOptions { optional bool skip_pre_upgrade = 1; + optional WasmMemoryPersistence wasm_memory_persistence = 2; } message CanisterInstallModeV2 { diff --git a/rs/protobuf/src/gen/state/types.v1.rs b/rs/protobuf/src/gen/state/types.v1.rs index 430f3df5a05..20bdfbb2df4 100644 --- a/rs/protobuf/src/gen/state/types.v1.rs +++ b/rs/protobuf/src/gen/state/types.v1.rs @@ -122,6 +122,8 @@ impl RejectCode { pub struct CanisterUpgradeOptions { #[prost(bool, optional, tag = "1")] pub skip_pre_upgrade: ::core::option::Option, + #[prost(enumeration = "WasmMemoryPersistence", optional, tag = "2")] + pub wasm_memory_persistence: ::core::option::Option, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -176,3 +178,32 @@ impl CanisterInstallMode { } } } +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum WasmMemoryPersistence { + Unspecified = 0, + Keep = 1, + Replace = 2, +} +impl WasmMemoryPersistence { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + WasmMemoryPersistence::Unspecified => "WASM_MEMORY_PERSISTENCE_UNSPECIFIED", + WasmMemoryPersistence::Keep => "WASM_MEMORY_PERSISTENCE_KEEP", + WasmMemoryPersistence::Replace => "WASM_MEMORY_PERSISTENCE_REPLACE", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "WASM_MEMORY_PERSISTENCE_UNSPECIFIED" => Some(Self::Unspecified), + "WASM_MEMORY_PERSISTENCE_KEEP" => Some(Self::Keep), + "WASM_MEMORY_PERSISTENCE_REPLACE" => Some(Self::Replace), + _ => None, + } + } +} diff --git a/rs/protobuf/src/gen/types/types.v1.rs b/rs/protobuf/src/gen/types/types.v1.rs index cd3b0b61cc5..d2f1a88a456 100644 --- a/rs/protobuf/src/gen/types/types.v1.rs +++ b/rs/protobuf/src/gen/types/types.v1.rs @@ -4,6 +4,8 @@ pub struct CanisterUpgradeOptions { #[prost(bool, optional, tag = "1")] pub skip_pre_upgrade: ::core::option::Option, + #[prost(enumeration = "WasmMemoryPersistence", optional, tag = "2")] + pub wasm_memory_persistence: ::core::option::Option, } #[derive(serde::Serialize, serde::Deserialize)] #[allow(clippy::derive_partial_eq_without_eq)] @@ -72,6 +74,47 @@ impl CanisterInstallMode { } } } +#[derive( + serde::Serialize, + serde::Deserialize, + Clone, + Copy, + Debug, + PartialEq, + Eq, + Hash, + PartialOrd, + Ord, + ::prost::Enumeration, +)] +#[repr(i32)] +pub enum WasmMemoryPersistence { + Unspecified = 0, + Keep = 1, + Replace = 2, +} +impl WasmMemoryPersistence { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + WasmMemoryPersistence::Unspecified => "WASM_MEMORY_PERSISTENCE_UNSPECIFIED", + WasmMemoryPersistence::Keep => "WASM_MEMORY_PERSISTENCE_KEEP", + WasmMemoryPersistence::Replace => "WASM_MEMORY_PERSISTENCE_REPLACE", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "WASM_MEMORY_PERSISTENCE_UNSPECIFIED" => Some(Self::Unspecified), + "WASM_MEMORY_PERSISTENCE_KEEP" => Some(Self::Keep), + "WASM_MEMORY_PERSISTENCE_REPLACE" => Some(Self::Replace), + _ => None, + } + } +} #[derive(serde::Serialize, serde::Deserialize, Eq, Hash)] #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] diff --git a/rs/replica_tests/tests/canister_lifecycle.rs b/rs/replica_tests/tests/canister_lifecycle.rs index 940a88279d1..c52f5730648 100644 --- a/rs/replica_tests/tests/canister_lifecycle.rs +++ b/rs/replica_tests/tests/canister_lifecycle.rs @@ -988,6 +988,7 @@ fn test_canister_skip_upgrade() { management::install_code(canister_id, UNIVERSAL_CANISTER_WASM).with_mode( management::InstallMode::Upgrade(Some(CanisterUpgradeOptions { skip_pre_upgrade: Some(false), + wasm_memory_persistence: None, })), ), )), @@ -1000,6 +1001,7 @@ fn test_canister_skip_upgrade() { management::install_code(canister_id, UNIVERSAL_CANISTER_WASM).with_mode( management::InstallMode::Upgrade(Some(CanisterUpgradeOptions { skip_pre_upgrade: Some(true), + wasm_memory_persistence: None, })) ), )), @@ -1013,6 +1015,7 @@ fn test_canister_skip_upgrade() { management::install_code(canister_id, UNIVERSAL_CANISTER_WASM).with_mode( management::InstallMode::Upgrade(Some(CanisterUpgradeOptions { skip_pre_upgrade: Some(false), + wasm_memory_persistence: None, })), ), )), diff --git a/rs/types/management_canister_types/src/lib.rs b/rs/types/management_canister_types/src/lib.rs index fa416046beb..860df8addf6 100644 --- a/rs/types/management_canister_types/src/lib.rs +++ b/rs/types/management_canister_types/src/lib.rs @@ -23,6 +23,7 @@ use ic_protobuf::types::v1::CanisterInstallModeV2 as CanisterInstallModeV2Proto; use ic_protobuf::types::v1::{ CanisterInstallMode as CanisterInstallModeProto, CanisterUpgradeOptions as CanisterUpgradeOptionsProto, + WasmMemoryPersistence as WasmMemoryPersistenceProto, }; use ic_protobuf::{proxy::ProxyDecodeError, registry::crypto::v1 as pb_registry_crypto}; use num_traits::cast::ToPrimitive; @@ -1052,16 +1053,53 @@ pub enum CanisterInstallMode { Upgrade = 3, } +impl CanisterInstallMode { + pub fn iter() -> Iter<'static, CanisterInstallMode> { + static MODES: [CanisterInstallMode; 3] = [ + CanisterInstallMode::Install, + CanisterInstallMode::Reinstall, + CanisterInstallMode::Upgrade, + ]; + MODES.iter() + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, Eq, Hash, CandidType, Copy)] +/// Wasm main memory retention on upgrades. +/// Currently used to specify the persistence of Wasm main memory. +pub enum WasmMemoryPersistence { + /// Retain the main memory across upgrades. + /// Used for enhanced orthogonal persistence, as implemented in Motoko + Keep, + /// Reinitialize the main memory on upgrade. + /// Default behavior without enhanced orthogonal persistence. + Replace, +} + +impl WasmMemoryPersistence { + pub fn iter() -> Iter<'static, WasmMemoryPersistence> { + static MODES: [WasmMemoryPersistence; 2] = + [WasmMemoryPersistence::Keep, WasmMemoryPersistence::Replace]; + MODES.iter() + } +} + #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, Eq, Hash, CandidType, Copy, Default)] /// Struct used for encoding/decoding: /// `record { -/// skip_pre_upgrade: opt bool +/// skip_pre_upgrade: opt bool; +/// wasm_memory_persistence : opt variant { +/// keep; +/// replace; +/// }; /// }` /// Extendibility for the future: Adding new optional fields ensures both backwards- and /// forwards-compatibility in Candid. pub struct CanisterUpgradeOptions { /// Determine whether the pre-upgrade hook should be skipped during upgrade. pub skip_pre_upgrade: Option, + /// Support for enhanced orthogonal persistence: Retain the main memory on upgrade. + pub wasm_memory_persistence: Option, } /// The mode with which a canister is installed. @@ -1088,18 +1126,45 @@ pub enum CanisterInstallModeV2 { impl CanisterInstallModeV2 { pub fn iter() -> Iter<'static, CanisterInstallModeV2> { - static MODES: [CanisterInstallModeV2; 6] = [ + static MODES: [CanisterInstallModeV2; 12] = [ CanisterInstallModeV2::Install, CanisterInstallModeV2::Reinstall, CanisterInstallModeV2::Upgrade(None), CanisterInstallModeV2::Upgrade(Some(CanisterUpgradeOptions { skip_pre_upgrade: None, + wasm_memory_persistence: None, + })), + CanisterInstallModeV2::Upgrade(Some(CanisterUpgradeOptions { + skip_pre_upgrade: None, + wasm_memory_persistence: Some(WasmMemoryPersistence::Keep), + })), + CanisterInstallModeV2::Upgrade(Some(CanisterUpgradeOptions { + skip_pre_upgrade: None, + wasm_memory_persistence: Some(WasmMemoryPersistence::Replace), + })), + CanisterInstallModeV2::Upgrade(Some(CanisterUpgradeOptions { + skip_pre_upgrade: Some(false), + wasm_memory_persistence: None, + })), + CanisterInstallModeV2::Upgrade(Some(CanisterUpgradeOptions { + skip_pre_upgrade: Some(false), + wasm_memory_persistence: Some(WasmMemoryPersistence::Keep), })), CanisterInstallModeV2::Upgrade(Some(CanisterUpgradeOptions { skip_pre_upgrade: Some(false), + wasm_memory_persistence: Some(WasmMemoryPersistence::Replace), + })), + CanisterInstallModeV2::Upgrade(Some(CanisterUpgradeOptions { + skip_pre_upgrade: Some(true), + wasm_memory_persistence: None, + })), + CanisterInstallModeV2::Upgrade(Some(CanisterUpgradeOptions { + skip_pre_upgrade: Some(true), + wasm_memory_persistence: Some(WasmMemoryPersistence::Keep), })), CanisterInstallModeV2::Upgrade(Some(CanisterUpgradeOptions { skip_pre_upgrade: Some(true), + wasm_memory_persistence: Some(WasmMemoryPersistence::Replace), })), ]; MODES.iter() @@ -1179,11 +1244,22 @@ impl TryFrom for CanisterInstallModeV2 { ) => Ok(CanisterInstallModeV2::Upgrade(Some( CanisterUpgradeOptions { skip_pre_upgrade: upgrade_mode.skip_pre_upgrade, + wasm_memory_persistence: match upgrade_mode.wasm_memory_persistence { + None => None, + Some(mode) => Some(match WasmMemoryPersistenceProto::try_from(mode).ok() { + Some(persistence) => WasmMemoryPersistence::try_from(persistence), + None => Err(CanisterInstallModeError( + format!("Invalid `WasmMemoryPersistence` value: {mode}") + .to_string(), + )), + }?), + }, }, ))), } } } + impl From for String { fn from(mode: CanisterInstallMode) -> Self { let result = match mode { @@ -1228,6 +1304,12 @@ impl From<&CanisterInstallModeV2> for CanisterInstallModeV2Proto { ic_protobuf::types::v1::canister_install_mode_v2::CanisterInstallModeV2::Mode2( CanisterUpgradeOptionsProto { skip_pre_upgrade: upgrade_options.skip_pre_upgrade, + wasm_memory_persistence: upgrade_options.wasm_memory_persistence.map( + |mode| { + let proto: WasmMemoryPersistenceProto = mode.into(); + proto.into() + }, + ), }, ) } @@ -1248,6 +1330,62 @@ impl From for CanisterInstallMode { } } +impl TryFrom for WasmMemoryPersistence { + type Error = CanisterInstallModeError; + + fn try_from(item: WasmMemoryPersistenceProto) -> Result { + match item { + WasmMemoryPersistenceProto::Keep => Ok(WasmMemoryPersistence::Keep), + WasmMemoryPersistenceProto::Replace => Ok(WasmMemoryPersistence::Replace), + WasmMemoryPersistenceProto::Unspecified => Err(CanisterInstallModeError( + format!("Invalid `WasmMemoryPersistence` value: {item:?}").to_string(), + )), + } + } +} + +impl From for WasmMemoryPersistenceProto { + fn from(item: WasmMemoryPersistence) -> Self { + match item { + WasmMemoryPersistence::Keep => WasmMemoryPersistenceProto::Keep, + WasmMemoryPersistence::Replace => WasmMemoryPersistenceProto::Replace, + } + } +} + +#[test] +fn wasm_persistence_round_trip() { + for persistence in WasmMemoryPersistence::iter() { + let encoded: WasmMemoryPersistenceProto = (*persistence).into(); + let decoded = WasmMemoryPersistence::try_from(encoded).unwrap(); + assert_eq!(*persistence, decoded); + } + + WasmMemoryPersistence::try_from(WasmMemoryPersistenceProto::Unspecified).unwrap_err(); +} + +#[test] +fn canister_install_mode_round_trip() { + fn canister_install_mode_round_trip_aux(mode: CanisterInstallMode) { + let pb_mode: i32 = (&mode).into(); + let dec_mode = CanisterInstallMode::try_from(pb_mode).unwrap(); + assert_eq!(mode, dec_mode); + } + + canister_install_mode_round_trip_aux(CanisterInstallMode::Install); + canister_install_mode_round_trip_aux(CanisterInstallMode::Reinstall); + canister_install_mode_round_trip_aux(CanisterInstallMode::Upgrade); +} + +#[test] +fn canister_install_mode_v2_round_trip() { + for mode in CanisterInstallModeV2::iter() { + let encoded: CanisterInstallModeV2Proto = mode.into(); + let decoded = CanisterInstallModeV2::try_from(encoded).unwrap(); + assert_eq!(*mode, decoded); + } +} + impl Payload<'_> for CanisterStatusResultV2 {} /// Struct used for encoding/decoding diff --git a/rs/universal_canister/lib/src/management.rs b/rs/universal_canister/lib/src/management.rs index 5585e0c9c45..bf7db267445 100644 --- a/rs/universal_canister/lib/src/management.rs +++ b/rs/universal_canister/lib/src/management.rs @@ -72,6 +72,7 @@ pub fn create_canister(cycles: (u64, u64)) -> CandidCallBuilder From> for Call { } } +#[derive(CandidType, Deserialize)] +pub enum WasmMemoryPersistence { + Keep, + Replace, +} + #[derive(CandidType, Deserialize)] pub struct CanisterUpgradeOptions { pub skip_pre_upgrade: Option, + pub wasm_memory_persistence: Option, } #[derive(CandidType, Deserialize)]