Repository: dfinity/ic
Pull request #4734: chore: [MR-675] Deserialize wasm with hash always present
Author: login: ShuoWangNSL
Labels: name: chore
name: @execution
name: @ic-message-routing-owners
name: @ic-interface-owners
name: CI_ALL_BAZEL_TARGETS
description: Runs all bazel targets
name: CI_OVERRIDE_BUF_BREAKING
description: Skip buf-breaking (protobuf) check (explain in PR description why)
Created: 2025-04-08T22:53:21Z | Merged: 2025-04-15T11:24:03Z

[MR-675](https://dfinity.atlassian.net/browse/MR-675)

`binary_hash` was [introduced](https://github.com/dfinity/ic/commit/61958e6dfdc9959ea0cb28b6de9dbc91a4434f71) as an optional field in `ExecutionStateBits` for backwards compatibility reasons. That was because when loading checkpoints written by even older replica versions, `binary_hash` would not exist.

In production, we have populated  `binary_hash` with `Some` value for long time and serialize it to disk. It is OK to remove the optional tag and deserialization won't have issues.

In addition to simplifying code, my primary goal of this PR is to get rid of the usage of `canister_module.as_slice()` when deserializing Wasm files so that we know for sure that it will be always lazy loading after we implement [MR-651](https://dfinity.atlassian.net/browse/MR-651) in the future.

The `binary_hash`  in `CanisterSnapshotBits` follows the same pattern.


[MR-675]: https://dfinity.atlassian.net/browse/MR-675?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
[MR-651]: https://dfinity.atlassian.net/browse/MR-651?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ


Diff:
diff --git a/rs/protobuf/def/state/canister_snapshot_bits/v1/canister_snapshot_bits.proto b/rs/protobuf/def/state/canister_snapshot_bits/v1/canister_snapshot_bits.proto
index d8b8195cf991..d8a3114fe4ad 100644
--- a/rs/protobuf/def/state/canister_snapshot_bits/v1/canister_snapshot_bits.proto
+++ b/rs/protobuf/def/state/canister_snapshot_bits/v1/canister_snapshot_bits.proto
@@ -22,7 +22,7 @@ message CanisterSnapshotBits {
   uint64 taken_at_timestamp = 3;
   uint64 canister_version = 4;
   bytes certified_data = 5;
-  optional bytes binary_hash = 6;
+  bytes binary_hash = 6;
   canister_state_bits.v1.WasmChunkStoreMetadata wasm_chunk_store_metadata = 7;
   uint64 stable_memory_size = 8;
   uint64 wasm_memory_size = 9;
diff --git a/rs/protobuf/def/state/canister_state_bits/v1/canister_state_bits.proto b/rs/protobuf/def/state/canister_state_bits/v1/canister_state_bits.proto
index 30b0ede6eefd..c93699f65359 100644
--- a/rs/protobuf/def/state/canister_state_bits/v1/canister_state_bits.proto
+++ b/rs/protobuf/def/state/canister_state_bits/v1/canister_state_bits.proto
@@ -142,7 +142,7 @@ message ExecutionStateBits {
   repeated WasmMethod exports = 3;
   uint64 last_executed_round = 4;
   WasmMetadata metadata = 5;
-  optional bytes binary_hash = 6;
+  bytes binary_hash = 6;
   optional NextScheduledMethod next_scheduled_method = 7;
   bool is_wasm64 = 8;
 }
diff --git a/rs/protobuf/src/gen/state/state.canister_snapshot_bits.v1.rs b/rs/protobuf/src/gen/state/state.canister_snapshot_bits.v1.rs
index 781343b77bf2..512ccd79ea83 100644
--- a/rs/protobuf/src/gen/state/state.canister_snapshot_bits.v1.rs
+++ b/rs/protobuf/src/gen/state/state.canister_snapshot_bits.v1.rs
@@ -18,8 +18,8 @@ pub struct CanisterSnapshotBits {
     pub canister_version: u64,
     #[prost(bytes = "vec", tag = "5")]
     pub certified_data: ::prost::alloc::vec::Vec<u8>,
-    #[prost(bytes = "vec", optional, tag = "6")]
-    pub binary_hash: ::core::option::Option<::prost::alloc::vec::Vec<u8>>,
+    #[prost(bytes = "vec", tag = "6")]
+    pub binary_hash: ::prost::alloc::vec::Vec<u8>,
     #[prost(message, optional, tag = "7")]
     pub wasm_chunk_store_metadata:
         ::core::option::Option<super::super::canister_state_bits::v1::WasmChunkStoreMetadata>,
diff --git a/rs/protobuf/src/gen/state/state.canister_state_bits.v1.rs b/rs/protobuf/src/gen/state/state.canister_state_bits.v1.rs
index b06cd2fc5202..1a3e2c7b1c4f 100644
--- a/rs/protobuf/src/gen/state/state.canister_state_bits.v1.rs
+++ b/rs/protobuf/src/gen/state/state.canister_state_bits.v1.rs
@@ -232,8 +232,8 @@ pub struct ExecutionStateBits {
     pub last_executed_round: u64,
     #[prost(message, optional, tag = "5")]
     pub metadata: ::core::option::Option<WasmMetadata>,
-    #[prost(bytes = "vec", optional, tag = "6")]
-    pub binary_hash: ::core::option::Option<::prost::alloc::vec::Vec<u8>>,
+    #[prost(bytes = "vec", tag = "6")]
+    pub binary_hash: ::prost::alloc::vec::Vec<u8>,
     #[prost(enumeration = "NextScheduledMethod", optional, tag = "7")]
     pub next_scheduled_method: ::core::option::Option<i32>,
     #[prost(bool, tag = "8")]
diff --git a/rs/replicated_state/src/canister_state/tests.rs b/rs/replicated_state/src/canister_state/tests.rs
index f635923a1354..4b5e82b392a8 100644
--- a/rs/replicated_state/src/canister_state/tests.rs
+++ b/rs/replicated_state/src/canister_state/tests.rs
@@ -795,16 +795,13 @@ fn wasm_can_be_loaded_from_a_file() {
     let wasm_in_memory = CanisterModule::new(vec![0x00, 0x61, 0x73, 0x6d]);
     tmp.write_all(wasm_in_memory.as_slice())
         .expect("failed to write Wasm to a temporary file");
-    let wasm_on_disk = CanisterModule::new_from_file(tmp.path().to_owned(), None)
-        .expect("failed to read Wasm from disk");
     let wasm_hash = wasm_in_memory.module_hash();
     let wasm_on_disk_with_hash =
-        CanisterModule::new_from_file(tmp.path().to_owned(), Some(wasm_hash.into()))
+        CanisterModule::new_from_file(tmp.path().to_owned(), wasm_hash.into())
             .expect("failed to read Wasm from disk");
 
     assert_eq!(wasm_in_memory.file(), None);
-    assert_eq!(wasm_on_disk.file(), Some(tmp.path()));
-    assert_eq!(wasm_in_memory, wasm_on_disk);
+    assert_eq!(wasm_on_disk_with_hash.file(), Some(tmp.path()));
     assert_eq!(wasm_in_memory, wasm_on_disk_with_hash);
 }
 
diff --git a/rs/state_layout/src/state_layout.rs b/rs/state_layout/src/state_layout.rs
index c9270f73c2df..467cacae84d8 100644
--- a/rs/state_layout/src/state_layout.rs
+++ b/rs/state_layout/src/state_layout.rs
@@ -135,7 +135,7 @@ pub struct ExecutionStateBits {
     pub exports: ExportedFunctions,
     pub last_executed_round: ExecutionRound,
     pub metadata: WasmMetadata,
-    pub binary_hash: Option<WasmHash>,
+    pub binary_hash: WasmHash,
     pub next_scheduled_method: NextScheduledMethod,
     pub is_wasm64: bool,
 }
@@ -197,7 +197,7 @@ pub struct CanisterSnapshotBits {
     /// The canister version at the time of taking the snapshot.
     pub canister_version: u64,
     /// The hash of the canister wasm.
-    pub binary_hash: Option<WasmHash>,
+    pub binary_hash: WasmHash,
     /// The certified data blob belonging to the canister.
     pub certified_data: Vec<u8>,
     /// The metadata required for a wasm chunk store.
@@ -2446,10 +2446,7 @@ impl<T> WasmFile<T>
 where
     T: ReadPolicy,
 {
-    pub fn deserialize(
-        &self,
-        module_hash: Option<WasmHash>,
-    ) -> Result<CanisterModule, LayoutError> {
+    pub fn deserialize(&self, module_hash: WasmHash) -> Result<CanisterModule, LayoutError> {
         CanisterModule::new_from_file(self.path.clone(), module_hash).map_err(|err| {
             LayoutError::IoError {
                 path: self.path.clone(),
@@ -2768,7 +2765,7 @@ impl From<&ExecutionStateBits> for pb_canister_state_bits::ExecutionStateBits {
             exports: (&item.exports).into(),
             last_executed_round: item.last_executed_round.get(),
             metadata: Some((&item.metadata).into()),
-            binary_hash: item.binary_hash.as_ref().map(|h| h.to_vec()),
+            binary_hash: item.binary_hash.to_vec(),
             next_scheduled_method: Some(
                 pb_canister_state_bits::NextScheduledMethod::from(item.next_scheduled_method)
                     .into(),
@@ -2786,18 +2783,14 @@ impl TryFrom<pb_canister_state_bits::ExecutionStateBits> for ExecutionStateBits
         for g in value.exported_globals.into_iter() {
             globals.push(g.try_into()?);
         }
-        let binary_hash = match value.binary_hash {
-            Some(hash) => {
-                let hash: [u8; 32] =
-                    hash.try_into()
-                        .map_err(|e| ProxyDecodeError::ValueOutOfRange {
-                            typ: "BinaryHash",
-                            err: format!("Expected a 32-byte long module hash, got {:?}", e),
-                        })?;
-                Some(hash.into())
-            }
-            None => None,
-        };
+        let binary_hash: [u8; 32] =
+            value
+                .binary_hash
+                .try_into()
+                .map_err(|e| ProxyDecodeError::ValueOutOfRange {
+                    typ: "BinaryHash",
+                    err: format!("Expected a 32-byte long module hash, got {:?}", e),
+                })?;
 
         Ok(Self {
             exported_globals: globals,
@@ -2806,7 +2799,7 @@ impl TryFrom<pb_canister_state_bits::ExecutionStateBits> for ExecutionStateBits
             last_executed_round: value.last_executed_round.into(),
             metadata: try_from_option_field(value.metadata, "ExecutionStateBits::metadata")
                 .unwrap_or_default(),
-            binary_hash,
+            binary_hash: WasmHash::from(binary_hash),
             next_scheduled_method: match value.next_scheduled_method {
                 Some(method_id) => pb_canister_state_bits::NextScheduledMethod::try_from(method_id)
                     .unwrap_or_default()
@@ -2825,7 +2818,7 @@ impl From<CanisterSnapshotBits> for pb_canister_snapshot_bits::CanisterSnapshotB
             canister_id: Some((item.canister_id).into()),
             taken_at_timestamp: item.taken_at_timestamp.as_nanos_since_unix_epoch(),
             canister_version: item.canister_version,
-            binary_hash: item.binary_hash.as_ref().map(|h| h.to_vec()),
+            binary_hash: item.binary_hash.to_vec(),
             certified_data: item.certified_data.clone(),
             wasm_chunk_store_metadata: Some((&item.wasm_chunk_store_metadata).into()),
             stable_memory_size: item.stable_memory_size.get() as u64,
@@ -2855,18 +2848,13 @@ impl TryFrom<pb_canister_snapshot_bits::CanisterSnapshotBits> for CanisterSnapsh
         let canister_id: CanisterId =
             try_from_option_field(item.canister_id, "CanisterSnapshotBits::canister_id")?;
 
-        let binary_hash = match item.binary_hash {
-            Some(hash) => {
-                let hash: [u8; 32] =
-                    hash.try_into()
-                        .map_err(|e| ProxyDecodeError::ValueOutOfRange {
-                            typ: "BinaryHash",
-                            err: format!("Expected a 32-byte long module hash, got {:?}", e),
-                        })?;
-                Some(hash.into())
-            }
-            None => None,
-        };
+        let binary_hash: [u8; 32] =
+            item.binary_hash
+                .try_into()
+                .map_err(|e| ProxyDecodeError::ValueOutOfRange {
+                    typ: "BinaryHash",
+                    err: format!("Expected a 32-byte long module hash, got {:?}", e),
+                })?;
 
         let mut exported_globals = Vec::with_capacity(item.exported_globals.len());
         for global in item.exported_globals.into_iter() {
@@ -2889,7 +2877,7 @@ impl TryFrom<pb_canister_snapshot_bits::CanisterSnapshotBits> for CanisterSnapsh
             canister_id,
             taken_at_timestamp: Time::from_nanos_since_unix_epoch(item.taken_at_timestamp),
             canister_version: item.canister_version,
-            binary_hash,
+            binary_hash: WasmHash::from(binary_hash),
             certified_data: item.certified_data,
             wasm_chunk_store_metadata: try_from_option_field(
                 item.wasm_chunk_store_metadata,
diff --git a/rs/state_layout/src/state_layout/tests.rs b/rs/state_layout/src/state_layout/tests.rs
index 5dae717813f5..79c5498a8744 100644
--- a/rs/state_layout/src/state_layout/tests.rs
+++ b/rs/state_layout/src/state_layout/tests.rs
@@ -208,7 +208,7 @@ fn test_canister_snapshots_decode() {
         canister_id,
         taken_at_timestamp: UNIX_EPOCH,
         canister_version: 3,
-        binary_hash: Some(WasmHash::from(&CanisterModule::new(vec![2, 3, 4]))),
+        binary_hash: WasmHash::from(&CanisterModule::new(vec![2, 3, 4])),
         certified_data: vec![3, 4, 7],
         wasm_chunk_store_metadata: WasmChunkStoreMetadata::default(),
         stable_memory_size: NumWasmPages::new(10),
diff --git a/rs/state_manager/src/lib.rs b/rs/state_manager/src/lib.rs
index ed0503f48637..bea077ec4c0b 100644
--- a/rs/state_manager/src/lib.rs
+++ b/rs/state_manager/src/lib.rs
@@ -1124,13 +1124,13 @@ fn switch_to_checkpoint(
                 Arc::clone(fd_factory),
             )?);
 
-        let wasm_binary = snapshot_layout.wasm().deserialize(Some(
+        let wasm_binary = snapshot_layout.wasm().deserialize(
             new_snapshot
                 .execution_snapshot()
                 .wasm_binary
                 .module_hash()
                 .into(),
-        ))?;
+        )?;
         new_snapshot.execution_snapshot_mut().wasm_binary = wasm_binary;
     }
 
@@ -1143,7 +1143,7 @@ fn switch_to_checkpoint(
             let embedder_cache = Arc::clone(&tip_state.wasm_binary.embedder_cache);
             let wasm_binary = canister_layout
                 .wasm()
-                .deserialize(Some(tip_state.wasm_binary.binary.module_hash().into()))?;
+                .deserialize(tip_state.wasm_binary.binary.module_hash().into())?;
             debug_assert_eq!(
                 tip_state.wasm_binary.binary.as_slice(),
                 wasm_binary.as_slice()
diff --git a/rs/state_manager/src/tip.rs b/rs/state_manager/src/tip.rs
index f7574b0b48ae..c0352bf66b8c 100644
--- a/rs/state_manager/src/tip.rs
+++ b/rs/state_manager/src/tip.rs
@@ -907,7 +907,7 @@ fn serialize_canister_to_tip(
                 exports: execution_state.exports.clone(),
                 last_executed_round: execution_state.last_executed_round,
                 metadata: execution_state.metadata.clone(),
-                binary_hash: Some(execution_state.wasm_binary.binary.module_hash().into()),
+                binary_hash: execution_state.wasm_binary.binary.module_hash().into(),
                 next_scheduled_method: execution_state.next_scheduled_method,
                 is_wasm64: execution_state.wasm_execution_mode.is_wasm64(),
             })
@@ -1021,7 +1021,7 @@ fn serialize_snapshot_to_tip(
             canister_id: canister_snapshot.canister_id(),
             taken_at_timestamp: *canister_snapshot.taken_at_timestamp(),
             canister_version: canister_snapshot.canister_version(),
-            binary_hash: Some(canister_snapshot.canister_module().module_hash().into()),
+            binary_hash: canister_snapshot.canister_module().module_hash().into(),
             certified_data: canister_snapshot.certified_data().clone(),
             wasm_chunk_store_metadata: canister_snapshot.chunk_store().metadata().clone(),
             stable_memory_size: canister_snapshot.stable_memory().size,
diff --git a/rs/types/wasm_types/src/lib.rs b/rs/types/wasm_types/src/lib.rs
index 91a6390e4ff7..51d0364f229c 100644
--- a/rs/types/wasm_types/src/lib.rs
+++ b/rs/types/wasm_types/src/lib.rs
@@ -71,15 +71,11 @@ impl CanisterModule {
         }
     }
 
-    pub fn new_from_file(path: PathBuf, module_hash: Option<WasmHash>) -> std::io::Result<Self> {
+    pub fn new_from_file(path: PathBuf, module_hash: WasmHash) -> std::io::Result<Self> {
         let module = ModuleStorage::mmap_file(path)?;
-        // It should only be necessary to compute the hash here when
-        // loading checkpoints written by older replica versions
-        let module_hash =
-            module_hash.map_or_else(|| ic_crypto_sha2::Sha256::hash(module.as_slice()), |h| h.0);
         Ok(Self {
             module,
-            module_hash,
+            module_hash: module_hash.0,
         })
     }
 


Reviews:
user: login: ShuoWangNSL
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T23:04:11Z
user: login: ShuoWangNSL
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T23:04:24Z
user: login: ShuoWangNSL
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T23:09:35Z
user: login: stiegerc
body: Assuming your claims are true, LGTM.

edit: by that I mean "In production, we have populated binary_hash with Some value for long time and serialize it to disk" This I didn't check, but assuming there isn't an old state still lying around that we may want to use, then the code looks fine to me. I also checked again with @alin-at-dfinity about dumping `optional` from protobuf; this is also ok.

Didn't @oggy-dfin introduce forward/backward checks for this kind of thing?
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T06:28:39Z
user: login: schneiderstefan
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T11:38:59Z
user: login: maksymar
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T15:13:18Z
user: login: alin-at-dfinity
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T15:21:07Z
user: login: maksymar
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T07:42:55Z
user: login: ShuoWangNSL
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T10:36:25Z

Review comments:
diff_hunk: @@ -907,7 +907,7 @@ fn serialize_canister_to_tip(
                 exports: execution_state.exports.clone(),
                 last_executed_round: execution_state.last_executed_round,
                 metadata: execution_state.metadata.clone(),
-                binary_hash: Some(execution_state.wasm_binary.binary.module_hash().into()),
path: rs/state_manager/src/tip.rs
user: login: ShuoWangNSL
body: `binary_hash` has always been populated with `Some` value in `ExecutionStateBits` for long time.
created_at: 2025-04-08T23:04:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1021,7 +1021,7 @@ fn serialize_snapshot_to_tip(
             canister_id: canister_snapshot.canister_id(),
             taken_at_timestamp: *canister_snapshot.taken_at_timestamp(),
             canister_version: canister_snapshot.canister_version(),
-            binary_hash: Some(canister_snapshot.canister_module().module_hash().into()),
path: rs/state_manager/src/tip.rs
user: login: ShuoWangNSL
body: `binary_hash` has always been populated with `Some` value in `CanisterSnapshotBits` for long time.
created_at: 2025-04-08T23:04:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -71,15 +71,11 @@ impl CanisterModule {
         }
     }
 
-    pub fn new_from_file(path: PathBuf, module_hash: Option<WasmHash>) -> std::io::Result<Self> {
+    pub fn new_from_file(path: PathBuf, module_hash: WasmHash) -> std::io::Result<Self> {
         let module = ModuleStorage::mmap_file(path)?;
-        // It should only be necessary to compute the hash here when
-        // loading checkpoints written by older replica versions
-        let module_hash =
-            module_hash.map_or_else(|| ic_crypto_sha2::Sha256::hash(module.as_slice()), |h| h.0);
path: rs/types/wasm_types/src/lib.rs
user: login: ShuoWangNSL
body: In addition to simplifying code, my primary goal of this PR is to get rid of the usage of `as_slice()` in this function so that we know for sure that `new_from_file()` will be always lazy loading.
created_at: 2025-04-08T23:09:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -907,7 +907,7 @@ fn serialize_canister_to_tip(
                 exports: execution_state.exports.clone(),
                 last_executed_round: execution_state.last_executed_round,
                 metadata: execution_state.metadata.clone(),
-                binary_hash: Some(execution_state.wasm_binary.binary.module_hash().into()),
path: rs/state_manager/src/tip.rs
user: login: maksymar
body: NIT: Maybe I'm over cautious, but would it be reasonable to add an extra attempt on deserialisation? after it failed to deserialise bytes, try to deserialise option bytes and only then fail.

UPD: I see others also expressed concerns about that, and I understand it was set to `Some` for quite a while, so LGTM and it's up to you if you want to add an extra safety measure of deserializing optional.
created_at: 2025-04-10T15:13:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -907,7 +907,7 @@ fn serialize_canister_to_tip(
                 exports: execution_state.exports.clone(),
                 last_executed_round: execution_state.last_executed_round,
                 metadata: execution_state.metadata.clone(),
-                binary_hash: Some(execution_state.wasm_binary.binary.module_hash().into()),
path: rs/state_manager/src/tip.rs
user: login: ShuoWangNSL
body: > after it failed to deserialise bytes, try to deserialise option bytes and only then fail.

This is the current behavior, which handles old data when we introduce new fields. Keeping this behavior will retain most of the code logic without much simplification.

In the checkpointing logic, when serializing pbuf files to checkpoints, we first ensure no old pbuf files exists, i.e. we always write brand new files. This helps us safely reason about that all pbuf files are written by the current code, which populates the `wasm_hash`. 

Alternatively, I could add some metrics counting missing `wasm_hash` and wait to see it in production and then merge this PR. However, that will probably take another 2~3 weeks. I prefer merging it now and continue my other PR on top of this.
created_at: 2025-04-15T10:36:25Z
author_association: CONTRIBUTOR

PR comments:
user: login: ShuoWangNSL
created_at: 2025-04-08T23:33:21Z
body: CI requires adding label `CI_OVERRIDE_BUF_BREAKING`. Indeed, if `binary_hash` was not present on disk, serializing it back will results in an empty `Vec<u8>` and fails the length check. 

The change is safe because we have populated the field with `Some` value for long time.
author_association: CONTRIBUTOR
user: login: schneiderstefan
created_at: 2025-04-09T11:46:21Z
body: I think it's a worthwhile simplification. Not sure however about it really helping avoiding `.as_slice()`, as that is only called if we read a `None` from file, which will never happen (and if it would it would break your PR).

Just to be safe, could you manually run one of the upgrade/downgrade system tests and also //rs/ledger_suite/icrc1:icrc_ledger_suite_integration_golden_state_upgrade_downgrade_test_u256, please. The latter has recently discovered an upgrade bug with the snapshot protobufs.
author_association: CONTRIBUTOR
user: login: ShuoWangNSL
created_at: 2025-04-09T14:52:51Z
body: > I also checked again with @alin-at-dfinity about dumping `optional` from protobuf; this is also ok.

Yes. This is because these fields are not designed to be optional. It was set to optional for backwards compatibility reasons. If we are not consistently populating it with `Some` value, directly dumping `optional` from protobuf is not OK because we want to distinguish a `None` and an empty `Vec`.



author_association: CONTRIBUTOR
user: login: ShuoWangNSL
created_at: 2025-04-09T14:55:18Z
body: > run one of the upgrade/downgrade system tests and also //rs/ledger_suite/icrc1:icrc_ledger_suite_integration_golden_state_upgrade_downgrade_test_u256

Sure.  I find it convenient to trigger all system tests by adding tag `CI_ALL_BAZEL_TARGETS` and these tests are all covered in the [pipeline](https://github.com/dfinity/ic/actions/runs/14345997905/job/40215788602?pr=4734).

[12728](https://github.com/dfinity/ic/actions/runs/14345997905/job/40215788602?pr=4734#step:5:12734)
//rs/ledger_suite/icrc1/ledger:ledger_test                               PASSED in 139.5s
[12729](https://github.com/dfinity/ic/actions/runs/14345997905/job/40215788602?pr=4734#step:5:12735)
//rs/ledger_suite/icrc1/ledger:ledger_test_u256                          PASSED in 131.3s

[12730](https://github.com/dfinity/ic/actions/runs/14345997905/job/40215788602?pr=4734#step:5:12736)
//rs/tests/consensus/upgrade:downgrade_app_subnet_test                   PASSED in 663.5s
[13035](https://github.com/dfinity/ic/actions/runs/14345997905/job/40215788602?pr=4734#step:5:13041)
//rs/tests/consensus/upgrade:downgrade_app_subnet_test_head_nns          PASSED in 578.9s
[13036](https://github.com/dfinity/ic/actions/runs/14345997905/job/40215788602?pr=4734#step:5:13042)
//rs/tests/consensus/upgrade:unassigned_node_upgrade_test                PASSED in 232.5s
[13037](https://github.com/dfinity/ic/actions/runs/14345997905/job/40215788602?pr=4734#step:5:13043)
//rs/tests/consensus/upgrade:upgrade_app_subnet_test                     PASSED in 631.4s
[13038](https://github.com/dfinity/ic/actions/runs/14345997905/job/40215788602?pr=4734#step:5:13044)
//rs/tests/consensus/upgrade:upgrade_app_subnet_test_head_nns            PASSED in 606.9s
[13039](https://github.com/dfinity/ic/actions/runs/14345997905/job/40215788602?pr=4734#step:5:13045)
//rs/tests/consensus/upgrade:upgrade_with_alternative_urls               PASSED in 240.9s

author_association: CONTRIBUTOR
user: login: ShuoWangNSL
created_at: 2025-04-09T14:56:46Z
body: > I think it's a worthwhile simplification. Not sure however about it really helping avoiding `.as_slice()`, as that is only called if we read a `None` from file, which will never happen (and if it would it would break your PR).

Indeed, it won't really affect things in production. This is PR is more of a refactoring that helps reasoning about things in the code level.
author_association: CONTRIBUTOR